feat: IconGridSelect, CrossLink, SearchPalette components + entity crosslinks
New components: - IconGridSelect: Visual grid selector replacing <select> dropdowns, with icon + label cells, fixed-position popup, smart placement - CrossLink: Inline clickable badge for cross-entity navigation, hover highlights primary, used on entity cards - SearchPalette: Ctrl+K global command palette, searches all entity types via cached data, grouped results, keyboard navigation Integration: - Targets: type selector uses IconGridSelect (4-column grid with icons) - Targets: bot crosslink on telegram/email/matrix target cards - Command Trackers: provider and config badges → CrossLinks - Command Trackers: listener bot name → CrossLink - Command Configs: template config shown as CrossLink on card - Notification Trackers: provider CrossLink added to card - Layout: SearchPalette mounted globally Infrastructure: - Added CommandConfig, CommandTemplateConfig, CommandTracker types - Added notificationTrackersCache, commandTrackersCache to caches - Added allCaches map and fetchAllCaches() for search palette - Added searchPalette i18n keys (EN/RU)
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import Snackbar from '$lib/components/Snackbar.svelte';
|
||||
import SearchPalette from '$lib/components/SearchPalette.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
@@ -440,6 +441,7 @@
|
||||
</Modal>
|
||||
|
||||
<Snackbar />
|
||||
<SearchPalette />
|
||||
|
||||
<style>
|
||||
@media (max-width: 767px) {
|
||||
|
||||
@@ -11,8 +11,15 @@
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import CrossLink from '$lib/components/CrossLink.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
|
||||
function templateName(id: number | null): string {
|
||||
if (!id) return '';
|
||||
const tpl = cmdTemplateConfigs.find((c: any) => c.id === id);
|
||||
return tpl?.name || `#${id}`;
|
||||
}
|
||||
|
||||
let configs = $derived(commandConfigsCache.items);
|
||||
let cmdTemplateConfigs = $derived(commandTemplateConfigsCache.items);
|
||||
let loaded = $state(false);
|
||||
@@ -243,10 +250,15 @@
|
||||
</span>
|
||||
<span class="text-xs text-[var(--color-muted-foreground)]">{cfg.locale?.toUpperCase()}</span>
|
||||
</div>
|
||||
<p class="text-xs text-[var(--color-muted-foreground)] mt-0.5">
|
||||
{t('commandConfig.responseMode')}: {cfg.response_mode === 'media' ? t('commandConfig.modeMedia') : t('commandConfig.modeText')}
|
||||
· {t('commandConfig.defaultCount')}: {cfg.default_count}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span class="text-xs text-[var(--color-muted-foreground)]">
|
||||
{t('commandConfig.responseMode')}: {cfg.response_mode === 'media' ? t('commandConfig.modeMedia') : t('commandConfig.modeText')}
|
||||
· {t('commandConfig.defaultCount')}: {cfg.default_count}
|
||||
</span>
|
||||
{#if cfg.command_template_config_id}
|
||||
<CrossLink href="/command-template-configs" icon="mdiCodeBracesBox" label={templateName(cfg.command_template_config_id)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => editConfig(cfg)} />
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import CrossLink from '$lib/components/CrossLink.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import type { ServiceProvider, TelegramBot } from '$lib/types';
|
||||
|
||||
@@ -231,8 +232,8 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span style="color: var(--color-primary);"><MdiIcon name={trk.icon || 'mdiConsoleLine'} size={20} /></span>
|
||||
<p class="font-medium">{trk.name}</p>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{providerName(trk.provider_id)}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{configName(trk.command_config_id)}</span>
|
||||
<CrossLink href="/providers" icon="mdiServer" label={providerName(trk.provider_id)} />
|
||||
<CrossLink href="/command-configs" icon="mdiCog" label={configName(trk.command_config_id)} />
|
||||
<span class="text-xs px-1.5 py-0.5 rounded font-mono {trk.enabled
|
||||
? 'bg-emerald-500/10 text-emerald-500'
|
||||
: 'bg-red-500/10 text-red-500'}">
|
||||
@@ -269,7 +270,7 @@
|
||||
<div class="flex items-center justify-between text-sm px-2 py-1 rounded hover:bg-[var(--color-muted)]">
|
||||
<div class="flex items-center gap-2">
|
||||
<MdiIcon name="mdiRobot" size={14} />
|
||||
<span class="font-medium">{listener.name || listener.listener_type}</span>
|
||||
<CrossLink href="/telegram-bots" icon="mdiRobot" label={listener.name || listener.listener_type} />
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-500 font-mono">{listener.listener_type}</span>
|
||||
</div>
|
||||
<IconButton icon="mdiClose" title={t('commandTracker.removeListener')} size={14}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import CrossLink from '$lib/components/CrossLink.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import type { Tracker, ServiceProvider, NotificationTarget, TrackingConfig, TemplateConfig } from '$lib/types';
|
||||
|
||||
@@ -234,6 +235,11 @@
|
||||
return p?.type || '';
|
||||
}
|
||||
|
||||
function getProviderName(id: number): string {
|
||||
const p = providers.find(p => p.id === id);
|
||||
return p?.name || `#${id}`;
|
||||
}
|
||||
|
||||
function configsForTracker(tracker: any, configs: (TrackingConfig | TemplateConfig)[]): any[] {
|
||||
const pt = getProviderType(tracker);
|
||||
return pt ? configs.filter((c: any) => c.provider_type === pt) : configs;
|
||||
@@ -373,6 +379,7 @@
|
||||
<span class="text-xs px-1.5 py-0.5 rounded {tracker.enabled ? 'bg-[var(--color-success-bg)] text-[var(--color-success-fg)]' : 'bg-[var(--color-muted)] text-[var(--color-muted-foreground)]'}">
|
||||
{tracker.enabled ? t('notificationTracker.active') : t('notificationTracker.paused')}
|
||||
</span>
|
||||
t <CrossLink href="/providers" icon="mdiServer" label={getProviderName(tracker.provider_id)} />
|
||||
</div>
|
||||
<p class="text-sm text-[var(--color-muted-foreground)]">
|
||||
{(tracker.collection_ids || []).length} {t('notificationTracker.albums_count')} · {t('notificationTracker.every')} {tracker.scan_interval}s · {(tracker.tracker_targets || []).length} {t('notificationTracker.linkedTargets')}
|
||||
|
||||
@@ -14,9 +14,34 @@
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import CrossLink from '$lib/components/CrossLink.svelte';
|
||||
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import type { NotificationTarget, TelegramBot, TelegramChat, EmailBot, MatrixBot } from '$lib/types';
|
||||
|
||||
function getBotName(target: any): string | null {
|
||||
if (target.type === 'telegram' && target.config?.bot_id) {
|
||||
const bot = telegramBots.find(b => b.id === target.config.bot_id);
|
||||
return bot?.name || null;
|
||||
}
|
||||
if (target.type === 'email' && target.config?.email_bot_id) {
|
||||
const bot = emailBots.find(b => b.id === target.config.email_bot_id);
|
||||
return bot?.name || null;
|
||||
}
|
||||
if (target.type === 'matrix' && target.config?.matrix_bot_id) {
|
||||
const bot = matrixBots.find(b => b.id === target.config.matrix_bot_id);
|
||||
return bot?.name || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBotHref(target: any): string {
|
||||
if (target.type === 'telegram') return '/telegram-bots';
|
||||
if (target.type === 'email') return '/telegram-bots?tab=email';
|
||||
if (target.type === 'matrix') return '/telegram-bots?tab=matrix';
|
||||
return '/telegram-bots';
|
||||
}
|
||||
|
||||
const ALL_TYPES = ['telegram', 'webhook', 'email', 'discord', 'slack', 'ntfy', 'matrix'] as const;
|
||||
type TargetType = typeof ALL_TYPES[number];
|
||||
const TYPE_ICONS: Record<string, string> = {
|
||||
@@ -28,6 +53,12 @@
|
||||
discord: 'targets.descDiscord', slack: 'targets.descSlack', ntfy: 'targets.descNtfy', matrix: 'targets.descMatrix',
|
||||
};
|
||||
|
||||
const typeGridItems = $derived(ALL_TYPES.map(tt => ({
|
||||
value: tt,
|
||||
icon: TYPE_ICONS[tt] || 'mdiTarget',
|
||||
label: tt.charAt(0).toUpperCase() + tt.slice(1),
|
||||
})));
|
||||
|
||||
let allTargets = $derived(targetsCache.items);
|
||||
let activeType = $derived(page.url.searchParams.get('type') as TargetType | null);
|
||||
let targets = $derived(activeType ? allTargets.filter(t => t.type === activeType) : allTargets);
|
||||
@@ -189,13 +220,8 @@
|
||||
<form onsubmit={save} class="space-y-4">
|
||||
{#if !activeType}
|
||||
<div>
|
||||
<label for="tgt-type" class="block text-sm font-medium mb-1">{t('targets.type')}</label>
|
||||
<select id="tgt-type" bind:value={formType}
|
||||
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
||||
{#each ALL_TYPES as tt}
|
||||
<option value={tt}>{tt.charAt(0).toUpperCase() + tt.slice(1)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<label class="block text-sm font-medium mb-1">{t('targets.type')}</label>
|
||||
<IconGridSelect items={typeGridItems} bind:value={formType} columns={4} />
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
@@ -380,6 +406,7 @@
|
||||
<p class="font-medium">{target.name}</p>
|
||||
{#if !activeType}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.type}</span>{/if}
|
||||
{#if target.receiver_count}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.receiver_count} receiver(s)</span>{/if}
|
||||
{#if getBotName(target)}<CrossLink href={getBotHref(target)} icon="mdiRobot" label={getBotName(target)} />{/if}
|
||||
</div>
|
||||
<p class="text-sm text-[var(--color-muted-foreground)]">
|
||||
{#if target.type === 'telegram'}
|
||||
|
||||
Reference in New Issue
Block a user