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:
@@ -0,0 +1,60 @@
|
||||
import type { ProviderDescriptor } from './types';
|
||||
|
||||
export const giteaDescriptor: ProviderDescriptor = {
|
||||
type: 'gitea',
|
||||
defaultName: 'Gitea',
|
||||
icon: 'mdiGit',
|
||||
hasUrl: true,
|
||||
urlPlaceholder: 'https://gitea.example.com',
|
||||
webhookBased: true,
|
||||
|
||||
configFields: [
|
||||
{
|
||||
key: 'webhook_secret', configKey: 'webhook_secret',
|
||||
label: 'providers.webhookSecret', editLabel: 'providers.webhookSecretKeep',
|
||||
type: 'password', required: 'create-only', hint: 'providers.webhookSecretHint',
|
||||
},
|
||||
{
|
||||
key: 'api_token', configKey: 'api_token',
|
||||
label: 'providers.apiToken',
|
||||
type: 'password', optional: true, hint: 'providers.apiTokenHint',
|
||||
},
|
||||
],
|
||||
|
||||
buildConfig(form, editing) {
|
||||
const config: Record<string, any> = { url: form.url };
|
||||
if (form.api_token) config.api_token = form.api_token;
|
||||
if (form.webhook_secret) config.webhook_secret = form.webhook_secret;
|
||||
if (!editing && !form.webhook_secret) {
|
||||
return { config, error: 'providers.webhookSecretRequired' };
|
||||
}
|
||||
return { config };
|
||||
},
|
||||
|
||||
hasConfigChanged(form, existing) {
|
||||
return form.url !== (existing.url || '') ||
|
||||
!!form.api_token || !!form.webhook_secret;
|
||||
},
|
||||
|
||||
eventFields: [
|
||||
{ key: 'track_push', label: 'trackingConfig.push', default: true },
|
||||
{ key: 'track_issue_opened', label: 'trackingConfig.issueOpened', default: true },
|
||||
{ key: 'track_issue_closed', label: 'trackingConfig.issueClosed', default: true },
|
||||
{ key: 'track_issue_commented', label: 'trackingConfig.issueCommented', default: false },
|
||||
{ key: 'track_pr_opened', label: 'trackingConfig.prOpened', default: true },
|
||||
{ key: 'track_pr_closed', label: 'trackingConfig.prClosed', default: true },
|
||||
{ key: 'track_pr_merged', label: 'trackingConfig.prMerged', default: true },
|
||||
{ key: 'track_pr_commented', label: 'trackingConfig.prCommented', default: false },
|
||||
{ key: 'track_release_published', label: 'trackingConfig.releasePublished', default: true },
|
||||
],
|
||||
|
||||
collectionMeta: {
|
||||
label: 'notificationTracker.repositories',
|
||||
icon: 'mdiGit',
|
||||
placeholder: 'notificationTracker.selectRepositories',
|
||||
countLabel: 'notificationTracker.repos_count',
|
||||
desc: () => '',
|
||||
},
|
||||
|
||||
webhookUrlPattern: '/api/webhooks/gitea/{id}',
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ProviderDescriptor } from './types';
|
||||
|
||||
export const googlePhotosDescriptor: ProviderDescriptor = {
|
||||
type: 'google_photos',
|
||||
defaultName: 'Google Photos',
|
||||
icon: 'mdiGoogle',
|
||||
hasUrl: false,
|
||||
|
||||
configFields: [
|
||||
{
|
||||
key: 'gp_client_id', configKey: 'client_id',
|
||||
label: 'providers.gpClientId', type: 'text',
|
||||
required: 'create-only', placeholder: '123456789.apps.googleusercontent.com',
|
||||
},
|
||||
{
|
||||
key: 'gp_client_secret', configKey: 'client_secret',
|
||||
label: 'providers.gpClientSecret', editLabel: 'providers.gpClientSecretKeep',
|
||||
type: 'password', required: 'create-only',
|
||||
},
|
||||
{
|
||||
key: 'gp_refresh_token', configKey: 'refresh_token',
|
||||
label: 'providers.gpRefreshToken', editLabel: 'providers.gpRefreshTokenKeep',
|
||||
type: 'password', required: 'create-only', hint: 'providers.gpRefreshTokenHint',
|
||||
},
|
||||
],
|
||||
|
||||
buildConfig(form, editing) {
|
||||
const config: Record<string, any> = {};
|
||||
if (form.gp_client_id) config.client_id = form.gp_client_id;
|
||||
if (form.gp_client_secret) config.client_secret = form.gp_client_secret;
|
||||
if (form.gp_refresh_token) config.refresh_token = form.gp_refresh_token;
|
||||
if (!editing && (!form.gp_client_id || !form.gp_client_secret || !form.gp_refresh_token)) {
|
||||
return { config, error: 'providers.gpAllFieldsRequired' };
|
||||
}
|
||||
return { config };
|
||||
},
|
||||
|
||||
hasConfigChanged(form, existing) {
|
||||
return form.gp_client_id !== (existing.client_id || '') ||
|
||||
!!form.gp_client_secret || !!form.gp_refresh_token;
|
||||
},
|
||||
|
||||
eventFields: [
|
||||
{ key: 'track_assets_added', label: 'trackingConfig.assetsAdded', default: true },
|
||||
{ key: 'track_assets_removed', label: 'trackingConfig.assetsRemoved', default: false },
|
||||
{ key: 'track_collection_renamed', label: 'trackingConfig.albumRenamed', default: true },
|
||||
{ key: 'track_collection_deleted', label: 'trackingConfig.albumDeleted', default: true },
|
||||
{ key: 'track_sharing_changed', label: 'trackingConfig.sharingChanged', default: false },
|
||||
],
|
||||
|
||||
collectionMeta: {
|
||||
label: 'notificationTracker.albums',
|
||||
icon: 'mdiGoogle',
|
||||
placeholder: 'notificationTracker.selectAlbums',
|
||||
countLabel: 'notificationTracker.albums_count',
|
||||
desc: (col) => `${col.assetCount ?? col.asset_count ?? col.mediaItemsCount ?? 0} items`,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { ProviderDescriptor } from './types';
|
||||
|
||||
export const immichDescriptor: ProviderDescriptor = {
|
||||
type: 'immich',
|
||||
defaultName: 'Immich',
|
||||
icon: 'mdiImageMultiple',
|
||||
hasUrl: true,
|
||||
urlPlaceholder: undefined, // uses generic i18n placeholder
|
||||
|
||||
configFields: [
|
||||
{
|
||||
key: 'api_key', configKey: 'api_key',
|
||||
label: 'providers.apiKey', editLabel: 'providers.apiKeyKeep',
|
||||
type: 'password', required: 'create-only',
|
||||
},
|
||||
{
|
||||
key: 'external_domain', configKey: 'external_domain',
|
||||
label: 'providers.externalDomain',
|
||||
type: 'text', optional: true, placeholder: 'https://photos.example.com',
|
||||
},
|
||||
],
|
||||
|
||||
buildConfig(form, editing) {
|
||||
const config: Record<string, any> = { url: form.url };
|
||||
if (form.api_key) config.api_key = form.api_key;
|
||||
if (form.external_domain) config.external_domain = form.external_domain;
|
||||
if (!editing) config.api_key = form.api_key;
|
||||
return { config };
|
||||
},
|
||||
|
||||
hasConfigChanged(form, existing) {
|
||||
return form.url !== (existing.url || '') ||
|
||||
!!form.api_key ||
|
||||
form.external_domain !== (existing.external_domain || '');
|
||||
},
|
||||
|
||||
eventFields: [
|
||||
{ key: 'track_assets_added', label: 'trackingConfig.assetsAdded', default: true },
|
||||
{ key: 'track_assets_removed', label: 'trackingConfig.assetsRemoved', default: false },
|
||||
{ key: 'track_collection_renamed', label: 'trackingConfig.albumRenamed', default: true },
|
||||
{ key: 'track_collection_deleted', label: 'trackingConfig.albumDeleted', default: true },
|
||||
{ key: 'track_sharing_changed', label: 'trackingConfig.sharingChanged', default: false },
|
||||
{ key: 'track_images', label: 'trackingConfig.trackImages', default: true },
|
||||
{ key: 'track_videos', label: 'trackingConfig.trackVideos', default: true },
|
||||
{ key: 'notify_favorites_only', label: 'trackingConfig.favoritesOnly', default: false, hint: 'hints.favoritesOnly' },
|
||||
{ key: 'include_tags', label: 'trackingConfig.includePeople', default: true },
|
||||
{ key: 'include_asset_details', label: 'trackingConfig.includeDetails', default: false },
|
||||
],
|
||||
|
||||
extraTrackingFields: [
|
||||
{ key: 'max_assets_to_show', label: 'trackingConfig.maxAssets', type: 'number', min: 0, max: 50, defaultValue: 5, hint: 'hints.maxAssets' },
|
||||
{ key: 'assets_order_by', label: 'trackingConfig.sortBy', type: 'grid-select', gridItems: 'sortByItems', gridColumns: 2, defaultValue: 'none' },
|
||||
{ key: 'assets_order', label: 'trackingConfig.sortOrder', type: 'grid-select', gridItems: 'sortOrderItems', gridColumns: 2, defaultValue: 'descending' },
|
||||
],
|
||||
|
||||
featureSections: [
|
||||
{
|
||||
key: 'periodic', legend: 'trackingConfig.periodicSummary', legendHint: 'hints.periodicSummary',
|
||||
enabledField: 'periodic_enabled', enabledDefault: false,
|
||||
fields: [
|
||||
{ key: 'periodic_interval_days', label: 'trackingConfig.intervalDays', type: 'number', min: 1, defaultValue: 1 },
|
||||
{ key: 'periodic_start_date', label: 'trackingConfig.startDate', type: 'number', defaultValue: '2025-01-01' }, // rendered as date input
|
||||
{ key: 'periodic_times', label: 'trackingConfig.times', type: 'number', defaultValue: '12:00' }, // rendered as text input
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'scheduled', legend: 'trackingConfig.scheduledAssets', legendHint: 'hints.scheduledAssets',
|
||||
enabledField: 'scheduled_enabled', enabledDefault: false,
|
||||
fields: [
|
||||
{ key: 'scheduled_times', label: 'trackingConfig.times', type: 'number', defaultValue: '09:00' },
|
||||
{ key: 'scheduled_collection_mode', label: 'trackingConfig.albumMode', type: 'grid-select', gridItems: 'albumModeItems', gridColumns: 3, defaultValue: 'per_collection' },
|
||||
{ key: 'scheduled_limit', label: 'trackingConfig.maxAssets', type: 'number', min: 1, max: 100, defaultValue: 10, hint: 'hints.maxAssets' },
|
||||
{ key: 'scheduled_asset_type', label: 'trackingConfig.assetType', type: 'grid-select', gridItems: 'assetTypeItems', gridColumns: 3, defaultValue: 'all' },
|
||||
{ key: 'scheduled_min_rating', label: 'trackingConfig.minRating', type: 'number', min: 0, max: 5, defaultValue: 0, hint: 'hints.minRating' },
|
||||
],
|
||||
checkboxes: [
|
||||
{ key: 'scheduled_favorite_only', label: 'trackingConfig.favoritesOnly', default: false, hint: 'hints.favoritesOnly' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'memory', legend: 'trackingConfig.memoryMode', legendHint: 'hints.memoryMode',
|
||||
enabledField: 'memory_enabled', enabledDefault: false,
|
||||
fields: [
|
||||
{ key: 'memory_source', label: 'trackingConfig.memorySource', type: 'grid-select', gridItems: 'memorySourceItems', gridColumns: 2, defaultValue: 'albums' },
|
||||
{ key: 'memory_times', label: 'trackingConfig.times', type: 'number', defaultValue: '09:00' },
|
||||
{ key: 'memory_collection_mode', label: 'trackingConfig.albumMode', type: 'grid-select', gridItems: 'albumModeItems', gridColumns: 3, defaultValue: 'combined' },
|
||||
{ key: 'memory_limit', label: 'trackingConfig.maxAssets', type: 'number', min: 1, max: 100, defaultValue: 10 },
|
||||
{ key: 'memory_asset_type', label: 'trackingConfig.assetType', type: 'grid-select', gridItems: 'assetTypeItems', gridColumns: 3, defaultValue: 'all' },
|
||||
{ key: 'memory_min_rating', label: 'trackingConfig.minRating', type: 'number', min: 0, max: 5, defaultValue: 0 },
|
||||
],
|
||||
checkboxes: [
|
||||
{ key: 'memory_favorite_only', label: 'trackingConfig.favoritesOnly', default: false, hint: 'hints.favoritesOnly' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
collectionMeta: {
|
||||
label: 'notificationTracker.albums',
|
||||
icon: 'mdiImageMultiple',
|
||||
placeholder: 'notificationTracker.selectAlbums',
|
||||
countLabel: 'notificationTracker.albums_count',
|
||||
desc: (col) => `${col.assetCount ?? col.asset_count ?? 0} assets`,
|
||||
},
|
||||
|
||||
async onBeforeSave({ form, previousCollectionIds, collections, api: apiFn }) {
|
||||
const newIds = (form.collection_ids as string[]).filter(id => !previousCollectionIds.includes(id));
|
||||
if (newIds.length === 0) return { proceed: true };
|
||||
|
||||
interface SharedLink { is_accessible: boolean; is_expired: boolean; has_password: boolean }
|
||||
const warnings: { id: string; name: string; issue: string }[] = [];
|
||||
|
||||
for (const albumId of newIds) {
|
||||
try {
|
||||
const links = await apiFn<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 problematic = links.find((l) => l.is_expired || l.has_password);
|
||||
warnings.push({
|
||||
id: albumId,
|
||||
name: album?.albumName || album?.name || albumId,
|
||||
issue: problematic
|
||||
? (problematic.is_expired ? 'expired' : 'password-protected')
|
||||
: 'missing',
|
||||
});
|
||||
}
|
||||
} catch { /* shared-link check failed, proceed */ }
|
||||
}
|
||||
|
||||
if (warnings.length > 0) return { warnings, proceed: false };
|
||||
return { proceed: true };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Provider descriptor registry.
|
||||
*
|
||||
* Single entry point for all provider-specific UI logic.
|
||||
* Components call `getDescriptor(type)` instead of if/else chains.
|
||||
*/
|
||||
|
||||
import type { ProviderDescriptor } from './types';
|
||||
import { immichDescriptor } from './immich';
|
||||
import { giteaDescriptor } from './gitea';
|
||||
import { plankaDescriptor } from './planka';
|
||||
import { schedulerDescriptor } from './scheduler';
|
||||
import { nutDescriptor } from './nut';
|
||||
import { googlePhotosDescriptor } from './google-photos';
|
||||
|
||||
const REGISTRY: ReadonlyMap<string, ProviderDescriptor> = new Map([
|
||||
['immich', immichDescriptor],
|
||||
['gitea', giteaDescriptor],
|
||||
['planka', plankaDescriptor],
|
||||
['scheduler', schedulerDescriptor],
|
||||
['nut', nutDescriptor],
|
||||
['google_photos', googlePhotosDescriptor],
|
||||
]);
|
||||
|
||||
/** Look up a provider descriptor by type. Returns null for unknown types. */
|
||||
export function getDescriptor(type: string): ProviderDescriptor | null {
|
||||
return REGISTRY.get(type) ?? null;
|
||||
}
|
||||
|
||||
/** All registered descriptors (stable order). */
|
||||
export function allDescriptors(): ProviderDescriptor[] {
|
||||
return [...REGISTRY.values()];
|
||||
}
|
||||
|
||||
/** All registered provider type strings. */
|
||||
export function allProviderTypes(): string[] {
|
||||
return [...REGISTRY.keys()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a default form state for the tracking config "Event tracking" fieldset.
|
||||
* Merges event field defaults from ALL providers so the form can load any
|
||||
* existing config without missing keys, but only the active provider's fields
|
||||
* are rendered.
|
||||
*/
|
||||
export function buildTrackingFormDefaults(): Record<string, any> {
|
||||
const defaults: Record<string, any> = {};
|
||||
for (const desc of REGISTRY.values()) {
|
||||
for (const field of desc.eventFields) {
|
||||
defaults[field.key] = field.default;
|
||||
}
|
||||
for (const extra of desc.extraTrackingFields ?? []) {
|
||||
defaults[extra.key] = extra.defaultValue ?? '';
|
||||
}
|
||||
for (const section of desc.featureSections ?? []) {
|
||||
defaults[section.enabledField] = section.enabledDefault;
|
||||
for (const f of section.fields) {
|
||||
defaults[f.key] = f.defaultValue ?? '';
|
||||
}
|
||||
for (const cb of section.checkboxes ?? []) {
|
||||
defaults[cb.key] = cb.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a default flat form state for the provider config form.
|
||||
* All config fields from all providers are included with empty/default values.
|
||||
*/
|
||||
export function buildProviderFormDefaults(): Record<string, any> {
|
||||
const defaults: Record<string, any> = {
|
||||
name: '', type: '', url: '', icon: '',
|
||||
};
|
||||
for (const desc of REGISTRY.values()) {
|
||||
for (const field of desc.configFields) {
|
||||
defaults[field.key] = field.defaultValue ?? (field.type === 'number' ? 0 : '');
|
||||
}
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
export type { ProviderDescriptor, ConfigField, EventTrackingField, ExtraTrackingField, FeatureSection, CollectionMeta } from './types';
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { ProviderDescriptor } from './types';
|
||||
|
||||
export const nutDescriptor: ProviderDescriptor = {
|
||||
type: 'nut',
|
||||
defaultName: 'NUT',
|
||||
icon: 'mdiBatteryCharging80',
|
||||
hasUrl: false,
|
||||
|
||||
configFields: [
|
||||
{
|
||||
key: 'nut_host', configKey: 'host',
|
||||
label: 'providers.nutHost', type: 'text',
|
||||
required: true, placeholder: '192.168.1.100 or ups.local',
|
||||
},
|
||||
{
|
||||
key: 'nut_port', configKey: 'port',
|
||||
label: 'providers.nutPort', type: 'number',
|
||||
min: 1, max: 65535, defaultValue: 3493,
|
||||
},
|
||||
{
|
||||
key: 'nut_username', configKey: 'username',
|
||||
label: 'providers.nutUsername', type: 'text',
|
||||
optional: true, hint: 'providers.nutUsernameHint',
|
||||
},
|
||||
{
|
||||
key: 'nut_password', configKey: 'password',
|
||||
label: 'providers.nutPassword', type: 'password',
|
||||
optional: true, hint: 'providers.nutPasswordHint',
|
||||
},
|
||||
],
|
||||
|
||||
buildConfig(form) {
|
||||
const config: Record<string, any> = {
|
||||
host: form.nut_host,
|
||||
port: form.nut_port || 3493,
|
||||
};
|
||||
if (form.nut_username) config.username = form.nut_username;
|
||||
if (form.nut_password) config.password = form.nut_password;
|
||||
return { config };
|
||||
},
|
||||
|
||||
hasConfigChanged() {
|
||||
// NUT fields are always sent (no secret masking)
|
||||
return true;
|
||||
},
|
||||
|
||||
eventFields: [
|
||||
{ key: 'track_ups_online', label: 'trackingConfig.upsOnline', default: true },
|
||||
{ key: 'track_ups_on_battery', label: 'trackingConfig.upsOnBattery', default: true },
|
||||
{ key: 'track_ups_low_battery', label: 'trackingConfig.upsLowBattery', default: true },
|
||||
{ key: 'track_ups_battery_restored', label: 'trackingConfig.upsBatteryRestored', default: true },
|
||||
{ key: 'track_ups_comms_lost', label: 'trackingConfig.upsCommsLost', default: true },
|
||||
{ key: 'track_ups_comms_restored', label: 'trackingConfig.upsCommsRestored', default: true },
|
||||
{ key: 'track_ups_replace_battery', label: 'trackingConfig.upsReplaceBattery', default: true },
|
||||
{ key: 'track_ups_overload', label: 'trackingConfig.upsOverload', default: true },
|
||||
],
|
||||
|
||||
collectionMeta: {
|
||||
label: 'notificationTracker.upsDevices',
|
||||
icon: 'mdiBatteryCharging80',
|
||||
placeholder: 'notificationTracker.selectUpsDevices',
|
||||
countLabel: 'notificationTracker.devices_count',
|
||||
desc: (col) => col.description || '',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ProviderDescriptor } from './types';
|
||||
|
||||
export const plankaDescriptor: ProviderDescriptor = {
|
||||
type: 'planka',
|
||||
defaultName: 'Planka',
|
||||
icon: 'mdiViewDashboard',
|
||||
hasUrl: true,
|
||||
urlPlaceholder: 'https://planka.example.com',
|
||||
webhookBased: true,
|
||||
|
||||
configFields: [
|
||||
{
|
||||
key: 'webhook_secret', configKey: 'webhook_secret',
|
||||
label: 'providers.webhookSecret', editLabel: 'providers.webhookSecretKeep',
|
||||
type: 'password', required: 'create-only', hint: 'providers.plankaWebhookSecretHint',
|
||||
},
|
||||
{
|
||||
key: 'api_key', configKey: 'api_key',
|
||||
label: 'providers.apiKey',
|
||||
type: 'password', optional: true, hint: 'providers.plankaApiKeyHint',
|
||||
},
|
||||
],
|
||||
|
||||
buildConfig(form, editing) {
|
||||
const config: Record<string, any> = { url: form.url };
|
||||
if (form.api_key) config.api_key = form.api_key;
|
||||
if (form.webhook_secret) config.webhook_secret = form.webhook_secret;
|
||||
if (!editing && !form.webhook_secret) {
|
||||
return { config, error: 'providers.webhookSecretRequired' };
|
||||
}
|
||||
return { config };
|
||||
},
|
||||
|
||||
hasConfigChanged(form, existing) {
|
||||
return form.url !== (existing.url || '') ||
|
||||
!!form.api_key || !!form.webhook_secret;
|
||||
},
|
||||
|
||||
eventFields: [
|
||||
{ key: 'track_card_created', label: 'trackingConfig.cardCreated', default: true },
|
||||
{ key: 'track_card_updated', label: 'trackingConfig.cardUpdated', default: false },
|
||||
{ key: 'track_card_moved', label: 'trackingConfig.cardMoved', default: true },
|
||||
{ key: 'track_card_deleted', label: 'trackingConfig.cardDeleted', default: false },
|
||||
{ key: 'track_card_commented', label: 'trackingConfig.cardCommented', default: true },
|
||||
{ key: 'track_comment_updated', label: 'trackingConfig.commentUpdated', default: false },
|
||||
{ key: 'track_board_created', label: 'trackingConfig.boardCreated', default: true },
|
||||
{ key: 'track_board_updated', label: 'trackingConfig.boardUpdated', default: false },
|
||||
{ key: 'track_board_deleted', label: 'trackingConfig.boardDeleted', default: true },
|
||||
{ key: 'track_list_created', label: 'trackingConfig.listCreated', default: false },
|
||||
{ key: 'track_list_updated', label: 'trackingConfig.listUpdated', default: false },
|
||||
{ key: 'track_list_deleted', label: 'trackingConfig.listDeleted', default: false },
|
||||
{ key: 'track_attachment_created', label: 'trackingConfig.attachmentCreated', default: true },
|
||||
{ key: 'track_card_label_added', label: 'trackingConfig.cardLabelAdded', default: false },
|
||||
{ key: 'track_task_completed', label: 'trackingConfig.taskCompleted', default: true },
|
||||
],
|
||||
|
||||
collectionMeta: {
|
||||
label: 'notificationTracker.boards',
|
||||
icon: 'mdiViewDashboard',
|
||||
placeholder: 'notificationTracker.selectBoards',
|
||||
countLabel: 'notificationTracker.boards_count',
|
||||
desc: () => '',
|
||||
},
|
||||
|
||||
webhookUrlPattern: '/api/webhooks/planka/{id}',
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ProviderDescriptor } from './types';
|
||||
|
||||
export const schedulerDescriptor: ProviderDescriptor = {
|
||||
type: 'scheduler',
|
||||
defaultName: 'Scheduler',
|
||||
icon: 'mdiClockOutline',
|
||||
hasUrl: false,
|
||||
|
||||
configFields: [],
|
||||
|
||||
buildConfig() {
|
||||
return { config: {} };
|
||||
},
|
||||
|
||||
hasConfigChanged() {
|
||||
return false;
|
||||
},
|
||||
|
||||
eventFields: [
|
||||
{ key: 'track_scheduled_message', label: 'trackingConfig.scheduledMessage', default: true },
|
||||
],
|
||||
|
||||
collectionMeta: null,
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Provider descriptor interface.
|
||||
*
|
||||
* Each service provider type has exactly one descriptor that declares
|
||||
* everything the UI needs: config form fields, event tracking fields,
|
||||
* collection metadata, validation, and provider-specific hooks.
|
||||
*
|
||||
* Components consume descriptors via the registry (see ./index.ts)
|
||||
* instead of if/else chains keyed on provider type strings.
|
||||
*/
|
||||
|
||||
import type { api } from '$lib/api';
|
||||
|
||||
// ── Config form ──────────────────────────────────────────────────────
|
||||
|
||||
export interface ConfigField {
|
||||
/** Form field key (flat namespace, e.g. "api_key", "nut_host"). */
|
||||
key: string;
|
||||
/** Key in the config payload sent to the API (defaults to `key`). */
|
||||
configKey?: string;
|
||||
/** i18n key for the field label. */
|
||||
label: string;
|
||||
type: 'text' | 'password' | 'number';
|
||||
placeholder?: string;
|
||||
/**
|
||||
* - `true` → always required
|
||||
* - `'create-only'` → required only when creating, blank = "keep existing" on edit
|
||||
* - `false` / omitted → optional
|
||||
*/
|
||||
required?: boolean | 'create-only';
|
||||
/** Show "(optional)" suffix next to label. */
|
||||
optional?: boolean;
|
||||
/** i18n key for hint text below the field. */
|
||||
hint?: string;
|
||||
/** Edit-mode label override (e.g. "API key (leave blank to keep)"). */
|
||||
editLabel?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
/** Default value for this field. */
|
||||
defaultValue?: string | number;
|
||||
}
|
||||
|
||||
// ── Event tracking (TrackingConfig form) ─────────────────────────────
|
||||
|
||||
export interface EventTrackingField {
|
||||
/** Form field key, e.g. "track_assets_added". */
|
||||
key: string;
|
||||
/** i18n key for checkbox label. */
|
||||
label: string;
|
||||
/** Default value when creating a new tracking config. */
|
||||
default: boolean;
|
||||
/** Optional i18n key for a Hint tooltip. */
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/** Extra non-boolean fields shown after event checkboxes (e.g. max_assets). */
|
||||
export interface ExtraTrackingField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'number' | 'grid-select';
|
||||
/** Grid-select item source function name from grid-items.ts. */
|
||||
gridItems?: string;
|
||||
gridColumns?: number;
|
||||
hint?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
defaultValue?: string | number;
|
||||
}
|
||||
|
||||
/** A feature section like periodic summary, scheduled assets, memory mode. */
|
||||
export interface FeatureSection {
|
||||
/** Unique key, e.g. "periodic", "scheduled", "memory". */
|
||||
key: string;
|
||||
/** i18n key for fieldset legend. */
|
||||
legend: string;
|
||||
/** Hint tooltip for legend. */
|
||||
legendHint?: string;
|
||||
/** Form field that toggles this section on/off. */
|
||||
enabledField: string;
|
||||
/** Default value for the enabled field. */
|
||||
enabledDefault: boolean;
|
||||
/** Fields shown when section is enabled. */
|
||||
fields: ExtraTrackingField[];
|
||||
/** Boolean fields (checkboxes) inside the section. */
|
||||
checkboxes?: EventTrackingField[];
|
||||
}
|
||||
|
||||
// ── Collection metadata (TrackerForm) ────────────────────────────────
|
||||
|
||||
export interface CollectionMeta {
|
||||
/** i18n key for collection noun ("Albums", "Repositories", etc.). */
|
||||
label: string;
|
||||
/** MDI icon name. */
|
||||
icon: string;
|
||||
/** i18n key for multi-select placeholder. */
|
||||
placeholder: string;
|
||||
/** i18n key for count badge in tracker list (e.g. "3 albums"). */
|
||||
countLabel: string;
|
||||
/** Per-collection description line. */
|
||||
desc: (col: any) => string;
|
||||
}
|
||||
|
||||
// ── Main descriptor ──────────────────────────────────────────────────
|
||||
|
||||
export interface ProviderDescriptor {
|
||||
/** Provider type string (must match backend). */
|
||||
type: string;
|
||||
/** Default display name for new providers. */
|
||||
defaultName: string;
|
||||
/** MDI icon name. */
|
||||
icon: string;
|
||||
|
||||
// ── Provider config form ──
|
||||
/** Whether this provider shows a URL field. */
|
||||
hasUrl: boolean;
|
||||
/** Placeholder for the URL field. */
|
||||
urlPlaceholder?: string;
|
||||
/** Provider-specific config fields (API key, webhook secret, etc.). */
|
||||
configFields: ConfigField[];
|
||||
/** Build the config payload from flat form state. Return error i18n key to abort. */
|
||||
buildConfig(form: Record<string, any>, editing: boolean): { config: Record<string, any>; error?: string };
|
||||
/** Detect whether any config field changed during edit (secrets are blank on load). */
|
||||
hasConfigChanged(form: Record<string, any>, existing: Record<string, any>): boolean;
|
||||
|
||||
// ── Event tracking ──
|
||||
/** Checkbox fields for the "Event tracking" fieldset. */
|
||||
eventFields: EventTrackingField[];
|
||||
/** Extra non-boolean controls shown after the event checkboxes. */
|
||||
extraTrackingFields?: ExtraTrackingField[];
|
||||
/** Feature sections (periodic, scheduled, memory) — only for providers that support them. */
|
||||
featureSections?: FeatureSection[];
|
||||
|
||||
// ── Collections / Trackers ──
|
||||
/** Null means this provider has no collections (e.g. scheduler). */
|
||||
collectionMeta: CollectionMeta | null;
|
||||
/** Whether this provider is webhook-based (hides scan_interval). */
|
||||
webhookBased?: boolean;
|
||||
|
||||
// ── Webhook URL display ──
|
||||
/** Pattern shown in edit mode, e.g. "/api/webhooks/gitea/{id}". */
|
||||
webhookUrlPattern?: string;
|
||||
|
||||
// ── Provider-specific hooks ──
|
||||
/**
|
||||
* Called after collection selection changes (before save).
|
||||
* E.g. Immich shared-link validation.
|
||||
*/
|
||||
onBeforeSave?(ctx: {
|
||||
form: Record<string, any>;
|
||||
previousCollectionIds: string[];
|
||||
collections: any[];
|
||||
api: typeof api;
|
||||
}): Promise<{ warnings?: { id: string; name: string; issue: string }[]; proceed: boolean }>;
|
||||
}
|
||||
Reference in New Issue
Block a user