From 83ce1316219dfe3e23e1ce4d82edb4a85c5d5948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Tue, 21 Apr 2026 19:40:38 +0200 Subject: [PATCH 1/6] Change domain member group poc --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 + src/libs/actions/Domain.ts | 19 +++ .../Members/DomainMemberDetailsPage.tsx | 23 +++- .../domain/Members/MemberChangeGroupPage.tsx | 111 ++++++++++++++++++ 10 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/pages/domain/Members/MemberChangeGroupPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1cd0ca41d6ec..23b872710e34 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -4060,6 +4060,10 @@ const ROUTES = { route: 'domain/:domainAccountID/members/move', getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members/move` as const, }, + DOMAIN_MEMBER_CHANGE_GROUP: { + route: 'domain/:domainAccountID/members/:accountID/change-group', + getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}/change-group` as const, + }, MULTIFACTOR_AUTHENTICATION_MAGIC_CODE: `multifactor-authentication/magic-code`, MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST: 'multifactor-authentication/scenario/biometrics-test', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3f9ec7551053..e6f7014448d6 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -1008,6 +1008,7 @@ const SCREENS = { MEMBER_FORCE_TWO_FACTOR_AUTH: 'Member_Force_Two_Factor_Auth', MEMBER_LOCK_ACCOUNT: 'Member_Lock_Account', MEMBERS_MOVE_TO_GROUP: 'Members_Move_To_Group', + MEMBER_CHANGE_GROUP: 'Member_Change_Group', }, MULTIFACTOR_AUTHENTICATION: { MAGIC_CODE: 'Multifactor_Authentication_Magic_Code', diff --git a/src/languages/en.ts b/src/languages/en.ts index 936ae26c37fe..c132cbea7bc8 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9099,6 +9099,7 @@ const translations = { other: 'Close accounts', }), moveToGroup: 'Move to group', + securityGroup: 'Security group', chooseWhereToMove: ({count}: {count: number}) => `Choose where to move ${count} ${count === 1 ? 'member' : 'members'}.`, error: { addMember: 'Unable to add this member. Please try again.', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 71ad31690360..ef35cf5e575d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -965,6 +965,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/domain/Members/TwoFactorAuth/DomainMemberResetTwoFactorAuthPage').default, [SCREENS.DOMAIN.MEMBER_LOCK_ACCOUNT]: () => require('../../../../pages/domain/Members/DomainReportSuspiciousActivityPage').default, [SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP]: () => require('../../../../pages/domain/Members/MoveUsersBetweenGroupsPage').default, + [SCREENS.DOMAIN.MEMBER_CHANGE_GROUP]: () => require('../../../../pages/domain/Members/MemberChangeGroupPage').default, }); const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts index b0a4b1f2b48e..807da162cd57 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts @@ -16,6 +16,7 @@ const DOMAIN_TO_RHP: Partial['config'] = { [SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP]: { path: ROUTES.DOMAIN_MEMBERS_MOVE_TO_GROUP.route, }, + [SCREENS.DOMAIN.MEMBER_CHANGE_GROUP]: { + path: ROUTES.DOMAIN_MEMBER_CHANGE_GROUP.route, + }, }, }, [SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index d5b8557e1722..b7e844f1bd2a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1602,6 +1602,10 @@ type SettingsNavigatorParamList = { [SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP]: { domainAccountID: number; }; + [SCREENS.DOMAIN.MEMBER_CHANGE_GROUP]: { + domainAccountID: number; + accountID: number; + }; } & ReimbursementAccountNavigatorParamList; type DomainCardNavigatorParamList = { diff --git a/src/libs/actions/Domain.ts b/src/libs/actions/Domain.ts index 7d79b955103b..aba8717ddab5 100644 --- a/src/libs/actions/Domain.ts +++ b/src/libs/actions/Domain.ts @@ -1811,6 +1811,24 @@ function changeDomainSecurityGroup( API.write(WRITE_COMMANDS.CHANGE_DOMAIN_SECURITY_GROUP, parameters, {optimisticData, successData, failureData}); } +function clearChangeDomainSecurityGroupError(domainAccountID: number, memberEmail: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, { + memberErrors: { + [memberEmail]: { + changeDomainSecurityGroupErrors: null, + }, + }, + }); + + Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, { + member: { + [memberEmail]: { + changeDomainSecurityGroup: null, + }, + }, + }); +} + function setDomainMembersSelectedForMove(memberAccountIDs: string[]) { Onyx.set(ONYXKEYS.DOMAIN_MEMBERS_SELECTED_FOR_MOVE, memberAccountIDs); } @@ -1856,6 +1874,7 @@ export { resetDomainMemberTwoFactorAuth, exportMembersToCSV, changeDomainSecurityGroup, + clearChangeDomainSecurityGroupError, setDomainMembersSelectedForMove, clearDomainMembersSelectedForMove, }; diff --git a/src/pages/domain/Members/DomainMemberDetailsPage.tsx b/src/pages/domain/Members/DomainMemberDetailsPage.tsx index 7498cba366c3..d718f33f69e3 100644 --- a/src/pages/domain/Members/DomainMemberDetailsPage.tsx +++ b/src/pages/domain/Members/DomainMemberDetailsPage.tsx @@ -7,7 +7,9 @@ import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import DecisionModal from '@components/DecisionModal'; import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import {ModalActions} from '@components/Modal/Global/ModalContext'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import VacationDelegateMenuItem from '@components/VacationDelegateMenuItem'; import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -15,7 +17,13 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearTwoFactorAuthExemptEmailsErrors, clearValidateDomainTwoFactorCodeError, closeUserAccount, setTwoFactorAuthExemptEmailForDomain} from '@libs/actions/Domain'; +import { + clearChangeDomainSecurityGroupError, + clearTwoFactorAuthExemptEmailsErrors, + clearValidateDomainTwoFactorCodeError, + closeUserAccount, + setTwoFactorAuthExemptEmailForDomain, +} from '@libs/actions/Domain'; import {requestUnlockAccount} from '@libs/actions/User'; import {getLatestError} from '@libs/ErrorUtils'; import Navigation from '@navigation/Navigation'; @@ -135,6 +143,19 @@ function DomainMemberDetailsPage({route}: DomainMemberDetailsPageProps) { accountID={accountID} avatarButton={avatarButton} > + clearChangeDomainSecurityGroupError(domainAccountID, memberLogin)} + > + Navigation.navigate(ROUTES.DOMAIN_MEMBER_CHANGE_GROUP.getRoute(domainAccountID, accountID))} + shouldShowRightIcon + brickRoadIndicator={domainErrors?.memberErrors?.[memberLogin]?.changeDomainSecurityGroupErrors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + Navigation.navigate(ROUTES.DOMAIN_VACATION_DELEGATE.getRoute(domainAccountID, accountID))} diff --git a/src/pages/domain/Members/MemberChangeGroupPage.tsx b/src/pages/domain/Members/MemberChangeGroupPage.tsx new file mode 100644 index 000000000000..bc27b5f42ccf --- /dev/null +++ b/src/pages/domain/Members/MemberChangeGroupPage.tsx @@ -0,0 +1,111 @@ +import {domainNameSelector, groupsSelector, selectSecurityGroupForAccount} from '@selectors/Domain'; +import React, {useCallback, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; +import type {ListItem} from '@components/SelectionList/ListItem/types'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {changeDomainSecurityGroup} from '@libs/actions/Domain'; +import {getLoginByAccountID} from '@libs/PersonalDetailsUtils'; +import Navigation from '@navigation/Navigation'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Domain} from '@src/types/onyx'; + +type SecurityGroupItem = ListItem & { + value: string; +}; + +type MemberChangeGroupPageProps = PlatformStackScreenProps; + +function MemberChangeGroupPage({route}: MemberChangeGroupPageProps) { + const {domainAccountID, accountID} = route.params; + const {translate} = useLocalize(); + + const [selectedGroupId, setSelectedGroupId] = useState(); + const [domainName] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: domainNameSelector}); + const [securityGroups] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: groupsSelector}); + + const securityGroupSelector = useCallback((domain: OnyxEntry) => selectSecurityGroupForAccount(accountID)(domain), [accountID]); + const [userSecurityGroup] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { + selector: securityGroupSelector, + }); + + const currentGroupId = userSecurityGroup?.key.replace(CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX, ''); + + const data: SecurityGroupItem[] = (securityGroups ?? []).map(({id, details}) => ({ + text: details.name ?? '', + keyForList: id, + value: id, + isSelected: id === (selectedGroupId ?? currentGroupId), + })); + + const handleSelectRow = (item: SecurityGroupItem) => { + setSelectedGroupId(item.value); + }; + + const handleSave = () => { + if (!selectedGroupId || !domainName || !userSecurityGroup) { + return; + } + + if (selectedGroupId === currentGroupId) { + Navigation.goBack(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, accountID)); + return; + } + + const memberLogin = getLoginByAccountID(accountID); + if (!memberLogin) { + return; + } + + const newSecurityGroupKey: `${typeof CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${string}` = `${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${selectedGroupId}`; + changeDomainSecurityGroup(domainAccountID, domainName, memberLogin, accountID, userSecurityGroup.key, userSecurityGroup.securityGroup, newSecurityGroupKey); + Navigation.goBack(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, accountID)); + }; + + return ( + + + { + Navigation.goBack(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, accountID)); + }} + /> + + data={data} + onSelectRow={handleSelectRow} + ListItem={SingleSelectListItem} + initiallyFocusedItemKey={currentGroupId} + /> + +