Add real-time Jinja2 syntax validation with debounced API check
All checks were successful
Validate / Hassfest (push) Successful in 3s
All checks were successful
Validate / Hassfest (push) Successful in 3s
Validates template syntax as user types (800ms debounce). Calls preview-raw API and shows red error text below the editor if Jinja2 parsing fails. Clears error when template is valid. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,25 @@
|
|||||||
let error = $state('');
|
let error = $state('');
|
||||||
let confirmDelete = $state<any>(null);
|
let confirmDelete = $state<any>(null);
|
||||||
let slotPreview = $state<Record<string, string>>({});
|
let slotPreview = $state<Record<string, string>>({});
|
||||||
|
let slotErrors = $state<Record<string, string>>({});
|
||||||
|
let validateTimers: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||||
|
|
||||||
|
function validateSlot(slotKey: string, template: string) {
|
||||||
|
// Clear previous timer
|
||||||
|
if (validateTimers[slotKey]) clearTimeout(validateTimers[slotKey]);
|
||||||
|
if (!template) { slotErrors = { ...slotErrors, [slotKey]: '' }; return; }
|
||||||
|
|
||||||
|
// Debounce 800ms
|
||||||
|
validateTimers[slotKey] = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) });
|
||||||
|
slotErrors = { ...slotErrors, [slotKey]: res.error || '' };
|
||||||
|
} catch {
|
||||||
|
// Network error, don't show as template error
|
||||||
|
slotErrors = { ...slotErrors, [slotKey]: '' };
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
const defaultForm = () => ({
|
const defaultForm = () => ({
|
||||||
name: '', description: '', icon: '',
|
name: '', description: '', icon: '',
|
||||||
@@ -161,7 +180,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if (slot.rows || 2) > 2}
|
{#if (slot.rows || 2) > 2}
|
||||||
<JinjaEditor value={(form as any)[slot.key] || ''} onchange={(v) => (form as any)[slot.key] = v} rows={slot.rows || 6} />
|
<JinjaEditor value={(form as any)[slot.key] || ''} onchange={(v) => { (form as any)[slot.key] = v; validateSlot(slot.key, v); }} rows={slot.rows || 6} />
|
||||||
|
{#if slotErrors[slot.key]}
|
||||||
|
<p class="mt-1 text-xs" style="color: var(--color-error-fg);">Syntax error: {slotErrors[slot.key]}</p>
|
||||||
|
{/if}
|
||||||
{#if slotPreview[slot.key]}
|
{#if slotPreview[slot.key]}
|
||||||
<div class="mt-1 p-2 bg-[var(--color-muted)] rounded text-sm">
|
<div class="mt-1 p-2 bg-[var(--color-muted)] rounded text-sm">
|
||||||
<pre class="whitespace-pre-wrap">{slotPreview[slot.key]}</pre>
|
<pre class="whitespace-pre-wrap">{slotPreview[slot.key]}</pre>
|
||||||
@@ -197,15 +219,11 @@
|
|||||||
{#if config.icon}<MdiIcon name={config.icon} />{/if}
|
{#if config.icon}<MdiIcon name={config.icon} />{/if}
|
||||||
<p class="font-medium">{config.name}</p>
|
<p class="font-medium">{config.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<pre class="text-xs text-[var(--color-muted-foreground)] mt-1 whitespace-pre-wrap font-mono bg-[var(--color-muted)] rounded p-2">{config.message_assets_added?.slice(0, 120)}...</pre>
|
{#if config.description}
|
||||||
{#if slotPreview['message_assets_added_' + config.id]}
|
<p class="text-sm text-[var(--color-muted-foreground)] mt-1">{config.description}</p>
|
||||||
<div class="mt-2 p-2 bg-[var(--color-success-bg)] rounded text-sm">
|
|
||||||
<pre class="whitespace-pre-wrap">{slotPreview['message_assets_added_' + config.id]}</pre>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 ml-4">
|
<div class="flex items-center gap-1 ml-4">
|
||||||
<IconButton icon="mdiEye" title={t('templateConfig.preview')} onclick={() => preview(config.id, 'message_assets_added')} />
|
|
||||||
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => edit(config)} />
|
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => edit(config)} />
|
||||||
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => remove(config.id)} variant="danger" />
|
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => remove(config.id)} variant="danger" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user