pr feedback

This commit is contained in:
Alex Tran
2025-12-02 21:50:07 +00:00
parent 290de9d27c
commit bd4355a75f
30 changed files with 572 additions and 578 deletions

View File

@@ -1,4 +1,5 @@
{ {
"get_people_error": "Error getting people",
"about": "About", "about": "About",
"account": "Account", "account": "Account",
"account_settings": "Account Settings", "account_settings": "Account Settings",

View File

@@ -199,8 +199,8 @@ Class | Method | HTTP request | Description
*PeopleApi* | [**updatePeople**](doc//PeopleApi.md#updatepeople) | **PUT** /people | Update people *PeopleApi* | [**updatePeople**](doc//PeopleApi.md#updatepeople) | **PUT** /people | Update people
*PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person *PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person
*PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin *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* | [**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* | [**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* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue
*QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs *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) - [PinCodeSetupDto](doc//PinCodeSetupDto.md)
- [PlacesResponseDto](doc//PlacesResponseDto.md) - [PlacesResponseDto](doc//PlacesResponseDto.md)
- [PluginActionResponseDto](doc//PluginActionResponseDto.md) - [PluginActionResponseDto](doc//PluginActionResponseDto.md)
- [PluginContext](doc//PluginContext.md) - [PluginContextType](doc//PluginContextType.md)
- [PluginFilterResponseDto](doc//PluginFilterResponseDto.md) - [PluginFilterResponseDto](doc//PluginFilterResponseDto.md)
- [PluginResponseDto](doc//PluginResponseDto.md) - [PluginResponseDto](doc//PluginResponseDto.md)
- [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md) - [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md)

View File

@@ -217,7 +217,7 @@ part 'model/pin_code_reset_dto.dart';
part 'model/pin_code_setup_dto.dart'; part 'model/pin_code_setup_dto.dart';
part 'model/places_response_dto.dart'; part 'model/places_response_dto.dart';
part 'model/plugin_action_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_filter_response_dto.dart';
part 'model/plugin_response_dto.dart'; part 'model/plugin_response_dto.dart';
part 'model/plugin_trigger_response_dto.dart'; part 'model/plugin_trigger_response_dto.dart';

View File

@@ -73,6 +73,57 @@ class PluginsApi {
return null; return null;
} }
/// List all plugin triggers
///
/// Retrieve a list of all available plugin triggers.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getPluginTriggersWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/plugins/triggers';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
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<List<PluginTriggerResponseDto>?> 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<PluginTriggerResponseDto>') as List)
.cast<PluginTriggerResponseDto>()
.toList(growable: false);
}
return null;
}
/// List all plugins /// List all plugins
/// ///
/// Retrieve a list of plugins available to the authenticated user. /// Retrieve a list of plugins available to the authenticated user.
@@ -123,55 +174,4 @@ class PluginsApi {
} }
return null; return null;
} }
/// List all plugin triggers
///
/// Retrieve a list of all available plugin triggers.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getTriggersWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/plugins/triggers';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
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<List<PluginTriggerResponseDto>?> 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<PluginTriggerResponseDto>') as List)
.cast<PluginTriggerResponseDto>()
.toList(growable: false);
}
return null;
}
} }

View File

@@ -482,8 +482,8 @@ class ApiClient {
return PlacesResponseDto.fromJson(value); return PlacesResponseDto.fromJson(value);
case 'PluginActionResponseDto': case 'PluginActionResponseDto':
return PluginActionResponseDto.fromJson(value); return PluginActionResponseDto.fromJson(value);
case 'PluginContext': case 'PluginContextType':
return PluginContextTypeTransformer().decode(value); return PluginContextTypeTypeTransformer().decode(value);
case 'PluginFilterResponseDto': case 'PluginFilterResponseDto':
return PluginFilterResponseDto.fromJson(value); return PluginFilterResponseDto.fromJson(value);
case 'PluginResponseDto': case 'PluginResponseDto':

View File

@@ -127,8 +127,8 @@ String parameterToString(dynamic value) {
if (value is Permission) { if (value is Permission) {
return PermissionTypeTransformer().encode(value).toString(); return PermissionTypeTransformer().encode(value).toString();
} }
if (value is PluginContext) { if (value is PluginContextType) {
return PluginContextTypeTransformer().encode(value).toString(); return PluginContextTypeTypeTransformer().encode(value).toString();
} }
if (value is PluginTriggerType) { if (value is PluginTriggerType) {
return PluginTriggerTypeTypeTransformer().encode(value).toString(); return PluginTriggerTypeTypeTransformer().encode(value).toString();

View File

@@ -32,7 +32,7 @@ class PluginActionResponseDto {
Object? schema; Object? schema;
List<PluginContext> supportedContexts; List<PluginContextType> supportedContexts;
String title; String title;
@@ -90,7 +90,7 @@ class PluginActionResponseDto {
methodName: mapValueOfType<String>(json, r'methodName')!, methodName: mapValueOfType<String>(json, r'methodName')!,
pluginId: mapValueOfType<String>(json, r'pluginId')!, pluginId: mapValueOfType<String>(json, r'pluginId')!,
schema: mapValueOfType<Object>(json, r'schema'), schema: mapValueOfType<Object>(json, r'schema'),
supportedContexts: PluginContext.listFromJson(json[r'supportedContexts']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']),
title: mapValueOfType<String>(json, r'title')!, title: mapValueOfType<String>(json, r'title')!,
); );
} }

View File

@@ -11,9 +11,9 @@
part of openapi.api; part of openapi.api;
class PluginContext { class PluginContextType {
/// Instantiate a new enum with the provided [value]. /// Instantiate a new enum with the provided [value].
const PluginContext._(this.value); const PluginContextType._(this.value);
/// The underlying value of this enum member. /// The underlying value of this enum member.
final String value; final String value;
@@ -23,24 +23,24 @@ class PluginContext {
String toJson() => value; String toJson() => value;
static const asset = PluginContext._(r'asset'); static const asset = PluginContextType._(r'asset');
static const album = PluginContext._(r'album'); static const album = PluginContextType._(r'album');
static const person = PluginContext._(r'person'); static const person = PluginContextType._(r'person');
/// List of all possible values in this [enum][PluginContext]. /// List of all possible values in this [enum][PluginContextType].
static const values = <PluginContext>[ static const values = <PluginContextType>[
asset, asset,
album, album,
person, person,
]; ];
static PluginContext? fromJson(dynamic value) => PluginContextTypeTransformer().decode(value); static PluginContextType? fromJson(dynamic value) => PluginContextTypeTypeTransformer().decode(value);
static List<PluginContext> listFromJson(dynamic json, {bool growable = false,}) { static List<PluginContextType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginContext>[]; final result = <PluginContextType>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = PluginContext.fromJson(row); final value = PluginContextType.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -50,16 +50,16 @@ class PluginContext {
} }
} }
/// Transformation class that can [encode] an instance of [PluginContext] to String, /// Transformation class that can [encode] an instance of [PluginContextType] to String,
/// and [decode] dynamic data back to [PluginContext]. /// and [decode] dynamic data back to [PluginContextType].
class PluginContextTypeTransformer { class PluginContextTypeTypeTransformer {
factory PluginContextTypeTransformer() => _instance ??= const PluginContextTypeTransformer._(); 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, /// 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] /// 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, /// 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. /// 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) { if (data != null) {
switch (data) { switch (data) {
case r'asset': return PluginContext.asset; case r'asset': return PluginContextType.asset;
case r'album': return PluginContext.album; case r'album': return PluginContextType.album;
case r'person': return PluginContext.person; case r'person': return PluginContextType.person;
default: default:
if (!allowNull) { if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data'); throw ArgumentError('Unknown enum value to decode: $data');
@@ -82,7 +82,7 @@ class PluginContextTypeTransformer {
return null; return null;
} }
/// Singleton [PluginContextTypeTransformer] instance. /// Singleton [PluginContextTypeTypeTransformer] instance.
static PluginContextTypeTransformer? _instance; static PluginContextTypeTypeTransformer? _instance;
} }

View File

@@ -32,7 +32,7 @@ class PluginFilterResponseDto {
Object? schema; Object? schema;
List<PluginContext> supportedContexts; List<PluginContextType> supportedContexts;
String title; String title;
@@ -90,7 +90,7 @@ class PluginFilterResponseDto {
methodName: mapValueOfType<String>(json, r'methodName')!, methodName: mapValueOfType<String>(json, r'methodName')!,
pluginId: mapValueOfType<String>(json, r'pluginId')!, pluginId: mapValueOfType<String>(json, r'pluginId')!,
schema: mapValueOfType<Object>(json, r'schema'), schema: mapValueOfType<Object>(json, r'schema'),
supportedContexts: PluginContext.listFromJson(json[r'supportedContexts']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']),
title: mapValueOfType<String>(json, r'title')!, title: mapValueOfType<String>(json, r'title')!,
); );
} }

View File

@@ -13,44 +13,44 @@ part of openapi.api;
class PluginTriggerResponseDto { class PluginTriggerResponseDto {
/// Returns a new [PluginTriggerResponseDto] instance. /// Returns a new [PluginTriggerResponseDto] instance.
PluginTriggerResponseDto({ PluginTriggerResponseDto({
required this.context, required this.contextType,
required this.description, required this.description,
required this.name, required this.name,
required this.triggerType, required this.type,
}); });
PluginContext context; PluginContextType contextType;
String description; String description;
String name; String name;
PluginTriggerType triggerType; PluginTriggerType type;
@override @override
bool operator ==(Object other) => identical(this, other) || other is PluginTriggerResponseDto && bool operator ==(Object other) => identical(this, other) || other is PluginTriggerResponseDto &&
other.context == context && other.contextType == contextType &&
other.description == description && other.description == description &&
other.name == name && other.name == name &&
other.triggerType == triggerType; other.type == type;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(context.hashCode) + (contextType.hashCode) +
(description.hashCode) + (description.hashCode) +
(name.hashCode) + (name.hashCode) +
(triggerType.hashCode); (type.hashCode);
@override @override
String toString() => 'PluginTriggerResponseDto[context=$context, description=$description, name=$name, triggerType=$triggerType]'; String toString() => 'PluginTriggerResponseDto[contextType=$contextType, description=$description, name=$name, type=$type]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'context'] = this.context; json[r'contextType'] = this.contextType;
json[r'description'] = this.description; json[r'description'] = this.description;
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'triggerType'] = this.triggerType; json[r'type'] = this.type;
return json; return json;
} }
@@ -63,10 +63,10 @@ class PluginTriggerResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return PluginTriggerResponseDto( return PluginTriggerResponseDto(
context: PluginContext.fromJson(json[r'context'])!, contextType: PluginContextType.fromJson(json[r'contextType'])!,
description: mapValueOfType<String>(json, r'description')!, description: mapValueOfType<String>(json, r'description')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
triggerType: PluginTriggerType.fromJson(json[r'triggerType'])!, type: PluginTriggerType.fromJson(json[r'type'])!,
); );
} }
return null; return null;
@@ -114,10 +114,10 @@ class PluginTriggerResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'context', 'contextType',
'description', 'description',
'name', 'name',
'triggerType', 'type',
}; };
} }

