mirror of
https://github.com/immich-app/immich.git
synced 2025-12-25 17:24:58 +03:00
pr feedback
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
|
||||
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
|
||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { formatLabel, getComponentFromSchema } from '$lib/utils/workflow';
|
||||
import { formatLabel, getComponentFromSchema, type ComponentConfig } from '$lib/utils/workflow';
|
||||
import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button, Field, Input, MultiSelect, Select, Switch, Text, modalManager, type SelectItem } from '@immich/ui';
|
||||
import { mdiPlus } from '@mdi/js';
|
||||
@@ -43,56 +43,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchMetadata = async () => {
|
||||
const metadataUpdates: Record<
|
||||
string,
|
||||
AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]
|
||||
> = {};
|
||||
|
||||
for (const [key, component] of Object.entries(components)) {
|
||||
const value = actualConfig[key];
|
||||
if (!value || pickerMetadata[key]) {
|
||||
continue; // Skip if no value or already loaded
|
||||
}
|
||||
|
||||
const isAlbumPicker = component.subType === 'album-picker';
|
||||
const isPeoplePicker = component.subType === 'people-picker';
|
||||
|
||||
if (!isAlbumPicker && !isPeoplePicker) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
// Multiple selection
|
||||
if (isAlbumPicker) {
|
||||
const albums = await Promise.all(value.map((id) => getAlbumInfo({ id })));
|
||||
metadataUpdates[key] = albums;
|
||||
} else if (isPeoplePicker) {
|
||||
const people = await Promise.all(value.map((id) => getPerson({ id })));
|
||||
metadataUpdates[key] = people;
|
||||
}
|
||||
} else if (typeof value === 'string' && value) {
|
||||
// Single selection
|
||||
if (isAlbumPicker) {
|
||||
const album = await getAlbumInfo({ id: value });
|
||||
metadataUpdates[key] = album;
|
||||
} else if (isPeoplePicker) {
|
||||
const person = await getPerson({ id: value });
|
||||
metadataUpdates[key] = person;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch metadata for ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(metadataUpdates).length > 0) {
|
||||
pickerMetadata = { ...pickerMetadata, ...metadataUpdates };
|
||||
}
|
||||
};
|
||||
|
||||
void fetchMetadata();
|
||||
void fetchMetadata(components);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -148,6 +99,55 @@
|
||||
}
|
||||
});
|
||||
|
||||
const fetchMetadata = async (components: Record<string, ComponentConfig>) => {
|
||||
const metadataUpdates: Record<
|
||||
string,
|
||||
AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]
|
||||
> = {};
|
||||
|
||||
for (const [key, component] of Object.entries(components)) {
|
||||
const value = actualConfig[key];
|
||||
if (!value || pickerMetadata[key]) {
|
||||
continue; // Skip if no value or already loaded
|
||||
}
|
||||
|
||||
const isAlbumPicker = component.subType === 'album-picker';
|
||||
const isPeoplePicker = component.subType === 'people-picker';
|
||||
|
||||
if (!isAlbumPicker && !isPeoplePicker) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
// Multiple selection
|
||||
if (isAlbumPicker) {
|
||||
const albums = await Promise.all(value.map((id) => getAlbumInfo({ id })));
|
||||
metadataUpdates[key] = albums;
|
||||
} else if (isPeoplePicker) {
|
||||
const people = await Promise.all(value.map((id) => getPerson({ id })));
|
||||
metadataUpdates[key] = people;
|
||||
}
|
||||
} else if (typeof value === 'string' && value) {
|
||||
// Single selection
|
||||
if (isAlbumPicker) {
|
||||
const album = await getAlbumInfo({ id: value });
|
||||
metadataUpdates[key] = album;
|
||||
} else if (isPeoplePicker) {
|
||||
const person = await getPerson({ id: value });
|
||||
metadataUpdates[key] = person;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch metadata for ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(metadataUpdates).length > 0) {
|
||||
pickerMetadata = { ...pickerMetadata, ...metadataUpdates };
|
||||
}
|
||||
};
|
||||
|
||||
const handleAlbumPicker = async (key: string, multiple: boolean) => {
|
||||
const albums = await modalManager.show(AlbumPickerModal, { shared: false });
|
||||
if (albums && albums.length > 0) {
|
||||
@@ -161,7 +161,9 @@
|
||||
};
|
||||
|
||||
const handlePeoplePicker = async (key: string, multiple: boolean) => {
|
||||
const people = await modalManager.show(PeoplePickerModal, { multiple });
|
||||
const currentIds = (actualConfig[key] as string[] | undefined) ?? [];
|
||||
const excludedIds = multiple ? currentIds : [];
|
||||
const people = await modalManager.show(PeoplePickerModal, { multiple, excludedIds });
|
||||
if (people && people.length > 0) {
|
||||
const value = multiple ? people.map((p) => p.id) : people[0].id;
|
||||
updateConfig(key, value);
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={containerEl}
|
||||
class="hidden sm:block fixed w-64 z-50 hover:cursor-grab select-none"
|
||||
class="hidden sm:block fixed w-64 hover:cursor-grab select-none"
|
||||
style="left: {position.x}px; top: {position.y}px;"
|
||||
class:cursor-grabbing={isDragging}
|
||||
onmousedown={handleMouseDown}
|
||||
@@ -112,7 +112,7 @@
|
||||
<span class="text-[10px] font-semibold uppercase tracking-wide">{$t('filters')}</span>
|
||||
</div>
|
||||
<div class="space-y-1 pl-5">
|
||||
{#each filters as filter, index (filter.id)}
|
||||
{#each filters as filter, index (index)}
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="shrink-0 h-4 w-4 rounded-full bg-light-200 text-[10px] font-medium flex items-center justify-center"
|
||||
@@ -138,7 +138,7 @@
|
||||
<span class="text-[10px] font-semibold uppercase tracking-wide">{$t('actions')}</span>
|
||||
</div>
|
||||
<div class="space-y-1 pl-5">
|
||||
{#each actions as action, index (action.id)}
|
||||
{#each actions as action, index (index)}
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="shrink-0 h-4 w-4 rounded-full bg-light-200 text-[10px] font-medium flex items-center justify-center"
|
||||
@@ -156,7 +156,7 @@
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="hidden sm:flex fixed right-6 bottom-6 z-50 h-14 w-14 items-center justify-center rounded-full bg-primary text-light shadow-lg hover:bg-primary/90 transition-colors"
|
||||
class="hidden sm:flex fixed right-6 bottom-6 h-14 w-14 items-center justify-center rounded-full bg-primary text-light shadow-lg hover:bg-primary/90 transition-colors"
|
||||
title={$t('workflow_summary')}
|
||||
onclick={() => (isOpen = true)}
|
||||
>
|
||||
|
||||
@@ -39,12 +39,12 @@
|
||||
? 'bg-primary text-light'
|
||||
: 'text-light-100 bg-light-300 group-hover:bg-light-500'}"
|
||||
>
|
||||
<Icon icon={getTriggerIcon(trigger.triggerType)} size="24" />
|
||||
<Icon icon={getTriggerIcon(trigger.type)} size="24" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<Text class="font-semibold mb-1">{trigger.name}</Text>
|
||||
{#if trigger.description}
|
||||
<Text class="text-sm">{trigger.description}</Text>
|
||||
<Text size="small">{trigger.description}</Text>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,6 @@ export enum AppRoute {
|
||||
LARGE_FILES = '/utilities/large-files',
|
||||
GEOLOCATION = '/utilities/geolocation',
|
||||
WORKFLOWS = '/utilities/workflows',
|
||||
WORKFLOWS_EDIT = '/utilities/workflows/edit',
|
||||
|
||||
FOLDERS = '/folders',
|
||||
TAGS = '/tags',
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { PluginActionResponseDto, PluginFilterResponseDto } from '@immich/sdk';
|
||||
import { Icon, Modal, ModalBody } from '@immich/ui';
|
||||
import { Icon, Modal, ModalBody, Text } from '@immich/ui';
|
||||
import { mdiFilterOutline, mdiPlayCircleOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
filters: PluginFilterResponseDto[];
|
||||
actions: PluginActionResponseDto[];
|
||||
addedFilters?: PluginFilterResponseDto[];
|
||||
addedActions?: PluginActionResponseDto[];
|
||||
onClose: (result?: { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto }) => void;
|
||||
type?: 'filter' | 'action';
|
||||
}
|
||||
|
||||
let { filters, actions, addedFilters = [], addedActions = [], onClose, type }: Props = $props();
|
||||
|
||||
// Filter out already-added items
|
||||
const availableFilters = $derived(filters.filter((f) => !addedFilters.some((af) => af.id === f.id)));
|
||||
const availableActions = $derived(actions.filter((a) => !addedActions.some((aa) => aa.id === a.id)));
|
||||
let { filters, actions, onClose, type }: Props = $props();
|
||||
|
||||
type StepType = 'filter' | 'action';
|
||||
|
||||
@@ -30,7 +24,7 @@
|
||||
<ModalBody>
|
||||
<div class="space-y-6">
|
||||
<!-- Filters Section -->
|
||||
{#if availableFilters.length > 0 && (!type || type === 'filter')}
|
||||
{#if filters.length > 0 && (!type || type === 'filter')}
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="h-6 w-6 rounded-md bg-warning-100 flex items-center justify-center">
|
||||
<Icon icon={mdiFilterOutline} size="16" class="text-warning" />
|
||||
@@ -38,7 +32,7 @@
|
||||
<h3 class="text-sm font-semibold">Filters</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
{#each availableFilters as filter (filter.id)}
|
||||
{#each filters as filter (filter.id)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => handleSelect('filter', filter)}
|
||||
@@ -56,7 +50,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- Actions Section -->
|
||||
{#if availableActions.length > 0 && (!type || type === 'action')}
|
||||
{#if actions.length > 0 && (!type || type === 'action')}
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="h-6 w-6 rounded-md bg-success-50 flex items-center justify-center">
|
||||
@@ -65,7 +59,7 @@
|
||||
<h3 class="text-sm font-semibold">Actions</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
{#each availableActions as action (action.id)}
|
||||
{#each actions as action (action.id)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => handleSelect('action', action)}
|
||||
@@ -74,7 +68,7 @@
|
||||
<div class="flex-1">
|
||||
<p class="font-medium text-sm">{action.title}</p>
|
||||
{#if action.description}
|
||||
<p class="text-xs text-light-500 mt-1">{action.description}</p>
|
||||
<Text size="small" class="text-light-500 mt-1">{action.description}</Text>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import SearchBar from '$lib/elements/SearchBar.svelte';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button, HStack, LoadingSpinner, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -9,10 +10,11 @@
|
||||
|
||||
interface Props {
|
||||
multiple?: boolean;
|
||||
excludedIds?: string[];
|
||||
onClose: (people?: PersonResponseDto[]) => void;
|
||||
}
|
||||
|
||||
let { multiple = false, onClose }: Props = $props();
|
||||
let { multiple = false, excludedIds = [], onClose }: Props = $props();
|
||||
|
||||
let people: PersonResponseDto[] = $state([]);
|
||||
let loading = $state(true);
|
||||
@@ -20,13 +22,20 @@
|
||||
let selectedPeople: PersonResponseDto[] = $state([]);
|
||||
|
||||
const filteredPeople = $derived(
|
||||
searchName ? people.filter((person) => person.name.toLowerCase().includes(searchName.toLowerCase())) : people,
|
||||
people
|
||||
.filter((person) => !excludedIds.includes(person.id))
|
||||
.filter((person) => !searchName || person.name.toLowerCase().includes(searchName.toLowerCase())),
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
const result = await getAllPeople({ withHidden: false });
|
||||
people = result.people;
|
||||
loading = false;
|
||||
try {
|
||||
loading = true;
|
||||
const result = await getAllPeople({ withHidden: false });
|
||||
people = result.people;
|
||||
loading = false;
|
||||
} catch (error) {
|
||||
handleError(error, $t('get_people_error'));
|
||||
}
|
||||
});
|
||||
|
||||
const togglePerson = (person: PersonResponseDto) => {
|
||||
@@ -86,11 +95,11 @@
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
{#if multiple && selectedPeople.length > 0}
|
||||
{#if multiple}
|
||||
<ModalFooter>
|
||||
<HStack fullWidth gap={4}>
|
||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
||||
<Button shape="round" fullWidth onclick={handleSubmit}>
|
||||
<Button shape="round" fullWidth onclick={handleSubmit} disabled={selectedPeople.length === 0}>
|
||||
{$t('select_count', { values: { count: selectedPeople.length } })}
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
@@ -2,9 +2,11 @@ import {
|
||||
PluginTriggerType,
|
||||
updateWorkflow as updateWorkflowApi,
|
||||
type PluginActionResponseDto,
|
||||
type PluginContext,
|
||||
type PluginContextType,
|
||||
type PluginFilterResponseDto,
|
||||
type PluginTriggerResponseDto,
|
||||
type WorkflowActionItemDto,
|
||||
type WorkflowFilterItemDto,
|
||||
type WorkflowResponseDto,
|
||||
type WorkflowUpdateDto,
|
||||
} from '@immich/sdk';
|
||||
@@ -18,316 +20,280 @@ export interface WorkflowPayload {
|
||||
actions: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
export class WorkflowService {
|
||||
private availableTriggers: PluginTriggerResponseDto[];
|
||||
private availableFilters: PluginFilterResponseDto[];
|
||||
private availableActions: PluginActionResponseDto[];
|
||||
/**
|
||||
* Get filters that support the given context
|
||||
*/
|
||||
export const getFiltersByContext = (
|
||||
availableFilters: PluginFilterResponseDto[],
|
||||
context: PluginContextType,
|
||||
): PluginFilterResponseDto[] => {
|
||||
return availableFilters.filter((filter) => filter.supportedContexts.includes(context));
|
||||
};
|
||||
|
||||
constructor(
|
||||
triggers: PluginTriggerResponseDto[],
|
||||
filters: PluginFilterResponseDto[],
|
||||
actions: PluginActionResponseDto[],
|
||||
) {
|
||||
this.availableTriggers = triggers;
|
||||
this.availableFilters = filters;
|
||||
this.availableActions = actions;
|
||||
/**
|
||||
* Get actions that support the given context
|
||||
*/
|
||||
export const getActionsByContext = (
|
||||
availableActions: PluginActionResponseDto[],
|
||||
context: PluginContextType,
|
||||
): PluginActionResponseDto[] => {
|
||||
return availableActions.filter((action) => action.supportedContexts.includes(context));
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize filter configurations from existing workflow
|
||||
*/
|
||||
export const initializeFilterConfigs = (
|
||||
workflow: WorkflowResponseDto,
|
||||
availableFilters: PluginFilterResponseDto[],
|
||||
): Record<string, unknown> => {
|
||||
const configs: Record<string, unknown> = {};
|
||||
|
||||
if (workflow.filters) {
|
||||
for (const workflowFilter of workflow.filters) {
|
||||
const filterDef = availableFilters.find((f) => f.id === workflowFilter.pluginFilterId);
|
||||
if (filterDef) {
|
||||
configs[filterDef.methodName] = workflowFilter.filterConfig ?? {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filters that support the given context
|
||||
*/
|
||||
getFiltersByContext(context: PluginContext): PluginFilterResponseDto[] {
|
||||
return this.availableFilters.filter((filter) => filter.supportedContexts.includes(context));
|
||||
return configs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize action configurations from existing workflow
|
||||
*/
|
||||
export const initializeActionConfigs = (
|
||||
workflow: WorkflowResponseDto,
|
||||
availableActions: PluginActionResponseDto[],
|
||||
): Record<string, unknown> => {
|
||||
const configs: Record<string, unknown> = {};
|
||||
|
||||
if (workflow.actions) {
|
||||
for (const workflowAction of workflow.actions) {
|
||||
const actionDef = availableActions.find((a) => a.id === workflowAction.pluginActionId);
|
||||
if (actionDef) {
|
||||
configs[actionDef.methodName] = workflowAction.actionConfig ?? {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actions that support the given context
|
||||
*/
|
||||
getActionsByContext(context: PluginContext): PluginActionResponseDto[] {
|
||||
return this.availableActions.filter((action) => action.supportedContexts.includes(context));
|
||||
}
|
||||
return configs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize filter configurations from existing workflow
|
||||
*/
|
||||
initializeFilterConfigs(
|
||||
workflow: WorkflowResponseDto,
|
||||
contextFilters?: PluginFilterResponseDto[],
|
||||
): Record<string, unknown> {
|
||||
const filters = contextFilters ?? this.availableFilters;
|
||||
const configs: Record<string, unknown> = {};
|
||||
/**
|
||||
* Build workflow payload from current state
|
||||
*/
|
||||
export const buildWorkflowPayload = (
|
||||
name: string,
|
||||
description: string,
|
||||
enabled: boolean,
|
||||
triggerType: string,
|
||||
orderedFilters: PluginFilterResponseDto[],
|
||||
orderedActions: PluginActionResponseDto[],
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
): WorkflowPayload => {
|
||||
const filters = orderedFilters.map((filter) => ({
|
||||
[filter.methodName]: filterConfigs[filter.methodName] ?? {},
|
||||
}));
|
||||
|
||||
if (workflow.filters) {
|
||||
for (const workflowFilter of workflow.filters) {
|
||||
const filterDef = filters.find((f) => f.id === workflowFilter.filterId);
|
||||
if (filterDef) {
|
||||
configs[filterDef.methodName] = workflowFilter.filterConfig ?? {};
|
||||
const actions = orderedActions.map((action) => ({
|
||||
[action.methodName]: actionConfigs[action.methodName] ?? {},
|
||||
}));
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
triggerType,
|
||||
filters,
|
||||
actions,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse JSON workflow and update state
|
||||
*/
|
||||
export const parseWorkflowJson = (
|
||||
jsonString: string,
|
||||
availableTriggers: PluginTriggerResponseDto[],
|
||||
availableFilters: PluginFilterResponseDto[],
|
||||
availableActions: PluginActionResponseDto[],
|
||||
): {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
data?: {
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
trigger?: PluginTriggerResponseDto;
|
||||
filters: PluginFilterResponseDto[];
|
||||
actions: PluginActionResponseDto[];
|
||||
filterConfigs: Record<string, unknown>;
|
||||
actionConfigs: Record<string, unknown>;
|
||||
};
|
||||
} => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
// Find trigger
|
||||
const trigger = availableTriggers.find((t) => t.type === parsed.triggerType);
|
||||
|
||||
// Parse filters
|
||||
const filters: PluginFilterResponseDto[] = [];
|
||||
const filterConfigs: Record<string, unknown> = {};
|
||||
if (Array.isArray(parsed.filters)) {
|
||||
for (const filterObj of parsed.filters) {
|
||||
const methodName = Object.keys(filterObj)[0];
|
||||
const filter = availableFilters.find((f) => f.methodName === methodName);
|
||||
if (filter) {
|
||||
filters.push(filter);
|
||||
filterConfigs[methodName] = (filterObj as Record<string, unknown>)[methodName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize action configurations from existing workflow
|
||||
*/
|
||||
initializeActionConfigs(
|
||||
workflow: WorkflowResponseDto,
|
||||
contextActions?: PluginActionResponseDto[],
|
||||
): Record<string, unknown> {
|
||||
const actions = contextActions ?? this.availableActions;
|
||||
const configs: Record<string, unknown> = {};
|
||||
|
||||
if (workflow.actions) {
|
||||
for (const workflowAction of workflow.actions) {
|
||||
const actionDef = actions.find((a) => a.id === workflowAction.actionId);
|
||||
if (actionDef) {
|
||||
configs[actionDef.methodName] = workflowAction.actionConfig ?? {};
|
||||
// Parse actions
|
||||
const actions: PluginActionResponseDto[] = [];
|
||||
const actionConfigs: Record<string, unknown> = {};
|
||||
if (Array.isArray(parsed.actions)) {
|
||||
for (const actionObj of parsed.actions) {
|
||||
const methodName = Object.keys(actionObj)[0];
|
||||
const action = availableActions.find((a) => a.methodName === methodName);
|
||||
if (action) {
|
||||
actions.push(action);
|
||||
actionConfigs[methodName] = (actionObj as Record<string, unknown>)[methodName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize ordered filters from existing workflow
|
||||
*/
|
||||
initializeOrderedFilters(
|
||||
workflow: WorkflowResponseDto,
|
||||
contextFilters?: PluginFilterResponseDto[],
|
||||
): PluginFilterResponseDto[] {
|
||||
if (!workflow.filters) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters = contextFilters ?? this.availableFilters;
|
||||
return workflow.filters
|
||||
.map((wf) => filters.find((f) => f.id === wf.filterId))
|
||||
.filter(Boolean) as PluginFilterResponseDto[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize ordered actions from existing workflow
|
||||
*/
|
||||
initializeOrderedActions(
|
||||
workflow: WorkflowResponseDto,
|
||||
contextActions?: PluginActionResponseDto[],
|
||||
): PluginActionResponseDto[] {
|
||||
if (!workflow.actions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions = contextActions ?? this.availableActions;
|
||||
return workflow.actions
|
||||
.map((wa) => actions.find((a) => a.id === wa.actionId))
|
||||
.filter(Boolean) as PluginActionResponseDto[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build workflow payload from current state
|
||||
*/
|
||||
buildWorkflowPayload(
|
||||
name: string,
|
||||
description: string,
|
||||
enabled: boolean,
|
||||
triggerType: string,
|
||||
orderedFilters: PluginFilterResponseDto[],
|
||||
orderedActions: PluginActionResponseDto[],
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
): WorkflowPayload {
|
||||
const filters = orderedFilters.map((filter) => ({
|
||||
[filter.methodName]: filterConfigs[filter.methodName] ?? {},
|
||||
}));
|
||||
|
||||
const actions = orderedActions.map((action) => ({
|
||||
[action.methodName]: actionConfigs[action.methodName] ?? {},
|
||||
}));
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
triggerType,
|
||||
filters,
|
||||
actions,
|
||||
success: true,
|
||||
data: {
|
||||
name: parsed.name ?? '',
|
||||
description: parsed.description ?? '',
|
||||
enabled: parsed.enabled ?? false,
|
||||
trigger,
|
||||
filters,
|
||||
actions,
|
||||
filterConfigs,
|
||||
actionConfigs,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid JSON',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse JSON workflow and update state
|
||||
*/
|
||||
parseWorkflowJson(jsonString: string): {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
data?: {
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
trigger?: PluginTriggerResponseDto;
|
||||
filters: PluginFilterResponseDto[];
|
||||
actions: PluginActionResponseDto[];
|
||||
filterConfigs: Record<string, unknown>;
|
||||
actionConfigs: Record<string, unknown>;
|
||||
};
|
||||
} {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
// Find trigger
|
||||
const trigger = this.availableTriggers.find((t) => t.triggerType === parsed.triggerType);
|
||||
|
||||
// Parse filters
|
||||
const filters: PluginFilterResponseDto[] = [];
|
||||
const filterConfigs: Record<string, unknown> = {};
|
||||
if (Array.isArray(parsed.filters)) {
|
||||
for (const filterObj of parsed.filters) {
|
||||
const methodName = Object.keys(filterObj)[0];
|
||||
const filter = this.availableFilters.find((f) => f.methodName === methodName);
|
||||
if (filter) {
|
||||
filters.push(filter);
|
||||
filterConfigs[methodName] = (filterObj as Record<string, unknown>)[methodName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse actions
|
||||
const actions: PluginActionResponseDto[] = [];
|
||||
const actionConfigs: Record<string, unknown> = {};
|
||||
if (Array.isArray(parsed.actions)) {
|
||||
for (const actionObj of parsed.actions) {
|
||||
const methodName = Object.keys(actionObj)[0];
|
||||
const action = this.availableActions.find((a) => a.methodName === methodName);
|
||||
if (action) {
|
||||
actions.push(action);
|
||||
actionConfigs[methodName] = (actionObj as Record<string, unknown>)[methodName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
name: parsed.name ?? '',
|
||||
description: parsed.description ?? '',
|
||||
enabled: parsed.enabled ?? false,
|
||||
trigger,
|
||||
filters,
|
||||
actions,
|
||||
filterConfigs,
|
||||
actionConfigs,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid JSON',
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Check if workflow has changes compared to previous version
|
||||
*/
|
||||
export const hasWorkflowChanged = (
|
||||
previousWorkflow: WorkflowResponseDto,
|
||||
enabled: boolean,
|
||||
name: string,
|
||||
description: string,
|
||||
triggerType: string,
|
||||
orderedFilters: PluginFilterResponseDto[],
|
||||
orderedActions: PluginActionResponseDto[],
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
availableFilters: PluginFilterResponseDto[],
|
||||
availableActions: PluginActionResponseDto[],
|
||||
): boolean => {
|
||||
// Check enabled state
|
||||
if (enabled !== previousWorkflow.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workflow has changes compared to previous version
|
||||
*/
|
||||
hasWorkflowChanged(
|
||||
previousWorkflow: WorkflowResponseDto,
|
||||
enabled: boolean,
|
||||
name: string,
|
||||
description: string,
|
||||
triggerType: string,
|
||||
orderedFilters: PluginFilterResponseDto[],
|
||||
orderedActions: PluginActionResponseDto[],
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
): boolean {
|
||||
// Check enabled state
|
||||
if (enabled !== previousWorkflow.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check name or description
|
||||
if (name !== (previousWorkflow.name ?? '') || description !== (previousWorkflow.description ?? '')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check trigger
|
||||
if (triggerType !== previousWorkflow.triggerType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check filters order/items
|
||||
const previousFilterIds = previousWorkflow.filters?.map((f) => f.filterId) ?? [];
|
||||
const currentFilterIds = orderedFilters.map((f) => f.id);
|
||||
if (JSON.stringify(previousFilterIds) !== JSON.stringify(currentFilterIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check actions order/items
|
||||
const previousActionIds = previousWorkflow.actions?.map((a) => a.actionId) ?? [];
|
||||
const currentActionIds = orderedActions.map((a) => a.id);
|
||||
if (JSON.stringify(previousActionIds) !== JSON.stringify(currentActionIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check filter configs
|
||||
const previousFilterConfigs: Record<string, unknown> = {};
|
||||
for (const wf of previousWorkflow.filters ?? []) {
|
||||
const filterDef = this.availableFilters.find((f) => f.id === wf.filterId);
|
||||
if (filterDef) {
|
||||
previousFilterConfigs[filterDef.methodName] = wf.filterConfig ?? {};
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check action configs
|
||||
const previousActionConfigs: Record<string, unknown> = {};
|
||||
for (const wa of previousWorkflow.actions ?? []) {
|
||||
const actionDef = this.availableActions.find((a) => a.id === wa.actionId);
|
||||
if (actionDef) {
|
||||
previousActionConfigs[actionDef.methodName] = wa.actionConfig ?? {};
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Check name or description
|
||||
if (name !== (previousWorkflow.name ?? '') || description !== (previousWorkflow.description ?? '')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateWorkflow(
|
||||
workflowId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
enabled: boolean,
|
||||
triggerType: PluginTriggerType,
|
||||
orderedFilters: PluginFilterResponseDto[],
|
||||
orderedActions: PluginActionResponseDto[],
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
): Promise<WorkflowResponseDto> {
|
||||
const filters = orderedFilters.map((filter) => ({
|
||||
filterId: filter.id,
|
||||
filterConfig: filterConfigs[filter.methodName] ?? {},
|
||||
}));
|
||||
|
||||
const actions = orderedActions.map((action) => ({
|
||||
actionId: action.id,
|
||||
actionConfig: actionConfigs[action.methodName] ?? {},
|
||||
}));
|
||||
|
||||
const updateDto: WorkflowUpdateDto = {
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
filters,
|
||||
actions,
|
||||
triggerType,
|
||||
};
|
||||
|
||||
return updateWorkflowApi({ id: workflowId, workflowUpdateDto: updateDto });
|
||||
// Check trigger
|
||||
if (triggerType !== previousWorkflow.triggerType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check filters order/items
|
||||
const previousFilterIds = previousWorkflow.filters?.map((f) => f.pluginFilterId) ?? [];
|
||||
const currentFilterIds = orderedFilters.map((f) => f.id);
|
||||
if (JSON.stringify(previousFilterIds) !== JSON.stringify(currentFilterIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check actions order/items
|
||||
const previousActionIds = previousWorkflow.actions?.map((a) => a.pluginActionId) ?? [];
|
||||
const currentActionIds = orderedActions.map((a) => a.id);
|
||||
if (JSON.stringify(previousActionIds) !== JSON.stringify(currentActionIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check filter configs
|
||||
const previousFilterConfigs: Record<string, unknown> = {};
|
||||
for (const wf of previousWorkflow.filters ?? []) {
|
||||
const filterDef = availableFilters.find((f) => f.id === wf.pluginFilterId);
|
||||
if (filterDef) {
|
||||
previousFilterConfigs[filterDef.methodName] = wf.filterConfig ?? {};
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check action configs
|
||||
const previousActionConfigs: Record<string, unknown> = {};
|
||||
for (const wa of previousWorkflow.actions ?? []) {
|
||||
const actionDef = availableActions.find((a) => a.id === wa.pluginActionId);
|
||||
if (actionDef) {
|
||||
previousActionConfigs[actionDef.methodName] = wa.actionConfig ?? {};
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a workflow via API
|
||||
*/
|
||||
export const handleUpdateWorkflow = async (
|
||||
workflowId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
enabled: boolean,
|
||||
triggerType: PluginTriggerType,
|
||||
orderedFilters: PluginFilterResponseDto[],
|
||||
orderedActions: PluginActionResponseDto[],
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
): Promise<WorkflowResponseDto> => {
|
||||
const filters = orderedFilters.map((filter) => ({
|
||||
pluginFilterId: filter.id,
|
||||
filterConfig: filterConfigs[filter.methodName] ?? {},
|
||||
})) as WorkflowFilterItemDto[];
|
||||
|
||||
const actions = orderedActions.map((action) => ({
|
||||
pluginActionId: action.id,
|
||||
actionConfig: actionConfigs[action.methodName] ?? {},
|
||||
})) as WorkflowActionItemDto[];
|
||||
|
||||
const updateDto: WorkflowUpdateDto = {
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
filters,
|
||||
actions,
|
||||
triggerType,
|
||||
};
|
||||
|
||||
return updateWorkflowApi({ id: workflowId, workflowUpdateDto: updateDto });
|
||||
};
|
||||
|
||||
@@ -76,15 +76,15 @@
|
||||
enabled: workflow.enabled,
|
||||
triggerType: workflow.triggerType,
|
||||
filters: orderedFilters.map((filter) => {
|
||||
const meta = pluginFilterLookup.get(filter.filterId);
|
||||
const key = meta?.methodName ?? filter.filterId;
|
||||
const meta = pluginFilterLookup.get(filter.pluginFilterId);
|
||||
const key = meta?.methodName ?? filter.pluginFilterId;
|
||||
return {
|
||||
[key]: filter.filterConfig ?? {},
|
||||
};
|
||||
}),
|
||||
actions: orderedActions.map((action) => {
|
||||
const meta = pluginActionLookup.get(action.actionId);
|
||||
const key = meta?.methodName ?? action.actionId;
|
||||
const meta = pluginActionLookup.get(action.pluginActionId);
|
||||
const key = meta?.methodName ?? action.pluginActionId;
|
||||
return {
|
||||
[key]: action.actionConfig ?? {},
|
||||
};
|
||||
@@ -123,7 +123,7 @@
|
||||
};
|
||||
|
||||
const handleEditWorkflow = async (workflow: WorkflowResponseDto) => {
|
||||
await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`);
|
||||
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
|
||||
};
|
||||
|
||||
const handleCreateWorkflow = async () => {
|
||||
@@ -137,7 +137,7 @@
|
||||
},
|
||||
});
|
||||
|
||||
await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`);
|
||||
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
|
||||
};
|
||||
|
||||
const getFilterLabel = (filterId: string) => {
|
||||
@@ -289,7 +289,7 @@
|
||||
</span>
|
||||
{:else}
|
||||
{#each workflow.filters as workflowFilter (workflowFilter.id)}
|
||||
{@render chipItem(getFilterLabel(workflowFilter.filterId))}
|
||||
{@render chipItem(getFilterLabel(workflowFilter.pluginFilterId))}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -309,7 +309,7 @@
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each workflow.actions as workflowAction (workflowAction.id)}
|
||||
{@render chipItem(getActionLabel(workflowAction.actionId))}
|
||||
{@render chipItem(getActionLabel(workflowAction.pluginActionId))}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getPlugins, getWorkflows } from '@immich/sdk';
|
||||
import type { PageLoad } from '../$types';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url);
|
||||
|
||||
@@ -11,7 +11,17 @@
|
||||
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 {
|
||||
buildWorkflowPayload,
|
||||
getActionsByContext,
|
||||
getFiltersByContext,
|
||||
handleUpdateWorkflow,
|
||||
hasWorkflowChanged,
|
||||
initializeActionConfigs,
|
||||
initializeFilterConfigs,
|
||||
parseWorkflowJson,
|
||||
type WorkflowPayload,
|
||||
} from '$lib/services/workflow.service';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import type { PluginActionResponseDto, PluginFilterResponseDto, PluginTriggerResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
@@ -57,7 +67,6 @@
|
||||
const triggers = data.triggers;
|
||||
const filters = data.plugins.flatMap((plugin) => plugin.filters);
|
||||
const actions = data.plugins.flatMap((plugin) => plugin.actions);
|
||||
const workflowService = new WorkflowService(triggers, filters, actions);
|
||||
|
||||
let previousWorkflow = data.workflow;
|
||||
let editWorkflow = $state(data.workflow);
|
||||
@@ -67,26 +76,28 @@
|
||||
let name: string = $derived(editWorkflow.name ?? '');
|
||||
let description: string = $derived(editWorkflow.description ?? '');
|
||||
|
||||
let selectedTrigger = $state(triggers.find((t) => t.triggerType === editWorkflow.triggerType) ?? triggers[0]);
|
||||
let selectedTrigger = $state(triggers.find((t) => t.type === editWorkflow.triggerType) ?? triggers[0]);
|
||||
|
||||
let triggerType = $derived(selectedTrigger.triggerType);
|
||||
let triggerType = $derived(selectedTrigger.type);
|
||||
|
||||
let supportFilters = $derived(workflowService.getFiltersByContext(selectedTrigger.context));
|
||||
let supportActions = $derived(workflowService.getActionsByContext(selectedTrigger.context));
|
||||
let supportFilters = $derived(getFiltersByContext(filters, selectedTrigger.contextType));
|
||||
let supportActions = $derived(getActionsByContext(actions, selectedTrigger.contextType));
|
||||
|
||||
let orderedFilters: PluginFilterResponseDto[] = $derived(
|
||||
workflowService.initializeOrderedFilters(editWorkflow, supportFilters),
|
||||
let selectedFilters: PluginFilterResponseDto[] = $derived(
|
||||
(editWorkflow.filters ?? []).flatMap((workflowFilter) =>
|
||||
supportFilters.filter((supportedFilter) => supportedFilter.id === workflowFilter.pluginFilterId),
|
||||
),
|
||||
);
|
||||
let orderedActions: PluginActionResponseDto[] = $derived(
|
||||
workflowService.initializeOrderedActions(editWorkflow, supportActions),
|
||||
);
|
||||
let filterConfigs: Record<string, unknown> = $derived(
|
||||
workflowService.initializeFilterConfigs(editWorkflow, supportFilters),
|
||||
);
|
||||
let actionConfigs: Record<string, unknown> = $derived(
|
||||
workflowService.initializeActionConfigs(editWorkflow, supportActions),
|
||||
|
||||
let selectedActions: PluginActionResponseDto[] = $derived(
|
||||
(editWorkflow.actions ?? []).flatMap((workflowAction) =>
|
||||
supportActions.filter((supportedAction) => supportedAction.id === workflowAction.pluginActionId),
|
||||
),
|
||||
);
|
||||
|
||||
let filterConfigs: Record<string, unknown> = $derived(initializeFilterConfigs(editWorkflow, supportFilters));
|
||||
let actionConfigs: Record<string, unknown> = $derived(initializeActionConfigs(editWorkflow, supportActions));
|
||||
|
||||
$effect(() => {
|
||||
editWorkflow.triggerType = triggerType;
|
||||
});
|
||||
@@ -94,10 +105,10 @@
|
||||
// Clear filters and actions when trigger changes (context changes)
|
||||
let previousContext = $state<string | undefined>(undefined);
|
||||
$effect(() => {
|
||||
const currentContext = selectedTrigger.context;
|
||||
const currentContext = selectedTrigger.contextType;
|
||||
if (previousContext !== undefined && previousContext !== currentContext) {
|
||||
orderedFilters = [];
|
||||
orderedActions = [];
|
||||
selectedFilters = [];
|
||||
selectedActions = [];
|
||||
filterConfigs = {};
|
||||
actionConfigs = {};
|
||||
}
|
||||
@@ -106,14 +117,14 @@
|
||||
|
||||
const updateWorkflow = async () => {
|
||||
try {
|
||||
const updated = await workflowService.updateWorkflow(
|
||||
const updated = await handleUpdateWorkflow(
|
||||
editWorkflow.id,
|
||||
name,
|
||||
description,
|
||||
editWorkflow.enabled,
|
||||
triggerType,
|
||||
orderedFilters,
|
||||
orderedActions,
|
||||
selectedFilters,
|
||||
selectedActions,
|
||||
filterConfigs,
|
||||
actionConfigs,
|
||||
);
|
||||
@@ -131,13 +142,13 @@
|
||||
};
|
||||
|
||||
const jsonContent = $derived(
|
||||
workflowService.buildWorkflowPayload(
|
||||
buildWorkflowPayload(
|
||||
name,
|
||||
description,
|
||||
editWorkflow.enabled,
|
||||
triggerType,
|
||||
orderedFilters,
|
||||
orderedActions,
|
||||
selectedFilters,
|
||||
selectedActions,
|
||||
filterConfigs,
|
||||
actionConfigs,
|
||||
),
|
||||
@@ -153,7 +164,7 @@
|
||||
});
|
||||
|
||||
const syncFromJson = () => {
|
||||
const result = workflowService.parseWorkflowJson(JSON.stringify(jsonEditorContent));
|
||||
const result = parseWorkflowJson(JSON.stringify(jsonEditorContent), triggers, filters, actions);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
@@ -168,24 +179,26 @@
|
||||
selectedTrigger = result.data.trigger;
|
||||
}
|
||||
|
||||
orderedFilters = result.data.filters;
|
||||
orderedActions = result.data.actions;
|
||||
selectedFilters = result.data.filters;
|
||||
selectedActions = result.data.actions;
|
||||
filterConfigs = result.data.filterConfigs;
|
||||
actionConfigs = result.data.actionConfigs;
|
||||
}
|
||||
};
|
||||
|
||||
let hasChanges: boolean = $derived(
|
||||
workflowService.hasWorkflowChanged(
|
||||
hasWorkflowChanged(
|
||||
previousWorkflow,
|
||||
editWorkflow.enabled,
|
||||
name,
|
||||
description,
|
||||
triggerType,
|
||||
orderedFilters,
|
||||
orderedActions,
|
||||
selectedFilters,
|
||||
selectedActions,
|
||||
filterConfigs,
|
||||
actionConfigs,
|
||||
filters,
|
||||
actions,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -211,10 +224,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilters = [...orderedFilters];
|
||||
const newFilters = [...selectedFilters];
|
||||
const [draggedItem] = newFilters.splice(draggedFilterIndex, 1);
|
||||
newFilters.splice(index, 0, draggedItem);
|
||||
orderedFilters = newFilters;
|
||||
selectedFilters = newFilters;
|
||||
};
|
||||
|
||||
const handleFilterDragEnd = () => {
|
||||
@@ -238,10 +251,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const newActions = [...orderedActions];
|
||||
const newActions = [...selectedActions];
|
||||
const [draggedItem] = newActions.splice(draggedActionIndex, 1);
|
||||
newActions.splice(index, 0, draggedItem);
|
||||
orderedActions = newActions;
|
||||
selectedActions = newActions;
|
||||
};
|
||||
|
||||
const handleActionDragEnd = () => {
|
||||
@@ -253,26 +266,24 @@
|
||||
const result = (await modalManager.show(AddWorkflowStepModal, {
|
||||
filters: supportFilters,
|
||||
actions: supportActions,
|
||||
addedFilters: orderedFilters,
|
||||
addedActions: orderedActions,
|
||||
type,
|
||||
})) as { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto } | undefined;
|
||||
|
||||
if (result) {
|
||||
if (result.type === 'filter') {
|
||||
orderedFilters = [...orderedFilters, result.item as PluginFilterResponseDto];
|
||||
selectedFilters = [...selectedFilters, result.item as PluginFilterResponseDto];
|
||||
} else if (result.type === 'action') {
|
||||
orderedActions = [...orderedActions, result.item as PluginActionResponseDto];
|
||||
selectedActions = [...selectedActions, result.item as PluginActionResponseDto];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFilter = (index: number) => {
|
||||
orderedFilters = orderedFilters.filter((_, i) => i !== index);
|
||||
selectedFilters = selectedFilters.filter((_, i) => i !== index);
|
||||
};
|
||||
|
||||
const handleRemoveAction = (index: number) => {
|
||||
orderedActions = orderedActions.filter((_, i) => i !== index);
|
||||
selectedActions = selectedActions.filter((_, i) => i !== index);
|
||||
};
|
||||
|
||||
const handleTriggerChange = async (newTrigger: PluginTriggerResponseDto) => {
|
||||
@@ -340,8 +351,6 @@
|
||||
</svelte:head>
|
||||
|
||||
<main class="pt-24 immich-scrollbar">
|
||||
<WorkflowSummarySidebar trigger={selectedTrigger} filters={orderedFilters} actions={orderedActions} />
|
||||
|
||||
<Container size="medium" class="p-4" center>
|
||||
{#if viewMode === 'json'}
|
||||
<WorkflowJsonEditor
|
||||
@@ -392,7 +401,7 @@
|
||||
{#each triggers as trigger (trigger.name)}
|
||||
<WorkflowTriggerCard
|
||||
{trigger}
|
||||
selected={selectedTrigger.triggerType === trigger.triggerType}
|
||||
selected={selectedTrigger.type === trigger.type}
|
||||
onclick={() => handleTriggerChange(trigger)}
|
||||
/>
|
||||
{/each}
|
||||
@@ -414,10 +423,10 @@
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
{#if orderedFilters.length === 0}
|
||||
{#if selectedFilters.length === 0}
|
||||
{@render emptyCreateButton($t('add_filter'), $t('add_filter_description'), () => handleAddStep('filter'))}
|
||||
{:else}
|
||||
{#each orderedFilters as filter, index (filter.id)}
|
||||
{#each selectedFilters as filter, index (index)}
|
||||
{#if index > 0}
|
||||
{@render stepSeparator()}
|
||||
{/if}
|
||||
@@ -483,10 +492,10 @@
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
{#if orderedActions.length === 0}
|
||||
{#if selectedActions.length === 0}
|
||||
{@render emptyCreateButton($t('add_action'), $t('add_action_description'), () => handleAddStep('action'))}
|
||||
{:else}
|
||||
{#each orderedActions as action, index (action.id)}
|
||||
{#each selectedActions as action, index (index)}
|
||||
{#if index > 0}
|
||||
{@render stepSeparator()}
|
||||
{/if}
|
||||
@@ -539,6 +548,8 @@
|
||||
</VStack>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<WorkflowSummarySidebar trigger={selectedTrigger} filters={selectedFilters} actions={selectedActions} />
|
||||
</main>
|
||||
|
||||
<ControlAppBar onClose={() => goto(AppRoute.WORKFLOWS)} backIcon={mdiArrowLeft} tailwindClasses="fixed! top-0! w-full">
|
||||
@@ -1,6 +1,6 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getPlugins, getTriggers, getWorkflow } from '@immich/sdk';
|
||||
import { getPlugins, getPluginTriggers, getWorkflow } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url, params }) => {
|
||||
@@ -8,7 +8,7 @@ export const load = (async ({ url, params }) => {
|
||||
const [plugins, workflow, triggers] = await Promise.all([
|
||||
getPlugins(),
|
||||
getWorkflow({ id: params.workflowId }),
|
||||
getTriggers(),
|
||||
getPluginTriggers(),
|
||||
]);
|
||||
const $t = await getFormatter();
|
||||
|
||||
Reference in New Issue
Block a user