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:
committed by
GitHub
parent
db5338cf32
commit
2aeacf341f
@@ -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
|
||||
|
||||
+1
@@ -190,6 +190,7 @@ export const ChartSettingItem = ({
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ y: 4 }}
|
||||
description={getChartSettingsValues(item.id) as string}
|
||||
contextualTextPosition={'right'}
|
||||
hasSubMenu
|
||||
|
||||
+28
-69
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+36
-51
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+19
@@ -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;
|
||||
`;
|
||||
+1
@@ -204,6 +204,7 @@ export const WorkflowEditActionFindRecords = ({
|
||||
dropdownComponents={
|
||||
!isFormDisabled && (
|
||||
<WorkflowObjectDropdownContent
|
||||
dropdownId={dropdownId}
|
||||
onOptionClick={handleOptionClick}
|
||||
/>
|
||||
)
|
||||
|
||||
+43
-47
@@ -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>
|
||||
);
|
||||
|
||||
+2
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
+93
-111
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user