feat(frontend): autogenerate entity names from type/provider

Mirror the providers form pattern (defaultName tied to type) across
bots, targets, trackers, actions, and configs. Each form now derives
form.name from the selected type or provider while the user hasn't
manually edited it; switching to edit-mode flips the manualEdited
flag so existing names are preserved.

Defaults: bots → "<Type> Bot"; targets → type label; notification
trackers → "<provider> Tracker"; command trackers → "<provider>
Commands"; actions → "<provider> <Action Type>"; tracking/template/
command/command-template configs → "<descriptor.defaultName>
<Suffix>". TargetForm and TrackerForm grew an optional onnameinput
prop so parents can flag manual edits in subform inputs.
This commit is contained in:
2026-05-07 13:01:52 +03:00
parent 349e9136a4
commit 5bd63a2191
13 changed files with 143 additions and 21 deletions
+15 -1
View File
@@ -40,7 +40,19 @@
schedule_type: 'interval', schedule_interval: 3600, schedule_cron: '', schedule_type: 'interval', schedule_interval: 3600, schedule_cron: '',
enabled: false, enabled: false,
}); });
let nameManuallyEdited = $state(false);
let error = $state(''); let error = $state('');
function actionTypeLabel(at: string): string {
return at.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
const provider = providers.find((p: any) => p.id === form.provider_id);
const at = actionTypeLabel(form.action_type || '');
form.name = provider ? `${provider.name} ${at}`.trim() : at || 'Action';
}
});
let loadError = $state(''); let loadError = $state('');
let submitting = $state(false); let submitting = $state(false);
let loaded = $state(false); let loaded = $state(false);
@@ -98,6 +110,7 @@
config: {}, schedule_type: 'interval', schedule_interval: 3600, schedule_cron: '', config: {}, schedule_type: 'interval', schedule_interval: 3600, schedule_cron: '',
enabled: false, enabled: false,
}; };
nameManuallyEdited = false;
editing = null; showForm = true; editing = null; showForm = true;
} }
@@ -109,6 +122,7 @@
schedule_interval: action.schedule_interval, schedule_interval: action.schedule_interval,
schedule_cron: action.schedule_cron, enabled: action.enabled, schedule_cron: action.schedule_cron, enabled: action.enabled,
}; };
nameManuallyEdited = true;
editing = action.id; showForm = true; editing = action.id; showForm = true;
} }
@@ -245,7 +259,7 @@
<label for="act-name" class="block text-sm font-medium mb-1">{t('actions.name')}</label> <label for="act-name" class="block text-sm font-medium mb-1">{t('actions.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="act-name" bind:value={form.name} required <input id="act-name" bind:value={form.name} oninput={() => nameManuallyEdited = true} required
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
+12 -3
View File
@@ -30,8 +30,16 @@
smtp_username: '', smtp_password: '', smtp_use_tls: true, smtp_username: '', smtp_password: '', smtp_use_tls: true,
}); });
let emailForm = $state(defaultEmailForm()); let emailForm = $state(defaultEmailForm());
let nameManuallyEdited = $state(false);
function openNewEmail() { emailForm = defaultEmailForm(); editingEmail = null; showEmailForm = true; } const DEFAULT_BOT_NAME = 'Email Bot';
$effect(() => {
if (showEmailForm && !nameManuallyEdited && !editingEmail) {
emailForm.name = DEFAULT_BOT_NAME;
}
});
function openNewEmail() { emailForm = defaultEmailForm(); nameManuallyEdited = false; editingEmail = null; showEmailForm = true; }
function editEmailBot(bot: EmailBot) { function editEmailBot(bot: EmailBot) {
emailForm = { emailForm = {
name: bot.name, icon: bot.icon || '', email: bot.email, name: bot.name, icon: bot.icon || '', email: bot.email,
@@ -39,6 +47,7 @@
smtp_username: bot.smtp_username, smtp_password: '', smtp_username: bot.smtp_username, smtp_password: '',
smtp_use_tls: bot.smtp_use_tls, smtp_use_tls: bot.smtp_use_tls,
}; };
nameManuallyEdited = true;
editingEmail = bot.id; showEmailForm = true; editingEmail = bot.id; showEmailForm = true;
} }
@@ -54,7 +63,7 @@
await api('/email-bots', { method: 'POST', body: JSON.stringify(body) }); await api('/email-bots', { method: 'POST', body: JSON.stringify(body) });
snackSuccess(t('snack.emailBotCreated')); snackSuccess(t('snack.emailBotCreated'));
} }
emailForm = defaultEmailForm(); showEmailForm = false; editingEmail = null; await onreload(); emailForm = defaultEmailForm(); nameManuallyEdited = false; showEmailForm = false; editingEmail = null; await onreload();
} catch (err: any) { error = err.message; snackError(err.message); } } catch (err: any) { error = err.message; snackError(err.message); }
finally { emailSubmitting = false; } finally { emailSubmitting = false; }
} }
@@ -107,7 +116,7 @@
<label for="ebot-name" class="block text-sm font-medium mb-1">{t('emailBot.name')}</label> <label for="ebot-name" class="block text-sm font-medium mb-1">{t('emailBot.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={emailForm.icon} onselect={(v: string) => emailForm.icon = v} /> <IconPicker value={emailForm.icon} onselect={(v: string) => emailForm.icon = v} />
<input id="ebot-name" bind:value={emailForm.name} required placeholder={t('emailBot.namePlaceholder')} <input id="ebot-name" bind:value={emailForm.name} oninput={() => nameManuallyEdited = true} required placeholder={t('emailBot.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
+12 -3
View File
@@ -29,14 +29,23 @@
name: '', icon: '', homeserver_url: '', access_token: '', display_name: '', name: '', icon: '', homeserver_url: '', access_token: '', display_name: '',
}); });
let matrixForm = $state(defaultMatrixForm()); let matrixForm = $state(defaultMatrixForm());
let nameManuallyEdited = $state(false);
function openNewMatrix() { matrixForm = defaultMatrixForm(); editingMatrix = null; showMatrixForm = true; } const DEFAULT_BOT_NAME = 'Matrix Bot';
$effect(() => {
if (showMatrixForm && !nameManuallyEdited && !editingMatrix) {
matrixForm.name = DEFAULT_BOT_NAME;
}
});
function openNewMatrix() { matrixForm = defaultMatrixForm(); nameManuallyEdited = false; editingMatrix = null; showMatrixForm = true; }
function editMatrixBot(bot: MatrixBot) { function editMatrixBot(bot: MatrixBot) {
matrixForm = { matrixForm = {
name: bot.name, icon: bot.icon || '', name: bot.name, icon: bot.icon || '',
homeserver_url: bot.homeserver_url, access_token: '', homeserver_url: bot.homeserver_url, access_token: '',
display_name: bot.display_name || '', display_name: bot.display_name || '',
}; };
nameManuallyEdited = true;
editingMatrix = bot.id; showMatrixForm = true; editingMatrix = bot.id; showMatrixForm = true;
} }
@@ -52,7 +61,7 @@
await api('/matrix-bots', { method: 'POST', body: JSON.stringify(body) }); await api('/matrix-bots', { method: 'POST', body: JSON.stringify(body) });
snackSuccess(t('snack.matrixBotCreated')); snackSuccess(t('snack.matrixBotCreated'));
} }
matrixForm = defaultMatrixForm(); showMatrixForm = false; editingMatrix = null; await onreload(); matrixForm = defaultMatrixForm(); nameManuallyEdited = false; showMatrixForm = false; editingMatrix = null; await onreload();
} catch (err: any) { error = err.message; snackError(err.message); } } catch (err: any) { error = err.message; snackError(err.message); }
finally { matrixSubmitting = false; } finally { matrixSubmitting = false; }
} }
@@ -105,7 +114,7 @@
<label for="mbot-name" class="block text-sm font-medium mb-1">{t('matrixBot.name')}</label> <label for="mbot-name" class="block text-sm font-medium mb-1">{t('matrixBot.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={matrixForm.icon} onselect={(v: string) => matrixForm.icon = v} /> <IconPicker value={matrixForm.icon} onselect={(v: string) => matrixForm.icon = v} />
<input id="mbot-name" bind:value={matrixForm.name} required placeholder={t('matrixBot.namePlaceholder')} <input id="mbot-name" bind:value={matrixForm.name} oninput={() => nameManuallyEdited = true} required placeholder={t('matrixBot.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
+12 -4
View File
@@ -29,10 +29,18 @@
let showForm = $state(false); let showForm = $state(false);
let editing = $state<number | null>(null); let editing = $state<number | null>(null);
let form = $state({ name: '', icon: '', token: '' }); let form = $state({ name: '', icon: '', token: '' });
let nameManuallyEdited = $state(false);
let error = $state(''); let error = $state('');
let submitting = $state(false); let submitting = $state(false);
let confirmDelete = $state<{ id: number; onconfirm: () => Promise<void> } | null>(null); let confirmDelete = $state<{ id: number; onconfirm: () => Promise<void> } | null>(null);
const DEFAULT_BOT_NAME = 'Telegram Bot';
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
form.name = DEFAULT_BOT_NAME;
}
});
// Per-bot expandable sections // Per-bot expandable sections
let chats = $state<Record<number, TelegramChat[]>>({}); let chats = $state<Record<number, TelegramChat[]>>({});
let chatsLoading = $state<Record<number, boolean>>({}); let chatsLoading = $state<Record<number, boolean>>({});
@@ -52,8 +60,8 @@
let botListenerStatus = $state<Record<number, CommandTrackerSummary[]>>({}); let botListenerStatus = $state<Record<number, CommandTrackerSummary[]>>({});
let botListenerLoading = $state<Record<number, boolean>>({}); let botListenerLoading = $state<Record<number, boolean>>({});
function openNew() { form = { name: '', icon: '', token: '' }; editing = null; showForm = true; } function openNew() { form = { name: '', icon: '', token: '' }; nameManuallyEdited = false; editing = null; showForm = true; }
function editBot(bot: TelegramBot) { form = { name: bot.name, icon: bot.icon || '', token: '' }; editing = bot.id; showForm = true; } function editBot(bot: TelegramBot) { form = { name: bot.name, icon: bot.icon || '', token: '' }; nameManuallyEdited = true; editing = bot.id; showForm = true; }
async function saveBot(e: SubmitEvent) { async function saveBot(e: SubmitEvent) {
e.preventDefault(); error = ''; submitting = true; e.preventDefault(); error = ''; submitting = true;
@@ -65,7 +73,7 @@
await api('/telegram-bots', { method: 'POST', body: JSON.stringify(form) }); await api('/telegram-bots', { method: 'POST', body: JSON.stringify(form) });
snackSuccess(t('snack.botRegistered')); snackSuccess(t('snack.botRegistered'));
} }
form = { name: '', icon: '', token: '' }; showForm = false; editing = null; await onreload(); form = { name: '', icon: '', token: '' }; nameManuallyEdited = false; showForm = false; editing = null; await onreload();
} catch (err: any) { error = err.message; snackError(err.message); } } catch (err: any) { error = err.message; snackError(err.message); }
finally { submitting = false; } finally { submitting = false; }
} }
@@ -312,7 +320,7 @@
<label for="bot-name" class="block text-sm font-medium mb-1">{t('telegramBot.name')}</label> <label for="bot-name" class="block text-sm font-medium mb-1">{t('telegramBot.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="bot-name" bind:value={form.name} required placeholder={t('telegramBot.namePlaceholder')} <input id="bot-name" bind:value={form.name} oninput={() => nameManuallyEdited = true} required placeholder={t('telegramBot.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
@@ -21,6 +21,7 @@
import { highlightFromUrl } from '$lib/highlight'; import { highlightFromUrl } from '$lib/highlight';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte'; import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte'; import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import { getDescriptor } from '$lib/providers';
import type { CommandConfig } from '$lib/types'; import type { CommandConfig } from '$lib/types';
function templateName(id: number | null): string { function templateName(id: number | null): string {
@@ -69,6 +70,14 @@
command_template_config_id: null as number | null, command_template_config_id: null as number | null,
}); });
let form = $state(defaultForm()); let form = $state(defaultForm());
let nameManuallyEdited = $state(false);
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
const desc = getDescriptor(form.provider_type);
form.name = desc ? `${desc.defaultName} Commands` : 'Commands';
}
});
let allCapabilities = $derived(capabilitiesCache.items); let allCapabilities = $derived(capabilitiesCache.items);
let providerCommands = $derived<{key: string, icon: string}[]>( let providerCommands = $derived<{key: string, icon: string}[]>(
@@ -107,6 +116,7 @@
// Auto-select first matching template for the chosen provider_type // Auto-select first matching template for the chosen provider_type
const match = cmdTemplateConfigs.find((c) => c.provider_type === form.provider_type); const match = cmdTemplateConfigs.find((c) => c.provider_type === form.provider_type);
if (match) form.command_template_config_id = match.id; if (match) form.command_template_config_id = match.id;
nameManuallyEdited = false;
editing = null; editing = null;
showForm = true; showForm = true;
} }
@@ -142,6 +152,7 @@
rate_limits: { search: cfg.rate_limits?.search ?? 30, default: cfg.rate_limits?.default ?? 10 }, rate_limits: { search: cfg.rate_limits?.search ?? 30, default: cfg.rate_limits?.default ?? 10 },
command_template_config_id: cfg.command_template_config_id ?? null, command_template_config_id: cfg.command_template_config_id ?? null,
}; };
nameManuallyEdited = true;
editing = cfg.id; editing = cfg.id;
showForm = true; showForm = true;
} }
@@ -165,7 +176,7 @@
await api('/command-configs', { method: 'POST', body }); await api('/command-configs', { method: 'POST', body });
snackSuccess(t('snack.commandConfigSaved')); snackSuccess(t('snack.commandConfigSaved'));
} }
form = defaultForm(); showForm = false; editing = null; await load(); form = defaultForm(); nameManuallyEdited = false; showForm = false; editing = null; await load();
} catch (err: any) { error = err.message; snackError(err.message); } } catch (err: any) { error = err.message; snackError(err.message); }
finally { submitting = false; } finally { submitting = false; }
} }
@@ -214,7 +225,7 @@
<label for="cfg-name" class="block text-sm font-medium mb-1">{t('commandConfig.name')}</label> <label for="cfg-name" class="block text-sm font-medium mb-1">{t('commandConfig.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="cfg-name" bind:value={form.name} required placeholder={t('commandConfig.namePlaceholder')} <input id="cfg-name" bind:value={form.name} oninput={() => nameManuallyEdited = true} required placeholder={t('commandConfig.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
@@ -26,6 +26,7 @@
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte'; import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import { highlightFromUrl } from '$lib/highlight'; import { highlightFromUrl } from '$lib/highlight';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte'; import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import { getDescriptor } from '$lib/providers';
interface CmdTemplateConfig { interface CmdTemplateConfig {
id: number; id: number;
@@ -120,6 +121,14 @@
slots: {} as Record<string, Record<string, string>>, slots: {} as Record<string, Record<string, string>>,
}); });
let form = $state(defaultForm()); let form = $state(defaultForm());
let nameManuallyEdited = $state(false);
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
const desc = getDescriptor(form.provider_type);
form.name = desc ? `${desc.defaultName} Command Templates` : 'Command Templates';
}
});
// Provider capabilities // Provider capabilities
let allCapabilities = $state<Record<string, any>>({}); let allCapabilities = $state<Record<string, any>>({});
@@ -257,6 +266,7 @@
form = defaultForm(); form = defaultForm();
const typesWithCmdSlots = providerTypes.filter(t => (allCapabilities[t]?.command_slots?.length || 0) > 0); const typesWithCmdSlots = providerTypes.filter(t => (allCapabilities[t]?.command_slots?.length || 0) > 0);
if (typesWithCmdSlots.length > 0) form.provider_type = typesWithCmdSlots[0]; if (typesWithCmdSlots.length > 0) form.provider_type = typesWithCmdSlots[0];
nameManuallyEdited = false;
editing = null; editing = null;
showForm = true; showForm = true;
activeLocale = primaryLocale; activeLocale = primaryLocale;
@@ -280,6 +290,7 @@
icon: c.icon || '', icon: c.icon || '',
slots: slotsCopy, slots: slotsCopy,
}; };
nameManuallyEdited = true;
editing = c.id; editing = c.id;
showForm = true; showForm = true;
activeLocale = primaryLocale; activeLocale = primaryLocale;
@@ -432,7 +443,7 @@
<label for="ct-name" class="block text-sm font-medium mb-1">{t('cmdTemplateConfig.name')}</label> <label for="ct-name" class="block text-sm font-medium mb-1">{t('cmdTemplateConfig.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="ct-name" bind:value={form.name} required placeholder={t('cmdTemplateConfig.namePlaceholder')} <input id="ct-name" bind:value={form.name} oninput={() => nameManuallyEdited = true} required placeholder={t('cmdTemplateConfig.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
@@ -61,6 +61,14 @@
enabled: true, enabled: true,
}); });
let form = $state(defaultForm()); let form = $state(defaultForm());
let nameManuallyEdited = $state(false);
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
const provider = providers.find(p => p.id === form.provider_id);
form.name = provider ? `${provider.name} Commands` : 'Commands';
}
});
// Filter command configs by selected provider's type // Filter command configs by selected provider's type
let filteredConfigs = $derived.by(() => { let filteredConfigs = $derived.by(() => {
@@ -110,6 +118,7 @@
const firstCfg = commandConfigs.find(c => c.provider_type === ptype); const firstCfg = commandConfigs.find(c => c.provider_type === ptype);
if (firstCfg) form.command_config_id = firstCfg.id; if (firstCfg) form.command_config_id = firstCfg.id;
} }
nameManuallyEdited = false;
editing = null; editing = null;
showForm = true; showForm = true;
} }
@@ -141,6 +150,7 @@
command_config_id: trk.command_config_id, command_config_id: trk.command_config_id,
enabled: trk.enabled, enabled: trk.enabled,
}; };
nameManuallyEdited = true;
editing = trk.id; editing = trk.id;
showForm = true; showForm = true;
} }
@@ -156,7 +166,7 @@
await api('/command-trackers', { method: 'POST', body }); await api('/command-trackers', { method: 'POST', body });
snackSuccess(t('snack.commandTrackerCreated')); snackSuccess(t('snack.commandTrackerCreated'));
} }
form = defaultForm(); showForm = false; editing = null; await load(); form = defaultForm(); nameManuallyEdited = false; showForm = false; editing = null; await load();
} catch (err: any) { error = err.message; snackError(err.message); } } catch (err: any) { error = err.message; snackError(err.message); }
finally { submitting = false; } finally { submitting = false; }
} }
@@ -288,7 +298,7 @@
<label for="trk-name" class="block text-sm font-medium mb-1">{t('commandTracker.name')}</label> <label for="trk-name" class="block text-sm font-medium mb-1">{t('commandTracker.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="trk-name" bind:value={form.name} required placeholder={t('commandTracker.namePlaceholder')} <input id="trk-name" bind:value={form.name} oninput={() => nameManuallyEdited = true} required placeholder={t('commandTracker.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
@@ -70,11 +70,19 @@
filters: {} as Record<string, any>, filters: {} as Record<string, any>,
}); });
let form = $state(defaultForm()); let form = $state(defaultForm());
let nameManuallyEdited = $state(false);
let selectedProviderType = $derived( let selectedProviderType = $derived(
providers.find(p => p.id === form.provider_id)?.type || '' providers.find(p => p.id === form.provider_id)?.type || ''
); );
let error = $state(''); let error = $state('');
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
const provider = providers.find(p => p.id === form.provider_id);
form.name = provider ? `${provider.name} Tracker` : 'Tracker';
}
});
// Linked targets management // Linked targets management
let expandedTracker = $state<number | null>(null); let expandedTracker = $state<number | null>(null);
let addingTarget = $state<Record<number, boolean>>({}); let addingTarget = $state<Record<number, boolean>>({});
@@ -210,6 +218,7 @@
form = defaultForm(); form = defaultForm();
// Auto-select first provider if any // Auto-select first provider if any
if (providers.length > 0) form.provider_id = providers[0].id; if (providers.length > 0) form.provider_id = providers[0].id;
nameManuallyEdited = false;
editing = null; showForm = true; collections = []; users = []; previousCollectionIds = []; editing = null; showForm = true; collections = []; users = []; previousCollectionIds = [];
} }
@@ -224,6 +233,7 @@
filters: trk.filters || {}, filters: trk.filters || {},
}; };
previousCollectionIds = [...(trk.collection_ids || [])]; previousCollectionIds = [...(trk.collection_ids || [])];
nameManuallyEdited = true;
editing = trk.id; showForm = true; editing = trk.id; showForm = true;
if (form.provider_id) { if (form.provider_id) {
await Promise.all([loadCollections(), loadUsers()]); await Promise.all([loadCollections(), loadUsers()]);
@@ -491,6 +501,7 @@
onsave={save} onsave={save}
ontoggleCollection={toggleCollection} ontoggleCollection={toggleCollection}
{formatDate} {formatDate}
onnameinput={() => nameManuallyEdited = true}
/> />
{/if} {/if}
@@ -35,6 +35,7 @@
onsave: (e: SubmitEvent) => void; onsave: (e: SubmitEvent) => void;
ontoggleCollection?: (collectionId: string) => void; ontoggleCollection?: (collectionId: string) => void;
formatDate?: (dateStr: string) => string; formatDate?: (dateStr: string) => string;
onnameinput?: () => void;
} }
let { let {
@@ -53,6 +54,7 @@
onsave, onsave,
ontoggleCollection, ontoggleCollection,
formatDate, formatDate,
onnameinput,
}: Props = $props(); }: Props = $props();
let descriptor = $derived(getDescriptor(providerType)); let descriptor = $derived(getDescriptor(providerType));
@@ -95,7 +97,7 @@
<label for="trk-name" class="block text-sm font-medium mb-1">{t('notificationTracker.name')}</label> <label for="trk-name" class="block text-sm font-medium mb-1">{t('notificationTracker.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="trk-name" bind:value={form.name} required placeholder={t('notificationTracker.namePlaceholder')} class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> <input id="trk-name" bind:value={form.name} oninput={() => onnameinput?.()} required placeholder={t('notificationTracker.namePlaceholder')} class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
<div> <div>
+15
View File
@@ -129,6 +129,7 @@
child_target_ids: [] as number[], child_target_ids: [] as number[],
}); });
let form = $state(defaultForm()); let form = $state(defaultForm());
let nameManuallyEdited = $state(false);
let error = $state(''); let error = $state('');
let loaded = $state(false); let loaded = $state(false);
let submitting = $state(false); let submitting = $state(false);
@@ -137,6 +138,17 @@
let confirmDelete = $state<NotificationTarget | null>(null); let confirmDelete = $state<NotificationTarget | null>(null);
let formEl = $state<HTMLElement | undefined>(); let formEl = $state<HTMLElement | undefined>();
const TARGET_TYPE_DEFAULT_NAMES: Record<TargetType, string> = {
telegram: 'Telegram', webhook: 'Webhook', email: 'Email',
discord: 'Discord', slack: 'Slack', ntfy: 'ntfy', matrix: 'Matrix',
broadcast: 'Broadcast',
};
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
form.name = TARGET_TYPE_DEFAULT_NAMES[formType] ?? '';
}
});
async function scrollToForm() { async function scrollToForm() {
await tick(); await tick();
formEl?.scrollIntoView({ behavior: 'smooth', block: 'start' }); formEl?.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -213,6 +225,7 @@
if (formType === 'telegram' && telegramBots.length > 0) form.bot_id = telegramBots[0].id; if (formType === 'telegram' && telegramBots.length > 0) form.bot_id = telegramBots[0].id;
if (formType === 'email' && emailBots.length > 0) form.email_bot_id = emailBots[0].id; if (formType === 'email' && emailBots.length > 0) form.email_bot_id = emailBots[0].id;
if (formType === 'matrix' && matrixBots.length > 0) form.matrix_bot_id = matrixBots[0].id; if (formType === 'matrix' && matrixBots.length > 0) form.matrix_bot_id = matrixBots[0].id;
nameManuallyEdited = false;
editing = null; editing = null;
showTelegramSettings = false; showTelegramSettings = false;
showForm = true; showForm = true;
@@ -242,6 +255,7 @@
// broadcast // broadcast
child_target_ids: c.child_target_ids || [], child_target_ids: c.child_target_ids || [],
}; };
nameManuallyEdited = true;
editing = tgt.id; editing = tgt.id;
showTelegramSettings = false; showTelegramSettings = false;
showForm = true; showForm = true;
@@ -476,6 +490,7 @@
bind:showTelegramSettings bind:showTelegramSettings
onsave={save} onsave={save}
ontoggleTelegramSettings={() => showTelegramSettings = !showTelegramSettings} ontoggleTelegramSettings={() => showTelegramSettings = !showTelegramSettings}
onnameinput={() => nameManuallyEdited = true}
/> />
{/if} {/if}
@@ -49,6 +49,7 @@
showTelegramSettings: boolean; showTelegramSettings: boolean;
onsave: (e: SubmitEvent) => void; onsave: (e: SubmitEvent) => void;
ontoggleTelegramSettings: () => void; ontoggleTelegramSettings: () => void;
onnameinput?: () => void;
} }
let { let {
@@ -70,6 +71,7 @@
showTelegramSettings = $bindable(), showTelegramSettings = $bindable(),
onsave, onsave,
ontoggleTelegramSettings, ontoggleTelegramSettings,
onnameinput,
}: Props = $props(); }: Props = $props();
</script> </script>
@@ -87,7 +89,7 @@
<label for="tgt-name" class="block text-sm font-medium mb-1">{t('targets.name')}</label> <label for="tgt-name" class="block text-sm font-medium mb-1">{t('targets.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="tgt-name" bind:value={form.name} required placeholder={t('targets.namePlaceholder')} class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> <input id="tgt-name" bind:value={form.name} oninput={() => onnameinput?.()} required placeholder={t('targets.namePlaceholder')} class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
{#if formType === 'telegram'} {#if formType === 'telegram'}
@@ -28,6 +28,7 @@
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte'; import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte'; import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import { getDescriptor } from '$lib/providers';
import type { TemplateConfig } from '$lib/types'; import type { TemplateConfig } from '$lib/types';
let allTemplateConfigs = $derived(templateConfigsCache.items); let allTemplateConfigs = $derived(templateConfigsCache.items);
@@ -194,8 +195,16 @@
date_only_format: '%d.%m.%Y', date_only_format: '%d.%m.%Y',
}); });
let form = $state(defaultForm()); let form = $state(defaultForm());
let nameManuallyEdited = $state(false);
let previewTargetType = $state('telegram'); let previewTargetType = $state('telegram');
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
const desc = getDescriptor(form.provider_type);
form.name = desc ? `${desc.defaultName} Templates` : 'Templates';
}
});
// Provider capabilities: from shared cache // Provider capabilities: from shared cache
let allCapabilities = $derived(capabilitiesCache.items); let allCapabilities = $derived(capabilitiesCache.items);
let providerTypes = $derived(Object.keys(allCapabilities)); let providerTypes = $derived(Object.keys(allCapabilities));
@@ -291,6 +300,7 @@
function openNew() { function openNew() {
form = defaultForm(); form = defaultForm();
if (providerTypes.length > 0) form.provider_type = providerTypes[0]; if (providerTypes.length > 0) form.provider_type = providerTypes[0];
nameManuallyEdited = false;
editing = null; showForm = true; activeLocale = primaryLocale; slotPreview = {}; slotErrors = {}; dateFormatPreview = {}; expandedSlots = new Set(); showPreviewFor = new Set(); slotFilter = ''; editing = null; showForm = true; activeLocale = primaryLocale; slotPreview = {}; slotErrors = {}; dateFormatPreview = {}; expandedSlots = new Set(); showPreviewFor = new Set(); slotFilter = '';
refreshDateFormatPreview(); refreshDateFormatPreview();
} }
@@ -304,6 +314,7 @@
date_format: c.date_format || '%d.%m.%Y, %H:%M UTC', date_format: c.date_format || '%d.%m.%Y, %H:%M UTC',
date_only_format: c.date_only_format || '%d.%m.%Y', date_only_format: c.date_only_format || '%d.%m.%Y',
}; };
nameManuallyEdited = true;
editing = c.id; showForm = true; activeLocale = primaryLocale; editing = c.id; showForm = true; activeLocale = primaryLocale;
slotPreview = {}; slotErrors = {}; dateFormatPreview = {}; slotPreview = {}; slotErrors = {}; dateFormatPreview = {};
expandedSlots = new Set(); showPreviewFor = new Set(); slotFilter = ''; expandedSlots = new Set(); showPreviewFor = new Set(); slotFilter = '';
@@ -439,7 +450,7 @@
<label for="tpc-name" class="block text-sm font-medium mb-1">{t('templateConfig.name')}</label> <label for="tpc-name" class="block text-sm font-medium mb-1">{t('templateConfig.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="tpc-name" bind:value={form.name} required placeholder={t('templateConfig.namePlaceholder')} <input id="tpc-name" bind:value={form.name} oninput={() => nameManuallyEdited = true} required placeholder={t('templateConfig.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>
@@ -190,6 +190,14 @@
}); });
let form: Record<string, any> = $state(defaultForm()); let form: Record<string, any> = $state(defaultForm());
let descriptor = $derived(getDescriptor(form.provider_type)); let descriptor = $derived(getDescriptor(form.provider_type));
let nameManuallyEdited = $state(false);
$effect(() => {
if (showForm && !nameManuallyEdited && !editing) {
const desc = getDescriptor(form.provider_type);
form.name = desc ? `${desc.defaultName} Tracking` : 'Tracking';
}
});
onMount(() => { onMount(() => {
topbarAction.set({ topbarAction.set({
@@ -230,9 +238,10 @@
window.history.replaceState(null, '', cleanUrl); window.history.replaceState(null, '', cleanUrl);
} }
function openNew() { form = defaultForm(); editing = null; showForm = true; } function openNew() { form = defaultForm(); nameManuallyEdited = false; editing = null; showForm = true; }
function edit(c: any) { function edit(c: any) {
form = { ...defaultForm(), ...c }; form = { ...defaultForm(), ...c };
nameManuallyEdited = true;
editing = c.id; showForm = true; editing = c.id; showForm = true;
} }
@@ -288,7 +297,7 @@
<label for="tc-name" class="block text-sm font-medium mb-1">{t('trackingConfig.name')}</label> <label for="tc-name" class="block text-sm font-medium mb-1">{t('trackingConfig.name')}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} /> <IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
<input id="tc-name" bind:value={form.name} required placeholder={t('trackingConfig.namePlaceholder')} <input id="tc-name" bind:value={form.name} oninput={() => nameManuallyEdited = true} required placeholder={t('trackingConfig.namePlaceholder')}
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /> class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div> </div>
</div> </div>