feat: locale-aware notification templates + UX improvements
- Add locale support to notification templates (matching command template
pattern): TemplateSlot now has locale field with (config_id, slot_name,
locale) uniqueness, nested API format {slot: {locale: template}}
- Migration merges separate EN/RU system configs into unified per-provider
configs; seeds create one config per provider with multi-locale slots
- Locale-aware dispatch with EN fallback in NotificationDispatcher
- Frontend locale tabs (EN/RU) on template config editor
- Fix tracking config cards not showing default provider icons
- Global provider filter, search palette, and various UX polish
This commit is contained in:
@@ -180,7 +180,7 @@
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { t } from '$lib/i18n';
|
||||
import MdiIcon from './MdiIcon.svelte';
|
||||
import { requestHighlight } from '$lib/highlight';
|
||||
import { providerDefaultIcon } from '$lib/grid-items';
|
||||
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
|
||||
import {
|
||||
fetchAllCaches,
|
||||
providersCache,
|
||||
@@ -44,7 +46,7 @@
|
||||
|
||||
const GROUPS: readonly { key: string; label: string; icon: string; href: string; mapFn: (e: CacheEntity) => { detail: string; icon: string } }[] = [
|
||||
{ key: 'providers', label: 'nav.providers', icon: 'mdiServer', href: '/providers',
|
||||
mapFn: (e) => ({ detail: String(e.type || ''), icon: String(e.icon || 'mdiServer') }) },
|
||||
mapFn: (e) => ({ detail: String(e.type || ''), icon: providerDefaultIcon(e as any) }) },
|
||||
{ key: 'notification_trackers', label: 'nav.notification', icon: 'mdiRadar', href: '/notification-trackers',
|
||||
mapFn: (e) => ({ detail: e.enabled ? 'enabled' : 'disabled', icon: String(e.icon || 'mdiRadar') }) },
|
||||
{ key: 'tracking_configs', label: 'nav.trackingConfigs', icon: 'mdiCog', href: '/tracking-configs',
|
||||
@@ -87,10 +89,20 @@
|
||||
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const all: SearchResult[] = [];
|
||||
|
||||
const gpid = globalProviderFilter.id;
|
||||
const gpt = globalProviderFilter.providerType;
|
||||
const providerScoped = new Set(['notification_trackers', 'command_trackers', 'actions']);
|
||||
const typeScoped = new Set(['tracking_configs', 'template_configs', 'command_configs', 'command_template_configs']);
|
||||
|
||||
for (const group of GROUPS) {
|
||||
const cache = cacheMap[group.key];
|
||||
if (!cache) continue;
|
||||
for (const entity of cache.items) {
|
||||
// Apply global provider filter
|
||||
if (gpid && group.key === 'providers' && entity.id !== gpid) continue;
|
||||
if (gpid && providerScoped.has(group.key) && (entity as any).provider_id !== gpid) continue;
|
||||
if (gpt && typeScoped.has(group.key) && (entity as any).provider_type !== gpt) continue;
|
||||
|
||||
const mapped = group.mapFn(entity);
|
||||
const name = entity.name || '';
|
||||
const searchable = `${name} ${mapped.detail} ${t(group.label)}`.toLowerCase();
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
import { t } from '$lib/i18n';
|
||||
import type { GridItem } from '$lib/components/IconGridSelect.svelte';
|
||||
|
||||
/** Default icon for each provider type. Use instead of hardcoded 'mdiServer'. */
|
||||
const PROVIDER_TYPE_ICONS: Record<string, string> = {
|
||||
immich: 'mdiImageMultiple',
|
||||
gitea: 'mdiGit',
|
||||
planka: 'mdiViewDashboard',
|
||||
scheduler: 'mdiClockOutline',
|
||||
};
|
||||
|
||||
/** Get the default icon for a provider, falling back by type then generic. */
|
||||
export function providerDefaultIcon(provider: { icon?: string; type?: string }): string {
|
||||
if (provider.icon) return provider.icon;
|
||||
if (provider.type && PROVIDER_TYPE_ICONS[provider.type]) return PROVIDER_TYPE_ICONS[provider.type];
|
||||
return 'mdiServer';
|
||||
}
|
||||
|
||||
// --- Sort ---
|
||||
|
||||
export const sortByItems = (): GridItem[] => [
|
||||
@@ -108,8 +123,8 @@ export const providerTypeFilterItems = (): GridItem[] => [
|
||||
// --- Provider type ---
|
||||
|
||||
export const providerTypeItems = (): GridItem[] => [
|
||||
{ value: 'immich', icon: 'mdiCamera', label: t('providers.typeImmich'), desc: t('gridDesc.providerImmich') },
|
||||
{ value: 'gitea', icon: 'mdiGit', label: t('providers.typeGitea'), desc: t('gridDesc.providerGitea') },
|
||||
{ value: 'planka', icon: 'mdiViewDashboard', label: t('providers.typePlanka'), desc: t('gridDesc.providerPlanka') },
|
||||
{ value: 'scheduler', icon: 'mdiClockOutline', label: t('providers.typeScheduler'), desc: t('gridDesc.providerScheduler') },
|
||||
{ value: 'immich', icon: PROVIDER_TYPE_ICONS.immich, label: t('providers.typeImmich'), desc: t('gridDesc.providerImmich') },
|
||||
{ value: 'gitea', icon: PROVIDER_TYPE_ICONS.gitea, label: t('providers.typeGitea'), desc: t('gridDesc.providerGitea') },
|
||||
{ value: 'planka', icon: PROVIDER_TYPE_ICONS.planka, label: t('providers.typePlanka'), desc: t('gridDesc.providerPlanka') },
|
||||
{ value: 'scheduler', icon: PROVIDER_TYPE_ICONS.scheduler, label: t('providers.typeScheduler'), desc: t('gridDesc.providerScheduler') },
|
||||
];
|
||||
|
||||
@@ -335,6 +335,11 @@
|
||||
"clickToCopy": "Click to copy chat ID",
|
||||
"chatsDiscovered": "Chats discovered",
|
||||
"chatDeleted": "Chat removed",
|
||||
"chatName": "Name",
|
||||
"chatType": "Type",
|
||||
"chatLang": "Lang",
|
||||
"chatId": "Chat ID",
|
||||
"languageUpdated": "Chat language updated",
|
||||
"cmdLocale": "Bot language",
|
||||
"searchCooldown": "Search cooldown (s)",
|
||||
"saveConfig": "Save config",
|
||||
|
||||
@@ -335,6 +335,11 @@
|
||||
"clickToCopy": "Нажмите, чтобы скопировать ID чата",
|
||||
"chatsDiscovered": "Чаты обнаружены",
|
||||
"chatDeleted": "Чат удалён",
|
||||
"chatName": "Имя",
|
||||
"chatType": "Тип",
|
||||
"chatLang": "Язык",
|
||||
"chatId": "ID чата",
|
||||
"languageUpdated": "Язык чата обновлён",
|
||||
"cmdLocale": "Язык бота",
|
||||
"searchCooldown": "Кулдаун поиска (с)",
|
||||
"saveConfig": "Сохранить настройки",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Global provider filter — persisted to localStorage.
|
||||
*
|
||||
* When set, pages should filter entities to show only those
|
||||
* belonging to the selected provider. null = show all.
|
||||
*/
|
||||
|
||||
import { providersCache } from './caches.svelte';
|
||||
|
||||
const STORAGE_KEY = 'global_provider_id';
|
||||
|
||||
let _providerId = $state<number | null>(null);
|
||||
let _initialized = $state(false);
|
||||
|
||||
function loadFromStorage(): void {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = parseInt(stored, 10);
|
||||
_providerId = isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
// Load on module init
|
||||
loadFromStorage();
|
||||
|
||||
export const globalProviderFilter = {
|
||||
get id() { return _providerId; },
|
||||
get initialized() { return _initialized; },
|
||||
|
||||
set(id: number | null) {
|
||||
_providerId = id;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
if (id != null) {
|
||||
localStorage.setItem(STORAGE_KEY, String(id));
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.set(null);
|
||||
},
|
||||
|
||||
/** The currently selected provider object (reactive). */
|
||||
get provider() {
|
||||
if (_providerId == null) return null;
|
||||
return providersCache.items.find(p => p.id === _providerId) ?? null;
|
||||
},
|
||||
|
||||
/** The provider type string, or null. */
|
||||
get providerType() {
|
||||
return this.provider?.type ?? null;
|
||||
},
|
||||
};
|
||||
@@ -156,7 +156,7 @@ export interface TemplateConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
slots: Record<string, string>;
|
||||
slots: Record<string, Record<string, string>>;
|
||||
date_format: string;
|
||||
date_only_format: string;
|
||||
created_at: string;
|
||||
|
||||
Reference in New Issue
Block a user