diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx index ed90400e8c..323675355f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx @@ -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 (
{displayLabel} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/mergeDefaultFunctionInputAndFunctionInput.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/mergeDefaultFunctionInputAndFunctionInput.test.ts index a3720eb539..233315172d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/mergeDefaultFunctionInputAndFunctionInput.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/mergeDefaultFunctionInputAndFunctionInput.test.ts @@ -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 }); + }); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput.ts index 2f0d3634d2..da97586419 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput.ts @@ -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; diff --git a/packages/twenty-shared/src/logic-function/__tests__/get-function-input-schema.test.ts b/packages/twenty-shared/src/logic-function/__tests__/get-function-input-schema.test.ts index 79e411bba0..8a62274886 100644 --- a/packages/twenty-shared/src/logic-function/__tests__/get-function-input-schema.test.ts +++ b/packages/twenty-shared/src/logic-function/__tests__/get-function-input-schema.test.ts @@ -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 generic syntax the same way as T[]', () => { + const fileContent = ` + export const main = (params: { + briefs: Array; + names: Array; + readonlyNames: ReadonlyArray; + }): 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 }): void => { + return; + }; + `; + const result = getFunctionInputSchema(fileContent); + + expect(result).toEqual([ + { + type: 'object', + properties: { + value: {}, + }, + }, + ]); + }); + it('should analyze a complex function correctly', () => { const fileContent = ` function testFunction( diff --git a/packages/twenty-shared/src/logic-function/get-function-input-schema.ts b/packages/twenty-shared/src/logic-function/get-function-input-schema.ts index 215be75a1b..4da182aaf8 100644 --- a/packages/twenty-shared/src/logic-function/get-function-input-schema.ts +++ b/packages/twenty-shared/src/logic-function/get-function-input-schema.ts @@ -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: { diff --git a/packages/twenty-shared/src/workflow/utils/__tests__/getDefaultFunctionInputFromInputSchema.test.ts b/packages/twenty-shared/src/workflow/utils/__tests__/getDefaultFunctionInputFromInputSchema.test.ts index 19bd5ea1f2..51931b74c2 100644 --- a/packages/twenty-shared/src/workflow/utils/__tests__/getDefaultFunctionInputFromInputSchema.test.ts +++ b/packages/twenty-shared/src/workflow/utils/__tests__/getDefaultFunctionInputFromInputSchema.test.ts @@ -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: [] }, + ]); + }); });