feat: locale-aware command templates, debounced auto-sync, entity pickers
- Locale-aware templates: CommandTemplateSlot now has a locale column, allowing each slot to have per-language variants (EN/RU). Templates are resolved at runtime from the Telegram user's language_code. - Merged system configs: "Default Commands (EN)" and "(RU)" merged into a single "Default Commands" config with locale-aware slots. Migration handles existing data automatically. - Configurable command descriptions: hardcoded COMMAND_DESCRIPTIONS replaced with desc_* template slots (desc_status, desc_help, etc.) that users can customize per locale. setMyCommands registers all locales explicitly. - Removed locale from CommandConfig: no longer needed since locale is derived from the Telegram user's language at runtime. - Debounced command auto-sync: after command config/tracker changes, affected bots are marked dirty and synced after a 30s debounce window. Manual "Sync with Telegram" button still works. - Entity pickers in LinkedTargetsSection: replaced 6 plain <select> elements with EntitySelect components (search, icons, keyboard nav). Added onselect callback and size="sm" props to EntitySelect.
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
slots: Record<string, string>;
|
||||
slots: Record<string, Record<string, string>>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
description: string;
|
||||
}
|
||||
|
||||
const LOCALES = ['en', 'ru'] as const;
|
||||
|
||||
let configs = $state<CmdTemplateConfig[]>([]);
|
||||
let loaded = $state(false);
|
||||
let showForm = $state(false);
|
||||
@@ -48,6 +50,7 @@
|
||||
let validateTimers: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||
let varsRef = $state<Record<string, any>>({});
|
||||
let showVarsFor = $state<string | null>(null);
|
||||
let activeLocale = $state<string>('en');
|
||||
|
||||
// Provider capabilities
|
||||
let allCapabilities = $state<Record<string, any>>({});
|
||||
@@ -61,10 +64,21 @@
|
||||
name: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
slots: {} as Record<string, string>,
|
||||
slots: {} as Record<string, Record<string, string>>,
|
||||
});
|
||||
let form = $state(defaultForm());
|
||||
|
||||
/** Get slot template for current locale, with fallback. */
|
||||
function getSlotValue(slotName: string): string {
|
||||
return form.slots[slotName]?.[activeLocale] || '';
|
||||
}
|
||||
|
||||
/** Set slot template for current locale. */
|
||||
function setSlotValue(slotName: string, value: string) {
|
||||
if (!form.slots[slotName]) form.slots[slotName] = {};
|
||||
form.slots[slotName][activeLocale] = value;
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
|
||||
async function load() {
|
||||
@@ -122,7 +136,7 @@
|
||||
|
||||
function refreshAllPreviews() {
|
||||
for (const slot of commandSlots) {
|
||||
const template = form.slots[slot.name] || '';
|
||||
const template = getSlotValue(slot.name);
|
||||
if (template) validateSlot(slot.name, template, true);
|
||||
}
|
||||
}
|
||||
@@ -131,20 +145,27 @@
|
||||
form = defaultForm();
|
||||
editing = null;
|
||||
showForm = true;
|
||||
activeLocale = 'en';
|
||||
slotPreview = {};
|
||||
slotErrors = {};
|
||||
}
|
||||
|
||||
function edit(c: CmdTemplateConfig) {
|
||||
// Deep copy nested slots
|
||||
const slotsCopy: Record<string, Record<string, string>> = {};
|
||||
for (const [k, v] of Object.entries(c.slots)) {
|
||||
slotsCopy[k] = { ...v };
|
||||
}
|
||||
form = {
|
||||
provider_type: c.provider_type,
|
||||
name: c.name,
|
||||
description: c.description || '',
|
||||
icon: c.icon || '',
|
||||
slots: { ...c.slots },
|
||||
slots: slotsCopy,
|
||||
};
|
||||
editing = c.id;
|
||||
showForm = true;
|
||||
activeLocale = 'en';
|
||||
slotPreview = {};
|
||||
slotErrors = {};
|
||||
setTimeout(() => refreshAllPreviews(), 100);
|
||||
@@ -170,15 +191,20 @@
|
||||
}
|
||||
|
||||
function clone(c: CmdTemplateConfig) {
|
||||
const slotsCopy: Record<string, Record<string, string>> = {};
|
||||
for (const [k, v] of Object.entries(c.slots)) {
|
||||
slotsCopy[k] = { ...v };
|
||||
}
|
||||
form = {
|
||||
provider_type: c.provider_type,
|
||||
name: `${c.name} (Copy)`,
|
||||
description: c.description || '',
|
||||
icon: c.icon || '',
|
||||
slots: { ...c.slots },
|
||||
slots: slotsCopy,
|
||||
};
|
||||
editing = null;
|
||||
showForm = true;
|
||||
activeLocale = 'en';
|
||||
slotPreview = {};
|
||||
slotErrors = {};
|
||||
setTimeout(() => refreshAllPreviews(), 100);
|
||||
@@ -245,8 +271,20 @@
|
||||
|
||||
<fieldset class="border border-[var(--color-border)] rounded-md p-3">
|
||||
<legend class="text-sm font-medium px-1">{t('cmdTemplateConfig.commandResponses')}</legend>
|
||||
<p class="text-xs text-[var(--color-muted-foreground)] mb-3">{t('cmdTemplateConfig.commandResponsesHint')}</p>
|
||||
<div class="space-y-3 mt-2">
|
||||
<p class="text-xs text-[var(--color-muted-foreground)] mb-2">{t('cmdTemplateConfig.commandResponsesHint')}</p>
|
||||
|
||||
<!-- Locale tabs -->
|
||||
<div class="flex gap-1 mb-3 border-b border-[var(--color-border)]">
|
||||
{#each LOCALES as loc}
|
||||
<button type="button"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded-t-md transition-colors {activeLocale === loc ? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)]' : 'text-[var(--color-muted-foreground)] hover:bg-[var(--color-muted)]'}"
|
||||
onclick={() => { activeLocale = loc; refreshAllPreviews(); }}>
|
||||
{loc.toUpperCase()}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each commandSlots as slot}
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
@@ -257,8 +295,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
<JinjaEditor
|
||||
value={form.slots[slot.name] || ''}
|
||||
onchange={(v: string) => { form.slots[slot.name] = v; validateSlot(slot.name, v); }}
|
||||
value={getSlotValue(slot.name)}
|
||||
onchange={(v: string) => { setSlotValue(slot.name, v); validateSlot(slot.name, v); }}
|
||||
rows={3}
|
||||
errorLine={slotErrorLines[slot.name] || null}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user