mirror of
https://github.com/immich-app/immich.git
synced 2025-12-19 01:11:07 +03:00
wip
This commit is contained in:
71
i18n/en.json
71
i18n/en.json
@@ -14,9 +14,11 @@
|
|||||||
"add_a_location": "Add a location",
|
"add_a_location": "Add a location",
|
||||||
"add_a_name": "Add a name",
|
"add_a_name": "Add a name",
|
||||||
"add_a_title": "Add a title",
|
"add_a_title": "Add a title",
|
||||||
|
"add_action": "Add action",
|
||||||
"add_birthday": "Add a birthday",
|
"add_birthday": "Add a birthday",
|
||||||
"add_endpoint": "Add endpoint",
|
"add_endpoint": "Add endpoint",
|
||||||
"add_exclusion_pattern": "Add exclusion pattern",
|
"add_exclusion_pattern": "Add exclusion pattern",
|
||||||
|
"add_filter": "Add filter",
|
||||||
"add_location": "Add location",
|
"add_location": "Add location",
|
||||||
"add_more_users": "Add more users",
|
"add_more_users": "Add more users",
|
||||||
"add_partner": "Add partner",
|
"add_partner": "Add partner",
|
||||||
@@ -454,6 +456,7 @@
|
|||||||
"album_remove_user": "Remove user?",
|
"album_remove_user": "Remove user?",
|
||||||
"album_remove_user_confirmation": "Are you sure you want to 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_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_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_summary": "Album summary",
|
||||||
"album_updated": "Album updated",
|
"album_updated": "Album updated",
|
||||||
@@ -475,6 +478,7 @@
|
|||||||
"albums_default_sort_order_description": "Initial asset sort order when creating new albums.",
|
"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_feature_description": "Collections of assets that can be shared with other users.",
|
||||||
"albums_on_device_count": "Albums on device ({count})",
|
"albums_on_device_count": "Albums on device ({count})",
|
||||||
|
"albums_selected": "{count, plural, one {# album selected} other {# albums selected}}",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"all_albums": "All albums",
|
"all_albums": "All albums",
|
||||||
"all_people": "All people",
|
"all_people": "All people",
|
||||||
@@ -511,10 +515,12 @@
|
|||||||
"archived_count": "{count, plural, other {Archived #}}",
|
"archived_count": "{count, plural, other {Archived #}}",
|
||||||
"are_these_the_same_person": "Are these the same person?",
|
"are_these_the_same_person": "Are these the same person?",
|
||||||
"are_you_sure_to_do_this": "Are you sure you want to do this?",
|
"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_delete_err_read_only": "Cannot delete read only asset(s), skipping",
|
||||||
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
|
||||||
"asset_added_to_album": "Added to album",
|
"asset_added_to_album": "Added to album",
|
||||||
"asset_adding_to_album": "Adding to album…",
|
"asset_adding_to_album": "Adding to album…",
|
||||||
|
"asset_created": "Asset created",
|
||||||
"asset_description_updated": "Asset description has been updated",
|
"asset_description_updated": "Asset description has been updated",
|
||||||
"asset_filename_is_offline": "Asset {filename} is offline",
|
"asset_filename_is_offline": "Asset {filename} is offline",
|
||||||
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
||||||
@@ -697,6 +703,8 @@
|
|||||||
"change_password_form_password_mismatch": "Passwords do not match",
|
"change_password_form_password_mismatch": "Passwords do not match",
|
||||||
"change_password_form_reenter_new_password": "Re-enter New Password",
|
"change_password_form_reenter_new_password": "Re-enter New Password",
|
||||||
"change_pin_code": "Change PIN code",
|
"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",
|
"change_your_password": "Change your password",
|
||||||
"changed_visibility_successfully": "Changed visibility successfully",
|
"changed_visibility_successfully": "Changed visibility successfully",
|
||||||
"charging": "Charging",
|
"charging": "Charging",
|
||||||
@@ -771,6 +779,7 @@
|
|||||||
"create_album": "Create album",
|
"create_album": "Create album",
|
||||||
"create_album_page_untitled": "Untitled",
|
"create_album_page_untitled": "Untitled",
|
||||||
"create_api_key": "Create API key",
|
"create_api_key": "Create API key",
|
||||||
|
"create_first_workflow": "Create first workflow",
|
||||||
"create_library": "Create Library",
|
"create_library": "Create Library",
|
||||||
"create_link": "Create link",
|
"create_link": "Create link",
|
||||||
"create_link_to_share": "Create link to share",
|
"create_link_to_share": "Create link to share",
|
||||||
@@ -785,6 +794,7 @@
|
|||||||
"create_tag": "Create tag",
|
"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_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_user": "Create user",
|
||||||
|
"create_workflow": "Create workflow",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"created_at": "Created",
|
"created_at": "Created",
|
||||||
"creating_linked_albums": "Creating linked albums...",
|
"creating_linked_albums": "Creating linked albums...",
|
||||||
@@ -913,16 +923,19 @@
|
|||||||
"edit_tag": "Edit tag",
|
"edit_tag": "Edit tag",
|
||||||
"edit_title": "Edit Title",
|
"edit_title": "Edit Title",
|
||||||
"edit_user": "Edit user",
|
"edit_user": "Edit user",
|
||||||
|
"edit_workflow": "Edit workflow",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
"editor_close_without_save_prompt": "The changes will not be saved",
|
"editor_close_without_save_prompt": "The changes will not be saved",
|
||||||
"editor_close_without_save_title": "Close editor?",
|
"editor_close_without_save_title": "Close editor?",
|
||||||
"editor_crop_tool_h2_aspect_ratios": "Aspect ratios",
|
"editor_crop_tool_h2_aspect_ratios": "Aspect ratios",
|
||||||
"editor_crop_tool_h2_rotation": "Rotation",
|
"editor_crop_tool_h2_rotation": "Rotation",
|
||||||
|
"editor_mode": "Editor mode",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"email_notifications": "Email notifications",
|
"email_notifications": "Email notifications",
|
||||||
"empty_folder": "This folder is empty",
|
"empty_folder": "This folder is empty",
|
||||||
"empty_trash": "Empty trash",
|
"empty_trash": "Empty trash",
|
||||||
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
|
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
|
||||||
|
"disable": "Disable",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"enable_backup": "Enable Backup",
|
"enable_backup": "Enable Backup",
|
||||||
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
|
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
|
||||||
@@ -1156,6 +1169,7 @@
|
|||||||
"hi_user": "Hi {name} ({email})",
|
"hi_user": "Hi {name} ({email})",
|
||||||
"hide_all_people": "Hide all people",
|
"hide_all_people": "Hide all people",
|
||||||
"hide_gallery": "Hide gallery",
|
"hide_gallery": "Hide gallery",
|
||||||
|
"hide_json": "Hide JSON",
|
||||||
"hide_named_person": "Hide person {name}",
|
"hide_named_person": "Hide person {name}",
|
||||||
"hide_password": "Hide password",
|
"hide_password": "Hide password",
|
||||||
"hide_person": "Hide person",
|
"hide_person": "Hide person",
|
||||||
@@ -1231,6 +1245,8 @@
|
|||||||
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
|
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
|
||||||
"items_count": "{count, plural, one {# item} other {# items}}",
|
"items_count": "{count, plural, one {# item} other {# items}}",
|
||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
|
"json_editor": "JSON editor",
|
||||||
|
"json_error": "JSON error",
|
||||||
"keep": "Keep",
|
"keep": "Keep",
|
||||||
"keep_all": "Keep All",
|
"keep_all": "Keep All",
|
||||||
"keep_this_delete_others": "Keep this, delete others",
|
"keep_this_delete_others": "Keep this, delete others",
|
||||||
@@ -1399,11 +1415,13 @@
|
|||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"move": "Move",
|
"move": "Move",
|
||||||
|
"move_down": "Move down",
|
||||||
"move_off_locked_folder": "Move out of locked folder",
|
"move_off_locked_folder": "Move out of locked folder",
|
||||||
"move_to": "Move to",
|
"move_to": "Move to",
|
||||||
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
||||||
"move_to_locked_folder": "Move to 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_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_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
|
||||||
"moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
|
"moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
|
||||||
"moved_to_trash": "Moved to trash",
|
"moved_to_trash": "Moved to trash",
|
||||||
@@ -1413,6 +1431,7 @@
|
|||||||
"my_albums": "My albums",
|
"my_albums": "My albums",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_or_nickname": "Name or nickname",
|
"name_or_nickname": "Name or nickname",
|
||||||
|
"name_required": "Name is required",
|
||||||
"navigate": "Navigate",
|
"navigate": "Navigate",
|
||||||
"navigate_to_time": "Navigate to Time",
|
"navigate_to_time": "Navigate to Time",
|
||||||
"network_requirement_photos_upload": "Use cellular data to backup photos",
|
"network_requirement_photos_upload": "Use cellular data to backup photos",
|
||||||
@@ -1437,6 +1456,7 @@
|
|||||||
"next": "Next",
|
"next": "Next",
|
||||||
"next_memory": "Next memory",
|
"next_memory": "Next memory",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
|
"no_actions_added": "No actions added yet",
|
||||||
"no_albums_message": "Create an album to organize your photos and videos",
|
"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_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.",
|
"no_albums_yet": "It looks like you do not have any albums yet.",
|
||||||
@@ -1446,11 +1466,13 @@
|
|||||||
"no_cast_devices_found": "No cast devices found",
|
"no_cast_devices_found": "No cast devices found",
|
||||||
"no_checksum_local": "No checksum available - cannot fetch local assets",
|
"no_checksum_local": "No checksum available - cannot fetch local assets",
|
||||||
"no_checksum_remote": "No checksum available - cannot fetch remote asset",
|
"no_checksum_remote": "No checksum available - cannot fetch remote asset",
|
||||||
|
"no_configuration_needed": "No configuration needed",
|
||||||
"no_devices": "No authorized devices",
|
"no_devices": "No authorized devices",
|
||||||
"no_duplicates_found": "No duplicates were found.",
|
"no_duplicates_found": "No duplicates were found.",
|
||||||
"no_exif_info_available": "No exif info available",
|
"no_exif_info_available": "No exif info available",
|
||||||
"no_explore_results_message": "Upload more photos to explore your collection.",
|
"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_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_libraries_message": "Create an external library to view your photos and videos",
|
||||||
"no_local_assets_found": "No local assets found with this checksum",
|
"no_local_assets_found": "No local assets found with this checksum",
|
||||||
"no_location_set": "No location set",
|
"no_location_set": "No location set",
|
||||||
@@ -1464,6 +1486,7 @@
|
|||||||
"no_results_description": "Try a synonym or more general keyword",
|
"no_results_description": "Try a synonym or more general keyword",
|
||||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||||
"no_uploads_in_progress": "No uploads in progress",
|
"no_uploads_in_progress": "No uploads in progress",
|
||||||
|
"no_workflows_yet": "No workflows yet",
|
||||||
"not_allowed": "Not allowed",
|
"not_allowed": "Not allowed",
|
||||||
"not_available": "N/A",
|
"not_available": "N/A",
|
||||||
"not_in_any_album": "Not in any album",
|
"not_in_any_album": "Not in any album",
|
||||||
@@ -1545,6 +1568,7 @@
|
|||||||
"people": "People",
|
"people": "People",
|
||||||
"people_edits_count": "Edited {count, plural, one {# person} other {# people}}",
|
"people_edits_count": "Edited {count, plural, one {# person} other {# people}}",
|
||||||
"people_feature_description": "Browsing photos and videos grouped by 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",
|
"people_sidebar_description": "Display a link to People in the sidebar",
|
||||||
"permanent_deletion_warning": "Permanent deletion warning",
|
"permanent_deletion_warning": "Permanent deletion warning",
|
||||||
"permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets",
|
"permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets",
|
||||||
@@ -1569,6 +1593,8 @@
|
|||||||
"person_age_years": "{years, plural, other {# years}} old",
|
"person_age_years": "{years, plural, other {# years}} old",
|
||||||
"person_birthdate": "Born on {date}",
|
"person_birthdate": "Born on {date}",
|
||||||
"person_hidden": "{name}{hidden, select, true { (hidden)} other {}}",
|
"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.",
|
"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": "Photos",
|
||||||
"photos_and_videos": "Photos & Videos",
|
"photos_and_videos": "Photos & Videos",
|
||||||
@@ -1821,23 +1847,19 @@
|
|||||||
"select_album": "Select album",
|
"select_album": "Select album",
|
||||||
"select_album_cover": "Select album cover",
|
"select_album_cover": "Select album cover",
|
||||||
"select_albums": "Select albums",
|
"select_albums": "Select albums",
|
||||||
"album_selected": "Album selected",
|
|
||||||
"albums_selected": "{count, plural, one {# album selected} other {# albums selected}}",
|
|
||||||
"select_person": "Select person",
|
|
||||||
"select_people": "Select people",
|
|
||||||
"person_selected": "Person selected",
|
|
||||||
"people_selected": "{count, plural, one {# person selected} other {# people selected}}",
|
|
||||||
"select_count": "{count, plural, one {Select #} other {Select #}}",
|
|
||||||
"select_all": "Select all",
|
"select_all": "Select all",
|
||||||
"select_all_duplicates": "Select all duplicates",
|
"select_all_duplicates": "Select all duplicates",
|
||||||
"select_all_in": "Select all in {group}",
|
"select_all_in": "Select all in {group}",
|
||||||
"select_avatar_color": "Select avatar color",
|
"select_avatar_color": "Select avatar color",
|
||||||
|
"select_count": "{count, plural, one {Select #} other {Select #}}",
|
||||||
"select_face": "Select face",
|
"select_face": "Select face",
|
||||||
"select_featured_photo": "Select featured photo",
|
"select_featured_photo": "Select featured photo",
|
||||||
"select_from_computer": "Select from computer",
|
"select_from_computer": "Select from computer",
|
||||||
"select_keep_all": "Select keep all",
|
"select_keep_all": "Select keep all",
|
||||||
"select_library_owner": "Select library owner",
|
"select_library_owner": "Select library owner",
|
||||||
"select_new_face": "Select new face",
|
"select_new_face": "Select new face",
|
||||||
|
"select_people": "Select people",
|
||||||
|
"select_person": "Select person",
|
||||||
"select_person_to_tag": "Select a person to tag",
|
"select_person_to_tag": "Select a person to tag",
|
||||||
"select_photos": "Select photos",
|
"select_photos": "Select photos",
|
||||||
"select_trash_all": "Select trash all",
|
"select_trash_all": "Select trash all",
|
||||||
@@ -1967,6 +1989,7 @@
|
|||||||
"show_hidden_people": "Show hidden people",
|
"show_hidden_people": "Show hidden people",
|
||||||
"show_in_timeline": "Show in timeline",
|
"show_in_timeline": "Show in timeline",
|
||||||
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
|
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
|
||||||
|
"show_json": "Show JSON",
|
||||||
"show_keyboard_shortcuts": "Show keyboard shortcuts",
|
"show_keyboard_shortcuts": "Show keyboard shortcuts",
|
||||||
"show_metadata": "Show metadata",
|
"show_metadata": "Show metadata",
|
||||||
"show_or_hide_info": "Show or hide info",
|
"show_or_hide_info": "Show or hide info",
|
||||||
@@ -2099,6 +2122,8 @@
|
|||||||
"trash_page_select_assets_btn": "Select assets",
|
"trash_page_select_assets_btn": "Select assets",
|
||||||
"trash_page_title": "Trash ({count})",
|
"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}}.",
|
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
||||||
|
"trigger": "Trigger",
|
||||||
|
"trigger_type": "Trigger type",
|
||||||
"troubleshoot": "Troubleshoot",
|
"troubleshoot": "Troubleshoot",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||||
@@ -2129,7 +2154,9 @@
|
|||||||
"unstack": "Un-stack",
|
"unstack": "Un-stack",
|
||||||
"unstack_action_prompt": "{count} unstacked",
|
"unstack_action_prompt": "{count} unstacked",
|
||||||
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
||||||
|
"unsupported_field_type": "Unsupported field type",
|
||||||
"untagged": "Untagged",
|
"untagged": "Untagged",
|
||||||
|
"untitled_workflow": "Untitled workflow",
|
||||||
"up_next": "Up next",
|
"up_next": "Up next",
|
||||||
"update_location_action_prompt": "Update the location of {count} selected assets with:",
|
"update_location_action_prompt": "Update the location of {count} selected assets with:",
|
||||||
"updated_at": "Updated",
|
"updated_at": "Updated",
|
||||||
@@ -2175,6 +2202,7 @@
|
|||||||
"utilities": "Utilities",
|
"utilities": "Utilities",
|
||||||
"validate": "Validate",
|
"validate": "Validate",
|
||||||
"validate_endpoint_error": "Please enter a valid URL",
|
"validate_endpoint_error": "Please enter a valid URL",
|
||||||
|
"validation_error": "Validation error",
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"version_announcement_closing": "Your friend, Alex",
|
"version_announcement_closing": "Your friend, Alex",
|
||||||
@@ -2205,6 +2233,7 @@
|
|||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
"viewer_unstack": "Un-Stack",
|
"viewer_unstack": "Un-Stack",
|
||||||
"visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}",
|
"visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}",
|
||||||
|
"visual_builder": "Visual builder",
|
||||||
"waiting": "Waiting",
|
"waiting": "Waiting",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
"week": "Week",
|
"week": "Week",
|
||||||
@@ -2215,6 +2244,7 @@
|
|||||||
"workflow_deleted": "Workflow deleted",
|
"workflow_deleted": "Workflow deleted",
|
||||||
"workflow_json": "Workflow JSON",
|
"workflow_json": "Workflow JSON",
|
||||||
"workflow_json_help": "Edit the workflow configuration in JSON format. Changes will sync to the visual builder.",
|
"workflow_json_help": "Edit the workflow configuration in JSON format. Changes will sync to the visual builder.",
|
||||||
|
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
|
||||||
"workflow_updated": "Workflow updated",
|
"workflow_updated": "Workflow updated",
|
||||||
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters.",
|
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters.",
|
||||||
"wrong_pin_code": "Wrong PIN code",
|
"wrong_pin_code": "Wrong PIN code",
|
||||||
@@ -2224,30 +2254,5 @@
|
|||||||
"you_dont_have_any_shared_links": "You don't have any shared links",
|
"you_dont_have_any_shared_links": "You don't have any shared links",
|
||||||
"your_wifi_name": "Your Wi-Fi name",
|
"your_wifi_name": "Your Wi-Fi name",
|
||||||
"zoom_image": "Zoom Image",
|
"zoom_image": "Zoom Image",
|
||||||
"zoom_to_bounds": "Zoom to bounds",
|
"zoom_to_bounds": "Zoom to bounds"
|
||||||
"add_action": "Add action",
|
|
||||||
"add_filter": "Add filter",
|
|
||||||
"array_field_not_fully_supported": "Array fields require manual JSON editing",
|
|
||||||
"asset_created": "Asset created",
|
|
||||||
"create_first_workflow": "Create first workflow",
|
|
||||||
"create_workflow": "Create workflow",
|
|
||||||
"edit_workflow": "Edit workflow",
|
|
||||||
"editor_mode": "Editor mode",
|
|
||||||
"hide_json": "Hide JSON",
|
|
||||||
"json_editor": "JSON editor",
|
|
||||||
"json_error": "JSON error",
|
|
||||||
"move_down": "Move down",
|
|
||||||
"move_up": "Move up",
|
|
||||||
"name_required": "Name is required",
|
|
||||||
"no_actions_added": "No actions added yet",
|
|
||||||
"no_configuration_needed": "No configuration needed",
|
|
||||||
"no_filters_added": "No filters added yet",
|
|
||||||
"no_workflows_yet": "No workflows yet",
|
|
||||||
"person_recognized": "Person recognized",
|
|
||||||
"show_json": "Show JSON",
|
|
||||||
"trigger_type": "Trigger type",
|
|
||||||
"unsupported_field_type": "Unsupported field type",
|
|
||||||
"untitled_workflow": "Untitled workflow",
|
|
||||||
"validation_error": "Validation error",
|
|
||||||
"visual_builder": "Visual builder"
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
mobile/openapi/lib/model/workflow_update_dto.dart
generated
19
mobile/openapi/lib/model/workflow_update_dto.dart
generated
@@ -18,7 +18,7 @@ class WorkflowUpdateDto {
|
|||||||
this.enabled,
|
this.enabled,
|
||||||
this.filters = const [],
|
this.filters = const [],
|
||||||
this.name,
|
this.name,
|
||||||
required this.triggerType,
|
this.triggerType,
|
||||||
});
|
});
|
||||||
|
|
||||||
List<WorkflowActionItemDto> actions;
|
List<WorkflowActionItemDto> actions;
|
||||||
@@ -49,7 +49,13 @@ class WorkflowUpdateDto {
|
|||||||
///
|
///
|
||||||
String? name;
|
String? name;
|
||||||
|
|
||||||
PluginTriggerType triggerType;
|
///
|
||||||
|
/// 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
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowUpdateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is WorkflowUpdateDto &&
|
||||||
@@ -68,7 +74,7 @@ class WorkflowUpdateDto {
|
|||||||
(enabled == null ? 0 : enabled!.hashCode) +
|
(enabled == null ? 0 : enabled!.hashCode) +
|
||||||
(filters.hashCode) +
|
(filters.hashCode) +
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
(triggerType.hashCode);
|
(triggerType == null ? 0 : triggerType!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name, triggerType=$triggerType]';
|
String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name, triggerType=$triggerType]';
|
||||||
@@ -92,7 +98,11 @@ class WorkflowUpdateDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'name'] = null;
|
// json[r'name'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.triggerType != null) {
|
||||||
json[r'triggerType'] = this.triggerType;
|
json[r'triggerType'] = this.triggerType;
|
||||||
|
} else {
|
||||||
|
// json[r'triggerType'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +120,7 @@ class WorkflowUpdateDto {
|
|||||||
enabled: mapValueOfType<bool>(json, r'enabled'),
|
enabled: mapValueOfType<bool>(json, r'enabled'),
|
||||||
filters: WorkflowFilterItemDto.listFromJson(json[r'filters']),
|
filters: WorkflowFilterItemDto.listFromJson(json[r'filters']),
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
triggerType: PluginTriggerType.fromJson(json[r'triggerType'])!,
|
triggerType: PluginTriggerType.fromJson(json[r'triggerType']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -158,7 +168,6 @@ class WorkflowUpdateDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'triggerType',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22985,9 +22985,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
|
||||||
"triggerType"
|
|
||||||
],
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1763,7 +1763,7 @@ export type WorkflowUpdateDto = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
filters?: WorkflowFilterItemDto[];
|
filters?: WorkflowFilterItemDto[];
|
||||||
name?: string;
|
name?: string;
|
||||||
triggerType: PluginTriggerType;
|
triggerType?: PluginTriggerType;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* List all activities
|
* List all activities
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export class WorkflowCreateDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class WorkflowUpdateDto {
|
export class WorkflowUpdateDto {
|
||||||
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' })
|
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', optional: true })
|
||||||
triggerType!: PluginTriggerType;
|
triggerType?: PluginTriggerType;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class WorkflowService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workflow = await this.findOrFail(id);
|
const workflow = await this.findOrFail(id);
|
||||||
const trigger = this.getTriggerOrFail(workflow.triggerType);
|
const trigger = this.getTriggerOrFail(dto.triggerType ?? workflow.triggerType);
|
||||||
|
|
||||||
const { filters, actions, ...workflowUpdate } = dto;
|
const { filters, actions, ...workflowUpdate } = dto;
|
||||||
const filterInserts = filters && (await this.validateAndMapFilters(filters, trigger.context));
|
const filterInserts = filters && (await this.validateAndMapFilters(filters, trigger.context));
|
||||||
|
|||||||
@@ -611,6 +611,52 @@ describe(WorkflowService.name, () => {
|
|||||||
sut.update(auth, created.id, { actions: [{ actionId: factory.uuid(), actionConfig: {} }] }),
|
sut.update(auth, created.id, { actions: [{ actionId: factory.uuid(), actionConfig: {} }] }),
|
||||||
).rejects.toThrow();
|
).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: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await sut.update(auth, created.id, {
|
||||||
|
triggerType: PluginTriggerType.AssetCreate,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updated.triggerType).toBe(PluginTriggerType.AssetCreate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use existing trigger type when triggerType not provided in update', 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: [{ filterId: testFilterId }],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await sut.update(auth, created.id, {
|
||||||
|
filters: [
|
||||||
|
{ filterId: testFilterId, filterConfig: { updated: true } },
|
||||||
|
{ filterId: testFilterId, filterConfig: { second: true } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updated.triggerType).toBe(PluginTriggerType.AssetCreate);
|
||||||
|
expect(updated.filters).toHaveLength(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ActionBuilder from '$lib/components/workflow/ActionBuilder.svelte';
|
|
||||||
import FilterBuilder from '$lib/components/workflow/FilterBuilder.svelte';
|
|
||||||
import GroupTab from '$lib/elements/GroupTab.svelte';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import {
|
|
||||||
createWorkflow,
|
|
||||||
PluginTriggerType,
|
|
||||||
updateWorkflow,
|
|
||||||
type PluginResponseDto,
|
|
||||||
type WorkflowResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Switch, Textarea } from '@immich/ui';
|
|
||||||
import { mdiAutoFix } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
workflow?: WorkflowResponseDto;
|
|
||||||
plugins: PluginResponseDto[];
|
|
||||||
onClose: () => void;
|
|
||||||
onSave: (workflow: WorkflowResponseDto) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { workflow, plugins, onClose, onSave }: Props = $props();
|
|
||||||
|
|
||||||
const isEditMode = !!workflow;
|
|
||||||
|
|
||||||
// Form state
|
|
||||||
let name = $state(workflow?.name || '');
|
|
||||||
let description = $state(workflow?.description || '');
|
|
||||||
let triggerType = $state<PluginTriggerType>(workflow?.triggerType || PluginTriggerType.AssetCreate);
|
|
||||||
let enabled = $state(workflow?.enabled ?? true);
|
|
||||||
let filters = $state<Array<{ filterId: string; filterConfig?: object }>>(
|
|
||||||
workflow?.filters.map((f) => ({ filterId: f.filterId, filterConfig: f.filterConfig || undefined })) || [],
|
|
||||||
);
|
|
||||||
let actions = $state<Array<{ actionId: string; actionConfig?: object }>>(
|
|
||||||
workflow?.actions.map((a) => ({ actionId: a.actionId, actionConfig: a.actionConfig || undefined })) || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Editor mode state
|
|
||||||
let editorMode = $state<'visual' | 'json'>('visual');
|
|
||||||
let jsonText = $state('');
|
|
||||||
let jsonError = $state('');
|
|
||||||
|
|
||||||
// Sync JSON when switching to JSON mode
|
|
||||||
const syncToJson = () => {
|
|
||||||
const workflowData = {
|
|
||||||
...(isEditMode ? { id: workflow!.id } : {}),
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
triggerType,
|
|
||||||
enabled,
|
|
||||||
filters,
|
|
||||||
actions,
|
|
||||||
};
|
|
||||||
jsonText = JSON.stringify(workflowData, null, 2);
|
|
||||||
jsonError = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sync visual form when switching from JSON mode
|
|
||||||
const syncFromJson = () => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(jsonText);
|
|
||||||
name = parsed.name || '';
|
|
||||||
description = parsed.description || '';
|
|
||||||
triggerType = parsed.triggerType || PluginTriggerType.AssetCreate;
|
|
||||||
enabled = parsed.enabled ?? true;
|
|
||||||
filters = parsed.filters || [];
|
|
||||||
actions = parsed.actions || [];
|
|
||||||
jsonError = '';
|
|
||||||
} catch (error) {
|
|
||||||
jsonError = error instanceof Error ? error.message : 'Invalid JSON';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModeChange = (newMode: 'visual' | 'json') => {
|
|
||||||
if (newMode === 'json' && editorMode === 'visual') {
|
|
||||||
syncToJson();
|
|
||||||
} else if (newMode === 'visual' && editorMode === 'json') {
|
|
||||||
syncFromJson();
|
|
||||||
}
|
|
||||||
editorMode = newMode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
// If in JSON mode, sync from JSON first
|
|
||||||
if (editorMode === 'json') {
|
|
||||||
syncFromJson();
|
|
||||||
if (jsonError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name.trim()) {
|
|
||||||
handleError(new Error($t('name_required')), $t('validation_error'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trigger =
|
|
||||||
triggerType === PluginTriggerType.AssetCreate
|
|
||||||
? PluginTriggerType.AssetCreate
|
|
||||||
: PluginTriggerType.PersonRecognized;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result: WorkflowResponseDto;
|
|
||||||
result = await (isEditMode
|
|
||||||
? updateWorkflow({
|
|
||||||
id: workflow!.id,
|
|
||||||
workflowUpdateDto: {
|
|
||||||
name,
|
|
||||||
description: description || undefined,
|
|
||||||
enabled,
|
|
||||||
filters,
|
|
||||||
actions,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: createWorkflow({
|
|
||||||
workflowCreateDto: {
|
|
||||||
name,
|
|
||||||
description: description || undefined,
|
|
||||||
triggerType: trigger,
|
|
||||||
enabled,
|
|
||||||
filters,
|
|
||||||
actions,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
onSave(result);
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, isEditMode ? $t('errors.unable_to_create') : $t('errors.unable_to_create'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal title={isEditMode ? $t('edit_workflow') : $t('create_workflow')} icon={mdiAutoFix} {onClose} size="large">
|
|
||||||
<ModalBody>
|
|
||||||
<div class="mb-4">
|
|
||||||
<GroupTab
|
|
||||||
filters={['visual', 'json']}
|
|
||||||
labels={[$t('visual_builder'), $t('json_editor')]}
|
|
||||||
selected={editorMode}
|
|
||||||
label={$t('editor_mode')}
|
|
||||||
onSelect={(mode) => handleModeChange(mode as 'visual' | 'json')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if editorMode === 'visual'}
|
|
||||||
<form
|
|
||||||
id="workflow-form"
|
|
||||||
onsubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
void handleSubmit();
|
|
||||||
}}
|
|
||||||
class="mt-4 flex flex-col gap-4"
|
|
||||||
>
|
|
||||||
<Field label={$t('name')} required>
|
|
||||||
<Input bind:value={name} required />
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label={$t('description')}>
|
|
||||||
<Textarea bind:value={description} />
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
{#if !isEditMode}
|
|
||||||
<Field label={$t('trigger_type')} required>
|
|
||||||
<select bind:value={triggerType} class="immich-form-input w-full" required>
|
|
||||||
<option value={PluginTriggerType.AssetCreate}>{$t('asset_created')}</option>
|
|
||||||
<option value={PluginTriggerType.PersonRecognized}>{$t('person_recognized')}</option>
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
{:else}
|
|
||||||
<Field label={$t('trigger_type')}>
|
|
||||||
<Input value={triggerType} disabled />
|
|
||||||
</Field>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Field label={$t('enabled')}>
|
|
||||||
<Switch bind:checked={enabled} />
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<div class="border-t pt-4 dark:border-gray-700">
|
|
||||||
<h3 class="mb-2 font-semibold">{$t('filter')}</h3>
|
|
||||||
<FilterBuilder bind:filters {triggerType} {plugins} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-t pt-4 dark:border-gray-700">
|
|
||||||
<h3 class="mb-2 font-semibold">{$t('actions')}</h3>
|
|
||||||
<ActionBuilder bind:actions {triggerType} {plugins} />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{:else}
|
|
||||||
<div class="mt-4 flex flex-col gap-4">
|
|
||||||
{#if jsonError}
|
|
||||||
<div class="rounded-lg bg-red-100 p-3 text-sm text-red-800 dark:bg-red-900 dark:text-red-200">
|
|
||||||
{$t('json_error')}: {jsonError}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<Field label={$t('workflow_json')}>
|
|
||||||
<textarea bind:value={jsonText} class="immich-form-input h-96 w-full font-mono text-sm" spellcheck="false"
|
|
||||||
></textarea>
|
|
||||||
</Field>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{$t('workflow_json_help')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack fullWidth>
|
|
||||||
<Button color="secondary" fullWidth onclick={onClose}>{$t('cancel')}</Button>
|
|
||||||
<Button type="submit" fullWidth form="workflow-form" onclick={handleSubmit}>
|
|
||||||
{isEditMode ? $t('save') : $t('create')}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
16
web/src/lib/modals/WorkflowNavigationConfirmModal.svelte
Normal file
16
web/src/lib/modals/WorkflowNavigationConfirmModal.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ConfirmModal } from '@immich/ui';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: (confirmed: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { onClose }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
confirmColor="primary"
|
||||||
|
prompt={$t('workflow_navigation_prompt')}
|
||||||
|
onClose={(confirmed) => (confirmed ? onClose(true) : onClose(false))}
|
||||||
|
/>
|
||||||
19
web/src/lib/modals/WorkflowTriggerUpdateConfirmModal.svelte
Normal file
19
web/src/lib/modals/WorkflowTriggerUpdateConfirmModal.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ConfirmModal } from '@immich/ui';
|
||||||
|
import { mdiLightningBolt } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: (confirmed: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { onClose }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
confirmColor="primary"
|
||||||
|
title={$t('change_trigger')}
|
||||||
|
icon={mdiLightningBolt}
|
||||||
|
prompt={$t('change_trigger_prompt')}
|
||||||
|
onClose={(confirmed) => (confirmed ? onClose(true) : onClose(false))}
|
||||||
|
/>
|
||||||
@@ -2,28 +2,37 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import type { WorkflowPayload } from '$lib/services/workflow.service';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
deleteWorkflow,
|
deleteWorkflow,
|
||||||
PluginTriggerType,
|
PluginTriggerType,
|
||||||
updateWorkflow,
|
updateWorkflow,
|
||||||
|
type PluginActionResponseDto,
|
||||||
|
type PluginFilterResponseDto,
|
||||||
|
type PluginResponseDto,
|
||||||
type WorkflowResponseDto,
|
type WorkflowResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { Button, Card, CardBody, CardTitle, HStack, Icon, IconButton, toastManager } from '@immich/ui';
|
|
||||||
import {
|
import {
|
||||||
mdiChevronDown,
|
Button,
|
||||||
mdiChevronUp,
|
Card,
|
||||||
mdiContentCopy,
|
CardBody,
|
||||||
mdiDelete,
|
CardDescription,
|
||||||
mdiPencil,
|
CardHeader,
|
||||||
mdiPlay,
|
CardTitle,
|
||||||
mdiPlayPause,
|
CodeBlock,
|
||||||
mdiPlus,
|
HStack,
|
||||||
} from '@mdi/js';
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
MenuItemType,
|
||||||
|
menuManager,
|
||||||
|
Text,
|
||||||
|
toastManager,
|
||||||
|
} from '@immich/ui';
|
||||||
|
import { mdiCodeJson, mdiDelete, mdiDotsVertical, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -33,24 +42,57 @@
|
|||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let workflows = $state<WorkflowResponseDto[]>(data.workflows);
|
let workflows = $state<WorkflowResponseDto[]>(data.workflows);
|
||||||
// svelte-ignore non_reactive_update
|
const expandedWorkflows = new SvelteSet<string>();
|
||||||
let expandedWorkflows = new SvelteSet();
|
|
||||||
|
const pluginFilterLookup = new SvelteMap<string, PluginFilterResponseDto & { pluginTitle: string }>();
|
||||||
|
const pluginActionLookup = new SvelteMap<string, PluginActionResponseDto & { pluginTitle: string }>();
|
||||||
|
|
||||||
|
for (const plugin of data.plugins as PluginResponseDto[]) {
|
||||||
|
for (const filter of plugin.filters ?? []) {
|
||||||
|
pluginFilterLookup.set(filter.id, { ...filter, pluginTitle: plugin.title });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const action of plugin.actions ?? []) {
|
||||||
|
pluginActionLookup.set(action.id, { ...action, pluginTitle: plugin.title });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggleExpanded = (id: string) => {
|
const toggleExpanded = (id: string) => {
|
||||||
const newExpanded = new SvelteSet(expandedWorkflows);
|
if (expandedWorkflows.has(id)) {
|
||||||
if (newExpanded.has(id)) {
|
expandedWorkflows.delete(id);
|
||||||
newExpanded.delete(id);
|
|
||||||
} else {
|
} else {
|
||||||
newExpanded.add(id);
|
expandedWorkflows.add(id);
|
||||||
}
|
}
|
||||||
expandedWorkflows = newExpanded;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyWorkflow = async (workflow: WorkflowResponseDto) => {
|
const buildShareableWorkflow = (workflow: WorkflowResponseDto): WorkflowPayload => {
|
||||||
const workflowJson = JSON.stringify(workflow, null, 2);
|
const orderedFilters = [...(workflow.filters ?? [])].sort((a, b) => a.order - b.order);
|
||||||
await copyToClipboard(workflowJson);
|
const orderedActions = [...(workflow.actions ?? [])].sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: workflow.name ?? '',
|
||||||
|
description: workflow.description ?? '',
|
||||||
|
enabled: workflow.enabled,
|
||||||
|
triggerType: workflow.triggerType,
|
||||||
|
filters: orderedFilters.map((wfFilter) => {
|
||||||
|
const meta = pluginFilterLookup.get(wfFilter.filterId);
|
||||||
|
const key = meta?.methodName ?? wfFilter.filterId;
|
||||||
|
return {
|
||||||
|
[key]: wfFilter.filterConfig ?? {},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
actions: orderedActions.map((wfAction) => {
|
||||||
|
const meta = pluginActionLookup.get(wfAction.actionId);
|
||||||
|
const key = meta?.methodName ?? wfAction.actionId;
|
||||||
|
return {
|
||||||
|
[key]: wfAction.actionConfig ?? {},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getWorkflowJson = (workflow: WorkflowResponseDto) => JSON.stringify(buildShareableWorkflow(workflow), null, 2);
|
||||||
|
|
||||||
const handleToggleEnabled = async (workflow: WorkflowResponseDto) => {
|
const handleToggleEnabled = async (workflow: WorkflowResponseDto) => {
|
||||||
try {
|
try {
|
||||||
const updated = await updateWorkflow({
|
const updated = await updateWorkflow({
|
||||||
@@ -93,6 +135,38 @@
|
|||||||
await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`);
|
await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WorkflowChip = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilterChips = (workflow: WorkflowResponseDto): WorkflowChip[] => {
|
||||||
|
return [...(workflow.filters ?? [])]
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
.map((filter) => {
|
||||||
|
const meta = pluginFilterLookup.get(filter.filterId);
|
||||||
|
return {
|
||||||
|
id: filter.id,
|
||||||
|
title: meta?.title ?? $t('filter'),
|
||||||
|
subtitle: meta?.pluginTitle ?? $t('workflow'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionChips = (workflow: WorkflowResponseDto): WorkflowChip[] => {
|
||||||
|
return [...(workflow.actions ?? [])]
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
.map((action) => {
|
||||||
|
const meta = pluginActionLookup.get(action.actionId);
|
||||||
|
return {
|
||||||
|
id: action.id,
|
||||||
|
title: meta?.title ?? $t('action'),
|
||||||
|
subtitle: meta?.pluginTitle ?? $t('workflow'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getTriggerLabel = (triggerType: string) => {
|
const getTriggerLabel = (triggerType: string) => {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
AssetCreate: $t('asset_created'),
|
AssetCreate: $t('asset_created'),
|
||||||
@@ -100,12 +174,39 @@
|
|||||||
};
|
};
|
||||||
return labels[triggerType] || triggerType;
|
return labels[triggerType] || triggerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatTimestamp = (iso?: string) => {
|
||||||
|
if (!iso) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
return dateFormatter.format(new Date(iso));
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowWithMeta = {
|
||||||
|
workflow: WorkflowResponseDto;
|
||||||
|
filterChips: WorkflowChip[];
|
||||||
|
actionChips: WorkflowChip[];
|
||||||
|
workflowJson: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWorkflowsWithMeta = (): WorkflowWithMeta[] =>
|
||||||
|
workflows.map((workflow) => ({
|
||||||
|
workflow,
|
||||||
|
filterChips: getFilterChips(workflow),
|
||||||
|
actionChips: getActionChips(workflow),
|
||||||
|
workflowJson: getWorkflowJson(workflow),
|
||||||
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<UserPageLayout title={data.meta.title} scrollbar={false}>
|
<UserPageLayout title={data.meta.title} scrollbar={false}>
|
||||||
{#snippet buttons()}
|
{#snippet buttons()}
|
||||||
<HStack gap={1}>
|
<HStack gap={1}>
|
||||||
<Button shape="round" color="primary" onclick={handleCreateWorkflow}>
|
<Button size="small" variant="ghost" color="secondary" onclick={handleCreateWorkflow}>
|
||||||
<Icon icon={mdiPlus} size="18" />
|
<Icon icon={mdiPlus} size="18" />
|
||||||
{$t('create_workflow')}
|
{$t('create_workflow')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -127,95 +228,165 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="my-5 flex flex-col gap-4">
|
<div class="my-6 grid gap-6">
|
||||||
{#each workflows as workflow (workflow.id)}
|
{#each getWorkflowsWithMeta() as { workflow, filterChips, actionChips, workflowJson } (workflow.id)}
|
||||||
<Card color="secondary">
|
<Card class="border border-gray-200/70 shadow-xl shadow-gray-900/5 dark:border-gray-700/60">
|
||||||
<CardBody>
|
<CardHeader
|
||||||
<div class="flex flex-col gap-2">
|
class={`flex flex-col px-8 py-6 gap-4 sm:flex-row sm:items-center sm:gap-6 ${workflow.enabled ? 'bg-linear-to-r from-green-50 to-white dark:from-green-950/40 dark:to-gray-900' : 'bg-neutral-50 dark:bg-neutral-900'}`}
|
||||||
<div class="flex items-start justify-between">
|
>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<CardTitle>{workflow.name || $t('untitled_workflow')}</CardTitle>
|
<div class="flex items-center gap-3">
|
||||||
{#if workflow.description}
|
<span class={workflow.enabled ? 'relative flex h-3 w-3' : 'flex h-3 w-3'}>
|
||||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{workflow.description}</p>
|
{#if workflow.enabled}
|
||||||
|
<span class="absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
<span
|
||||||
<HStack gap={1}>
|
class={workflow.enabled
|
||||||
<IconButton
|
? 'relative inline-flex h-3 w-3 rounded-full bg-green-500'
|
||||||
shape="round"
|
: 'relative inline-flex h-3 w-3 rounded-full bg-gray-400 dark:bg-gray-600'}
|
||||||
color="secondary"
|
></span>
|
||||||
icon={workflow.enabled ? mdiPlay : mdiPlayPause}
|
|
||||||
aria-label={workflow.enabled ? $t('disabled') : $t('enabled')}
|
|
||||||
onclick={() => handleToggleEnabled(workflow)}
|
|
||||||
class={workflow.enabled ? 'text-green-500' : 'text-gray-400'}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
shape="round"
|
|
||||||
color="secondary"
|
|
||||||
icon={mdiContentCopy}
|
|
||||||
aria-label={$t('copy_to_clipboard')}
|
|
||||||
onclick={() => handleCopyWorkflow(workflow)}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
shape="round"
|
|
||||||
color="secondary"
|
|
||||||
icon={mdiPencil}
|
|
||||||
aria-label={$t('edit')}
|
|
||||||
onclick={() => handleEditWorkflow(workflow)}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
shape="round"
|
|
||||||
color="secondary"
|
|
||||||
icon={mdiDelete}
|
|
||||||
aria-label={$t('delete')}
|
|
||||||
onclick={() => handleDeleteWorkflow(workflow)}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 text-sm">
|
|
||||||
<span
|
|
||||||
class="rounded-full px-3 py-1 {workflow.enabled
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
|
||||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'}"
|
|
||||||
>
|
|
||||||
{workflow.enabled ? $t('enabled') : $t('disabled')}
|
|
||||||
</span>
|
</span>
|
||||||
<span class="rounded-full bg-blue-100 px-3 py-1 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
<CardTitle>{workflow.name}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription class="mt-1 text-sm">
|
||||||
|
{workflow.description || $t('workflows_help_text')}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="text-right">
|
||||||
|
<Text size="tiny" class="text-gray-500 dark:text-gray-400">{$t('created_at')}</Text>
|
||||||
|
<Text size="small" class="font-medium">
|
||||||
|
{formatTimestamp(workflow.createdAt)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
shape="round"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
icon={mdiDotsVertical}
|
||||||
|
aria-label={$t('menu')}
|
||||||
|
onclick={(event: MouseEvent) => {
|
||||||
|
void menuManager.show({
|
||||||
|
target: event.currentTarget as HTMLElement,
|
||||||
|
position: 'top-left',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: workflow.enabled ? $t('disable') : $t('enable'),
|
||||||
|
color: workflow.enabled ? 'warning' : 'success',
|
||||||
|
icon: workflow.enabled ? mdiPause : mdiPlay,
|
||||||
|
onSelect: () => void handleToggleEnabled(workflow),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('edit'),
|
||||||
|
icon: mdiPencil,
|
||||||
|
onSelect: () => void handleEditWorkflow(workflow),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: expandedWorkflows.has(workflow.id) ? $t('hide_json') : $t('show_json'),
|
||||||
|
icon: mdiCodeJson,
|
||||||
|
onSelect: () => toggleExpanded(workflow.id),
|
||||||
|
},
|
||||||
|
MenuItemType.Divider,
|
||||||
|
{
|
||||||
|
title: $t('delete'),
|
||||||
|
icon: mdiDelete,
|
||||||
|
color: 'danger',
|
||||||
|
onSelect: () => void handleDeleteWorkflow(workflow),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardBody class="space-y-6">
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
<!-- Trigger Section -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-gray-100/80 bg-gray-50/90 p-4 dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<dt class="mb-3 text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-400">
|
||||||
|
{$t('trigger')}
|
||||||
|
</dt>
|
||||||
|
<span
|
||||||
|
class="inline-block rounded-xl border border-gray-200/80 bg-white/70 px-3 py-1.5 text-sm font-medium shadow-sm dark:border-gray-600 dark:bg-gray-700/80 dark:text-white"
|
||||||
|
>
|
||||||
{getTriggerLabel(workflow.triggerType)}
|
{getTriggerLabel(workflow.triggerType)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
class="rounded-full bg-purple-100 px-3 py-1 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
|
|
||||||
>
|
|
||||||
{workflow.filters.length}
|
|
||||||
{workflow.filters.length === 1 ? $t('filter') : $t('filter')}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="rounded-full bg-orange-100 px-3 py-1 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
|
|
||||||
>
|
|
||||||
{workflow.actions.length}
|
|
||||||
{workflow.actions.length === 1 ? $t('action') : $t('actions')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<!-- Filters Section -->
|
||||||
type="button"
|
<div
|
||||||
onclick={() => toggleExpanded(workflow.id)}
|
class="rounded-2xl border border-gray-100/80 bg-gray-50/90 p-4 dark:border-gray-700 dark:bg-gray-800"
|
||||||
class="flex items-center gap-1 text-sm text-primary hover:underline"
|
|
||||||
>
|
>
|
||||||
<Icon icon={expandedWorkflows.has(workflow.id) ? mdiChevronUp : mdiChevronDown} size="18" />
|
<div class="mb-3 flex items-center justify-between">
|
||||||
{expandedWorkflows.has(workflow.id) ? $t('hide_json') : $t('show_json')}
|
<dt class="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-400">
|
||||||
</button>
|
{$t('filter')}
|
||||||
|
</dt>
|
||||||
{#if expandedWorkflows.has(workflow.id)}
|
<dd
|
||||||
<div class="mt-2">
|
class="rounded-full bg-gray-200 px-2.5 py-0.5 text-sm font-semibold text-gray-700 dark:bg-gray-700 dark:text-gray-200"
|
||||||
<pre class="overflow-x-auto rounded-lg bg-gray-100 p-4 text-xs dark:bg-gray-800">{JSON.stringify(
|
>
|
||||||
workflow,
|
{workflow.filters.length}
|
||||||
null,
|
</dd>
|
||||||
2,
|
|
||||||
)}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#if filterChips.length === 0}
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{$t('no_filters_added')}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
{#each filterChips as chip (chip.id)}
|
||||||
|
<span
|
||||||
|
class="rounded-xl border border-gray-200/80 bg-white/70 px-3 py-1.5 text-sm shadow-sm dark:border-gray-600 dark:bg-gray-700/80"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">{chip.title}</span>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions Section -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-gray-100/80 bg-gray-50/90 p-4 dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<dt class="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-400">
|
||||||
|
{$t('actions')}
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
class="rounded-full bg-gray-200 px-2.5 py-0.5 text-sm font-semibold text-gray-700 dark:bg-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
{workflow.actions.length}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#if actionChips.length === 0}
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{$t('no_actions_added')}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
{#each actionChips as chip (chip.id)}
|
||||||
|
<span
|
||||||
|
class="rounded-xl border border-gray-200/80 bg-white/70 px-3 py-1.5 text-sm shadow-sm dark:border-gray-600 dark:bg-gray-700/80"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">{chip.title}</span>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if expandedWorkflows.has(workflow.id)}
|
||||||
|
<div>
|
||||||
|
<p class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-200">Workflow JSON</p>
|
||||||
|
<CodeBlock code={workflowJson} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { beforeNavigate, goto } from '$app/navigation';
|
||||||
import { dragAndDrop } from '$lib/actions/drag-and-drop';
|
import { dragAndDrop } from '$lib/actions/drag-and-drop';
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import SchemaFormFields from '$lib/components/workflow/schema-form/SchemaFormFields.svelte';
|
import SchemaFormFields from '$lib/components/workflow/schema-form/SchemaFormFields.svelte';
|
||||||
@@ -6,8 +7,11 @@
|
|||||||
import WorkflowJsonEditor from '$lib/components/workflows/workflow-json-editor.svelte';
|
import WorkflowJsonEditor from '$lib/components/workflows/workflow-json-editor.svelte';
|
||||||
import WorkflowTriggerCard from '$lib/components/workflows/workflow-trigger-card.svelte';
|
import WorkflowTriggerCard from '$lib/components/workflows/workflow-trigger-card.svelte';
|
||||||
import AddWorkflowStepModal from '$lib/modals/AddWorkflowStepModal.svelte';
|
import AddWorkflowStepModal from '$lib/modals/AddWorkflowStepModal.svelte';
|
||||||
|
import WorkflowNavigationConfirmModal from '$lib/modals/WorkflowNavigationConfirmModal.svelte';
|
||||||
|
import WorkflowTriggerUpdateConfirmModal from '$lib/modals/WorkflowTriggerUpdateConfirmModal.svelte';
|
||||||
import { WorkflowService, type WorkflowPayload } from '$lib/services/workflow.service';
|
import { WorkflowService, type WorkflowPayload } from '$lib/services/workflow.service';
|
||||||
import type { PluginActionResponseDto, PluginFilterResponseDto } from '@immich/sdk';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import type { PluginActionResponseDto, PluginFilterResponseDto, PluginTriggerResponseDto } from '@immich/sdk';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -39,6 +43,7 @@
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
}
|
}
|
||||||
@@ -97,6 +102,18 @@
|
|||||||
|
|
||||||
const updateWorkflow = async () => {
|
const updateWorkflow = async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('Updating workflow with:', {
|
||||||
|
id: editWorkflow.id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
enabled: editWorkflow.enabled,
|
||||||
|
triggerType,
|
||||||
|
orderedFilters: orderedFilters.map((f) => ({ id: f.id, methodName: f.methodName })),
|
||||||
|
orderedActions: orderedActions.map((a) => ({ id: a.id, methodName: a.methodName })),
|
||||||
|
filterConfigs,
|
||||||
|
actionConfigs,
|
||||||
|
});
|
||||||
|
|
||||||
const updated = await workflowService.updateWorkflow(
|
const updated = await workflowService.updateWorkflow(
|
||||||
editWorkflow.id,
|
editWorkflow.id,
|
||||||
name,
|
name,
|
||||||
@@ -113,7 +130,8 @@
|
|||||||
previousWorkflow = updated;
|
previousWorkflow = updated;
|
||||||
editWorkflow = updated;
|
editWorkflow = updated;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update workflow:', error);
|
console.log('error', error);
|
||||||
|
handleError(error, 'Failed to update workflow');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,6 +279,34 @@
|
|||||||
const handleRemoveAction = (index: number) => {
|
const handleRemoveAction = (index: number) => {
|
||||||
orderedActions = orderedActions.filter((_, i) => i !== index);
|
orderedActions = orderedActions.filter((_, i) => i !== index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTriggerChange = async (newTrigger: PluginTriggerResponseDto) => {
|
||||||
|
const isConfirmed = await modalManager.show(WorkflowTriggerUpdateConfirmModal);
|
||||||
|
|
||||||
|
if (!isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTrigger = newTrigger;
|
||||||
|
};
|
||||||
|
|
||||||
|
let allowNavigation = $state(false);
|
||||||
|
|
||||||
|
beforeNavigate(({ cancel, to }) => {
|
||||||
|
if (hasChanges && !allowNavigation) {
|
||||||
|
cancel();
|
||||||
|
|
||||||
|
modalManager
|
||||||
|
.show(WorkflowNavigationConfirmModal)
|
||||||
|
.then((isConfirmed) => {
|
||||||
|
if (isConfirmed && to) {
|
||||||
|
allowNavigation = true;
|
||||||
|
void goto(to.url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet cardOrder(index: number)}
|
{#snippet cardOrder(index: number)}
|
||||||
@@ -387,7 +433,7 @@
|
|||||||
<WorkflowTriggerCard
|
<WorkflowTriggerCard
|
||||||
{trigger}
|
{trigger}
|
||||||
selected={selectedTrigger.triggerType === trigger.triggerType}
|
selected={selectedTrigger.triggerType === trigger.triggerType}
|
||||||
onclick={() => (selectedTrigger = trigger)}
|
onclick={() => handleTriggerChange(trigger)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user