From 734873e5954ecbdc3c8ad31ab6b6bada3aa54769 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 31 Mar 2026 16:08:29 -0600 Subject: [PATCH 1/4] Revert "Merge pull request #85818 from software-mansion-labs/jnowakow/deleted-transactions-on-search" This reverts commit 95ec5555b60b0d575947ee7733404ffab240042b, reversing changes made to af0e8f628ec238240665a1f35d8c24d1657228eb. --- src/CONST/index.ts | 4 -- .../ActionCell/actionTranslationsMap.ts | 1 - .../Search/SearchList/ListItem/StatusCell.tsx | 11 ++- .../ListItem/TransactionListItem.tsx | 14 ++-- .../Search/SearchList/ListItem/types.ts | 2 - src/components/Search/SearchList/index.tsx | 6 -- src/components/TransactionItemRow/index.tsx | 3 - src/hooks/useSearchBulkActions.ts | 25 +------ src/hooks/useUndeleteTransactions.ts | 33 --------- src/languages/de.ts | 2 - src/languages/en.ts | 2 - src/languages/es.ts | 2 - src/languages/fr.ts | 2 - src/languages/it.ts | 2 - src/languages/ja.ts | 2 - src/languages/nl.ts | 2 - src/languages/pl.ts | 2 - src/languages/pt-BR.ts | 2 - src/languages/zh-hans.ts | 2 - src/libs/ReportUtils.ts | 11 +-- src/libs/SearchUIUtils.ts | 46 +++++------- src/libs/TransactionUtils/index.ts | 5 -- src/libs/actions/IOU/index.ts | 25 ++++--- src/libs/actions/Search.ts | 7 +- src/libs/actions/Transaction.ts | 72 ++++++++----------- src/styles/theme/themes/dark.ts | 4 -- src/styles/theme/themes/light.ts | 4 -- src/styles/theme/types.ts | 2 +- tests/unit/ReportUtilsTest.ts | 15 ---- tests/unit/Search/SearchUIUtilsTest.ts | 1 - 30 files changed, 76 insertions(+), 235 deletions(-) delete mode 100644 src/hooks/useUndeleteTransactions.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4a2860d368871..3ef50fee4c666 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1281,7 +1281,6 @@ const CONST = { MAX_COUNT_BEFORE_FOCUS_UPDATE: 30, MIN_INITIAL_REPORT_ACTION_COUNT: 15, UNREPORTED_REPORT_ID: '0', - TRASH_REPORT_ID: '-1', SPLIT_REPORT_ID: '-2', SECONDARY_ACTIONS: { SUBMIT: 'submit', @@ -7376,7 +7375,6 @@ const CONST = { DONE: 'done', EXPORT_TO_ACCOUNTING: 'exportToAccounting', PAID: 'paid', - UNDELETE: 'undelete', }, HAS_VALUES: { RECEIPT: 'receipt', @@ -7398,7 +7396,6 @@ const CONST = { REJECT: 'reject', CHANGE_REPORT: 'changeReport', SPLIT: 'split', - UNDELETE: 'undelete', }, TRANSACTION_TYPE: { CASH: 'cash', @@ -7633,7 +7630,6 @@ const CONST = { APPROVED: 'approved', DONE: 'done', PAID: 'paid', - DELETED: 'deleted', }, EXPENSE_REPORT: { ALL: '', diff --git a/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts b/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts index 90d490038bd32..b27d9ee4f0905 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts +++ b/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts @@ -10,7 +10,6 @@ const actionTranslationsMap: Record = [CONST.SEARCH.ACTION_TYPES.EXPORT_TO_ACCOUNTING]: 'common.export', [CONST.SEARCH.ACTION_TYPES.DONE]: 'common.done', [CONST.SEARCH.ACTION_TYPES.PAID]: 'iou.settledExpensify', - [CONST.SEARCH.ACTION_TYPES.UNDELETE]: 'search.bulkActions.undelete', }; export default actionTranslationsMap; diff --git a/src/components/Search/SearchList/ListItem/StatusCell.tsx b/src/components/Search/SearchList/ListItem/StatusCell.tsx index d0326959178ed..87b3797a61fd4 100644 --- a/src/components/Search/SearchList/ListItem/StatusCell.tsx +++ b/src/components/Search/SearchList/ListItem/StatusCell.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import StatusBadge from '@components/StatusBadge'; import useLocalize from '@hooks/useLocalize'; @@ -15,18 +15,15 @@ type StatusCellProps = { /** Whether the report's state/status is pending */ isPending?: boolean; - - /** Whether the transaction was deleted */ - isDeleted?: boolean; }; -function StatusCell({stateNum, statusNum, isPending, isDeleted}: StatusCellProps) { +function StatusCell({stateNum, statusNum, isPending}: StatusCellProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const statusText = getReportStatusTranslation({stateNum, statusNum, isDeleted, translate}); - const reportStatusColorStyle = getReportStatusColorStyle(theme, stateNum, statusNum, isDeleted); + const statusText = useMemo(() => getReportStatusTranslation({stateNum, statusNum, translate}), [stateNum, statusNum, translate]); + const reportStatusColorStyle = useMemo(() => getReportStatusColorStyle(theme, stateNum, statusNum), [theme, stateNum, statusNum]); if (!statusText || !reportStatusColorStyle) { return null; diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem.tsx index 39dc5f6983a34..3644adfcfe081 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem.tsx @@ -24,7 +24,7 @@ import {handleActionButtonPress as handleActionButtonPressUtil} from '@libs/acti import {syncMissingAttendeesViolation} from '@libs/AttendeeUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isInvoiceReport} from '@libs/ReportUtils'; -import {isDeletedTransaction as isDeletedTransactionUtil, isViolationDismissed, mergeProhibitedViolations, shouldShowViolation} from '@libs/TransactionUtils'; +import {isViolationDismissed, mergeProhibitedViolations, shouldShowViolation} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -51,10 +51,8 @@ function TransactionListItem({ customCardNames, lastPaymentMethod, personalPolicyID, - onUndelete, }: TransactionListItemProps) { const transactionItem = item as unknown as TransactionListItemType; - const isDeletedTransaction = isDeletedTransactionUtil(transactionItem); const styles = useThemeStyles(); const theme = useTheme(); @@ -167,7 +165,6 @@ function TransactionListItem({ onDelegateAccessRestricted: showDelegateNoAccessModal, personalPolicyID, ownerBillingGracePeriodEnd, - onUndelete: () => onUndelete?.(transactionItem.transactionID), }); }; @@ -181,10 +178,10 @@ function TransactionListItem({ onLongPressRow?.(item)} - onPress={isDeletedTransaction ? undefined : () => onSelectRow(item, transactionPreviewData)} + onPress={() => onSelectRow(item, transactionPreviewData)} disabled={isDisabled && !item.isSelected} accessibilityLabel={item.text ?? ''} - role={isDeletedTransaction ? getButtonRole(true) : 'none'} + role={getButtonRole(true)} isNested onMouseDown={(e) => e.preventDefault()} hoverStyle={[!item.isDisabled && styles.hoveredComponentBG, item.isSelected && styles.activeComponentBG]} @@ -194,7 +191,6 @@ function TransactionListItem({ style={[ pressableStyle, isFocused && StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG), - isDeletedTransaction && styles.cursorDefault, ]} onFocus={onFocus} wrapperStyle={[styles.mb2, styles.mh5, styles.flex1, animatedHighlightStyle, styles.userSelectNone]} @@ -205,7 +201,7 @@ function TransactionListItem({ @@ -233,7 +229,7 @@ function TransactionListItem({ checkboxSentryLabel={CONST.SENTRY_LABEL.SEARCH.TRANSACTION_LIST_ITEM_CHECKBOX} style={[styles.p3, styles.pv2, shouldUseNarrowLayout ? styles.pt2 : {}]} violations={transactionViolations} - onArrowRightPress={isDeletedTransaction ? undefined : () => onSelectRow(item, transactionPreviewData)} + onArrowRightPress={() => onSelectRow(item, transactionPreviewData)} isHover={hovered} customCardNames={customCardNames} reportActions={exportedReportActions} diff --git a/src/components/Search/SearchList/ListItem/types.ts b/src/components/Search/SearchList/ListItem/types.ts index 978e126498384..4f19c2436a4ea 100644 --- a/src/components/Search/SearchList/ListItem/types.ts +++ b/src/components/Search/SearchList/ListItem/types.ts @@ -404,8 +404,6 @@ type TransactionListItemProps = ListItemProps & { lastPaymentMethod?: OnyxEntry; /** The user's personal policy ID */ personalPolicyID?: string; - /** Callback to undelete a transaction by its ID */ - onUndelete?: (transactionID: string) => void; }; type TransactionGroupListItemProps = ListItemProps & { diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index e9dd35bd59209..73fd8a20e4325 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -27,7 +27,6 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; -import useUndeleteTransactions from '@hooks/useUndeleteTransactions'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import DateUtils from '@libs/DateUtils'; @@ -302,9 +301,6 @@ function SearchList({ const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); - const undeleteTransactions = useUndeleteTransactions(); - - const handleUndelete = (transactionID: string) => undeleteTransactions([transactionID]); const route = useRoute(); const {getScrollOffset} = useContext(ScrollOffsetContext); @@ -454,7 +450,6 @@ function SearchList({ customCardNames={customCardNames} onFocus={onFocus} newTransactionID={newTransactionID} - onUndelete={handleUndelete} keyForList={item.keyForList} /> @@ -487,7 +482,6 @@ function SearchList({ personalPolicyID, customCardNames, selectedTransactions, - handleUndelete, ], ); diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 7086165862af0..ff78b83014fee 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -39,7 +39,6 @@ import { getCreated as getTransactionCreated, hasMissingSmartscanFields, isAmountMissing, - isDeletedTransaction as isDeletedTransactionUtil, isMerchantMissing, isScanning, isTimeRequest, @@ -204,7 +203,6 @@ function TransactionItemRow({ const createdAt = getTransactionCreated(transactionItem); const expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const transactionThreadReportID = reportActions ? getIOUActionForTransactionID(reportActions, transactionItem.transactionID)?.childReportID : undefined; - const isDeletedTransaction = isDeletedTransactionUtil(transactionItem); const isDateColumnWide = dateColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; const isSubmittedColumnWide = submittedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; @@ -617,7 +615,6 @@ function TransactionItemRow({ ); diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 51e9cae9de8a2..6ca38e98111b3 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -49,7 +49,7 @@ import { import {serializeQueryJSONForBackend} from '@libs/SearchQueryUtils'; import {navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {hasTransactionBeenRejected, isDeletedTransaction} from '@libs/TransactionUtils'; +import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import {canIOUBePaid, dismissRejectUseExplanation, initBulkEditDraftTransaction} from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -71,7 +71,6 @@ import usePersonalPolicy from './usePersonalPolicy'; import useSelfDMReport from './useSelfDMReport'; import useTheme from './useTheme'; import useThemeStyles from './useThemeStyles'; -import useUndeleteTransactions from './useUndeleteTransactions'; type SearchHeaderOptionValue = DeepValueOf | undefined; @@ -119,7 +118,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED); const {isBetaEnabled} = usePermissions(); const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); - const undeleteTransactions = useUndeleteTransactions(); // Cache the last search results that had data, so the merge option remains available // while results are temporarily unset (e.g. during sorting/loading). @@ -160,7 +158,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { 'Exclamation', 'MoneyBag', 'ArrowSplit', - 'RotateLeft', 'QBOSquare', 'XeroSquare', 'NetSuiteSquare', @@ -716,23 +713,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return CONST.EMPTY_ARRAY as unknown as Array>; } - const allSelectedAreDeleted = selectedTransactionsKeys.length > 0 && selectedTransactionsKeys.every((id) => isDeletedTransaction(selectedTransactions[id] ?? {})); - - if (allSelectedAreDeleted) { - return [ - { - icon: expensifyIcons.RotateLeft, - text: translate('search.bulkActions.undelete'), - value: CONST.SEARCH.BULK_ACTION_TYPES.UNDELETE, - shouldCloseModalOnSelect: true, - onSelected: () => { - undeleteTransactions(selectedTransactionsKeys); - clearSelectedTransactions(); - }, - }, - ]; - } - const options: Array> = []; const isAnyTransactionOnHold = Object.values(selectedTransactions).some((transaction) => transaction.isHeld); @@ -1231,7 +1211,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { expensifyIcons.ArrowSplit, expensifyIcons.Pencil, expensifyIcons.Trashcan, - expensifyIcons.RotateLeft, expensifyIcons.Exclamation, translate, areAllMatchingItemsSelected, @@ -1274,8 +1253,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { firstTransaction, firstTransactionPolicy, handleDeleteSelectedTransactions, - undeleteTransactions, - currentUserPersonalDetails?.email, theme.icon, styles.colorMuted, styles.fontWeightNormal, diff --git a/src/hooks/useUndeleteTransactions.ts b/src/hooks/useUndeleteTransactions.ts deleted file mode 100644 index bc4e8452920ed..0000000000000 --- a/src/hooks/useUndeleteTransactions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {changeTransactionsReport} from '@libs/actions/Transaction'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import useAllTransactions from './useAllTransactions'; -import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; -import useLocalize from './useLocalize'; -import useOnyx from './useOnyx'; -import usePermissions from './usePermissions'; - -function useUndeleteTransactions() { - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const allTransactions = useAllTransactions(); - const {isBetaEnabled} = usePermissions(); - const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); - const {translate, toLocaleDigit} = useLocalize(); - const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${personalPolicyID}`); - - return (transactionIDs: string[]) => { - changeTransactionsReport({ - transactionIDs, - isASAPSubmitBetaEnabled, - accountID: currentUserPersonalDetails.accountID ?? CONST.DEFAULT_NUMBER_ID, - email: currentUserPersonalDetails.email ?? '', - policy, - allTransactions, - translate, - toLocaleDigit, - }); - }; -} - -export default useUndeleteTransactions; diff --git a/src/languages/de.ts b/src/languages/de.ts index 19af3b65f4a12..d5551c4edcfc4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1624,7 +1624,6 @@ const translations: TranslationDeepObject = { failedToApproveViaDEW: (reason: string) => `Genehmigung fehlgeschlagen. ${reason}`, cannotDuplicateDistanceExpense: 'Sie können Entfernungsausgaben nicht über mehrere Arbeitsbereiche hinweg duplizieren, da sich die Sätze zwischen den Arbeitsbereichen unterscheiden können.', - deleted: 'Gelöscht', }, transactionMerge: { listPage: { @@ -7402,7 +7401,6 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und unhold: 'Zurückhalten aufheben', reject: 'Ablehnen', noOptionsAvailable: 'Für die ausgewählte Ausgabengruppe sind keine Optionen verfügbar.', - undelete: 'Wiederherstellen', }, filtersHeader: 'Filter', filters: { diff --git a/src/languages/en.ts b/src/languages/en.ts index dd449f3de55cc..30a2e6b809eff 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1363,7 +1363,6 @@ const translations = { }), settledExpensify: 'Paid', done: 'Done', - deleted: 'Deleted', settledElsewhere: 'Paid elsewhere', individual: 'Individual', business: 'Business', @@ -7394,7 +7393,6 @@ const translations = { hold: 'Hold', unhold: 'Remove hold', reject: 'Reject', - undelete: 'Undelete', noOptionsAvailable: 'No options available for the selected group of expenses.', }, filtersHeader: 'Filters', diff --git a/src/languages/es.ts b/src/languages/es.ts index 31005af87c4cf..498caf99fdf8f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1232,7 +1232,6 @@ const translations: TranslationDeepObject = { }), settledExpensify: 'Pagado', done: 'Listo', - deleted: 'Eliminado', settledElsewhere: 'Pagado de otra forma', individual: 'Individual', business: 'Empresa', @@ -7299,7 +7298,6 @@ ${amount} para ${merchant} - ${date}`, hold: 'Retener', unhold: 'Desbloquear', reject: 'Rechazar', - undelete: 'Restaurar', noOptionsAvailable: 'No hay opciones disponibles para el grupo de gastos seleccionado.', }, filtersHeader: 'Filtros', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index bd2d04bba985a..7ea6f71c4755c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1629,7 +1629,6 @@ const translations: TranslationDeepObject = { `impossible d’approuver via les règles de l’espace de travail. ${reason}`, failedToApproveViaDEW: (reason: string) => `échec de l’approbation. ${reason}`, cannotDuplicateDistanceExpense: 'Vous ne pouvez pas dupliquer des dépenses de distance entre espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.', - deleted: 'Supprimé', }, transactionMerge: { listPage: { @@ -7425,7 +7424,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip unhold: 'Supprimer la mise en attente', reject: 'Rejeter', noOptionsAvailable: 'Aucune option n’est disponible pour le groupe de dépenses sélectionné.', - undelete: 'Restaurer', }, filtersHeader: 'Filtres', filters: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 67c09794b7b70..f590d8e7837d3 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1622,7 +1622,6 @@ const translations: TranslationDeepObject = { `approvazione non riuscita tramite le regole dello spazio di lavoro. ${reason}`, failedToApproveViaDEW: (reason: string) => `approvazione non riuscita. ${reason}`, cannotDuplicateDistanceExpense: 'Non puoi duplicare le spese chilometriche tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse.', - deleted: 'Eliminato', }, transactionMerge: { listPage: { @@ -7389,7 +7388,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo unhold: 'Rimuovi blocco', reject: 'Rifiuta', noOptionsAvailable: 'Nessuna opzione disponibile per il gruppo di spese selezionato.', - undelete: 'Ripristina', }, filtersHeader: 'Filtri', filters: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index e148476ebdf95..786cd61204ac8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1602,7 +1602,6 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `ワークスペースルールで承認に失敗しました。${reason}`, failedToApproveViaDEW: (reason: string) => `承認に失敗しました。${reason}`, cannotDuplicateDistanceExpense: '距離精算はワークスペースごとにレートが異なる可能性があるため、ワークスペース間で複製することはできません。', - deleted: '削除済み', }, transactionMerge: { listPage: { @@ -7301,7 +7300,6 @@ ${reportName} unhold: '保留を解除', reject: '却下', noOptionsAvailable: '選択した経費グループには利用できるオプションがありません。', - undelete: '削除を取り消す', }, filtersHeader: 'フィルター', filters: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index f59c5361e5689..8f8e0c5bcc4f3 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1618,7 +1618,6 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `goedkeuren via werkruimte­regels is mislukt. ${reason}`, failedToApproveViaDEW: (reason: string) => `goedkeuren mislukt. ${reason}`, cannotDuplicateDistanceExpense: 'Je kunt afstandsvergoedingen niet dupliceren tussen werkruimtes, omdat de tarieven per werkruimte kunnen verschillen.', - deleted: 'Verwijderd', }, transactionMerge: { listPage: { @@ -7368,7 +7367,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar unhold: 'Blokkering opheffen', reject: 'Afwijzen', noOptionsAvailable: 'Geen opties beschikbaar voor de geselecteerde groep onkosten.', - undelete: 'Terugzetten', }, filtersHeader: 'Filters', filters: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index dca93fb9f9b34..9e272f59645cf 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1618,7 +1618,6 @@ const translations: TranslationDeepObject = { `nie udało się zatwierdzić przez zasady w przestrzeni roboczej. ${reason}`, failedToApproveViaDEW: (reason: string) => `nie udało się zaakceptować. ${reason}`, cannotDuplicateDistanceExpense: 'Nie możesz duplikować wydatków za przejazdy między przestrzeniami roboczymi, ponieważ stawki mogą się różnić między poszczególnymi przestrzeniami.', - deleted: 'Usunięto', }, transactionMerge: { listPage: { @@ -7355,7 +7354,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i unhold: 'Usuń blokadę', reject: 'Odrzuć', noOptionsAvailable: 'Brak opcji dostępnych dla wybranej grupy wydatków.', - undelete: 'Cofnij usunięcie', }, filtersHeader: 'Filtry', filters: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 5519fbc97cf29..d346ceb82b5cc 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1615,7 +1615,6 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `falha ao aprovar pelas regras do workspace. ${reason}`, failedToApproveViaDEW: (reason: string) => `falha ao aprovar. ${reason}`, cannotDuplicateDistanceExpense: 'Você não pode duplicar despesas de distância entre espaços de trabalho porque as tarifas podem ser diferentes entre eles.', - deleted: 'Excluído', }, transactionMerge: { listPage: { @@ -7358,7 +7357,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e unhold: 'Remover bloqueio', reject: 'Rejeitar', noOptionsAvailable: 'Nenhuma opção disponível para o grupo de despesas selecionado.', - undelete: 'Restaurar', }, filtersHeader: 'Filtros', filters: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c9f9c81a75282..d8d233c51f69f 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1572,7 +1572,6 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `未能通过工作区规则批准。${reason}`, failedToApproveViaDEW: (reason: string) => `批准失败。${reason}`, cannotDuplicateDistanceExpense: '你无法在不同工作区之间复制里程报销,因为各个工作区的费率可能不同。', - deleted: '已删除', }, transactionMerge: { listPage: { @@ -7175,7 +7174,6 @@ ${reportName} unhold: '解除保留', reject: '拒绝', noOptionsAvailable: '所选报销的费用组没有可用选项。', - undelete: '取消删除', }, filtersHeader: '筛选器', filters: { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 54107c6399e0e..ebd12ac5c7a53 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -997,7 +997,6 @@ type GetReportNameParams = { type GetReportStatusParams = { stateNum?: number; statusNum?: number; - isDeleted?: boolean; translate: LocaleContextProps['translate']; }; @@ -13257,10 +13256,7 @@ function buildOptimisticMarkedAsResolvedReportAction(created = DateUtils.getDBTi * ======================================== */ -function getReportStatusTranslation({stateNum, statusNum, isDeleted, translate}: GetReportStatusParams): string { - if (isDeleted) { - return translate('iou.deleted'); - } +function getReportStatusTranslation({stateNum, statusNum, translate}: GetReportStatusParams): string { if (stateNum === undefined || statusNum === undefined) { return ''; } @@ -13288,10 +13284,7 @@ function getReportStatusTranslation({stateNum, statusNum, isDeleted, translate}: return ''; } -function getReportStatusColorStyle(theme: ThemeColors, stateNum?: number, statusNum?: number, isDeleted?: boolean): {backgroundColor?: ColorValue; textColor?: ColorValue} | undefined { - if (isDeleted) { - return theme.reportStatusBadge.deleted; - } +function getReportStatusColorStyle(theme: ThemeColors, stateNum?: number, statusNum?: number): {backgroundColor?: ColorValue; textColor?: ColorValue} | undefined { if (stateNum === undefined || statusNum === undefined) { return undefined; } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index fee25679711f0..7d3bb910fcff1 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -164,7 +164,6 @@ import { getAmount as getTransactionAmount, getCreated as getTransactionCreatedDate, getMerchant as getTransactionMerchant, - isDeletedTransaction, isPending, isScanning, isViolationDismissed, @@ -342,18 +341,18 @@ const transactionQuarterGroupColumnNamesToSortingProperty: TransactionQuarterGro ...transactionGroupBaseSortingProperties, }; -type ExpenseStatusPredicate = (expenseReport?: OnyxTypes.Report, transactionReportID?: string) => boolean; - -const expenseStatusActionMapping: Record = { - [CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN, - [CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]: (expenseReport) => +const expenseStatusActionMapping = { + [CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport?: OnyxTypes.Report) => + expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN, + [CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]: (expenseReport?: OnyxTypes.Report) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED, - [CONST.SEARCH.STATUS.EXPENSE.APPROVED]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED, - [CONST.SEARCH.STATUS.EXPENSE.PAID]: (expenseReport) => + [CONST.SEARCH.STATUS.EXPENSE.APPROVED]: (expenseReport?: OnyxTypes.Report) => + expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED, + [CONST.SEARCH.STATUS.EXPENSE.PAID]: (expenseReport?: OnyxTypes.Report) => (expenseReport?.stateNum ?? 0) >= CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED, - [CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, - [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport, transactionReportID) => !expenseReport && transactionReportID !== CONST.REPORT.TRASH_REPORT_ID, - [CONST.SEARCH.STATUS.EXPENSE.DELETED]: (_expenseReport, transactionReportID) => transactionReportID === CONST.REPORT.TRASH_REPORT_ID, + [CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport?: OnyxTypes.Report) => + expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, + [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport?: OnyxTypes.Report) => !expenseReport, [CONST.SEARCH.STATUS.EXPENSE.ALL]: () => true, }; @@ -392,7 +391,6 @@ function getExpenseStatusOptions(translate: LocalizedTranslate): Array { - return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report, transactionItem.reportID) : false; + return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report) : false; }); } else { - shouldShow = isValidExpenseStatus(status) ? expenseStatusActionMapping[status](report, transactionItem.reportID) : false; + shouldShow = isValidExpenseStatus(status) ? expenseStatusActionMapping[status](report) : false; } } } @@ -1944,11 +1942,6 @@ function getActions( } const transaction = isTransaction ? data[key] : undefined; - - if (transaction && isDeletedTransaction(transaction)) { - return [CONST.SEARCH.ACTION_TYPES.UNDELETE]; - } - // Tracked and unreported expenses don't have a report, so we return early. if (!report) { return [CONST.SEARCH.ACTION_TYPES.VIEW]; @@ -2123,7 +2116,6 @@ function createAndOpenSearchTransactionThread( shouldNavigate = true, ) { const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; - const isDeleted = isDeletedTransaction(item); const iouReportAction = getIOUActionForReportID(isFromSelfDM ? findSelfDMReportID() : item.reportID, item.transactionID); const moneyRequestReportActionID = item.reportAction?.reportActionID ?? undefined; const previewData = transactionPreviewData @@ -2137,16 +2129,16 @@ function createAndOpenSearchTransactionThread( // The transaction thread can be created from the chat page and the snapshot data is stale if (hasActualTransactionThread) { transactionThreadReport = getReportOrDraftReport(iouReportAction.childReportID); - } - - // Only create a new thread when there's no existing childReportID. - // When childReportID exists but the report isn't in Onyx (e.g. search snapshot didn't include it), - // skip creation so the navigation below falls back to the real childReportID. - if (!transactionThreadReport && !hasActualTransactionThread) { + } else { + // For legacy transactions without an IOU action in the backend, pass transaction data + // This allows OpenReport to create the IOU action and transaction thread on the backend const reportActionID = moneyRequestReportActionID ?? iouReportAction?.reportActionID; + // Pass transaction data when there's no reportActionID OR when the item is from self DM + // (unreported transactions have a valid reportActionID but still need transaction data for proper detection) const shouldPassTransactionData = !reportActionID || isFromSelfDM; const transaction = shouldPassTransactionData ? getTransactionFromTransactionListItem(item) : undefined; const transactionViolations = shouldPassTransactionData ? item.violations : undefined; + // Use the full reportAction to preserve originalMessage.type (e.g., "track") for proper expense type detection const reportActionToPass = iouReportAction ?? item.reportAction ?? ({reportActionID} as OnyxTypes.ReportAction); transactionThreadReport = createTransactionThreadReport( introSelected, @@ -2163,7 +2155,7 @@ function createAndOpenSearchTransactionThread( if (shouldNavigate) { // Navigate to transaction thread if there are multiple transactions in the report, or to the parent report if it's the only transaction const isFromOneTransactionReport = isOneTransactionReport(item.report); - const shouldNavigateToTransactionThread = (!isFromOneTransactionReport || isFromSelfDM || isDeleted) && transactionThreadReport?.reportID !== CONST.REPORT.UNREPORTED_REPORT_ID; + const shouldNavigateToTransactionThread = (!isFromOneTransactionReport || isFromSelfDM) && transactionThreadReport?.reportID !== CONST.REPORT.UNREPORTED_REPORT_ID; // When we have an actual transaction thread (childReportID from Onyx) but the report isn't in Onyx yet // (e.g. Search didn't return the IOU action for deleted items), use childReportID directly so we don't navigate with undefined const targetReportID = shouldNavigateToTransactionThread diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 0524bf105e2c0..32aced2e63380 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -137,10 +137,6 @@ type BuildOptimisticTransactionParams = { isDemoTransactionParam?: boolean; }; -function isDeletedTransaction(transaction: {reportID?: string}): boolean { - return transaction.reportID === CONST.REPORT.TRASH_REPORT_ID; -} - function hasDistanceCustomUnit(transaction: OnyxEntry | Partial): boolean { return transaction?.comment?.type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && transaction?.comment?.customUnit?.name === CONST.CUSTOM_UNITS.NAME_DISTANCE; } @@ -3008,7 +3004,6 @@ export { isDistanceTypeRequest, recalculateUnreportedTransactionDetails, hasSmartScanFailedWithMissingFields, - isDeletedTransaction, }; export type {TransactionChanges}; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 83b0c187bab65..c761efd4e144e 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -11930,18 +11930,18 @@ function setMultipleMoneyRequestParticipantsFromReport(transactionIDs: string[], return Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, updatedTransactions); } -type ExpenseReportStatusPredicate = (expenseReport: OnyxEntry, transactionReportID?: string) => boolean; - -const expenseReportStatusFilterMapping: Record = { - [CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.OPEN, - [CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]: (expenseReport) => +const expenseReportStatusFilterMapping = { + [CONST.SEARCH.STATUS.EXPENSE.DRAFTS]: (expenseReport: OnyxEntry) => + expenseReport?.stateNum === CONST.REPORT.STATE_NUM.OPEN && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.OPEN, + [CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]: (expenseReport: OnyxEntry) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED, - [CONST.SEARCH.STATUS.EXPENSE.APPROVED]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED, - [CONST.SEARCH.STATUS.EXPENSE.PAID]: (expenseReport) => + [CONST.SEARCH.STATUS.EXPENSE.APPROVED]: (expenseReport: OnyxEntry) => + expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED, + [CONST.SEARCH.STATUS.EXPENSE.PAID]: (expenseReport: OnyxEntry) => (expenseReport?.stateNum ?? 0) >= CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED, - [CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, - [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport, transactionReportID) => !expenseReport && transactionReportID !== CONST.REPORT.TRASH_REPORT_ID, - [CONST.SEARCH.STATUS.EXPENSE.DELETED]: (_expenseReport, transactionReportID) => transactionReportID === CONST.REPORT.TRASH_REPORT_ID, + [CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport: OnyxEntry) => + expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, + [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport: OnyxEntry) => !expenseReport, [CONST.SEARCH.STATUS.EXPENSE.ALL]: () => true, }; @@ -11961,15 +11961,14 @@ function shouldOptimisticallyUpdateSearch( } let shouldOptimisticallyUpdateByStatus; const status = currentSearchQueryJSON.status; - const transactionReportID = transaction?.reportID; if (Array.isArray(status)) { shouldOptimisticallyUpdateByStatus = status.some((val) => { const expenseStatus = val as ValueOf; - return expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); + return expenseReportStatusFilterMapping[expenseStatus](iouReport); }); } else { const expenseStatus = status as ValueOf; - shouldOptimisticallyUpdateByStatus = expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); + shouldOptimisticallyUpdateByStatus = expenseReportStatusFilterMapping[expenseStatus](iouReport); } if (currentSearchQueryJSON.policyID?.length && iouReport?.policyID) { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 5c666d5ebc871..2bf4fd770f80b 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -110,7 +110,6 @@ type HandleActionButtonPressParams = { onDelegateAccessRestricted?: () => void; personalPolicyID: string | undefined; ownerBillingGracePeriodEnd: OnyxEntry; - onUndelete?: () => void; }; type BulkDeleteReportsParams = { @@ -142,14 +141,13 @@ function handleActionButtonPress({ onDelegateAccessRestricted, personalPolicyID, ownerBillingGracePeriodEnd, - onUndelete, }: HandleActionButtonPressParams) { // The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid. // We need the transactionID to display the loading indicator for that list item's action. const allReportTransactions = (isTransactionGroupListItemType(item) ? item.transactions : [item]) as Transaction[]; const hasHeldExpense = hasHeldExpenses('', allReportTransactions); - if (hasHeldExpense && item.action !== CONST.SEARCH.ACTION_TYPES.SUBMIT && item.action !== CONST.SEARCH.ACTION_TYPES.UNDELETE) { + if (hasHeldExpense && item.action !== CONST.SEARCH.ACTION_TYPES.SUBMIT) { goToItem(); return; } @@ -204,9 +202,6 @@ function handleActionButtonPress({ exportToIntegrationOnSearch(hash, [item.reportID], connectedIntegration, currentSearchKey); return; } - case CONST.SEARCH.ACTION_TYPES.UNDELETE: - onUndelete?.(); - return; default: goToItem(); } diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 1c444f985e71d..f3bfc8a02f6b9 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -36,7 +36,6 @@ import { buildOptimisticUnreportedTransactionAction, buildTransactionThread, findSelfDMReportID, - getIOUReportActionMessage, getReportTransactions, getTransactionDetails, hasViolations as hasViolationsReportUtils, @@ -44,7 +43,6 @@ import { } from '@libs/ReportUtils'; import { hasPendingRTERViolation, - isDeletedTransaction, isManagedCardTransaction, isOnHold, recalculateUnreportedTransactionDetails, @@ -1014,14 +1012,11 @@ function changeTransactionsReport({ const destinationCurrency = newReport?.currency ?? policy?.outputCurrency; for (const transaction of transactions) { - const isDeletedExpense = isDeletedTransaction(transaction); const isUnreportedExpense = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; - const selfDMReportID = existingSelfDMReportID ?? selfDMReport?.reportID; - // Skip lookup for deleted transactions: the old IOU action is already cleaned up - // during deletion and its transaction thread is deleted, so reusing it is harmful. - const oldIOUAction = isDeletedExpense ? undefined : getIOUActionForReportID(isUnreportedExpense ? selfDMReportID : transaction.reportID, transaction.transactionID); + const selfDMReportID = existingSelfDMReportID ?? selfDMReport?.reportID; + const oldIOUAction = getIOUActionForReportID(isUnreportedExpense ? selfDMReportID : transaction.reportID, transaction.transactionID); if (!transaction.reportID || transaction.reportID === reportID) { continue; } @@ -1037,36 +1032,29 @@ function changeTransactionsReport({ const optimisticMoneyRequestReportActionID = rand64(); const originalMessage = getOriginalMessage(oldIOUAction) as OriginalMessageIOU; - const actionType = isUnreported ? CONST.IOU.REPORT_ACTION_TYPE.TRACK : CONST.IOU.REPORT_ACTION_TYPE.CREATE; const newIOUAction = { ...oldIOUAction, originalMessage: { ...originalMessage, - IOUTransactionID: originalMessage?.IOUTransactionID ?? transaction.transactionID, IOUReportID: reportID, - type: actionType, + type: isUnreported ? CONST.IOU.REPORT_ACTION_TYPE.TRACK : CONST.IOU.REPORT_ACTION_TYPE.CREATE, }, reportActionID: optimisticMoneyRequestReportActionID, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - actionName: oldIOUAction?.actionName ?? CONST.REPORT.ACTIONS.TYPE.IOU, + actionName: oldIOUAction?.actionName ?? CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION, created: oldIOUAction?.created ?? DateUtils.getDBTime(), - ...(!oldIOUAction && { - message: getIOUReportActionMessage(reportID, actionType, Math.abs(transaction.amount), transaction.comment?.comment ?? '', transaction.currency), - }), }; const {comment, modifiedAmount, modifiedCurrency, modifiedMerchant} = isUnreported ? recalculateUnreportedTransactionDetails(transaction, destinationCurrency, translate, toLocaleDigit) : {}; - // 1. Optimistically update the transaction with full data and changed fields. - // Spreading the full transaction ensures the TRANSACTION collection has complete data - // (e.g. amount) even when the existing entry was incomplete from search results. + // 1. Optimistically change the reportID on the passed transactions + // Only set pendingAction for transactions that need convertedAmount recalculation optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, value: { - ...transaction, reportID, comment, modifiedAmount, @@ -1243,15 +1231,15 @@ function changeTransactionsReport({ // 4. Optimistically update the IOU action reportID const trackExpenseActionableWhisper = isUnreportedExpense ? getTrackExpenseActionableWhisper(transaction.transactionID, selfDMReportID) : undefined; - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetReportID}`, - value: { - [newIOUAction.reportActionID]: newIOUAction, - }, - }); - if (oldIOUAction) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetReportID}`, + value: { + [newIOUAction.reportActionID]: newIOUAction, + }, + }); + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${isUnreportedExpense ? selfDMReportID : oldReportID}`, @@ -1284,22 +1272,24 @@ function changeTransactionsReport({ [newIOUAction.reportActionID]: {pendingAction: null}, }, }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetReportID}`, - value: { - [newIOUAction.reportActionID]: null, - }, - }); if (oldIOUAction) { - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${isUnreportedExpense ? selfDMReportID : oldReportID}`, - value: { - [oldIOUAction.reportActionID]: oldIOUAction, - ...(trackExpenseActionableWhisper ? {[trackExpenseActionableWhisper.reportActionID]: trackExpenseActionableWhisper} : {}), + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetReportID}`, + value: { + [newIOUAction.reportActionID]: null, + }, }, - }); + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${isUnreportedExpense ? selfDMReportID : oldReportID}`, + value: { + [oldIOUAction.reportActionID]: oldIOUAction, + ...(trackExpenseActionableWhisper ? {[trackExpenseActionableWhisper.reportActionID]: trackExpenseActionableWhisper} : {}), + }, + }, + ); } const shouldRemoveOtherParticipants = !isManagedCardTransaction(transaction); @@ -1420,7 +1410,7 @@ function changeTransactionsReport({ const baseTransactionData = { movedReportActionID: movedAction.reportActionID, moneyRequestPreviewReportActionID: newIOUAction.reportActionID, - ...(transactionThreadCreatedReportActionID + ...(oldIOUAction && !oldIOUAction.childReportID ? { transactionThreadReportID, transactionThreadCreatedReportActionID, diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts index 3a3aa0c5511af..668ea7fc15fae 100644 --- a/src/styles/theme/themes/dark.ts +++ b/src/styles/theme/themes/dark.ts @@ -193,10 +193,6 @@ const darkTheme = { backgroundColor: colors.pink700, textColor: colors.pink200, }, - deleted: { - backgroundColor: colors.tangerine700, - textColor: colors.productDark900, - }, }, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts index 407d087e26caf..87000a8f5114d 100644 --- a/src/styles/theme/themes/light.ts +++ b/src/styles/theme/themes/light.ts @@ -193,10 +193,6 @@ const lightTheme = { backgroundColor: colors.pink200, textColor: colors.pink700, }, - deleted: { - backgroundColor: colors.tangerine500, - textColor: colors.productLight100, - }, }, statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT, diff --git a/src/styles/theme/types.ts b/src/styles/theme/types.ts index 8ab7ccf191e59..b635cd6730a0d 100644 --- a/src/styles/theme/types.ts +++ b/src/styles/theme/types.ts @@ -119,7 +119,7 @@ type ThemeColors = { trialTimer: Color; reportStatusBadge: Record< - 'draft' | 'outstanding' | 'paid' | 'approved' | 'closed' | 'deleted', + 'draft' | 'outstanding' | 'paid' | 'approved' | 'closed', { backgroundColor: Color; textColor: Color; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index b1746053c2cc8..0671ffc257177 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -9869,21 +9869,6 @@ describe('ReportUtils', () => { expect(getReportStatusTranslation({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: undefined, translate: mockTranslate})).toBe(''); expect(getReportStatusTranslation({stateNum: undefined, statusNum: CONST.REPORT.STATUS_NUM.OPEN, translate: mockTranslate})).toBe(''); }); - - it('should return "Deleted" when isDeleted is true', () => { - const result = getReportStatusTranslation({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, isDeleted: true, translate: mockTranslate}); - expect(result).toBe(mockTranslate('iou.deleted')); - }); - - it('should return "Deleted" when isDeleted is true regardless of stateNum and statusNum', () => { - expect(getReportStatusTranslation({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, isDeleted: true, translate: mockTranslate})).toBe( - mockTranslate('iou.deleted'), - ); - expect(getReportStatusTranslation({stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, isDeleted: true, translate: mockTranslate})).toBe( - mockTranslate('iou.deleted'), - ); - expect(getReportStatusTranslation({stateNum: undefined, statusNum: undefined, isDeleted: true, translate: mockTranslate})).toBe(mockTranslate('iou.deleted')); - }); }); describe('buildOptimisticReportPreview', () => { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 7dcb3f9d7d0c3..da557b959a94f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -7525,7 +7525,6 @@ describe('SearchUIUtils', () => { }); test('Should fallback to childReportID from IOU action when transaction thread report is not in Onyx', async () => { - (createTransactionThreadReport as jest.Mock).mockReset(); const childReportID = 'child-thread-456'; // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const multiTransactionItem = transactionsListItems.at(2) as TransactionListItemType; From bfddc5b09281a6b76acdec2cb3bb3bc707efc6ca Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 31 Mar 2026 16:13:05 -0600 Subject: [PATCH 2/4] Fix useSearchBulkActions hook lint warnings Align cached search results and callback memoization to satisfy react hooks lint rules. Made-with: Cursor --- src/hooks/useSearchBulkActions.ts | 71 ++++++++++--------------------- 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 6ca38e98111b3..c85c47b3e95ed 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useCallback, useMemo, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -55,7 +55,7 @@ import {canIOUBePaid, dismissRejectUseExplanation, initBulkEditDraftTransaction} import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BillingGraceEndPeriod, Report, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {BillingGraceEndPeriod, Report, Transaction, TransactionViolations} from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import useAllTransactions from './useAllTransactions'; import useBulkPayOptions from './useBulkPayOptions'; @@ -68,6 +68,7 @@ import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; import usePermissions from './usePermissions'; import usePersonalPolicy from './usePersonalPolicy'; +import usePreviousDefined from './usePreviousDefined'; import useSelfDMReport from './useSelfDMReport'; import useTheme from './useTheme'; import useThemeStyles from './useThemeStyles'; @@ -100,6 +101,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const {clearSelectedTransactions, selectAllMatchingItems} = useSearchActionsContext(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {accountID} = currentUserPersonalDetails; + const currentUserEmail = currentUserPersonalDetails.email ?? ''; + const currentUserLogin = currentUserPersonalDetails.login ?? ''; const allTransactions = useAllTransactions(); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); @@ -121,14 +124,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { // Cache the last search results that had data, so the merge option remains available // while results are temporarily unset (e.g. during sorting/loading). - const lastNonEmptySearchResultsRef = useRef(undefined); - useEffect(() => { - if (!currentSearchResults?.data) { - return; - } - lastNonEmptySearchResultsRef.current = currentSearchResults; - }, [currentSearchResults]); - const searchResults = currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResultsRef.current; + const searchResults = usePreviousDefined(currentSearchResults?.data ? currentSearchResults : undefined); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); @@ -476,8 +472,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { deleteAppReport({ report, selfDMReport, - currentUserEmailParam: currentUserPersonalDetails?.email ?? '', - currentUserAccountIDParam: currentUserPersonalDetails?.accountID, + currentUserEmailParam: currentUserEmail, + currentUserAccountIDParam: accountID, reportTransactions: validTransactions, allTransactionViolations, bankAccountList, @@ -526,8 +522,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { transactions, allReports, selfDMReport, - currentUserPersonalDetails?.email, - currentUserPersonalDetails?.accountID, + currentUserEmail, + currentUserPersonalDetails.email, toLocaleDigit, isExpenseReportType, selectedReportIDs, @@ -679,16 +675,15 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { ], ); - const onBulkPaySelectedRef = useRef(onBulkPaySelected); - useEffect(() => { - onBulkPaySelectedRef.current = onBulkPaySelected; - }); - const stableOnBulkPaySelected = useCallback((paymentMethod?: PaymentMethodType, additionalData?: BulkPaySelectionData) => { - onBulkPaySelectedRef.current?.(paymentMethod, additionalData); - }, []); + const stableOnBulkPaySelected = useCallback( + (paymentMethod?: PaymentMethodType, additionalData?: BulkPaySelectionData) => { + onBulkPaySelected(paymentMethod, additionalData); + }, + [onBulkPaySelected], + ); const areAllTransactionsFromSubmitter = useMemo(() => { - if (!currentUserPersonalDetails?.accountID) { + if (!accountID) { return false; } @@ -706,7 +701,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return isCurrentUserSubmitter(getReportOrDraftReport(id, reports)); }) ); - }, [selectedTransactionReportIDs, currentUserPersonalDetails?.accountID, currentSearchResults?.data]); + }, [selectedTransactionReportIDs, accountID, currentSearchResults?.data]); const headerButtonsOptions = useMemo(() => { if (selectedTransactionsKeys.length === 0 || status == null || !hash) { @@ -746,13 +741,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return false; } - const reportExportOptions = getSecondaryExportReportActions( - currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, - currentUserPersonalDetails?.login ?? '', - completeReport, - bankAccountList, - reportPolicy, - ); + const reportExportOptions = getSecondaryExportReportActions(accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserLogin, completeReport, bankAccountList, reportPolicy); return reportExportOptions.includes(exportOption); }; @@ -1198,20 +1187,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { selectedTransactions, queryJSON?.type, expensifyIcons, - expensifyIcons.Export, - expensifyIcons.ArrowRight, - expensifyIcons.Table, - expensifyIcons.ThumbsUp, - expensifyIcons.ThumbsDown, - expensifyIcons.Send, - expensifyIcons.MoneyBag, - expensifyIcons.Stopwatch, - expensifyIcons.ArrowCollapse, - expensifyIcons.DocumentMerge, - expensifyIcons.ArrowSplit, - expensifyIcons.Pencil, - expensifyIcons.Trashcan, - expensifyIcons.Exclamation, translate, areAllMatchingItemsSelected, isOffline, @@ -1228,13 +1203,14 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { integrationsExportTemplates, csvExportLayouts, allReports, + accountID, currentUserPersonalDetails.accountID, - currentUserPersonalDetails?.login, + currentUserLogin, bankAccountList, - styles.integrationIcon, styles.colorMuted, styles.fontWeightNormal, styles.textWrap, + styles.integrationIcon, showConfirmModal, clearSelectedTransactions, handleBasicExport, @@ -1254,9 +1230,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { firstTransactionPolicy, handleDeleteSelectedTransactions, theme.icon, - styles.colorMuted, - styles.fontWeightNormal, - styles.textWrap, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, currentSearchKey, From 42d1d1a73cc74e7f3ed69ef122ed8448206e41ba Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 1 Apr 2026 09:26:56 -0600 Subject: [PATCH 3/4] resolve conflicts --- src/hooks/useSearchBulkActions.ts | 72 +++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 7e58561773a17..3458c55c1310f 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useState} from 'react'; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -56,7 +56,7 @@ import {canIOUBePaid, dismissRejectUseExplanation, initBulkEditDraftTransaction} import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BillingGraceEndPeriod, Policy, Report, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {BillingGraceEndPeriod, Policy, Report, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import useAllTransactions from './useAllTransactions'; import useBulkPayOptions from './useBulkPayOptions'; @@ -69,7 +69,6 @@ import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; import usePermissions from './usePermissions'; import usePersonalPolicy from './usePersonalPolicy'; -import usePreviousDefined from './usePreviousDefined'; import useSelfDMReport from './useSelfDMReport'; import useTheme from './useTheme'; import useThemeStyles from './useThemeStyles'; @@ -107,8 +106,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const {clearSelectedTransactions, selectAllMatchingItems} = useSearchActionsContext(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {accountID} = currentUserPersonalDetails; - const currentUserEmail = currentUserPersonalDetails.email ?? ''; - const currentUserLogin = currentUserPersonalDetails.login ?? ''; const allTransactions = useAllTransactions(); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); @@ -130,7 +127,14 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { // Cache the last search results that had data, so the merge option remains available // while results are temporarily unset (e.g. during sorting/loading). - const searchResults = usePreviousDefined(currentSearchResults?.data ? currentSearchResults : undefined); + const lastNonEmptySearchResultsRef = useRef(undefined); + useEffect(() => { + if (!currentSearchResults?.data) { + return; + } + lastNonEmptySearchResultsRef.current = currentSearchResults; + }, [currentSearchResults]); + const searchResults = currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResultsRef.current; const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); @@ -480,8 +484,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { deleteAppReport({ report, selfDMReport, - currentUserEmailParam: currentUserEmail, - currentUserAccountIDParam: accountID, + currentUserEmailParam: currentUserPersonalDetails?.email ?? '', + currentUserAccountIDParam: currentUserPersonalDetails?.accountID, reportTransactions: validTransactions, allTransactionViolations, bankAccountList, @@ -530,8 +534,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { transactions, allReports, selfDMReport, - currentUserEmail, - currentUserPersonalDetails.email, + currentUserPersonalDetails?.email, + currentUserPersonalDetails?.accountID, toLocaleDigit, isExpenseReportType, selectedReportIDs, @@ -694,15 +698,16 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { ], ); - const stableOnBulkPaySelected = useCallback( - (paymentMethod?: PaymentMethodType, additionalData?: BulkPaySelectionData) => { - onBulkPaySelected(paymentMethod, additionalData); - }, - [onBulkPaySelected], - ); + const onBulkPaySelectedRef = useRef(onBulkPaySelected); + useEffect(() => { + onBulkPaySelectedRef.current = onBulkPaySelected; + }); + const stableOnBulkPaySelected = useCallback((paymentMethod?: PaymentMethodType, additionalData?: BulkPaySelectionData) => { + onBulkPaySelectedRef.current?.(paymentMethod, additionalData); + }, []); const areAllTransactionsFromSubmitter = useMemo(() => { - if (!accountID) { + if (!currentUserPersonalDetails?.accountID) { return false; } @@ -720,7 +725,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return isCurrentUserSubmitter(getReportOrDraftReport(id, reports)); }) ); - }, [selectedTransactionReportIDs, accountID, currentSearchResults?.data]); + }, [selectedTransactionReportIDs, currentUserPersonalDetails?.accountID, currentSearchResults?.data]); const headerButtonsOptions = useMemo(() => { if (selectedTransactionsKeys.length === 0 || status == null || !hash) { @@ -760,7 +765,13 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return false; } - const reportExportOptions = getSecondaryExportReportActions(accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserLogin, completeReport, bankAccountList, reportPolicy); + const reportExportOptions = getSecondaryExportReportActions( + currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, + currentUserPersonalDetails?.login ?? '', + completeReport, + bankAccountList, + reportPolicy, + ); return reportExportOptions.includes(exportOption); }; @@ -1206,6 +1217,20 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { selectedTransactions, queryJSON?.type, expensifyIcons, + expensifyIcons.Export, + expensifyIcons.ArrowRight, + expensifyIcons.Table, + expensifyIcons.ThumbsUp, + expensifyIcons.ThumbsDown, + expensifyIcons.Send, + expensifyIcons.MoneyBag, + expensifyIcons.Stopwatch, + expensifyIcons.ArrowCollapse, + expensifyIcons.DocumentMerge, + expensifyIcons.ArrowSplit, + expensifyIcons.Pencil, + expensifyIcons.Trashcan, + expensifyIcons.Exclamation, translate, areAllMatchingItemsSelected, isOffline, @@ -1222,14 +1247,13 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { integrationsExportTemplates, csvExportLayouts, allReports, - accountID, currentUserPersonalDetails.accountID, - currentUserLogin, + currentUserPersonalDetails?.login, bankAccountList, + styles.integrationIcon, styles.colorMuted, styles.fontWeightNormal, styles.textWrap, - styles.integrationIcon, showConfirmModal, clearSelectedTransactions, handleBasicExport, @@ -1249,7 +1273,11 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { firstTransaction, firstTransactionPolicy, handleDeleteSelectedTransactions, + currentUserPersonalDetails?.email, theme.icon, + styles.colorMuted, + styles.fontWeightNormal, + styles.textWrap, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, currentSearchKey, From d3a510efe9e914690ccf726239c12028bc6d70c7 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 1 Apr 2026 10:16:27 -0600 Subject: [PATCH 4/4] rm dependency --- src/hooks/useSearchBulkActions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 3458c55c1310f..db726e6adde07 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1273,7 +1273,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { firstTransaction, firstTransactionPolicy, handleDeleteSelectedTransactions, - currentUserPersonalDetails?.email, theme.icon, styles.colorMuted, styles.fontWeightNormal,