Add search to new sidebar item picker (#22041)

## 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`

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22041?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-24 14:05:23 +02:00
committed by GitHub
parent 5ff1d997c7
commit 1589b9b912
7 changed files with 460 additions and 131 deletions
@@ -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 (
<SidePanelNewSidebarItemMainMenu
onSelectRecord={() =>
navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemRecord)
}
onSelectObject={() =>
navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemObjectPicker)
}
onSelectView={() =>
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemViewObjectPicker,
)
}
/>
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search a nav item...`}
searchValue={searchValue}
onSearchChange={setSearchValue}
>
{hasSearchQuery ? (
<SidePanelNewSidebarItemSearchResults
searchValue={trimmedSearchValue}
/>
) : (
<SidePanelNewSidebarItemMainMenu
onSelectRecord={() =>
navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemRecord)
}
onSelectObject={() =>
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemObjectPicker,
)
}
onSelectView={() =>
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemViewObjectPicker,
)
}
/>
)}
</SidePanelSubViewWithSearch>
);
};
@@ -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 (
<SidePanelSubViewWithSearch
@@ -102,7 +52,7 @@ export const SidePanelNewSidebarItemRecordSubPage = () => {
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList
selectableItemIds={selectableItemIds}
loading={recordSearchLoading}
loading={isRecordSearchLoading}
noResults={isEmpty}
noResultsText={noResultsText}
>
@@ -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 (
<SidePanelAddToNavigationDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList
selectableItemIds={selectableItemIds}
loading={isRecordSearchLoading}
noResults={isEmpty}
noResultsText={t`No results found`}
>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<SidePanelGroup heading={t`Objects`}>
{filteredObjectMetadataItems.map((objectMetadataItem, index) => (
<SidePanelObjectMenuItem
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
onSelect={handleSelectObject}
variant="add"
dragIndex={index}
/>
))}
</SidePanelGroup>
<SidePanelGroup heading={t`Views`}>
{filteredViews.map((view, index) => {
const objectMetadataItem = getObjectMetadataItemForView(view);
const objectIconColor = isDefined(objectMetadataItem)
? getObjectColorWithFallback(objectMetadataItem)
: undefined;
return (
<SelectableListItem
key={view.id}
itemId={view.id}
onEnter={() => handleSelectView(view)}
>
<SidePanelItemWithAddToNavigationDrag
customIconContent={
isDefined(objectMetadataItem) ? (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(objectMetadataItem.icon)}
ViewIcon={getIcon(view.icon)}
objectColor={objectIconColor}
/>
) : 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,
}}
/>
</SelectableListItem>
);
})}
</SidePanelGroup>
<SidePanelGroup heading={t`Records`}>
{availableSearchRecords.map((record, index) => (
<SidePanelNewSidebarItemRecordItem
key={record.recordId}
record={record}
dragIndex={
filteredObjectMetadataItems.length +
filteredViews.length +
index
}
/>
))}
</SidePanelGroup>
{placeholder}
</div>
</SidePanelList>
)}
</SidePanelAddToNavigationDroppable>
);
};
@@ -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,
};
};
@@ -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,
]);
};
@@ -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,
@@ -12,6 +12,11 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<typeof SettingsRestPlayground> = {
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, {