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
4 changes: 2 additions & 2 deletions src/components/Search/SearchWriteActionsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ function useReconcileSelectionWithData({
const liveSelectionEntry: SelectedTransactionInfo = {
...baseEntry,
isSelected: !isExcluded && (areAllMatchingItemsSelected || !!previousSelection?.isSelected || propagateSelectionToAllRows),
canReject: transactionItem.report ? canRejectReportAction(transactionItem.report, currentUserAccountID) : false,
canReject: transactionItem.report ? canRejectReportAction(transactionItem.report, currentUserAccountID, transactionItem.policy) : false,
policyID: transactionItem.report?.policyID,
groupKey: previousSelection?.groupKey ?? (propagateSelectionToAllRows && !isExpenseReportType ? reportKey : undefined),
isSelectedViaGroup: previousSelection?.isSelectedViaGroup,
Expand Down Expand Up @@ -253,7 +253,7 @@ function useReconcileSelectionWithData({
const liveSelectionEntry: SelectedTransactionInfo = {
...baseEntry,
isSelected: areAllMatchingItemsSelected || !!flatPreviousSelection?.isSelected,
canReject: transactionItem.report ? canRejectReportAction(transactionItem.report, currentUserAccountID) : false,
canReject: transactionItem.report ? canRejectReportAction(transactionItem.report, currentUserAccountID, transactionItem.policy) : false,
policyID: transactionItem.report?.policyID,
};
liveSelectionEntries.set(listKey, liveSelectionEntry);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/selectionBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function mapTransactionItemToSelectedEntry({
parentReport,
}: MapTransactionItemToSelectedEntryParams): [string, SelectedTransactionInfo] {
const {canHoldRequest, canUnholdRequest} = canHoldUnholdReportAction(item.report, item.reportAction, item.holdReportAction, item, item.policy, currentUserAccountID);
const canRejectRequest = item.report ? canRejectReportAction(item.report, currentUserAccountID) : false;
const canRejectRequest = item.report ? canRejectReportAction(item.report, currentUserAccountID, item.policy) : false;
const amount = hasValidModifiedAmount(item) ? Number(item.modifiedAmount) : item.amount;
const isUnreported = isExpenseUnreported(item);
const reportForSplit = item.report ?? (isUnreported ? selfDMReport : undefined);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ function useSelectedTransactionsActions({

const hasNoRejectedTransaction = selectedTransactionIDs.every((id) => !hasTransactionBeenRejected(allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + id] ?? []));
const canRejectTransactions =
selectedTransactionsList.length > 0 && isMoneyRequestReport && !!report && canRejectReportAction(report, session?.accountID) && hasNoRejectedTransaction;
selectedTransactionsList.length > 0 && isMoneyRequestReport && !!report && canRejectReportAction(report, session?.accountID, policy) && hasNoRejectedTransaction;
if (canRejectTransactions) {
options.push({
text: translate('search.bulkActions.reject'),
Expand Down
10 changes: 10 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ function isArchivedPolicy(policy: OnyxInputOrEntry<Policy>): boolean {
return !!policy?.archivedDate;
}

/**
* Whether the policy is archived or is optimistically pending deletion. Deleting a workspace
* archives it on the backend, but the optimistic data only sets pendingAction, so report state
* transitions must also treat a pending delete as archived while the request is in flight.
*/
function isArchivedOrPendingDeletePolicy(policy: OnyxInputOrEntry<Policy>): boolean {
return isArchivedPolicy(policy) || policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}

/**
* Filter out the active policies, which will exclude policies with pending deletion
* and policies the current user doesn't belong to.
Expand Down Expand Up @@ -3215,6 +3224,7 @@ export {
arePolicyRulesEnabled,
isPolicyFeatureEnabled,
isPolicyFieldListEmpty,
isArchivedOrPendingDeletePolicy,
isArchivedPolicy,
getUberConnectionErrorDirectlyFromPolicy,
isPolicyOwner,
Expand Down
18 changes: 13 additions & 5 deletions src/libs/ReportPreviewActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getValidConnectedIntegration,
hasDynamicExternalWorkflow,
hasIntegrationAutoSync,
isArchivedOrPendingDeletePolicy,
isGroupPolicy,
isPreferredExporter,
isSubmitterApproveBlockedOnSubmitWorkspace,
Expand All @@ -29,6 +30,7 @@ import {
isInvoiceReport,
isIOUReport,
isOpenReport,
isPayBlockedByArchivedState,
isPayer,
isProcessingReport,
isReportApproved,
Expand All @@ -38,15 +40,16 @@ import {hasOnlyPendingCardTransactions, hasSmartScanFailedWithMissingFields, has

function canSubmit(
report: Report,
isReportArchived: boolean,
currentUserAccountID: number,
currentUserEmail: string,
ownerLogin: string | undefined,
violations?: OnyxCollection<TransactionViolation[]>,
policy?: Policy,
transactions?: Transaction[],
) {
if (isReportArchived) {
// State transitions are blocked only on archived or pending-delete policies. Reports archived for other reasons
// (e.g. the submitter was unshared from the policy) can still move through the workflow.
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

Expand Down Expand Up @@ -78,6 +81,10 @@ function canSubmit(
}

function canApprove(report: Report, currentUserAccountID: number, reportMetadata: OnyxEntry<ReportMetadata>, policy?: Policy, transactions?: Transaction[]) {
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

if (isSubmitterApproveBlockedOnSubmitWorkspace(policy, report.ownerAccountID, currentUserAccountID)) {
return false;
}
Expand Down Expand Up @@ -123,7 +130,9 @@ function canPay(
policy?: Policy,
invoiceReceiverPolicy?: Policy,
) {
if (isReportArchived) {
const isExpense = isExpenseReport(report);

if (isPayBlockedByArchivedState(report, policy, isReportArchived)) {
return false;
}

Expand All @@ -135,7 +144,6 @@ function canPay(
(isGroupPolicy(policy) &&
policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL &&
canMemberWrite(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_PAYMENTS));
const isExpense = isExpenseReport(report);
const isPaymentsEnabled = arePaymentsEnabled(policy);
const isProcessing = isProcessingReport(report);
const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false;
Expand Down Expand Up @@ -268,7 +276,7 @@ function getReportPreviewAction({
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW;
}

if (canSubmit(report, isReportArchived, currentUserAccountID, currentUserLogin, ownerLogin, violationsData, policy, transactions)) {
if (canSubmit(report, currentUserAccountID, currentUserLogin, ownerLogin, violationsData, policy, transactions)) {
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT;
}
if (canApprove(report, currentUserAccountID, reportMetadata, policy, transactions)) {
Expand Down
18 changes: 13 additions & 5 deletions src/libs/ReportPrimaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getValidConnectedIntegration,
hasDynamicExternalWorkflow,
hasIntegrationAutoSync,
isArchivedOrPendingDeletePolicy,
isGroupPolicy,
isPaidGroupPolicy,
isPolicyAdmin as isPolicyAdminPolicyUtils,
Expand Down Expand Up @@ -48,6 +49,7 @@ import {
isInvoiceReport as isInvoiceReportUtils,
isIOUReport as isIOUReportUtils,
isOpenReport as isOpenReportUtils,
isPayBlockedByArchivedState,
isPayer,
isProcessingReport as isProcessingReportUtils,
isReportApproved as isReportApprovedUtils,
Expand Down Expand Up @@ -118,12 +120,13 @@ function isSubmitAction(
reportMetadata: OnyxEntry<ReportMetadata>,
ownerLogin: string | undefined,
policy?: Policy,
reportNameValuePairs?: ReportNameValuePairs,
violations?: OnyxCollection<TransactionViolation[]>,
currentUserEmail?: string,
currentUserAccountID?: number,
) {
if (isArchivedReport(reportNameValuePairs)) {
// State transitions are blocked only on archived or pending-delete policies. Reports archived for other reasons
// (e.g. the submitter was unshared from the policy) can still move through the workflow.
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

Expand Down Expand Up @@ -161,6 +164,10 @@ function isSubmitAction(
}

function isApproveAction(report: Report, reportTransactions: Transaction[], currentUserAccountID: number, reportMetadata: OnyxEntry<ReportMetadata>, policy?: Policy) {
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

if (isSubmitterApproveBlockedOnSubmitWorkspace(policy, report.ownerAccountID, currentUserAccountID)) {
return false;
}
Expand Down Expand Up @@ -214,10 +221,11 @@ function isPrimaryPayAction({
isSecondaryAction,
canNonPayerAdminPay,
}: IsPrimaryPayActionParams) {
if (isArchivedReport(reportNameValuePairs) || isChatReportArchived) {
const isExpenseReport = isExpenseReportUtils(report);

if (isPayBlockedByArchivedState(report, policy, isArchivedReport(reportNameValuePairs) || !!isChatReportArchived)) {
return false;
}
const isExpenseReport = isExpenseReportUtils(report);
if (isExpenseReport && !isPaidGroupPolicy(policy)) {
return false;
}
Expand Down Expand Up @@ -539,7 +547,7 @@ function getReportPrimaryAction(params: GetReportPrimaryActionParams): ValueOf<t

if (
isCurrentUserSubmitter(report, currentUserAccountID) &&
isSubmitAction(report, reportTransactions, reportMetadata, ownerLogin, policy, reportNameValuePairs, violations, currentUserLogin, currentUserAccountID) &&
isSubmitAction(report, reportTransactions, reportMetadata, ownerLogin, policy, violations, currentUserLogin, currentUserAccountID) &&
!allExpensesHeld
) {
return CONST.REPORT.PRIMARY_ACTIONS.SUBMIT;
Expand Down
35 changes: 26 additions & 9 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getSubmitToAccountID,
getValidConnectedIntegration,
hasDynamicExternalWorkflow,
isArchivedOrPendingDeletePolicy,
isGroupPolicy,
isInstantSubmitEnabled,
isPolicyAdmin,
Expand Down Expand Up @@ -192,10 +193,8 @@ function isSubmitAction({
report,
reportTransactions,
policy,
reportNameValuePairs,
reportActions,
reportMetadata,
isChatReportArchived = false,
primaryAction,
violations,
currentUserLogin,
Expand All @@ -205,17 +204,17 @@ function isSubmitAction({
report: Report;
reportTransactions: Transaction[];
policy?: Policy;
reportNameValuePairs?: ReportNameValuePairs;
reportActions?: ReportAction[];
reportMetadata?: OnyxEntry<ReportMetadata>;
isChatReportArchived?: boolean;
primaryAction?: ValueOf<typeof CONST.REPORT.PRIMARY_ACTIONS> | '';
violations?: OnyxCollection<TransactionViolation[]>;
currentUserLogin?: string;
currentUserAccountID: number;
ownerLogin: string | undefined;
}): boolean {
if (isArchivedReport(reportNameValuePairs) || isChatReportArchived) {
// State transitions are blocked only on archived or pending-delete policies. Reports archived for other reasons
// (e.g. the submitter was unshared from the policy) can still move through the workflow.
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

Expand Down Expand Up @@ -306,6 +305,10 @@ function isApproveAction(
reportMetadata: OnyxEntry<ReportMetadata>,
policy?: Policy,
): boolean {
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

if (isSubmitterApproveBlockedOnSubmitWorkspace(policy, report.ownerAccountID, currentUserAccountID)) {
return false;
}
Expand Down Expand Up @@ -373,6 +376,10 @@ function isApproveAction(
}

function isUnapproveAction(currentUserLogin: string, currentUserAccountID: number, report: Report, policy?: Policy): boolean {
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

const isExpenseReport = isExpenseReportUtils(report);
const isReportApprover = isPolicyApprover(policy, currentUserLogin);
const isReportApproved = isReportApprovedUtils({report});
Expand Down Expand Up @@ -429,6 +436,10 @@ function isCancelPaymentAction(
return false;
}

if (isExpenseReport && isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

const isPayer = isPayerUtils(currentAccountID, currentUserEmail, report, bankAccountList, policy, false);

// A P2P "send money" payment made with the Expensify wallet that is waiting for the receiver to set up their
Expand Down Expand Up @@ -724,6 +735,10 @@ function shouldShowEditSplitInDeleteAction(
}

function isRetractAction(report: Report, policy?: Policy): boolean {
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

const isExpenseReport = isExpenseReportUtils(report);

// This should be removed after we change how instant submit works
Expand All @@ -747,6 +762,10 @@ function isRetractAction(report: Report, policy?: Policy): boolean {
}

function isReopenAction(report: Report, policy?: Policy): boolean {
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

const isExpenseReport = isExpenseReportUtils(report);
if (!isExpenseReport) {
return false;
Expand Down Expand Up @@ -1025,10 +1044,8 @@ function getSecondaryReportActions({
report,
reportTransactions,
policy,
reportNameValuePairs,
reportActions,
reportMetadata,
isChatReportArchived,
primaryAction,
violations,
currentUserLogin,
Expand Down Expand Up @@ -1071,7 +1088,7 @@ function getSecondaryReportActions({
options.push(CONST.REPORT.SECONDARY_ACTIONS.REMOVE_HOLD);
}

if (canRejectReportAction(report, currentUserAccountID)) {
if (canRejectReportAction(report, currentUserAccountID, policy)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.REJECT);
}

Expand Down Expand Up @@ -1207,7 +1224,7 @@ function getSecondaryTransactionThreadActions({
options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REMOVE_HOLD);
}

if (canRejectReportAction(parentReport, currentUserAccountID)) {
if (canRejectReportAction(parentReport, currentUserAccountID, policy)) {
options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT);
}

Expand Down
18 changes: 17 additions & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import {
canMemberWrite as canMemberWritePolicyUtils,
hasDependentTags as hasDependentTagsPolicyUtils,
hasDynamicExternalWorkflow,
isArchivedOrPendingDeletePolicy,
isExpensifyTeam,
isGroupPolicyByType,
isGroupPolicy as isGroupPolicyPolicyUtils,
Expand Down Expand Up @@ -13634,7 +13635,7 @@ function getReportPersonalDetailsParticipants(report: Report, personalDetailsPar
};
}

function canRejectReportAction(report: Report, currentUserAccountID: number | undefined): boolean {
function canRejectReportAction(report: Report, currentUserAccountID: number | undefined, policy: OnyxEntry<Policy>): boolean {
const isReportBeingProcessed = isProcessingReport(report);
const isIOU = isIOUReport(report);
const isInvoice = isInvoiceReport(report);
Expand All @@ -13644,6 +13645,11 @@ function canRejectReportAction(report: Report, currentUserAccountID: number | un
return false;
}

// Rejecting changes the report state, which is blocked on archived or pending-delete policies.
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}

if (isIOU) {
return false; // Disable IOU
}
Expand All @@ -13659,6 +13665,15 @@ function canRejectReportAction(report: Report, currentUserAccountID: number | un
return false;
}

/**
* Whether Pay is blocked by archived state. Expense reports key on the policy archived or pending-delete state,
* so reports archived for other reasons (e.g. the submitter was unshared from the policy) can still be paid.
* IOU and invoice reports have no policy archived state, so the passed report/chat archived flag blocks instead.
*/
function isPayBlockedByArchivedState(report: OnyxInputOrEntry<Report>, policy: OnyxInputOrEntry<Policy>, isReportOrChatArchived: boolean): boolean {
return isExpenseReport(report) ? isArchivedOrPendingDeletePolicy(policy) : isReportOrChatArchived;
}

function hasReportBeenReopened(report: OnyxEntry<Report>, reportActions?: OnyxEntry<ReportActions> | ReportAction[]): boolean {
if (report?.hasReportBeenReopened !== undefined) {
return report.hasReportBeenReopened;
Expand Down Expand Up @@ -14611,6 +14626,7 @@ export {
pushTransactionAutoSelectionsOnyxData,
navigateOnDeleteExpense,
canRejectReportAction,
isPayBlockedByArchivedState,
hasReportBeenReopened,
hasReportBeenRetracted,
getNextApproverAccountID,
Expand Down
Loading
Loading