Variables not coming from a Record step should be available in Record Picker (#12708)

We want code and webhook variables available in Record Picker since
those can contains uuid.

This PR:
- update `WorkflowVariablesDropdownObjectItems.tsx` so it manages fields
properly
- factorise both dropdown into a commun hook
- update filterOutputSchema.ts so it does not filter fields that are not
FieldMetadata types
- set relation fields as record object in variable schema so those can
be selected as full record

Before


https://github.com/user-attachments/assets/f4f85402-c056-4fd8-8474-d86bef9d4bc3

After


https://github.com/user-attachments/assets/c6589e18-7dfa-4fc8-a525-3a580e265896
This commit is contained in:
Thomas Trompette
2025-06-19 11:33:21 +02:00
committed by GitHub
parent c16b625752
commit 07cf1ed71d
16 changed files with 229 additions and 159 deletions
@@ -2,32 +2,19 @@ import { DropdownMenuHeader } 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 {
BaseOutputSchema,
LinkOutputSchema,
StepOutputSchema,
} from '@/workflow/workflow-variables/types/StepOutputSchema';
import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema';
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
import { isLinkOutputSchema } from '@/workflow/workflow-variables/utils/isLinkOutputSchema';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronLeft,
OverflowingTextWithTooltip,
useIcons,
} from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useVariableDropdown } from '../hooks/useVariableDropdown';
type WorkflowVariablesDropdownFieldItemsProps = {
step: StepOutputSchema;
@@ -40,83 +27,19 @@ export const WorkflowVariablesDropdownFieldItems = ({
onSelect,
onBack,
}: WorkflowVariablesDropdownFieldItemsProps) => {
const [currentPath, setCurrentPath] = useState<string[]>([]);
const [searchInputValue, setSearchInputValue] = useState('');
const { getIcon } = useIcons();
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
workflowSelectedNodeComponentState,
);
const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState,
'workflow-serverless-function-tab-list-component-id',
);
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilComponentStateV2(
workflowDiagramTriggerNodeSelectionComponentState,
);
const getDisplayedSubStepFields = () => {
const currentSubStep = getCurrentSubStepFromPath(step, currentPath);
if (isLinkOutputSchema(currentSubStep)) {
return { link: currentSubStep.link };
} else if (isRecordOutputSchema(currentSubStep)) {
return currentSubStep.fields;
} else if (isBaseOutputSchema(currentSubStep)) {
return currentSubStep;
}
};
const handleSelectField = (key: string) => {
const currentSubStep = getCurrentSubStepFromPath(step, currentPath);
const handleSelectBaseOutputSchema = (
baseOutputSchema: BaseOutputSchema,
) => {
if (!baseOutputSchema[key]?.isLeaf) {
setCurrentPath([...currentPath, key]);
setSearchInputValue('');
} else {
onSelect(`{{${step.id}.${[...currentPath, key].join('.')}}}`);
}
};
const handleSelectLinkOutputSchema = (
linkOutputSchema: LinkOutputSchema,
) => {
setWorkflowSelectedNode(step.id);
setWorkflowDiagramTriggerNodeSelection(step.id);
if (isDefined(linkOutputSchema.link.tab)) {
setActiveTabId(linkOutputSchema.link.tab);
}
};
if (isLinkOutputSchema(currentSubStep)) {
handleSelectLinkOutputSchema(currentSubStep);
} else 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 displayedObject = getDisplayedSubStepFields();
const options = displayedObject ? Object.entries(displayedObject) : [];
const filteredOptions = searchInputValue
? options.filter(
([_, value]) =>
value.label &&
value.label.toLowerCase().includes(searchInputValue.toLowerCase()),
)
: options;
const {
searchInputValue,
setSearchInputValue,
handleSelectField,
goBack,
filteredOptions,
currentPath,
} = useVariableDropdown({
step,
onSelect,
onBack,
});
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
@@ -5,19 +5,19 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath';
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema';
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { useState } from 'react';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { t } from '@lingui/core/macro';
import {
IconChevronLeft,
OverflowingTextWithTooltip,
useIcons,
} from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useVariableDropdown } from '../hooks/useVariableDropdown';
type WorkflowVariablesDropdownObjectItemsProps = {
step: StepOutputSchema;
@@ -30,19 +30,19 @@ export const WorkflowVariablesDropdownObjectItems = ({
onSelect,
onBack,
}: WorkflowVariablesDropdownObjectItemsProps) => {
const [currentPath, setCurrentPath] = useState<string[]>([]);
const [searchInputValue, setSearchInputValue] = useState('');
const { getIcon } = useIcons();
const getDisplayedSubStepFields = () => {
const currentSubStep = getCurrentSubStepFromPath(step, currentPath);
if (isRecordOutputSchema(currentSubStep)) {
return currentSubStep.fields;
} else if (isBaseOutputSchema(currentSubStep)) {
return currentSubStep;
}
};
const {
currentPath,
filteredOptions,
searchInputValue,
setSearchInputValue,
handleSelectField,
goBack,
} = useVariableDropdown({
step,
onSelect,
onBack,
});
const getDisplayedSubStepObject = () => {
const currentSubStep = getCurrentSubStepFromPath(step, currentPath);
@@ -66,19 +66,6 @@ export const WorkflowVariablesDropdownObjectItems = ({
);
};
const handleSelectField = (key: string) => {
setCurrentPath([...currentPath, key]);
setSearchInputValue('');
};
const goBack = () => {
if (currentPath.length === 0) {
onBack();
} else {
setCurrentPath(currentPath.slice(0, -1));
}
};
const displayedSubStepObject = getDisplayedSubStepObject();
const shouldDisplaySubStepObject = searchInputValue
@@ -88,16 +75,9 @@ export const WorkflowVariablesDropdownObjectItems = ({
.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;
const shouldDisplayObject =
shouldDisplaySubStepObject && displayedSubStepObject?.label;
const nameSingular = displayedSubStepObject?.nameSingular;
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
@@ -120,29 +100,36 @@ export const WorkflowVariablesDropdownObjectItems = ({
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{shouldDisplaySubStepObject && displayedSubStepObject?.label && (
{shouldDisplayObject && (
<MenuItemSelect
selected={false}
focused={false}
onClick={handleSelectObject}
text={displayedSubStepObject.label}
text={displayedSubStepObject?.label || ''}
hasSubMenu={false}
LeftIcon={
displayedSubStepObject.icon
? getIcon(displayedSubStepObject.icon)
: undefined
}
contextualText={t`Pick a ${nameSingular} record`}
/>
)}
{filteredOptions.map(([key, value]) => (
{filteredOptions.length > 0 && shouldDisplayObject && (
<DropdownMenuSeparator />
)}
{filteredOptions.map(([key, option]) => (
<MenuItemSelect
key={key}
selected={false}
focused={false}
onClick={() => handleSelectField(key)}
text={value.label || key}
hasSubMenu={!value.isLeaf}
LeftIcon={value.icon ? getIcon(value.icon) : undefined}
text={option.label || key}
hasSubMenu={!option.isLeaf}
LeftIcon={option.icon ? getIcon(option.icon) : undefined}
contextualText={
option.isLeaf ? option?.value?.toString() : undefined
}
/>
))}
</DropdownMenuItemsContainer>