From 364054bb46ccfc3a5f6036ea2ea748bacdef356d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:57:20 +0100 Subject: [PATCH 01/11] prepare logic for includeAgentsOnly --- src/hooks/usePersonalDetailOptions.ts | 10 +++++++++- src/hooks/usePersonalDetailSearchSelector/base.ts | 9 ++++++++- src/pages/workspace/DynamicWorkspaceInvitePage.tsx | 11 ++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/hooks/usePersonalDetailOptions.ts b/src/hooks/usePersonalDetailOptions.ts index aefd8f4c04bb..ee837f370b0b 100644 --- a/src/hooks/usePersonalDetailOptions.ts +++ b/src/hooks/usePersonalDetailOptions.ts @@ -23,6 +23,11 @@ type UseFilteredOptionsConfig = { enabled?: boolean; /** When set, only the personal details whose login is in this set are turned into options */ includeLoginsOnly?: Set; + /** + * When set, only the agents in this set are turned into options. + * This only applies to agents, other regular accounts are still returned. If you want the list to only contain agents then you should use includeLoginsOnly + */ + includeAgentsOnly?: Set; /* Whether to include report errors in the option data (default: false) */ shouldStoreReportErrors?: boolean; /* Whether to include brick road indicator status in the option data (default: false) */ @@ -128,6 +133,7 @@ type BuildOptionsDataParams = { accountID: number; allPersonalDetails: OnyxEntry; includeLoginsOnly: Set | undefined; + includeAgentsOnly: Set | undefined; reports: OnyxCollection; reportAttributes: FilteredReportAttributes | undefined; reportNameValuePairs: PrivateIsArchivedMap | undefined; @@ -142,6 +148,7 @@ function buildOptionsData({ accountID, allPersonalDetails, includeLoginsOnly, + includeAgentsOnly, reports, reportAttributes, reportNameValuePairs, @@ -179,7 +186,7 @@ function buildOptionsData({ * /> */ function usePersonalDetailOptions(config: UseFilteredOptionsConfig = {}): UseFilteredOptionsResult { - const {enabled = true, includeLoginsOnly, shouldStoreReportErrors = false, shouldShowBrickRoadIndicator = false} = config; + const {enabled = true, includeLoginsOnly, includeAgentsOnly, shouldStoreReportErrors = false, shouldShowBrickRoadIndicator = false} = config; const reportAttributesSelector = createReportAttributesSelector(shouldStoreReportErrors, shouldShowBrickRoadIndicator); @@ -200,6 +207,7 @@ function usePersonalDetailOptions(config: UseFilteredOptionsConfig = {}): UseFil accountID, allPersonalDetails, includeLoginsOnly, + includeAgentsOnly, reports, reportAttributes, reportNameValuePairs, diff --git a/src/hooks/usePersonalDetailSearchSelector/base.ts b/src/hooks/usePersonalDetailSearchSelector/base.ts index 436eb9c41d90..9eea1c2d1370 100644 --- a/src/hooks/usePersonalDetailSearchSelector/base.ts +++ b/src/hooks/usePersonalDetailSearchSelector/base.ts @@ -41,6 +41,12 @@ type UseSearchSelectorConfig = { /** When set, only the logins in this set are turned into options */ includeLoginsOnly?: Set; + /** + * When set, only the agents in this set are turned into options. + * This only applies to agents, other regular accounts are still returned. If you want the list to only contain agents then you should use includeLoginsOnly + */ + includeAgentsOnly?: Set; + includeRecentReports?: boolean; includeCurrentUser?: boolean; includeDomainEmail?: boolean; @@ -187,6 +193,7 @@ function usePersonalDetailSearchSelectorBase({ excludeLogins = CONST.EMPTY_OBJECT, excludeFromSuggestionsOnly = CONST.EMPTY_OBJECT, includeLoginsOnly, + includeAgentsOnly, includeRecentReports = true, onSelectionChange, onSingleSelect, @@ -203,7 +210,7 @@ function usePersonalDetailSearchSelectorBase({ }: UseSearchSelectorConfig): UseSearchSelectorReturn { const {translate, formatPhoneNumber} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {options: defaultOptions, currentOption, isLoading: isPersonalDetailsOptionsLoading} = usePersonalDetailOptions({enabled: shouldInitialize, includeLoginsOnly}); + const {options: defaultOptions, currentOption, isLoading: isPersonalDetailsOptionsLoading} = usePersonalDetailOptions({enabled: shouldInitialize, includeLoginsOnly, includeAgentsOnly}); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const [loginList] = useOnyx(ONYXKEYS.LOGINS, {selector: expensifyLoginsSelector}); diff --git a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx index 2d04d942b0ac..e8384e7df97f 100644 --- a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx +++ b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx @@ -20,6 +20,7 @@ import {setWorkspaceInviteMembersDraft} from '@libs/actions/Policy/Member'; import {clearErrors, openWorkspaceInvitePage as policyOpenWorkspaceInvitePage} from '@libs/actions/Policy/Policy'; import {searchUserInServer} from '@libs/actions/Report'; import {READ_COMMANDS} from '@libs/API/types'; +import {extractCollectionItemID} from '@libs/CollectionUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import getPlatform from '@libs/getPlatform'; import HttpUtils from '@libs/HttpUtils'; @@ -35,14 +36,14 @@ import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, getSoftExclusion import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; +import ONYXKEYS, {OnyxCollectionKey} from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {InvitedEmailsToAccountIDs, Policy} from '@src/types/onyx'; +import type {Agent, InvitedEmailsToAccountIDs, Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; @@ -66,6 +67,9 @@ function DynamicWorkspaceInvitePageContent({route, policy, invitedEmailsToAccoun const [personalDetails] = useAllPersonalDetails(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {})); + const [managedAgentsAccountIDs] = useOnyx(ONYXKEYS.COLLECTION.AGENT, { + selector: (agents: OnyxCollection) => new Set(Object.keys(agents ?? {}).map((key) => Number(extractCollectionItemID(key as `${OnyxCollectionKey}${string}`)))), + }); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList, employeePersonalDetails); policyOpenWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); @@ -139,6 +143,7 @@ function DynamicWorkspaceInvitePageContent({route, policy, invitedEmailsToAccoun initialSelected, initialExtraOptions, shouldKeepSelectedInAvailableOptions: true, + includeAgentsOnly: managedAgentsAccountIDs, }); const sections: Array> = useMemo(() => { From c8a6bd3e2f6cabd860827c64326c487a7a59b23e Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:06:06 +0100 Subject: [PATCH 02/11] add logic for includeAgentsOnly --- src/hooks/usePersonalDetailOptions.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/hooks/usePersonalDetailOptions.ts b/src/hooks/usePersonalDetailOptions.ts index ee837f370b0b..d4e031848d2b 100644 --- a/src/hooks/usePersonalDetailOptions.ts +++ b/src/hooks/usePersonalDetailOptions.ts @@ -157,9 +157,21 @@ function buildOptionsData({ shouldStoreReportErrors, shouldShowBrickRoadIndicator, }: BuildOptionsDataParams) { - const personalDetails = includeLoginsOnly ? filterPersonalDetailsByLogins(allPersonalDetails, includeLoginsOnly) : allPersonalDetails; + let personalDetails = allPersonalDetails; + if (includeLoginsOnly) { + personalDetails = filterPersonalDetailsByLogins(personalDetails, includeLoginsOnly); + } + if (includeAgentsOnly) { + personalDetails = Object.fromEntries( + Object.entries(personalDetails ?? {}).filter(([, details]) => { + if (!details?.isCustomAgent) { + return true; + } + return includeAgentsOnly.has(details.accountID); + }), + ); + } const accountIDToReportIDMap = generateAccountIDToReportIDMap(reports, accountID); - return memoizedCreateOptionList(accountID, personalDetails, accountIDToReportIDMap, reports, reportAttributes, reportNameValuePairs ?? {}, formatPhoneNumber, translate, { shouldStoreReportErrors, shouldShowBrickRoadIndicator, From c6d106bf3e3c1c3ca2e473724cd0e97d0994abbd Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 07:55:40 +0100 Subject: [PATCH 03/11] calculate managedAgentsAccountIDs outside onyx selector --- src/pages/workspace/DynamicWorkspaceInvitePage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx index 816205e0e083..36aa36d018cb 100644 --- a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx +++ b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx @@ -67,9 +67,8 @@ function DynamicWorkspaceInvitePageContent({route, policy, invitedEmailsToAccoun const [personalDetails] = useAllPersonalDetails(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {})); - const [managedAgentsAccountIDs] = useOnyx(ONYXKEYS.COLLECTION.AGENT, { - selector: (agents: OnyxCollection) => new Set(Object.keys(agents ?? {}).map((key) => Number(extractCollectionItemID(key as `${OnyxCollectionKey}${string}`)))), - }); + const [allAgents] = useOnyx(ONYXKEYS.COLLECTION.AGENT); + const managedAgentsAccountIDs = new Set(Object.keys(allAgents ?? {}).map((key) => Number(extractCollectionItemID(key as `${OnyxCollectionKey}${string}`)))); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList, employeePersonalDetails); policyOpenWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); From 34c5b110d00873befad2520be9538ae640ab7556 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:26:28 +0100 Subject: [PATCH 04/11] lint --- src/pages/workspace/DynamicWorkspaceInvitePage.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx index 36aa36d018cb..e0d656a1bbe8 100644 --- a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx +++ b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx @@ -36,17 +36,18 @@ import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, getSoftExclusion import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; -import ONYXKEYS, {OnyxCollectionKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Agent, InvitedEmailsToAccountIDs, Policy} from '@src/types/onyx'; +import type {InvitedEmailsToAccountIDs, Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; +import OnyxKeys from 'react-native-onyx/dist/OnyxKeys'; import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; @@ -68,7 +69,11 @@ function DynamicWorkspaceInvitePageContent({route, policy, invitedEmailsToAccoun const [account] = useOnyx(ONYXKEYS.ACCOUNT); const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {})); const [allAgents] = useOnyx(ONYXKEYS.COLLECTION.AGENT); - const managedAgentsAccountIDs = new Set(Object.keys(allAgents ?? {}).map((key) => Number(extractCollectionItemID(key as `${OnyxCollectionKey}${string}`)))); + const managedAgentsAccountIDs = new Set( + Object.keys(allAgents ?? {}) + .filter((key) => OnyxKeys.isCollectionMemberKey(ONYXKEYS.COLLECTION.AGENT, key)) + .map((key) => Number(extractCollectionItemID(key))), + ); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList, employeePersonalDetails); policyOpenWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); From 7ef0642a36e85bf6f43798cc02c84f00116300c4 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:53:37 +0100 Subject: [PATCH 05/11] extend test to cover includeAgentsOnly --- tests/unit/usePersonalDetailOptionsTest.tsx | 27 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/unit/usePersonalDetailOptionsTest.tsx b/tests/unit/usePersonalDetailOptionsTest.tsx index af335a5b3eb3..4922939c7bfb 100644 --- a/tests/unit/usePersonalDetailOptionsTest.tsx +++ b/tests/unit/usePersonalDetailOptionsTest.tsx @@ -25,6 +25,8 @@ const PERSONAL_DETAILS: PersonalDetailsList = Object.fromEntries( {accountID: 2, login: 'member@example.com', displayName: 'Member'}, {accountID: 3, login: 'outsider@example.com', displayName: 'Outsider'}, {accountID: 4, login: 'other@example.com', displayName: 'Other'}, + {accountID: 5, login: 'agent_5@expensify.ai', displayName: 'Agent A', isCustomAgent: true}, + {accountID: 6, login: 'agent_6@expensify.ai', displayName: 'Agent B', isCustomAgent: true}, ].map((personalDetail) => [personalDetail.accountID, personalDetail]), ); @@ -38,8 +40,8 @@ function Wrapper({children}: {children: React.ReactNode}) { ); } -const renderOptions = async (includeLoginsOnly?: Set) => { - const {result} = renderHook(() => usePersonalDetailOptions({includeLoginsOnly}), {wrapper: Wrapper}); +const renderOptions = async (includeLoginsOnly?: Set, includeAgentsOnly?: Set) => { + const {result} = renderHook(() => usePersonalDetailOptions({includeLoginsOnly, includeAgentsOnly}), {wrapper: Wrapper}); await waitForBatchedUpdatesWithAct(); return result; }; @@ -73,7 +75,14 @@ describe('usePersonalDetailOptions', () => { it('builds an option for every personal detail when no allowlist is given', async () => { const result = await renderOptions(); - expect(loginsOf(result.current.options)).toEqual(['me@example.com', 'member@example.com', 'other@example.com', 'outsider@example.com']); + expect(loginsOf(result.current.options)).toEqual([ + 'agent_5@expensify.ai', + 'agent_6@expensify.ai', + 'me@example.com', + 'member@example.com', + 'other@example.com', + 'outsider@example.com', + ]); expect(result.current.currentOption?.login).toBe(CURRENT_USER_EMAIL); expect(result.current.isLoading).toBe(false); }); @@ -103,4 +112,16 @@ describe('usePersonalDetailOptions', () => { expect(result.current.options).toEqual([]); expect(result.current.isLoading).toBe(false); }); + + it('excludes agents not in agents allow list', async () => { + const result = await renderOptions(undefined, new Set([5])); + + expect(loginsOf(result.current.options)).toEqual(['agent_5@expensify.ai', 'me@example.com', 'member@example.com', 'other@example.com', 'outsider@example.com']); + }); + + it('excludes all agents if allow list is empty', async () => { + const result = await renderOptions(undefined, new Set()); + + expect(loginsOf(result.current.options)).toEqual(['me@example.com', 'member@example.com', 'other@example.com', 'outsider@example.com']); + }); }); From 5f8c14c18effb1e535efc422f5d2cb01c9ae6b1c Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:56:34 +0100 Subject: [PATCH 06/11] rename includeAgentsOnly to agentsWhitelist --- src/hooks/usePersonalDetailOptions.ts | 15 +++++++-------- src/hooks/usePersonalDetailSearchSelector/base.ts | 7 +++---- .../workspace/DynamicWorkspaceInvitePage.tsx | 2 +- tests/unit/usePersonalDetailOptionsTest.tsx | 4 ++-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/hooks/usePersonalDetailOptions.ts b/src/hooks/usePersonalDetailOptions.ts index d4e031848d2b..5159af6a4d38 100644 --- a/src/hooks/usePersonalDetailOptions.ts +++ b/src/hooks/usePersonalDetailOptions.ts @@ -25,9 +25,8 @@ type UseFilteredOptionsConfig = { includeLoginsOnly?: Set; /** * When set, only the agents in this set are turned into options. - * This only applies to agents, other regular accounts are still returned. If you want the list to only contain agents then you should use includeLoginsOnly */ - includeAgentsOnly?: Set; + agentsWhitelist?: Set; /* Whether to include report errors in the option data (default: false) */ shouldStoreReportErrors?: boolean; /* Whether to include brick road indicator status in the option data (default: false) */ @@ -133,7 +132,7 @@ type BuildOptionsDataParams = { accountID: number; allPersonalDetails: OnyxEntry; includeLoginsOnly: Set | undefined; - includeAgentsOnly: Set | undefined; + agentsWhitelist: Set | undefined; reports: OnyxCollection; reportAttributes: FilteredReportAttributes | undefined; reportNameValuePairs: PrivateIsArchivedMap | undefined; @@ -148,7 +147,7 @@ function buildOptionsData({ accountID, allPersonalDetails, includeLoginsOnly, - includeAgentsOnly, + agentsWhitelist, reports, reportAttributes, reportNameValuePairs, @@ -161,13 +160,13 @@ function buildOptionsData({ if (includeLoginsOnly) { personalDetails = filterPersonalDetailsByLogins(personalDetails, includeLoginsOnly); } - if (includeAgentsOnly) { + if (agentsWhitelist) { personalDetails = Object.fromEntries( Object.entries(personalDetails ?? {}).filter(([, details]) => { if (!details?.isCustomAgent) { return true; } - return includeAgentsOnly.has(details.accountID); + return agentsWhitelist.has(details.accountID); }), ); } @@ -198,7 +197,7 @@ function buildOptionsData({ * /> */ function usePersonalDetailOptions(config: UseFilteredOptionsConfig = {}): UseFilteredOptionsResult { - const {enabled = true, includeLoginsOnly, includeAgentsOnly, shouldStoreReportErrors = false, shouldShowBrickRoadIndicator = false} = config; + const {enabled = true, includeLoginsOnly, agentsWhitelist, shouldStoreReportErrors = false, shouldShowBrickRoadIndicator = false} = config; const reportAttributesSelector = createReportAttributesSelector(shouldStoreReportErrors, shouldShowBrickRoadIndicator); @@ -219,7 +218,7 @@ function usePersonalDetailOptions(config: UseFilteredOptionsConfig = {}): UseFil accountID, allPersonalDetails, includeLoginsOnly, - includeAgentsOnly, + agentsWhitelist, reports, reportAttributes, reportNameValuePairs, diff --git a/src/hooks/usePersonalDetailSearchSelector/base.ts b/src/hooks/usePersonalDetailSearchSelector/base.ts index 9eea1c2d1370..c1db8b56dcc8 100644 --- a/src/hooks/usePersonalDetailSearchSelector/base.ts +++ b/src/hooks/usePersonalDetailSearchSelector/base.ts @@ -43,9 +43,8 @@ type UseSearchSelectorConfig = { /** * When set, only the agents in this set are turned into options. - * This only applies to agents, other regular accounts are still returned. If you want the list to only contain agents then you should use includeLoginsOnly */ - includeAgentsOnly?: Set; + agentsWhitelist?: Set; includeRecentReports?: boolean; includeCurrentUser?: boolean; @@ -193,7 +192,7 @@ function usePersonalDetailSearchSelectorBase({ excludeLogins = CONST.EMPTY_OBJECT, excludeFromSuggestionsOnly = CONST.EMPTY_OBJECT, includeLoginsOnly, - includeAgentsOnly, + agentsWhitelist, includeRecentReports = true, onSelectionChange, onSingleSelect, @@ -210,7 +209,7 @@ function usePersonalDetailSearchSelectorBase({ }: UseSearchSelectorConfig): UseSearchSelectorReturn { const {translate, formatPhoneNumber} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {options: defaultOptions, currentOption, isLoading: isPersonalDetailsOptionsLoading} = usePersonalDetailOptions({enabled: shouldInitialize, includeLoginsOnly, includeAgentsOnly}); + const {options: defaultOptions, currentOption, isLoading: isPersonalDetailsOptionsLoading} = usePersonalDetailOptions({enabled: shouldInitialize, includeLoginsOnly, agentsWhitelist}); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const [loginList] = useOnyx(ONYXKEYS.LOGINS, {selector: expensifyLoginsSelector}); diff --git a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx index e0d656a1bbe8..51b69b3dac4e 100644 --- a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx +++ b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx @@ -147,7 +147,7 @@ function DynamicWorkspaceInvitePageContent({route, policy, invitedEmailsToAccoun initialSelected, initialExtraOptions, shouldKeepSelectedInAvailableOptions: true, - includeAgentsOnly: managedAgentsAccountIDs, + agentsWhitelist: managedAgentsAccountIDs, }); const sections: Array> = useMemo(() => { diff --git a/tests/unit/usePersonalDetailOptionsTest.tsx b/tests/unit/usePersonalDetailOptionsTest.tsx index 4922939c7bfb..798f68bb0386 100644 --- a/tests/unit/usePersonalDetailOptionsTest.tsx +++ b/tests/unit/usePersonalDetailOptionsTest.tsx @@ -40,8 +40,8 @@ function Wrapper({children}: {children: React.ReactNode}) { ); } -const renderOptions = async (includeLoginsOnly?: Set, includeAgentsOnly?: Set) => { - const {result} = renderHook(() => usePersonalDetailOptions({includeLoginsOnly, includeAgentsOnly}), {wrapper: Wrapper}); +const renderOptions = async (includeLoginsOnly?: Set, agentsWhitelist?: Set) => { + const {result} = renderHook(() => usePersonalDetailOptions({includeLoginsOnly, agentsWhitelist}), {wrapper: Wrapper}); await waitForBatchedUpdatesWithAct(); return result; }; From 860bbd227f347766f9fa53b468e36a4827c01f2b Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:22:49 +0100 Subject: [PATCH 07/11] use useAgent instead of useOnyx --- src/pages/workspace/DynamicWorkspaceInvitePage.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx index 51b69b3dac4e..f62fc1f0fa96 100644 --- a/src/pages/workspace/DynamicWorkspaceInvitePage.tsx +++ b/src/pages/workspace/DynamicWorkspaceInvitePage.tsx @@ -7,6 +7,7 @@ import SelectionListWithSections from '@components/SelectionList/SelectionListWi import type {Section} from '@components/SelectionList/SelectionListWithSections/types'; import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; +import useAgents from '@hooks/useAgents'; import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -20,7 +21,6 @@ import {setWorkspaceInviteMembersDraft} from '@libs/actions/Policy/Member'; import {clearErrors, openWorkspaceInvitePage as policyOpenWorkspaceInvitePage} from '@libs/actions/Policy/Policy'; import {searchUserInServer} from '@libs/actions/Report'; import {READ_COMMANDS} from '@libs/API/types'; -import {extractCollectionItemID} from '@libs/CollectionUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import getPlatform from '@libs/getPlatform'; import HttpUtils from '@libs/HttpUtils'; @@ -47,7 +47,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; -import OnyxKeys from 'react-native-onyx/dist/OnyxKeys'; import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; @@ -68,12 +67,9 @@ function DynamicWorkspaceInvitePageContent({route, policy, invitedEmailsToAccoun const [personalDetails] = useAllPersonalDetails(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {})); - const [allAgents] = useOnyx(ONYXKEYS.COLLECTION.AGENT); - const managedAgentsAccountIDs = new Set( - Object.keys(allAgents ?? {}) - .filter((key) => OnyxKeys.isCollectionMemberKey(ONYXKEYS.COLLECTION.AGENT, key)) - .map((key) => Number(extractCollectionItemID(key))), - ); + const {agents} = useAgents(); + const agentsAccountIDs = new Set(agents.map(({accountID}) => accountID)); + const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList, employeePersonalDetails); policyOpenWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); @@ -147,7 +143,7 @@ function DynamicWorkspaceInvitePageContent({route, policy, invitedEmailsToAccoun initialSelected, initialExtraOptions, shouldKeepSelectedInAvailableOptions: true, - agentsWhitelist: managedAgentsAccountIDs, + agentsWhitelist: agentsAccountIDs, }); const sections: Array> = useMemo(() => { From 7b9bba693e5210bed4299480a697c53602b43840 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:37:41 +0100 Subject: [PATCH 08/11] add filterAgentsPersonalDetailsByAccountIDs --- src/hooks/usePersonalDetailOptions.ts | 11 ++--------- src/libs/PersonalDetailOptionsListUtils/index.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/hooks/usePersonalDetailOptions.ts b/src/hooks/usePersonalDetailOptions.ts index 5159af6a4d38..6947c831128b 100644 --- a/src/hooks/usePersonalDetailOptions.ts +++ b/src/hooks/usePersonalDetailOptions.ts @@ -2,7 +2,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import memoize, {equivalentArgsComparator} from '@libs/memoize'; -import {createOptionList, filterPersonalDetailsByLogins} from '@libs/PersonalDetailOptionsListUtils'; +import {createOptionList, filterAgentsPersonalDetailsByAccountIDs, filterPersonalDetailsByLogins} from '@libs/PersonalDetailOptionsListUtils'; import type {OptionData, PrivateIsArchivedMap} from '@libs/PersonalDetailOptionsListUtils/types'; import {isOneOnOneChat, isSelfDM} from '@libs/ReportUtils'; import {registerSessionCleanupCallback} from '@libs/SessionCleanup'; @@ -161,14 +161,7 @@ function buildOptionsData({ personalDetails = filterPersonalDetailsByLogins(personalDetails, includeLoginsOnly); } if (agentsWhitelist) { - personalDetails = Object.fromEntries( - Object.entries(personalDetails ?? {}).filter(([, details]) => { - if (!details?.isCustomAgent) { - return true; - } - return agentsWhitelist.has(details.accountID); - }), - ); + personalDetails = filterAgentsPersonalDetailsByAccountIDs(personalDetails, agentsWhitelist); } const accountIDToReportIDMap = generateAccountIDToReportIDMap(reports, accountID); return memoizedCreateOptionList(accountID, personalDetails, accountIDToReportIDMap, reports, reportAttributes, reportNameValuePairs ?? {}, formatPhoneNumber, translate, { diff --git a/src/libs/PersonalDetailOptionsListUtils/index.ts b/src/libs/PersonalDetailOptionsListUtils/index.ts index a7d79f745d12..ec3377b77c8b 100644 --- a/src/libs/PersonalDetailOptionsListUtils/index.ts +++ b/src/libs/PersonalDetailOptionsListUtils/index.ts @@ -500,6 +500,21 @@ function filterPersonalDetailsByLogins(personalDetails: OnyxEntry, accountIDs: Set): PersonalDetailsList { + const filteredPersonalDetails: PersonalDetailsList = {}; + for (const [accountID, personalDetail] of Object.entries(personalDetails ?? {})) { + if (personalDetail?.isCustomAgent && !accountIDs.has(Number(accountID))) { + continue; + } + filteredPersonalDetails[accountID] = personalDetail; + } + return filteredPersonalDetails; +} + /** * Helper method that returns the text to be used for the header's message and title (if any) */ @@ -565,6 +580,7 @@ export { getValidOptions, createOptionList, filterPersonalDetailsByLogins, + filterAgentsPersonalDetailsByAccountIDs, getHeaderMessage, getUserToInviteOption, getFilteredRecentAttendees, From a28079c83a98772afab5a1304ee6bf1e56865de3 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:39:52 +0100 Subject: [PATCH 09/11] update comment --- src/hooks/usePersonalDetailOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/usePersonalDetailOptions.ts b/src/hooks/usePersonalDetailOptions.ts index 6947c831128b..91a578764003 100644 --- a/src/hooks/usePersonalDetailOptions.ts +++ b/src/hooks/usePersonalDetailOptions.ts @@ -112,7 +112,7 @@ const createReportAttributesSelector = /** * Building an option per personal details entry is the expensive step of this hook and depends only on Onyx values. * Arguments are compared one level deep because the maps are rebuilt each render from those same unchanged values. - * A single entry is held, so a consumer narrowing the list with `includeLoginsOnly` evicts the whole-list one. + * A single entry is held, so a consumer narrowing the list with `includeLoginsOnly` or `agentsWhitelist` evicts the whole-list one. */ const memoizedCreateOptionList = memoize(createOptionList, { maxSize: 1, From 0e5d1c981dfe4b8e1bc3381fab0027211091d34a Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:24:18 +0100 Subject: [PATCH 10/11] spellcheck --- src/libs/PersonalDetailOptionsListUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PersonalDetailOptionsListUtils/index.ts b/src/libs/PersonalDetailOptionsListUtils/index.ts index ec3377b77c8b..296281ccd778 100644 --- a/src/libs/PersonalDetailOptionsListUtils/index.ts +++ b/src/libs/PersonalDetailOptionsListUtils/index.ts @@ -502,7 +502,7 @@ function filterPersonalDetailsByLogins(personalDetails: OnyxEntry, accountIDs: Set): PersonalDetailsList { const filteredPersonalDetails: PersonalDetailsList = {}; From 3bbc2f327d0df2f85b8166fae9418f962fc5f882 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:15:05 +0100 Subject: [PATCH 11/11] add beta badge to workspace agents --- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- src/pages/workspace/getWorkspaceMenuItems.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index f7c6489b674d..923024fd1e18 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -329,7 +329,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac highlighted={!!item?.highlighted} focused={getItemScreenNames(item).some((screenName) => activeRoute?.startsWith(screenName))} role={CONST.ROLE.TAB} - badgeText={item.badgeText} + badgeText={item.badgeTranslationKey ? translate(item.badgeTranslationKey) : item.badgeText} shouldIconUseAutoWidthStyle sentryLabel={item.sentryLabel} shouldGreyOutWhenDisabled={hasPolicyCreationError} diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index e22df7e3f2a8..3b094e1eb52d 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -85,6 +85,7 @@ type WorkspaceMenuItem = WithSentryLabel & { getRoute: () => Route; brickRoadIndicator?: ValueOf; screenName: WorkspaceTopLevelScreens; + badgeTranslationKey?: TranslationPaths; badgeText?: string; highlighted?: boolean; @@ -265,6 +266,7 @@ function getWorkspaceMenuItems({ getRoute: () => ROUTES.WORKSPACE_AGENTS.getRoute(policyID), screenName: SCREENS.WORKSPACE.AGENTS, sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.AGENTS, + badgeTranslationKey: 'common.beta', }); }