mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 09:15:05 +03:00
wip
This commit is contained in:
89
web/src/lib/actions/drag-and-drop.ts
Normal file
89
web/src/lib/actions/drag-and-drop.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export interface DragAndDropOptions {
|
||||
index: number;
|
||||
onDragStart?: (index: number) => void;
|
||||
onDragEnter?: (index: number) => void;
|
||||
onDrop?: (e: DragEvent, index: number) => void;
|
||||
onDragEnd?: () => void;
|
||||
isDragging?: boolean;
|
||||
isDragOver?: boolean;
|
||||
}
|
||||
|
||||
export function dragAndDrop(node: HTMLElement, options: DragAndDropOptions) {
|
||||
let { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options;
|
||||
|
||||
const handleDragStart = () => {
|
||||
onDragStart?.(index);
|
||||
};
|
||||
|
||||
const handleDragEnter = () => {
|
||||
onDragEnter?.(index);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
onDrop?.(e, index);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
onDragEnd?.();
|
||||
};
|
||||
|
||||
node.setAttribute('draggable', 'true');
|
||||
node.setAttribute('role', 'button');
|
||||
node.setAttribute('tabindex', '0');
|
||||
|
||||
node.addEventListener('dragstart', handleDragStart);
|
||||
node.addEventListener('dragenter', handleDragEnter);
|
||||
node.addEventListener('dragover', handleDragOver);
|
||||
node.addEventListener('drop', handleDrop);
|
||||
node.addEventListener('dragend', handleDragEnd);
|
||||
|
||||
// Update classes based on drag state
|
||||
const updateClasses = (dragging: boolean, dragOver: boolean) => {
|
||||
// Remove all drag-related classes first
|
||||
node.classList.remove('opacity-50', 'border-gray-400', 'dark:border-gray-500', 'border-solid');
|
||||
|
||||
// Add back only the active ones
|
||||
if (dragging) {
|
||||
node.classList.add('opacity-50');
|
||||
}
|
||||
|
||||
if (dragOver) {
|
||||
node.classList.add('border-gray-400', 'dark:border-gray-500', 'border-solid');
|
||||
node.classList.remove('border-transparent');
|
||||
} else {
|
||||
node.classList.add('border-transparent');
|
||||
}
|
||||
};
|
||||
|
||||
updateClasses(isDragging || false, isDragOver || false);
|
||||
|
||||
return {
|
||||
update(newOptions: DragAndDropOptions) {
|
||||
index = newOptions.index;
|
||||
onDragStart = newOptions.onDragStart;
|
||||
onDragEnter = newOptions.onDragEnter;
|
||||
onDrop = newOptions.onDrop;
|
||||
onDragEnd = newOptions.onDragEnd;
|
||||
|
||||
const newIsDragging = newOptions.isDragging || false;
|
||||
const newIsDragOver = newOptions.isDragOver || false;
|
||||
|
||||
if (newIsDragging !== isDragging || newIsDragOver !== isDragOver) {
|
||||
isDragging = newIsDragging;
|
||||
isDragOver = newIsDragOver;
|
||||
updateClasses(isDragging, isDragOver);
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
node.removeEventListener('dragstart', handleDragStart);
|
||||
node.removeEventListener('dragenter', handleDragEnter);
|
||||
node.removeEventListener('dragover', handleDragOver);
|
||||
node.removeEventListener('drop', handleDrop);
|
||||
node.removeEventListener('dragend', handleDragEnd);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -241,7 +241,7 @@
|
||||
{@const label = component.title || component.label || key}
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-1 bg-gray-50 dark:bg-subtle border border-gray-200 dark:border-gray-700 p-4 rounded-xl"
|
||||
class="flex flex-col gap-1 bg-gray-100 dark:bg-subtle border border-gray-200 dark:border-gray-700 p-4 rounded-xl"
|
||||
>
|
||||
<!-- Select component -->
|
||||
{#if component.type === 'select'}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import type { PluginActionResponseDto, PluginFilterResponseDto, PluginTriggerResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiChevronRight, mdiFilterOutline, mdiFlashOutline, mdiPlayCircleOutline } from '@mdi/js';
|
||||
|
||||
interface Props {
|
||||
trigger: PluginTriggerResponseDto;
|
||||
filters: PluginFilterResponseDto[];
|
||||
actions: PluginActionResponseDto[];
|
||||
}
|
||||
|
||||
let { trigger, filters, actions }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="fixed right-20 top-1/2 -translate-y-1/2 z-10 w-64">
|
||||
<div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-lg p-4">
|
||||
<h3 class="text-sm font-semibold mb-4 text-gray-700 dark:text-gray-300">Workflow Summary</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Trigger -->
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="shrink-0 mt-0.5">
|
||||
<div class="h-6 w-6 rounded-md bg-indigo-100 dark:bg-primary/20 flex items-center justify-center">
|
||||
<Icon icon={mdiFlashOutline} size="16" class="text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">{trigger.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
{#if filters.length > 0}
|
||||
<div class="flex justify-center">
|
||||
<Icon icon={mdiChevronRight} size="20" class="text-gray-400 rotate-90" />
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="shrink-0 mt-0.5">
|
||||
<div class="h-6 w-6 rounded-md bg-amber-100 dark:bg-amber-950 flex items-center justify-center">
|
||||
<Icon icon={mdiFilterOutline} size="16" class="text-warning" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="space-y-1">
|
||||
{#each filters as filter, index (filter.id)}
|
||||
<p class="text-xs text-gray-700 dark:text-gray-300 truncate">
|
||||
{index + 1}. {filter.title}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Arrow -->
|
||||
{#if actions.length > 0}
|
||||
<div class="flex justify-center">
|
||||
<Icon icon={mdiChevronRight} size="20" class="text-gray-400 rotate-90" />
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="shrink-0 mt-0.5">
|
||||
<div class="h-6 w-6 rounded-md bg-teal-100 dark:bg-teal-950 flex items-center justify-center">
|
||||
<Icon icon={mdiPlayCircleOutline} size="16" class="text-success" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="space-y-1">
|
||||
{#each actions as action, index (action.id)}
|
||||
<p class="text-xs text-gray-700 dark:text-gray-300 truncate">
|
||||
{index + 1}. {action.title}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { dragAndDrop } from '$lib/actions/drag-and-drop';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import SchemaFormFields from '$lib/components/workflow/schema-form/SchemaFormFields.svelte';
|
||||
import WorkflowCardConnector from '$lib/components/workflows/workflow-card-connector.svelte';
|
||||
import WorkflowSummarySidebar from '$lib/components/workflows/workflow-summary-sidebar.svelte';
|
||||
import WorkflowTriggerCard from '$lib/components/workflows/workflow-trigger-card.svelte';
|
||||
import type { PluginResponseDto } from '@immich/sdk';
|
||||
import type { PluginActionResponseDto, PluginFilterResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -11,7 +13,6 @@
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CodeBlock,
|
||||
Container,
|
||||
Field,
|
||||
HStack,
|
||||
@@ -24,6 +25,7 @@
|
||||
} from '@immich/ui';
|
||||
import {
|
||||
mdiContentSave,
|
||||
mdiDragVertical,
|
||||
mdiFilterOutline,
|
||||
mdiFlashOutline,
|
||||
mdiInformationOutline,
|
||||
@@ -38,10 +40,9 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let plugins = $state<PluginResponseDto[]>(data.plugins);
|
||||
let triggers = data.triggers;
|
||||
let filters = plugins.flatMap((plugin) => plugin.filters);
|
||||
let action = plugins.flatMap((plugin) => plugin.actions);
|
||||
const triggers = data.triggers;
|
||||
const filters = data.plugins.flatMap((plugin) => plugin.filters);
|
||||
const action = data.plugins.flatMap((plugin) => plugin.actions);
|
||||
|
||||
let previousWorkflow = data.workflow;
|
||||
let editWorkflow = $state(data.workflow);
|
||||
@@ -54,6 +55,10 @@
|
||||
|
||||
let supportFilters = $derived(filters.filter((filter) => filter.supportedContexts.includes(selectedTrigger.context)));
|
||||
let supportActions = $derived(action.filter((action) => action.supportedContexts.includes(selectedTrigger.context)));
|
||||
|
||||
let orderedFilters: PluginFilterResponseDto[] = $derived(supportFilters);
|
||||
let orderedActions: PluginActionResponseDto[] = $derived(supportActions);
|
||||
|
||||
$effect(() => {
|
||||
editWorkflow.triggerType = triggerType;
|
||||
});
|
||||
@@ -62,13 +67,73 @@
|
||||
|
||||
let canSave: boolean = $derived(!isEqual(previousWorkflow, editWorkflow));
|
||||
|
||||
let filterConfigs = $state({});
|
||||
let actionConfigs = $state({});
|
||||
let filterConfigs: Record<string, unknown> = $state({});
|
||||
let actionConfigs: Record<string, unknown> = $state({});
|
||||
|
||||
$inspect(filterConfigs).with(console.log);
|
||||
// Drag and drop handlers
|
||||
let draggedFilterIndex: number | null = $state(null);
|
||||
let draggedActionIndex: number | null = $state(null);
|
||||
let dragOverFilterIndex: number | null = $state(null);
|
||||
let dragOverActionIndex: number | null = $state(null);
|
||||
|
||||
const handleFilterDragStart = (index: number) => {
|
||||
draggedFilterIndex = index;
|
||||
};
|
||||
|
||||
const handleFilterDragEnter = (index: number) => {
|
||||
if (draggedFilterIndex !== null && draggedFilterIndex !== index) {
|
||||
dragOverFilterIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterDrop = (e: DragEvent, index: number) => {
|
||||
e.preventDefault();
|
||||
if (draggedFilterIndex === null || draggedFilterIndex === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilters = [...orderedFilters];
|
||||
const [draggedItem] = newFilters.splice(draggedFilterIndex, 1);
|
||||
newFilters.splice(index, 0, draggedItem);
|
||||
orderedFilters = newFilters;
|
||||
};
|
||||
|
||||
const handleFilterDragEnd = () => {
|
||||
draggedFilterIndex = null;
|
||||
dragOverFilterIndex = null;
|
||||
};
|
||||
|
||||
const handleActionDragStart = (index: number) => {
|
||||
draggedActionIndex = index;
|
||||
};
|
||||
|
||||
const handleActionDragEnter = (index: number) => {
|
||||
if (draggedActionIndex !== null && draggedActionIndex !== index) {
|
||||
dragOverActionIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionDrop = (e: DragEvent, index: number) => {
|
||||
e.preventDefault();
|
||||
if (draggedActionIndex === null || draggedActionIndex === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newActions = [...orderedActions];
|
||||
const [draggedItem] = newActions.splice(draggedActionIndex, 1);
|
||||
newActions.splice(index, 0, draggedItem);
|
||||
orderedActions = newActions;
|
||||
};
|
||||
|
||||
const handleActionDragEnd = () => {
|
||||
draggedActionIndex = null;
|
||||
dragOverActionIndex = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title} scrollbar={false}>
|
||||
<WorkflowSummarySidebar trigger={selectedTrigger} filters={orderedFilters} actions={orderedActions} />
|
||||
|
||||
{#snippet buttons()}
|
||||
<HStack gap={4} class="me-4">
|
||||
<HStack gap={2}>
|
||||
@@ -141,7 +206,7 @@
|
||||
|
||||
<WorkflowCardConnector />
|
||||
|
||||
<Card expandable expanded={true}>
|
||||
<Card expandable expanded={false}>
|
||||
<CardHeader class="bg-amber-50 dark:bg-[#5e4100]">
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon icon={mdiFilterOutline} size="20" class="mt-1 text-warning" />
|
||||
@@ -153,15 +218,47 @@
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
<div class="my-4">
|
||||
<!-- <div class="my-4">
|
||||
<p>Payload</p>
|
||||
<CodeBlock code={JSON.stringify(filterConfigs, null, 2)} lineNumbers></CodeBlock>
|
||||
<CodeBlock code={JSON.stringify(orderedFilterPayload, null, 2)} lineNumbers></CodeBlock>
|
||||
</div> -->
|
||||
|
||||
{#each orderedFilters as filter, index (filter.id)}
|
||||
{#if index > 0}
|
||||
<div class="relative flex justify-center py-4">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t-2 border-dashed border-muted"></div>
|
||||
</div>
|
||||
|
||||
{#each supportFilters as filter (filter.id)}
|
||||
<h1 class="grid grid-cols-2 gap-4 font-bold mt-5 mb-2">{filter.title}</h1>
|
||||
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="bg-white dark:bg-gray-900 px-2 font-semibold">THEN</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
use:dragAndDrop={{
|
||||
index,
|
||||
onDragStart: handleFilterDragStart,
|
||||
onDragEnter: handleFilterDragEnter,
|
||||
onDrop: handleFilterDrop,
|
||||
onDragEnd: handleFilterDragEnd,
|
||||
isDragging: draggedFilterIndex === index,
|
||||
isDragOver: dragOverFilterIndex === index,
|
||||
}}
|
||||
class="mb-4 cursor-move rounded-lg border-2 p-4 transition-all bg-gray-50 dark:bg-gray-950/20 border-dashed border-transparent hover:border-gray-300 dark:hover:border-gray-600"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="h-8 w-8 rounded-lg borderflex place-items-center place-content-center shrink-0 border">
|
||||
<p class="font-mono text-sm font-bold">
|
||||
{index + 1}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="font-bold text-lg mb-3">{filter.title}</h1>
|
||||
<SchemaFormFields schema={filter.schema} bind:config={filterConfigs} configKey={filter.methodName} />
|
||||
</div>
|
||||
<Icon icon={mdiDragVertical} class="mt-1 text-primary shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -169,7 +266,7 @@
|
||||
<WorkflowCardConnector />
|
||||
|
||||
<Card expandable>
|
||||
<CardHeader class="bg-teal-50 dark:bg-teal-950">
|
||||
<CardHeader class="bg-success/10 dark:bg-teal-950">
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon icon={mdiPlayCircleOutline} size="20" class="mt-1 text-success" />
|
||||
<div class="flex flex-col">
|
||||
@@ -180,14 +277,47 @@
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
<div class="my-4">
|
||||
<!-- <div class="my-4">
|
||||
<p>Payload</p>
|
||||
<CodeBlock code={JSON.stringify(actionConfigs, null, 2)} lineNumbers></CodeBlock>
|
||||
</div>
|
||||
<CodeBlock code={JSON.stringify(orderedActionPayload, null, 2)} lineNumbers></CodeBlock>
|
||||
</div> -->
|
||||
|
||||
{#each supportActions as action (action.id)}
|
||||
<h1 class="grid grid-cols-2 gap-4 font-bold">{action.title}</h1>
|
||||
{#each orderedActions as action, index (action.id)}
|
||||
{#if index > 0}
|
||||
<div class="relative flex justify-center py-4">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t-2 border-dashed border-muted"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="bg-white dark:bg-gray-900 px-2 font-semibold">THEN</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
use:dragAndDrop={{
|
||||
index,
|
||||
onDragStart: handleActionDragStart,
|
||||
onDragEnter: handleActionDragEnter,
|
||||
onDrop: handleActionDrop,
|
||||
onDragEnd: handleActionDragEnd,
|
||||
isDragging: draggedActionIndex === index,
|
||||
isDragOver: dragOverActionIndex === index,
|
||||
}}
|
||||
class="mb-4 cursor-move rounded-lg border-2 p-4 transition-all bg-gray-50 dark:bg-gray-950/20 border-dashed border-transparent hover:border-gray-300 dark:hover:border-gray-600"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="h-8 w-8 rounded-lg borderflex place-items-center place-content-center shrink-0 border">
|
||||
<p class="font-mono text-sm font-bold">
|
||||
{index + 1}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="font-bold text-lg mb-3">{action.title}</h1>
|
||||
<SchemaFormFields schema={action.schema} bind:config={actionConfigs} configKey={action.methodName} />
|
||||
</div>
|
||||
<Icon icon={mdiDragVertical} class="mt-1 text-primary shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user