From de2385cc58e4085c1091d4e248591c4d28f69974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 19 Jan 2025 20:36:52 +0100 Subject: [PATCH 1/4] separate getValidPersonalDetailOptions function --- src/libs/OptionsListUtils.ts | 75 ++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 66d1198a2423..d0829bf606f9 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1383,6 +1383,48 @@ function getValidReports( return validReportOptions; } +function getValidPersonalDetailOptions( + options: OptionList['personalDetails'], + { + loginsToExclude = {}, + includeDomainEmail, + shouldBoldTitleByDefault = false, + currentUserRef, + }: { + loginsToExclude?: Record; + includeDomainEmail?: boolean; + shouldBoldTitleByDefault: boolean; + // If the current user is found in the options and you pass an object ref, it will be assigned + currentUserRef?: { + current?: OptionData; + }; + }, +) { + const personalDetailsOptions: OptionData[] = []; + for (let i = 0; i < options.length; i++) { + // eslint-disable-next-line rulesdir/prefer-at + const detail = options[i]; + if (!detail?.login || !detail.accountID || !!detail?.isOptimisticPersonalDetail || (!includeDomainEmail && Str.isDomainEmail(detail.login))) { + continue; + } + + if (currentUserRef && !!currentUserLogin && detail.login === currentUserLogin) { + // eslint-disable-next-line no-param-reassign + currentUserRef.current = detail; + } + + if (loginsToExclude[detail.login]) { + continue; + } + + detail.isBold = shouldBoldTitleByDefault; + + personalDetailsOptions.push(detail); + } + + return personalDetailsOptions; +} + /** * Options are reports and personal details. This function filters out the options that are not valid to be displayed. */ @@ -1433,8 +1475,10 @@ function getValidOptions( } // Get valid personal details and check if we can find the current user: - const personalDetailsOptions: OptionData[] = []; - let currentUserOption: OptionData | undefined; + let personalDetailsOptions: OptionData[] = []; + const currentUserRef = { + current: undefined as OptionData | undefined, + }; if (includeP2P) { let personalDetailLoginsToExclude = loginsToExclude; if (currentUserLogin) { @@ -1443,31 +1487,19 @@ function getValidOptions( [currentUserLogin]: true, }; } - for (let i = 0; i < options.personalDetails.length; i++) { - // eslint-disable-next-line rulesdir/prefer-at - const detail = options.personalDetails[i]; - if (!detail?.login || !detail.accountID || !!detail?.isOptimisticPersonalDetail || (!includeDomainEmail && Str.isDomainEmail(detail.login))) { - continue; - } - - if (!!currentUserLogin && detail.login === currentUserLogin) { - currentUserOption = detail; - } - if (personalDetailLoginsToExclude[detail.login]) { - continue; - } - - detail.isBold = shouldBoldTitleByDefault; - - personalDetailsOptions.push(detail); - } + personalDetailsOptions = getValidPersonalDetailOptions(options.personalDetails, { + loginsToExclude: personalDetailLoginsToExclude, + shouldBoldTitleByDefault, + includeDomainEmail, + currentUserRef, + }); } return { personalDetails: personalDetailsOptions, recentReports: recentReportOptions, - currentUserOption, + currentUserOption: currentUserRef.current, // User to invite is generated by the search input of a user. // As this function isn't concerned with any search input yet, this is null (will be set when using filterOptions). userToInvite: null, @@ -1976,6 +2008,7 @@ export { isCurrentUser, isPersonalDetailsReady, getValidOptions, + getValidPersonalDetailOptions, getSearchOptions, getShareDestinationOptions, getMemberInviteOptions, From c5653c8acb6b0b32de575be487fe3bf221726587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 19 Jan 2025 20:43:32 +0100 Subject: [PATCH 2/4] perf: Only filter for personal details Reducing work by using the correct function as we are only interested in personal details here --- .../Search/SearchRouter/SearchRouterList.tsx | 34 ++++++++----------- src/libs/OptionsListUtils.ts | 2 +- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index aa37fb1e34a0..2496934813a4 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -19,7 +19,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {searchInServer} from '@libs/actions/Report'; import {getCardDescription, isCard, isCardHiddenFromSearch, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; -import {combineOrderingOfReportsAndPersonalDetails, getSearchOptions, getValidOptions} from '@libs/OptionsListUtils'; +import {combineOrderingOfReportsAndPersonalDetails, getSearchOptions, getValidOptions, getValidPersonalDetailOptions} from '@libs/OptionsListUtils'; import type {Options, SearchOption} from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import {getAllTaxRates} from '@libs/PolicyUtils'; @@ -158,39 +158,35 @@ function SearchRouterList( return []; } - const filteredOptions = getValidOptions( - { - reports: options.reports, - personalDetails: options.personalDetails, - }, - { - excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - includeSelfDM: true, - showChatPreviewLine: true, - shouldBoldTitleByDefault: false, - }, - ); + const currentUserRef = { + current: undefined as OptionData | undefined, + }; + const filteredOptions = getValidPersonalDetailOptions(options.personalDetails, { + loginsToExclude: CONST.EXPENSIFY_EMAILS_OBJECT, + shouldBoldTitleByDefault: false, + currentUserRef, + }); // This cast is needed as something is incorrect in types OptionsListUtils.getOptions around l1490 and includeRecentReports types - const personalDetailsFromOptions = filteredOptions.personalDetails.map((option) => (option as SearchOption).item); + const personalDetailsFromOptions = filteredOptions.map((option) => (option as SearchOption).item); const autocompleteOptions = Object.values(personalDetailsFromOptions) - .filter((details): details is NonNullable => !!(details && details?.login)) + .filter((details): details is NonNullable => !!details?.login) .map((details) => { return { name: details.displayName ?? Str.removeSMSDomain(details.login ?? ''), accountID: details.accountID.toString(), }; }); - const currentUser = filteredOptions.currentUserOption ? (filteredOptions.currentUserOption as SearchOption).item : undefined; - if (currentUser) { + const currentUser = currentUserRef.current; + if (currentUser && currentUser.accountID) { autocompleteOptions.push({ name: currentUser.displayName ?? Str.removeSMSDomain(currentUser.login ?? ''), - accountID: currentUser.accountID?.toString(), + accountID: currentUser.accountID.toString(), }); } return autocompleteOptions; - }, [areOptionsInitialized, options.personalDetails, options.reports]); + }, [areOptionsInitialized, options.personalDetails]); const taxAutocompleteList = useMemo(() => getAutocompleteTaxList(taxRates, policy), [policy, taxRates]); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index d0829bf606f9..8d0e8b84b8d6 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1387,7 +1387,7 @@ function getValidPersonalDetailOptions( options: OptionList['personalDetails'], { loginsToExclude = {}, - includeDomainEmail, + includeDomainEmail = false, shouldBoldTitleByDefault = false, currentUserRef, }: { From 311a3b0e134c45ec0825661439afe4c924d0cd5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 19 Jan 2025 20:49:57 +0100 Subject: [PATCH 3/4] perf: only calculate participants for autocomplete when needed --- .../Search/SearchRouter/SearchRouterList.tsx | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 2496934813a4..37b4e9b45c64 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -19,7 +19,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {searchInServer} from '@libs/actions/Report'; import {getCardDescription, isCard, isCardHiddenFromSearch, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; -import {combineOrderingOfReportsAndPersonalDetails, getSearchOptions, getValidOptions, getValidPersonalDetailOptions} from '@libs/OptionsListUtils'; +import memoize from '@libs/memoize'; +import {combineOrderingOfReportsAndPersonalDetails, getSearchOptions, getValidPersonalDetailOptions} from '@libs/OptionsListUtils'; import type {Options, SearchOption} from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import {getAllTaxRates} from '@libs/PolicyUtils'; @@ -153,40 +154,44 @@ function SearchRouterList( const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList), [userCardList, workspaceCardFeeds]); const cardAutocompleteList = Object.values(allCards); - const participantsAutocompleteList = useMemo(() => { - if (!areOptionsInitialized) { - return []; - } - - const currentUserRef = { - current: undefined as OptionData | undefined, - }; - const filteredOptions = getValidPersonalDetailOptions(options.personalDetails, { - loginsToExclude: CONST.EXPENSIFY_EMAILS_OBJECT, - shouldBoldTitleByDefault: false, - currentUserRef, - }); + const getParticipantsAutocompleteList = useMemo( + () => + memoize(() => { + if (!areOptionsInitialized) { + return []; + } - // This cast is needed as something is incorrect in types OptionsListUtils.getOptions around l1490 and includeRecentReports types - const personalDetailsFromOptions = filteredOptions.map((option) => (option as SearchOption).item); - const autocompleteOptions = Object.values(personalDetailsFromOptions) - .filter((details): details is NonNullable => !!details?.login) - .map((details) => { - return { - name: details.displayName ?? Str.removeSMSDomain(details.login ?? ''), - accountID: details.accountID.toString(), + const currentUserRef = { + current: undefined as OptionData | undefined, }; - }); - const currentUser = currentUserRef.current; - if (currentUser && currentUser.accountID) { - autocompleteOptions.push({ - name: currentUser.displayName ?? Str.removeSMSDomain(currentUser.login ?? ''), - accountID: currentUser.accountID.toString(), - }); - } - - return autocompleteOptions; - }, [areOptionsInitialized, options.personalDetails]); + const filteredOptions = getValidPersonalDetailOptions(options.personalDetails, { + loginsToExclude: CONST.EXPENSIFY_EMAILS_OBJECT, + shouldBoldTitleByDefault: false, + currentUserRef, + }); + + // This cast is needed as something is incorrect in types OptionsListUtils.getOptions around l1490 and includeRecentReports types + const personalDetailsFromOptions = filteredOptions.map((option) => (option as SearchOption).item); + const autocompleteOptions = Object.values(personalDetailsFromOptions) + .filter((details): details is NonNullable => !!details?.login) + .map((details) => { + return { + name: details.displayName ?? Str.removeSMSDomain(details.login ?? ''), + accountID: details.accountID.toString(), + }; + }); + const currentUser = currentUserRef.current; + if (currentUser && currentUser.accountID) { + autocompleteOptions.push({ + name: currentUser.displayName ?? Str.removeSMSDomain(currentUser.login ?? ''), + accountID: currentUser.accountID.toString(), + }); + } + + return autocompleteOptions; + }), + [areOptionsInitialized, options.personalDetails], + ); const taxAutocompleteList = useMemo(() => getAutocompleteTaxList(taxRates, policy), [policy, taxRates]); @@ -272,7 +277,7 @@ function SearchRouterList( })); } case CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM: { - const filteredParticipants = participantsAutocompleteList + const filteredParticipants = getParticipantsAutocompleteList() .filter((participant) => participant.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(participant.name.toLowerCase())) .slice(0, 10); @@ -284,7 +289,7 @@ function SearchRouterList( })); } case CONST.SEARCH.SYNTAX_FILTER_KEYS.TO: { - const filteredParticipants = participantsAutocompleteList + const filteredParticipants = getParticipantsAutocompleteList() .filter((participant) => participant.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(participant.name.toLowerCase())) .slice(0, 10); @@ -359,7 +364,7 @@ function SearchRouterList( currencyAutocompleteList, recentCurrencyAutocompleteList, taxAutocompleteList, - participantsAutocompleteList, + getParticipantsAutocompleteList, searchOptions.recentReports, typeAutocompleteList, statusAutocompleteList, From d803219dbca1eeb00afcc53c72a1097c9a949868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Mar 2025 20:24:38 +0100 Subject: [PATCH 4/4] fix after merge --- src/libs/OptionsListUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6c681ef1606d..dba6e86adfce 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2254,6 +2254,7 @@ export { filterReports, getIsUserSubmittedExpenseOrScannedReceipt, getManagerMcTestParticipant, + isSelectedManagerMcTest, }; export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Option, OptionTree, ReportAndPersonalDetailOptions, GetUserToInviteConfig};