diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index f708d54de396..c470f0c50806 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -429,7 +429,7 @@ function MoneyRequestConfirmationList({ text = translate('common.next'); } } else if (isTypeTrackExpense) { - text = translate('iou.createExpenseWithAmount', {amount: formattedAmount}); + text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.splitExpense'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute || isPerDiemRequest) { @@ -438,7 +438,7 @@ function MoneyRequestConfirmationList({ text = translate('iou.submitAmount', {amount: formattedAmount}); } } else { - const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.createExpenseWithAmount'; + const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount'; text = translate(translationKey, {amount: formattedAmount}); } return [ diff --git a/src/languages/en.ts b/src/languages/en.ts index 7a95ddb28e0f..3ffa4dc902f5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -870,7 +870,6 @@ const translations = { createExpense: 'Create expense', trackExpense: 'Track expense', chooseRecipient: 'Choose recipient', - createExpenseWithAmount: ({amount}: {amount: string}) => `Create ${amount} expense`, confirmDetails: 'Confirm details', pay: 'Pay', cancelPayment: 'Cancel payment', diff --git a/src/languages/es.ts b/src/languages/es.ts index 363a125be662..b08b75f1bd63 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -866,7 +866,6 @@ const translations = { trackExpense: 'Seguimiento de gastos', chooseRecipient: 'Elige destinatario', confirmDetails: 'Confirma los detalles', - createExpenseWithAmount: ({amount}: {amount: string}) => `Crear un gasto de ${amount}`, pay: 'Pagar', cancelPayment: 'Cancelar el pago', cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?', diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index eea0c46f3712..38e4c5776e3e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -108,8 +108,6 @@ type GetOptionsConfig = { action?: IOUAction; recentAttendees?: Attendee[]; shouldBoldTitleByDefault?: boolean; - shouldSeparateWorkspaceChat?: boolean; - shouldSeparateSelfDMChat?: boolean; }; type GetUserToInviteConfig = { @@ -140,8 +138,6 @@ type Options = { personalDetails: ReportUtils.OptionData[]; userToInvite: ReportUtils.OptionData | null; currentUserOption: ReportUtils.OptionData | null | undefined; - workspaceChats?: ReportUtils.OptionData[]; - selfDMChat?: ReportUtils.OptionData | undefined; }; type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; @@ -169,7 +165,7 @@ type OrderReportOptionsConfig = { preferRecentExpenseReports?: boolean; }; -type ReportAndPersonalDetailOptions = Pick; +type ReportAndPersonalDetailOptions = Pick; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -1005,23 +1001,6 @@ function orderReportOptionsWithSearch( ); } -function orderWorkspaceOptions(options: ReportUtils.OptionData[]): ReportUtils.OptionData[] { - return lodashOrderBy( - options, - [ - (option) => { - // Put default workspace on top - if (option.isPolicyExpenseChat && option.policyID === activePolicyID) { - return 0; - } - - return 1; - }, - ], - ['asc'], - ); -} - function sortComparatorReportOptionByArchivedStatus(option: ReportUtils.OptionData) { return option.private_isArchived ? 1 : 0; } @@ -1049,12 +1028,10 @@ function orderOptions(options: ReportAndPersonalDetailOptions, searchValue?: str orderedReportOptions = orderReportOptions(options.recentReports); } const orderedPersonalDetailsOptions = orderPersonalDetailsOptions(options.personalDetails); - const orderedWorkspaceChats = orderWorkspaceOptions(options?.workspaceChats ?? []); return { recentReports: orderedReportOptions, personalDetails: orderedPersonalDetailsOptions, - workspaceChats: orderedWorkspaceChats, }; } @@ -1163,8 +1140,6 @@ function getValidOptions( action, recentAttendees, shouldBoldTitleByDefault = true, - shouldSeparateSelfDMChat = false, - shouldSeparateWorkspaceChat = false, }: GetOptionsConfig = {}, ): Options { const topmostReportId = Navigation.getTopmostReportId() ?? '-1'; @@ -1240,12 +1215,6 @@ function getValidOptions( return true; }); - let workspaceChats: ReportUtils.OptionData[] = []; - - if (shouldSeparateWorkspaceChat) { - workspaceChats = allReportOptions.filter((option) => option.isOwnPolicyExpenseChat && !option.private_isArchived); - } - const allPersonalDetailsOptions = includeP2P ? options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail && (includeDomainEmail || !Str.isDomainEmail(detail.login))) : []; @@ -1356,15 +1325,6 @@ function getValidOptions( } const currentUserOption = allPersonalDetailsOptions.find((personalDetailsOption) => personalDetailsOption.login === currentUserLogin); - let selfDMChat: ReportUtils.OptionData | undefined; - - if (shouldSeparateWorkspaceChat) { - recentReportOptions = recentReportOptions.filter((option) => !option.isPolicyExpenseChat); - } - if (shouldSeparateSelfDMChat) { - selfDMChat = recentReportOptions.find((option) => option.isSelfDM); - recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM); - } return { personalDetails: personalDetailsOptions, @@ -1373,8 +1333,6 @@ function getValidOptions( // User to invite is generated by the search input of a user. // As this function isn't concerned with any search input yet, this is null (will be set when using filterOptions). userToInvite: null, - workspaceChats, - selfDMChat, }; } @@ -1615,7 +1573,6 @@ function formatSectionsFromSearchTerm( filteredPersonalDetails: ReportUtils.OptionData[], personalDetails: OnyxEntry = {}, shouldGetOptionDetails = false, - filteredWorkspaceChats: ReportUtils.OptionData[] = [], ): SectionForSearchTerm { // We show the selected participants at the top of the list when there is no search term or maximum number of participants has already been selected // However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results @@ -1641,9 +1598,8 @@ function formatSectionsFromSearchTerm( const selectedParticipantsWithoutDetails = selectedOptions.filter((participant) => { const accountID = participant.accountID ?? null; const isPartOfSearchTerm = getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm); - const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID) || filteredWorkspaceChats.some((report) => report.accountID === accountID); + const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID); const isReportInPersonalDetails = filteredPersonalDetails.some((personalDetail) => personalDetail.accountID === accountID); - return isPartOfSearchTerm && !isReportInRecentReports && !isReportInPersonalDetails; }); @@ -1729,23 +1685,6 @@ function filterReports(reports: ReportUtils.OptionData[], searchTerms: string[]) return filteredReports; } -function filterWorkspaceChats(reports: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] { - const filteredReports = searchTerms.reduceRight( - (items, term) => - filterArrayByMatch(items, term, (item) => { - const values: string[] = []; - if (item.text) { - values.push(item.text); - } - return uniqFast(values); - }), - // We start from all unfiltered reports: - reports, - ); - - return filteredReports; -} - function filterPersonalDetails(personalDetails: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] { return searchTerms.reduceRight( (items, term) => @@ -1797,34 +1736,6 @@ function filterUserToInvite(options: Omit, searchValue: }); } -function filterSelfDMChat(report: ReportUtils.OptionData, searchTerms: string[]): ReportUtils.OptionData | undefined { - const isMatch = searchTerms.every((term) => { - const values: string[] = []; - - if (report.text) { - values.push(report.text); - } - if (report.login) { - values.push(report.login); - values.push(report.login.replace(CONST.EMAIL_SEARCH_REGEX, '')); - } - if (report.isThread) { - if (report.alternateText) { - values.push(report.alternateText); - } - } else if (!!report.isChatRoom || !!report.isPolicyExpenseChat) { - if (report.subtitle) { - values.push(report.subtitle); - } - } - - // Remove duplicate values and check if the term matches any value - return uniqFast(values).some((value) => value.includes(term)); - }); - - return isMatch ? report : undefined; -} - function filterOptions(options: Options, searchInputValue: string, config?: FilterUserToInviteConfig): Options { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); @@ -1842,17 +1753,12 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt searchValue, config, ); - const workspaceChats = filterWorkspaceChats(options.workspaceChats ?? [], searchTerms); - - const selfDMChat = options.selfDMChat ? filterSelfDMChat(options.selfDMChat, searchTerms) : undefined; return { personalDetails, recentReports, userToInvite, currentUserOption, - workspaceChats, - selfDMChat, }; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9c6377474e78..d65d879125ef 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6471,7 +6471,7 @@ function isReportNotFound(report: OnyxEntry): boolean { /** * Check if the report is the parent report of the currently viewed report or at least one child report has report action */ -function shouldHideReport(report: OnyxEntry, currentReportId: string | undefined): boolean { +function shouldHideReport(report: OnyxEntry, currentReportId: string): boolean { const currentReport = getReportOrDraftReport(currentReportId); const parentReport = getParentReport(!isEmptyObject(currentReport) ? currentReport : undefined); const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? {}; @@ -6641,7 +6641,7 @@ function hasReportErrorsOtherThanFailedReceipt(report: Report, doesReportHaveVio type ShouldReportBeInOptionListParams = { report: OnyxEntry; - currentReportId: string | undefined; + currentReportId: string; isInFocusMode: boolean; betas: OnyxEntry; policies: OnyxCollection; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index dfa0d7f37b8b..07e34d9692b1 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -7,6 +7,8 @@ import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import EmptySelectionListContent from '@components/EmptySelectionListContent'; import FormHelpMessage from '@components/FormHelpMessage'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; import {usePersonalDetails} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; @@ -32,7 +34,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Participant} from '@src/types/onyx/IOU'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type MoneyRequestParticipantsSelectorProps = { /** Callback to request parent modal to go to next step, which should be split */ @@ -41,6 +42,9 @@ type MoneyRequestParticipantsSelectorProps = { /** Callback to add participants in MoneyRequestModal */ onParticipantsAdded: (value: Participant[]) => void; + /** Callback to navigate to Track Expense confirmation flow */ + onTrackExpensePress?: () => void; + /** Selected participants from MoneyRequestModal with login */ participants?: Participant[] | typeof CONST.EMPTY_ARRAY; @@ -49,9 +53,20 @@ type MoneyRequestParticipantsSelectorProps = { /** The action of the IOU, i.e. create, split, move */ action: IOUAction; + + /** Whether we should display the Track Expense button at the top of the participants list */ + shouldDisplayTrackExpenseButton?: boolean; }; -function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onFinish, onParticipantsAdded, iouType, action}: MoneyRequestParticipantsSelectorProps) { +function MoneyRequestParticipantsSelector({ + participants = CONST.EMPTY_ARRAY, + onTrackExpensePress, + onFinish, + onParticipantsAdded, + iouType, + action, + shouldDisplayTrackExpenseButton, +}: MoneyRequestParticipantsSelectorProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); @@ -108,9 +123,6 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF includeP2P: !isCategorizeOrShareAction, includeInvoiceRooms: iouType === CONST.IOU.TYPE.INVOICE, action, - shouldSeparateSelfDMChat: true, - shouldSeparateWorkspaceChat: true, - includeSelfDM: true, }, ); @@ -130,8 +142,6 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF personalDetails: [], currentUserOption: null, headerMessage: '', - workspaceChats: [], - selfDMChat: null, }; } @@ -158,7 +168,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( debouncedSearchTerm, - participants.map((participant) => ({...participant, reportID: participant.reportID})) as ReportUtils.OptionData[], + participants.map((participant) => ({...participant, reportID: participant.reportID ?? '-1'})), chatOptions.recentReports, chatOptions.personalDetails, personalDetails, @@ -167,17 +177,6 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF newSections.push(formatResults.section); - newSections.push({ - title: translate('workspace.common.workspace'), - data: chatOptions.workspaceChats ?? [], - shouldShow: (chatOptions.workspaceChats ?? []).length > 0, - }); - newSections.push({ - title: translate('workspace.invoices.paymentMethods.personal'), - data: chatOptions.selfDMChat ? [chatOptions.selfDMChat] : [], - shouldShow: !!chatOptions.selfDMChat, - }); - newSections.push({ title: translate('common.recents'), data: chatOptions.recentReports, @@ -192,11 +191,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF if ( chatOptions.userToInvite && - !OptionsListUtils.isCurrentUser({ - ...chatOptions.userToInvite, - accountID: chatOptions.userToInvite?.accountID ?? CONST.DEFAULT_NUMBER_ID, - status: chatOptions.userToInvite?.status ?? undefined, - }) + !OptionsListUtils.isCurrentUser({...chatOptions.userToInvite, accountID: chatOptions.userToInvite?.accountID ?? -1, status: chatOptions.userToInvite?.status ?? undefined}) ) { newSections.push({ title: undefined, @@ -209,7 +204,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF } const headerMessage = OptionsListUtils.getHeaderMessage( - (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length + (chatOptions.workspaceChats ?? []).length !== 0 || !isEmptyObject(chatOptions.selfDMChat), + (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length !== 0, !!chatOptions?.userToInvite, debouncedSearchTerm.trim(), participants.some((participant) => OptionsListUtils.getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), @@ -223,8 +218,6 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF participants, chatOptions.recentReports, chatOptions.personalDetails, - chatOptions.selfDMChat, - chatOptions.workspaceChats, chatOptions.userToInvite, personalDetails, translate, @@ -240,7 +233,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF (option: Participant) => { const newParticipants: Participant[] = [ { - ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID', 'isSelfDM', 'text', 'phoneNumber'), + ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), selected: true, iouType, }, @@ -257,10 +250,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF } onParticipantsAdded(newParticipants); - - if (!option.isSelfDM) { - onFinish(); - } + onFinish(); }, // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes [onFinish, onParticipantsAdded, currentUserLogin], @@ -346,7 +336,6 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF sections.forEach((section) => { length += section.data.length; }); - return length; }, [areOptionsInitialized, sections]); @@ -354,6 +343,22 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent; + const headerContent = useMemo(() => { + if (!shouldDisplayTrackExpenseButton) { + return; + } + + // We only display the track expense button if the user is coming from the combined submit/track flow. + return ( + + ); + }, [shouldDisplayTrackExpenseButton, translate, onTrackExpensePress]); + const footerContent = useMemo(() => { if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { return; @@ -439,6 +444,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} onSelectRow={onSelectRow} shouldSingleExecuteRowSelect + headerContent={headerContent} footerContent={footerContent} listEmptyContent={} headerMessage={header} diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index df2f39158056..6a67a1040f1b 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -69,6 +69,7 @@ function IOURequestStepParticipants({ const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), []); const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`); + const shouldDisplayTrackExpenseButton = !!selfDMReportID && iouType === CONST.IOU.TYPE.CREATE; const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; @@ -87,31 +88,10 @@ function IOURequestStepParticipants({ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename ?? '', receiptPath ?? '', () => {}, iouRequestType, iouType, transactionID, reportID, receiptType ?? ''); }, [receiptType, receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID, action]); - const trackExpense = useCallback(() => { - // If coming from the combined submit/track flow and the user proceeds to just track the expense, - // we will use the track IOU type in the confirmation flow. - if (!selfDMReportID) { - return; - } - - const rateID = DistanceRequestUtils.getCustomUnitRateID(selfDMReportID); - IOU.setCustomUnitRateID(transactionID, rateID); - IOU.setMoneyRequestParticipantsFromReport(transactionID, selfDMReport); - const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); - Navigation.navigate(iouConfirmationPageRoute); - }, [action, selfDMReport, selfDMReportID, transactionID]); - const addParticipant = useCallback( (val: Participant[]) => { HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); - const firstParticipant = val.at(0); - - if (firstParticipant?.isSelfDM) { - trackExpense(); - return; - } - const firstParticipantReportID = val.at(0)?.reportID; const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID); const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && ReportUtils.isInvoiceRoomWithID(firstParticipantReportID); @@ -130,7 +110,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = firstParticipantReportID ?? reportID; }, - [iouType, reportID, trackExpense, transactionID], + [iouType, reportID, transactionID], ); const handleNavigation = useCallback( @@ -183,6 +163,21 @@ function IOURequestStepParticipants({ IOUUtils.navigateToStartMoneyRequestStep(iouRequestType, iouType, transactionID, reportID, action); }, [iouRequestType, iouType, transactionID, reportID, action]); + const trackExpense = () => { + // If coming from the combined submit/track flow and the user proceeds to just track the expense, + // we will use the track IOU type in the confirmation flow. + if (!selfDMReportID) { + return; + } + + const rateID = DistanceRequestUtils.getCustomUnitRateID(selfDMReportID); + IOU.setCustomUnitRateID(transactionID, rateID); + IOU.setMoneyRequestParticipantsFromReport(transactionID, selfDMReport); + const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); + + handleNavigation(iouConfirmationPageRoute); + }; + useEffect(() => { const isCategorizing = action === CONST.IOU.ACTION.CATEGORIZE; const isShareAction = action === CONST.IOU.ACTION.SHARE; @@ -211,8 +206,10 @@ function IOURequestStepParticipants({ participants={isSplitRequest ? participants : []} onParticipantsAdded={addParticipant} onFinish={goToNextStep} + onTrackExpensePress={trackExpense} iouType={iouType} action={action} + shouldDisplayTrackExpenseButton={shouldDisplayTrackExpenseButton} /> );