This commit is contained in:
Alex Tran
2025-11-24 19:10:07 +00:00
parent 1f25422958
commit 380d03476e
12 changed files with 460 additions and 368 deletions

View File

@@ -1,217 +0,0 @@
<script lang="ts">
import ActionBuilder from '$lib/components/workflow/ActionBuilder.svelte';
import FilterBuilder from '$lib/components/workflow/FilterBuilder.svelte';
import GroupTab from '$lib/elements/GroupTab.svelte';
import { handleError } from '$lib/utils/handle-error';
import {
createWorkflow,
PluginTriggerType,
updateWorkflow,
type PluginResponseDto,
type WorkflowResponseDto,
} from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Switch, Textarea } from '@immich/ui';
import { mdiAutoFix } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
workflow?: WorkflowResponseDto;
plugins: PluginResponseDto[];
onClose: () => void;
onSave: (workflow: WorkflowResponseDto) => void;
}
let { workflow, plugins, onClose, onSave }: Props = $props();
const isEditMode = !!workflow;
// Form state
let name = $state(workflow?.name || '');
let description = $state(workflow?.description || '');
let triggerType = $state<PluginTriggerType>(workflow?.triggerType || PluginTriggerType.AssetCreate);
let enabled = $state(workflow?.enabled ?? true);
let filters = $state<Array<{ filterId: string; filterConfig?: object }>>(
workflow?.filters.map((f) => ({ filterId: f.filterId, filterConfig: f.filterConfig || undefined })) || [],
);
let actions = $state<Array<{ actionId: string; actionConfig?: object }>>(
workflow?.actions.map((a) => ({ actionId: a.actionId, actionConfig: a.actionConfig || undefined })) || [],
);
// Editor mode state
let editorMode = $state<'visual' | 'json'>('visual');
let jsonText = $state('');
let jsonError = $state('');
// Sync JSON when switching to JSON mode
const syncToJson = () => {
const workflowData = {
...(isEditMode ? { id: workflow!.id } : {}),
name,
description,
triggerType,
enabled,
filters,
actions,
};
jsonText = JSON.stringify(workflowData, null, 2);
jsonError = '';
};
// Sync visual form when switching from JSON mode
const syncFromJson = () => {
try {
const parsed = JSON.parse(jsonText);
name = parsed.name || '';
description = parsed.description || '';
triggerType = parsed.triggerType || PluginTriggerType.AssetCreate;
enabled = parsed.enabled ?? true;
filters = parsed.filters || [];
actions = parsed.actions || [];
jsonError = '';
} catch (error) {
jsonError = error instanceof Error ? error.message : 'Invalid JSON';
}
};
const handleModeChange = (newMode: 'visual' | 'json') => {
if (newMode === 'json' && editorMode === 'visual') {
syncToJson();
} else if (newMode === 'visual' && editorMode === 'json') {
syncFromJson();
}
editorMode = newMode;
};
const handleSubmit = async () => {
// If in JSON mode, sync from JSON first
if (editorMode === 'json') {
syncFromJson();
if (jsonError) {
return;
}
}
if (!name.trim()) {
handleError(new Error($t('name_required')), $t('validation_error'));
return;
}
const trigger =
triggerType === PluginTriggerType.AssetCreate
? PluginTriggerType.AssetCreate
: PluginTriggerType.PersonRecognized;
try {
let result: WorkflowResponseDto;
result = await (isEditMode
? updateWorkflow({
id: workflow!.id,
workflowUpdateDto: {
name,
description: description || undefined,
enabled,
filters,
actions,
},
})
: createWorkflow({
workflowCreateDto: {
name,
description: description || undefined,
triggerType: trigger,
enabled,
filters,
actions,
},
}));
onSave(result);
onClose();
} catch (error) {
handleError(error, isEditMode ? $t('errors.unable_to_create') : $t('errors.unable_to_create'));
}
};
</script>
<Modal title={isEditMode ? $t('edit_workflow') : $t('create_workflow')} icon={mdiAutoFix} {onClose} size="large">
<ModalBody>
<div class="mb-4">
<GroupTab
filters={['visual', 'json']}
labels={[$t('visual_builder'), $t('json_editor')]}
selected={editorMode}
label={$t('editor_mode')}
onSelect={(mode) => handleModeChange(mode as 'visual' | 'json')}
/>
</div>
{#if editorMode === 'visual'}
<form
id="workflow-form"
onsubmit={(e) => {
e.preventDefault();
void handleSubmit();
}}
class="mt-4 flex flex-col gap-4"
>
<Field label={$t('name')} required>
<Input bind:value={name} required />
</Field>
<Field label={$t('description')}>
<Textarea bind:value={description} />
</Field>
{#if !isEditMode}
<Field label={$t('trigger_type')} required>
<select bind:value={triggerType} class="immich-form-input w-full" required>
<option value={PluginTriggerType.AssetCreate}>{$t('asset_created')}</option>
<option value={PluginTriggerType.PersonRecognized}>{$t('person_recognized')}</option>
</select>
</Field>
{:else}
<Field label={$t('trigger_type')}>
<Input value={triggerType} disabled />
</Field>
{/if}
<Field label={$t('enabled')}>
<Switch bind:checked={enabled} />
</Field>
<div class="border-t pt-4 dark:border-gray-700">
<h3 class="mb-2 font-semibold">{$t('filter')}</h3>
<FilterBuilder bind:filters {triggerType} {plugins} />
</div>
<div class="border-t pt-4 dark:border-gray-700">
<h3 class="mb-2 font-semibold">{$t('actions')}</h3>
<ActionBuilder bind:actions {triggerType} {plugins} />
</div>
</form>
{:else}
<div class="mt-4 flex flex-col gap-4">
{#if jsonError}
<div class="rounded-lg bg-red-100 p-3 text-sm text-red-800 dark:bg-red-900 dark:text-red-200">
{$t('json_error')}: {jsonError}
</div>
{/if}
<Field label={$t('workflow_json')}>
<textarea bind:value={jsonText} class="immich-form-input h-96 w-full font-mono text-sm" spellcheck="false"
></textarea>
</Field>
<p class="text-sm text-gray-600 dark:text-gray-400">
{$t('workflow_json_help')}
</p>
</div>
{/if}
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button color="secondary" fullWidth onclick={onClose}>{$t('cancel')}</Button>
<Button type="submit" fullWidth form="workflow-form" onclick={handleSubmit}>
{isEditMode ? $t('save') : $t('create')}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { ConfirmModal } from '@immich/ui';
import { t } from 'svelte-i18n';
type Props = {
onClose: (confirmed: boolean) => void;
};
let { onClose }: Props = $props();
</script>
<ConfirmModal
confirmColor="primary"
prompt={$t('workflow_navigation_prompt')}
onClose={(confirmed) => (confirmed ? onClose(true) : onClose(false))}
/>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { ConfirmModal } from '@immich/ui';
import { mdiLightningBolt } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
onClose: (confirmed: boolean) => void;
};
let { onClose }: Props = $props();
</script>
<ConfirmModal
confirmColor="primary"
title={$t('change_trigger')}
icon={mdiLightningBolt}
prompt={$t('change_trigger_prompt')}
onClose={(confirmed) => (confirmed ? onClose(true) : onClose(false))}
/>

View File

@@ -2,28 +2,37 @@
import { goto } from '$app/navigation';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import { AppRoute } from '$lib/constants';
import { copyToClipboard } from '$lib/utils';
import type { WorkflowPayload } from '$lib/services/workflow.service';
import { handleError } from '$lib/utils/handle-error';
import {
createWorkflow,
deleteWorkflow,
PluginTriggerType,
updateWorkflow,
type PluginActionResponseDto,
type PluginFilterResponseDto,
type PluginResponseDto,
type WorkflowResponseDto,
} from '@immich/sdk';
import { Button, Card, CardBody, CardTitle, HStack, Icon, IconButton, toastManager } from '@immich/ui';
import {
mdiChevronDown,
mdiChevronUp,
mdiContentCopy,
mdiDelete,
mdiPencil,
mdiPlay,
mdiPlayPause,
mdiPlus,
} from '@mdi/js';
Button,
Card,
CardBody,
CardDescription,
CardHeader,
CardTitle,
CodeBlock,
HStack,
Icon,
IconButton,
MenuItemType,
menuManager,
Text,
toastManager,
} from '@immich/ui';
import { mdiCodeJson, mdiDelete, mdiDotsVertical, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { PageData } from './$types';
interface Props {
@@ -33,24 +42,57 @@
let { data }: Props = $props();
let workflows = $state<WorkflowResponseDto[]>(data.workflows);
// svelte-ignore non_reactive_update
let expandedWorkflows = new SvelteSet();
const expandedWorkflows = new SvelteSet<string>();
const pluginFilterLookup = new SvelteMap<string, PluginFilterResponseDto & { pluginTitle: string }>();
const pluginActionLookup = new SvelteMap<string, PluginActionResponseDto & { pluginTitle: string }>();
for (const plugin of data.plugins as PluginResponseDto[]) {
for (const filter of plugin.filters ?? []) {
pluginFilterLookup.set(filter.id, { ...filter, pluginTitle: plugin.title });
}
for (const action of plugin.actions ?? []) {
pluginActionLookup.set(action.id, { ...action, pluginTitle: plugin.title });
}
}
const toggleExpanded = (id: string) => {
const newExpanded = new SvelteSet(expandedWorkflows);
if (newExpanded.has(id)) {
newExpanded.delete(id);
if (expandedWorkflows.has(id)) {
expandedWorkflows.delete(id);
} else {
newExpanded.add(id);
expandedWorkflows.add(id);
}
expandedWorkflows = newExpanded;
};
const handleCopyWorkflow = async (workflow: WorkflowResponseDto) => {
const workflowJson = JSON.stringify(workflow, null, 2);
await copyToClipboard(workflowJson);
const buildShareableWorkflow = (workflow: WorkflowResponseDto): WorkflowPayload => {
const orderedFilters = [...(workflow.filters ?? [])].sort((a, b) => a.order - b.order);
const orderedActions = [...(workflow.actions ?? [])].sort((a, b) => a.order - b.order);
return {
name: workflow.name ?? '',
description: workflow.description ?? '',
enabled: workflow.enabled,
triggerType: workflow.triggerType,
filters: orderedFilters.map((wfFilter) => {
const meta = pluginFilterLookup.get(wfFilter.filterId);
const key = meta?.methodName ?? wfFilter.filterId;
return {
[key]: wfFilter.filterConfig ?? {},
};
}),
actions: orderedActions.map((wfAction) => {
const meta = pluginActionLookup.get(wfAction.actionId);
const key = meta?.methodName ?? wfAction.actionId;
return {
[key]: wfAction.actionConfig ?? {},
};
}),
};
};
const getWorkflowJson = (workflow: WorkflowResponseDto) => JSON.stringify(buildShareableWorkflow(workflow), null, 2);
const handleToggleEnabled = async (workflow: WorkflowResponseDto) => {
try {
const updated = await updateWorkflow({
@@ -93,6 +135,38 @@
await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`);
};
type WorkflowChip = {
id: string;
title: string;
subtitle: string;
};
const getFilterChips = (workflow: WorkflowResponseDto): WorkflowChip[] => {
return [...(workflow.filters ?? [])]
.sort((a, b) => a.order - b.order)
.map((filter) => {
const meta = pluginFilterLookup.get(filter.filterId);
return {
id: filter.id,
title: meta?.title ?? $t('filter'),
subtitle: meta?.pluginTitle ?? $t('workflow'),
};
});
};
const getActionChips = (workflow: WorkflowResponseDto): WorkflowChip[] => {
return [...(workflow.actions ?? [])]
.sort((a, b) => a.order - b.order)
.map((action) => {
const meta = pluginActionLookup.get(action.actionId);
return {
id: action.id,
title: meta?.title ?? $t('action'),
subtitle: meta?.pluginTitle ?? $t('workflow'),
};
});
};
const getTriggerLabel = (triggerType: string) => {
const labels: Record<string, string> = {
AssetCreate: $t('asset_created'),
@@ -100,12 +174,39 @@
};
return labels[triggerType] || triggerType;
};
const dateFormatter = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
});
const formatTimestamp = (iso?: string) => {
if (!iso) {
return '—';
}
return dateFormatter.format(new Date(iso));
};
type WorkflowWithMeta = {
workflow: WorkflowResponseDto;
filterChips: WorkflowChip[];
actionChips: WorkflowChip[];
workflowJson: string;
};
const getWorkflowsWithMeta = (): WorkflowWithMeta[] =>
workflows.map((workflow) => ({
workflow,
filterChips: getFilterChips(workflow),
actionChips: getActionChips(workflow),
workflowJson: getWorkflowJson(workflow),
}));
</script>
<UserPageLayout title={data.meta.title} scrollbar={false}>
{#snippet buttons()}
<HStack gap={1}>
<Button shape="round" color="primary" onclick={handleCreateWorkflow}>
<Button size="small" variant="ghost" color="secondary" onclick={handleCreateWorkflow}>
<Icon icon={mdiPlus} size="18" />
{$t('create_workflow')}
</Button>
@@ -127,95 +228,165 @@
</Button>
</div>
{:else}
<div class="my-5 flex flex-col gap-4">
{#each workflows as workflow (workflow.id)}
<Card color="secondary">
<CardBody>
<div class="flex flex-col gap-2">
<div class="flex items-start justify-between">
<div class="flex-1">
<CardTitle>{workflow.name || $t('untitled_workflow')}</CardTitle>
{#if workflow.description}
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{workflow.description}</p>
<div class="my-6 grid gap-6">
{#each getWorkflowsWithMeta() as { workflow, filterChips, actionChips, workflowJson } (workflow.id)}
<Card class="border border-gray-200/70 shadow-xl shadow-gray-900/5 dark:border-gray-700/60">
<CardHeader
class={`flex flex-col px-8 py-6 gap-4 sm:flex-row sm:items-center sm:gap-6 ${workflow.enabled ? 'bg-linear-to-r from-green-50 to-white dark:from-green-950/40 dark:to-gray-900' : 'bg-neutral-50 dark:bg-neutral-900'}`}
>
<div class="flex-1">
<div class="flex items-center gap-3">
<span class={workflow.enabled ? 'relative flex h-3 w-3' : 'flex h-3 w-3'}>
{#if workflow.enabled}
<span class="absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
{/if}
</div>
<HStack gap={1}>
<IconButton
shape="round"
color="secondary"
icon={workflow.enabled ? mdiPlay : mdiPlayPause}
aria-label={workflow.enabled ? $t('disabled') : $t('enabled')}
onclick={() => handleToggleEnabled(workflow)}
class={workflow.enabled ? 'text-green-500' : 'text-gray-400'}
/>
<IconButton
shape="round"
color="secondary"
icon={mdiContentCopy}
aria-label={$t('copy_to_clipboard')}
onclick={() => handleCopyWorkflow(workflow)}
/>
<IconButton
shape="round"
color="secondary"
icon={mdiPencil}
aria-label={$t('edit')}
onclick={() => handleEditWorkflow(workflow)}
/>
<IconButton
shape="round"
color="secondary"
icon={mdiDelete}
aria-label={$t('delete')}
onclick={() => handleDeleteWorkflow(workflow)}
/>
</HStack>
</div>
<div class="flex flex-wrap gap-2 text-sm">
<span
class="rounded-full px-3 py-1 {workflow.enabled
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'}"
>
{workflow.enabled ? $t('enabled') : $t('disabled')}
<span
class={workflow.enabled
? 'relative inline-flex h-3 w-3 rounded-full bg-green-500'
: 'relative inline-flex h-3 w-3 rounded-full bg-gray-400 dark:bg-gray-600'}
></span>
</span>
<span class="rounded-full bg-blue-100 px-3 py-1 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<CardTitle>{workflow.name}</CardTitle>
</div>
<CardDescription class="mt-1 text-sm">
{workflow.description || $t('workflows_help_text')}
</CardDescription>
</div>
<div class="flex items-center gap-4">
<div class="text-right">
<Text size="tiny" class="text-gray-500 dark:text-gray-400">{$t('created_at')}</Text>
<Text size="small" class="font-medium">
{formatTimestamp(workflow.createdAt)}
</Text>
</div>
<IconButton
shape="round"
variant="ghost"
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 ? 'warning' : 'success',
icon: workflow.enabled ? mdiPause : mdiPlay,
onSelect: () => void handleToggleEnabled(workflow),
},
{
title: $t('edit'),
icon: mdiPencil,
onSelect: () => void handleEditWorkflow(workflow),
},
{
title: expandedWorkflows.has(workflow.id) ? $t('hide_json') : $t('show_json'),
icon: mdiCodeJson,
onSelect: () => toggleExpanded(workflow.id),
},
MenuItemType.Divider,
{
title: $t('delete'),
icon: mdiDelete,
color: 'danger',
onSelect: () => void handleDeleteWorkflow(workflow),
},
],
});
}}
/>
</div>
</CardHeader>
<CardBody class="space-y-6">
<div class="grid gap-4 md:grid-cols-3">
<!-- Trigger Section -->
<div
class="rounded-2xl border border-gray-100/80 bg-gray-50/90 p-4 dark:border-gray-700 dark:bg-gray-800"
>
<dt class="mb-3 text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-400">
{$t('trigger')}
</dt>
<span
class="inline-block rounded-xl border border-gray-200/80 bg-white/70 px-3 py-1.5 text-sm font-medium shadow-sm dark:border-gray-600 dark:bg-gray-700/80 dark:text-white"
>
{getTriggerLabel(workflow.triggerType)}
</span>
<span
class="rounded-full bg-purple-100 px-3 py-1 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
>
{workflow.filters.length}
{workflow.filters.length === 1 ? $t('filter') : $t('filter')}
</span>
<span
class="rounded-full bg-orange-100 px-3 py-1 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
>
{workflow.actions.length}
{workflow.actions.length === 1 ? $t('action') : $t('actions')}
</span>
</div>
<button
type="button"
onclick={() => toggleExpanded(workflow.id)}
class="flex items-center gap-1 text-sm text-primary hover:underline"
<!-- Filters Section -->
<div
class="rounded-2xl border border-gray-100/80 bg-gray-50/90 p-4 dark:border-gray-700 dark:bg-gray-800"
>
<Icon icon={expandedWorkflows.has(workflow.id) ? mdiChevronUp : mdiChevronDown} size="18" />
{expandedWorkflows.has(workflow.id) ? $t('hide_json') : $t('show_json')}
</button>
{#if expandedWorkflows.has(workflow.id)}
<div class="mt-2">
<pre class="overflow-x-auto rounded-lg bg-gray-100 p-4 text-xs dark:bg-gray-800">{JSON.stringify(
workflow,
null,
2,
)}</pre>
<div class="mb-3 flex items-center justify-between">
<dt class="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-400">
{$t('filter')}
</dt>
<dd
class="rounded-full bg-gray-200 px-2.5 py-0.5 text-sm font-semibold text-gray-700 dark:bg-gray-700 dark:text-gray-200"
>
{workflow.filters.length}
</dd>
</div>
{/if}
<div class="flex flex-wrap gap-2">
{#if filterChips.length === 0}
<span class="text-sm text-gray-500 dark:text-gray-400">
{$t('no_filters_added')}
</span>
{:else}
{#each filterChips as chip (chip.id)}
<span
class="rounded-xl border border-gray-200/80 bg-white/70 px-3 py-1.5 text-sm shadow-sm dark:border-gray-600 dark:bg-gray-700/80"
>
<span class="font-medium text-gray-900 dark:text-white">{chip.title}</span>
</span>
{/each}
{/if}
</div>
</div>
<!-- Actions Section -->
<div
class="rounded-2xl border border-gray-100/80 bg-gray-50/90 p-4 dark:border-gray-700 dark:bg-gray-800"
>
<div class="mb-3 flex items-center justify-between">
<dt class="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-400">
{$t('actions')}
</dt>
<dd
class="rounded-full bg-gray-200 px-2.5 py-0.5 text-sm font-semibold text-gray-700 dark:bg-gray-700 dark:text-gray-200"
>
{workflow.actions.length}
</dd>
</div>
<div class="flex flex-wrap gap-2">
{#if actionChips.length === 0}
<span class="text-sm text-gray-500 dark:text-gray-400">
{$t('no_actions_added')}
</span>
{:else}
{#each actionChips as chip (chip.id)}
<span
class="rounded-xl border border-gray-200/80 bg-white/70 px-3 py-1.5 text-sm shadow-sm dark:border-gray-600 dark:bg-gray-700/80"
>
<span class="font-medium text-gray-900 dark:text-white">{chip.title}</span>
</span>
{/each}
{/if}
</div>
</div>
</div>
{#if expandedWorkflows.has(workflow.id)}
<div>
<p class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-200">Workflow JSON</p>
<CodeBlock code={workflowJson} />
</div>
{/if}
</CardBody>
</Card>
{/each}

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { beforeNavigate, goto } from '$app/navigation';
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';
@@ -6,8 +7,11 @@
import WorkflowJsonEditor from '$lib/components/workflows/workflow-json-editor.svelte';
import WorkflowTriggerCard from '$lib/components/workflows/workflow-trigger-card.svelte';
import AddWorkflowStepModal from '$lib/modals/AddWorkflowStepModal.svelte';
import WorkflowNavigationConfirmModal from '$lib/modals/WorkflowNavigationConfirmModal.svelte';
import WorkflowTriggerUpdateConfirmModal from '$lib/modals/WorkflowTriggerUpdateConfirmModal.svelte';
import { WorkflowService, type WorkflowPayload } from '$lib/services/workflow.service';
import type { PluginActionResponseDto, PluginFilterResponseDto } from '@immich/sdk';
import { handleError } from '$lib/utils/handle-error';
import type { PluginActionResponseDto, PluginFilterResponseDto, PluginTriggerResponseDto } from '@immich/sdk';
import {
Button,
Card,
@@ -39,6 +43,7 @@
} from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
@@ -97,6 +102,18 @@
const updateWorkflow = async () => {
try {
console.log('Updating workflow with:', {
id: editWorkflow.id,
name,
description,
enabled: editWorkflow.enabled,
triggerType,
orderedFilters: orderedFilters.map((f) => ({ id: f.id, methodName: f.methodName })),
orderedActions: orderedActions.map((a) => ({ id: a.id, methodName: a.methodName })),
filterConfigs,
actionConfigs,
});
const updated = await workflowService.updateWorkflow(
editWorkflow.id,
name,
@@ -113,7 +130,8 @@
previousWorkflow = updated;
editWorkflow = updated;
} catch (error) {
console.error('Failed to update workflow:', error);
console.log('error', error);
handleError(error, 'Failed to update workflow');
}
};
@@ -261,6 +279,34 @@
const handleRemoveAction = (index: number) => {
orderedActions = orderedActions.filter((_, i) => i !== index);
};
const handleTriggerChange = async (newTrigger: PluginTriggerResponseDto) => {
const isConfirmed = await modalManager.show(WorkflowTriggerUpdateConfirmModal);
if (!isConfirmed) {
return;
}
selectedTrigger = newTrigger;
};
let allowNavigation = $state(false);
beforeNavigate(({ cancel, to }) => {
if (hasChanges && !allowNavigation) {
cancel();
modalManager
.show(WorkflowNavigationConfirmModal)
.then((isConfirmed) => {
if (isConfirmed && to) {
allowNavigation = true;
void goto(to.url);
}
})
.catch(() => {});
}
});
</script>
{#snippet cardOrder(index: number)}
@@ -387,7 +433,7 @@
<WorkflowTriggerCard
{trigger}
selected={selectedTrigger.triggerType === trigger.triggerType}
onclick={() => (selectedTrigger = trigger)}
onclick={() => handleTriggerChange(trigger)}
/>
{/each}
</div>