From 7a4ba4bb6110ee8c7edd969dac66933458aa0608 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Tue, 16 Dec 2025 16:59:16 +0100 Subject: [PATCH 01/14] add members page to domains --- src/ONYXKEYS.ts | 4 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 4 + src/libs/DomainUtils.ts | 31 +++ .../Navigators/DomainSplitNavigator.tsx | 8 + .../linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/pages/domain/Admins/DomainAdminsPage.tsx | 132 ++---------- src/pages/domain/BaseDomainMembersPage.tsx | 195 ++++++++++++++++++ .../domain/Members/DomainMembersPage.tsx | 44 ++++ 12 files changed, 315 insertions(+), 115 deletions(-) create mode 100644 src/libs/DomainUtils.ts create mode 100644 src/pages/domain/BaseDomainMembersPage.tsx create mode 100644 src/pages/domain/Members/DomainMembersPage.tsx diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2a7e41df0b04..6eac4637675a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -737,6 +737,9 @@ const ONYXKEYS = { /** Stores domain admin account ID */ EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_', + + /** Stores domain security group */ + DOMAIN_SECURITY_GROUP: 'expensify_securityGroup', }, /** List of Form ids */ @@ -1124,6 +1127,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; [ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata; [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number; + [ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP] : number; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b065091c0eb7..0da4b6f3f22f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3452,6 +3452,10 @@ const ROUTES = { route: 'domain/:domainAccountID/admins', getRoute: (domainAccountID: number) => `domain/${domainAccountID}/admins` as const, }, + DOMAIN_MEMBERS: { + route: 'domain/:accountID/members', + getRoute: (accountID: number) => `domain/${accountID}/members` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2e074b7bde2a..7ad585961c8d 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -853,6 +853,7 @@ const SCREENS = { INITIAL: 'Domain_Initial', SAML: 'Domain_SAML', ADMINS: 'Domain_Admins', + MEMBERS: 'Domain_Members', }, } as const; diff --git a/src/languages/en.ts b/src/languages/en.ts index 8f20c1f98ff5..ab195c7f7db8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7840,6 +7840,10 @@ const translations = { title: 'Admins', findAdmin: 'Find admin', }, + members:{ + title: 'Members', + findMember: 'Find member', + } }, }; diff --git a/src/libs/DomainUtils.ts b/src/libs/DomainUtils.ts new file mode 100644 index 000000000000..8ce192e98cbb --- /dev/null +++ b/src/libs/DomainUtils.ts @@ -0,0 +1,31 @@ +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; + +/** + * Extracts a list of member IDs (accountIDs) from the domain object. + * * It iterates through the security groups in the domain, extracts account IDs from the 'shared' property, + * and returns a unique list of numbers. + * + * @param domain - The domain object from Onyx + * @returns An array of unique member account IDs + */ +function selectMemberIDs(domain: OnyxTypes.Domain | undefined): number[] { + if (!domain) { + return []; + } + + const memberIDs = Object.entries(domain) + .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP)) + .flatMap(([, value]) => { + const groupData = value as {shared?: Record}; + if (!groupData?.shared) { + return []; + } + return Object.keys(groupData.shared); + }) + .map((id) => Number(id)) + .filter((id) => !Number.isNaN(id)); + return [...new Set(memberIDs)]; +} + +export default selectMemberIDs; diff --git a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx index c7d883abc54c..1b1bd2260715 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx @@ -14,6 +14,8 @@ import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; const loadDomainInitialPage = () => require('../../../../pages/domain/DomainInitialPage').default; const loadDomainSamlPage = () => require('../../../../pages/domain/DomainSamlPage').default; const loadDomainAdminsPage = () => require('../../../../pages/domain/Admins/DomainAdminsPage').default; +const loadDomainMembersPage = () => require('../../../../pages/domain/members/DomainMembersPage').default; + const Split = createSplitNavigator(); @@ -50,6 +52,12 @@ function DomainSplitNavigator({route, navigation}: PlatformStackScreenProps + + diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts index b4b63e99d5b8..1a5c2dda42d4 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts @@ -5,6 +5,7 @@ import SCREENS from '@src/SCREENS'; const DOMAIN_TO_RHP: Partial> = { [SCREENS.DOMAIN.INITIAL]: [], [SCREENS.DOMAIN.SAML]: [SCREENS.DOMAIN.VERIFY, SCREENS.DOMAIN.VERIFIED], + [SCREENS.DOMAIN.MEMBERS]: [], }; export default DOMAIN_TO_RHP; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 9a222edfd068..3e524117f3fd 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1930,6 +1930,9 @@ const config: LinkingOptions['config'] = { [SCREENS.DOMAIN.ADMINS]: { path: ROUTES.DOMAIN_ADMINS.route, }, + [SCREENS.DOMAIN.MEMBERS]: { + path: ROUTES.DOMAIN_MEMBERS.route, + }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 6a3b78395267..64d05893bcb6 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2415,6 +2415,9 @@ type DomainSplitNavigatorParamList = { [SCREENS.DOMAIN.ADMINS]: { domainAccountID: number; }; + [SCREENS.DOMAIN.MEMBERS]: { + accountID: number; + }; }; type OnboardingModalNavigatorParamList = { diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 76ff9711127d..e7c60ae9712e 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,104 +1,29 @@ -import {adminAccountIDsSelector} from '@selectors/Domain'; +import { adminAccountIDsSelector } from '@selectors/Domain'; import React from 'react'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SearchBar from '@components/SearchBar'; -import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; -import SelectionList from '@components/SelectionListWithSections'; -import TableListItem from '@components/SelectionListWithSections/TableListItem'; -import type {ListItem} from '@components/SelectionListWithSections/types'; -import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSearchResults from '@hooks/useSearchResults'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {sortAlphabetically} from '@libs/OptionsListUtils'; -import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; -import tokenizedSearch from '@libs/tokenizedSearch'; -import Navigation from '@navigation/Navigation'; -import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; -import type {DomainSplitNavigatorParamList} from '@navigation/types'; -import {getCurrentUserAccountID} from '@userActions/Report'; -import CONST from '@src/CONST'; +import type { PlatformStackScreenProps } from '@navigation/PlatformStackNavigation/types'; +import type { DomainSplitNavigatorParamList } from '@navigation/types'; +import { getCurrentUserAccountID } from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; -type DomainAdminsPageProps = PlatformStackScreenProps; -type AdminOption = Omit & { - accountID: number; - login: string; -}; +type DomainAdminsPageProps = PlatformStackScreenProps; function DomainAdminsPage({route}: DomainAdminsPageProps) { const {domainAccountID} = route.params; + const {translate} = useLocalize(); - const {translate, formatPhoneNumber, localeCompare} = useLocalize(); - const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations(['Members']); - const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true}); const [adminAccountIDs, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: adminAccountIDsSelector, }); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - - const data: AdminOption[] = []; - for (const accountID of adminAccountIDs ?? []) { - const details = personalDetails?.[accountID]; - data.push({ - keyForList: String(accountID), - accountID, - login: details?.login ?? '', - text: formatPhoneNumber(getDisplayNameOrDefault(details)), - alternateText: formatPhoneNumber(details?.login ?? ''), - icons: [ - { - source: details?.avatar ?? icons.FallbackAvatar, - name: formatPhoneNumber(details?.login ?? ''), - type: CONST.ICON_TYPE_AVATAR, - id: accountID, - }, - ], - }); - } - - const filterMember = (adminOption: AdminOption, searchQuery: string) => { - const results = tokenizedSearch([adminOption], searchQuery, (option) => [option.text ?? '', option.alternateText ?? '']); - return results.length > 0; - }; - const sortMembers = (adminOptions: AdminOption[]) => sortAlphabetically(adminOptions, 'text', localeCompare); - const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers); - - const getCustomListHeader = () => { - if (filteredData.length === 0) { - return null; - } - - return ( - - ); - }; - - const listHeaderContent = - data.length > CONST.SEARCH_ITEM_LIMIT ? ( - - ) : null; if (isLoadingOnyxValue(domainMetadata)) { return ; @@ -108,38 +33,15 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const isAdmin = adminAccountIDs?.includes(currentUserAccountID); return ( - - Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} - shouldShow={!isAdmin} - shouldForceFullScreen - > - - {}} - shouldShowListEmptyContent={false} - listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]} - showScrollIndicator={false} - addBottomSafeAreaPadding - customListHeader={getCustomListHeader()} - /> - - + {}} + shouldShowLoading={!isAdmin} + /> ); } diff --git a/src/pages/domain/BaseDomainMembersPage.tsx b/src/pages/domain/BaseDomainMembersPage.tsx new file mode 100644 index 000000000000..5622670b0a87 --- /dev/null +++ b/src/pages/domain/BaseDomainMembersPage.tsx @@ -0,0 +1,195 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {FallbackAvatar} from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import SearchBar from '@components/SearchBar'; +import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; +import SelectionList from '@components/SelectionListWithSections'; +import TableListItem from '@components/SelectionListWithSections/TableListItem'; +import type {ListItem} from '@components/SelectionListWithSections/types'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchResults from '@hooks/useSearchResults'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getLatestError} from '@libs/ErrorUtils'; +import {sortAlphabetically} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import tokenizedSearch from '@libs/tokenizedSearch'; +import Navigation from '@navigation/Navigation'; +import {clearAddAdminError, clearRemoveAdminError} from '@userActions/Domain'; +import CONST from '@src/CONST'; +import {getAdminKey} from '@src/libs/DomainUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; + +type MemberOption = Omit & { + accountID: number; + login: string; +}; + +type BaseDomainMembersPageProps = { + /** The domain ID */ + domainID: number; + + /** The domain object from Onyx (required for error handling logic) */ + domain: OnyxTypes.Domain | undefined; + + /** The list of accountIDs to display (Admins or Members) */ + accountIDs: number[]; + + /** The title of the header */ + headerTitle: string; + + /** Placeholder text for the search bar */ + searchPlaceholder: string; + + /** Content to display in the header (e.g., Add/Settings buttons) */ + headerContent?: React.ReactNode; + + /** Callback fired when a row is selected */ + onSelectRow: (item: MemberOption) => void; + + /** Whether to show the loading state (blocking view) */ + shouldShowLoading?: boolean; +}; + +function BaseDomainMembersPage({ + domainID, + domain, + accountIDs, + headerTitle, + searchPlaceholder, + headerContent, + onSelectRow, + shouldShowLoading = false, + }: BaseDomainMembersPageProps) { + const {formatPhoneNumber, localeCompare} = useLocalize(); + const styles = useThemeStyles(); + const illustrations = useMemoizedLazyIllustrations(['LaptopOnDeskWithCoffeeAndKey', 'LockClosed', 'OpenSafe', 'ShieldYellow', 'Members'] as const); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + const [domainPendingActions] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainID}`, {canBeMissing: true}); + const [domainErrors] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainID}`, {canBeMissing: true}); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + + const data: MemberOption[] = []; + for (const accountID of accountIDs) { + const details = personalDetails?.[accountID]; + data.push({ + keyForList: String(accountID), + accountID, + login: details?.login ?? '', + text: formatPhoneNumber(getDisplayNameOrDefault(details)), + alternateText: formatPhoneNumber(details?.login ?? ''), + icons: [ + { + source: details?.avatar ?? FallbackAvatar, + name: formatPhoneNumber(details?.login ?? ''), + type: CONST.ICON_TYPE_AVATAR, + id: accountID, + }, + ], + pendingAction: domainPendingActions?.admin?.[accountID], + errors: getLatestError(domainErrors?.adminErrors?.[accountID]), + }); + } + + const filterMember = useCallback((option: MemberOption, searchQuery: string) => { + const results = tokenizedSearch([option], searchQuery, (item) => [item.text ?? '', item.alternateText ?? '']); + return results.length > 0; + }, []); + + const sortMembers = useCallback( + (options: MemberOption[]) => sortAlphabetically(options, 'text', localeCompare), + [localeCompare], + ); + + const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers); + + const getCustomListHeader = () => { + if (filteredData.length === 0) { + return null; + } + return ; + }; + + return ( + + Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} + shouldShow={shouldShowLoading} + shouldForceFullScreen + shouldDisplaySearchRouter + > + + {!shouldUseNarrowLayout && !!headerContent && ( + {headerContent} + )} + + + {shouldUseNarrowLayout && !!headerContent && ( + {headerContent} + )} + + + -1 ? ( + + ) : null + } + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + ListItem={TableListItem} + onSelectRow={onSelectRow} + shouldShowListEmptyContent={false} + listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]} + showScrollIndicator={false} + addBottomSafeAreaPadding + customListHeader={getCustomListHeader()} + onDismissError={(item) => { + const adminKey = getAdminKey(domain, item.accountID); + if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + clearAddAdminError(domainID, item.accountID); + } else if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && adminKey) { + clearRemoveAdminError(domainID, item.accountID, adminKey); + } + }} + /> + + + + ); +} + +BaseDomainMembersPage.displayName = 'BaseDomainMembersPage'; + +export type {MemberOption}; +export default BaseDomainMembersPage; diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx new file mode 100644 index 000000000000..701fc001dfaa --- /dev/null +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {DomainSplitNavigatorParamList} from '@navigation/types'; +import selectMemberIDs from '@src/libs/DomainUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage' + +type DomainMembersPageProps = PlatformStackScreenProps; + +function DomainMembersPage({route}: DomainMembersPageProps) { + const domainID = route.params.accountID; + const {translate} = useLocalize(); + + const [domain, fetchStatus] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, {canBeMissing: false}); + + const [memberIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, { + canBeMissing: true, + selector: selectMemberIDs, + }); + + const shouldShowLoading = fetchStatus.status !== 'loading' && !domain; + + + return ( + {}} + shouldShowLoading={shouldShowLoading} + /> + ); +} + +DomainMembersPage.displayName = 'DomainMembersPage'; + +export default DomainMembersPage; From 276fd9e7d0d2a663b47a88cd5d3dece13f497355 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Wed, 17 Dec 2025 11:34:55 +0100 Subject: [PATCH 02/14] fix general members component after marge from main --- src/ROUTES.ts | 4 +- src/libs/Navigation/types.ts | 2 +- src/pages/domain/Admins/DomainAdminsPage.tsx | 28 ++-- src/pages/domain/BaseDomainMembersPage.tsx | 133 +++++++----------- .../domain/Members/DomainMembersPage.tsx | 18 +-- 5 files changed, 77 insertions(+), 108 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5dcb5459b87f..dbabd1274e82 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3457,8 +3457,8 @@ const ROUTES = { getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/admins/${accountID}` as const, }, DOMAIN_MEMBERS: { - route: 'domain/:accountID/members', - getRoute: (accountID: number) => `domain/${accountID}/members` as const, + route: 'domain/:domainAccountID/members', + getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members` as const, }, } as const; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 90724fc8910c..c7e3d5f5d8f0 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2420,7 +2420,7 @@ type DomainSplitNavigatorParamList = { domainAccountID: number; }; [SCREENS.DOMAIN.MEMBERS]: { - accountID: number; + domainAccountID: number; }; }; diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 51b6f7881490..f2084756bde4 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,16 +1,18 @@ -import { adminAccountIDsSelector } from '@selectors/Domain'; +import {adminAccountIDsSelector} from '@selectors/Domain'; import React from 'react'; +import type {MemberOption} from '@pages/domain/BaseDomainMembersPage'; +import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import type { PlatformStackScreenProps } from '@navigation/PlatformStackNavigation/types'; -import type { DomainSplitNavigatorParamList } from '@navigation/types'; -import { getCurrentUserAccountID } from '@userActions/Report'; +import Navigation from '@navigation/Navigation'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {DomainSplitNavigatorParamList} from '@navigation/types'; +import {getCurrentUserAccountID} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; - type DomainAdminsPageProps = PlatformStackScreenProps; @@ -18,8 +20,6 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const {domainAccountID} = route.params; const {translate} = useLocalize(); - const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true}); - const [adminAccountIDs, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: adminAccountIDsSelector, @@ -30,17 +30,19 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { } const currentUserAccountID = getCurrentUserAccountID(); - const isAdmin = adminAccountIDs?.includes(currentUserAccountID); + const isUserAdmin = adminAccountIDs?.includes(currentUserAccountID); + + const handleSelectRow = (item: MemberOption) => { + Navigation.navigate(ROUTES.DOMAIN_ADMIN_DETAILS.getRoute(domainAccountID, item.accountID)); + }; return ( {}} - shouldShowLoading={!isAdmin} + onSelectRow={handleSelectRow} + shouldShowNotFoundView={!isUserAdmin} /> ); } diff --git a/src/pages/domain/BaseDomainMembersPage.tsx b/src/pages/domain/BaseDomainMembersPage.tsx index 5622670b0a87..826b7335a697 100644 --- a/src/pages/domain/BaseDomainMembersPage.tsx +++ b/src/pages/domain/BaseDomainMembersPage.tsx @@ -1,10 +1,9 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollViewWithContext from '@components/ScrollViewWithContext'; import SearchBar from '@components/SearchBar'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import SelectionList from '@components/SelectionListWithSections'; @@ -16,17 +15,13 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getLatestError} from '@libs/ErrorUtils'; import {sortAlphabetically} from '@libs/OptionsListUtils'; import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; import Navigation from '@navigation/Navigation'; -import {clearAddAdminError, clearRemoveAdminError} from '@userActions/Domain'; import CONST from '@src/CONST'; -import {getAdminKey} from '@src/libs/DomainUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; type MemberOption = Omit & { accountID: number; @@ -34,13 +29,7 @@ type MemberOption = Omit & { }; type BaseDomainMembersPageProps = { - /** The domain ID */ - domainID: number; - - /** The domain object from Onyx (required for error handling logic) */ - domain: OnyxTypes.Domain | undefined; - - /** The list of accountIDs to display (Admins or Members) */ + /** The list of accountIDs to display */ accountIDs: number[]; /** The title of the header */ @@ -55,50 +44,46 @@ type BaseDomainMembersPageProps = { /** Callback fired when a row is selected */ onSelectRow: (item: MemberOption) => void; - /** Whether to show the loading state (blocking view) */ - shouldShowLoading?: boolean; + /** Whether to show the Not Found View (e.g. if user is not admin, or data is missing) */ + shouldShowNotFoundView?: boolean; }; function BaseDomainMembersPage({ - domainID, - domain, accountIDs, headerTitle, searchPlaceholder, headerContent, onSelectRow, - shouldShowLoading = false, + shouldShowNotFoundView = false, }: BaseDomainMembersPageProps) { const {formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations(['LaptopOnDeskWithCoffeeAndKey', 'LockClosed', 'OpenSafe', 'ShieldYellow', 'Members'] as const); + const illustrations = useMemoizedLazyIllustrations(['Members']); const {shouldUseNarrowLayout} = useResponsiveLayout(); - - const [domainPendingActions] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainID}`, {canBeMissing: true}); - const [domainErrors] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainID}`, {canBeMissing: true}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const data: MemberOption[] = []; - for (const accountID of accountIDs) { - const details = personalDetails?.[accountID]; - data.push({ - keyForList: String(accountID), - accountID, - login: details?.login ?? '', - text: formatPhoneNumber(getDisplayNameOrDefault(details)), - alternateText: formatPhoneNumber(details?.login ?? ''), - icons: [ - { - source: details?.avatar ?? FallbackAvatar, - name: formatPhoneNumber(details?.login ?? ''), - type: CONST.ICON_TYPE_AVATAR, - id: accountID, - }, - ], - pendingAction: domainPendingActions?.admin?.[accountID], - errors: getLatestError(domainErrors?.adminErrors?.[accountID]), - }); - } + const data: MemberOption[] = useMemo(() => { + const options: MemberOption[] = []; + for (const accountID of accountIDs) { + const details = personalDetails?.[accountID]; + options.push({ + keyForList: String(accountID), + accountID, + login: details?.login ?? '', + text: formatPhoneNumber(getDisplayNameOrDefault(details)), + alternateText: formatPhoneNumber(details?.login ?? ''), + icons: [ + { + source: details?.avatar ?? FallbackAvatar, + name: formatPhoneNumber(details?.login ?? ''), + type: CONST.ICON_TYPE_AVATAR, + id: accountID, + }, + ], + }); + } + return options; + }, [accountIDs, personalDetails, formatPhoneNumber]); const filterMember = useCallback((option: MemberOption, searchQuery: string) => { const results = tokenizedSearch([option], searchQuery, (item) => [item.text ?? '', item.alternateText ?? '']); @@ -119,6 +104,15 @@ function BaseDomainMembersPage({ return ; }; + const listHeaderContent = data.length > CONST.SEARCH_ITEM_LIMIT ? ( + + ) : null; + return ( Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} - shouldShow={shouldShowLoading} + shouldShow={shouldShowNotFoundView} shouldForceFullScreen - shouldDisplaySearchRouter > {headerContent} )} - - -1 ? ( - - ) : null - } - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - ListItem={TableListItem} - onSelectRow={onSelectRow} - shouldShowListEmptyContent={false} - listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]} - showScrollIndicator={false} - addBottomSafeAreaPadding - customListHeader={getCustomListHeader()} - onDismissError={(item) => { - const adminKey = getAdminKey(domain, item.accountID); - if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { - clearAddAdminError(domainID, item.accountID); - } else if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && adminKey) { - clearRemoveAdminError(domainID, item.accountID, adminKey); - } - }} - /> - + customListHeader={getCustomListHeader()} + containerStyle={[styles.flex1]} + /> ); diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index 701fc001dfaa..0eebdc7353aa 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -1,40 +1,36 @@ import React from 'react'; +import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; import type {DomainSplitNavigatorParamList} from '@navigation/types'; import selectMemberIDs from '@src/libs/DomainUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage' type DomainMembersPageProps = PlatformStackScreenProps; function DomainMembersPage({route}: DomainMembersPageProps) { - const domainID = route.params.accountID; + const {domainAccountID} = route.params; const {translate} = useLocalize(); - const [domain, fetchStatus] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, {canBeMissing: false}); + const [domain, fetchStatus] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: false}); - const [memberIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, { + const [memberIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: selectMemberIDs, }); - const shouldShowLoading = fetchStatus.status !== 'loading' && !domain; - + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundView = fetchStatus.status !== 'loading' && !domain; return ( {}} - shouldShowLoading={shouldShowLoading} + shouldShowNotFoundView={shouldShowNotFoundView} /> ); } From 4fc833913add1418487e3dfd3ce4512ea4231e57 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Wed, 17 Dec 2025 11:46:26 +0100 Subject: [PATCH 03/14] fix SecurityGroup type --- src/ONYXKEYS.ts | 2 +- src/types/onyx/Domain.ts | 21 ++++++++++++++++++++- src/types/onyx/index.ts | 3 ++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6eac4637675a..c2015f93dccd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1127,7 +1127,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; [ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata; [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number; - [ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP] : number; + [ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP] : OnyxTypes.DomainSecurityGroup; }; type OnyxValuesMapping = { diff --git a/src/types/onyx/Domain.ts b/src/types/onyx/Domain.ts index 22a3bd61f603..1ae1dc0ac948 100644 --- a/src/types/onyx/Domain.ts +++ b/src/types/onyx/Domain.ts @@ -94,6 +94,25 @@ type SamlMetadata = { samlMetadataError: OnyxCommon.Errors; }; -export {type SamlMetadata}; +/** Model of Security Group data */ +type SecurityGroup = { + /** Name of the security group (e.g. "Employees") */ + name: string; + + /** Whether the security group restricts policy creation */ + enableRestrictedPolicyCreation: boolean; + + /** Whether strict policy rules are enabled for this group */ + enableStrictPolicyRules: boolean; + + /** + * A map of member account IDs to their permission level within the group. + * Key: The accountID of the member. + * Value: The permission level (e.g. "read"). + */ + shared: Record; +}; + +export {type SamlMetadata, type SecurityGroup}; export default Domain; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index a1e57eebd55e..948f876fbd84 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedVa import type DismissedProductTraining from './DismissedProductTraining'; import type DismissedReferralBanners from './DismissedReferralBanners'; import type Domain from './Domain'; -import type {SamlMetadata} from './Domain'; +import type {SamlMetadata, SecurityGroup as DomainSecurityGroup} from './Domain'; import type Download from './Download'; import type DuplicateWorkspace from './DuplicateWorkspace'; import type ExpensifyCardBankAccountMetadata from './ExpensifyCardBankAccountMetadata'; @@ -298,4 +298,5 @@ export type { HybridApp, AppReview, SamlMetadata, + DomainSecurityGroup }; From 5d2698e9cc56005474e464ef0d862ba78bb7967c Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Thu, 18 Dec 2025 01:44:30 +0100 Subject: [PATCH 04/14] review changes --- src/libs/DomainUtils.ts | 31 ------------------- .../linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts | 1 - src/pages/domain/Admins/DomainAdminsPage.tsx | 5 +-- src/pages/domain/BaseDomainMembersPage.tsx | 16 +++------- .../domain/Members/DomainMembersPage.tsx | 2 +- src/selectors/Domain.ts | 29 ++++++++++++++++- 6 files changed, 34 insertions(+), 50 deletions(-) delete mode 100644 src/libs/DomainUtils.ts diff --git a/src/libs/DomainUtils.ts b/src/libs/DomainUtils.ts deleted file mode 100644 index 8ce192e98cbb..000000000000 --- a/src/libs/DomainUtils.ts +++ /dev/null @@ -1,31 +0,0 @@ -import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; - -/** - * Extracts a list of member IDs (accountIDs) from the domain object. - * * It iterates through the security groups in the domain, extracts account IDs from the 'shared' property, - * and returns a unique list of numbers. - * - * @param domain - The domain object from Onyx - * @returns An array of unique member account IDs - */ -function selectMemberIDs(domain: OnyxTypes.Domain | undefined): number[] { - if (!domain) { - return []; - } - - const memberIDs = Object.entries(domain) - .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP)) - .flatMap(([, value]) => { - const groupData = value as {shared?: Record}; - if (!groupData?.shared) { - return []; - } - return Object.keys(groupData.shared); - }) - .map((id) => Number(id)) - .filter((id) => !Number.isNaN(id)); - return [...new Set(memberIDs)]; -} - -export default selectMemberIDs; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts index 67a98a121779..c7769a21a367 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts @@ -6,7 +6,6 @@ const DOMAIN_TO_RHP: Partial { - Navigation.navigate(ROUTES.DOMAIN_ADMIN_DETAILS.getRoute(domainAccountID, item.accountID)); - }; return ( Navigation.navigate(ROUTES.DOMAIN_ADMIN_DETAILS.getRoute(domainAccountID, item.accountID))} shouldShowNotFoundView={!isUserAdmin} /> ); diff --git a/src/pages/domain/BaseDomainMembersPage.tsx b/src/pages/domain/BaseDomainMembersPage.tsx index 826b7335a697..8e1fbcef5711 100644 --- a/src/pages/domain/BaseDomainMembersPage.tsx +++ b/src/pages/domain/BaseDomainMembersPage.tsx @@ -9,7 +9,6 @@ import CustomListHeader from '@components/SelectionListWithModal/CustomListHeade import SelectionList from '@components/SelectionListWithSections'; import TableListItem from '@components/SelectionListWithSections/TableListItem'; import type {ListItem} from '@components/SelectionListWithSections/types'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -22,6 +21,7 @@ import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import IconAsset from '@src/types/utils/IconAsset'; type MemberOption = Omit & { accountID: number; @@ -44,8 +44,7 @@ type BaseDomainMembersPageProps = { /** Callback fired when a row is selected */ onSelectRow: (item: MemberOption) => void; - /** Whether to show the Not Found View (e.g. if user is not admin, or data is missing) */ - shouldShowNotFoundView?: boolean; + hederIcon: IconAsset; }; function BaseDomainMembersPage({ @@ -54,11 +53,10 @@ function BaseDomainMembersPage({ searchPlaceholder, headerContent, onSelectRow, - shouldShowNotFoundView = false, + hederIcon }: BaseDomainMembersPageProps) { const {formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations(['Members']); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); @@ -120,15 +118,10 @@ function BaseDomainMembersPage({ shouldShowOfflineIndicatorInWideScreen testID={BaseDomainMembersPage.displayName} > - Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} - shouldShow={shouldShowNotFoundView} - shouldForceFullScreen - > {!shouldUseNarrowLayout && !!headerContent && ( @@ -155,7 +148,6 @@ function BaseDomainMembersPage({ customListHeader={getCustomListHeader()} containerStyle={[styles.flex1]} /> - ); } diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index 0eebdc7353aa..2b2c65046ba5 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -4,9 +4,9 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; import type {DomainSplitNavigatorParamList} from '@navigation/types'; -import selectMemberIDs from '@src/libs/DomainUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import {selectMemberIDs} from '@selectors/Domain'; type DomainMembersPageProps = PlatformStackScreenProps; diff --git a/src/selectors/Domain.ts b/src/selectors/Domain.ts index c24e8cc4225c..bde6acc6e1d1 100644 --- a/src/selectors/Domain.ts +++ b/src/selectors/Domain.ts @@ -46,4 +46,31 @@ function adminAccountIDsSelector(domain: OnyxEntry): number[] { ); } -export {domainMemberSamlSettingsSelector, domainSamlSettingsStateSelector, domainNameSelector, metaIdentitySelector, adminAccountIDsSelector}; +/** + * Extracts a list of member IDs (accountIDs) from the domain object. + * * It iterates through the security groups in the domain, extracts account IDs from the 'shared' property, + * and returns a unique list of numbers. + * + * @param domain - The domain object from Onyx + * @returns An array of unique member account IDs + */ +function selectMemberIDs(domain: Domain | undefined): number[] { + if (!domain) { + return []; + } + + const memberIDs = Object.entries(domain) + .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP)) + .flatMap(([, value]) => { + const groupData = value as {shared?: Record}; + if (!groupData?.shared) { + return []; + } + return Object.keys(groupData.shared); + }) + .map((id) => Number(id)) + .filter((id) => !Number.isNaN(id)); + return [...new Set(memberIDs)]; +} + +export {domainMemberSamlSettingsSelector, domainSamlSettingsStateSelector, domainNameSelector, metaIdentitySelector, adminAccountIDsSelector, selectMemberIDs}; From d376360f6e5ff133e4ec767bc3068340569624c0 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Thu, 18 Dec 2025 20:38:25 +0100 Subject: [PATCH 05/14] changes after review without components from main --- src/ONYXKEYS.ts | 4 +-- .../Navigators/DomainSplitNavigator.tsx | 2 +- src/pages/domain/BaseDomainMembersPage.tsx | 34 +++++++++++-------- .../domain/Members/DomainMembersPage.tsx | 6 ---- src/selectors/Domain.ts | 2 +- src/types/onyx/Domain.ts | 18 +++------- src/types/onyx/index.ts | 2 +- 7 files changed, 29 insertions(+), 39 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index c2015f93dccd..654fe17293e1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -739,7 +739,7 @@ const ONYXKEYS = { EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_', /** Stores domain security group */ - DOMAIN_SECURITY_GROUP: 'expensify_securityGroup', + DOMAIN_SECURITY_GROUP_PREFIX: 'expensify_securityGroup_', }, /** List of Form ids */ @@ -1127,7 +1127,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; [ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata; [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number; - [ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP] : OnyxTypes.DomainSecurityGroup; + [ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP_PREFIX] : OnyxTypes.DomainSecurityGroup; }; type OnyxValuesMapping = { diff --git a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx index 115b30098624..c6d75b979786 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx @@ -14,7 +14,7 @@ import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; const loadDomainInitialPage = () => require('../../../../pages/domain/DomainInitialPage').default; const loadDomainSamlPage = () => require('../../../../pages/domain/DomainSamlPage').default; const loadDomainAdminsPage = () => require('../../../../pages/domain/Admins/DomainAdminsPage').default; -const loadDomainMembersPage = () => require('../../../../pages/domain/members/DomainMembersPage').default; +const loadDomainMembersPage = () => require('../../../../pages/domain/Members/DomainMembersPage').default; const Split = createSplitNavigator(); diff --git a/src/pages/domain/BaseDomainMembersPage.tsx b/src/pages/domain/BaseDomainMembersPage.tsx index 8e1fbcef5711..02af12aaf314 100644 --- a/src/pages/domain/BaseDomainMembersPage.tsx +++ b/src/pages/domain/BaseDomainMembersPage.tsx @@ -1,8 +1,6 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import SearchBar from '@components/SearchBar'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; @@ -20,8 +18,8 @@ import tokenizedSearch from '@libs/tokenizedSearch'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import IconAsset from '@src/types/utils/IconAsset'; +import type IconAsset from '@src/types/utils/IconAsset'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; type MemberOption = Omit & { accountID: number; @@ -44,7 +42,8 @@ type BaseDomainMembersPageProps = { /** Callback fired when a row is selected */ onSelectRow: (item: MemberOption) => void; - hederIcon: IconAsset; + /** Icon displayed in the header of the tab */ + headerIcon?: IconAsset; }; function BaseDomainMembersPage({ @@ -53,27 +52,32 @@ function BaseDomainMembersPage({ searchPlaceholder, headerContent, onSelectRow, - hederIcon + headerIcon }: BaseDomainMembersPageProps) { const {formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); + const data: MemberOption[] = useMemo(() => { const options: MemberOption[] = []; + for (const accountID of accountIDs) { const details = personalDetails?.[accountID]; + const login = details?.login ?? ''; + options.push({ keyForList: String(accountID), accountID, - login: details?.login ?? '', + login, text: formatPhoneNumber(getDisplayNameOrDefault(details)), - alternateText: formatPhoneNumber(details?.login ?? ''), + alternateText: formatPhoneNumber(login), icons: [ { - source: details?.avatar ?? FallbackAvatar, - name: formatPhoneNumber(details?.login ?? ''), + source: details?.avatar ?? icons.FallbackAvatar, + name: formatPhoneNumber(login), type: CONST.ICON_TYPE_AVATAR, id: accountID, }, @@ -83,10 +87,10 @@ function BaseDomainMembersPage({ return options; }, [accountIDs, personalDetails, formatPhoneNumber]); - const filterMember = useCallback((option: MemberOption, searchQuery: string) => { + const filterMember = (option: MemberOption, searchQuery: string) => { const results = tokenizedSearch([option], searchQuery, (item) => [item.text ?? '', item.alternateText ?? '']); return results.length > 0; - }, []); + }; const sortMembers = useCallback( (options: MemberOption[]) => sortAlphabetically(options, 'text', localeCompare), @@ -121,7 +125,7 @@ function BaseDomainMembersPage({ {!shouldUseNarrowLayout && !!headerContent && ( @@ -142,11 +146,11 @@ function BaseDomainMembersPage({ ListItem={TableListItem} onSelectRow={onSelectRow} shouldShowListEmptyContent={false} - listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]} + listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : styles.pr3} showScrollIndicator={false} addBottomSafeAreaPadding customListHeader={getCustomListHeader()} - containerStyle={[styles.flex1]} + containerStyle={styles.flex1} /> ); diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index 2b2c65046ba5..9d505035b1ad 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -14,23 +14,17 @@ function DomainMembersPage({route}: DomainMembersPageProps) { const {domainAccountID} = route.params; const {translate} = useLocalize(); - const [domain, fetchStatus] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: false}); - const [memberIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: selectMemberIDs, }); - // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundView = fetchStatus.status !== 'loading' && !domain; - return ( {}} - shouldShowNotFoundView={shouldShowNotFoundView} /> ); } diff --git a/src/selectors/Domain.ts b/src/selectors/Domain.ts index bde6acc6e1d1..c522c317e74a 100644 --- a/src/selectors/Domain.ts +++ b/src/selectors/Domain.ts @@ -60,7 +60,7 @@ function selectMemberIDs(domain: Domain | undefined): number[] { } const memberIDs = Object.entries(domain) - .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP)) + .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP_PREFIX)) .flatMap(([, value]) => { const groupData = value as {shared?: Record}; if (!groupData?.shared) { diff --git a/src/types/onyx/Domain.ts b/src/types/onyx/Domain.ts index 1ae1dc0ac948..dbbb8696d18b 100644 --- a/src/types/onyx/Domain.ts +++ b/src/types/onyx/Domain.ts @@ -1,5 +1,6 @@ import type ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxCommon from './OnyxCommon'; +import SecurityGroup from '@src/types/onyx/SecurityGroup'; /** * A utility type that creates a record where all keys are strings that start with a specified prefix. @@ -47,8 +48,8 @@ type Domain = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Whether setting SAML required setting has failed and why */ samlRequiredError?: OnyxCommon.Errors; }> & - PrefixedRecord; - + PrefixedRecord & + PrefixedRecord; /** Model of SAML metadata */ type SamlMetadata = { /** @@ -95,16 +96,7 @@ type SamlMetadata = { }; /** Model of Security Group data */ -type SecurityGroup = { - /** Name of the security group (e.g. "Employees") */ - name: string; - - /** Whether the security group restricts policy creation */ - enableRestrictedPolicyCreation: boolean; - - /** Whether strict policy rules are enabled for this group */ - enableStrictPolicyRules: boolean; - +type DomainSecurityGroup = SecurityGroup & { /** * A map of member account IDs to their permission level within the group. * Key: The accountID of the member. @@ -113,6 +105,6 @@ type SecurityGroup = { shared: Record; }; -export {type SamlMetadata, type SecurityGroup}; +export {type SamlMetadata, type DomainSecurityGroup}; export default Domain; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 948f876fbd84..b759ed5b8792 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedVa import type DismissedProductTraining from './DismissedProductTraining'; import type DismissedReferralBanners from './DismissedReferralBanners'; import type Domain from './Domain'; -import type {SamlMetadata, SecurityGroup as DomainSecurityGroup} from './Domain'; +import type {SamlMetadata, DomainSecurityGroup} from './Domain'; import type Download from './Download'; import type DuplicateWorkspace from './DuplicateWorkspace'; import type ExpensifyCardBankAccountMetadata from './ExpensifyCardBankAccountMetadata'; From f8d5411ff667b8d673f4291bf0b01ae910dafc0f Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Thu, 18 Dec 2025 23:32:13 +0100 Subject: [PATCH 06/14] changes after review with components from main --- src/pages/domain/Admins/DomainAdminsPage.tsx | 167 ++++-------------- src/pages/domain/BaseDomainMembersPage.tsx | 76 ++++---- .../domain/Members/DomainMembersPage.tsx | 8 +- 3 files changed, 84 insertions(+), 167 deletions(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index e29ca899962e..678c33781804 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,31 +1,17 @@ +import React, {useCallback} from 'react'; import {adminAccountIDsSelector, technicalContactEmailSelector} from '@selectors/Domain'; -import React from 'react'; -import {View} from 'react-native'; import Badge from '@components/Badge'; import Button from '@components/Button'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SearchBar from '@components/SearchBar'; -import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; -import SelectionList from '@components/SelectionListWithSections'; -import TableListItem from '@components/SelectionListWithSections/TableListItem'; -import type {ListItem} from '@components/SelectionListWithSections/types'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; -import {sortAlphabetically} from '@libs/OptionsListUtils'; -import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; -import tokenizedSearch from '@libs/tokenizedSearch'; import Navigation from '@navigation/Navigation'; import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; import type {DomainSplitNavigatorParamList} from '@navigation/types'; -import DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper'; +import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; import {getCurrentUserAccountID} from '@userActions/Report'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -33,25 +19,21 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; type DomainAdminsPageProps = PlatformStackScreenProps; -type AdminOption = Omit & { - accountID: number; - login: string; -}; - function DomainAdminsPage({route}: DomainAdminsPageProps) { const {domainAccountID} = route.params; - - const {translate, formatPhoneNumber, localeCompare} = useLocalize(); + const {translate} = useLocalize(); const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations(['Members']); - const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar', 'Gear']); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const illustrations = useMemoizedLazyIllustrations(['Members']); + const icons = useMemoizedLazyExpensifyIcons(['Gear']); const [adminAccountIDs, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: adminAccountIDsSelector, }); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + const [technicalContactEmail] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, { canBeMissing: false, selector: technicalContactEmailSelector, @@ -60,114 +42,39 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const currentUserAccountID = getCurrentUserAccountID(); const isAdmin = adminAccountIDs?.includes(currentUserAccountID); - const data: AdminOption[] = []; - for (const accountID of adminAccountIDs ?? []) { - const details = personalDetails?.[accountID]; - data.push({ - keyForList: String(accountID), - accountID, - login: details?.login ?? '', - text: formatPhoneNumber(getDisplayNameOrDefault(details)), - alternateText: formatPhoneNumber(details?.login ?? ''), - icons: [ - { - source: details?.avatar ?? icons.FallbackAvatar, - name: formatPhoneNumber(details?.login ?? ''), - type: CONST.ICON_TYPE_AVATAR, - id: accountID, - }, - ], - rightElement: technicalContactEmail === details?.login && , - }); - } - - const filterMember = (adminOption: AdminOption, searchQuery: string) => { - const results = tokenizedSearch([adminOption], searchQuery, (option) => [option.text ?? '', option.alternateText ?? '']); - return results.length > 0; - }; - const sortMembers = (adminOptions: AdminOption[]) => sortAlphabetically(adminOptions, 'text', localeCompare); - const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers); - - const getCustomListHeader = () => { - if (filteredData.length === 0) { - return null; - } - - return ( - - ); - }; - - const getHeaderButtons = () => { - if (!isAdmin) { - return null; - } - return ( - -