diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-field.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-field.spec.ts new file mode 100644 index 000000000..885d06ce4 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-field.spec.ts @@ -0,0 +1,217 @@ +import { FieldMetadataType } from 'twenty-shared'; + +import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; +import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; +import { camelToTitleCase } from 'src/utils/camel-to-title-case'; + +jest.mock('src/engine/utils/generate-fake-value'); +jest.mock('src/utils/camel-to-title-case'); +jest.mock('src/engine/metadata-modules/field-metadata/composite-types', () => { + const mockCompositeTypeDefinitions = new Map(); + + mockCompositeTypeDefinitions.set(FieldMetadataType.LINKS, { + properties: [ + { name: 'label', type: FieldMetadataType.TEXT }, + { name: 'url', type: FieldMetadataType.TEXT }, + ], + }); + + mockCompositeTypeDefinitions.set(FieldMetadataType.CURRENCY, { + properties: [ + { name: 'amount', type: FieldMetadataType.NUMBER }, + { name: 'currencyCode', type: FieldMetadataType.TEXT }, + ], + }); + + return { + compositeTypeDefinitions: mockCompositeTypeDefinitions, + }; +}); + +describe('generateFakeField', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Default mock implementations + (generateFakeValue as jest.Mock).mockImplementation( + (type) => `fake-${type}`, + ); + (camelToTitleCase as jest.Mock).mockImplementation((str) => `Title ${str}`); + }); + + describe('for simple field types', () => { + it('should generate a leaf node for TEXT type', () => { + (generateFakeValue as jest.Mock).mockReturnValueOnce('Fake Text'); + + const result = generateFakeField({ + type: FieldMetadataType.TEXT, + label: 'Text Field', + }); + + expect(result).toEqual({ + isLeaf: true, + type: FieldMetadataType.TEXT, + icon: undefined, + label: 'Text Field', + value: 'Fake Text', + }); + + expect(generateFakeValue).toHaveBeenCalledWith( + FieldMetadataType.TEXT, + 'FieldMetadataType', + ); + }); + + it('should generate a leaf node for NUMBER type with icon', () => { + (generateFakeValue as jest.Mock).mockReturnValueOnce(42); + + const result = generateFakeField({ + type: FieldMetadataType.NUMBER, + label: 'Number Field', + icon: 'IconNumber', + }); + + expect(result).toEqual({ + isLeaf: true, + type: FieldMetadataType.NUMBER, + icon: 'IconNumber', + label: 'Number Field', + value: 42, + }); + }); + + it('should generate a leaf node for DATE type', () => { + const fakeDate = new Date('2023-01-01'); + + (generateFakeValue as jest.Mock).mockReturnValueOnce(fakeDate); + + const result = generateFakeField({ + type: FieldMetadataType.DATE, + label: 'Date Field', + }); + + expect(result).toEqual({ + isLeaf: true, + type: FieldMetadataType.DATE, + icon: undefined, + label: 'Date Field', + value: fakeDate, + }); + }); + }); + + describe('for composite field types', () => { + it('should generate a node with properties for LINKS type', () => { + (generateFakeValue as jest.Mock) + .mockReturnValueOnce('Fake Label') + .mockReturnValueOnce('https://example.com'); + + (camelToTitleCase as jest.Mock) + .mockReturnValueOnce('Label') + .mockReturnValueOnce('Url'); + + const result = generateFakeField({ + type: FieldMetadataType.LINKS, + label: 'Links Field', + }); + + expect(result).toEqual({ + isLeaf: false, + icon: undefined, + label: 'Links Field', + value: { + label: { + isLeaf: true, + type: FieldMetadataType.TEXT, + label: 'Label', + value: 'Fake Label', + }, + url: { + isLeaf: true, + type: FieldMetadataType.TEXT, + label: 'Url', + value: 'https://example.com', + }, + }, + }); + + expect(generateFakeValue).toHaveBeenCalledTimes(2); + expect(camelToTitleCase).toHaveBeenCalledWith('label'); + expect(camelToTitleCase).toHaveBeenCalledWith('url'); + }); + + it('should generate a node with properties for CURRENCY type', () => { + (generateFakeValue as jest.Mock) + .mockReturnValueOnce(100) + .mockReturnValueOnce('USD'); + + (camelToTitleCase as jest.Mock) + .mockReturnValueOnce('Amount') + .mockReturnValueOnce('Currency Code'); + + const result = generateFakeField({ + type: FieldMetadataType.CURRENCY, + label: 'Currency Field', + icon: 'IconCurrency', + }); + + expect(result).toEqual({ + isLeaf: false, + icon: 'IconCurrency', + label: 'Currency Field', + value: { + amount: { + isLeaf: true, + type: FieldMetadataType.NUMBER, + label: 'Amount', + value: 100, + }, + currencyCode: { + isLeaf: true, + type: FieldMetadataType.TEXT, + label: 'Currency Code', + value: 'USD', + }, + }, + }); + }); + }); + + describe('edge cases', () => { + it('should handle unknown field types as leaf nodes', () => { + const unknownType = 'UNKNOWN_TYPE' as FieldMetadataType; + + (generateFakeValue as jest.Mock).mockReturnValueOnce('Unknown Value'); + + const result = generateFakeField({ + type: unknownType, + label: 'Unknown Field', + }); + + expect(result).toEqual({ + isLeaf: true, + type: unknownType, + icon: undefined, + label: 'Unknown Field', + value: 'Unknown Value', + }); + }); + + it('should handle empty label', () => { + (generateFakeValue as jest.Mock).mockReturnValueOnce('Fake Boolean'); + + const result = generateFakeField({ + type: FieldMetadataType.BOOLEAN, + label: '', + }); + + expect(result).toEqual({ + isLeaf: true, + type: FieldMetadataType.BOOLEAN, + icon: undefined, + label: '', + value: 'Fake Boolean', + }); + }); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts new file mode 100644 index 000000000..701388878 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts @@ -0,0 +1,63 @@ +import { FieldMetadataType } from 'twenty-shared'; + +import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response'; + +describe('generateFakeFormResponse', () => { + it('should generate fake responses for a form schema', () => { + const schema = [ + { + name: 'name', + type: FieldMetadataType.TEXT, + label: 'Name', + }, + { + name: 'age', + type: FieldMetadataType.NUMBER, + label: 'Age', + }, + { + name: 'email', + type: FieldMetadataType.EMAILS, + label: 'Email', + }, + ]; + + const result = generateFakeFormResponse(schema); + + expect(result).toEqual({ + email: { + isLeaf: false, + label: 'Email', + value: { + additionalEmails: { + isLeaf: true, + label: ' Additional Emails', + type: FieldMetadataType.RAW_JSON, + value: null, + }, + primaryEmail: { + isLeaf: true, + label: ' Primary Email', + type: FieldMetadataType.TEXT, + value: 'My text', + }, + }, + icon: undefined, + }, + name: { + isLeaf: true, + label: 'Name', + type: FieldMetadataType.TEXT, + value: 'My text', + icon: undefined, + }, + age: { + isLeaf: true, + label: 'Age', + type: FieldMetadataType.NUMBER, + value: 20, + icon: undefined, + }, + }); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts new file mode 100644 index 000000000..f9fdb59d3 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts @@ -0,0 +1,47 @@ +import { FieldMetadataType } from 'twenty-shared'; + +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; +import { + Leaf, + Node, +} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { camelToTitleCase } from 'src/utils/camel-to-title-case'; + +export const generateFakeField = ({ + type, + label, + icon, +}: { + type: FieldMetadataType; + label: string; + icon?: string; +}): Leaf | Node => { + const compositeType = compositeTypeDefinitions.get(type); + + if (!compositeType) { + return { + isLeaf: true, + type: type, + icon: icon, + label: label, + value: generateFakeValue(type, 'FieldMetadataType'), + }; + } else { + return { + isLeaf: false, + icon: icon, + label: label, + value: compositeType.properties.reduce((acc, property) => { + acc[property.name] = { + isLeaf: true, + type: property.type, + label: camelToTitleCase(property.name), + value: generateFakeValue(property.type, 'FieldMetadataType'), + }; + + return acc; + }, {}), + }; + } +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts new file mode 100644 index 000000000..f48b390b6 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts @@ -0,0 +1,19 @@ +import { + Leaf, + Node, +} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; +import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; + +export const generateFakeFormResponse = ( + formMetadata: FormFieldMetadata[], +): Record => { + return formMetadata.reduce((acc, formFieldMetadata) => { + acc[formFieldMetadata.name] = generateFakeField({ + type: formFieldMetadata.type, + label: formFieldMetadata.label, + }); + + return acc; + }, {}); +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts index a41f3514a..3fd213011 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts @@ -1,13 +1,11 @@ -import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; import { Leaf, Node, RecordOutputSchema, } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; -import { camelToTitleCase } from 'src/utils/camel-to-title-case'; const generateObjectRecordFields = ( objectMetadataEntity: ObjectMetadataEntity, @@ -17,33 +15,12 @@ const generateObjectRecordFields = ( if (!shouldGenerateFieldFakeValue(field)) { return acc; } - const compositeType = compositeTypeDefinitions.get(field.type); - if (!compositeType) { - acc[field.name] = { - isLeaf: true, - type: field.type, - icon: field.icon, - label: field.label, - value: generateFakeValue(field.type, 'FieldMetadataType'), - }; - } else { - acc[field.name] = { - isLeaf: false, - icon: field.icon, - label: field.label, - value: compositeType.properties.reduce((acc, property) => { - acc[property.name] = { - isLeaf: true, - type: property.type, - label: camelToTitleCase(property.name), - value: generateFakeValue(property.type, 'FieldMetadataType'), - }; - - return acc; - }, {}), - }; - } + acc[field.name] = generateFakeField({ + type: field.type, + label: field.label, + icon: field.icon, + }); return acc; }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index bde6bad8e..00ae360f5 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -9,8 +9,10 @@ import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event'; +import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; import { WorkflowAction, WorkflowActionType, @@ -77,6 +79,10 @@ export class WorkflowSchemaWorkspaceService { workspaceId, objectMetadataRepository: this.objectMetadataRepository, }); + case WorkflowActionType.FORM: + return this.computeFormActionOutputSchema({ + formMetadata: step.settings.input, + }); case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution default: return {}; @@ -174,4 +180,12 @@ export class WorkflowSchemaWorkspaceService { private computeSendEmailActionOutputSchema(): OutputSchema { return { success: { isLeaf: true, type: 'boolean', value: true } }; } + + private computeFormActionOutputSchema({ + formMetadata, + }: { + formMetadata: FormFieldMetadata[]; + }): OutputSchema { + return generateFakeFormResponse(formMetadata); + } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts index 306008bd5..a57f191ad 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts @@ -1,8 +1,11 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type'; export type FormFieldMetadata = { label: string; - type: string; + name: string; + type: FieldMetadataType; placeholder?: string; settings?: Record; };