diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index bea2c312ac951..e55f9aaffe6f6 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -1,3 +1,4 @@ +import {delegateEmailSelector} from '@selectors/Account'; import React from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -15,6 +16,7 @@ import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentU import useHasOutstandingChildTask from '@hooks/useHasOutstandingChildTask'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useParentReport from '@hooks/useParentReport'; import useParentReportAction from '@hooks/useParentReportAction'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -31,6 +33,7 @@ import Parser from '@libs/Parser'; import {isCanceledTaskReport, isOpenTaskReport, isReportManager} from '@libs/ReportUtils'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -114,6 +117,7 @@ function TaskPreview({ const parentReport = useParentReport(taskContextReport?.reportID); const isParentReportArchived = useReportIsArchived(parentReport?.reportID); const hasOutstandingChildTask = useHasOutstandingChildTask(taskContextReport); + const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const isTaskActionable = canActionTask(taskContextReport, parentReportAction, currentUserPersonalDetails.accountID, parentReport, isParentReportArchived); const hasAssignee = taskAssigneeAccountID > 0; const personalDetails = usePersonalDetails(); @@ -163,9 +167,9 @@ function TaskPreview({ disabled={!isTaskActionable} onPress={callFunctionIfActionIsAllowed(() => { if (isTaskCompleted) { - reopenTask(taskContextReport, parentReport, currentUserPersonalDetails.accountID, taskReportID); + reopenTask(taskContextReport, parentReport, currentUserPersonalDetails.accountID, delegateEmail, taskReportID); } else { - completeTask(taskContextReport, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, taskReportID); + completeTask(taskContextReport, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, delegateEmail, taskReportID); } })} accessibilityLabel={translate('task.task')} diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 416bf70a8794d..c881de66f6a4a 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -1,3 +1,4 @@ +import {delegateEmailSelector} from '@selectors/Account'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -57,6 +58,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); + const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); useEffect(() => { setTaskReport(report); @@ -151,9 +153,9 @@ function TaskView({report, parentReport, action}: TaskViewProps) { return; } if (isCompleted) { - reopenTask(report, parentReport, currentUserPersonalDetails.accountID); + reopenTask(report, parentReport, currentUserPersonalDetails.accountID, delegateEmail); } else { - completeTask(report, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction); + completeTask(report, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, delegateEmail); } })} isChecked={isCompleted} diff --git a/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx b/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx index 10346b4291361..c9801a3d18cae 100644 --- a/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx +++ b/src/components/Search/SearchList/ListItem/TaskListItemRow.tsx @@ -1,3 +1,4 @@ +import {delegateEmailSelector} from '@selectors/Account'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; @@ -10,6 +11,7 @@ import TextWithTooltip from '@components/TextWithTooltip'; import useHasOutstandingChildTask from '@hooks/useHasOutstandingChildTask'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useParentReport from '@hooks/useParentReport'; import useParentReportAction from '@hooks/useParentReportAction'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -21,6 +23,7 @@ import {callFunctionIfActionIsAllowed} from '@libs/actions/Session'; import {canActionTask, completeTask} from '@libs/actions/Task'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; import AvatarWithTextCell from './AvatarWithTextCell'; import DateCell from './DateCell'; @@ -79,6 +82,7 @@ function ActionCell({taskItem, isLargeScreenWidth}: TaskCellProps) { const isParentReportArchived = useReportIsArchived(parentReport?.reportID); const hasOutstandingChildTask = useHasOutstandingChildTask(taskItem.report); const parentReportAction = useParentReportAction(taskItem.report); + const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const isTaskActionable = canActionTask(taskItem.report, parentReportAction, session?.accountID, parentReport, isParentReportArchived); const isTaskCompleted = taskItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED && taskItem.stateNum === CONST.REPORT.STATE_NUM.APPROVED; @@ -108,7 +112,7 @@ function ActionCell({taskItem, isLargeScreenWidth}: TaskCellProps) { style={[styles.w100]} isDisabled={!isTaskActionable} onPress={callFunctionIfActionIsAllowed(() => { - completeTask(taskItem as Report, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, taskItem.reportID); + completeTask(taskItem as Report, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, delegateEmail, taskItem.reportID); })} /> ); diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 2dd4dee842742..ded1f70f6778e 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -1,8 +1,10 @@ +import {delegateEmailSelector} from '@selectors/Account'; import React from 'react'; import {View} from 'react-native'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useHasOutstandingChildTask from '@hooks/useHasOutstandingChildTask'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useParentReport from '@hooks/useParentReport'; import useParentReportAction from '@hooks/useParentReportAction'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -12,6 +14,7 @@ import {isActiveTaskEditRoute} from '@libs/TaskUtils'; import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import {canActionTask, completeTask, reopenTask} from '@userActions/Task'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import Button from './Button'; @@ -28,6 +31,7 @@ function TaskHeaderActionButton({report}: TaskHeaderActionButtonProps) { const isParentReportArchived = useReportIsArchived(parentReport?.reportID); const hasOutstandingChildTask = useHasOutstandingChildTask(report); const parentReportAction = useParentReportAction(report); + const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const isTaskActionable = canActionTask(report, parentReportAction, currentUserPersonalDetails?.accountID, parentReport, isParentReportArchived); if (!canWriteInReport(report)) { @@ -46,9 +50,9 @@ function TaskHeaderActionButton({report}: TaskHeaderActionButtonProps) { return; } if (isCompletedTaskReport(report)) { - reopenTask(report, parentReport, currentUserPersonalDetails.accountID); + reopenTask(report, parentReport, currentUserPersonalDetails.accountID, delegateEmail); } else { - completeTask(report, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction); + completeTask(report, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, delegateEmail); } })} style={styles.flex1} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 82789d2a4eb50..a3b69f311932f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7794,6 +7794,7 @@ function updateReportPreview( function buildOptimisticTaskReportAction( taskReportID: string, actionName: typeof CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED | typeof CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED | typeof CONST.REPORT.ACTIONS.TYPE.TASK_CANCELLED, + delegateEmailParam: string | undefined, message = '', actorAccountID = deprecatedCurrentUserAccountID, createdOffset = 0, @@ -7805,7 +7806,9 @@ function buildOptimisticTaskReportAction( html: message, whisperedTo: [], }; - const delegateAccountDetails = getPersonalDetailByEmail(delegateEmail); + // Falls back to module-level delegateEmail (from Onyx.connect) for callers not yet migrated; will be removed in https://github.com/Expensify/App/issues/66417 + const effectiveDelegateEmail = delegateEmailParam ?? delegateEmail; + const delegateAccountDetails = effectiveDelegateEmail ? getPersonalDetailByEmail(effectiveDelegateEmail) : undefined; return { actionName, @@ -11904,7 +11907,8 @@ function prepareOnboardingOnyxData({ } const completedTaskReportAction = isTaskAutoCompleted - ? buildOptimisticTaskReportAction(currentTask.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, 'marked as complete', actorAccountID, 2) + ? // Will be refactored in next PR; full restructure tracked in https://github.com/Expensify/App/issues/66417 + buildOptimisticTaskReportAction(currentTask.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, undefined, 'marked as complete', actorAccountID, 2) : null; if (task.type === CONST.ONBOARDING_TASK_TYPE.CREATE_WORKSPACE) { createWorkspaceTaskReportID = currentTask.reportID; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 9da1f31fed5be..2d93d02e3b827 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2923,7 +2923,8 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData | undefined, + delegateEmail: string | undefined, ): { optimisticData: Array>; failureData: Array>; @@ -393,7 +394,7 @@ function buildTaskData( parameters: CompleteTaskParams; } { const message = `marked as complete`; - const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, message); + const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, delegateEmail, message); const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -504,6 +505,7 @@ function completeTask( hasOutstandingChildTaskInParentReport: boolean, hasOutstandingChildTask: boolean, parentReportAction: OnyxEntry | undefined, + delegateEmail: string | undefined, reportIDFromAction?: string, ): OnyxData { const taskReportID = taskReport?.reportID ?? reportIDFromAction; @@ -518,6 +520,7 @@ function completeTask( hasOutstandingChildTaskInParentReport, hasOutstandingChildTask, parentReportAction, + delegateEmail, ); playSound(SOUNDS.SUCCESS); @@ -528,13 +531,19 @@ function completeTask( /** * Reopen a closed task */ -function reopenTask(taskReport: OnyxEntry, parentReport: OnyxEntry, currentUserAccountID: number, reportIDFromAction?: string) { +function reopenTask( + taskReport: OnyxEntry, + parentReport: OnyxEntry, + currentUserAccountID: number, + delegateEmail: string | undefined, + reportIDFromAction?: string, +) { const taskReportID = taskReport?.reportID ?? reportIDFromAction; if (!taskReportID) { return; } const message = `marked as incomplete`; - const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED, message); + const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED, delegateEmail, message); const hasOutstandingChildTask = taskReport?.managerID === currentUserAccountID ? true : parentReport?.hasOutstandingChildTask; const optimisticData: Array> = [ @@ -1173,13 +1182,14 @@ function deleteTask( hasOutstandingChildTask: boolean, parentReportAction: OnyxEntry, conciergeReportID: string | undefined, + delegateEmail: string | undefined, ancestors: ReportUtils.Ancestor[] = [], ) { if (!report) { return; } const message = `deleted task: ${report.reportName}`; - const optimisticCancelReportAction = ReportUtils.buildOptimisticTaskReportAction(report.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_CANCELLED, message); + const optimisticCancelReportAction = ReportUtils.buildOptimisticTaskReportAction(report.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_CANCELLED, delegateEmail, message); const optimisticReportActionID = optimisticCancelReportAction.reportActionID; const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report, isReportArchived); @@ -1437,7 +1447,8 @@ function getFinishOnboardingTaskOnyxData( if (taskReport && canActionTask(taskReport, parentReportAction, currentUserAccountID, taskParentReport, isParentReportArchived)) { if (taskReport) { if (taskReport.stateNum !== CONST.REPORT.STATE_NUM.APPROVED || taskReport.statusNum !== CONST.REPORT.STATUS_NUM.APPROVED) { - return completeTask(taskReport, taskParentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction); + // Will be refactored in next PR; full restructure tracked in https://github.com/Expensify/App/issues/66417 + return completeTask(taskReport, taskParentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, undefined); } } } diff --git a/src/libs/actions/Workflow.ts b/src/libs/actions/Workflow.ts index 7100b98ddeb31..a650c73326482 100644 --- a/src/libs/actions/Workflow.ts +++ b/src/libs/actions/Workflow.ts @@ -92,7 +92,8 @@ function createApprovalWorkflow({approvalWorkflow, policy, addExpenseApprovalsTa addExpenseApprovalsTaskReport && (addExpenseApprovalsTaskReport.stateNum !== CONST.REPORT.STATE_NUM.APPROVED || addExpenseApprovalsTaskReport.statusNum !== CONST.REPORT.STATUS_NUM.APPROVED) ) { - completeTask(addExpenseApprovalsTaskReport, false, false, undefined); + // Will be refactored in next PR; full restructure tracked in https://github.com/Expensify/App/issues/66417 + completeTask(addExpenseApprovalsTaskReport, false, false, undefined, undefined); } } diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e64a853fef2c5..6ef95fb455171 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,4 +1,5 @@ import {StackActions} from '@react-navigation/native'; +import {delegateEmailSelector} from '@selectors/Account'; import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import React, {useCallback, useEffect, useMemo} from 'react'; import {InteractionManager, View} from 'react-native'; @@ -197,6 +198,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const [betas] = useOnyx(ONYXKEYS.BETAS); const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {showConfirmModal} = useConfirmModal(); const isPolicyAdmin = useMemo(() => isPolicyAdminUtil(policy), [policy]); @@ -541,7 +543,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail isAnonymousAction: false, action: callFunctionIfActionIsAllowed(() => { Navigation.goBack(backTo); - reopenTask(report, parentReport, currentUserPersonalDetails?.accountID); + reopenTask(report, parentReport, currentUserPersonalDetails?.accountID, delegateEmail); }), }); } @@ -642,6 +644,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail amountOwed, ownerBillingGracePeriodEnd, iouTransaction, + delegateEmail, ]); const displayNamesWithTooltips = useMemo(() => { @@ -890,7 +893,17 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const deleteTransaction = useCallback(() => { if (caseID === CASES.DEFAULT) { - deleteTask(report, parentReport, isReportArchived, currentUserPersonalDetails.accountID, hasOutstandingChildTask, parentReportAction, conciergeReportID, ancestors); + deleteTask( + report, + parentReport, + isReportArchived, + currentUserPersonalDetails.accountID, + hasOutstandingChildTask, + parentReportAction, + conciergeReportID, + delegateEmail, + ancestors, + ); return; } @@ -943,6 +956,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail deleteTransactions, removeTransaction, conciergeReportID, + delegateEmail, ]); // Where to navigate back to after deleting the transaction and its report. diff --git a/tests/actions/TaskTest.ts b/tests/actions/TaskTest.ts index b8ecf58e965a2..e5d455b0b811f 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -768,6 +768,9 @@ describe('actions/Task', () => { const mockTaskReportID = 'task_report_456'; const mockParentReportID = 'parent_report_789'; const mockParentReportActionID = 'parent_action_123'; + const DELEGATE_EMAIL = 'delegate@example.com'; + const DELEGATE_ACCOUNT_ID = 999; + const CURRENT_USER_ACCOUNT_ID = 123; beforeEach(async () => { jest.clearAllMocks(); @@ -779,7 +782,14 @@ describe('actions/Task', () => { await Onyx.clear(); await Onyx.set(ONYXKEYS.SESSION, { email: 'user@example.com', - accountID: 123, + accountID: CURRENT_USER_ACCOUNT_ID, + }); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [DELEGATE_ACCOUNT_ID]: { + accountID: DELEGATE_ACCOUNT_ID, + login: DELEGATE_EMAIL, + displayName: 'Delegate User', + }, }); }); await waitForBatchedUpdatesWithAct(); @@ -823,7 +833,7 @@ describe('actions/Task', () => { await waitForBatchedUpdatesWithAct(); // When: Call completeTask - completeTask(taskReport, false, false, parentReportAction); + completeTask(taskReport, false, false, parentReportAction, undefined); await waitForBatchedUpdatesWithAct(); @@ -889,7 +899,7 @@ describe('actions/Task', () => { await waitForBatchedUpdatesWithAct(); // When: Call completeTask - completeTask(taskReport, false, false, undefined); + completeTask(taskReport, false, false, undefined, undefined); await waitForBatchedUpdatesWithAct(); @@ -916,6 +926,54 @@ describe('actions/Task', () => { expect(parentReportActionUpdate).toBeUndefined(); }); + + it('should include delegateAccountID in optimistic report action when delegateEmail is provided', () => { + const reportID = 'task_report_complete_delegate_1'; + const taskReport = { + reportID, + type: CONST.REPORT.TYPE.TASK, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + }; + + completeTask(taskReport, false, false, undefined, DELEGATE_EMAIL); + + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting mock call args to verify optimistic data structure + const calls = (API.write as jest.Mock).mock.calls; + const [, , onyxData] = calls.at(0) as [unknown, unknown, OnyxData]; + const optimisticData = onyxData.optimisticData ?? []; + + const reportActionsUpdate = optimisticData.find((update: {key: string}) => update.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + expect(reportActionsUpdate).toBeDefined(); + + const reportAction = Object.values(reportActionsUpdate?.value as Record).at(0); + expect(reportAction?.delegateAccountID).toBe(DELEGATE_ACCOUNT_ID); + }); + + it('should set delegateAccountID to undefined when delegateEmail is undefined', () => { + const reportID = 'task_report_complete_delegate_2'; + const taskReport = { + reportID, + type: CONST.REPORT.TYPE.TASK, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + }; + + completeTask(taskReport, false, false, undefined, undefined); + + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting mock call args to verify optimistic data structure + const calls = (API.write as jest.Mock).mock.calls; + const [, , onyxData] = calls.at(0) as [unknown, unknown, OnyxData]; + const optimisticData = onyxData.optimisticData ?? []; + + const reportActionsUpdate = optimisticData.find((update: {key: string}) => update.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + expect(reportActionsUpdate).toBeDefined(); + + const reportAction = Object.values(reportActionsUpdate?.value as Record).at(0); + expect(reportAction?.delegateAccountID).toBeUndefined(); + }); }); describe('getNavigationUrlOnTaskDelete', () => { @@ -1159,6 +1217,8 @@ describe('actions/Task', () => { let doesReportHaveVisibleActionsSpy: jest.SpyInstance; let getMostRecentReportIDSpy: jest.SpyInstance; const mockCurrentUserAccountID = 123; + const DELEGATE_EMAIL = 'delegate@example.com'; + const DELEGATE_ACCOUNT_ID = 999; beforeEach(async () => { jest.clearAllMocks(); @@ -1175,6 +1235,13 @@ describe('actions/Task', () => { email: 'user@example.com', accountID: mockCurrentUserAccountID, }); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [DELEGATE_ACCOUNT_ID]: { + accountID: DELEGATE_ACCOUNT_ID, + login: DELEGATE_EMAIL, + displayName: 'Delegate User', + }, + }); }); await waitForBatchedUpdatesWithAct(); }); @@ -1185,7 +1252,7 @@ describe('actions/Task', () => { }); it('should return early when report is undefined', () => { - deleteTask(undefined, undefined, false, mockCurrentUserAccountID, false, undefined, 'concierge_123'); + deleteTask(undefined, undefined, false, mockCurrentUserAccountID, false, undefined, 'concierge_123', undefined); // eslint-disable-next-line rulesdir/no-multiple-api-calls expect(API.write).not.toHaveBeenCalled(); @@ -1226,7 +1293,7 @@ describe('actions/Task', () => { doesReportHaveVisibleActionsSpy.mockReturnValue(true); - deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, parentReportAction, 'concierge_123'); + deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, parentReportAction, 'concierge_123', undefined); // eslint-disable-next-line rulesdir/no-multiple-api-calls expect(API.write).toHaveBeenCalledWith( @@ -1270,7 +1337,7 @@ describe('actions/Task', () => { // No visible actions means the task report should be deleted doesReportHaveVisibleActionsSpy.mockReturnValue(false); - const result = deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, undefined, 'concierge_123'); + const result = deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, undefined, 'concierge_123', undefined); expect(result).toBe(`r/${parentReportID}`); expect(Navigation.goBack).toHaveBeenCalled(); @@ -1298,7 +1365,7 @@ describe('actions/Task', () => { doesReportHaveVisibleActionsSpy.mockReturnValue(false); getMostRecentReportIDSpy.mockReturnValue(conciergeReportID); - const result = deleteTask(taskReport, undefined, false, mockCurrentUserAccountID, false, undefined, conciergeReportID); + const result = deleteTask(taskReport, undefined, false, mockCurrentUserAccountID, false, undefined, conciergeReportID, undefined); expect(result).toBe(`r/${conciergeReportID}`); expect(getMostRecentReportIDSpy).toHaveBeenCalledWith(taskReport, conciergeReportID); @@ -1333,7 +1400,7 @@ describe('actions/Task', () => { // Has visible actions, so should not navigate away doesReportHaveVisibleActionsSpy.mockReturnValue(true); - const result = deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, undefined, 'concierge_123'); + const result = deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, undefined, 'concierge_123', undefined); expect(result).toBeUndefined(); expect(Navigation.goBack).not.toHaveBeenCalled(); @@ -1360,7 +1427,7 @@ describe('actions/Task', () => { doesReportHaveVisibleActionsSpy.mockReturnValue(false); getMostRecentReportIDSpy.mockReturnValue(undefined); - const result = deleteTask(taskReport, undefined, false, mockCurrentUserAccountID, false, undefined, undefined); + const result = deleteTask(taskReport, undefined, false, mockCurrentUserAccountID, false, undefined, undefined, undefined); // API.write should still be called // eslint-disable-next-line rulesdir/no-multiple-api-calls @@ -1370,5 +1437,67 @@ describe('actions/Task', () => { expect(result).toBeUndefined(); expect(Navigation.goBack).not.toHaveBeenCalled(); }); + + it('should include delegateAccountID in optimistic report action when delegateEmail is provided', async () => { + const reportID = 'task_report_delete_delegate_1'; + const taskReport = { + reportID, + type: CONST.REPORT.TYPE.TASK, + reportName: 'Delegate Delete Task', + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + ownerAccountID: mockCurrentUserAccountID, + }; + + await act(async () => { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, taskReport); + }); + await waitForBatchedUpdatesWithAct(); + + deleteTask(taskReport, undefined, false, mockCurrentUserAccountID, false, undefined, undefined, DELEGATE_EMAIL); + + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting mock call args to verify optimistic data structure + const calls = (API.write as jest.Mock).mock.calls; + const [, , onyxData] = calls.at(0) as [unknown, unknown, OnyxData]; + const optimisticData = onyxData.optimisticData ?? []; + + const reportActionsUpdate = optimisticData.find((update: {key: string}) => update.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + expect(reportActionsUpdate).toBeDefined(); + + const reportActions = reportActionsUpdate?.value as Record; + const cancelAction = Object.values(reportActions).find((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.TASK_CANCELLED); + expect(cancelAction?.delegateAccountID).toBe(DELEGATE_ACCOUNT_ID); + }); + + it('should set delegateAccountID to undefined when delegateEmail is undefined', async () => { + const reportID = 'task_report_delete_delegate_2'; + const taskReport = { + reportID, + type: CONST.REPORT.TYPE.TASK, + reportName: 'No Delegate Delete Task', + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + ownerAccountID: mockCurrentUserAccountID, + }; + + await act(async () => { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, taskReport); + }); + await waitForBatchedUpdatesWithAct(); + + deleteTask(taskReport, undefined, false, mockCurrentUserAccountID, false, undefined, undefined, undefined); + + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting mock call args to verify optimistic data structure + const calls = (API.write as jest.Mock).mock.calls; + const [, , onyxData] = calls.at(0) as [unknown, unknown, OnyxData]; + const optimisticData = onyxData.optimisticData ?? []; + + const reportActionsUpdate = optimisticData.find((update: {key: string}) => update.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + expect(reportActionsUpdate).toBeDefined(); + + const reportActions = reportActionsUpdate?.value as Record; + const cancelAction = Object.values(reportActions).find((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.TASK_CANCELLED); + expect(cancelAction?.delegateAccountID).toBeUndefined(); + }); }); }); diff --git a/tests/actions/WorkflowTest.ts b/tests/actions/WorkflowTest.ts index 68f82f04304f9..5fff4468b2eb1 100644 --- a/tests/actions/WorkflowTest.ts +++ b/tests/actions/WorkflowTest.ts @@ -308,7 +308,7 @@ describe('actions/Workflow', () => { await mockFetch.resume(); await waitForBatchedUpdates(); - expect(completeTaskMock).toHaveBeenCalledWith(addExpenseApprovalsTaskReport, false, false, undefined); + expect(completeTaskMock).toHaveBeenCalledWith(addExpenseApprovalsTaskReport, false, false, undefined, undefined); }); it('should not auto-complete the task if it is already approved', async () => {