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 88ff784a2..1a9a9cd4b 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 a5117db33..aa117bd46 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 1340f848c..3b3086bb3 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 65286e5f2..71af62edb 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 4c075885d..6d3745e89 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 af8c41489..467807b47 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 cfb58e9b3..543293372 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 0974d6841..e6f47c430 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 7870ccec8..2b9694d26 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 (
-
+
);