sanitize assigner collaboration payloads

Collaboration restores some empty assigner variable selectors as null, which later reaches the assigner panel and crashes variable-selector reads that expect arrays.

Normalize assigner operation items when converting versioned payloads and when applying operation list updates so variable-mode selectors always stay arrays. Add targeted tests for both the helper path and useConfig exposure.
This commit is contained in:
hjlarry 2026-04-14 11:29:21 +08:00
parent ed6fc3f4a1
commit 7e044fc602
4 changed files with 75 additions and 7 deletions

View File

@ -65,4 +65,20 @@ describe('assigner use-config helpers', () => {
expect(updateOperationItems(legacyInputs, nextItems).items).toEqual(nextItems)
expect(legacyInputs.items).toHaveLength(1)
})
it('sanitizes variable-selector items restored from collaboration payloads', () => {
const dirtyItems = [{
variable_selector: null as unknown as AssignerNodeType['items'][number]['variable_selector'],
input_type: AssignerNodeInputType.variable,
operation: WriteMode.overwrite,
value: null,
}]
expect(updateOperationItems(createInputs('2'), dirtyItems).items).toEqual([{
variable_selector: [],
input_type: AssignerNodeInputType.variable,
operation: WriteMode.overwrite,
value: [],
}])
})
})

View File

@ -95,4 +95,24 @@ describe('useConfig', () => {
expect(result.current.filterToAssignedVar({ type: VarType.string } as never, VarType.arrayString, WriteMode.append)).toBe(true)
expect(result.current.filterToAssignedVar({ type: VarType.number } as never, VarType.arrayString, WriteMode.append)).toBe(false)
})
it('should normalize collaboration-restored null selectors before exposing inputs', () => {
const dirtyPayload = createPayload({
version: '2',
items: [createOperation({
variable_selector: null as unknown as AssignerNodeOperation['variable_selector'],
input_type: AssignerNodeInputType.variable,
value: null,
})],
})
const { result } = renderHook(() => useConfig('assigner-node', dirtyPayload))
expect(result.current.inputs.items).toEqual([expect.objectContaining({
variable_selector: [],
input_type: AssignerNodeInputType.variable,
operation: WriteMode.overwrite,
value: [],
})])
})
})

View File

@ -3,6 +3,7 @@ import type { AssignerNodeOperation, AssignerNodeType } from './types'
import { produce } from 'immer'
import { VarType } from '../../types'
import { WriteMode } from './types'
import { normalizeOperationItems } from './utils'
export const filterVarByType = (varType: VarType) => {
return (variable: Var) => {
@ -86,5 +87,5 @@ export const updateOperationItems = (
inputs: AssignerNodeType,
items: AssignerNodeOperation[],
) => produce(inputs, (draft) => {
draft.items = [...items]
draft.items = normalizeOperationItems(items)
})

View File

@ -1,4 +1,4 @@
import type { AssignerNodeType } from './types'
import type { AssignerNodeOperation, AssignerNodeType } from './types'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { AssignerNodeInputType, WriteMode } from './types'
@ -62,18 +62,49 @@ const convertOldWriteMode = (oldMode: string): WriteMode => {
}
}
const normalizeVariableSelector = (value: unknown) => {
return Array.isArray(value) ? value : []
}
export const normalizeOperationItems = (items: unknown): AssignerNodeOperation[] => {
if (!Array.isArray(items))
return []
return items.map((item) => {
const operationItem = (item || {}) as Partial<AssignerNodeOperation>
const inputType = operationItem.input_type === AssignerNodeInputType.constant
? AssignerNodeInputType.constant
: AssignerNodeInputType.variable
return {
variable_selector: normalizeVariableSelector(operationItem.variable_selector),
input_type: inputType,
operation: Object.values(WriteMode).includes(operationItem.operation as WriteMode)
? operationItem.operation as WriteMode
: WriteMode.overwrite,
value: inputType === AssignerNodeInputType.variable
? normalizeVariableSelector(operationItem.value)
: operationItem.value,
}
})
}
export const convertV1ToV2 = (payload: any): AssignerNodeType => {
if (payload.version === '2' && payload.items)
return payload as AssignerNodeType
if (payload.version === '2' && payload.items) {
return {
...payload,
items: normalizeOperationItems(payload.items),
} as AssignerNodeType
}
return {
...payload,
version: '2',
items: [{
items: normalizeOperationItems([{
variable_selector: payload.assigned_variable_selector || [],
input_type: AssignerNodeInputType.variable,
operation: convertOldWriteMode(payload.write_mode),
value: payload.input_variable_selector || [],
}],
...payload,
}]),
}
}