diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 1cdbbc341c29..30c202295c87 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -66,10 +66,9 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen }, [areOptionsInitialized, isScreenTransitionEnd, options]); const chatOptions = useMemo(() => { - return OptionsListUtils.filterOptions(defaultOptions, cleanSearchTerm, { + return OptionsListUtils.filterAndOrderOptions(defaultOptions, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS, - maxRecentReportsToShow: 0, }); }, [defaultOptions, cleanSearchTerm, selectedOptions]); diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index fc692c01f824..6fec134a608a 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -54,7 +54,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: return defaultListOptions; } - return OptionsListUtils.getOptions( + return OptionsListUtils.getValidOptions( { reports: options.reports, personalDetails: options.personalDetails, @@ -62,13 +62,12 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS, - maxRecentReportsToShow: 0, }, ); }, [areOptionsInitialized, options.personalDetails, options.reports, selectedOptions]); const chatOptions = useMemo(() => { - return OptionsListUtils.filterOptions(defaultOptions, cleanSearchTerm, { + return OptionsListUtils.filterAndOrderOptions(defaultOptions, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 632f51f7f05a..3fe7cc9e2de4 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -147,7 +147,7 @@ function SearchRouterList( return []; } - const filteredOptions = OptionsListUtils.getOptions( + const filteredOptions = OptionsListUtils.getValidOptions( { reports: options.reports, personalDetails: options.personalDetails, @@ -378,7 +378,7 @@ function SearchRouterList( } Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS); - const filteredOptions = OptionsListUtils.filterOptions(searchOptions, autocompleteQueryValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(searchOptions, autocompleteQueryValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS); const reportOptions: OptionData[] = [...filteredOptions.recentReports, ...filteredOptions.personalDetails]; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2470a02fb62a..eb78cdaa4f3e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,7 +1,6 @@ /* eslint-disable no-continue */ import {Str} from 'expensify-common'; import lodashOrderBy from 'lodash/orderBy'; -import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SetNonNullable} from 'type-fest'; @@ -92,13 +91,11 @@ type Section = SectionBase & { type GetOptionsConfig = { betas?: OnyxEntry; selectedOptions?: Option[]; - maxRecentReportsToShow?: number; excludeLogins?: string[]; includeMultipleParticipantReports?: boolean; includeRecentReports?: boolean; includeSelfDM?: boolean; showChatPreviewLine?: boolean; - sortPersonalDetailsByAlphaAsc?: boolean; forcePolicyNamePreview?: boolean; includeOwnedWorkspaceChats?: boolean; includeThreads?: boolean; @@ -110,20 +107,16 @@ type GetOptionsConfig = { includeInvoiceRooms?: boolean; includeDomainEmail?: boolean; action?: IOUAction; - shouldAcceptName?: boolean; recentAttendees?: Attendee[]; shouldBoldTitleByDefault?: boolean; }; type GetUserToInviteConfig = { searchValue: string; - excludeUnknownUsers?: boolean; optionsToExclude?: Array>; - selectedOptions?: Array>; reportActions?: ReportActions; - showChatPreviewLine?: boolean; shouldAcceptName?: boolean; -}; +} & Pick; type MemberForList = { text: string; @@ -150,15 +143,18 @@ type Options = { type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; -type FilterOptionsConfig = Pick & { +type FilterUserToInviteConfig = Pick & { + canInviteUser?: boolean; + excludeLogins?: string[]; +}; + +type OrderOptionsConfig = { + /* When sortByReportTypeInSearch flag is true, recentReports will include the personalDetails options as well. */ + sortByReportTypeInSearch?: boolean; + maxRecentReportsToShow?: number; preferChatroomsOverThreads?: boolean; preferPolicyExpenseChat?: boolean; preferRecentExpenseReports?: boolean; - /* When sortByReportTypeInSearch flag is true, recentReports will include the personalDetails options as well. */ - sortByReportTypeInSearch?: boolean; - reportActions?: ReportActions; - excludeUnknownUsers?: boolean; - canInviteUser?: boolean; }; /** @@ -640,6 +636,7 @@ function createOption( policyID: undefined, isOptimisticPersonalDetail: false, lastMessageText: '', + lastVisibleActionCreated: undefined, }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -676,6 +673,7 @@ function createOption( result.policyID = report.policyID; result.isSelfDM = ReportUtils.isSelfDM(report); result.notificationPreference = ReportUtils.getReportNotificationPreference(report); + result.lastVisibleActionCreated = report.lastVisibleActionCreated; const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true); @@ -917,20 +915,36 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry personalDetail.text?.toLowerCase()], 'asc'); +} + /** - * Options need to be sorted in the specific order + * Orders report options without grouping them by kind. + * Usually used when there is no search value + */ +function orderReportOptions(options: ReportUtils.OptionData[]) { + return lodashOrderBy(options, [sortComparatorReportOptionByArchivedStatus, sortComparatorReportOptionByDate], ['asc', 'desc']); +} + +/** + * Ordering for report options when you have a search value, will order them by kind additionally. * @param options - list of options to be sorted * @param searchValue - search string * @returns a sorted list of options */ -function orderOptions( +function orderReportOptionsWithSearch( options: ReportUtils.OptionData[], - searchValue: string | undefined, - {preferChatroomsOverThreads = false, preferPolicyExpenseChat = false, preferRecentExpenseReports = false} = {}, + searchValue: string, + {preferChatroomsOverThreads = false, preferPolicyExpenseChat = false, preferRecentExpenseReports = false}: OrderOptionsConfig = {}, ) { + const orderedByDate = orderReportOptions(options); + return lodashOrderBy( - options, + orderedByDate, [ + // Sorting by kind: (option) => { if (option.isPolicyExpenseChat && preferPolicyExpenseChat && option.policyID === activePolicyID) { return 0; @@ -969,6 +983,35 @@ function orderOptions( ); } +function sortComparatorReportOptionByArchivedStatus(option: ReportUtils.OptionData) { + return option.private_isArchived ? 1 : 0; +} + +function sortComparatorReportOptionByDate(options: ReportUtils.OptionData) { + // If there is no date (ie. a personal detail option), the option will be sorted to the bottom + // (comparing a dateString > '' returns true, and we are sorting descending, so the dateString will come before '') + return options.lastVisibleActionCreated ?? ''; +} + +type ReportAndPersonalDetailOptions = Pick; + +function orderOptions(options: ReportAndPersonalDetailOptions): ReportAndPersonalDetailOptions; +function orderOptions(options: ReportAndPersonalDetailOptions, searchValue: string, config?: OrderOptionsConfig): ReportAndPersonalDetailOptions; +function orderOptions(options: ReportAndPersonalDetailOptions, searchValue?: string, config?: OrderOptionsConfig) { + let orderedReportOptions: ReportUtils.OptionData[]; + if (searchValue) { + orderedReportOptions = orderReportOptionsWithSearch(options.recentReports, searchValue, config); + } else { + orderedReportOptions = orderReportOptions(options.recentReports); + } + const orderedPersonalDetailsOptions = orderPersonalDetailsOptions(options.personalDetails); + + return { + recentReports: orderedReportOptions, + personalDetails: orderedPersonalDetailsOptions, + }; +} + function canCreateOptimisticPersonalDetailOption({ recentReportOptions, personalDetailsOptions, @@ -977,7 +1020,6 @@ function canCreateOptimisticPersonalDetailOption({ recentReportOptions: ReportUtils.OptionData[]; personalDetailsOptions: ReportUtils.OptionData[]; currentUserOption?: ReportUtils.OptionData | null; - excludeUnknownUsers: boolean; }) { return recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; } @@ -991,7 +1033,6 @@ function canCreateOptimisticPersonalDetailOption({ */ function getUserToInviteOption({ searchValue, - excludeUnknownUsers = false, optionsToExclude = [], selectedOptions = [], reportActions = {}, @@ -1006,7 +1047,7 @@ function getUserToInviteOption({ const isInOptionToExclude = optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1; - if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude || excludeUnknownUsers) { + if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude) { return null; } @@ -1043,19 +1084,17 @@ function getUserToInviteOption({ } /** - * filter options based on specific conditions + * Options are reports and personal details. This function filters out the options that are not valid to be displayed. */ -function getOptions( +function getValidOptions( options: OptionList, { betas = [], selectedOptions = [], - maxRecentReportsToShow = 0, excludeLogins = [], includeMultipleParticipantReports = false, includeRecentReports = true, showChatPreviewLine = false, - sortPersonalDetailsByAlphaAsc = true, forcePolicyNamePreview = false, includeOwnedWorkspaceChats = false, includeThreads = false, @@ -1093,25 +1132,11 @@ function getOptions( }); }); - // Sorting the reports works like this: - // - Order everything by the last message timestamp (descending) - // - When searching, self DM is put at the top - // - All archived reports should remain at the bottom - const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { - const report = option.item; - if (option.private_isArchived) { - return CONST.DATE.UNIX_EPOCH; - } - - return report?.lastVisibleActionCreated; - }); - orderedReportOptions.reverse(); - - const allReportOptions = orderedReportOptions.filter((option) => { + const allReportOptions = filteredReportOptions.filter((option) => { const report = option.item; if (!report) { - return; + return false; } const isThread = option.isThread; @@ -1123,51 +1148,45 @@ function getOptions( const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report); if (isPolicyExpenseChat && report.isOwnPolicyExpenseChat && !includeOwnedWorkspaceChats) { - return; + return false; } // When passing includeP2P false we are trying to hide features from users that are not ready for P2P and limited to workspace chats only. if (!includeP2P && !isPolicyExpenseChat) { - return; + return false; } if (isSelfDM && !includeSelfDM) { - return; + return false; } if (isThread && !includeThreads) { - return; + return false; } if (isTaskReport && !includeTasks) { - return; + return false; } if (isMoneyRequestReport && !includeMoneyRequests) { - return; + return false; } // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. if (includeOwnedWorkspaceChats && ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(report)) { - return; + return false; } if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { - return; + return false; } - return option; + return true; }); - const havingLoginPersonalDetails = includeP2P + const allPersonalDetailsOptions = includeP2P ? options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail && (includeDomainEmail || !Str.isDomainEmail(detail.login))) : []; - let allPersonalDetailsOptions = havingLoginPersonalDetails; - - if (sortPersonalDetailsByAlphaAsc) { - // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 - allPersonalDetailsOptions = lodashOrderBy(allPersonalDetailsOptions, [(personalDetail) => personalDetail.text?.toLowerCase()], 'asc'); - } const optionsToExclude: Option[] = [{login: CONST.EMAIL.NOTIFICATIONS}]; @@ -1195,11 +1214,6 @@ function getOptions( */ reportOption.alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); - // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value - if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { - break; - } - // Skip notifications@expensify.com if (reportOption.login === CONST.EMAIL.NOTIFICATIONS) { continue; @@ -1297,10 +1311,10 @@ function getOptions( function getSearchOptions(options: OptionList, betas: Beta[] = [], isUsedInChatFinder = true): Options { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const optionList = getOptions(options, { + const optionList = getValidOptions(options, { betas, + includeRecentReports: true, includeMultipleParticipantReports: true, - maxRecentReportsToShow: 0, // Unlimited showChatPreviewLine: isUsedInChatFinder, includeP2P: true, forcePolicyNamePreview: true, @@ -1311,14 +1325,18 @@ function getSearchOptions(options: OptionList, betas: Beta[] = [], isUsedInChatF includeSelfDM: true, shouldBoldTitleByDefault: !isUsedInChatFinder, }); + const orderedOptions = orderOptions(optionList); Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); - return optionList; + return { + ...optionList, + ...orderedOptions, + }; } function getShareLogOptions(options: OptionList, betas: Beta[] = []): Options { - return getOptions(options, { + return getValidOptions(options, { betas, includeMultipleParticipantReports: true, includeP2P: true, @@ -1363,7 +1381,7 @@ function getAttendeeOptions( includeInvoiceRooms = false, action: IOUAction | undefined = undefined, ) { - return getOptions( + return getValidOptions( {reports, personalDetails}, { betas, @@ -1373,7 +1391,6 @@ function getAttendeeOptions( includeRecentReports: false, includeP2P, includeSelectedOptions: false, - maxRecentReportsToShow: 0, includeSelfDM: false, includeInvoiceRooms, action, @@ -1394,12 +1411,11 @@ function getShareDestinationOptions( excludeLogins: string[] = [], includeOwnedWorkspaceChats = true, ) { - return getOptions( + return getValidOptions( {reports, personalDetails}, { betas, selectedOptions, - maxRecentReportsToShow: 0, // Unlimited includeMultipleParticipantReports: true, showChatPreviewLine: true, forcePolicyNamePreview: true, @@ -1450,17 +1466,23 @@ function getMemberInviteOptions( reports: Array> = [], includeRecentReports = false, ): Options { - return getOptions( + const options = getValidOptions( {reports, personalDetails}, { betas, includeP2P: true, excludeLogins, - sortPersonalDetailsByAlphaAsc: true, includeSelectedOptions, includeRecentReports, }, ); + + const orderedOptions = orderOptions(options); + return { + ...options, + personalDetails: orderedOptions.personalDetails, + recentReports: orderedOptions.recentReports, + }; } /** @@ -1596,112 +1618,157 @@ function filteredPersonalDetailsOfRecentReports(recentReports: ReportUtils.Optio /** * Filters options based on the search input value */ -function filterOptions(options: Options, searchInputValue: string, config?: FilterOptionsConfig): Options { - const { - sortByReportTypeInSearch = false, - canInviteUser = true, - maxRecentReportsToShow = 0, - excludeLogins = [], - preferChatroomsOverThreads = false, - preferPolicyExpenseChat = false, - preferRecentExpenseReports = false, - excludeUnknownUsers = false, - } = config ?? {}; - if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) { - const recentReports = options.recentReports.slice(0, maxRecentReportsToShow); - const personalDetails = filteredPersonalDetailsOfRecentReports(recentReports, options.personalDetails); - return { - ...options, - recentReports, - personalDetails, - }; +function filterReports(reports: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] { + // We search eventually for multiple whitespace separated search terms. + // We start with the search term at the end, and then narrow down those filtered search results with the next search term. + // We repeat (reduce) this until all search terms have been used: + const filteredReports = searchTerms.reduceRight( + (items, term) => + filterArrayByMatch(items, term, (item) => { + const values: string[] = []; + if (item.text) { + values.push(item.text); + } + + if (item.login) { + values.push(item.login); + values.push(item.login.replace(CONST.EMAIL_SEARCH_REGEX, '')); + } + + if (item.isThread) { + if (item.alternateText) { + values.push(item.alternateText); + } + } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { + if (item.subtitle) { + values.push(item.subtitle); + } + } + + return uniqFast(values); + }), + // We start from all unfiltered reports: + reports, + ); + + return filteredReports; +} + +function filterPersonalDetails(personalDetails: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] { + return searchTerms.reduceRight( + (items, term) => + filterArrayByMatch(items, term, (item) => { + const values = getPersonalDetailSearchTerms(item); + return uniqFast(values); + }), + personalDetails, + ); +} + +function filterCurrentUserOption(currentUserOption: ReportUtils.OptionData | null | undefined, searchTerms: string[]): ReportUtils.OptionData | null | undefined { + return searchTerms.reduceRight((item, term) => { + if (!item) { + return null; + } + + const currentUserOptionSearchText = uniqFast(getCurrentUserSearchTerms(item)).join(' '); + return isSearchStringMatch(term, currentUserOptionSearchText) ? item : null; + }, currentUserOption); +} + +function filterUserToInvite(options: Omit, searchValue: string, config?: FilterUserToInviteConfig): ReportUtils.OptionData | null { + const {canInviteUser = true, excludeLogins = []} = config ?? {}; + if (!canInviteUser) { + return null; } - const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); - const searchTerms = searchValue ? searchValue.split(' ') : []; + const canCreateOptimisticDetail = canCreateOptimisticPersonalDetailOption({ + recentReportOptions: options.recentReports, + personalDetailsOptions: options.personalDetails, + currentUserOption: options.currentUserOption, + }); - const optionsToExclude: Option[] = [{login: CONST.EMAIL.NOTIFICATIONS}]; + if (!canCreateOptimisticDetail) { + return null; + } + const optionsToExclude: Option[] = [{login: CONST.EMAIL.NOTIFICATIONS}]; excludeLogins.forEach((login) => { optionsToExclude.push({login}); }); - const matchResults = searchTerms.reduceRight((items, term) => { - const recentReports = filterArrayByMatch(items.recentReports, term, (item) => { - const values: string[] = []; - if (item.text) { - values.push(item.text); - } + return getUserToInviteOption({ + searchValue, + optionsToExclude, + ...config, + }); +} - if (item.login) { - values.push(item.login); - values.push(item.login.replace(CONST.EMAIL_SEARCH_REGEX, '')); - } +function filterOptions(options: Options, searchInputValue: string, config?: FilterUserToInviteConfig): Options { + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); + const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); + const searchTerms = searchValue ? searchValue.split(' ') : []; - if (item.isThread) { - if (item.alternateText) { - values.push(item.alternateText); - } - } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { - if (item.subtitle) { - values.push(item.subtitle); - } - } + const recentReports = filterReports(options.recentReports, searchTerms); + const personalDetails = filterPersonalDetails(options.personalDetails, searchTerms); + const currentUserOption = filterCurrentUserOption(options.currentUserOption, searchTerms); + const userToInvite = filterUserToInvite( + { + recentReports, + personalDetails, + currentUserOption, + }, + searchValue, + config, + ); - return uniqFast(values); - }); - const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => uniqFast(getPersonalDetailSearchTerms(item))); + return { + personalDetails, + recentReports, + userToInvite, + currentUserOption, + }; +} - const currentUserOptionSearchText = items.currentUserOption ? uniqFast(getCurrentUserSearchTerms(items.currentUserOption)).join(' ') : ''; +type FilterAndOrderConfig = FilterUserToInviteConfig & OrderOptionsConfig; - const currentUserOption = isSearchStringMatch(term, currentUserOptionSearchText) ? items.currentUserOption : null; - return { - recentReports: recentReports ?? [], - personalDetails: personalDetails ?? [], - userToInvite: null, - currentUserOption, - }; - }, options); +/** + * Filters and orders the options based on the search input value. + * Note that personal details that are part of the recent reports will always be shown as part of the recent reports (ie. DMs). + */ +function filterAndOrderOptions(options: Options, searchInputValue: string, config: FilterAndOrderConfig = {}): Options { + const {sortByReportTypeInSearch = false} = config; - const {recentReports, personalDetails} = matchResults; + let filterResult = options; + if (searchInputValue.trim().length > 0) { + filterResult = filterOptions(options, searchInputValue, config); + } - const personalDetailsWithoutDMs = filteredPersonalDetailsOfRecentReports(recentReports, personalDetails); + let {recentReports: filteredReports, personalDetails: filteredPersonalDetails} = filterResult; - let filteredPersonalDetails: ReportUtils.OptionData[] = personalDetailsWithoutDMs; - let filteredRecentReports: ReportUtils.OptionData[] = recentReports; - if (sortByReportTypeInSearch) { - filteredRecentReports = recentReports.concat(personalDetailsWithoutDMs); - filteredPersonalDetails = []; + if (typeof config?.maxRecentReportsToShow === 'number') { + filteredReports = orderReportOptionsWithSearch(filteredReports, searchInputValue, config); + filteredReports = filteredReports.slice(0, config.maxRecentReportsToShow); } - let userToInvite = null; - if (canInviteUser) { - const canCreateOptimisticDetail = canCreateOptimisticPersonalDetailOption({ - recentReportOptions: filteredRecentReports, - personalDetailsOptions: filteredPersonalDetails, - currentUserOption: matchResults.currentUserOption, - excludeUnknownUsers, - }); - if (canCreateOptimisticDetail) { - userToInvite = getUserToInviteOption({ - searchValue, - selectedOptions: config?.selectedOptions, - optionsToExclude, - }); - } - } + const personalDetailsWithoutDMs = filteredPersonalDetailsOfRecentReports(filteredReports, filteredPersonalDetails); + const orderedPersonalDetails = orderPersonalDetailsOptions(personalDetailsWithoutDMs); - if (maxRecentReportsToShow > 0 && recentReports.length > maxRecentReportsToShow) { - recentReports.splice(maxRecentReportsToShow); + // sortByReportTypeInSearch option will show the personal details as part of the recent reports + if (sortByReportTypeInSearch) { + filteredReports = filteredReports.concat(orderedPersonalDetails); + filteredPersonalDetails = []; + } else { + filteredPersonalDetails = orderedPersonalDetails; } - const sortedRecentReports = orderOptions(filteredRecentReports, searchValue, {preferChatroomsOverThreads, preferPolicyExpenseChat, preferRecentExpenseReports}); + const orderedReports = orderReportOptionsWithSearch(filteredReports, searchInputValue, config); + return { + recentReports: orderedReports, personalDetails: filteredPersonalDetails, - recentReports: sortedRecentReports, - userToInvite, - currentUserOption: matchResults.currentUserOption, + userToInvite: filterResult.userToInvite, + currentUserOption: filterResult.currentUserOption, }; } @@ -1727,7 +1794,7 @@ export { getAvatarsForAccountIDs, isCurrentUser, isPersonalDetailsReady, - getOptions, + getValidOptions, getSearchOptions, getShareDestinationOptions, getMemberInviteOptions, @@ -1752,7 +1819,11 @@ export { getShareLogOptions, filterOptions, filteredPersonalDetailsOfRecentReports, + orderReportOptions, + orderReportOptionsWithSearch, + orderPersonalDetailsOptions, orderOptions, + filterAndOrderOptions, createOptionList, createOptionFromReport, getReportOption, diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index 8cb683a8f200..2a7f1d0cb81b 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -70,7 +70,7 @@ function InviteReportParticipantsPage({betas, report, didScreenTransitionEnd}: I }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails, options.reports]); const inviteOptions = useMemo( - () => OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, {excludeLogins: excludedUsers}), + () => OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchTerm, {excludeLogins: excludedUsers}), [debouncedSearchTerm, defaultOptions, excludedUsers], ); diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 89715702c49d..65aa253ff09d 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -50,7 +50,7 @@ function useOptions() { }); const defaultOptions = useMemo(() => { - const filteredOptions = OptionsListUtils.getOptions( + const filteredOptions = OptionsListUtils.getValidOptions( { reports: listOptions.reports ?? [], personalDetails: listOptions.personalDetails ?? [], @@ -58,7 +58,6 @@ function useOptions() { { betas: betas ?? [], selectedOptions, - maxRecentReportsToShow: 0, includeSelfDM: true, }, ); @@ -66,7 +65,7 @@ function useOptions() { }, [betas, listOptions.personalDetails, listOptions.reports, selectedOptions]); const options = useMemo(() => { - const filteredOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, { + const filteredOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchTerm, { selectedOptions, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index fd737ba6ee48..79d7cfe4acc5 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -102,7 +102,7 @@ function RoomInvitePage({ if (debouncedSearchTerm.trim() === '') { return defaultOptions; } - const filteredOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, {excludeLogins: excludedUsers}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchTerm, {excludeLogins: excludedUsers}); return filteredOptions; }, [debouncedSearchTerm, defaultOptions, excludedUsers]); diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index a47129e5064d..db28fb46d165 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -85,11 +85,13 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde action, ); if (isPaidGroupPolicy) { - optionList.recentReports = OptionsListUtils.orderOptions(optionList.recentReports, searchTerm, { + const orderedOptions = OptionsListUtils.orderOptions(optionList, searchTerm, { preferChatroomsOverThreads: true, preferPolicyExpenseChat: !!action, preferRecentExpenseReports: action === CONST.IOU.ACTION.CREATE, }); + optionList.recentReports = orderedOptions.recentReports; + optionList.personalDetails = orderedOptions.personalDetails; } if (optionList.currentUserOption && !isCurrentUserAttendee) { optionList.recentReports = [optionList.currentUserOption, ...optionList.personalDetails]; @@ -121,7 +123,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde headerMessage: '', }; } - const newOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, { + const newOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchTerm, { excludeLogins: CONST.EXPENSIFY_EMAILS, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, preferPolicyExpenseChat: isPaidGroupPolicy, diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index bf5101848cd9..07e34d9692b1 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -105,7 +105,7 @@ function MoneyRequestParticipantsSelector({ }; } - const optionList = OptionsListUtils.getOptions( + const optionList = OptionsListUtils.getValidOptions( { reports: options.reports, personalDetails: options.personalDetails, @@ -123,20 +123,16 @@ function MoneyRequestParticipantsSelector({ includeP2P: !isCategorizeOrShareAction, includeInvoiceRooms: iouType === CONST.IOU.TYPE.INVOICE, action, - maxRecentReportsToShow: 0, }, ); - if (isPaidGroupPolicy) { - optionList.recentReports = OptionsListUtils.orderOptions(optionList.recentReports, undefined, { - preferChatroomsOverThreads: true, - preferPolicyExpenseChat: !!action, - preferRecentExpenseReports: action === CONST.IOU.ACTION.CREATE, - }); - } + const orderedOptions = OptionsListUtils.orderOptions(optionList); - return optionList; - }, [action, areOptionsInitialized, betas, didScreenTransitionEnd, iouType, isCategorizeOrShareAction, isPaidGroupPolicy, options.personalDetails, options.reports, participants]); + return { + ...optionList, + ...orderedOptions, + }; + }, [action, areOptionsInitialized, betas, didScreenTransitionEnd, iouType, isCategorizeOrShareAction, options.personalDetails, options.reports, participants]); const chatOptions = useMemo(() => { if (!areOptionsInitialized) { @@ -149,7 +145,7 @@ function MoneyRequestParticipantsSelector({ }; } - const newOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, { + const newOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchTerm, { canInviteUser: !isCategorizeOrShareAction, selectedOptions: participants as Participant[], excludeLogins: CONST.EXPENSIFY_EMAILS, diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 2b79d441e686..47b418179049 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -56,7 +56,7 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { return defaultOptions; } - const filteredOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchValue, { + const filteredOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchValue, { preferChatroomsOverThreads: true, sortByReportTypeInSearch: true, }); diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 45efa45e4e3d..9be88b4c0814 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -28,7 +28,7 @@ function useOptions() { const existingDelegates = useMemo(() => account?.delegatedAccess?.delegates?.map((delegate) => delegate.email) ?? [], [account?.delegatedAccess?.delegates]); const defaultOptions = useMemo(() => { - const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getOptions( + const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getValidOptions( { reports: optionsList.reports, personalDetails: optionsList.personalDetails, @@ -36,7 +36,6 @@ function useOptions() { { betas, excludeLogins: [...CONST.EXPENSIFY_EMAILS, ...existingDelegates], - maxRecentReportsToShow: 0, }, ); @@ -57,7 +56,7 @@ function useOptions() { }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); const options = useMemo(() => { - const filteredOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchValue.trim(), { + const filteredOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { excludeLogins: [...CONST.EXPENSIFY_EMAILS, ...existingDelegates], maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index a728ae7fb9ff..fdb59b0ead0c 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -40,7 +40,7 @@ function useOptions() { const {options: optionsList, areOptionsInitialized} = useOptionsList(); const defaultOptions = useMemo(() => { - const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getOptions( + const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getValidOptions( { reports: optionsList.reports, personalDetails: optionsList.personalDetails, @@ -48,7 +48,6 @@ function useOptions() { { betas, excludeLogins: CONST.EXPENSIFY_EMAILS, - maxRecentReportsToShow: 0, }, ); @@ -69,7 +68,7 @@ function useOptions() { }, [optionsList.reports, optionsList.personalDetails, betas, isLoading]); const options = useMemo(() => { - const filteredOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchValue.trim(), { + const filteredOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { excludeLogins: CONST.EXPENSIFY_EMAILS, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 9b85337aedcd..543afb4ba002 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -82,7 +82,7 @@ function TaskShareDestinationSelectorModal() { if (debouncedSearchValue.trim() === '') { return defaultOptions; } - const filteredReports = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchValue.trim(), { + const filteredReports = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { excludeLogins: CONST.EXPENSIFY_EMAILS, canInviteUser: false, }); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 76dd8e52a78e..ca09300ca0d9 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -88,7 +88,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); const inviteOptions = useMemo( - () => OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, {excludeLogins: excludedUsers}), + () => OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchTerm, {excludeLogins: excludedUsers}), [debouncedSearchTerm, defaultOptions, excludedUsers], ); diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index 19629c602477..11a756041a3b 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -108,8 +108,8 @@ describe('OptionsListUtils', () => { test('[OptionsListUtils] getFilteredOptions with search value', async () => { await waitForBatchedUpdates(); await measureFunction(() => { - const formattedOptions = OptionsListUtils.getOptions({reports: options.reports, personalDetails: options.personalDetails}, {betas: mockedBetas}); - OptionsListUtils.filterOptions(formattedOptions, SEARCH_VALUE); + const formattedOptions = OptionsListUtils.getValidOptions({reports: options.reports, personalDetails: options.personalDetails}, {betas: mockedBetas}); + OptionsListUtils.filterAndOrderOptions(formattedOptions, SEARCH_VALUE); }); }); diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index cad6c010bb67..8916b7c3bac8 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -412,14 +412,13 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length); }); - it('getOptions()', () => { - const MAX_RECENT_REPORTS = 5; - - // When we call getOptions() with no search value - let results = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxRecentReportsToShow: MAX_RECENT_REPORTS}); - - // We should expect maximum of 5 recent reports to be returned - expect(results.recentReports.length).toBe(MAX_RECENT_REPORTS); + it('orderOptions()', () => { + // When we call getValidOptions() with no search value + let results: Pick = OptionsListUtils.getValidOptions({ + reports: OPTIONS.reports, + personalDetails: OPTIONS.personalDetails, + }); + results = OptionsListUtils.orderOptions(results); // We should expect all personalDetails except the currently logged in user to be returned // Filtering of personalDetails that have reports is done in filterOptions @@ -442,31 +441,29 @@ describe('OptionsListUtils', () => { expect(personalDetailWithExistingReport?.reportID).toBe('2'); // When we only pass personal details - results = OptionsListUtils.getOptions({personalDetails: OPTIONS.personalDetails, reports: []}); + results = OptionsListUtils.getValidOptions({personalDetails: OPTIONS.personalDetails, reports: []}); + results = OptionsListUtils.orderOptions(results); // We should expect personal details sorted alphabetically expect(results.personalDetails.at(0)?.text).toBe('Black Panther'); expect(results.personalDetails.at(1)?.text).toBe('Black Widow'); expect(results.personalDetails.at(2)?.text).toBe('Captain America'); expect(results.personalDetails.at(3)?.text).toBe('Invisible Woman'); + }); + it('getValidOptions()', () => { // When we don't include personal detail to the result - results = OptionsListUtils.getOptions( - { - personalDetails: [], - reports: [], - }, - { - maxRecentReportsToShow: 0, - }, - ); + let results = OptionsListUtils.getValidOptions({ + personalDetails: [], + reports: [], + }); // Then no personal detail options will be returned expect(results.personalDetails.length).toBe(0); // Test for Concierge's existence in chat options - results = OptionsListUtils.getOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}); + results = OptionsListUtils.getValidOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the currently logged in user) @@ -475,7 +472,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getOptions( + results = OptionsListUtils.getValidOptions( { reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails, @@ -491,7 +488,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getOptions({reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, {excludeLogins: [CONST.EMAIL.CHRONOS]}); + results = OptionsListUtils.getValidOptions({reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, {excludeLogins: [CONST.EMAIL.CHRONOS]}); // All the personalDetails should be returned minus the currently logged in user and Concierge // Filtering of personalDetails that have reports is done in filterOptions @@ -499,7 +496,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getOptions( + results = OptionsListUtils.getValidOptions( { reports: OPTIONS_WITH_RECEIPTS.reports, personalDetails: OPTIONS_WITH_RECEIPTS.personalDetails, @@ -515,48 +512,28 @@ describe('OptionsListUtils', () => { expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); - it('getOptions() for group Chat', () => { - // When we call getOptions() with no search value - let results = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + it('getValidOptions() for group Chat', () => { + // When we call getValidOptions() with no search value + let results = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); // We should expect all the personalDetails to show except the currently logged in user // Filtering of personalDetails that have reports is done in filterOptions expect(results.personalDetails.length).toBe(Object.values(OPTIONS.personalDetails).length - 1); - // All personal details including those that have reports should be returned - // We should expect personal details sorted alphabetically - expect(results.personalDetails.at(0)?.text).toBe('Black Panther'); - expect(results.personalDetails.at(1)?.text).toBe('Black Widow'); - expect(results.personalDetails.at(2)?.text).toBe('Captain America'); - expect(results.personalDetails.at(3)?.text).toBe('Invisible Woman'); - expect(results.personalDetails.at(4)?.text).toBe('Mister Fantastic'); - expect(results.personalDetails.at(5)?.text).toBe('Mr Sinister'); - expect(results.personalDetails.at(6)?.text).toBe('Spider-Man'); - expect(results.personalDetails.at(7)?.text).toBe('The Incredible Hulk'); - expect(results.personalDetails.at(8)?.text).toBe('Thor'); - // And none of our personalDetails should include any of the users with recent reports const reportLogins = results.recentReports.map((reportOption) => reportOption.login); const personalDetailsOverlapWithReports = results.personalDetails.every((personalDetailOption) => reportLogins.includes(personalDetailOption.login)); expect(personalDetailsOverlapWithReports).toBe(false); - // When we provide no selected options to getOptions() - results = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxRecentReportsToShow: 5}); - - // Then one of our older report options (not in our five most recent) should appear in the personalDetails - // but not in recentReports - expect(results.recentReports.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); - expect(results.personalDetails.every((option) => option.login !== 'peterparker@expensify.com')).toBe(false); - - // When we provide a "selected" option to getOptions() - results = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {excludeLogins: ['peterparker@expensify.com']}); + // When we provide a "selected" option to getValidOptions() + results = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {excludeLogins: ['peterparker@expensify.com']}); // Then the option should not appear anywhere in either list expect(results.recentReports.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); expect(results.personalDetails.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); // Test Concierge's existence in new group options - results = OptionsListUtils.getOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}); + results = OptionsListUtils.getValidOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the currently logged in user) @@ -565,7 +542,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getOptions( + results = OptionsListUtils.getValidOptions( { reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails, @@ -583,7 +560,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getOptions({reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, {excludeLogins: [CONST.EMAIL.CHRONOS]}); + results = OptionsListUtils.getValidOptions({reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, {excludeLogins: [CONST.EMAIL.CHRONOS]}); // We should expect all the personalDetails to show (minus // the currently logged in user and Concierge) @@ -593,7 +570,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getOptions( + results = OptionsListUtils.getValidOptions( { reports: OPTIONS_WITH_RECEIPTS.reports, personalDetails: OPTIONS_WITH_RECEIPTS.personalDetails, @@ -673,10 +650,10 @@ describe('OptionsListUtils', () => { expect(formattedMembers.every((personalDetail) => !personalDetail.isDisabled)).toBe(true); }); - describe('filterOptions', () => { + describe('filterAndOrderOptions', () => { it('should return all options when search is empty', () => { const options = OptionsListUtils.getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, ''); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, ''); expect(filteredOptions.recentReports.length + filteredOptions.personalDetails.length).toBe(12); }); @@ -685,19 +662,26 @@ describe('OptionsListUtils', () => { const searchText = 'man'; const options = OptionsListUtils.getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {sortByReportTypeInSearch: true}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText, {sortByReportTypeInSearch: true}); + + // When sortByReportTypeInSearch is true, we expect all options to be part of the recentReports list and reports should be first: + expect(filteredOptions.personalDetails.length).toBe(0); + + // Expect to only find reports that matched our search text: expect(filteredOptions.recentReports.length).toBe(4); - expect(filteredOptions.recentReports.at(0)?.text).toBe('Invisible Woman'); - expect(filteredOptions.recentReports.at(1)?.text).toBe('Spider-Man'); - expect(filteredOptions.recentReports.at(2)?.text).toBe('Black Widow'); - expect(filteredOptions.recentReports.at(3)?.text).toBe('Mister Fantastic, Invisible Woman'); + + // This items should be ordered by most recent action (and other criteria such as whether they are archived): + expect(filteredOptions.recentReports.at(0)?.text).toBe('Invisible Woman'); // '2022-11-22 03:26:02.019' + expect(filteredOptions.recentReports.at(1)?.text).toBe('Spider-Man'); // '2022-11-22 03:26:02.016' + expect(filteredOptions.recentReports.at(2)?.text).toBe('Black Widow'); // This is a personal detail, which has no lastVisibleActionCreated, but matches the login + expect(filteredOptions.recentReports.at(3)?.text).toBe('Mister Fantastic, Invisible Woman'); // This again is a report with '2022-11-22 03:26:02.015' }); it('should filter users by email', () => { const searchText = 'mistersinister@marauders.com'; const options = OptionsListUtils.getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports.at(0)?.text).toBe('Mr Sinister'); @@ -706,7 +690,7 @@ describe('OptionsListUtils', () => { it('should find archived chats', () => { const searchText = 'Archived'; const options = OptionsListUtils.getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(1); expect(!!filteredOptions.recentReports.at(0)?.private_isArchived).toBe(true); @@ -717,7 +701,7 @@ describe('OptionsListUtils', () => { const OPTIONS_WITH_PERIODS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_PERIODS, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {sortByReportTypeInSearch: true}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText, {sortByReportTypeInSearch: true}); expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports.at(0)?.login).toBe('barry.allen@expensify.com'); @@ -727,7 +711,7 @@ describe('OptionsListUtils', () => { const searchText = 'avengers'; const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_WORKSPACE_ROOM, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports.at(0)?.subtitle).toBe('Avengers Room'); @@ -737,7 +721,7 @@ describe('OptionsListUtils', () => { const searchText = 'reedrichards@expensify.com'; const options = OptionsListUtils.getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports.at(0)?.login).toBe(searchText); @@ -748,7 +732,7 @@ describe('OptionsListUtils', () => { const OPTIONS_WITH_CHATROOMS = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_CHAT_ROOM); const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_CHATROOMS, [CONST.BETAS.ALL]); - const filterOptions = OptionsListUtils.filterOptions(options, searchText); + const filterOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filterOptions.recentReports.length).toBe(2); expect(filterOptions.recentReports.at(1)?.isChatRoom).toBe(true); @@ -758,7 +742,7 @@ describe('OptionsListUtils', () => { const searchText = 'fantastic'; const options = OptionsListUtils.getSearchOptions(OPTIONS); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(2); expect(filteredOptions.recentReports.at(0)?.text).toBe('Mister Fantastic'); @@ -769,7 +753,7 @@ describe('OptionsListUtils', () => { const searchText = 'test@email.com'; const options = OptionsListUtils.getSearchOptions(OPTIONS); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filteredOptions.userToInvite?.login).toBe(searchText); }); @@ -777,8 +761,8 @@ describe('OptionsListUtils', () => { it('should not return any results if the search value is on an exluded logins list', () => { const searchText = 'admin@expensify.com'; - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {excludeLogins: CONST.EXPENSIFY_EMAILS}); - const filterOptions = OptionsListUtils.filterOptions(options, searchText, {excludeLogins: CONST.EXPENSIFY_EMAILS}); + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {excludeLogins: CONST.EXPENSIFY_EMAILS}); + const filterOptions = OptionsListUtils.filterAndOrderOptions(options, searchText, {excludeLogins: CONST.EXPENSIFY_EMAILS}); expect(filterOptions.recentReports.length).toBe(0); }); @@ -786,7 +770,7 @@ describe('OptionsListUtils', () => { const searchText = 'test@email.com'; const options = OptionsListUtils.getSearchOptions(OPTIONS); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {excludeLogins: CONST.EXPENSIFY_EMAILS}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText, {excludeLogins: CONST.EXPENSIFY_EMAILS}); expect(filteredOptions.userToInvite?.login).toBe(searchText); }); @@ -795,29 +779,33 @@ describe('OptionsListUtils', () => { const searchText = ''; const options = OptionsListUtils.getSearchOptions(OPTIONS); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {maxRecentReportsToShow: 2}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText, {maxRecentReportsToShow: 2}); + + // Note: in the past maxRecentReportsToShow: 0 would return all recent reports, this has changed, and is expected to return none now + const limitToZeroOptions = OptionsListUtils.filterAndOrderOptions(options, searchText, {maxRecentReportsToShow: 0}); expect(filteredOptions.recentReports.length).toBe(2); + expect(limitToZeroOptions.recentReports.length).toBe(0); }); it('should not return any user to invite if email exists on the personal details list', () => { const searchText = 'natasharomanoff@expensify.com'; const options = OptionsListUtils.getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, searchText); expect(filteredOptions.personalDetails.length).toBe(1); expect(filteredOptions.userToInvite).toBe(null); }); it('should not return any options if search value does not match any personal details (getMemberInviteOptions)', () => { const options = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, []); - const filteredOptions = OptionsListUtils.filterOptions(options, 'magneto'); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'magneto'); expect(filteredOptions.personalDetails.length).toBe(0); }); it('should return one personal detail if search value matches an email (getMemberInviteOptions)', () => { const options = OptionsListUtils.getMemberInviteOptions(OPTIONS.personalDetails, []); - const filteredOptions = OptionsListUtils.filterOptions(options, 'peterparker@expensify.com'); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'peterparker@expensify.com'); expect(filteredOptions.personalDetails.length).toBe(1); expect(filteredOptions.personalDetails.at(0)?.text).toBe('Spider-Man'); @@ -833,7 +821,7 @@ describe('OptionsListUtils', () => { return filtered; }, []); const options = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, []); - const filteredOptions = OptionsListUtils.filterOptions(options, 'mutants'); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'mutants'); expect(filteredOptions.recentReports.length).toBe(0); }); @@ -849,7 +837,7 @@ describe('OptionsListUtils', () => { }, []); const options = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, []); - const filteredOptions = OptionsListUtils.filterOptions(options, 'Avengers Room'); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'Avengers Room'); expect(filteredOptions.recentReports.length).toBe(1); }); @@ -865,14 +853,14 @@ describe('OptionsListUtils', () => { }, []); const options = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, []); - const filteredOptions = OptionsListUtils.filterOptions(options, 'Mutants Lair'); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'Mutants Lair'); expect(filteredOptions.recentReports.length).toBe(0); }); - it('should show the option from personal details when searching for personal detail with no existing report (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, 'hulk'); + it('should show the option from personal details when searching for personal detail with no existing report', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'hulk'); expect(filteredOptions.recentReports.length).toBe(0); @@ -880,46 +868,35 @@ describe('OptionsListUtils', () => { expect(filteredOptions.personalDetails.at(0)?.login).toBe('brucebanner@expensify.com'); }); - it('should return all matching reports and personal details (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxRecentReportsToShow: 5}); - const filteredOptions = OptionsListUtils.filterOptions(options, '.com'); - - expect(filteredOptions.recentReports.at(0)?.text).toBe('Captain America'); - - // We expect that only personal details that are not in the reports are included here - expect(filteredOptions.personalDetails.length).toBe(4); - expect(filteredOptions.personalDetails.at(0)?.login).toBe('natasharomanoff@expensify.com'); - }); - - it('should not return any options or user to invite if there are no search results and the string does not match a potential email or phone (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, 'marc@expensify'); + it('should not return any options or user to invite if there are no search results and the string does not match a potential email or phone', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'marc@expensify'); expect(filteredOptions.recentReports.length).toBe(0); expect(filteredOptions.personalDetails.length).toBe(0); expect(filteredOptions.userToInvite).toBe(null); }); - it('should not return any options but should return an user to invite if no matching options exist and the search value is a potential email (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, 'marc@expensify.com'); + it('should not return any options but should return an user to invite if no matching options exist and the search value is a potential email', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'marc@expensify.com'); expect(filteredOptions.recentReports.length).toBe(0); expect(filteredOptions.personalDetails.length).toBe(0); expect(filteredOptions.userToInvite).not.toBe(null); }); - it('should return user to invite when search term has a period with options for it that do not contain the period (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, 'peter.parker@expensify.com'); + it('should return user to invite when search term has a period with options for it that do not contain the period', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'peter.parker@expensify.com'); expect(filteredOptions.recentReports.length).toBe(0); expect(filteredOptions.userToInvite).not.toBe(null); }); - it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, '5005550006'); + it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, '5005550006'); expect(filteredOptions.recentReports.length).toBe(0); expect(filteredOptions.personalDetails.length).toBe(0); @@ -927,9 +904,9 @@ describe('OptionsListUtils', () => { expect(filteredOptions.userToInvite?.login).toBe('+15005550006'); }); - it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with country code added (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, '+15005550006'); + it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with country code added', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, '+15005550006'); expect(filteredOptions.recentReports.length).toBe(0); expect(filteredOptions.personalDetails.length).toBe(0); @@ -937,9 +914,9 @@ describe('OptionsListUtils', () => { expect(filteredOptions.userToInvite?.login).toBe('+15005550006'); }); - it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with special characters added (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, '+1 (800)324-3233'); + it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with special characters added', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, '+1 (800)324-3233'); expect(filteredOptions.recentReports.length).toBe(0); expect(filteredOptions.personalDetails.length).toBe(0); @@ -947,37 +924,37 @@ describe('OptionsListUtils', () => { expect(filteredOptions.userToInvite?.login).toBe('+18003243233'); }); - it('should not return any options or user to invite if contact number contains alphabet characters (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, '998243aaaa'); + it('should not return any options or user to invite if contact number contains alphabet characters', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, '998243aaaa'); expect(filteredOptions.recentReports.length).toBe(0); expect(filteredOptions.personalDetails.length).toBe(0); expect(filteredOptions.userToInvite).toBe(null); }); - it('should not return any options if search value does not match any personal details (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, 'magneto'); + it('should not return any options if search value does not match any personal details', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'magneto'); expect(filteredOptions.personalDetails.length).toBe(0); }); - it('should return one recent report and no personal details if a search value provides an email (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const filteredOptions = OptionsListUtils.filterOptions(options, 'peterparker@expensify.com', {sortByReportTypeInSearch: true}); + it('should return one recent report and no personal details if a search value provides an email', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'peterparker@expensify.com', {sortByReportTypeInSearch: true}); expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports.at(0)?.text).toBe('Spider-Man'); expect(filteredOptions.personalDetails.length).toBe(0); }); - it('should return all matching reports and personal details (getOptions)', () => { - const options = OptionsListUtils.getOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxRecentReportsToShow: 5}); - const filteredOptions = OptionsListUtils.filterOptions(options, '.com'); + it('should return all matching reports and personal details', () => { + const options = OptionsListUtils.getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, '.com', {maxRecentReportsToShow: 5}); expect(filteredOptions.personalDetails.length).toBe(4); - expect(filteredOptions.recentReports.length).toBe(5); expect(filteredOptions.personalDetails.at(0)?.login).toBe('natasharomanoff@expensify.com'); + expect(filteredOptions.recentReports.length).toBe(5); expect(filteredOptions.recentReports.at(0)?.text).toBe('Captain America'); expect(filteredOptions.recentReports.at(1)?.text).toBe('Mr Sinister'); expect(filteredOptions.recentReports.at(2)?.text).toBe('Black Panther'); @@ -985,7 +962,7 @@ describe('OptionsListUtils', () => { it('should return matching option when searching (getSearchOptions)', () => { const options = OptionsListUtils.getSearchOptions(OPTIONS); - const filteredOptions = OptionsListUtils.filterOptions(options, 'spider'); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'spider'); expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports.at(0)?.text).toBe('Spider-Man'); @@ -993,7 +970,7 @@ describe('OptionsListUtils', () => { it('should return latest lastVisibleActionCreated item on top when search value matches multiple items (getSearchOptions)', () => { const options = OptionsListUtils.getSearchOptions(OPTIONS); - const filteredOptions = OptionsListUtils.filterOptions(options, 'fantastic'); + const filteredOptions = OptionsListUtils.filterAndOrderOptions(options, 'fantastic'); expect(filteredOptions.recentReports.length).toBe(2); expect(filteredOptions.recentReports.at(0)?.text).toBe('Mister Fantastic'); @@ -1004,7 +981,7 @@ describe('OptionsListUtils', () => { .then(() => { const OPTIONS_WITH_PERIODS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); const results = OptionsListUtils.getSearchOptions(OPTIONS_WITH_PERIODS); - const filteredResults = OptionsListUtils.filterOptions(results, 'barry.allen@expensify.com', {sortByReportTypeInSearch: true}); + const filteredResults = OptionsListUtils.filterAndOrderOptions(results, 'barry.allen@expensify.com', {sortByReportTypeInSearch: true}); expect(filteredResults.recentReports.length).toBe(1); expect(filteredResults.recentReports.at(0)?.text).toBe('The Flash'); @@ -1018,7 +995,6 @@ describe('OptionsListUtils', () => { recentReportOptions: OPTIONS.reports, personalDetailsOptions: OPTIONS.personalDetails, currentUserOption: null, - excludeUnknownUsers: false, }); expect(canCreate).toBe(false);