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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user