Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 12 additions & 56 deletions src/pages/inbox/report/PureReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
import {getBankAccountLastFourDigits} from '@libs/PaymentUtils';
import Permissions from '@libs/Permissions';
import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import {getCleanedTagName, hasDynamicExternalWorkflow, isPolicyAdmin, isPolicyMember, isPolicyOwner} from '@libs/PolicyUtils';
import {getCleanedTagName, isPolicyAdmin, isPolicyMember, isPolicyOwner} from '@libs/PolicyUtils';
import {containsActionableFollowUps, parseFollowupsFromHtml} from '@libs/ReportActionFollowupUtils';
import {
extractLinksFromMessageHtml,
Expand Down Expand Up @@ -170,8 +170,6 @@ import {
getWorkspaceTagUpdateMessage,
getWorkspaceTaxUpdateMessage,
getWorkspaceUpdateFieldMessage,
hasPendingDEWApprove,
hasPendingDEWSubmit,
isActionableAddPaymentCard,
isActionableCardFraudAlert,
isActionableJoinRequest,
Expand All @@ -188,7 +186,6 @@ import {
isDeletedAction,
isDeletedParentAction as isDeletedParentActionUtils,
isIOURequestReportAction,
isMarkAsClosedAction,
isMessageDeleted,
isMoneyRequestAction,
isPendingRemove,
Expand Down Expand Up @@ -250,6 +247,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage';
import {isEmptyObject, isEmptyValueObject} from '@src/types/utils/EmptyObject';
import ApprovalFlowContent, {isApprovalFlowAction} from './actionContents/ApprovalFlowContent';
import SimpleMessageContent, {isSimpleMessageAction} from './actionContents/SimpleMessageContent';
import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions';
import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu';
Expand Down Expand Up @@ -1364,47 +1362,16 @@ function PureReportActionItem({
originalReport={originalReport}
/>
);
} else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) || isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED) || isMarkAsClosedAction(action)) {
const wasSubmittedViaHarvesting = !isMarkAsClosedAction(action) ? (getOriginalMessage(action)?.harvesting ?? false) : false;
const isDEWPolicy = hasDynamicExternalWorkflow(policy);

const isPendingAdd = action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
if (wasSubmittedViaHarvesting) {
children = (
<ReportActionItemMessageWithExplain
message={translate('iou.automaticallySubmitted')}
action={action}
childReport={childReport}
originalReport={originalReport}
/>
);
} else if (hasPendingDEWSubmit(reportMetadata, isDEWPolicy) && isPendingAdd) {
children = <ReportActionItemBasicMessage message={translate('iou.queuedToSubmitViaDEW')} />;
} else if (isDEWPolicy) {
// Don't show a memo for DEW actions, it's shown in the Concierge action below
children = <ReportActionItemBasicMessage message={translate('iou.submitted')} />;
} else {
children = <ReportActionItemBasicMessage message={translate('iou.submitted', getOriginalMessage(action)?.message)} />;
}
} else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.APPROVED)) {
const wasAutoApproved = getOriginalMessage(action)?.automaticAction ?? false;
const isDEWPolicy = hasDynamicExternalWorkflow(policy);
const isPendingAdd = action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;

if (wasAutoApproved) {
children = (
<ReportActionItemMessageWithExplain
message={translate('iou.automaticallyApproved')}
action={action}
childReport={childReport}
originalReport={originalReport}
/>
);
} else if (hasPendingDEWApprove(reportMetadata, isDEWPolicy) && isPendingAdd) {
children = <ReportActionItemBasicMessage message={translate('iou.queuedToApproveViaDEW')} />;
} else {
children = <ReportActionItemBasicMessage message={translate('iou.approvedMessage')} />;
}
} else if (isApprovalFlowAction(action)) {
children = (
<ApprovalFlowContent
action={action}
policy={policy}
reportMetadata={reportMetadata}
childReport={childReport}
originalReport={originalReport}
/>
);
} else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) {
const wasAutoPaid = getOriginalMessage(action)?.automaticAction ?? false;
const paymentType = getOriginalMessage(action)?.paymentType;
Expand Down Expand Up @@ -1453,17 +1420,6 @@ function PureReportActionItem({
children = <ReportActionItemBasicMessage message={getReimbursedMessage(translate, action, report, currentUserAccountID)} />;
} else if (isSimpleMessageAction(action)) {
children = <SimpleMessageContent action={action} />;
} else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) {
const wasAutoForwarded = getOriginalMessage(action)?.automaticAction ?? false;
if (wasAutoForwarded) {
children = (
<ReportActionItemBasicMessage>
<RenderHTML html={`<comment><muted-text>${translate('iou.automaticallyForwarded')}</muted-text></comment>`} />
</ReportActionItemBasicMessage>
);
} else {
children = <ReportActionItemBasicMessage message={translate('iou.forwarded')} />;
}
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_UPGRADE) {
children = <ReportActionItemBasicMessage message={translate('workspaceActions.upgradedWorkspace')} />;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_FORCE_UPGRADE) {
Expand Down
102 changes: 102 additions & 0 deletions src/pages/inbox/report/actionContents/ApprovalFlowContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import RenderHTML from '@components/RenderHTML';
import useLocalize from '@hooks/useLocalize';
import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils';
import {getOriginalMessage, hasPendingDEWApprove, hasPendingDEWSubmit, isActionOfType, isMarkAsClosedAction} from '@libs/ReportActionsUtils';
import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage';
import ReportActionItemMessageWithExplain from '@pages/inbox/report/ReportActionItemMessageWithExplain';
import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';

type ApprovalFlowContentProps = {
action: OnyxTypes.ReportAction;
policy: OnyxEntry<OnyxTypes.Policy>;
reportMetadata: OnyxEntry<OnyxTypes.ReportMetadata>;
childReport: OnyxEntry<OnyxTypes.Report>;
originalReport: OnyxEntry<OnyxTypes.Report>;
};

function isApprovalFlowAction(action: OnyxTypes.ReportAction): boolean {
return (
isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) ||
isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED) ||
isMarkAsClosedAction(action) ||
isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.APPROVED) ||
isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.FORWARDED)
);
}

