From 5c9d8cd7fddc03167f580eb64169cda0f82e39e2 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Thu, 13 Apr 2023 12:43:49 -0400 Subject: [PATCH 001/784] Update Expensify-Lounge.md --- docs/articles/other/Expensify-Lounge.md | 82 ++++++++++++++----------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/docs/articles/other/Expensify-Lounge.md b/docs/articles/other/Expensify-Lounge.md index 925ab43f2548..c5214817977e 100644 --- a/docs/articles/other/Expensify-Lounge.md +++ b/docs/articles/other/Expensify-Lounge.md @@ -5,52 +5,64 @@ description: How to get the most out of the Expensify Lounge. # What is the Expensify Lounge? -The Expensify Lounge is a place for people to get shit done in a beautiful environment with great coffee and people to collaborate with. Please use this guide to learn how to utilize the Expensify Lounge best. +The Expensify Lounge is a place where people go to Get Shit Done. It's a beautiful environment with great coffee and a group of people to collaborate with. Check out this guide on how to best utilize the Expensify Lounge! # The Two Rules -*Number One - Get Shit Done* -The lounge is a space for people to get work done. It is optimized to be the perfect environment for you to focus on your work and collaborate with others to advance your ideas. We provide this by asking our members to keep in mind: +### Rule #1 - Get Shit Done -- **#focus** - Use the space for how it was designed, and do not distract from others' focus. It is beautiful, social, and collaborative, but it is built for work. -- **#urgency** - Working remotely is great but often introduces delay. Use the lounge to meet with whoever you need when time is of the essence. +The Lounge is a space for people to get work done. It is optimized to be the perfect environment for you to focus on your work, collaborate with others, and advance your most wild and creative ideas. To make this a reality, we ask our members to keep the following in mind: + +- **#focus** - Use the space for how it was designed and do not distract from others' focus. The space is beautiful, social, and collaborative, but it was created to help our members work effectively. +- **#urgency** - Working remotely is great, but there's nothing like real-time collaboration with your colleagues. Use the lounge to meet with co-workers IRL to continue the progress on whatever it is you're working on. - **#results** - Don't mistake time for effort or effort for output. Upon arrival, visualize what you want to accomplish, and don't leave until it's done. -*Number Two - Don’t Ruin it for Everyone Else* -We want this place to be incredible for everyone and to get always be getting better. We don’t want too many rules, but we do need some guardrails. The following guiding principles allow us to do that: -- **#writeitdown** - If you can help others learn from you, do so. Write a blog post, a document, or a post in Expensify Chat to share with others so they can learn from you. This includes making the Expensify Lounge a better space. Feel free to write down any improvements so we can continue to get better. -- **#showup** - If you are in the lounge, be fully present. Meet others, and collaborate in social rooms. The point is to build a community of people getting shit done; you’ll get out what you put in. -- **#oneteam** - We provide an inclusive community for all. Do not discriminate against others, and aim to go out of your way to include people that want to be included. -- **#nocreeps** - Do not make people feel uncomfortable with your words or actions. If you are made to feel uncomfortable or think others are, use this escalation process. +### Rule #2 - Don’t Ruin it for Everyone Else + +We want this place to be incredible, innovative, and always elvoving. To achieve that, we have some general guidelines: + +- **#writeitdown** - If you can help others learn from you, do so. Write a blog post, a document, or a post in Expensify Chat to share with others. This includes making the Expensify Lounge a better space. Feel free to write down any improvements so we can make it better. +- **#showup** - If you are in the lounge, be fully present. Meet others, and collaborate in social rooms. The point is to build a community of people who are focused on getting shit done; you’ll get out what you put in. +- **#oneteam** - Providing an inclusive community is our priority, and we do not tolerate any form of discrimination. Aim to go out of your way to include people who want to be included. +- **#nocreeps** - Do not make people feel uncomfortable with your words or actions. If you are made to feel uncomfortable or notice this happening to someone else, you can use the escalation process outlined in the FAQ section. # How to Use the Expensify Lounge -Below is a guide on how to help our members get the most out of the lounge centered around the rules above. +Keeping those two rules in mind, below is a guide on how our members can get the most out of the lounge. -Rule #1 - Getting Shit done: -- **Order drinks from Concierge** - [Write Concierge here](https://new.expensify.com/concierge) to ask lounge questions or order beverages. Concierge will bring it to you where you are sitting! -- **Using an office** - Offices are first come, first serve so go into any open one. Please keep office use to an hour or less. We do not allow any reservations at the moment. -- **Lounge hours** - The lounge will be open from 8am-6pm PT, Monday through Friday and closed on some major holidays. Go to our google maps profile to see our opening for specific holidays. -- **Make the lounge better** - Make any suggestions to improve your lounge experience in [#announce - Expensify Lounge](https://new.expensify.com/r/8292963527436014). +### Rule #1 - Getting Shit Done: +- **Order drinks from Concierge** - [Write Concierge here](https://new.expensify.com/concierge) to ask lounge questions or order beverages. Concierge will bring your order directly to you! +- **Using an office** - Offices are first come, first serve. If an office is open, feel free to use it! Please keep office use to under an hour. We currently do not allow reserving offices. +- **Lounge hours** - The lounge will be open from 8am-6pm PT, Monday through Friday and closed on some major holidays. You can review our Google Maps profile to check our holiday hours. +- **Make the lounge better** - Make any suggestions to improve the lounge experience in [#announce - Expensify Lounge](https://new.expensify.com/r/8292963527436014). -Rule #2 - Not Ruining it for Everyone Else -- **Offices are for calls** - Please do not occupy an office unless you have a call or collaborative meeting happening. Please do not stay in the offices longer than an hour. -- **Respect other people** - Please do not be too loud or distracting from others trying to work. While in chat spaces, be respectful of others’ viewpoints and keep a positive environment. -- **Stay home if you’re sick** - If you feel sick, please consider not visiting the lounge, or consider wearing a mask in public areas. -- **If you see something, say something** - If you are made to feel uncomfortable or witness others being made uncomfortable, let Concierge know. If this is happening in Chat, use our moderation tools (outlined below in the FAQ) to apply the applicable level of moderation, which includes flagging our team to take action. +### Rule #2 - Not Ruining it for Everyone Else: +- **Offices are for calls** - Please do not occupy an office unless you have a call or collaborative meeting happening, and don't stay in an office for longer than an hour. +- **Respect other people** - Please do not be too loud or distracting while others are trying to work. While collaborating in Expensify Chat, be respectful of others’ viewpoints and keep a positive environment. +- **Stay home if you’re sick** - If you feel sick, please do not visit the lounge, or consider wearing a mask in public areas. +- **If you see something, say something** - If you are made to feel uncomfortable or witness others being made uncomfortable, let Concierge know. If this is happening in Expensify Chat, use our moderation tools (outlined below in the FAQ) to apply the applicable level of moderation. -Thanks for joining us! We’re happy you are here to live rich, have fun, and save the world with us. +We’re so happy you are here to live rich, have fun, and save the world with us. Now, go enjoy the Expensify Lounge, and let's Get Shit Done! # FAQs -- What is Concierge? - - Concierge is our automated system that answers members with questions in real-time. Concierge has also been built with Expensify Lounges in mind, where questions regarding the local lounge get routed directly to Concierge IRL, who welcomes you into the lounge. Write Concierge for any drink requests or questions, and they’ll take care of everything for you. -- Who is invited to the Expensify Lounge? - - Everyone is invited to the Expensify Lounge! Whether you're an existing customer, or are just looking for a great space to get shit done, we'd love to have you. -- How do I escalate bad behavior? - - If you think something needs to be escalated from a chat space, use our escalation feature in chat to denote a chat as: - - “Spam” or “Inconsiderate” - this will send a whisper to the sender of the message warning them of the violation, and the message will have a flag applied to it which will be visible to all users. Concierge will not review these flags. - - “Intimidating” or “Bullying” - The message will be immediately hidden, and the content will be reviewed by our team. Users will be able to click to reveal the hidden message. After review and if verified to be one of the above, the message will be permanently hidden, and a message will be sent to the sender of the message, warning them of the violation. - - “Harassment” or “Assault” - The message will be immediately hidden and reviewed by our team. The user will be sent a message to warning them of the violation. Users will not be able to view the removed message. If not, the message will be visible again as usual. Concierge will also be able to block the user if they determine it necessary. - - If you witness something IRL, please write to Concierge referencing which lounge you are in, and they will escalate it appropriately. -- Where are other Expensify Lounge locations? - - Right now, we only have the San Francisco Lounge, but be on the lookout for more coming soon! + +#### What is Concierge? + +Concierge is our automated system that answers member questions in real-time. Questions regarding the local lounge will be routed directly to the lounge's Concierge. You can send Concierge a message if you have a drink request or general questions. They’ll take care of everything for you! + +#### Who is invited to the Expensify Lounge? + +Everyone is invited to the Expensify Lounge! Whether you're an existing customer, or you're someone looking for a great space to Get Shit Done, we'd love to have you. + +#### How do I escalate something that's making me or someone else uncomfortable? + +If you see something in Expensify Chat that should be escalated, you can use the escalation feature to mark a chat as: +- **Spam or Inconsiderate**: This will send a whisper to the sender of the message warning them of the violation, and the message will have a flag applied to it which will be visible to all users. Concierge will not review these flags. +- **Intimidating or Bullying**: The message will be immediately hidden, and the content will be reviewed by our team. After reviewing the message, and it's confirmed intimidation or bullying, the message will be permanently hidden and we'll communicate the violation to the sender of the message. +- **Harassment or Assault**: The message will be immediately hidden and reviewed by our team. The user will be sent a message to warning them of the violation, and Concierge can block the user if that's deemed necessary. + +If you witness something in-person, please write to Concierge referencing which lounge you are in, and they will escalate the issue appropriately. + +#### Where are other Expensify Lounge locations? + +Right now, we only have the San Francisco Lounge, but be on the lookout for more coming soon! From 977e1226c0118110ab30a65b1f336bbf0a142ef4 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 12 Apr 2024 10:10:06 +0200 Subject: [PATCH 002/784] 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 a9fedf4df0364c03b083c695762c56ea971b8d9d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 12 Apr 2024 16:17:34 +0700 Subject: [PATCH 003/784] feture: Stop using reportAction.originalMessage or reportAction.message.text --- .../ReportActionItem/ReportPreview.tsx | 2 +- src/libs/ReportActionsUtils.ts | 17 ++++++++++++-- src/libs/ReportUtils.ts | 9 +++++--- src/libs/TaskUtils.ts | 3 ++- src/libs/actions/IOU.ts | 22 +++++++++---------- src/libs/actions/Report.ts | 4 ++-- src/libs/actions/Task.ts | 2 +- src/pages/home/report/ReportActionItem.tsx | 2 +- tests/actions/IOUTest.ts | 2 +- tests/ui/UnreadIndicatorsTest.tsx | 3 ++- 10 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 190343e48abd..4dabab867523 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -170,7 +170,7 @@ function ReportPreview({ // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") let displayAmount = ''; - const actionMessage = action.message?.[0]?.text ?? ''; + const actionMessage = ReportActionUtils.getReportActionText(action); const splits = actionMessage.split(' '); splits.forEach((split) => { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 69917ce35c6b..f0774955726c 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,3 +1,4 @@ +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import fastMerge from 'expensify-common/lib/fastMerge'; import _ from 'lodash'; import lodashFindLast from 'lodash/findLast'; @@ -27,7 +28,7 @@ import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; -import type {OptimisticIOUReportAction} from './ReportUtils'; +import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; type LastVisibleMessage = { lastMessageTranslationKey?: string; @@ -915,6 +916,16 @@ function getMemberChangeMessageElements(reportAction: OnyxEntry): ]; } +function getReportActionHtml(reportAction: PartialReportAction): string | undefined { + return reportAction?.message?.[0]?.html; +} + +function getReportActionText(reportAction: PartialReportAction): string { + const html = getReportActionHtml(reportAction); + const parser = new ExpensiMark(); + return html ? parser.htmlToText(html) : ''; +} + function getMemberChangeMessageFragment(reportAction: OnyxEntry): Message { const messageElements: readonly MemberChangeMessageElement[] = getMemberChangeMessageElements(reportAction); const html = messageElements @@ -932,7 +943,7 @@ function getMemberChangeMessageFragment(reportAction: OnyxEntry): return { html: `${html}`, - text: reportAction?.message?.[0] ? reportAction?.message?.[0]?.text : '', + text: reportAction?.message?.[0] ? getReportActionText(reportAction) : '', type: CONST.REPORT.MESSAGE.TYPE.COMMENT, }; } @@ -1151,6 +1162,8 @@ export { isCurrentActionUnread, isActionableJoinRequest, isActionableJoinRequestPending, + getReportActionText, + getReportActionHtml, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6197a29cd4a6..57b404d66d68 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -192,6 +192,8 @@ type OptimisticIOUReportAction = Pick< | 'childReportID' >; +type PartialReportAction = OnyxEntry | Partial | OptimisticIOUReportAction | OptimisticApprovedReportAction | OptimisticSubmittedReportAction | undefined; + type ReportRouteParams = { reportID: string; isSubReportPageRoute: boolean; @@ -2890,7 +2892,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu const parentReportActionMessage = ( ReportActionsUtils.isApprovedOrSubmittedReportAction(parentReportAction) ? ReportActionsUtils.getReportActionMessageText(parentReportAction) - : parentReportAction?.message?.[0]?.text ?? '' + : ReportActionsUtils.getReportActionText(parentReportAction) ).replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; @@ -4646,7 +4648,7 @@ function canFlagReportAction(reportAction: OnyxEntry, reportID: st reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST; if (ReportActionsUtils.isWhisperAction(reportAction)) { // Allow flagging welcome message whispers as they can be set by any room creator - if (report?.description && !isCurrentUserAction && isOriginalMessageHaveHtml && reportAction?.originalMessage?.html === report.description) { + if (report?.description && !isCurrentUserAction && isOriginalMessageHaveHtml && ReportActionsUtils.getReportActionHtml(reportAction) === report.description) { return true; } @@ -5199,7 +5201,7 @@ function getTaskAssigneeChatOnyxData( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const displayname = allPersonalDetails?.[assigneeAccountID]?.displayName || allPersonalDetails?.[assigneeAccountID]?.login || ''; optimisticAssigneeAddComment = buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeAccountID, `assigned to ${displayname}`, parentReportID); - const lastAssigneeCommentText = formatReportLastMessageText(optimisticAssigneeAddComment.reportAction.message?.[0]?.text ?? ''); + const lastAssigneeCommentText = formatReportLastMessageText(ReportActionsUtils.getReportActionText(optimisticAssigneeAddComment.reportAction as ReportAction)); const optimisticAssigneeReport = { lastVisibleActionCreated: currentTime, lastMessageText: lastAssigneeCommentText, @@ -6038,4 +6040,5 @@ export type { Ancestor, OptimisticIOUReportAction, TransactionDetails, + PartialReportAction, }; diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index 19e1025a09c8..c60110527a60 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -6,6 +6,7 @@ import type {Report} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import * as Localize from './Localize'; +import {getReportActionText} from './ReportActionsUtils'; let allReports: OnyxCollection = {}; Onyx.connect({ @@ -29,7 +30,7 @@ function getTaskReportActionMessage(action: OnyxEntry): Pick; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLDCOMMENT) { - children = ; + children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { children = ; } else { diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index c8c74d4198ab..76ef36e710c5 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -2886,7 +2886,7 @@ describe('actions/IOU', () => { const ioupreview = ReportActionsUtils.getReportPreviewAction(chatReport?.reportID ?? '', iouReport?.reportID ?? ''); expect(ioupreview).toBeTruthy(); - expect(ioupreview?.message?.[0]?.text).toBe('rory@expensifail.com owes $300.00'); + expect(ReportActionsUtils.getReportActionText(ioupreview)).toBe('rory@expensifail.com owes $300.00'); // When we delete the first money request // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 2aeee2cc77bf..7747441bb2b4 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -15,6 +15,7 @@ import LocalNotification from '@libs/Notification/LocalNotification'; import * as NumberUtils from '@libs/NumberUtils'; import * as Pusher from '@libs/Pusher/pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; +import {getReportActionText} from '@libs/ReportActionsUtils'; import FontUtils from '@styles/utils/FontUtils'; import * as AppActions from '@userActions/App'; import * as Report from '@userActions/Report'; @@ -604,7 +605,7 @@ describe('Unread Indicators', () => { // Simulate the response from the server so that the comment can be deleted in this test lastReportAction = reportActions ? CollectionUtils.lastItem(reportActions) : undefined; Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - lastMessageText: lastReportAction?.message?.[0]?.text, + lastMessageText: getReportActionText(lastReportAction), lastVisibleActionCreated: DateUtils.getDBTime(lastReportAction?.timestamp), lastActorAccountID: lastReportAction?.actorAccountID, reportID: REPORT_ID, From d66c2e8c561dd4a9609c9cf60becdd39905589ee Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 12 Apr 2024 13:06:40 +0200 Subject: [PATCH 004/784] 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 9c9cd8459b79a3aaa3aa1230cf73a3586598c6de Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Sat, 13 Apr 2024 17:21:53 +0700 Subject: [PATCH 005/784] refactor using getReportActionMessage --- .../extractAttachmentsFromReport.ts | 4 +-- src/libs/ReportActionsUtils.ts | 36 ++++++++++++------- src/libs/ReportUtils.ts | 20 +++++------ src/libs/TaskUtils.ts | 4 +-- src/libs/actions/IOU.ts | 28 +++++++-------- src/libs/actions/Report.ts | 12 +++---- src/libs/actions/ReportActions.ts | 2 +- src/pages/home/report/ReportActionItem.tsx | 2 +- .../home/report/ReportActionItemMessage.tsx | 2 +- .../report/ReportActionItemMessageEdit.tsx | 2 +- .../home/report/ReportActionItemSingle.tsx | 3 +- src/pages/home/sidebar/SidebarLinksData.tsx | 3 +- tests/actions/IOUTest.ts | 4 +-- tests/perf-test/SidebarUtils.perf-test.ts | 3 +- 14 files changed, 70 insertions(+), 55 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts index d1185f88ccd5..d62ee6fefdeb 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts @@ -76,9 +76,9 @@ function extractAttachmentsFromReport(parentReportAction?: OnyxEntry', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`); + const html = (ReportActionsUtils.getReportActionMessage(action)?.html ?? '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`); htmlParser.write(html); }); htmlParser.end(); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f0774955726c..6e8d03861383 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -17,7 +17,7 @@ import type { OriginalMessageReimbursementDequeued, } from '@src/types/onyx/OriginalMessage'; import type Report from '@src/types/onyx/Report'; -import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; +import type {Message, OriginalMessage, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -108,19 +108,23 @@ function isDeletedAction(reportAction: OnyxEntry): boolean { - return (reportAction?.message?.[0]?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; + return (getReportActionMessage(reportAction)?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } function isReversedTransaction(reportAction: OnyxEntry) { - return (reportAction?.message?.[0]?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0; + return (getReportActionMessage(reportAction)?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0; } function isPendingRemove(reportAction: OnyxEntry | EmptyObject): boolean { if (isEmptyObject(reportAction)) { return false; } - return reportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; + return getReportActionMessage(reportAction)?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; } function isMoneyRequestAction(reportAction: OnyxEntry): reportAction is ReportAction & OriginalMessageIOU { @@ -381,7 +385,7 @@ function getMostRecentIOURequestActionID(reportActions: ReportAction[] | null): * Returns array of links inside a given report action */ function extractLinksFromMessageHtml(reportAction: OnyxEntry): string[] { - const htmlContent = reportAction?.message?.[0]?.html; + const htmlContent = getReportActionMessage(reportAction)?.html; // Regex to get link in href prop inside of component const regex = /]*?\s+)?href="([^"]*)"/gi; @@ -582,7 +586,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo } if (updatedReportAction.message[0]) { - updatedReportAction.message[0].html = reportAction.message?.[0]?.html?.replace('%baseURL', environmentURL); + updatedReportAction.message[0].html = getReportActionMessage(reportAction)?.html?.replace('%baseURL', environmentURL); } return updatedReportAction; @@ -600,7 +604,7 @@ function getLastVisibleAction(reportID: string, actionsToMerge: OnyxCollection = {}, reportAction: OnyxEntry | undefined = undefined): LastVisibleMessage { const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, actionsToMerge); - const message = lastVisibleAction?.message?.[0]; + const message = getReportActionMessage(lastVisibleAction); if (message && isReportMessageAttachment(message)) { return { @@ -786,7 +790,7 @@ function isCreatedTaskReportAction(reportAction: OnyxEntry): boole * A helper method to identify if the message is deleted or not. */ function isMessageDeleted(reportAction: OnyxEntry): boolean { - return reportAction?.message?.[0]?.isDeletedParentAction ?? false; + return getReportActionMessage(reportAction)?.isDeletedParentAction ?? false; } /** @@ -836,7 +840,7 @@ function getAllReportActions(reportID: string): ReportActions { * */ function isReportActionAttachment(reportAction: OnyxEntry): boolean { - const message = reportAction?.message?.[0]; + const message = getReportActionMessage(reportAction); if (reportAction && ('isAttachment' in reportAction || 'attachmentInfo' in reportAction)) { return reportAction?.isAttachment ?? !!reportAction?.attachmentInfo ?? false; @@ -916,8 +920,10 @@ function getMemberChangeMessageElements(reportAction: OnyxEntry): ]; } -function getReportActionHtml(reportAction: PartialReportAction): string | undefined { - return reportAction?.message?.[0]?.html; +function getReportActionHtml(reportAction: PartialReportAction) { + // @ts-expect-error Handle refactor not using message array, will remove when Back End is changed complete https://github.com/Expensify/App/issues/39797 + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return reportAction?.message?.html || reportAction?.message?.[0]?.html; } function getReportActionText(reportAction: PartialReportAction): string { @@ -926,6 +932,10 @@ function getReportActionText(reportAction: PartialReportAction): string { return html ? parser.htmlToText(html) : ''; } +function getReportActionOriginalMessage(reportAction: PartialReportAction) { + return (reportAction?.originalMessage ?? reportAction?.message?.[0] ?? reportAction?.message) as OriginalMessage; +} + function getMemberChangeMessageFragment(reportAction: OnyxEntry): Message { const messageElements: readonly MemberChangeMessageElement[] = getMemberChangeMessageElements(reportAction); const html = messageElements @@ -943,7 +953,7 @@ function getMemberChangeMessageFragment(reportAction: OnyxEntry): return { html: `${html}`, - text: reportAction?.message?.[0] ? getReportActionText(reportAction) : '', + text: getReportActionMessage(reportAction) ? getReportActionText(reportAction) : '', type: CONST.REPORT.MESSAGE.TYPE.COMMENT, }; } @@ -1164,6 +1174,8 @@ export { isActionableJoinRequestPending, getReportActionText, getReportActionHtml, + getReportActionMessage, + getReportActionOriginalMessage, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 57b404d66d68..cefe340ab51f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -711,7 +711,7 @@ function isTaskReport(report: OnyxEntry): boolean { * In this case, we have added the key to the report itself */ function isCanceledTaskReport(report: OnyxEntry | EmptyObject = {}, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { - if (!isEmptyObject(parentReportAction) && (parentReportAction?.message?.[0]?.isDeletedParentAction ?? false)) { + if (!isEmptyObject(parentReportAction) && (ReportActionsUtils.getReportActionMessage(parentReportAction)?.isDeletedParentAction ?? false)) { return true; } @@ -2612,7 +2612,7 @@ function getReportPreviewMessage( isForListPreview = false, originalReportAction: OnyxEntry | EmptyObject = iouReportAction, ): string { - const reportActionMessage = iouReportAction?.message?.[0]?.html ?? ''; + const reportActionMessage = ReportActionsUtils.getReportActionMessage(iouReportAction)?.html ?? ''; if (isEmptyObject(report) || !report?.reportID) { // The iouReport is not found locally after SignIn because the OpenApp API won't return iouReports if they're settled @@ -2884,7 +2884,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu return formattedName; } - if (parentReportAction?.message?.[0]?.isDeletedParentAction) { + if (ReportActionsUtils.getReportActionMessage(parentReportAction)?.isDeletedParentAction) { return Localize.translateLocal('parentReportAction.deletedMessage'); } @@ -2898,9 +2898,9 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu return `[${Localize.translateLocal('common.attachment')}]`; } if ( - parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || - parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN || - parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE + ReportActionsUtils.getReportActionMessage(parentReportAction)?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || + ReportActionsUtils.getReportActionMessage(parentReportAction)?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN || + ReportActionsUtils.getReportActionMessage(parentReportAction)?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE ) { return Localize.translateLocal('parentReportAction.hiddenMessage'); } @@ -3207,15 +3207,15 @@ function updateOptimisticParentReportAction(parentReportAction: OnyxEntry = {}; Onyx.connect({ @@ -31,7 +31,7 @@ function getTaskReportActionMessage(action: OnyxEntry): Pick 0) { @@ -4402,7 +4402,7 @@ function getSendMoneyParams( value: { ...optimisticIOUReport, lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), - lastMessageHtml: optimisticIOUReportAction.message?.[0]?.html, + lastMessageHtml: ReportActionsUtils.getReportActionMessage(optimisticIOUReportAction)?.html, }, }; const optimisticTransactionThreadData: OnyxUpdate = { @@ -4642,7 +4642,7 @@ function getPayMoneyRequestParams( hasOutstandingChildRequest: false, iouReportID: null, lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), - lastMessageHtml: optimisticIOUReportAction.message?.[0]?.html, + lastMessageHtml: ReportActionsUtils.getReportActionMessage(optimisticIOUReportAction)?.html, }, }, { @@ -4661,7 +4661,7 @@ function getPayMoneyRequestParams( value: { ...iouReport, lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), - lastMessageHtml: optimisticIOUReportAction.message?.[0]?.html, + lastMessageHtml: ReportActionsUtils.getReportActionMessage(optimisticIOUReportAction)?.html, hasOutstandingChildRequest: false, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, pendingFields: { @@ -4883,7 +4883,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full value: { ...expenseReport, lastMessageText: ReportActionsUtils.getReportActionText(optimisticApprovedReportAction), - lastMessageHtml: optimisticApprovedReportAction.message?.[0]?.html, + lastMessageHtml: ReportActionsUtils.getReportActionMessage(optimisticApprovedReportAction)?.html, stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED, pendingFields: { @@ -4992,7 +4992,7 @@ function submitReport(expenseReport: OnyxTypes.Report) { value: { ...expenseReport, lastMessageText: ReportActionsUtils.getReportActionText(optimisticSubmittedReportAction), - lastMessageHtml: optimisticSubmittedReportAction.message?.[0]?.html ?? '', + lastMessageHtml: ReportActionsUtils.getReportActionMessage(optimisticSubmittedReportAction)?.html ?? '', stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }, @@ -5118,7 +5118,7 @@ function cancelPayment(expenseReport: OnyxTypes.Report, chatReport: OnyxTypes.Re value: { ...expenseReport, lastMessageText: ReportActionsUtils.getReportActionText(optimisticReportAction), - lastMessageHtml: optimisticReportAction.message?.[0]?.html, + lastMessageHtml: ReportActionsUtils.getReportActionMessage(optimisticReportAction)?.html, stateNum, statusNum, }, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a1a2dca64f7d..abc27d2a49f6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -400,7 +400,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { // Always prefer the file as the last action over text const lastAction = attachmentAction ?? reportCommentAction; const currentTime = DateUtils.getDBTimeWithSkew(); - const lastComment = lastAction?.message?.[0]; + const lastComment = ReportActionsUtils.getReportActionMessage(lastAction); const lastCommentText = ReportUtils.formatReportLastMessageText(lastComment?.text ?? ''); const optimisticReport: Partial = { @@ -1302,7 +1302,7 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry = { [reportActionID]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -2606,7 +2606,7 @@ function openLastOpenedPublicRoom(lastOpenedPublicRoomID: string) { /** Flag a comment as offensive */ function flagComment(reportID: string, reportAction: OnyxEntry, severity: string) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); - const message = reportAction?.message?.[0]; + const message = ReportActionsUtils.getReportActionMessage(reportAction); if (!message) { return; @@ -2794,7 +2794,7 @@ function completeEngagementModal(text: string, choice: ValueOf, resolution: ValueOf) { - const message = reportAction?.message?.[0]; + const message = ReportActionsUtils.getReportActionMessage(reportAction); if (!message) { return; } diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index ae886e0309dc..3416a99b41ce 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -28,7 +28,7 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, k } // Delete the failed task report too - const taskReportID = reportAction.message?.[0]?.taskReportID; + const taskReportID = ReportActionUtils.getReportActionMessage(reportAction)?.taskReportID; if (taskReportID && ReportActionUtils.isCreatedTaskReportAction(reportAction)) { Report.deleteReport(taskReportID); } diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 1318bfa803c2..4a2712ced35f 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -277,7 +277,7 @@ function ReportActionItem({ // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator // Removed messages should not be shown anyway and should not need this flow - const latestDecision = action.message?.[0]?.moderationDecision?.decision ?? ''; + const latestDecision = ReportActionsUtils.getReportActionMessage(action)?.moderationDecision?.decision ?? ''; useEffect(() => { if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { return; diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index b2f120f16ef4..891bf2820681 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -100,7 +100,7 @@ function ReportActionItemMessage({action, transaction, displayAsGroup, reportID, // to decide if the fragment should be from left to right for RTL display names e.g. Arabic for proper // formatting. isFragmentContainingDisplayName={index === 0} - moderationDecision={action.message?.[0]?.moderationDecision?.decision} + moderationDecision={ReportActionsUtils.getReportActionMessage(action)?.moderationDecision?.decision} /> )); diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index fc3c92434fc4..680e6e41bf35 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -118,7 +118,7 @@ function ReportActionItemMessageEdit( useEffect(() => { const parser = new ExpensiMark(); - const originalMessage = parser.htmlToMarkdown(action.message?.[0]?.html ?? ''); + const originalMessage = parser.htmlToMarkdown(ReportActionsUtils.getReportActionMessage(action)?.html ?? ''); if ( ReportActionsUtils.isDeletedAction(action) || Boolean(action.message && draftMessage === originalMessage) || diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 1e0dc432b3fc..fd09f8c13fc0 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -18,6 +18,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; +import {getReportActionMessage} from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; @@ -245,7 +246,7 @@ function ReportActionItemSingle({ delegateAccountID={action?.delegateAccountID} isSingleLine actorIcon={icon} - moderationDecision={action?.message?.[0]?.moderationDecision?.decision} + moderationDecision={getReportActionMessage(action)?.moderationDecision?.decision} /> ))} diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index e56962a331a2..4e5b7ab3c690 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -16,6 +16,7 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import {getReportActionMessage} from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import * as Policy from '@userActions/Policy'; @@ -226,7 +227,7 @@ const reportActionsSelector = (reportActions: OnyxEntry (reportActions && Object.values(reportActions).map((reportAction) => { const {reportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = reportAction.message?.[0]?.moderationDecision?.decision; + const decision = getReportActionMessage(reportAction)?.moderationDecision?.decision; return { reportActionID, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 76ef36e710c5..c2539ae0c2e6 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -2832,7 +2832,7 @@ describe('actions/IOU', () => { callback: (reportActionsForReport) => { Onyx.disconnect(connectionID); createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; - expect(createIOUAction?.message?.[0]?.isDeletedParentAction).toBeTruthy(); + expect(ReportActionsUtils.getReportActionMessage(createIOUAction)?.isDeletedParentAction).toBeTruthy(); resolve(); }, }); @@ -2851,7 +2851,7 @@ describe('actions/IOU', () => { callback: (reportActionsForReport) => { Onyx.disconnect(connectionID); createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; - expect(createIOUAction?.message?.[0]?.isDeletedParentAction).toBeTruthy(); + expect(ReportActionsUtils.getReportActionMessage(createIOUAction)?.isDeletedParentAction).toBeTruthy(); resolve(); }, }); diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 8566abb97c7f..06073483f86d 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -2,6 +2,7 @@ import {rand} from '@ngneat/falso'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; +import {getReportActionMessage} from '@libs/ReportActionsUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -60,7 +61,7 @@ const allReportActions = Object.fromEntries( message: [ { moderationDecision: { - decision: reportActions[key].message?.[0]?.moderationDecision?.decision, + decision: getReportActionMessage(reportActions[key])?.moderationDecision?.decision, }, }, ], From ce44bde0c6a7e1acd428ec7b5d118d13b12f3b01 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 15 Apr 2024 09:01:34 +0200 Subject: [PATCH 006/784] 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 007/784] 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 008/784] 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 009/784] 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 010/784] 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 011/784] 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 012/784] 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 013/784] 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 014/784] 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 015/784] 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 016/784] 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 017/784] 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 018/784] 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 019/784] 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 020/784] 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 021/784] 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 022/784] 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 023/784] 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 024/784] 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 025/784] 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 026/784] 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 027/784] 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 028/784] 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 && ( +