diff --git a/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx b/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx index 8e374b0a08c8e..7b0950622030b 100644 --- a/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx +++ b/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx @@ -69,8 +69,15 @@ function BaseSelectionListItemRenderer({ return onCheckboxPress ? () => onCheckboxPress(item) : undefined; }; + // We add this render condition to allow customizing how an item is rendered in the list, + // without requiring that item to be a ListItem. + if (item.shouldOnlyRenderHeaderContent) { + return item.headerContent; + } + return ( <> + {item.headerContent && item.headerContent} = { /** Whether the brick road indicator should be shown */ brickRoadIndicator?: BrickRoad | '' | null; + /** Allows us to render only the header content, making it easier to embed items other than ListItem into the list */ + shouldOnlyRenderHeaderContent?: boolean; + + /** Element to render above the ListItem */ + headerContent?: ReactNode; + /** Element to render below the ListItem */ footerContent?: ReactNode; diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 923dd8b993ec1..13254d452a20c 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1461,7 +1461,7 @@ function optionsOrderBy(options: T[], comparator: (option: if (!peekedValue) { throw new Error('Heap is empty, cannot peek value'); } - if (comparator(option) > comparator(peekedValue)) { + if (reversed ? comparator(option) < comparator(peekedValue) : comparator(option) > comparator(peekedValue)) { heap.pop(); heap.push(option); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3f5b2f9cdbd11..abe2504ac617c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9,6 +9,7 @@ import isEmpty from 'lodash/isEmpty'; import isNumber from 'lodash/isNumber'; import mapValues from 'lodash/mapValues'; import lodashMaxBy from 'lodash/maxBy'; +import type {ReactNode} from 'react'; import type {ColorValue} from 'react-native'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -899,6 +900,8 @@ type OptionData = { lastName?: string; avatar?: AvatarSource; timezone?: Timezone; + shouldOnlyRenderHeaderContent?: boolean; + headerContent?: ReactNode; } & Report & ReportNameValuePairs; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 664f5170bab10..d0a2ea8069e66 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -272,6 +272,21 @@ function MoneyRequestParticipantsSelector({ !(contactPermissionState === RESULTS.GRANTED || contactPermissionState === RESULTS.LIMITED) && inputHelperText === translate('common.noResultsFound'); + const importContactsButtonComponent = useMemo(() => { + const shouldShowImportContactsButton = contactState?.showImportUI ?? showImportContacts; + if (!shouldShowImportContactsButton) { + return null; + } + return ( + + ); + }, [icons.UserPlus, contactState?.showImportUI, showImportContacts, translate]); + /** * Returns the sections needed for the OptionsSelector * @returns {Array} @@ -349,6 +364,16 @@ function MoneyRequestParticipantsSelector({ headerMessage = inputHelperText; } + const sectionsLength = newSections.reduce((value, section) => value + section.data.length, 0); + if (sectionsLength > 0 && importContactsButtonComponent) { + // Always render the "Import contacts" button at the top of the list + newSections.unshift({ + title: undefined, + data: [{shouldOnlyRenderHeaderContent: true, headerContent: importContactsButtonComponent}], + shouldShow: true, + }); + } + return [newSections, headerMessage]; }, [ areOptionsInitialized, @@ -368,6 +393,7 @@ function MoneyRequestParticipantsSelector({ isPerDiemRequest, showImportContacts, inputHelperText, + importContactsButtonComponent, ]); /** @@ -504,22 +530,6 @@ function MoneyRequestParticipantsSelector({ [isIOUSplit, addParticipantToSelection, addSingleParticipant], ); - const footerContentAbovePaginationComponent = useMemo(() => { - const shouldShowImportContactsButton = contactState?.showImportUI ?? showImportContacts; - if (!shouldShowImportContactsButton) { - return null; - } - return ( - - ); - }, [icons.UserPlus, contactState?.showImportUI, showImportContacts, styles.mb3, translate]); - const ClickableImportContactTextComponent = useMemo(() => { if (searchTerm.length || isSearchingForReports) { return; @@ -580,7 +590,6 @@ function MoneyRequestParticipantsSelector({ } footerContent={footerContent} listEmptyContent={EmptySelectionListContentWithPermission} - footerContentAbovePagination={footerContentAbovePaginationComponent} headerMessage={header} showLoadingPlaceholder={showLoadingPlaceholder} canSelectMultiple={isIOUSplit && isAllowedToSplit} diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 679a3a3f462c8..177486b740221 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -2448,6 +2448,23 @@ describe('OptionsListUtils', () => { const result = optionsOrderBy(options, comparator, 0); expect(result).toEqual([]); }); + + it('returns the older options up to the specified limit', () => { + const options: OptionData[] = [ + {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z'} as OptionData, + {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z'} as OptionData, + {reportID: '3', lastVisibleActionCreated: '2022-01-01T09:00:00Z'} as OptionData, + {reportID: '4', lastVisibleActionCreated: '2022-01-01T13:00:00Z'} as OptionData, + ]; + const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; + // We will pass reversed === true to sort the list in ascending order + const result = optionsOrderBy(options, comparator, 2, undefined, true); + expect(result.length).toBe(2); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(result.at(0)!.reportID).toBe('3'); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(result.at(1)!.reportID).toBe('1'); + }); }); describe('sortAlphabetically', () => {