function ApprovalFlowContent({action, policy, reportMetadata, childReport, originalReport}: ApprovalFlowContentProps) {
const {translate} = useLocalize();
const isDEWPolicy = hasDynamicExternalWorkflow(policy);
const isPendingAdd = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;

if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) || isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED) || isMarkAsClosedAction(action)) {
const wasSubmittedViaHarvesting = !isMarkAsClosedAction(action) ? (getOriginalMessage(action)?.harvesting ?? false) : false;

if (wasSubmittedViaHarvesting) {
return (
<ReportActionItemMessageWithExplain
message={translate('iou.automaticallySubmitted')}
action={action}
childReport={childReport}
originalReport={originalReport}
/>
);
}

if (hasPendingDEWSubmit(reportMetadata, isDEWPolicy) && isPendingAdd) {
return <ReportActionItemBasicMessage message={translate('iou.queuedToSubmitViaDEW')} />;
}

if (isDEWPolicy) {
// Don't show a memo for DEW actions, it's shown in the Concierge action below
return <ReportActionItemBasicMessage message={translate('iou.submitted')} />;
}

return <ReportActionItemBasicMessage message={translate('iou.submitted', getOriginalMessage(action)?.message)} />;
}

if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.APPROVED)) {
const wasAutoApproved = getOriginalMessage(action)?.automaticAction ?? false;

if (wasAutoApproved) {
return (
<ReportActionItemMessageWithExplain
message={translate('iou.automaticallyApproved')}
action={action}
childReport={childReport}
originalReport={originalReport}
/>
);
}

if (hasPendingDEWApprove(reportMetadata, isDEWPolicy) && isPendingAdd) {
return <ReportActionItemBasicMessage message={translate('iou.queuedToApproveViaDEW')} />;
}

return <ReportActionItemBasicMessage message={translate('iou.approvedMessage')} />;
}

if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) {
const wasAutoForwarded = getOriginalMessage(action)?.automaticAction ?? false;

if (wasAutoForwarded) {
return (
<ReportActionItemBasicMessage>
<RenderHTML html={`<comment><muted-text>${translate('iou.automaticallyForwarded')}</muted-text></comment>`} />
</ReportActionItemBasicMessage>
);
}

return <ReportActionItemBasicMessage message={translate('iou.forwarded')} />;
}

return null;
}

ApprovalFlowContent.displayName = 'ApprovalFlowContent';

export default ApprovalFlowContent;
export {isApprovalFlowAction};
124 changes: 124 additions & 0 deletions tests/ui/PureReportActionItemTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,130 @@ describe('PureReportActionItem', () => {
expect(screen.getByText(translateLocal('iou.submitted'))).toBeOnTheScreen();
expect(screen.queryByText(translateLocal('iou.queuedToSubmitViaDEW'))).not.toBeOnTheScreen();
});

