Skip to content

Commit 3d77112

Browse files
committed
fix: new schemaformfield has value of the same type
1 parent 5156438 commit 3d77112

File tree

4 files changed

+127
-100
lines changed

4 files changed

+127
-100
lines changed

web/src/lib/components/workflows/SchemaFormFields.svelte

Lines changed: 11 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
2-
import { formatLabel, getComponentFromSchema, type ComponentConfig } from '$lib/utils/workflow';
3-
import { Field, Input, MultiSelect, Select, Switch, Text, type SelectItem } from '@immich/ui';
2+
import { getComponentDefaultValue, getComponentFromSchema } from '$lib/utils/workflow';
3+
import { Field, Input, MultiSelect, Select, Switch, Text } from '@immich/ui';
44
import WorkflowPickerField from './WorkflowPickerField.svelte';
55
66
type Props = {
@@ -25,24 +25,6 @@
2525
config = configKey ? { ...config, [configKey]: { ...actualConfig, ...updates } } : { ...config, ...updates };
2626
};
2727
28-
// Helper to determine default value for a component based on its type
29-
const getDefaultValue = (component: ComponentConfig): unknown => {
30-
if (component.defaultValue !== undefined) {
31-
return component.defaultValue;
32-
}
33-
34-
// Initialize with appropriate empty value based on component type
35-
if (component.type === 'multiselect' || (component.type === 'text' && component.subType === 'people-picker')) {
36-
return [];
37-
}
38-
39-
if (component.type === 'switch') {
40-
return false;
41-
}
42-
43-
return '';
44-
};
45-
4628
// Derive which keys need initialization (missing from actualConfig)
4729
const uninitializedKeys = $derived.by(() => {
4830
if (!components) {
@@ -51,7 +33,7 @@
5133
5234
return Object.entries(components)
5335
.filter(([key]) => actualConfig[key] === undefined)
54-
.map(([key, component]) => ({ key, component, defaultValue: getDefaultValue(component) }));
36+
.map(([key, component]) => ({ key, component, defaultValue: getComponentDefaultValue(component) }));
5537
});
5638
5739
// Derive the batch updates needed
@@ -63,10 +45,6 @@
6345
return updates;
6446
});
6547
66-
let selectValue = $state<SelectItem>();
67-
let switchValue = $state<boolean>(false);
68-
let multiSelectValue = $state<SelectItem[]>([]);
69-
7048
// Initialize config namespace if needed
7149
$effect(() => {
7250
if (configKey && !config[configKey]) {
@@ -81,26 +59,6 @@
8159
}
8260
});
8361
84-
// Sync UI state for components with default values
85-
$effect(() => {
86-
for (const { component } of uninitializedKeys) {
87-
if (component.defaultValue === undefined) {
88-
continue;
89-
}
90-
91-
if (component.type === 'select') {
92-
selectValue = {
93-
label: formatLabel(String(component.defaultValue)),
94-
value: String(component.defaultValue),
95-
};
96-
}
97-
98-
if (component.type === 'switch') {
99-
switchValue = Boolean(component.defaultValue);
100-
}
101-
}
102-
});
103-
10462
const isPickerField = (subType: string | undefined) => subType === 'album-picker' || subType === 'people-picker';
10563
</script>
10664

@@ -123,14 +81,16 @@
12381
{@const options = component.options?.map((opt) => {
12482
return { label: opt.label, value: String(opt.value) };
12583
}) || [{ label: 'N/A', value: '' }]}
84+
{@const currentValue = actualConfig[key]}
85+
{@const selectedItem = options.find((opt) => opt.value === String(currentValue)) ?? options[0]}
12686

12787
<Field
12888
{label}
12989
required={component.required}
13090
description={component.description}
13191
requiredIndicator={component.required}
13292
>
133-
<Select data={options} onChange={(opt) => updateConfig(key, opt.value)} bind:value={selectValue} />
93+
<Select data={options} onChange={(opt) => updateConfig(key, opt.value)} value={selectedItem} />
13494
</Field>
13595
{/if}
13696

