From 3406da3451c8910f877ca1922fc7ef6e03f8df5b Mon Sep 17 00:00:00 2001 From: "Eugene Voloshchak (via MelvinBot)" Date: Mon, 9 Mar 2026 17:27:43 +0000 Subject: [PATCH 1/3] Fix: Filter members with existing workflows from new approval picker Members already assigned to non-default approval workflows were appearing in the "Expenses from" picker when creating a new workflow. Filter them out in addApprovalAction while leaving the edit flow unchanged (it uses mergeWorkflowMembersWithAvailableMembers). Co-authored-by: Eugene Voloshchak --- .../workflows/WorkspaceWorkflowsPage.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index e9ffc40bf3ea9..a2cde6a5d29cc 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -100,6 +100,22 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { localeCompare, }); + // Filter out members who are already assigned to a non-default approval workflow. + // This prevents them from appearing in the "Expenses from" picker when creating a new workflow. + // The edit flow is unaffected as it uses mergeWorkflowMembersWithAvailableMembers separately. + const availableMembersForNewWorkflow = useMemo(() => { + const membersInExistingWorkflows = new Set(); + for (const workflow of approvalWorkflows) { + if (workflow.isDefault) { + continue; + } + for (const member of workflow.members) { + membersInExistingWorkflows.add(member.email); + } + } + return availableMembers.filter((member) => !membersInExistingWorkflows.has(member.email)); + }, [approvalWorkflows, availableMembers]); + const hasValidExistingAccounts = getEligibleExistingBusinessBankAccounts(bankAccountList, policy?.outputCurrency, true).length > 0; const isAdvanceApproval = (approvalWorkflows.length > 1 || (approvalWorkflows?.at(0)?.approvers ?? []).length > 1) && isControlPolicy(policy); @@ -158,7 +174,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const addApprovalAction = useCallback(() => { setApprovalWorkflow({ ...INITIAL_APPROVAL_WORKFLOW, - availableMembers, + availableMembers: availableMembersForNewWorkflow, usedApproverEmails, }); @@ -174,7 +190,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { } Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(route.params.policyID)); - }, [policy, route.params.policyID, availableMembers, usedApproverEmails]); + }, [policy, route.params.policyID, availableMembersForNewWorkflow, usedApproverEmails]); const filteredApprovalWorkflows = policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.ADVANCED || policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.DYNAMICEXTERNAL From b1fa0821034c1ea6f14e946b380450c812d0e8ab Mon Sep 17 00:00:00 2001 From: "Eugene Voloshchak (via MelvinBot)" Date: Wed, 11 Mar 2026 23:27:43 +0000 Subject: [PATCH 2/3] Move member filtering logic to WorkflowUtils.ts Extract filterAvailableMembersForNewWorkflow from WorkspaceWorkflowsPage into WorkflowUtils.ts as a standalone utility function. Co-authored-by: Eugene Voloshchak --- src/libs/WorkflowUtils.ts | 18 ++++++++++++++++++ .../workflows/WorkspaceWorkflowsPage.tsx | 18 ++---------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 61f784d8f2283..bd81db204418c 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -574,6 +574,23 @@ function getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList: BankAccou }); } +/** + * Filter out members who are already assigned to a non-default approval workflow. + * This prevents them from appearing in the "Expenses from" picker when creating a new workflow. + */ +function filterAvailableMembersForNewWorkflow(approvalWorkflows: ApprovalWorkflow[], availableMembers: Member[]): Member[] { + const membersInExistingWorkflows = new Set(); + for (const workflow of approvalWorkflows) { + if (workflow.isDefault) { + continue; + } + for (const member of workflow.members) { + membersInExistingWorkflows.add(member.email); + } + } + return availableMembers.filter((member) => !membersInExistingWorkflows.has(member.email)); +} + /** * Combine workflow members with available members, deduplicating by email. */ @@ -587,6 +604,7 @@ export { calculateApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, + filterAvailableMembersForNewWorkflow, getApprovalLimitDescription, getEligibleExistingBusinessBankAccounts, getOpenConnectedToPolicyBusinessBankAccounts, diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index a2cde6a5d29cc..713dd40fc865c 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -51,7 +51,7 @@ import { } from '@libs/PolicyUtils'; import {hasInProgressVBBA} from '@libs/ReimbursementAccountUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; -import {convertPolicyEmployeesToApprovalWorkflows, getEligibleExistingBusinessBankAccounts, INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils'; +import {convertPolicyEmployeesToApprovalWorkflows, filterAvailableMembersForNewWorkflow, getEligibleExistingBusinessBankAccounts, INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils'; import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import ExpenseReportRulesSection from '@pages/workspace/rules/ExpenseReportRulesSection'; @@ -100,21 +100,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { localeCompare, }); - // Filter out members who are already assigned to a non-default approval workflow. - // This prevents them from appearing in the "Expenses from" picker when creating a new workflow. - // The edit flow is unaffected as it uses mergeWorkflowMembersWithAvailableMembers separately. - const availableMembersForNewWorkflow = useMemo(() => { - const membersInExistingWorkflows = new Set(); - for (const workflow of approvalWorkflows) { - if (workflow.isDefault) { - continue; - } - for (const member of workflow.members) { - membersInExistingWorkflows.add(member.email); - } - } - return availableMembers.filter((member) => !membersInExistingWorkflows.has(member.email)); - }, [approvalWorkflows, availableMembers]); + const availableMembersForNewWorkflow = useMemo(() => filterAvailableMembersForNewWorkflow(approvalWorkflows, availableMembers), [approvalWorkflows, availableMembers]); const hasValidExistingAccounts = getEligibleExistingBusinessBankAccounts(bankAccountList, policy?.outputCurrency, true).length > 0; From eea42d11bebd1e8ee53780cf30b06ef30f329efd Mon Sep 17 00:00:00 2001 From: "Eugene Voloshchak (via MelvinBot)" Date: Wed, 11 Mar 2026 23:38:23 +0000 Subject: [PATCH 3/3] Add unit tests for filterAvailableMembersForNewWorkflow Co-authored-by: Eugene Voloshchak --- tests/unit/WorkflowUtilsTest.ts | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/unit/WorkflowUtilsTest.ts b/tests/unit/WorkflowUtilsTest.ts index 8c32b0cc0a307..794ed8fd7747a 100644 --- a/tests/unit/WorkflowUtilsTest.ts +++ b/tests/unit/WorkflowUtilsTest.ts @@ -4,6 +4,7 @@ import { calculateApprovers, convertApprovalWorkflowToPolicyEmployees, convertPolicyEmployeesToApprovalWorkflows, + filterAvailableMembersForNewWorkflow, getApprovalLimitDescription, getOpenConnectedToPolicyBusinessBankAccounts, mergeWorkflowMembersWithAvailableMembers, @@ -1006,6 +1007,60 @@ describe('WorkflowUtils', () => { }); }); + describe('filterAvailableMembersForNewWorkflow', () => { + it('Should return all members when there are no non-default workflows', () => { + const availableMembers = [buildMember(1), buildMember(2), buildMember(3)]; + const workflows = [buildWorkflow([1, 2, 3], [1], {isDefault: true})]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual(availableMembers); + }); + + it('Should filter out members already in a non-default workflow', () => { + const availableMembers = [buildMember(1), buildMember(2), buildMember(3)]; + const workflows = [buildWorkflow([1, 2, 3], [1], {isDefault: true}), buildWorkflow([2], [4])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual([buildMember(1), buildMember(3)]); + }); + + it('Should filter out members from multiple non-default workflows', () => { + const availableMembers = [buildMember(1), buildMember(2), buildMember(3), buildMember(4)]; + const workflows = [buildWorkflow([1, 2, 3, 4], [1], {isDefault: true}), buildWorkflow([1], [5]), buildWorkflow([3], [6])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual([buildMember(2), buildMember(4)]); + }); + + it('Should return all members when workflows list is empty', () => { + const availableMembers = [buildMember(1), buildMember(2)]; + + const result = filterAvailableMembersForNewWorkflow([], availableMembers); + + expect(result).toEqual(availableMembers); + }); + + it('Should return empty array when all members are in non-default workflows', () => { + const availableMembers = [buildMember(1), buildMember(2)]; + const workflows = [buildWorkflow([1, 2], [3])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual([]); + }); + + it('Should return empty array when available members list is empty', () => { + const workflows = [buildWorkflow([1], [2])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, []); + + expect(result).toEqual([]); + }); + }); + describe('getOpenConnectedToPolicyBusinessBankAccounts', () => { const matchingBankAccountID = 12345;