Merge mai/cronus/builder-on-new: visual ↔ JSON editor toggle on /admin/feedback/new
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,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>
|
||||
|
||||
Reference in New Issue
Block a user