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
5 changes: 3 additions & 2 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const {isOffline} = useNetwork();
const isOnHold = isOnHoldTransactionUtils(transaction);

const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState<PaymentMethodType>();
const [requestType, setRequestType] = useState<ActionHandledType>();
Expand Down Expand Up @@ -577,8 +578,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
if (!moneyRequestReport) {
return [];
}
return getSecondaryReportActions(moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot, canUseTableReportView);
}, [moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot, canUseTableReportView]);
return getSecondaryReportActions(moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot, canUseTableReportView, policies);
}, [moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot, canUseTableReportView, policies]);

const secondaryActionsImplementation: Record<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>, DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>>> = {
[CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: {
Expand Down
5 changes: 0 additions & 5 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1239,10 +1239,6 @@ function getAllPoliciesLength() {
return Object.keys(allPolicies ?? {}).length;
}

function getAllPolicies() {
return Object.values(allPolicies ?? {}).filter((p) => !!p);
}

function getActivePolicy(): OnyxEntry<Policy> {
return getPolicy(activePolicyId);
}
Expand Down Expand Up @@ -1532,7 +1528,6 @@ export {
getWorkflowApprovalsUnavailable,
getNetSuiteImportCustomFieldLabel,
getAllPoliciesLength,
getAllPolicies,
getActivePolicy,
getUserFriendlyWorkspaceType,
isPolicyAccessible,
Expand Down
18 changes: 12 additions & 6 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportAction, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx';
import {isApprover as isApproverUtils} from './actions/Policy/Member';
import {getCurrentUserAccountID} from './actions/Report';
import {
arePaymentsEnabled as arePaymentsEnabledUtils,
getAllPolicies,
getConnectedIntegration,
getCorrectedAutoReportingFrequency,
getSubmitToAccountID,
Expand All @@ -18,6 +18,7 @@ import {getIOUActionForReportID, getIOUActionForTransactionID, getOneTransaction
import {
canAddTransaction,
canEditFieldOfMoneyRequest,
canEditReportPolicy,
isArchivedReport,
isClosedReport as isClosedReportUtils,
isCurrentUserSubmitter,
Expand Down Expand Up @@ -318,10 +319,14 @@ function isHoldActionForTransaction(report: Report, reportTransaction: Transacti
return isProcessingReport;
}

function isChangeWorkspaceAction(report: Report, policy?: Policy): boolean {
const policies = getAllPolicies();
const session = getSession();
return policies.filter((newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, session, policy)).length > 0;
function isChangeWorkspaceAction(report: Report, policies: OnyxCollection<Policy>): boolean {
const availablePolicies = Object.values(policies ?? {}).filter((newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, policies));
let hasAvailablePolicies = availablePolicies.length > 1;
if (!hasAvailablePolicies && availablePolicies.length === 1) {
hasAvailablePolicies = !report.policyID || report.policyID !== availablePolicies?.at(0)?.id;
}
const reportPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`];
return hasAvailablePolicies && canEditReportPolicy(report, reportPolicy);
}

function isMoveTransactionAction(reportTransactions: Transaction[], reportActions?: ReportAction[]) {
Expand Down Expand Up @@ -395,6 +400,7 @@ function getSecondaryReportActions(
reportActions?: ReportAction[],
canUseRetractNewDot?: boolean,
canUseTableReportView?: boolean,
policies?: OnyxCollection<Policy>,
): Array<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> {
const options: Array<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> = [];

Expand Down Expand Up @@ -438,7 +444,7 @@ function getSecondaryReportActions(

options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF);

if (isChangeWorkspaceAction(report, policy)) {
if (isChangeWorkspaceAction(report, policies)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
}

Expand Down
109 changes: 39 additions & 70 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/o
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import {createDraftTransaction, getIOUReportActionToApproveOrPay, setMoneyRequestParticipants, unholdRequest} from './actions/IOU';
import {isApprover as isApproverMember} from './actions/Policy/Member';
import {createDraftWorkspace} from './actions/Policy/Policy';
import {autoSwitchToFocusMode} from './actions/PriorityMode';
import {hasCreditBankAccount} from './actions/ReimbursementAccount/store';
Expand Down Expand Up @@ -129,6 +128,7 @@ import {
isPaidGroupPolicy as isPaidGroupPolicyPolicyUtils,
isPolicyAdmin as isPolicyAdminPolicyUtils,
isPolicyAuditor,
isPolicyMember,
isPolicyOwner,
isSubmitAndClose,
shouldShowPolicy,
Expand Down Expand Up @@ -3823,6 +3823,40 @@ function canEditMoneyRequest(reportAction: OnyxInputOrEntry<ReportAction<typeof
return !isReportApproved({report: moneyRequestReport}) && !isSettled(moneyRequestReport?.reportID) && !isClosedReport(moneyRequestReport) && isRequestor;
}

function canEditReportPolicy(report: OnyxEntry<Report>, reportPolicy: OnyxEntry<Policy>): boolean {
const isAdmin = isPolicyAdminPolicyUtils(reportPolicy);
const isManager = isReportManager(report);
const isSubmitter = isReportOwner(report);
const isReportAuditor = isAuditor(report);
const isIOUType = isIOUReport(report);
const isInvoiceType = isInvoiceReport(report);
const isExpenseType = isExpenseReport(report);
const isOpen = isOpenReport(report);
const isSubmitted = isProcessingReport(report);

if (isIOUType) {
return isOpen || isSubmitted;
}

if (isInvoiceType) {
return isOpen && !isReportAuditor;
}

if (isExpenseType) {
if (isOpen) {
return isSubmitter || isAdmin;
}

if (isSubmitted) {
return (isSubmitter && isAwaitingFirstLevelApproval(report)) || isManager || isAdmin;
}
Comment on lines +3850 to +3852

@cretadn22 cretadn22 May 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior is now considered incorrect and was removed in #88530


return isManager || isAdmin;
}

return false;
}

/**
* Checks if the current user can edit the provided property of an expense
*
Expand Down Expand Up @@ -10446,78 +10480,12 @@ function hasExportError(reportActions: OnyxEntry<ReportActions> | ReportAction[]
return Object.values(reportActions).some((action) => isIntegrationMessageAction(action));
}

function verifyState(report: OnyxEntry<Report>, validStates: Array<ValueOf<typeof CONST.REPORT.STATE_NUM>>): boolean {
if (report?.stateNum === undefined || report?.stateNum === null) {
return false;
}
return validStates.includes(report?.stateNum);
}

function verifyStatus(report: OnyxEntry<Report>, validStatuses: Array<ValueOf<typeof CONST.REPORT.STATUS_NUM>>): boolean {
if (report?.statusNum === undefined || report?.statusNum === null) {
return false;
}
return validStatuses.includes(report?.statusNum);
}

/**
* Determines whether the report can be moved to the workspace.
*/
function isWorkspaceEligibleForReportChange(newPolicy: OnyxEntry<Policy>, report: OnyxEntry<Report>, session: OnyxEntry<Session>, currentPolicy?: OnyxEntry<Policy>): boolean {
if (!session?.accountID) {
return false;
}

const isIOU = isIOUReport(report);
const submitterLogin = report?.ownerAccountID && getLoginByAccountID(report?.ownerAccountID);
const isSubmitterMember = !!submitterLogin && !!newPolicy?.employeeList?.[submitterLogin];
const managerLogin = report?.managerID && getLoginByAccountID(report?.managerID);
const isManagerMember = !!managerLogin && !!newPolicy?.employeeList?.[managerLogin];
const isCurrentUserAdmin = isPolicyAdminPolicyUtils(newPolicy, session?.email);
const isAdminOfCurrentPolicy = isPolicyAdminPolicyUtils(currentPolicy, session?.email);
const isPaidGroupPolicyType = isPaidGroupPolicyPolicyUtils(newPolicy);
const isReportOpenOrSubmitted = verifyState(report, [CONST.REPORT.STATE_NUM.OPEN, CONST.REPORT.STATE_NUM.SUBMITTED]);

// For IOUs, the sender and receiver can only change the workspace if:
// 1. The sender AND receiver are both members of the new policy OR
// 2. The sender OR receiver is an admin of the new policy. In this case, changing the policy also invites the non-member to the policy
if (isIOU && isReportOpenOrSubmitted && isPaidGroupPolicyType && ((isSubmitterMember && isManagerMember) || isCurrentUserAdmin)) {
return true;
}

// From this point on, reports must be of type Expense, the policy must be a paid type.
// The submitter and manager must also be policy members OR the current user is an admin so they can invite the non-members to the policy.
// Additionally, if the report is not open or submitted, the current user must be an admin.
// We're temporarily disabling moving reports to a workspace if the submitter is not a member of the new policy because this flow requires additional API changes.
const isExpenseReportType = isExpenseReport(report);
if (!isExpenseReportType || !isPaidGroupPolicyType || !isSubmitterMember || (!isReportOpenOrSubmitted && !isAdminOfCurrentPolicy)) {
return false;
}

const isCurrentUserReportSubmitter = session.accountID === report?.ownerAccountID;
if (isCurrentUserReportSubmitter) {
return true;
}

const isCurrentUserReportApprover = isApproverMember(newPolicy, session.accountID);
if (isCurrentUserReportApprover && verifyState(report, [CONST.REPORT.STATE_NUM.SUBMITTED])) {
return true;
}

const isCurrentUserReportPayer = isPayer(session, report, false, newPolicy);
if (isCurrentUserReportPayer && verifyState(report, [CONST.REPORT.STATE_NUM.APPROVED])) {
return true;
}

if (
isCurrentUserAdmin &&
verifyState(report, [CONST.REPORT.STATE_NUM.APPROVED]) &&
verifyStatus(report, [CONST.REPORT.STATUS_NUM.APPROVED, CONST.REPORT.STATUS_NUM.REIMBURSED, CONST.REPORT.STATUS_NUM.CLOSED])
) {
return true;
}
Comment on lines -10513 to -10518

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from the #76421 checklist: during the refactor and cleanup of conditions, we missed this case. As a result, the available workspace is shown, but the backend prevents the change


return false;
function isWorkspaceEligibleForReportChange(newPolicy: OnyxEntry<Policy>, report: OnyxEntry<Report>, policies: OnyxCollection<Policy>): boolean {
const submitterEmail = getLoginByAccountID(report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID);
return isPaidGroupPolicyPolicyUtils(newPolicy) && (isPolicyMember(submitterEmail, newPolicy?.id) || isPolicyAdmin(newPolicy?.id, policies));
Comment on lines +10486 to +10488

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB: Currently, the solution section lacks a description of this change, so we can't verify this logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I need to update the doc with this logic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change returns incorrect available workspaces

Screen.Recording.2025-05-21.at.17.25.16.mp4

@luacmartins luacmartins May 21, 2025 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah the workspaces are correct, but this request is failing with 411 Cannot create policy expense chat for non Policy member. I have a BE PR to add support for this, but basically we'll need a method similar to MoveIOUReportToPolicyAndInviteSubmitter, but for paid policies

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BE PR is still WIP. @suneox let's continue review here and not block on that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got it. Overall, the change set is look good. I'm just a bit confused about this part, so I'll continue the checklist based on the current state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #79075. We should also check here for isPendingDeletePolicy otherwise the change workspace option will be shown whilst offline.

}

function getApprovalChain(policy: OnyxEntry<Policy>, expenseReport: OnyxEntry<Report>): string[] {
Expand Down Expand Up @@ -10792,6 +10760,7 @@ export {
canCurrentUserOpenReport,
canDeleteReportAction,
canHoldUnholdReportAction,
canEditReportPolicy,
canEditFieldOfMoneyRequest,
canEditMoneyRequest,
canEditPolicyDescription,
Expand Down
3 changes: 1 addition & 2 deletions src/pages/ReportChangeWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
[session?.email, report, reportID],
);

const reportPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`];
const {sections, shouldShowNoResultsFoundMessage, shouldShowSearchInput} = useWorkspaceList({
policies,
currentUserLogin: session?.email,
shouldShowPendingDeletePolicy: false,
selectedPolicyID: report.policyID,
searchTerm: debouncedSearchTerm,
additionalFilter: (newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, session, reportPolicy),
additionalFilter: (newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, policies),
});

if (!isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report)) {
Expand Down
Loading
Loading