@@ -147,6 +107,8 @@
147107
{@const options = component.options?.map((opt) => {
148108
return { label: opt.label, value: String(opt.value) };
149109
}) || [{ label: 'N/A', value: '' }]}
110+
{@const currentValues = (actualConfig[key] as string[]) ?? []}
111+
{@const selectedItems = options.filter((opt) => currentValues.includes(opt.value))}
150112

151113
<Field
152114
{label}
@@ -161,20 +123,21 @@
161123
key,
162124
opt.map((o) => o.value),
163125
)}
164-
bind:values={multiSelectValue}
126+
values={selectedItems}
165127
/>
166128
</Field>
167129
{/if}
168130

169131
<!-- Switch component -->
170132
{:else if component.type === 'switch'}
133+
{@const checked = Boolean(actualConfig[key])}
171134
<Field
172135
{label}
173136
description={component.description}
174137
requiredIndicator={component.required}
175138
required={component.required}
176139
>
177-
<Switch bind:checked={switchValue} onCheckedChange={(check) => updateConfig(key, check)} />
140+
<Switch {checked} onCheckedChange={(check) => updateConfig(key, check)} />
178141
</Field>
179142

180143
<!-- Text input -->

web/src/lib/services/workflow.service.ts

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,69 @@ export const getActionsByContext = (
5858
};
5959

