From 977e1226c0118110ab30a65b1f336bbf0a142ef4 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 12 Apr 2024 10:10:06 +0200 Subject: [PATCH 001/252] 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 002/252] 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 003/252] 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 004/252] 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 005/252] 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 006/252] 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 007/252] 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 008/252] 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 009/252] 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 f16c1e8ae05002426a87204e03d3115f6d42b106 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 19 Apr 2024 11:56:12 +0200 Subject: [PATCH 010/252] wip --- src/components/ReportActionItem/MoneyRequestView.tsx | 1 + src/libs/TransactionUtils.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c5cad0eccdeb..e2116ac346f1 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -272,6 +272,7 @@ function MoneyRequestView({ Transaction.clearError(transaction.transactionID); }} > + transactionId: {transaction?.transactionID} {showMapAsImage ? ( diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index a5b85b87e37e..eb097f4901be 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -589,6 +589,13 @@ function getRecentTransactions(transactions: Record, size = 2): .slice(0, size); } +/** + * Check if transaction is duplicated + */ +function isDuplicate(transactionID: string, checkDissmissed: boolean): boolean { + return true; +} + /** * Check if transaction is on hold */ @@ -597,6 +604,10 @@ function isOnHold(transaction: OnyxEntry): boolean { return false; } + if (isDuplicate(transaction.transactionID, true)) { + return true; + } + return !!transaction.comment?.hold; } @@ -700,6 +711,7 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, + isDuplicate, }; export type {TransactionChanges}; From 7e90c81446e3f1032da29857c8a07d2adc485ee5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 19 Apr 2024 12:57:29 +0200 Subject: [PATCH 011/252] feat: added new routes --- src/ROUTES.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7d73d8e55503..34bdb22e9778 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -707,6 +707,30 @@ const ROUTES = { route: 'r/:reportID/transaction/:transactionID/receipt', getRoute: (reportID: string, transactionID: string) => `r/${reportID}/transaction/${transactionID}/receipt` as const, }, + TRANSACTION_DUPLICATE_REVIEW_PAGE: { + route: 'r/:threadReportID/duplicates/review', + getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review`, + }, + TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: { + route: 'r/:threadReportID/duplicates/review/merchant', + getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/merchant`, + }, + TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: { + route: 'r/:threadReportID/duplicates/review/category', + getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/category`, + }, + TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: { + route: 'r/:threadReportID/duplicates/review/tag', + getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/tag`, + }, + TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: { + route: 'r/:threadReportID/duplicates/confirm', + getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/confirm`, + }, + TRANSACTION_DUPLICATE_CONFIRM: { + route: 'r/:threadReportID/duplicates/review/description', + getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/description`, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const, From 4aaef6c20b75f901b66dbe6031e47b5ff390b58b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 12:15:32 +0200 Subject: [PATCH 012/252] 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 013/252] 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 014/252] 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 015/252] 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 016/252] 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 7a8e5f39c39015ed37018e98e8174a991ec55792 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Apr 2024 16:50:54 +0200 Subject: [PATCH 017/252] feat: added navigation config for review dupe detection, created initial screen --- src/ROUTES.ts | 44 +++++++++---------- src/SCREENS.ts | 10 +++++ .../ModalStackNavigators/index.tsx | 6 +++ .../Navigators/RightModalNavigator.tsx | 4 ++ src/libs/Navigation/linkingConfig/config.ts | 13 ++++++ src/libs/Navigation/types.ts | 23 ++++++++++ src/pages/TransactionDuplicate/Review.tsx | 13 ++++++ 7 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 src/pages/TransactionDuplicate/Review.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2ad96e234ffd..184af4f25630 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -708,28 +708,28 @@ const ROUTES = { }, TRANSACTION_DUPLICATE_REVIEW_PAGE: { route: 'r/:threadReportID/duplicates/review', - getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review`, - }, - TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: { - route: 'r/:threadReportID/duplicates/review/merchant', - getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/merchant`, - }, - TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: { - route: 'r/:threadReportID/duplicates/review/category', - getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/category`, - }, - TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: { - route: 'r/:threadReportID/duplicates/review/tag', - getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/tag`, - }, - TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: { - route: 'r/:threadReportID/duplicates/confirm', - getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/confirm`, - }, - TRANSACTION_DUPLICATE_CONFIRM: { - route: 'r/:threadReportID/duplicates/review/description', - getRoute: (threadReportID: string) => `/r/${threadReportID}/duplicates/review/description`, - }, + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review` as const, + }, + // TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: { + // route: 'r/:threadReportID/duplicates/review/merchant', + // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/merchant` as const, + // }, + // TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: { + // route: 'r/:threadReportID/duplicates/review/category', + // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/category` as const, + // }, + // TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: { + // route: 'r/:threadReportID/duplicates/review/tag', + // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tag` as const, + // }, + // TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: { + // route: 'r/:threadReportID/duplicates/confirm', + // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const, + // }, + // TRANSACTION_DUPLICATE_CONFIRM: { + // route: 'r/:threadReportID/duplicates/review/description', + // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/description` as const, + // }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index aed70dc1e949..eaf49e98e9df 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -131,6 +131,7 @@ const SCREENS = { ROOM_INVITE: 'RoomInvite', REFERRAL: 'Referral', PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', + TRANSACTION_DUPLICATE: 'TransactionDuplicate', }, ONBOARDING_MODAL: { ONBOARDING: 'Onboarding', @@ -167,6 +168,15 @@ const SCREENS = { STATE_SELECTOR: 'Money_Request_State_Selector', }, + TRANSACTION_DUPLICATE: { + REVIEW: 'Transaction_Duplicate_Review', + // MERCHANT: 'Transaction_Duplicate_Merchant', + // CATEGORY: 'Transaction_Duplicate_Category', + // TAG: 'Transaction_Duplicate_Tag', + // DESCRIPTION: 'Transaction_Duplicate_Description', + // CONFIRM: 'Transaction_Duplicate_Confirm', + }, + IOU_SEND: { ADD_BANK_ACCOUNT: 'IOU_Send_Add_Bank_Account', ADD_DEBIT_CARD: 'IOU_Send_Add_Debit_Card', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 6ec283f709c0..15a750f4f688 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -28,6 +28,7 @@ import type { SplitDetailsNavigatorParamList, TaskDetailsNavigatorParamList, TeachersUniteNavigatorParamList, + TransactionDuplicateNavigatorParamList, WalletStatementNavigatorParamList, WorkspaceSwitcherNavigatorParamList, } from '@navigation/types'; @@ -321,6 +322,10 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({ [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: () => require('../../../../pages/ProcessMoneyRequestHoldPage').default as React.ComponentType, }); +const TransactionDuplicateStackNavigator = createModalStackNavigator({ + [SCREENS.TRANSACTION_DUPLICATE.REVIEW]: () => require('../../../../pages/TransactionDuplicate/Review').default as React.ComponentType, +}); + export { AddPersonalBankAccountModalStackNavigator, DetailsModalStackNavigator, @@ -351,4 +356,5 @@ export { WalletStatementStackNavigator, ProcessMoneyRequestHoldStackNavigator, WorkspaceSettingsModalStackNavigator, + TransactionDuplicateStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index c421bdc82028..f3b859a58f74 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -137,6 +137,10 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) { name="ProcessMoneyRequestHold" component={ModalStackNavigators.ProcessMoneyRequestHoldStackNavigator} /> + diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 3964b7dcd074..3d4699929beb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -589,6 +589,19 @@ const config: LinkingOptions['config'] = { [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD, }, }, + [SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE]: { + screens: { + [SCREENS.TRANSACTION_DUPLICATE.REVIEW]: { + path: ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.route, + exact: true, + }, + // [SCREENS.TRANSACTION_DUPLICATE.MERCHANT]: ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.route, + // [SCREENS.TRANSACTION_DUPLICATE.CATEGORY]: ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.route, + // [SCREENS.TRANSACTION_DUPLICATE.TAG]: ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.route, + // [SCREENS.TRANSACTION_DUPLICATE.DESCRIPTION]: ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.route, + // [SCREENS.TRANSACTION_DUPLICATE.CONFIRM]: ROUTES.TRANSACTION_DUPLICATE_CONFIRM.route, + }, + }, [SCREENS.RIGHT_MODAL.SPLIT_DETAILS]: { screens: { [SCREENS.SPLIT_DETAILS.ROOT]: ROUTES.SPLIT_BILL_DETAILS.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f564ee01cbf7..c691d17650e6 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -619,6 +619,27 @@ type PrivateNotesNavigatorParamList = { }; }; +type TransactionDuplicateNavigatorParamList = { + [SCREENS.TRANSACTION_DUPLICATE.REVIEW]: { + threadReportID: string; + }; + // [SCREENS.TRANSACTION_DUPLICATE.MERCHANT]: { + // threadReportID: string; + // }; + // [SCREENS.TRANSACTION_DUPLICATE.CATEGORY]: { + // threadReportID: string; + // }; + // [SCREENS.TRANSACTION_DUPLICATE.TAG]: { + // threadReportID: string; + // }; + // [SCREENS.TRANSACTION_DUPLICATE.DESCRIPTION]: { + // threadReportID: string; + // }; + // [SCREENS.TRANSACTION_DUPLICATE.CONFIRM]: { + // threadReportID: string; + // }; +}; + type LeftModalNavigatorParamList = { [SCREENS.LEFT_MODAL.CHAT_FINDER]: NavigatorScreenParams; [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: NavigatorScreenParams; @@ -651,6 +672,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.PROCESS_MONEY_REQUEST_HOLD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REFERRAL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE]: NavigatorScreenParams; }; type WorkspacesCentralPaneNavigatorParamList = { @@ -863,4 +885,5 @@ export type { WelcomeVideoModalNavigatorParamList, WorkspaceSwitcherNavigatorParamList, WorkspacesCentralPaneNavigatorParamList, + TransactionDuplicateNavigatorParamList, }; diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx new file mode 100644 index 000000000000..5d0c6fc3b996 --- /dev/null +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; + +function TransactionDuplicateReview() { + return ( + + Review + + ); +} + +export default TransactionDuplicateReview; From 05f60cb7e1f6e5cf4d1474ef8d0ac39f8021d896 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 22 Apr 2024 17:01:25 +0200 Subject: [PATCH 018/252] 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 019/252] 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 020/252] 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 021/252] 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 022/252] 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 023/252] 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 024/252] 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 93c886446509a97880b2f3167d713ad318390c44 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Apr 2024 15:56:09 +0200 Subject: [PATCH 025/252] feat: added new pages for duplicate transaction flow, added new endpoint config --- .../MoneyRequestPreviewContent.tsx | 13 ++++++ .../API/parameters/DismissViolationParams.ts | 6 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Transaction.ts | 43 +++++++++++++++--- .../DuplicateTransactionItem.tsx | 38 ++++++++++++++++ .../DuplicateTransactionsList.tsx | 35 +++++++++++++++ src/pages/TransactionDuplicate/Review.tsx | 44 ++++++++++++++++--- 8 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 src/libs/API/parameters/DismissViolationParams.ts create mode 100644 src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx create mode 100644 src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 7f70a3e538a9..399dad3d9e0b 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -1,9 +1,11 @@ +import {useRoute} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import {truncate} from 'lodash'; import lodashSortBy from 'lodash/sortBy'; import React from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; +import Button from '@components/Button'; import ConfirmedRoute from '@components/ConfirmedRoute'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -33,6 +35,7 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type {IOUMessage} from '@src/types/onyx/OriginalMessage'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -63,6 +66,7 @@ function MoneyRequestPreviewContent({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); + const route = useRoute(); const parser = new ExpensiMark(); const sessionAccountID = session?.accountID; @@ -97,6 +101,7 @@ function MoneyRequestPreviewContent({ const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; const shouldShowRBR = hasViolations || hasFieldErrors || (!(isSettled && !isSettlementOrApprovalPartial) && !(ReportUtils.isReportApproved(iouReport) && !isSettlementOrApprovalPartial) && isOnHold); + const isReviewDuplicateTransaction = route.path === `/${ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(route.params?.threadReportID)}`; /* Show the merchant for IOUs and expenses only if: @@ -345,6 +350,14 @@ function MoneyRequestPreviewContent({ ]} > {childContainer} + {isReviewDuplicateTransaction && ( +