ui: visual ↔ JSON toggle on /admin/feedback/new (t-fdbck-builder-on-new)
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.
This commit is contained in:
@@ -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,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 @@
|
||||
|
||||
<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;">
|
||||
<button type="button" class="fb-btn fb-btn--secondary fb-btn--sm" onclick={pasteSample}>
|
||||
<Icon name="plus" /> Insert sample
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<textarea
|
||||
id="fb-new-form"
|
||||
class="fb-textarea"
|
||||
rows="14"
|
||||
placeholder={'{\n "questions": [\n { "id": "q1", "type": "short_text", "label": "Your name?" }\n ]\n}'}
|
||||
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>
|
||||
<textarea
|
||||
id="fb-new-form"
|
||||
class="fb-textarea"
|
||||
rows="14"
|
||||
placeholder={'{\n "questions": [\n { "id": "q1", "type": "short_text", "label": "Your name?" }\n ]\n}'}
|
||||
bind:value={formJson}
|
||||
style="font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 0.85rem; margin-top: 0.6rem;"
|
||||
></textarea>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user