View File

@@ -8023,7 +8023,7 @@
"/plugins/triggers": { "/plugins/triggers": {
"get": { "get": {
"description": "Retrieve a list of all available plugin triggers.", "description": "Retrieve a list of all available plugin triggers.",
"operationId": "getTriggers", "operationId": "getPluginTriggers",
"parameters": [], "parameters": [],
"responses": { "responses": {
"200": { "200": {
@@ -18331,7 +18331,7 @@
}, },
"supportedContexts": { "supportedContexts": {
"items": { "items": {
"$ref": "#/components/schemas/PluginContext" "$ref": "#/components/schemas/PluginContextType"
}, },
"type": "array" "type": "array"
}, },
@@ -18350,7 +18350,7 @@
], ],
"type": "object" "type": "object"
}, },
"PluginContext": { "PluginContextType": {
"enum": [ "enum": [
"asset", "asset",
"album", "album",
@@ -18378,7 +18378,7 @@
}, },
"supportedContexts": { "supportedContexts": {
"items": { "items": {
"$ref": "#/components/schemas/PluginContext" "$ref": "#/components/schemas/PluginContextType"
}, },
"type": "array" "type": "array"
}, },
@@ -18452,10 +18452,10 @@
}, },
"PluginTriggerResponseDto": { "PluginTriggerResponseDto": {
"properties": { "properties": {
"context": { "contextType": {
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/PluginContext" "$ref": "#/components/schemas/PluginContextType"
} }
] ]
}, },
@@ -18465,7 +18465,7 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"triggerType": { "type": {
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/PluginTriggerType" "$ref": "#/components/schemas/PluginTriggerType"
@@ -18474,10 +18474,10 @@
} }
}, },
"required": [ "required": [
"context", "contextType",
"description", "description",
"name", "name",
"triggerType" "type"
], ],
"type": "object" "type": "object"
}, },

View File

@@ -942,7 +942,7 @@ export type PluginActionResponseDto = {
methodName: string; methodName: string;
pluginId: string; pluginId: string;
schema: object | null; schema: object | null;
supportedContexts: PluginContext[]; supportedContexts: PluginContextType[];
title: string; title: string;
}; };
export type PluginFilterResponseDto = { export type PluginFilterResponseDto = {
@@ -951,7 +951,7 @@ export type PluginFilterResponseDto = {
methodName: string; methodName: string;
pluginId: string; pluginId: string;
schema: object | null; schema: object | null;
supportedContexts: PluginContext[]; supportedContexts: PluginContextType[];
title: string; title: string;
}; };
export type PluginResponseDto = { export type PluginResponseDto = {
@@ -967,10 +967,10 @@ export type PluginResponseDto = {
version: string; version: string;
}; };
export type PluginTriggerResponseDto = { export type PluginTriggerResponseDto = {
context: PluginContext; contextType: PluginContextType;
description: string; description: string;
name: string; name: string;
triggerType: PluginTriggerType; "type": PluginTriggerType;
}; };
export type QueueResponseDto = { export type QueueResponseDto = {
isPaused: boolean; isPaused: boolean;
@@ -3666,7 +3666,7 @@ export function getPlugins(opts?: Oazapfts.RequestOpts) {
/** /**
* List all plugin triggers * List all plugin triggers
*/ */
export function getTriggers(opts?: Oazapfts.RequestOpts) { export function getPluginTriggers(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: PluginTriggerResponseDto[]; data: PluginTriggerResponseDto[];
@@ -5436,7 +5436,7 @@ export enum PartnerDirection {
SharedBy = "shared-by", SharedBy = "shared-by",
SharedWith = "shared-with" SharedWith = "shared-with"
} }
export enum PluginContext { export enum PluginContextType {
Asset = "asset", Asset = "asset",
Album = "album", Album = "album",
Person = "person" Person = "person"

View File

@@ -19,7 +19,7 @@ export class PluginController {
description: 'Retrieve a list of all available plugin triggers.', description: 'Retrieve a list of all available plugin triggers.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'), history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
}) })
getTriggers(): PluginTriggerResponseDto[] { getPluginTriggers(): PluginTriggerResponseDto[] {
return this.service.getTriggers(); return this.service.getTriggers();
} }

View File

@@ -1,16 +1,16 @@
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
import { PluginAction, PluginFilter } from 'src/database'; 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 type { JSONSchema } from 'src/types/plugin-schema.types';
import { ValidateEnum } from 'src/validation'; import { ValidateEnum } from 'src/validation';
export class PluginTriggerResponseDto { export class PluginTriggerResponseDto {
name!: string; name!: string;
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' }) @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' })
triggerType!: PluginTriggerType; type!: PluginTriggerType;
description!: string; description!: string;
@ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' })
context!: PluginContext; contextType!: PluginContextType;
} }
export class PluginResponseDto { export class PluginResponseDto {
@@ -33,8 +33,8 @@ export class PluginFilterResponseDto {
title!: string; title!: string;
description!: string; description!: string;
@ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' })
supportedContexts!: PluginContext[]; supportedContexts!: PluginContextType[];
schema!: JSONSchema | null; schema!: JSONSchema | null;
} }
@@ -45,8 +45,8 @@ export class PluginActionResponseDto {
title!: string; title!: string;
description!: string; description!: string;
@ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' })
supportedContexts!: PluginContext[]; supportedContexts!: PluginContextType[];
schema!: JSONSchema | null; schema!: JSONSchema | null;
} }

View File

@@ -2,22 +2,22 @@ import { PluginContext, PluginTriggerType } from 'src/enum';
export type PluginTrigger = { export type PluginTrigger = {
name: string; name: string;
triggerType: PluginTriggerType; type: PluginTriggerType;
description: string; description: string;
context: PluginContext; contextType: PluginContext;
}; };
export const pluginTriggers: PluginTrigger[] = [ export const pluginTriggers: PluginTrigger[] = [
{ {
name: 'Asset Uploaded', name: 'Asset Uploaded',
triggerType: PluginTriggerType.AssetCreate, type: PluginTriggerType.AssetCreate,
description: 'Triggered when a new asset is uploaded', description: 'Triggered when a new asset is uploaded',
context: PluginContext.Asset, contextType: PluginContext.Asset,
}, },
{ {
name: 'Person Recognized', name: 'Person Recognized',
triggerType: PluginTriggerType.PersonRecognized, type: PluginTriggerType.PersonRecognized,
description: 'Triggered when a person is detected', description: 'Triggered when a person is detected',
context: PluginContext.Person, contextType: PluginContext.Person,
}, },
]; ];

View File

@@ -7,6 +7,8 @@ from
"workflow" "workflow"
where where
"id" = $1 "id" = $1
order by
"createdAt" desc
-- WorkflowRepository.getWorkflowsByOwner -- WorkflowRepository.getWorkflowsByOwner
select select
@@ -16,7 +18,7 @@ from
where where
"ownerId" = $1 "ownerId" = $1
order by order by
"name" "createdAt" desc
-- WorkflowRepository.getWorkflowsByTrigger -- WorkflowRepository.getWorkflowsByTrigger
select select

View File

@@ -12,12 +12,22 @@ export class WorkflowRepository {
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
getWorkflow(id: string) { 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] }) @GenerateSql({ params: [DummyValue.UUID] })
getWorkflowsByOwner(ownerId: string) { 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] }) @GenerateSql({ params: [PluginTriggerType.AssetCreate] })

