diff --git a/i18n/en.json b/i18n/en.json index 8effffa3ad..3d2a7625f4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,4 +1,5 @@ { + "get_people_error": "Error getting people", "about": "About", "account": "Account", "account_settings": "Account Settings", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a91696524d..f44503e458 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -199,8 +199,8 @@ Class | Method | HTTP request | Description *PeopleApi* | [**updatePeople**](doc//PeopleApi.md#updatepeople) | **PUT** /people | Update people *PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person *PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin +*PluginsApi* | [**getPluginTriggers**](doc//PluginsApi.md#getplugintriggers) | **GET** /plugins/triggers | List all plugin triggers *PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins -*PluginsApi* | [**getTriggers**](doc//PluginsApi.md#gettriggers) | **GET** /plugins/triggers | List all plugin triggers *QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue *QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue *QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs @@ -466,7 +466,7 @@ Class | Method | HTTP request | Description - [PinCodeSetupDto](doc//PinCodeSetupDto.md) - [PlacesResponseDto](doc//PlacesResponseDto.md) - [PluginActionResponseDto](doc//PluginActionResponseDto.md) - - [PluginContext](doc//PluginContext.md) + - [PluginContextType](doc//PluginContextType.md) - [PluginFilterResponseDto](doc//PluginFilterResponseDto.md) - [PluginResponseDto](doc//PluginResponseDto.md) - [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3ccce45c95..05d1803979 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -217,7 +217,7 @@ part 'model/pin_code_reset_dto.dart'; part 'model/pin_code_setup_dto.dart'; part 'model/places_response_dto.dart'; part 'model/plugin_action_response_dto.dart'; -part 'model/plugin_context.dart'; +part 'model/plugin_context_type.dart'; part 'model/plugin_filter_response_dto.dart'; part 'model/plugin_response_dto.dart'; part 'model/plugin_trigger_response_dto.dart'; diff --git a/mobile/openapi/lib/api/plugins_api.dart b/mobile/openapi/lib/api/plugins_api.dart index 1660bd8509..5735fba379 100644 --- a/mobile/openapi/lib/api/plugins_api.dart +++ b/mobile/openapi/lib/api/plugins_api.dart @@ -73,6 +73,57 @@ class PluginsApi { return null; } + /// List all plugin triggers + /// + /// Retrieve a list of all available plugin triggers. + /// + /// Note: This method returns the HTTP [Response]. + Future getPluginTriggersWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/plugins/triggers'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// List all plugin triggers + /// + /// Retrieve a list of all available plugin triggers. + Future?> getPluginTriggers() async { + final response = await getPluginTriggersWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// List all plugins /// /// Retrieve a list of plugins available to the authenticated user. @@ -123,55 +174,4 @@ class PluginsApi { } return null; } - - /// List all plugin triggers - /// - /// Retrieve a list of all available plugin triggers. - /// - /// Note: This method returns the HTTP [Response]. - Future getTriggersWithHttpInfo() async { - // ignore: prefer_const_declarations - final apiPath = r'/plugins/triggers'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// List all plugin triggers - /// - /// Retrieve a list of all available plugin triggers. - Future?> getTriggers() async { - final response = await getTriggersWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index e82880760f..39aea82c89 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -482,8 +482,8 @@ class ApiClient { return PlacesResponseDto.fromJson(value); case 'PluginActionResponseDto': return PluginActionResponseDto.fromJson(value); - case 'PluginContext': - return PluginContextTypeTransformer().decode(value); + case 'PluginContextType': + return PluginContextTypeTypeTransformer().decode(value); case 'PluginFilterResponseDto': return PluginFilterResponseDto.fromJson(value); case 'PluginResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 2c97eeb314..39caa4534f 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -127,8 +127,8 @@ String parameterToString(dynamic value) { if (value is Permission) { return PermissionTypeTransformer().encode(value).toString(); } - if (value is PluginContext) { - return PluginContextTypeTransformer().encode(value).toString(); + if (value is PluginContextType) { + return PluginContextTypeTypeTransformer().encode(value).toString(); } if (value is PluginTriggerType) { return PluginTriggerTypeTypeTransformer().encode(value).toString(); diff --git a/mobile/openapi/lib/model/plugin_action_response_dto.dart b/mobile/openapi/lib/model/plugin_action_response_dto.dart index 75b23fc8a4..5ba54f6eb5 100644 --- a/mobile/openapi/lib/model/plugin_action_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_action_response_dto.dart @@ -32,7 +32,7 @@ class PluginActionResponseDto { Object? schema; - List supportedContexts; + List supportedContexts; String title; @@ -90,7 +90,7 @@ class PluginActionResponseDto { methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, schema: mapValueOfType(json, r'schema'), - supportedContexts: PluginContext.listFromJson(json[r'supportedContexts']), + supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); } diff --git a/mobile/openapi/lib/model/plugin_context.dart b/mobile/openapi/lib/model/plugin_context_type.dart similarity index 51% rename from mobile/openapi/lib/model/plugin_context.dart rename to mobile/openapi/lib/model/plugin_context_type.dart index efb701c7d0..797d2c3d3b 100644 --- a/mobile/openapi/lib/model/plugin_context.dart +++ b/mobile/openapi/lib/model/plugin_context_type.dart @@ -11,9 +11,9 @@ part of openapi.api; -class PluginContext { +class PluginContextType { /// Instantiate a new enum with the provided [value]. - const PluginContext._(this.value); + const PluginContextType._(this.value); /// The underlying value of this enum member. final String value; @@ -23,24 +23,24 @@ class PluginContext { String toJson() => value; - static const asset = PluginContext._(r'asset'); - static const album = PluginContext._(r'album'); - static const person = PluginContext._(r'person'); + static const asset = PluginContextType._(r'asset'); + static const album = PluginContextType._(r'album'); + static const person = PluginContextType._(r'person'); - /// List of all possible values in this [enum][PluginContext]. - static const values = [ + /// List of all possible values in this [enum][PluginContextType]. + static const values = [ asset, album, person, ]; - static PluginContext? fromJson(dynamic value) => PluginContextTypeTransformer().decode(value); + static PluginContextType? fromJson(dynamic value) => PluginContextTypeTypeTransformer().decode(value); - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = PluginContext.fromJson(row); + final value = PluginContextType.fromJson(row); if (value != null) { result.add(value); } @@ -50,16 +50,16 @@ class PluginContext { } } -/// Transformation class that can [encode] an instance of [PluginContext] to String, -/// and [decode] dynamic data back to [PluginContext]. -class PluginContextTypeTransformer { - factory PluginContextTypeTransformer() => _instance ??= const PluginContextTypeTransformer._(); +/// Transformation class that can [encode] an instance of [PluginContextType] to String, +/// and [decode] dynamic data back to [PluginContextType]. +class PluginContextTypeTypeTransformer { + factory PluginContextTypeTypeTransformer() => _instance ??= const PluginContextTypeTypeTransformer._(); - const PluginContextTypeTransformer._(); + const PluginContextTypeTypeTransformer._(); - String encode(PluginContext data) => data.value; + String encode(PluginContextType data) => data.value; - /// Decodes a [dynamic value][data] to a PluginContext. + /// Decodes a [dynamic value][data] to a PluginContextType. /// /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] @@ -67,12 +67,12 @@ class PluginContextTypeTransformer { /// /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, /// and users are still using an old app with the old code. - PluginContext? decode(dynamic data, {bool allowNull = true}) { + PluginContextType? decode(dynamic data, {bool allowNull = true}) { if (data != null) { switch (data) { - case r'asset': return PluginContext.asset; - case r'album': return PluginContext.album; - case r'person': return PluginContext.person; + case r'asset': return PluginContextType.asset; + case r'album': return PluginContextType.album; + case r'person': return PluginContextType.person; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); @@ -82,7 +82,7 @@ class PluginContextTypeTransformer { return null; } - /// Singleton [PluginContextTypeTransformer] instance. - static PluginContextTypeTransformer? _instance; + /// Singleton [PluginContextTypeTypeTransformer] instance. + static PluginContextTypeTypeTransformer? _instance; } diff --git a/mobile/openapi/lib/model/plugin_filter_response_dto.dart b/mobile/openapi/lib/model/plugin_filter_response_dto.dart index 8ed6acec78..5873d72f07 100644 --- a/mobile/openapi/lib/model/plugin_filter_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_filter_response_dto.dart @@ -32,7 +32,7 @@ class PluginFilterResponseDto { Object? schema; - List supportedContexts; + List supportedContexts; String title; @@ -90,7 +90,7 @@ class PluginFilterResponseDto { methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, schema: mapValueOfType(json, r'schema'), - supportedContexts: PluginContext.listFromJson(json[r'supportedContexts']), + supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); } diff --git a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart index 246e0b4acf..4eabecdc42 100644 --- a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -13,44 +13,44 @@ part of openapi.api; class PluginTriggerResponseDto { /// Returns a new [PluginTriggerResponseDto] instance. PluginTriggerResponseDto({ - required this.context, + required this.contextType, required this.description, required this.name, - required this.triggerType, + required this.type, }); - PluginContext context; + PluginContextType contextType; String description; String name; - PluginTriggerType triggerType; + PluginTriggerType type; @override bool operator ==(Object other) => identical(this, other) || other is PluginTriggerResponseDto && - other.context == context && + other.contextType == contextType && other.description == description && other.name == name && - other.triggerType == triggerType; + other.type == type; @override int get hashCode => // ignore: unnecessary_parenthesis - (context.hashCode) + + (contextType.hashCode) + (description.hashCode) + (name.hashCode) + - (triggerType.hashCode); + (type.hashCode); @override - String toString() => 'PluginTriggerResponseDto[context=$context, description=$description, name=$name, triggerType=$triggerType]'; + String toString() => 'PluginTriggerResponseDto[contextType=$contextType, description=$description, name=$name, type=$type]'; Map toJson() { final json = {}; - json[r'context'] = this.context; + json[r'contextType'] = this.contextType; json[r'description'] = this.description; json[r'name'] = this.name; - json[r'triggerType'] = this.triggerType; + json[r'type'] = this.type; return json; } @@ -63,10 +63,10 @@ class PluginTriggerResponseDto { final json = value.cast(); return PluginTriggerResponseDto( - context: PluginContext.fromJson(json[r'context'])!, + contextType: PluginContextType.fromJson(json[r'contextType'])!, description: mapValueOfType(json, r'description')!, name: mapValueOfType(json, r'name')!, - triggerType: PluginTriggerType.fromJson(json[r'triggerType'])!, + type: PluginTriggerType.fromJson(json[r'type'])!, ); } return null; @@ -114,10 +114,10 @@ class PluginTriggerResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'context', + 'contextType', 'description', 'name', - 'triggerType', + 'type', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 87d14e2f92..ad0c05571f 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8023,7 +8023,7 @@ "/plugins/triggers": { "get": { "description": "Retrieve a list of all available plugin triggers.", - "operationId": "getTriggers", + "operationId": "getPluginTriggers", "parameters": [], "responses": { "200": { @@ -18331,7 +18331,7 @@ }, "supportedContexts": { "items": { - "$ref": "#/components/schemas/PluginContext" + "$ref": "#/components/schemas/PluginContextType" }, "type": "array" }, @@ -18350,7 +18350,7 @@ ], "type": "object" }, - "PluginContext": { + "PluginContextType": { "enum": [ "asset", "album", @@ -18378,7 +18378,7 @@ }, "supportedContexts": { "items": { - "$ref": "#/components/schemas/PluginContext" + "$ref": "#/components/schemas/PluginContextType" }, "type": "array" }, @@ -18452,10 +18452,10 @@ }, "PluginTriggerResponseDto": { "properties": { - "context": { + "contextType": { "allOf": [ { - "$ref": "#/components/schemas/PluginContext" + "$ref": "#/components/schemas/PluginContextType" } ] }, @@ -18465,7 +18465,7 @@ "name": { "type": "string" }, - "triggerType": { + "type": { "allOf": [ { "$ref": "#/components/schemas/PluginTriggerType" @@ -18474,10 +18474,10 @@ } }, "required": [ - "context", + "contextType", "description", "name", - "triggerType" + "type" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7f4bcc33e8..644299b188 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -942,7 +942,7 @@ export type PluginActionResponseDto = { methodName: string; pluginId: string; schema: object | null; - supportedContexts: PluginContext[]; + supportedContexts: PluginContextType[]; title: string; }; export type PluginFilterResponseDto = { @@ -951,7 +951,7 @@ export type PluginFilterResponseDto = { methodName: string; pluginId: string; schema: object | null; - supportedContexts: PluginContext[]; + supportedContexts: PluginContextType[]; title: string; }; export type PluginResponseDto = { @@ -967,10 +967,10 @@ export type PluginResponseDto = { version: string; }; export type PluginTriggerResponseDto = { - context: PluginContext; + contextType: PluginContextType; description: string; name: string; - triggerType: PluginTriggerType; + "type": PluginTriggerType; }; export type QueueResponseDto = { isPaused: boolean; @@ -3666,7 +3666,7 @@ export function getPlugins(opts?: Oazapfts.RequestOpts) { /** * List all plugin triggers */ -export function getTriggers(opts?: Oazapfts.RequestOpts) { +export function getPluginTriggers(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: PluginTriggerResponseDto[]; @@ -5436,7 +5436,7 @@ export enum PartnerDirection { SharedBy = "shared-by", SharedWith = "shared-with" } -export enum PluginContext { +export enum PluginContextType { Asset = "asset", Album = "album", Person = "person" diff --git a/server/src/controllers/plugin.controller.ts b/server/src/controllers/plugin.controller.ts index b1aebbbbf4..52c833e93d 100644 --- a/server/src/controllers/plugin.controller.ts +++ b/server/src/controllers/plugin.controller.ts @@ -19,7 +19,7 @@ export class PluginController { description: 'Retrieve a list of all available plugin triggers.', history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'), }) - getTriggers(): PluginTriggerResponseDto[] { + getPluginTriggers(): PluginTriggerResponseDto[] { return this.service.getTriggers(); } diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index 2d69f8ea6a..0ba81c4518 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,16 +1,16 @@ import { IsNotEmpty, IsString } from 'class-validator'; import { PluginAction, PluginFilter } from 'src/database'; -import { PluginContext, PluginTriggerType } from 'src/enum'; +import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum'; import type { JSONSchema } from 'src/types/plugin-schema.types'; import { ValidateEnum } from 'src/validation'; export class PluginTriggerResponseDto { name!: string; @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' }) - triggerType!: PluginTriggerType; + type!: PluginTriggerType; description!: string; - @ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) - context!: PluginContext; + @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' }) + contextType!: PluginContextType; } export class PluginResponseDto { @@ -33,8 +33,8 @@ export class PluginFilterResponseDto { title!: string; description!: string; - @ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) - supportedContexts!: PluginContext[]; + @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' }) + supportedContexts!: PluginContextType[]; schema!: JSONSchema | null; } @@ -45,8 +45,8 @@ export class PluginActionResponseDto { title!: string; description!: string; - @ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) - supportedContexts!: PluginContext[]; + @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' }) + supportedContexts!: PluginContextType[]; schema!: JSONSchema | null; } diff --git a/server/src/plugins.ts b/server/src/plugins.ts index 2e1838aae7..2adf748e64 100644 --- a/server/src/plugins.ts +++ b/server/src/plugins.ts @@ -2,22 +2,22 @@ import { PluginContext, PluginTriggerType } from 'src/enum'; export type PluginTrigger = { name: string; - triggerType: PluginTriggerType; + type: PluginTriggerType; description: string; - context: PluginContext; + contextType: PluginContext; }; export const pluginTriggers: PluginTrigger[] = [ { name: 'Asset Uploaded', - triggerType: PluginTriggerType.AssetCreate, + type: PluginTriggerType.AssetCreate, description: 'Triggered when a new asset is uploaded', - context: PluginContext.Asset, + contextType: PluginContext.Asset, }, { name: 'Person Recognized', - triggerType: PluginTriggerType.PersonRecognized, + type: PluginTriggerType.PersonRecognized, description: 'Triggered when a person is detected', - context: PluginContext.Person, + contextType: PluginContext.Person, }, ]; diff --git a/server/src/queries/workflow.repository.sql b/server/src/queries/workflow.repository.sql index 3797c5bb06..27dc21dffe 100644 --- a/server/src/queries/workflow.repository.sql +++ b/server/src/queries/workflow.repository.sql @@ -7,6 +7,8 @@ from "workflow" where "id" = $1 +order by + "createdAt" desc -- WorkflowRepository.getWorkflowsByOwner select @@ -16,7 +18,7 @@ from where "ownerId" = $1 order by - "name" + "createdAt" desc -- WorkflowRepository.getWorkflowsByTrigger select diff --git a/server/src/repositories/workflow.repository.ts b/server/src/repositories/workflow.repository.ts index 4ae657cfbf..deaf2aa2fc 100644 --- a/server/src/repositories/workflow.repository.ts +++ b/server/src/repositories/workflow.repository.ts @@ -12,12 +12,22 @@ export class WorkflowRepository { @GenerateSql({ params: [DummyValue.UUID] }) getWorkflow(id: string) { - return this.db.selectFrom('workflow').selectAll().where('id', '=', id).executeTakeFirst(); + return this.db + .selectFrom('workflow') + .selectAll() + .where('id', '=', id) + .orderBy('createdAt', 'desc') + .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID] }) getWorkflowsByOwner(ownerId: string) { - return this.db.selectFrom('workflow').selectAll().where('ownerId', '=', ownerId).orderBy('name').execute(); + return this.db + .selectFrom('workflow') + .selectAll() + .where('ownerId', '=', ownerId) + .orderBy('createdAt', 'desc') + .execute(); } @GenerateSql({ params: [PluginTriggerType.AssetCreate] }) diff --git a/server/src/services/plugin.service.ts b/server/src/services/plugin.service.ts index 23f6709b9c..d82b34c847 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -116,12 +116,12 @@ export class PluginService extends BaseService { } private async loadPluginToDatabase(manifest: PluginManifestDto, basePath: string): Promise { - // const currentPlugin = await this.pluginRepository.getPluginByName(manifest.name); - // if (currentPlugin != null && currentPlugin.version === manifest.version) { - // this.logger.log(`Plugin ${manifest.name} is up to date (version ${manifest.version}). Skipping`); - // return; - // } - // + const currentPlugin = await this.pluginRepository.getPluginByName(manifest.name); + if (currentPlugin != null && currentPlugin.version === manifest.version) { + this.logger.log(`Plugin ${manifest.name} is up to date (version ${manifest.version}). Skipping`); + return; + } + const { plugin, filters, actions } = await this.pluginRepository.loadPlugin(manifest, basePath); this.logger.log(`Upserted plugin: ${plugin.name} (ID: ${plugin.id}, version: ${plugin.version})`); diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 10b240aa8e..1a65182b1f 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -16,10 +16,10 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class WorkflowService extends BaseService { async create(auth: AuthDto, dto: WorkflowCreateDto): Promise { - const trigger = this.getTriggerOrFail(dto.triggerType); + const context = this.getContextForTrigger(dto.triggerType); - const filterInserts = await this.validateAndMapFilters(dto.filters, trigger.context); - const actionInserts = await this.validateAndMapActions(dto.actions, trigger.context); + const filterInserts = await this.validateAndMapFilters(dto.filters, context); + const actionInserts = await this.validateAndMapActions(dto.actions, context); const workflow = await this.workflowRepository.createWorkflow( { @@ -56,11 +56,11 @@ export class WorkflowService extends BaseService { } const workflow = await this.findOrFail(id); - const trigger = this.getTriggerOrFail(dto.triggerType ?? workflow.triggerType); + const context = this.getContextForTrigger(dto.triggerType ?? workflow.triggerType); const { filters, actions, ...workflowUpdate } = dto; - const filterInserts = filters && (await this.validateAndMapFilters(filters, trigger.context)); - const actionInserts = actions && (await this.validateAndMapActions(actions, trigger.context)); + const filterInserts = filters && (await this.validateAndMapFilters(filters, context)); + const actionInserts = actions && (await this.validateAndMapActions(actions, context)); const updatedWorkflow = await this.workflowRepository.updateWorkflow( id, @@ -124,12 +124,12 @@ export class WorkflowService extends BaseService { })); } - private getTriggerOrFail(triggerType: PluginTriggerType) { - const trigger = pluginTriggers.find((t) => t.triggerType === triggerType); + private getContextForTrigger(type: PluginTriggerType) { + const trigger = pluginTriggers.find((t) => t.type === type); if (!trigger) { - throw new BadRequestException(`Invalid trigger type: ${triggerType}`); + throw new BadRequestException(`Invalid trigger type: ${type}`); } - return trigger; + return trigger.contextType; } private async findOrFail(id: string) { diff --git a/web/src/lib/components/workflows/SchemaFormFields.svelte b/web/src/lib/components/workflows/SchemaFormFields.svelte index 6346eb870a..b88d6bbd97 100644 --- a/web/src/lib/components/workflows/SchemaFormFields.svelte +++ b/web/src/lib/components/workflows/SchemaFormFields.svelte @@ -2,7 +2,7 @@ import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte'; import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils'; - import { formatLabel, getComponentFromSchema } from '$lib/utils/workflow'; + import { formatLabel, getComponentFromSchema, type ComponentConfig } from '$lib/utils/workflow'; import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; import { Button, Field, Input, MultiSelect, Select, Switch, Text, modalManager, type SelectItem } from '@immich/ui'; import { mdiPlus } from '@mdi/js'; @@ -43,56 +43,7 @@ return; } - const fetchMetadata = async () => { - const metadataUpdates: Record< - string, - AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[] - > = {}; - - for (const [key, component] of Object.entries(components)) { - const value = actualConfig[key]; - if (!value || pickerMetadata[key]) { - continue; // Skip if no value or already loaded - } - - const isAlbumPicker = component.subType === 'album-picker'; - const isPeoplePicker = component.subType === 'people-picker'; - - if (!isAlbumPicker && !isPeoplePicker) { - continue; - } - - try { - if (Array.isArray(value) && value.length > 0) { - // Multiple selection - if (isAlbumPicker) { - const albums = await Promise.all(value.map((id) => getAlbumInfo({ id }))); - metadataUpdates[key] = albums; - } else if (isPeoplePicker) { - const people = await Promise.all(value.map((id) => getPerson({ id }))); - metadataUpdates[key] = people; - } - } else if (typeof value === 'string' && value) { - // Single selection - if (isAlbumPicker) { - const album = await getAlbumInfo({ id: value }); - metadataUpdates[key] = album; - } else if (isPeoplePicker) { - const person = await getPerson({ id: value }); - metadataUpdates[key] = person; - } - } - } catch (error) { - console.error(`Failed to fetch metadata for ${key}:`, error); - } - } - - if (Object.keys(metadataUpdates).length > 0) { - pickerMetadata = { ...pickerMetadata, ...metadataUpdates }; - } - }; - - void fetchMetadata(); + void fetchMetadata(components); }); $effect(() => { @@ -148,6 +99,55 @@ } }); + const fetchMetadata = async (components: Record) => { + const metadataUpdates: Record< + string, + AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[] + > = {}; + + for (const [key, component] of Object.entries(components)) { + const value = actualConfig[key]; + if (!value || pickerMetadata[key]) { + continue; // Skip if no value or already loaded + } + + const isAlbumPicker = component.subType === 'album-picker'; + const isPeoplePicker = component.subType === 'people-picker'; + + if (!isAlbumPicker && !isPeoplePicker) { + continue; + } + + try { + if (Array.isArray(value) && value.length > 0) { + // Multiple selection + if (isAlbumPicker) { + const albums = await Promise.all(value.map((id) => getAlbumInfo({ id }))); + metadataUpdates[key] = albums; + } else if (isPeoplePicker) { + const people = await Promise.all(value.map((id) => getPerson({ id }))); + metadataUpdates[key] = people; + } + } else if (typeof value === 'string' && value) { + // Single selection + if (isAlbumPicker) { + const album = await getAlbumInfo({ id: value }); + metadataUpdates[key] = album; + } else if (isPeoplePicker) { + const person = await getPerson({ id: value }); + metadataUpdates[key] = person; + } + } + } catch (error) { + console.error(`Failed to fetch metadata for ${key}:`, error); + } + } + + if (Object.keys(metadataUpdates).length > 0) { + pickerMetadata = { ...pickerMetadata, ...metadataUpdates }; + } + }; + const handleAlbumPicker = async (key: string, multiple: boolean) => { const albums = await modalManager.show(AlbumPickerModal, { shared: false }); if (albums && albums.length > 0) { @@ -161,7 +161,9 @@ }; const handlePeoplePicker = async (key: string, multiple: boolean) => { - const people = await modalManager.show(PeoplePickerModal, { multiple }); + const currentIds = (actualConfig[key] as string[] | undefined) ?? []; + const excludedIds = multiple ? currentIds : []; + const people = await modalManager.show(PeoplePickerModal, { multiple, excludedIds }); if (people && people.length > 0) { const value = multiple ? people.map((p) => p.id) : people[0].id; updateConfig(key, value); diff --git a/web/src/lib/components/workflows/WorkflowSummary.svelte b/web/src/lib/components/workflows/WorkflowSummary.svelte index de9bb70b65..9ac991fa0a 100644 --- a/web/src/lib/components/workflows/WorkflowSummary.svelte +++ b/web/src/lib/components/workflows/WorkflowSummary.svelte @@ -63,7 +63,7 @@
- {#each filters as filter, index (filter.id)} + {#each filters as filter, index (index)}
{$t('actions')}
- {#each actions as action, index (action.id)} + {#each actions as action, index (index)}
(isOpen = true)} > diff --git a/web/src/lib/components/workflows/WorkflowTriggerCard.svelte b/web/src/lib/components/workflows/WorkflowTriggerCard.svelte index 1a90b0af48..9eb0de7b50 100644 --- a/web/src/lib/components/workflows/WorkflowTriggerCard.svelte +++ b/web/src/lib/components/workflows/WorkflowTriggerCard.svelte @@ -39,12 +39,12 @@ ? 'bg-primary text-light' : 'text-light-100 bg-light-300 group-hover:bg-light-500'}" > - +
{trigger.name} {#if trigger.description} - {trigger.description} + {trigger.description} {/if}
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 3036159801..27f2ea1187 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -56,7 +56,6 @@ export enum AppRoute { LARGE_FILES = '/utilities/large-files', GEOLOCATION = '/utilities/geolocation', WORKFLOWS = '/utilities/workflows', - WORKFLOWS_EDIT = '/utilities/workflows/edit', FOLDERS = '/folders', TAGS = '/tags', diff --git a/web/src/lib/modals/AddWorkflowStepModal.svelte b/web/src/lib/modals/AddWorkflowStepModal.svelte index c917ff7a53..5ae2e6bfc0 100644 --- a/web/src/lib/modals/AddWorkflowStepModal.svelte +++ b/web/src/lib/modals/AddWorkflowStepModal.svelte @@ -1,23 +1,17 @@