Flatten system object pickers (#22161)

## Summary
- Flatten system object entries into the first-level workflow object
pickers.
- Flatten dashboard Source and record-page Field picker advanced/system
entries into the main searchable list.
- Keep regular entries first, place system/advanced entries at the
bottom, and cap page-layout picker height at 340px.
- Add keyboard selection support to workflow object pickers through
`SelectableList`.

## Review notes
- Removed now-unused Advanced submenu state, submenu headers, and
duplicated filtering paths.
- Kept the width behavior scoped to each existing dropdown; the shared
page-layout wrapper only controls height/scrolling.
- No blocking issues found in the final reviewed diff.

## Screenshots

### Workflow record type picker
| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/dc3f7ef933bdca6c39599c45c827db0f27f01e7d/before-workflow-record-type-advanced.png"
width="320" /> | <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/2ee195a8562e13d0f4307e7891ab60cfaecae8cf/after-workflow-record-type-flat.png"
width="320" /> |

### Dashboard Source picker
| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/45f57728ec7643c7048e857829e977d49b9b365c/before-dashboard-source-tall.png"
width="420" /> | <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/f8b864ea5e3d04a00b9b6a613e8bf97dd545933c/after-dashboard-source-340px.png"
width="420" /> |

### Record page Field picker
| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/ea5f14ccac6c0d9b3d953b4047dc7491702f756a/before-record-field-advanced.png"
width="320" /> | <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/9d0717cf3cfcb47ed17d595ba83b19a37718c3eb/after-record-field-flat.png"
width="320" /> |

## Checks
- `npx oxfmt --check` on touched files
- `git diff --check`
- `npx tsc -p tsconfig.json --noEmit --pretty false --noErrorTruncation
| rg
"(ChartDataSourceDropdownContent|FieldWidgetFieldDropdownContent|PageLayoutDropdownContentContainer|WorkflowObjectDropdownContent|WorkflowEditTriggerDatabaseEventForm|WorkflowEditActionFindRecords|WorkflowEditActionPickRecord|ChartSettingItem)"`
returned no touched-file diagnostics
- Browser verification: dashboard Source top/bottom, record-page Field
top/bottom, workflow Record Type top/bottom, and ArrowDown selection in
workflow Record Type menu


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22161?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas des Francs
2026-06-25 14:10:30 +02:00
committed by GitHub
parent db5338cf32
commit 2aeacf341f
9 changed files with 255 additions and 278 deletions
+32
View File
@@ -80,6 +80,38 @@ const processUserData = (
};
```
## Collection Transformations
```typescript
// ✅ Prefer array transformations for simple mapping, filtering, and classification
const activeUsers = users.filter((user) => user.isActive === true);
const userNames = users.map((user) => user.name);
const { payingUsers, nonPayingUsers } = users.reduce<{
payingUsers: User[];
nonPayingUsers: User[];
}>(
(accumulator, user) => {
if (user.isPaying === true) {
accumulator.payingUsers.push(user);
} else {
accumulator.nonPayingUsers.push(user);
}
return accumulator;
},
{ payingUsers: [], nonPayingUsers: [] },
);
// ❌ Avoid manual loops when map, filter, or reduce keeps the code clear
const payingUsers: User[] = [];
for (const user of users) {
if (user.isPaying === true) {
payingUsers.push(user);
}
}
```
## Comments
```typescript
// ✅ Use short-form comments, NOT JSDoc blocks
@@ -190,6 +190,7 @@ export const ChartSettingItem = ({
</DropdownContent>
}
dropdownPlacement="bottom-end"
dropdownOffset={{ y: 4 }}
description={getChartSettingsValues(item.id) as string}
contextualTextPosition={'right'}
hasSubMenu
@@ -6,9 +6,10 @@ import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/
import { useResetChartDraftFiltersSettings } from '@/side-panel/pages/page-layout/hooks/useResetChartDraftFiltersSettings';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import {
StyledPageLayoutDropdownContentContainer,
StyledPageLayoutDropdownMenuItemsContainer,
} from '@/side-panel/pages/page-layout/components/dropdown-content/PageLayoutDropdownContentContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
@@ -19,19 +20,13 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, IconSettings } from 'twenty-ui/icon';
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
const ADVANCED_OBJECTS_MENU_ITEM_ID = 'advanced-objects';
export const ChartDataSourceDropdownContent = () => {
const [searchQuery, setSearchQuery] = useState('');
const [isAdvancedObjectsMenuOpened, setIsAdvancedObjectsMenuOpened] =
useState(false);
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
@@ -51,21 +46,25 @@ export const ChartDataSourceDropdownContent = () => {
dropdownId,
);
const objectsWithReadAccess = filterReadableActiveObjectMetadataItems(
objectMetadataItems,
objectPermissionsByObjectMetadataId,
);
const searchableObjects = useMemo(() => {
const objectsWithReadAccess = filterReadableActiveObjectMetadataItems(
objectMetadataItems,
objectPermissionsByObjectMetadataId,
);
const regularObjects = objectsWithReadAccess
.filter((item) => !item.isSystem)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const regularObjects = objectsWithReadAccess
.filter((item) => item.isSystem === false)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const systemObjects = objectsWithReadAccess
.filter((item) => item.isSystem)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const systemObjects = objectsWithReadAccess
.filter((item) => item.isSystem === true)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
return [...regularObjects, ...systemObjects];
}, [objectMetadataItems, objectPermissionsByObjectMetadataId]);
const availableObjectMetadataItems = filterBySearchQuery({
items: isAdvancedObjectsMenuOpened ? systemObjects : regularObjects,
items: searchableObjects,
searchQuery,
getSearchableValues: (item) => [item.labelPlural, item.namePlural],
});
@@ -104,30 +103,8 @@ export const ChartDataSourceDropdownContent = () => {
closeDropdown();
};
const handleAdvancedObjectsClick = () => {
setIsAdvancedObjectsMenuOpened(true);
setSearchQuery('');
};
const handleBack = () => {
setIsAdvancedObjectsMenuOpened(false);
setSearchQuery('');
};
return (
<>
{isAdvancedObjectsMenuOpened && (
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={handleBack}
Icon={IconChevronLeft}
/>
}
>
<Trans>Advanced objects</Trans>
</DropdownMenuHeader>
)}
<StyledPageLayoutDropdownContentContainer>
<DropdownMenuSearchInput
autoFocus
type="text"
@@ -136,18 +113,13 @@ export const ChartDataSourceDropdownContent = () => {
value={searchQuery}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<StyledPageLayoutDropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={[
...availableObjectMetadataItems.map(
(objectMetadataItem) => objectMetadataItem.id,
),
...(!isAdvancedObjectsMenuOpened
? [ADVANCED_OBJECTS_MENU_ITEM_ID]
: []),
]}
selectableItemIdArray={availableObjectMetadataItems.map(
(objectMetadataItem) => objectMetadataItem.id,
)}
>
{availableObjectMetadataItems.map((objectMetadataItem) => (
<SelectableListItem
@@ -170,21 +142,8 @@ export const ChartDataSourceDropdownContent = () => {
/>
</SelectableListItem>
))}
{!isAdvancedObjectsMenuOpened && (
<SelectableListItem
itemId={ADVANCED_OBJECTS_MENU_ITEM_ID}
onEnter={handleAdvancedObjectsClick}
>
<MenuItem
text={t`Advanced objects`}
LeftIcon={IconSettings}
onClick={handleAdvancedObjectsClick}
hasSubMenu
/>
</SelectableListItem>
)}
</SelectableList>
</DropdownMenuItemsContainer>
</>
</StyledPageLayoutDropdownMenuItemsContainer>
</StyledPageLayoutDropdownContentContainer>
);
};
@@ -11,9 +11,10 @@ import {
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { DropdownAdvancedSectionHeader } from '@/ui/layout/dropdown/components/DropdownAdvancedSectionHeader';
import { DropdownAdvancedSectionMenuItem } from '@/ui/layout/dropdown/components/DropdownAdvancedSectionMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import {
StyledPageLayoutDropdownContentContainer,
StyledPageLayoutDropdownMenuItemsContainer,
} from '@/side-panel/pages/page-layout/components/dropdown-content/PageLayoutDropdownContentContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
@@ -24,7 +25,6 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useMemo, useState } from 'react';
import { useIcons } from 'twenty-ui/icon';
import { MenuItemSelect } from 'twenty-ui/navigation';
@@ -33,7 +33,6 @@ import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
export const FieldWidgetFieldDropdownContent = () => {
const [searchQuery, setSearchQuery] = useState('');
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
const { pageLayoutId, objectNameSingular } =
usePageLayoutIdFromContextStore();
@@ -51,25 +50,30 @@ export const FieldWidgetFieldDropdownContent = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const advancedFieldMetadataItems = useMemo(
const { advancedFieldMetadataItems, regularFieldMetadataItems } = useMemo(
() =>
allFieldWidgetFieldMetadataItems.filter((fieldMetadataItem) =>
isAdvancedRelationFieldMetadataItem(
fieldMetadataItem,
objectMetadataItems,
),
),
[allFieldWidgetFieldMetadataItems, objectMetadataItems],
);
const regularFieldMetadataItems = useMemo(
() =>
allFieldWidgetFieldMetadataItems.filter(
(fieldMetadataItem) =>
!isAdvancedRelationFieldMetadataItem(
allFieldWidgetFieldMetadataItems.reduce<{
advancedFieldMetadataItems: typeof allFieldWidgetFieldMetadataItems;
regularFieldMetadataItems: typeof allFieldWidgetFieldMetadataItems;
}>(
(accumulator, fieldMetadataItem) => {
const isAdvancedField = isAdvancedRelationFieldMetadataItem(
fieldMetadataItem,
objectMetadataItems,
),
);
if (isAdvancedField) {
accumulator.advancedFieldMetadataItems.push(fieldMetadataItem);
} else {
accumulator.regularFieldMetadataItems.push(fieldMetadataItem);
}
return accumulator;
},
{
advancedFieldMetadataItems: [],
regularFieldMetadataItems: [],
},
),
[allFieldWidgetFieldMetadataItems, objectMetadataItems],
);
@@ -92,30 +96,17 @@ export const FieldWidgetFieldDropdownContent = () => {
const { getIcon } = useIcons();
const searchableFieldMetadataItems = [
...regularFieldMetadataItems,
...advancedFieldMetadataItems,
];
const availableFields = filterBySearchQuery({
items: isAdvancedOpen
? advancedFieldMetadataItems
: regularFieldMetadataItems,
items: searchableFieldMetadataItems,
searchQuery,
getSearchableValues: (item) => [item.label],
});
const shouldShowAdvanced =
!isAdvancedOpen &&
advancedFieldMetadataItems.length > 0 &&
(!isNonEmptyString(searchQuery) ||
searchQuery.toLowerCase().includes('advanced'));
const handleOpenAdvanced = () => {
setIsAdvancedOpen(true);
setSearchQuery('');
};
const handleBackFromAdvanced = () => {
setIsAdvancedOpen(false);
setSearchQuery('');
};
const { fieldMetadataItem: currentFieldMetadataItem } =
useFieldMetadataItemById(currentFieldMetadataId ?? '');
@@ -146,7 +137,7 @@ export const FieldWidgetFieldDropdownContent = () => {
},
});
if (widgetInEditMode && selectedField) {
if (isDefined(widgetInEditMode) && isDefined(selectedField)) {
updatePageLayoutWidget(widgetInEditMode.id, {
title: selectedField.label,
});
@@ -156,10 +147,7 @@ export const FieldWidgetFieldDropdownContent = () => {
};
return (
<>
{isAdvancedOpen && (
<DropdownAdvancedSectionHeader onBack={handleBackFromAdvanced} />
)}
<StyledPageLayoutDropdownContentContainer>
<DropdownMenuSearchInput
autoFocus
type="text"
@@ -168,7 +156,7 @@ export const FieldWidgetFieldDropdownContent = () => {
value={searchQuery}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<StyledPageLayoutDropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
@@ -198,10 +186,7 @@ export const FieldWidgetFieldDropdownContent = () => {
</SelectableListItem>
))}
</SelectableList>
{shouldShowAdvanced && (
<DropdownAdvancedSectionMenuItem onClick={handleOpenAdvanced} />
)}
</DropdownMenuItemsContainer>
</>
</StyledPageLayoutDropdownMenuItemsContainer>
</StyledPageLayoutDropdownContentContainer>
);
};
@@ -0,0 +1,19 @@
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { styled } from '@linaria/react';
const PAGE_LAYOUT_DROPDOWN_CONTENT_MAX_HEIGHT = 340;
export const StyledPageLayoutDropdownContentContainer = styled.div`
display: flex;
flex-direction: column;
max-height: ${PAGE_LAYOUT_DROPDOWN_CONTENT_MAX_HEIGHT}px;
min-height: 0;
overflow: hidden;
`;
export const StyledPageLayoutDropdownMenuItemsContainer = styled(
DropdownMenuItemsContainer,
)`
flex: 1;
min-height: 0;
`;
@@ -204,6 +204,7 @@ export const WorkflowEditActionFindRecords = ({
dropdownComponents={
!isFormDisabled && (
<WorkflowObjectDropdownContent
dropdownId={dropdownId}
onOptionClick={handleOptionClick}
/>
)
@@ -1,36 +1,38 @@
import { ObjectMetadataIcon } from '@/object-metadata/components/ObjectMetadataIcon';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { DropdownAdvancedSectionHeader } from '@/ui/layout/dropdown/components/DropdownAdvancedSectionHeader';
import { DropdownAdvancedSectionMenuItem } from '@/ui/layout/dropdown/components/DropdownAdvancedSectionMenuItem';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { isNonEmptyString } from '@sniptt/guards';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useState } from 'react';
import { MenuItem } from 'twenty-ui/navigation';
type WorkflowObjectDropdownContentProps = {
dropdownId: string;
onOptionClick: (value: string) => void;
showAdvancedOption?: boolean;
};
export const WorkflowObjectDropdownContent = ({
dropdownId,
onOptionClick,
showAdvancedOption = true,
}: WorkflowObjectDropdownContentProps) => {
const [searchInputValue, setSearchInputValue] = useState('');
const [isSystemObjectsOpen, setIsSystemObjectsOpen] = useState(false);
const { objectMetadataItems } = useFilteredObjectMetadataItems();
const nonSystemObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
objectMetadataItem.isActive === true &&
objectMetadataItem.isSystem === false,
);
const systemObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectMetadataItem.isActive && objectMetadataItem.isSystem,
objectMetadataItem.isActive === true &&
objectMetadataItem.isSystem === true,
);
const matchesSearchFilter = (
@@ -52,12 +54,6 @@ export const WorkflowObjectDropdownContent = ({
const searchInputLowerCase = searchInputValue.toLowerCase();
const shouldShowAdvanced =
showAdvancedOption &&
!isSystemObjectsOpen &&
(!isNonEmptyString(searchInputValue) ||
searchInputLowerCase.includes('advanced'));
const filteredNonSystemObjects = nonSystemObjectMetadataItems.filter(
(objectMetadataItem) =>
matchesSearchFilter(objectMetadataItem, searchInputLowerCase),
@@ -68,19 +64,19 @@ export const WorkflowObjectDropdownContent = ({
matchesSearchFilter(objectMetadataItem, searchInputLowerCase),
);
const filteredObjects = isSystemObjectsOpen
? filteredSystemObjects
: filteredNonSystemObjects;
const filteredObjects = [
...filteredNonSystemObjects,
...filteredSystemObjects,
];
const handleSystemObjectsClick = () => {
setIsSystemObjectsOpen(true);
setSearchInputValue('');
};
const selectableItemIdArray = filteredObjects.map(
(objectMetadataItem) => objectMetadataItem.nameSingular,
);
const handleBack = () => {
setIsSystemObjectsOpen(false);
setSearchInputValue('');
};
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
dropdownId,
);
const handleSearchInputChange = (
event: React.ChangeEvent<HTMLInputElement>,
@@ -88,17 +84,8 @@ export const WorkflowObjectDropdownContent = ({
setSearchInputValue(event.target.value);
};
const handleAdvancedClick = () => {
if (!isSystemObjectsOpen) {
handleSystemObjectsClick();
}
};
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
{isSystemObjectsOpen && (
<DropdownAdvancedSectionHeader onBack={handleBack} />
)}
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
@@ -106,19 +93,28 @@ export const WorkflowObjectDropdownContent = ({
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredObjects.map((objectMetadataItem) => (
<MenuItem
key={objectMetadataItem.nameSingular}
LeftIcon={() => (
<ObjectMetadataIcon objectMetadataItem={objectMetadataItem} />
)}
text={objectMetadataItem.labelPlural}
onClick={() => onOptionClick(objectMetadataItem.nameSingular)}
/>
))}
{shouldShowAdvanced && (
<DropdownAdvancedSectionMenuItem onClick={handleAdvancedClick} />
)}
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={selectableItemIdArray}
>
{filteredObjects.map((objectMetadataItem) => (
<SelectableListItem
key={objectMetadataItem.nameSingular}
itemId={objectMetadataItem.nameSingular}
onEnter={() => onOptionClick(objectMetadataItem.nameSingular)}
>
<MenuItem
focused={selectedItemId === objectMetadataItem.nameSingular}
LeftIcon={() => (
<ObjectMetadataIcon objectMetadataItem={objectMetadataItem} />
)}
text={objectMetadataItem.labelPlural}
onClick={() => onOptionClick(objectMetadataItem.nameSingular)}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownContent>
);
@@ -259,6 +259,7 @@ export const WorkflowEditActionPickRecord = ({
dropdownComponents={
!isFormDisabled && (
<WorkflowObjectDropdownContent
dropdownId={dropdownId}
onOptionClick={handleObjectChange}
/>
)
@@ -293,6 +294,7 @@ export const WorkflowEditActionPickRecord = ({
dropdownComponents={
!isFormDisabled && (
<WorkflowObjectDropdownContent
dropdownId={loadBalanceObjectDropdownId}
onOptionClick={handleLoadBalanceObjectChange}
/>
)
@@ -4,13 +4,15 @@ import { type FieldMultiSelectValue } from '@/object-record/record-field/ui/type
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
import { type WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
@@ -19,11 +21,11 @@ import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/Workflo
import { WorkflowStepFilterBuilder } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterBuilder';
import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
import { useCallback, useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { IconChevronLeft, IconSettings } from 'twenty-ui/icon';
import { type SelectOption } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -39,13 +41,18 @@ const StyledRecordTypeSelectContainer = styled.div<{ fullWidth?: boolean }>`
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
`;
const filterOptionsBySearch = <T extends { label: string }>(
const filterOptionsBySearch = <T extends { label: string; value: string }>(
options: T[],
searchValue: string,
): T[] => {
if (!searchValue) return options;
if (searchValue === '') return options;
const searchValueLowerCase = searchValue.toLowerCase();
return options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase()),
[option.label, option.value].some((searchableValue) =>
searchableValue.toLowerCase().includes(searchValueLowerCase),
),
);
};
@@ -70,7 +77,6 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
const { getSelectIconPropsFromObjectMetadataItem } =
useObjectMetadataSelectHelpers();
const [searchInputValue, setSearchInputValue] = useState('');
const [isSystemObjectsOpen, setIsSystemObjectsOpen] = useState(false);
const dropdownId = 'workflow-edit-trigger-record-type';
const { closeDropdown } = useCloseDropdown();
@@ -84,29 +90,46 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
const isUpsertEvent = triggerEvent.event === 'upserted';
const isFieldFilteringSupported = isUpdateEvent || isUpsertEvent;
const defaultSelectedOption = useMemo(
const defaultSelectedOption = useMemo<SelectOption<string>>(
() => ({ label: t`Select an option`, value: '' }),
[t],
);
const regularObjects = objectMetadataItems
.filter((item) => item.isActive && !item.isSystem)
.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
...getSelectIconPropsFromObjectMetadataItem(item),
}));
const { regularObjects, systemObjects } = useMemo(() => {
return objectMetadataItems.reduce<{
regularObjects: SelectOption<string>[];
systemObjects: SelectOption<string>[];
}>(
(accumulator, item) => {
if (item.isActive === false) {
return accumulator;
}
const systemObjects = objectMetadataItems
.filter((item) => item.isActive && item.isSystem)
.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
...getSelectIconPropsFromObjectMetadataItem(item),
}));
const option = {
label: item.labelPlural,
value: item.nameSingular,
...getSelectIconPropsFromObjectMetadataItem(item),
};
if (item.isSystem === true) {
accumulator.systemObjects.push(option);
} else {
accumulator.regularObjects.push(option);
}
return accumulator;
},
{ regularObjects: [], systemObjects: [] },
);
}, [getSelectIconPropsFromObjectMetadataItem, objectMetadataItems]);
const selectableOptions = useMemo(
() => [...regularObjects, ...systemObjects],
[regularObjects, systemObjects],
);
const selectedOption =
[...regularObjects, ...systemObjects].find(
selectableOptions.find(
(option) => option.value === triggerEvent?.objectType,
) || defaultSelectedOption;
@@ -114,14 +137,19 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
(item) => item.nameSingular === selectedOption.value,
);
const filteredRegularObjects = useMemo(
() => filterOptionsBySearch(regularObjects, searchInputValue),
[regularObjects, searchInputValue],
const filteredObjects = useMemo(
() => [
...filterOptionsBySearch(regularObjects, searchInputValue),
...filterOptionsBySearch(systemObjects, searchInputValue),
],
[regularObjects, searchInputValue, systemObjects],
);
const filteredSystemObjects = useMemo(
() => filterOptionsBySearch(systemObjects, searchInputValue),
[systemObjects, searchInputValue],
const selectableItemIdArray = filteredObjects.map((option) => option.value);
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
dropdownId,
);
const handleOptionClick = (value: string) => {
@@ -174,30 +202,13 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
});
};
const handleSystemObjectsClick = () => {
setIsSystemObjectsOpen(true);
setSearchInputValue('');
};
const handleBack = () => {
setIsSystemObjectsOpen(false);
setSearchInputValue('');
};
const handleSearchInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setSearchInputValue(event.target.value);
},
[],
);
return (
<>
<WorkflowStepBody>
<StyledRecordTypeSelectContainer fullWidth>
<StyledLabel>{t`Record Type`}</StyledLabel>
<Dropdown
dropdownId="workflow-edit-trigger-record-type"
dropdownId={dropdownId}
dropdownPlacement="bottom-start"
clickableComponent={
<SelectControl
@@ -207,71 +218,42 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
}
dropdownComponents={
<>
{!triggerOptions.readonly &&
(isSystemObjectsOpen ? (
<DropdownContent
widthInPixels={GenericDropdownContentWidth.ExtraLarge}
>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={handleBack}
Icon={IconChevronLeft}
/>
}
{!triggerOptions.readonly && (
<DropdownContent
widthInPixels={GenericDropdownContentWidth.ExtraLarge}
>
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
onChange={(event) =>
setSearchInputValue(event.target.value)
}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={selectableItemIdArray}
>
<Trans>Advanced</Trans>
</DropdownMenuHeader>
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
onChange={handleSearchInputChange}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredSystemObjects.map((option) => (
<MenuItem
{filteredObjects.map((option) => (
<SelectableListItem
key={option.value}
LeftIcon={option.Icon}
text={option.label}
onClick={() => handleOptionClick(option.value)}
/>
itemId={option.value}
onEnter={() => handleOptionClick(option.value)}
>
<MenuItem
focused={selectedItemId === option.value}
LeftIcon={option.Icon}
text={option.label}
onClick={() => handleOptionClick(option.value)}
/>
</SelectableListItem>
))}
</DropdownMenuItemsContainer>
</DropdownContent>
) : (
<DropdownContent
widthInPixels={GenericDropdownContentWidth.ExtraLarge}
>
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
onChange={handleSearchInputChange}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredRegularObjects.map((option) => (
<MenuItem
key={option.value}
LeftIcon={option.Icon}
text={option.label}
onClick={() => handleOptionClick(option.value)}
/>
))}
{(!searchInputValue ||
'advanced'.includes(
searchInputValue.toLowerCase(),
)) && (
<MenuItem
text={t`Advanced`}
LeftIcon={IconSettings}
onClick={handleSystemObjectsClick}
hasSubMenu
/>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownContent>
)}
</>
}
dropdownOffset={{ y: 4 }}