View File

@@ -116,12 +116,12 @@ export class PluginService extends BaseService {
} }
private async loadPluginToDatabase(manifest: PluginManifestDto, basePath: string): Promise<void> { private async loadPluginToDatabase(manifest: PluginManifestDto, basePath: string): Promise<void> {
// const currentPlugin = await this.pluginRepository.getPluginByName(manifest.name); const currentPlugin = await this.pluginRepository.getPluginByName(manifest.name);
// if (currentPlugin != null && currentPlugin.version === manifest.version) { if (currentPlugin != null && currentPlugin.version === manifest.version) {
// this.logger.log(`Plugin ${manifest.name} is up to date (version ${manifest.version}). Skipping`); this.logger.log(`Plugin ${manifest.name} is up to date (version ${manifest.version}). Skipping`);
// return; return;
// } }
//
const { plugin, filters, actions } = await this.pluginRepository.loadPlugin(manifest, basePath); const { plugin, filters, actions } = await this.pluginRepository.loadPlugin(manifest, basePath);
this.logger.log(`Upserted plugin: ${plugin.name} (ID: ${plugin.id}, version: ${plugin.version})`); this.logger.log(`Upserted plugin: ${plugin.name} (ID: ${plugin.id}, version: ${plugin.version})`);

View File

@@ -16,10 +16,10 @@ import { BaseService } from 'src/services/base.service';
@Injectable() @Injectable()
export class WorkflowService extends BaseService { export class WorkflowService extends BaseService {
async create(auth: AuthDto, dto: WorkflowCreateDto): Promise<WorkflowResponseDto> { async create(auth: AuthDto, dto: WorkflowCreateDto): Promise<WorkflowResponseDto> {
const trigger = this.getTriggerOrFail(dto.triggerType); const context = this.getContextForTrigger(dto.triggerType);
const filterInserts = await this.validateAndMapFilters(dto.filters, trigger.context); const filterInserts = await this.validateAndMapFilters(dto.filters, context);
const actionInserts = await this.validateAndMapActions(dto.actions, trigger.context); const actionInserts = await this.validateAndMapActions(dto.actions, context);
const workflow = await this.workflowRepository.createWorkflow( const workflow = await this.workflowRepository.createWorkflow(
{ {
@@ -56,11 +56,11 @@ export class WorkflowService extends BaseService {
} }
const workflow = await this.findOrFail(id); 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 { filters, actions, ...workflowUpdate } = dto;
const filterInserts = filters && (await this.validateAndMapFilters(filters, trigger.context)); const filterInserts = filters && (await this.validateAndMapFilters(filters, context));
const actionInserts = actions && (await this.validateAndMapActions(actions, trigger.context)); const actionInserts = actions && (await this.validateAndMapActions(actions, context));
const updatedWorkflow = await this.workflowRepository.updateWorkflow( const updatedWorkflow = await this.workflowRepository.updateWorkflow(
id, id,
@@ -124,12 +124,12 @@ export class WorkflowService extends BaseService {
})); }));
} }
private getTriggerOrFail(triggerType: PluginTriggerType) { private getContextForTrigger(type: PluginTriggerType) {
const trigger = pluginTriggers.find((t) => t.triggerType === triggerType); const trigger = pluginTriggers.find((t) => t.type === type);
if (!trigger) { 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) { private async findOrFail(id: string) {

View File

@@ -2,7 +2,7 @@
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte'; import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte'; import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils'; 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 { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
import { Button, Field, Input, MultiSelect, Select, Switch, Text, modalManager, type SelectItem } from '@immich/ui'; import { Button, Field, Input, MultiSelect, Select, Switch, Text, modalManager, type SelectItem } from '@immich/ui';
import { mdiPlus } from '@mdi/js'; import { mdiPlus } from '@mdi/js';
@@ -43,56 +43,7 @@
return; return;
} }
const fetchMetadata = async () => { void fetchMetadata(components);
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();
}); });
$effect(() => { $effect(() => {
@@ -148,6 +99,55 @@
} }
}); });
const fetchMetadata = async (components: Record<string, ComponentConfig>) => {
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 handleAlbumPicker = async (key: string, multiple: boolean) => {
const albums = await modalManager.show(AlbumPickerModal, { shared: false }); const albums = await modalManager.show(AlbumPickerModal, { shared: false });
if (albums && albums.length > 0) { if (albums && albums.length > 0) {
@@ -161,7 +161,9 @@
}; };
const handlePeoplePicker = async (key: string, multiple: boolean) => { 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) { if (people && people.length > 0) {
const value = multiple ? people.map((p) => p.id) : people[0].id; const value = multiple ? people.map((p) => p.id) : people[0].id;
updateConfig(key, value); updateConfig(key, value);

View File

@@ -63,7 +63,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
bind:this={containerEl} bind:this={containerEl}
class="hidden sm:block fixed w-64 z-50 hover:cursor-grab select-none" class="hidden sm:block fixed w-64 hover:cursor-grab select-none"
style="left: {position.x}px; top: {position.y}px;" style="left: {position.x}px; top: {position.y}px;"
class:cursor-grabbing={isDragging} class:cursor-grabbing={isDragging}
onmousedown={handleMouseDown} onmousedown={handleMouseDown}
@@ -112,7 +112,7 @@
<span class="text-[10px] font-semibold uppercase tracking-wide">{$t('filters')}</span> <span class="text-[10px] font-semibold uppercase tracking-wide">{$t('filters')}</span>
</div> </div>
<div class="space-y-1 pl-5"> <div class="space-y-1 pl-5">
{#each filters as filter, index (filter.id)} {#each filters as filter, index (index)}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <span
class="shrink-0 h-4 w-4 rounded-full bg-light-200 text-[10px] font-medium flex items-center justify-center" class="shrink-0 h-4 w-4 rounded-full bg-light-200 text-[10px] font-medium flex items-center justify-center"
@@ -138,7 +138,7 @@
<span class="text-[10px] font-semibold uppercase tracking-wide">{$t('actions')}</span> <span class="text-[10px] font-semibold uppercase tracking-wide">{$t('actions')}</span>
</div> </div>
<div class="space-y-1 pl-5"> <div class="space-y-1 pl-5">
{#each actions as action, index (action.id)} {#each actions as action, index (index)}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <span
class="shrink-0 h-4 w-4 rounded-full bg-light-200 text-[10px] font-medium flex items-center justify-center" class="shrink-0 h-4 w-4 rounded-full bg-light-200 text-[10px] font-medium flex items-center justify-center"
@@ -156,7 +156,7 @@
{:else} {:else}
<button <button
type="button" type="button"
class="hidden sm:flex fixed right-6 bottom-6 z-50 h-14 w-14 items-center justify-center rounded-full bg-primary text-light shadow-lg hover:bg-primary/90 transition-colors" class="hidden sm:flex fixed right-6 bottom-6 h-14 w-14 items-center justify-center rounded-full bg-primary text-light shadow-lg hover:bg-primary/90 transition-colors"
title={$t('workflow_summary')} title={$t('workflow_summary')}
onclick={() => (isOpen = true)} onclick={() => (isOpen = true)}
> >

View File

@@ -39,12 +39,12 @@
? 'bg-primary text-light' ? 'bg-primary text-light'
: 'text-light-100 bg-light-300 group-hover:bg-light-500'}" : 'text-light-100 bg-light-300 group-hover:bg-light-500'}"
> >
<Icon icon={getTriggerIcon(trigger.triggerType)} size="24" /> <Icon icon={getTriggerIcon(trigger.type)} size="24" />
</div> </div>
<div class="flex-1"> <div class="flex-1">
<Text class="font-semibold mb-1">{trigger.name}</Text> <Text class="font-semibold mb-1">{trigger.name}</Text>
{#if trigger.description} {#if trigger.description}
<Text class="text-sm">{trigger.description}</Text> <Text size="small">{trigger.description}</Text>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -56,7 +56,6 @@ export enum AppRoute {
LARGE_FILES = '/utilities/large-files', LARGE_FILES = '/utilities/large-files',
GEOLOCATION = '/utilities/geolocation', GEOLOCATION = '/utilities/geolocation',
WORKFLOWS = '/utilities/workflows', WORKFLOWS = '/utilities/workflows',
WORKFLOWS_EDIT = '/utilities/workflows/edit',
FOLDERS = '/folders', FOLDERS = '/folders',
TAGS = '/tags', TAGS = '/tags',

View File

@@ -1,23 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { PluginActionResponseDto, PluginFilterResponseDto } from '@immich/sdk'; import type { PluginActionResponseDto, PluginFilterResponseDto } from '@immich/sdk';
import { Icon, Modal, ModalBody } from '@immich/ui'; import { Icon, Modal, ModalBody, Text } from '@immich/ui';
import { mdiFilterOutline, mdiPlayCircleOutline } from '@mdi/js'; import { mdiFilterOutline, mdiPlayCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
filters: PluginFilterResponseDto[]; filters: PluginFilterResponseDto[];
actions: PluginActionResponseDto[]; actions: PluginActionResponseDto[];
addedFilters?: PluginFilterResponseDto[];
addedActions?: PluginActionResponseDto[];
onClose: (result?: { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto }) => void; onClose: (result?: { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto }) => void;
type?: 'filter' | 'action'; type?: 'filter' | 'action';
} }
let { filters, actions, addedFilters = [], addedActions = [], onClose, type }: Props = $props(); let { filters, actions, onClose, type }: Props = $props();
// Filter out already-added items
const availableFilters = $derived(filters.filter((f) => !addedFilters.some((af) => af.id === f.id)));
const availableActions = $derived(actions.filter((a) => !addedActions.some((aa) => aa.id === a.id)));
type StepType = 'filter' | 'action'; type StepType = 'filter' | 'action';
@@ -30,7 +24,7 @@
<ModalBody> <ModalBody>
<div class="space-y-6"> <div class="space-y-6">
<!-- Filters Section --> <!-- Filters Section -->
{#if availableFilters.length > 0 && (!type || type === 'filter')} {#if filters.length > 0 && (!type || type === 'filter')}
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
<div class="h-6 w-6 rounded-md bg-warning-100 flex items-center justify-center"> <div class="h-6 w-6 rounded-md bg-warning-100 flex items-center justify-center">
<Icon icon={mdiFilterOutline} size="16" class="text-warning" /> <Icon icon={mdiFilterOutline} size="16" class="text-warning" />
@@ -38,7 +32,7 @@
<h3 class="text-sm font-semibold">Filters</h3> <h3 class="text-sm font-semibold">Filters</h3>
</div> </div>
<div class="grid grid-cols-1 gap-2"> <div class="grid grid-cols-1 gap-2">
{#each availableFilters as filter (filter.id)} {#each filters as filter (filter.id)}
<button <button
type="button" type="button"
onclick={() => handleSelect('filter', filter)} onclick={() => handleSelect('filter', filter)}
@@ -56,7 +50,7 @@
{/if} {/if}
<!-- Actions Section --> <!-- Actions Section -->
{#if availableActions.length > 0 && (!type || type === 'action')} {#if actions.length > 0 && (!type || type === 'action')}
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
<div class="h-6 w-6 rounded-md bg-success-50 flex items-center justify-center"> <div class="h-6 w-6 rounded-md bg-success-50 flex items-center justify-center">
@@ -65,7 +59,7 @@
<h3 class="text-sm font-semibold">Actions</h3> <h3 class="text-sm font-semibold">Actions</h3>
</div> </div>
<div class="grid grid-cols-1 gap-2"> <div class="grid grid-cols-1 gap-2">
{#each availableActions as action (action.id)} {#each actions as action (action.id)}
<button <button
type="button" type="button"
onclick={() => handleSelect('action', action)} onclick={() => handleSelect('action', action)}
@@ -74,7 +68,7 @@
<div class="flex-1"> <div class="flex-1">
<p class="font-medium text-sm">{action.title}</p> <p class="font-medium text-sm">{action.title}</p>
{#if action.description} {#if action.description}
<p class="text-xs text-light-500 mt-1">{action.description}</p> <Text size="small" class="text-light-500 mt-1">{action.description}</Text>
{/if} {/if}
</div> </div>
</button> </button>

View File

@@ -2,6 +2,7 @@
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import SearchBar from '$lib/elements/SearchBar.svelte'; import SearchBar from '$lib/elements/SearchBar.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { getAllPeople, type PersonResponseDto } from '@immich/sdk'; import { getAllPeople, type PersonResponseDto } from '@immich/sdk';
import { Button, HStack, LoadingSpinner, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { Button, HStack, LoadingSpinner, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -9,10 +10,11 @@
interface Props { interface Props {
multiple?: boolean; multiple?: boolean;
excludedIds?: string[];
onClose: (people?: PersonResponseDto[]) => void; onClose: (people?: PersonResponseDto[]) => void;
} }
let { multiple = false, onClose }: Props = $props(); let { multiple = false, excludedIds = [], onClose }: Props = $props();
let people: PersonResponseDto[] = $state([]); let people: PersonResponseDto[] = $state([]);
let loading = $state(true); let loading = $state(true);
@@ -20,13 +22,20 @@
let selectedPeople: PersonResponseDto[] = $state([]); let selectedPeople: PersonResponseDto[] = $state([]);
const filteredPeople = $derived( const filteredPeople = $derived(
searchName ? people.filter((person) => person.name.toLowerCase().includes(searchName.toLowerCase())) : people, people
.filter((person) => !excludedIds.includes(person.id))
.filter((person) => !searchName || person.name.toLowerCase().includes(searchName.toLowerCase())),
); );
onMount(async () => { onMount(async () => {
const result = await getAllPeople({ withHidden: false }); try {
people = result.people; loading = true;
loading = false; const result = await getAllPeople({ withHidden: false });
people = result.people;
loading = false;
} catch (error) {
handleError(error, $t('get_people_error'));
}
}); });
const togglePerson = (person: PersonResponseDto) => { const togglePerson = (person: PersonResponseDto) => {
@@ -86,11 +95,11 @@
</div> </div>
</ModalBody> </ModalBody>
{#if multiple && selectedPeople.length > 0} {#if multiple}
<ModalFooter> <ModalFooter>
<HStack fullWidth gap={4}> <HStack fullWidth gap={4}>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button> <Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" fullWidth onclick={handleSubmit}> <Button shape="round" fullWidth onclick={handleSubmit} disabled={selectedPeople.length === 0}>
{$t('select_count', { values: { count: selectedPeople.length } })} {$t('select_count', { values: { count: selectedPeople.length } })}
</Button> </Button>
</HStack> </HStack>

View File

@@ -2,9 +2,11 @@ import {
PluginTriggerType, PluginTriggerType,
updateWorkflow as updateWorkflowApi, updateWorkflow as updateWorkflowApi,
type PluginActionResponseDto, type PluginActionResponseDto,
type PluginContext, type PluginContextType,
type PluginFilterResponseDto, type PluginFilterResponseDto,
type PluginTriggerResponseDto, type PluginTriggerResponseDto,
type WorkflowActionItemDto,
type WorkflowFilterItemDto,
type WorkflowResponseDto, type WorkflowResponseDto,
type WorkflowUpdateDto, type WorkflowUpdateDto,
} from '@immich/sdk'; } from '@immich/sdk';
@@ -18,316 +20,280 @@ export interface WorkflowPayload {
actions: Record<string, unknown>[]; actions: Record<string, unknown>[];
} }
export class WorkflowService { /**
private availableTriggers: PluginTriggerResponseDto[]; * Get filters that support the given context
private availableFilters: PluginFilterResponseDto[]; */
private availableActions: PluginActionResponseDto[]; export const getFiltersByContext = (
availableFilters: PluginFilterResponseDto[],
context: PluginContextType,
): PluginFilterResponseDto[] => {
return availableFilters.filter((filter) => filter.supportedContexts.includes(context));
};
constructor( /**
triggers: PluginTriggerResponseDto[], * Get actions that support the given context
filters: PluginFilterResponseDto[], */
actions: PluginActionResponseDto[], export const getActionsByContext = (
) { availableActions: PluginActionResponseDto[],
this.availableTriggers = triggers; context: PluginContextType,
this.availableFilters = filters; ): PluginActionResponseDto[] => {
this.availableActions = actions; return availableActions.filter((action) => action.supportedContexts.includes(context));
};
/**
* Initialize filter configurations from existing workflow
*/
export const initializeFilterConfigs = (
workflow: WorkflowResponseDto,
availableFilters: PluginFilterResponseDto[],
): Record<string, unknown> => {
const configs: Record<string, unknown> = {};
if (workflow.filters) {
for (const workflowFilter of workflow.filters) {
const filterDef = availableFilters.find((f) => f.id === workflowFilter.pluginFilterId);
if (filterDef) {
configs[filterDef.methodName] = workflowFilter.filterConfig ?? {};
}
}
} }
/** return configs;
* Get filters that support the given context };
*/
getFiltersByContext(context: PluginContext): PluginFilterResponseDto[] { /**
return this.availableFilters.filter((filter) => filter.supportedContexts.includes(context)); * Initialize action configurations from existing workflow
*/
export const initializeActionConfigs = (
workflow: WorkflowResponseDto,
availableActions: PluginActionResponseDto[],
): Record<string, unknown> => {
const configs: Record<string, unknown> = {};
if (workflow.actions) {
for (const workflowAction of workflow.actions) {
const actionDef = availableActions.find((a) => a.id === workflowAction.pluginActionId);
if (actionDef) {
configs[actionDef.methodName] = workflowAction.actionConfig ?? {};
}
}
} }
/** return configs;
* Get actions that support the given context };
*/
getActionsByContext(context: PluginContext): PluginActionResponseDto[] {
return this.availableActions.filter((action) => action.supportedContexts.includes(context));
}
/** /**
* Initialize filter configurations from existing workflow * Build workflow payload from current state
*/ */
initializeFilterConfigs( export const buildWorkflowPayload = (
workflow: WorkflowResponseDto, name: string,
contextFilters?: PluginFilterResponseDto[], description: string,
): Record<string, unknown> { enabled: boolean,
const filters = contextFilters ?? this.availableFilters; triggerType: string,
const configs: Record<string, unknown> = {}; orderedFilters: PluginFilterResponseDto[],
orderedActions: PluginActionResponseDto[],
filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>,
): WorkflowPayload => {
const filters = orderedFilters.map((filter) => ({
[filter.methodName]: filterConfigs[filter.methodName] ?? {},
}));
if (workflow.filters) { const actions = orderedActions.map((action) => ({
for (const workflowFilter of workflow.filters) { [action.methodName]: actionConfigs[action.methodName] ?? {},
const filterDef = filters.find((f) => f.id === workflowFilter.filterId); }));
if (filterDef) {
configs[filterDef.methodName] = workflowFilter.filterConfig ?? {}; return {
name,
description,
enabled,
triggerType,
filters,
actions,
};
};
/**
* Parse JSON workflow and update state
*/
export const parseWorkflowJson = (
jsonString: string,
availableTriggers: PluginTriggerResponseDto[],
availableFilters: PluginFilterResponseDto[],
availableActions: PluginActionResponseDto[],
): {
success: boolean;
error?: string;
data?: {
name: string;
description: string;
enabled: boolean;
trigger?: PluginTriggerResponseDto;
filters: PluginFilterResponseDto[];
actions: PluginActionResponseDto[];
filterConfigs: Record<string, unknown>;
actionConfigs: Record<string, unknown>;
};
} => {
try {
const parsed = JSON.parse(jsonString);
// Find trigger
const trigger = availableTriggers.find((t) => t.type === parsed.triggerType);
// Parse filters
const filters: PluginFilterResponseDto[] = [];
const filterConfigs: Record<string, unknown> = {};
if (Array.isArray(parsed.filters)) {
for (const filterObj of parsed.filters) {
const methodName = Object.keys(filterObj)[0];
const filter = availableFilters.find((f) => f.methodName === methodName);
if (filter) {
filters.push(filter);
filterConfigs[methodName] = (filterObj as Record<string, unknown>)[methodName];
} }
} }
} }
return configs; // Parse actions
} const actions: PluginActionResponseDto[] = [];
const actionConfigs: Record<string, unknown> = {};
/** if (Array.isArray(parsed.actions)) {
* Initialize action configurations from existing workflow for (const actionObj of parsed.actions) {
*/ const methodName = Object.keys(actionObj)[0];
initializeActionConfigs( const action = availableActions.find((a) => a.methodName === methodName);
workflow: WorkflowResponseDto, if (action) {
contextActions?: PluginActionResponseDto[], actions.push(action);
): Record<string, unknown> { actionConfigs[methodName] = (actionObj as Record<string, unknown>)[methodName];
const actions = contextActions ?? this.availableActions;
const configs: Record<string, unknown> = {};
if (workflow.actions) {
for (const workflowAction of workflow.actions) {
const actionDef = actions.find((a) => a.id === workflowAction.actionId);
if (actionDef) {
configs[actionDef.methodName] = workflowAction.actionConfig ?? {};
} }
} }
} }
return configs;
}
/**
* Initialize ordered filters from existing workflow
*/
initializeOrderedFilters(
workflow: WorkflowResponseDto,
contextFilters?: PluginFilterResponseDto[],
): PluginFilterResponseDto[] {
if (!workflow.filters) {
return [];
}
const filters = contextFilters ?? this.availableFilters;
return workflow.filters
.map((wf) => filters.find((f) => f.id === wf.filterId))
.filter(Boolean) as PluginFilterResponseDto[];
}
/**
* Initialize ordered actions from existing workflow
*/
initializeOrderedActions(
workflow: WorkflowResponseDto,
contextActions?: PluginActionResponseDto[],
): PluginActionResponseDto[] {
if (!workflow.actions) {
return [];
}
const actions = contextActions ?? this.availableActions;
return workflow.actions
.map((wa) => actions.find((a) => a.id === wa.actionId))
.filter(Boolean) as PluginActionResponseDto[];
}
/**
* Build workflow payload from current state
*/
buildWorkflowPayload(
name: string,
description: string,
enabled: boolean,
triggerType: string,
orderedFilters: PluginFilterResponseDto[],
orderedActions: PluginActionResponseDto[],
filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>,
): WorkflowPayload {
const filters = orderedFilters.map((filter) => ({
[filter.methodName]: filterConfigs[filter.methodName] ?? {},
}));
const actions = orderedActions.map((action) => ({
[action.methodName]: actionConfigs[action.methodName] ?? {},
}));
return { return {
name, success: true,
description, data: {
enabled, name: parsed.name ?? '',
triggerType, description: parsed.description ?? '',
filters, enabled: parsed.enabled ?? false,
actions, trigger,
filters,
actions,
filterConfigs,
actionConfigs,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Invalid JSON',
}; };
} }
};
/** /**
* Parse JSON workflow and update state * Check if workflow has changes compared to previous version
*/ */
parseWorkflowJson(jsonString: string): { export const hasWorkflowChanged = (
success: boolean; previousWorkflow: WorkflowResponseDto,
error?: string; enabled: boolean,
data?: { name: string,
name: string; description: string,
description: string; triggerType: string,
enabled: boolean; orderedFilters: PluginFilterResponseDto[],
trigger?: PluginTriggerResponseDto; orderedActions: PluginActionResponseDto[],
filters: PluginFilterResponseDto[]; filterConfigs: Record<string, unknown>,
actions: PluginActionResponseDto[]; actionConfigs: Record<string, unknown>,
filterConfigs: Record<string, unknown>; availableFilters: PluginFilterResponseDto[],
actionConfigs: Record<string, unknown>; availableActions: PluginActionResponseDto[],
}; ): boolean => {
} { // Check enabled state
try { if (enabled !== previousWorkflow.enabled) {
const parsed = JSON.parse(jsonString); return true;
// Find trigger
const trigger = this.availableTriggers.find((t) => t.triggerType === parsed.triggerType);
// Parse filters
const filters: PluginFilterResponseDto[] = [];
const filterConfigs: Record<string, unknown> = {};
if (Array.isArray(parsed.filters)) {
for (const filterObj of parsed.filters) {
const methodName = Object.keys(filterObj)[0];
const filter = this.availableFilters.find((f) => f.methodName === methodName);
if (filter) {
filters.push(filter);
filterConfigs[methodName] = (filterObj as Record<string, unknown>)[methodName];
}
}
}
// Parse actions
const actions: PluginActionResponseDto[] = [];
const actionConfigs: Record<string, unknown> = {};
if (Array.isArray(parsed.actions)) {
for (const actionObj of parsed.actions) {
const methodName = Object.keys(actionObj)[0];
const action = this.availableActions.find((a) => a.methodName === methodName);
if (action) {
actions.push(action);
actionConfigs[methodName] = (actionObj as Record<string, unknown>)[methodName];
}
}
}
return {
success: true,
data: {
name: parsed.name ?? '',
description: parsed.description ?? '',
enabled: parsed.enabled ?? false,
trigger,
filters,
actions,
filterConfigs,
actionConfigs,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Invalid JSON',
};
}
} }
/** // Check name or description
* Check if workflow has changes compared to previous version if (name !== (previousWorkflow.name ?? '') || description !== (previousWorkflow.description ?? '')) {
*/ return true;
hasWorkflowChanged(
previousWorkflow: WorkflowResponseDto,
enabled: boolean,
name: string,
description: string,
triggerType: string,
orderedFilters: PluginFilterResponseDto[],
orderedActions: PluginActionResponseDto[],
filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>,
): boolean {
// Check enabled state
if (enabled !== previousWorkflow.enabled) {
return true;
}
// Check name or description
if (name !== (previousWorkflow.name ?? '') || description !== (previousWorkflow.description ?? '')) {
return true;
}
// Check trigger
if (triggerType !== previousWorkflow.triggerType) {
return true;
}
// Check filters order/items
const previousFilterIds = previousWorkflow.filters?.map((f) => f.filterId) ?? [];
const currentFilterIds = orderedFilters.map((f) => f.id);
if (JSON.stringify(previousFilterIds) !== JSON.stringify(currentFilterIds)) {
return true;
}
// Check actions order/items
const previousActionIds = previousWorkflow.actions?.map((a) => a.actionId) ?? [];
const currentActionIds = orderedActions.map((a) => a.id);
if (JSON.stringify(previousActionIds) !== JSON.stringify(currentActionIds)) {
return true;
}
// Check filter configs
const previousFilterConfigs: Record<string, unknown> = {};
for (const wf of previousWorkflow.filters ?? []) {
const filterDef = this.availableFilters.find((f) => f.id === wf.filterId);
if (filterDef) {
previousFilterConfigs[filterDef.methodName] = wf.filterConfig ?? {};
}
}
if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) {
return true;
}
// Check action configs
const previousActionConfigs: Record<string, unknown> = {};
for (const wa of previousWorkflow.actions ?? []) {
const actionDef = this.availableActions.find((a) => a.id === wa.actionId);
if (actionDef) {
previousActionConfigs[actionDef.methodName] = wa.actionConfig ?? {};
}
}
if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) {
return true;
}
return false;
} }
async updateWorkflow( // Check trigger
workflowId: string, if (triggerType !== previousWorkflow.triggerType) {
name: string, return true;
description: string,
enabled: boolean,
triggerType: PluginTriggerType,
orderedFilters: PluginFilterResponseDto[],
orderedActions: PluginActionResponseDto[],
filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>,
): Promise<WorkflowResponseDto> {
const filters = orderedFilters.map((filter) => ({
filterId: filter.id,
filterConfig: filterConfigs[filter.methodName] ?? {},
}));
const actions = orderedActions.map((action) => ({
actionId: action.id,
actionConfig: actionConfigs[action.methodName] ?? {},
}));
const updateDto: WorkflowUpdateDto = {
name,
description,
enabled,
filters,
actions,
triggerType,
};
return updateWorkflowApi({ id: workflowId, workflowUpdateDto: updateDto });
} }
}
// Check filters order/items
const previousFilterIds = previousWorkflow.filters?.map((f) => f.pluginFilterId) ?? [];
const currentFilterIds = orderedFilters.map((f) => f.id);
if (JSON.stringify(previousFilterIds) !== JSON.stringify(currentFilterIds)) {
return true;
}
// Check actions order/items
const previousActionIds = previousWorkflow.actions?.map((a) => a.pluginActionId) ?? [];
const currentActionIds = orderedActions.map((a) => a.id);
if (JSON.stringify(previousActionIds) !== JSON.stringify(currentActionIds)) {
return true;
}
// Check filter configs
const previousFilterConfigs: Record<string, unknown> = {};
for (const wf of previousWorkflow.filters ?? []) {
const filterDef = availableFilters.find((f) => f.id === wf.pluginFilterId);
if (filterDef) {
previousFilterConfigs[filterDef.methodName] = wf.filterConfig ?? {};
}
}
if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) {
return true;
}
// Check action configs
const previousActionConfigs: Record<string, unknown> = {};
for (const wa of previousWorkflow.actions ?? []) {
const actionDef = availableActions.find((a) => a.id === wa.pluginActionId);
if (actionDef) {
previousActionConfigs[actionDef.methodName] = wa.actionConfig ?? {};
}
}
if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) {
return true;
}
return false;
};
/**
* Update a workflow via API
*/
export const handleUpdateWorkflow = async (
workflowId: string,
name: string,
description: string,
enabled: boolean,
triggerType: PluginTriggerType,
orderedFilters: PluginFilterResponseDto[],
orderedActions: PluginActionResponseDto[],
filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>,
): Promise<WorkflowResponseDto> => {
const filters = orderedFilters.map((filter) => ({
pluginFilterId: filter.id,
filterConfig: filterConfigs[filter.methodName] ?? {},
})) as WorkflowFilterItemDto[];
const actions = orderedActions.map((action) => ({
pluginActionId: action.id,
actionConfig: actionConfigs[action.methodName] ?? {},
})) as WorkflowActionItemDto[];
const updateDto: WorkflowUpdateDto = {
name,
description,
enabled,
filters,
actions,
triggerType,
};
return updateWorkflowApi({ id: workflowId, workflowUpdateDto: updateDto });
};

View File

@@ -76,15 +76,15 @@
enabled: workflow.enabled, enabled: workflow.enabled,
triggerType: workflow.triggerType, triggerType: workflow.triggerType,
filters: orderedFilters.map((filter) => { filters: orderedFilters.map((filter) => {
const meta = pluginFilterLookup.get(filter.filterId); const meta = pluginFilterLookup.get(filter.pluginFilterId);
const key = meta?.methodName ?? filter.filterId; const key = meta?.methodName ?? filter.pluginFilterId;
return { return {
[key]: filter.filterConfig ?? {}, [key]: filter.filterConfig ?? {},
}; };
}), }),
actions: orderedActions.map((action) => { actions: orderedActions.map((action) => {
const meta = pluginActionLookup.get(action.actionId); const meta = pluginActionLookup.get(action.pluginActionId);
const key = meta?.methodName ?? action.actionId; const key = meta?.methodName ?? action.pluginActionId;
return { return {
[key]: action.actionConfig ?? {}, [key]: action.actionConfig ?? {},
}; };
@@ -123,7 +123,7 @@
}; };
const handleEditWorkflow = async (workflow: WorkflowResponseDto) => { const handleEditWorkflow = async (workflow: WorkflowResponseDto) => {
await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`); await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
}; };
const handleCreateWorkflow = async () => { const handleCreateWorkflow = async () => {
@@ -137,7 +137,7 @@
}, },
}); });
await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`); await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
}; };
const getFilterLabel = (filterId: string) => { const getFilterLabel = (filterId: string) => {
@@ -289,7 +289,7 @@
</span> </span>
{:else} {:else}
{#each workflow.filters as workflowFilter (workflowFilter.id)} {#each workflow.filters as workflowFilter (workflowFilter.id)}
{@render chipItem(getFilterLabel(workflowFilter.filterId))} {@render chipItem(getFilterLabel(workflowFilter.pluginFilterId))}
{/each} {/each}
{/if} {/if}
</div> </div>
@@ -309,7 +309,7 @@
{:else} {:else}
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each workflow.actions as workflowAction (workflowAction.id)} {#each workflow.actions as workflowAction (workflowAction.id)}
{@render chipItem(getActionLabel(workflowAction.actionId))} {@render chipItem(getActionLabel(workflowAction.pluginActionId))}
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@@ -1,7 +1,7 @@
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getPlugins, getWorkflows } from '@immich/sdk'; import { getPlugins, getWorkflows } from '@immich/sdk';
import type { PageLoad } from '../$types'; import type { PageLoad } from './$types';
export const load = (async ({ url }) => { export const load = (async ({ url }) => {
await authenticate(url); await authenticate(url);

View File

@@ -11,7 +11,17 @@
import AddWorkflowStepModal from '$lib/modals/AddWorkflowStepModal.svelte'; import AddWorkflowStepModal from '$lib/modals/AddWorkflowStepModal.svelte';
import WorkflowNavigationConfirmModal from '$lib/modals/WorkflowNavigationConfirmModal.svelte'; import WorkflowNavigationConfirmModal from '$lib/modals/WorkflowNavigationConfirmModal.svelte';
import WorkflowTriggerUpdateConfirmModal from '$lib/modals/WorkflowTriggerUpdateConfirmModal.svelte'; import WorkflowTriggerUpdateConfirmModal from '$lib/modals/WorkflowTriggerUpdateConfirmModal.svelte';
import { WorkflowService, type WorkflowPayload } from '$lib/services/workflow.service'; import {
buildWorkflowPayload,
getActionsByContext,
getFiltersByContext,
handleUpdateWorkflow,
hasWorkflowChanged,
initializeActionConfigs,
initializeFilterConfigs,
parseWorkflowJson,
type WorkflowPayload,
} from '$lib/services/workflow.service';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import type { PluginActionResponseDto, PluginFilterResponseDto, PluginTriggerResponseDto } from '@immich/sdk'; import type { PluginActionResponseDto, PluginFilterResponseDto, PluginTriggerResponseDto } from '@immich/sdk';
import { import {
@@ -57,7 +67,6 @@
const triggers = data.triggers; const triggers = data.triggers;
const filters = data.plugins.flatMap((plugin) => plugin.filters); const filters = data.plugins.flatMap((plugin) => plugin.filters);
const actions = data.plugins.flatMap((plugin) => plugin.actions); const actions = data.plugins.flatMap((plugin) => plugin.actions);
const workflowService = new WorkflowService(triggers, filters, actions);
let previousWorkflow = data.workflow; let previousWorkflow = data.workflow;
let editWorkflow = $state(data.workflow); let editWorkflow = $state(data.workflow);
@@ -67,26 +76,28 @@
let name: string = $derived(editWorkflow.name ?? ''); let name: string = $derived(editWorkflow.name ?? '');
let description: string = $derived(editWorkflow.description ?? ''); let description: string = $derived(editWorkflow.description ?? '');
let selectedTrigger = $state(triggers.find((t) => t.triggerType === editWorkflow.triggerType) ?? triggers[0]); let selectedTrigger = $state(triggers.find((t) => t.type === editWorkflow.triggerType) ?? triggers[0]);
let triggerType = $derived(selectedTrigger.triggerType); let triggerType = $derived(selectedTrigger.type);
let supportFilters = $derived(workflowService.getFiltersByContext(selectedTrigger.context)); let supportFilters = $derived(getFiltersByContext(filters, selectedTrigger.contextType));
let supportActions = $derived(workflowService.getActionsByContext(selectedTrigger.context)); let supportActions = $derived(getActionsByContext(actions, selectedTrigger.contextType));
let orderedFilters: PluginFilterResponseDto[] = $derived( let selectedFilters: PluginFilterResponseDto[] = $derived(
workflowService.initializeOrderedFilters(editWorkflow, supportFilters), (editWorkflow.filters ?? []).flatMap((workflowFilter) =>
supportFilters.filter((supportedFilter) => supportedFilter.id === workflowFilter.pluginFilterId),
),
); );
let orderedActions: PluginActionResponseDto[] = $derived(
workflowService.initializeOrderedActions(editWorkflow, supportActions), let selectedActions: PluginActionResponseDto[] = $derived(
); (editWorkflow.actions ?? []).flatMap((workflowAction) =>
let filterConfigs: Record<string, unknown> = $derived( supportActions.filter((supportedAction) => supportedAction.id === workflowAction.pluginActionId),
workflowService.initializeFilterConfigs(editWorkflow, supportFilters), ),
);
let actionConfigs: Record<string, unknown> = $derived(
workflowService.initializeActionConfigs(editWorkflow, supportActions),
); );
let filterConfigs: Record<string, unknown> = $derived(initializeFilterConfigs(editWorkflow, supportFilters));
let actionConfigs: Record<string, unknown> = $derived(initializeActionConfigs(editWorkflow, supportActions));
$effect(() => { $effect(() => {
editWorkflow.triggerType = triggerType; editWorkflow.triggerType = triggerType;
}); });
@@ -94,10 +105,10 @@
// Clear filters and actions when trigger changes (context changes) // Clear filters and actions when trigger changes (context changes)
let previousContext = $state<string | undefined>(undefined); let previousContext = $state<string | undefined>(undefined);
$effect(() => { $effect(() => {
const currentContext = selectedTrigger.context; const currentContext = selectedTrigger.contextType;
if (previousContext !== undefined && previousContext !== currentContext) { if (previousContext !== undefined && previousContext !== currentContext) {
orderedFilters = []; selectedFilters = [];
orderedActions = []; selectedActions = [];
filterConfigs = {}; filterConfigs = {};
actionConfigs = {}; actionConfigs = {};
} }
@@ -106,14 +117,14 @@
const updateWorkflow = async () => { const updateWorkflow = async () => {
try { try {
const updated = await workflowService.updateWorkflow( const updated = await handleUpdateWorkflow(
editWorkflow.id, editWorkflow.id,
name, name,
description, description,
editWorkflow.enabled, editWorkflow.enabled,
triggerType, triggerType,
orderedFilters, selectedFilters,
orderedActions, selectedActions,
filterConfigs, filterConfigs,
actionConfigs, actionConfigs,
); );
@@ -131,13 +142,13 @@
}; };
const jsonContent = $derived( const jsonContent = $derived(
workflowService.buildWorkflowPayload( buildWorkflowPayload(
name, name,
description, description,
editWorkflow.enabled, editWorkflow.enabled,
triggerType, triggerType,
orderedFilters, selectedFilters,
orderedActions, selectedActions,
filterConfigs, filterConfigs,
actionConfigs, actionConfigs,
), ),
@@ -153,7 +164,7 @@
}); });
const syncFromJson = () => { const syncFromJson = () => {
const result = workflowService.parseWorkflowJson(JSON.stringify(jsonEditorContent)); const result = parseWorkflowJson(JSON.stringify(jsonEditorContent), triggers, filters, actions);
if (!result.success) { if (!result.success) {
return; return;
@@ -168,24 +179,26 @@
selectedTrigger = result.data.trigger; selectedTrigger = result.data.trigger;
} }
orderedFilters = result.data.filters; selectedFilters = result.data.filters;
orderedActions = result.data.actions; selectedActions = result.data.actions;
filterConfigs = result.data.filterConfigs; filterConfigs = result.data.filterConfigs;
actionConfigs = result.data.actionConfigs; actionConfigs = result.data.actionConfigs;
} }
}; };
let hasChanges: boolean = $derived( let hasChanges: boolean = $derived(
workflowService.hasWorkflowChanged( hasWorkflowChanged(
previousWorkflow, previousWorkflow,
editWorkflow.enabled, editWorkflow.enabled,
name, name,
description, description,
triggerType, triggerType,
orderedFilters, selectedFilters,
orderedActions, selectedActions,
filterConfigs, filterConfigs,
actionConfigs, actionConfigs,
filters,
actions,
), ),
); );
@@ -211,10 +224,10 @@
return; return;
} }
const newFilters = [...orderedFilters]; const newFilters = [...selectedFilters];
const [draggedItem] = newFilters.splice(draggedFilterIndex, 1); const [draggedItem] = newFilters.splice(draggedFilterIndex, 1);
newFilters.splice(index, 0, draggedItem); newFilters.splice(index, 0, draggedItem);
orderedFilters = newFilters; selectedFilters = newFilters;
}; };
const handleFilterDragEnd = () => { const handleFilterDragEnd = () => {
@@ -238,10 +251,10 @@
return; return;
} }
const newActions = [...orderedActions]; const newActions = [...selectedActions];
const [draggedItem] = newActions.splice(draggedActionIndex, 1); const [draggedItem] = newActions.splice(draggedActionIndex, 1);
newActions.splice(index, 0, draggedItem); newActions.splice(index, 0, draggedItem);
orderedActions = newActions; selectedActions = newActions;
}; };
const handleActionDragEnd = () => { const handleActionDragEnd = () => {
@@ -253,26 +266,24 @@
const result = (await modalManager.show(AddWorkflowStepModal, { const result = (await modalManager.show(AddWorkflowStepModal, {
filters: supportFilters, filters: supportFilters,
actions: supportActions, actions: supportActions,
addedFilters: orderedFilters,
addedActions: orderedActions,
type, type,
})) as { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto } | undefined; })) as { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto } | undefined;
if (result) { if (result) {
if (result.type === 'filter') { if (result.type === 'filter') {
orderedFilters = [...orderedFilters, result.item as PluginFilterResponseDto]; selectedFilters = [...selectedFilters, result.item as PluginFilterResponseDto];
} else if (result.type === 'action') { } else if (result.type === 'action') {
orderedActions = [...orderedActions, result.item as PluginActionResponseDto]; selectedActions = [...selectedActions, result.item as PluginActionResponseDto];
} }
} }
}; };
const handleRemoveFilter = (index: number) => { const handleRemoveFilter = (index: number) => {
orderedFilters = orderedFilters.filter((_, i) => i !== index); selectedFilters = selectedFilters.filter((_, i) => i !== index);
}; };
const handleRemoveAction = (index: number) => { const handleRemoveAction = (index: number) => {
orderedActions = orderedActions.filter((_, i) => i !== index); selectedActions = selectedActions.filter((_, i) => i !== index);
}; };
const handleTriggerChange = async (newTrigger: PluginTriggerResponseDto) => { const handleTriggerChange = async (newTrigger: PluginTriggerResponseDto) => {
@@ -340,8 +351,6 @@
</svelte:head> </svelte:head>
<main class="pt-24 immich-scrollbar"> <main class="pt-24 immich-scrollbar">
<WorkflowSummarySidebar trigger={selectedTrigger} filters={orderedFilters} actions={orderedActions} />
<Container size="medium" class="p-4" center> <Container size="medium" class="p-4" center>
{#if viewMode === 'json'} {#if viewMode === 'json'}
<WorkflowJsonEditor <WorkflowJsonEditor
@@ -392,7 +401,7 @@
{#each triggers as trigger (trigger.name)} {#each triggers as trigger (trigger.name)}
<WorkflowTriggerCard <WorkflowTriggerCard
{trigger} {trigger}
selected={selectedTrigger.triggerType === trigger.triggerType} selected={selectedTrigger.type === trigger.type}
onclick={() => handleTriggerChange(trigger)} onclick={() => handleTriggerChange(trigger)}
/> />
{/each} {/each}
@@ -414,10 +423,10 @@
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{#if orderedFilters.length === 0} {#if selectedFilters.length === 0}
{@render emptyCreateButton($t('add_filter'), $t('add_filter_description'), () => handleAddStep('filter'))} {@render emptyCreateButton($t('add_filter'), $t('add_filter_description'), () => handleAddStep('filter'))}
{:else} {:else}
{#each orderedFilters as filter, index (filter.id)} {#each selectedFilters as filter, index (index)}
{#if index > 0} {#if index > 0}
{@render stepSeparator()} {@render stepSeparator()}
{/if} {/if}
@@ -483,10 +492,10 @@
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{#if orderedActions.length === 0} {#if selectedActions.length === 0}
{@render emptyCreateButton($t('add_action'), $t('add_action_description'), () => handleAddStep('action'))} {@render emptyCreateButton($t('add_action'), $t('add_action_description'), () => handleAddStep('action'))}
{:else} {:else}
{#each orderedActions as action, index (action.id)} {#each selectedActions as action, index (index)}
{#if index > 0} {#if index > 0}
{@render stepSeparator()} {@render stepSeparator()}
{/if} {/if}
@@ -539,6 +548,8 @@
</VStack> </VStack>
{/if} {/if}
</Container> </Container>
<WorkflowSummarySidebar trigger={selectedTrigger} filters={selectedFilters} actions={selectedActions} />
</main> </main>
<ControlAppBar onClose={() => goto(AppRoute.WORKFLOWS)} backIcon={mdiArrowLeft} tailwindClasses="fixed! top-0! w-full"> <ControlAppBar onClose={() => goto(AppRoute.WORKFLOWS)} backIcon={mdiArrowLeft} tailwindClasses="fixed! top-0! w-full">

View File

@@ -1,6 +1,6 @@
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getPlugins, getTriggers, getWorkflow } from '@immich/sdk'; import { getPlugins, getPluginTriggers, getWorkflow } from '@immich/sdk';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async ({ url, params }) => { export const load = (async ({ url, params }) => {
@@ -8,7 +8,7 @@ export const load = (async ({ url, params }) => {
const [plugins, workflow, triggers] = await Promise.all([ const [plugins, workflow, triggers] = await Promise.all([
getPlugins(), getPlugins(),
getWorkflow({ id: params.workflowId }), getWorkflow({ id: params.workflowId }),
getTriggers(), getPluginTriggers(),
]); ]);
const $t = await getFormatter(); const $t = await getFormatter();