From 8783180cf3f7380dbb5c13733d08e074bba38571 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 28 May 2026 17:23:49 -0400 Subject: [PATCH] refactor: plugin manifest (#28673) --- i18n/en.json | 1 + mobile/openapi/lib/model/job_name.dart | 6 +- .../model/plugin_template_response_dto.dart | 17 +++- open-api/immich-openapi-specs.json | 12 ++- packages/plugin-core/manifest.json | 29 +++---- packages/plugin-core/src/index.ts | 2 +- packages/plugin-sdk/src/types.ts | 18 ++--- packages/sdk/src/fetch-client.ts | 4 +- server/src/dtos/plugin-manifest.dto.ts | 1 + server/src/dtos/plugin.dto.ts | 3 + server/src/enum.ts | 2 +- .../src/repositories/workflow.repository.ts | 4 +- .../services/workflow-execution.service.ts | 77 +++++++++++++------ server/src/services/workflow.service.ts | 2 +- server/src/types.ts | 2 +- .../workflow/workflow-core-plugin.spec.ts | 18 ++--- .../lib/components/SchemaConfiguration.svelte | 2 +- web/src/lib/modals/PluginMethodPicker.svelte | 2 +- web/src/lib/modals/WorkflowEditTrigger.svelte | 37 --------- ...lte => WorkflowTemplatePickerModal.svelte} | 7 +- web/src/lib/services/workflow.service.ts | 4 +- web/src/lib/types.ts | 2 +- .../[workflowId]/WorkflowStepCard.svelte | 4 +- .../[workflowId]/WorkflowSummary.svelte | 2 +- 24 files changed, 140 insertions(+), 118 deletions(-) delete mode 100644 web/src/lib/modals/WorkflowEditTrigger.svelte rename web/src/lib/modals/{WorkflowTemplatePicker.svelte => WorkflowTemplatePickerModal.svelte} (87%) diff --git a/i18n/en.json b/i18n/en.json index 4ebd24b56b..dba0caf393 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2233,6 +2233,7 @@ "slideshow_repeat": "Repeat slideshow", "slideshow_repeat_description": "Loop back to beginning when slideshow ends", "slideshow_settings": "Slideshow settings", + "smart_album": "Smart album", "sort_albums_by": "Sort albums by...", "sort_created": "Date created", "sort_items": "Number of items", diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 444b080c12..511e1158e9 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -77,7 +77,7 @@ class JobName { static const versionCheck = JobName._(r'VersionCheck'); static const ocrQueueAll = JobName._(r'OcrQueueAll'); static const ocr = JobName._(r'Ocr'); - static const workflowAssetCreate = JobName._(r'WorkflowAssetCreate'); + static const workflowAssetTrigger = JobName._(r'WorkflowAssetTrigger'); /// List of all possible values in this [enum][JobName]. static const values = [ @@ -135,7 +135,7 @@ class JobName { versionCheck, ocrQueueAll, ocr, - workflowAssetCreate, + workflowAssetTrigger, ]; static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); @@ -228,7 +228,7 @@ class JobNameTypeTransformer { case r'VersionCheck': return JobName.versionCheck; case r'OcrQueueAll': return JobName.ocrQueueAll; case r'Ocr': return JobName.ocr; - case r'WorkflowAssetCreate': return JobName.workflowAssetCreate; + case r'WorkflowAssetTrigger': return JobName.workflowAssetTrigger; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/plugin_template_response_dto.dart b/mobile/openapi/lib/model/plugin_template_response_dto.dart index 4625da37d3..9f54753f49 100644 --- a/mobile/openapi/lib/model/plugin_template_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_template_response_dto.dart @@ -18,6 +18,7 @@ class PluginTemplateResponseDto { this.steps = const [], required this.title, required this.trigger, + this.uiHints = const [], }); /// Template description @@ -34,13 +35,17 @@ class PluginTemplateResponseDto { WorkflowTrigger trigger; + /// Ui hints, for example \"smart-album\" + List uiHints; + @override bool operator ==(Object other) => identical(this, other) || other is PluginTemplateResponseDto && other.description == description && other.key == key && _deepEquality.equals(other.steps, steps) && other.title == title && - other.trigger == trigger; + other.trigger == trigger && + _deepEquality.equals(other.uiHints, uiHints); @override int get hashCode => @@ -49,10 +54,11 @@ class PluginTemplateResponseDto { (key.hashCode) + (steps.hashCode) + (title.hashCode) + - (trigger.hashCode); + (trigger.hashCode) + + (uiHints.hashCode); @override - String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger]'; + String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger, uiHints=$uiHints]'; Map toJson() { final json = {}; @@ -61,6 +67,7 @@ class PluginTemplateResponseDto { json[r'steps'] = this.steps; json[r'title'] = this.title; json[r'trigger'] = this.trigger; + json[r'uiHints'] = this.uiHints; return json; } @@ -78,6 +85,9 @@ class PluginTemplateResponseDto { steps: PluginTemplateStepResponseDto.listFromJson(json[r'steps']), title: mapValueOfType(json, r'title')!, trigger: WorkflowTrigger.fromJson(json[r'trigger'])!, + uiHints: json[r'uiHints'] is Iterable + ? (json[r'uiHints'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; @@ -130,6 +140,7 @@ class PluginTemplateResponseDto { 'steps', 'title', 'trigger', + 'uiHints', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5857e6b1ce..20033cbc09 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -18171,7 +18171,7 @@ "VersionCheck", "OcrQueueAll", "Ocr", - "WorkflowAssetCreate" + "WorkflowAssetTrigger" ], "type": "string" }, @@ -20219,6 +20219,13 @@ "trigger": { "$ref": "#/components/schemas/WorkflowTrigger", "description": "Workflow trigger" + }, + "uiHints": { + "description": "Ui hints, for example \"smart-album\"", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -20226,7 +20233,8 @@ "key", "steps", "title", - "trigger" + "trigger", + "uiHints" ], "type": "object" }, diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json index 3111678862..7a9a5f26cd 100644 --- a/packages/plugin-core/manifest.json +++ b/packages/plugin-core/manifest.json @@ -20,19 +20,20 @@ "caseSensitive": false } }, - { - "method": "immich-plugin-core#assetAddToAlbums", - "config": { - "albumIds": [] - } - }, { "method": "immich-plugin-core#assetArchive", "config": { "inverse": false } + }, + { + "method": "immich-plugin-core#assetAddToAlbums", + "config": { + "albumIds": [] + } } - ] + ], + "uiHints": ["SmartAlbum"] } ], "methods": [ @@ -65,7 +66,7 @@ }, "required": ["pattern"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "filterFileType", @@ -85,7 +86,7 @@ }, "required": ["fileTypes"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "filterPerson", @@ -99,7 +100,7 @@ "array": true, "title": "Person IDs", "description": "List of person to match", - "uiHint": "personI" + "uiHint": "personId" }, "matchAny": { "type": "boolean", @@ -110,7 +111,7 @@ }, "required": ["personIds"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "assetArchive", @@ -187,7 +188,7 @@ "title": "Album IDs", "array": true, "description": "Target album IDs", - "uiHint": "albumId" + "uiHint": "AlbumId" } }, "required": ["albumIds"] @@ -272,14 +273,14 @@ "type": "string", "title": "Album ID", "description": "Target album ID", - "uiHint": "albumId" + "uiHint": "AlbumId" }, "albumIds": { "type": "string", "title": "Album IDs", "description": "Target album IDs", "array": true, - "uiHint": "albumId" + "uiHint": "AlbumId" } } } diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index 85a4a449e7..2b498614fa 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -93,7 +93,7 @@ export const assetTrash = () => { changes: { asset: config.inverse ? { deletedAt: null, status: AssetStatus.Active } - : { deletedAt: new Date(), status: AssetStatus.Trashed }, + : { deletedAt: new Date().toISOString(), status: AssetStatus.Trashed }, }, })); }; diff --git a/packages/plugin-sdk/src/types.ts b/packages/plugin-sdk/src/types.ts index 54cca3a5aa..2613922e95 100644 --- a/packages/plugin-sdk/src/types.ts +++ b/packages/plugin-sdk/src/types.ts @@ -68,19 +68,19 @@ export type AssetV1 = { ownerId: string; type: AssetType; originalPath: string; - fileCreatedAt: Date; - fileModifiedAt: Date; + fileCreatedAt: string; + fileModifiedAt: string; isFavorite: boolean; checksum: Buffer; // sha1 checksum livePhotoVideoId: string | null; - updatedAt: Date; - createdAt: Date; + updatedAt: string; + createdAt: string; originalFileName: string; isOffline: boolean; libraryId: string | null; isExternal: boolean; - deletedAt: Date | null; - localDateTime: Date; + deletedAt: string | null; + localDateTime: string; stackId: string | null; duplicateId: string | null; status: AssetStatus; @@ -93,8 +93,8 @@ export type AssetV1 = { exifImageHeight: number | null; fileSizeInByte: number | null; orientation: string | null; - dateTimeOriginal: Date | null; - modifyDate: Date | null; + dateTimeOriginal: string | null; + modifyDate: string | null; lensModel: string | null; fNumber: number | null; focalLength: number | null; @@ -116,7 +116,7 @@ export type AssetV1 = { autoStackId: string | null; rating: number | null; tags: string[] | null; - updatedAt: Date | null; + updatedAt: string | null; } | null; }; }; diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 66d7a996a3..2e35f25f3d 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -1535,6 +1535,8 @@ export type PluginTemplateResponseDto = { title: string; /** Workflow trigger */ trigger: WorkflowTrigger; + /** Ui hints, for example "smart-album" */ + uiHints: string[]; }; export type QueueResponseDto = { /** Whether the queue is paused */ @@ -7144,7 +7146,7 @@ export enum JobName { VersionCheck = "VersionCheck", OcrQueueAll = "OcrQueueAll", Ocr = "Ocr", - WorkflowAssetCreate = "WorkflowAssetCreate" + WorkflowAssetTrigger = "WorkflowAssetTrigger" } export enum SearchSuggestionType { Country = "country", diff --git a/server/src/dtos/plugin-manifest.dto.ts b/server/src/dtos/plugin-manifest.dto.ts index c8f043fde1..b175c6e1bb 100644 --- a/server/src/dtos/plugin-manifest.dto.ts +++ b/server/src/dtos/plugin-manifest.dto.ts @@ -38,6 +38,7 @@ const PluginManifestTemplateSchema = z description: z.string().min(1).describe('Template description'), trigger: WorkflowTriggerSchema.describe('Workflow trigger'), steps: z.array(PluginManifestTemplateStepSchema).describe('Workflow steps'), + uiHints: z.array(z.string()).optional().default([]).describe('Ui hints, for example "smart-album"'), }) .meta({ id: 'PluginManifestTemplateDto' }); diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index 074321bb44..62ff365a43 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -58,6 +58,7 @@ const PluginTemplateResponseSchema = z description: z.string().describe('Template description'), trigger: WorkflowTriggerSchema.describe('Workflow trigger'), steps: z.array(PluginTemplateStepResponseSchema).describe('Workflow steps'), + uiHints: z.array(z.string()).describe('Ui hints, for example "smart-album"'), }) .meta({ id: 'PluginTemplateResponseDto' }); @@ -91,6 +92,7 @@ export type PluginTemplate = { config?: Record | null; enabled?: boolean; }>; + uiHints: string[]; }; export const mapTemplate = (plugin: { name: string }, template: PluginTemplate): PluginTemplateResponseDto => { @@ -104,6 +106,7 @@ export const mapTemplate = (plugin: { name: string }, template: PluginTemplate): config: step.config ?? null, enabled: step.enabled, })), + uiHints: template.uiHints ?? [], }; }; diff --git a/server/src/enum.ts b/server/src/enum.ts index bc52e65f83..baf34b806e 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -866,7 +866,7 @@ export enum JobName { Ocr = 'Ocr', // Workflow - WorkflowAssetCreate = 'WorkflowAssetCreate', + WorkflowAssetTrigger = 'WorkflowAssetTrigger', } export const JobNameSchema = z.enum(JobName).describe('Job name').meta({ id: 'JobName' }); diff --git a/server/src/repositories/workflow.repository.ts b/server/src/repositories/workflow.repository.ts index 69ecb83ae9..1b888b759b 100644 --- a/server/src/repositories/workflow.repository.ts +++ b/server/src/repositories/workflow.repository.ts @@ -45,10 +45,10 @@ export class WorkflowRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - search(dto: WorkflowSearchDto & { ownerId?: string }) { + search(dto: WorkflowSearchDto & { userId?: string }) { return this.queryBuilder() .$if(!!dto.id, (qb) => qb.where('id', '=', dto.id!)) - .$if(!!dto.ownerId, (qb) => qb.where('ownerId', '=', dto.ownerId!)) + .$if(!!dto.userId, (qb) => qb.where('ownerId', '=', dto.userId!)) .$if(!!dto.trigger, (qb) => qb.where('trigger', '=', dto.trigger!)) .$if(dto.enabled !== undefined, (qb) => qb.where('enabled', '=', dto.enabled!)) .orderBy('createdAt', 'desc') diff --git a/server/src/services/workflow-execution.service.ts b/server/src/services/workflow-execution.service.ts index a1ecf4526d..0f600e117b 100644 --- a/server/src/services/workflow-execution.service.ts +++ b/server/src/services/workflow-execution.service.ts @@ -1,9 +1,8 @@ import { CurrentPlugin } from '@extism/extism'; import { WorkflowChanges, WorkflowEventData, WorkflowEventPayload, WorkflowResponse } from '@immich/plugin-sdk'; import { HttpException, UnauthorizedException } from '@nestjs/common'; -import _ from 'lodash'; import { join } from 'node:path'; -import { OnEvent, OnJob } from 'src/decorators'; +import { DummyValue, OnEvent, OnJob } from 'src/decorators'; import { AlbumsAddAssetsDto } from 'src/dtos/album.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -21,6 +20,7 @@ import { } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { AlbumService } from 'src/services/album.service'; +import { AssetService } from 'src/services/asset.service'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; @@ -32,9 +32,11 @@ const dummy = () => { type ExecuteOptions = { read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData }>; - write: (changes: WorkflowChanges) => Promise; + write: (auth: AuthDto, changes: WorkflowChanges) => Promise; }; +type AssetTrigger = { userId: string; assetId: string; trigger: WorkflowTrigger }; + export class WorkflowExecutionService extends BaseService { private jwtSecret!: string; @@ -62,7 +64,6 @@ export class WorkflowExecutionService extends BaseService { const albumAddAssets = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) => albumService.addAssets(authDto, ...args), ); - const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) => albumService.addAssetsToAlbums(authDto, ...args), ); @@ -247,20 +248,25 @@ export class WorkflowExecutionService extends BaseService { } @OnEvent({ name: 'AssetCreate' }) - async onAssetCreate({ asset }: ArgOf<'AssetCreate'>) { - const dto = { ownerId: asset.ownerId, trigger: WorkflowTrigger.AssetCreate }; - const items = await this.workflowRepository.search(dto); + onAssetCreate({ asset: { ownerId: userId, id: assetId } }: ArgOf<'AssetCreate'>) { + return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate }); + } + + private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) { + const items = await this.workflowRepository.search({ userId, trigger }); await this.jobRepository.queueAll( items.map((workflow) => ({ - name: JobName.WorkflowAssetCreate, - data: { workflowId: workflow.id, assetId: asset.id }, + name: JobName.WorkflowAssetTrigger, + data: { workflowId: workflow.id, assetId, trigger }, })), ); } - @OnJob({ name: JobName.WorkflowAssetCreate, queue: QueueName.Workflow }) - handleAssetCreate({ workflowId, assetId }: JobOf) { + @OnJob({ name: JobName.WorkflowAssetTrigger, queue: QueueName.Workflow }) + handleAssetTrigger({ workflowId, assetId }: JobOf) { return this.execute(workflowId, (type) => { + const assetService = BaseService.create(AssetService, this); + switch (type) { case WorkflowType.AssetV1: { return { @@ -271,19 +277,16 @@ export class WorkflowExecutionService extends BaseService { authUserId: asset.ownerId, }; }, - write: async (changes) => { - if (changes.asset) { - await this.assetRepository.update({ - id: assetId, - ..._.omitBy( - { - isFavorite: changes.asset?.isFavorite, - visibility: changes.asset?.visibility, - }, - _.isUndefined, - ), - }); + write: async (auth, changes) => { + const asset = changes.asset; + if (!asset) { + return; } + + await assetService.update(auth, assetId, { + isFavorite: asset.isFavorite, + visibility: asset.visibility, + }); }, } satisfies ExecuteOptions; } @@ -301,7 +304,19 @@ export class WorkflowExecutionService extends BaseService { } // TODO infer from steps - const type = 'AssetV1' as T; + let type: T | undefined; + for (const targetType of Object.values(WorkflowType)) { + const missing = workflow.steps.some((step) => !step.types.includes(targetType)); + if (!missing) { + type = targetType as unknown as T; + break; + } + } + + if (!type) { + throw new Error('Unable to infer workflow event type from steps'); + } + const handler = getHandler(type); if (!handler) { this.logger.error(`Misconfigured workflow ${workflowId}: no handler for type ${type}`); @@ -337,7 +352,19 @@ export class WorkflowExecutionService extends BaseService { payload, ); if (result?.changes) { - await write(result.changes); + await write( + { + user: { + id: readResult.authUserId, + }, + session: { + id: DummyValue.UUID, + // TODO move this to auth.elevated or similar + hasElevatedPermission: true, + }, + } as AuthDto, + result.changes, + ); ({ data } = await read(type)); } diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 0a62a60887..382ae2fe06 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -23,7 +23,7 @@ export class WorkflowService extends BaseService { } async search(auth: AuthDto, dto: WorkflowSearchDto): Promise { - const workflows = await this.workflowRepository.search({ ...dto, ownerId: auth.user.id }); + const workflows = await this.workflowRepository.search({ ...dto, userId: auth.user.id }); return workflows.map((workflow) => mapWorkflow(workflow)); } diff --git a/server/src/types.ts b/server/src/types.ts index c7dc1f5e18..480bd93118 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -404,7 +404,7 @@ export type JobItem = | { name: JobName.Ocr; data: IEntityJob } // Workflow - | { name: JobName.WorkflowAssetCreate; data: { workflowId: string; assetId: string } } + | { name: JobName.WorkflowAssetTrigger; data: { workflowId: string; assetId: string } } // Editor | { name: JobName.AssetEditThumbnailGeneration; data: IEntityJob }; diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 99f6c67d5c..fffddfc32a 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -137,7 +137,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetArchive' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Archive, @@ -154,7 +154,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetArchive', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Timeline, @@ -173,7 +173,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetLock' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Locked, @@ -190,7 +190,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetLock', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Timeline, @@ -209,7 +209,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetFavorite' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true }); }); @@ -224,7 +224,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetFavorite', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false }); }); @@ -242,7 +242,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id); }); @@ -261,7 +261,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album1.id, album2.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AlbumRepository).getAssetIds(album1.id, [asset.id])).resolves.toContain(asset.id); await expect(ctx.get(AlbumRepository).getAssetIds(album2.id, [asset.id])).resolves.toContain(asset.id); @@ -279,7 +279,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy(); await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id); }); diff --git a/web/src/lib/components/SchemaConfiguration.svelte b/web/src/lib/components/SchemaConfiguration.svelte index e48f46a402..2cc0b68089 100644 --- a/web/src/lib/components/SchemaConfiguration.svelte +++ b/web/src/lib/components/SchemaConfiguration.svelte @@ -64,7 +64,7 @@ {/each} -{:else if schema.uiHint === 'albumId'} +{:else if schema.uiHint === 'AlbumId'} {:else if schema.enum && schema.array} diff --git a/web/src/lib/modals/PluginMethodPicker.svelte b/web/src/lib/modals/PluginMethodPicker.svelte index bbfa2ca83f..230f5a2257 100644 --- a/web/src/lib/modals/PluginMethodPicker.svelte +++ b/web/src/lib/modals/PluginMethodPicker.svelte @@ -24,7 +24,7 @@
{method.title} - {#if method.uiHints.includes('filter')} + {#if method.uiHints.includes('Filter')} {$t('plugin_method_filter_type')} diff --git a/web/src/lib/modals/WorkflowEditTrigger.svelte b/web/src/lib/modals/WorkflowEditTrigger.svelte deleted file mode 100644 index d72bff3c6d..0000000000 --- a/web/src/lib/modals/WorkflowEditTrigger.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - -
- {#each pluginManager.triggers as item (item.trigger)} - (selected = item)}> -
- {getTriggerName($t, item.trigger)} - {getTriggerDescription($t, item.trigger)} -
-
- {/each} -
-
diff --git a/web/src/lib/modals/WorkflowTemplatePicker.svelte b/web/src/lib/modals/WorkflowTemplatePickerModal.svelte similarity index 87% rename from web/src/lib/modals/WorkflowTemplatePicker.svelte rename to web/src/lib/modals/WorkflowTemplatePickerModal.svelte index 05306671fc..0f3aad0ee6 100644 --- a/web/src/lib/modals/WorkflowTemplatePicker.svelte +++ b/web/src/lib/modals/WorkflowTemplatePickerModal.svelte @@ -2,7 +2,7 @@ import { pluginManager } from '$lib/managers/plugin-manager.svelte'; import { handleCreateWorkflow } from '$lib/services/workflow.service'; import { type PluginTemplateResponseDto } from '@immich/sdk'; - import { FormModal, Icon, ListButton, Text } from '@immich/ui'; + import { Badge, FormModal, Icon, ListButton, Text } from '@immich/ui'; import { mdiFlashOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -59,6 +59,11 @@ {template.title} {template.description}
+ {#if template.uiHints.includes('SmartAlbum')} +
+ {$t('smart_album')} +
+ {/if} {/each} diff --git a/web/src/lib/services/workflow.service.ts b/web/src/lib/services/workflow.service.ts index 79358b9ee4..c81b1b2706 100644 --- a/web/src/lib/services/workflow.service.ts +++ b/web/src/lib/services/workflow.service.ts @@ -26,7 +26,7 @@ import type { MessageFormatter } from 'svelte-i18n'; import { goto } from '$app/navigation'; import { eventManager } from '$lib/managers/event-manager.svelte'; import WorkflowDuplicateModal from '$lib/modals/WorkflowDuplicateModal.svelte'; -import WorkflowTemplatePicker from '$lib/modals/WorkflowTemplatePicker.svelte'; +import WorkflowTemplatePickerModal from '$lib/modals/WorkflowTemplatePickerModal.svelte'; import { Route } from '$lib/route'; import { copyToClipboard, downloadJson } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; @@ -50,7 +50,7 @@ export const getWorkflowsActions = ($t: MessageFormatter) => { const UseTemplate: ActionItem = { title: $t('browse_templates'), icon: mdiFileDocumentMultipleOutline, - onAction: () => modalManager.show(WorkflowTemplatePicker, {}), + onAction: () => modalManager.show(WorkflowTemplatePickerModal, {}), }; return { Create, UseTemplate }; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b0e1466da1..ef69a6b08e 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -104,7 +104,7 @@ export type JSONSchemaProperty = { array?: boolean; properties?: Record; required?: string[]; - uiHint?: 'albumId' | 'assetId' | 'personId'; + uiHint?: 'AlbumId' | 'AssetId' | 'PersonId'; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte index 661b90bd73..d6b20d3cc8 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte @@ -26,7 +26,7 @@ let { step, index, onEdit, onDelete, onInsertBefore, onDrop }: Props = $props(); const method = $derived(pluginManager.getMethod(step.method)); - const isFilter = $derived(method?.uiHints?.includes('filter') ?? false); + const isFilter = $derived(method?.uiHints?.includes('Filter') ?? false); const configEntries = $derived( Object.entries(step.config ?? {}).filter(([, value]) => value !== null && value !== undefined && value !== ''), ); @@ -75,7 +75,7 @@ target: document.body, props: { description: method?.description, - isFilter: method?.uiHints?.includes('filter') ?? false, + isFilter: method?.uiHints?.includes('Filter') ?? false, label: step ? pluginManager.getMethodLabel(step.method) : '', stepNumber: index + 1, }, diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte index 8b44e45219..3d6e29e896 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte @@ -67,7 +67,7 @@ for (const [i, step] of workflow.steps.entries()) { const method = pluginManager.getMethod(step.method); - const isFilter = method?.uiHints?.includes('filter') ?? false; + const isFilter = method?.uiHints?.includes('Filter') ?? false; const type = isFilter ? $t('filter') : $t('action'); const label = pluginManager.getMethodLabel(step.method); lines.push(` [${i + 1}] ${type.toUpperCase()} ยท ${label}`);