diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 3a373f4f1b4c..8349b37c3576 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -11,6 +11,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
+import {buildOptimisticNextStepForPreventSelfApprovalsEnabled} from '@libs/NextStepUtils';
import {getConnectedIntegration} from '@libs/PolicyUtils';
import {getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
import {
@@ -207,7 +208,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const shouldShowStatusBar =
hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions;
- const shouldShowNextStep = transactions?.length !== 0 && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
+
+ // When prevent self-approval is enabled, we need to show the optimistic next step
+ // We should always show this optimistic message for policies with preventSelfApproval
+ // to avoid any flicker during transitions between online/offline states
+ const optimisticNextStep = policy?.preventSelfApproval ? buildOptimisticNextStepForPreventSelfApprovalsEnabled() : nextStep;
+
+ const shouldShowNextStep = transactions?.length !== 0 && isFromPaidPolicy && !!optimisticNextStep?.message?.length && !shouldShowStatusBar;
const shouldShowAnyButton =
isDuplicate ||
shouldShowSettlementButton ||
@@ -510,7 +517,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
/>
)}
- {shouldShowNextStep && }
+ {shouldShowNextStep && }
{!!statusBarProps && (
`and enable workflows, then add ${featureName} to unlock this feature.`,
enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `and enable ${featureName} to unlock this feature.`,
- preventSelfApprovalsModalText: ({managerEmail}: {managerEmail: string}) =>
- `Any members currently approving their own expenses will be removed and replaced with the default approver for this workspace (${managerEmail}).`,
- preventSelfApprovalsConfirmButton: 'Prevent self-approvals',
- preventSelfApprovalsModalTitle: 'Prevent self-approvals?',
- preventSelfApprovalsDisabledSubtitle: "Self approvals can't be enabled until this workspace has at least two members.",
},
categoryRules: {
title: 'Category rules',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 6052ce92814a..a851560bdf8a 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -4793,11 +4793,6 @@ const translations = {
unlockFeatureGoToSubtitle: 'Ir a',
unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`,
enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `y habilita ${featureName} para desbloquear esta función.`,
- preventSelfApprovalsModalText: ({managerEmail}: {managerEmail: string}) =>
- `Todos los miembros que actualmente estén aprobando sus propios gastos serán eliminados y reemplazados con el aprobador predeterminado de este espacio de trabajo (${managerEmail}).`,
- preventSelfApprovalsConfirmButton: 'Evitar autoaprobaciones',
- preventSelfApprovalsModalTitle: '¿Evitar autoaprobaciones?',
- preventSelfApprovalsDisabledSubtitle: 'Las aprobaciones propias no pueden habilitarse hasta que este espacio de trabajo tenga al menos dos miembros.',
},
categoryRules: {
title: 'Reglas de categoría',
diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts
index afb42bad9232..b9f1abf81edc 100644
--- a/src/libs/NextStepUtils.ts
+++ b/src/libs/NextStepUtils.ts
@@ -82,6 +82,34 @@ function getNextApproverDisplayName(report: OnyxEntry, isUnapprove?: boo
return getDisplayNameForParticipant({accountID: approverAccountID}) ?? getPersonalDetailsForAccountID(approverAccountID).login;
}
+function buildOptimisticNextStepForPreventSelfApprovalsEnabled() {
+ const optimisticNextStep: ReportNextStep = {
+ type: 'alert',
+ icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
+ message: [
+ {
+ text: "Oops! Looks like you're submitting to ",
+ },
+ {
+ text: 'yourself',
+ type: 'strong',
+ },
+ {
+ text: '. Approving your own reports is ',
+ },
+ {
+ text: 'forbidden',
+ type: 'strong',
+ },
+ {
+ text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.',
+ },
+ ],
+ };
+
+ return optimisticNextStep;
+}
+
/**
* Generates an optimistic nextStep based on a current report status and other properties.
*
@@ -416,4 +444,4 @@ function buildNextStep(report: OnyxEntry, predictedNextStatus: ValueOf {
return domainName;
};
-/**
- * Returns an array of user emails who are currently self-approving:
- * i.e. user.submitsTo === their own email.
- */
-function getAllSelfApprovers(policy: OnyxEntry): string[] {
- const defaultApprover = policy?.approver ?? policy?.owner;
- if (!policy?.employeeList || !defaultApprover) {
- return [];
- }
- return Object.keys(policy.employeeList).filter((email) => {
- const employee = policy?.employeeList?.[email] ?? {};
- return employee?.submitsTo === email && employee?.email !== defaultApprover;
- });
-}
-
-/**
- * Checks if the workspace has only one user and if there no approver for the policy.
- * If so, we can't enable the "Prevent Self Approvals" feature.
- */
-function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean {
- if (!policy?.employeeList || !policy.approver) {
- return false;
- }
-
- const employeeEmails = Object.keys(policy.employeeList);
-
- return employeeEmails.length > 1;
-}
-
function isPrefferedExporter(policy: Policy) {
const user = getCurrentUserEmail();
const exporters = [
@@ -1383,12 +1354,10 @@ function isAutoSyncEnabled(policy: Policy) {
export {
canEditTaxRate,
- canEnablePreventSelfApprovals,
extractPolicyIDFromPath,
escapeTagName,
getActivePolicies,
getPerDiemCustomUnits,
- getAllSelfApprovers,
getAdminEmployees,
getCleanedTagName,
getConnectedIntegration,
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index daf6d6d972ab..8f46146ec9c1 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -55,7 +55,7 @@ import {getMemberAccountIDsForWorkspace, isDeletedPolicyEmployee, isExpensifyTea
import {getDisplayNameForParticipant} from '@libs/ReportUtils';
import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils';
import {close} from '@userActions/Modal';
-import {dismissAddedWithPrimaryLoginMessages, setPolicyPreventSelfApproval} from '@userActions/Policy/Policy';
+import {dismissAddedWithPrimaryLoginMessages} from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -239,10 +239,8 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
return;
}
- const previousEmployeesCount = Object.keys(policy?.employeeList ?? {}).length;
// Remove the admin from the list
const accountIDsToRemove = session?.accountID ? selectedEmployees.filter((id) => id !== session.accountID) : selectedEmployees;
- const newEmployeesCount = previousEmployeesCount - accountIDsToRemove.length;
// Check if any of the account IDs are approvers
const hasApprovers = accountIDsToRemove.some((accountID) => isApprover(policy, accountID));
@@ -274,10 +272,6 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
setRemoveMembersConfirmModalVisible(false);
InteractionManager.runAfterInteractions(() => {
removeMembers(accountIDsToRemove, route.params.policyID);
- if (newEmployeesCount === 1 && policy?.preventSelfApproval) {
- // We can't let the "Prevent Self Approvals" enabled if there's only one workspace user
- setPolicyPreventSelfApproval(route.params.policyID, false);
- }
});
};
diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx
index 1ec0ee65e956..916b9b941c94 100644
--- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx
+++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx
@@ -1,6 +1,4 @@
-import React, {useMemo, useState} from 'react';
-import {useOnyx} from 'react-native-onyx';
-import ConfirmModal from '@components/ConfirmModal';
+import React from 'react';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Section from '@components/Section';
@@ -11,8 +9,7 @@ import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {canEnablePreventSelfApprovals, getAllSelfApprovers, getWorkflowApprovalsUnavailable} from '@libs/PolicyUtils';
-import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils';
+import {getWorkflowApprovalsUnavailable} from '@libs/PolicyUtils';
import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
import {
enableAutoApprovalOptions,
@@ -21,12 +18,8 @@ import {
setPolicyPreventMemberCreatedTitle,
setPolicyPreventSelfApproval,
} from '@userActions/Policy/Policy';
-import {updateApprovalWorkflow} from '@userActions/Workflow';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
-import type {Approver, Member} from '@src/types/onyx/ApprovalWorkflow';
type ExpenseReportRulesSectionProps = {
policyID: string;
@@ -36,48 +29,10 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const policy = usePolicy(policyID);
- const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
- const customReportNamesUnavailable = !policy?.areReportFieldsEnabled;
- // Auto-approvals and self-approvals are unavailable due to the policy workflows settings
const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy);
+ const customReportNamesUnavailable = !policy?.areReportFieldsEnabled;
const autoPayApprovedReportsUnavailable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO;
- const [isPreventSelfApprovalsModalVisible, setIsPreventSelfApprovalsModalVisible] = useState(false);
- const isPreventSelfApprovalsDisabled = !canEnablePreventSelfApprovals(policy) && !policy?.preventSelfApproval;
- const selfApproversEmails = getAllSelfApprovers(policy);
-
- function handleTogglePreventSelfApprovals(isEnabled: boolean) {
- if (!isEnabled) {
- setPolicyPreventSelfApproval(policyID, false);
- return;
- }
-
- if (selfApproversEmails.length === 0) {
- setPolicyPreventSelfApproval(policyID, true);
- } else {
- setIsPreventSelfApprovalsModalVisible(true);
- }
- }
-
- const {currentApprovalWorkflows, defaultWorkflowMembers, usedApproverEmails} = useMemo(() => {
- if (!policy || !personalDetails) {
- return {};
- }
-
- const defaultApprover = policy?.approver ?? policy.owner;
- const result = convertPolicyEmployeesToApprovalWorkflows({
- employees: policy.employeeList ?? {},
- defaultApprover,
- personalDetails,
- });
-
- return {
- defaultWorkflowMembers: result.availableMembers,
- usedApproverEmails: result.usedApproverEmails,
- currentApprovalWorkflows: result.approvalWorkflows.filter((workflow) => !workflow.isDefault),
- };
- }, [personalDetails, policy]);
-
const renderFallbackSubtitle = ({featureName, variant = 'unlock'}: {featureName: string; variant?: 'unlock' | 'enable'}) => {
return (
@@ -137,21 +92,15 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
},
{
title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'),
- subtitle: (() => {
- if (workflowApprovalsUnavailable) {
- return renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()});
- }
- if (isPreventSelfApprovalsDisabled) {
- return translate('workspace.rules.expenseReportRules.preventSelfApprovalsDisabledSubtitle');
- }
- return translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle');
- })(),
+ subtitle: workflowApprovalsUnavailable
+ ? renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()})
+ : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'),
switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'),
isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable,
- disabled: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled,
- showLockIcon: workflowApprovalsUnavailable || isPreventSelfApprovalsDisabled,
+ disabled: workflowApprovalsUnavailable,
+ showLockIcon: workflowApprovalsUnavailable,
pendingAction: policy?.pendingFields?.preventSelfApproval,
- onToggle: (isEnabled: boolean) => handleTogglePreventSelfApprovals(isEnabled),
+ onToggle: (isEnabled: boolean) => setPolicyPreventSelfApproval(policyID, isEnabled),
},
{
title: translate('workspace.rules.expenseReportRules.autoApproveCompliantReportsTitle'),
@@ -230,92 +179,36 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
];
return (
- <>
-
- {optionItems.map(({title, subtitle, isActive, subMenuItems, showLockIcon, disabled, onToggle, pendingAction}, index) => {
- const showBorderBottom = index !== optionItems.length - 1;
-
- return (
-
- );
- })}
-
- {
- setPolicyPreventSelfApproval(policyID, true);
-
- const defaultApprover = policy?.approver ?? policy?.owner;
- if (!defaultApprover) {
- setIsPreventSelfApprovalsModalVisible(false);
- return;
- }
-
- currentApprovalWorkflows?.forEach((workflow: ApprovalWorkflow) => {
- const oldApprovers = workflow.approvers ?? [];
- const approversToRemove = oldApprovers.filter((approver: Approver) => selfApproversEmails.includes(approver?.email));
- const newApprovers = oldApprovers.filter((approver: Approver) => !selfApproversEmails.includes(approver?.email));
-
- if (!newApprovers.some((a) => a.email === defaultApprover)) {
- newApprovers.unshift({
- email: defaultApprover,
- displayName: defaultApprover,
- });
- }
-
- const oldMembers = workflow.members ?? [];
- const newMembers = oldMembers.map((member: Member) => {
- const isSelfApprover = selfApproversEmails.includes(member.email);
- return isSelfApprover ? {...member, submitsTo: defaultApprover} : member;
- });
-
- const newWorkflow = {
- ...workflow,
- approvers: newApprovers,
- availableMembers: [...workflow.members, ...defaultWorkflowMembers],
- members: newMembers,
- usedApproverEmails,
- isDefault: workflow.isDefault ?? false,
- action: CONST.APPROVAL_WORKFLOW.ACTION.EDIT,
- errors: null,
- };
-
- const membersToRemove: Member[] = [];
-
- updateApprovalWorkflow(policyID, newWorkflow, membersToRemove, approversToRemove);
- });
- setIsPreventSelfApprovalsModalVisible(false);
- }}
- onCancel={() => setIsPreventSelfApprovalsModalVisible(false)}
- />
- >
+
+ {optionItems.map(({title, subtitle, isActive, subMenuItems, showLockIcon, disabled, onToggle, pendingAction}, index) => {
+ const showBorderBottom = index !== optionItems.length - 1;
+
+ return (
+
+ );
+ })}
+
);
}
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
index 348fc1b3a299..1ec2f67eebd9 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
@@ -14,12 +14,12 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils';
import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
-import * as Workflow from '@userActions/Workflow';
+import {clearApprovalWorkflow, removeApprovalWorkflow, setApprovalWorkflow, updateApprovalWorkflow, validateApprovalWorkflow} from '@userActions/Workflow';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
@@ -39,12 +39,12 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const formRef = useRef(null);
- const updateApprovalWorkflow = useCallback(() => {
+ const updateApprovalWorkflowCallback = useCallback(() => {
if (!approvalWorkflow || !initialApprovalWorkflow) {
return;
}
- if (!Workflow.validateApprovalWorkflow(approvalWorkflow)) {
+ if (!validateApprovalWorkflow(approvalWorkflow)) {
return;
}
@@ -53,11 +53,11 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
const approversToRemove = initialApprovalWorkflow.approvers.filter((initialApprover) => !approvalWorkflow.approvers.some((approver) => approver.email === initialApprover.email));
Navigation.dismissModal();
InteractionManager.runAfterInteractions(() => {
- Workflow.updateApprovalWorkflow(route.params.policyID, approvalWorkflow, membersToRemove, approversToRemove);
+ updateApprovalWorkflow(route.params.policyID, approvalWorkflow, membersToRemove, approversToRemove);
});
}, [approvalWorkflow, initialApprovalWorkflow, route.params.policyID]);
- const removeApprovalWorkflow = useCallback(() => {
+ const removeApprovalWorkflowCallback = useCallback(() => {
if (!initialApprovalWorkflow) {
return;
}
@@ -66,7 +66,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
Navigation.dismissModal();
InteractionManager.runAfterInteractions(() => {
// Remove the approval workflow using the initial data as it could be already edited
- Workflow.removeApprovalWorkflow(route.params.policyID, initialApprovalWorkflow);
+ removeApprovalWorkflow(route.params.policyID, initialApprovalWorkflow);
});
}, [initialApprovalWorkflow, route.params.policyID]);
@@ -92,8 +92,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
}, [personalDetails, policy, route.params.firstApproverEmail]);
// eslint-disable-next-line rulesdir/no-negated-variables
- const shouldShowNotFoundView =
- (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy) || !currentApprovalWorkflow;
+ const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy) || !currentApprovalWorkflow;
// Set the initial approval workflow when the page is loaded
useEffect(() => {
@@ -102,10 +101,10 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
}
if (!currentApprovalWorkflow) {
- return Workflow.clearApprovalWorkflow();
+ return clearApprovalWorkflow();
}
- Workflow.setApprovalWorkflow({
+ setApprovalWorkflow({
...currentApprovalWorkflow,
availableMembers: [...currentApprovalWorkflow.members, ...defaultWorkflowMembers],
usedApproverEmails,
@@ -127,8 +126,8 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
{
formRef.current?.scrollTo({y: 0, animated: true});
}}
@@ -160,7 +159,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
setIsDeleteModalVisible(false)}
prompt={translate('workflowsEditApprovalsPage.deletePrompt')}
confirmText={translate('common.delete')}