From 1589b9b91267db6a40eb4d8b535fe01e807dddac Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Wed, 24 Jun 2026 14:05:23 +0200 Subject: [PATCH] Add search to new sidebar item picker (#22041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add search to the custom layout “New menu item” side panel. - Group search results by Objects, Views, and Records. - Reuse the existing record search behavior through a shared hook and preserve add-to-navigation drag/select flows. ## Video - Recording: https://gist.githubusercontent.com/Bonapara/c78107650efd94b580e38426b9fc2dbd/raw/755c87fab253281a9c68e5a24cbfdff6c9248af1/search-nav-item-custom-layout.webm ## Verification - Browser plugin: opened layout customization, clicked `Add menu item`, searched `o`, and verified `Objects`, `Views`, and `Records` result groups with object/view/record results. - `npx oxlint --type-aware -c packages/twenty-front/.oxlintrc.json packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage.tsx packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemSearchResults.tsx packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords.ts` - `npx nx typecheck twenty-front` - `npx nx lint twenty-front` Review in cubic --- .../SidePanelNewSidebarItemPage.tsx | 50 +++- .../SidePanelNewSidebarItemRecordSubPage.tsx | 86 ++---- .../SidePanelNewSidebarItemSearchResults.tsx | 265 ++++++++++++++++++ ...vailableNavigationMenuItemSearchRecords.ts | 76 +++++ .../hooks/useSearchableObjectNameSingulars.ts | 33 +++ .../hooks/useSidePanelSearchRecords.tsx | 76 ++--- .../SettingsRestPlayground.stories.tsx | 5 + 7 files changed, 460 insertions(+), 131 deletions(-) create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemSearchResults.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords.ts create mode 100644 packages/twenty-front/src/modules/side-panel/hooks/useSearchableObjectNameSingulars.ts diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage.tsx index affae8cfee..751277645a 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage.tsx @@ -1,23 +1,47 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; + import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory'; import { SidePanelNewSidebarItemMainMenu } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemMainMenu'; +import { SidePanelNewSidebarItemSearchResults } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemSearchResults'; +import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages'; export const SidePanelNewSidebarItemPage = () => { + const { t } = useLingui(); const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory(); + const [searchValue, setSearchValue] = useState(''); + const trimmedSearchValue = searchValue.trim(); + const hasSearchQuery = isNonEmptyString(trimmedSearchValue); return ( - - navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemRecord) - } - onSelectObject={() => - navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemObjectPicker) - } - onSelectView={() => - navigateToSidePanelSubPage( - SidePanelSubPages.NewSidebarItemViewObjectPicker, - ) - } - /> + + {hasSearchQuery ? ( + + ) : ( + + navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemRecord) + } + onSelectObject={() => + navigateToSidePanelSubPage( + SidePanelSubPages.NewSidebarItemObjectPicker, + ) + } + onSelectView={() => + navigateToSidePanelSubPage( + SidePanelSubPages.NewSidebarItemViewObjectPicker, + ) + } + /> + )} + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx index 512e0699f4..df117b4207 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx @@ -1,90 +1,40 @@ +import { isNonEmptyString } from '@sniptt/guards'; import { useLingui } from '@lingui/react/macro'; -import { useMemo, useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { useDebounce } from 'use-debounce'; +import { useState } from 'react'; -import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults'; -import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems'; +import { useAvailableNavigationMenuItemSearchRecords } from '@/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords'; import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; import { SidePanelObjectFilterDropdown } from '@/side-panel/components/SidePanelObjectFilterDropdown'; -import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState'; import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { SidePanelNewSidebarItemRecordItem } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordItem'; -import { useQuery } from '@apollo/client/react'; -import { SearchDocument } from '~/generated/graphql'; - -type SearchRecordBase = { - recordId: string; - objectNameSingular: string; - label: string; - imageUrl?: string | null; -}; export const SidePanelNewSidebarItemRecordSubPage = () => { const { t } = useLingui(); - const { currentItems } = useNavigationMenuItemEditController(); const [recordSearchInput, setRecordSearchInput] = useState(''); - const [deferredRecordSearchInput] = useDebounce(recordSearchInput, 300); - const coreClient = useApolloCoreClient(); - const { readableObjectMetadataItems } = useReadableObjectMetadataItems(); const [selectedObjectNameSingular, setSelectedObjectNameSingular] = useState< string | null >(null); - const sidePanelShowHiddenObjects = useAtomStateValue( - sidePanelShowHiddenObjectsState, - ); - - const includedObjectNameSingulars = useMemo(() => { - if (isDefined(selectedObjectNameSingular)) { - return [selectedObjectNameSingular]; - } - - return readableObjectMetadataItems - .filter((item) => sidePanelShowHiddenObjects || item.isSearchable) - .map((item) => item.nameSingular); - }, [ - readableObjectMetadataItems, + const { + availableSearchRecords, + isSearchDebouncing, + recordSearchLoading, + trimmedSearchInput, + } = useAvailableNavigationMenuItemSearchRecords({ + searchInput: recordSearchInput, selectedObjectNameSingular, - sidePanelShowHiddenObjects, - ]); + skip: !isNonEmptyString(recordSearchInput.trim()), + }); - const { data: searchData, loading: recordSearchLoading } = useQuery( - SearchDocument, - { - client: coreClient, - variables: { - searchInput: deferredRecordSearchInput ?? '', - limit: MAX_SEARCH_RESULTS, - includedObjectNameSingulars, - }, - }, - ); - - const recordIdsAlreadyAdded = new Set( - currentItems.flatMap((item) => - isDefined(item.targetRecordId) ? [item.targetRecordId] : [], - ), - ); - - const searchRecords = - searchData?.search?.edges?.map((edge) => edge.node) ?? []; - const availableSearchRecords = searchRecords.filter( - (record) => !recordIdsAlreadyAdded.has(record.recordId), - ) as SearchRecordBase[]; - - const isEmpty = availableSearchRecords.length === 0 && !recordSearchLoading; + const isRecordSearchLoading = recordSearchLoading || isSearchDebouncing; + const isEmpty = availableSearchRecords.length === 0 && !isRecordSearchLoading; const selectableItemIds = isEmpty ? [] : availableSearchRecords.map((record) => record.recordId); - const noResultsText = - deferredRecordSearchInput.length > 0 - ? t`No results found` - : t`Type to search records`; + const noResultsText = isNonEmptyString(trimmedSearchInput) + ? t`No results found` + : t`Type to search records`; return ( { {({ innerRef, droppableProps, placeholder }) => ( diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemSearchResults.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemSearchResults.tsx new file mode 100644 index 0000000000..586077ad95 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemSearchResults.tsx @@ -0,0 +1,265 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { useLingui } from '@lingui/react/macro'; +import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/icon'; + +import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; +import { SidePanelNewSidebarItemRecordItem } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordItem'; +import { SidePanelObjectMenuItem } from '@/navigation-menu-item/edit/side-panel/components/SidePanelObjectMenuItem'; +import { useAvailableNavigationMenuItemSearchRecords } from '@/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords'; +import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem'; +import { isViewDisplayableInNavigationMenu } from '@/navigation-menu-item/edit/side-panel/utils/isViewDisplayableInNavigationMenu'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; +import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; +import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; +import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; +import { SidePanelList } from '@/side-panel/components/SidePanelList'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { type View } from '@/views/types/View'; +import { ViewKey } from '@/views/types/ViewKey'; +import { filterBySearchQuery } from '~/utils/filterBySearchQuery'; + +type SidePanelNewSidebarItemSearchResultsProps = { + searchValue: string; +}; + +export const SidePanelNewSidebarItemSearchResults = ({ + searchValue, +}: SidePanelNewSidebarItemSearchResultsProps) => { + const { t } = useLingui(); + const { getIcon } = useIcons(); + const { currentItems, createItem } = useNavigationMenuItemEditController(); + const [ + pendingInsertionNavigationMenuItem, + setPendingInsertionNavigationMenuItem, + ] = useAtomState(pendingInsertionNavigationMenuItemState); + const { openNavigationMenuItemInSidePanel } = + useOpenNavigationMenuItemInSidePanel(); + const { objectMetadataItems } = useObjectMetadataItems(); + const { activeNonSystemObjectMetadataItems } = + useFilteredObjectMetadataItems(); + const { views, objectMetadataIdsWithIndexView, viewIdsAlreadyAdded } = + useNavigationMenuObjectMetadataForSection(currentItems); + const trimmedSearchValue = searchValue.trim(); + const { availableSearchRecords, isSearchDebouncing, recordSearchLoading } = + useAvailableNavigationMenuItemSearchRecords({ + searchInput: trimmedSearchValue, + skip: !isNonEmptyString(trimmedSearchValue), + }); + const isRecordSearchLoading = recordSearchLoading || isSearchDebouncing; + + const availableViews = views + .filter((view) => view.key !== ViewKey.INDEX) + .filter((view) => !viewIdsAlreadyAdded.has(view.id)) + .filter(isViewDisplayableInNavigationMenu); + + const objectMetadataIdsWithDisplayableViews = new Set( + availableViews.map((view) => view.objectMetadataId), + ); + + const { + availableObjectMetadataItems, + availableSystemObjectMetadataItems, + objectMetadataItemsWithViews, + availableSystemObjectMetadataItemsForView, + } = getAvailableObjectMetadataForNewSidebarItem({ + objectMetadataItems, + activeNonSystemObjectMetadataItems, + objectMetadataIdsWithIndexView, + objectMetadataIdsWithDisplayableViews, + }); + + const searchableObjectMetadataItems = [ + ...availableObjectMetadataItems, + ...availableSystemObjectMetadataItems, + ].toSorted((a, b) => a.labelPlural.localeCompare(b.labelPlural)); + + const searchableObjectMetadataItemsWithViews = [ + ...objectMetadataItemsWithViews, + ...availableSystemObjectMetadataItemsForView, + ]; + + const filteredObjectMetadataItems = filterBySearchQuery({ + items: searchableObjectMetadataItems, + searchQuery: trimmedSearchValue, + getSearchableValues: (item) => [ + item.labelPlural, + item.labelSingular, + item.namePlural, + item.nameSingular, + ], + }); + + const objectMetadataItemIdsWithViews = new Set( + searchableObjectMetadataItemsWithViews.map((item) => item.id), + ); + + const filteredViews = filterBySearchQuery({ + items: availableViews + .filter((view) => + objectMetadataItemIdsWithViews.has(view.objectMetadataId), + ) + .sort((viewA, viewB) => viewA.name.localeCompare(viewB.name)), + searchQuery: trimmedSearchValue, + getSearchableValues: (view) => [view.name], + }); + + const selectableItemIds = [ + ...filteredObjectMetadataItems.map((item) => item.id), + ...filteredViews.map((view) => view.id), + ...availableSearchRecords.map((record) => record.recordId), + ]; + + const isEmpty = + filteredObjectMetadataItems.length === 0 && + filteredViews.length === 0 && + availableSearchRecords.length === 0 && + !isRecordSearchLoading; + + const handleSelectObject = ( + objectMetadataItem: EnrichedObjectMetadataItem, + ) => { + const itemId = createItem( + { + type: NavigationMenuItemType.OBJECT, + targetObjectMetadataId: objectMetadataItem.id, + color: getObjectColorWithFallback(objectMetadataItem), + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId, + targetIndex: pendingInsertionNavigationMenuItem?.position, + }, + ); + + setPendingInsertionNavigationMenuItem(null); + openNavigationMenuItemInSidePanel({ + itemId, + pageTitle: objectMetadataItem.labelSingular, + pageIcon: getIcon(objectMetadataItem.icon), + }); + }; + + const handleSelectView = (view: View) => { + const objectMetadataItem = objectMetadataItems.find( + (item) => item.id === view.objectMetadataId, + ); + const viewIconColor = isDefined(objectMetadataItem) + ? getObjectColorWithFallback(objectMetadataItem) + : undefined; + + const itemId = createItem( + { + type: NavigationMenuItemType.VIEW, + viewId: view.id, + color: viewIconColor, + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null, + targetIndex: pendingInsertionNavigationMenuItem?.position, + }, + ); + + setPendingInsertionNavigationMenuItem(null); + openNavigationMenuItemInSidePanel({ + itemId, + pageTitle: view.name, + pageIcon: getIcon(view.icon), + }); + }; + + const getObjectMetadataItemForView = (view: View) => + objectMetadataItems.find((item) => item.id === view.objectMetadataId); + + return ( + + {({ innerRef, droppableProps, placeholder }) => ( + + {/* oxlint-disable-next-line react/jsx-props-no-spreading */} +
+ + {filteredObjectMetadataItems.map((objectMetadataItem, index) => ( + + ))} + + + {filteredViews.map((view, index) => { + const objectMetadataItem = getObjectMetadataItemForView(view); + const objectIconColor = isDefined(objectMetadataItem) + ? getObjectColorWithFallback(objectMetadataItem) + : undefined; + + return ( + handleSelectView(view)} + > + + ) : undefined + } + icon={ + isDefined(objectMetadataItem) + ? undefined + : getIcon(view.icon) + } + label={view.name} + id={view.id} + onClick={() => handleSelectView(view)} + dragIndex={filteredObjectMetadataItems.length + index} + payload={{ + type: NavigationMenuItemType.VIEW, + viewId: view.id, + label: view.name, + }} + /> + + ); + })} + + + {availableSearchRecords.map((record, index) => ( + + ))} + + {placeholder} +
+
+ )} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords.ts new file mode 100644 index 0000000000..528f5d6480 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAvailableNavigationMenuItemSearchRecords.ts @@ -0,0 +1,76 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { useMemo } from 'react'; +import { useDebounce } from 'use-debounce'; + +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords'; +import { useSearchableObjectNameSingulars } from '@/side-panel/hooks/useSearchableObjectNameSingulars'; +import { isDefined } from 'twenty-shared/utils'; + +export type NavigationMenuItemSearchRecord = { + recordId: string; + objectNameSingular: string; + label: string; + imageUrl?: string | null; +}; + +type UseAvailableNavigationMenuItemSearchRecordsParams = { + searchInput: string; + selectedObjectNameSingular?: string | null; + skip?: boolean; +}; + +export const useAvailableNavigationMenuItemSearchRecords = ({ + searchInput, + selectedObjectNameSingular = null, + skip = false, +}: UseAvailableNavigationMenuItemSearchRecordsParams) => { + const { currentItems } = useNavigationMenuItemEditController(); + const trimmedSearchInput = searchInput.trim(); + + const [deferredSearchInput] = useDebounce(trimmedSearchInput, 300); + + const includedObjectNameSingulars = useSearchableObjectNameSingulars({ + selectedObjectNameSingular, + }); + + const { loading: recordSearchLoading, searchRecords } = + useObjectRecordSearchRecords({ + objectNameSingulars: includedObjectNameSingulars, + searchInput: deferredSearchInput, + skip: skip || !isNonEmptyString(deferredSearchInput), + }); + + const recordIdsAlreadyAdded = useMemo( + () => + new Set( + currentItems.flatMap((item) => + isDefined(item.targetRecordId) ? [item.targetRecordId] : [], + ), + ), + [currentItems], + ); + + const availableSearchRecords = useMemo( + () => + searchRecords + .filter((record) => !recordIdsAlreadyAdded.has(record.recordId)) + .map( + (record): NavigationMenuItemSearchRecord => ({ + recordId: record.recordId, + objectNameSingular: record.objectNameSingular, + label: record.label, + imageUrl: record.imageUrl, + }), + ), + [recordIdsAlreadyAdded, searchRecords], + ); + + return { + availableSearchRecords, + deferredSearchInput, + isSearchDebouncing: trimmedSearchInput !== deferredSearchInput, + recordSearchLoading, + trimmedSearchInput, + }; +}; diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useSearchableObjectNameSingulars.ts b/packages/twenty-front/src/modules/side-panel/hooks/useSearchableObjectNameSingulars.ts new file mode 100644 index 0000000000..827fd36e20 --- /dev/null +++ b/packages/twenty-front/src/modules/side-panel/hooks/useSearchableObjectNameSingulars.ts @@ -0,0 +1,33 @@ +import { useMemo } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems'; +import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; + +type UseSearchableObjectNameSingularsParams = { + selectedObjectNameSingular?: string | null; +}; + +export const useSearchableObjectNameSingulars = ({ + selectedObjectNameSingular = null, +}: UseSearchableObjectNameSingularsParams = {}) => { + const { readableObjectMetadataItems } = useReadableObjectMetadataItems(); + const sidePanelShowHiddenObjects = useAtomStateValue( + sidePanelShowHiddenObjectsState, + ); + + return useMemo(() => { + if (isDefined(selectedObjectNameSingular)) { + return [selectedObjectNameSingular]; + } + + return readableObjectMetadataItems + .filter((item) => sidePanelShowHiddenObjects || item.isSearchable) + .map((item) => item.nameSingular); + }, [ + readableObjectMetadataItems, + selectedObjectNameSingular, + sidePanelShowHiddenObjects, + ]); +}; diff --git a/packages/twenty-front/src/modules/side-panel/pages/search/hooks/useSidePanelSearchRecords.tsx b/packages/twenty-front/src/modules/side-panel/pages/search/hooks/useSidePanelSearchRecords.tsx index 779b61a571..13ba7f7cf4 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/search/hooks/useSidePanelSearchRecords.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/search/hooks/useSidePanelSearchRecords.tsx @@ -1,16 +1,12 @@ -import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults'; +import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords'; import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems'; +import { useSearchableObjectNameSingulars } from '@/side-panel/hooks/useSearchableObjectNameSingulars'; import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState'; import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState'; -import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState'; -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useMemo } from 'react'; -import { isDefined } from 'twenty-shared/utils'; import { useDebounce } from 'use-debounce'; -import { useQuery } from '@apollo/client/react'; -import { SearchDocument } from '~/generated/graphql'; export type SearchResultItem = { id: string; @@ -32,56 +28,36 @@ export const useSidePanelSearchRecords = () => { const sidePanelSearchObjectFilter = useAtomStateValue( sidePanelSearchObjectFilterState, ); - const sidePanelShowHiddenObjects = useAtomStateValue( - sidePanelShowHiddenObjectsState, - ); - const coreClient = useApolloCoreClient(); + const trimmedSidePanelSearch = sidePanelSearch.trim(); - const [deferredSidePanelSearch] = useDebounce(sidePanelSearch, 300); + const [deferredSidePanelSearch] = useDebounce(trimmedSidePanelSearch, 300); const { readableObjectMetadataItems } = useReadableObjectMetadataItems(); + const includedObjectNameSingulars = useSearchableObjectNameSingulars({ + selectedObjectNameSingular: sidePanelSearchObjectFilter, + }); - const includedObjectNameSingulars = useMemo(() => { - if (isDefined(sidePanelSearchObjectFilter)) { - return [sidePanelSearchObjectFilter]; - } - - return readableObjectMetadataItems - .filter((item) => sidePanelShowHiddenObjects || item.isSearchable) - .map((item) => item.nameSingular); - }, [ - readableObjectMetadataItems, - sidePanelSearchObjectFilter, - sidePanelShowHiddenObjects, - ]); - - const { data: searchData, loading } = useQuery(SearchDocument, { - client: coreClient, - variables: { - searchInput: deferredSidePanelSearch ?? '', - limit: MAX_SEARCH_RESULTS, - includedObjectNameSingulars, - }, + const { loading, searchRecords } = useObjectRecordSearchRecords({ + objectNameSingulars: includedObjectNameSingulars, + searchInput: deferredSidePanelSearch, }); const searchResultItems: SearchResultItem[] = useMemo(() => { - return (searchData?.search.edges.map((edge) => edge.node) ?? []).map( - (searchRecord) => ({ - id: searchRecord.recordId, - label: searchRecord.label, - objectNameSingular: searchRecord.objectNameSingular, - recordId: searchRecord.recordId, - imageUrl: searchRecord.imageUrl, - objectLabel: - readableObjectMetadataItems.find( - (item) => item.nameSingular === searchRecord.objectNameSingular, - )?.labelSingular ?? searchRecord.objectNameSingular, - avatarType: - searchRecord.objectNameSingular === CoreObjectNameSingular.Company - ? ('squared' as const) - : ('rounded' as const), - }), - ); - }, [searchData, readableObjectMetadataItems]); + return searchRecords.map((searchRecord) => ({ + id: searchRecord.recordId, + label: searchRecord.label, + objectNameSingular: searchRecord.objectNameSingular, + recordId: searchRecord.recordId, + imageUrl: searchRecord.imageUrl, + objectLabel: + readableObjectMetadataItems.find( + (item) => item.nameSingular === searchRecord.objectNameSingular, + )?.labelSingular ?? searchRecord.objectNameSingular, + avatarType: + searchRecord.objectNameSingular === CoreObjectNameSingular.Company + ? ('squared' as const) + : ('rounded' as const), + })); + }, [searchRecords, readableObjectMetadataItems]); return { loading, diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx index 7b5e518df8..137474c094 100644 --- a/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx @@ -12,6 +12,11 @@ import { graphqlMocks } from '~/testing/graphqlMocks'; const meta: Meta = { title: 'Pages/Settings/Playground/RestPlayground', component: SettingsRestPlayground, + // Scalar renders an embedded API reference that manipulates browser history + // and can reload the headless storybook test iframe. Exclude this + // 3rd-party-tool wrapper from the vitest run; it remains available in + // Storybook UI. (!test removes the auto-applied `test` tag.) + tags: ['!test'], decorators: [ (Story) => { jotaiStore.set(playgroundApiKeyState.atom, {