From 3cc1efa97068fc0e06519f5927d2a741a1386870 Mon Sep 17 00:00:00 2001 From: mAi Date: Wed, 6 May 2026 14:05:55 +0200 Subject: [PATCH] =?UTF-8?q?ui:=20visual=20=E2=86=94=20JSON=20toggle=20on?= =?UTF-8?q?=20/admin/feedback/new=20(t-fdbck-builder-on-new)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit m's complaint: "I already want the visual editor/json editor switch — why only after creating an empty form, that makes no sense". Three steps to get to the obvious starting place — create empty, navigate to detail, switch tab — is friction. Mirrors the detail-page Edit-tab pattern verbatim: - editMode / editForm / editFormJson state, plus syncJsonFromVisual / syncVisualFromJson / switchEditMode helpers ported 1:1 from /admin/feedback/[id]/+page.svelte. The two pages now author questions the same way. - Default mode: Visual, with a null editForm + "No questions yet." + "+ Add questions" button. Clicking the button calls ensureBuilderForm() which seeds the same { id: 'q1', label: 'Question 1', type: 'short_text', required: false } stub the detail page seeds. - JSON mode unchanged: textarea + "Insert sample" + helper text. - Submit logic resolves form_definition from whichever mode is active (mirrors detail-page saveEdits parsedForm branch). - Disclosure framing kept ("Add questions now (advanced)") — collapsed by default so the title-+-chat-only path stays uncluttered. Reuses FormBuilder.svelte directly. No new component, no new dep. POST /api/admin/feedback contract unchanged. --- src/routes/admin/feedback/new/+page.svelte | 154 +++++++++++++++++---- 1 file changed, 128 insertions(+), 26 deletions(-) diff --git a/src/routes/admin/feedback/new/+page.svelte b/src/routes/admin/feedback/new/+page.svelte index 1fa5d2a..5a85aaa 100644 --- a/src/routes/admin/feedback/new/+page.svelte +++ b/src/routes/admin/feedback/new/+page.svelte @@ -2,6 +2,8 @@ import '$lib/styles/feedback.css'; import { goto } from '$app/navigation'; import Icon from '$lib/components/Icon.svelte'; + import FormBuilder from '$lib/components/FormBuilder.svelte'; + import { FeedbackFormDefinitionSchema, type FeedbackFormDefinition } from '$lib/schemas'; let creating = $state(false); let createError = $state(null); @@ -9,7 +11,13 @@ let title = $state(''); let description = $state(''); let chatEnabled = $state(true); - let formJson = $state(''); + + // Question authoring — mirrors the detail-page Edit tab. editForm is the + // canonical structured form; editFormJson is its textarea-friendly + // serialization. The two stay in sync on mode switches. + let editMode = $state<'visual' | 'json'>('visual'); + let editForm = $state(null); + let editFormJson = $state(''); const SAMPLE_FORM = JSON.stringify( { @@ -26,8 +34,44 @@ 2, ); + function syncJsonFromVisual(): void { + editFormJson = editForm ? JSON.stringify(editForm, null, 2) : ''; + } + + function syncVisualFromJson(): boolean { + const trimmed = editFormJson.trim(); + if (!trimmed) { + editForm = null; + return true; + } + try { + const parsed = JSON.parse(trimmed); + editForm = FeedbackFormDefinitionSchema.parse(parsed); + return true; + } catch (err) { + createError = `Invalid JSON: ${err instanceof Error ? err.message : 'parse error'}`; + return false; + } + } + + function switchEditMode(next: 'visual' | 'json'): void { + if (next === editMode) return; + createError = null; + if (next === 'json') syncJsonFromVisual(); + else if (!syncVisualFromJson()) return; + editMode = next; + } + + function ensureBuilderForm(): void { + if (!editForm) { + editForm = { + questions: [{ id: 'q1', label: 'Question 1', type: 'short_text', required: false }], + }; + } + } + function pasteSample(): void { - formJson = SAMPLE_FORM; + editFormJson = SAMPLE_FORM; } async function createInstance(e: SubmitEvent): Promise { @@ -35,15 +79,30 @@ creating = true; createError = null; - let parsedForm: unknown = null; - const trimmed = formJson.trim(); - if (trimmed) { - try { - parsedForm = JSON.parse(trimmed); - } catch (err) { - createError = `Invalid JSON: ${err instanceof Error ? err.message : 'parse error'}`; - creating = false; - return; + // Resolve form_definition from whichever mode the user is in. Mirrors + // detail-page saveEdits(). + let parsedForm: FeedbackFormDefinition | null = null; + if (editMode === 'json') { + const trimmed = editFormJson.trim(); + if (trimmed) { + try { + parsedForm = FeedbackFormDefinitionSchema.parse(JSON.parse(trimmed)); + } catch (err) { + createError = `Invalid JSON: ${err instanceof Error ? err.message : 'parse error'}`; + creating = false; + return; + } + } + } else { + parsedForm = editForm; + if (parsedForm) { + try { + parsedForm = FeedbackFormDefinitionSchema.parse(parsedForm); + } catch (err) { + createError = `Invalid questions: ${err instanceof Error ? err.message : 'parse error'}`; + creating = false; + return; + } } } @@ -122,22 +181,54 @@
Add questions now (advanced) -

- You can also edit questions visually after the form is created. -

-
- + +
+
+ Questions +
+ + +
+
+ + {#if editMode === 'visual'} + {#if editForm} + + {:else} +

No questions yet.

+ + {/if} + {:else} +
+ +
+ +

+ You can also edit questions visually after the form is created. +

+ {/if}
-
{#if createError} @@ -166,4 +257,15 @@ .fb-new-advanced[open] > summary { color: var(--color-text-primary); } + .fb-new-questions { + margin-top: var(--space-3); + } + .fb-new-questions__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); + margin-bottom: var(--space-3); + flex-wrap: wrap; + }