Infinite scrolling in relation picker menu (#12051)

https://github.com/user-attachments/assets/4be785e0-ea8a-4c8e-840e-6fa0a663d7ba

Closes #11938

---------

Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
Abdul Rahman
2025-05-23 20:53:09 +05:30
committed by GitHub
parent 6ef9a3b4c9
commit af5762c8ba
37 changed files with 1867 additions and 562 deletions

View File

@ -65,7 +65,7 @@ export const useObjectRecordSearchRecords = ({
const effectiveData = loading ? previousData : data;
const searchRecords = useMemo(
() => effectiveData?.search || [],
() => effectiveData?.search.edges.map((edge) => edge.node) || [],
[effectiveData],
);

View File

@ -1,26 +1,21 @@
import { MultipleRecordPickerMenuItems } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems';
import { MultipleRecordPickerItemsDisplay } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerItemsDisplay';
import { MultipleRecordPickerOnClickOutsideEffect } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerOnClickOutsideEffect';
import { MultipleRecordPickerSearchInput } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerSearchInput';
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { multipleRecordPickerPickableMorphItemsLengthComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { useRef } from 'react';
import { useRecoilCallback } from 'recoil';
@ -59,16 +54,6 @@ export const MultipleRecordPicker = ({
selectableListComponentInstanceId,
);
const multipleRecordPickerIsLoading = useRecoilComponentValueV2(
multipleRecordPickerIsLoadingComponentState,
componentInstanceId,
);
const itemsLength = useRecoilComponentValueV2(
multipleRecordPickerPickableMorphItemsLengthComponentSelector,
componentInstanceId,
);
const multipleRecordPickerSearchFilterState =
useRecoilComponentCallbackStateV2(
multipleRecordPickerSearchFilterComponentState,
@ -106,13 +91,16 @@ export const MultipleRecordPicker = ({
[multipleRecordPickerSearchFilterState, onCreate],
);
const createNewButton = isDefined(onCreate) && (
<CreateNewButton
onClick={handleCreateNewButtonClick}
LeftIcon={IconPlus}
text="Add New"
/>
);
const createNewButtonSection =
isDefined(onCreate) && !hasObjectReadOnlyPermission ? (
<DropdownMenuItemsContainer scrollable={false}>
<CreateNewButton
onClick={handleCreateNewButtonClick}
LeftIcon={IconPlus}
text="Add New"
/>
</DropdownMenuItemsContainer>
) : null;
return (
<MultipleRecordPickerComponentInstanceContext.Provider
@ -125,43 +113,15 @@ export const MultipleRecordPicker = ({
<DropdownMenu ref={containerRef} data-select-disable width={200}>
{layoutDirection === 'search-bar-on-bottom' && (
<>
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
</DropdownMenuItemsContainer>
)}
<DropdownMenuSeparator />
{itemsLength > 0 && (
<MultipleRecordPickerMenuItems onChange={onChange} />
)}
{multipleRecordPickerIsLoading && (
<>
<DropdownMenuSkeletonItem />
<DropdownMenuSeparator />
</>
)}
{itemsLength > 0 && <DropdownMenuSeparator />}
{createNewButtonSection}
<MultipleRecordPickerItemsDisplay onChange={onChange} />
</>
)}
<MultipleRecordPickerSearchInput />
{layoutDirection === 'search-bar-on-top' && (
<>
<DropdownMenuSeparator />
{multipleRecordPickerIsLoading && (
<>
<DropdownMenuSkeletonItem />
<DropdownMenuSeparator />
</>
)}
{itemsLength > 0 && (
<MultipleRecordPickerMenuItems onChange={onChange} />
)}
{itemsLength > 0 && <DropdownMenuSeparator />}
{isDefined(onCreate) && (
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
</DropdownMenuItemsContainer>
)}
<MultipleRecordPickerItemsDisplay onChange={onChange} />
{createNewButtonSection}
</>
)}
</DropdownMenu>

View File

@ -0,0 +1,96 @@
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
import { multipleRecordPickerPaginationState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPaginationState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { multipleRecordPickerPaginationSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPaginationSelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { useCallback } from 'react';
import { useInView } from 'react-intersection-observer';
import { useRecoilCallback } from 'recoil';
import { GRAY_SCALE } from 'twenty-ui/theme';
const StyledText = styled.div`
align-items: center;
box-shadow: none;
color: ${GRAY_SCALE.gray40};
display: flex;
height: 32px;
margin-left: ${({ theme }) => theme.spacing(8)};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledIntersectionObserver = styled.div`
height: 1px;
`;
export const MultipleRecordPickerFetchMoreLoader = () => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
MultipleRecordPickerComponentInstanceContext,
);
const paginationState = useRecoilComponentValueV2(
multipleRecordPickerPaginationSelector,
componentInstanceId,
);
const isLoading = useRecoilComponentValueV2(
multipleRecordPickerIsLoadingComponentState,
componentInstanceId,
);
const searchFilter = useRecoilComponentValueV2(
multipleRecordPickerSearchFilterComponentState,
componentInstanceId,
);
const { performSearch } = useMultipleRecordPickerPerformSearch();
const fetchMore = useRecoilCallback(
({ snapshot }) =>
async () => {
const paginationState = snapshot
.getLoadable(
multipleRecordPickerPaginationState.atomFamily({
instanceId: componentInstanceId,
}),
)
.getValue();
if (isLoading || !paginationState.hasNextPage) {
return;
}
performSearch({
multipleRecordPickerInstanceId: componentInstanceId,
forceSearchFilter: searchFilter,
loadMore: true,
});
},
[componentInstanceId, performSearch, searchFilter, isLoading],
);
const { ref } = useInView({
onChange: useCallback(
(inView: boolean) => {
if (inView) {
fetchMore();
}
},
[fetchMore],
),
});
if (!paginationState.hasNextPage) {
return null;
}
return (
<div>
<StyledIntersectionObserver ref={ref} />
{isLoading && <StyledText>Loading more...</StyledText>}
</div>
);
};

View File

@ -0,0 +1,41 @@
import { MultipleRecordPickerMenuItems } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems';
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
import { multipleRecordPickerPickableMorphItemsLengthComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const MultipleRecordPickerItemsDisplay = ({
onChange,
}: {
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
}) => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
MultipleRecordPickerComponentInstanceContext,
);
const isLoading = useRecoilComponentValueV2(
multipleRecordPickerIsLoadingComponentState,
componentInstanceId,
);
const itemsLength = useRecoilComponentValueV2(
multipleRecordPickerPickableMorphItemsLengthComponentSelector,
componentInstanceId,
);
return (
<>
<DropdownMenuSeparator />
{isLoading && itemsLength === 0 ? (
<DropdownMenuSkeletonItem />
) : (
<MultipleRecordPickerMenuItems onChange={onChange} />
)}
<DropdownMenuSeparator />
</>
);
};

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { MultipleRecordPickerFetchMoreLoader } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader';
import { MultipleRecordPickerMenuItem } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem';
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
@ -21,6 +22,14 @@ export const StyledSelectableItem = styled(SelectableListItem)`
width: 100%;
`;
const StyledEmptyText = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
`;
type MultipleRecordPickerMenuItemsProps = {
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
};
@ -77,25 +86,30 @@ export const MultipleRecordPickerMenuItems = ({
return (
<DropdownMenuItemsContainer hasMaxHeight>
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={pickableRecordIds}
hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker}
>
{pickableRecordIds.map((recordId) => {
return (
<MultipleRecordPickerMenuItem
key={recordId}
recordId={recordId}
onChange={(morphItem) => {
handleChange(morphItem);
onChange?.(morphItem);
resetSelectedItem();
}}
/>
);
})}
</SelectableList>
{pickableRecordIds.length === 0 ? (
<StyledEmptyText>No results found</StyledEmptyText>
) : (
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={pickableRecordIds}
hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker}
>
{pickableRecordIds.map((recordId) => {
return (
<MultipleRecordPickerMenuItem
key={recordId}
recordId={recordId}
onChange={(morphItem) => {
handleChange(morphItem);
onChange?.(morphItem);
resetSelectedItem();
}}
/>
);
})}
<MultipleRecordPickerFetchMoreLoader />
</SelectableList>
)}
</DropdownMenuItemsContainer>
);
};

View File

@ -4,7 +4,8 @@ import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useCallback } from 'react';
import { useRecoilCallback } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
export const MultipleRecordPickerSearchInput = () => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
@ -16,17 +17,33 @@ export const MultipleRecordPickerSearchInput = () => {
const { performSearch } = useMultipleRecordPickerPerformSearch();
const handleFilterChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setRecordPickerSearchFilter(event.currentTarget.value);
performSearch({
multipleRecordPickerInstanceId: componentInstanceId,
forceSearchFilter: event.currentTarget.value,
});
},
[componentInstanceId, performSearch, setRecordPickerSearchFilter],
const debouncedSearch = useDebouncedCallback(
useRecoilCallback(
({ set }) =>
(searchFilter: string) => {
set(
multipleRecordPickerSearchFilterComponentState.atomFamily({
instanceId: componentInstanceId,
}),
searchFilter,
);
performSearch({
multipleRecordPickerInstanceId: componentInstanceId,
forceSearchFilter: searchFilter,
});
},
[componentInstanceId, performSearch],
),
500,
);
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newSearchFilter = event.currentTarget.value;
setRecordPickerSearchFilter(newSearchFilter);
debouncedSearch(newSearchFilter);
};
return (
<DropdownMenuSearchInput
value={recordPickerSearchFilter}

View File

@ -1,7 +1,8 @@
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
import { search } from '@/command-menu/graphql/queries/search';
import { SEARCH_QUERY } from '@/command-menu/graphql/queries/search';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { usePerformCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords';
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
import { multipleRecordPickerPaginationState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPaginationState';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
@ -12,6 +13,9 @@ import { isNonEmptyArray } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { SearchRecord } from '~/generated-metadata/graphql';
import { SearchResultEdge } from '~/generated/graphql';
const MULTIPLE_RECORD_PICKER_PAGE_SIZE = 30;
export const useMultipleRecordPickerPerformSearch = () => {
const client = useApolloClient();
@ -26,14 +30,40 @@ export const useMultipleRecordPickerPerformSearch = () => {
forceSearchFilter = '',
forceSearchableObjectMetadataItems = [],
forcePickableMorphItems = [],
loadMore = false,
}: {
multipleRecordPickerInstanceId: string;
forceSearchFilter?: string;
forceSearchableObjectMetadataItems?: ObjectMetadataItem[];
forcePickableMorphItems?: RecordPickerPickableMorphItem[];
loadMore?: boolean;
}) => {
const { getLoadable } = snapshot;
const paginationState = getLoadable(
multipleRecordPickerPaginationState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
}),
).getValue();
set(
multipleRecordPickerIsLoadingComponentState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
}),
true,
);
set(
multipleRecordPickerPaginationState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
}),
{
...paginationState,
endCursor: loadMore ? paginationState.endCursor : null,
hasNextPage: loadMore ? paginationState.hasNextPage : true,
},
);
const recordPickerSearchFilter = getLoadable(
multipleRecordPickerSearchFilterComponentState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
@ -70,6 +100,7 @@ export const useMultipleRecordPickerPerformSearch = () => {
const [
searchRecordsFilteredOnPickedRecords,
searchRecordsExcludingPickedRecords,
pageInfo,
] = await performSearchQueries({
client,
searchFilter,
@ -77,28 +108,83 @@ export const useMultipleRecordPickerPerformSearch = () => {
pickedRecordIds: selectedPickableMorphItems.map(
({ recordId }) => recordId,
),
after: loadMore ? paginationState.endCursor : null,
});
const pickedMorphItems = pickableMorphItems.filter(
({ isSelected }) => isSelected,
const existingMorphItems = getLoadable(
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
}),
).getValue();
const allPickedItems = [
...existingMorphItems.filter(({ isSelected }) => isSelected),
...pickableMorphItems.filter(({ isSelected }) => isSelected),
];
const uniquePickedItems = allPickedItems.reduce(
(acc, item) => {
if (!acc.some((existing) => existing.recordId === item.recordId)) {
acc.push(item);
}
return acc;
},
[] as typeof allPickedItems,
);
// We update the existing pickedMorphItems to be matching the search filter
const updatedPickedMorphItems = pickedMorphItems.map((morphItem) => {
const record = searchRecordsFilteredOnPickedRecords.find(
({ recordId }) => recordId === morphItem.recordId,
);
const updatedPickedItems = uniquePickedItems.map((morphItem) => {
if (!searchFilter) {
return {
...morphItem,
isMatchingSearchFilter: true,
};
}
const isMatchingSearchFilter =
searchRecordsFilteredOnPickedRecords.some(
({ recordId }) => recordId === morphItem.recordId,
) ||
searchRecordsExcludingPickedRecords.some(
({ recordId }) => recordId === morphItem.recordId,
);
return {
...morphItem,
isMatchingSearchFilter: isDefined(record),
isMatchingSearchFilter,
};
});
const updatedNonPickedExistingItems = existingMorphItems
.filter((item) => !item.isSelected)
.map((morphItem) => {
if (!searchFilter) {
return {
...morphItem,
isMatchingSearchFilter: true,
};
}
const isMatchingSearchFilter =
searchRecordsFilteredOnPickedRecords.some(
({ recordId }) => recordId === morphItem.recordId,
) ||
searchRecordsExcludingPickedRecords.some(
({ recordId }) => recordId === morphItem.recordId,
);
return {
...morphItem,
isMatchingSearchFilter,
};
});
const searchRecordsFilteredOnPickedRecordsWithoutDuplicates =
searchRecordsFilteredOnPickedRecords.filter(
(searchRecord) =>
!updatedPickedMorphItems.some(
!updatedPickedItems.some(
({ recordId }) => recordId === searchRecord.recordId,
) &&
!updatedNonPickedExistingItems.some(
({ recordId }) => recordId === searchRecord.recordId,
),
);
@ -109,13 +195,17 @@ export const useMultipleRecordPickerPerformSearch = () => {
!searchRecordsFilteredOnPickedRecords.some(
({ recordId }) => recordId === searchRecord.recordId,
) &&
!pickedMorphItems.some(
!updatedPickedItems.some(
({ recordId }) => recordId === searchRecord.recordId,
) &&
!updatedNonPickedExistingItems.some(
({ recordId }) => recordId === searchRecord.recordId,
),
);
const morphItems = [
...updatedPickedMorphItems,
const newMorphItems = [
...updatedPickedItems,
...updatedNonPickedExistingItems,
...searchRecordsFilteredOnPickedRecordsWithoutDuplicates.map(
({ recordId, objectNameSingular }) => ({
isMatchingSearchFilter: true,
@ -140,6 +230,20 @@ export const useMultipleRecordPickerPerformSearch = () => {
),
];
const morphItems = loadMore
? newMorphItems.reduce(
(acc, item) => {
if (
!acc.some((existing) => existing.recordId === item.recordId)
) {
acc.push(item);
}
return acc;
},
[] as typeof newMorphItems,
)
: newMorphItems;
set(
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
@ -234,6 +338,24 @@ export const useMultipleRecordPickerPerformSearch = () => {
},
);
}
set(
multipleRecordPickerPaginationState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
}),
{
...paginationState,
endCursor: pageInfo.endCursor,
hasNextPage: pageInfo.hasNextPage,
},
);
set(
multipleRecordPickerIsLoadingComponentState.atomFamily({
instanceId: multipleRecordPickerInstanceId,
}),
false,
);
},
[client, performCombinedFindManyRecords],
);
@ -246,32 +368,46 @@ const performSearchQueries = async ({
searchFilter,
searchableObjectMetadataItems,
pickedRecordIds,
limit = MULTIPLE_RECORD_PICKER_PAGE_SIZE,
after = null,
}: {
client: ApolloClient<object>;
searchFilter: string;
searchableObjectMetadataItems: ObjectMetadataItem[];
pickedRecordIds: string[];
}): Promise<[SearchRecord[], SearchRecord[]]> => {
limit?: number;
after?: string | null;
}): Promise<
[
SearchRecord[],
SearchRecord[],
{ hasNextPage: boolean; endCursor: string | null },
]
> => {
if (searchableObjectMetadataItems.length === 0) {
return [[], []];
return [[], [], { hasNextPage: false, endCursor: null }];
}
const searchRecords = async (filter: any) => {
const { data } = await client.query({
query: search,
query: SEARCH_QUERY,
variables: {
searchInput: searchFilter,
includedObjectNameSingulars: searchableObjectMetadataItems.map(
({ nameSingular }) => nameSingular,
),
filter,
limit: MAX_SEARCH_RESULTS,
limit,
after,
},
});
return data.search;
return {
records: data.search.edges.map((edge: SearchResultEdge) => edge.node),
pageInfo: data.search.pageInfo,
};
};
const searchRecordsExcludingPickedRecords = await searchRecords(
const searchRecordsExcludingPickedRecordsResult = await searchRecords(
pickedRecordIds.length > 0
? {
not: {
@ -283,17 +419,18 @@ const performSearchQueries = async ({
: undefined,
);
const searchRecordsIncludingPickedRecords =
const searchRecordsIncludingPickedRecordsResult =
pickedRecordIds.length > 0
? await searchRecords({
id: {
in: pickedRecordIds,
},
})
: [];
: { records: [], pageInfo: { hasNextPage: false, endCursor: null } };
return [
searchRecordsIncludingPickedRecords,
searchRecordsExcludingPickedRecords,
searchRecordsIncludingPickedRecordsResult.records,
searchRecordsExcludingPickedRecordsResult.records,
searchRecordsExcludingPickedRecordsResult.pageInfo,
];
};

View File

@ -0,0 +1,17 @@
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export type MultipleRecordPickerPaginationState = {
endCursor: string | null;
hasNextPage: boolean;
};
export const multipleRecordPickerPaginationState =
createComponentStateV2<MultipleRecordPickerPaginationState>({
key: 'multipleRecordPickerPaginationState',
defaultValue: {
endCursor: null,
hasNextPage: false,
},
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
});

View File

@ -0,0 +1,19 @@
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerPaginationState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPaginationState';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
export const multipleRecordPickerPaginationSelector = createComponentSelectorV2(
{
key: 'multipleRecordPickerPaginationSelector',
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
get:
({ instanceId }) =>
({ get }) => {
return get(
multipleRecordPickerPaginationState.atomFamily({
instanceId,
}),
);
},
},
);