Merge mai/cronus/builder-on-new: visual ↔ JSON editor toggle on /admin/feedback/new

This commit is contained in:
mAi
2026-05-06 14:06:06 +02:00

View File

@@ -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<string | null>(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<FeedbackFormDefinition | null>(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<void> {
@@ -35,17 +79,32 @@
creating = true;
createError = null;
let parsedForm: unknown = null;
const trimmed = formJson.trim();
// 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 = JSON.parse(trimmed);
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;
}
}
}
try {
const res = await fetch('/api/admin/feedback', {
@@ -122,10 +181,37 @@
<details class="fb-question fb-new-advanced">
<summary>Add questions now (advanced)</summary>
<p class="fb-question__help" style="margin-top: 0.6rem;">
You can also edit questions visually after the form is created.
</p>
<div class="fb-save-row" style="margin-top: 0.75rem;">
<div class="fb-new-questions">
<div class="fb-new-questions__head">
<span class="fb-question__label" style="margin: 0;">Questions</span>
<div class="fb-segment" role="tablist" aria-label="Editor mode">
<button
type="button"
class="fb-segment__btn"
class:fb-segment__btn--active={editMode === 'visual'}
onclick={() => switchEditMode('visual')}
>Visual</button>
<button
type="button"
class="fb-segment__btn"
class:fb-segment__btn--active={editMode === 'json'}
onclick={() => switchEditMode('json')}
>JSON</button>
</div>
</div>
{#if editMode === 'visual'}
{#if editForm}
<FormBuilder bind:value={editForm as FeedbackFormDefinition} />
{:else}
<p class="fb-empty">No questions yet.</p>
<button type="button" class="fb-btn fb-btn--secondary fb-btn--sm" onclick={ensureBuilderForm}>
<Icon name="plus" /> Add questions
</button>
{/if}
{:else}
<div class="fb-save-row" style="margin-top: 0;">
<button type="button" class="fb-btn fb-btn--secondary fb-btn--sm" onclick={pasteSample}>
<Icon name="plus" /> Insert sample
</button>
@@ -135,9 +221,14 @@
class="fb-textarea"
rows="14"
placeholder={'{\n "questions": [\n { "id": "q1", "type": "short_text", "label": "Your name?" }\n ]\n}'}
bind:value={formJson}
bind:value={editFormJson}
style="font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 0.85rem; margin-top: 0.6rem;"
></textarea>
<p class="fb-question__help" style="margin-top: 0.6rem;">
You can also edit questions visually after the form is created.
</p>
{/if}
</div>
</details>
{#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;
}
</style>