diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordFieldChip.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordFieldChip.tsx
index b1d1fbf92..654e13ffe 100644
--- a/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordFieldChip.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordFieldChip.tsx
@@ -1,3 +1,4 @@
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordChip } from '@/object-record/components/RecordChip';
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@@ -39,13 +40,18 @@ export const WorkflowSingleRecordFieldChip = ({
objectNameSingular,
onRemove,
}: WorkflowSingleRecordFieldChipProps) => {
+ const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
+
if (
!!draftValue &&
draftValue.type === 'variable' &&
isStandaloneVariableString(draftValue.value)
) {
return (
-
+
);
}
diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordPicker.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordPicker.tsx
index c97167aa6..1f6711521 100644
--- a/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordPicker.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/WorkflowSingleRecordPicker.tsx
@@ -12,7 +12,6 @@ import { SingleRecordSelect } from '@/object-record/relation-picker/components/S
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
-import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
@@ -22,7 +21,7 @@ import SearchVariablesDropdown from '@/workflow/search-variables/components/Sear
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
-import { useCallback, useState } from 'react';
+import { useCallback } from 'react';
import { isValidUuid } from '~/utils/isValidUuid';
const StyledFormSelectContainer = styled.div`
@@ -62,6 +61,16 @@ const StyledSearchVariablesDropdownContainer = styled.div`
export type RecordId = string;
export type Variable = string;
+type WorkflowSingleRecordPickerValue =
+ | {
+ type: 'static';
+ value: RecordId;
+ }
+ | {
+ type: 'variable';
+ value: Variable;
+ };
+
export type WorkflowSingleRecordPickerProps = {
label?: string;
defaultValue: RecordId | Variable;
@@ -75,16 +84,7 @@ export const WorkflowSingleRecordPicker = ({
objectNameSingular,
onChange,
}: WorkflowSingleRecordPickerProps) => {
- const [draftValue, setDraftValue] = useState<
- | {
- type: 'static';
- value: RecordId;
- }
- | {
- type: 'variable';
- value: Variable;
- }
- >(
+ const draftValue: WorkflowSingleRecordPickerValue =
isStandaloneVariableString(defaultValue)
? {
type: 'variable',
@@ -93,10 +93,9 @@ export const WorkflowSingleRecordPicker = ({
: {
type: 'static',
value: defaultValue || '',
- },
- );
+ };
- const { record } = useFindOneRecord({
+ const { record: selectedRecord } = useFindOneRecord({
objectRecordId:
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
? defaultValue
@@ -106,10 +105,6 @@ export const WorkflowSingleRecordPicker = ({
skip: !isValidUuid(defaultValue),
});
- const [selectedRecord, setSelectedRecord] = useState<
- ObjectRecord | undefined
- >(record);
-
const dropdownId = `workflow-record-picker-${objectNameSingular}`;
const variablesDropdownId = `workflow-record-picker-${objectNameSingular}-variables`;
@@ -126,32 +121,16 @@ export const WorkflowSingleRecordPicker = ({
const handleRecordSelected = (
selectedEntity: RecordForSelect | null | undefined,
) => {
- setDraftValue({
- type: 'static',
- value: selectedEntity?.record?.id ?? '',
- });
- setSelectedRecord(selectedEntity?.record);
- closeDropdown();
-
onChange?.(selectedEntity?.record?.id ?? '');
+ closeDropdown();
};
const handleVariableTagInsert = (variable: string) => {
- setDraftValue({
- type: 'variable',
- value: variable,
- });
- setSelectedRecord(undefined);
- closeDropdown();
-
onChange?.(variable);
+ closeDropdown();
};
const handleUnlinkVariable = () => {
- setDraftValue({
- type: 'static',
- value: '',
- });
closeDropdown();
onChange('');
@@ -211,6 +190,7 @@ export const WorkflowSingleRecordPicker = ({
inputId={variablesDropdownId}
onVariableSelect={handleVariableTagInsert}
disabled={false}
+ objectNameSingularToSelect={objectNameSingular}
/>
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 b20d6695b..965a41f40 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
@@ -1,16 +1,16 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
-import { SearchVariablesDropdownStepItem } from '@/workflow/search-variables/components/SearchVariablesDropdownStepItem';
-import SearchVariablesDropdownStepSubItem from '@/workflow/search-variables/components/SearchVariablesDropdownStepSubItem';
+import { SearchVariablesDropdownFieldItems } from '@/workflow/search-variables/components/SearchVariablesDropdownFieldItems';
+import { SearchVariablesDropdownObjectItems } from '@/workflow/search-variables/components/SearchVariablesDropdownObjectItems';
+import { SearchVariablesDropdownWorkflowStepItems } from '@/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems';
import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/search-variables/constants/SearchVariablesDropdownId';
import { useAvailableVariablesInWorkflowStep } from '@/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep';
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
-import { IconVariablePlus } from 'twenty-ui';
+import { IconVariablePlus, isDefined } from 'twenty-ui';
const StyledDropdownVariableButtonContainer = styled(
StyledDropdownButtonContainer,
@@ -26,21 +26,28 @@ const StyledDropdownVariableButtonContainer = styled(
}
`;
+const StyledDropdownComponetsContainer = styled.div`
+ background-color: ${({ theme }) => theme.background.transparent.light};
+`;
+
const SearchVariablesDropdown = ({
inputId,
onVariableSelect,
disabled,
+ objectNameSingularToSelect,
}: {
inputId: string;
onVariableSelect: (variableName: string) => void;
disabled?: boolean;
+ objectNameSingularToSelect?: string;
}) => {
const theme = useTheme();
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`;
- const { isDropdownOpen } = useDropdown(dropdownId);
- const availableVariablesInWorkflowStep =
- useAvailableVariablesInWorkflowStep();
+ const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
+ const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
+ objectNameSingularToSelect,
+ });
const initialStep =
availableVariablesInWorkflowStep.length === 1
@@ -59,12 +66,44 @@ const SearchVariablesDropdown = ({
const handleSubItemSelect = (subItem: string) => {
onVariableSelect(subItem);
+ setSelectedStep(undefined);
+ closeDropdown();
};
const handleBack = () => {
setSelectedStep(undefined);
};
+ const renderSearchVariablesDropdownComponents = () => {
+ if (!isDefined(selectedStep)) {
+ return (
+
+ );
+ }
+
+ if (isDefined(objectNameSingularToSelect)) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
if (disabled === true) {
return (
}
dropdownComponents={
-
- {selectedStep ? (
-
- ) : (
-
- )}
-
+
+ {renderSearchVariablesDropdownComponents()}
+
}
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 4 }}
diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx
new file mode 100644
index 000000000..8dcd83572
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx
@@ -0,0 +1,133 @@
+import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
+import {
+ BaseOutputSchema,
+ OutputSchema,
+ StepOutputSchema,
+} from '@/workflow/search-variables/types/StepOutputSchema';
+import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema';
+import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema';
+import { useTheme } from '@emotion/react';
+
+import { useState } from 'react';
+import {
+ HorizontalSeparator,
+ IconChevronLeft,
+ MenuItemSelect,
+ OverflowingTextWithTooltip,
+ useIcons,
+} from 'twenty-ui';
+
+type SearchVariablesDropdownFieldItemsProps = {
+ step: StepOutputSchema;
+ onSelect: (value: string) => void;
+ onBack: () => void;
+};
+
+export const SearchVariablesDropdownFieldItems = ({
+ step,
+ onSelect,
+ onBack,
+}: SearchVariablesDropdownFieldItemsProps) => {
+ const theme = useTheme();
+ const [currentPath, setCurrentPath] = useState([]);
+ const [searchInputValue, setSearchInputValue] = useState('');
+ const { getIcon } = useIcons();
+
+ const getCurrentSubStep = (): OutputSchema => {
+ let currentSubStep = step.outputSchema;
+
+ for (const key of currentPath) {
+ if (isRecordOutputSchema(currentSubStep)) {
+ currentSubStep = currentSubStep.fields[key]?.value;
+ } else if (isBaseOutputSchema(currentSubStep)) {
+ currentSubStep = currentSubStep[key]?.value;
+ }
+ }
+
+ return currentSubStep;
+ };
+
+ const getDisplayedSubStepFields = () => {
+ const currentSubStep = getCurrentSubStep();
+
+ if (isRecordOutputSchema(currentSubStep)) {
+ return currentSubStep.fields;
+ } else if (isBaseOutputSchema(currentSubStep)) {
+ return currentSubStep;
+ }
+ };
+
+ const handleSelectField = (key: string) => {
+ const currentSubStep = getCurrentSubStep();
+ const handleSelectBaseOutputSchema = (
+ baseOutputSchema: BaseOutputSchema,
+ ) => {
+ if (!baseOutputSchema[key]?.isLeaf) {
+ setCurrentPath([...currentPath, key]);
+ setSearchInputValue('');
+ } else {
+ onSelect(`{{${step.id}.${[...currentPath, key].join('.')}}}`);
+ }
+ };
+
+ if (isRecordOutputSchema(currentSubStep)) {
+ handleSelectBaseOutputSchema(currentSubStep.fields);
+ } else if (isBaseOutputSchema(currentSubStep)) {
+ handleSelectBaseOutputSchema(currentSubStep);
+ }
+ };
+
+ const goBack = () => {
+ if (currentPath.length === 0) {
+ onBack();
+ } else {
+ setCurrentPath(currentPath.slice(0, -1));
+ }
+ };
+
+ const headerLabel = currentPath.length === 0 ? step.name : currentPath.at(-1);
+ const displayedObject = getDisplayedSubStepFields();
+ const options = displayedObject ? Object.entries(displayedObject) : [];
+
+ const filteredOptions = searchInputValue
+ ? options.filter(
+ ([_, value]) =>
+ value.label &&
+ value.label.toLowerCase().includes(searchInputValue.toLowerCase()),
+ )
+ : options;
+
+ return (
+
+
+
+
+
+ setSearchInputValue(event.target.value)}
+ />
+
+ {filteredOptions.map(([key, value]) => (
+ handleSelectField(key)}
+ text={value.label || key}
+ hasSubMenu={!value.isLeaf}
+ LeftIcon={value.icon ? getIcon(value.icon) : undefined}
+ />
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx
new file mode 100644
index 000000000..92aace99a
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx
@@ -0,0 +1,159 @@
+import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
+import {
+ OutputSchema,
+ StepOutputSchema,
+} from '@/workflow/search-variables/types/StepOutputSchema';
+import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema';
+import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema';
+import { useTheme } from '@emotion/react';
+
+import { useState } from 'react';
+import {
+ HorizontalSeparator,
+ IconChevronLeft,
+ MenuItemSelect,
+ OverflowingTextWithTooltip,
+ useIcons,
+} from 'twenty-ui';
+
+type SearchVariablesDropdownObjectItemsProps = {
+ step: StepOutputSchema;
+ onSelect: (value: string) => void;
+ onBack: () => void;
+};
+
+export const SearchVariablesDropdownObjectItems = ({
+ step,
+ onSelect,
+ onBack,
+}: SearchVariablesDropdownObjectItemsProps) => {
+ const theme = useTheme();
+ const [currentPath, setCurrentPath] = useState([]);
+ const [searchInputValue, setSearchInputValue] = useState('');
+ const { getIcon } = useIcons();
+
+ const getCurrentSubStep = (): OutputSchema => {
+ let currentSubStep = step.outputSchema;
+
+ for (const key of currentPath) {
+ if (isRecordOutputSchema(currentSubStep)) {
+ currentSubStep = currentSubStep.fields[key]?.value;
+ } else if (isBaseOutputSchema(currentSubStep)) {
+ currentSubStep = currentSubStep[key]?.value;
+ }
+ }
+
+ return currentSubStep;
+ };
+
+ const getDisplayedSubStepFields = () => {
+ const currentSubStep = getCurrentSubStep();
+
+ if (isRecordOutputSchema(currentSubStep)) {
+ return currentSubStep.fields;
+ } else if (isBaseOutputSchema(currentSubStep)) {
+ return currentSubStep;
+ }
+ };
+
+ const getDisplayedSubStepObject = () => {
+ const currentSubStep = getCurrentSubStep();
+
+ return currentSubStep.object;
+ };
+
+ const handleSelectObject = () => {
+ const currentSubStep = getCurrentSubStep();
+
+ if (!isRecordOutputSchema(currentSubStep)) {
+ return;
+ }
+
+ onSelect(
+ `{{${step.id}.${[...currentPath, currentSubStep.object.fieldIdName].join('.')}}}`,
+ );
+ };
+
+ const handleSelectField = (key: string) => {
+ setCurrentPath([...currentPath, key]);
+ setSearchInputValue('');
+ };
+
+ const goBack = () => {
+ if (currentPath.length === 0) {
+ onBack();
+ } else {
+ setCurrentPath(currentPath.slice(0, -1));
+ }
+ };
+
+ const headerLabel = currentPath.length === 0 ? step.name : currentPath.at(-1);
+
+ const displayedSubStepObject = getDisplayedSubStepObject();
+
+ const shouldDisplaySubStepObject = searchInputValue
+ ? displayedSubStepObject?.label &&
+ displayedSubStepObject.label
+ .toLowerCase()
+ .includes(searchInputValue.toLowerCase())
+ : true;
+
+ const displayedFields = getDisplayedSubStepFields();
+ const options = displayedFields ? Object.entries(displayedFields) : [];
+
+ const filteredOptions = searchInputValue
+ ? options.filter(
+ ([_, value]) =>
+ value.label &&
+ value.label.toLowerCase().includes(searchInputValue.toLowerCase()),
+ )
+ : options;
+
+ return (
+
+
+
+
+
+ setSearchInputValue(event.target.value)}
+ />
+
+ {shouldDisplaySubStepObject && displayedSubStepObject?.label && (
+
+ )}
+ {filteredOptions.map(([key, value]) => (
+ handleSelectField(key)}
+ text={value.label || key}
+ hasSubMenu={!value.isLeaf}
+ LeftIcon={value.icon ? getIcon(value.icon) : undefined}
+ />
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx
deleted file mode 100644
index 2a62fa8cf..000000000
--- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
-import { MenuItem, MenuItemSelect } from 'twenty-ui';
-
-type SearchVariablesDropdownStepItemProps = {
- steps: StepOutputSchema[];
- onSelect: (value: string) => void;
-};
-
-export const SearchVariablesDropdownStepItem = ({
- steps,
- onSelect,
-}: SearchVariablesDropdownStepItemProps) => {
- return steps.length > 0 ? (
- <>
- {steps.map((item, _index) => (
- onSelect(item.id)}
- text={item.name}
- LeftIcon={undefined}
- hasSubMenu
- />
- ))}
- >
- ) : (
-