diff --git a/src/languages/de.ts b/src/languages/de.ts
index f81370936d01..fc16cd73b66b 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -3260,6 +3260,8 @@ ${amount} für ${merchant} – ${date}`,
defaultWorkflowApproverNotWorkspaceMember: 'Diese genehmigende Person ist nicht mehr Mitglied des Arbeitsbereichs. Wählen Sie eine neue genehmigende Person.',
overLimitApproverNotWorkspaceMember:
'Die zusätzliche genehmigende Person für Berichte über dem Limit ist nicht mehr Mitglied des Arbeitsbereichs. Wählen Sie eine neue zusätzliche genehmigende Person.',
+ overLimitApproverInWorkflow:
+ 'Die zusätzliche Genehmigungsperson für Berichte über dem Limit ist bereits eine Genehmigungsperson in diesem Workflow. Wählen Sie eine andere zusätzliche Genehmigungsperson.',
approverCircularReference: (name1: string, name2: string) =>
`${name1} genehmigt bereits Berichte für ${name2}. Bitte wähle eine andere approvierende Person, um einen zirkulären Workflow zu vermeiden.`,
emptyContent: {
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 576923fa7a55..dd3fb9a2ebb2 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -3319,6 +3319,7 @@ ${amount} για ${merchant} - ${date}`,
approverNotWorkspaceMember: 'Αυτός ο εγκρίνων δεν είναι πλέον μέλος του χώρου εργασίας. Επιλέξτε νέο εγκρίνοντα ή διαγράψτε αυτή τη ροή εργασίας.',
defaultWorkflowApproverNotWorkspaceMember: 'Αυτός ο εγκρίνων δεν είναι πλέον μέλος του χώρου εργασίας. Επιλέξτε νέο εγκρίνοντα.',
overLimitApproverNotWorkspaceMember: 'Ο πρόσθετος εγκρίνων για αναφορές πάνω από το όριο δεν είναι πλέον μέλος του χώρου εργασίας. Επιλέξτε νέο πρόσθετο εγκρίνοντα.',
+ overLimitApproverInWorkflow: 'Ο επιπλέον εγκρίνων για αναφορές πάνω από το όριο είναι ήδη εγκρίνων σε αυτή τη ροή εργασίας. Επιλέξτε έναν διαφορετικό επιπλέον εγκρίνοντα.',
approverCircularReference: (name1: string, name2: string) =>
`Ο/Η ${name1} ήδη εγκρίνει αναφορές προς τον/την ${name2}. Παρακαλούμε επιλέξτε διαφορετικό εγκρίνων για να αποφύγετε έναν κυκλικό κύκλο έγκρισης.`,
emptyContent: {
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c8bdaa542c82..5f981c4f0b97 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -3413,6 +3413,7 @@ const translations = {
approverNotWorkspaceMember: 'This approver is no longer a workspace member. Choose a new approver or delete this workflow.',
defaultWorkflowApproverNotWorkspaceMember: 'This approver is no longer a workspace member. Choose a new approver.',
overLimitApproverNotWorkspaceMember: 'The additional approver for reports over the limit is no longer a workspace member. Choose a new additional approver.',
+ overLimitApproverInWorkflow: 'The additional approver for reports over the limit is already an approver in this workflow. Choose a different additional approver.',
approverCircularReference: (name1: string, name2: string) =>
`${name1} already approves reports to ${name2}. Please choose a different approver to avoid a circular workflow.`,
emptyContent: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 43a401c5de6c..538d43284fbf 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -3156,6 +3156,7 @@ ${amount} para ${merchant} - ${date}`,
approverNotWorkspaceMember: 'Este aprobador ya no es miembro del espacio de trabajo. Elige un nuevo aprobador o elimina este flujo de trabajo.',
defaultWorkflowApproverNotWorkspaceMember: 'Este aprobador ya no es miembro del espacio de trabajo. Elige un nuevo aprobador.',
overLimitApproverNotWorkspaceMember: 'El aprobador adicional para los informes que superan el límite ya no es miembro del espacio de trabajo. Elige un nuevo aprobador adicional.',
+ overLimitApproverInWorkflow: 'El aprobador adicional para los informes que superan el límite ya es un aprobador en este flujo de trabajo. Elige un aprobador adicional diferente.',
approverCircularReference: (name1, name2) =>
`${name1} ya aprueba informes a ${name2}. Por favor, elige un aprobador diferente para evitar un flujo de trabajo circular.`,
emptyContent: {
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 16025f958d57..7d4ba57750d8 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -3265,6 +3265,8 @@ ${amount} pour ${merchant} - ${date}`,
defaultWorkflowApproverNotWorkspaceMember: 'Cet approbateur n’est plus membre de l’espace de travail. Choisissez un nouvel approbateur.',
overLimitApproverNotWorkspaceMember:
'L’approbateur supplémentaire pour les notes de frais au-delà de la limite n’est plus membre de l’espace de travail. Choisissez un nouvel approbateur supplémentaire.',
+ overLimitApproverInWorkflow:
+ 'L’approbateur supplémentaire pour les notes de frais dépassant la limite est déjà un approbateur dans ce flux de travail. Choisissez un autre approbateur supplémentaire.',
approverCircularReference: (name1: string, name2: string) =>
`${name1} approuve déjà les notes de frais de ${name2}. Veuillez choisir un autre approbateur pour éviter un circuit d'approbation circulaire.`,
emptyContent: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index c69b696bf807..bd3f13b4d885 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -3245,6 +3245,7 @@ ${amount} per ${merchant} - ${date}`,
approverNotWorkspaceMember: 'Questo approvatore non è più un membro dello spazio di lavoro. Scegli un nuovo approvatore oppure elimina questo workflow.',
defaultWorkflowApproverNotWorkspaceMember: 'Questo approvatore non è più un membro dello spazio di lavoro. Scegli un nuovo approvatore.',
overLimitApproverNotWorkspaceMember: 'L’approvatore aggiuntivo per i report oltre il limite non è più un membro dello spazio di lavoro. Scegli un nuovo approvatore aggiuntivo.',
+ overLimitApproverInWorkflow: 'L’approvatore aggiuntivo per i report oltre il limite è già un approvatore in questo workflow. Scegli un altro approvatore aggiuntivo.',
approverCircularReference: (name1: string, name2: string) =>
`${name1} approva già i report per ${name2}. Scegli un altro approvatore per evitare un flusso di lavoro circolare.`,
emptyContent: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index be5282d2266d..e70804a17316 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -3220,6 +3220,7 @@ ${date} の ${merchant} への ${amount}`,
approverNotWorkspaceMember: 'この承認者はワークスペースのメンバーではなくなりました。新しい承認者を選択するか、このワークフローを削除してください。',
defaultWorkflowApproverNotWorkspaceMember: 'この承認者はワークスペースのメンバーではなくなりました。新しい承認者を選択してください。',
overLimitApproverNotWorkspaceMember: '上限を超えるレポートの追加承認者は、ワークスペースのメンバーではなくなりました。新しい追加承認者を選択してください。',
+ overLimitApproverInWorkflow: '上限を超えるレポートの追加承認者として選択されたユーザーは、このワークフロー内ですでに承認者になっています。別の追加承認者を選択してください。',
approverCircularReference: (name1: string, name2: string) =>
`${name1} はすでに ${name2} にレポートを承認しています。ワークフローが循環しないよう、別の承認者を選択してください。`,
emptyContent: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 60520425de68..277314f40081 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -3248,6 +3248,7 @@ ${amount} voor ${merchant} - ${date}`,
approverNotWorkspaceMember: 'Deze fiatteur is geen lid meer van de workspace. Kies een nieuwe fiatteur of verwijder deze workflow.',
defaultWorkflowApproverNotWorkspaceMember: 'Deze fiatteur is geen lid meer van de workspace. Kies een nieuwe fiatteur.',
overLimitApproverNotWorkspaceMember: 'De extra fiatteur voor rapporten boven de limiet is geen lid meer van de workspace. Kies een nieuwe extra fiatteur.',
+ overLimitApproverInWorkflow: 'De extra fiatteur voor rapporten boven de limiet is al een fiatteur in deze workflow. Kies een andere extra fiatteur.',
approverCircularReference: (name1: string, name2: string) =>
`${name1} keurt al rapporten goed voor ${name2}. Kies een andere fiatteur om een cirkelstroom te voorkomen.`,
emptyContent: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 3ecfb0add1a4..be659ecab1cc 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -3281,6 +3281,7 @@ ${amount} dla ${merchant} - ${date}`,
approverNotWorkspaceMember: 'Ten zatwierdzający nie jest już członkiem tego workspace’u. Wybierz nowego zatwierdzającego albo usuń ten workflow.',
defaultWorkflowApproverNotWorkspaceMember: 'Ten zatwierdzający nie jest już członkiem tego workspace’u. Wybierz nowego zatwierdzającego.',
overLimitApproverNotWorkspaceMember: 'Dodatkowy zatwierdzający dla raportów powyżej limitu nie jest już członkiem tego workspace’u. Wybierz nowego dodatkowego zatwierdzającego.',
+ overLimitApproverInWorkflow: 'Dodatkowy akceptujący dla raportów powyżej limitu jest już akceptującym w tym przepływie pracy. Wybierz innego dodatkowego akceptującego.',
approverCircularReference: (name1: string, name2: string) =>
`${name1} już zatwierdza raporty dla ${name2}. Wybierz innego zatwierdzającego, aby uniknąć zapętlenia przepływu pracy.`,
emptyContent: {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index fb2be3403595..24df5eac03ec 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -3238,6 +3238,7 @@ ${amount} para ${merchant} - ${date}`,
approverNotWorkspaceMember: 'Esse aprovador não é mais membro do workspace. Escolha um novo aprovador ou exclua este fluxo de trabalho.',
defaultWorkflowApproverNotWorkspaceMember: 'Esse aprovador não é mais membro do workspace. Escolha um novo aprovador.',
overLimitApproverNotWorkspaceMember: 'O aprovador adicional para relatórios acima do limite não é mais membro do workspace. Escolha um novo aprovador adicional.',
+ overLimitApproverInWorkflow: 'O aprovador adicional para relatórios acima do limite já é um aprovador neste fluxo de trabalho. Escolha um aprovador adicional diferente.',
approverCircularReference: (name1: string, name2: string) =>
`${name1} já aprova relatórios para ${name2}. Escolha um aprovador diferente para evitar um fluxo de aprovação circular.`,
emptyContent: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 40a0d421f177..28c8a2d9b982 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -3145,6 +3145,7 @@ ${amount},商户:${merchant} - 日期:${date}`,
approverNotWorkspaceMember: '此审批人已不再是工作区成员。请选择新的审批人或删除此工作流程。',
defaultWorkflowApproverNotWorkspaceMember: '此审批人已不再是工作区成员。请选择新的审批人。',
overLimitApproverNotWorkspaceMember: '超出限额报告的额外审批人已不再是工作区成员。请选择新的额外审批人。',
+ overLimitApproverInWorkflow: '此限额以上报表的额外审批人已是此工作流中的审批人。请选择其他额外审批人。',
approverCircularReference: (name1: string, name2: string) => `${name1} 已经将报表提交给 ${name2} 审批。请选择其他审批人以避免形成循环审批流程。`,
emptyContent: {
title: '没有可显示的成员',
diff --git a/src/libs/actions/Workflow.ts b/src/libs/actions/Workflow.ts
index 10bf8e37c5b8..ed848d2621b5 100644
--- a/src/libs/actions/Workflow.ts
+++ b/src/libs/actions/Workflow.ts
@@ -754,9 +754,10 @@ type ApprovalWorkflowOnyxValidated = Omit & {
/**
* Validates the approval workflow and sets the errors on the approval workflow
* @param approvalWorkflow the approval workflow to validate
+ * @param isMultipleApproversBetaEnabled whether the `MULTIPLE_APPROVERS` beta is enabled, which saves the workflow as approval workflow rules
* @returns true if the approval workflow is valid, false otherwise
*/
-function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): approvalWorkflow is ApprovalWorkflowOnyxValidated {
+function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx, isMultipleApproversBetaEnabled: boolean): approvalWorkflow is ApprovalWorkflowOnyxValidated {
const errors: Record = {};
for (const [approverIndex, approver] of approvalWorkflow.approvers.entries()) {
@@ -773,6 +774,15 @@ function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): appro
errors[`approver-${approverIndex}`] = nonMemberApproverError;
}
+ // Someone who already approves in this workflow approves every report anyway, so the limit can't add them
+ if (
+ isMultipleApproversBetaEnabled &&
+ approver?.overLimitForwardsTo &&
+ approvalWorkflow.approvers.some((workflowApprover) => workflowApprover?.email === approver.overLimitForwardsTo)
+ ) {
+ errors[`approver-${approverIndex}`] = 'workflowsPage.overLimitApproverInWorkflow';
+ }
+
// Validate that if overLimitForwardsTo is set, approvalLimit must also be set
if (approver?.overLimitForwardsTo && (!approver?.approvalLimit || approver.approvalLimit <= 0)) {
errors[`approver-${approverIndex}`] = 'workflowsApprovalLimitPage.enterAmountError';
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
index c6923dc4b12e..bc8141d4420a 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
@@ -102,6 +102,12 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa
if (isEmailAlreadyInApprovers && visibleSelectedApproverEmail !== email) {
return null;
}
+
+ // Another step's additional approver can't also approve here, or that step's limit would add no one
+ const isOtherStepsAdditionalApprover = approversFromWorkflow?.some((approver, index) => approver?.overLimitForwardsTo === email && index !== approverIndex);
+ if (isMultipleApproversBetaEnabled && isOtherStepsAdditionalApprover && visibleSelectedApproverEmail !== email) {
+ return null;
+ }
if (!isMultipleApproversBetaEnabled && !isDefault && approverIndex === 0 && defaultApprover === email) {
return null;
}
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
index a8a6ae89e05a..3ff2431d4d48 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
@@ -67,7 +67,7 @@ function WorkspaceWorkflowsApprovalsCreatePage({policy, isLoadingReportData = tr
return;
}
- if (!validateApprovalWorkflow(approvalWorkflow)) {
+ if (!validateApprovalWorkflow(approvalWorkflow, isBetaEnabled(CONST.BETAS.MULTIPLE_APPROVERS))) {
return;
}
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
index 0086f7100a66..26124ed591d4 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
@@ -118,7 +118,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
return;
}
- if (!validateApprovalWorkflow(approvalWorkflow)) {
+ if (!validateApprovalWorkflow(approvalWorkflow, isBetaEnabled(CONST.BETAS.MULTIPLE_APPROVERS))) {
return;
}
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsOverLimitApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsOverLimitApproverPage.tsx
index 30f3067b78dc..b9406ca8ce88 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsOverLimitApproverPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsOverLimitApproverPage.tsx
@@ -6,6 +6,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
+import usePermissions from '@hooks/usePermissions';
import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin';
import usePersonalDetailsByEmail from '@hooks/usePersonalDetailsByEmail';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -42,6 +43,8 @@ function WorkspaceWorkflowsApprovalsOverLimitApproverPage({policy, personalDetai
const personalDetailsByEmail = usePersonalDetailsByEmail();
const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {}));
const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
+ const {isBetaEnabled} = usePermissions();
+ const isMultipleApproversBetaEnabled = isBetaEnabled(CONST.BETAS.MULTIPLE_APPROVERS);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']);
const policyID = route.params.policyID;
@@ -51,6 +54,7 @@ function WorkspaceWorkflowsApprovalsOverLimitApproverPage({policy, personalDetai
const employeeList = policy?.employeeList;
const isDefault = approvalWorkflow?.isDefault;
const membersEmail = approvalWorkflow?.members.map((member) => member.email);
+ const workflowApproverEmails = new Set(approvalWorkflow?.approvers?.map((approver) => approver?.email));
const currentApproverEmail = currentApprover?.email;
@@ -88,6 +92,11 @@ function WorkspaceWorkflowsApprovalsOverLimitApproverPage({policy, personalDetai
return null;
}
+ // Someone who already approves in this workflow approves every report anyway, so the limit can't add them
+ if (isMultipleApproversBetaEnabled && workflowApproverEmails.has(email)) {
+ return null;
+ }
+
const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
if (!accountID) {
return null;
diff --git a/tests/actions/WorkflowTest.ts b/tests/actions/WorkflowTest.ts
index 7b3840c4ed93..32c9f138425d 100644
--- a/tests/actions/WorkflowTest.ts
+++ b/tests/actions/WorkflowTest.ts
@@ -204,7 +204,7 @@ describe('actions/Workflow', () => {
await Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, approvalWorkflow);
// When the admin tries to save it
- const isValid = validateApprovalWorkflow(approvalWorkflow);
+ const isValid = validateApprovalWorkflow(approvalWorkflow, true);
await waitForBatchedUpdates();
// Then the save is blocked and the approver shows the error offering to replace them or delete the workflow
@@ -223,7 +223,7 @@ describe('actions/Workflow', () => {
await Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, approvalWorkflow);
// When the admin tries to save it
- const isValid = validateApprovalWorkflow(approvalWorkflow);
+ const isValid = validateApprovalWorkflow(approvalWorkflow, true);
await waitForBatchedUpdates();
// Then the save is blocked, and since the default workflow can't be deleted the error only asks for a new approver
@@ -242,7 +242,7 @@ describe('actions/Workflow', () => {
await Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, approvalWorkflow);
// When the admin tries to save it
- const isValid = validateApprovalWorkflow(approvalWorkflow);
+ const isValid = validateApprovalWorkflow(approvalWorkflow, true);
await waitForBatchedUpdates();
// Then the save is blocked and the approver shows the error asking for a new additional approver
@@ -250,6 +250,66 @@ describe('actions/Workflow', () => {
const workflowState = await getOnyxValue(ONYXKEYS.APPROVAL_WORKFLOW);
expect(workflowState?.errors?.['approver-0']).toBe('workflowsPage.overLimitApproverNotWorkspaceMember');
});
+
+ it.each([
+ [
+ 'earlier in the workflow',
+ [
+ {email: employee1Email, displayName: employee1Email},
+ {email: employee2Email, displayName: employee2Email, approvalLimit: 5000, overLimitForwardsTo: employee1Email},
+ {email: employee3Email, displayName: employee3Email},
+ ],
+ 1,
+ ],
+ [
+ 'later in the workflow',
+ [
+ {email: employee1Email, displayName: employee1Email, approvalLimit: 5000, overLimitForwardsTo: employee3Email},
+ {email: employee2Email, displayName: employee2Email},
+ {email: employee3Email, displayName: employee3Email},
+ ],
+ 0,
+ ],
+ ])('should block saving a workflow whose additional approver for reports over the limit is already an approver %s', async (_description, approvers, limitedApproverIndex) => {
+ // Given a workflow saved as approval workflow rules, whose reports over an approver's limit would go to someone
+ // who already approves in it, so the limit adds no one
+ const approvalWorkflow: ApprovalWorkflowOnyx = {
+ ...INITIAL_APPROVAL_WORKFLOW,
+ members: [{email: employee4Email, displayName: employee4Email}],
+ approvers,
+ };
+ await Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, approvalWorkflow);
+
+ // When the admin tries to save it
+ const isValid = validateApprovalWorkflow(approvalWorkflow, true);
+ await waitForBatchedUpdates();
+
+ // Then the save is blocked and the limited approver shows the error asking for a different additional approver
+ expect(isValid).toBe(false);
+ const workflowState = await getOnyxValue(ONYXKEYS.APPROVAL_WORKFLOW);
+ expect(workflowState?.errors?.[`approver-${limitedApproverIndex}`]).toBe('workflowsPage.overLimitApproverInWorkflow');
+ });
+
+ it('should allow saving a workflow whose additional approver for reports over the limit is not one of its approvers', async () => {
+ // Given a workflow saved as approval workflow rules, whose reports over the first approver's limit also go to
+ // someone outside it
+ const approvalWorkflow: ApprovalWorkflowOnyx = {
+ ...INITIAL_APPROVAL_WORKFLOW,
+ members: [{email: employee4Email, displayName: employee4Email}],
+ approvers: [
+ {email: employee1Email, displayName: employee1Email, approvalLimit: 5000, overLimitForwardsTo: ownerEmail},
+ {email: employee2Email, displayName: employee2Email},
+ ],
+ };
+ await Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, approvalWorkflow);
+
+ // When the admin tries to save it
+ const isValid = validateApprovalWorkflow(approvalWorkflow, true);
+ await waitForBatchedUpdates();
+
+ // Then it saves, since the limit adds someone who wouldn't otherwise approve the report
+ expect(isValid).toBe(true);
+ });
});
describe('setApprovalWorkflowApprover', () => {
diff --git a/tests/ui/WorkspaceWorkflowsApprovalsApproverPageTest.tsx b/tests/ui/WorkspaceWorkflowsApprovalsApproverPageTest.tsx
index 4f4cf3419630..150a02b5ccdf 100644
--- a/tests/ui/WorkspaceWorkflowsApprovalsApproverPageTest.tsx
+++ b/tests/ui/WorkspaceWorkflowsApprovalsApproverPageTest.tsx
@@ -5,6 +5,7 @@ import ApproverSelectionList from '@components/ApproverSelectionList';
import ComposeProviders from '@components/ComposeProviders';
import {LocaleContextProvider} from '@components/LocaleContextProvider';
import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import PersonalDetailsByLoginProvider from '@components/PersonalDetailsByLoginProvider';
import Navigation from '@libs/Navigation/Navigation';
import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils';
@@ -117,7 +118,7 @@ async function editWorkflowWithRemovedApprover() {
async function renderPage(approverIndex: number) {
const result = render(
-
+
{
expect(getRows().some((row) => row.login === REMOVED_EMAIL)).toBe(false);
});
+ it("never offers another step's additional approver for reports over the limit, but still offers a step its own", async () => {
+ // Given the rules-based workflow Bob > Erin > an approver who left, where Bob's reports over his limit also go to the admin
+ const approvalWorkflow = await getOnyxValue(ONYXKEYS.APPROVAL_WORKFLOW);
+ await act(async () => {
+ await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.MULTIPLE_APPROVERS]);
+ await Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {
+ approvers: approvalWorkflow?.approvers.map((approver, index) => (index === 0 && approver ? {...approver, approvalLimit: 5000, overLimitForwardsTo: ADMIN_EMAIL} : approver)),
+ });
+ });
+ await waitForBatchedUpdatesWithAct();
+
+ // When the admin opens Erin's slot
+ const {unmount} = await renderPage(1);
+
+ // Then the admin can't be picked, since approving there would make Bob's limit add no one
+ expect(getRows().some((row) => row.login === ADMIN_EMAIL)).toBe(false);
+ unmount();
+
+ // When the admin opens Bob's own slot
+ await renderPage(0);
+
+ // Then the admin can be picked, since replacing Bob clears his limit
+ expect(getRows().some((row) => row.login === ADMIN_EMAIL)).toBe(true);
+ });
+
it('lists a flagged approver only once when they are back on the workspace', async () => {
// Given the approver who left was invited back while the edited workflow still has them flagged
await act(async () => {
diff --git a/tests/ui/WorkspaceWorkflowsApprovalsOverLimitApproverPageTest.tsx b/tests/ui/WorkspaceWorkflowsApprovalsOverLimitApproverPageTest.tsx
new file mode 100644
index 000000000000..6af46e5a2e90
--- /dev/null
+++ b/tests/ui/WorkspaceWorkflowsApprovalsOverLimitApproverPageTest.tsx
@@ -0,0 +1,150 @@
+import {act, render} from '@testing-library/react-native';
+
+import type {SelectionListApprover} from '@components/ApproverSelectionList';
+import ApproverSelectionList from '@components/ApproverSelectionList';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import PersonalDetailsByLoginProvider from '@components/PersonalDetailsByLoginProvider';
+
+import {INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils';
+
+import WorkspaceWorkflowsApprovalsOverLimitApproverPage from '@pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsOverLimitApproverPage';
+
+import {setApprovalWorkflow} from '@userActions/Workflow';
+
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import SCREENS from '@src/SCREENS';
+import type {Policy} from '@src/types/onyx';
+import type {PersonalDetailsList} from '@src/types/onyx/PersonalDetails';
+
+import type {PropsWithChildren} from 'react';
+
+import React from 'react';
+import Onyx from 'react-native-onyx';
+
+import createMock from '../utils/createMock';
+import {buildPersonalDetails} from '../utils/TestHelper';
+import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
+
+const POLICY_ID = 'workflow-over-limit-approver-test-policy';
+const ADMIN_EMAIL = 'admin@example.com';
+const ADMIN_ACCOUNT_ID = 1;
+const BOB_EMAIL = 'bob@example.com';
+const BOB_ACCOUNT_ID = 2;
+const ERIN_EMAIL = 'erin@example.com';
+const ERIN_ACCOUNT_ID = 3;
+const DAVE_EMAIL = 'dave@example.com';
+const DAVE_ACCOUNT_ID = 4;
+const FRANK_EMAIL = 'frank@example.com';
+const FRANK_ACCOUNT_ID = 5;
+
+// The tests read the rows the page hands the list, so the list itself renders nothing.
+jest.mock('@components/ApproverSelectionList', () => jest.fn(() => null));
+jest.mock('@pages/workspace/AccessOrNotFoundWrapper', () => jest.fn(({children}: PropsWithChildren) => children));
+
+jest.mock('@react-navigation/native', () => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const actualNav = jest.requireActual('@react-navigation/native');
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return {
+ ...actualNav,
+ useNavigationState: () => [],
+ };
+});
+
+jest.mock('@libs/Navigation/Navigation', () => ({
+ goBack: jest.fn(),
+ navigate: jest.fn(),
+}));
+
+function buildPolicy(): Policy {
+ return createMock({
+ id: POLICY_ID,
+ name: 'Test Workspace',
+ type: CONST.POLICY.TYPE.CORPORATE,
+ role: CONST.POLICY.ROLE.ADMIN,
+ owner: ADMIN_EMAIL,
+ approver: ADMIN_EMAIL,
+ approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED,
+ areWorkflowsEnabled: true,
+ employeeList: {
+ [ADMIN_EMAIL]: {email: ADMIN_EMAIL, role: CONST.POLICY.ROLE.ADMIN, submitsTo: ADMIN_EMAIL},
+ [BOB_EMAIL]: {email: BOB_EMAIL, role: CONST.POLICY.ROLE.USER, submitsTo: ADMIN_EMAIL},
+ [ERIN_EMAIL]: {email: ERIN_EMAIL, role: CONST.POLICY.ROLE.USER, submitsTo: ADMIN_EMAIL},
+ [DAVE_EMAIL]: {email: DAVE_EMAIL, role: CONST.POLICY.ROLE.USER, submitsTo: ADMIN_EMAIL},
+ [FRANK_EMAIL]: {email: FRANK_EMAIL, role: CONST.POLICY.ROLE.USER, submitsTo: ADMIN_EMAIL},
+ },
+ });
+}
+
+function buildPersonalDetailsList(): PersonalDetailsList {
+ return {
+ [ADMIN_ACCOUNT_ID]: buildPersonalDetails(ADMIN_EMAIL, ADMIN_ACCOUNT_ID, 'admin'),
+ [BOB_ACCOUNT_ID]: buildPersonalDetails(BOB_EMAIL, BOB_ACCOUNT_ID, 'bob'),
+ [ERIN_ACCOUNT_ID]: buildPersonalDetails(ERIN_EMAIL, ERIN_ACCOUNT_ID, 'erin'),
+ [DAVE_ACCOUNT_ID]: buildPersonalDetails(DAVE_EMAIL, DAVE_ACCOUNT_ID, 'dave'),
+ [FRANK_ACCOUNT_ID]: buildPersonalDetails(FRANK_EMAIL, FRANK_ACCOUNT_ID, 'frank'),
+ };
+}
+
+async function renderPage(approverIndex: number) {
+ const result = render(
+
+
+ ,
+ );
+ await waitForBatchedUpdatesWithAct();
+ return result;
+}
+
+function getRowLogins() {
+ const rows: SelectionListApprover[] = jest.mocked(ApproverSelectionList).mock.lastCall?.[0].allApprovers ?? [];
+ return rows.map((row) => row.login);
+}
+
+describe('WorkspaceWorkflowsApprovalsOverLimitApproverPage', () => {
+ beforeAll(() => {
+ Onyx.init({keys: ONYXKEYS});
+ });
+
+ beforeEach(async () => {
+ jest.mocked(ApproverSelectionList).mockClear();
+ await act(async () => {
+ await Onyx.clear();
+ await Onyx.set(ONYXKEYS.HAS_LOADED_APP, true);
+ await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.MULTIPLE_APPROVERS]);
+ await Onyx.set(ONYXKEYS.IS_LOADING_REPORT_DATA, false);
+ await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy());
+ await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, buildPersonalDetailsList());
+ await Onyx.merge(ONYXKEYS.SESSION, {email: ADMIN_EMAIL, accountID: ADMIN_ACCOUNT_ID});
+ setApprovalWorkflow({
+ ...INITIAL_APPROVAL_WORKFLOW,
+ action: CONST.APPROVAL_WORKFLOW.ACTION.EDIT,
+ members: [{email: DAVE_EMAIL, displayName: DAVE_EMAIL}],
+ approvers: [
+ {email: BOB_EMAIL, displayName: BOB_EMAIL, approvalLimit: 5000},
+ {email: ERIN_EMAIL, displayName: ERIN_EMAIL},
+ ],
+ });
+ });
+ await waitForBatchedUpdatesWithAct();
+ });
+
+ it('never offers an approver of the workflow as the additional approver for reports over the limit', async () => {
+ // Given the rules-based workflow Bob > Erin, where Bob is setting a limit, and Erin already approves every report after him
+
+ // When the admin picks who else approves Bob's reports over the limit
+ await renderPage(0);
+
+ // Then neither Bob nor Erin can be picked, since the limit would add no one, while everyone else can
+ const logins = getRowLogins();
+ expect(logins).not.toContain(BOB_EMAIL);
+ expect(logins).not.toContain(ERIN_EMAIL);
+ expect(logins).toEqual(expect.arrayContaining([ADMIN_EMAIL, DAVE_EMAIL, FRANK_EMAIL]));
+ });
+});