6060
/**
61-
* Initialize filter configurations from existing workflow
61+
* Remap configs when items are reordered (drag-drop)
62+
* Moves config from old index to new index position
6263
*/
63-
export const initializeFilterConfigs = (
64-
workflow: WorkflowResponseDto,
65-
availableFilters: PluginFilterResponseDto[],
64+
export const remapConfigsOnReorder = (
65+
configs: Record<string, unknown>,
66+
prefix: 'filter' | 'action',
67+
fromIndex: number,
68+
toIndex: number,
69+
totalCount: number,
70+
): Record<string, unknown> => {
71+
const newConfigs: Record<string, unknown> = {};
72+
73+
// Create an array of configs in order
74+
const configArray: unknown[] = [];
75+
for (let i = 0; i < totalCount; i++) {
76+
configArray.push(configs[`${prefix}_${i}`] ?? {});
77+
}
78+
79+
// Move the item from fromIndex to toIndex
80+
const [movedItem] = configArray.splice(fromIndex, 1);
81+
configArray.splice(toIndex, 0, movedItem);
82+
83+
// Rebuild the configs object with new indices
84+
for (let i = 0; i < configArray.length; i++) {
85+
newConfigs[`${prefix}_${i}`] = configArray[i];
86+
}
87+
88+
return newConfigs;
89+
};
90+
91+
/**
92+
* Remap configs when an item is removed
93+
* Shifts all configs after the removed index down by one
94+
*/
95+
export const remapConfigsOnRemove = (
96+
configs: Record<string, unknown>,
97+
prefix: 'filter' | 'action',
98+
removedIndex: number,
99+
totalCount: number,
66100
): Record<string, unknown> => {
101+
const newConfigs: Record<string, unknown> = {};
102+
103+
let newIndex = 0;
104+
for (let i = 0; i < totalCount; i++) {
105+
if (i !== removedIndex) {
106+
newConfigs[`${prefix}_${newIndex}`] = configs[`${prefix}_${i}`] ?? {};
107+
newIndex++;
108+
}
109+
}
110+
111+
return newConfigs;
112+
};
113+
114+
/**
115+
* Initialize filter configurations from existing workflow
116+
* Uses index-based keys to support multiple filters of the same type
117+
*/
118+
export const initializeFilterConfigs = (workflow: WorkflowResponseDto): Record<string, unknown> => {
67119
const configs: Record<string, unknown> = {};
68120

69121
if (workflow.filters) {
70-
for (const workflowFilter of workflow.filters) {
71-
const filterDef = availableFilters.find((f) => f.id === workflowFilter.pluginFilterId);
72-
if (filterDef) {
73-
configs[filterDef.methodName] = workflowFilter.filterConfig ?? {};
74-
}
122+
for (const [index, workflowFilter] of workflow.filters.entries()) {
123+
configs[`filter_${index}`] = workflowFilter.filterConfig ?? {};
75124
}
76125
}
77126

@@ -80,19 +129,14 @@ export const initializeFilterConfigs = (
80129

81130
/**
82131
* Initialize action configurations from existing workflow
132+
* Uses index-based keys to support multiple actions of the same type
83133
*/
84-
export const initializeActionConfigs = (
85-
workflow: WorkflowResponseDto,
86-
availableActions: PluginActionResponseDto[],
87-
): Record<string, unknown> => {
134+
export const initializeActionConfigs = (workflow: WorkflowResponseDto): Record<string, unknown> => {
88135
const configs: Record<string, unknown> = {};
89136

90137
if (workflow.actions) {
91-
for (const workflowAction of workflow.actions) {
92-
const actionDef = availableActions.find((a) => a.id === workflowAction.pluginActionId);
93-
if (actionDef) {
94-
configs[actionDef.methodName] = workflowAction.actionConfig ?? {};
95-
}
138+
for (const [index, workflowAction] of workflow.actions.entries()) {
139+
configs[`action_${index}`] = workflowAction.actionConfig ?? {};
96140
}
97141
}
98142

@@ -101,6 +145,7 @@ export const initializeActionConfigs = (
101145

102146
/**
103147
* Build workflow payload from current state
148+
* Uses index-based keys to support multiple filters/actions of the same type
104149
*/
105150
export const buildWorkflowPayload = (
106151
name: string,
@@ -112,12 +157,12 @@ export const buildWorkflowPayload = (
112157
filterConfigs: Record<string, unknown>,
113158
actionConfigs: Record<string, unknown>,
114159
): WorkflowPayload => {
115-
const filters = orderedFilters.map((filter) => ({
116-
[filter.methodName]: filterConfigs[filter.methodName] ?? {},
160+
const filters = orderedFilters.map((filter, index) => ({
161+
[filter.methodName]: filterConfigs[`filter_${index}`] ?? {},
117162
}));
118163

119-
const actions = orderedActions.map((action) => ({
120-
[action.methodName]: actionConfigs[action.methodName] ?? {},
164+
const actions = orderedActions.map((action, index) => ({
165+
[action.methodName]: actionConfigs[`action_${index}`] ?? {},
121166
}));
122167

123168
return {
@@ -158,30 +203,30 @@ export const parseWorkflowJson = (
158203
// Find trigger
159204
const trigger = availableTriggers.find((t) => t.type === parsed.triggerType);
160205

161-
// Parse filters
206+
// Parse filters (using index-based keys to support multiple of same type)
162207
const filters: PluginFilterResponseDto[] = [];
163208
const filterConfigs: Record<string, unknown> = {};
164209
if (Array.isArray(parsed.filters)) {
165-
for (const filterObj of parsed.filters) {
210+
for (const [index, filterObj] of parsed.filters.entries()) {
166211
const methodName = Object.keys(filterObj)[0];
167212
const filter = availableFilters.find((f) => f.methodName === methodName);
168213
if (filter) {
169214
filters.push(filter);
170-
filterConfigs[methodName] = (filterObj as Record<string, unknown>)[methodName];
215+
filterConfigs[`filter_${index}`] = (filterObj as Record<string, unknown>)[methodName];
171216
}
172217
}
173218
}
174219

175-
// Parse actions
220+
// Parse actions (using index-based keys to support multiple of same type)
176221
const actions: PluginActionResponseDto[] = [];
177222
const actionConfigs: Record<string, unknown> = {};
178223
if (Array.isArray(parsed.actions)) {
179-
for (const actionObj of parsed.actions) {
224+
for (const [index, actionObj] of parsed.actions.entries()) {
180225
const methodName = Object.keys(actionObj)[0];
181226
const action = availableActions.find((a) => a.methodName === methodName);
182227
if (action) {
183228
actions.push(action);
184-
actionConfigs[methodName] = (actionObj as Record<string, unknown>)[methodName];
229+
actionConfigs[`action_${index}`] = (actionObj as Record<string, unknown>)[methodName];
185230
}
186231
}
187232
}
@@ -220,8 +265,6 @@ export const hasWorkflowChanged = (
220265
orderedActions: PluginActionResponseDto[],
221266
filterConfigs: Record<string, unknown>,
222267
actionConfigs: Record<string, unknown>,
223-
availableFilters: PluginFilterResponseDto[],
224-
availableActions: PluginActionResponseDto[],
225268
): boolean => {
226269
// Check enabled state
227270
if (enabled !== previousWorkflow.enabled) {
@@ -252,25 +295,19 @@ export const hasWorkflowChanged = (
252295
return true;
253296
}
254297

255-
// Check filter configs
298+
// Check filter configs (using index-based keys)
256299
const previousFilterConfigs: Record<string, unknown> = {};
257-
for (const wf of previousWorkflow.filters ?? []) {
258-
const filterDef = availableFilters.find((f) => f.id === wf.pluginFilterId);
259-
if (filterDef) {
260-
previousFilterConfigs[filterDef.methodName] = wf.filterConfig ?? {};
261-
}
300+
for (const [index, wf] of (previousWorkflow.filters ?? []).entries()) {
301+
previousFilterConfigs[`filter_${index}`] = wf.filterConfig ?? {};
262302
}
263303
if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) {
264304
return true;
265305
}
266306

267-
// Check action configs
307+
// Check action configs (using index-based keys)
268308
const previousActionConfigs: Record<string, unknown> = {};
269-
for (const wa of previousWorkflow.actions ?? []) {
270-
const actionDef = availableActions.find((a) => a.id === wa.pluginActionId);
271-
if (actionDef) {
272-
previousActionConfigs[actionDef.methodName] = wa.actionConfig ?? {};
273-
}
309+
for (const [index, wa] of (previousWorkflow.actions ?? []).entries()) {
310+
previousActionConfigs[`action_${index}`] = wa.actionConfig ?? {};
274311
}
275312
if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) {
276313
return true;
@@ -293,14 +330,14 @@ export const handleUpdateWorkflow = async (
293330
filterConfigs: Record<string, unknown>,
294331
actionConfigs: Record<string, unknown>,
295332
): Promise<WorkflowResponseDto> => {
296-
const filters = orderedFilters.map((filter) => ({
333+
const filters = orderedFilters.map((filter, index) => ({
297334
pluginFilterId: filter.id,
298-
filterConfig: filterConfigs[filter.methodName] ?? {},
335+
filterConfig: filterConfigs[`filter_${index}`] ?? {},
299336
})) as WorkflowFilterItemDto[];
300337

301-
const actions = orderedActions.map((action) => ({
338+
const actions = orderedActions.map((action, index) => ({
302339
pluginActionId: action.id,
303-
actionConfig: actionConfigs[action.methodName] ?? {},
340+
actionConfig: actionConfigs[`action_${index}`] ?? {},
304341
})) as WorkflowActionItemDto[];
305342

306343
const updateDto: WorkflowUpdateDto = {

web/src/lib/utils/workflow.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ interface JSONSchema {
2828
required?: string[];
2929
}
3030

31+
export const getComponentDefaultValue = (component: ComponentConfig): unknown => {
32+
if (component.defaultValue !== undefined) {
33+
return component.defaultValue;
34+
}
35+
36+
// Initialize with appropriate empty value based on component type
37+
if (component.type === 'multiselect' || (component.type === 'text' && component.subType === 'people-picker')) {
38+
return [];
39+
}
40+
41+
if (component.type === 'switch') {
42+
return false;
43+
}
44+
45+
return '';
46+
};
47+
3148
export const getComponentFromSchema = (schema: object | null): Record<string, ComponentConfig> | null => {
3249
if (!schema || !isJSONSchema(schema) || !schema.properties) {
3350
return null;

0 commit comments

Comments
 (0)