diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 69408ceef921..985079c205d2 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -19,7 +19,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {searchInServer} from '@libs/actions/Report'; import {getCardFeedKey, getCardFeedNamesWithType} from '@libs/CardFeedUtils'; import {getCardDescription, isCard, isCardHiddenFromSearch, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; -import {combineOrderingOfReportsAndPersonalDetails, getSearchOptions, getValidOptions} 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, getCleanedTagName} from '@libs/PolicyUtils'; @@ -169,44 +170,44 @@ function SearchAutocompleteList( }, [translate, workspaceCardFeeds, userCardList]); const feedAutoCompleteList = useMemo(() => Object.entries(cardFeedNamesWithType).map(([cardFeedKey, cardFeedName]) => ({cardFeedKey, cardFeedName})), [cardFeedNamesWithType]); - const participantsAutocompleteList = useMemo(() => { - if (!areOptionsInitialized) { - return []; - } - - const filteredOptions = getValidOptions( - { - reports: options.reports, - personalDetails: options.personalDetails, - }, - { - excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - includeSelfDM: true, - showChatPreviewLine: true, - shouldBoldTitleByDefault: false, - }, - ); + 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.personalDetails.map((option) => (option as SearchOption).item); - const autocompleteOptions = Object.values(personalDetailsFromOptions) - .filter((details): details is NonNullable => !!(details && 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 = filteredOptions.currentUserOption ? (filteredOptions.currentUserOption as SearchOption).item : undefined; - if (currentUser) { - autocompleteOptions.push({ - name: currentUser.displayName ?? Str.removeSMSDomain(currentUser.login ?? ''), - accountID: currentUser.accountID?.toString(), - }); - } - - return autocompleteOptions; - }, [areOptionsInitialized, options.personalDetails, options.reports]); + 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]); @@ -296,7 +297,7 @@ function SearchAutocompleteList( })); } 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); @@ -308,7 +309,7 @@ function SearchAutocompleteList( })); } 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); @@ -403,7 +404,7 @@ function SearchAutocompleteList( currencyAutocompleteList, recentCurrencyAutocompleteList, taxAutocompleteList, - participantsAutocompleteList, + getParticipantsAutocompleteList, searchOptions.recentReports, typeAutocompleteList, statusAutocompleteList, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 659554908ea6..dba6e86adfce 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1468,6 +1468,62 @@ function getIsUserSubmittedExpenseOrScannedReceipt(): boolean { return !!nvpDismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP]; } +/** + * Helper method to check if participant email is Manager McTest + */ +function isSelectedManagerMcTest(email: string | null | undefined): boolean { + return email === CONST.EMAIL.MANAGER_MCTEST; +} + +function getValidPersonalDetailOptions( + options: OptionList['personalDetails'], + { + loginsToExclude = {}, + includeDomainEmail = false, + 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)) || + // Exclude the setup specialist from the list of personal details as it's a fallback if guide is not assigned + detail?.login === CONST.SETUP_SPECIALIST_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. */ @@ -1538,8 +1594,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) { @@ -1548,32 +1606,13 @@ 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)) || - // Exclude the setup specialist from the list of personal details as it's a fallback if guide is not assigned - detail?.login === CONST.SETUP_SPECIALIST_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, + }); } if (excludeHiddenThreads) { @@ -1583,7 +1622,7 @@ function getValidOptions( 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, @@ -2164,6 +2203,7 @@ export { isCurrentUser, isPersonalDetailsReady, getValidOptions, + getValidPersonalDetailOptions, getSearchOptions, getShareDestinationOptions, getMemberInviteOptions, @@ -2214,6 +2254,7 @@ export { filterReports, getIsUserSubmittedExpenseOrScannedReceipt, getManagerMcTestParticipant, + isSelectedManagerMcTest, }; export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Option, OptionTree, ReportAndPersonalDetailOptions, GetUserToInviteConfig};