diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index d43242332657..3d0d3d93c883 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -390,6 +390,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo outstandingReportsByPolicyID, isChatReportArchived, isProduction, + isOffline, }) : []; diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx index cddb34a778f5..5c0a367dd183 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx @@ -167,6 +167,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn outstandingReportsByPolicyID, isChatReportArchived, isProduction, + isOffline, }) : []; diff --git a/src/hooks/useReportPrimaryAction.ts b/src/hooks/useReportPrimaryAction.ts index dbb7780644bb..df6403ba8209 100644 --- a/src/hooks/useReportPrimaryAction.ts +++ b/src/hooks/useReportPrimaryAction.ts @@ -14,6 +14,7 @@ import {personalDetailsLoginSelector} from '@src/selectors/PersonalDetails'; import type {ValueOf} from 'type-fest'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; +import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; import useReportIsArchived from './useReportIsArchived'; import useTransactionsAndViolationsForReport from './useTransactionsAndViolationsForReport'; @@ -21,6 +22,7 @@ import useTransactionsAndViolationsForReport from './useTransactionsAndViolation function useReportPrimaryAction(reportID: string | undefined): ValueOf | '' { const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning} = usePaymentAnimationsContext(); const {login: currentUserLogin, accountID} = useCurrentUserPersonalDetails(); + const {isOffline} = useNetwork(); const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`); @@ -66,6 +68,7 @@ function useReportPrimaryAction(reportID: string | undefined): ValueOf { @@ -167,6 +168,7 @@ function useSelectionModeReportActions({ outstandingReportsByPolicyID, isChatReportArchived, isProduction, + isOffline, }); })(); diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index 0d605e08083c..3efa89f59487 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -82,6 +82,8 @@ type GetReportPrimaryActionParams = { isChatReportArchived: boolean; invoiceReceiverPolicy?: Policy; ownerLogin: string | undefined; + /** Whether the client is currently offline. Will become required once #66407 lands. */ + isOffline?: boolean; }; type IsPrimaryPayActionParams = { @@ -318,7 +320,7 @@ function isExportAction(report: Report, currentUserLogin: string, policy?: Polic return false; } -function isRemoveHoldAction(report: Report, chatReport: OnyxEntry, reportTransactions: Transaction[]) { +function isRemoveHoldAction(report: Report, chatReport: OnyxEntry, reportTransactions: Transaction[], isOffline?: boolean) { const isClosedReport = isClosedReportUtils(report); if (isClosedReport) { return false; @@ -331,7 +333,7 @@ function isRemoveHoldAction(report: Report, chatReport: OnyxEntry, repor } const reportActions = getAllReportActions(report.reportID); - const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions); + const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions, isOffline); if (!transactionThreadReportID) { return false; @@ -483,6 +485,7 @@ function getReportPrimaryAction(params: GetReportPrimaryActionParams): ValueOf, currentUserAccountID: number | undefined, + isOffline?: boolean, ): boolean { - const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions); + const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions, isOffline); const isOneExpenseReport = reportTransactions.length === 1; const transaction = reportTransactions.at(0); @@ -839,6 +840,7 @@ function isRemoveHoldAction( reportActions?: ReportAction[], policy?: Policy, primaryAction?: ValueOf | '', + isOffline?: boolean, ): boolean { const isClosedReport = isClosedReportUtils(report); if (isClosedReport) { @@ -851,7 +853,7 @@ function isRemoveHoldAction( return false; } - const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions); + const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions, isOffline); if (!transactionThreadReportID) { return false; @@ -931,6 +933,7 @@ function getSecondaryReportActions({ isChatReportArchived = false, parentReport, isProduction, + isOffline, }: { currentUserLogin: string; currentUserAccountID: number; @@ -951,6 +954,8 @@ function getSecondaryReportActions({ isChatReportArchived?: boolean; parentReport?: OnyxEntry; isProduction: boolean; + /** Whether the client is currently offline. Will become required once #66407 lands. */ + isOffline?: boolean; }): Array> { const options: Array> = []; @@ -995,6 +1000,7 @@ function getSecondaryReportActions({ reportMetadata, isChatReportArchived, ownerLogin: submitterLogin, + isOffline, }); if ( @@ -1040,11 +1046,11 @@ function getSecondaryReportActions({ options.push(CONST.REPORT.SECONDARY_ACTIONS.REOPEN); } - if (isHoldAction(report, chatReport, reportTransactions, reportActions, policy, currentUserAccountID)) { + if (isHoldAction(report, chatReport, reportTransactions, reportActions, policy, currentUserAccountID, isOffline)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.HOLD); } - if (isRemoveHoldAction(report, chatReport, reportTransactions, reportActions, policy, primaryAction)) { + if (isRemoveHoldAction(report, chatReport, reportTransactions, reportActions, policy, primaryAction, isOffline)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.REMOVE_HOLD); } diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index 056af383597d..72649f7f127c 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -1212,6 +1212,89 @@ describe('getPrimaryAction', () => { ).toBe(CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD); }); + it('should thread isOffline into the one-transaction thread lookup when detecting REMOVE HOLD', async () => { + const report = createMock({ + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + const policy = createMock({}); + const HOLD_ACTION_ID = 'HOLD_ACTION_ID'; + const REPORT_ACTION_ID = 'REPORT_ACTION_ID'; + const PENDING_DELETE_REPORT_ACTION_ID = 'PENDING_DELETE_REPORT_ACTION_ID'; + const TRANSACTION_ID = 'TRANSACTION_ID'; + const PENDING_DELETE_TRANSACTION_ID = 'PENDING_DELETE_TRANSACTION_ID'; + const CHILD_REPORT_ID = 'CHILD_REPORT_ID'; + const transaction = createMock({ + transactionID: TRANSACTION_ID, + comment: { + hold: HOLD_ACTION_ID, + }, + }); + + const reportAction = createMock({ + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + reportActionID: REPORT_ACTION_ID, + actorAccountID: CURRENT_USER_ACCOUNT_ID, + childReportID: CHILD_REPORT_ID, + message: [ + { + html: 'html', + }, + ], + originalMessage: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + IOUTransactionID: TRANSACTION_ID, + }, + }); + + // A second IOU action that has been deleted while offline. When offline, it is still counted as a + // transaction, so the report is no longer a one-transaction report and REMOVE HOLD is not offered. + const pendingDeleteReportAction = createMock({ + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + reportActionID: PENDING_DELETE_REPORT_ACTION_ID, + actorAccountID: CURRENT_USER_ACCOUNT_ID, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + message: [ + { + html: '', + }, + ], + originalMessage: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + IOUTransactionID: PENDING_DELETE_TRANSACTION_ID, + }, + }); + + const holdAction = { + reportActionID: HOLD_ACTION_ID, + reportID: CHILD_REPORT_ID, + actorAccountID: CURRENT_USER_ACCOUNT_ID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [REPORT_ACTION_ID]: reportAction, + [PENDING_DELETE_REPORT_ACTION_ID]: pendingDeleteReportAction, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${CHILD_REPORT_ID}`, {[HOLD_ACTION_ID]: holdAction}); + + const params = { + currentUserLogin: CURRENT_USER_EMAIL, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + report, + ownerLogin: '', + chatReport, + reportTransactions: [transaction], + violations: {}, + bankAccountList: {}, + policy, + isChatReportArchived: false, + }; + + expect(getReportPrimaryAction({...params, isOffline: false})).toBe(CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD); + expect(getReportPrimaryAction({...params, isOffline: true})).not.toBe(CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD); + }); + it('should return REMOVE HOLD over APPROVE when all expenses are held and the manager can unhold', async () => { const MEMBER_ACCOUNT_ID = 2; const HOLD_ACTION_ID = 'HOLD_ACTION_ID';