fix: new schemaformfield has value of the same type

This commit is contained in:
Alex Tran
2025-12-05 21:09:05 +00:00
parent 5156438336
commit 3d771127d2
4 changed files with 127 additions and 100 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { formatLabel, getComponentFromSchema, type ComponentConfig } from '$lib/utils/workflow'; import { getComponentDefaultValue, getComponentFromSchema } from '$lib/utils/workflow';
import { Field, Input, MultiSelect, Select, Switch, Text, type SelectItem } from '@immich/ui'; import { Field, Input, MultiSelect, Select, Switch, Text } from '@immich/ui';
import WorkflowPickerField from './WorkflowPickerField.svelte'; import WorkflowPickerField from './WorkflowPickerField.svelte';
type Props = { type Props = {
@@ -25,24 +25,6 @@
config = configKey ? { ...config, [configKey]: { ...actualConfig, ...updates } } : { ...config, ...updates }; config = configKey ? { ...config, [configKey]: { ...actualConfig, ...updates } } : { ...config, ...updates };
}; };
// Helper to determine default value for a component based on its type
const getDefaultValue = (component: ComponentConfig): unknown => {
if (component.defaultValue !== undefined) {
return component.defaultValue;
}
// Initialize with appropriate empty value based on component type
if (component.type === 'multiselect' || (component.type === 'text' && component.subType === 'people-picker')) {
return [];
}
if (component.type === 'switch') {
return false;
}
return '';
};
// Derive which keys need initialization (missing from actualConfig) // Derive which keys need initialization (missing from actualConfig)
const uninitializedKeys = $derived.by(() => { const uninitializedKeys = $derived.by(() => {
if (!components) { if (!components) {
@@ -51,7 +33,7 @@
return Object.entries(components) return Object.entries(components)
.filter(([key]) => actualConfig[key] === undefined) .filter(([key]) => actualConfig[key] === undefined)
.map(([key, component]) => ({ key, component, defaultValue: getDefaultValue(component) })); .map(([key, component]) => ({ key, component, defaultValue: getComponentDefaultValue(component) }));
}); });
// Derive the batch updates needed // Derive the batch updates needed
@@ -63,10 +45,6 @@
return updates; return updates;
}); });
let selectValue = $state<SelectItem>();
let switchValue = $state<boolean>(false);
let multiSelectValue = $state<SelectItem[]>([]);
// Initialize config namespace if needed // Initialize config namespace if needed
$effect(() => { $effect(() => {
if (configKey && !config[configKey]) { if (configKey && !config[configKey]) {
@@ -81,26 +59,6 @@
} }
}); });
// Sync UI state for components with default values
$effect(() => {
for (const { component } of uninitializedKeys) {
if (component.defaultValue === undefined) {
continue;
}
if (component.type === 'select') {
selectValue = {
label: formatLabel(String(component.defaultValue)),
value: String(component.defaultValue),
};
}
if (component.type === 'switch') {
switchValue = Boolean(component.defaultValue);
}
}
});
const isPickerField = (subType: string | undefined) => subType === 'album-picker' || subType === 'people-picker'; const isPickerField = (subType: string | undefined) => subType === 'album-picker' || subType === 'people-picker';
</script> </script>
@@ -123,6 +81,8 @@
{@const options = component.options?.map((opt) => { {@const options = component.options?.map((opt) => {
return { label: opt.label, value: String(opt.value) }; return { label: opt.label, value: String(opt.value) };
}) || [{ label: 'N/A', value: '' }]} }) || [{ label: 'N/A', value: '' }]}
{@const currentValue = actualConfig[key]}
{@const selectedItem = options.find((opt) => opt.value === String(currentValue)) ?? options[0]}
<Field <Field
{label} {label}
@@ -130,7 +90,7 @@
description={component.description} description={component.description}
requiredIndicator={component.required} 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> </Field>
{/if} {/if}
@@ -147,6 +107,8 @@
{@const options = component.options?.map((opt) => { {@const options = component.options?.map((opt) => {
return { label: opt.label, value: String(opt.value) }; return { label: opt.label, value: String(opt.value) };
}) || [{ label: 'N/A', value: '' }]} }) || [{ label: 'N/A', value: '' }]}
{@const currentValues = (actualConfig[key] as string[]) ?? []}
{@const selectedItems = options.filter((opt) => currentValues.includes(opt.value))}
<Field <Field
{label} {label}
@@ -161,20 +123,21 @@
key, key,
opt.map((o) => o.value), opt.map((o) => o.value),
)} )}
bind:values={multiSelectValue} values={selectedItems}
/> />
</Field> </Field>
{/if} {/if}
<!-- Switch component --> <!-- Switch component -->
{:else if component.type === 'switch'} {:else if component.type === 'switch'}
{@const checked = Boolean(actualConfig[key])}
<Field <Field
{label} {label}
description={component.description} description={component.description}
requiredIndicator={component.required} requiredIndicator={component.required}
required={component.required} required={component.required}
> >
<Switch bind:checked={switchValue} onCheckedChange={(check) => updateConfig(key, check)} /> <Switch {checked} onCheckedChange={(check) => updateConfig(key, check)} />
</Field> </Field>
<!-- Text input --> <!-- Text input -->

