Jinja2 syntax highlighting + description field + preview toggle
Some checks failed
Validate / Hassfest (push) Has been cancelled
Some checks failed
Validate / Hassfest (push) Has been cancelled
JinjaEditor:
- Custom StreamLanguage parser for Jinja2 syntax highlighting:
{{ variables }} in blue, {% statements %} in purple, {# comments #} in gray
- Replaced HTML mode (didn't understand Jinja2 syntax)
- Proper monospace font (Consolas/Monaco)
TemplateConfig:
- Added `description` field to model + seed defaults with descriptions
- Description shown on template cards instead of raw template text
- Description input in create/edit form
Preview:
- Toggle behavior: clicking Preview again hides the preview
- Per-slot preview uses preview-raw API (renders current editor content)
i18n: added common.description, templateConfig.descriptionPlaceholder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
|
||||
26
frontend/src/lib/components/IconButton.svelte
Normal file
26
frontend/src/lib/components/IconButton.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import MdiIcon from './MdiIcon.svelte';
|
||||
|
||||
let { icon, title = '', onclick, disabled = false, variant = 'default', size = 16, class: className = '' } = $props<{
|
||||
icon: string;
|
||||
title?: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
disabled?: boolean;
|
||||
variant?: 'default' | 'danger' | 'success';
|
||||
size?: number;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const variantClasses = {
|
||||
default: 'text-[var(--color-muted-foreground)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-muted)]',
|
||||
danger: 'text-[var(--color-muted-foreground)] hover:text-[var(--color-destructive)] hover:bg-[var(--color-error-bg)]',
|
||||
success: 'text-[var(--color-muted-foreground)] hover:text-[var(--color-success-fg)] hover:bg-[var(--color-success-bg)]',
|
||||
};
|
||||
</script>
|
||||
|
||||
<button type="button" {title} {onclick} {disabled}
|
||||
class="inline-flex items-center justify-center w-7 h-7 rounded-md transition-colors
|
||||
disabled:opacity-40 disabled:pointer-events-none {variantClasses[variant]} {className}"
|
||||
>
|
||||
<MdiIcon name={icon} {size} />
|
||||
</button>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
|
||||
import { EditorView, placeholder as cmPlaceholder } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { getTheme } from '$lib/theme.svelte';
|
||||
|
||||
@@ -17,9 +17,41 @@
|
||||
let view: EditorView;
|
||||
const theme = getTheme();
|
||||
|
||||
// Simple Jinja2 stream parser for syntax highlighting
|
||||
const jinjaLang = StreamLanguage.define({
|
||||
token(stream) {
|
||||
// Jinja2 comment {# ... #}
|
||||
if (stream.match('{#')) {
|
||||
stream.skipTo('#}') && stream.match('#}');
|
||||
return 'comment';
|
||||
}
|
||||
// Jinja2 expression {{ ... }}
|
||||
if (stream.match('{{')) {
|
||||
while (!stream.eol()) {
|
||||
if (stream.match('}}')) return 'variableName';
|
||||
stream.next();
|
||||
}
|
||||
return 'variableName';
|
||||
}
|
||||
// Jinja2 statement {% ... %}
|
||||
if (stream.match('{%')) {
|
||||
while (!stream.eol()) {
|
||||
if (stream.match('%}')) return 'keyword';
|
||||
stream.next();
|
||||
}
|
||||
return 'keyword';
|
||||
}
|
||||
// Regular text
|
||||
while (stream.next()) {
|
||||
if (stream.peek() === '{') break;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const extensions = [
|
||||
html(), // Jinja2 is close enough to HTML template syntax for highlighting
|
||||
jinjaLang,
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
onchange(update.state.doc.toString());
|
||||
@@ -27,10 +59,14 @@
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.theme({
|
||||
'&': { fontSize: '13px', fontFamily: 'monospace' },
|
||||
'&': { fontSize: '13px', fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace" },
|
||||
'.cm-content': { minHeight: `${rows * 1.5}em`, padding: '8px' },
|
||||
'.cm-editor': { borderRadius: '0.375rem', border: '1px solid var(--color-border)' },
|
||||
'.cm-focused': { outline: '2px solid var(--color-primary)', outlineOffset: '0px' },
|
||||
// Jinja2 syntax colors
|
||||
'.ͼc': { color: '#e879f9' }, // keyword ({% %}) - purple
|
||||
'.ͼd': { color: '#38bdf8' }, // variableName ({{ }}) - blue
|
||||
'.ͼ5': { color: '#6b7280' }, // comment ({# #}) - gray
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -50,7 +86,6 @@
|
||||
return () => view.destroy();
|
||||
});
|
||||
|
||||
// Sync external value changes (e.g. when editing different config)
|
||||
$effect(() => {
|
||||
if (view && view.state.doc.toString() !== value) {
|
||||
view.dispatch({
|
||||
|
||||
@@ -237,6 +237,7 @@
|
||||
"newConfig": "New Config",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Default EN",
|
||||
"descriptionPlaceholder": "e.g. English templates for family notifications",
|
||||
"noConfigs": "No template configs yet.",
|
||||
"eventMessages": "Event Messages",
|
||||
"assetsAdded": "Assets added",
|
||||
@@ -296,6 +297,7 @@
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"description": "Description",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"error": "Error",
|
||||
|
||||
@@ -237,6 +237,7 @@
|
||||
"newConfig": "Новая конфигурация",
|
||||
"name": "Название",
|
||||
"namePlaceholder": "По умолчанию RU",
|
||||
"descriptionPlaceholder": "напр. Русские шаблоны для семейных уведомлений",
|
||||
"noConfigs": "Конфигураций шаблонов пока нет.",
|
||||
"eventMessages": "Сообщения о событиях",
|
||||
"assetsAdded": "Добавлены файлы",
|
||||
@@ -296,6 +297,7 @@
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"description": "Описание",
|
||||
"close": "Закрыть",
|
||||
"confirm": "Подтвердить",
|
||||
"error": "Ошибка",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import IconPicker from '$lib/components/IconPicker.svelte';
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
|
||||
let servers = $state<any[]>([]);
|
||||
let showForm = $state(false);
|
||||
@@ -76,7 +77,11 @@
|
||||
<PageHeader title={t('servers.title')} description={t('servers.description')}>
|
||||
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
|
||||
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
||||
{showForm ? t('servers.cancel') : t('servers.addServer')}
|
||||
{#if showForm}
|
||||
{t('servers.cancel')}
|
||||
{:else}
|
||||
<span class="flex items-center gap-1"><MdiIcon name="mdiPlus" size={14} />{t('servers.addServer')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</PageHeader>
|
||||
|
||||
@@ -136,9 +141,9 @@
|
||||
<p class="text-sm text-[var(--color-muted-foreground)]">{server.url}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick={() => edit(server)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
|
||||
<button onclick={() => startDelete(server)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('servers.delete')}</button>
|
||||
<div class="flex items-center gap-1">
|
||||
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => edit(server)} />
|
||||
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => startDelete(server)} variant="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
|
||||
let targets = $state<any[]>([]);
|
||||
let trackingConfigs = $state<any[]>([]);
|
||||
@@ -268,10 +269,10 @@
|
||||
{target.type === 'telegram' ? `Chat: ${target.config.chat_id || '***'}` : target.config.url || ''}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick={() => edit(target)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
|
||||
<button onclick={() => test(target.id)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('targets.test')}</button>
|
||||
<button onclick={() => confirmDelete = target} class="text-xs text-[var(--color-destructive)] hover:underline">{t('targets.delete')}</button>
|
||||
<div class="flex items-center gap-1">
|
||||
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => edit(target)} />
|
||||
<IconButton icon="mdiSend" title={t('targets.test')} onclick={() => test(target.id)} />
|
||||
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => confirmDelete = target} variant="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import IconPicker from '$lib/components/IconPicker.svelte';
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
|
||||
let bots = $state<any[]>([]);
|
||||
let loaded = $state(false);
|
||||
@@ -120,13 +121,12 @@
|
||||
</div>
|
||||
<p class="text-xs text-[var(--color-muted-foreground)] font-mono">{bot.token_preview}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<button onclick={() => loadChats(bot.id)}
|
||||
class="text-xs text-[var(--color-muted-foreground)] hover:underline">
|
||||
{t('telegramBot.chats')} {expandedBot === bot.id ? '▲' : '▼'}
|
||||
</button>
|
||||
<button onclick={() => remove(bot.id)}
|
||||
class="text-xs text-[var(--color-destructive)] hover:underline">{t('common.delete')}</button>
|
||||
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => remove(bot.id)} variant="danger" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
let slotPreview = $state<Record<string, string>>({});
|
||||
|
||||
const defaultForm = () => ({
|
||||
name: '', icon: '',
|
||||
name: '', description: '', icon: '',
|
||||
message_assets_added: '',
|
||||
message_assets_removed: '',
|
||||
message_album_renamed: '',
|
||||
@@ -79,12 +80,25 @@
|
||||
}
|
||||
|
||||
async function previewSlot(slotKey: string) {
|
||||
// Toggle: if already showing, hide it
|
||||
if (slotPreview[slotKey]) { delete slotPreview[slotKey]; slotPreview = { ...slotPreview }; return; }
|
||||
const template = (form as any)[slotKey] || '';
|
||||
if (!template) { slotPreview[slotKey] = '(empty)'; return; }
|
||||
if (!template) { slotPreview = { ...slotPreview, [slotKey]: '(empty)' }; return; }
|
||||
try {
|
||||
const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) });
|
||||
slotPreview[slotKey] = res.error ? `Error: ${res.error}` : res.rendered;
|
||||
} catch (err: any) { slotPreview[slotKey] = `Error: ${err.message}`; }
|
||||
slotPreview = { ...slotPreview, [slotKey]: res.error ? `Error: ${res.error}` : res.rendered };
|
||||
} catch (err: any) { slotPreview = { ...slotPreview, [slotKey]: `Error: ${err.message}` }; }
|
||||
}
|
||||
|
||||
async function preview(configId: number, slotKey: string) {
|
||||
const config = configs.find(c => c.id === configId);
|
||||
if (!config) return;
|
||||
const template = config[slotKey] || '';
|
||||
if (!template) return;
|
||||
try {
|
||||
const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) });
|
||||
slotPreview[slotKey + '_' + configId] = res.error ? `Error: ${res.error}` : res.rendered;
|
||||
} catch (err: any) { slotPreview[slotKey + '_' + configId] = `Error: ${err.message}`; }
|
||||
}
|
||||
|
||||
function remove(id: number) {
|
||||
@@ -121,6 +135,11 @@
|
||||
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tpc-desc" class="block text-sm font-medium mb-1">{t('common.description')}</label>
|
||||
<input id="tpc-desc" bind:value={form.description} placeholder={t('templateConfig.descriptionPlaceholder')}
|
||||
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||
</div>
|
||||
|
||||
{#each templateSlots as group}
|
||||
<fieldset class="border border-[var(--color-border)] rounded-md p-3">
|
||||
@@ -178,11 +197,17 @@
|
||||
{#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, 150)}...</pre>
|
||||
<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}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 ml-4">
|
||||
<button onclick={() => edit(config)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
|
||||
<button onclick={() => remove(config.id)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('common.delete')}</button>
|
||||
<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>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
|
||||
let loaded = $state(false);
|
||||
let loadError = $state('');
|
||||
@@ -219,24 +220,18 @@
|
||||
</div>
|
||||
<p class="text-sm text-[var(--color-muted-foreground)]">{tracker.album_ids.length} {t('trackers.albums_count')} · {t('trackers.every')} {tracker.scan_interval}s · {tracker.target_ids.length} {t('trackers.targets')}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick={() => edit(tracker)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
|
||||
<button onclick={async () => { await api(`/trackers/${tracker.id}/trigger`, { method: 'POST' }); }} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.test')}</button>
|
||||
<button onclick={() => testPeriodic(tracker)} disabled={testingPeriodic[tracker.id]} class="text-xs text-[var(--color-muted-foreground)] hover:underline disabled:opacity-50">
|
||||
{testingPeriodic[tracker.id] ? '...' : t('trackers.testPeriodic')}
|
||||
</button>
|
||||
<button onclick={() => testMemory(tracker)} disabled={testingMemory[tracker.id]} class="text-xs text-[var(--color-muted-foreground)] hover:underline disabled:opacity-50">
|
||||
{testingMemory[tracker.id] ? '...' : t('trackers.testMemory')}
|
||||
</button>
|
||||
<div class="flex items-center gap-1">
|
||||
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => edit(tracker)} />
|
||||
<IconButton icon="mdiPlay" title={t('common.test')} onclick={async () => { await api(`/trackers/${tracker.id}/trigger`, { method: 'POST' }); }} />
|
||||
<IconButton icon="mdiCalendarClock" title={t('trackers.testPeriodic')} onclick={() => testPeriodic(tracker)} disabled={testingPeriodic[tracker.id]} />
|
||||
<IconButton icon="mdiHistory" title={t('trackers.testMemory')} onclick={() => testMemory(tracker)} disabled={testingMemory[tracker.id]} />
|
||||
{#if testFeedback[tracker.id]}
|
||||
<span class="text-xs {testFeedback[tracker.id] === 'ok' ? 'text-[var(--color-success-fg)]' : 'text-[var(--color-destructive)]'}">
|
||||
{testFeedback[tracker.id] === 'ok' ? '\u2713' : '\u2717'}
|
||||
</span>
|
||||
{/if}
|
||||
<button onclick={() => toggle(tracker)} disabled={toggling[tracker.id]} class="text-xs text-[var(--color-muted-foreground)] hover:underline disabled:opacity-50">
|
||||
{toggling[tracker.id] ? '...' : tracker.enabled ? t('trackers.pause') : t('trackers.resume')}
|
||||
</button>
|
||||
<button onclick={() => startDelete(tracker)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('trackers.delete')}</button>
|
||||
<IconButton icon={tracker.enabled ? 'mdiPause' : 'mdiPlay'} title={tracker.enabled ? t('trackers.pause') : t('trackers.resume')} onclick={() => toggle(tracker)} disabled={toggling[tracker.id]} />
|
||||
<IconButton icon="mdiDelete" title={t('trackers.delete')} onclick={() => startDelete(tracker)} variant="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
|
||||
let configs = $state<any[]>([]);
|
||||
let loaded = $state(false);
|
||||
@@ -208,9 +209,9 @@
|
||||
{config.memory_enabled ? ' · memory' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick={() => edit(config)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
|
||||
<button onclick={() => remove(config.id)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('common.delete')}</button>
|
||||
<div class="flex items-center gap-1">
|
||||
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => edit(config)} />
|
||||
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => remove(config.id)} variant="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
|
||||
const auth = getAuth();
|
||||
let users = $state<any[]>([]);
|
||||
@@ -101,10 +102,10 @@
|
||||
<p class="font-medium">{user.username}</p>
|
||||
<p class="text-sm text-[var(--color-muted-foreground)]">{user.role} · {t('users.joined')} {new Date(user.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1">
|
||||
{#if user.id !== auth.user?.id}
|
||||
<button onclick={() => openResetPassword(user)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">🔑</button>
|
||||
<button onclick={() => remove(user.id)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('users.delete')}</button>
|
||||
<IconButton icon="mdiKeyVariant" title={t('common.changePassword')} onclick={() => openResetPassword(user)} />
|
||||
<IconButton icon="mdiDelete" title={t('users.delete')} onclick={() => remove(user.id)} variant="danger" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,7 @@ class TemplateConfig(SQLModel, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="user.id")
|
||||
name: str # e.g. "Default EN", "Default RU"
|
||||
description: str = Field(default="") # Short description shown on card
|
||||
icon: str = Field(default="")
|
||||
|
||||
# Event-driven notification templates (full Jinja2)
|
||||
|
||||
@@ -48,8 +48,8 @@ async def _seed_default_templates():
|
||||
return
|
||||
|
||||
# user_id=0 means system-owned (available to all users)
|
||||
en = TemplateConfig(user_id=0, name="Default EN", icon="mdiTranslate", **get_default_templates("en"))
|
||||
ru = TemplateConfig(user_id=0, name="По умолчанию RU", icon="mdiTranslate", **get_default_templates("ru"))
|
||||
en = TemplateConfig(user_id=0, name="Default EN", description="Default English notification templates", icon="mdiTranslate", **get_default_templates("en"))
|
||||
ru = TemplateConfig(user_id=0, name="По умолчанию RU", description="Шаблоны уведомлений на русском языке", icon="mdiTranslate", **get_default_templates("ru"))
|
||||
session.add(en)
|
||||
session.add(ru)
|
||||
await session.commit()
|
||||
|
||||
Reference in New Issue
Block a user