From 75e65f38e592e1686995930dff2957ef649658d1 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 10 Mar 2026 08:31:20 -0600 Subject: [PATCH] Revert "Reapply "Feat: bulk edit multiple"" --- src/CONST/index.ts | 4 - src/ONYXKEYS.ts | 9 - src/ROUTES.ts | 13 - src/SCREENS.ts | 10 - .../ReportActionItem/MoneyRequestView.tsx | 28 +- src/components/TaxPicker.tsx | 21 +- src/hooks/useSearchBulkActions.ts | 30 +- src/hooks/useSelectedTransactionsActions.ts | 34 +- src/languages/de.ts | 3 - src/languages/en.ts | 4 - src/languages/es.ts | 3 - src/languages/fr.ts | 3 - src/languages/it.ts | 3 - src/languages/ja.ts | 3 - src/languages/nl.ts | 3 - src/languages/pl.ts | 3 - src/languages/pt-BR.ts | 3 - src/languages/zh-hans.ts | 3 - .../parameters/UpdateMoneyRequestParams.ts | 2 - src/libs/API/types.ts | 2 - src/libs/DebugUtils.ts | 3 - .../ModalStackNavigators/index.tsx | 10 - src/libs/Navigation/linkingConfig/config.ts | 10 - src/libs/ReportUtils.ts | 67 --- src/libs/SearchUIUtils.ts | 35 -- src/libs/TagsOptionsListUtils.ts | 83 +-- src/libs/actions/IOU/index.ts | 566 ++---------------- .../SearchEditMultipleAmountPage.tsx | 145 ----- .../SearchEditMultipleBooleanPage.tsx | 83 --- .../SearchEditMultipleCategoryPage.tsx | 61 -- .../SearchEditMultipleDatePage.tsx | 77 --- .../SearchEditMultipleDescriptionPage.tsx | 87 --- .../SearchEditMultipleMerchantPage.tsx | 85 --- .../SearchEditMultiplePage.tsx | 288 --------- .../SearchEditMultipleTagPage.tsx | 86 --- .../SearchEditMultipleTaxPage.tsx | 77 --- .../SearchEditMultipleUtils.ts | 63 -- .../iou/request/step/IOURequestStepTag.tsx | 53 +- src/types/form/SearchEditMultipleDateForm.ts | 18 - .../form/SearchEditMultipleDescriptionForm.ts | 18 - .../form/SearchEditMultipleMerchantForm.ts | 18 - src/types/form/index.ts | 3 - src/types/onyx/Transaction.ts | 3 - tests/actions/IOUTest.ts | 518 ---------------- tests/unit/Search/SearchUIUtilsTest.ts | 43 -- tests/unit/canEditFieldOfMoneyRequestTest.ts | 16 - tests/unit/canEditMultipleTransactionsTest.ts | 229 ------- .../useSelectedTransactionsActions.test.ts | 44 +- 48 files changed, 122 insertions(+), 2851 deletions(-) delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleTagPage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleTaxPage.tsx delete mode 100644 src/pages/Search/SearchEditMultiple/SearchEditMultipleUtils.ts delete mode 100644 src/types/form/SearchEditMultipleDateForm.ts delete mode 100644 src/types/form/SearchEditMultipleDescriptionForm.ts delete mode 100644 src/types/form/SearchEditMultipleMerchantForm.ts delete mode 100644 tests/unit/canEditMultipleTransactionsTest.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 705e7b3fe613..f1ece754652b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3104,8 +3104,6 @@ const CONST = { QUANTITY_MAX_LENGTH: 12, // 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', - // This is the transactionID used when bulk editing multiple expenses - OPTIMISTIC_BULK_EDIT_TRANSACTION_ID: 'optimisticBulkEditTransactionID', // This is the transactionID used when going through the distance split expense flow so that it mimics a draft transaction OPTIMISTIC_DISTANCE_SPLIT_TRANSACTION_ID: '2', // Note: These payment types are used when building IOU reportAction message values in the server and should @@ -4381,7 +4379,6 @@ const CONST = { TAX_RATE: 'taxRate', TAX_AMOUNT: 'taxAmount', REIMBURSABLE: 'reimbursable', - BILLABLE: 'billable', REPORT: 'report', }, FOOTER: { @@ -7172,7 +7169,6 @@ const CONST = { TAG: 'tag', }, BULK_ACTION_TYPES: { - EDIT: 'edit', EXPORT: 'export', APPROVE: 'approve', PAY: 'pay', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 44eeb2f50b74..90fbdba1ce94 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1016,12 +1016,6 @@ const ONYXKEYS = { WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft', ENABLE_GLOBAL_REIMBURSEMENTS: 'enableGlobalReimbursementsForm', ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT: 'enableGlobalReimbursementsFormDraft', - SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM: 'searchEditMultipleDescriptionForm', - SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM_DRAFT: 'searchEditMultipleDescriptionFormDraft', - SEARCH_EDIT_MULTIPLE_MERCHANT_FORM: 'searchEditMultipleMerchantForm', - SEARCH_EDIT_MULTIPLE_MERCHANT_FORM_DRAFT: 'searchEditMultipleMerchantFormDraft', - SEARCH_EDIT_MULTIPLE_DATE_FORM: 'searchEditMultipleDateForm', - SEARCH_EDIT_MULTIPLE_DATE_FORM_DRAFT: 'searchEditMultipleDateFormDraft', CREATE_DOMAIN_FORM: 'createDomainForm', CREATE_DOMAIN_FORM_DRAFT: 'createDomainFormDraft', SPLIT_EXPENSE_EDIT_DATES: 'splitExpenseEditDates', @@ -1159,9 +1153,6 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm; [ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS]: FormTypes.EnableGlobalReimbursementsForm; - [ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM]: FormTypes.SearchEditMultipleDescriptionForm; - [ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM]: FormTypes.SearchEditMultipleMerchantForm; - [ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM]: FormTypes.SearchEditMultipleDateForm; [ONYXKEYS.FORMS.CREATE_DOMAIN_FORM]: FormTypes.CreateDomainForm; [ONYXKEYS.FORMS.SPLIT_EXPENSE_EDIT_DATES]: FormTypes.SplitExpenseEditDateForm; [ONYXKEYS.FORMS.EXPENSE_RULE_FORM]: FormTypes.ExpenseRuleForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cbca11029f2e..ad93c82a850e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -192,19 +192,6 @@ const ROUTES = { }, }, SEARCH_REJECT_REASON_RHP: 'search/reject', - SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP: 'search/edit-multiple-transactions', - SEARCH_EDIT_MULTIPLE_AMOUNT_RHP: 'search/edit-multiple/amount', - SEARCH_EDIT_MULTIPLE_DESCRIPTION_RHP: 'search/edit-multiple/description', - SEARCH_EDIT_MULTIPLE_MERCHANT_RHP: 'search/edit-multiple/merchant', - SEARCH_EDIT_MULTIPLE_DATE_RHP: 'search/edit-multiple/date', - SEARCH_EDIT_MULTIPLE_CATEGORY_RHP: 'search/edit-multiple/category', - SEARCH_EDIT_MULTIPLE_TAG_RHP: { - route: 'search/edit-multiple/tag/:tagListIndex', - getRoute: (tagListIndex = 0) => `search/edit-multiple/tag/${tagListIndex}` as const, - }, - SEARCH_EDIT_MULTIPLE_BILLABLE_RHP: 'search/edit-multiple/billable', - SEARCH_EDIT_MULTIPLE_REIMBURSABLE_RHP: 'search/edit-multiple/reimbursable', - SEARCH_EDIT_MULTIPLE_TAX_RHP: 'search/edit-multiple/tax', MOVE_TRANSACTIONS_SEARCH_RHP: { route: 'search/move-transactions/search/:backTo?', getRoute: (backTo?: string) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b874dc62d60e..c005d3b6d5b8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -98,16 +98,6 @@ const SCREENS = { TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', TRANSACTION_HOLD_REASON_SEARCH: 'Search_Transaction_Hold_Reason_Search', SEARCH_REJECT_REASON_RHP: 'Search_Reject_Reason_RHP', - EDIT_MULTIPLE_TRANSACTIONS_RHP: 'Search_Edit_Multiple_Transactions_RHP', - EDIT_MULTIPLE_AMOUNT_RHP: 'Search_Edit_Multiple_Amount_RHP', - EDIT_MULTIPLE_DESCRIPTION_RHP: 'Search_Edit_Multiple_Description_RHP', - EDIT_MULTIPLE_MERCHANT_RHP: 'Search_Edit_Multiple_Merchant_RHP', - EDIT_MULTIPLE_DATE_RHP: 'Search_Edit_Multiple_Date_RHP', - EDIT_MULTIPLE_CATEGORY_RHP: 'Search_Edit_Multiple_Category_RHP', - EDIT_MULTIPLE_TAG_RHP: 'Search_Edit_Multiple_Tag_RHP', - EDIT_MULTIPLE_BILLABLE_RHP: 'Search_Edit_Multiple_Billable_RHP', - EDIT_MULTIPLE_REIMBURSABLE_RHP: 'Search_Edit_Multiple_Reimbursable_RHP', - EDIT_MULTIPLE_TAX_RHP: 'Search_Edit_Multiple_Tax_RHP', TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP: 'Search_Transactions_Change_Report_Search', }, SETTINGS: { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index aa8838e9d75c..963210195bea 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -77,7 +77,7 @@ import { isTrackExpenseReportNew, shouldEnableNegative, } from '@libs/ReportUtils'; -import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils'; +import {hasEnabledTags} from '@libs/TagsOptionsListUtils'; import { getBillable, getCurrency, @@ -87,6 +87,7 @@ import { getOriginalAmountForDisplay, getOriginalTransactionWithSplitInfo, getReimbursable, + getTagArrayFromName, getTagForDisplay, getTaxName, hasMissingSmartscanFields, @@ -701,7 +702,30 @@ function MoneyRequestView({ const tagForDisplay = getTagForDisplay(updatedTransaction ?? transaction, index); let shouldShow = false; if (hasDependentTags) { - shouldShow = shouldShowDependentTagList(index, transactionTag, tags); + if (index === 0) { + shouldShow = true; + } else { + const prevTagValue = getTagForDisplay(transaction, index - 1); + if (!prevTagValue) { + shouldShow = false; + } else { + const parentTag = getTagArrayFromName(transactionTag ?? '') + .slice(0, index) + .join(':'); + + const availableTags = Object.values(tags).filter((policyTag) => { + const filterRegex = policyTag.rules?.parentTagsFilter; + if (!filterRegex) { + return true; + } + + const regex = new RegExp(filterRegex); + return regex.test(parentTag ?? ''); + }); + + shouldShow = availableTags.some((tag) => tag.enabled); + } + } } else { shouldShow = !!tagForDisplay || (canEdit && hasEnabledOptions(tags)); } diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index ec04c45e5ec4..b7d0c2e12eff 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -40,25 +40,9 @@ type TaxPickerProps = { * If enabled, the content will have a bottom padding equal to account for the safe bottom area inset. */ addBottomSafeAreaPadding?: boolean; - - /** - * If enabled, allows deselecting the currently selected tax rate by tapping it again. - * When disabled (default), tapping the selected tax rate will dismiss the picker without calling onSubmit. - */ - allowDeselect?: boolean; }; -function TaxPicker({ - selectedTaxRate = '', - policyID, - transactionID, - onSubmit, - action, - iouType, - onDismiss = Navigation.goBack, - addBottomSafeAreaPadding, - allowDeselect = false, -}: TaxPickerProps) { +function TaxPicker({selectedTaxRate = '', policyID, transactionID, onSubmit, action, iouType, onDismiss = Navigation.goBack, addBottomSafeAreaPadding}: TaxPickerProps) { const {translate, localeCompare} = useLocalize(); const [searchValue, setSearchValue] = useState(''); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); @@ -105,8 +89,7 @@ function TaxPicker({ const selectedOptionKey = sections?.at(0)?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList; const handleSelectRow = (newSelectedOption: TaxRatesOption) => { - // If deselection is not allowed and the same option is selected, just dismiss - if (!allowDeselect && selectedOptionKey === newSelectedOption.keyForList) { + if (selectedOptionKey === newSelectedOption.keyForList) { onDismiss(); return; } diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 428fdd910200..dbec630cd31a 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -37,7 +37,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; import {isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; import { - canEditMultipleTransactions, getReportOrDraftReport, isBusinessInvoiceRoom, isCurrentUserSubmitter, @@ -49,7 +48,7 @@ import {navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import {canIOUBePaid, dismissRejectUseExplanation, initBulkEditDraftTransaction} from '@userActions/IOU'; +import {canIOUBePaid, dismissRejectUseExplanation} from '@userActions/IOU'; import {openOldDotLink} from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -89,7 +88,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const {accountID} = currentUserPersonalDetails; const allTransactions = useAllTransactions(); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); - const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const selfDMReport = useSelfDMReport(); const [lastPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); @@ -142,7 +140,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { 'Exclamation', 'MoneyBag', 'ArrowSplit', - 'Pencil', ] as const); const selectedTransactionReportIDs = useMemo( @@ -751,29 +748,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return [exportButtonOption]; } - const isExpenseReportSearch = typeExpenseReport || searchResults?.search.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; - const selectedTransactionsList = Object.values(selectedTransactions) - .map((transaction) => transaction.transaction) - .filter((transaction): transaction is Transaction => !!transaction); - const canEditMultiple = canEditMultipleTransactions(selectedTransactionsList, allReportActions, allReports, policies, isExpenseReportSearch); - - if (canEditMultiple) { - options.push({ - icon: expensifyIcons.Pencil, - text: translate('search.bulkActions.editMultiple'), - value: CONST.SEARCH.BULK_ACTION_TYPES.EDIT, - shouldCloseModalOnSelect: true, - onSelected: () => { - const selectedTransactionIDs = Object.keys(selectedTransactions).filter((transactionID) => { - const selectedTransaction = selectedTransactions[transactionID]; - return !!selectedTransaction?.transaction?.transactionID || !!allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - }); - initBulkEditDraftTransaction(selectedTransactionIDs); - Navigation.navigate(ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP); - }, - }); - } - const areSelectedTransactionsIncludedInReports = selectedTransactionsKeys.every((id) => selectedTransactions[id].reportID ? selectedReportIDs.includes(selectedTransactions[id].reportID) : true, ); @@ -1064,7 +1038,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { expensifyIcons.ArrowCollapse, expensifyIcons.DocumentMerge, expensifyIcons.ArrowSplit, - expensifyIcons.Pencil, expensifyIcons.Trashcan, expensifyIcons.Exclamation, translate, @@ -1079,7 +1052,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { selectedTransactionReportIDs, selectedPolicyIDs, policies, - allReportActions, integrationsExportTemplates, csvExportLayouts, handleBasicExport, diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 1b32b2166f0f..4b83063fc547 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -4,7 +4,6 @@ import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; -import {initBulkEditDraftTransaction} from '@libs/actions/IOU'; import {unholdRequest} from '@libs/actions/IOU/Hold'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; import {exportReportToCSV} from '@libs/actions/Report'; @@ -17,7 +16,6 @@ import { canDeleteCardTransactionByLiabilityType, canDeleteTransaction, canEditFieldOfMoneyRequest, - canEditMultipleTransactions, canHoldUnholdReportAction, canRejectReportAction, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, @@ -80,28 +78,13 @@ function useSelectedTransactionsActions({ const {selectedTransactionIDs, currentSearchHash, selectedTransactions: selectedTransactionsMeta} = useSearchStateContext(); const {clearSelectedTransactions} = useSearchActionsContext(); const allTransactions = useAllTransactions(); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); - const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); - const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES); const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); - const expensifyIcons = useMemoizedLazyExpensifyIcons([ - 'Stopwatch', - 'Trashcan', - 'ArrowRight', - 'Table', - 'DocumentMerge', - 'Export', - 'ArrowCollapse', - 'ArrowSplit', - 'ThumbsDown', - 'Pencil', - ] as const); - + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'Trashcan', 'ArrowRight', 'Table', 'DocumentMerge', 'Export', 'ArrowCollapse', 'ArrowSplit', 'ThumbsDown']); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs); const isReportArchived = useReportIsArchived(report?.reportID); const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy}); @@ -203,20 +186,6 @@ function useSelectedTransactionsActions({ let computedOptions: Array> = []; if (selectedTransactionIDs.length) { const options = []; - - const canEditMultiple = canEditMultipleTransactions(selectedTransactionsList, allReportActions, allReports, allPolicies); - - if (canEditMultiple) { - options.push({ - text: translate('search.bulkActions.editMultiple'), - icon: expensifyIcons.Pencil, - value: CONST.SEARCH.BULK_ACTION_TYPES.EDIT, - onSelected: () => { - initBulkEditDraftTransaction(selectedTransactionIDs); - Navigation.navigate(ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP); - }, - }); - } const isMoneyRequestReport = isMoneyRequestReportUtils(report); const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; @@ -434,7 +403,6 @@ function useSelectedTransactionsActions({ }, }); } - computedOptions = options; } diff --git a/src/languages/de.ts b/src/languages/de.ts index 542b40a8eef4..60da0dc3cf75 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7266,9 +7266,6 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und topMerchants: 'Top-Händler', groupedExpenses: 'gruppierte Ausgaben', bulkActions: { - editMultiple: 'Mehrere bearbeiten', - editMultipleTitle: 'Mehrere Ausgaben bearbeiten', - editMultipleDescription: 'Änderungen werden für alle ausgewählten Ausgaben festgelegt und überschreiben alle zuvor festgelegten Werte.', approve: 'Genehmigen', pay: 'Bezahlen', delete: 'Löschen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 0b033038a8f4..1575e86df7d3 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7230,10 +7230,6 @@ const translations = { spendOverTime: 'Spend over time', groupedExpenses: 'grouped expenses', bulkActions: { - editMultiple: 'Edit multiple', - editMultipleTitle: 'Edit multiple expenses', - // cspell:disable - editMultipleDescription: "Changes will be set for all selected expenses and will override any previously set values. Just sayin'.", approve: 'Approve', pay: 'Pay', delete: 'Delete', diff --git a/src/languages/es.ts b/src/languages/es.ts index ceca7d565f16..dd8facb5e9d8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7091,9 +7091,6 @@ ${amount} para ${merchant} - ${date}`, deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?', groupedExpenses: 'gastos agrupados', bulkActions: { - editMultiple: 'Editar múltiples', - editMultipleTitle: 'Editar múltiples gastos', - editMultipleDescription: 'Los cambios se aplicarán a todos los gastos seleccionados y anularán cualquier valor previamente establecido.', approve: 'Aprobar', pay: 'Pagar', delete: 'Eliminar', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index bac2579dfa9d..fb331f143acd 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7288,9 +7288,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip topMerchants: 'Commerçants principaux', groupedExpenses: 'dépenses groupées', bulkActions: { - editMultiple: 'Modifier plusieurs', - editMultipleTitle: 'Modifier plusieurs dépenses', - editMultipleDescription: 'Les modifications seront appliquées à toutes les dépenses sélectionnées et remplaceront toutes les valeurs précédemment définies.', approve: 'Approuver', pay: 'Payer', delete: 'Supprimer', diff --git a/src/languages/it.ts b/src/languages/it.ts index c29a275fb8f2..87ad02d0fb57 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7252,9 +7252,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo topMerchants: 'Commercianti principali', groupedExpenses: 'spese raggruppate', bulkActions: { - editMultiple: 'Modifica multipli', - editMultipleTitle: 'Modifica più spese', - editMultipleDescription: 'Le modifiche verranno applicate a tutte le spese selezionate e sostituiranno i valori precedentemente impostati.', approve: 'Approva', pay: 'Paga', delete: 'Elimina', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 1b91fe9524a7..2027a24d1c92 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7174,9 +7174,6 @@ ${reportName} topMerchants: '上位加盟店', groupedExpenses: 'グループ化された経費', bulkActions: { - editMultiple: '複数を編集', - editMultipleTitle: '複数の経費を編集', - editMultipleDescription: '変更は選択したすべての経費に適用され、以前に設定された値は上書きされます。', approve: '承認', pay: '支払う', delete: '削除', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index a96ebff19b6e..362162538eaa 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7231,9 +7231,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar topMerchants: 'Topverkopers', groupedExpenses: 'gegroepeerde uitgaven', bulkActions: { - editMultiple: 'Meerdere bewerken', - editMultipleTitle: 'Meerdere uitgaven bewerken', - editMultipleDescription: 'Wijzigingen worden toegepast op alle geselecteerde uitgaven en overschrijven eerder ingestelde waarden.', approve: 'Goedkeuren', pay: 'Betalen', delete: 'Verwijderen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 411c7f56b2b7..eb1a10786ac2 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7220,9 +7220,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i topMerchants: 'Najlepsi sprzedawcy', groupedExpenses: 'zgrupowane wydatki', bulkActions: { - editMultiple: 'Edytuj wiele', - editMultipleTitle: 'Edytuj wiele wydatków', - editMultipleDescription: 'Zmiany zostaną zastosowane do wszystkich wybranych wydatków i zastąpią wcześniej ustawione wartości.', approve: 'Zatwierdź', pay: 'Zapłać', delete: 'Usuń', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fcd6645a6638..33fc39e7c936 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7223,9 +7223,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e topMerchants: 'Principais comerciantes', groupedExpenses: 'despesas agrupadas', bulkActions: { - editMultiple: 'Editar múltiplos', - editMultipleTitle: 'Editar múltiplas despesas', - editMultipleDescription: 'As alterações serão aplicadas a todas as despesas selecionadas e substituirão quaisquer valores definidos anteriormente.', approve: 'Aprovar', pay: 'Pagar', delete: 'Excluir', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 687da7fdc228..113c102cff9b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7057,9 +7057,6 @@ ${reportName} topMerchants: '热门商家', groupedExpenses: '已分组的报销费用', bulkActions: { - editMultiple: '批量编辑', - editMultipleTitle: '编辑多个费用', - editMultipleDescription: '更改将应用于所有选定的费用,并将覆盖之前设置的任何值。', approve: '批准', pay: '支付', delete: '删除', diff --git a/src/libs/API/parameters/UpdateMoneyRequestParams.ts b/src/libs/API/parameters/UpdateMoneyRequestParams.ts index 68dbb7a8f679..0f210d6c661d 100644 --- a/src/libs/API/parameters/UpdateMoneyRequestParams.ts +++ b/src/libs/API/parameters/UpdateMoneyRequestParams.ts @@ -4,8 +4,6 @@ type UpdateMoneyRequestParams = Partial & { reportID?: string; transactionID?: string; reportActionID?: string; - /** Used for bulk updates - JSON stringified object containing only changed fields */ - updates?: string; }; export default UpdateMoneyRequestParams; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 80ceab7f20dc..237d17517315 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -205,7 +205,6 @@ const WRITE_COMMANDS = { DELETE_REPORT_FIELD: 'RemoveReportField', SET_REPORT_NAME: 'RenameReport', COMPLETE_SPLIT_BILL: 'CompleteSplitBill', - UPDATE_MONEY_REQUEST: 'UpdateMoneyRequest', UPDATE_MONEY_REQUEST_ATTENDEES: 'UpdateMoneyRequestAttendees', UPDATE_MONEY_REQUEST_DATE: 'UpdateMoneyRequestDate', UPDATE_MONEY_REQUEST_REIMBURSABLE: 'UpdateMoneyRequestReimbursable', @@ -760,7 +759,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams; [WRITE_COMMANDS.DELETE_REPORT_FIELD]: Parameters.DeleteReportFieldParams; [WRITE_COMMANDS.COMPLETE_SPLIT_BILL]: Parameters.CompleteSplitBillParams; - [WRITE_COMMANDS.UPDATE_MONEY_REQUEST]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams; diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 8f7b95fce2f4..770f148035f6 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -996,8 +996,6 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) return validateNumber(value); case 'iouRequestType': return validateConstantEnum(value, CONST.IOU.REQUEST_TYPE); - case 'selectedTransactionIDs': - return validateArray(value, 'string'); case 'participants': return validateArray>(value, { accountID: 'number', @@ -1092,7 +1090,6 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION, routes: CONST.RED_BRICK_ROAD_PENDING_ACTION, transactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, - selectedTransactionIDs: CONST.RED_BRICK_ROAD_PENDING_ACTION, tag: CONST.RED_BRICK_ROAD_PENDING_ACTION, transactionType: CONST.RED_BRICK_ROAD_PENDING_ACTION, isFromGlobalCreate: CONST.RED_BRICK_ROAD_PENDING_ACTION, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index f5adce765a74..9bc4968b6360 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -996,16 +996,6 @@ const SearchReportActionsModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchHoldReasonPage').default, [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: () => require('../../../../pages/Search/SearchRejectReasonPage').default, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require('../../../../pages/Search/SearchTransactionsChangeReport').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_TRANSACTIONS_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultiplePage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_AMOUNT_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_DESCRIPTION_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_MERCHANT_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_DATE_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleDatePage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_CATEGORY_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_TAG_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleTagPage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_BILLABLE_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_REIMBURSABLE_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage').default, - [SCREENS.SEARCH.EDIT_MULTIPLE_TAX_RHP]: () => require('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleTaxPage').default, }); const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 9c7f79bf7d4d..517f361c2fed 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1891,16 +1891,6 @@ const config: LinkingOptions['config'] = { exact: true, }, [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: ROUTES.SEARCH_REJECT_REASON_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_TRANSACTIONS_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_AMOUNT_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_AMOUNT_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_DESCRIPTION_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_DESCRIPTION_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_MERCHANT_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_MERCHANT_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_DATE_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_DATE_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_CATEGORY_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_CATEGORY_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_TAG_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_TAG_RHP.route, - [SCREENS.SEARCH.EDIT_MULTIPLE_BILLABLE_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_BILLABLE_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_REIMBURSABLE_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_REIMBURSABLE_RHP, - [SCREENS.SEARCH.EDIT_MULTIPLE_TAX_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_TAX_RHP, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: { path: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP.route, exact: true, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 21f2fd9bccc7..6ab69dce14aa 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4816,67 +4816,6 @@ function canEditReportPolicy(report: OnyxEntry, reportPolicy: OnyxEntry< return false; } -/** - * Checks if the user can edit multiple transactions - */ -function canEditMultipleTransactions( - selectedTransactions: Transaction[], - reportActions: OnyxCollection, - reports: OnyxCollection, - policies: OnyxCollection, - areReportsSelected = false, -): boolean { - if (areReportsSelected) { - return false; - } - - if (selectedTransactions.length < 2) { - return false; - } - - for (const transaction of selectedTransactions) { - const isUnreportedExpense = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; - if (isUnreportedExpense) { - return true; - } - - // Do not allow editing split expenses in bulk - if (transaction.comment?.source === CONST.IOU.TYPE.SPLIT || transaction.comment?.splits) { - return false; - } - - const reportAction = getIOUActionForTransactionID(Object.values(reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] ?? {}), transaction.transactionID); - const report = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]; - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - - const isApproved = isReportApproved({report}); - - if (isApproved || isSettled(report)) { - return false; - } - - const fieldsToCheck = [ - CONST.EDIT_REQUEST_FIELD.AMOUNT, - CONST.EDIT_REQUEST_FIELD.MERCHANT, - CONST.EDIT_REQUEST_FIELD.CATEGORY, - CONST.EDIT_REQUEST_FIELD.TAG, - CONST.EDIT_REQUEST_FIELD.DESCRIPTION, - CONST.EDIT_REQUEST_FIELD.DATE, - CONST.EDIT_REQUEST_FIELD.BILLABLE, - CONST.EDIT_REQUEST_FIELD.REIMBURSABLE, - CONST.EDIT_REQUEST_FIELD.TAX_RATE, - ]; - - const isTransactionEditable = fieldsToCheck.some((field) => canEditFieldOfMoneyRequest(reportAction, field, undefined, undefined, undefined, transaction, report, policy)); - - if (!isTransactionEditable) { - return false; - } - } - - return true; -} - /** * Checks if the current user can edit the provided property of an expense * @@ -4902,7 +4841,6 @@ function canEditFieldOfMoneyRequest( CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE, CONST.EDIT_REQUEST_FIELD.REPORT, - CONST.EDIT_REQUEST_FIELD.BILLABLE, ]; if (!isMoneyRequestAction(reportAction) || !canEditMoneyRequest(reportAction, isChatReportArchived, report, policy, linkedTransaction)) { @@ -4918,10 +4856,6 @@ function canEditFieldOfMoneyRequest( const moneyRequestReport = report ?? (iouMessage?.IOUReportID ? (getReport(iouMessage?.IOUReportID, allReports) ?? ({} as Report)) : ({} as Report)); const transaction = linkedTransaction ?? allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${iouMessage?.IOUTransactionID}`] ?? ({} as Transaction); - if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.BILLABLE && isInvoiceReport(moneyRequestReport) && isReportApproved({report: moneyRequestReport})) { - return false; - } - if (isSettled(String(moneyRequestReport.reportID)) || isReportIDApproved(String(moneyRequestReport.reportID))) { return false; } @@ -13149,7 +13083,6 @@ export { canHoldUnholdReportAction, canEditReportPolicy, canEditFieldOfMoneyRequest, - canEditMultipleTransactions, canEditMoneyRequest, canEditReportAction, canEditReportDescription, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c5192dfdefb8..6c6fa1abe27f 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -4569,40 +4569,6 @@ function getTableMinWidth(columns: SearchColumnType[]) { return minWidth; } -/** - * Determine policyID based on selected transactions: - * - If all selected transactions belong to the same policy, use that policy - * - Otherwise, fall back to the user's active workspace policy - */ -function getSearchBulkEditPolicyID( - selectedTransactionIDs: string[], - activePolicyID: string | undefined, - allTransactions: OnyxCollection | undefined, - allReports: OnyxCollection | undefined, -): string | undefined { - if (selectedTransactionIDs.length === 0) { - return activePolicyID; - } - - const policyIDs = selectedTransactionIDs.map((transactionID) => { - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (!transaction?.reportID) { - return undefined; - } - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]; - return report?.policyID; - }); - - const firstPolicyID = policyIDs.at(0); - const allSamePolicy = policyIDs.every((policyID) => policyID === firstPolicyID); - - if (allSamePolicy && firstPolicyID) { - return firstPolicyID; - } - - return activePolicyID; -} - function filterValidHasValues(hasValues: string[] | undefined, type: SearchDataTypes | undefined, translate: LocalizedTranslate): string[] | undefined { if (!hasValues || !type) { return undefined; @@ -4734,7 +4700,6 @@ function applySelectionToItem( } export { - getSearchBulkEditPolicyID, getSuggestedSearches, getListItem, getSections, diff --git a/src/libs/TagsOptionsListUtils.ts b/src/libs/TagsOptionsListUtils.ts index b829fdf2161e..b1e93e683785 100644 --- a/src/libs/TagsOptionsListUtils.ts +++ b/src/libs/TagsOptionsListUtils.ts @@ -3,10 +3,9 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleCon import CONST from '@src/CONST'; import type {Policy, PolicyTag, PolicyTagLists, PolicyTags, Transaction} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; -import {insertTagIntoTransactionTagsString} from './IOUUtils'; import {hasEnabledOptions} from './OptionsListUtils'; import type {Option} from './OptionsListUtils'; -import {getCleanedTagName, getTagList, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isMultiLevelTags as isMultiLevelTagsPolicyUtils} from './PolicyUtils'; +import {getCleanedTagName, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isMultiLevelTags as isMultiLevelTagsPolicyUtils} from './PolicyUtils'; import tokenizedSearch from './tokenizedSearch'; import {getTagArrayFromName, getTagForDisplay} from './TransactionUtils'; @@ -30,16 +29,6 @@ type TagVisibility = { shouldShow: boolean; }; -type UpdatedTransactionTagParams = { - transactionTag: string; - selectedTagName: string; - currentTag: string; - tagListIndex: number; - policyTags: OnyxEntry; - hasDependentTags: boolean; - hasMultipleTagLists: boolean; -}; - /** * Transforms the provided tags into option objects. * @@ -236,35 +225,6 @@ function getTagVisibility({ }); } -/** - * Determines whether a dependent tag list should be shown based on the selected parent tag - * and available enabled tags for the current level. - */ -function shouldShowDependentTagList(tagListIndex: number, transactionTag: string | undefined, tags: PolicyTags | undefined): boolean { - if (tagListIndex === 0) { - return true; - } - - const tagParts = getTagArrayFromName(transactionTag ?? ''); - const previousTagValue = tagParts.at(tagListIndex - 1); - if (!previousTagValue) { - return false; - } - - const parentTag = tagParts.slice(0, tagListIndex).join(':'); - const availableTags = Object.values(tags ?? {}).filter((policyTag) => { - const filterRegex = policyTag.rules?.parentTagsFilter; - if (!filterRegex) { - return true; - } - - const regex = new RegExp(filterRegex); - return regex.test(parentTag ?? ''); - }); - - return availableTags.some((tag) => tag.enabled); -} - /** * Checks if any tag from policy tag lists exists in the transaction tag string. * @@ -290,45 +250,6 @@ function hasMatchingTag(policyTagLists: OnyxEntry, transactionTa }); } -function getUpdatedTransactionTag({transactionTag, selectedTagName, currentTag, tagListIndex, policyTags, hasDependentTags, hasMultipleTagLists}: UpdatedTransactionTagParams): string { - const isSelectedTag = selectedTagName === currentTag; - - if (hasDependentTags) { - const tagParts = transactionTag ? getTagArrayFromName(transactionTag) : []; - - if (isSelectedTag) { - // Deselect: clear this and all child tags. - tagParts.splice(tagListIndex); - } else { - // Select new tag: replace this index and clear child tags. - tagParts.splice(tagListIndex, tagParts.length - tagListIndex, selectedTagName); - - const policyTagLists = getTagLists(policyTags); - // Auto-select subsequent tags if there is only one enabled tag - for (let i = tagListIndex + 1; i < policyTagLists.length; i++) { - const availableNextLevelTags = getTagList(policyTags, i); - const enabledTags = Object.values(availableNextLevelTags.tags).filter((tag) => tag.enabled); - - if (enabledTags.length === 1) { - // If there is only one enabled tag, we can auto-select it. - const firstTag = enabledTags.at(0); - if (firstTag) { - tagParts.push(firstTag.name); - } - } else { - // If there are no enabled tags or more than one, stop auto-selecting. - break; - } - } - } - - return tagParts.join(':'); - } - - // Independent tags (fallback): use comma-separated list. - return insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTagName, tagListIndex, hasMultipleTagLists); -} - /** * Gets enabled tags filtered by parent tag at a specific index level. * @@ -353,5 +274,5 @@ function getEnabledTags(tags: PolicyTags, tag: string, index: number) { }); } -export {getTagsOptions, getTagListSections, hasEnabledTags, sortTags, getTagVisibility, hasMatchingTag, getUpdatedTransactionTag, shouldShowDependentTagList, getEnabledTags}; +export {getTagsOptions, getTagListSections, hasEnabledTags, sortTags, getTagVisibility, hasMatchingTag, getEnabledTags}; export type {SelectedTagOption, TagVisibility, TagOption}; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index c449ca8168fb..4514bf6d2592 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -93,7 +93,6 @@ import { import { getAllReportActions, getIOUActionForReportID, - getIOUActionForTransactionID, getLastVisibleAction, getLastVisibleMessage, getOriginalMessage, @@ -136,7 +135,6 @@ import { buildOptimisticSubmittedReportAction, buildOptimisticUnapprovedReportAction, canBeAutoReimbursed, - canEditFieldOfMoneyRequest, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, findSelfDMReportID, generateReportID, @@ -241,7 +239,7 @@ import {buildPolicyData, generatePolicyID} from '@userActions/Policy/Policy'; import type {BuildPolicyDataKeys} from '@userActions/Policy/Policy'; import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; import type {GuidedSetupData} from '@userActions/Report'; -import {buildInviteToRoomOnyxData, completeOnboarding, createTransactionThreadReport, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; +import {buildInviteToRoomOnyxData, completeOnboarding, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeRecentWaypoints} from '@userActions/Transaction'; import {removeDraftTransaction, removeDraftTransactions, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; @@ -1020,14 +1018,6 @@ Onyx.connect({ callback: (value) => (personalDetailsList = value), }); -let introSelectedOnyx: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.NVP_INTRO_SELECTED, - callback: (value) => { - introSelectedOnyx = value; - }, -}); - // Use connectWithoutView because this is created for non-UI task only let recentAttendees: OnyxEntry; Onyx.connectWithoutView({ @@ -4512,56 +4502,6 @@ function calculateDiffAmount( return null; } -function getUpdatedMoneyRequestReportData( - iouReport: OnyxTypes.OnyxInputOrEntry, - updatedTransaction: OnyxTypes.OnyxInputOrEntry, - transaction: OnyxEntry, - isTransactionOnHold: boolean, - policy: OnyxEntry, - actorAccountID?: number, - transactionChanges?: TransactionChanges, -) { - const calculatedDiffAmount = calculateDiffAmount(iouReport, updatedTransaction, transaction); - const isTotalIndeterminate = calculatedDiffAmount === null; - const diff = calculatedDiffAmount ?? 0; - - let updatedMoneyRequestReport: OnyxTypes.OnyxInputOrEntry; - if (!iouReport) { - updatedMoneyRequestReport = null; - } else if ((isExpenseReport(iouReport) || isInvoiceReportReportUtils(iouReport)) && !Number.isNaN(iouReport.total) && iouReport.total !== undefined) { - // For expense report, the amount is negative, so we should subtract total from diff - updatedMoneyRequestReport = { - ...iouReport, - total: iouReport.total - diff, - }; - if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') { - updatedMoneyRequestReport.nonReimbursableTotal -= diff; - } - if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') { - updatedMoneyRequestReport.nonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount; - } - if (!isTransactionOnHold) { - if (typeof updatedMoneyRequestReport.unheldTotal === 'number') { - updatedMoneyRequestReport.unheldTotal -= diff; - } - if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') { - updatedMoneyRequestReport.unheldNonReimbursableTotal -= diff; - } - if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') { - updatedMoneyRequestReport.unheldNonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount; - } - } - // Only recalculate reportName when reimbursable status changes and the report uses a formula title - if (transactionChanges && 'reimbursable' in transactionChanges) { - updatedMoneyRequestReport = maybeUpdateReportNameForFormulaTitle(updatedMoneyRequestReport, policy); - } - } else { - updatedMoneyRequestReport = updateIOUOwnerAndTotal(iouReport, actorAccountID ?? CONST.DEFAULT_NUMBER_ID, diff, getCurrency(transaction), false, true, isTransactionOnHold); - } - - return {updatedMoneyRequestReport, isTotalIndeterminate}; -} - type GetUpdateMoneyRequestParamsType = { transactionID: string | undefined; transactionThreadReport: OnyxEntry; @@ -4778,16 +4718,53 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U } // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. - // If the diff is indeterminate we cannot calculate the new iou report total from front-end due to currency differences. - const {updatedMoneyRequestReport, isTotalIndeterminate} = getUpdatedMoneyRequestReportData( - iouReport, - updatedTransaction, - transaction, - isTransactionOnHold, - policy, - updatedReportAction?.actorAccountID, - transactionChanges, - ); + const calculatedDiffAmount = calculateDiffAmount(iouReport, updatedTransaction, transaction); + // If calculatedDiffAmount is null it means we cannot calculate the new iou report total from front-end due to currency differences. + const isTotalIndeterminate = calculatedDiffAmount === null; + const diff = calculatedDiffAmount ?? 0; + + let updatedMoneyRequestReport: OnyxTypes.OnyxInputOrEntry; + if (!iouReport) { + updatedMoneyRequestReport = null; + } else if ((isExpenseReport(iouReport) || isInvoiceReportReportUtils(iouReport)) && !Number.isNaN(iouReport.total) && iouReport.total !== undefined) { + // For expense report, the amount is negative, so we should subtract total from diff + updatedMoneyRequestReport = { + ...iouReport, + total: iouReport.total - diff, + }; + if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') { + updatedMoneyRequestReport.nonReimbursableTotal -= diff; + } + if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') { + updatedMoneyRequestReport.nonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount; + } + if (!isTransactionOnHold) { + if (typeof updatedMoneyRequestReport.unheldTotal === 'number') { + updatedMoneyRequestReport.unheldTotal -= diff; + } + if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') { + updatedMoneyRequestReport.unheldNonReimbursableTotal -= diff; + } + if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') { + updatedMoneyRequestReport.unheldNonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount; + } + } + + // Only recalculate reportName when reimbursable status changes and the report uses a formula title + if ('reimbursable' in transactionChanges) { + updatedMoneyRequestReport = maybeUpdateReportNameForFormulaTitle(updatedMoneyRequestReport, policy); + } + } else { + updatedMoneyRequestReport = updateIOUOwnerAndTotal( + iouReport, + updatedReportAction?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID, + diff, + getCurrency(transaction), + false, + true, + isTransactionOnHold, + ); + } optimisticData.push( { @@ -13941,449 +13918,6 @@ function addReportApprover( API.write(WRITE_COMMANDS.ADD_REPORT_APPROVER, params, onyxData); } -function removeUnchangedBulkEditFields( - transactionChanges: TransactionChanges, - transaction: OnyxTypes.Transaction, - baseIOUReport: OnyxEntry | null, - policy: OnyxEntry, -): TransactionChanges { - const iouType = isInvoiceReportReportUtils(baseIOUReport ?? undefined) ? CONST.IOU.TYPE.INVOICE : CONST.IOU.TYPE.SUBMIT; - const allowNegative = shouldEnableNegative(baseIOUReport ?? undefined, policy, iouType); - const currentDetails = getTransactionDetails(transaction, undefined, policy, allowNegative); - if (!currentDetails) { - return transactionChanges; - } - - const changeKeys = Object.keys(transactionChanges) as Array; - if (changeKeys.length === 0) { - return transactionChanges; - } - - let filteredChanges: TransactionChanges = {}; - - for (const field of changeKeys) { - const nextValue = transactionChanges[field]; - const currentValue = currentDetails[field as keyof TransactionDetails]; - - if (nextValue !== currentValue) { - filteredChanges = { - ...filteredChanges, - [field]: nextValue, - }; - } - } - - return filteredChanges; -} - -function updateMultipleMoneyRequests( - transactionIDs: string[], - changes: TransactionChanges, - policy: OnyxEntry, - reports: OnyxCollection, - transactions: OnyxCollection, - reportActions: OnyxCollection, - policyCategories: OnyxEntry, - hash?: number, -) { - // Track running totals per report so multiple edits in the same report compound correctly. - const optimisticReportsByID: Record = {}; - for (const transactionID of transactionIDs) { - const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (!transaction) { - continue; - } - - const iouReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`] ?? undefined; - const baseIouReport = iouReport?.reportID ? (optimisticReportsByID[iouReport.reportID] ?? iouReport) : iouReport; - const isFromExpenseReport = isExpenseReport(baseIouReport); - - const transactionReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] ?? {}; - const reportAction = getIOUActionForTransactionID(Object.values(transactionReportActions), transactionID); - let transactionThreadReportID = transaction.transactionThreadReportID ?? reportAction?.childReportID; - let transactionThread = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; - - // Offline-created expenses can be missing a transaction thread until it's opened once. - // Ensure the thread exists before adding optimistic MODIFIED_EXPENSE actions so - // bulk-edit comments are visible immediately while still offline. - if (!transactionThreadReportID && iouReport?.reportID) { - const optimisticTransactionThread = createTransactionThreadReport(introSelectedOnyx, currentUserEmail, userAccountID, iouReport, reportAction, transaction); - if (optimisticTransactionThread?.reportID) { - transactionThreadReportID = optimisticTransactionThread.reportID; - transactionThread = optimisticTransactionThread; - } - } - - const isUnreportedExpense = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; - const canEditField = (field: ValueOf) => { - // Unreported (track) expenses have no report, so there is no reportAction to validate against. - // They are never approved or settled, so all bulk-editable fields are allowed. - if (isUnreportedExpense) { - return true; - } - return canEditFieldOfMoneyRequest(reportAction, field, undefined, false, undefined, transaction, iouReport, policy); - }; - - let transactionChanges: TransactionChanges = {}; - - if (changes.merchant && canEditField(CONST.EDIT_REQUEST_FIELD.MERCHANT)) { - transactionChanges.merchant = changes.merchant; - } - if (changes.created && canEditField(CONST.EDIT_REQUEST_FIELD.DATE)) { - transactionChanges.created = changes.created; - } - if (changes.amount !== undefined && canEditField(CONST.EDIT_REQUEST_FIELD.AMOUNT)) { - transactionChanges.amount = changes.amount; - } - if (changes.currency && canEditField(CONST.EDIT_REQUEST_FIELD.CURRENCY)) { - transactionChanges.currency = changes.currency; - } - if (changes.category !== undefined && canEditField(CONST.EDIT_REQUEST_FIELD.CATEGORY)) { - transactionChanges.category = changes.category; - } - if (changes.tag && canEditField(CONST.EDIT_REQUEST_FIELD.TAG)) { - transactionChanges.tag = changes.tag; - } - if (changes.comment && canEditField(CONST.EDIT_REQUEST_FIELD.DESCRIPTION)) { - transactionChanges.comment = getParsedComment(changes.comment); - } - if (changes.taxCode && canEditField(CONST.EDIT_REQUEST_FIELD.TAX_RATE)) { - transactionChanges.taxCode = changes.taxCode; - } - if (changes.billable !== undefined && canEditField(CONST.EDIT_REQUEST_FIELD.BILLABLE)) { - transactionChanges.billable = changes.billable; - } - if (changes.reimbursable !== undefined && canEditField(CONST.EDIT_REQUEST_FIELD.REIMBURSABLE)) { - transactionChanges.reimbursable = changes.reimbursable; - } - - transactionChanges = removeUnchangedBulkEditFields(transactionChanges, transaction, baseIouReport, policy); - - const updates: Record = {}; - if (transactionChanges.merchant) { - updates.merchant = transactionChanges.merchant; - } - if (transactionChanges.created) { - updates.created = transactionChanges.created; - } - if (transactionChanges.currency) { - updates.currency = transactionChanges.currency; - } - if (transactionChanges.category !== undefined) { - updates.category = transactionChanges.category; - } - if (transactionChanges.tag) { - updates.tag = transactionChanges.tag; - } - if (transactionChanges.comment) { - updates.comment = transactionChanges.comment; - } - if (transactionChanges.taxCode) { - updates.taxCode = transactionChanges.taxCode; - } - if (transactionChanges.amount !== undefined) { - updates.amount = transactionChanges.amount; - } - if (transactionChanges.billable !== undefined) { - updates.billable = transactionChanges.billable; - } - if (transactionChanges.reimbursable !== undefined) { - updates.reimbursable = transactionChanges.reimbursable; - } - - // Skip if no updates - if (Object.keys(updates).length === 0) { - continue; - } - - // Generate optimistic report action ID - const modifiedExpenseReportActionID = NumberUtils.rand64(); - - const optimisticData: Array< - OnyxUpdate< - typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - > - > = []; - const successData: Array< - OnyxUpdate< - typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - > - > = []; - const failureData: Array< - OnyxUpdate< - typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - > - > = []; - const snapshotOptimisticData: Array> = []; - const snapshotFailureData: Array> = []; - - // Pending fields for the transaction - const pendingFields: OnyxTypes.Transaction['pendingFields'] = Object.fromEntries(Object.keys(transactionChanges).map((field) => [field, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])); - const clearedPendingFields = getClearedPendingFields(transactionChanges); - - const errorFields = Object.fromEntries(Object.keys(pendingFields).map((field) => [field, getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage')])); - - // Build updated transaction - const updatedTransaction = getUpdatedTransaction({ - transaction, - transactionChanges, - isFromExpenseReport, - policy, - }); - const isTransactionOnHold = isOnHold(transaction); - - // Optimistically update violations so they disappear immediately when the edited field resolves them - if (policy) { - const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; - let optimisticViolations = - transactionChanges.amount !== undefined || transactionChanges.created || transactionChanges.currency - ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - : currentTransactionViolations; - optimisticViolations = - transactionChanges.category !== undefined && transactionChanges.category === '' - ? optimisticViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY) - : optimisticViolations; - const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy.id}`] ?? {}; - optimisticData.push( - ViolationsUtils.getViolationsOnyxData( - updatedTransaction, - optimisticViolations, - policy, - policyTagList, - policyCategories ?? {}, - hasDependentTags(policy, policyTagList), - isInvoiceReportReportUtils(iouReport), - isSelfDM(iouReport), - iouReport, - isFromExpenseReport, - ), - ); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: currentTransactionViolations, - }); - } - - // Optimistic transaction update - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - ...updatedTransaction, - pendingFields, - isLoading: false, - errorFields: null, - }, - }); - - // Optimistically update the search snapshot so the search list reflects the - // new values immediately (the snapshot is the exclusive data source for search - // result rendering and is not automatically updated by the TRANSACTION write above). - if (hash) { - snapshotOptimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}` as const, - value: { - // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: updatedTransaction, - }, - }, - }); - snapshotFailureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}` as const, - value: { - // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction, - }, - }, - }); - } - - // To build proper offline update message, we need to include the currency - const optimisticTransactionChanges = - transactionChanges?.amount !== undefined && !transactionChanges?.currency ? {...transactionChanges, currency: getCurrency(transaction)} : transactionChanges; - - // Build optimistic modified expense report action - const optimisticReportAction = buildOptimisticModifiedExpenseReportAction( - transactionThread, - transaction, - optimisticTransactionChanges, - isFromExpenseReport, - policy, - updatedTransaction, - ); - - const {updatedMoneyRequestReport, isTotalIndeterminate} = getUpdatedMoneyRequestReportData( - baseIouReport, - updatedTransaction, - transaction, - isTransactionOnHold, - policy, - optimisticReportAction?.actorAccountID, - transactionChanges, - ); - - if (updatedMoneyRequestReport) { - if (updatedMoneyRequestReport.reportID) { - optimisticReportsByID[updatedMoneyRequestReport.reportID] = updatedMoneyRequestReport; - } - optimisticData.push( - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: {...updatedMoneyRequestReport, ...(isTotalIndeterminate && {pendingFields: {total: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}})}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.parentReportID}`, - value: getOutstandingChildRequest(updatedMoneyRequestReport), - }, - ); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: {pendingAction: null, ...(isTotalIndeterminate && {pendingFields: {total: null}})}, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: {...iouReport, ...(isTotalIndeterminate && {pendingFields: {total: null}})}, - }); - } - - // Optimistic report action - if (transactionThreadReportID) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, - value: { - [modifiedExpenseReportActionID]: { - ...optimisticReportAction, - reportActionID: modifiedExpenseReportActionID, - }, - }, - }); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, - value: { - lastReadTime: optimisticReportAction.created, - }, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, - value: { - lastReadTime: transactionThread?.lastReadTime, - }, - }); - } - - // Success data - clear pending fields - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - pendingFields: clearedPendingFields, - }, - }); - - if (transactionThreadReportID) { - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, - value: { - [modifiedExpenseReportActionID]: {pendingAction: null}, - }, - }); - } - - // Failure data - revert transaction - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - ...transaction, - pendingFields: clearedPendingFields, - errorFields, - }, - }); - - // Failure data - remove optimistic report action - if (transactionThreadReportID) { - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, - value: { - [modifiedExpenseReportActionID]: { - pendingAction: null, - errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage'), - }, - }, - }); - } - - const params = { - transactionID, - reportActionID: modifiedExpenseReportActionID, - updates: JSON.stringify(updates), - }; - - API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST, params, { - optimisticData: [...optimisticData, ...snapshotOptimisticData] as Array< - OnyxUpdate< - | typeof ONYXKEYS.COLLECTION.TRANSACTION - | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS - | typeof ONYXKEYS.COLLECTION.SNAPSHOT - | typeof ONYXKEYS.COLLECTION.REPORT - | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - > - >, - successData, - failureData: [...failureData, ...snapshotFailureData] as Array< - OnyxUpdate< - | typeof ONYXKEYS.COLLECTION.TRANSACTION - | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS - | typeof ONYXKEYS.COLLECTION.SNAPSHOT - | typeof ONYXKEYS.COLLECTION.REPORT - | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - > - >, - }); - } -} - -/** - * Initializes the bulk-edit draft transaction under one fixed placeholder ID. - * We keep a single draft in Onyx to store the shared edits for a multi-select, - * then apply those edits to each real transaction later. The placeholder ID is - * just the storage key and never equals any actual transactionID. - */ -function initBulkEditDraftTransaction(selectedTransactionIDs: string[]) { - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`, { - transactionID: CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID, - selectedTransactionIDs, - }); -} - -/** - * Clears the draft transaction used for bulk editing - */ -function clearBulkEditDraftTransaction() { - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`, null); -} - -/** - * Updates the draft transaction for bulk editing multiple expenses - */ -function updateBulkEditDraftTransaction(transactionChanges: NullishDeep) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`, transactionChanges); -} - export { approveMoneyRequest, canApproveIOU, @@ -14488,10 +14022,6 @@ export { getUpdateMoneyRequestParams, getUpdateTrackExpenseParams, getReportPreviewAction, - updateMultipleMoneyRequests, - initBulkEditDraftTransaction, - clearBulkEditDraftTransaction, - updateBulkEditDraftTransaction, mergePolicyRecentlyUsedCurrencies, mergePolicyRecentlyUsedCategories, getAllPersonalDetails, diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx deleted file mode 100644 index e5d78a5944e7..000000000000 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useMemo, useRef, useState} from 'react'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import isTextInputFocused from '@components/TextInput/BaseTextInput/isTextInputFocused'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import {updateBulkEditDraftTransaction} from '@libs/actions/IOU'; -import {convertToBackendAmount} from '@libs/CurrencyUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import {isInvoiceReport, isIOUReport, shouldEnableNegative} from '@libs/ReportUtils'; -import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils'; -import MoneyRequestAmountForm from '@pages/iou/MoneyRequestAmountForm'; -import IOURequestStepCurrencyModal from '@pages/iou/request/step/IOURequestStepCurrencyModal'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -type CurrentMoney = {amount: string; currency: string}; - -function SearchEditMultipleAmountPage() { - const {translate} = useLocalize(); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); - - const textInput = useRef(null); - const focusTimeoutRef = useRef(null); - - const selectedTransactionIDs = useMemo(() => draftTransaction?.selectedTransactionIDs ?? [], [draftTransaction?.selectedTransactionIDs]); - - const policyID = getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, allTransactions, allReports); - const policy = policyID ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : undefined; - const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; - - const initialCurrency = draftTransaction?.currency ?? policyCurrency; - const [selectedCurrency, setSelectedCurrency] = useState(initialCurrency); - const [isCurrencyPickerVisible, setIsCurrencyPickerVisible] = useState(false); - - useFocusEffect(() => { - if (isCurrencyPickerVisible) { - return; - } - focusTimeoutRef.current = setTimeout(() => textInput.current?.focus(), CONST.ANIMATED_TRANSITION + 100); - return () => { - if (!focusTimeoutRef.current) { - return; - } - clearTimeout(focusTimeoutRef.current); - }; - }); - - const amount = draftTransaction?.amount ?? 0; - const allowNegative = useMemo(() => { - if (!selectedTransactionIDs.length) { - return false; - } - - return selectedTransactionIDs.every((transactionID) => { - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (!transaction) { - return false; - } - - const transactionReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]; - const iouReport = - transactionReport?.type === CONST.REPORT.TYPE.CHAT && transactionReport?.parentReportID - ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionReport.parentReportID}`] - : transactionReport; - if (!iouReport) { - return false; - } - - const transactionPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${iouReport.policyID}`]; - const iouType = isInvoiceReport(iouReport) ? CONST.IOU.TYPE.INVOICE : CONST.IOU.TYPE.SUBMIT; - return shouldEnableNegative(iouReport, transactionPolicy, iouType); - }); - }, [selectedTransactionIDs, allTransactions, allReports, policies]); - const isP2P = selectedTransactionIDs.some((transactionID) => { - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (!transaction) { - return false; - } - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]; - return isIOUReport(report); - }); - - const amountForForm = allowNegative ? amount : Math.abs(amount); - - const saveAmount = (currentMoney: CurrentMoney) => { - const newAmount = convertToBackendAmount(Number.parseFloat(currentMoney.amount)); - // TODO: Currency update should be handled in a separate PR - updateBulkEditDraftTransaction({ - amount: newAmount, - }); - Navigation.goBack(); - }; - - const showCurrencyPicker = () => { - if (isTextInputFocused(textInput)) { - textInput.current?.blur(); - } - setIsCurrencyPickerVisible(true); - }; - - return ( - - - setIsCurrencyPickerVisible(false)} - headerText={translate('common.selectCurrency')} - value={selectedCurrency} - onInputChange={(value) => setSelectedCurrency(value)} - /> - { - textInput.current = e; - }} - onCurrencyButtonPress={showCurrencyPicker} - onSubmitButtonPress={saveAmount} - allowFlippingAmount={allowNegative} - /> - - ); -} - -export default SearchEditMultipleAmountPage; diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx deleted file mode 100644 index f467fdf001c5..000000000000 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import React, {useMemo} from 'react'; -import {View} from 'react-native'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; -import type {ListItem} from '@components/SelectionList/ListItem/types'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {updateBulkEditDraftTransaction} from '@libs/actions/IOU'; -import Navigation from '@libs/Navigation/Navigation'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; - -type BooleanOption = ListItem & { - value: boolean; -}; - -function SearchEditMultipleBooleanPage() { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const route = useRoute(); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); - - const isBillableScreen = route.name === SCREENS.SEARCH.EDIT_MULTIPLE_BILLABLE_RHP; - const selectedValue = isBillableScreen ? draftTransaction?.billable : draftTransaction?.reimbursable; - const title = isBillableScreen ? translate('common.billable') : translate('common.reimbursable'); - const testID = isBillableScreen ? 'SearchEditMultipleBillablePage' : 'SearchEditMultipleReimbursablePage'; - - const items = useMemo( - () => [ - { - value: true, - keyForList: CONST.SEARCH.BOOLEAN.YES, - text: translate('common.yes'), - isSelected: selectedValue === true, - }, - { - value: false, - keyForList: CONST.SEARCH.BOOLEAN.NO, - text: translate('common.no'), - isSelected: selectedValue === false, - }, - ], - [selectedValue, translate], - ); - - const selectValue = (item: BooleanOption) => { - const shouldClear = selectedValue === item.value; - if (isBillableScreen) { - updateBulkEditDraftTransaction({billable: shouldClear ? null : item.value}); - } else { - updateBulkEditDraftTransaction({reimbursable: shouldClear ? null : item.value}); - } - Navigation.goBack(); - }; - - return ( - - - - - - - ); -} - -export default SearchEditMultipleBooleanPage; diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx deleted file mode 100644 index 0d2555ab3237..000000000000 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import CategoryPicker from '@components/CategoryPicker'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import type {ListItem} from '@components/SelectionListWithSections/types'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import {updateBulkEditDraftTransaction} from '@libs/actions/IOU'; -import Navigation from '@libs/Navigation/Navigation'; -import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -function SearchEditMultipleCategoryPage() { - const {translate} = useLocalize(); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); - const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); - - const selectedTransactionIDs = draftTransaction?.selectedTransactionIDs ?? []; - - const policyID = getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, allTransactions, allReports); - - const currentCategory = draftTransaction?.category ?? ''; - - const saveCategory = (item: ListItem) => { - const nextCategory = item.searchText ?? ''; - if (!nextCategory || nextCategory === currentCategory) { - updateBulkEditDraftTransaction({ - category: null, - }); - Navigation.goBack(); - return; - } - updateBulkEditDraftTransaction({ - category: nextCategory, - }); - Navigation.goBack(); - }; - - return ( - - - - - ); -} - -export default SearchEditMultipleCategoryPage; diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx deleted file mode 100644 index d0188800f5e5..000000000000 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import DatePicker from '@components/DatePicker'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {updateBulkEditDraftTransaction} from '@libs/actions/IOU'; -import Navigation from '@libs/Navigation/Navigation'; -import {isValidDate} from '@libs/ValidationUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/SearchEditMultipleDateForm'; - -function SearchEditMultipleDatePage() { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); - - const currentDate = draftTransaction?.created ?? ''; - - const validate = (value: FormOnyxValues): FormInputErrors => { - const errors: FormInputErrors = {}; - const dateValue = value.date; - if (dateValue && !isValidDate(dateValue)) { - errors.date = translate('common.error.dateInvalid'); - } - return errors; - }; - - const saveDate = (value: FormOnyxValues) => { - const newDate = value.date; - updateBulkEditDraftTransaction({ - created: newDate, - }); - Navigation.goBack(); - }; - - return ( - - - - - - - - - ); -} - -export default SearchEditMultipleDatePage; diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx deleted file mode 100644 index d23491a63c28..000000000000 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import TextInput from '@components/TextInput'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {updateBulkEditDraftTransaction} from '@libs/actions/IOU'; -import {addErrorMessage} from '@libs/ErrorUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/SearchEditMultipleDescriptionForm'; - -function SearchEditMultipleDescriptionPage() { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {inputCallbackRef} = useAutoFocusInput(); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); - - const currentDescription = draftTransaction?.comment?.comment ?? ''; - - const validate = (value: FormOnyxValues): FormInputErrors => { - const errors: FormInputErrors = {}; - - if ((value.description?.length ?? 0) > CONST.DESCRIPTION_LIMIT) { - addErrorMessage(errors, INPUT_IDS.DESCRIPTION, translate('common.error.characterLimitExceedCounter', value.description?.length ?? 0, CONST.DESCRIPTION_LIMIT)); - } - - return errors; - }; - - const saveDescription = (value: FormOnyxValues) => { - const newDescription = value.description?.trim() ?? ''; - updateBulkEditDraftTransaction({ - comment: {comment: newDescription}, - }); - Navigation.goBack(); - }; - - return ( - - - - - - - - - ); -} - -export default SearchEditMultipleDescriptionPage; diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx deleted file mode 100644 index b60ce401bc53..000000000000 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import TextInput from '@components/TextInput'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {updateBulkEditDraftTransaction} from '@libs/actions/IOU'; -import Navigation from '@libs/Navigation/Navigation'; -import {isInvalidMerchantValue, isValidInputLength} from '@libs/ValidationUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/SearchEditMultipleMerchantForm'; - -function SearchEditMultipleMerchantPage() { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {inputCallbackRef} = useAutoFocusInput(); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); - - const currentMerchant = draftTransaction?.merchant ?? ''; - - const validate = (value: FormOnyxValues): FormInputErrors => { - const errors: FormInputErrors = {}; - const {isValid, byteLength} = isValidInputLength(value.merchant ?? '', CONST.MERCHANT_NAME_MAX_BYTES); - - const trimmedMerchant = (value.merchant ?? '').trim(); - if (trimmedMerchant && isInvalidMerchantValue(trimmedMerchant)) { - errors.merchant = translate('iou.error.invalidMerchant'); - } else if (!isValid) { - errors.merchant = translate('common.error.characterLimitExceedCounter', byteLength, CONST.MERCHANT_NAME_MAX_BYTES); - } - - return errors; - }; - - const saveMerchant = (value: FormOnyxValues) => { - const newMerchant = value.merchant?.trim() ?? ''; - updateBulkEditDraftTransaction({ - merchant: newMerchant, - }); - Navigation.goBack(); - }; - - return ( - - - - - - - - - ); -} - -export default SearchEditMultipleMerchantPage; diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx deleted file mode 100644 index 4a7e05e2752f..000000000000 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import React, {useEffect} from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import Button from '@components/Button'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {clearBulkEditDraftTransaction, updateMultipleMoneyRequests} from '@libs/actions/IOU'; -import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import {hasEnabledOptions} from '@libs/OptionsListUtils'; -import {getCleanedTagName, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils} from '@libs/PolicyUtils'; -import {canEditFieldOfMoneyRequest, isIOUReport} from '@libs/ReportUtils'; -import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils'; -import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils'; -import {getTagArrayFromName, getTaxName, isDistanceRequest, isManagedCardTransaction, isPerDiemRequest, isTimeRequest} from '@libs/TransactionUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type {Route} from '@src/ROUTES'; -import type {TransactionChanges} from '@src/types/onyx/Transaction'; -import {getCommonDependentTag, getTransactionEditContext} from './SearchEditMultipleUtils'; - -function SearchEditMultiplePage() { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {currentSearchHash} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); - const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); - const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); - - const selectedTransactionIDs = draftTransaction?.selectedTransactionIDs ?? []; - - const selectedTransactionContexts = selectedTransactionIDs.flatMap((transactionID) => { - const context = getTransactionEditContext(transactionID, allTransactions, allReports, allReportActions, policies); - return context ? [context] : []; - }); - - const hasCustomUnitTransaction = selectedTransactionContexts.some(({transaction}) => isDistanceRequest(transaction) || isPerDiemRequest(transaction)); - - const hasPerDiemOrTimeTransaction = selectedTransactionContexts.some(({transaction}) => isPerDiemRequest(transaction) || isTimeRequest(transaction)); - - const isFieldDisabledForAnyTransaction = (field: ValueOf) => - selectedTransactionContexts.some(({transaction, report, reportAction, transactionPolicy}) => { - // Unreported expenses have no report actions yet but are always editable - if (!transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) { - return false; - } - return !canEditFieldOfMoneyRequest(reportAction, field, undefined, false, undefined, transaction, report, transactionPolicy); - }); - - const hasPartiallyEditableTransaction = isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.AMOUNT); - - const hasPartiallyEditableMerchantTransaction = - isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.MERCHANT) || selectedTransactionContexts.some(({transaction}) => isDistanceRequest(transaction)); - - const hasPartiallyEditableTaxRateTransaction = - isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.TAX_RATE) || selectedTransactionContexts.some(({transaction}) => isDistanceRequest(transaction)); - - const hasPartiallyEditableDateTransaction = isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.DATE); - - const areSelectedTransactionsBillable = selectedTransactionContexts.every( - ({transaction, transactionPolicy}) => transactionPolicy?.disabledFields?.defaultBillable === false || !!transaction.billable, - ); - - const areSelectedTransactionsReimbursable = selectedTransactionContexts.every( - ({transaction, report, transactionPolicy}) => !isIOUReport(report) && transactionPolicy?.disabledFields?.reimbursable === false && !isManagedCardTransaction(transaction), - ); - - const policyID = getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, allTransactions, allReports); - - const policy = policyID ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : undefined; - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); - const policyTagLists = getTagLists(policyTags); - - const isTaxTrackingEnabled = !hasPerDiemOrTimeTransaction && !!policy?.tax?.trackingEnabled; - const areCategoriesEnabled = !!policy?.areCategoriesEnabled && hasEnabledOptions(policyCategories ?? {}); - const areTagsEnabled = !!policy?.areTagsEnabled && hasEnabledTags(policyTagLists); - - useEffect(() => { - return () => { - clearBulkEditDraftTransaction(); - }; - }, []); - - const save = () => { - if (!draftTransaction) { - return; - } - - const changes: TransactionChanges = {}; - if (draftTransaction.amount !== undefined) { - changes.amount = draftTransaction.amount; - } - if (draftTransaction.merchant) { - changes.merchant = draftTransaction.merchant; - } - if (draftTransaction.comment?.comment) { - changes.comment = draftTransaction.comment.comment; - } - if (draftTransaction.created) { - changes.created = draftTransaction.created; - } - if (draftTransaction.category) { - changes.category = draftTransaction.category; - } - if (draftTransaction.tag) { - changes.tag = draftTransaction.tag; - } - if (draftTransaction.taxCode) { - changes.taxCode = draftTransaction.taxCode; - } - if (typeof draftTransaction.billable === 'boolean') { - changes.billable = draftTransaction.billable; - } - if (typeof draftTransaction.reimbursable === 'boolean') { - changes.reimbursable = draftTransaction.reimbursable; - } - - if (Object.keys(changes).length === 0) { - Navigation.dismissToPreviousRHP(); - return; - } - - updateMultipleMoneyRequests(selectedTransactionIDs, changes, policy, allReports, allTransactions, allReportActions, policyCategories, currentSearchHash); - // Bulk edit can start from report (ID-based selection) or search (map-based selection), - // so clear both stores to keep deselection behavior consistent. - clearSelectedTransactions(true); - clearSelectedTransactions(); - - Navigation.dismissToPreviousRHP(); - }; - - const currency = policy?.outputCurrency ?? CONST.CURRENCY.USD; - - // TODO: Currency editing and currency symbol should be handled in a separate PR - const selectedTransactionsList = selectedTransactionIDs.map((transactionID) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]); - const commonDependentTag = getCommonDependentTag(selectedTransactionsList); - const dependentTagSource = draftTransaction?.tag === undefined ? commonDependentTag : draftTransaction?.tag; - const tagsArray = getTagArrayFromName(draftTransaction?.tag ?? ''); - const hasDependentTags = hasDependentTagsPolicyUtils(policy, policyTags); - const tagFields: Array<{description: string; title: string; route: Route; disabled?: boolean}> = areTagsEnabled - ? policyTagLists.flatMap((tagList, tagListIndex) => { - const tagName = tagsArray.at(tagListIndex) ?? ''; - const tagTitle = tagName ? getCleanedTagName(tagName) : ''; - const description = policyTagLists.length > 1 ? tagList.name : translate('common.tag'); - let shouldShow = true; - - if (hasDependentTags) { - shouldShow = shouldShowDependentTagList(tagListIndex, dependentTagSource, tagList.tags); - } - - if (!shouldShow) { - return []; - } - - return [ - { - description: description || translate('common.tag'), - title: tagTitle, - route: ROUTES.SEARCH_EDIT_MULTIPLE_TAG_RHP.getRoute(tagListIndex), - disabled: false, - }, - ]; - }) - : []; - - const getBooleanTitle = (value?: boolean) => { - if (value === undefined) { - return ''; - } - return value ? translate('common.yes') : translate('common.no'); - }; - - const fields: Array<{description: string; title: string; route: Route; disabled?: boolean; shouldParseTitle?: boolean}> = [ - { - description: translate('iou.amount'), - title: draftTransaction?.amount !== undefined ? convertToDisplayStringWithoutCurrency(draftTransaction.amount, currency) : '', - route: ROUTES.SEARCH_EDIT_MULTIPLE_AMOUNT_RHP, - disabled: hasCustomUnitTransaction || hasPartiallyEditableTransaction, - }, - { - description: translate('common.description'), - title: draftTransaction?.comment?.comment ?? '', - route: ROUTES.SEARCH_EDIT_MULTIPLE_DESCRIPTION_RHP, - shouldParseTitle: true, - }, - { - description: translate('common.merchant'), - title: draftTransaction?.merchant ?? '', - route: ROUTES.SEARCH_EDIT_MULTIPLE_MERCHANT_RHP, - disabled: hasPartiallyEditableMerchantTransaction, - }, - { - description: translate('common.date'), - title: draftTransaction?.created ?? '', - route: ROUTES.SEARCH_EDIT_MULTIPLE_DATE_RHP, - disabled: hasPartiallyEditableDateTransaction, - }, - ...(areCategoriesEnabled - ? [ - { - description: translate('common.category'), - title: draftTransaction?.category ?? '', - route: ROUTES.SEARCH_EDIT_MULTIPLE_CATEGORY_RHP, - }, - ] - : []), - ...tagFields, - ...(isTaxTrackingEnabled - ? [ - { - description: translate('iou.taxRate'), - title: draftTransaction?.taxCode ? (getTaxName(policy, draftTransaction) ?? '') : '', - route: ROUTES.SEARCH_EDIT_MULTIPLE_TAX_RHP, - disabled: hasPartiallyEditableTaxRateTransaction, - }, - ] - : []), - ...(areSelectedTransactionsBillable - ? [ - { - description: translate('common.billable'), - title: getBooleanTitle(draftTransaction?.billable), - route: ROUTES.SEARCH_EDIT_MULTIPLE_BILLABLE_RHP, - }, - ] - : []), - ...(areSelectedTransactionsReimbursable - ? [ - { - description: translate('common.reimbursable'), - title: getBooleanTitle(draftTransaction?.reimbursable), - route: ROUTES.SEARCH_EDIT_MULTIPLE_REIMBURSABLE_RHP, - }, - ] - : []), - ]; - - return ( - - - - - {translate('search.bulkActions.editMultipleDescription')} - {fields.map((field) => ( - Navigation.navigate(field.route)} - shouldShowRightIcon={!field.disabled} - disabled={field.disabled} - interactive={!field.disabled} - shouldParseTitle={field.shouldParseTitle} - /> - ))} - -