From 4e27213df1d4e4f69e66bb3f7be27737291cc80f Mon Sep 17 00:00:00 2001 From: ls-root Date: Mon, 2 Feb 2026 14:43:20 +0100 Subject: [PATCH] feat(frontend): preview tab (#11475) --- .../src/app/json-editor/_components/note.tsx | 2 +- .../src/app/json-editor/_schemas/schemas.ts | 7 +- frontend/src/app/json-editor/page.tsx | 64 +++++++++++++------ .../app/scripts/_components/script-item.tsx | 23 ++----- frontend/src/app/scripts/page.tsx | 20 +++++- .../components/navigation/mobile-sidebar.tsx | 2 +- frontend/src/lib/types.ts | 12 ++-- 7 files changed, 79 insertions(+), 51 deletions(-) diff --git a/frontend/src/app/json-editor/_components/note.tsx b/frontend/src/app/json-editor/_components/note.tsx index 92514c4fd..4bb2b20ec 100644 --- a/frontend/src/app/json-editor/_components/note.tsx +++ b/frontend/src/app/json-editor/_components/note.tsx @@ -105,7 +105,7 @@ function Note({ const addNote = useCallback(() => { setScript({ ...script, - notes: [...script.notes, { text: "", type: "" }], + notes: [...script.notes, { text: "", type: "info" }], }); }, [script, setScript]); diff --git a/frontend/src/app/json-editor/_schemas/schemas.ts b/frontend/src/app/json-editor/_schemas/schemas.ts index b4ca966b5..8b803d2b9 100644 --- a/frontend/src/app/json-editor/_schemas/schemas.ts +++ b/frontend/src/app/json-editor/_schemas/schemas.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { AlertColors } from "@/config/site-config"; export const InstallMethodSchema = z.object({ type: z.enum(["default", "alpine"], { @@ -16,7 +17,9 @@ export const InstallMethodSchema = z.object({ const NoteSchema = z.object({ text: z.string().min(1, "Note text cannot be empty"), - type: z.string().min(1, "Note type cannot be empty"), + type: z.enum(Object.keys(AlertColors) as [keyof typeof AlertColors, ...(keyof typeof AlertColors)[]], { + message: `Type must be one of: ${Object.keys(AlertColors).join(", ")}`, + }), }); export const ScriptSchema = z.object({ @@ -42,7 +45,7 @@ export const ScriptSchema = z.object({ username: z.string().nullable(), password: z.string().nullable(), }), - notes: z.array(NoteSchema), + notes: z.array(NoteSchema).optional().default([]), }).refine((data) => { if (data.disable === true && !data.disable_description) { return false; diff --git a/frontend/src/app/json-editor/page.tsx b/frontend/src/app/json-editor/page.tsx index 5d1bcc2b9..f8dc22c47 100644 --- a/frontend/src/app/json-editor/page.tsx +++ b/frontend/src/app/json-editor/page.tsx @@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { fetchCategories } from "@/lib/data"; import { cn } from "@/lib/utils"; @@ -30,6 +31,7 @@ import Note from "./_components/note"; import { nord } from "react-syntax-highlighter/dist/esm/styles/hljs"; import SyntaxHighlighter from "react-syntax-highlighter"; +import { ScriptItem } from "../scripts/_components/script-item"; const initialScript: Script = { name: "", @@ -60,6 +62,7 @@ export default function JSONGenerator() { const [isCopied, setIsCopied] = useState(false); const [isValid, setIsValid] = useState(false); const [categories, setCategories] = useState([]); + const [currentTab, setCurrentTab] = useState<"json" | "preview">("json"); const [zodErrors, setZodErrors] = useState(null); useEffect(() => { @@ -68,6 +71,13 @@ export default function JSONGenerator() { .catch((error) => console.error("Error fetching categories:", error)); }, []); + useEffect(() => { + if (!isValid && currentTab === "preview") { + setCurrentTab("json"); + toast.error("Switched to JSON tab due to invalid configuration."); + } + }, [isValid, currentTab]); + const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => { setScript((prev) => { const updated = { ...prev, [key]: value }; @@ -196,7 +206,7 @@ export default function JSONGenerator() { updateScript("config_path", e.target.value || null)} + onChange={(e) => updateScript("config_path", e.target.value || "")} />
@@ -323,25 +333,41 @@ export default function JSONGenerator() {
- {validationAlert} -
-
- - -
+ setCurrentTab(value as "json" | "preview")} + value={currentTab} + > + + JSON + Preview + + + {validationAlert} +
+
+ + +
- - {JSON.stringify(script, null, 2)} - -
+ + {JSON.stringify(script, null, 2)} + +
+ + + + +
); diff --git a/frontend/src/app/scripts/_components/script-item.tsx b/frontend/src/app/scripts/_components/script-item.tsx index 50873eeb1..c521c8e39 100644 --- a/frontend/src/app/scripts/_components/script-item.tsx +++ b/frontend/src/app/scripts/_components/script-item.tsx @@ -4,7 +4,8 @@ import { X, HelpCircle } from "lucide-react"; import { Suspense } from "react"; import Image from "next/image"; -import type { AppVersion, Script } from "@/lib/types"; +import type { AppVersion } from "@/lib/types"; +import type { Script } from "@/app/json-editor/_schemas/schemas"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; @@ -26,7 +27,6 @@ import Alerts from "./script-items/alerts"; type ScriptItemProps = { item: Script; - setSelectedScript: (script: string | null) => void; }; function ScriptHeader({ item }: { item: Script }) { @@ -135,25 +135,10 @@ function VersionInfo({ item }: { item: Script }) { ); } -export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) { - const closeScript = () => { - window.history.pushState({}, document.title, window.location.pathname); - setSelectedScript(null); - }; - +export function ScriptItem({ item }: ScriptItemProps) { return (
-
-

Selected Script

- -
-
}> @@ -162,7 +147,7 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) { {item.disable && item.disable_description && ( - ) } + )} {!item.disable && ( <> diff --git a/frontend/src/app/scripts/page.tsx b/frontend/src/app/scripts/page.tsx index 33d7c1f5e..034d20cad 100644 --- a/frontend/src/app/scripts/page.tsx +++ b/frontend/src/app/scripts/page.tsx @@ -1,6 +1,6 @@ "use client"; import { Suspense, useEffect, useState } from "react"; -import { Loader2 } from "lucide-react"; +import { Loader2, X } from "lucide-react"; import { useQueryState } from "nuqs"; import type { Category, Script } from "@/lib/types"; @@ -20,6 +20,11 @@ function ScriptContent() { const [item, setItem] = useState