Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Tran 54d7b0feb9 feat: workflow template 2026-05-21 22:16:59 -05:00
5 changed files with 116 additions and 4 deletions
+2
View File
@@ -2417,6 +2417,7 @@
"use_browser_locale_description": "Format dates, times, and numbers based on your browser locale", "use_browser_locale_description": "Format dates, times, and numbers based on your browser locale",
"use_current_connection": "Use current connection", "use_current_connection": "Use current connection",
"use_custom_date_range": "Use custom date range instead", "use_custom_date_range": "Use custom date range instead",
"use_template": "Use template",
"user": "User", "user": "User",
"user_has_been_deleted": "This user has been deleted.", "user_has_been_deleted": "This user has been deleted.",
"user_id": "User ID", "user_id": "User ID",
@@ -2491,6 +2492,7 @@
"workflow_name": "Workflow name", "workflow_name": "Workflow name",
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?", "workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
"workflow_summary": "Workflow summary", "workflow_summary": "Workflow summary",
"workflow_templates": "Workflow templates",
"workflow_update_success": "Workflow updated successfully", "workflow_update_success": "Workflow updated successfully",
"workflow_updated": "Workflow updated", "workflow_updated": "Workflow updated",
"workflows": "Workflows", "workflows": "Workflows",
@@ -0,0 +1,35 @@
<script lang="ts">
import { workflowTemplates, type WorkflowTemplate } from '$lib/templates/workflow-templates';
import { FormModal, Icon, ListButton, Text } from '@immich/ui';
import { t } from 'svelte-i18n';
type Props = {
onClose: (template?: WorkflowTemplate) => void;
};
const { onClose }: Props = $props();
let selected = $state<WorkflowTemplate>();
const onSubmit = () => onClose(selected);
</script>
<FormModal title={$t('workflow_templates')} {onClose} {onSubmit} disabled={!selected} size="medium">
<div class="flex flex-col gap-2">
{#each workflowTemplates as template (template.id)}
<ListButton selected={selected?.id === template.id} onclick={() => (selected = template)}>
<div class="flex w-full items-center gap-3 text-start">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-immich-primary/10 text-immich-primary dark:bg-immich-dark-primary/15 dark:text-immich-dark-primary"
>
<Icon icon={template.icon} size="18" />
</div>
<div class="min-w-0 grow">
<Text fontWeight="medium">{template.name}</Text>
<Text size="tiny" color="muted">{template.description}</Text>
</div>
</div>
</ListButton>
{/each}
</div>
</FormModal>
+21 -2
View File
@@ -10,10 +10,11 @@ import {
type WorkflowUpdateDto, type WorkflowUpdateDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui'; import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiCodeJson, mdiDelete, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js'; import { mdiCodeJson, mdiDelete, mdiFileDocumentMultipleOutline, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n'; import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import WorkflowTemplatePicker from '$lib/modals/WorkflowTemplatePicker.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
@@ -33,7 +34,25 @@ export const getWorkflowsActions = ($t: MessageFormatter) => {
}), }),
}; };
return { Create }; const UseTemplate: ActionItem = {
title: $t('use_template'),
icon: mdiFileDocumentMultipleOutline,
onAction: async () => {
const template = await modalManager.show(WorkflowTemplatePicker, {});
if (!template) {
return;
}
await handleCreateWorkflow({
trigger: template.trigger,
steps: template.steps,
name: template.name,
description: template.description,
enabled: false,
});
},
};
return { Create, UseTemplate };
}; };
export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowResponseDto) => { export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowResponseDto) => {
@@ -0,0 +1,56 @@
import { WorkflowTrigger, type WorkflowStepDto } from '@immich/sdk';
import { mdiAccountGroupOutline, mdiMonitorScreenshot } from '@mdi/js';
export type WorkflowTemplate = {
id: string;
name: string;
description: string;
icon: string;
trigger: WorkflowTrigger;
steps: WorkflowStepDto[];
};
export const workflowTemplates: WorkflowTemplate[] = [
{
id: '1',
name: 'Archive screenshots to album',
description: 'Add uploads with "screenshot" in the filename to an album and archive them',
icon: mdiMonitorScreenshot,
trigger: WorkflowTrigger.AssetCreate,
steps: [
{
method: 'immich-plugin-core#assetFileFilter',
config: {
pattern: 'screenshot',
matchType: 'contains',
caseSensitive: false,
},
},
{
method: 'immich-plugin-core#assetAddToAlbums',
config: { albumIds: [] },
},
{
method: 'immich-plugin-core#assetArchive',
config: { inverse: false },
},
],
},
{
id: '2',
name: 'Add person to album',
description: 'Add assets to an album when a specific person is recognized',
icon: mdiAccountGroupOutline,
trigger: WorkflowTrigger.PersonRecognized,
steps: [
{
method: 'immich-plugin-core#filterPerson',
config: { personIds: [], matchAny: true },
},
{
method: 'immich-plugin-core#assetAddToAlbums',
config: { albumIds: [] },
},
],
},
];
+2 -2
View File
@@ -59,7 +59,7 @@
}); });
}; };
const { Create } = $derived(getWorkflowsActions($t)); const { Create, UseTemplate } = $derived(getWorkflowsActions($t));
const onWorkflowCreate = async (response: WorkflowResponseDto) => { const onWorkflowCreate = async (response: WorkflowResponseDto) => {
await goto(Route.viewWorkflow(response)); await goto(Route.viewWorkflow(response));
@@ -76,7 +76,7 @@
<OnEvents {onWorkflowCreate} {onWorkflowUpdate} {onWorkflowDelete} /> <OnEvents {onWorkflowCreate} {onWorkflowUpdate} {onWorkflowDelete} />
<UserPageLayout title={data.meta.title} actions={[Create]} scrollbar={false}> <UserPageLayout title={data.meta.title} actions={[UseTemplate, Create]} scrollbar={false}>
<section class="flex place-content-center sm:mx-4"> <section class="flex place-content-center sm:mx-4">
<Container center size="large" class="pb-28"> <Container center size="large" class="pb-28">
{#if workflows.length === 0} {#if workflows.length === 0}