Add record picker in form action (#11331)

Record picker becomes a form field that could be used in another context
than workflows.

Settings
<img width="488" alt="Capture d’écran 2025-04-02 à 10 55 53"
src="https://github.com/user-attachments/assets/a9fc09ff-28cd-4ede-8aaa-af1e986cda8e"
/>

Execution
<img width="936" alt="Capture d’écran 2025-04-02 à 10 57 36"
src="https://github.com/user-attachments/assets/d796aeeb-cae1-4e59-b388-5b8d08739ea8"
/>
This commit is contained in:
Thomas Trompette
2025-04-02 17:08:33 +02:00
committed by GitHub
parent 2bc9691021
commit 7488c6727a
21 changed files with 400 additions and 154 deletions

View File

@ -1,10 +1,24 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps';
import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response';
import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find(
(item) => item.nameSingular === 'company',
)!;
describe('generateFakeFormResponse', () => {
it('should generate fake responses for a form schema', () => {
const schema = [
let objectMetadataRepository;
beforeEach(() => {
objectMetadataRepository = {
findOneOrFail: jest.fn().mockResolvedValue(companyMockObjectMetadataItem),
};
});
it('should generate fake responses for a form schema', async () => {
const schema: FormFieldMetadata[] = [
{
id: '96939213-49ac-4dee-949d-56e6c7be98e6',
name: 'name',
@ -19,34 +33,22 @@ describe('generateFakeFormResponse', () => {
},
{
id: '96939213-49ac-4dee-949d-56e6c7be98e8',
name: 'email',
type: FieldMetadataType.EMAILS,
label: 'Email',
name: 'company',
type: 'RECORD',
label: 'Company',
settings: {
objectName: 'company',
},
},
];
const result = generateFakeFormResponse(schema);
const result = await generateFakeFormResponse({
formMetadata: schema,
workspaceId: '1',
objectMetadataRepository,
});
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',
@ -61,6 +63,22 @@ describe('generateFakeFormResponse', () => {
value: 20,
icon: undefined,
},
company: {
isLeaf: false,
label: 'Company',
value: {
_outputSchemaType: 'RECORD',
fields: {},
object: {
isLeaf: true,
label: 'Company',
fieldIdName: 'id',
icon: undefined,
nameSingular: 'company',
value: 'A company',
},
},
},
});
});
});

View File

@ -1,19 +1,58 @@
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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 { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record';
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,
});
export const generateFakeFormResponse = async ({
formMetadata,
workspaceId,
objectMetadataRepository,
}: {
formMetadata: FormFieldMetadata[];
workspaceId: string;
objectMetadataRepository: Repository<ObjectMetadataEntity>;
}): Promise<Record<string, Leaf | Node>> => {
const result = await Promise.all(
formMetadata.map(async (formFieldMetadata) => {
if (formFieldMetadata.type === 'RECORD') {
if (!formFieldMetadata?.settings?.objectName) {
return undefined;
}
return acc;
const objectMetadata = await objectMetadataRepository.findOneOrFail({
where: {
nameSingular: formFieldMetadata?.settings?.objectName,
workspaceId,
},
relations: ['fields'],
});
return {
[formFieldMetadata.name]: {
isLeaf: false,
label: formFieldMetadata.label,
value: generateFakeObjectRecord(objectMetadata),
},
};
} else {
return {
[formFieldMetadata.name]: generateFakeField({
type: formFieldMetadata.type,
label: formFieldMetadata.label,
}),
};
}
}),
);
return result.filter(isDefined).reduce((acc, curr) => {
return { ...acc, ...curr };
}, {});
};

View File

@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action';
@ -83,6 +83,8 @@ export class WorkflowSchemaWorkspaceService {
case WorkflowActionType.FORM:
return this.computeFormActionOutputSchema({
formMetadata: step.settings.input,
workspaceId,
objectMetadataRepository: this.objectMetadataRepository,
});
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
default:
@ -182,11 +184,19 @@ export class WorkflowSchemaWorkspaceService {
return { success: { isLeaf: true, type: 'boolean', value: true } };
}
private computeFormActionOutputSchema({
private async computeFormActionOutputSchema({
formMetadata,
workspaceId,
objectMetadataRepository,
}: {
formMetadata: FormFieldMetadata[];
}): OutputSchema {
return generateFakeFormResponse(formMetadata);
workspaceId: string;
objectMetadataRepository: Repository<ObjectMetadataEntity>;
}): Promise<OutputSchema> {
return generateFakeFormResponse({
formMetadata,
workspaceId,
objectMetadataRepository,
});
}
}