Skip to content

Commit

Permalink
Select full record in variable dropdown (#8851)
Browse files Browse the repository at this point in the history
Output schema is now separated in two sections:
- object, that gather all informations on the selectable object
- fields, that display object fields in a record context, or simply the
available fields from the previous steps

The dropdown variable has now a new mode:
- if objectNameSingularToSelect is defined, it goes into an object mode.
Only objects of the right type will be shown
- if not set, it will use the already existing mode, to select a field

When an object is selected, it actually set the id of the object



https://github.com/user-attachments/assets/1c95f8fd-10f0-4c1c-aeb7-c7d847e89536
  • Loading branch information
thomtrp authored Dec 5, 2024
1 parent 33e6980 commit 36e4357
Show file tree
Hide file tree
Showing 22 changed files with 931 additions and 265 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -39,13 +40,18 @@ export const WorkflowSingleRecordFieldChip = ({
objectNameSingular,
onRemove,
}: WorkflowSingleRecordFieldChipProps) => {
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });

if (
!!draftValue &&
draftValue.type === 'variable' &&
isStandaloneVariableString(draftValue.value)
) {
return (
<VariableChip rawVariableName={draftValue.value} onRemove={onRemove} />
<VariableChip
rawVariableName={objectMetadataItem.labelSingular}
onRemove={onRemove}
/>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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`
Expand Down Expand Up @@ -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;
Expand All @@ -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',
Expand All @@ -93,10 +93,9 @@ export const WorkflowSingleRecordPicker = ({
: {
type: 'static',
value: defaultValue || '',
},
);
};

const { record } = useFindOneRecord({
const { record: selectedRecord } = useFindOneRecord({
objectRecordId:
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
? defaultValue
Expand All @@ -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`;

Expand All @@ -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('');
Expand Down Expand Up @@ -211,6 +190,7 @@ export const WorkflowSingleRecordPicker = ({
inputId={variablesDropdownId}
onVariableSelect={handleVariableTagInsert}
disabled={false}
objectNameSingularToSelect={objectNameSingular}
/>
</StyledSearchVariablesDropdownContainer>
</StyledFormFieldInputRowContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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 (
<SearchVariablesDropdownWorkflowStepItems
dropdownId={dropdownId}
steps={availableVariablesInWorkflowStep}
onSelect={handleStepSelect}
/>
);
}

if (isDefined(objectNameSingularToSelect)) {
return (
<SearchVariablesDropdownObjectItems
step={selectedStep}
onSelect={handleSubItemSelect}
onBack={handleBack}
/>
);
}

return (
<SearchVariablesDropdownFieldItems
step={selectedStep}
onSelect={handleSubItemSelect}
onBack={handleBack}
/>
);
};

if (disabled === true) {
return (
<StyledDropdownVariableButtonContainer
Expand Down Expand Up @@ -97,20 +136,9 @@ const SearchVariablesDropdown = ({
</StyledDropdownVariableButtonContainer>
}
dropdownComponents={
<DropdownMenuItemsContainer hasMaxHeight>
{selectedStep ? (
<SearchVariablesDropdownStepSubItem
step={selectedStep}
onSelect={handleSubItemSelect}
onBack={handleBack}
/>
) : (
<SearchVariablesDropdownStepItem
steps={availableVariablesInWorkflowStep}
onSelect={handleStepSelect}
/>
)}
</DropdownMenuItemsContainer>
<StyledDropdownComponetsContainer>
{renderSearchVariablesDropdownComponents()}
</StyledDropdownComponetsContainer>
}
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 4 }}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
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 (
<DropdownMenuItemsContainer>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={goBack}>
<OverflowingTextWithTooltip text={headerLabel} />
</DropdownMenuHeader>
<HorizontalSeparator
color={theme.background.transparent.primary}
noMargin
/>
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
onChange={(event) => setSearchInputValue(event.target.value)}
/>
<HorizontalSeparator
color={theme.background.transparent.primary}
noMargin
/>
{filteredOptions.map(([key, value]) => (
<MenuItemSelect
key={key}
selected={false}
hovered={false}
onClick={() => handleSelectField(key)}
text={value.label || key}
hasSubMenu={!value.isLeaf}
LeftIcon={value.icon ? getIcon(value.icon) : undefined}
/>
))}
</DropdownMenuItemsContainer>
);
};
Loading

0 comments on commit 36e4357

Please sign in to comment.