ui: minimalist /admin/feedback/[id] detail — header collapse + share strip
The header was the densest surface in the app: 8 controls in a single
flex-wrap row plus a separate Share section bolted between header and tabs.
This commit collapses both into something readable.
Header:
- 8 visible controls → 3 visible. Status pill is now clickable and toggles
between open/closed (optimistic), replacing the Close/Reopen button. The
⋯ menu absorbs Copy /f/<slug>, Export CSV, Export JSON, and a separator
before Delete (still in red). All gone from the top-row strip: the raw
/f/slug, Copy link, Preview, Close/Reopen, CSV, JSON, Delete buttons.
- Quiet text-only .fb-back-link replaces the chip-style "← All forms"
button.
- New .fb-detail-head primitive lays out title-block on the left + actions
on the right with proper flex-wrap behaviour.
Share:
- Standalone <section data-fb-share> deleted. Its job moves to a new
inline .fb-share-strip directly under the title in the header.
- Strip always shows a usable URL: short_url if it exists, else the raw
/f/<slug>. Copy + Open ↗ buttons sit alongside.
- Below the strip, a compact <details class="fb-share-strip__replace">
holds the slug input + Create/Replace button. Summary text adapts to
whether a short link already exists.
Tab body:
- Drop the inner <h2> in every tab body (the active tab pill names the
section). All four tab bodies now use .fb-tab-body for consistent top
padding (var(--space-6)).
- "X responses already received…" warning becomes a muted .fb-question__help
line, not a .fb-banner box.
- Visual / JSON toggle becomes a real .fb-segment control matching the
shape of .fb-tabs (consistency).
- Save row uses .fb-save-row with the version pill ("Current version: vN")
rendered as a quiet .fb-version-note next to the Save button instead of
decorating the H2 like before.
- Submissions table extracted to a small <style> block (.fb-detail-table)
instead of inline style="..." chunks.
Click-outside-to-close + Escape close any open ⋯ menu, mirroring the list
page. Polling, refresh, and all backend contracts unchanged.
Delete still uses confirm() per m's override — deletion remains a deliberate
two-step action, no undo toast.
This commit is contained in:
@@ -36,6 +36,10 @@
|
||||
);
|
||||
let editFormJson = $state(inst.form_definition ? JSON.stringify(inst.form_definition, null, 2) : '');
|
||||
|
||||
// Optimistic status pill — same pattern as the list page.
|
||||
let optimisticStatus = $state<'open' | 'closed' | null>(null);
|
||||
const effectiveStatus = $derived<'open' | 'closed'>(optimisticStatus ?? inst.status);
|
||||
|
||||
function syncJsonFromVisual(): void {
|
||||
editFormJson = editForm ? JSON.stringify(editForm, null, 2) : '';
|
||||
}
|
||||
@@ -112,9 +116,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function setStatus(next: 'open' | 'closed'): Promise<void> {
|
||||
actionError = null;
|
||||
async function toggleStatus(): Promise<void> {
|
||||
if (actionInFlight) return;
|
||||
const current = effectiveStatus;
|
||||
const next: 'open' | 'closed' = current === 'open' ? 'closed' : 'open';
|
||||
optimisticStatus = next;
|
||||
actionInFlight = true;
|
||||
actionError = null;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/feedback/${inst.id}`, {
|
||||
method: 'PATCH',
|
||||
@@ -124,11 +132,14 @@
|
||||
if (!res.ok) {
|
||||
const j = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
actionError = j.error ?? `Error ${res.status}`;
|
||||
optimisticStatus = null;
|
||||
return;
|
||||
}
|
||||
await refresh();
|
||||
optimisticStatus = null;
|
||||
} catch (e) {
|
||||
actionError = e instanceof Error ? e.message : 'Network error';
|
||||
optimisticStatus = null;
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
}
|
||||
@@ -230,6 +241,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function copyShareStripUrl(): Promise<void> {
|
||||
if (inst.short_url) {
|
||||
await copyShortUrl();
|
||||
} else {
|
||||
await copyLink();
|
||||
shareCopied = true;
|
||||
setTimeout(() => (shareCopied = false), 1500);
|
||||
}
|
||||
}
|
||||
|
||||
async function createShareLink(): Promise<void> {
|
||||
shareError = null;
|
||||
shareInFlight = true;
|
||||
@@ -259,6 +280,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCsv(): Promise<void> {
|
||||
window.location.href = `/api/admin/feedback/${inst.id}/export?format=csv`;
|
||||
}
|
||||
|
||||
async function exportJson(): Promise<void> {
|
||||
window.location.href = `/api/admin/feedback/${inst.id}/export?format=json`;
|
||||
}
|
||||
|
||||
function fmtDateTime(iso: string): string {
|
||||
return new Date(iso).toLocaleString();
|
||||
}
|
||||
@@ -274,16 +303,25 @@
|
||||
return summarizeAnswer(sub.answers?.[qid]);
|
||||
}
|
||||
|
||||
async function exportCsv(): Promise<void> {
|
||||
window.location.href = `/api/admin/feedback/${inst.id}/export?format=csv`;
|
||||
// Click-outside-to-close + Escape for any open <details class="fb-menu">.
|
||||
function onDocClick(e: MouseEvent): void {
|
||||
const target = e.target as Node | null;
|
||||
if (!target) return;
|
||||
document.querySelectorAll<HTMLDetailsElement>('details.fb-menu[open]').forEach((d) => {
|
||||
if (!d.contains(target)) d.open = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function exportJson(): Promise<void> {
|
||||
window.location.href = `/api/admin/feedback/${inst.id}/export?format=json`;
|
||||
function onDocKey(e: KeyboardEvent): void {
|
||||
if (e.key !== 'Escape') return;
|
||||
document.querySelectorAll<HTMLDetailsElement>('details.fb-menu[open]').forEach((d) => {
|
||||
d.open = false;
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
pollHandle = setInterval(refresh, 5000);
|
||||
document.addEventListener('click', onDocClick);
|
||||
document.addEventListener('keydown', onDocKey);
|
||||
return () => {
|
||||
if (pollHandle) clearInterval(pollHandle);
|
||||
};
|
||||
@@ -291,6 +329,10 @@
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollHandle) clearInterval(pollHandle);
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('click', onDocClick);
|
||||
document.removeEventListener('keydown', onDocKey);
|
||||
}
|
||||
void invalidateAll;
|
||||
});
|
||||
</script>
|
||||
@@ -301,104 +343,95 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="fb-shell">
|
||||
<a href="/admin/feedback" class="fb-back-link">
|
||||
<Icon name="arrow-left" /> Forms
|
||||
</a>
|
||||
|
||||
<header class="fb-header">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
|
||||
<a href="/admin/feedback" class="fb-btn fb-btn--ghost fb-btn--sm"><Icon name="arrow-left" /> All forms</a>
|
||||
</div>
|
||||
<h1 style="margin-top: 0.5rem;">{inst.title}</h1>
|
||||
{#if inst.description}<p>{inst.description}</p>{/if}
|
||||
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin-top: 0.5rem;">
|
||||
<span style="font-size: 0.85rem; padding: 0.2rem 0.6rem; border-radius: 999px; font-weight: 500; {inst.status === 'closed' ? 'background:#fef3c7; color:#78350f;' : 'background:var(--color-primary-light); color:var(--color-primary-hover);'}">
|
||||
{inst.status}
|
||||
</span>
|
||||
<span style="font-size: 0.85rem; color: var(--color-text-muted);">/f/{inst.slug}</span>
|
||||
<button type="button" class="fb-btn fb-btn--ghost fb-btn--sm" onclick={copyLink}><Icon name="copy" /> Copy link</button>
|
||||
<a class="fb-btn fb-btn--ghost fb-btn--sm" href="/f/{inst.slug}" target="_blank" rel="noopener"><Icon name="external-link" /> Preview</a>
|
||||
{#if inst.status === 'open'}
|
||||
<button type="button" class="fb-btn fb-btn--secondary fb-btn--sm" disabled={actionInFlight} onclick={() => setStatus('closed')}>
|
||||
<Icon name="lock" /> Close
|
||||
<div class="fb-detail-head">
|
||||
<div class="fb-detail-head__title">
|
||||
<h1>{inst.title}</h1>
|
||||
{#if inst.description}<p>{inst.description}</p>{/if}
|
||||
</div>
|
||||
<div class="fb-detail-head__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="fb-status-pill fb-status-pill--{effectiveStatus}"
|
||||
disabled={actionInFlight}
|
||||
onclick={toggleStatus}
|
||||
aria-label={effectiveStatus === 'open' ? 'Open — click to close' : 'Closed — click to reopen'}
|
||||
title={effectiveStatus === 'open' ? 'Click to close' : 'Click to reopen'}
|
||||
>
|
||||
<span class="fb-status-pill__dot"></span>{effectiveStatus}
|
||||
</button>
|
||||
{:else}
|
||||
<button type="button" class="fb-btn fb-btn--secondary fb-btn--sm" disabled={actionInFlight} onclick={() => setStatus('open')}>
|
||||
<Icon name="unlock" /> Reopen
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" class="fb-btn fb-btn--ghost fb-btn--sm" onclick={exportCsv}><Icon name="download" /> CSV</button>
|
||||
<button type="button" class="fb-btn fb-btn--ghost fb-btn--sm" onclick={exportJson}><Icon name="download" /> JSON</button>
|
||||
<button type="button" class="fb-btn fb-btn--danger fb-btn--sm" disabled={actionInFlight} onclick={destroy}><Icon name="trash" /> Delete</button>
|
||||
<details class="fb-menu">
|
||||
<!-- svelte-ignore a11y_no_redundant_roles -->
|
||||
<summary class="fb-menu__btn" role="button" aria-label="More actions">⋯</summary>
|
||||
<div class="fb-menu__panel" role="menu">
|
||||
<button type="button" class="fb-menu__item" onclick={copyLink}>
|
||||
<Icon name="copy" /> Copy /f/{inst.slug}
|
||||
</button>
|
||||
<button type="button" class="fb-menu__item" onclick={exportCsv}>
|
||||
<Icon name="download" /> Export CSV
|
||||
</button>
|
||||
<button type="button" class="fb-menu__item" onclick={exportJson}>
|
||||
<Icon name="download" /> Export JSON
|
||||
</button>
|
||||
<hr class="fb-menu__divider" />
|
||||
<button
|
||||
type="button"
|
||||
class="fb-menu__item fb-menu__item--danger"
|
||||
disabled={actionInFlight}
|
||||
onclick={destroy}
|
||||
>
|
||||
<Icon name="trash" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if actionError}
|
||||
<div class="fb-banner fb-banner--error">{actionError}</div>
|
||||
{/if}
|
||||
|
||||
<section class="fb-section" data-fb-share>
|
||||
<h2>Share</h2>
|
||||
{#if inst.short_url}
|
||||
<p style="margin: 0 0 0.5rem 0; color: var(--color-text-muted); font-size: 0.85rem;">
|
||||
Memorable short link — resolves to <code>/f/{inst.slug}</code>.
|
||||
</p>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||
<div class="fb-share-strip">
|
||||
{#if inst.short_url}
|
||||
<a
|
||||
class="fb-share-strip__url"
|
||||
href={inst.short_url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 0.95rem; word-break: break-all;"
|
||||
>
|
||||
{inst.short_url}
|
||||
</a>
|
||||
<button type="button" class="fb-btn fb-btn--ghost" onclick={copyShortUrl}>
|
||||
{shareCopied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<a class="fb-btn fb-btn--ghost" href={inst.short_url} target="_blank" rel="noopener">
|
||||
Open ↗
|
||||
</a>
|
||||
</div>
|
||||
<details style="margin-top: 0.75rem;">
|
||||
<summary style="cursor: pointer; color: var(--color-text-muted); font-size: 0.85rem;">
|
||||
Replace with a different short link
|
||||
</summary>
|
||||
<div style="margin-top: 0.5rem;">
|
||||
<label class="fb-question__label" for="fb-share-slug-replace">Custom slug (optional)</label>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||
<input
|
||||
id="fb-share-slug-replace"
|
||||
class="fb-input"
|
||||
maxlength="64"
|
||||
placeholder="e.g. vote, session-feedback"
|
||||
bind:value={shareSlugInput}
|
||||
style="max-width: 320px;"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="fb-btn"
|
||||
disabled={shareInFlight}
|
||||
onclick={createShareLink}
|
||||
>
|
||||
{shareInFlight ? 'Creating…' : 'Create new'}
|
||||
</button>
|
||||
</div>
|
||||
<p style="margin: 0.4rem 0 0 0; color: var(--color-text-muted); font-size: 0.8rem;">
|
||||
Leave blank for a random short code, or pick a memorable slug like <code>vote</code> or <code>session-feedback</code>.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<p style="margin: 0 0 0.5rem 0; color: var(--color-text-muted); font-size: 0.85rem;">
|
||||
Create a memorable short link (e.g. <code>https://msbls.de/vote</code>) that redirects to this form.
|
||||
</p>
|
||||
<div style="margin-top: 0.5rem;">
|
||||
>{inst.short_url}</a>
|
||||
{:else}
|
||||
<a
|
||||
class="fb-share-strip__url"
|
||||
href={`/f/${inst.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>/f/{inst.slug}</a>
|
||||
{/if}
|
||||
<button type="button" class="fb-btn fb-btn--ghost fb-btn--sm" onclick={copyShareStripUrl}>
|
||||
<Icon name="copy" /> {shareCopied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<a
|
||||
class="fb-btn fb-btn--ghost fb-btn--sm"
|
||||
href={inst.short_url ?? `/f/${inst.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<Icon name="external-link" /> Open
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<details class="fb-share-strip__replace">
|
||||
<summary>{inst.short_url ? 'Replace short link' : 'Create memorable short link'}</summary>
|
||||
<div>
|
||||
<label class="fb-question__label" for="fb-share-slug">Custom slug (optional)</label>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||
<div class="fb-save-row" style="margin-top: 0.4rem;">
|
||||
<input
|
||||
id="fb-share-slug"
|
||||
class="fb-input"
|
||||
maxlength="64"
|
||||
placeholder="e.g. vote, session-feedback"
|
||||
bind:value={shareSlugInput}
|
||||
style="max-width: 320px;"
|
||||
style="max-width: 320px; flex: 1;"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -406,18 +439,23 @@
|
||||
disabled={shareInFlight}
|
||||
onclick={createShareLink}
|
||||
>
|
||||
{shareInFlight ? 'Creating…' : 'Create short link'}
|
||||
{shareInFlight ? 'Creating…' : (inst.short_url ? 'Replace' : 'Create')}
|
||||
</button>
|
||||
</div>
|
||||
<p style="margin: 0.4rem 0 0 0; color: var(--color-text-muted); font-size: 0.8rem;">
|
||||
Leave blank for a random short code, or pick a memorable slug like <code>vote</code> or <code>session-feedback</code>.
|
||||
<p class="fb-question__help" style="margin-top: 0.4rem;">
|
||||
Leave blank for a random short code, or pick a memorable slug like
|
||||
<code>vote</code> or <code>session-feedback</code>.
|
||||
</p>
|
||||
{#if shareError}
|
||||
<div class="fb-inline-error">{shareError}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if shareError}
|
||||
<div class="fb-banner fb-banner--error" style="margin-top: 0.5rem;">{shareError}</div>
|
||||
{/if}
|
||||
</section>
|
||||
</details>
|
||||
</header>
|
||||
|
||||
{#if actionError}
|
||||
<div class="fb-banner fb-banner--error">{actionError}</div>
|
||||
{/if}
|
||||
|
||||
<div class="fb-tabs">
|
||||
{#each [
|
||||
@@ -440,12 +478,11 @@
|
||||
</div>
|
||||
|
||||
{#if activeTab === 'chat'}
|
||||
<section class="fb-section">
|
||||
<h2>Live chat</h2>
|
||||
<section class="fb-section fb-tab-body">
|
||||
{#if posts.length === 0}
|
||||
<p style="color: var(--color-text-muted);">No messages yet.</p>
|
||||
<p class="fb-empty">No messages yet.</p>
|
||||
{:else}
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<div class="fb-detail-list">
|
||||
{#each posts as p (p.id)}
|
||||
<div class="fb-chat__post {p.hidden ? 'fb-chat__post--hidden' : ''}">
|
||||
<div class="fb-chat__meta">
|
||||
@@ -470,40 +507,36 @@
|
||||
{/if}
|
||||
</section>
|
||||
{:else if activeTab === 'results'}
|
||||
<section class="fb-section">
|
||||
<h2>Results{formVersion ? ` · v${formVersion}` : ''}</h2>
|
||||
<section class="fb-section fb-tab-body">
|
||||
{#if results}
|
||||
<Results {results} />
|
||||
{:else}
|
||||
<p style="color: var(--color-text-muted);">No questions configured.</p>
|
||||
<p class="fb-empty">No questions configured.</p>
|
||||
{/if}
|
||||
</section>
|
||||
{:else if activeTab === 'submissions'}
|
||||
<section class="fb-section">
|
||||
<h2>Responses</h2>
|
||||
<section class="fb-section fb-tab-body">
|
||||
{#if submissions.length === 0}
|
||||
<p style="color: var(--color-text-muted);">No responses yet.</p>
|
||||
<p class="fb-empty">No responses yet.</p>
|
||||
{:else}
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">
|
||||
<div class="fb-detail-table-wrap">
|
||||
<table class="fb-detail-table">
|
||||
<thead>
|
||||
<tr style="border-bottom: 1px solid var(--color-border-primary);">
|
||||
<th style="text-align: left; padding: 0.5rem 0.4rem;">When</th>
|
||||
<th style="text-align: left; padding: 0.5rem 0.4rem;">Name</th>
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<th>Name</th>
|
||||
{#each questions as q (q.id)}
|
||||
<th style="text-align: left; padding: 0.5rem 0.4rem;">{q.label}</th>
|
||||
<th>{q.label}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each submissions as s (s.id)}
|
||||
<tr style="border-bottom: 1px solid var(--color-border-primary);">
|
||||
<td style="padding: 0.5rem 0.4rem; white-space: nowrap; color: var(--color-text-muted);">{fmtDateTime(s.created_at)}</td>
|
||||
<td style="padding: 0.5rem 0.4rem;">{s.display_name ?? 'anonymous'}</td>
|
||||
<tr>
|
||||
<td class="fb-detail-table__date">{fmtDateTime(s.created_at)}</td>
|
||||
<td>{s.display_name ?? 'anonymous'}</td>
|
||||
{#each questions as q (q.id)}
|
||||
<td style="padding: 0.5rem 0.4rem; max-width: 320px; word-break: break-word;">
|
||||
{answerCellFor(q.id, s)}
|
||||
</td>
|
||||
<td class="fb-detail-table__cell">{answerCellFor(q.id, s)}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -513,12 +546,12 @@
|
||||
{/if}
|
||||
</section>
|
||||
{:else if activeTab === 'edit'}
|
||||
<section class="fb-section">
|
||||
<h2>Edit{formVersion ? ` · v${formVersion}` : ''}</h2>
|
||||
<section class="fb-section fb-tab-body">
|
||||
{#if submissions.length > 0}
|
||||
<div class="fb-banner">
|
||||
{submissions.length} responses already received. Saving will automatically bump the version — earlier responses keep their original snapshot.
|
||||
</div>
|
||||
<p class="fb-question__help" style="margin-bottom: var(--space-4);">
|
||||
{submissions.length} responses already received. Saving will automatically
|
||||
bump the version — earlier responses keep their original snapshot.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="fb-question">
|
||||
<label class="fb-question__label" for="fb-edit-title">Title</label>
|
||||
@@ -528,31 +561,37 @@
|
||||
<label class="fb-question__label" for="fb-edit-desc">Description</label>
|
||||
<textarea id="fb-edit-desc" class="fb-textarea" maxlength="2000" rows="2" bind:value={editDescription}></textarea>
|
||||
</div>
|
||||
<div class="fb-question">
|
||||
<label class="fb-option-row" style="display:inline-flex;">
|
||||
<input type="checkbox" bind:checked={editChatEnabled} />
|
||||
<span>Enable live chat</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="fb-question">
|
||||
<label class="fb-option-row" style="display:inline-flex;">
|
||||
<input type="checkbox" bind:checked={editLiveResults} />
|
||||
<span>Show live results on the participant page after submitting</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="fb-question">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; flex-wrap: wrap;">
|
||||
<label class="fb-toggle">
|
||||
<span class="fb-toggle__text">
|
||||
<span class="fb-toggle__label">Live chat</span>
|
||||
<span class="fb-toggle__hint">Let participants post messages in real time.</span>
|
||||
</span>
|
||||
<input type="checkbox" bind:checked={editChatEnabled} />
|
||||
</label>
|
||||
|
||||
<label class="fb-toggle">
|
||||
<span class="fb-toggle__text">
|
||||
<span class="fb-toggle__label">Live results</span>
|
||||
<span class="fb-toggle__hint">Show participants the live aggregate after they submit.</span>
|
||||
</span>
|
||||
<input type="checkbox" bind:checked={editLiveResults} />
|
||||
</label>
|
||||
|
||||
<div class="fb-question" style="margin-top: var(--space-5);">
|
||||
<div class="fb-edit-questions-head">
|
||||
<span class="fb-question__label" style="margin: 0;">Questions</span>
|
||||
<div style="display: inline-flex; gap: 0.25rem; margin-left: auto;">
|
||||
<div class="fb-segment" role="tablist" aria-label="Editor mode">
|
||||
<button
|
||||
type="button"
|
||||
class="fb-btn fb-btn--sm {editMode === 'visual' ? 'fb-btn--secondary' : 'fb-btn--ghost'}"
|
||||
class="fb-segment__btn"
|
||||
class:fb-segment__btn--active={editMode === 'visual'}
|
||||
onclick={() => switchEditMode('visual')}
|
||||
>Visual</button>
|
||||
<button
|
||||
type="button"
|
||||
class="fb-btn fb-btn--sm {editMode === 'json' ? 'fb-btn--secondary' : 'fb-btn--ghost'}"
|
||||
class="fb-segment__btn"
|
||||
class:fb-segment__btn--active={editMode === 'json'}
|
||||
onclick={() => switchEditMode('json')}
|
||||
>JSON</button>
|
||||
</div>
|
||||
@@ -562,8 +601,10 @@
|
||||
{#if editForm}
|
||||
<FormBuilder bind:value={editForm as FeedbackFormDefinition} />
|
||||
{:else}
|
||||
<p style="color: var(--color-text-muted);">No questions configured.</p>
|
||||
<button type="button" class="fb-btn fb-btn--secondary fb-btn--sm" onclick={ensureBuilderForm}><Icon name="plus" /> Add questions</button>
|
||||
<p class="fb-empty">No questions configured.</p>
|
||||
<button type="button" class="fb-btn fb-btn--secondary fb-btn--sm" onclick={ensureBuilderForm}>
|
||||
<Icon name="plus" /> Add questions
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<textarea
|
||||
@@ -576,9 +617,57 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button type="button" class="fb-btn" disabled={actionInFlight} onclick={saveEdits}>
|
||||
<Icon name="check" /> {actionInFlight ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
<div class="fb-save-row">
|
||||
<button type="button" class="fb-btn" disabled={actionInFlight} onclick={saveEdits}>
|
||||
<Icon name="check" /> {actionInFlight ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
{#if formVersion !== null}
|
||||
<span class="fb-version-note">Current version: v{formVersion}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.fb-detail-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.fb-detail-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.fb-detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.fb-detail-table thead tr {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
.fb-detail-table tbody tr {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
.fb-detail-table th,
|
||||
.fb-detail-table td {
|
||||
text-align: left;
|
||||
padding: 0.5rem 0.4rem;
|
||||
}
|
||||
.fb-detail-table__date {
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.fb-detail-table__cell {
|
||||
max-width: 320px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.fb-edit-questions-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user