pull/28764/merge
Daniel Dietzler 2026-06-03 08:04:05 -05:00 committed by GitHub
commit 7afa80ff70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 78 additions and 45 deletions

View File

@ -6,6 +6,7 @@
import WorkflowTriggerPicker from '$lib/modals/WorkflowTriggerPicker.svelte'; import WorkflowTriggerPicker from '$lib/modals/WorkflowTriggerPicker.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getWorkflowActions, handleUpdateWorkflow } from '$lib/services/workflow.service'; import { getWorkflowActions, handleUpdateWorkflow } from '$lib/services/workflow.service';
import { generateId } from '$lib/utils/generate-id';
import { getTriggerDescription, getTriggerName } from '$lib/utils/workflow'; import { getTriggerDescription, getTriggerName } from '$lib/utils/workflow';
import type { WorkflowResponseDto, WorkflowUpdateDto } from '@immich/sdk'; import type { WorkflowResponseDto, WorkflowUpdateDto } from '@immich/sdk';
import { import {
@ -44,6 +45,7 @@
} from '@mdi/js'; } from '@mdi/js';
import { cloneDeep, isEqual } from 'lodash-es'; import { cloneDeep, isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { flip } from 'svelte/animate';
import type { PageData } from './$types'; import type { PageData } from './$types';
import WorkflowJsonEditor from './WorkflowJsonEditor.svelte'; import WorkflowJsonEditor from './WorkflowJsonEditor.svelte';
import WorkflowStepCard from './WorkflowStepCard.svelte'; import WorkflowStepCard from './WorkflowStepCard.svelte';
@ -62,12 +64,13 @@
let { data }: Props = $props(); let { data }: Props = $props();
let { id, enabled, name, description, trigger } = $derived(data.workflow); let { id, enabled, name, description, trigger } = $derived(data.workflow);
let steps = $state(data.workflow.steps); let steps = $state(data.workflow.steps.map((step) => ({ ...step, id: generateId() })));
let savedWorkflow = $state(cloneDeep(data.workflow)); let savedWorkflow = $state(cloneDeep(data.workflow));
let allowNavigation = $state(false); let allowNavigation = $state(false);
let isShowingNavigationDialog = $state(false); let isShowingNavigationDialog = $state(false);
let isSaving = $state(false); let isSaving = $state(false);
let editMode = $state<EditMode>('visual'); let editMode = $state<EditMode>('visual');
let dragSourceId: string | undefined;
const workflowSummary = $derived({ name, description, trigger, steps }); const workflowSummary = $derived({ name, description, trigger, steps });
const workflowJsonContent = $derived<WorkflowJsonContent>({ name, description, enabled, trigger, steps }); const workflowJsonContent = $derived<WorkflowJsonContent>({ name, description, enabled, trigger, steps });
@ -77,20 +80,23 @@
name !== savedWorkflow.name || name !== savedWorkflow.name ||
description !== savedWorkflow.description || description !== savedWorkflow.description ||
!isEqual(trigger, savedWorkflow.trigger) || !isEqual(trigger, savedWorkflow.trigger) ||
!isEqual(steps, savedWorkflow.steps), !isEqual(
steps.map(({ id: _, ...step }) => step),
savedWorkflow.steps,
),
); );
const handleAddStep = async () => { const handleAddStep = async () => {
const step = await modalManager.show(WorkflowAddStepModal, { trigger }); const step = await modalManager.show(WorkflowAddStepModal, { trigger });
if (step) { if (step) {
steps.push(step); steps.push({ ...step, id: generateId() });
} }
}; };
const handleInsertStep = async (index: number) => { const handleInsertStep = async (index: number) => {
const step = await modalManager.show(WorkflowAddStepModal, { trigger }); const step = await modalManager.show(WorkflowAddStepModal, { trigger });
if (step) { if (step) {
steps = [...steps.slice(0, index), step, ...steps.slice(index)]; steps = [...steps.slice(0, index), { ...step, id: generateId() }, ...steps.slice(index)];
} }
}; };
@ -102,20 +108,53 @@
const result = await modalManager.show(WorkflowEditStepModal, { trigger, step: cloneDeep(step) }); const result = await modalManager.show(WorkflowEditStepModal, { trigger, step: cloneDeep(step) });
if (result) { if (result) {
steps[index] = result; steps[index] = { ...result, id: generateId() };
} }
}; };
const handleDrop = (index: number, event: DragEvent) => { const handleDragEnd = (event: DragEvent) => {
if (!event.dataTransfer) { if (!event.dataTransfer || !dragSourceId) {
return; return;
} }
const from = Number(event.dataTransfer.getData('text/plain')); const ghostIndex = steps.findIndex(({ id }) => id === 'ghost');
if (ghostIndex === -1) {
return;
}
const from = steps.findIndex(({ id }) => id === dragSourceId);
const next = [...steps]; const next = [...steps];
const [moved] = next.splice(from, 1); const [step] = next.splice(from, 1);
next.splice(index, 0, moved); next[ghostIndex > from ? ghostIndex - 1 : ghostIndex] = step;
steps = next;
dragSourceId = undefined;
};
const handleDragOver = (index: number, event: DragEvent, boundingRect: DOMRect) => {
if (!event.dataTransfer || !dragSourceId) {
return;
}
const fromIndex = steps.findIndex(({ id }) => dragSourceId === id);
const ghostIndex = steps.findIndex(({ id }) => id === 'ghost');
const shiftedIndex = event.clientY > boundingRect.top + boundingRect.height / 2 ? index + 1 : index;
if (index === fromIndex || shiftedIndex === fromIndex) {
if (ghostIndex !== -1) {
steps.splice(ghostIndex, 1);
}
return;
}
if (
(ghostIndex !== -1 && Math.abs(shiftedIndex - ghostIndex) <= (ghostIndex > shiftedIndex ? 0 : 1)) ||
Math.abs(shiftedIndex - fromIndex) <= (fromIndex > shiftedIndex ? 0 : 1)
) {
return;
}
const next = steps.filter(({ id }) => id !== 'ghost');
next.splice(shiftedIndex, 0, { ...steps[fromIndex], id: 'ghost' });
steps = next; steps = next;
}; };
@ -131,7 +170,7 @@
name = content.name; name = content.name;
description = content.description; description = content.description;
trigger = content.trigger; trigger = content.trigger;
steps = cloneDeep(content.steps); steps = cloneDeep(content.steps).map((step) => ({ ...step, id: generateId() }));
}; };
const onClose = () => goto(Route.workflows()); const onClose = () => goto(Route.workflows());
@ -344,15 +383,19 @@
</CardHeader> </CardHeader>
</Card> </Card>
{#each steps as step, index (step.method + index)} {#each steps as step, index (step.id)}
<WorkflowStepCard <div class="w-full" animate:flip={{ duration: 120 }}>
{step} <WorkflowStepCard
{index} {step}
onEdit={handleEditStep} {index}
onDelete={handleDeleteStep} onEdit={handleEditStep}
onInsertBefore={handleInsertStep} onDelete={handleDeleteStep}
onDrop={handleDrop} onInsertBefore={handleInsertStep}
/> onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragStart={(event) => (dragSourceId = event.dataTransfer?.getData('text/plain'))}
/>
</div>
{/each} {/each}
<Button <Button

View File

@ -4,7 +4,6 @@
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const albumNameCache = new Map<string, Promise<string>>(); const albumNameCache = new Map<string, Promise<string>>();
const getAlbumName = (id: string): Promise<string> => { const getAlbumName = (id: string): Promise<string> => {
let albumName = albumNameCache.get(id); let albumName = albumNameCache.get(id);
if (!albumName) { if (!albumName) {
@ -20,7 +19,7 @@
<script lang="ts"> <script lang="ts">
import { pluginManager } from '$lib/managers/plugin-manager.svelte'; import { pluginManager } from '$lib/managers/plugin-manager.svelte';
import type { JSONSchemaProperty } from '$lib/types'; import type { JSONSchemaProperty } from '$lib/types';
import type { WorkflowStepDto } from '@immich/sdk'; import { type WorkflowStepDto } from '@immich/sdk';
import { Badge, Card, CardBody, CardDescription, CardHeader, CardTitle, Icon, IconButton } from '@immich/ui'; import { Badge, Card, CardBody, CardDescription, CardHeader, CardTitle, Icon, IconButton } from '@immich/ui';
import { import {
mdiAutoFix, mdiAutoFix,
@ -35,15 +34,17 @@
import WorkflowStepDragImage from './WorkflowStepDragImage.svelte'; import WorkflowStepDragImage from './WorkflowStepDragImage.svelte';
type Props = { type Props = {
step: WorkflowStepDto; step: WorkflowStepDto & { id: string };
index: number; index: number;
onEdit: (index: number) => void; onEdit: (index: number) => void;
onDelete: (index: number) => void; onDelete: (index: number) => void;
onInsertBefore: (index: number) => void; onInsertBefore: (index: number) => void;
onDrop: (index: number, event: DragEvent) => void; onDragOver: (index: number, event: DragEvent, boundingRect: DOMRect) => void;
onDragEnd: (event: DragEvent) => void;
onDragStart: (event: DragEvent) => void;
}; };
let { step, index, onEdit, onDelete, onInsertBefore, onDrop }: Props = $props(); let { step, index, onEdit, onDelete, onInsertBefore, onDragOver, onDragEnd, onDragStart }: Props = $props();
const method = $derived(pluginManager.getMethod(step.method)); const method = $derived(pluginManager.getMethod(step.method));
const isFilter = $derived(method?.uiHints?.includes('Filter') ?? false); const isFilter = $derived(method?.uiHints?.includes('Filter') ?? false);
@ -51,12 +52,12 @@
const configEntries = $derived( const configEntries = $derived(
Object.entries(step.config ?? {}).filter(([, value]) => value !== null && value !== undefined && value !== ''), Object.entries(step.config ?? {}).filter(([, value]) => value !== null && value !== undefined && value !== ''),
); );
const isGhost = $derived(step.id === 'ghost');
const getUiHint = (key: string) => schema?.properties?.[key]?.uiHint; const getUiHint = (key: string) => schema?.properties?.[key]?.uiHint;
const toIds = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : [String(value)]); const toIds = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : [String(value)]);
let dragImage = $state<Element>(); let dragImage = $state<Element>();
let isDropTarget = $state(false); let isDropTarget = $state(false);
let hoverDrag = $state(false);
const truncate = (input: string, max = 24) => (input.length > max ? input.slice(0, max - 1) + '…' : input); const truncate = (input: string, max = 24) => (input.length > max ? input.slice(0, max - 1) + '…' : input);
@ -93,7 +94,7 @@
} }
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', String(index)); event.dataTransfer.setData('text/plain', step.id);
mount(WorkflowStepDragImage, { mount(WorkflowStepDragImage, {
target: document.body, target: document.body,
@ -107,31 +108,23 @@
dragImage = document.body.querySelector('#workflow-step-drag-image')!; dragImage = document.body.querySelector('#workflow-step-drag-image')!;
event.dataTransfer.setDragImage(dragImage, 16, 22); event.dataTransfer.setDragImage(dragImage, 16, 22);
onDragStart(event);
}; };
const handleDrop = (index: number, event: DragEvent) => { const handleDragOver = (event: DragEvent & { currentTarget: HTMLElement }) => {
if (!event.dataTransfer) { if (isGhost) {
return; return;
} }
event.preventDefault(); event.preventDefault();
const from = Number(event.dataTransfer.getData('text/plain'));
if (from === index) {
return;
}
onDrop(index, event);
};
const handleDragOver = (event: DragEvent) => {
event.preventDefault();
isDropTarget = true; isDropTarget = true;
onDragOver(index, event, event.currentTarget.getBoundingClientRect());
}; };
const handleDragEnd = () => { const handleDragEnd = (event: DragEvent) => {
dragImage?.remove(); dragImage?.remove();
dragImage = undefined; dragImage = undefined;
isDropTarget = false; isDropTarget = false;
onDragEnd(event);
}; };
</script> </script>
@ -157,13 +150,12 @@
class:scale-[0.99]={!!dragImage} class:scale-[0.99]={!!dragImage}
ondragover={handleDragOver} ondragover={handleDragOver}
ondragleave={() => (isDropTarget = false)} ondragleave={() => (isDropTarget = false)}
ondrop={(event) => handleDrop(index, event)}
role="listitem" role="listitem"
> >
<Card <Card
class="shadow-none transition-colors {isDropTarget class="shadow-none transition-colors {isDropTarget
? 'border-primary ring-2 ring-primary-200' ? 'border-primary ring-2 ring-primary-200'
: hoverDrag : isGhost
? 'border-dashed border-primary' ? 'border-dashed border-primary'
: ''}" : ''}"
> >
@ -174,8 +166,6 @@
class="flex shrink-0 cursor-grab items-center justify-center rounded-md border border-transparent p-1 text-light-400 select-none hover:border-primary-200 hover:bg-primary-50 hover:text-primary active:cursor-grabbing" class="flex shrink-0 cursor-grab items-center justify-center rounded-md border border-transparent p-1 text-light-400 select-none hover:border-primary-200 hover:bg-primary-50 hover:text-primary active:cursor-grabbing"
aria-label={$t('drag_to_reorder')} aria-label={$t('drag_to_reorder')}
draggable="true" draggable="true"
onmouseenter={() => (hoverDrag = true)}
onmouseleave={() => (hoverDrag = false)}
ondragstart={(event) => handleDragStart(index, event)} ondragstart={(event) => handleDragStart(index, event)}
ondragend={handleDragEnd} ondragend={handleDragEnd}
title={$t('drag_to_reorder')} title={$t('drag_to_reorder')}