View File

@@ -58,20 +58,69 @@ export const getActionsByContext = (
}; };
/** /**
* Initialize filter configurations from existing workflow * Remap configs when items are reordered (drag-drop)
* Moves config from old index to new index position
*/ */
export const initializeFilterConfigs = ( export const remapConfigsOnReorder = (
workflow: WorkflowResponseDto, configs: Record<string, unknown>,
availableFilters: PluginFilterResponseDto[], prefix: 'filter' | 'action',
fromIndex: number,
toIndex: number,
totalCount: number,
): Record<string, unknown> => { ): Record<string, unknown> => {
const newConfigs: Record<string, unknown> = {};
// Create an array of configs in order
const configArray: unknown[] = [];
for (let i = 0; i < totalCount; i++) {
configArray.push(configs[`${prefix}_${i}`] ?? {});
}
// 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;
};
/**
* Remap configs when an item is removed
* Shifts all configs after the removed index down by one
*/
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;
};
/**
* Initialize filter configurations from existing workflow
* Uses index-based keys to support multiple filters of the same type
*/
export const initializeFilterConfigs = (workflow: WorkflowResponseDto): Record<string, unknown> => {
const configs: Record<string, unknown> = {}; const configs: Record<string, unknown> = {};
if (workflow.filters) { if (workflow.filters) {
for (const workflowFilter of workflow.filters) { for (const [index, workflowFilter] of workflow.filters.entries()) {
const filterDef = availableFilters.find((f) => f.id === workflowFilter.pluginFilterId); configs[`filter_${index}`] = workflowFilter.filterConfig ?? {};
if (filterDef) {
configs[filterDef.methodName] = workflowFilter.filterConfig ?? {};
}
} }
} }
@@ -80,19 +129,14 @@ export const initializeFilterConfigs = (
/** /**
* Initialize action configurations from existing workflow * Initialize action configurations from existing workflow
* Uses index-based keys to support multiple actions of the same type
*/ */
export const initializeActionConfigs = ( export const initializeActionConfigs = (workflow: WorkflowResponseDto): Record<string, unknown> => {
workflow: WorkflowResponseDto,
availableActions: PluginActionResponseDto[],
): Record<string, unknown> => {
const configs: Record<string, unknown> = {}; const configs: Record<string, unknown> = {};
if (workflow.actions) { if (workflow.actions) {
for (const workflowAction of workflow.actions) { for (const [index, workflowAction] of workflow.actions.entries()) {
const actionDef = availableActions.find((a) => a.id === workflowAction.pluginActionId); configs[`action_${index}`] = workflowAction.actionConfig ?? {};
if (actionDef) {
configs[actionDef.methodName] = workflowAction.actionConfig ?? {};
}
} }
} }
@@ -101,6 +145,7 @@ export const initializeActionConfigs = (
/** /**
* Build workflow payload from current state * Build workflow payload from current state
* Uses index-based keys to support multiple filters/actions of the same type
*/ */
export const buildWorkflowPayload = ( export const buildWorkflowPayload = (
name: string, name: string,
@@ -112,12 +157,12 @@ export const buildWorkflowPayload = (
filterConfigs: Record<string, unknown>, filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>, actionConfigs: Record<string, unknown>,
): WorkflowPayload => { ): WorkflowPayload => {
const filters = orderedFilters.map((filter) => ({ const filters = orderedFilters.map((filter, index) => ({
[filter.methodName]: filterConfigs[filter.methodName] ?? {}, [filter.methodName]: filterConfigs[`filter_${index}`] ?? {},
})); }));
const actions = orderedActions.map((action) => ({ const actions = orderedActions.map((action, index) => ({
[action.methodName]: actionConfigs[action.methodName] ?? {}, [action.methodName]: actionConfigs[`action_${index}`] ?? {},
})); }));
return { return {
@@ -158,30 +203,30 @@ export const parseWorkflowJson = (
// Find trigger // Find trigger
const trigger = availableTriggers.find((t) => t.type === parsed.triggerType); const trigger = availableTriggers.find((t) => t.type === parsed.triggerType);
// Parse filters // Parse filters (using index-based keys to support multiple of same type)
const filters: PluginFilterResponseDto[] = []; const filters: PluginFilterResponseDto[] = [];
const filterConfigs: Record<string, unknown> = {}; const filterConfigs: Record<string, unknown> = {};
if (Array.isArray(parsed.filters)) { 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 methodName = Object.keys(filterObj)[0];
const filter = availableFilters.find((f) => f.methodName === methodName); const filter = availableFilters.find((f) => f.methodName === methodName);
if (filter) { if (filter) {
filters.push(filter); filters.push(filter);
filterConfigs[methodName] = (filterObj as Record<string, unknown>)[methodName]; filterConfigs[`filter_${index}`] = (filterObj as Record<string, unknown>)[methodName];
} }
} }
} }
// Parse actions // Parse actions (using index-based keys to support multiple of same type)
const actions: PluginActionResponseDto[] = []; const actions: PluginActionResponseDto[] = [];
const actionConfigs: Record<string, unknown> = {}; const actionConfigs: Record<string, unknown> = {};
if (Array.isArray(parsed.actions)) { 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 methodName = Object.keys(actionObj)[0];
const action = availableActions.find((a) => a.methodName === methodName); const action = availableActions.find((a) => a.methodName === methodName);
if (action) { if (action) {
actions.push(action); actions.push(action);
actionConfigs[methodName] = (actionObj as Record<string, unknown>)[methodName]; actionConfigs[`action_${index}`] = (actionObj as Record<string, unknown>)[methodName];
} }
} }
} }
@@ -220,8 +265,6 @@ export const hasWorkflowChanged = (
orderedActions: PluginActionResponseDto[], orderedActions: PluginActionResponseDto[],
filterConfigs: Record<string, unknown>, filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>, actionConfigs: Record<string, unknown>,
availableFilters: PluginFilterResponseDto[],
availableActions: PluginActionResponseDto[],
): boolean => { ): boolean => {
// Check enabled state // Check enabled state
if (enabled !== previousWorkflow.enabled) { if (enabled !== previousWorkflow.enabled) {
@@ -252,25 +295,19 @@ export const hasWorkflowChanged = (
return true; return true;
} }
// Check filter configs // Check filter configs (using index-based keys)
const previousFilterConfigs: Record<string, unknown> = {}; const previousFilterConfigs: Record<string, unknown> = {};
for (const wf of previousWorkflow.filters ?? []) { for (const [index, wf] of (previousWorkflow.filters ?? []).entries()) {
const filterDef = availableFilters.find((f) => f.id === wf.pluginFilterId); previousFilterConfigs[`filter_${index}`] = wf.filterConfig ?? {};
if (filterDef) {
previousFilterConfigs[filterDef.methodName] = wf.filterConfig ?? {};
}
} }
if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) { if (JSON.stringify(previousFilterConfigs) !== JSON.stringify(filterConfigs)) {
return true; return true;
} }
// Check action configs // Check action configs (using index-based keys)
const previousActionConfigs: Record<string, unknown> = {}; const previousActionConfigs: Record<string, unknown> = {};
for (const wa of previousWorkflow.actions ?? []) { for (const [index, wa] of (previousWorkflow.actions ?? []).entries()) {
const actionDef = availableActions.find((a) => a.id === wa.pluginActionId); previousActionConfigs[`action_${index}`] = wa.actionConfig ?? {};
if (actionDef) {
previousActionConfigs[actionDef.methodName] = wa.actionConfig ?? {};
}
} }
if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) { if (JSON.stringify(previousActionConfigs) !== JSON.stringify(actionConfigs)) {
return true; return true;
@@ -293,14 +330,14 @@ export const handleUpdateWorkflow = async (
filterConfigs: Record<string, unknown>, filterConfigs: Record<string, unknown>,
actionConfigs: Record<string, unknown>, actionConfigs: Record<string, unknown>,
): Promise<WorkflowResponseDto> => { ): Promise<WorkflowResponseDto> => {
const filters = orderedFilters.map((filter) => ({ const filters = orderedFilters.map((filter, index) => ({
pluginFilterId: filter.id, pluginFilterId: filter.id,
filterConfig: filterConfigs[filter.methodName] ?? {}, filterConfig: filterConfigs[`filter_${index}`] ?? {},
})) as WorkflowFilterItemDto[]; })) as WorkflowFilterItemDto[];
const actions = orderedActions.map((action) => ({ const actions = orderedActions.map((action, index) => ({
pluginActionId: action.id, pluginActionId: action.id,
actionConfig: actionConfigs[action.methodName] ?? {}, actionConfig: actionConfigs[`action_${index}`] ?? {},
})) as WorkflowActionItemDto[]; })) as WorkflowActionItemDto[];
const updateDto: WorkflowUpdateDto = { const updateDto: WorkflowUpdateDto = {

View File

@@ -28,6 +28,23 @@ interface JSONSchema {
required?: string[]; required?: string[];
} }
export const getComponentDefaultValue = (component: ComponentConfig): unknown => {
if (component.defaultValue !== undefined) {
return component.defaultValue;
}
// Initialize with appropriate empty value based on component type
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 => { export const getComponentFromSchema = (schema: object | null): Record<string, ComponentConfig> | null => {
if (!schema || !isJSONSchema(schema) || !schema.properties) { if (!schema || !isJSONSchema(schema) || !schema.properties) {
return null; return null;

View File

@@ -18,6 +18,8 @@
initializeActionConfigs, initializeActionConfigs,
initializeFilterConfigs, initializeFilterConfigs,
parseWorkflowJson, parseWorkflowJson,
remapConfigsOnRemove,
remapConfigsOnReorder,
type WorkflowPayload, type WorkflowPayload,
} from '$lib/services/workflow.service'; } from '$lib/services/workflow.service';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@@ -93,8 +95,8 @@
), ),
); );
let filterConfigs: Record<string, unknown> = $derived(initializeFilterConfigs(editWorkflow, supportFilters)); let filterConfigs: Record<string, unknown> = $derived(initializeFilterConfigs(editWorkflow));
let actionConfigs: Record<string, unknown> = $derived(initializeActionConfigs(editWorkflow, supportActions)); let actionConfigs: Record<string, unknown> = $derived(initializeActionConfigs(editWorkflow));
$effect(() => { $effect(() => {
editWorkflow.triggerType = triggerType; editWorkflow.triggerType = triggerType;
@@ -195,8 +197,6 @@
selectedActions, selectedActions,
filterConfigs, filterConfigs,
actionConfigs, actionConfigs,
filters,
actions,
), ),
); );
@@ -222,6 +222,9 @@
return; return;
} }
// Remap configs to follow the new order
filterConfigs = remapConfigsOnReorder(filterConfigs, 'filter', draggedFilterIndex, index, selectedFilters.length);
const newFilters = [...selectedFilters]; const newFilters = [...selectedFilters];
const [draggedItem] = newFilters.splice(draggedFilterIndex, 1); const [draggedItem] = newFilters.splice(draggedFilterIndex, 1);
newFilters.splice(index, 0, draggedItem); newFilters.splice(index, 0, draggedItem);
@@ -249,6 +252,9 @@
return; return;
} }
// Remap configs to follow the new order
actionConfigs = remapConfigsOnReorder(actionConfigs, 'action', draggedActionIndex, index, selectedActions.length);
const newActions = [...selectedActions]; const newActions = [...selectedActions];
const [draggedItem] = newActions.splice(draggedActionIndex, 1); const [draggedItem] = newActions.splice(draggedActionIndex, 1);
newActions.splice(index, 0, draggedItem); newActions.splice(index, 0, draggedItem);
@@ -277,10 +283,14 @@
}; };
const handleRemoveFilter = (index: number) => { const handleRemoveFilter = (index: number) => {
// Remap configs to account for the removed item
filterConfigs = remapConfigsOnRemove(filterConfigs, 'filter', index, selectedFilters.length);
selectedFilters = selectedFilters.filter((_, i) => i !== index); selectedFilters = selectedFilters.filter((_, i) => i !== index);
}; };
const handleRemoveAction = (index: number) => { const handleRemoveAction = (index: number) => {
// Remap configs to account for the removed item
actionConfigs = remapConfigsOnRemove(actionConfigs, 'action', index, selectedActions.length);
selectedActions = selectedActions.filter((_, i) => i !== index); selectedActions = selectedActions.filter((_, i) => i !== index);
}; };
@@ -473,7 +483,7 @@
<SchemaFormFields <SchemaFormFields
schema={filter.schema} schema={filter.schema}
bind:config={filterConfigs} bind:config={filterConfigs}
configKey={filter.methodName} configKey={`filter_${index}`}
/> />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -542,7 +552,7 @@
<SchemaFormFields <SchemaFormFields
schema={action.schema} schema={action.schema}
bind:config={actionConfigs} bind:config={actionConfigs}
configKey={action.methodName} configKey={`action_${index}`}
/> />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">