it('should display DEW queued message for pending APPROVED action when policy has DEW enabled', async () => {
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.APPROVED, {automaticAction: false});
action.pendingAction = CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;

const dewPolicy = {
id: 'testPolicy',
name: 'Test DEW Policy',
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.ADMIN,
owner: 'owner@test.com',
outputCurrency: CONST.CURRENCY.USD,
isPolicyExpenseChatEnabled: true,
approvalMode: CONST.POLICY.APPROVAL_MODE.DYNAMICEXTERNAL,
} as const;

const reportMetadata = {
pendingExpenseAction: CONST.EXPENSE_PENDING_ACTION.APPROVE,
};

await act(async () => {
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}testPolicy`, dewPolicy);
});
await waitForBatchedUpdatesWithAct();

render(
<ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, HTMLEngineProvider]}>
<OptionsListContextProvider>
<ScreenWrapper testID="test">
<PortalProvider>
<PureReportActionItem
personalPolicyID={undefined}
currentUserEmail={undefined}
policy={dewPolicy as Policy}
report={{reportID: 'testReport', policyID: 'testPolicy'}}
parentReportAction={undefined}
action={action}
displayAsGroup={false}
shouldDisplayNewMarker={false}
index={0}
isFirstVisibleReportAction={false}
taskReport={undefined}
linkedReport={undefined}
iouReportOfLinkedReport={undefined}
reportMetadata={reportMetadata}
currentUserAccountID={ACTOR_ACCOUNT_ID}
betas={undefined}
draftTransactionIDs={[]}
userBillingGracePeriodEnds={undefined}
/>
</PortalProvider>
</ScreenWrapper>
</OptionsListContextProvider>
</ComposeProviders>,
);
await waitForBatchedUpdatesWithAct();

expect(screen.getByText(actorEmail)).toBeOnTheScreen();
expect(screen.getByText(translateLocal('iou.queuedToApproveViaDEW'))).toBeOnTheScreen();
});

it('should display submitted without memo for SUBMITTED action on DEW policy that is not pending', async () => {
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: false, message: 'my memo'});

const dewPolicy = {
id: 'testPolicy',
name: 'Test DEW Policy',
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.ADMIN,
owner: 'owner@test.com',
outputCurrency: CONST.CURRENCY.USD,
isPolicyExpenseChatEnabled: true,
approvalMode: CONST.POLICY.APPROVAL_MODE.DYNAMICEXTERNAL,
} as const;

await act(async () => {
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}testPolicy`, dewPolicy);
});
await waitForBatchedUpdatesWithAct();

render(
<ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider, HTMLEngineProvider]}>
<OptionsListContextProvider>
<ScreenWrapper testID="test">
<PortalProvider>
<PureReportActionItem
personalPolicyID={undefined}
currentUserEmail={undefined}
policy={dewPolicy as Policy}
report={{reportID: 'testReport', policyID: 'testPolicy'}}
parentReportAction={undefined}
action={action}
displayAsGroup={false}
shouldDisplayNewMarker={false}
index={0}
isFirstVisibleReportAction={false}
taskReport={undefined}
linkedReport={undefined}
iouReportOfLinkedReport={undefined}
currentUserAccountID={ACTOR_ACCOUNT_ID}
betas={undefined}
draftTransactionIDs={[]}
userBillingGracePeriodEnds={undefined}
/>
</PortalProvider>
</ScreenWrapper>
</OptionsListContextProvider>
</ComposeProviders>,
);
await waitForBatchedUpdatesWithAct();

expect(screen.getByText(actorEmail)).toBeOnTheScreen();
// DEW policy should show submitted without the memo (memo is shown in the Concierge action)
expect(screen.getByText(translateLocal('iou.submitted'))).toBeOnTheScreen();
expect(screen.queryByText('my memo')).not.toBeOnTheScreen();
});

it('CLOSED action with amount (MARK_AS_CLOSED) renders submitted message', async () => {
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.CLOSED, {amount: 5000});
renderItemWithAction(action);
await waitForBatchedUpdatesWithAct();

expect(screen.getByText(translateLocal('iou.submitted'))).toBeOnTheScreen();
});
});

describe('Followup list buttons', () => {
Expand Down
Loading