Skip to content
81 changes: 41 additions & 40 deletions src/components/Search/SearchAutocompleteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<PersonalDetails>).item);
const autocompleteOptions = Object.values(personalDetailsFromOptions)
.filter((details): details is NonNullable<PersonalDetails> => !!(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<PersonalDetails>).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<PersonalDetails>).item);
const autocompleteOptions = Object.values(personalDetailsFromOptions)
.filter((details): details is NonNullable<PersonalDetails> => !!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]);

Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand Down Expand Up @@ -403,7 +404,7 @@ function SearchAutocompleteList(
currencyAutocompleteList,
recentCurrencyAutocompleteList,
taxAutocompleteList,
participantsAutocompleteList,
getParticipantsAutocompleteList,
searchOptions.recentReports,
typeAutocompleteList,
statusAutocompleteList,
Expand Down
97 changes: 69 additions & 28 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean>;
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.
*/
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -2164,6 +2203,7 @@ export {
isCurrentUser,
isPersonalDetailsReady,
getValidOptions,
getValidPersonalDetailOptions,
getSearchOptions,
getShareDestinationOptions,
getMemberInviteOptions,
Expand Down Expand Up @@ -2214,6 +2254,7 @@ export {
filterReports,
getIsUserSubmittedExpenseOrScannedReceipt,
getManagerMcTestParticipant,
isSelectedManagerMcTest,
};

export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Option, OptionTree, ReportAndPersonalDetailOptions, GetUserToInviteConfig};