Compare commits

...

5 Commits

Author SHA1 Message Date
Alex Tran
77578947f8 more refactor 2025-12-05 20:26:02 +00:00
Alex Tran
76ec9e3ebf refactor ActionItem 2025-12-05 16:55:10 +00:00
Alex Tran
1e238e7a48 refactor ActionItem 2025-12-05 16:37:10 +00:00
Alex Tran
63e38f347e pr feedback 2025-12-05 16:13:50 +00:00
Alex Tran
6222c4e97f use for Props 2025-12-05 15:15:09 +00:00
14 changed files with 391 additions and 192 deletions

View File

@@ -0,0 +1,105 @@
import type { Attachment } from 'svelte/attachments';
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(options: DragAndDropOptions): Attachment {
return (node: Element) => {
const element = node as HTMLElement;
const { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options;
const isFormElement = (el: HTMLElement) => {
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT';
};
const handleDragStart = (e: DragEvent) => {
// Prevent drag if it originated from an input, textarea, or select element
const target = e.target as HTMLElement;
if (isFormElement(target)) {
e.preventDefault();
return;
}
onDragStart?.(index);
};
const handleDragEnter = () => {
onDragEnter?.(index);
};
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
};
const handleDrop = (e: DragEvent) => {
onDrop?.(e, index);
};
const handleDragEnd = () => {
onDragEnd?.();
};
// Disable draggable when focusing on form elements (fixes Firefox input interaction)
const handleFocusIn = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (isFormElement(target)) {
element.setAttribute('draggable', 'false');
}
};
const handleFocusOut = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (isFormElement(target)) {
element.setAttribute('draggable', 'true');
}
};
// Update classes based on drag state
const updateClasses = (dragging: boolean, dragOver: boolean) => {
// Remove all drag-related classes first
element.classList.remove('opacity-50', 'border-light-500', 'border-solid');
// Add back only the active ones
if (dragging) {
element.classList.add('opacity-50');
}
if (dragOver) {
element.classList.add('border-light-500', 'border-solid');
element.classList.remove('border-transparent');
} else {
element.classList.add('border-transparent');
}
};
element.setAttribute('draggable', 'true');
element.setAttribute('role', 'button');
element.setAttribute('tabindex', '0');
element.addEventListener('dragstart', handleDragStart);
element.addEventListener('dragenter', handleDragEnter);
element.addEventListener('dragover', handleDragOver);
element.addEventListener('drop', handleDrop);
element.addEventListener('dragend', handleDragEnd);
element.addEventListener('focusin', handleFocusIn);
element.addEventListener('focusout', handleFocusOut);
updateClasses(isDragging || false, isDragOver || false);
return () => {
element.removeEventListener('dragstart', handleDragStart);
element.removeEventListener('dragenter', handleDragEnter);
element.removeEventListener('dragover', handleDragOver);
element.removeEventListener('drop', handleDrop);
element.removeEventListener('dragend', handleDragEnd);
element.removeEventListener('focusin', handleFocusIn);
element.removeEventListener('focusout', handleFocusOut);
};
};
}

View File

@@ -3,11 +3,11 @@
import { Field, Input, MultiSelect, Select, Switch, Text, type SelectItem } from '@immich/ui';
import WorkflowPickerField from './WorkflowPickerField.svelte';
interface Props {
type Props = {
schema: object | null;
config: Record<string, unknown>;
configKey?: string;
}
};
let { schema = null, config = $bindable({}), configKey }: Props = $props();

View File

@@ -1,7 +1,7 @@
<script lang="ts">
interface Props {
type Props = {
animated?: boolean;
}
};
let { animated = true }: Props = $props();
</script>

View File

@@ -5,11 +5,11 @@
import { mdiCodeJson } from '@mdi/js';
import { JSONEditor, Mode, type Content, type OnChangeStatus } from 'svelte-jsoneditor';
interface Props {
type Props = {
jsonContent: WorkflowPayload;
onApply: () => void;
onContentChange: (content: WorkflowPayload) => void;
}
};
let { jsonContent, onApply, onContentChange }: Props = $props();

