diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx index 79d587885..2bddf2ead 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx @@ -40,6 +40,7 @@ const StyledEndIcon = styled.div` `; const StyledChildrenWrapper = styled.span` + overflow: hidden; padding: 0 ${({ theme }) => theme.spacing(1)}; `; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx index ec761aa46..d2e9c11a6 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx @@ -4,9 +4,8 @@ import { forwardRef, InputHTMLAttributes } from 'react'; import { TEXT_INPUT_STYLE } from 'twenty-ui'; const StyledDropdownMenuSearchInputContainer = styled.div` - --vertical-padding: ${({ theme }) => theme.spacing(1)}; - align-items: center; + --vertical-padding: ${({ theme }) => theme.spacing(2)}; display: flex; flex-direction: row; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx index c79d77976..4ef0fbfe5 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx @@ -62,6 +62,7 @@ const SearchVariablesDropdown = ({ return ( { const [currentPath, setCurrentPath] = useState([]); const [searchInputValue, setSearchInputValue] = useState(''); + const { getIcon } = useIcons(); - const getSelectedObject = () => { + const getSelectedObject = (): OutputSchema => { let selected = step.outputSchema; for (const key of currentPath) { - selected = selected[key]; + selected = selected[key]?.value; } return selected; }; @@ -30,7 +38,7 @@ const SearchVariablesDropdownStepSubItem = ({ const handleSelect = (key: string) => { const selectedObject = getSelectedObject(); - if (isObject(selectedObject[key])) { + if (!selectedObject[key]?.isLeaf) { setCurrentPath([...currentPath, key]); setSearchInputValue(''); } else { @@ -59,7 +67,7 @@ const SearchVariablesDropdownStepSubItem = ({ return ( <> - {headerLabel} + handleSelect(key)} text={key} - hasSubMenu={isObject(value)} - LeftIcon={undefined} + hasSubMenu={!value.isLeaf} + LeftIcon={value.icon ? getIcon(value.icon) : undefined} /> ))} diff --git a/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts index acc688e90..d322579d8 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts @@ -1,5 +1,22 @@ +import { InputSchemaPropertyType } from '@/workflow/types/InputSchema'; + +type Leaf = { + isLeaf: true; + type?: InputSchemaPropertyType; + icon?: string; + value: any; +}; + +type Node = { + isLeaf: false; + icon?: string; + value: OutputSchema; +}; + +export type OutputSchema = Record; + export type StepOutputSchema = { id: string; name: string; - outputSchema: Record; + outputSchema: OutputSchema; }; diff --git a/packages/twenty-front/src/modules/workflow/types/InputSchema.ts b/packages/twenty-front/src/modules/workflow/types/InputSchema.ts index 5970bc56f..3bbe695e2 100644 --- a/packages/twenty-front/src/modules/workflow/types/InputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/types/InputSchema.ts @@ -1,10 +1,13 @@ -type InputSchemaPropertyType = +import { FieldMetadataType } from '~/generated/graphql'; + +export type InputSchemaPropertyType = | 'string' | 'number' | 'boolean' | 'object' | 'array' - | 'unknown'; + | 'unknown' + | FieldMetadataType; type InputSchemaProperty = { type: InputSchemaPropertyType; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts index 61fdc5075..eab6db6d3 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts @@ -10,7 +10,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service'; -import { OutputSchema } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type'; +import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type'; @Resolver() @UseGuards(WorkspaceAuthGuard, UserAuthGuard) diff --git a/packages/twenty-server/src/engine/utils/generate-fake-value.ts b/packages/twenty-server/src/engine/utils/generate-fake-value.ts index 3cc669d83..29532b07d 100644 --- a/packages/twenty-server/src/engine/utils/generate-fake-value.ts +++ b/packages/twenty-server/src/engine/utils/generate-fake-value.ts @@ -1,4 +1,12 @@ -export const generateFakeValue = (valueType: string): any => { +type FakeValueTypes = + | string + | number + | boolean + | Date + | FakeValueTypes[] + | { [key: string]: FakeValueTypes }; + +export const generateFakeValue = (valueType: string): FakeValueTypes => { if (valueType === 'string') { return 'generated-string-value'; } else if (valueType === 'number') { diff --git a/packages/twenty-server/src/modules/code-introspection/types/input-schema.type.ts b/packages/twenty-server/src/modules/code-introspection/types/input-schema.type.ts index 312615cab..2a718d89a 100644 --- a/packages/twenty-server/src/modules/code-introspection/types/input-schema.type.ts +++ b/packages/twenty-server/src/modules/code-introspection/types/input-schema.type.ts @@ -1,10 +1,13 @@ -type InputSchemaPropertyType = +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +export type InputSchemaPropertyType = | 'string' | 'number' | 'boolean' | 'object' | 'array' - | 'unknown'; + | 'unknown' + | FieldMetadataType; export type InputSchemaProperty = { type: InputSchemaPropertyType; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/types/output-schema.type.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/types/output-schema.type.ts new file mode 100644 index 000000000..061f1cc74 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/types/output-schema.type.ts @@ -0,0 +1,16 @@ +import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type'; + +type Leaf = { + isLeaf: true; + icon?: string; + type?: InputSchemaPropertyType; + value: any; +}; + +type Node = { + isLeaf: false; + icon?: string; + value: OutputSchema; +}; + +export type OutputSchema = Record; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts index ac2d065e6..5a9850e61 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts @@ -1,76 +1,88 @@ import { v4 } from 'uuid'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record'; +import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type'; -export const generateFakeObjectRecordEvent = ( +export const generateFakeObjectRecordEvent = ( objectMetadataEntity: ObjectMetadataEntity, action: DatabaseEventAction, -): - | ObjectRecordCreateEvent - | ObjectRecordUpdateEvent - | ObjectRecordDeleteEvent - | ObjectRecordDestroyEvent => { +): OutputSchema => { const recordId = v4(); const userId = v4(); const workspaceMemberId = v4(); - const after = generateFakeObjectRecord(objectMetadataEntity); + const after = generateFakeObjectRecord(objectMetadataEntity); + const formattedObjectMetadataEntity = Object.entries( + objectMetadataEntity, + ).reduce((acc: OutputSchema, [key, value]) => { + acc[key] = { isLeaf: true, value }; + + return acc; + }, {}); + + const baseResult: OutputSchema = { + recordId: { isLeaf: true, type: 'string', value: recordId }, + userId: { isLeaf: true, type: 'string', value: userId }, + workspaceMemberId: { + isLeaf: true, + type: 'string', + value: workspaceMemberId, + }, + objectMetadata: { + isLeaf: false, + value: formattedObjectMetadataEntity, + }, + }; if (action === DatabaseEventAction.CREATED) { return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, + ...baseResult, properties: { - after, + isLeaf: false, + value: { after: { isLeaf: false, value: after } }, }, - } satisfies ObjectRecordCreateEvent; + }; } - const before = generateFakeObjectRecord(objectMetadataEntity); + const before = generateFakeObjectRecord(objectMetadataEntity); if (action === DatabaseEventAction.UPDATED) { return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, + ...baseResult, properties: { - before, - after, + isLeaf: false, + value: { + before: { isLeaf: false, value: before }, + after: { isLeaf: false, value: after }, + }, }, - } satisfies ObjectRecordUpdateEvent; + }; } if (action === DatabaseEventAction.DELETED) { return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, + ...baseResult, properties: { - before, + isLeaf: false, + value: { + before: { isLeaf: false, value: before }, + }, }, - } satisfies ObjectRecordDeleteEvent; + }; } if (action === DatabaseEventAction.DESTROYED) { return { - recordId, - userId, - workspaceMemberId, - objectMetadata: objectMetadataEntity, + ...baseResult, properties: { - before, + isLeaf: false, + value: { + before: { isLeaf: false, value: before }, + }, }, - } satisfies ObjectRecordDestroyEvent; + }; } throw new Error(`Unknown action '${action}'`); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts index b35b14da2..5b61eda5e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts @@ -1,16 +1,40 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/utils/should-generate-field-fake-value'; +import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type'; +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -export const generateFakeObjectRecord = ( +export const generateFakeObjectRecord = ( objectMetadataEntity: ObjectMetadataEntity, -): Entity => - objectMetadataEntity.fields.reduce((acc, field) => { +): OutputSchema => + objectMetadataEntity.fields.reduce((acc: OutputSchema, field) => { if (!shouldGenerateFieldFakeValue(field)) { return acc; } + const compositeType = compositeTypeDefinitions.get(field.type); - acc[field.name] = generateFakeValue(field.type); + if (!compositeType) { + acc[field.name] = { + isLeaf: true, + type: field.type, + icon: field.icon, + value: generateFakeValue(field.type), + }; + } else { + acc[field.name] = { + isLeaf: false, + icon: field.icon, + value: compositeType.properties.reduce((acc, property) => { + acc[property.name] = { + isLeaf: true, + type: property.type, + value: generateFakeValue(property.type), + }; + + return acc; + }, {}), + }; + } return acc; - }, {} as Entity); + }, {}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts index 67ad2f1ca..1b4029447 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.workspace-service.ts @@ -14,7 +14,6 @@ import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record'; import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event'; -import { WorkflowSendEmailStepOutputSchema } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action'; import { WorkflowRecordCRUDType } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type'; import { WorkflowAction, @@ -25,6 +24,8 @@ import { WorkflowTriggerType, } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; import { isDefined } from 'src/utils/is-defined'; +import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type'; +import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type'; @Injectable() export class WorkflowBuilderWorkspaceService { @@ -41,7 +42,7 @@ export class WorkflowBuilderWorkspaceService { }: { step: WorkflowTrigger | WorkflowAction; workspaceId: string; - }): Promise { + }): Promise { const stepType = step.type; switch (stepType) { @@ -100,7 +101,7 @@ export class WorkflowBuilderWorkspaceService { eventName: string; workspaceId: string; objectMetadataRepository: Repository; - }) { + }): Promise { const [nameSingular, action] = eventName.split('.'); if (!checkStringIsDatabaseEventAction(action)) { @@ -125,7 +126,7 @@ export class WorkflowBuilderWorkspaceService { ); } - private async computeRecordCrudOutputSchema({ + private async computeRecordCrudOutputSchema({ objectType, operationType, workspaceId, @@ -135,8 +136,8 @@ export class WorkflowBuilderWorkspaceService { operationType: string; workspaceId: string; objectMetadataRepository: Repository; - }) { - const recordOutputSchema = await this.computeRecordOutputSchema({ + }): Promise { + const recordOutputSchema = await this.computeRecordOutputSchema({ objectType, workspaceId, objectMetadataRepository, @@ -144,16 +145,21 @@ export class WorkflowBuilderWorkspaceService { if (operationType === WorkflowRecordCRUDType.READ) { return { - first: recordOutputSchema, - last: recordOutputSchema, - totalCount: generateFakeValue('number'), + first: { isLeaf: false, icon: 'IconAlpha', value: recordOutputSchema }, + last: { isLeaf: false, icon: 'IconOmega', value: recordOutputSchema }, + totalCount: { + isLeaf: true, + icon: 'IconSum', + type: 'number', + value: generateFakeValue('number'), + }, }; } return recordOutputSchema; } - private async computeRecordOutputSchema({ + private async computeRecordOutputSchema({ objectType, workspaceId, objectMetadataRepository, @@ -161,7 +167,7 @@ export class WorkflowBuilderWorkspaceService { objectType: string; workspaceId: string; objectMetadataRepository: Repository; - }) { + }): Promise { const objectMetadata = await objectMetadataRepository.findOneOrFail({ where: { nameSingular: objectType, @@ -174,11 +180,11 @@ export class WorkflowBuilderWorkspaceService { return {}; } - return generateFakeObjectRecord(objectMetadata); + return generateFakeObjectRecord(objectMetadata); } - private computeSendEmailActionOutputSchema(): WorkflowSendEmailStepOutputSchema { - return { success: true }; + private computeSendEmailActionOutputSchema(): OutputSchema { + return { success: { isLeaf: true, type: 'boolean', value: true } }; } private async computeCodeActionOutputSchema({ @@ -193,7 +199,7 @@ export class WorkflowBuilderWorkspaceService { workspaceId: string; serverlessFunctionService: ServerlessFunctionService; codeIntrospectionService: CodeIntrospectionService; - }) { + }): Promise { if (serverlessFunctionId === '') { return {}; } @@ -223,6 +229,19 @@ export class WorkflowBuilderWorkspaceService { serverlessFunctionVersion, ); - return resultFromFakeInput.data ?? {}; + return resultFromFakeInput.data + ? Object.entries(resultFromFakeInput.data).reduce( + (acc: OutputSchema, [key, value]) => { + acc[key] = { + isLeaf: true, + value, + type: typeof value as InputSchemaPropertyType, + }; + + return acc; + }, + {}, + ) + : {}; } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type.ts index 221809cb3..f5d78e33e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type.ts @@ -1,8 +1,7 @@ import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type'; import { WorkflowSendEmailActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-settings.type'; import { WorkflowRecordCRUDActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-settings.type'; - -export type OutputSchema = object; +import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type'; export type BaseWorkflowActionSettings = { input: object; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts index 1222c32f4..75f091c51 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts @@ -1,4 +1,4 @@ -import { OutputSchema } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type'; +import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type'; export enum WorkflowTriggerType { DATABASE_EVENT = 'DATABASE_EVENT',