Fix array-typed parameters in code/logic-function action forms (#21102)
## Problem `any[]` type prevented value input: <img width="544" height="349" alt="Screenshot 2026-06-01 at 14 08 47" src="https://github.com/user-attachments/assets/956238d0-6fea-4be1-b75a-ab0e6e6424ac" /> In the workflow Code action (and the Logic Function action), parameters typed as any[], string[], etc. rendered as an empty grey box instead of an "Enter value" text input. After any debounced save, even a properly initialised array field would also collapse into an empty container. The Array<T> / ReadonlyArray<T> generic form fell through to a generic text input by accident (which "looked" right, but for the wrong reason — no schema info downstream). ## Root causes Three places treated arrays as plain objects via @sniptt/guards' isObject (which is true for arrays): 1. WorkflowEditActionCodeFields.tsx — arrays went into the nested-fields branch; Object.entries([]) is empty → empty container, no placeholder. 2. mergeDefaultFunctionInputAndFunctionInput.ts — recursed into arrays during merge, turning [] into {}. Triggered on every debounced save, so the bug surfaced after any edit. 3. get-function-input-schema.ts — only handled T[] (SyntaxKind.ArrayType); Array<T> (SyntaxKind.TypeReference) was unrecognised, so the form lost any item-type info.
This commit is contained in:
+3
-3
@@ -10,8 +10,8 @@ import { getWorkflowCodeFieldsEnumSelectOptions } from '@/workflow/workflow-step
|
||||
import { getWorkflowCodeFieldsLeafKind } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyArray, isNonEmptyString, isObject } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined, isPlainObject } from 'twenty-shared/utils';
|
||||
import { type FunctionInput, type InputSchema } from 'twenty-shared/workflow';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
@@ -59,7 +59,7 @@ export const WorkflowEditActionCodeFields = ({
|
||||
? schemaProperty.label
|
||||
: inputKey;
|
||||
|
||||
if (inputValue !== null && isObject(inputValue)) {
|
||||
if (isPlainObject(inputValue)) {
|
||||
return (
|
||||
<div key={pathKey}>
|
||||
<InputLabel>{displayLabel}</InputLabel>
|
||||
|
||||
+24
@@ -24,4 +24,28 @@ describe('mergeDefaultFunctionInputAndFunctionInput', () => {
|
||||
}),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should preserve array values without recursing into them as nested objects', () => {
|
||||
const newInput = { briefs: [], b: null };
|
||||
const oldInput = { briefs: [], b: 5 };
|
||||
|
||||
expect(
|
||||
mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput,
|
||||
oldInput,
|
||||
}),
|
||||
).toEqual({ briefs: [], b: 5 });
|
||||
});
|
||||
|
||||
it('should keep an array typed by the user instead of resetting it', () => {
|
||||
const newInput = { briefs: [], b: null };
|
||||
const oldInput = { briefs: '["a", "b"]', b: null };
|
||||
|
||||
expect(
|
||||
mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput,
|
||||
oldInput,
|
||||
}),
|
||||
).toEqual({ briefs: '["a", "b"]', b: null });
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { isPlainObject } from 'twenty-shared/utils';
|
||||
import { type FunctionInput } from 'twenty-shared/workflow';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
|
||||
export const mergeDefaultFunctionInputAndFunctionInput = ({
|
||||
newInput,
|
||||
@@ -16,12 +16,12 @@ export const mergeDefaultFunctionInputAndFunctionInput = ({
|
||||
|
||||
if (!(key in oldInput)) {
|
||||
result[key] = newValue;
|
||||
} else if (newValue === null && isObject(oldValue)) {
|
||||
} else if (newValue === null && isPlainObject(oldValue)) {
|
||||
result[key] = null;
|
||||
} else if (isObject(newValue)) {
|
||||
} else if (isPlainObject(newValue)) {
|
||||
result[key] = mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput: newValue,
|
||||
oldInput: isObject(oldValue) ? oldValue : {},
|
||||
oldInput: isPlainObject(oldValue) ? oldValue : {},
|
||||
});
|
||||
} else {
|
||||
result[key] = oldValue;
|
||||
|
||||
@@ -26,6 +26,66 @@ describe('getFunctionInputSchema', () => {
|
||||
expect(result).toEqual([{ type: 'string' }, { type: 'number' }]);
|
||||
});
|
||||
|
||||
it('should parse any[] as an array with unknown items', () => {
|
||||
const fileContent = `
|
||||
export const main = (params: { briefs: any[] }): void => {
|
||||
return;
|
||||
};
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
briefs: { type: 'array', items: {} },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse Array<T> generic syntax the same way as T[]', () => {
|
||||
const fileContent = `
|
||||
export const main = (params: {
|
||||
briefs: Array<any>;
|
||||
names: Array<string>;
|
||||
readonlyNames: ReadonlyArray<string>;
|
||||
}): void => {
|
||||
return;
|
||||
};
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
briefs: { type: 'array', items: {} },
|
||||
names: { type: 'array', items: { type: 'string' } },
|
||||
readonlyNames: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fall back to an unknown type for unrecognized type references', () => {
|
||||
const fileContent = `
|
||||
export const main = (params: { value: Map<string, number> }): void => {
|
||||
return;
|
||||
};
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should analyze a complex function correctly', () => {
|
||||
const fileContent = `
|
||||
function testFunction(
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createSourceFile,
|
||||
type FunctionDeclaration,
|
||||
type FunctionLikeDeclaration,
|
||||
type Identifier,
|
||||
type LiteralTypeNode,
|
||||
type Node,
|
||||
type PropertySignature,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
type StringLiteral,
|
||||
SyntaxKind,
|
||||
type TypeNode,
|
||||
type TypeReferenceNode,
|
||||
type UnionTypeNode,
|
||||
type VariableStatement,
|
||||
} from 'typescript';
|
||||
@@ -31,6 +33,24 @@ const getTypeString = (typeNode: TypeNode): InputJsonSchema => {
|
||||
type: 'array',
|
||||
items: getTypeString((typeNode as ArrayTypeNode).elementType),
|
||||
};
|
||||
case SyntaxKind.TypeReference: {
|
||||
const typeReferenceNode = typeNode as TypeReferenceNode;
|
||||
const typeName =
|
||||
typeReferenceNode.typeName.kind === SyntaxKind.Identifier
|
||||
? (typeReferenceNode.typeName as Identifier).text
|
||||
: undefined;
|
||||
|
||||
if (typeName === 'Array' || typeName === 'ReadonlyArray') {
|
||||
const elementType = typeReferenceNode.typeArguments?.[0];
|
||||
|
||||
return {
|
||||
type: 'array',
|
||||
items: isDefined(elementType) ? getTypeString(elementType) : {},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
case SyntaxKind.ObjectKeyword:
|
||||
return { type: 'object' };
|
||||
case SyntaxKind.TypeLiteral: {
|
||||
|
||||
+16
@@ -1,3 +1,4 @@
|
||||
import { type InputJsonSchema } from '@/logic-function';
|
||||
import { getFunctionInputFromInputSchema, type InputSchema } from '@/workflow';
|
||||
|
||||
describe('getDefaultFunctionInputFromInputSchema', () => {
|
||||
@@ -40,4 +41,19 @@ describe('getDefaultFunctionInputFromInputSchema', () => {
|
||||
expectedResult,
|
||||
);
|
||||
});
|
||||
|
||||
it('should init arrays with unknown items (e.g. any[]) as empty arrays', () => {
|
||||
const inputSchema: InputJsonSchema[] = [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
briefs: { type: 'array', items: {} },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getFunctionInputFromInputSchema(inputSchema)).toEqual([
|
||||
{ briefs: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user