From 12a52965e4a988abde965cca0bc7a435728f7a47 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 12 May 2025 10:32:44 +0200 Subject: [PATCH 01/10] Add screen - not working --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/hooks/useSelectedTransactionsActions.ts | 15 +- .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 6 + .../iou/request/step/IOURequestEditReport.tsx | 128 ++++++++++++++++++ .../step/withWritableReportOrNotFound.tsx | 3 +- 8 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestEditReport.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7ade1df2713b9..570f74b5b0dd8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -686,6 +686,10 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_EDIT_REPORT: { + route: ':action/report/:reportID/edit', + getRoute: (action: IOUAction, reportID: string, backTo = '') => getUrlWithBackToParam(`${action as string}/report/${reportID}`, backTo), + }, SETTINGS_TAGS_ROOT: { route: 'settings/:policyID/tags', getRoute: (policyID: string | undefined, backTo = '') => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ca53350f69c78..03f0b5aa2170a 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -266,6 +266,7 @@ const SCREENS = { STEP_TIME_EDIT: 'Money_Request_Time_Edit', STEP_SUBRATE_EDIT: 'Money_Request_SubRate_Edit', STEP_REPORT: 'Money_Request_Report', + EDIT_REPORT: 'Money_Request_Edit_Report', }, TRANSACTION_DUPLICATE: { diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 358fe61f5e3e0..9a38782993bbc 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -11,6 +11,7 @@ import {getTransaction} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {OriginalMessageIOU, Report, ReportAction, Session} from '@src/types/onyx'; +import useActiveRoute from './useActiveRoute'; import useLocalize from './useLocalize'; // We do not use PRIMARY_REPORT_ACTIONS or SECONDARY_REPORT_ACTIONS because they weren't meant to be used in this situation. `value` property of returned options is later ingored. @@ -33,6 +34,7 @@ function useSelectedTransactionsActions({ const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + const {getReportRHPActiveRoute} = useActiveRoute(); const handleDeleteTransactions = useCallback(() => { const iouActions = reportActions.filter((action) => isMoneyRequestAction(action)); @@ -72,6 +74,17 @@ function useSelectedTransactionsActions({ let canHoldTransactions = selectedTransactions.length > 0 && isMoneyRequestReport && !isReportReimbursed; let canUnholdTransactions = selectedTransactions.length > 0 && isMoneyRequestReport; + options.push({ + text: 'Move Expenses', + icon: Expensicons.Document, // TODO change + value: 'MOVE', + onSelected: () => { + const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.CREATE, '12', report?.reportID ?? '-1'); + console.log(route); + Navigation.navigate(route); + }, + }); + selectedTransactions.forEach((selectedTransaction) => { if (!canHoldTransactions && !canHoldTransactions) { return; @@ -156,7 +169,7 @@ function useSelectedTransactionsActions({ }); } return options; - }, [onExportFailed, report, reportActions, selectedTransactionsID, session?.accountID, setSelectedTransactionsID, translate, showDeleteModal]); + }, [selectedTransactionsID, report, translate, getReportRHPActiveRoute, reportActions, setSelectedTransactionsID, onExportFailed, session?.accountID, showDeleteModal]); return { options: computedOptions, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index bd13b29b1dd4b..3ae3528f83b39 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -94,6 +94,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepParticipants').default, [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default, [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default, + [SCREENS.MONEY_REQUEST.EDIT_REPORT]: () => require('../../../../pages/iou/request/step/IOURequestEditReport').default, [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default, [SCREENS.MONEY_REQUEST.STEP_TAG]: () => require('../../../../pages/iou/request/step/IOURequestStepTag').default, [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6b04d7e1fda8f..003ac65137e04 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1311,6 +1311,7 @@ const config: LinkingOptions['config'] = { }, [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: ROUTES.SETTINGS_CATEGORIES_ROOT.route, [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: ROUTES.SETTINGS_TAGS_ROOT.route, + [SCREENS.MONEY_REQUEST.EDIT_REPORT]: ROUTES.MONEY_REQUEST_EDIT_REPORT.route, [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: ROUTES.MONEY_REQUEST_STEP_SEND_FROM.route, [SCREENS.MONEY_REQUEST.STEP_REPORT]: ROUTES.MONEY_REQUEST_STEP_REPORT.route, [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a18884f2257cf..439422a6cd4f3 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1150,6 +1150,12 @@ type MoneyRequestNavigatorParamList = { reportID: string; backTo: Routes; }; + [SCREENS.MONEY_REQUEST.EDIT_REPORT]: { + action: IOUAction; + iouType: IOUType; + reportID: string; + backTo: Routes; + }; [SCREENS.MONEY_REQUEST.STEP_REPORT]: { action: IOUAction; iouType: IOUType; diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx new file mode 100644 index 0000000000000..81db91cf7fa31 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -0,0 +1,128 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import Text from '@components/Text'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; +import Navigation from '@libs/Navigation/Navigation'; +import {getOutstandingReportsForUser} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Report} from '@src/types/onyx'; +import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; + +type ReportListItem = ListItem & { + /** reportID of the report */ + value: string; +}; + +type IOURequestEditReportProps = WithWritableReportOrNotFoundProps; + +/** + * This function narrows down the data from Onyx to just the properties that we want to trigger a re-render of the component. + * This helps minimize re-rendering and makes the entire component more performant. + */ +const reportSelector = (report: OnyxEntry): OnyxEntry => + report && { + ownerAccountID: report.ownerAccountID, + reportID: report.reportID, + policyID: report.policyID, + reportName: report.reportName, + stateNum: report.stateNum, + statusNum: report.statusNum, + type: report.type, + }; + +function IOURequestEditReport({route}: IOURequestEditReportProps) { + return ( + + {' '} + Siema + + ); + // const {translate} = useLocalize(); + // const {backTo, action} = route.params; + // const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, reportSelector), canBeMissing: true}); + // const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + // const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + // const isEditing = action === CONST.IOU.ACTION.EDIT; + // // We need to get the policyID because it's not defined in the transaction object before we select a report manually. + // const transactionReport = Object.values(allReports ?? {}).find( + // (report) => report?.reportID === transaction?.reportID || (transaction?.participants && report?.reportID === transaction?.participants?.at(0)?.reportID), + // ); + // const expenseReports = getOutstandingReportsForUser(transactionReport?.policyID, transactionReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, allReports ?? {}); + // const reportOptions: ReportListItem[] = useMemo(() => { + // if (!allReports) { + // return []; + // } + + // const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transaction?.reportID); + // return expenseReports + // .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0) + // .filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase())) + // .filter((report): report is NonNullable => report !== undefined) + // .map((report) => ({ + // text: report.reportName, + // value: report.reportID, + // keyForList: report.reportID, + // isSelected: isTransactionReportCorrect ? report.reportID === transaction?.reportID : expenseReports.at(0)?.reportID === report.reportID, + // })); + // }, [allReports, debouncedSearchValue, expenseReports, transaction?.reportID]); + + // const navigateBack = () => { + // Navigation.goBack(backTo); + // }; + + // const selectReport = (item: ReportListItem) => { + // if (!transaction) { + // return; + // } + // if (item.value !== transaction.reportID) { + // setTransactionReport(transaction.transactionID, item.value, !isEditing); + // if (isEditing) { + // changeTransactionsReport([transaction.transactionID], item.value); + // } + // } + // Navigation.goBack(backTo); + // }; + + // const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); + + // return ( + // + // = CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined} + // shouldSingleExecuteRowSelect + // headerMessage={headerMessage} + // initiallyFocusedOptionKey={transaction?.reportID} + // ListItem={UserListItem} + // /> + // + // ); +} + +IOURequestEditReport.displayName = 'IOURequestEditReport'; + +export default IOURequestEditReport; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index e522167e261e1..0df2895adec36 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -48,7 +48,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_DESTINATION | typeof SCREENS.MONEY_REQUEST.STEP_TIME | typeof SCREENS.MONEY_REQUEST.STEP_TIME_EDIT - | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE; + | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE + | typeof SCREENS.MONEY_REQUEST.EDIT_REPORT; type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & PlatformStackScreenProps; From 4c06432ba001162f930faba20423af002a49de64 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 12 May 2025 15:58:45 +0200 Subject: [PATCH 02/10] Move selected transactions --- assets/images/document-merge.svg | 9 ++ src/ROUTES.ts | 4 +- src/components/Icon/Expensicons.ts | 2 + .../MoneyRequestReportTransactionList.tsx | 17 +-- src/hooks/useSelectedTransactionsActions.ts | 45 ++++-- .../iou/request/step/IOURequestEditReport.tsx | 130 ++++-------------- .../step/IOURequestEditReportCommon.tsx | 100 ++++++++++++++ .../iou/request/step/IOURequestStepReport.tsx | 89 ++---------- 8 files changed, 189 insertions(+), 207 deletions(-) create mode 100644 assets/images/document-merge.svg create mode 100644 src/pages/iou/request/step/IOURequestEditReportCommon.tsx diff --git a/assets/images/document-merge.svg b/assets/images/document-merge.svg new file mode 100644 index 0000000000000..80f38b5d3eb62 --- /dev/null +++ b/assets/images/document-merge.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 570f74b5b0dd8..10177ed90ace0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -687,8 +687,8 @@ const ROUTES = { getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_EDIT_REPORT: { - route: ':action/report/:reportID/edit', - getRoute: (action: IOUAction, reportID: string, backTo = '') => getUrlWithBackToParam(`${action as string}/report/${reportID}`, backTo), + route: ':action/:iouType/report/:reportID/edit', + getRoute: (action: IOUAction, iouType: IOUType, reportID: string, backTo = '') => getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${reportID}/edit`, backTo), }, SETTINGS_TAGS_ROOT: { route: 'settings/:policyID/tags', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 28e1336207563..357347be1521a 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -69,6 +69,7 @@ import CreditCardExclamation from '@assets/images/credit-card-exclamation.svg'; import CreditCardHourglass from '@assets/images/credit-card-hourglass.svg'; import CreditCard from '@assets/images/creditcard.svg'; import Crosshair from '@assets/images/crosshair.svg'; +import DocumentMerge from '@assets/images/document-merge.svg'; import DocumentPlus from '@assets/images/document-plus.svg'; import DocumentSlash from '@assets/images/document-slash.svg'; import Document from '@assets/images/document.svg'; @@ -277,6 +278,7 @@ export { DeletedRoomAvatar, Document, DocumentSlash, + DocumentMerge, DomainRoomAvatar, DotIndicator, DotIndicatorUnfilled, diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index bfb37070d64cd..64bbf185a0a3c 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -124,18 +124,19 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, const {sortBy, sortOrder} = sortConfig; - const newTransactionID = useMemo(() => { + const newTransactionsID = useMemo(() => { if (!prevTransactions || transactions.length === prevTransactions.length) { return CONST.EMPTY_ARRAY as unknown as string[]; } return transactions .filter((transaction) => !prevTransactions.some((prevTransaction) => prevTransaction.transactionID === transaction.transactionID)) - .reduce((latest, t) => { - const inserted = t?.inserted ?? 0; - const latestInserted = latest?.inserted ?? 0; - return inserted > latestInserted ? t : latest; - }, transactions.at(0))?.transactionID; + .reduce((acc, t) => { + if (!prevTransactions.some((prevTransaction) => prevTransaction.transactionID === t.transactionID)) { + acc.push(t.transactionID); + } + return acc; + }, [] as string[]); }, [prevTransactions, transactions]); const sortedTransactions: TransactionWithOptionalHighlight[] = useMemo(() => { @@ -143,9 +144,9 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, .sort((a, b) => compareValues(a[getTransactionKey(a, sortBy)], b[getTransactionKey(b, sortBy)], sortOrder, sortBy)) .map((transaction) => ({ ...transaction, - shouldBeHighlighted: newTransactionID === transaction.transactionID, + shouldBeHighlighted: newTransactionsID?.includes(transaction.transactionID), })); - }, [newTransactionID, sortBy, sortOrder, transactions]); + }, [newTransactionsID, sortBy, sortOrder, transactions]); const navigateToTransaction = useCallback( (activeTransaction: OnyxTypes.Transaction) => { diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 9a38782993bbc..1f6fc3fca66b7 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -6,12 +6,19 @@ import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {exportReportToCSV} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import {getIOUActionForTransactionID, getOriginalMessage, isDeletedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {canDeleteCardTransactionByLiabilityType, canDeleteTransaction, canHoldUnholdReportAction, isMoneyRequestReport as isMoneyRequestReportUtils} from '@libs/ReportUtils'; +import { + canDeleteCardTransactionByLiabilityType, + canDeleteTransaction, + canHoldUnholdReportAction, + isInvoiceReport, + isMoneyRequestReport as isMoneyRequestReportUtils, + isTrackExpenseReport, +} from '@libs/ReportUtils'; import {getTransaction} from '@libs/TransactionUtils'; +import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {OriginalMessageIOU, Report, ReportAction, Session} from '@src/types/onyx'; -import useActiveRoute from './useActiveRoute'; import useLocalize from './useLocalize'; // We do not use PRIMARY_REPORT_ACTIONS or SECONDARY_REPORT_ACTIONS because they weren't meant to be used in this situation. `value` property of returned options is later ingored. @@ -34,7 +41,16 @@ function useSelectedTransactionsActions({ const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - const {getReportRHPActiveRoute} = useActiveRoute(); + const isTrackExpense = isTrackExpenseReport(report); + const isInvoice = isInvoiceReport(report); + let iouType: IOUType = CONST.IOU.TYPE.SUBMIT; + + if (isTrackExpense) { + iouType = CONST.IOU.TYPE.TRACK; + } + if (isInvoice) { + iouType = CONST.IOU.TYPE.INVOICE; + } const handleDeleteTransactions = useCallback(() => { const iouActions = reportActions.filter((action) => isMoneyRequestAction(action)); @@ -74,17 +90,6 @@ function useSelectedTransactionsActions({ let canHoldTransactions = selectedTransactions.length > 0 && isMoneyRequestReport && !isReportReimbursed; let canUnholdTransactions = selectedTransactions.length > 0 && isMoneyRequestReport; - options.push({ - text: 'Move Expenses', - icon: Expensicons.Document, // TODO change - value: 'MOVE', - onSelected: () => { - const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.CREATE, '12', report?.reportID ?? '-1'); - console.log(route); - Navigation.navigate(route); - }, - }); - selectedTransactions.forEach((selectedTransaction) => { if (!canHoldTransactions && !canHoldTransactions) { return; @@ -149,6 +154,16 @@ function useSelectedTransactionsActions({ }, }); + options.push({ + text: 'Move Expenses', + icon: Expensicons.DocumentMerge, + value: 'MOVE', + onSelected: () => { + const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID ?? '-1'); + Navigation.navigate(route); + }, + }); + const canAllSelectedTransactionsBeRemoved = selectedTransactionsID.every((transactionID) => { const canRemoveTransaction = canDeleteCardTransactionByLiabilityType(transactionID); const action = getIOUActionForTransactionID(reportActions, transactionID); @@ -169,7 +184,7 @@ function useSelectedTransactionsActions({ }); } return options; - }, [selectedTransactionsID, report, translate, getReportRHPActiveRoute, reportActions, setSelectedTransactionsID, onExportFailed, session?.accountID, showDeleteModal]); + }, [selectedTransactionsID, report, translate, reportActions, setSelectedTransactionsID, onExportFailed, iouType, session?.accountID, showDeleteModal]); return { options: computedOptions, diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 81db91cf7fa31..fb9ab6961b2e4 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -1,24 +1,13 @@ -import React, {useMemo} from 'react'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import React from 'react'; import {useOnyx} from 'react-native-onyx'; -import SelectionList from '@components/SelectionList'; +import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext'; import type {ListItem} from '@components/SelectionList/types'; -import UserListItem from '@components/SelectionList/UserListItem'; -import Text from '@components/Text'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDebouncedState from '@hooks/useDebouncedState'; -import useLocalize from '@hooks/useLocalize'; -import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; +import {changeTransactionsReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; -import {getOutstandingReportsForUser} from '@libs/ReportUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Report} from '@src/types/onyx'; -import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; -import StepScreenWrapper from './StepScreenWrapper'; -import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import IOURequestEditReportCommon from './IOURequestEditReportCommon'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -29,100 +18,33 @@ type ReportListItem = ListItem & { type IOURequestEditReportProps = WithWritableReportOrNotFoundProps; -/** - * This function narrows down the data from Onyx to just the properties that we want to trigger a re-render of the component. - * This helps minimize re-rendering and makes the entire component more performant. - */ -const reportSelector = (report: OnyxEntry): OnyxEntry => - report && { - ownerAccountID: report.ownerAccountID, - reportID: report.reportID, - policyID: report.policyID, - reportName: report.reportName, - stateNum: report.stateNum, - statusNum: report.statusNum, - type: report.type, - }; - function IOURequestEditReport({route}: IOURequestEditReportProps) { - return ( - - {' '} - Siema - - ); - // const {translate} = useLocalize(); - // const {backTo, action} = route.params; - // const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, reportSelector), canBeMissing: true}); - // const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - // const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - // const isEditing = action === CONST.IOU.ACTION.EDIT; - // // We need to get the policyID because it's not defined in the transaction object before we select a report manually. - // const transactionReport = Object.values(allReports ?? {}).find( - // (report) => report?.reportID === transaction?.reportID || (transaction?.participants && report?.reportID === transaction?.participants?.at(0)?.reportID), - // ); - // const expenseReports = getOutstandingReportsForUser(transactionReport?.policyID, transactionReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, allReports ?? {}); - // const reportOptions: ReportListItem[] = useMemo(() => { - // if (!allReports) { - // return []; - // } + const {backTo, reportID} = route.params; - // const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transaction?.reportID); - // return expenseReports - // .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0) - // .filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase())) - // .filter((report): report is NonNullable => report !== undefined) - // .map((report) => ({ - // text: report.reportName, - // value: report.reportID, - // keyForList: report.reportID, - // isSelected: isTransactionReportCorrect ? report.reportID === transaction?.reportID : expenseReports.at(0)?.reportID === report.reportID, - // })); - // }, [allReports, debouncedSearchValue, expenseReports, transaction?.reportID]); + const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext(); - // const navigateBack = () => { - // Navigation.goBack(backTo); - // }; + const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false}); - // const selectReport = (item: ReportListItem) => { - // if (!transaction) { - // return; - // } - // if (item.value !== transaction.reportID) { - // setTransactionReport(transaction.transactionID, item.value, !isEditing); - // if (isEditing) { - // changeTransactionsReport([transaction.transactionID], item.value); - // } - // } - // Navigation.goBack(backTo); - // }; - - // const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); + const selectReport = (item: ReportListItem) => { + if (selectedTransactionsID.length === 0) { + return; + } + if (item.value !== transactionReport?.reportID) { + changeTransactionsReport(selectedTransactionsID, item.value); + setSelectedTransactionsID([]); + } + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item.value)); + }; - // return ( - // - // = CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined} - // shouldSingleExecuteRowSelect - // headerMessage={headerMessage} - // initiallyFocusedOptionKey={transaction?.reportID} - // ListItem={UserListItem} - // /> - // - // ); + return ( + + ); } IOURequestEditReport.displayName = 'IOURequestEditReport'; -export default IOURequestEditReport; +export default withWritableReportOrNotFound(IOURequestEditReport); diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx new file mode 100644 index 0000000000000..d02afa8e85dfd --- /dev/null +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -0,0 +1,100 @@ +import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import {getOutstandingReportsForUser} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; +import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; +import StepScreenWrapper from './StepScreenWrapper'; + +type ReportListItem = ListItem & { + /** reportID of the report */ + value: string; +}; + +/** + * This function narrows down the data from Onyx to just the properties that we want to trigger a re-render of the component. + * This helps minimize re-rendering and makes the entire component more performant. + */ +const reportSelector = (report: OnyxEntry): OnyxEntry => + report && { + ownerAccountID: report.ownerAccountID, + reportID: report.reportID, + policyID: report.policyID, + reportName: report.reportName, + stateNum: report.stateNum, + statusNum: report.statusNum, + type: report.type, + }; + +type Props = { + backTo: Route | undefined; + transactionReport: OnyxEntry; + selectReport: (item: ReportListItem) => void; +}; + +function IOURequestEditReportCommon({backTo, transactionReport, selectReport}: Props) { + const {translate} = useLocalize(); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, reportSelector), canBeMissing: true}); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + + const expenseReports = getOutstandingReportsForUser(transactionReport?.policyID, transactionReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, allReports ?? {}); + const reportOptions: ReportListItem[] = useMemo(() => { + if (!allReports) { + return []; + } + + const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transactionReport?.reportID); + return expenseReports + .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0) + .filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase())) + .filter((report): report is NonNullable => report !== undefined) + .map((report) => ({ + text: report.reportName, + value: report.reportID, + keyForList: report.reportID, + isSelected: isTransactionReportCorrect ? report.reportID === transactionReport?.reportID : expenseReports.at(0)?.reportID === report.reportID, + })); + }, [allReports, debouncedSearchValue, expenseReports, transactionReport?.reportID]); + + const navigateBack = () => { + Navigation.goBack(backTo); + }; + + const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); + + return ( + + = CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined} + shouldSingleExecuteRowSelect + headerMessage={headerMessage} + initiallyFocusedOptionKey={transactionReport?.reportID} + ListItem={UserListItem} + /> + + ); +} + +export default IOURequestEditReportCommon; diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index a7d3622e6b5f6..9091956d67a07 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -1,21 +1,12 @@ -import React, {useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import React from 'react'; import {useOnyx} from 'react-native-onyx'; -import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; -import UserListItem from '@components/SelectionList/UserListItem'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDebouncedState from '@hooks/useDebouncedState'; -import useLocalize from '@hooks/useLocalize'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; -import {getOutstandingReportsForUser} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {Report} from '@src/types/onyx'; -import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; -import StepScreenWrapper from './StepScreenWrapper'; +import IOURequestEditReportCommon from './IOURequestEditReportCommon'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; @@ -28,54 +19,13 @@ type ReportListItem = ListItem & { type IOURequestStepReportProps = WithWritableReportOrNotFoundProps & WithFullTransactionOrNotFoundProps; -/** - * This function narrows down the data from Onyx to just the properties that we want to trigger a re-render of the component. - * This helps minimize re-rendering and makes the entire component more performant. - */ -const reportSelector = (report: OnyxEntry): OnyxEntry => - report && { - ownerAccountID: report.ownerAccountID, - reportID: report.reportID, - policyID: report.policyID, - reportName: report.reportName, - stateNum: report.stateNum, - statusNum: report.statusNum, - type: report.type, - }; - function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { - const {translate} = useLocalize(); const {backTo, action} = route.params; - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, reportSelector), canBeMissing: true}); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const isEditing = action === CONST.IOU.ACTION.EDIT; - // We need to get the policyID because it's not defined in the transaction object before we select a report manually. - const transactionReport = Object.values(allReports ?? {}).find( - (report) => report?.reportID === transaction?.reportID || (transaction?.participants && report?.reportID === transaction?.participants?.at(0)?.reportID), - ); - const expenseReports = getOutstandingReportsForUser(transactionReport?.policyID, transactionReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, allReports ?? {}); - const reportOptions: ReportListItem[] = useMemo(() => { - if (!allReports) { - return []; - } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportID = transaction?.reportID || transaction?.participants?.at(0)?.reportID; + const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true}); - const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transaction?.reportID); - return expenseReports - .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0) - .filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase())) - .filter((report): report is NonNullable => report !== undefined) - .map((report) => ({ - text: report.reportName, - value: report.reportID, - keyForList: report.reportID, - isSelected: isTransactionReportCorrect ? report.reportID === transaction?.reportID : expenseReports.at(0)?.reportID === report.reportID, - })); - }, [allReports, debouncedSearchValue, expenseReports, transaction?.reportID]); - - const navigateBack = () => { - Navigation.goBack(backTo); - }; + const isEditing = action === CONST.IOU.ACTION.EDIT; const selectReport = (item: ReportListItem) => { if (!transaction) { @@ -90,29 +40,12 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { Navigation.goBack(backTo); }; - const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); - return ( - - = CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined} - shouldSingleExecuteRowSelect - headerMessage={headerMessage} - initiallyFocusedOptionKey={transaction?.reportID} - ListItem={UserListItem} - /> - + ); } From 4ea1d2a9f91ea44aa98aae649f0608bef4a8fafe Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 12 May 2025 17:16:08 +0200 Subject: [PATCH 03/10] Fix navigation on goBack --- src/libs/Navigation/Navigation.ts | 66 ++++++++++--------- .../iou/request/step/IOURequestEditReport.tsx | 2 +- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 8a204ab436b3a..3f59de1555577 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -520,49 +520,51 @@ function navigateToReportWithPolicyCheck( forceReplace = false, ref = navigationRef, ) { - const targetReport = reportID ? {reportID, ...allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]} : report; - const policyID = policyIDToCheck ?? getPolicyIDFromState(navigationRef.getRootState() as State); - const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID); - const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID); + isNavigationReady().then(() => { + const targetReport = reportID ? {reportID, ...allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]} : report; + const policyID = policyIDToCheck ?? getPolicyIDFromState(navigationRef.getRootState() as State); + const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID); + const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID); - if ((shouldOpenAllWorkspace && !policyID) || !shouldOpenAllWorkspace) { - linkTo(ref.current, ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID, reportActionID, referrer, undefined, undefined, backTo), {forceReplace: !!forceReplace}); - return; - } + if ((shouldOpenAllWorkspace && !policyID) || !shouldOpenAllWorkspace) { + linkTo(ref.current, ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID, reportActionID, referrer, undefined, undefined, backTo), {forceReplace: !!forceReplace}); + return; + } - const params: Record = { - reportID: targetReport?.reportID, - }; + const params: Record = { + reportID: targetReport?.reportID, + }; - if (reportActionID) { - params.reportActionID = reportActionID; - } + if (reportActionID) { + params.reportActionID = reportActionID; + } - if (referrer) { - params.referrer = referrer; - } + if (referrer) { + params.referrer = referrer; + } - if (forceReplace) { + if (forceReplace) { + ref.dispatch( + StackActions.replace(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, { + policyID: undefined, + screen: SCREENS.REPORT, + params, + }), + ); + return; + } + + if (backTo) { + params.backTo = backTo; + } ref.dispatch( - StackActions.replace(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, { + StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, { policyID: undefined, screen: SCREENS.REPORT, params, }), ); - return; - } - - if (backTo) { - params.backTo = backTo; - } - ref.dispatch( - StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, { - policyID: undefined, - screen: SCREENS.REPORT, - params, - }), - ); + }); } function getReportRouteByID(reportID?: string, routes: NavigationRoute[] = navigationRef.getRootState().routes): NavigationRoute | null { diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index fb9ab6961b2e4..48c67ef54cc04 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -33,7 +33,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { changeTransactionsReport(selectedTransactionsID, item.value); setSelectedTransactionsID([]); } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item.value)); + Navigation.dismissModalWithReport({reportID: item.value}); }; return ( From bfb15db990871f2db2a6464ebf208aee46524a76 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 12 May 2025 17:21:23 +0200 Subject: [PATCH 04/10] fix linter --- src/ROUTES.ts | 7 ++++++- src/hooks/useSelectedTransactionsActions.ts | 2 +- src/pages/iou/request/step/IOURequestEditReport.tsx | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 10177ed90ace0..0ff4d584f6e1e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -688,7 +688,12 @@ const ROUTES = { }, MONEY_REQUEST_EDIT_REPORT: { route: ':action/:iouType/report/:reportID/edit', - getRoute: (action: IOUAction, iouType: IOUType, reportID: string, backTo = '') => getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${reportID}/edit`, backTo), + getRoute: (action: IOUAction, iouType: IOUType, reportID?: string, backTo = '') => { + if (!reportID) { + Log.warn('Invalid reportID while building route MONEY_REQUEST_EDIT_REPORT'); + } + getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${reportID}/edit`, backTo); + }, }, SETTINGS_TAGS_ROOT: { route: 'settings/:policyID/tags', diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 1f6fc3fca66b7..f2379660a01d2 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -159,7 +159,7 @@ function useSelectedTransactionsActions({ icon: Expensicons.DocumentMerge, value: 'MOVE', onSelected: () => { - const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID ?? '-1'); + const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID); Navigation.navigate(route); }, }); diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 48c67ef54cc04..223ee5877fa39 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -5,7 +5,6 @@ import type {ListItem} from '@components/SelectionList/types'; import {changeTransactionsReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import IOURequestEditReportCommon from './IOURequestEditReportCommon'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; From 319ffd23ff683effb7a89055e8e8951911cd5c0f Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 12 May 2025 17:34:20 +0200 Subject: [PATCH 05/10] Add missing return --- src/ROUTES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0ff4d584f6e1e..1311990dfec18 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -692,7 +692,7 @@ const ROUTES = { if (!reportID) { Log.warn('Invalid reportID while building route MONEY_REQUEST_EDIT_REPORT'); } - getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${reportID}/edit`, backTo); + return getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${reportID}/edit`, backTo); }, }, SETTINGS_TAGS_ROOT: { From eae492982a8b4e518f016e19b893b120b0b1b184 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 12 May 2025 19:26:17 +0200 Subject: [PATCH 06/10] Apply suggestions after review --- .../MoneyRequestReportTransactionList.tsx | 14 ++++++-------- src/hooks/useSelectedTransactionsActions.ts | 7 ++++--- .../request/step/IOURequestEditReportCommon.tsx | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 64bbf185a0a3c..762e1364dc600 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -129,14 +129,12 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions, return CONST.EMPTY_ARRAY as unknown as string[]; } - return transactions - .filter((transaction) => !prevTransactions.some((prevTransaction) => prevTransaction.transactionID === transaction.transactionID)) - .reduce((acc, t) => { - if (!prevTransactions.some((prevTransaction) => prevTransaction.transactionID === t.transactionID)) { - acc.push(t.transactionID); - } - return acc; - }, [] as string[]); + return transactions.reduce((acc, t) => { + if (!prevTransactions.some((prevTransaction) => prevTransaction.transactionID === t.transactionID)) { + acc.push(t.transactionID); + } + return acc; + }, [] as string[]); }, [prevTransactions, transactions]); const sortedTransactions: TransactionWithOptionalHighlight[] = useMemo(() => { diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index f2379660a01d2..562f5ccd18fb4 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -24,6 +24,7 @@ import useLocalize from './useLocalize'; // We do not use PRIMARY_REPORT_ACTIONS or SECONDARY_REPORT_ACTIONS because they weren't meant to be used in this situation. `value` property of returned options is later ingored. const HOLD = 'HOLD'; const UNHOLD = 'UNHOLD'; +const MOVE = 'MOVE'; function useSelectedTransactionsActions({ report, @@ -41,11 +42,11 @@ function useSelectedTransactionsActions({ const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - const isTrackExpense = isTrackExpenseReport(report); + const isTrackExpenseThread = isTrackExpenseReport(report); const isInvoice = isInvoiceReport(report); let iouType: IOUType = CONST.IOU.TYPE.SUBMIT; - if (isTrackExpense) { + if (isTrackExpenseThread) { iouType = CONST.IOU.TYPE.TRACK; } if (isInvoice) { @@ -157,7 +158,7 @@ function useSelectedTransactionsActions({ options.push({ text: 'Move Expenses', icon: Expensicons.DocumentMerge, - value: 'MOVE', + value: MOVE, onSelected: () => { const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID); Navigation.navigate(route); diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index d02afa8e85dfd..ac645e7dc186c 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -44,7 +44,7 @@ type Props = { function IOURequestEditReportCommon({backTo, transactionReport, selectReport}: Props) { const {translate} = useLocalize(); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, reportSelector), canBeMissing: true}); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (reports) => mapOnyxCollectionItems(reports, reportSelector), canBeMissing: true}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); From 2afe731b1c9d1b59721dbf810cb7c7a15c85b78f Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Tue, 13 May 2025 12:38:00 +0200 Subject: [PATCH 07/10] Fixes after review --- src/CONST.ts | 1 + src/components/MoneyReportHeader.tsx | 33 ++++++++++++++++++- src/hooks/useSelectedTransactionsActions.ts | 28 +++++++++++----- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportSecondaryActionUtils.ts | 21 +++++++++++- .../iou/request/step/IOURequestStepReport.tsx | 2 +- 7 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 58714a58c3773..0fc85ab51165a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1206,6 +1206,7 @@ const CONST = { DELETE: 'delete', ADD_EXPENSE: 'addExpense', REOPEN: 'reopen', + MOVE_EXPENSE: 'moveExpense', }, PRIMARY_ACTIONS: { SUBMIT: 'submit', diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 06ef767b5af2e..06241ae869313 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; @@ -41,6 +42,7 @@ import { isInvoiceReport, isProcessingReport, isReportOwner, + isTrackExpenseReport, navigateToDetailsPage, reportTransactionsSelector, } from '@libs/ReportUtils'; @@ -131,6 +133,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth; const route = useRoute(); + const {getReportRHPActiveRoute} = useActiveRoute(); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`, {canBeMissing: true}); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -205,6 +208,20 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea [moneyRequestReport, chatReport, policy, transaction], ); + const isInvoice = isInvoiceReport(moneyRequestReport); + const isTrackExpense = isTrackExpenseReport(moneyRequestReport); + + const iouType = useMemo(() => { + if (isTrackExpense) { + return CONST.IOU.TYPE.TRACK; + } + if (isInvoice) { + return CONST.IOU.TYPE.INVOICE; + } + + return CONST.IOU.TYPE.SUBMIT; + }, [isTrackExpense, isInvoice]); + const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext(); @@ -273,7 +290,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setIsNoDelegateAccessMenuVisible(true); } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); - } else if (isInvoiceReport(moneyRequestReport)) { + } else if (isInvoice) { startAnimation(); payInvoice(type, chatReport, moneyRequestReport, payAsBusiness, methodID, paymentMethod); } else { @@ -646,6 +663,20 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea Navigation.navigate(ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.getRoute(moneyRequestReport.reportID)); }, }, + [CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE]: { + text: translate('iou.moveExpenses', {count: 1}), + icon: Expensicons.DocumentMerge, + value: CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE, + onSelected: () => { + if (!moneyRequestReport || !transaction) { + return; + } + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, moneyRequestReport.reportID, getReportRHPActiveRoute()), + ); + }, + }, [CONST.REPORT.SECONDARY_ACTIONS.DELETE]: { text: translate('common.delete'), icon: Expensicons.Trashcan, diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 19cfe37bfcc75..8fe05fdfc0565 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -9,7 +9,9 @@ import {getIOUActionForTransactionID, getOriginalMessage, isDeletedAction, isMon import { canDeleteCardTransactionByLiabilityType, canDeleteTransaction, + canEditFieldOfMoneyRequest, canHoldUnholdReportAction, + canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isInvoiceReport, isMoneyRequestReport as isMoneyRequestReportUtils, isTrackExpenseReport, @@ -155,16 +157,26 @@ function useSelectedTransactionsActions({ }, }); - options.push({ - text: 'Move Expenses', - icon: Expensicons.DocumentMerge, - value: MOVE, - onSelected: () => { - const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID); - Navigation.navigate(route); - }, + const canAllExpensesBeMoved = selectedTransactions.every((transaction) => { + const iouReportAction = getIOUActionForTransactionID(reportActions, transaction.transactionID); + + const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT); + return canMoveExpense; }); + const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report); + if (canAllExpensesBeMoved && canUserPerformWriteAction) { + options.push({ + text: translate('iou.moveExpenses', {count: selectedTransactionsID.length}), + icon: Expensicons.DocumentMerge, + value: MOVE, + onSelected: () => { + const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID); + Navigation.navigate(route); + }, + }); + } + const canAllSelectedTransactionsBeRemoved = selectedTransactionsID.every((transactionID) => { const canRemoveTransaction = canDeleteCardTransactionByLiabilityType(transactionID); const action = getIOUActionForTransactionID(reportActions, transactionID); diff --git a/src/languages/en.ts b/src/languages/en.ts index 4b7f87af8cd2e..fba03a5522868 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1214,6 +1214,7 @@ const translations = { dates: 'Dates', rates: 'Rates', submitsTo: ({name}: SubmitsToParams) => `Submits to ${name}`, + moveExpenses: () => ({one: 'Move expense', other: 'Move expenses'}), }, share: { shareToExpensify: 'Share to Expensify', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5ee41dd924b98..72f55b0d353b0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1212,6 +1212,7 @@ const translations = { dates: 'Fechas', rates: 'Tasas', submitsTo: ({name}: SubmitsToParams) => `Se envĂ­a a ${name}`, + moveExpenses: () => ({one: 'Mover gasto', other: 'Mover gastos'}), }, share: { shareToExpensify: 'Compartir para Expensify', diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 78bb2e4d455d0..b7c660063995d 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -14,9 +14,10 @@ import { hasIntegrationAutoSync, isPrefferedExporter, } from './PolicyUtils'; -import {getIOUActionForReportID, isPayAction} from './ReportActionsUtils'; +import {getIOUActionForReportID, getIOUActionForTransactionID, isPayAction} from './ReportActionsUtils'; import { canAddTransaction, + canEditFieldOfMoneyRequest, isArchivedReport, isClosedReport as isClosedReportUtils, isCurrentUserSubmitter, @@ -319,6 +320,20 @@ function isChangeWorkspaceAction(report: Report, policy?: Policy): boolean { return policies.filter((newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, session, policy)).length > 0; } +function isMoveTransactionAction(reportTransactions: Transaction[], reportActions?: ReportAction[]) { + const transaction = reportTransactions.at(0); + + if (reportTransactions.length !== 1 || !transaction || !reportActions) { + return false; + } + + const iouReportAction = getIOUActionForTransactionID(reportActions, transaction.transactionID); + + const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT); + + return canMoveExpense; +} + function isDeleteAction(report: Report, reportTransactions: Transaction[]): boolean { const isExpenseReport = isExpenseReportUtils(report); const isIOUReport = isIOUReportUtils(report); @@ -419,6 +434,10 @@ function getSecondaryReportActions( options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); } + if (isMoveTransactionAction(reportTransactions, reportActions)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE); + } + options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS); if (isDeleteAction(report, reportTransactions)) { diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 9091956d67a07..df93e7d921bec 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -37,7 +37,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { changeTransactionsReport([transaction.transactionID], item.value); } } - Navigation.goBack(backTo); + Navigation.dismissModalWithReport({reportID: item.value}); }; return ( From 0b36b706ac5c3903a2fb767d6808a257dc8d3213 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Tue, 13 May 2025 12:56:37 +0200 Subject: [PATCH 08/10] fix linter and ts --- src/components/MoneyReportHeader.tsx | 2 +- src/hooks/useSelectedTransactionsActions.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 06241ae869313..d457b283f0684 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -298,7 +298,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea payMoneyRequest(type, chatReport, moneyRequestReport, true); } }, - [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation], + [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, isInvoice, moneyRequestReport, startAnimation], ); const confirmApproval = () => { diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 8fe05fdfc0565..a32fa669e7dc8 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -158,6 +158,10 @@ function useSelectedTransactionsActions({ }); const canAllExpensesBeMoved = selectedTransactions.every((transaction) => { + // typescript is failing without this check + if (!transaction) { + return false; + } const iouReportAction = getIOUActionForTransactionID(reportActions, transaction.transactionID); const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT); From 8b9cc721648292439d4af190f2118c7058220a1b Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 14 May 2025 09:50:22 +0200 Subject: [PATCH 09/10] Apply review suggestions --- src/components/MoneyReportHeader.tsx | 18 +++++++++--------- src/hooks/useSelectedTransactionsActions.ts | 5 ++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d9d3d9ba25946..cd7e084927940 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -39,10 +39,10 @@ import { hasUpdatedTotal, isAllowedToApproveExpenseReport, isExported as isExportedUtils, - isInvoiceReport, + isInvoiceReport as isInvoiceReportUtil, isProcessingReport, isReportOwner, - isTrackExpenseReport, + isTrackExpenseReport as isTrackExpenseReportUtil, navigateToDetailsPage, reportTransactionsSelector, } from '@libs/ReportUtils'; @@ -208,19 +208,19 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea [moneyRequestReport, chatReport, policy, transaction], ); - const isInvoice = isInvoiceReport(moneyRequestReport); - const isTrackExpense = isTrackExpenseReport(moneyRequestReport); + const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); + const isTrackExpenseReport = isTrackExpenseReportUtil(moneyRequestReport); const iouType = useMemo(() => { - if (isTrackExpense) { + if (isTrackExpenseReport) { return CONST.IOU.TYPE.TRACK; } - if (isInvoice) { + if (isInvoiceReport) { return CONST.IOU.TYPE.INVOICE; } return CONST.IOU.TYPE.SUBMIT; - }, [isTrackExpense, isInvoice]); + }, [isTrackExpenseReport, isInvoiceReport]); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); @@ -290,7 +290,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setIsNoDelegateAccessMenuVisible(true); } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); - } else if (isInvoice) { + } else if (isInvoiceReport) { startAnimation(); payInvoice(type, chatReport, moneyRequestReport, payAsBusiness, methodID, paymentMethod); } else { @@ -298,7 +298,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea payMoneyRequest(type, chatReport, moneyRequestReport, true); } }, - [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, isInvoice, moneyRequestReport, startAnimation], + [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, isInvoiceReport, moneyRequestReport, startAnimation], ); const confirmApproval = () => { diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index a32fa669e7dc8..1625c2826a33c 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -157,8 +157,7 @@ function useSelectedTransactionsActions({ }, }); - const canAllExpensesBeMoved = selectedTransactions.every((transaction) => { - // typescript is failing without this check + const canSelectedExpensesBeMoved = selectedTransactions.every((transaction) => { if (!transaction) { return false; } @@ -169,7 +168,7 @@ function useSelectedTransactionsActions({ }); const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report); - if (canAllExpensesBeMoved && canUserPerformWriteAction) { + if (canSelectedExpensesBeMoved && canUserPerformWriteAction) { options.push({ text: translate('iou.moveExpenses', {count: selectedTransactionsID.length}), icon: Expensicons.DocumentMerge, From de2f138e079e65815d1c125e2fa7bb1766ca6d46 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Thu, 15 May 2025 11:48:21 +0200 Subject: [PATCH 10/10] refactor getting transactions --- src/hooks/useSelectedTransactionsActions.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 091d44d8d9cbb..02bdf3529da69 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -1,4 +1,5 @@ import {useCallback, useMemo, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext'; import {deleteMoneyRequest, unholdRequest} from '@libs/actions/IOU'; @@ -16,11 +17,11 @@ import { isMoneyRequestReport as isMoneyRequestReportUtils, isTrackExpenseReport, } from '@libs/ReportUtils'; -import {getTransaction} from '@libs/TransactionUtils'; import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {OriginalMessageIOU, Report, ReportAction, Session} from '@src/types/onyx'; +import type {OriginalMessageIOU, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; import useLocalize from './useLocalize'; // We do not use PRIMARY_REPORT_ACTIONS or SECONDARY_REPORT_ACTIONS because they weren't meant to be used in this situation. `value` property of returned options is later ignored. @@ -42,6 +43,19 @@ function useSelectedTransactionsActions({ onExportFailed?: () => void; }) { const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext(); + const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); + const selectedTransactions = useMemo( + () => + selectedTransactionsID.reduce((acc, transactionID) => { + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (transaction) { + acc.push(transaction); + } + return acc; + }, [] as Transaction[]), + [allTransactions, selectedTransactionsID], + ); + const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const isTrackExpenseThread = isTrackExpenseReport(report); @@ -88,7 +102,6 @@ function useSelectedTransactionsActions({ } const options = []; const isMoneyRequestReport = isMoneyRequestReportUtils(report); - const selectedTransactions = selectedTransactionsID.map((transactionID) => getTransaction(transactionID)).filter((t) => !!t); const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; let canHoldTransactions = selectedTransactions.length > 0 && isMoneyRequestReport && !isReportReimbursed; let canUnholdTransactions = selectedTransactions.length > 0 && isMoneyRequestReport; @@ -200,7 +213,7 @@ function useSelectedTransactionsActions({ }); } return options; - }, [selectedTransactionsID, report, translate, reportActions, setSelectedTransactionsID, onExportFailed, iouType, session?.accountID, showDeleteModal]); + }, [selectedTransactionsID, report, selectedTransactions, translate, reportActions, setSelectedTransactionsID, onExportFailed, iouType, session?.accountID, showDeleteModal]); return { options: computedOptions,