Add real-time Jinja2 syntax validation with debounced API check
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:
2026-03-19 19:29:52 +03:00
parent ce21733ae6
commit 31873a8ffd

View File

@@ -23,6 +23,25 @@
let error = $state('');
let confirmDelete = $state<any>(null);
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 = () => ({
name: '', description: '', icon: '',
@@ -161,7 +180,10 @@
</div>
</div>
{#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]}
<div class="mt-1 p-2 bg-[var(--color-muted)] rounded text-sm">
<pre class="whitespace-pre-wrap">{slotPreview[slot.key]}</pre>
@@ -197,15 +219,11 @@
{#if config.icon}<MdiIcon name={config.icon} />{/if}
<p class="font-medium">{config.name}</p>
</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 slotPreview['message_assets_added_' + config.id]}
<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 config.description}
<p class="text-sm text-[var(--color-muted-foreground)] mt-1">{config.description}</p>
{/if}
</div>
<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="mdiDelete" title={t('common.delete')} onclick={() => remove(config.id)} variant="danger" />
</div>