diff --git a/i18n/en.json b/i18n/en.json index 5903d7850e..521eac10b1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5,6 +5,7 @@ "acknowledge": "Acknowledge", "action": "Action", "action_common_update": "Update", + "action_description": "A set of action to perform on the filtered assets", "actions": "Actions", "active": "Active", "active_count": "Active: {count}", @@ -15,9 +16,13 @@ "add_a_location": "Add a location", "add_a_name": "Add a name", "add_a_title": "Add a title", + "add_action": "Add action", + "add_action_description": "Click to add an action to perform", "add_birthday": "Add a birthday", "add_endpoint": "Add endpoint", "add_exclusion_pattern": "Add exclusion pattern", + "add_filter": "Add filter", + "add_filter_description": "Click to add a filter condition", "add_location": "Add location", "add_more_users": "Add more users", "add_partner": "Add partner", @@ -36,6 +41,7 @@ "add_to_shared_album": "Add to shared album", "add_upload_to_stack": "Add upload to stack", "add_url": "Add URL", + "add_workflow_step": "Add workflow step", "added_to_archive": "Added to archive", "added_to_favorites": "Added to favorites", "added_to_favorites_count": "Added {count, number} to favorites", @@ -467,6 +473,7 @@ "album_remove_user": "Remove user?", "album_remove_user_confirmation": "Are you sure you want to remove {user}?", "album_search_not_found": "No albums found matching your search", + "album_selected": "Album selected", "album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.", "album_summary": "Album summary", "album_updated": "Album updated", @@ -488,6 +495,7 @@ "albums_default_sort_order_description": "Initial asset sort order when creating new albums.", "albums_feature_description": "Collections of assets that can be shared with other users.", "albums_on_device_count": "Albums on device ({count})", + "albums_selected": "{count, plural, one {# album selected} other {# albums selected}}", "all": "All", "all_albums": "All albums", "all_people": "All people", @@ -524,10 +532,12 @@ "archived_count": "{count, plural, other {Archived #}}", "are_these_the_same_person": "Are these the same person?", "are_you_sure_to_do_this": "Are you sure you want to do this?", + "array_field_not_fully_supported": "Array fields require manual JSON editing", "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_added_to_album": "Added to album", "asset_adding_to_album": "Adding to album…", + "asset_created": "Asset created", "asset_description_updated": "Asset description has been updated", "asset_filename_is_offline": "Asset {filename} is offline", "asset_has_unassigned_faces": "Asset has unassigned faces", @@ -711,6 +721,8 @@ "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", "change_pin_code": "Change PIN code", + "change_trigger": "Change trigger", + "change_trigger_prompt": "Are you sure you want to change the trigger? This will remove all existing actions and filters.", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", "charging": "Charging", @@ -787,6 +799,7 @@ "create_album": "Create album", "create_album_page_untitled": "Untitled", "create_api_key": "Create API key", + "create_first_workflow": "Create first workflow", "create_library": "Create Library", "create_link": "Create link", "create_link_to_share": "Create link to share", @@ -801,6 +814,7 @@ "create_tag": "Create tag", "create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.", "create_user": "Create user", + "create_workflow": "Create workflow", "created": "Created", "created_at": "Created", "creating_linked_albums": "Creating linked albums...", @@ -867,6 +881,7 @@ "deselect_all": "Deselect All", "details": "Details", "direction": "Direction", + "disable": "Disable", "disabled": "Disabled", "disallow_edits": "Disallow edits", "discord": "Discord", @@ -929,11 +944,13 @@ "edit_tag": "Edit tag", "edit_title": "Edit Title", "edit_user": "Edit user", + "edit_workflow": "Edit workflow", "editor": "Editor", "editor_close_without_save_prompt": "The changes will not be saved", "editor_close_without_save_title": "Close editor?", "editor_crop_tool_h2_aspect_ratios": "Aspect ratios", "editor_crop_tool_h2_rotation": "Rotation", + "editor_mode": "Editor mode", "email": "Email", "email_notifications": "Email notifications", "empty_folder": "This folder is empty", @@ -1014,6 +1031,7 @@ "unable_to_complete_oauth_login": "Unable to complete OAuth login", "unable_to_connect": "Unable to connect", "unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https", + "unable_to_create": "Unable to create workflow", "unable_to_create_admin_account": "Unable to create admin account", "unable_to_create_api_key": "Unable to create a new API Key", "unable_to_create_library": "Unable to create library", @@ -1024,6 +1042,7 @@ "unable_to_delete_exclusion_pattern": "Unable to delete exclusion pattern", "unable_to_delete_shared_link": "Unable to delete shared link", "unable_to_delete_user": "Unable to delete user", + "unable_to_delete_workflow": "Unable to delete workflow", "unable_to_download_files": "Unable to download files", "unable_to_edit_exclusion_pattern": "Unable to edit exclusion pattern", "unable_to_empty_trash": "Unable to empty trash", @@ -1074,6 +1093,7 @@ "unable_to_update_settings": "Unable to update settings", "unable_to_update_timeline_display_status": "Unable to update timeline display status", "unable_to_update_user": "Unable to update user", + "unable_to_update_workflow": "Unable to update workflow", "unable_to_upload_file": "Unable to upload file" }, "exclusion_pattern": "Exclusion pattern", @@ -1126,8 +1146,10 @@ "filename": "Filename", "filetype": "Filetype", "filter": "Filter", + "filter_description": "Conditions to filter the target assets", "filter_people": "Filter people", "filter_places": "Filter places", + "filters": "Filters", "find_them_fast": "Find them fast by name with search", "first": "First", "fix_incorrect_match": "Fix incorrect match", @@ -1143,6 +1165,7 @@ "general": "General", "geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map", "get_help": "Get Help", + "get_people_error": "Error getting people", "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "Getting Started", "go_back": "Go back", @@ -1175,6 +1198,7 @@ "hide_named_person": "Hide person {name}", "hide_password": "Hide password", "hide_person": "Hide person", + "hide_schema": "Hide schema", "hide_text_recognition": "Hide text recognition", "hide_unnamed_people": "Hide unnamed people", "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", @@ -1247,6 +1271,8 @@ "ios_debug_info_processing_ran_at": "Processing ran {dateTime}", "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Jobs", + "json_editor": "JSON editor", + "json_error": "JSON error", "keep": "Keep", "keep_all": "Keep All", "keep_this_delete_others": "Keep this, delete others", @@ -1416,11 +1442,13 @@ "monthly_title_text_date_format": "MMMM y", "more": "More", "move": "Move", + "move_down": "Move down", "move_off_locked_folder": "Move out of locked folder", "move_to": "Move to", "move_to_lock_folder_action_prompt": "{count} added to the locked folder", "move_to_locked_folder": "Move to locked folder", "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder", + "move_up": "Move up", "moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive", "moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library", "moved_to_trash": "Moved to trash", @@ -1430,6 +1458,7 @@ "my_albums": "My albums", "name": "Name", "name_or_nickname": "Name or nickname", + "name_required": "Name is required", "navigate": "Navigate", "navigate_to_time": "Navigate to Time", "network_requirement_photos_upload": "Use cellular data to backup photos", @@ -1454,6 +1483,7 @@ "next": "Next", "next_memory": "Next memory", "no": "No", + "no_actions_added": "No actions added yet", "no_albums_message": "Create an album to organize your photos and videos", "no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.", "no_albums_yet": "It looks like you do not have any albums yet.", @@ -1463,11 +1493,13 @@ "no_cast_devices_found": "No cast devices found", "no_checksum_local": "No checksum available - cannot fetch local assets", "no_checksum_remote": "No checksum available - cannot fetch remote asset", + "no_configuration_needed": "No configuration needed", "no_devices": "No authorized devices", "no_duplicates_found": "No duplicates were found.", "no_exif_info_available": "No exif info available", "no_explore_results_message": "Upload more photos to explore your collection.", "no_favorites_message": "Add favorites to quickly find your best pictures and videos", + "no_filters_added": "No filters added yet", "no_libraries_message": "Create an external library to view your photos and videos", "no_local_assets_found": "No local assets found with this checksum", "no_location_set": "No location set", @@ -1563,6 +1595,7 @@ "people": "People", "people_edits_count": "Edited {count, plural, one {# person} other {# people}}", "people_feature_description": "Browsing photos and videos grouped by people", + "people_selected": "{count, plural, one {# person selected} other {# people selected}}", "people_sidebar_description": "Display a link to People in the sidebar", "permanent_deletion_warning": "Permanent deletion warning", "permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets", @@ -1587,6 +1620,8 @@ "person_age_years": "{years, plural, other {# years}} old", "person_birthdate": "Born on {date}", "person_hidden": "{name}{hidden, select, true { (hidden)} other {}}", + "person_recognized": "Person recognized", + "person_selected": "Person selected", "photo_shared_all_users": "Looks like you shared your photos with all users or you don't have any user to share with.", "photos": "Photos", "photos_and_videos": "Photos & Videos", @@ -1836,17 +1871,22 @@ "second": "Second", "see_all_people": "See all people", "select": "Select", + "select_album": "Select album", "select_album_cover": "Select album cover", + "select_albums": "Select albums", "select_all": "Select all", "select_all_duplicates": "Select all duplicates", "select_all_in": "Select all in {group}", "select_avatar_color": "Select avatar color", + "select_count": "{count, plural, one {Select #} other {Select #}}", "select_face": "Select face", "select_featured_photo": "Select featured photo", "select_from_computer": "Select from computer", "select_keep_all": "Select keep all", "select_library_owner": "Select library owner", "select_new_face": "Select new face", + "select_people": "Select people", + "select_person": "Select person", "select_person_to_tag": "Select a person to tag", "select_photos": "Select photos", "select_trash_all": "Select trash all", @@ -1982,6 +2022,7 @@ "show_password": "Show password", "show_person_options": "Show person options", "show_progress_bar": "Show Progress Bar", + "show_schema": "Show schema", "show_search_options": "Show search options", "show_shared_links": "Show shared links", "show_slideshow_transition": "Show slideshow transition", @@ -2109,6 +2150,13 @@ "trash_page_select_assets_btn": "Select assets", "trash_page_title": "Trash ({count})", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.", + "trigger": "Trigger", + "trigger_asset_uploaded": "Asset Uploaded", + "trigger_asset_uploaded_description": "Triggered when a new asset is uploaded", + "trigger_description": "An event that kick off the workflow", + "trigger_person_recognized": "Person Recognized", + "trigger_person_recognized_description": "Triggered when a person is detected", + "trigger_type": "Trigger type", "troubleshoot": "Troubleshoot", "type": "Type", "unable_to_change_pin_code": "Unable to change PIN code", @@ -2139,7 +2187,9 @@ "unstack": "Un-stack", "unstack_action_prompt": "{count} unstacked", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", + "unsupported_field_type": "Unsupported field type", "untagged": "Untagged", + "untitled_workflow": "Untitled workflow", "up_next": "Up next", "update_location_action_prompt": "Update the location of {count} selected assets with:", "updated_at": "Updated", @@ -2185,6 +2235,7 @@ "utilities": "Utilities", "validate": "Validate", "validate_endpoint_error": "Please enter a valid URL", + "validation_error": "Validation error", "variables": "Variables", "version": "Version", "version_announcement_closing": "Your friend, Alex", @@ -2216,6 +2267,8 @@ "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack", "visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}", + "visual": "Visual", + "visual_builder": "Visual builder", "waiting": "Waiting", "waiting_count": "Waiting: {count}", "warning": "Warning", @@ -2224,7 +2277,19 @@ "welcome_to_immich": "Welcome to Immich", "width": "Width", "wifi_name": "Wi-Fi Name", - "workflow": "Workflow", + "workflow_delete_prompt": "Are you sure you want to delete this workflow?", + "workflow_deleted": "Workflow deleted", + "workflow_description": "Workflow description", + "workflow_info": "Workflow info", + "workflow_json": "Workflow JSON", + "workflow_json_help": "Edit the workflow configuration in JSON format. Changes will sync to the visual builder.", + "workflow_name": "Workflow name", + "workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?", + "workflow_summary": "Workflow summary", + "workflow_update_success": "Workflow updated successfully", + "workflow_updated": "Workflow updated", + "workflows": "Workflows", + "workflows_help_text": "Workflows automate actions on your assets based on triggers and filters", "wrong_pin_code": "Wrong PIN code", "year": "Year", "years_ago": "{years, plural, one {# year} other {# years}} ago", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 719807e74e..7277a99ac8 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -199,6 +199,7 @@ 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 *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 @@ -465,9 +466,10 @@ 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) - [PluginTriggerType](doc//PluginTriggerType.md) - [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseUpdate](doc//PurchaseUpdate.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 21730074aa..05d1803979 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -217,9 +217,10 @@ 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'; part 'model/plugin_trigger_type.dart'; part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; diff --git a/mobile/openapi/lib/api/plugins_api.dart b/mobile/openapi/lib/api/plugins_api.dart index 264d3049e8..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. diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 041be67015..39aea82c89 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -482,12 +482,14 @@ 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': return PluginResponseDto.fromJson(value); + case 'PluginTriggerResponseDto': + return PluginTriggerResponseDto.fromJson(value); case 'PluginTriggerType': return PluginTriggerTypeTypeTransformer().decode(value); case 'PurchaseResponse': 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 new file mode 100644 index 0000000000..a6ee1c6b69 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PluginTriggerResponseDto { + /// Returns a new [PluginTriggerResponseDto] instance. + PluginTriggerResponseDto({ + required this.contextType, + required this.type, + }); + + PluginContextType contextType; + + PluginTriggerType type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginTriggerResponseDto && + other.contextType == contextType && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (contextType.hashCode) + + (type.hashCode); + + @override + String toString() => 'PluginTriggerResponseDto[contextType=$contextType, type=$type]'; + + Map toJson() { + final json = {}; + json[r'contextType'] = this.contextType; + json[r'type'] = this.type; + return json; + } + + /// Returns a new [PluginTriggerResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginTriggerResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PluginTriggerResponseDto"); + if (value is Map) { + final json = value.cast(); + + return PluginTriggerResponseDto( + contextType: PluginContextType.fromJson(json[r'contextType'])!, + type: PluginTriggerType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginTriggerResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginTriggerResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginTriggerResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginTriggerResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'contextType', + 'type', + }; +} + diff --git a/mobile/openapi/lib/model/workflow_response_dto.dart b/mobile/openapi/lib/model/workflow_response_dto.dart index 5132e7cb73..1ad36f300b 100644 --- a/mobile/openapi/lib/model/workflow_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_response_dto.dart @@ -40,7 +40,7 @@ class WorkflowResponseDto { String ownerId; - WorkflowResponseDtoTriggerTypeEnum triggerType; + PluginTriggerType triggerType; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowResponseDto && @@ -105,7 +105,7 @@ class WorkflowResponseDto { id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, - triggerType: WorkflowResponseDtoTriggerTypeEnum.fromJson(json[r'triggerType'])!, + triggerType: PluginTriggerType.fromJson(json[r'triggerType'])!, ); } return null; @@ -165,77 +165,3 @@ class WorkflowResponseDto { }; } - -class WorkflowResponseDtoTriggerTypeEnum { - /// Instantiate a new enum with the provided [value]. - const WorkflowResponseDtoTriggerTypeEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const assetCreate = WorkflowResponseDtoTriggerTypeEnum._(r'AssetCreate'); - static const personRecognized = WorkflowResponseDtoTriggerTypeEnum._(r'PersonRecognized'); - - /// List of all possible values in this [enum][WorkflowResponseDtoTriggerTypeEnum]. - static const values = [ - assetCreate, - personRecognized, - ]; - - static WorkflowResponseDtoTriggerTypeEnum? fromJson(dynamic value) => WorkflowResponseDtoTriggerTypeEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = WorkflowResponseDtoTriggerTypeEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [WorkflowResponseDtoTriggerTypeEnum] to String, -/// and [decode] dynamic data back to [WorkflowResponseDtoTriggerTypeEnum]. -class WorkflowResponseDtoTriggerTypeEnumTypeTransformer { - factory WorkflowResponseDtoTriggerTypeEnumTypeTransformer() => _instance ??= const WorkflowResponseDtoTriggerTypeEnumTypeTransformer._(); - - const WorkflowResponseDtoTriggerTypeEnumTypeTransformer._(); - - String encode(WorkflowResponseDtoTriggerTypeEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a WorkflowResponseDtoTriggerTypeEnum. - /// - /// 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] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// 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. - WorkflowResponseDtoTriggerTypeEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'AssetCreate': return WorkflowResponseDtoTriggerTypeEnum.assetCreate; - case r'PersonRecognized': return WorkflowResponseDtoTriggerTypeEnum.personRecognized; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [WorkflowResponseDtoTriggerTypeEnumTypeTransformer] instance. - static WorkflowResponseDtoTriggerTypeEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/workflow_update_dto.dart b/mobile/openapi/lib/model/workflow_update_dto.dart index b36a396dc6..135c032b77 100644 --- a/mobile/openapi/lib/model/workflow_update_dto.dart +++ b/mobile/openapi/lib/model/workflow_update_dto.dart @@ -18,6 +18,7 @@ class WorkflowUpdateDto { this.enabled, this.filters = const [], this.name, + this.triggerType, }); List actions; @@ -48,13 +49,22 @@ class WorkflowUpdateDto { /// String? name; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginTriggerType? triggerType; + @override bool operator ==(Object other) => identical(this, other) || other is WorkflowUpdateDto && _deepEquality.equals(other.actions, actions) && other.description == description && other.enabled == enabled && _deepEquality.equals(other.filters, filters) && - other.name == name; + other.name == name && + other.triggerType == triggerType; @override int get hashCode => @@ -63,10 +73,11 @@ class WorkflowUpdateDto { (description == null ? 0 : description!.hashCode) + (enabled == null ? 0 : enabled!.hashCode) + (filters.hashCode) + - (name == null ? 0 : name!.hashCode); + (name == null ? 0 : name!.hashCode) + + (triggerType == null ? 0 : triggerType!.hashCode); @override - String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name]'; + String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name, triggerType=$triggerType]'; Map toJson() { final json = {}; @@ -87,6 +98,11 @@ class WorkflowUpdateDto { } else { // json[r'name'] = null; } + if (this.triggerType != null) { + json[r'triggerType'] = this.triggerType; + } else { + // json[r'triggerType'] = null; + } return json; } @@ -104,6 +120,7 @@ class WorkflowUpdateDto { enabled: mapValueOfType(json, r'enabled'), filters: WorkflowFilterItemDto.listFromJson(json[r'filters']), name: mapValueOfType(json, r'name'), + triggerType: PluginTriggerType.fromJson(json[r'triggerType']), ); } return null; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index fba6d2af80..4a51e3b88c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8020,6 +8020,55 @@ "x-immich-state": "Alpha" } }, + "/plugins/triggers": { + "get": { + "description": "Retrieve a list of all available plugin triggers.", + "operationId": "getPluginTriggers", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PluginTriggerResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "List all plugin triggers", + "tags": [ + "Plugins" + ], + "x-immich-history": [ + { + "version": "v2.3.0", + "state": "Added" + }, + { + "version": "v2.3.0", + "state": "Alpha" + } + ], + "x-immich-permission": "plugin.read", + "x-immich-state": "Alpha" + } + }, "/plugins/{id}": { "get": { "description": "Retrieve information about a specific plugin by its ID.", @@ -18282,7 +18331,7 @@ }, "supportedContexts": { "items": { - "$ref": "#/components/schemas/PluginContext" + "$ref": "#/components/schemas/PluginContextType" }, "type": "array" }, @@ -18301,7 +18350,7 @@ ], "type": "object" }, - "PluginContext": { + "PluginContextType": { "enum": [ "asset", "album", @@ -18329,7 +18378,7 @@ }, "supportedContexts": { "items": { - "$ref": "#/components/schemas/PluginContext" + "$ref": "#/components/schemas/PluginContextType" }, "type": "array" }, @@ -18401,6 +18450,29 @@ ], "type": "object" }, + "PluginTriggerResponseDto": { + "properties": { + "contextType": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginContextType" + } + ] + }, + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginTriggerType" + } + ] + } + }, + "required": [ + "contextType", + "type" + ], + "type": "object" + }, "PluginTriggerType": { "enum": [ "AssetCreate", @@ -23316,11 +23388,11 @@ "type": "string" }, "triggerType": { - "enum": [ - "AssetCreate", - "PersonRecognized" - ], - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/PluginTriggerType" + } + ] } }, "required": [ @@ -23358,6 +23430,13 @@ }, "name": { "type": "string" + }, + "triggerType": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginTriggerType" + } + ] } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2289e5f73e..2f6ce9208d 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 = { @@ -966,6 +966,10 @@ export type PluginResponseDto = { updatedAt: string; version: string; }; +export type PluginTriggerResponseDto = { + contextType: PluginContextType; + "type": PluginTriggerType; +}; export type QueueResponseDto = { isPaused: boolean; name: QueueName; @@ -1750,7 +1754,7 @@ export type WorkflowResponseDto = { id: string; name: string | null; ownerId: string; - triggerType: TriggerType; + triggerType: PluginTriggerType; }; export type WorkflowActionItemDto = { actionConfig?: object; @@ -1774,6 +1778,7 @@ export type WorkflowUpdateDto = { enabled?: boolean; filters?: WorkflowFilterItemDto[]; name?: string; + triggerType?: PluginTriggerType; }; /** * List all activities @@ -3656,6 +3661,17 @@ export function getPlugins(opts?: Oazapfts.RequestOpts) { ...opts })); } +/** + * List all plugin triggers + */ +export function getPluginTriggers(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PluginTriggerResponseDto[]; + }>("/plugins/triggers", { + ...opts + })); +} /** * Retrieve a plugin */ @@ -5418,11 +5434,15 @@ export enum PartnerDirection { SharedBy = "shared-by", SharedWith = "shared-with" } -export enum PluginContext { +export enum PluginContextType { Asset = "asset", Album = "album", Person = "person" } +export enum PluginTriggerType { + AssetCreate = "AssetCreate", + PersonRecognized = "PersonRecognized" +} export enum QueueJobStatus { Active = "active", Failed = "failed", @@ -5639,11 +5659,3 @@ export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = "client_secret_post", ClientSecretBasic = "client_secret_basic" } -export enum TriggerType { - AssetCreate = "AssetCreate", - PersonRecognized = "PersonRecognized" -} -export enum PluginTriggerType { - AssetCreate = "AssetCreate", - PersonRecognized = "PersonRecognized" -} diff --git a/plugins/manifest.json b/plugins/manifest.json index 1172530c1e..4d2de275ca 100644 --- a/plugins/manifest.json +++ b/plugins/manifest.json @@ -1,30 +1,36 @@ { "name": "immich-core", - "version": "2.0.0", + "version": "2.0.1", "title": "Immich Core", "description": "Core workflow capabilities for Immich", "author": "Immich Team", - "wasm": { "path": "dist/plugin.wasm" }, - "filters": [ { "methodName": "filterFileName", "title": "Filter by filename", "description": "Filter assets by filename pattern using text matching or regular expressions", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": { "type": "object", "properties": { "pattern": { "type": "string", + "title": "Filename pattern", "description": "Text or regex pattern to match against filename" }, "matchType": { "type": "string", - "enum": ["contains", "regex", "exact"], + "title": "Match type", + "enum": [ + "contains", + "regex", + "exact" + ], "default": "contains", "description": "Type of pattern matching to perform" }, @@ -34,43 +40,57 @@ "description": "Whether matching should be case-sensitive" } }, - "required": ["pattern"] + "required": [ + "pattern" + ] } }, { "methodName": "filterFileType", "title": "Filter by file type", "description": "Filter assets by file type", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": { "type": "object", "properties": { "fileTypes": { "type": "array", + "title": "File types", "items": { "type": "string", - "enum": ["IMAGE", "VIDEO"] + "enum": [ + "image", + "video" + ] }, "description": "Allowed file types" } }, - "required": ["fileTypes"] + "required": [ + "fileTypes" + ] } }, { "methodName": "filterPerson", "title": "Filter by person", "description": "Filter by detected person", - "supportedContexts": ["person"], + "supportedContexts": [ + "person" + ], "schema": { "type": "object", "properties": { "personIds": { "type": "array", + "title": "Person IDs", "items": { "type": "string" }, - "description": "List of person to match" + "description": "List of person to match", + "subType": "people-picker" }, "matchAny": { "type": "boolean", @@ -78,24 +98,29 @@ "description": "Match any name (true) or require all names (false)" } }, - "required": ["personIds"] + "required": [ + "personIds" + ] } } ], - "actions": [ { "methodName": "actionArchive", "title": "Archive", "description": "Move the asset to archive", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": {} }, { "methodName": "actionFavorite", "title": "Favorite", "description": "Mark the asset as favorite or unfavorite", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": { "type": "object", "properties": { @@ -111,16 +136,23 @@ "methodName": "actionAddToAlbum", "title": "Add to Album", "description": "Add the item to a specified album", - "supportedContexts": ["asset", "person"], + "supportedContexts": [ + "asset", + "person" + ], "schema": { "type": "object", "properties": { "albumId": { "type": "string", - "description": "Target album ID" + "title": "Album ID", + "description": "Target album ID", + "subType": "album-picker" } }, - "required": ["albumId"] + "required": [ + "albumId" + ] } } ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27c6c3f290..aeb0e5dc2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 24.10.4 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -109,16 +109,16 @@ importers: version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.0.0 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) vitest-fetch-mock: specifier: ^0.4.0 - version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) yaml: specifier: ^2.3.1 version: 2.8.2 @@ -290,7 +290,7 @@ importers: version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) open-api/typescript-sdk: dependencies: @@ -646,7 +646,7 @@ importers: version: 13.15.10 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) eslint: specifier: ^9.14.0 version: 9.39.2(jiti@2.6.1) @@ -700,10 +700,10 @@ importers: version: 1.5.9(@swc/core@1.15.5(@swc/helpers@0.5.17))(rollup@4.53.4) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) web: dependencies: @@ -718,7 +718,7 @@ importers: version: link:../open-api/typescript-sdk '@immich/ui': specifier: ^0.50.1 - version: 0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + version: 0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -806,6 +806,9 @@ importers: svelte-i18n: specifier: ^4.0.1 version: 4.0.1(svelte@5.43.3) + svelte-jsoneditor: + specifier: ^3.10.0 + version: 3.10.0(svelte@5.43.3) svelte-maplibre: specifier: ^1.2.5 version: 1.2.5(svelte@5.43.3) @@ -836,25 +839,25 @@ importers: version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))) + version: 3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))) '@sveltejs/enhanced-img': specifier: ^0.9.0 - version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': specifier: 6.2.1 - version: 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) @@ -878,7 +881,7 @@ importers: version: 1.5.6 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) dotenv: specifier: ^17.0.0 version: 17.2.3 @@ -938,10 +941,10 @@ importers: version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) packages: @@ -1086,6 +1089,9 @@ packages: resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -1795,6 +1801,30 @@ packages: '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.0': + resolution: {integrity: sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.11.3': + resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} + + '@codemirror/lint@6.9.2': + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} + + '@codemirror/search@6.5.11': + resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} + + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + + '@codemirror/view@6.38.8': + resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -2825,6 +2855,18 @@ packages: '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@fortawesome/fontawesome-common-types@7.1.0': + resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==} + engines: {node: '>=6'} + + '@fortawesome/free-regular-svg-icons@7.1.0': + resolution: {integrity: sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==} + engines: {node: '>=6'} + + '@fortawesome/free-solid-svg-icons@7.1.0': + resolution: {integrity: sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==} + engines: {node: '>=6'} + '@golevelup/nestjs-discovery@5.0.0': resolution: {integrity: sha512-NaIWLCLI+XvneUK05LH2idHLmLNITYT88YnpOuUQmllKtiJNIS3woSt7QXrMZ5k3qUWuZpehEVz1JtlX4I1KyA==} peerDependencies: @@ -3216,6 +3258,18 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -3252,6 +3306,10 @@ packages: peerDependencies: tslib: '2' + '@jsonquerylang/jsonquery@5.0.4': + resolution: {integrity: sha512-QdgVkapeGRxUqOOJuh2svDutejKaCizhupEmO4ZKSsaLolD7w5QhgrjmBNuS1wMCM5TyNKifK4i1wBDfNzO9xQ==} + hasBin: true + '@koa/cors@5.0.0': resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==} engines: {node: '>= 14.0.0'} @@ -3268,6 +3326,18 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@lezer/common@1.3.0': + resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.3': + resolution: {integrity: sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -3335,6 +3405,9 @@ packages: '@maplibre/vt-pbf@4.2.0': resolution: {integrity: sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==} + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@mdi/js@7.4.47': resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} @@ -3765,6 +3838,88 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + '@photo-sphere-viewer/core@5.14.0': resolution: {integrity: sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==} @@ -3981,6 +4136,13 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@replit/codemirror-indentation-markers@6.5.3': + resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -4316,6 +4478,9 @@ packages: peerDependencies: socket.io-adapter: ^2.5.4 + '@sphinxxxx/color-conversion@2.2.2': + resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -5892,6 +6057,13 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + codemirror-wrapped-line-indent@1.0.9: + resolution: {integrity: sha512-oc976hHLt35u6Ojbhub+IWOxEpapZSqYieLEdGhsgFZ4rtYQtdb5KjxzgjCCyVe3t0yk+a6hmaIOEsjU/tZRxQ==} + peerDependencies: + '@codemirror/language': ^6.9.0 + '@codemirror/state': ^6.2.1 + '@codemirror/view': ^6.17.1 + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -6107,6 +6279,9 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -6257,6 +6432,10 @@ packages: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -6276,6 +6455,10 @@ packages: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -6406,6 +6589,11 @@ packages: detect-europe-js@0.1.2: resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -6433,6 +6621,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} @@ -7410,6 +7602,10 @@ packages: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -7563,6 +7759,12 @@ packages: immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} + immutable-json-patch@6.0.2: + resolution: {integrity: sha512-KwCA5DXJiyldda8SPha1zB+6+vbEi5/jRRcYii/6yFXlyu9ZjiSH/wPq8Ri2Hk8iGjjTMcHW3Z21S4MOpl7sOw==} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -7880,6 +8082,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -7912,6 +8118,19 @@ packages: canvas: optional: true + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: 2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -7932,6 +8151,9 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-source-map@0.6.1: + resolution: {integrity: sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -7949,6 +8171,15 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonpath-plus@10.3.0: + resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} + engines: {node: '>=18.0.0'} + hasBin: true + + jsonrepair@3.13.1: + resolution: {integrity: sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==} + hasBin: true + jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -8390,6 +8621,9 @@ packages: memfs@4.51.1: resolution: {integrity: sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + memoizee@0.4.17: resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} engines: {node: '>=0.12'} @@ -8748,6 +8982,9 @@ packages: engines: {node: ^18 || >=20} hasBin: true + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -8813,6 +9050,9 @@ packages: node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-addon-api@8.5.0: resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} engines: {node: ^18 || ^20 || >= 21} @@ -10212,6 +10452,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + rtlcss@4.3.0: resolution: {integrity: sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==} engines: {node: '>=12.0.0'} @@ -10265,6 +10508,11 @@ packages: sanitize-html@2.17.0: resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + sass@1.94.2: + resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==} + engines: {node: '>=14.0.0'} + hasBin: true + sax@1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} @@ -10655,6 +10903,9 @@ packages: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -10698,6 +10949,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte-awesome@3.3.5: + resolution: {integrity: sha512-RIi+OI6CEn+fTdYy7UOgImEUWvdQSwP9SiMC44UKyFO+8+gjj+NgTG67hI8j2rTHQVvCP820Uj+4UoZG8CCUfA==} + peerDependencies: + svelte: '>= 3.43.1 < 6' + svelte-check@4.3.4: resolution: {integrity: sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==} engines: {node: '>= 18.0.0'} @@ -10715,6 +10971,9 @@ packages: svelte: optional: true + svelte-floating-ui@1.5.8: + resolution: {integrity: sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw==} + svelte-gestures@5.2.2: resolution: {integrity: sha512-Y+chXPaSx8OsPoFppUwPk8PJzgrZ7xoDJKXeiEc7JBqyKKzXer9hlf8F9O34eFuAWB4/WQEvccACvyBplESL7A==} @@ -10728,6 +10987,11 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5 + svelte-jsoneditor@3.10.0: + resolution: {integrity: sha512-0CnotYxakbKalCTcNcF1AVatEZ3ITslMySOxaphPnX2mHLNvJNX+NEgB/RaYv7/OMI/6ouKIsKsUNZiWBoWkMw==} + peerDependencies: + svelte: ^5.0.0 + svelte-maplibre@1.2.5: resolution: {integrity: sha512-Uklcbi6inW9GA0MuSusbXmFr/MQPmXrjuP8hS1+yFX3ySvCQ477tsM3I7Jo/fUDK3XAxFSIHW6hZfucnM3kXwQ==} peerDependencies: @@ -10754,6 +11018,9 @@ packages: peerDependencies: svelte: ^3.48.0 || ^4 || ^5 + svelte-select@5.8.3: + resolution: {integrity: sha512-nQsvflWmTCOZjssdrNptzfD1Ok45hHVMTL5IHay5DINk7dfu5Er+8KsVJnZMJdSircqtR0YlT4YkCFlxOUhVPA==} + svelte-toolbelt@0.10.6: resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -10971,6 +11238,13 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -10998,6 +11272,10 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -11005,6 +11283,10 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tree-dump@1.1.0: resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} engines: {node: '>=10.0'} @@ -11315,6 +11597,9 @@ packages: value-equal@1.0.1: resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + vanilla-picker@2.12.3: + resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -11439,10 +11724,17 @@ packages: vt-pbf@3.1.3: resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + watchpack@2.4.4: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} @@ -11540,14 +11832,26 @@ packages: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@11.0.0: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -11660,6 +11964,10 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -11946,6 +12254,15 @@ snapshots: transitivePeerDependencies: - chokidar + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + optional: true + '@aws-crypto/sha256-browser@5.2.0': dependencies: '@aws-crypto/sha256-js': 5.2.0 @@ -13103,6 +13420,57 @@ snapshots: '@borewit/text-codec@0.1.1': {} + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.3.0 + + '@codemirror/commands@6.10.0': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.3.0 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.11.3 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.11.3': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@lezer/common': 1.3.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.3 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.2': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + crelt: 1.0.6 + + '@codemirror/search@6.5.11': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + crelt: 1.0.6 + + '@codemirror/state@6.5.2': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.38.8': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@colors/colors@1.5.0': optional: true @@ -14509,6 +14877,16 @@ snapshots: dependencies: tslib: 2.8.1 + '@fortawesome/fontawesome-common-types@7.1.0': {} + + '@fortawesome/free-regular-svg-icons@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + + '@fortawesome/free-solid-svg-icons@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -14653,12 +15031,12 @@ snapshots: dependencies: svelte: 5.43.3 - '@immich/ui@0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)': + '@immich/ui@0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.3) '@internationalized/date': 3.10.0 '@mdi/js': 7.4.47 - bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) luxon: 3.7.2 simple-icons: 15.22.0 svelte: 5.43.3 @@ -14875,6 +15253,14 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -14911,6 +15297,8 @@ snapshots: '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) tslib: 2.8.1 + '@jsonquerylang/jsonquery@5.0.4': {} + '@koa/cors@5.0.0': dependencies: vary: 1.1.2 @@ -14938,6 +15326,22 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@lezer/common@1.3.0': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.3.0 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.3.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.3 + + '@lezer/lr@1.4.3': + dependencies: + '@lezer/common': 1.3.0 + '@lukeed/csprng@1.1.0': {} '@mapbox/geojson-rewind@0.5.2': @@ -15036,6 +15440,8 @@ snapshots: pbf: 4.0.1 supercluster: 8.0.1 + '@marijn/find-cluster-break@1.0.2': {} + '@mdi/js@7.4.47': {} '@mdi/react@1.6.1': @@ -15575,6 +15981,67 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + '@photo-sphere-viewer/core@5.14.0': dependencies: three: 0.179.1 @@ -15763,6 +16230,12 @@ snapshots: dependencies: react: 19.2.3 + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@rollup/pluginutils@5.3.0(rollup@4.53.4)': dependencies: '@types/estree': 1.0.8 @@ -16159,35 +16632,37 @@ snapshots: transitivePeerDependencies: - supports-color + '@sphinxxxx/color-conversion@2.2.2': {} + '@standard-schema/spec@1.1.0': {} '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) - '@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) magic-string: 0.30.21 sharp: 0.34.5 svelte: 5.43.3 svelte-parse-markup: 0.1.5(svelte@5.43.3) - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) vite-imagetools: 9.0.2(rollup@4.53.4) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -16200,28 +16675,28 @@ snapshots: set-cookie-parser: 2.7.2 sirv: 3.0.2 svelte: 5.43.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) optionalDependencies: '@opentelemetry/api': 1.9.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) debug: 4.4.3 svelte: 5.43.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.21 svelte: 5.43.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -16440,12 +16915,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) '@testing-library/dom@10.4.1': dependencies: @@ -16467,13 +16942,13 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte@5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@testing-library/svelte@5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 svelte: 5.43.3 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -17056,7 +17531,7 @@ snapshots: '@vercel/oidc@3.0.5': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17071,7 +17546,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -17083,13 +17558,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -17569,15 +18044,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) svelte: 5.43.3 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) tabbable: 6.3.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -17971,6 +18446,12 @@ snapshots: cluster-key-slot@1.1.2: {} + codemirror-wrapped-line-indent@1.0.9(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8): + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -18160,6 +18641,8 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.7.2 @@ -18332,6 +18815,12 @@ snapshots: cssom: 0.3.8 optional: true + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + optional: true + csstype@3.2.3: {} d3-array@3.2.4: @@ -18354,6 +18843,12 @@ snapshots: whatwg-url: 11.0.0 optional: true + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + optional: true + debounce@1.2.1: {} debounce@2.2.0: {} @@ -18442,6 +18937,9 @@ snapshots: detect-europe-js@0.1.2: {} + detect-libc@1.0.3: + optional: true + detect-libc@2.1.2: {} detect-node@2.1.0: {} @@ -18468,6 +18966,8 @@ snapshots: didyoumean@1.2.2: {} + diff-sequences@29.6.3: {} + dijkstrajs@1.0.3: {} dir-glob@3.0.1: @@ -19848,6 +20348,11 @@ snapshots: whatwg-encoding: 2.0.0 optional: true + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + optional: true + html-escaper@2.0.2: {} html-minifier-terser@6.1.0: @@ -20032,6 +20537,10 @@ snapshots: immediate@3.3.0: {} + immutable-json-patch@6.0.2: {} + + immutable@5.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -20310,6 +20819,8 @@ snapshots: jiti@2.6.1: {} + jmespath@0.16.0: {} + joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -20335,42 +20846,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)): - dependencies: - abab: 2.0.6 - acorn: 8.15.0 - acorn-globals: 7.0.1 - cssom: 0.5.0 - cssstyle: 2.3.0 - data-urls: 3.0.2 - decimal.js: 10.6.0 - domexception: 4.0.0 - escodegen: 2.1.0 - form-data: 4.0.5 - html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.23 - parse5: 7.3.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 4.1.4 - w3c-xmlserializer: 4.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - whatwg-url: 11.0.0 - ws: 8.18.3 - xml-name-validator: 4.0.0 - optionalDependencies: - canvas: 2.11.2(encoding@0.1.13) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - optional: true - jsdom@20.0.3(canvas@2.11.2): dependencies: abab: 2.0.6 @@ -20407,6 +20882,68 @@ snapshots: - utf-8-validate optional: true + jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 2.11.2(encoding@0.1.13) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsdom@26.1.0(canvas@2.11.2): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 2.11.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsep@1.4.0: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -20419,6 +20956,8 @@ snapshots: json-schema@0.4.0: {} + json-source-map@0.6.1: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-pretty-compact@4.0.0: {} @@ -20433,6 +20972,14 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpath-plus@10.3.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + + jsonrepair@3.13.1: {} + jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -21014,6 +21561,8 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 + memoize-one@6.0.0: {} + memoizee@0.4.17: dependencies: d: 1.0.2 @@ -21515,6 +22064,8 @@ snapshots: nanoid@5.1.6: {} + natural-compare-lite@1.4.0: {} + natural-compare@1.4.0: {} nearley@2.20.1: @@ -21581,6 +22132,9 @@ snapshots: node-addon-api@4.3.0: {} + node-addon-api@7.1.1: + optional: true + node-addon-api@8.5.0: {} node-emoji@1.11.0: @@ -23142,6 +23696,9 @@ snapshots: transitivePeerDependencies: - supports-color + rrweb-cssom@0.8.0: + optional: true + rtlcss@4.3.0: dependencies: escalade: 3.2.0 @@ -23157,14 +23714,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): + runed@0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.43.3 optionalDependencies: - '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) rw@1.3.3: {} @@ -23199,6 +23756,14 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.5.6 + sass@1.94.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + sax@1.4.3: {} saxes@6.0.0: @@ -23716,6 +24281,8 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + style-mod@4.1.3: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -23779,6 +24346,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte-awesome@3.3.5(svelte@5.43.3): + dependencies: + svelte: 5.43.3 + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.43.3)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -23802,6 +24373,11 @@ snapshots: optionalDependencies: svelte: 5.43.3 + svelte-floating-ui@1.5.8: + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/dom': 1.7.4 + svelte-gestures@5.2.2: {} svelte-highlight@7.9.0: @@ -23819,6 +24395,38 @@ snapshots: svelte: 5.43.3 tiny-glob: 0.2.9 + svelte-jsoneditor@3.10.0(svelte@5.43.3): + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.0 + '@codemirror/lang-json': 6.0.2 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.8 + '@fortawesome/free-regular-svg-icons': 7.1.0 + '@fortawesome/free-solid-svg-icons': 7.1.0 + '@jsonquerylang/jsonquery': 5.0.4 + '@lezer/highlight': 1.2.3 + '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8) + ajv: 8.17.1 + codemirror-wrapped-line-indent: 1.0.9(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8) + diff-sequences: 29.6.3 + immutable-json-patch: 6.0.2 + jmespath: 0.16.0 + json-source-map: 0.6.1 + jsonpath-plus: 10.3.0 + jsonrepair: 3.13.1 + lodash-es: 4.17.21 + memoize-one: 6.0.0 + natural-compare-lite: 1.4.0 + sass: 1.94.2 + svelte: 5.43.3 + svelte-awesome: 3.3.5(svelte@5.43.3) + svelte-select: 5.8.3 + vanilla-picker: 2.12.3 + svelte-maplibre@1.2.5(svelte@5.43.3): dependencies: d3-geo: 3.1.1 @@ -23836,10 +24444,14 @@ snapshots: dependencies: svelte: 5.43.3 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): + svelte-select@5.8.3: + dependencies: + svelte-floating-ui: 1.5.8 + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) style-to-object: 1.0.14 svelte: 5.43.3 transitivePeerDependencies: @@ -24125,6 +24737,14 @@ snapshots: tinyspy@4.0.4: {} + tldts-core@6.1.86: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + tmp@0.2.5: {} to-regex-range@5.0.1: @@ -24154,6 +24774,11 @@ snapshots: url-parse: 1.5.10 optional: true + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + optional: true + tr46@0.0.3: {} tr46@3.0.0: @@ -24161,6 +24786,11 @@ snapshots: punycode: 2.3.1 optional: true + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + optional: true + tree-dump@1.1.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -24472,6 +25102,10 @@ snapshots: value-equal@1.0.1: {} + vanilla-picker@2.12.3: + dependencies: + '@sphinxxxx/color-conversion': 2.2.2 + vary@1.1.2: {} vfile-location@3.2.0: {} @@ -24512,13 +25146,13 @@ snapshots: - rollup - supports-color - vite-node@3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vite-node@3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -24533,18 +25167,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2): dependencies: esbuild: 0.27.1 fdir: 6.5.0(picomatch@4.0.3) @@ -24557,22 +25191,23 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 + sass: 1.94.2 terser: 5.44.1 yaml: 2.8.2 - vitefu@1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)): + vitefu@1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)): optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)): dependencies: - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24590,14 +25225,14 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.4 happy-dom: 20.0.11 - jsdom: 20.0.3(canvas@2.11.2(encoding@0.1.13)) + jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti - less @@ -24612,11 +25247,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24634,14 +25269,14 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.1)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.4 happy-dom: 20.0.11 - jsdom: 20.0.3(canvas@2.11.2) + jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti - less @@ -24662,11 +25297,18 @@ snapshots: '@mapbox/vector-tile': 1.3.1 pbf: 3.3.0 + w3c-keyname@2.2.8: {} + w3c-xmlserializer@4.0.0: dependencies: xml-name-validator: 4.0.0 optional: true + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + optional: true + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 @@ -24863,14 +25505,28 @@ snapshots: iconv-lite: 0.6.3 optional: true + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + optional: true + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: + optional: true + whatwg-url@11.0.0: dependencies: tr46: 3.0.0 webidl-conversions: 7.0.0 optional: true + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + optional: true + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -24955,6 +25611,9 @@ snapshots: xml-name-validator@4.0.0: optional: true + xml-name-validator@5.0.0: + optional: true + xmlchars@2.2.0: optional: true diff --git a/server/src/controllers/plugin.controller.ts b/server/src/controllers/plugin.controller.ts index a0a4d14b0b..52c833e93d 100644 --- a/server/src/controllers/plugin.controller.ts +++ b/server/src/controllers/plugin.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { PluginResponseDto } from 'src/dtos/plugin.dto'; +import { PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto'; import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { PluginService } from 'src/services/plugin.service'; @@ -12,6 +12,17 @@ import { UUIDParamDto } from 'src/validation'; export class PluginController { constructor(private service: PluginService) {} + @Get('triggers') + @Authenticated({ permission: Permission.PluginRead }) + @Endpoint({ + summary: 'List all plugin triggers', + description: 'Retrieve a list of all available plugin triggers.', + history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'), + }) + getPluginTriggers(): PluginTriggerResponseDto[] { + return this.service.getTriggers(); + } + @Get() @Authenticated({ permission: Permission.PluginRead }) @Endpoint({ diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index ce80eccd65..a802bb1201 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,9 +1,16 @@ import { IsNotEmpty, IsString } from 'class-validator'; import { PluginAction, PluginFilter } from 'src/database'; -import { PluginContext } 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 { + @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' }) + type!: PluginTriggerType; + @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' }) + contextType!: PluginContextType; +} + export class PluginResponseDto { id!: string; name!: string; @@ -24,8 +31,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; } @@ -36,8 +43,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/dtos/workflow.dto.ts b/server/src/dtos/workflow.dto.ts index 7bfb90e11f..2dbff3b5e4 100644 --- a/server/src/dtos/workflow.dto.ts +++ b/server/src/dtos/workflow.dto.ts @@ -48,6 +48,9 @@ export class WorkflowCreateDto { } export class WorkflowUpdateDto { + @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', optional: true }) + triggerType?: PluginTriggerType; + @IsString() @IsNotEmpty() @Optional() @@ -74,6 +77,7 @@ export class WorkflowUpdateDto { export class WorkflowResponseDto { id!: string; ownerId!: string; + @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' }) triggerType!: PluginTriggerType; name!: string | null; description!: string; diff --git a/server/src/plugins.ts b/server/src/plugins.ts index 0c69483696..77f35e79f6 100644 --- a/server/src/plugins.ts +++ b/server/src/plugins.ts @@ -1,37 +1,17 @@ import { PluginContext, PluginTriggerType } from 'src/enum'; -import { JSONSchema } from 'src/types/plugin-schema.types'; export type PluginTrigger = { - name: string; type: PluginTriggerType; - description: string; - context: PluginContext; - schema: JSONSchema | null; + contextType: PluginContext; }; export const pluginTriggers: PluginTrigger[] = [ { - name: 'Asset Uploaded', type: PluginTriggerType.AssetCreate, - description: 'Triggered when a new asset is uploaded', - context: PluginContext.Asset, - schema: { - type: 'object', - properties: { - assetType: { - type: 'string', - description: 'Type of the asset', - default: 'ALL', - enum: ['Image', 'Video', 'All'], - }, - }, - }, + contextType: PluginContext.Asset, }, { - name: 'Person Recognized', type: PluginTriggerType.PersonRecognized, - description: 'Triggered when a person is detected in an asset', - context: PluginContext.Person, - schema: null, + 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 9336f0003a..d82b34c847 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -6,8 +6,9 @@ import { join } from 'node:path'; import { Asset, WorkflowAction, WorkflowFilter } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto'; -import { mapPlugin, PluginResponseDto } from 'src/dtos/plugin.dto'; +import { mapPlugin, PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto'; import { JobName, JobStatus, PluginTriggerType, QueueName } from 'src/enum'; +import { pluginTriggers } from 'src/plugins'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { PluginHostFunctions } from 'src/services/plugin-host.functions'; @@ -50,6 +51,10 @@ export class PluginService extends BaseService { await this.loadPlugins(); } + getTriggers(): PluginTriggerResponseDto[] { + return pluginTriggers; + } + // // CRUD operations for plugins // diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 301931421f..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(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.type === 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/server/test/medium/specs/services/workflow.service.spec.ts b/server/test/medium/specs/services/workflow.service.spec.ts index 1fddc2a7cf..229737c531 100644 --- a/server/test/medium/specs/services/workflow.service.spec.ts +++ b/server/test/medium/specs/services/workflow.service.spec.ts @@ -611,6 +611,100 @@ describe(WorkflowService.name, () => { sut.update(auth, created.id, { actions: [{ pluginActionId: factory.uuid(), actionConfig: {} }] }), ).rejects.toThrow(); }); + + it('should update trigger type', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.PersonRecognized, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [], + actions: [], + }); + + await sut.update(auth, created.id, { + triggerType: PluginTriggerType.AssetCreate, + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.triggerType).toBe(PluginTriggerType.AssetCreate); + }); + + it('should add filters', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.AssetCreate, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [], + actions: [], + }); + + await sut.update(auth, created.id, { + filters: [ + { pluginFilterId: testFilterId, filterConfig: { first: true } }, + { pluginFilterId: testFilterId, filterConfig: { second: true } }, + ], + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.filters).toHaveLength(2); + expect(fetched.filters[0].filterConfig).toEqual({ first: true }); + expect(fetched.filters[1].filterConfig).toEqual({ second: true }); + }); + + it('should replace existing filters', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.AssetCreate, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [{ pluginFilterId: testFilterId, filterConfig: { original: true } }], + actions: [], + }); + + await sut.update(auth, created.id, { + filters: [{ pluginFilterId: testFilterId, filterConfig: { replaced: true } }], + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.filters).toHaveLength(1); + expect(fetched.filters[0].filterConfig).toEqual({ replaced: true }); + }); + + it('should remove existing filters', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.AssetCreate, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [{ pluginFilterId: testFilterId, filterConfig: { toRemove: true } }], + actions: [], + }); + + await sut.update(auth, created.id, { + filters: [], + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.filters).toHaveLength(0); + }); }); describe('delete', () => { diff --git a/web/package.json b/web/package.json index 9addfb620a..8c0e05b6ac 100644 --- a/web/package.json +++ b/web/package.json @@ -58,6 +58,7 @@ "socket.io-client": "~4.8.0", "svelte-gestures": "^5.2.2", "svelte-i18n": "^4.0.1", + "svelte-jsoneditor": "^3.10.0", "svelte-maplibre": "^1.2.5", "svelte-persisted-store": "^0.12.0", "tabbable": "^6.2.0", diff --git a/web/src/lib/actions/drag-and-drop.ts b/web/src/lib/actions/drag-and-drop.ts new file mode 100644 index 0000000000..04de6d9744 --- /dev/null +++ b/web/src/lib/actions/drag-and-drop.ts @@ -0,0 +1,118 @@ +export interface DragAndDropOptions { + index: number; + onDragStart?: (index: number) => void; + onDragEnter?: (index: number) => void; + onDrop?: (e: DragEvent, index: number) => void; + onDragEnd?: () => void; + isDragging?: boolean; + isDragOver?: boolean; +} + +export function dragAndDrop(node: HTMLElement, options: DragAndDropOptions) { + let { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options; + + const isFormElement = (element: HTMLElement) => { + return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT'; + }; + + const handleDragStart = (e: DragEvent) => { + // Prevent drag if it originated from an input, textarea, or select element + const target = e.target as HTMLElement; + if (isFormElement(target)) { + e.preventDefault(); + return; + } + onDragStart?.(index); + }; + + const handleDragEnter = () => { + onDragEnter?.(index); + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = (e: DragEvent) => { + onDrop?.(e, index); + }; + + const handleDragEnd = () => { + onDragEnd?.(); + }; + + // Disable draggable when focusing on form elements (fixes Firefox input interaction) + const handleFocusIn = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + node.setAttribute('draggable', 'false'); + } + }; + + const handleFocusOut = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + node.setAttribute('draggable', 'true'); + } + }; + + node.setAttribute('draggable', 'true'); + node.setAttribute('role', 'button'); + node.setAttribute('tabindex', '0'); + + node.addEventListener('dragstart', handleDragStart); + node.addEventListener('dragenter', handleDragEnter); + node.addEventListener('dragover', handleDragOver); + node.addEventListener('drop', handleDrop); + node.addEventListener('dragend', handleDragEnd); + node.addEventListener('focusin', handleFocusIn); + node.addEventListener('focusout', handleFocusOut); + + // Update classes based on drag state + const updateClasses = (dragging: boolean, dragOver: boolean) => { + // Remove all drag-related classes first + node.classList.remove('opacity-50', 'border-gray-400', 'dark:border-gray-500', 'border-solid'); + + // Add back only the active ones + if (dragging) { + node.classList.add('opacity-50'); + } + + if (dragOver) { + node.classList.add('border-gray-400', 'dark:border-gray-500', 'border-solid'); + node.classList.remove('border-transparent'); + } else { + node.classList.add('border-transparent'); + } + }; + + updateClasses(isDragging || false, isDragOver || false); + + return { + update(newOptions: DragAndDropOptions) { + index = newOptions.index; + onDragStart = newOptions.onDragStart; + onDragEnter = newOptions.onDragEnter; + onDrop = newOptions.onDrop; + onDragEnd = newOptions.onDragEnd; + + const newIsDragging = newOptions.isDragging || false; + const newIsDragOver = newOptions.isDragOver || false; + + if (newIsDragging !== isDragging || newIsDragOver !== isDragOver) { + isDragging = newIsDragging; + isDragOver = newIsDragOver; + updateClasses(isDragging, isDragOver); + } + }, + destroy() { + node.removeEventListener('dragstart', handleDragStart); + node.removeEventListener('dragenter', handleDragEnter); + node.removeEventListener('dragover', handleDragOver); + node.removeEventListener('drop', handleDrop); + node.removeEventListener('dragend', handleDragEnd); + node.removeEventListener('focusin', handleFocusIn); + node.removeEventListener('focusout', handleFocusOut); + }, + }; +} diff --git a/web/src/lib/assets/empty-workflows.svg b/web/src/lib/assets/empty-workflows.svg new file mode 100644 index 0000000000..f601ca984a --- /dev/null +++ b/web/src/lib/assets/empty-workflows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/lib/attachments/drag-and-drop.svelte.ts b/web/src/lib/attachments/drag-and-drop.svelte.ts new file mode 100644 index 0000000000..950e8e5b80 --- /dev/null +++ b/web/src/lib/attachments/drag-and-drop.svelte.ts @@ -0,0 +1,105 @@ +import type { Attachment } from 'svelte/attachments'; + +export interface DragAndDropOptions { + index: number; + onDragStart?: (index: number) => void; + onDragEnter?: (index: number) => void; + onDrop?: (e: DragEvent, index: number) => void; + onDragEnd?: () => void; + isDragging?: boolean; + isDragOver?: boolean; +} + +export function dragAndDrop(options: DragAndDropOptions): Attachment { + return (node: Element) => { + const element = node as HTMLElement; + const { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options; + + const isFormElement = (el: HTMLElement) => { + return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'; + }; + + const handleDragStart = (e: DragEvent) => { + // Prevent drag if it originated from an input, textarea, or select element + const target = e.target as HTMLElement; + if (isFormElement(target)) { + e.preventDefault(); + return; + } + onDragStart?.(index); + }; + + const handleDragEnter = () => { + onDragEnter?.(index); + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = (e: DragEvent) => { + onDrop?.(e, index); + }; + + const handleDragEnd = () => { + onDragEnd?.(); + }; + + // Disable draggable when focusing on form elements (fixes Firefox input interaction) + const handleFocusIn = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + element.setAttribute('draggable', 'false'); + } + }; + + const handleFocusOut = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + element.setAttribute('draggable', 'true'); + } + }; + + // Update classes based on drag state + const updateClasses = (dragging: boolean, dragOver: boolean) => { + // Remove all drag-related classes first + element.classList.remove('opacity-50', 'border-light-500', 'border-solid'); + + // Add back only the active ones + if (dragging) { + element.classList.add('opacity-50'); + } + + if (dragOver) { + element.classList.add('border-light-500', 'border-solid'); + element.classList.remove('border-transparent'); + } else { + element.classList.add('border-transparent'); + } + }; + + element.setAttribute('draggable', 'true'); + element.setAttribute('role', 'button'); + element.setAttribute('tabindex', '0'); + + element.addEventListener('dragstart', handleDragStart); + element.addEventListener('dragenter', handleDragEnter); + element.addEventListener('dragover', handleDragOver); + element.addEventListener('drop', handleDrop); + element.addEventListener('dragend', handleDragEnd); + element.addEventListener('focusin', handleFocusIn); + element.addEventListener('focusout', handleFocusOut); + + updateClasses(isDragging || false, isDragOver || false); + + return () => { + element.removeEventListener('dragstart', handleDragStart); + element.removeEventListener('dragenter', handleDragEnter); + element.removeEventListener('dragover', handleDragOver); + element.removeEventListener('drop', handleDrop); + element.removeEventListener('dragend', handleDragEnd); + element.removeEventListener('focusin', handleFocusIn); + element.removeEventListener('focusout', handleFocusOut); + }; + }; +} diff --git a/web/src/lib/components/utilities-page/utilities-menu.svelte b/web/src/lib/components/utilities-page/utilities-menu.svelte index bf7090e310..ef9be59467 100644 --- a/web/src/lib/components/utilities-page/utilities-menu.svelte +++ b/web/src/lib/components/utilities-page/utilities-menu.svelte @@ -9,6 +9,7 @@ mdiCrosshairsGps, mdiImageSizeSelectLarge, mdiLinkEdit, + mdiStateMachine, } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -16,6 +17,7 @@ { href: AppRoute.DUPLICATES, icon: mdiContentDuplicate, label: $t('review_duplicates') }, { href: AppRoute.LARGE_FILES, icon: mdiImageSizeSelectLarge, label: $t('review_large_files') }, { href: AppRoute.GEOLOCATION, icon: mdiCrosshairsGps, label: $t('manage_geolocation') }, + { href: AppRoute.WORKFLOWS, icon: mdiStateMachine, label: $t('workflows') }, ]; diff --git a/web/src/lib/components/workflows/SchemaFormFields.svelte b/web/src/lib/components/workflows/SchemaFormFields.svelte new file mode 100644 index 0000000000..8854b70904 --- /dev/null +++ b/web/src/lib/components/workflows/SchemaFormFields.svelte @@ -0,0 +1,171 @@ + + +{#if components} +
+ {#each Object.entries(components) as [key, component] (key)} + {@const label = component.title || component.label || key} + +
+ + {#if component.type === 'select'} + {#if isPickerField(component.subType)} + updateConfig(key, value)} + /> + {:else} + {@const options = component.options?.map((opt) => { + return { label: opt.label, value: String(opt.value) }; + }) || [{ label: 'N/A', value: '' }]} + {@const currentValue = actualConfig[key]} + {@const selectedItem = options.find((opt) => opt.value === String(currentValue)) ?? options[0]} + + + updateConfig(key, e.currentTarget.value)} + required={component.required} + /> + + {/if} +
+ {/each} +
+{:else} + No configuration required +{/if} diff --git a/web/src/lib/components/workflows/WorkflowCardConnector.svelte b/web/src/lib/components/workflows/WorkflowCardConnector.svelte new file mode 100644 index 0000000000..9666311980 --- /dev/null +++ b/web/src/lib/components/workflows/WorkflowCardConnector.svelte @@ -0,0 +1,42 @@ + + +
+
+
+ {#if animated} +
+ {/if} +
+
+
+
+
+
+
+
+ + diff --git a/web/src/lib/components/workflows/WorkflowJsonEditor.svelte b/web/src/lib/components/workflows/WorkflowJsonEditor.svelte new file mode 100644 index 0000000000..5401eeb5f0 --- /dev/null +++ b/web/src/lib/components/workflows/WorkflowJsonEditor.svelte @@ -0,0 +1,69 @@ + + + + + +
+
+ +
+ Workflow JSON + Edit the workflow configuration directly in JSON format +
+
+ +
+
+ + +
+ +
+
+
+
+
+ + diff --git a/web/src/lib/components/workflows/WorkflowPickerField.svelte b/web/src/lib/components/workflows/WorkflowPickerField.svelte new file mode 100644 index 0000000000..6ad4fdbeb2 --- /dev/null +++ b/web/src/lib/components/workflows/WorkflowPickerField.svelte @@ -0,0 +1,104 @@ + + + +
+ {#if pickerMetadata && !Array.isArray(pickerMetadata)} + + {:else if pickerMetadata && Array.isArray(pickerMetadata) && pickerMetadata.length > 0} +
+ {#each pickerMetadata as item (item.id)} + removeItemFromSelection(item.id)} /> + {/each} +
+ {/if} + +
+
diff --git a/web/src/lib/components/workflows/WorkflowPickerItemCard.svelte b/web/src/lib/components/workflows/WorkflowPickerItemCard.svelte new file mode 100644 index 0000000000..7b9a3088ea --- /dev/null +++ b/web/src/lib/components/workflows/WorkflowPickerItemCard.svelte @@ -0,0 +1,57 @@ + + + + +
+ {#if isAlbum && 'albumThumbnailAssetId' in item} + {#if item.albumThumbnailAssetId} + {item.albumName} + {:else} +
+ {/if} + {:else if !isAlbum && 'name' in item} + {item.name} + {/if} +
+
+ + {isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''} + + {#if isAlbum && 'assetCount' in item} + + {$t('items_count', { values: { count: item.assetCount } })} + + {/if} +
+ + +
+
diff --git a/web/src/lib/components/workflows/WorkflowSummary.svelte b/web/src/lib/components/workflows/WorkflowSummary.svelte new file mode 100644 index 0000000000..0a3e4d62f9 --- /dev/null +++ b/web/src/lib/components/workflows/WorkflowSummary.svelte @@ -0,0 +1,184 @@ + + +{#if isOpen} + + +{:else} + +{/if} diff --git a/web/src/lib/components/workflows/WorkflowTriggerCard.svelte b/web/src/lib/components/workflows/WorkflowTriggerCard.svelte new file mode 100644 index 0000000000..dd0421caf2 --- /dev/null +++ b/web/src/lib/components/workflows/WorkflowTriggerCard.svelte @@ -0,0 +1,80 @@ + + + diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index c389ebf2ef..38933aea85 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -55,6 +55,7 @@ export enum AppRoute { DUPLICATES = '/utilities/duplicates', LARGE_FILES = '/utilities/large-files', GEOLOCATION = '/utilities/geolocation', + WORKFLOWS = '/utilities/workflows', FOLDERS = '/folders', TAGS = '/tags', diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index 66a2db8787..dff27ef4fd 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -8,6 +8,7 @@ import type { SharedLinkResponseDto, SystemConfigDto, UserAdminResponseDto, + WorkflowResponseDto, } from '@immich/sdk'; export type Events = { @@ -42,6 +43,9 @@ export type Events = { LibraryUpdate: [LibraryResponseDto]; LibraryDelete: [{ id: string }]; + WorkflowUpdate: [WorkflowResponseDto]; + WorkflowDelete: [WorkflowResponseDto]; + ReleaseEvent: [ReleaseEvent]; }; diff --git a/web/src/lib/modals/AddWorkflowStepModal.svelte b/web/src/lib/modals/AddWorkflowStepModal.svelte new file mode 100644 index 0000000000..8a351f17d4 --- /dev/null +++ b/web/src/lib/modals/AddWorkflowStepModal.svelte @@ -0,0 +1,80 @@ + + +{#snippet stepButton(title: string, description?: string, onclick?: () => void)} + +{/snippet} + + onClose()}> + +
+ + {#if filters.length > 0 && (!type || type === 'filter')} +
+ {#each filters as filter (filter.id)} + {@render stepButton(filter.title, filter.description, () => handleSelect('filter', filter))} + {/each} +
+ {/if} + + + {#if actions.length > 0 && (!type || type === 'action')} +
+ {#each actions as action (action.id)} + {@render stepButton(action.title, action.description, () => handleSelect('action', action))} + {/each} +
+ {/if} +
+
+
diff --git a/web/src/lib/modals/PeoplePickerModal.svelte b/web/src/lib/modals/PeoplePickerModal.svelte new file mode 100644 index 0000000000..66e98885a3 --- /dev/null +++ b/web/src/lib/modals/PeoplePickerModal.svelte @@ -0,0 +1,108 @@ + + + + +
+ + +
+ {#if loading} +
+ +
+ {:else if filteredPeople.length > 0} +
+ {#each filteredPeople as person (person.id)} + {@const isSelected = selectedPeople.some((p) => p.id === person.id)} + + {/each} +
+ {:else} +

{$t('no_people_found')}

+ {/if} +
+
+
+ + {#if multiple} + + + + + + + {/if} +
diff --git a/web/src/lib/services/queue.service.ts b/web/src/lib/services/queue.service.ts index 46219ef22a..8217e73634 100644 --- a/web/src/lib/services/queue.service.ts +++ b/web/src/lib/services/queue.service.ts @@ -241,7 +241,7 @@ export const asQueueItem = ($t: MessageFormatter, queue: { name: QueueName }): Q }, [QueueName.Workflow]: { icon: mdiStateMachine, - title: $t('workflow'), + title: $t('workflows'), }, }; diff --git a/web/src/lib/services/workflow.service.ts b/web/src/lib/services/workflow.service.ts new file mode 100644 index 0000000000..9777f1ebcc --- /dev/null +++ b/web/src/lib/services/workflow.service.ts @@ -0,0 +1,451 @@ +import { goto } from '$app/navigation'; +import { AppRoute } from '$lib/constants'; +import { eventManager } from '$lib/managers/event-manager.svelte'; +import { handleError } from '$lib/utils/handle-error'; +import { getFormatter } from '$lib/utils/i18n'; +import { + createWorkflow, + deleteWorkflow, + getAlbumInfo, + getPerson, + PluginTriggerType, + updateWorkflow, + type AlbumResponseDto, + type PersonResponseDto, + type PluginActionResponseDto, + type PluginContextType, + type PluginFilterResponseDto, + type PluginTriggerResponseDto, + type WorkflowActionItemDto, + type WorkflowFilterItemDto, + type WorkflowResponseDto, + type WorkflowUpdateDto, +} from '@immich/sdk'; +import { modalManager, toastManager, type ActionItem } from '@immich/ui'; +import { mdiCodeJson, mdiDelete, mdiPause, mdiPencil, mdiPlay } from '@mdi/js'; +import type { MessageFormatter } from 'svelte-i18n'; + +export type PickerSubType = 'album-picker' | 'people-picker'; +export type PickerMetadata = AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]; + +export interface WorkflowPayload { + name: string; + description: string; + enabled: boolean; + triggerType: string; + filters: Record[]; + actions: Record[]; +} + +/** + * Get filters that support the given context + */ +export const getFiltersByContext = ( + availableFilters: PluginFilterResponseDto[], + context: PluginContextType, +): PluginFilterResponseDto[] => { + return availableFilters.filter((filter) => filter.supportedContexts.includes(context)); +}; + +/** + * Get actions that support the given context + */ +export const getActionsByContext = ( + availableActions: PluginActionResponseDto[], + context: PluginContextType, +): PluginActionResponseDto[] => { + return availableActions.filter((action) => action.supportedContexts.includes(context)); +}; + +export const remapConfigsOnReorder = ( + configs: Record, + prefix: 'filter' | 'action', + fromIndex: number, + toIndex: number, + totalCount: number, +): Record => { + const newConfigs: Record = {}; + + // Create an array of configs in order + const configArray: unknown[] = []; + for (let i = 0; i < totalCount; i++) { + configArray.push(configs[`${prefix}_${i}`] ?? {}); + } + + // Move the item from fromIndex to toIndex + const [movedItem] = configArray.splice(fromIndex, 1); + configArray.splice(toIndex, 0, movedItem); + + // Rebuild the configs object with new indices + for (let i = 0; i < configArray.length; i++) { + newConfigs[`${prefix}_${i}`] = configArray[i]; + } + + return newConfigs; +}; + +/** + * Remap configs when an item is removed + * Shifts all configs after the removed index down by one + */ +export const remapConfigsOnRemove = ( + configs: Record, + prefix: 'filter' | 'action', + removedIndex: number, + totalCount: number, +): Record => { + const newConfigs: Record = {}; + + let newIndex = 0; + for (let i = 0; i < totalCount; i++) { + if (i !== removedIndex) { + newConfigs[`${prefix}_${newIndex}`] = configs[`${prefix}_${i}`] ?? {}; + newIndex++; + } + } + + return newConfigs; +}; + +export const initializeConfigs = ( + type: 'action' | 'filter', + workflow: WorkflowResponseDto, +): Record => { + const configs: Record = {}; + + if (workflow.filters && type == 'filter') { + for (const [index, workflowFilter] of workflow.filters.entries()) { + configs[`filter_${index}`] = workflowFilter.filterConfig ?? {}; + } + } + + if (workflow.actions && type == 'action') { + for (const [index, workflowAction] of workflow.actions.entries()) { + configs[`action_${index}`] = workflowAction.actionConfig ?? {}; + } + } + + return configs; +}; + +/** + * Build workflow payload from current state + * Uses index-based keys to support multiple filters/actions of the same type + */ +export const buildWorkflowPayload = ( + name: string, + description: string, + enabled: boolean, + triggerType: string, + orderedFilters: PluginFilterResponseDto[], + orderedActions: PluginActionResponseDto[], + filterConfigs: Record, + actionConfigs: Record, +): WorkflowPayload => { + const filters = orderedFilters.map((filter, index) => ({ + [filter.methodName]: filterConfigs[`filter_${index}`] ?? {}, + })); + + const actions = orderedActions.map((action, index) => ({ + [action.methodName]: actionConfigs[`action_${index}`] ?? {}, + })); + + return { + name, + description, + enabled, + triggerType, + filters, + actions, + }; +}; + +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; + actionConfigs: Record; + }; +} => { + try { + const parsed = JSON.parse(jsonString); + + const trigger = availableTriggers.find((t) => t.type === parsed.triggerType); + + const filters: PluginFilterResponseDto[] = []; + const filterConfigs: Record = {}; + if (Array.isArray(parsed.filters)) { + for (const [index, filterObj] of parsed.filters.entries()) { + const methodName = Object.keys(filterObj)[0]; + const filter = availableFilters.find((f) => f.methodName === methodName); + if (filter) { + filters.push(filter); + filterConfigs[`filter_${index}`] = (filterObj as Record)[methodName]; + } + } + } + + const actions: PluginActionResponseDto[] = []; + const actionConfigs: Record = {}; + if (Array.isArray(parsed.actions)) { + for (const [index, actionObj] of parsed.actions.entries()) { + const methodName = Object.keys(actionObj)[0]; + const action = availableActions.find((a) => a.methodName === methodName); + if (action) { + actions.push(action); + actionConfigs[`action_${index}`] = (actionObj as Record)[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', + }; + } +}; + +export const hasWorkflowChanged = ( + previousWorkflow: WorkflowResponseDto, + enabled: boolean, + name: string, + description: string, + triggerType: string, + orderedFilters: PluginFilterResponseDto[], + orderedActions: PluginActionResponseDto[], + filterConfigs: Record, + actionConfigs: Record, +): boolean => { + if (enabled !== previousWorkflow.enabled) { + return true; + } + + if (name !== (previousWorkflow.name ?? '') || description !== (previousWorkflow.description ?? '')) { + return true; + } + + if (triggerType !== previousWorkflow.triggerType) { + return true; + } + + const previousFilterIds = previousWorkflow.filters?.map((f) => f.pluginFilterId) ?? []; + const currentFilterIds = orderedFilters.map((f) => f.id); + if (JSON.stringify(previousFilterIds) !== JSON.stringify(currentFilterIds)) { + return true; + } + + const previousActionIds = previousWorkflow.actions?.map((a) => a.pluginActionId) ?? []; + const currentActionIds = orderedActions.map((a) => a.id); + if (JSON.stringify(previousActionIds) !== JSON.stringify(currentActionIds)) { + return true; + } + + const previousFilterConfigs: Record = {}; + for (const [index, wf] of (previousWorkflow.filters ?? []).entries()) { + previousFilterConfigs[`filter_${index}`] = wf.filterConfig ?? {}; + } + if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) { + return true; + } + + const previousActionConfigs: Record = {}; + for (const [index, wa] of (previousWorkflow.actions ?? []).entries()) { + previousActionConfigs[`action_${index}`] = wa.actionConfig ?? {}; + } + if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) { + return true; + } + + return false; +}; + +export const handleUpdateWorkflow = async ( + workflowId: string, + name: string, + description: string, + enabled: boolean, + triggerType: PluginTriggerType, + orderedFilters: PluginFilterResponseDto[], + orderedActions: PluginActionResponseDto[], + filterConfigs: Record, + actionConfigs: Record, +): Promise => { + const filters = orderedFilters.map((filter, index) => ({ + pluginFilterId: filter.id, + filterConfig: filterConfigs[`filter_${index}`] ?? {}, + })) as WorkflowFilterItemDto[]; + + const actions = orderedActions.map((action, index) => ({ + pluginActionId: action.id, + actionConfig: actionConfigs[`action_${index}`] ?? {}, + })) as WorkflowActionItemDto[]; + + const updateDto: WorkflowUpdateDto = { + name, + description, + enabled, + filters, + actions, + triggerType, + }; + + return updateWorkflow({ id: workflowId, workflowUpdateDto: updateDto }); +}; + +export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowResponseDto) => { + const ToggleEnabled: ActionItem = { + title: workflow.enabled ? $t('disable') : $t('enable'), + icon: workflow.enabled ? mdiPause : mdiPlay, + color: workflow.enabled ? 'danger' : 'primary', + onAction: async () => { + await handleToggleWorkflowEnabled(workflow); + }, + }; + + const Edit: ActionItem = { + title: $t('edit'), + icon: mdiPencil, + onAction: () => handleNavigateToWorkflow(workflow), + }; + + const Delete: ActionItem = { + title: $t('delete'), + icon: mdiDelete, + color: 'danger', + onAction: async () => { + await handleDeleteWorkflow(workflow); + }, + }; + + return { ToggleEnabled, Edit, Delete }; +}; + +export const getWorkflowShowSchemaAction = ( + $t: MessageFormatter, + isExpanded: boolean, + onToggle: () => void, +): ActionItem => ({ + title: isExpanded ? $t('hide_schema') : $t('show_schema'), + icon: mdiCodeJson, + onAction: onToggle, +}); + +export const handleCreateWorkflow = async (): Promise => { + const $t = await getFormatter(); + + try { + const workflow = await createWorkflow({ + workflowCreateDto: { + name: $t('untitled_workflow'), + triggerType: PluginTriggerType.AssetCreate, + filters: [], + actions: [], + enabled: false, + }, + }); + + await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`); + return workflow; + } catch (error) { + handleError(error, $t('errors.unable_to_create')); + } +}; + +export const handleToggleWorkflowEnabled = async ( + workflow: WorkflowResponseDto, +): Promise => { + const $t = await getFormatter(); + + try { + const updated = await updateWorkflow({ + id: workflow.id, + workflowUpdateDto: { enabled: !workflow.enabled }, + }); + + eventManager.emit('WorkflowUpdate', updated); + toastManager.success($t('workflow_updated')); + return updated; + } catch (error) { + handleError(error, $t('errors.unable_to_update_workflow')); + } +}; + +export const handleDeleteWorkflow = async (workflow: WorkflowResponseDto): Promise => { + const $t = await getFormatter(); + + const confirmed = await modalManager.showDialog({ + prompt: $t('workflow_delete_prompt'), + confirmColor: 'danger', + }); + + if (!confirmed) { + return false; + } + + try { + await deleteWorkflow({ id: workflow.id }); + eventManager.emit('WorkflowDelete', workflow); + toastManager.success($t('workflow_deleted')); + return true; + } catch (error) { + handleError(error, $t('errors.unable_to_delete_workflow')); + return false; + } +}; + +export const handleNavigateToWorkflow = async (workflow: WorkflowResponseDto): Promise => { + await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`); +}; + +export const fetchPickerMetadata = async ( + value: string | string[] | undefined, + subType: PickerSubType, +): Promise => { + if (!value) { + return undefined; + } + + const isAlbum = subType === 'album-picker'; + + try { + if (Array.isArray(value) && value.length > 0) { + // Multiple selection + return isAlbum + ? await Promise.all(value.map((id) => getAlbumInfo({ id }))) + : await Promise.all(value.map((id) => getPerson({ id }))); + } else if (typeof value === 'string' && value) { + // Single selection + return isAlbum ? await getAlbumInfo({ id: value }) : await getPerson({ id: value }); + } + } catch (error) { + console.error(`Failed to fetch picker metadata:`, error); + } + + return undefined; +}; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 100f807273..5ae025f59c 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -162,7 +162,7 @@ export const getQueueName = derived(t, ($t) => { [QueueName.Notifications]: $t('notifications'), [QueueName.BackupDatabase]: $t('admin.backup_database'), [QueueName.Ocr]: $t('admin.machine_learning_ocr'), - [QueueName.Workflow]: $t('workflow'), + [QueueName.Workflow]: $t('workflows'), }; return names[name]; diff --git a/web/src/lib/utils/workflow.ts b/web/src/lib/utils/workflow.ts new file mode 100644 index 0000000000..4fd84af40b --- /dev/null +++ b/web/src/lib/utils/workflow.ts @@ -0,0 +1,128 @@ +export type ComponentType = 'select' | 'multiselect' | 'text' | 'switch' | 'checkbox'; + +export interface ComponentConfig { + type: ComponentType; + label?: string; + description?: string; + defaultValue?: unknown; + required?: boolean; + options?: Array<{ label: string; value: string | number | boolean }>; + placeholder?: string; + subType?: string; + title?: string; +} + +interface JSONSchemaProperty { + type?: string; + description?: string; + default?: unknown; + enum?: unknown[]; + items?: JSONSchemaProperty; + subType?: string; + title?: string; +} + +interface JSONSchema { + type?: string; + properties?: Record; + required?: string[]; +} + +export const getComponentDefaultValue = (component: ComponentConfig): unknown => { + if (component.defaultValue !== undefined) { + return component.defaultValue; + } + + if (component.type === 'multiselect' || (component.type === 'text' && component.subType === 'people-picker')) { + return []; + } + + if (component.type === 'switch') { + return false; + } + + return ''; +}; + +export const getComponentFromSchema = (schema: object | null): Record | null => { + if (!schema || !isJSONSchema(schema) || !schema.properties) { + return null; + } + + const components: Record = {}; + const requiredFields = schema.required || []; + + for (const [propertyName, property] of Object.entries(schema.properties)) { + const config = getComponentForProperty(property, propertyName); + if (config) { + config.required = requiredFields.includes(propertyName); + components[propertyName] = config; + } + } + + return Object.keys(components).length > 0 ? components : null; +}; + +function isJSONSchema(obj: object): obj is JSONSchema { + return 'properties' in obj || 'type' in obj; +} + +function getComponentForProperty(property: JSONSchemaProperty, propertyName: string): ComponentConfig | null { + const { type, title, enum: enumValues, description, default: defaultValue, items } = property; + + const config: ComponentConfig = { + type: 'text', + label: formatLabel(propertyName), + description, + defaultValue, + title, + }; + + if (enumValues && enumValues.length > 0) { + config.type = 'select'; + config.options = enumValues.map((value: unknown) => ({ + label: formatLabel(String(value)), + value: value as string | number | boolean, + })); + return config; + } + + if (type === 'array' && items?.enum && items.enum.length > 0) { + config.type = 'multiselect'; + config.subType = items.subType; + config.options = items.enum.map((value: unknown) => ({ + label: formatLabel(String(value)), + value: value as string | number | boolean, + })); + + return config; + } + + if (type === 'boolean') { + config.type = 'switch'; + return config; + } + + if (type === 'string') { + config.type = 'text'; + config.subType = property.subType; + config.placeholder = description; + return config; + } + + if (type === 'array') { + config.type = 'multiselect'; + config.subType = property.subType; + return config; + } + + return config; +} + +export function formatLabel(propertyName: string): string { + return propertyName + .replaceAll(/([A-Z])/g, ' $1') + .replaceAll('_', ' ') + .replace(/^./, (str) => str.toUpperCase()) + .trim(); +} diff --git a/web/src/routes/(user)/utilities/workflows/+page.svelte b/web/src/routes/(user)/utilities/workflows/+page.svelte new file mode 100644 index 0000000000..84924901c8 --- /dev/null +++ b/web/src/routes/(user)/utilities/workflows/+page.svelte @@ -0,0 +1,281 @@ + + + + +{#snippet chipItem(title: string)} + + {title} + +{/snippet} + + + {#snippet buttons()} + + + + {/snippet} + +
+
+ {#if workflows.length === 0} + + {:else} +
+ {#each workflows as workflow (workflow.id)} + + +
+
+ + {workflow.name} +
+ + {workflow.description || $t('workflows_help_text')} + +
+ +
+ + showWorkflowMenu(event, workflow)} + /> +
+
+ + +
+ +
+
+ {$t('trigger')} +
+ {@render chipItem(getTriggerLabel(workflow.triggerType))} +
+ + +
+
+ {$t('filters')} +
+
+ {#if workflow.filters.length === 0} + + {$t('no_filters_added')} + + {:else} + {#each workflow.filters as workflowFilter (workflowFilter.id)} + {@render chipItem(getFilterLabel(workflowFilter.pluginFilterId))} + {/each} + {/if} +
+
+ + +
+
+ {$t('actions')} +
+ +
+ {#if workflow.actions.length === 0} + + {$t('no_actions_added')} + + {:else} +
+ {#each workflow.actions as workflowAction (workflowAction.id)} + {@render chipItem(getActionLabel(workflowAction.pluginActionId))} + {/each} +
+ {/if} +
+
+
+ + {#if expandedWorkflows.has(workflow.id)} + + + + + {/if} +
+
+ {/each} +
+ {/if} +
+
+
diff --git a/web/src/routes/(user)/utilities/workflows/+page.ts b/web/src/routes/(user)/utilities/workflows/+page.ts new file mode 100644 index 0000000000..4707fce9cd --- /dev/null +++ b/web/src/routes/(user)/utilities/workflows/+page.ts @@ -0,0 +1,18 @@ +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getPlugins, getWorkflows } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async ({ url }) => { + await authenticate(url); + const [workflows, plugins] = await Promise.all([getWorkflows(), getPlugins()]); + const $t = await getFormatter(); + + return { + workflows, + plugins, + meta: { + title: $t('workflows'), + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte b/web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte new file mode 100644 index 0000000000..eb47abb767 --- /dev/null +++ b/web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte @@ -0,0 +1,619 @@ + + +{#snippet cardOrder(index: number)} +
+ + {index + 1} + +
+{/snippet} + +{#snippet stepSeparator()} +
+ +
+ THEN +
+
+{/snippet} + +{#snippet emptyCreateButton(title: string, description: string, onclick: () => Promise)} + +{/snippet} + + + {data.meta.title} - Immich + + +
+ + {#if viewMode === 'json'} + (jsonEditorContent = content)} + /> + {:else} + + + +
+ +
+ + {$t('workflow_info')} + +
+
+
+ + + +
+ + + +
+ + + + + +