From 977e1226c0118110ab30a65b1f336bbf0a142ef4 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 12 Apr 2024 10:10:06 +0200 Subject: [PATCH 01/31] add filtering to money requests --- src/libs/OptionsListUtils.ts | 18 +++-- src/pages/SearchPage/index.tsx | 2 +- ...yForRefactorRequestParticipantsSelector.js | 69 ++++++++++++------- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 280ba825761f..98800cce03ca 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2219,7 +2219,7 @@ function formatSectionsFromSearchTerm( /** * Filters options based on the search input value */ -function filterOptions(options: Options, searchInputValue: string): Options { +function filterOptions(options: Options, searchInputValue: string, {sortByReportTypeInSearch = false}: Partial): Options { const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; @@ -2263,12 +2263,14 @@ function filterOptions(options: Options, searchInputValue: string): Options { if (item.alternateText) { values.push(item.alternateText); } + values = values.concat(getParticipantsLoginsArray(item)); } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { if (item.subtitle) { values.push(item.subtitle); } + } else { + values = values.concat(getParticipantsLoginsArray(item)); } - values = values.concat(getParticipantsLoginsArray(item)); return uniqFast(values); }); @@ -2287,11 +2289,17 @@ function filterOptions(options: Options, searchInputValue: string): Options { }; }, options); - const recentReports = matchResults.recentReports.concat(matchResults.personalDetails); + let {recentReports, personalDetails} = matchResults; + + if (sortByReportTypeInSearch) { + recentReports = recentReports.concat(matchResults.personalDetails); + personalDetails = []; + recentReports = orderOptions(recentReports, searchValue); + } return { - personalDetails: [], - recentReports: orderOptions(recentReports, searchValue), + personalDetails, + recentReports, userToInvite: null, currentUserOption: null, categoryOptions: [], diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 5576f64ba67a..fea1ee22f783 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -101,7 +101,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) }; } - const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue); + const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true}); const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, false, debouncedSearchValue); return { recentReports: newOptions.recentReports, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 3c65f0fa9a96..f7cc6132f487 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -80,21 +80,16 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan Report.searchInServer(debouncedSearchTerm.trim()); }, [debouncedSearchTerm]); - /** - * Returns the sections needed for the OptionsSelector - * - * @returns {Array} - */ - const [sections, newChatOptions] = useMemo(() => { - const newSections = []; + const chatOptions = useMemo(() => { if (!areOptionsInitialized || !didScreenTransitionEnd) { - return [newSections, {}]; + return {}; } - const chatOptions = OptionsListUtils.getFilteredOptions( + + return OptionsListUtils.getFilteredOptions( options.reports, options.personalDetails, betas, - debouncedSearchTerm, + '', participants, CONST.EXPENSIFY_EMAILS, @@ -112,12 +107,36 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, ); + }, [areOptionsInitialized, betas, canUseP2PDistanceRequests, didScreenTransitionEnd, iouRequestType, iouType, options.personalDetails, options.reports, participants]); + + const filteredOptions = useMemo(() => { + if (!areOptionsInitialized || !didScreenTransitionEnd || debouncedSearchTerm.trim() === '') { + return {}; + } + + const newOptions = OptionsListUtils.filterOptions(chatOptions, debouncedSearchTerm, {}); + + return newOptions; + }, [areOptionsInitialized, chatOptions, debouncedSearchTerm, didScreenTransitionEnd]); + + const requestMoneyOptions = debouncedSearchTerm.trim() !== '' ? filteredOptions : chatOptions; + + /** + * Returns the sections needed for the OptionsSelector + * + * @returns {Array} + */ + const [sections, newChatOptions] = useMemo(() => { + const newSections = []; + if (!areOptionsInitialized || !didScreenTransitionEnd) { + return [newSections, {}]; + } const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( debouncedSearchTerm, participants, - chatOptions.recentReports, - chatOptions.personalDetails, + requestMoneyOptions.recentReports, + requestMoneyOptions.personalDetails, maxParticipantsReached, personalDetails, true, @@ -131,20 +150,20 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan newSections.push({ title: translate('common.recents'), - data: chatOptions.recentReports, - shouldShow: !_.isEmpty(chatOptions.recentReports), + data: requestMoneyOptions.recentReports, + shouldShow: !_.isEmpty(options.recentReports), }); newSections.push({ title: translate('common.contacts'), - data: chatOptions.personalDetails, - shouldShow: !_.isEmpty(chatOptions.personalDetails), + data: requestMoneyOptions.personalDetails, + shouldShow: !_.isEmpty(options.personalDetails), }); - if (chatOptions.userToInvite && !OptionsListUtils.isCurrentUser(chatOptions.userToInvite)) { + if (requestMoneyOptions.userToInvite && !OptionsListUtils.isCurrentUser(requestMoneyOptions.userToInvite)) { newSections.push({ title: undefined, - data: _.map([chatOptions.userToInvite], (participant) => { + data: _.map([requestMoneyOptions.userToInvite], (participant) => { const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), @@ -155,18 +174,18 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan return [newSections, chatOptions]; }, [ areOptionsInitialized, - options.reports, - options.personalDetails, - betas, + didScreenTransitionEnd, debouncedSearchTerm, participants, - iouType, - canUseP2PDistanceRequests, - iouRequestType, + requestMoneyOptions.recentReports, + requestMoneyOptions.personalDetails, + requestMoneyOptions.userToInvite, maxParticipantsReached, personalDetails, translate, - didScreenTransitionEnd, + options.recentReports, + options.personalDetails, + chatOptions, ]); /** From d66c2e8c561dd4a9609c9cf60becdd39905589ee Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 12 Apr 2024 13:06:40 +0200 Subject: [PATCH 02/31] create optimistic user when filtering --- src/libs/OptionsListUtils.ts | 85 ++++++++++++------- ...yForRefactorRequestParticipantsSelector.js | 4 +- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 98800cce03ca..260eb3b72b51 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1528,6 +1528,39 @@ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | u ); } +function createOptimisticPersonalDetailOption(searchValue: string, {reportActions = {}, showChatPreviewLine = false}) { + const optimisticAccountID = UserUtils.generateAccountID(searchValue); + const personalDetailsExtended = { + ...allPersonalDetails, + [optimisticAccountID]: { + accountID: optimisticAccountID, + login: searchValue, + avatar: UserUtils.getDefaultAvatar(optimisticAccountID), + }, + }; + const optimisticUser = createOption([optimisticAccountID], personalDetailsExtended, null, reportActions, { + showChatPreviewLine, + }); + + optimisticUser.isOptimisticAccount = true; + optimisticUser.login = searchValue; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + optimisticUser.text = optimisticUser.text || searchValue; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + optimisticUser.alternateText = optimisticUser.alternateText || searchValue; + + // If user doesn't exist, use a default avatar + optimisticUser.icons = [ + { + source: UserUtils.getAvatar('', optimisticAccountID), + name: searchValue, + type: CONST.ICON_TYPE_AVATAR, + }, + ]; + + return optimisticUser; +} + /** * filter options based on specific conditions */ @@ -1844,33 +1877,7 @@ function getOptions( !excludeUnknownUsers ) { // Generates an optimistic account ID for new users not yet saved in Onyx - const optimisticAccountID = UserUtils.generateAccountID(searchValue); - const personalDetailsExtended = { - ...allPersonalDetails, - [optimisticAccountID]: { - accountID: optimisticAccountID, - login: searchValue, - avatar: UserUtils.getDefaultAvatar(optimisticAccountID), - }, - }; - userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, reportActions, { - showChatPreviewLine, - }); - userToInvite.isOptimisticAccount = true; - userToInvite.login = searchValue; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - userToInvite.text = userToInvite.text || searchValue; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - userToInvite.alternateText = userToInvite.alternateText || searchValue; - - // If user doesn't exist, use a default avatar - userToInvite.icons = [ - { - source: UserUtils.getAvatar('', optimisticAccountID), - name: searchValue, - type: CONST.ICON_TYPE_AVATAR, - }, - ]; + userToInvite = createOptimisticPersonalDetailOption(searchValue, {reportActions, showChatPreviewLine}); } // If we are prioritizing 1:1 chats in search, do it only once we started searching @@ -2219,8 +2226,13 @@ function formatSectionsFromSearchTerm( /** * Filters options based on the search input value */ -function filterOptions(options: Options, searchInputValue: string, {sortByReportTypeInSearch = false}: Partial): Options { - const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); +function filterOptions( + options: Options, + searchInputValue: string, + {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = []}: Partial, +): Options { + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); const searchTerms = searchValue ? searchValue.split(' ') : []; // The regex below is used to remove dots only from the local part of the user email (local-part@domain) @@ -2297,10 +2309,23 @@ function filterOptions(options: Options, searchInputValue: string, {sortByReport recentReports = orderOptions(recentReports, searchValue); } + let userToInvite = null; + if ( + canInviteUser && + searchValue && + !isCurrentUser({login: searchValue} as PersonalDetails) && + selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && + ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || + (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) + ) { + userToInvite = createOptimisticPersonalDetailOption(searchValue, {}); + } + return { personalDetails, recentReports, - userToInvite: null, + userToInvite, currentUserOption: null, categoryOptions: [], tagOptions: [], diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index f7cc6132f487..65f9bd003398 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -114,10 +114,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan return {}; } - const newOptions = OptionsListUtils.filterOptions(chatOptions, debouncedSearchTerm, {}); + const newOptions = OptionsListUtils.filterOptions(chatOptions, debouncedSearchTerm, {canInviteUser: true, betas}); return newOptions; - }, [areOptionsInitialized, chatOptions, debouncedSearchTerm, didScreenTransitionEnd]); + }, [areOptionsInitialized, betas, chatOptions, debouncedSearchTerm, didScreenTransitionEnd]); const requestMoneyOptions = debouncedSearchTerm.trim() !== '' ? filteredOptions : chatOptions; From ce44bde0c6a7e1acd428ec7b5d118d13b12f3b01 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 15 Apr 2024 09:01:34 +0200 Subject: [PATCH 03/31] exclude already created users and restricted emails --- src/libs/OptionsListUtils.ts | 34 +++++++++++++------ src/pages/SearchPage/index.tsx | 6 ++-- ...yForRefactorRequestParticipantsSelector.js | 13 ++++--- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 260eb3b72b51..6393aa5b352d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1528,6 +1528,9 @@ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | u ); } +/** + * Builds the option with optimistic personal details + */ function createOptimisticPersonalDetailOption(searchValue: string, {reportActions = {}, showChatPreviewLine = false}) { const optimisticAccountID = UserUtils.generateAccountID(searchValue); const personalDetailsExtended = { @@ -1859,6 +1862,7 @@ function getOptions( currentUserOption = undefined; } + // TODO: creating user to invite can be removed once we implement filtering in all search pages. This logic will be handled in filtering instead. let userToInvite: ReportUtils.OptionData | null = null; const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; const noOptionsMatchExactly = !personalDetailsOptions @@ -2229,7 +2233,7 @@ function formatSectionsFromSearchTerm( function filterOptions( options: Options, searchInputValue: string, - {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = []}: Partial, + {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = [], excludeUnknownUsers = false, excludeLogins = []}: Partial, ): Options { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); @@ -2310,16 +2314,24 @@ function filterOptions( } let userToInvite = null; - if ( - canInviteUser && - searchValue && - !isCurrentUser({login: searchValue} as PersonalDetails) && - selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && - ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && - (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) - ) { - userToInvite = createOptimisticPersonalDetailOption(searchValue, {}); + if (canInviteUser) { + const noOptions = recentReports.length + personalDetails.length === 0; + const noOptionsMatchExactly = !personalDetails + .concat(recentReports) + .find((option) => option.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() || option.login === searchValue?.toLowerCase()); + if ( + searchValue && + (noOptions || noOptionsMatchExactly) && + !isCurrentUser({login: searchValue} as PersonalDetails) && + selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && + ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || + (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + !excludeLogins.find((optionToExclude) => optionToExclude === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && + (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && + !excludeUnknownUsers + ) { + userToInvite = createOptimisticPersonalDetailOption(searchValue, {}); + } } return { diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index fea1ee22f783..4950d9221e75 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -101,12 +101,12 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) }; } - const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true}); - const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, false, debouncedSearchValue); + const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true, canInviteUser: true}); + const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, Boolean(newOptions.userToInvite), debouncedSearchValue); return { recentReports: newOptions.recentReports, personalDetails: newOptions.personalDetails, - userToInvite: null, + userToInvite: newOptions.userToInvite, headerMessage: header, }; }, [debouncedSearchValue, searchOptions]); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 65f9bd003398..6d3d41dcd254 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -114,10 +114,15 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan return {}; } - const newOptions = OptionsListUtils.filterOptions(chatOptions, debouncedSearchTerm, {canInviteUser: true, betas}); - + const newOptions = OptionsListUtils.filterOptions(chatOptions, debouncedSearchTerm, { + canInviteUser: true, + betas, + selectedOptions: participants, + excludeLogins: CONST.EXPENSIFY_EMAILS, + }); + console.log({newOptions}); return newOptions; - }, [areOptionsInitialized, betas, chatOptions, debouncedSearchTerm, didScreenTransitionEnd]); + }, [areOptionsInitialized, betas, chatOptions, debouncedSearchTerm, didScreenTransitionEnd, participants]); const requestMoneyOptions = debouncedSearchTerm.trim() !== '' ? filteredOptions : chatOptions; @@ -262,7 +267,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan ), [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], ); - + console.log({headerMessage}); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants From 047f320dbe0641efa67752e59ef348836632be85 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 15 Apr 2024 13:42:29 +0200 Subject: [PATCH 04/31] simplify generating header message --- ...yForRefactorRequestParticipantsSelector.js | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 6d3d41dcd254..7e17bdbbc1c7 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -85,7 +85,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan return {}; } - return OptionsListUtils.getFilteredOptions( + const optionList = OptionsListUtils.getFilteredOptions( options.reports, options.personalDetails, betas, @@ -107,6 +107,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, ); + + return optionList; }, [areOptionsInitialized, betas, canUseP2PDistanceRequests, didScreenTransitionEnd, iouRequestType, iouType, options.personalDetails, options.reports, participants]); const filteredOptions = useMemo(() => { @@ -115,23 +117,20 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan } const newOptions = OptionsListUtils.filterOptions(chatOptions, debouncedSearchTerm, { - canInviteUser: true, betas, selectedOptions: participants, excludeLogins: CONST.EXPENSIFY_EMAILS, }); - console.log({newOptions}); return newOptions; }, [areOptionsInitialized, betas, chatOptions, debouncedSearchTerm, didScreenTransitionEnd, participants]); - const requestMoneyOptions = debouncedSearchTerm.trim() !== '' ? filteredOptions : chatOptions; - /** * Returns the sections needed for the OptionsSelector * * @returns {Array} */ - const [sections, newChatOptions] = useMemo(() => { + const [sections, header] = useMemo(() => { + const requestMoneyOptions = debouncedSearchTerm.trim() !== '' ? filteredOptions : chatOptions; const newSections = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, {}]; @@ -176,21 +175,27 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan }); } - return [newSections, chatOptions]; + const headerMessage = OptionsListUtils.getHeaderMessage( + _.get(requestMoneyOptions, 'personalDetails', []).length + _.get(requestMoneyOptions, 'recentReports', []).length !== 0, + Boolean(requestMoneyOptions.userToInvite), + debouncedSearchTerm.trim(), + maxParticipantsReached, + _.some(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), + ); + + return [newSections, headerMessage]; }, [ + debouncedSearchTerm, + filteredOptions, + chatOptions, areOptionsInitialized, didScreenTransitionEnd, - debouncedSearchTerm, participants, - requestMoneyOptions.recentReports, - requestMoneyOptions.personalDetails, - requestMoneyOptions.userToInvite, maxParticipantsReached, personalDetails, translate, options.recentReports, options.personalDetails, - chatOptions, ]); /** @@ -256,18 +261,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan [participants, onParticipantsAdded], ); - const headerMessage = useMemo( - () => - OptionsListUtils.getHeaderMessage( - _.get(newChatOptions, 'personalDetails', []).length + _.get(newChatOptions, 'recentReports', []).length !== 0, - Boolean(newChatOptions.userToInvite), - debouncedSearchTerm.trim(), - maxParticipantsReached, - _.some(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), - ), - [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], - ); - console.log({headerMessage}); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants @@ -377,7 +370,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} onSelectRow={addSingleParticipant} footerContent={footerContent} - headerMessage={headerMessage} + headerMessage={header} showLoadingPlaceholder={!areOptionsInitialized || !didScreenTransitionEnd} rightHandSideComponent={itemRightSideComponent} /> From ccf5350874c2019f7673ebb5ac8ec65f373f63e3 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 15 Apr 2024 14:00:56 +0200 Subject: [PATCH 05/31] fix error when loading options --- src/pages/SearchPage/index.tsx | 2 +- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 4950d9221e75..49a3f343c360 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -101,7 +101,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) }; } - const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true, canInviteUser: true}); + const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true}); const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, Boolean(newOptions.userToInvite), debouncedSearchValue); return { recentReports: newOptions.recentReports, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 7e17bdbbc1c7..cfda1b193e80 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -133,7 +133,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan const requestMoneyOptions = debouncedSearchTerm.trim() !== '' ? filteredOptions : chatOptions; const newSections = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { - return [newSections, {}]; + return [newSections, '']; } const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( From f28e5e807a83b98b8ae1b17193aa0add5f431e53 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 15 Apr 2024 15:45:37 +0200 Subject: [PATCH 06/31] fix typechecks and tests --- src/libs/OptionsListUtils.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 39b12d12384c..ea4395522103 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2234,11 +2234,8 @@ function formatSectionsFromSearchTerm( /** * Filters options based on the search input value */ -function filterOptions( - options: Options, - searchInputValue: string, - {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = [], excludeUnknownUsers = false, excludeLogins = []}: Partial, -): Options { +function filterOptions(options: Options, searchInputValue: string, config?: Partial): Options { + const {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = [], excludeUnknownUsers = false, excludeLogins = []} = config ?? {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); const searchTerms = searchValue ? searchValue.split(' ') : []; From 2b4c5c51a5704db32fef630030bc5a8ed5d72190 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 15 Apr 2024 16:18:59 +0200 Subject: [PATCH 07/31] fix tests --- src/libs/OptionsListUtils.ts | 6 +++++- tests/unit/OptionsListUtilsTest.ts | 11 +++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ea4395522103..a10506c2cbae 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2234,7 +2234,11 @@ function formatSectionsFromSearchTerm( /** * Filters options based on the search input value */ -function filterOptions(options: Options, searchInputValue: string, config?: Partial): Options { +function filterOptions( + options: Options, + searchInputValue: string, + config?: Pick, +): Options { const {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = [], excludeUnknownUsers = false, excludeLogins = []} = config ?? {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index af5782b1ca32..0c33c07b31f2 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -2601,20 +2601,19 @@ describe('OptionsListUtils', () => { const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, ''); - expect(options.recentReports.length + options.personalDetails.length).toBe(filteredOptions.recentReports.length); + expect(options.recentReports.length + options.personalDetails.length).toBe(filteredOptions.recentReports.length + filteredOptions.personalDetails.length); }); it('should return filtered options in correct order', () => { const searchText = 'man'; const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); - expect(filteredOptions.recentReports.length).toBe(5); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {sortByReportTypeInSearch: true}); + expect(filteredOptions.recentReports.length).toBe(4); expect(filteredOptions.recentReports[0].text).toBe('Invisible Woman'); expect(filteredOptions.recentReports[1].text).toBe('Spider-Man'); expect(filteredOptions.recentReports[2].text).toBe('Black Widow'); expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic'); - expect(filteredOptions.recentReports[4].text).toBe("SHIELD's workspace (archived)"); }); it('should filter users by email', () => { @@ -2641,7 +2640,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); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {sortByReportTypeInSearch: true}); expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports[0].login).toBe('barry.allen@expensify.com'); @@ -2661,7 +2660,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.filterOptions(options, searchText, {}); expect(filteredOptions.recentReports.length).toBe(2); expect(filteredOptions.recentReports[0].login).toBe(searchText); From dd42f18422f26503424e0481e569a71472a149ab Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 16 Apr 2024 08:28:52 +0200 Subject: [PATCH 08/31] update filtering --- src/libs/OptionsListUtils.ts | 8 +++----- ...oneyTemporaryForRefactorRequestParticipantsSelector.js | 5 ++--- tests/unit/OptionsListUtilsTest.ts | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a10506c2cbae..3a56add090f7 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -193,6 +193,8 @@ type Options = { type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; +type FilterOptionsConfig = Pick; + /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can * be configured to display different results based on the options passed to the private getOptions() method. Public @@ -2234,11 +2236,7 @@ function formatSectionsFromSearchTerm( /** * Filters options based on the search input value */ -function filterOptions( - options: Options, - searchInputValue: string, - config?: Pick, -): Options { +function filterOptions(options: Options, searchInputValue: string, config?: FilterOptionsConfig): Options { const {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = [], excludeUnknownUsers = false, excludeLogins = []} = config ?? {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index cfda1b193e80..f06a8d81e69c 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -112,7 +112,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan }, [areOptionsInitialized, betas, canUseP2PDistanceRequests, didScreenTransitionEnd, iouRequestType, iouType, options.personalDetails, options.reports, participants]); const filteredOptions = useMemo(() => { - if (!areOptionsInitialized || !didScreenTransitionEnd || debouncedSearchTerm.trim() === '') { + if (!areOptionsInitialized || debouncedSearchTerm.trim() === '') { return {}; } @@ -122,11 +122,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan excludeLogins: CONST.EXPENSIFY_EMAILS, }); return newOptions; - }, [areOptionsInitialized, betas, chatOptions, debouncedSearchTerm, didScreenTransitionEnd, participants]); + }, [areOptionsInitialized, betas, chatOptions, debouncedSearchTerm, participants]); /** * Returns the sections needed for the OptionsSelector - * * @returns {Array} */ const [sections, header] = useMemo(() => { diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 0c33c07b31f2..0f015a121d7b 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -2660,7 +2660,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.filterOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(2); expect(filteredOptions.recentReports[0].login).toBe(searchText); From 477260e96b6e1bffa78676ef05dd0a41215349cb Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Apr 2024 08:38:43 +0200 Subject: [PATCH 09/31] prettier --- ...poraryForRefactorRequestParticipantsSelector.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 6a08b4a4c9bf..0c0efb9d289e 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -194,7 +194,19 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ); return [newSections, headerMessage]; - }, [debouncedSearchTerm, filteredOptions, chatOptions, areOptionsInitialized, didScreenTransitionEnd, participants, action, maxParticipantsReached, personalDetails, translate, options.recentReports]); + }, [ + debouncedSearchTerm, + filteredOptions, + chatOptions, + areOptionsInitialized, + didScreenTransitionEnd, + participants, + action, + maxParticipantsReached, + personalDetails, + translate, + options.recentReports, + ]); /** * Adds a single participant to the request From 4aaef6c20b75f901b66dbe6031e47b5ff390b58b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 12:15:32 +0200 Subject: [PATCH 10/31] get participants --- src/libs/OptionsListUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3759126c213d..962ebeb202df 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2303,6 +2303,11 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt values.push(item.login.replace(emailRegex, '')); } + if (!item.isChatRoom) { + const participantNames = getParticipantNames(item.participantsList ?? []); + values = values.concat(Array.from(participantNames)); + } + if (item.isThread) { if (item.alternateText) { values.push(item.alternateText); @@ -2315,7 +2320,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt } else { values = values.concat(getParticipantsLoginsArray(item)); } - return uniqFast(values); }); const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => From a8b581a5d37f4592b36673e8e51ca6df86a63282 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 13:40:15 +0200 Subject: [PATCH 11/31] code review updates --- src/libs/OptionsListUtils.ts | 95 +++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 962ebeb202df..c141698d0603 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,3 +1,5 @@ +import type {ParsedPhoneNumber} from 'awesome-phonenumber'; + /* eslint-disable no-continue */ import Str from 'expensify-common/lib/str'; // eslint-disable-next-line you-dont-need-lodash-underscore/get @@ -1544,6 +1546,45 @@ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | u ); } +function canCreateOptimisticPersonalDetailOption({ + searchValue, + recentReportOptions, + personalDetailsOptions, + currentUserOption, + selectedOptions, + excludeUnknownUsers, + betas, + optionsToExclude, + parsedPhoneNumber, +}: { + searchValue: string; + recentReportOptions: ReportUtils.OptionData[]; + personalDetailsOptions: ReportUtils.OptionData[]; + currentUserOption?: ReportUtils.OptionData | null; + selectedOptions: Array>; + excludeUnknownUsers: boolean; + betas: OnyxEntry; + optionsToExclude: string[]; + parsedPhoneNumber: ParsedPhoneNumber; +}) { + const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; + const noOptionsMatchExactly = !personalDetailsOptions + .concat(recentReportOptions) + .find((option) => option.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() || option.login === searchValue?.toLowerCase()); + + return ( + searchValue && + (noOptions || noOptionsMatchExactly) && + !isCurrentUser({login: searchValue} as PersonalDetails) && + selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && + ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || + (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + !optionsToExclude.find((optionToExclude) => optionToExclude === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && + (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && + !excludeUnknownUsers + ); +} + /** * Builds the option with optimistic personal details */ @@ -1880,21 +1921,18 @@ function getOptions( // TODO: creating user to invite can be removed once we implement filtering in all search pages. This logic will be handled in filtering instead. let userToInvite: ReportUtils.OptionData | null = null; - const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; - const noOptionsMatchExactly = !personalDetailsOptions - .concat(recentReportOptions) - .find((option) => option.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() || option.login === searchValue?.toLowerCase()); - if ( - searchValue && - (noOptions || noOptionsMatchExactly) && - !isCurrentUser({login: searchValue} as PersonalDetails) && - selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && - ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && - !optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && - (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && - !excludeUnknownUsers + canCreateOptimisticPersonalDetailOption({ + searchValue, + recentReportOptions, + personalDetailsOptions, + currentUserOption, + selectedOptions, + excludeUnknownUsers, + betas, + optionsToExclude: optionsToExclude.map(({login}) => login ?? ''), + parsedPhoneNumber, + }) ) { // Generates an optimistic account ID for new users not yet saved in Onyx userToInvite = createOptimisticPersonalDetailOption(searchValue, {reportActions, showChatPreviewLine}); @@ -2264,7 +2302,7 @@ function getFirstKeyForList(data?: Option[] | null) { function filterOptions(options: Options, searchInputValue: string, config?: FilterOptionsConfig): Options { const {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = [], excludeUnknownUsers = false, excludeLogins = []} = config ?? {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); + const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); const searchTerms = searchValue ? searchValue.split(' ') : []; // The regex below is used to remove dots only from the local part of the user email (local-part@domain) @@ -2338,29 +2376,28 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt }, options); let {recentReports, personalDetails} = matchResults; + const {currentUserOption} = matchResults; if (sortByReportTypeInSearch) { - recentReports = recentReports.concat(matchResults.personalDetails); + recentReports = recentReports.concat(personalDetails); personalDetails = []; recentReports = orderOptions(recentReports, searchValue); } let userToInvite = null; if (canInviteUser) { - const noOptions = recentReports.length + personalDetails.length === 0; - const noOptionsMatchExactly = !personalDetails - .concat(recentReports) - .find((option) => option.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() || option.login === searchValue?.toLowerCase()); if ( - searchValue && - (noOptions || noOptionsMatchExactly) && - !isCurrentUser({login: searchValue} as PersonalDetails) && - selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && - ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && - !excludeLogins.find((optionToExclude) => optionToExclude === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && - (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && - !excludeUnknownUsers + canCreateOptimisticPersonalDetailOption({ + searchValue, + recentReportOptions: recentReports, + personalDetailsOptions: personalDetails, + currentUserOption, + selectedOptions, + excludeUnknownUsers, + betas, + optionsToExclude: excludeLogins, + parsedPhoneNumber, + }) ) { userToInvite = createOptimisticPersonalDetailOption(searchValue, {}); } From 0bf553bab2aebfd9f04ef9a0b029a12b44be64cd Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 13:48:40 +0200 Subject: [PATCH 12/31] fix test --- tests/unit/OptionsListUtilsTest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 77b37de2205b..95e0cb7b6718 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -2618,11 +2618,12 @@ describe('OptionsListUtils', () => { const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {sortByReportTypeInSearch: true}); - expect(filteredOptions.recentReports.length).toBe(4); + expect(filteredOptions.recentReports.length).toBe(5); expect(filteredOptions.recentReports[0].text).toBe('Invisible Woman'); expect(filteredOptions.recentReports[1].text).toBe('Spider-Man'); expect(filteredOptions.recentReports[2].text).toBe('Black Widow'); expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic'); + expect(filteredOptions.recentReports[4].text).toBe("SHIELD's workspace (archived)"); }); it('should filter users by email', () => { From e873c968e383d8be47479283bed09a129a19fd51 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 16:28:53 +0200 Subject: [PATCH 13/31] search recent by workspace name --- src/CONST.ts | 1 + src/libs/OptionsListUtils.ts | 32 +++++++++--- ...yForRefactorRequestParticipantsSelector.js | 50 +++++++++++-------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index ab5a67274955..931fd9d03913 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1402,6 +1402,7 @@ const CONST = { }, IOU: { + MAX_RECENT_REPORTS_TO_SHOW: 5, // This is the transactionID used when going through the create expense flow so that it mimics a real transaction (like the edit flow) OPTIMISTIC_TRANSACTION_ID: '1', // Note: These payment types are used when building IOU reportAction message values in the server and should diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index c141698d0603..25c1e9bcf9ea 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -196,7 +196,10 @@ type Options = { type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; -type FilterOptionsConfig = Pick; +type FilterOptionsConfig = Pick< + GetOptionsConfig, + 'sortByReportTypeInSearch' | 'canInviteUser' | 'betas' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' +>; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -1845,9 +1848,9 @@ 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; - } + // if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { + // break; + // } // Skip notifications@expensify.com if (reportOption.login === CONST.EMAIL.NOTIFICATIONS) { @@ -2300,7 +2303,19 @@ function getFirstKeyForList(data?: Option[] | null) { * Filters options based on the search input value */ function filterOptions(options: Options, searchInputValue: string, config?: FilterOptionsConfig): Options { - const {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], selectedOptions = [], excludeUnknownUsers = false, excludeLogins = []} = config ?? {}; + const { + sortByReportTypeInSearch = false, + canInviteUser = true, + betas = [], + selectedOptions = [], + excludeUnknownUsers = false, + excludeLogins = [], + maxRecentReportsToShow = 0, + } = config ?? {}; + if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) { + return {...options, recentReports: options.recentReports.slice(0, maxRecentReportsToShow)}; + } + 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(' ') : []; @@ -2329,7 +2344,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt return keys; }; - const matchResults = searchTerms.reduceRight((items, term) => { + const matchResults = searchTerms.reduce((items, term) => { const recentReports = filterArrayByMatch(items.recentReports, term, (item) => { let values: string[] = []; if (item.text) { @@ -2358,12 +2373,17 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt } else { values = values.concat(getParticipantsLoginsArray(item)); } + return uniqFast(values); }); const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login ?? '', item.login?.replace(emailRegex, '') ?? '']), ); + if (maxRecentReportsToShow > 0 && recentReports.length > maxRecentReportsToShow) { + recentReports.splice(maxRecentReportsToShow); + } + return { recentReports: recentReports ?? [], personalDetails: personalDetails ?? [], diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index b40ab7166fef..7423a5728ef2 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -89,9 +89,18 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF Report.searchInServer(debouncedSearchTerm.trim()); }, [debouncedSearchTerm]); - const chatOptions = useMemo(() => { + const defaultOptions = useMemo(() => { if (!areOptionsInitialized || !didScreenTransitionEnd) { - return {}; + return { + userToInvite: null, + recentReports: [], + personalDetails: [], + currentUserOption: null, + headerMessage: '', + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; } const optionList = OptionsListUtils.getFilteredOptions( @@ -120,25 +129,34 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF return optionList; }, [action, areOptionsInitialized, betas, canUseP2PDistanceRequests, didScreenTransitionEnd, iouRequestType, iouType, options.personalDetails, options.reports, participants]); - const filteredOptions = useMemo(() => { - if (!areOptionsInitialized || debouncedSearchTerm.trim() === '') { - return {}; + const chatOptions = useMemo(() => { + if (!areOptionsInitialized) { + return { + userToInvite: null, + recentReports: [], + personalDetails: [], + currentUserOption: null, + headerMessage: '', + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; } - const newOptions = OptionsListUtils.filterOptions(chatOptions, debouncedSearchTerm, { + const newOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, { betas, selectedOptions: participants, excludeLogins: CONST.EXPENSIFY_EMAILS, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); return newOptions; - }, [areOptionsInitialized, betas, chatOptions, debouncedSearchTerm, participants]); - + }, [areOptionsInitialized, betas, defaultOptions, debouncedSearchTerm, participants]); /** * Returns the sections needed for the OptionsSelector * @returns {Array} */ const [sections, header] = useMemo(() => { - const requestMoneyOptions = debouncedSearchTerm.trim() !== '' ? filteredOptions : chatOptions; + const requestMoneyOptions = chatOptions; const newSections = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, '']; @@ -194,19 +212,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ); return [newSections, headerMessage]; - }, [ - debouncedSearchTerm, - filteredOptions, - chatOptions, - areOptionsInitialized, - didScreenTransitionEnd, - participants, - action, - maxParticipantsReached, - personalDetails, - translate, - options.recentReports, - ]); + }, [debouncedSearchTerm, chatOptions, areOptionsInitialized, didScreenTransitionEnd, participants, action, maxParticipantsReached, personalDetails, translate, options.recentReports]); /** * Adds a single participant to the expense From 17b178e2ec1d5e8aa4ad16c34f2d4232a4da34af Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 16:49:59 +0200 Subject: [PATCH 14/31] update displaying recent reports --- src/libs/OptionsListUtils.ts | 9 ++++--- ...yForRefactorRequestParticipantsSelector.js | 2 ++ tests/unit/OptionsListUtilsTest.ts | 26 ++++++++++++++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 25c1e9bcf9ea..c55a4f45049e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1848,9 +1848,9 @@ 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; - // } + if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { + break; + } // Skip notifications@expensify.com if (reportOption.login === CONST.EMAIL.NOTIFICATIONS) { @@ -2057,6 +2057,7 @@ function getFilteredOptions( canInviteUser = true, includeSelectedOptions = false, includeTaxRates = false, + maxRecentReportsToShow = 5, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, includePolicyReportFieldOptions = false, @@ -2071,7 +2072,7 @@ function getFilteredOptions( selectedOptions, includeRecentReports: true, includePersonalDetails: true, - maxRecentReportsToShow: 5, + maxRecentReportsToShow, excludeLogins, includeOwnedWorkspaceChats, includeP2P, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 7423a5728ef2..2ed051c55723 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -124,6 +124,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF [], (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && ![CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].includes(action), false, + false, + 0, ); return optionList; diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 95e0cb7b6718..a9f0e4d21984 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -2576,14 +2576,34 @@ describe('OptionsListUtils', () => { }, ]; - const result = OptionsListUtils.getFilteredOptions([], [], [], emptySearch, [], [], false, false, false, {}, [], false, {}, [], false, false, true, taxRatesWithDefault); + const result = OptionsListUtils.getFilteredOptions([], [], [], emptySearch, [], [], false, false, false, {}, [], false, {}, [], false, false, true, 5, taxRatesWithDefault); expect(result.taxRatesOptions).toStrictEqual(resultList); - const searchResult = OptionsListUtils.getFilteredOptions([], [], [], search, [], [], false, false, false, {}, [], false, {}, [], false, false, true, taxRatesWithDefault); + const searchResult = OptionsListUtils.getFilteredOptions([], [], [], search, [], [], false, false, false, {}, [], false, {}, [], false, false, true, 5, taxRatesWithDefault); expect(searchResult.taxRatesOptions).toStrictEqual(searchResultList); - const wrongSearchResult = OptionsListUtils.getFilteredOptions([], [], [], wrongSearch, [], [], false, false, false, {}, [], false, {}, [], false, false, true, taxRatesWithDefault); + const wrongSearchResult = OptionsListUtils.getFilteredOptions( + [], + [], + [], + wrongSearch, + [], + [], + false, + false, + false, + {}, + [], + false, + {}, + [], + false, + false, + true, + 5, + taxRatesWithDefault, + ); expect(wrongSearchResult.taxRatesOptions).toStrictEqual(wrongSearchResultList); }); From 05f60cb7e1f6e5cf4d1474ef8d0ac39f8021d896 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 17:01:25 +0200 Subject: [PATCH 15/31] update getFilteredOptions usage --- src/pages/EditReportFieldDropdown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx index 225051238e2b..b17a1588b774 100644 --- a/src/pages/EditReportFieldDropdown.tsx +++ b/src/pages/EditReportFieldDropdown.tsx @@ -86,6 +86,7 @@ function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptio false, false, undefined, + 5, undefined, undefined, true, From 767db865fe4ec7272aa47a56e6ff6cbd953c8ff8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 23 Apr 2024 12:38:41 +0200 Subject: [PATCH 16/31] resolve nab comments --- src/libs/OptionsListUtils.ts | 1 - ...yForRefactorRequestParticipantsSelector.js | 21 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index c55a4f45049e..1b302a29039e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1937,7 +1937,6 @@ function getOptions( parsedPhoneNumber, }) ) { - // Generates an optimistic account ID for new users not yet saved in Onyx userToInvite = createOptimisticPersonalDetailOption(searchValue, {reportActions, showChatPreviewLine}); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 9a65d83e48c5..ca44f155365e 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -164,7 +164,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF * @returns {Array} */ const [sections, header] = useMemo(() => { - const requestMoneyOptions = chatOptions; const newSections = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, '']; @@ -173,8 +172,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( debouncedSearchTerm, participants, - requestMoneyOptions.recentReports, - requestMoneyOptions.personalDetails, + chatOptions.recentReports, + chatOptions.personalDetails, maxParticipantsReached, personalDetails, true, @@ -188,22 +187,22 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF newSections.push({ title: translate('common.recents'), - data: requestMoneyOptions.recentReports, - shouldShow: requestMoneyOptions.recentReports.length > 0, + data: chatOptions.recentReports, + shouldShow: chatOptions.recentReports.length > 0, }); if (![CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].includes(action)) { newSections.push({ title: translate('common.contacts'), - data: requestMoneyOptions.personalDetails, - shouldShow: requestMoneyOptions.personalDetails.length > 0, + data: chatOptions.personalDetails, + shouldShow: chatOptions.personalDetails.length > 0, }); } - if (requestMoneyOptions.userToInvite && !OptionsListUtils.isCurrentUser(requestMoneyOptions.userToInvite)) { + if (chatOptions.userToInvite && !OptionsListUtils.isCurrentUser(chatOptions.userToInvite)) { newSections.push({ title: undefined, - data: lodashMap([requestMoneyOptions.userToInvite], (participant) => { + data: lodashMap([chatOptions.userToInvite], (participant) => { const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), @@ -212,8 +211,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF } const headerMessage = OptionsListUtils.getHeaderMessage( - lodashGet(requestMoneyOptions, 'personalDetails', []).length + lodashGet(requestMoneyOptions, 'recentReports', []).length !== 0, - Boolean(requestMoneyOptions.userToInvite), + lodashGet(chatOptions, 'personalDetails', []).length + lodashGet(chatOptions, 'recentReports', []).length !== 0, + Boolean(chatOptions.userToInvite), debouncedSearchTerm.trim(), maxParticipantsReached, lodashSome(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), From 6a669c6b2d0dc61066658f63744bdc733b73b675 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Apr 2024 08:41:53 +0200 Subject: [PATCH 17/31] code review updates --- src/libs/OptionsListUtils.ts | 6 ++---- src/pages/ChatFinderPage/index.tsx | 2 +- ...oraryForRefactorRequestParticipantsSelector.js | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a7f00d8284f1..58dcb3c78029 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,6 +1,5 @@ -import type {ParsedPhoneNumber} from 'awesome-phonenumber'; - /* eslint-disable no-continue */ +import type {ParsedPhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; // eslint-disable-next-line you-dont-need-lodash-underscore/get import lodashGet from 'lodash/get'; @@ -1922,7 +1921,6 @@ function getOptions( currentUserOption = undefined; } - // TODO: creating user to invite can be removed once we implement filtering in all search pages. This logic will be handled in filtering instead. let userToInvite: ReportUtils.OptionData | null = null; if ( canCreateOptimisticPersonalDetailOption({ @@ -2056,7 +2054,7 @@ function getFilteredOptions( canInviteUser = true, includeSelectedOptions = false, includeTaxRates = false, - maxRecentReportsToShow = 5, + maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, includePolicyReportFieldOptions = false, diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index 9fc08d897cb0..1da4fc337e47 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -102,7 +102,7 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa } const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true}); - const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, Boolean(newOptions.userToInvite), debouncedSearchValue); + const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, !!newOptions.userToInvite, debouncedSearchValue); return { recentReports: newOptions.recentReports, personalDetails: newOptions.personalDetails, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 75fcaf4bb7b8..2c64d6521ef1 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -142,7 +142,19 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ); return optionList; - }, [action, areOptionsInitialized, betas, canUseP2PDistanceRequests, didScreenTransitionEnd, iouRequestType, iouType, isCategorizeOrShareAction, options.personalDetails, options.reports, participants]); + }, [ + action, + areOptionsInitialized, + betas, + canUseP2PDistanceRequests, + didScreenTransitionEnd, + iouRequestType, + iouType, + isCategorizeOrShareAction, + options.personalDetails, + options.reports, + participants, + ]); const chatOptions = useMemo(() => { if (!areOptionsInitialized) { @@ -166,6 +178,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF }); return newOptions; }, [areOptionsInitialized, betas, defaultOptions, debouncedSearchTerm, participants]); + /** * Returns the sections needed for the OptionsSelector * @returns {Array} From d3fb66684ec5d6b2ea5db9b6819e1f04c8856f1e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Apr 2024 09:01:42 +0200 Subject: [PATCH 18/31] use reduceRight when filtering --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 58dcb3c78029..a92d75a63abb 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2343,7 +2343,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt return keys; }; - const matchResults = searchTerms.reduce((items, term) => { + const matchResults = searchTerms.reduceRight((items, term) => { const recentReports = filterArrayByMatch(items.recentReports, term, (item) => { let values: string[] = []; if (item.text) { From 4e3ec3933b77f6c92ecccf39fe006966cbefc34b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Apr 2024 09:23:53 +0200 Subject: [PATCH 19/31] fix typecheck --- src/libs/OptionsListUtils.ts | 2 +- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 2 +- tests/unit/OptionsListUtilsTest.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a92d75a63abb..955963eaa57c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2054,7 +2054,7 @@ function getFilteredOptions( canInviteUser = true, includeSelectedOptions = false, includeTaxRates = false, - maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + maxRecentReportsToShow: number = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, includePolicyReportFieldOptions = false, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2c64d6521ef1..d7028aa0229a 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -237,7 +237,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ); return [newSections, headerMessage]; - }, [debouncedSearchTerm, chatOptions, areOptionsInitialized, didScreenTransitionEnd, participants, action, maxParticipantsReached, personalDetails, translate]); + }, [debouncedSearchTerm, chatOptions, areOptionsInitialized, didScreenTransitionEnd, participants, maxParticipantsReached, personalDetails, translate]); /** * Adds a single participant to the expense diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 61da3d672322..701908b5d60f 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -452,6 +452,7 @@ describe('OptionsListUtils', () => { undefined, undefined, undefined, + 0, undefined, undefined, undefined, From 79a7140f670af51e87a54e04fcefc322d118fabb Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Apr 2024 10:12:07 +0200 Subject: [PATCH 20/31] add tests for canCreateOptimisticPersonalDetailOption --- src/libs/OptionsListUtils.ts | 10 ++- tests/unit/OptionsListUtilsTest.ts | 97 ++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 955963eaa57c..d763d37d7aae 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,5 +1,4 @@ /* eslint-disable no-continue */ -import type {ParsedPhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; // eslint-disable-next-line you-dont-need-lodash-underscore/get import lodashGet from 'lodash/get'; @@ -1557,7 +1556,6 @@ function canCreateOptimisticPersonalDetailOption({ excludeUnknownUsers, betas, optionsToExclude, - parsedPhoneNumber, }: { searchValue: string; recentReportOptions: ReportUtils.OptionData[]; @@ -1567,8 +1565,8 @@ function canCreateOptimisticPersonalDetailOption({ excludeUnknownUsers: boolean; betas: OnyxEntry; optionsToExclude: string[]; - parsedPhoneNumber: ParsedPhoneNumber; }) { + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchValue))); const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; const noOptionsMatchExactly = !personalDetailsOptions .concat(recentReportOptions) @@ -1580,7 +1578,7 @@ function canCreateOptimisticPersonalDetailOption({ !isCurrentUser({login: searchValue} as PersonalDetails) && selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + (parsedPhoneNumber?.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && !optionsToExclude.find((optionToExclude) => optionToExclude === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers @@ -1932,7 +1930,6 @@ function getOptions( excludeUnknownUsers, betas, optionsToExclude: optionsToExclude.map(({login}) => login ?? ''), - parsedPhoneNumber, }) ) { userToInvite = createOptimisticPersonalDetailOption(searchValue, {reportActions, showChatPreviewLine}); @@ -2415,7 +2412,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt excludeUnknownUsers, betas, optionsToExclude: excludeLogins, - parsedPhoneNumber, }) ) { userToInvite = createOptimisticPersonalDetailOption(searchValue, {}); @@ -2471,6 +2467,8 @@ export { getReportOption, getTaxRatesSection, getFirstKeyForList, + canCreateOptimisticPersonalDetailOption, + createOptimisticPersonalDetailOption, }; export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 701908b5d60f..629acaf54443 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -2749,4 +2749,101 @@ describe('OptionsListUtils', () => { expect(filteredOptions.recentReports[1].text).toBe('Mister Fantastic'); }); }); + + describe('canCreateOptimisticPersonalDetailOption', () => { + const VALID_EMAIL = 'valid@email.com'; + const INVALID_EMAIL = 'invalid-email'; + it('should allow to create optimistic personal detail option if email is valid', () => { + const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ + searchValue: VALID_EMAIL, + recentReportOptions: OPTIONS.reports, + personalDetailsOptions: OPTIONS.personalDetails, + currentUserOption: null, + selectedOptions: [], + excludeUnknownUsers: false, + betas: [CONST.BETAS.ALL], + optionsToExclude: [], + }); + + expect(canCreate).toBe(true); + }); + + it('should not allow to create option if email is not valid', () => { + const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ + searchValue: INVALID_EMAIL, + recentReportOptions: OPTIONS.reports, + personalDetailsOptions: OPTIONS.personalDetails, + currentUserOption: null, + selectedOptions: [], + excludeUnknownUsers: false, + betas: [CONST.BETAS.ALL], + optionsToExclude: [], + }); + + expect(canCreate).toBe(false); + }); + + it('should not allow to create option if email is already in the list', () => { + const optimisticOption = OptionsListUtils.createOptimisticPersonalDetailOption(VALID_EMAIL, {}); + const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ + searchValue: VALID_EMAIL, + recentReportOptions: OPTIONS.reports, + personalDetailsOptions: OPTIONS.personalDetails, + currentUserOption: null, + selectedOptions: [optimisticOption], + excludeUnknownUsers: false, + betas: [CONST.BETAS.ALL], + optionsToExclude: [], + }); + + expect(canCreate).toBe(false); + }); + + it('should not allow to create option if email is restricted', () => { + const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ + searchValue: VALID_EMAIL, + recentReportOptions: OPTIONS.reports, + personalDetailsOptions: OPTIONS.personalDetails, + currentUserOption: null, + selectedOptions: [], + excludeUnknownUsers: false, + betas: [CONST.BETAS.ALL], + optionsToExclude: [VALID_EMAIL], + }); + + expect(canCreate).toBe(false); + }); + + it('should not allow to create option if email is already on the list', () => { + const optimisticOption = OptionsListUtils.createOptimisticPersonalDetailOption(VALID_EMAIL, {}); + const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ + searchValue: VALID_EMAIL, + recentReportOptions: OPTIONS.reports, + personalDetailsOptions: [...OPTIONS.personalDetails, optimisticOption], + currentUserOption: null, + selectedOptions: [], + excludeUnknownUsers: false, + betas: [CONST.BETAS.ALL], + optionsToExclude: [], + }); + + expect(canCreate).toBe(false); + }); + + it('should not allow to create option if email is an email of current user', () => { + const currentUserEmail = 'tonystark@expensify.com'; + const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ + searchValue: currentUserEmail, + recentReportOptions: OPTIONS.reports, + personalDetailsOptions: OPTIONS.personalDetails, + currentUserOption: null, + selectedOptions: [], + excludeUnknownUsers: false, + betas: [CONST.BETAS.ALL], + optionsToExclude: [], + }); + + expect(canCreate).toBe(false); + }); + }); }); From 80e5b837de13c83634540c003bc0de399a9f59e4 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 24 Apr 2024 12:48:15 +0200 Subject: [PATCH 21/31] add more tests for filterOptions --- tests/unit/OptionsListUtilsTest.ts | 76 +++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 629acaf54443..89eb1e398795 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -262,6 +262,22 @@ describe('OptionsListUtils', () => { }, }; + const REPORTS_WITH_WORKSPACE: OnyxCollection = { + ...REPORTS, + '15': { + lastReadTime: '2021-01-14 11:25:39.295', + lastVisibleActionCreated: '2022-11-22 03:26:02.015', + isPinned: false, + isChatRoom: false, + reportID: '15', + participantAccountIDs: [2, 1], + visibleChatMemberAccountIDs: [2, 1], + reportName: 'Test Workspace', + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + }, + }; + const REPORTS_WITH_CHAT_ROOM = { ...REPORTS, 15: { @@ -354,14 +370,16 @@ describe('OptionsListUtils', () => { let OPTIONS_WITH_CONCIERGE: OptionsListUtils.OptionList; let OPTIONS_WITH_CHRONOS: OptionsListUtils.OptionList; let OPTIONS_WITH_RECEIPTS: OptionsListUtils.OptionList; - let OPTIONS_WITH_WORKSPACES: OptionsListUtils.OptionList; + let OPTIONS_WITH_WORKSPACE_ROOM: OptionsListUtils.OptionList; + let OPTIONS_WITH_WORKSPACE: OptionsListUtils.OptionList; beforeEach(() => { OPTIONS = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS); OPTIONS_WITH_CONCIERGE = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_CONCIERGE, REPORTS_WITH_CONCIERGE); OPTIONS_WITH_CHRONOS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_CHRONOS, REPORTS_WITH_CHRONOS); OPTIONS_WITH_RECEIPTS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_RECEIPTS, REPORTS_WITH_RECEIPTS); - OPTIONS_WITH_WORKSPACES = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE_ROOMS); + OPTIONS_WITH_WORKSPACE_ROOM = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE_ROOMS); + OPTIONS_WITH_WORKSPACE = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE); }); it('getSearchOptions()', () => { @@ -712,7 +730,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(1); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options - const filteredReportsWithWorkspaceRooms = Object.values(OPTIONS_WITH_WORKSPACES.reports).reduce((filtered, option) => { + const filteredReportsWithWorkspaceRooms = Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).reduce((filtered, option) => { const report = option.item; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { @@ -725,7 +743,7 @@ describe('OptionsListUtils', () => { results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], ''); // Then we should expect the DMS, the group chats and the workspace room to show // We should expect all the recent reports to show, excluding the archived rooms - expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACES.reports).length - 1); + expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).length - 1); // When we search for a workspace room results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], 'Avengers Room'); @@ -2707,9 +2725,9 @@ describe('OptionsListUtils', () => { expect(filteredOptions.recentReports[0].login).toBe('barry.allen@expensify.com'); }); - it('should include workspaces in the search results', () => { + it('should include workspace rooms in the search results', () => { const searchText = 'avengers'; - const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_WORKSPACES, '', [CONST.BETAS.ALL]); + const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_WORKSPACE_ROOM, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); @@ -2748,6 +2766,52 @@ describe('OptionsListUtils', () => { expect(filteredOptions.recentReports[0].text).toBe('Mister Fantastic'); expect(filteredOptions.recentReports[1].text).toBe('Mister Fantastic'); }); + + it('should return the user to invite when the search value is a valid, non-existent email', () => { + const searchText = 'test@email.com'; + + const options = OptionsListUtils.getSearchOptions(OPTIONS, ''); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filteredOptions.userToInvite?.login).toBe(searchText); + }); + + it('should not return any results if the search value is on an exluded logins list', () => { + const searchText = 'admin@expensify.com'; + + const options = OptionsListUtils.getFilteredOptions(OPTIONS.reports, OPTIONS.personalDetails, [], searchText, [], CONST.EXPENSIFY_EMAILS); + const filterOptions = OptionsListUtils.filterOptions(options, searchText, {excludeLogins: CONST.EXPENSIFY_EMAILS}); + expect(filterOptions.recentReports.length).toBe(0); + }); + + it('should return the user to invite when the search value is a valid, non-existent email and the user is not excluded', () => { + const searchText = 'test@email.com'; + + const options = OptionsListUtils.getSearchOptions(OPTIONS, ''); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {excludeLogins: CONST.EXPENSIFY_EMAILS}); + + expect(filteredOptions.userToInvite?.login).toBe(searchText); + }); + + it('should return the workspaces that match the participant login', () => { + const searchText = 'reedrichards@expensify.com'; + + const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_WORKSPACE, ''); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + const recentReportsNames = filteredOptions.recentReports.map((option) => option.text); + + expect(recentReportsNames).toContain('Test Workspace'); + }); + + it('should return limited amount of recent reports if the limit is set', () => { + const searchText = ''; + + const options = OptionsListUtils.getSearchOptions(OPTIONS, ''); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {maxRecentReportsToShow: 2}); + + expect(filteredOptions.recentReports.length).toBe(2); + }); }); describe('canCreateOptimisticPersonalDetailOption', () => { From 49635ed85dc871900d1406032c45927d03b56de0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 26 Apr 2024 08:07:23 +0200 Subject: [PATCH 22/31] fix types --- src/libs/OptionsListUtils.ts | 1 - src/pages/NewChatPage.tsx | 1 + .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7337ac2b01ea..92a4c9c95b94 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2031,7 +2031,6 @@ function getFilteredOptions( policyReportFieldOptions: string[] = [], recentlyUsedPolicyReportFieldOptions: string[] = [], includePersonalDetails = true, - maxRecentReportsToShow = 5, ) { return getOptions( {reports, personalDetails}, diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 3f0c9a23da3f..7df63034c6a0 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -70,6 +70,7 @@ function useOptions({isGroupChat}: NewChatPageProps) { true, undefined, undefined, + CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, undefined, true, ); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index addc174bc353..a5f6b5c8f8f9 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -123,14 +123,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, false, - 0, + isCategorizeOrShareAction ? 0 : undefined, undefined, undefined, undefined, undefined, undefined, !isCategorizeOrShareAction, - isCategorizeOrShareAction ? 0 : undefined, ); return optionList; From 49a421fdd7f954643bb37dea5513dbb944225857 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 26 Apr 2024 09:46:22 +0200 Subject: [PATCH 23/31] fix tests --- tests/unit/OptionsListUtilsTest.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 2ee8169bf4f5..50ca6d092056 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -318,6 +318,10 @@ describe('OptionsListUtils', () => { reportID: '15', participantAccountIDs: [2, 1], visibleChatMemberAccountIDs: [2, 1], + participants: { + 1: {}, + 2: {}, + }, reportName: 'Test Workspace', type: CONST.REPORT.TYPE.CHAT, chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, From 4bb2ecec0dffbd25ddcf7ac0363739eb69cd5b2d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 6 May 2024 10:41:48 +0200 Subject: [PATCH 24/31] fix tests --- src/libs/OptionsListUtils.ts | 47 +++---------- ...yForRefactorRequestParticipantsSelector.js | 9 +-- tests/unit/OptionsListUtilsTest.ts | 69 ------------------- 3 files changed, 10 insertions(+), 115 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 34712fa21ed1..24b50490313d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1572,42 +1572,6 @@ function canCreateOptimisticPersonalDetailOption({ return noOptions || noOptionsMatchExactly; } -/** - * Builds the option with optimistic personal details - */ -function createOptimisticPersonalDetailOption(searchValue: string, {reportActions = {}, showChatPreviewLine = false}) { - const optimisticAccountID = UserUtils.generateAccountID(searchValue); - const personalDetailsExtended = { - ...allPersonalDetails, - [optimisticAccountID]: { - accountID: optimisticAccountID, - login: searchValue, - avatar: UserUtils.getDefaultAvatar(optimisticAccountID), - }, - }; - const optimisticUser = createOption([optimisticAccountID], personalDetailsExtended, null, reportActions, { - showChatPreviewLine, - }); - - optimisticUser.isOptimisticAccount = true; - optimisticUser.login = searchValue; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - optimisticUser.text = optimisticUser.text || searchValue; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - optimisticUser.alternateText = optimisticUser.alternateText || searchValue; - - // If user doesn't exist, use a fallback avatar - optimisticUser.icons = [ - { - source: UserUtils.getAvatar('', optimisticAccountID), - name: searchValue, - type: CONST.ICON_TYPE_AVATAR, - }, - ]; - - return optimisticUser; -} - /** * We create a new user option if the following conditions are satisfied: * - There's no matching recent report and personal detail option @@ -2364,7 +2328,7 @@ function getFirstKeyForList(data?: Option[] | null) { * Filters options based on the search input value */ function filterOptions(options: Options, searchInputValue: string, config?: FilterOptionsConfig): Options { - const {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], maxRecentReportsToShow = 0} = config ?? {}; + const {sortByReportTypeInSearch = false, canInviteUser = true, betas = [], maxRecentReportsToShow = 0, excludeLogins = []} = config ?? {}; if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) { return {...options, recentReports: options.recentReports.slice(0, maxRecentReportsToShow)}; } @@ -2377,6 +2341,12 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) const emailRegex = /\.(?=[^\s@]*@)/g; + const optionsToExclude: Option[] = [{login: CONST.EMAIL.NOTIFICATIONS}]; + + excludeLogins.forEach((login) => { + optionsToExclude.push({login}); + }); + const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => { const keys: string[] = []; const visibleChatMemberAccountIDs = item.participantsList ?? []; @@ -2462,6 +2432,8 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt userToInvite = getUserToInviteOption({ searchValue, betas, + selectedOptions: config?.selectedOptions, + optionsToExclude, }); } } @@ -2516,7 +2488,6 @@ export { getTaxRatesSection, getFirstKeyForList, canCreateOptimisticPersonalDetailOption, - createOptimisticPersonalDetailOption, }; export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index fe6f4fc684f4..a4baeb6bd836 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -183,14 +183,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF return [newSections, '']; } - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( - debouncedSearchTerm, - participants, - chatOptions.recentReports, - chatOptions.personalDetails, - personalDetails, - true, - ); + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(debouncedSearchTerm, participants, chatOptions.recentReports, chatOptions.personalDetails, personalDetails, true); newSections.push(formatResults.section); diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 50ca6d092056..ca89a6a43c42 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -2870,84 +2870,18 @@ describe('OptionsListUtils', () => { describe('canCreateOptimisticPersonalDetailOption', () => { const VALID_EMAIL = 'valid@email.com'; - const INVALID_EMAIL = 'invalid-email'; it('should allow to create optimistic personal detail option if email is valid', () => { const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ searchValue: VALID_EMAIL, recentReportOptions: OPTIONS.reports, personalDetailsOptions: OPTIONS.personalDetails, currentUserOption: null, - selectedOptions: [], excludeUnknownUsers: false, - betas: [CONST.BETAS.ALL], - optionsToExclude: [], }); expect(canCreate).toBe(true); }); - it('should not allow to create option if email is not valid', () => { - const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ - searchValue: INVALID_EMAIL, - recentReportOptions: OPTIONS.reports, - personalDetailsOptions: OPTIONS.personalDetails, - currentUserOption: null, - selectedOptions: [], - excludeUnknownUsers: false, - betas: [CONST.BETAS.ALL], - optionsToExclude: [], - }); - - expect(canCreate).toBe(false); - }); - - it('should not allow to create option if email is already in the list', () => { - const optimisticOption = OptionsListUtils.createOptimisticPersonalDetailOption(VALID_EMAIL, {}); - const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ - searchValue: VALID_EMAIL, - recentReportOptions: OPTIONS.reports, - personalDetailsOptions: OPTIONS.personalDetails, - currentUserOption: null, - selectedOptions: [optimisticOption], - excludeUnknownUsers: false, - betas: [CONST.BETAS.ALL], - optionsToExclude: [], - }); - - expect(canCreate).toBe(false); - }); - - it('should not allow to create option if email is restricted', () => { - const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ - searchValue: VALID_EMAIL, - recentReportOptions: OPTIONS.reports, - personalDetailsOptions: OPTIONS.personalDetails, - currentUserOption: null, - selectedOptions: [], - excludeUnknownUsers: false, - betas: [CONST.BETAS.ALL], - optionsToExclude: [VALID_EMAIL], - }); - - expect(canCreate).toBe(false); - }); - - it('should not allow to create option if email is already on the list', () => { - const optimisticOption = OptionsListUtils.createOptimisticPersonalDetailOption(VALID_EMAIL, {}); - const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ - searchValue: VALID_EMAIL, - recentReportOptions: OPTIONS.reports, - personalDetailsOptions: [...OPTIONS.personalDetails, optimisticOption], - currentUserOption: null, - selectedOptions: [], - excludeUnknownUsers: false, - betas: [CONST.BETAS.ALL], - optionsToExclude: [], - }); - - expect(canCreate).toBe(false); - }); - it('should not allow to create option if email is an email of current user', () => { const currentUserEmail = 'tonystark@expensify.com'; const canCreate = OptionsListUtils.canCreateOptimisticPersonalDetailOption({ @@ -2955,10 +2889,7 @@ describe('OptionsListUtils', () => { recentReportOptions: OPTIONS.reports, personalDetailsOptions: OPTIONS.personalDetails, currentUserOption: null, - selectedOptions: [], excludeUnknownUsers: false, - betas: [CONST.BETAS.ALL], - optionsToExclude: [], }); expect(canCreate).toBe(false); From 42e2aa925dc255c9aef879b70c981e6e4c9507d9 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 6 May 2024 10:50:32 +0200 Subject: [PATCH 25/31] update to max recent reports to show --- src/pages/EditReportFieldDropdown.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx index b17a1588b774..579d8e6c53ca 100644 --- a/src/pages/EditReportFieldDropdown.tsx +++ b/src/pages/EditReportFieldDropdown.tsx @@ -10,6 +10,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentlyUsedReportFields} from '@src/types/onyx'; @@ -86,7 +87,7 @@ function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptio false, false, undefined, - 5, + CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, undefined, undefined, true, From 9504d746cd8c7c79e73f9aaa6bc6b873ce1a763b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 6 May 2024 17:16:56 +0200 Subject: [PATCH 26/31] fix not displaying workspaces in recents --- src/libs/OptionsListUtils.ts | 8 ++++---- ...oneyTemporaryForRefactorRequestParticipantsSelector.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 24b50490313d..d4db61a78944 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2403,10 +2403,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login ?? '', item.login?.replace(emailRegex, '') ?? '']), ); - if (maxRecentReportsToShow > 0 && recentReports.length > maxRecentReportsToShow) { - recentReports.splice(maxRecentReportsToShow); - } - return { recentReports: recentReports ?? [], personalDetails: personalDetails ?? [], @@ -2438,6 +2434,10 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt } } + if (maxRecentReportsToShow > 0 && recentReports.length > maxRecentReportsToShow) { + recentReports.splice(maxRecentReportsToShow); + } + return { personalDetails, recentReports, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index a4baeb6bd836..a405af5e7d6c 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -126,6 +126,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, false, + 0, isCategorizeOrShareAction ? 0 : undefined, undefined, undefined, From ad4a1700c93f9928eb932e68530cafbc7ebd5151 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 11 Jun 2024 11:22:28 +0200 Subject: [PATCH 27/31] resolve conficts --- src/libs/OptionsListUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 5d8c5d6cf816..74d8746250aa 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -218,7 +218,7 @@ type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: bo type FilterOptionsConfig = Pick< GetOptionsConfig, 'sortByReportTypeInSearch' | 'canInviteUser' | 'betas' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' -> & {preferChatroomsOverThreads: boolean};; +> & {preferChatroomsOverThreads: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -2158,9 +2158,7 @@ function getFilteredOptions( includePolicyReportFieldOptions = false, policyReportFieldOptions: string[] = [], recentlyUsedPolicyReportFieldOptions: string[] = [], - maxRecentReportsToShow = 5, includeInvoiceRooms = false, - includePersonalDetails = true, ) { return getOptions( {reports, personalDetails}, From fb19c5587f78ca52a14ea8c63ea830c79fc22561 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 11 Jun 2024 11:43:53 +0200 Subject: [PATCH 28/31] fix: fix typescript --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 74d8746250aa..e971bfb089ca 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -218,7 +218,7 @@ type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: bo type FilterOptionsConfig = Pick< GetOptionsConfig, 'sortByReportTypeInSearch' | 'canInviteUser' | 'betas' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' -> & {preferChatroomsOverThreads: boolean}; +> & {preferChatroomsOverThreads?: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can From 7716743edd3f4430371611f2e80b3c0d4d6a3bb9 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 11 Jun 2024 12:02:09 +0200 Subject: [PATCH 29/31] fix lint --- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index e327cb779e08..a16b844ffd2d 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -210,7 +210,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic const headerMessage = OptionsListUtils.getHeaderMessage( (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length !== 0, - Boolean(chatOptions?.userToInvite), + !!chatOptions?.userToInvite, debouncedSearchTerm.trim(), participants.some((participant) => participant?.searchText?.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), ); From 1d4cabc739777a01427c35592d6035e614858d2a Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Tue, 11 Jun 2024 14:58:25 +0200 Subject: [PATCH 30/31] adjust default return, fix tests --- src/libs/OptionsListUtils.ts | 2 +- tests/unit/OptionsListUtilsTest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e971bfb089ca..84fdfde397f0 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2505,7 +2505,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt } return { - personalDetails: [], + personalDetails, recentReports: orderOptions(recentReports, searchValue, {preferChatroomsOverThreads}), userToInvite, currentUserOption: matchResults.currentUserOption, diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 3133aec92e23..a08b43c337cb 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -491,7 +491,7 @@ describe('OptionsListUtils', () => { // When we don't include personal detail to the result results = OptionsListUtils.getFilteredOptions( [], - OPTIONS.personalDetails, + [], [], '', undefined, From c55c40fe0e35a21928742aca5ec0f9eab0b2a2bf Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Mon, 17 Jun 2024 16:02:47 +0200 Subject: [PATCH 31/31] fix: fix recent search logic --- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index a16b844ffd2d..b5263b158e77 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -114,7 +114,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, false, - isCategorizeOrShareAction ? 0 : undefined, + 0, undefined, undefined, undefined,