View File

@@ -1,19 +1,20 @@
<script lang="ts">
import WorkflowPickerItemCard from '$lib/components/workflows/WorkflowPickerItemCard.svelte';
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import { fetchPickerMetadata, type PickerMetadata } from '$lib/services/workflow.service';
import type { ComponentConfig } from '$lib/utils/workflow';
import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
import { Button, Card, CardBody, Field, IconButton, modalManager, Text } from '@immich/ui';
import { mdiClose, mdiPlus } from '@mdi/js';
import type { AlbumResponseDto, PersonResponseDto } from '@immich/sdk';
import { Button, Field, modalManager } from '@immich/ui';
import { mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
type Props = {
component: ComponentConfig;
configKey: string;
value: string | string[] | undefined;
onchange: (value: string | string[]) => void;
}
};
let { component, configKey, value = $bindable(), onchange }: Props = $props();
@@ -22,7 +23,7 @@
const isAlbum = $derived(subType === 'album-picker');
const multiple = $derived(component.type === 'multiselect' || Array.isArray(value));
let pickerMetadata = $state<AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]>();
let pickerMetadata = $state<PickerMetadata | undefined>();
$effect(() => {
if (!value) {
@@ -30,34 +31,20 @@
return;
}
void fetchMetadata();
if (!pickerMetadata) {
void loadMetadata();
}
});
const fetchMetadata = async () => {
if (!value || pickerMetadata) {
return;
}
try {
if (Array.isArray(value) && value.length > 0) {
// Multiple selection
pickerMetadata = await (isAlbum
? Promise.all(value.map((id) => getAlbumInfo({ id })))
: Promise.all(value.map((id) => getPerson({ id }))));
} else if (typeof value === 'string' && value) {
// Single selection
pickerMetadata = await (isAlbum ? getAlbumInfo({ id: value }) : getPerson({ id: value }));
}
} catch (error) {
console.error(`Failed to fetch metadata for ${configKey}:`, error);
}
const loadMetadata = async () => {
pickerMetadata = await fetchPickerMetadata(value, subType);
};
const handlePicker = async () => {
if (isAlbum) {
const albums = await modalManager.show(AlbumPickerModal, { shared: false });
if (albums && albums.length > 0) {
const newValue = multiple ? albums.map((a) => a.id) : albums[0].id;
const newValue = multiple ? albums.map((album) => album.id) : albums[0].id;
onchange(newValue);
pickerMetadata = multiple ? albums : albums[0];
}
@@ -66,7 +53,7 @@
const excludedIds = multiple ? currentIds : [];
const people = await modalManager.show(PeoplePickerModal, { multiple, excludedIds });
if (people && people.length > 0) {
const newValue = multiple ? people.map((p) => p.id) : people[0].id;
const newValue = multiple ? people.map((person) => person.id) : people[0].id;
onchange(newValue);
pickerMetadata = multiple ? people : people[0];
}
@@ -99,58 +86,14 @@
};
</script>
{#snippet pickerItemCard(item: AlbumResponseDto | PersonResponseDto, onRemove: () => void)}
<Card color="secondary">
<CardBody class="flex items-center gap-3">
<div class="shrink-0">
{#if isAlbum && 'albumThumbnailAssetId' in item}
{#if item.albumThumbnailAssetId}
<img
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
alt={item.albumName}
class="h-12 w-12 rounded-lg object-cover"
/>
{:else}
<div class="h-12 w-12 rounded-lg"></div>
{/if}
{:else if !isAlbum && 'name' in item}
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="h-12 w-12 rounded-full object-cover" />
{/if}
</div>
<div class="min-w-0 flex-1">
<Text class="font-semibold truncate">
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
</Text>
{#if isAlbum && 'assetCount' in item}
<Text size="small" color="muted">
{$t('items_count', { values: { count: item.assetCount } })}
</Text>
{/if}
</div>
<IconButton
type="button"
onclick={onRemove}
class="shrink-0"
shape="round"
aria-label={$t('remove')}
icon={mdiClose}
size="small"
variant="ghost"
color="secondary"
/>
</CardBody>
</Card>
{/snippet}
<Field {label} required={component.required} description={component.description} requiredIndicator={component.required}>
<div class="flex flex-col gap-3">
{#if pickerMetadata && !Array.isArray(pickerMetadata)}
{@render pickerItemCard(pickerMetadata, removeSelection)}
<WorkflowPickerItemCard item={pickerMetadata} {isAlbum} onRemove={removeSelection} />
{:else if pickerMetadata && Array.isArray(pickerMetadata) && pickerMetadata.length > 0}
<div class="flex flex-col gap-2">
{#each pickerMetadata as item (item.id)}
{@render pickerItemCard(item, () => removeItemFromSelection(item.id))}
<WorkflowPickerItemCard {item} {isAlbum} onRemove={() => removeItemFromSelection(item.id)} />
{/each}
</div>
{/if}

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import type { AlbumResponseDto, PersonResponseDto } from '@immich/sdk';
import { Card, CardBody, IconButton, Text } from '@immich/ui';
import { mdiClose } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
item: AlbumResponseDto | PersonResponseDto;
isAlbum: boolean;
onRemove: () => void;
};
let { item, isAlbum, onRemove }: Props = $props();
</script>
<Card color="secondary">
<CardBody class="flex items-center gap-3">
<div class="shrink-0">
{#if isAlbum && 'albumThumbnailAssetId' in item}
{#if item.albumThumbnailAssetId}
<img
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
alt={item.albumName}
class="h-12 w-12 rounded-lg object-cover"
/>
{:else}
<div class="h-12 w-12 rounded-lg"></div>
{/if}
{:else if !isAlbum && 'name' in item}
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="h-12 w-12 rounded-full object-cover" />
{/if}
</div>
<div class="min-w-0 flex-1">
<Text class="font-semibold truncate">
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
</Text>
{#if isAlbum && 'assetCount' in item}
<Text size="small" color="muted">
{$t('items_count', { values: { count: item.assetCount } })}
</Text>
{/if}
</div>
<IconButton
type="button"
onclick={onRemove}
class="shrink-0"
shape="round"
aria-label={$t('remove')}
icon={mdiClose}
size="small"
variant="ghost"
color="secondary"
/>
</CardBody>
</Card>

View File

@@ -9,11 +9,11 @@
import { mdiClose, mdiFilterOutline, mdiFlashOutline, mdiPlayCircleOutline, mdiViewDashboardOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
type Props = {
trigger: PluginTriggerResponseDto;
filters: PluginFilterResponseDto[];
actions: PluginActionResponseDto[];
}
};
let { trigger, filters, actions }: Props = $props();

View File

@@ -4,11 +4,11 @@
import { mdiFaceRecognition, mdiFileUploadOutline, mdiLightningBolt } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
type Props = {
trigger: PluginTriggerResponseDto;
selected: boolean;
onclick: () => void;
}
};
let { trigger, selected, onclick }: Props = $props();

View File

@@ -8,6 +8,7 @@ import type {
SharedLinkResponseDto,
SystemConfigDto,
UserAdminResponseDto,
WorkflowResponseDto,
} from '@immich/sdk';
export type Events = {
@@ -42,6 +43,9 @@ export type Events = {
LibraryUpdate: [LibraryResponseDto];
LibraryDelete: [{ id: string }];
WorkflowUpdate: [WorkflowResponseDto];
WorkflowDelete: [WorkflowResponseDto];
ReleaseEvent: [ReleaseEvent];
};

View File

@@ -4,12 +4,12 @@
import { mdiFilterOutline, mdiPlayCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
type Props = {
filters: PluginFilterResponseDto[];
actions: PluginActionResponseDto[];
onClose: (result?: { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto }) => void;
type?: 'filter' | 'action';
}
};
let { filters, actions, onClose, type }: Props = $props();

View File

@@ -8,11 +8,11 @@
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
interface Props {
type Props = {
multiple?: boolean;
excludedIds?: string[];
onClose: (people?: PersonResponseDto[]) => void;
}
};
let { multiple = false, excludedIds = [], onClose }: Props = $props();

View File

@@ -1,6 +1,17 @@
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import {
createWorkflow,
deleteWorkflow,
getAlbumInfo,
getPerson,
PluginTriggerType,
updateWorkflow as updateWorkflowApi,
updateWorkflow,
type AlbumResponseDto,
type PersonResponseDto,
type PluginActionResponseDto,
type PluginContextType,
type PluginFilterResponseDto,
@@ -10,6 +21,12 @@ import {
type WorkflowResponseDto,
type WorkflowUpdateDto,
} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiCodeJson, mdiDelete, mdiPause, mdiPencil, mdiPlay } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
export type PickerSubType = 'album-picker' | 'people-picker';
export type PickerMetadata = AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[];
export interface WorkflowPayload {
name: string;
@@ -295,5 +312,137 @@ export const handleUpdateWorkflow = async (
triggerType,
};
return updateWorkflowApi({ id: workflowId, workflowUpdateDto: updateDto });
return updateWorkflow({ id: workflowId, workflowUpdateDto: updateDto });
};
export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowResponseDto) => {
const ToggleEnabled: ActionItem = {
title: workflow.enabled ? $t('disable') : $t('enable'),
icon: workflow.enabled ? mdiPause : mdiPlay,
color: workflow.enabled ? 'danger' : 'primary',
onAction: async () => {
await handleToggleWorkflowEnabled(workflow);
},
};
const Edit: ActionItem = {
title: $t('edit'),
icon: mdiPencil,
onAction: () => handleNavigateToWorkflow(workflow),
};
const Delete: ActionItem = {
title: $t('delete'),
icon: mdiDelete,
color: 'danger',
onAction: async () => {
await handleDeleteWorkflow(workflow);
},
};
return { ToggleEnabled, Edit, Delete };
};
export const getWorkflowShowSchemaAction = (
$t: MessageFormatter,
isExpanded: boolean,
onToggle: () => void,
): ActionItem => ({
title: isExpanded ? $t('hide_schema') : $t('show_schema'),
icon: mdiCodeJson,
onAction: onToggle,
});
export const handleCreateWorkflow = async (): Promise<WorkflowResponseDto | undefined> => {
const $t = await getFormatter();
try {
const workflow = await createWorkflow({
workflowCreateDto: {
name: $t('untitled_workflow'),
triggerType: PluginTriggerType.AssetCreate,
filters: [],
actions: [],
enabled: false,
},
});
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
return workflow;
} catch (error) {
handleError(error, $t('errors.unable_to_create'));
}
};
export const handleToggleWorkflowEnabled = async (
workflow: WorkflowResponseDto,
): Promise<WorkflowResponseDto | undefined> => {
const $t = await getFormatter();
try {
const updated = await updateWorkflow({
id: workflow.id,
workflowUpdateDto: { enabled: !workflow.enabled },
});
eventManager.emit('WorkflowUpdate', updated);
toastManager.success($t('workflow_updated'));
return updated;
} catch (error) {
handleError(error, $t('errors.unable_to_update_workflow'));
}
};
export const handleDeleteWorkflow = async (workflow: WorkflowResponseDto): Promise<boolean> => {
const $t = await getFormatter();
const confirmed = await modalManager.showDialog({
prompt: $t('workflow_delete_prompt'),
confirmColor: 'danger',
});
if (!confirmed) {
return false;
}
try {
await deleteWorkflow({ id: workflow.id });
eventManager.emit('WorkflowDelete', workflow);
toastManager.success($t('workflow_deleted'));
return true;
} catch (error) {
handleError(error, $t('errors.unable_to_delete_workflow'));
return false;
}
};
export const handleNavigateToWorkflow = async (workflow: WorkflowResponseDto): Promise<void> => {
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
};
export const fetchPickerMetadata = async (
value: string | string[] | undefined,
subType: PickerSubType,
): Promise<PickerMetadata | undefined> => {
if (!value) {
return undefined;
}
const isAlbum = subType === 'album-picker';
try {
if (Array.isArray(value) && value.length > 0) {
// Multiple selection
return isAlbum
? await Promise.all(value.map((id) => getAlbumInfo({ id })))
: await Promise.all(value.map((id) => getPerson({ id })));
} else if (typeof value === 'string' && value) {
// Single selection
return isAlbum ? await getAlbumInfo({ id: value }) : await getPerson({ id: value });
}
} catch (error) {
console.error(`Failed to fetch picker metadata:`, error);
}
return undefined;
};

View File

@@ -1,19 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
import emptyWorkflows from '$lib/assets/empty-workflows.svg';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AppRoute } from '$lib/constants';
import type { WorkflowPayload } from '$lib/services/workflow.service';
import { handleError } from '$lib/utils/handle-error';
import {
createWorkflow,
deleteWorkflow,
PluginTriggerType,
updateWorkflow,
type PluginFilterResponseDto,
type WorkflowResponseDto,
} from '@immich/sdk';
getWorkflowActions,
getWorkflowShowSchemaAction,
handleCreateWorkflow,
type WorkflowPayload,
} from '$lib/services/workflow.service';
import type { PluginFilterResponseDto, WorkflowResponseDto } from '@immich/sdk';
import {
Button,
Card,
@@ -27,23 +23,22 @@
IconButton,
MenuItemType,
menuManager,
modalManager,
Text,
toastManager,
VStack,
} from '@immich/ui';
import { mdiClose, mdiCodeJson, mdiDelete, mdiDotsVertical, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js';
import { mdiClose, mdiDotsVertical, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { PageData } from './$types';
interface Props {
type Props = {
data: PageData;
}
};
let { data }: Props = $props();
let workflows = $state<WorkflowResponseDto[]>(data.workflows);
const expandedWorkflows = new SvelteSet<string>();
const pluginFilterLookup = new SvelteMap<string, PluginFilterResponseDto>();
@@ -95,54 +90,14 @@
const getJson = (workflow: WorkflowResponseDto) => JSON.stringify(constructPayload(workflow), null, 2);
const handleToggleEnabled = async (workflow: WorkflowResponseDto) => {
try {
const updated = await updateWorkflow({
id: workflow.id,
workflowUpdateDto: { enabled: !workflow.enabled },
});
workflows = workflows.map((w) => (w.id === updated.id ? updated : w));
toastManager.success($t('workflow_updated'));
} catch (error) {
handleError(error, $t('errors.unable_to_update_workflow'));
}
const onWorkflowUpdate = (updatedWorkflow: WorkflowResponseDto) => {
workflows = workflows.map((currentWorkflow) =>
currentWorkflow.id === updatedWorkflow.id ? updatedWorkflow : currentWorkflow,
);
};
const handleDeleteWorkflow = async (workflow: WorkflowResponseDto) => {
try {
const confirmed = await modalManager.showDialog({
prompt: $t('workflow_delete_prompt'),
confirmColor: 'danger',
});
if (!confirmed) {
return;
}
await deleteWorkflow({ id: workflow.id });
workflows = workflows.filter((w) => w.id !== workflow.id);
toastManager.success($t('workflow_deleted'));
} catch (error) {
handleError(error, $t('errors.unable_to_delete_workflow'));
}
};
const handleEditWorkflow = async (workflow: WorkflowResponseDto) => {
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
};
const handleCreateWorkflow = async () => {
const workflow = await createWorkflow({
workflowCreateDto: {
name: 'New workflow',
triggerType: PluginTriggerType.AssetCreate,
filters: [],
actions: [],
enabled: false,
},
});
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
const onWorkflowDelete = (deletedWorkflow: WorkflowResponseDto) => {
workflows = workflows.filter((currentWorkflow) => currentWorkflow.id !== deletedWorkflow.id);
};
const getFilterLabel = (filterId: string) => {
@@ -168,8 +123,25 @@
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(createdAt));
const showWorkflowMenu = (event: MouseEvent, workflow: WorkflowResponseDto) => {
const { ToggleEnabled, Edit, Delete } = getWorkflowActions($t, workflow);
void menuManager.show({
target: event.currentTarget as HTMLElement,
position: 'top-left',
items: [
ToggleEnabled,
Edit,
getWorkflowShowSchemaAction($t, expandedWorkflows.has(workflow.id), () => toggleShowingSchema(workflow.id)),
MenuItemType.Divider,
Delete,
],
});
};
</script>
<OnEvents {onWorkflowUpdate} {onWorkflowDelete} />
{#snippet chipItem(title: string)}
<span class="rounded-xl border border-gray-200/80 px-3 py-1.5 text-sm dark:border-gray-600 bg-light">
<span class="font-medium text-dark">{title}</span>
@@ -232,38 +204,7 @@
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 ? 'danger' : 'primary',
icon: workflow.enabled ? mdiPause : mdiPlay,
onAction: () => void handleToggleEnabled(workflow),
},
{
title: $t('edit'),
icon: mdiPencil,
onAction: () => void handleEditWorkflow(workflow),
},
{
title: expandedWorkflows.has(workflow.id) ? $t('hide_schema') : $t('show_schema'),
icon: mdiCodeJson,
onAction: () => toggleShowingSchema(workflow.id),
},
MenuItemType.Divider,
{
title: $t('delete'),
icon: mdiDelete,
color: 'danger',
onAction: () => void handleDeleteWorkflow(workflow),
},
],
});
}}
onclick={(event: MouseEvent) => showWorkflowMenu(event, workflow)}
/>
</div>
</CardHeader>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { beforeNavigate, goto } from '$app/navigation';
import { dragAndDrop } from '$lib/actions/drag-and-drop';
import { dragAndDrop } from '$lib/attachments/drag-and-drop.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import SchemaFormFields from '$lib/components/workflows/SchemaFormFields.svelte';
import WorkflowCardConnector from '$lib/components/workflows/WorkflowCardConnector.svelte';
@@ -56,9 +56,9 @@
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
type Props = {
data: PageData;
}
};
let { data }: Props = $props();
@@ -321,7 +321,7 @@
</script>
{#snippet cardOrder(index: number)}
<div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border">
<div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border bg-light-50">
<Text size="small" class="font-mono font-bold">
{index + 1}
</Text>
@@ -455,7 +455,7 @@
{@render stepSeparator()}
{/if}
<div
use:dragAndDrop={{
{@attach dragAndDrop({
index,
onDragStart: handleFilterDragStart,
onDragEnter: handleFilterDragEnter,
@@ -463,7 +463,7 @@
onDragEnd: handleFilterDragEnd,
isDragging: draggedFilterIndex === index,
isDragOver: dragOverFilterIndex === index,
}}
})}
class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300"
>
<div class="flex items-start gap-4">
@@ -524,7 +524,7 @@
{@render stepSeparator()}
{/if}
<div
use:dragAndDrop={{
{@attach dragAndDrop({
index,
onDragStart: handleActionDragStart,
onDragEnter: handleActionDragEnter,
@@ -532,7 +532,7 @@
onDragEnd: handleActionDragEnd,
isDragging: draggedActionIndex === index,
isDragOver: dragOverActionIndex === index,
}}
})}
class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300"
>
<div class="flex items-start gap-4">