mirror of
https://github.com/immich-app/immich.git
synced 2025-12-05 20:40:29 -08:00
Compare commits
3 Commits
77578947f8
...
7ed646178e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed646178e | ||
|
|
3d771127d2 | ||
|
|
5156438336 |
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { formatLabel, getComponentFromSchema } from '$lib/utils/workflow';
|
||||
import { Field, Input, MultiSelect, Select, Switch, Text, type SelectItem } from '@immich/ui';
|
||||
import { getComponentDefaultValue, getComponentFromSchema } from '$lib/utils/workflow';
|
||||
import { Field, Input, MultiSelect, Select, Switch, Text } from '@immich/ui';
|
||||
import WorkflowPickerField from './WorkflowPickerField.svelte';
|
||||
|
||||
type Props = {
|
||||
@@ -25,60 +25,37 @@
|
||||
config = configKey ? { ...config, [configKey]: { ...actualConfig, ...updates } } : { ...config, ...updates };
|
||||
};
|
||||
|
||||
let selectValue = $state<SelectItem>();
|
||||
let switchValue = $state<boolean>(false);
|
||||
let multiSelectValue = $state<SelectItem[]>([]);
|
||||
// Derive which keys need initialization (missing from actualConfig)
|
||||
const uninitializedKeys = $derived.by(() => {
|
||||
if (!components) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(components)
|
||||
.filter(([key]) => actualConfig[key] === undefined)
|
||||
.map(([key, component]) => ({ key, component, defaultValue: getComponentDefaultValue(component) }));
|
||||
});
|
||||
|
||||
// Derive the batch updates needed
|
||||
const pendingUpdates = $derived.by(() => {
|
||||
const updates: Record<string, unknown> = {};
|
||||
for (const { key, defaultValue } of uninitializedKeys) {
|
||||
updates[key] = defaultValue;
|
||||
}
|
||||
return updates;
|
||||
});
|
||||
|
||||
// Initialize config namespace if needed
|
||||
$effect(() => {
|
||||
// Initialize config for actions/filters with empty schemas
|
||||
if (configKey && !config[configKey]) {
|
||||
config = { ...config, [configKey]: {} };
|
||||
}
|
||||
});
|
||||
|
||||
if (components) {
|
||||
const updates: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, component] of Object.entries(components)) {
|
||||
// Only initialize if the key doesn't exist in config yet
|
||||
if (actualConfig[key] === undefined) {
|
||||
// Use default value if available, otherwise use appropriate empty value based on type
|
||||
const hasDefault = component.defaultValue !== undefined;
|
||||
|
||||
if (hasDefault) {
|
||||
updates[key] = component.defaultValue;
|
||||
} else {
|
||||
// Initialize with appropriate empty value based on component type
|
||||
if (
|
||||
component.type === 'multiselect' ||
|
||||
(component.type === 'text' && component.subType === 'people-picker')
|
||||
) {
|
||||
updates[key] = [];
|
||||
} else if (component.type === 'switch') {
|
||||
updates[key] = false;
|
||||
} else {
|
||||
updates[key] = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI state for components with default values
|
||||
if (hasDefault) {
|
||||
if (component.type === 'select') {
|
||||
selectValue = {
|
||||
label: formatLabel(String(component.defaultValue)),
|
||||
value: String(component.defaultValue),
|
||||
};
|
||||
}
|
||||
|
||||
if (component.type === 'switch') {
|
||||
switchValue = Boolean(component.defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
updateConfigBatch(updates);
|
||||
}
|
||||
// Apply pending config updates
|
||||
$effect(() => {
|
||||
if (Object.keys(pendingUpdates).length > 0) {
|
||||
updateConfigBatch(pendingUpdates);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -104,6 +81,8 @@
|
||||
{@const options = component.options?.map((opt) => {
|
||||
return { label: opt.label, value: String(opt.value) };
|
||||
}) || [{ label: 'N/A', value: '' }]}
|
||||
{@const currentValue = actualConfig[key]}
|
||||
{@const selectedItem = options.find((opt) => opt.value === String(currentValue)) ?? options[0]}
|
||||
|
||||
<Field
|
||||
{label}
|
||||
@@ -111,7 +90,7 @@
|
||||
description={component.description}
|
||||
requiredIndicator={component.required}
|
||||
>
|
||||
<Select data={options} onChange={(opt) => updateConfig(key, opt.value)} bind:value={selectValue} />
|
||||
<Select data={options} onChange={(opt) => updateConfig(key, opt.value)} value={selectedItem} />
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
@@ -128,6 +107,8 @@
|
||||
{@const options = component.options?.map((opt) => {
|
||||
return { label: opt.label, value: String(opt.value) };
|
||||
}) || [{ label: 'N/A', value: '' }]}
|
||||
{@const currentValues = (actualConfig[key] as string[]) ?? []}
|
||||
{@const selectedItems = options.filter((opt) => currentValues.includes(opt.value))}
|
||||
|
||||
<Field
|
||||
{label}
|
||||
@@ -142,20 +123,21 @@
|
||||
key,
|
||||
opt.map((o) => o.value),
|
||||
)}
|
||||
bind:values={multiSelectValue}
|
||||
values={selectedItems}
|
||||
/>
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
<!-- Switch component -->
|
||||
{:else if component.type === 'switch'}
|
||||
{@const checked = Boolean(actualConfig[key])}
|
||||
<Field
|
||||
{label}
|
||||
description={component.description}
|
||||
requiredIndicator={component.required}
|
||||
required={component.required}
|
||||
>
|
||||
<Switch bind:checked={switchValue} onCheckedChange={(check) => updateConfig(key, check)} />
|
||||
<Switch {checked} onCheckedChange={(check) => updateConfig(key, check)} />
|
||||
</Field>
|
||||
|
||||
<!-- Text input -->
|
||||
|
||||
@@ -57,42 +57,71 @@ export const getActionsByContext = (
|
||||
return availableActions.filter((action) => action.supportedContexts.includes(context));
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize filter configurations from existing workflow
|
||||
*/
|
||||
export const initializeFilterConfigs = (
|
||||
workflow: WorkflowResponseDto,
|
||||
availableFilters: PluginFilterResponseDto[],
|
||||
export const remapConfigsOnReorder = (
|
||||
configs: Record<string, unknown>,
|
||||
prefix: 'filter' | 'action',
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
totalCount: number,
|
||||
): Record<string, unknown> => {
|
||||
const configs: Record<string, unknown> = {};
|
||||
const newConfigs: 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 ?? {};
|
||||
}
|
||||
}
|
||||
// Create an array of configs in order
|
||||
const configArray: unknown[] = [];
|
||||
for (let i = 0; i < totalCount; i++) {
|
||||
configArray.push(configs[`${prefix}_${i}`] ?? {});
|
||||
}
|
||||
|
||||
return configs;
|
||||
// Move the item from fromIndex to toIndex
|
||||
const [movedItem] = configArray.splice(fromIndex, 1);
|
||||
configArray.splice(toIndex, 0, movedItem);
|
||||
|
||||
// Rebuild the configs object with new indices
|
||||
for (let i = 0; i < configArray.length; i++) {
|
||||
newConfigs[`${prefix}_${i}`] = configArray[i];
|
||||
}
|
||||
|
||||
return newConfigs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize action configurations from existing workflow
|
||||
* Remap configs when an item is removed
|
||||
* Shifts all configs after the removed index down by one
|
||||
*/
|
||||
export const initializeActionConfigs = (
|
||||
export const remapConfigsOnRemove = (
|
||||
configs: Record<string, unknown>,
|
||||
prefix: 'filter' | 'action',
|
||||
removedIndex: number,
|
||||
totalCount: number,
|
||||
): Record<string, unknown> => {
|
||||
const newConfigs: Record<string, unknown> = {};
|
||||
|
||||
let newIndex = 0;
|
||||
for (let i = 0; i < totalCount; i++) {
|
||||
if (i !== removedIndex) {
|
||||
newConfigs[`${prefix}_${newIndex}`] = configs[`${prefix}_${i}`] ?? {};
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return newConfigs;
|
||||
};
|
||||
|
||||
export const initializeConfigs = (
|
||||
type: 'action' | 'filter',
|
||||
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 ?? {};
|
||||
}
|
||||
if (workflow.filters && type == 'filter') {
|
||||
for (const [index, workflowFilter] of workflow.filters.entries()) {
|
||||
configs[`filter_${index}`] = workflowFilter.filterConfig ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
if (workflow.actions && type == 'action') {
|
||||
for (const [index, workflowAction] of workflow.actions.entries()) {
|
||||
configs[`action_${index}`] = workflowAction.actionConfig ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +130,7 @@ export const initializeActionConfigs = (
|
||||
|
||||
/**
|
||||
* Build workflow payload from current state
|
||||
* Uses index-based keys to support multiple filters/actions of the same type
|
||||
*/
|
||||
export const buildWorkflowPayload = (
|
||||
name: string,
|
||||
@@ -112,12 +142,12 @@ export const buildWorkflowPayload = (
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
): WorkflowPayload => {
|
||||
const filters = orderedFilters.map((filter) => ({
|
||||
[filter.methodName]: filterConfigs[filter.methodName] ?? {},
|
||||
const filters = orderedFilters.map((filter, index) => ({
|
||||
[filter.methodName]: filterConfigs[`filter_${index}`] ?? {},
|
||||
}));
|
||||
|
||||
const actions = orderedActions.map((action) => ({
|
||||
[action.methodName]: actionConfigs[action.methodName] ?? {},
|
||||
const actions = orderedActions.map((action, index) => ({
|
||||
[action.methodName]: actionConfigs[`action_${index}`] ?? {},
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -130,9 +160,6 @@ export const buildWorkflowPayload = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse JSON workflow and update state
|
||||
*/
|
||||
export const parseWorkflowJson = (
|
||||
jsonString: string,
|
||||
availableTriggers: PluginTriggerResponseDto[],
|
||||
@@ -155,33 +182,30 @@ export const parseWorkflowJson = (
|
||||
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) {
|
||||
for (const [index, filterObj] of parsed.filters.entries()) {
|
||||
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];
|
||||
filterConfigs[`filter_${index}`] = (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) {
|
||||
for (const [index, actionObj] of parsed.actions.entries()) {
|
||||
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];
|
||||
actionConfigs[`action_${index}`] = (actionObj as Record<string, unknown>)[methodName];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,9 +231,6 @@ export const parseWorkflowJson = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if workflow has changes compared to previous version
|
||||
*/
|
||||
export const hasWorkflowChanged = (
|
||||
previousWorkflow: WorkflowResponseDto,
|
||||
enabled: boolean,
|
||||
@@ -220,57 +241,42 @@ export const hasWorkflowChanged = (
|
||||
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 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.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 ?? {};
|
||||
}
|
||||
for (const [index, wf] of (previousWorkflow.filters ?? []).entries()) {
|
||||
previousFilterConfigs[`filter_${index}`] = 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 ?? {};
|
||||
}
|
||||
for (const [index, wa] of (previousWorkflow.actions ?? []).entries()) {
|
||||
previousActionConfigs[`action_${index}`] = wa.actionConfig ?? {};
|
||||
}
|
||||
if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) {
|
||||
return true;
|
||||
@@ -279,9 +285,6 @@ export const hasWorkflowChanged = (
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a workflow via API
|
||||
*/
|
||||
export const handleUpdateWorkflow = async (
|
||||
workflowId: string,
|
||||
name: string,
|
||||
@@ -293,14 +296,14 @@ export const handleUpdateWorkflow = async (
|
||||
filterConfigs: Record<string, unknown>,
|
||||
actionConfigs: Record<string, unknown>,
|
||||
): Promise<WorkflowResponseDto> => {
|
||||
const filters = orderedFilters.map((filter) => ({
|
||||
const filters = orderedFilters.map((filter, index) => ({
|
||||
pluginFilterId: filter.id,
|
||||
filterConfig: filterConfigs[filter.methodName] ?? {},
|
||||
filterConfig: filterConfigs[`filter_${index}`] ?? {},
|
||||
})) as WorkflowFilterItemDto[];
|
||||
|
||||
const actions = orderedActions.map((action) => ({
|
||||
const actions = orderedActions.map((action, index) => ({
|
||||
pluginActionId: action.id,
|
||||
actionConfig: actionConfigs[action.methodName] ?? {},
|
||||
actionConfig: actionConfigs[`action_${index}`] ?? {},
|
||||
})) as WorkflowActionItemDto[];
|
||||
|
||||
const updateDto: WorkflowUpdateDto = {
|
||||
|
||||
@@ -28,6 +28,22 @@ interface JSONSchema {
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
export const getComponentDefaultValue = (component: ComponentConfig): unknown => {
|
||||
if (component.defaultValue !== undefined) {
|
||||
return component.defaultValue;
|
||||
}
|
||||
|
||||
if (component.type === 'multiselect' || (component.type === 'text' && component.subType === 'people-picker')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (component.type === 'switch') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getComponentFromSchema = (schema: object | null): Record<string, ComponentConfig> | null => {
|
||||
if (!schema || !isJSONSchema(schema) || !schema.properties) {
|
||||
return null;
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
getFiltersByContext,
|
||||
handleUpdateWorkflow,
|
||||
hasWorkflowChanged,
|
||||
initializeActionConfigs,
|
||||
initializeFilterConfigs,
|
||||
initializeConfigs,
|
||||
parseWorkflowJson,
|
||||
remapConfigsOnRemove,
|
||||
remapConfigsOnReorder,
|
||||
type WorkflowPayload,
|
||||
} from '$lib/services/workflow.service';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -93,8 +94,8 @@
|
||||
),
|
||||
);
|
||||
|
||||
let filterConfigs: Record<string, unknown> = $derived(initializeFilterConfigs(editWorkflow, supportFilters));
|
||||
let actionConfigs: Record<string, unknown> = $derived(initializeActionConfigs(editWorkflow, supportActions));
|
||||
let filterConfigs: Record<string, unknown> = $derived(initializeConfigs('filter', editWorkflow));
|
||||
let actionConfigs: Record<string, unknown> = $derived(initializeConfigs('action', editWorkflow));
|
||||
|
||||
$effect(() => {
|
||||
editWorkflow.triggerType = triggerType;
|
||||
@@ -127,7 +128,6 @@
|
||||
actionConfigs,
|
||||
);
|
||||
|
||||
// Update the previous workflow state to the new values
|
||||
previousWorkflow = updated;
|
||||
editWorkflow = updated;
|
||||
|
||||
@@ -195,12 +195,9 @@
|
||||
selectedActions,
|
||||
filterConfigs,
|
||||
actionConfigs,
|
||||
filters,
|
||||
actions,
|
||||
),
|
||||
);
|
||||
|
||||
// Drag and drop handlers
|
||||
let draggedFilterIndex: number | null = $state(null);
|
||||
let draggedActionIndex: number | null = $state(null);
|
||||
let dragOverFilterIndex: number | null = $state(null);
|
||||
@@ -222,6 +219,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Remap configs to follow the new order
|
||||
filterConfigs = remapConfigsOnReorder(filterConfigs, 'filter', draggedFilterIndex, index, selectedFilters.length);
|
||||
|
||||
const newFilters = [...selectedFilters];
|
||||
const [draggedItem] = newFilters.splice(draggedFilterIndex, 1);
|
||||
newFilters.splice(index, 0, draggedItem);
|
||||
@@ -249,6 +249,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
actionConfigs = remapConfigsOnReorder(actionConfigs, 'action', draggedActionIndex, index, selectedActions.length);
|
||||
|
||||
const newActions = [...selectedActions];
|
||||
const [draggedItem] = newActions.splice(draggedActionIndex, 1);
|
||||
newActions.splice(index, 0, draggedItem);
|
||||
@@ -260,12 +262,12 @@
|
||||
dragOverActionIndex = null;
|
||||
};
|
||||
|
||||
const handleAddStep = async (type?: 'action' | 'filter') => {
|
||||
const result = (await modalManager.show(AddWorkflowStepModal, {
|
||||
const handleAddStep = async (type: 'action' | 'filter') => {
|
||||
const result = await modalManager.show(AddWorkflowStepModal, {
|
||||
filters: supportFilters,
|
||||
actions: supportActions,
|
||||
type,
|
||||
})) as { type: 'filter' | 'action'; item: PluginFilterResponseDto | PluginActionResponseDto } | undefined;
|
||||
});
|
||||
|
||||
if (result) {
|
||||
if (result.type === 'filter') {
|
||||
@@ -277,10 +279,12 @@
|
||||
};
|
||||
|
||||
const handleRemoveFilter = (index: number) => {
|
||||
filterConfigs = remapConfigsOnRemove(filterConfigs, 'filter', index, selectedFilters.length);
|
||||
selectedFilters = selectedFilters.filter((_, i) => i !== index);
|
||||
};
|
||||
|
||||
const handleRemoveAction = (index: number) => {
|
||||
actionConfigs = remapConfigsOnRemove(actionConfigs, 'action', index, selectedActions.length);
|
||||
selectedActions = selectedActions.filter((_, i) => i !== index);
|
||||
};
|
||||
|
||||
@@ -473,7 +477,7 @@
|
||||
<SchemaFormFields
|
||||
schema={filter.schema}
|
||||
bind:config={filterConfigs}
|
||||
configKey={filter.methodName}
|
||||
configKey={`filter_${index}`}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -542,7 +546,7 @@
|
||||
<SchemaFormFields
|
||||
schema={action.schema}
|
||||
bind:config={actionConfigs}
|
||||
configKey={action.methodName}
|
||||
configKey={`action_${index}`}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
|
||||
Reference in New Issue
Block a user