Merge 9771f56a8b into 963862b1b9
commit
7afa80ff70
|
|
@ -6,6 +6,7 @@
|
|||
import WorkflowTriggerPicker from '$lib/modals/WorkflowTriggerPicker.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getWorkflowActions, handleUpdateWorkflow } from '$lib/services/workflow.service';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { getTriggerDescription, getTriggerName } from '$lib/utils/workflow';
|
||||
import type { WorkflowResponseDto, WorkflowUpdateDto } from '@immich/sdk';
|
||||
import {
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
} from '@mdi/js';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { flip } from 'svelte/animate';
|
||||
import type { PageData } from './$types';
|
||||
import WorkflowJsonEditor from './WorkflowJsonEditor.svelte';
|
||||
import WorkflowStepCard from './WorkflowStepCard.svelte';
|
||||
|
|
@ -62,12 +64,13 @@
|
|||
let { data }: Props = $props();
|
||||
|
||||
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 allowNavigation = $state(false);
|
||||
let isShowingNavigationDialog = $state(false);
|
||||
let isSaving = $state(false);
|
||||
let editMode = $state<EditMode>('visual');
|
||||
let dragSourceId: string | undefined;
|
||||
|
||||
const workflowSummary = $derived({ name, description, trigger, steps });
|
||||
const workflowJsonContent = $derived<WorkflowJsonContent>({ name, description, enabled, trigger, steps });
|
||||
|
|
@ -77,20 +80,23 @@
|
|||
name !== savedWorkflow.name ||
|
||||
description !== savedWorkflow.description ||
|
||||
!isEqual(trigger, savedWorkflow.trigger) ||
|
||||
!isEqual(steps, savedWorkflow.steps),
|
||||
!isEqual(
|
||||
steps.map(({ id: _, ...step }) => step),
|
||||
savedWorkflow.steps,
|
||||
),
|
||||
);
|
||||
|
||||
const handleAddStep = async () => {
|
||||
const step = await modalManager.show(WorkflowAddStepModal, { trigger });
|
||||
if (step) {
|
||||
steps.push(step);
|
||||
steps.push({ ...step, id: generateId() });
|
||||
}
|
||||
};
|
||||
|
||||
const handleInsertStep = async (index: number) => {
|
||||
const step = await modalManager.show(WorkflowAddStepModal, { trigger });
|
||||
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) });
|
||||
if (result) {
|
||||
steps[index] = result;
|
||||
steps[index] = { ...result, id: generateId() };
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (index: number, event: DragEvent) => {
|
||||
if (!event.dataTransfer) {
|
||||
const handleDragEnd = (event: DragEvent) => {
|
||||
if (!event.dataTransfer || !dragSourceId) {
|
||||
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 [moved] = next.splice(from, 1);
|
||||
next.splice(index, 0, moved);
|
||||
const [step] = next.splice(from, 1);
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
@ -131,7 +170,7 @@
|
|||
name = content.name;
|
||||
description = content.description;
|
||||
trigger = content.trigger;
|
||||
steps = cloneDeep(content.steps);
|
||||
steps = cloneDeep(content.steps).map((step) => ({ ...step, id: generateId() }));
|
||||
};
|
||||
|
||||
const onClose = () => goto(Route.workflows());
|
||||
|
|
@ -344,15 +383,19 @@
|
|||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{#each steps as step, index (step.method + index)}
|
||||
<WorkflowStepCard
|
||||
{step}
|
||||
{index}
|
||||
onEdit={handleEditStep}
|
||||
onDelete={handleDeleteStep}
|
||||
onInsertBefore={handleInsertStep}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
{#each steps as step, index (step.id)}
|
||||
<div class="w-full" animate:flip={{ duration: 120 }}>
|
||||
<WorkflowStepCard
|
||||
{step}
|
||||
{index}
|
||||
onEdit={handleEditStep}
|
||||
onDelete={handleDeleteStep}
|
||||
onInsertBefore={handleInsertStep}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={(event) => (dragSourceId = event.dataTransfer?.getData('text/plain'))}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const albumNameCache = new Map<string, Promise<string>>();
|
||||
|
||||
const getAlbumName = (id: string): Promise<string> => {
|
||||
let albumName = albumNameCache.get(id);
|
||||
if (!albumName) {
|
||||
|
|
@ -20,7 +19,7 @@
|
|||
<script lang="ts">
|
||||
import { pluginManager } from '$lib/managers/plugin-manager.svelte';
|
||||
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 {
|
||||
mdiAutoFix,
|
||||
|
|
@ -35,15 +34,17 @@
|
|||
import WorkflowStepDragImage from './WorkflowStepDragImage.svelte';
|
||||
|
||||
type Props = {
|
||||
step: WorkflowStepDto;
|
||||
step: WorkflowStepDto & { id: string };
|
||||
index: number;
|
||||
onEdit: (index: number) => void;
|
||||
onDelete: (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 isFilter = $derived(method?.uiHints?.includes('Filter') ?? false);
|
||||
|
|
@ -51,12 +52,12 @@
|
|||
const configEntries = $derived(
|
||||
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 toIds = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : [String(value)]);
|
||||
let dragImage = $state<Element>();
|
||||
let isDropTarget = $state(false);
|
||||
let hoverDrag = $state(false);
|
||||
|
||||
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.setData('text/plain', String(index));
|
||||
event.dataTransfer.setData('text/plain', step.id);
|
||||
|
||||
mount(WorkflowStepDragImage, {
|
||||
target: document.body,
|
||||
|
|
@ -107,31 +108,23 @@
|
|||
|
||||
dragImage = document.body.querySelector('#workflow-step-drag-image')!;
|
||||
event.dataTransfer.setDragImage(dragImage, 16, 22);
|
||||
onDragStart(event);
|
||||
};
|
||||
|
||||
const handleDrop = (index: number, event: DragEvent) => {
|
||||
if (!event.dataTransfer) {
|
||||
const handleDragOver = (event: DragEvent & { currentTarget: HTMLElement }) => {
|
||||
if (isGhost) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
onDragOver(index, event, event.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
const handleDragEnd = (event: DragEvent) => {
|
||||
dragImage?.remove();
|
||||
dragImage = undefined;
|
||||
isDropTarget = false;
|
||||
onDragEnd(event);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -157,13 +150,12 @@
|
|||
class:scale-[0.99]={!!dragImage}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={() => (isDropTarget = false)}
|
||||
ondrop={(event) => handleDrop(index, event)}
|
||||
role="listitem"
|
||||
>
|
||||
<Card
|
||||
class="shadow-none transition-colors {isDropTarget
|
||||
? 'border-primary ring-2 ring-primary-200'
|
||||
: hoverDrag
|
||||
: isGhost
|
||||
? '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"
|
||||
aria-label={$t('drag_to_reorder')}
|
||||
draggable="true"
|
||||
onmouseenter={() => (hoverDrag = true)}
|
||||
onmouseleave={() => (hoverDrag = false)}
|
||||
ondragstart={(event) => handleDragStart(index, event)}
|
||||
ondragend={handleDragEnd}
|
||||
title={$t('drag_to_reorder')}
|
||||
|
|
|
|||
Loading…
Reference in New Issue