From 94676215ad4b031279423643275eb28bca53e9cb Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 18 Dec 2024 14:32:21 +0100 Subject: [PATCH] Add fields to update in update record action (#9108) - update backend action so it handles composite fields - add fields to update multiselect - generate form based on that field - add icons --- .../components/FormMultiSelectFieldInput.tsx | 21 +++-- .../display/components/MultiSelectDisplay.tsx | 1 + .../input/components/MultiSelectInput.tsx | 1 + .../src/modules/workflow/types/Workflow.ts | 1 + .../WorkflowEditActionFormUpdateRecord.tsx | 87 +++++++++++++++++++ ...workflow-version-step.workspace-service.ts | 3 +- .../workflow-record-crud-action-input.type.ts | 1 + .../update-record.workflow-action.ts | 85 +++++++++++++++++- .../components/MenuItemMultiSelectTag.tsx | 6 +- 9 files changed, 191 insertions(+), 15 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx index 88ff784a23..1a9a9cd4b9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx @@ -11,6 +11,7 @@ import { SelectOption } from '@/spreadsheet-import/types'; import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay'; import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput'; import { InputLabel } from '@/ui/input/components/InputLabel'; +import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { useId, useState } from 'react'; @@ -20,9 +21,9 @@ import { isDefined } from '~/utils/isDefined'; type FormMultiSelectFieldInputProps = { label?: string; defaultValue: FieldMultiSelectValue | string | undefined; + options: SelectOption[]; onPersist: (value: FieldMultiSelectValue | string) => void; VariablePicker?: VariablePickerComponent; - options: SelectOption[]; }; const StyledDisplayModeContainer = styled.button` @@ -50,9 +51,9 @@ const StyledSelectInputContainer = styled.div` export const FormMultiSelectFieldInput = ({ label, defaultValue, + options, onPersist, VariablePicker, - options, }: FormMultiSelectFieldInputProps) => { const inputId = useId(); @@ -189,13 +190,15 @@ export const FormMultiSelectFieldInput = ({ {draftValue.type === 'static' && draftValue.editingMode === 'edit' && ( - + + + )} diff --git a/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx index a5117db33c..aa117bd469 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx @@ -40,6 +40,7 @@ export const MultiSelectDisplay = ({ key={index} color={selectedOption.color ?? 'transparent'} text={selectedOption.label} + Icon={selectedOption.icon ?? undefined} /> ))} diff --git a/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx index 1340f848c1..3b3086bb3e 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx @@ -127,6 +127,7 @@ export const MultiSelectInput = ({ selected={values?.includes(option.value) || false} text={option.label} color={option.color ?? 'transparent'} + Icon={option.icon ?? undefined} onClick={() => onOptionSelected(formatNewSelectedOptions(option.value)) } diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 65286e5f21..71af62edb3 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -44,6 +44,7 @@ export type WorkflowUpdateRecordActionSettings = BaseWorkflowActionSettings & { objectName: string; objectRecord: ObjectRecord; objectRecordId: string; + fieldsToUpdate: string[]; }; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx index 4c075885d3..6d3745e893 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx @@ -12,9 +12,14 @@ import { useIcons, } from 'twenty-ui'; +import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; +import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; +import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput'; import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody'; +import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker'; import { JsonValue } from 'type-fest'; import { useDebouncedCallback } from 'use-debounce'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; type WorkflowEditActionFormUpdateRecordProps = { action: WorkflowUpdateRecordAction; @@ -31,9 +36,23 @@ type WorkflowEditActionFormUpdateRecordProps = { type UpdateRecordFormData = { objectName: string; objectRecordId: string; + fieldsToUpdate: string[]; [field: string]: unknown; }; +const AVAILABLE_FIELD_METADATA_TYPES = [ + FieldMetadataType.Text, + FieldMetadataType.Number, + FieldMetadataType.Date, + FieldMetadataType.Boolean, + FieldMetadataType.Select, + FieldMetadataType.MultiSelect, + FieldMetadataType.Emails, + FieldMetadataType.Links, + FieldMetadataType.FullName, + FieldMetadataType.Address, +]; + export const WorkflowEditActionFormUpdateRecord = ({ action, actionOptions, @@ -53,6 +72,7 @@ export const WorkflowEditActionFormUpdateRecord = ({ const [formData, setFormData] = useState({ objectName: action.settings.input.objectName, objectRecordId: action.settings.input.objectRecordId, + fieldsToUpdate: action.settings.input.fieldsToUpdate ?? [], ...action.settings.input.objectRecord, }); const isFormDisabled = actionOptions.readonly; @@ -75,6 +95,7 @@ export const WorkflowEditActionFormUpdateRecord = ({ setFormData({ objectName: action.settings.input.objectName, objectRecordId: action.settings.input.objectRecordId, + fieldsToUpdate: action.settings.input.fieldsToUpdate ?? [], ...action.settings.input.objectRecord, }); }, [action.settings.input]); @@ -88,6 +109,27 @@ export const WorkflowEditActionFormUpdateRecord = ({ throw new Error('Should have found the metadata item'); } + const inlineFieldMetadataItems = selectedObjectMetadataItem.fields + .filter( + (fieldMetadataItem) => + !fieldMetadataItem.isSystem && + fieldMetadataItem.isActive && + AVAILABLE_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type), + ) + .sort((fieldMetadataItemA, fieldMetadataItemB) => + fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), + ); + + const inlineFieldDefinitions = inlineFieldMetadataItems.map( + (fieldMetadataItem) => + formatFieldMetadataItemAsFieldDefinition({ + field: fieldMetadataItem, + objectMetadataItem: selectedObjectMetadataItem, + showLabel: true, + labelWidth: 90, + }), + ); + const saveAction = useDebouncedCallback( async (formData: UpdateRecordFormData) => { if (actionOptions.readonly === true) { @@ -97,6 +139,7 @@ export const WorkflowEditActionFormUpdateRecord = ({ const { objectName: updatedObjectName, objectRecordId: updatedObjectRecordId, + fieldsToUpdate: updatedFieldsToUpdate, ...updatedOtherFields } = formData; @@ -108,6 +151,7 @@ export const WorkflowEditActionFormUpdateRecord = ({ objectName: updatedObjectName, objectRecordId: updatedObjectRecordId ?? '', objectRecord: updatedOtherFields, + fieldsToUpdate: updatedFieldsToUpdate ?? [], }, }, }); @@ -154,6 +198,7 @@ export const WorkflowEditActionFormUpdateRecord = ({ const newFormData: UpdateRecordFormData = { objectName: updatedObjectName, objectRecordId: '', + fieldsToUpdate: [], }; setFormData(newFormData); @@ -172,6 +217,48 @@ export const WorkflowEditActionFormUpdateRecord = ({ objectNameSingular={formData.objectName} defaultValue={formData.objectRecordId} /> + + ({ + label: field.label, + value: field.metadata.fieldName, + icon: getIcon(field.iconName), + color: 'gray', + }))} + onPersist={(fieldsToUpdate) => + handleFieldChange('fieldsToUpdate', fieldsToUpdate) + } + /> + + + + {formData.fieldsToUpdate.map((fieldName) => { + const fieldDefinition = inlineFieldDefinitions.find( + (definition) => definition.metadata.fieldName === fieldName, + ); + + if (!isDefined(fieldDefinition)) { + return null; + } + + const currentValue = formData[ + fieldDefinition.metadata.fieldName + ] as JsonValue; + + return ( + { + handleFieldChange(fieldDefinition.metadata.fieldName, value); + }} + VariablePicker={WorkflowVariablePicker} + /> + ); + })} ); diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts index af8c414892..467807b474 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service.ts @@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; +import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema'; import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; @@ -20,7 +21,6 @@ import { WorkflowActionType, } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { isDefined } from 'src/utils/is-defined'; -import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema'; const TRIGGER_STEP_ID = 'trigger'; @@ -152,6 +152,7 @@ export class WorkflowVersionStepWorkspaceService { objectName: activeObjectMetadataItem?.nameSingular || '', objectRecord: {}, objectRecordId: '', + fieldsToUpdate: [], }, }, }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type.ts index cfb58e9b38..5432933723 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type.ts @@ -14,6 +14,7 @@ export type WorkflowUpdateRecordActionInput = { objectName: string; objectRecord: ObjectRecord; objectRecordId: string; + fieldsToUpdate: string[]; }; export type WorkflowDeleteRecordActionInput = { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts index 0974d6841f..e6f47c430d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts @@ -2,7 +2,11 @@ import { Injectable } from '@nestjs/common'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface'; +import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { RecordCRUDActionException, RecordCRUDActionExceptionCode, @@ -12,7 +16,11 @@ import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/wor @Injectable() export class UpdateRecordWorkflowAction implements WorkflowAction { - constructor(private readonly twentyORMManager: TwentyORMManager) {} + constructor( + private readonly twentyORMManager: TwentyORMManager, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, + ) {} async execute( workflowActionInput: WorkflowUpdateRecordActionInput, @@ -34,14 +42,85 @@ export class UpdateRecordWorkflowAction implements WorkflowAction { ); } + const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId; + + if (!workspaceId) { + throw new RecordCRUDActionException( + 'Failed to read: Workspace ID is required', + RecordCRUDActionExceptionCode.INVALID_REQUEST, + ); + } + + const currentCacheVersion = + await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); + + if (currentCacheVersion === undefined) { + throw new RecordCRUDActionException( + 'Failed to read: Metadata cache version not found', + RecordCRUDActionExceptionCode.INVALID_REQUEST, + ); + } + + const objectMetadataMaps = + await this.workspaceCacheStorageService.getObjectMetadataMaps( + workspaceId, + currentCacheVersion, + ); + + if (!objectMetadataMaps) { + throw new RecordCRUDActionException( + 'Failed to read: Object metadata collection not found', + RecordCRUDActionExceptionCode.INVALID_REQUEST, + ); + } + + const objectMetadataItemWithFieldsMaps = + getObjectMetadataMapItemByNameSingular( + objectMetadataMaps, + workflowActionInput.objectName, + ); + + if (!objectMetadataItemWithFieldsMaps) { + throw new RecordCRUDActionException( + `Failed to read: Object ${workflowActionInput.objectName} not found`, + RecordCRUDActionExceptionCode.INVALID_REQUEST, + ); + } + + if (workflowActionInput.fieldsToUpdate.length === 0) { + return { + result: { + ...objectRecord, + }, + }; + } + + const objectRecordWithFilteredFields = Object.keys( + workflowActionInput.objectRecord, + ).reduce((acc, key) => { + if (workflowActionInput.fieldsToUpdate.includes(key)) { + return { + ...acc, + [key]: workflowActionInput.objectRecord[key], + }; + } + + return acc; + }, {}); + + const objectRecordFormatted = formatData( + objectRecordWithFilteredFields, + objectMetadataItemWithFieldsMaps, + ); + await repository.update(workflowActionInput.objectRecordId, { - ...workflowActionInput.objectRecord, + ...objectRecordFormatted, }); return { result: { ...objectRecord, - ...workflowActionInput.objectRecord, + ...objectRecordWithFilteredFields, }, }; } diff --git a/packages/twenty-ui/src/navigation/menu-item/components/MenuItemMultiSelectTag.tsx b/packages/twenty-ui/src/navigation/menu-item/components/MenuItemMultiSelectTag.tsx index 7870ccec82..2b9694d26e 100644 --- a/packages/twenty-ui/src/navigation/menu-item/components/MenuItemMultiSelectTag.tsx +++ b/packages/twenty-ui/src/navigation/menu-item/components/MenuItemMultiSelectTag.tsx @@ -1,4 +1,4 @@ -import { Tag } from '@ui/display'; +import { IconComponent, Tag } from '@ui/display'; import { Checkbox, CheckboxShape, CheckboxSize } from '@ui/input'; import { ThemeColor } from '@ui/theme'; import { @@ -13,6 +13,7 @@ type MenuItemMultiSelectTagProps = { onClick?: () => void; color: ThemeColor | 'transparent'; text: string; + Icon?: IconComponent; }; export const MenuItemMultiSelectTag = ({ @@ -22,6 +23,7 @@ export const MenuItemMultiSelectTag = ({ onClick, isKeySelected, text, + Icon, }: MenuItemMultiSelectTagProps) => { return ( - + );