Generate fake form from metadata (#10641)
- add name to form field metadata - extract field generation from object record schema - use field generation to generate field from metadata - add tests
This commit is contained in:
@ -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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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<string, Leaf | Node> => {
|
||||||
|
return formMetadata.reduce((acc, formFieldMetadata) => {
|
||||||
|
acc[formFieldMetadata.name] = generateFakeField({
|
||||||
|
type: formFieldMetadata.type,
|
||||||
|
label: formFieldMetadata.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
@ -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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
|
||||||
import {
|
import {
|
||||||
Leaf,
|
Leaf,
|
||||||
Node,
|
Node,
|
||||||
RecordOutputSchema,
|
RecordOutputSchema,
|
||||||
} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
} 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 { 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 = (
|
const generateObjectRecordFields = (
|
||||||
objectMetadataEntity: ObjectMetadataEntity,
|
objectMetadataEntity: ObjectMetadataEntity,
|
||||||
@ -17,33 +15,12 @@ const generateObjectRecordFields = (
|
|||||||
if (!shouldGenerateFieldFakeValue(field)) {
|
if (!shouldGenerateFieldFakeValue(field)) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
|
||||||
|
|
||||||
if (!compositeType) {
|
acc[field.name] = generateFakeField({
|
||||||
acc[field.name] = {
|
type: field.type,
|
||||||
isLeaf: true,
|
label: field.label,
|
||||||
type: field.type,
|
icon: field.icon,
|
||||||
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;
|
|
||||||
}, {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
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 { 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 { 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 {
|
import {
|
||||||
WorkflowAction,
|
WorkflowAction,
|
||||||
WorkflowActionType,
|
WorkflowActionType,
|
||||||
@ -77,6 +79,10 @@ export class WorkflowSchemaWorkspaceService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
objectMetadataRepository: this.objectMetadataRepository,
|
objectMetadataRepository: this.objectMetadataRepository,
|
||||||
});
|
});
|
||||||
|
case WorkflowActionType.FORM:
|
||||||
|
return this.computeFormActionOutputSchema({
|
||||||
|
formMetadata: step.settings.input,
|
||||||
|
});
|
||||||
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
|
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
@ -174,4 +180,12 @@ export class WorkflowSchemaWorkspaceService {
|
|||||||
private computeSendEmailActionOutputSchema(): OutputSchema {
|
private computeSendEmailActionOutputSchema(): OutputSchema {
|
||||||
return { success: { isLeaf: true, type: 'boolean', value: true } };
|
return { success: { isLeaf: true, type: 'boolean', value: true } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeFormActionOutputSchema({
|
||||||
|
formMetadata,
|
||||||
|
}: {
|
||||||
|
formMetadata: FormFieldMetadata[];
|
||||||
|
}): OutputSchema {
|
||||||
|
return generateFakeFormResponse(formMetadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
|
||||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||||
|
|
||||||
export type FormFieldMetadata = {
|
export type FormFieldMetadata = {
|
||||||
label: string;
|
label: string;
|
||||||
type: string;
|
name: string;
|
||||||
|
type: FieldMetadataType;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user