feat: EntitySelect palette-style entity picker, replace select dropdowns

New EntitySelect component: modal palette with search, keyboard nav,
current-value indicator (left border), smooth overlay. Replaces native
<select> dropdowns for entity selection.

Replaced selects:
- Notification Trackers: provider selector
- Command Trackers: provider + command config selectors
- Command Configs: response template selector
- Targets: telegram bot, email bot, matrix bot selectors

Added reactive $effect watchers for loadCollections and loadBotChats
since EntitySelect uses bind:value instead of onchange.
This commit is contained in:
2026-03-22 00:21:57 +03:00
parent 86115f5c75
commit a3a1fe3d75
5 changed files with 359 additions and 46 deletions
@@ -15,6 +15,7 @@
import Hint from '$lib/components/Hint.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import CrossLink from '$lib/components/CrossLink.svelte';
import EntitySelect from '$lib/components/EntitySelect.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import { highlightFromUrl } from '$lib/highlight';
import type { Tracker, ServiceProvider, NotificationTarget, TrackingConfig, TemplateConfig } from '$lib/types';
@@ -23,6 +24,7 @@
let loadError = $state('');
let notificationTrackers = $state<Tracker[]>([]);
let providers = $derived(providersCache.items);
const providerItems = $derived(providers.map(p => ({ value: p.id, label: p.name, icon: p.icon || 'mdiServer', desc: p.type })));
let targets = $derived(targetsCache.items);
let trackingConfigs = $derived(trackingConfigsCache.items);
let templateConfigs = $derived(templateConfigsCache.items);
@@ -77,6 +79,15 @@
try { collections = await api(`/providers/${form.provider_id}/collections`); } catch { collections = []; }
}
// Auto-load collections when provider changes via EntitySelect
let _prevProviderId = 0;
$effect(() => {
if (showForm && form.provider_id && form.provider_id !== _prevProviderId) {
_prevProviderId = form.provider_id;
loadCollections();
}
});
function openNew() { form = defaultForm(); editing = null; showForm = true; collections = []; previousCollectionIds = []; }
async function edit(trk: any) {
form = {
@@ -318,11 +329,8 @@
</div>
</div>
<div>
<label for="trk-provider" class="block text-sm font-medium mb-1">{t('notificationTracker.server')}</label>
<select id="trk-provider" bind:value={form.provider_id} onchange={loadCollections} required class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
<option value={0} disabled>{t('notificationTracker.selectServer')}</option>
{#each providers as p}<option value={p.id}>{p.name}</option>{/each}
</select>
<label class="block text-sm font-medium mb-1">{t('notificationTracker.server')}</label>
<EntitySelect items={providerItems} bind:value={form.provider_id} placeholder={t('notificationTracker.selectServer')} />
</div>
{#if collections.length > 0}
<div>