diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx
index 38084a4d2..b6b97e45b 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx
@@ -1,9 +1,10 @@
import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput';
import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput';
+import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput';
import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput';
import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput';
-import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput';
+import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
@@ -15,13 +16,15 @@ import {
FieldFullNameValue,
FieldLinksValue,
FieldMetadata,
+ FieldMultiSelectValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
+import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
-import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
+import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
@@ -107,5 +110,13 @@ export const FormFieldInput = ({
onPersist={onPersist}
VariablePicker={VariablePicker}
/>
+ ) : isFieldMultiSelect(field) ? (
+
) : null;
};
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
new file mode 100644
index 000000000..88ff784a2
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx
@@ -0,0 +1,211 @@
+import styled from '@emotion/styled';
+
+import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
+import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
+import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
+import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
+import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
+import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
+import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
+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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
+import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
+import { useId, useState } from 'react';
+import { VisibilityHidden } from 'twenty-ui';
+import { isDefined } from '~/utils/isDefined';
+
+type FormMultiSelectFieldInputProps = {
+ label?: string;
+ defaultValue: FieldMultiSelectValue | string | undefined;
+ onPersist: (value: FieldMultiSelectValue | string) => void;
+ VariablePicker?: VariablePickerComponent;
+ options: SelectOption[];
+};
+
+const StyledDisplayModeContainer = styled.button`
+ width: 100%;
+ align-items: center;
+ display: flex;
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ font-family: inherit;
+ padding-inline: ${({ theme }) => theme.spacing(2)};
+
+ &:hover,
+ &[data-open='true'] {
+ background-color: ${({ theme }) => theme.background.transparent.lighter};
+ }
+`;
+
+const StyledSelectInputContainer = styled.div`
+ position: absolute;
+ z-index: 1;
+ top: ${({ theme }) => theme.spacing(8)};
+`;
+
+export const FormMultiSelectFieldInput = ({
+ label,
+ defaultValue,
+ onPersist,
+ VariablePicker,
+ options,
+}: FormMultiSelectFieldInputProps) => {
+ const inputId = useId();
+
+ const hotkeyScope = MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID;
+
+ const {
+ setHotkeyScopeAndMemorizePreviousScope,
+ goBackToPreviousHotkeyScope,
+ } = usePreviousHotkeyScope();
+
+ const [draftValue, setDraftValue] = useState<
+ | {
+ type: 'static';
+ value: FieldMultiSelectValue;
+ editingMode: 'view' | 'edit';
+ }
+ | {
+ type: 'variable';
+ value: string;
+ }
+ >(
+ isStandaloneVariableString(defaultValue)
+ ? {
+ type: 'variable',
+ value: defaultValue,
+ }
+ : {
+ type: 'static',
+ value: isDefined(defaultValue) ? defaultValue : [],
+ editingMode: 'view',
+ },
+ );
+
+ const handleDisplayModeClick = () => {
+ if (draftValue.type !== 'static') {
+ throw new Error(
+ 'This function can only be called when editing a static value.',
+ );
+ }
+
+ setDraftValue({
+ ...draftValue,
+ editingMode: 'edit',
+ });
+
+ setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
+ };
+
+ const onOptionSelected = (value: FieldMultiSelectValue) => {
+ if (draftValue.type !== 'static') {
+ throw new Error('Can only be called when editing a static value');
+ }
+
+ setDraftValue({
+ type: 'static',
+ value,
+ editingMode: 'edit',
+ });
+
+ onPersist(value);
+ };
+
+ const onCancel = () => {
+ if (draftValue.type !== 'static') {
+ throw new Error('Can only be called when editing a static value');
+ }
+
+ setDraftValue({
+ ...draftValue,
+ editingMode: 'view',
+ });
+
+ goBackToPreviousHotkeyScope();
+ };
+
+ const handleVariableTagInsert = (variableName: string) => {
+ setDraftValue({
+ type: 'variable',
+ value: variableName,
+ });
+
+ onPersist(variableName);
+ };
+
+ const handleUnlinkVariable = () => {
+ setDraftValue({
+ type: 'static',
+ value: [],
+ editingMode: 'view',
+ });
+
+ onPersist([]);
+ };
+
+ const selectedNames =
+ draftValue.type === 'static' ? draftValue.value : undefined;
+
+ const selectedOptions =
+ isDefined(selectedNames) && isDefined(options)
+ ? options.filter((option) =>
+ selectedNames.some((name) => option.value === name),
+ )
+ : undefined;
+
+ return (
+
+ {label ? {label} : null}
+
+
+
+ {draftValue.type === 'static' ? (
+
+ Edit
+
+ {isDefined(selectedOptions) ? (
+
+ ) : null}
+
+ ) : (
+
+ )}
+
+
+ {draftValue.type === 'static' &&
+ draftValue.editingMode === 'edit' && (
+
+ )}
+
+
+ {VariablePicker && (
+
+ )}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx
new file mode 100644
index 000000000..2b4df71f5
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx
@@ -0,0 +1,50 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { within } from '@storybook/test';
+import { FormMultiSelectFieldInput } from '../FormMultiSelectFieldInput';
+
+const meta: Meta = {
+ title: 'UI/Data/Field/Form/Input/FormMultiSelectFieldInput',
+ component: FormMultiSelectFieldInput,
+ args: {},
+ argTypes: {},
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ label: 'Work Policy',
+ defaultValue: ['WORK_POLICY_1', 'WORK_POLICY_2'],
+ options: [
+ {
+ label: 'Work Policy 1',
+ value: 'WORK_POLICY_1',
+ color: 'blue',
+ },
+ {
+ label: 'Work Policy 2',
+ value: 'WORK_POLICY_2',
+ color: 'green',
+ },
+ {
+ label: 'Work Policy 3',
+ value: 'WORK_POLICY_3',
+ color: 'red',
+ },
+ {
+ label: 'Work Policy 4',
+ value: 'WORK_POLICY_4',
+ color: 'yellow',
+ },
+ ],
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await canvas.findByText('Work Policy');
+ await canvas.findByText('Work Policy 1');
+ await canvas.findByText('Work Policy 2');
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx
index 7fcc1f4eb..42d258b77 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx
@@ -1,25 +1,10 @@
-import { styled } from '@linaria/react';
-import { Tag, THEME_COMMON } from 'twenty-ui';
+import { Tag } from 'twenty-ui';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useMultiSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useMultiSelectFieldDisplay';
+import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
-const spacing1 = THEME_COMMON.spacing(1);
-
-const StyledContainer = styled.div`
- align-items: center;
- display: flex;
- gap: ${spacing1};
- justify-content: flex-start;
-
- max-width: 100%;
-
- overflow: hidden;
-
- width: 100%;
-`;
-
export const MultiSelectFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();
@@ -44,15 +29,9 @@ export const MultiSelectFieldDisplay = () => {
))}
) : (
-
- {selectedOptions.map((selectedOption, index) => (
-
- ))}
-
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx
index b76fecda5..eb8939aa4 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx
@@ -1,28 +1,5 @@
-import styled from '@emotion/styled';
-import { useRef, useState } from 'react';
-import { useRecoilValue } from 'recoil';
-import { Key } from 'ts-key-enum';
-
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
-import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
-import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
-import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
-import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
-import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
-import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
-import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
-import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
-import { MenuItemMultiSelectTag } from 'twenty-ui';
-import { isDefined } from '~/utils/isDefined';
-import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
-
-const StyledRelationPickerContainer = styled.div`
- left: -1px;
- position: absolute;
- top: -1px;
-`;
+import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
type MultiSelectFieldInputProps = {
onCancel?: () => void;
@@ -31,112 +8,16 @@ type MultiSelectFieldInputProps = {
export const MultiSelectFieldInput = ({
onCancel,
}: MultiSelectFieldInputProps) => {
- const { selectedItemIdState } = useSelectableListStates({
- selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
- });
- const { resetSelectedItem } = useSelectableList(
- MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
- );
const { persistField, fieldDefinition, fieldValues, hotkeyScope } =
useMultiSelectField();
- const selectedItemId = useRecoilValue(selectedItemIdState);
- const [searchFilter, setSearchFilter] = useState('');
- const containerRef = useRef(null);
-
- const selectedOptions = fieldDefinition.metadata.options.filter((option) =>
- fieldValues?.includes(option.value),
- );
-
- const filteredOptionsInDropDown = fieldDefinition.metadata.options.filter(
- (option) => option.label.toLowerCase().includes(searchFilter.toLowerCase()),
- );
-
- const formatNewSelectedOptions = (value: string) => {
- const selectedOptionsValues = selectedOptions.map(
- (selectedOption) => selectedOption.value,
- );
- if (!selectedOptionsValues.includes(value)) {
- return [value, ...selectedOptionsValues];
- } else {
- return selectedOptionsValues.filter(
- (selectedOptionsValue) => selectedOptionsValue !== value,
- );
- }
- };
-
- useScopedHotkeys(
- Key.Escape,
- () => {
- onCancel?.();
- resetSelectedItem();
- },
- hotkeyScope,
- [onCancel, resetSelectedItem],
- );
-
- useListenClickOutside({
- refs: [containerRef],
- callback: (event) => {
- event.stopImmediatePropagation();
-
- const weAreNotInAnHTMLInput = !(
- event.target instanceof HTMLInputElement &&
- event.target.tagName === 'INPUT'
- );
- if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
- onCancel();
- }
- resetSelectedItem();
- },
- listenerId: 'MultiSelectFieldInput',
- });
-
- const optionIds = filteredOptionsInDropDown.map((option) => option.value);
return (
- {
- const option = filteredOptionsInDropDown.find(
- (option) => option.value === itemId,
- );
- if (isDefined(option)) {
- persistField(formatNewSelectedOptions(option.value));
- }
- }}
- >
-
-
-
- setSearchFilter(
- turnIntoEmptyStringIfWhitespacesOnly(event.currentTarget.value),
- )
- }
- autoFocus
- />
-
-
- {filteredOptionsInDropDown.map((option) => {
- return (
-
- persistField(formatNewSelectedOptions(option.value))
- }
- isKeySelected={selectedItemId === option.value}
- />
- );
- })}
-
-
-
-
+ options={fieldDefinition.metadata.options}
+ onCancel={onCancel}
+ onOptionSelected={persistField}
+ values={fieldValues}
+ />
);
};
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
new file mode 100644
index 000000000..a5117db33
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx
@@ -0,0 +1,47 @@
+import { Tag, THEME_COMMON } from 'twenty-ui';
+
+import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
+import { SelectOption } from '@/spreadsheet-import/types';
+import styled from '@emotion/styled';
+
+const spacing1 = THEME_COMMON.spacing(1);
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${spacing1};
+ justify-content: flex-start;
+
+ max-width: 100%;
+
+ overflow: hidden;
+
+ width: 100%;
+`;
+
+export const MultiSelectDisplay = ({
+ values,
+ options,
+}: {
+ values: FieldMultiSelectValue | undefined;
+ options: SelectOption[];
+}) => {
+ const selectedOptions = values
+ ? options?.filter((option) => values.includes(option.value))
+ : [];
+
+ if (!selectedOptions) return null;
+
+ return (
+
+ {selectedOptions.map((selectedOption, index) => (
+
+ ))}
+
+ );
+};
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
new file mode 100644
index 000000000..88c100c81
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx
@@ -0,0 +1,150 @@
+import styled from '@emotion/styled';
+import { useRef, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { Key } from 'ts-key-enum';
+
+import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
+import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
+import { SelectOption } from '@/spreadsheet-import/types';
+import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
+import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
+import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
+import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
+import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
+import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
+import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
+import { MenuItemMultiSelectTag } from 'twenty-ui';
+import { isDefined } from '~/utils/isDefined';
+import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
+
+const StyledRelationPickerContainer = styled.div`
+ left: -1px;
+ position: absolute;
+ top: -1px;
+`;
+
+type MultiSelectInputProps = {
+ values: FieldMultiSelectValue;
+ hotkeyScope: string;
+ onCancel?: () => void;
+ options: SelectOption[];
+ onOptionSelected: (value: FieldMultiSelectValue) => void;
+};
+
+export const MultiSelectInput = ({
+ values,
+ options,
+ hotkeyScope,
+ onCancel,
+ onOptionSelected,
+}: MultiSelectInputProps) => {
+ const { selectedItemIdState } = useSelectableListStates({
+ selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
+ });
+ const { resetSelectedItem } = useSelectableList(
+ MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
+ );
+
+ const selectedItemId = useRecoilValue(selectedItemIdState);
+ const [searchFilter, setSearchFilter] = useState('');
+ const containerRef = useRef(null);
+
+ const selectedOptions = options.filter((option) =>
+ values?.includes(option.value),
+ );
+
+ const filteredOptionsInDropDown = options.filter((option) =>
+ option.label.toLowerCase().includes(searchFilter.toLowerCase()),
+ );
+
+ const formatNewSelectedOptions = (value: string) => {
+ const selectedOptionsValues = selectedOptions.map(
+ (selectedOption) => selectedOption.value,
+ );
+ if (!selectedOptionsValues.includes(value)) {
+ return [value, ...selectedOptionsValues];
+ } else {
+ return selectedOptionsValues.filter(
+ (selectedOptionsValue) => selectedOptionsValue !== value,
+ );
+ }
+ };
+
+ useScopedHotkeys(
+ Key.Escape,
+ () => {
+ onCancel?.();
+ resetSelectedItem();
+ },
+ hotkeyScope,
+ [onCancel, resetSelectedItem],
+ );
+
+ useListenClickOutside({
+ refs: [containerRef],
+ callback: (event) => {
+ event.stopImmediatePropagation();
+
+ const weAreNotInAnHTMLInput = !(
+ event.target instanceof HTMLInputElement &&
+ event.target.tagName === 'INPUT'
+ );
+ if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
+ onCancel();
+ }
+ resetSelectedItem();
+ },
+ listenerId: 'MultiSelectFieldInput',
+ });
+
+ const optionIds = filteredOptionsInDropDown.map((option) => option.value);
+
+ return (
+ {
+ const option = filteredOptionsInDropDown.find(
+ (option) => option.value === itemId,
+ );
+ if (isDefined(option)) {
+ onOptionSelected(formatNewSelectedOptions(option.value));
+ }
+ }}
+ >
+
+
+
+ setSearchFilter(
+ turnIntoEmptyStringIfWhitespacesOnly(event.currentTarget.value),
+ )
+ }
+ autoFocus
+ />
+
+
+ {filteredOptionsInDropDown.map((option) => {
+ return (
+
+ onOptionSelected(formatNewSelectedOptions(option.value))
+ }
+ isKeySelected={selectedItemId === option.value}
+ />
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx
index aed2a1670..58a4ee114 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx
@@ -3,6 +3,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
import { Select, SelectOption } from '@/ui/input/components/Select';
+import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
import { WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
@@ -17,7 +18,6 @@ import {
import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
import { FieldMetadataType } from '~/generated/graphql';
-import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
type WorkflowEditActionFormCreateRecordProps = {
action: WorkflowCreateRecordAction;
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 2e14aded4..4c075885d 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
@@ -1,7 +1,7 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select';
-import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
+import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
@@ -12,9 +12,9 @@ import {
useIcons,
} from 'twenty-ui';
+import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
-import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
type WorkflowEditActionFormUpdateRecordProps = {
action: WorkflowUpdateRecordAction;
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts
index aee8cc34d..cdba717b8 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts
@@ -8,5 +8,4 @@ export class WorkflowExecutorException extends CustomException {
export enum WorkflowExecutorExceptionCode {
WORKFLOW_FAILED = 'WORKFLOW_FAILED',
- VARIABLE_EVALUATION_FAILED = 'VARIABLE_EVALUATION_FAILED',
}
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/__tests__/variable-resolver.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/__tests__/variable-resolver.util.spec.ts
index 0e73ec4a2..dc41cb81d 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/__tests__/variable-resolver.util.spec.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/__tests__/variable-resolver.util.spec.ts
@@ -31,7 +31,7 @@ describe('resolveInput', () => {
});
it('should handle non-existent variables', () => {
- expect(resolveInput('{{user.email}}', context)).toBe('');
+ expect(resolveInput('{{user.email}}', context)).toBe(undefined);
});
it('should resolve variables in an array', () => {
@@ -67,15 +67,11 @@ describe('resolveInput', () => {
const expected = {
user: {
displayName: 'John Doe',
- preferences: ['dark', 'true'],
+ preferences: ['dark', true],
},
staticData: [1, 2, 3],
};
expect(resolveInput(input, context)).toEqual(expected);
});
-
- it('should throw an error for invalid expressions', () => {
- expect(() => resolveInput('{{invalidFunction()}}', context)).toThrow();
- });
});
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/variable-resolver.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/variable-resolver.util.ts
index c4fc012d4..6c391aa52 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/variable-resolver.util.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/variable-resolver.util.ts
@@ -2,11 +2,6 @@ import { isNil, isString } from '@nestjs/common/utils/shared.utils';
import Handlebars from 'handlebars';
-import {
- WorkflowExecutorException,
- WorkflowExecutorExceptionCode,
-} from 'src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception';
-
const VARIABLE_PATTERN = RegExp('\\{\\{(.*?)\\}\\}', 'g');
export const resolveInput = (
@@ -81,18 +76,22 @@ const resolveString = (
});
};
-const evalFromContext = (
- input: string,
- context: Record,
-): string => {
+const evalFromContext = (input: string, context: Record) => {
try {
- const inferredInput = Handlebars.compile(input)(context);
+ Handlebars.registerHelper('json', (input: string) => JSON.stringify(input));
- return inferredInput ?? '';
+ const inputWithHelper = input
+ .replace('{{', '{{{ json ')
+ .replace('}}', ' }}}');
+
+ const inferredInput = Handlebars.compile(inputWithHelper)(context, {
+ helpers: {
+ json: (input: string) => JSON.stringify(input),
+ },
+ });
+
+ return JSON.parse(inferredInput) ?? '';
} catch (exception) {
- throw new WorkflowExecutorException(
- `Failed to evaluate variable ${input}: ${exception}`,
- WorkflowExecutorExceptionCode.VARIABLE_EVALUATION_FAILED,
- );
+ return undefined;
}
};
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 a659ca7ca..7870ccec8 100644
--- a/packages/twenty-ui/src/navigation/menu-item/components/MenuItemMultiSelectTag.tsx
+++ b/packages/twenty-ui/src/navigation/menu-item/components/MenuItemMultiSelectTag.tsx
@@ -11,7 +11,7 @@ type MenuItemMultiSelectTagProps = {
className?: string;
isKeySelected?: boolean;
onClick?: () => void;
- color: ThemeColor;
+ color: ThemeColor | 'transparent';
text: string;
};