diff --git a/assets/images/simple-illustrations/simple-illustration__user-shield.svg b/assets/images/simple-illustrations/simple-illustration__user-shield.svg new file mode 100644 index 000000000000..315a0efd4134 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__user-shield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 6dc1f5c03d4d..d7bce6e03019 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7907,6 +7907,13 @@ const CONST = { HEADER_ACTION_BUTTON: 'Task-HeaderActionButton', }, }, + + DOMAIN: { + /** Onyx prefix for domain admin account IDs */ + EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_', + /** Onyx prefix for domain security groups */ + DOMAIN_SECURITY_GROUP_PREFIX: 'domain_securityGroup_', + }, } as const; const CONTINUATION_DETECTION_SEARCH_FILTER_KEYS = [ diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5bf1bcf1f2c0..603a3507b0b0 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -761,9 +761,6 @@ const ONYXKEYS = { /** SAML login metadata for a domain */ SAML_METADATA: 'saml_metadata_', - /** Stores domain admin account ID */ - EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_', - /** Pending actions for a domain */ DOMAIN_PENDING_ACTIONS: 'domainPendingActions_', @@ -1160,7 +1157,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; [ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata; - [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number; [ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS]: OnyxTypes.DomainPendingActions; [ONYXKEYS.COLLECTION.DOMAIN_ERRORS]: OnyxTypes.DomainErrors; }; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0e3313852625..5119da278a08 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3600,6 +3600,10 @@ const ROUTES = { route: 'domain/:domainAccountID/admins/invite', getRoute: (domainAccountID: number) => `domain/${domainAccountID}/admins/invite` as const, }, + DOMAIN_MEMBERS: { + route: 'domain/:domainAccountID/members', + getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 85c9b8346785..590fb002eb3d 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -874,6 +874,7 @@ const SCREENS = { ADMINS_SETTINGS: 'Admins_Settings', ADD_PRIMARY_CONTACT: 'Add_Primary_Contact', ADD_ADMIN: 'Domain_Add_Admin', + MEMBERS: 'Domain_Members', }, } as const; diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index f1c505d7e1d7..ce7cf4c06406 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -156,6 +156,7 @@ import TrashCan from '@assets/images/simple-illustrations/simple-illustration__t import TravelAlerts from '@assets/images/simple-illustrations/simple-illustration__travelalerts.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__twocards-horizontal.svg'; +import UserShield from '@assets/images/simple-illustrations/simple-illustration__user-shield.svg'; import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg'; import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg'; import ExpensifyApprovedLogo from '@assets/images/subscription-details__approvedlogo.svg'; @@ -329,6 +330,7 @@ const Illustrations = { ShieldYellow, Clock, Members, + UserShield, }; /** diff --git a/src/languages/de.ts b/src/languages/de.ts index 020ccf7147e6..d970d75bc093 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7933,6 +7933,7 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`, cantRevokeAdminAccess: 'Adminzugriff kann dem technischen Ansprechpartner nicht entzogen werden', error: {removeAdmin: 'Dieser Benutzer kann nicht als Admin entfernt werden. Bitte versuchen Sie es erneut.'}, }, + members: {title: 'Mitglieder', findMember: 'Mitglied suchen'}, }, gps: { tooltip: 'GPS-Verfolgung läuft! Wenn du fertig bist, stoppe die Verfolgung unten.', diff --git a/src/languages/en.ts b/src/languages/en.ts index 9cc61cc8166f..2cc3aec1d4f3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7841,6 +7841,10 @@ const translations = { removeAdmin: 'Unable to remove this user as an Admin. Please try again.', }, }, + members: { + title: 'Members', + findMember: 'Find member', + }, }, }; diff --git a/src/languages/es.ts b/src/languages/es.ts index 222e2131599a..08e7aeab9646 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8009,6 +8009,10 @@ ${amount} para ${merchant} - ${date}`, cantRevokeAdminAccess: 'No se puede revocar el acceso de administrador del contacto técnico', error: {removeAdmin: 'No se pudo eliminar a este usuario como administrador. Por favor, inténtalo de nuevo.'}, }, + members: { + title: 'Miembros', + findMember: 'Buscar miembro', + }, }, gps: { tooltip: '¡Seguimiento por GPS en curso! Cuando termines, detén el seguimiento a continuación.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 7bc96c367822..c2d89cc6e4f0 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7937,6 +7937,7 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`, cantRevokeAdminAccess: 'Impossible de révoquer l’accès administrateur au contact technique', error: {removeAdmin: 'Impossible de supprimer cet utilisateur en tant qu’administrateur. Veuillez réessayer.'}, }, + members: {title: 'Membres', findMember: 'Rechercher un membre'}, }, gps: { tooltip: 'Suivi GPS en cours ! Quand vous avez terminé, arrêtez le suivi ci-dessous.', diff --git a/src/languages/it.ts b/src/languages/it.ts index f8ae6fef71c9..7c395735ca5c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7915,6 +7915,7 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`, cantRevokeAdminAccess: 'Impossibile revocare i privilegi di amministratore dal referente tecnico', error: {removeAdmin: 'Impossibile rimuovere questo utente come amministratore. Riprova.'}, }, + members: {title: 'Membri', findMember: 'Trova membro'}, }, gps: { tooltip: 'Monitoraggio GPS in corso! Quando hai finito, interrompi il monitoraggio qui sotto.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c9ea2e2d83b6..992bf294e18a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7853,6 +7853,7 @@ Expensify の使い方をお見せするための*テストレシート*がこ cantRevokeAdminAccess: '技術連絡先から管理者アクセス権を取り消すことはできません', error: {removeAdmin: 'このユーザーを管理者として削除できません。もう一度お試しください。'}, }, + members: {title: 'メンバー', findMember: 'メンバーを検索'}, }, gps: { tooltip: 'GPS 追跡を進行中です!完了したら、下で追跡を停止してください。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index efb0e5be92bd..1b050cbbcb9b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7904,6 +7904,7 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`, cantRevokeAdminAccess: 'Kan de beheerdersrechten niet intrekken van de technische contactpersoon', error: {removeAdmin: 'Kan deze gebruiker niet als beheerder verwijderen. Probeer het opnieuw.'}, }, + members: {title: 'Leden', findMember: 'Lid zoeken'}, }, gps: { tooltip: 'GPS-tracking bezig! Als je klaar bent, stop dan hieronder met tracken.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 4b158b362d7a..9cfef9445cf9 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7884,6 +7884,7 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`, cantRevokeAdminAccess: 'Nie można odebrać uprawnień administratora kontaktowi technicznemu', error: {removeAdmin: 'Nie można usunąć tego użytkownika jako administratora. Spróbuj ponownie.'}, }, + members: {title: 'Członkowie', findMember: 'Znajdź członka'}, }, gps: { tooltip: 'Śledzenie GPS w toku! Gdy skończysz, zatrzymaj śledzenie poniżej.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 94f3cf4ca44c..d5bcd20d7fcf 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7891,6 +7891,7 @@ Aqui está um *recibo de teste* para mostrar como funciona:`, cantRevokeAdminAccess: 'Não é possível revogar o acesso de administrador do contato técnico', error: {removeAdmin: 'Não foi possível remover este usuário como Administrador. Tente novamente.'}, }, + members: {title: 'Membros', findMember: 'Encontrar membro'}, }, gps: { tooltip: 'Rastreamento por GPS em andamento! Quando terminar, pare o rastreamento abaixo.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 29373376dc3a..b917d1abf583 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7708,6 +7708,7 @@ ${reportName} cantRevokeAdminAccess: '无法撤销技术联系人的管理员访问权限', error: {removeAdmin: '无法将此用户移除管理员角色。请重试。'}, }, + members: {title: '成员', findMember: '查找成员'}, }, gps: { tooltip: 'GPS 跟踪进行中!完成后,请在下方停止跟踪。', diff --git a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx index 1c94244e75d7..649f911492e0 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx @@ -14,6 +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 Split = createSplitNavigator(); @@ -50,6 +51,12 @@ function DomainSplitNavigator({route, navigation}: PlatformStackScreenProps + + diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1712346ca6a9..0881a7cda9d0 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -2014,6 +2014,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 58e320b05a4f..0322c1e09c33 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2511,6 +2511,9 @@ type DomainSplitNavigatorParamList = { [SCREENS.DOMAIN.ADMINS]: { domainAccountID: number; }; + [SCREENS.DOMAIN.MEMBERS]: { + domainAccountID: number; + }; }; type OnboardingModalNavigatorParamList = { diff --git a/src/libs/actions/Domain.ts b/src/libs/actions/Domain.ts index 25e041c7798b..34c4a7f3c966 100644 --- a/src/libs/actions/Domain.ts +++ b/src/libs/actions/Domain.ts @@ -7,6 +7,7 @@ import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import {getAuthToken} from '@libs/Network/NetworkStore'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type PrefixedRecord from '@src/types/utils/PrefixedRecord'; import type {ScimTokenWithState} from './ScimToken/ScimTokenUtils'; import {ScimTokenState} from './ScimToken/ScimTokenUtils'; @@ -527,7 +528,7 @@ function clearToggleConsolidatedDomainBillingErrors(domainAccountID: number) { } function addAdminToDomain(domainAccountID: number, accountID: number, targetEmail: string, domainName: string) { - const PERMISSION_KEY = `${ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX}${accountID}`; + const PERMISSION_KEY = `${CONST.DOMAIN.EXPENSIFY_ADMIN_ACCESS_PREFIX}${accountID}`; const optimisticData: OnyxUpdate[] = [ { @@ -535,7 +536,7 @@ function addAdminToDomain(domainAccountID: number, accountID: number, targetEmai key: `${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, value: { [PERMISSION_KEY]: accountID, - }, + } as PrefixedRecord, }, { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx b/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx index e274070b3ade..c7dcdec997ec 100644 --- a/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx +++ b/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx @@ -70,11 +70,13 @@ function DomainAddPrimaryContactPage({route}: DomainAddPrimaryContactPageProps) } const details = personalDetails?.[accountID]; - if (details?.login === technicalContactSettings?.technicalContactEmail) { + let isSelected = false; + if (!!details?.login && !!technicalContactSettings?.technicalContactEmail && details.login === technicalContactSettings.technicalContactEmail) { technicalContactEmailKey = String(accountID); + isSelected = true; } data.push({ - isSelected: details?.login === technicalContactSettings?.technicalContactEmail, + isSelected, keyForList: String(accountID), accountID, login: details?.login ?? '', diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 7b05f9a0a046..c241395abf69 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,56 +1,33 @@ import {adminAccountIDsSelector, adminPendingActionSelector, technicalContactSettingsSelector} 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'; -// eslint-disable-next-line no-restricted-imports -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 {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 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 {clearAdminError} from '@userActions/Domain'; 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'; -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', 'Plus']); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const illustrations = useMemoizedLazyIllustrations(['UserShield']); + const icons = useMemoizedLazyExpensifyIcons(['Gear', 'Plus']); - const [adminAccountIDs, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { + const [adminAccountIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: adminAccountIDsSelector, }); @@ -73,131 +50,53 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const currentUserAccountID = getCurrentUserAccountID(); const isAdmin = adminAccountIDs?.includes(currentUserAccountID); - const data: AdminOption[] = []; - for (const accountID of adminAccountIDs ?? []) { - const details = personalDetails?.[accountID]; - const isPrimaryContact = technicalContactSettings?.technicalContactEmail === details?.login; - const isPendingActionDelete = domainPendingAction?.[accountID]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - 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: isPrimaryContact && , - errors: getLatestError(domainErrors?.adminErrors?.[accountID]?.errors), - pendingAction: domainPendingAction?.[accountID]?.pendingAction, - isInteractive: !isPendingActionDelete, - isDisabled: isPendingActionDelete, - }); - } - - 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) { + const getCustomRightElement = (accountID: number) => { + const technicalContactEmail = technicalContactSettings?.technicalContactEmail; + const login = personalDetails?.[accountID]?.login; + if (!technicalContactEmail || !login || technicalContactEmail !== login) { return null; } - - return ( - - ); + return ; }; - const getHeaderButtons = () => { - if (!isAdmin) { - return null; - } - return ( - -