refactor: provider descriptor registry — eliminate provider-specific hardcoding
Replace all if/else chains keyed on provider type strings with a descriptor-driven architecture. Each provider type (immich, gitea, planka, scheduler, nut, google_photos) has a descriptor in frontend/src/lib/providers/ that declares config fields, event tracking fields, collection metadata, validation, and hooks. Components now use getDescriptor(type) and render dynamically. Dashboard provider card shows provider name + type when global filter is active. Grid-items derived from registry.
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
import { highlightFromUrl } from '$lib/highlight';
|
||||
import { providerDefaultIcon } from '$lib/grid-items';
|
||||
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
|
||||
import { getDescriptor } from '$lib/providers';
|
||||
import type { Tracker, TrackerTarget, TrackingConfig, TemplateConfig, NotificationTarget } from '$lib/types';
|
||||
|
||||
import TrackerForm from './TrackerForm.svelte';
|
||||
@@ -153,33 +154,22 @@
|
||||
e.preventDefault(); error = '';
|
||||
if (submitting) return;
|
||||
|
||||
const newAlbumIds = form.collection_ids.filter(id => !previousCollectionIds.includes(id));
|
||||
if (newAlbumIds.length > 0 && form.provider_id && selectedProviderType === 'immich') {
|
||||
// Delegate provider-specific pre-save checks to the descriptor
|
||||
const desc = getDescriptor(selectedProviderType);
|
||||
if (desc?.onBeforeSave && form.provider_id) {
|
||||
linkCheckLoading = true;
|
||||
try {
|
||||
const missingAlbums: { id: string; name: string; issue: string }[] = [];
|
||||
for (const albumId of newAlbumIds) {
|
||||
interface SharedLink { is_accessible: boolean; is_expired: boolean; has_password: boolean }
|
||||
const links = await api<SharedLink[]>(`/providers/${form.provider_id}/albums/${albumId}/shared-links`);
|
||||
const validLink = links.find((l) => l.is_accessible && !l.is_expired);
|
||||
if (!validLink) {
|
||||
const album = collections.find(c => c.id === albumId);
|
||||
const problematicLink = links.find((l) => l.is_expired || l.has_password);
|
||||
missingAlbums.push({
|
||||
id: albumId,
|
||||
name: album?.albumName || album?.name || albumId,
|
||||
issue: problematicLink
|
||||
? (problematicLink.is_expired ? 'expired' : 'password-protected')
|
||||
: 'missing',
|
||||
});
|
||||
const result = await desc.onBeforeSave({
|
||||
form, previousCollectionIds, collections, api,
|
||||
});
|
||||
if (!result.proceed) {
|
||||
if (result.warnings?.length) {
|
||||
linkWarning = { albums: result.warnings, providerId: form.provider_id };
|
||||
}
|
||||
}
|
||||
if (missingAlbums.length > 0) {
|
||||
linkWarning = { albums: missingAlbums, providerId: form.provider_id };
|
||||
linkCheckLoading = false;
|
||||
return;
|
||||
}
|
||||
} catch (e) { console.warn('Shared link check failed, proceeding:', e); }
|
||||
} catch (err) { console.warn('Pre-save check failed, proceeding:', err); }
|
||||
linkCheckLoading = false;
|
||||
}
|
||||
|
||||
@@ -277,15 +267,10 @@
|
||||
return p?.name || `#${id}`;
|
||||
}
|
||||
|
||||
const collectionCountLabel: Record<string, string> = {
|
||||
immich: 'notificationTracker.albums_count',
|
||||
gitea: 'notificationTracker.repos_count',
|
||||
planka: 'notificationTracker.boards_count',
|
||||
nut: 'notificationTracker.devices_count',
|
||||
};
|
||||
function getCollectionLabel(tracker: Tracker): string {
|
||||
const pt = getProviderType(tracker);
|
||||
return t(collectionCountLabel[pt] || 'notificationTracker.albums_count');
|
||||
const desc = getDescriptor(pt);
|
||||
return desc?.collectionMeta ? t(desc.collectionMeta.countLabel) : t('notificationTracker.collections_count');
|
||||
}
|
||||
|
||||
function configsForTracker(tracker: Tracker, configs: (TrackingConfig | TemplateConfig)[]): (TrackingConfig | TemplateConfig)[] {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import Hint from '$lib/components/Hint.svelte';
|
||||
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
||||
import MultiEntitySelect from '$lib/components/MultiEntitySelect.svelte';
|
||||
import { getDescriptor } from '$lib/providers';
|
||||
|
||||
interface Props {
|
||||
form: {
|
||||
@@ -45,17 +46,10 @@
|
||||
formatDate,
|
||||
}: Props = $props();
|
||||
|
||||
let descriptor = $derived(getDescriptor(providerType));
|
||||
let isScheduler = $derived(providerType === 'scheduler');
|
||||
let isWebhook = $derived(providerType === 'gitea' || providerType === 'planka');
|
||||
|
||||
// Collection label/icon/desc per provider type
|
||||
const collectionMeta: Record<string, { label: string; icon: string; placeholder: string; desc: (col: any) => string }> = {
|
||||
immich: { label: t('notificationTracker.albums'), icon: 'mdiImageMultiple', placeholder: t('notificationTracker.selectAlbums'), desc: (col) => `${col.assetCount ?? col.asset_count ?? 0} assets` },
|
||||
gitea: { label: t('notificationTracker.repositories'), icon: 'mdiGit', placeholder: t('notificationTracker.selectRepositories'), desc: () => '' },
|
||||
planka: { label: t('notificationTracker.boards'), icon: 'mdiViewDashboard', placeholder: t('notificationTracker.selectBoards'), desc: () => '' },
|
||||
nut: { label: t('notificationTracker.upsDevices'), icon: 'mdiBatteryCharging80', placeholder: t('notificationTracker.selectUpsDevices'), desc: (col) => col.description || '' },
|
||||
};
|
||||
let colMeta = $derived(collectionMeta[providerType] || { label: t('notificationTracker.albums'), icon: 'mdiServer', placeholder: t('notificationTracker.selectAlbums'), desc: () => '' });
|
||||
let isWebhook = $derived(descriptor?.webhookBased ?? false);
|
||||
let colMeta = $derived(descriptor?.collectionMeta);
|
||||
|
||||
// Custom variable management for scheduler
|
||||
function addVariable() {
|
||||
@@ -99,9 +93,9 @@
|
||||
<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 !isScheduler && collections.length > 0}
|
||||
{#if !isScheduler && colMeta && collections.length > 0}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">{colMeta.label}</label>
|
||||
<label class="block text-sm font-medium mb-1">{t(colMeta.label)}</label>
|
||||
<MultiEntitySelect
|
||||
items={collections.map(col => ({
|
||||
value: col.id,
|
||||
@@ -110,7 +104,7 @@
|
||||
desc: colMeta.desc(col),
|
||||
}))}
|
||||
bind:values={form.collection_ids}
|
||||
placeholder={colMeta.placeholder}
|
||||
placeholder={t(colMeta.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user