diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8d8835d5e826..692363b7d3cf 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -4069,6 +4069,10 @@ const ROUTES = { route: 'domain/:domainAccountID/members/move', getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members/move` as const, }, + DOMAIN_MEMBER_MOVE_TO_GROUP: { + route: 'domain/:domainAccountID/members/:accountID/move', + getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}/move` 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 2698b3034b9b..7b14fc188ea7 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -1010,6 +1010,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_MOVE_TO_GROUP: 'Member_Move_To_Group', GROUP_DETAILS: 'Domain_Group_Details', GROUP_EDIT_NAME: 'Domain_Group_Edit_Name', }, diff --git a/src/languages/de.ts b/src/languages/de.ts index 2a370da3ab52..700db23de4d7 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9136,6 +9136,8 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`, emptyMembers: {title: 'Keine Mitglieder in dieser Gruppe', subtitle: 'Fügen Sie ein Mitglied hinzu oder versuchen Sie, den Filter oben zu ändern.'}, moveToGroup: 'In Gruppe verschieben', chooseWhereToMove: ({count}: {count: number}) => `Wählen Sie aus, wohin Sie ${count} ${count === 1 ? 'Mitglied' : 'Mitglieder'} verschieben möchten.`, + domainGroup: 'Domain-Gruppe', + chooseWhereToMoveName: ({name}: {name: string}) => `Wähle aus, wohin ${name} verschoben werden soll.`, }, common: { settings: 'Einstellungen', diff --git a/src/languages/en.ts b/src/languages/en.ts index e9730712658c..8b60f8a7538e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9110,7 +9110,9 @@ const translations = { other: 'Close accounts', }), moveToGroup: 'Move to group', + domainGroup: 'Domain group', chooseWhereToMove: ({count}: {count: number}) => `Choose where to move ${count} ${count === 1 ? 'member' : 'members'}.`, + chooseWhereToMoveName: ({name}: {name: string}) => `Choose where to move ${name}.`, error: { addMember: 'Unable to add this member. Please try again.', removeMember: 'Unable to remove this user. Please try again.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 24c64d59ceef..fedcb62cb7f2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -9352,7 +9352,9 @@ ${amount} para ${merchant} - ${date}`, reportSuspiciousActivityConfirmationPrompt: 'Revisaremos la cuenta para verificar que sea seguro desbloquearla y nos comunicaremos a través de Concierge si tenemos alguna pregunta.', moveToGroup: 'Mover al grupo', + domainGroup: 'Grupo de dominio', chooseWhereToMove: ({count}: {count: number}) => `Elige a dónde mover ${count} ${count === 1 ? 'miembro' : 'miembros'}.`, + chooseWhereToMoveName: ({name}: {name: string}) => `Elige a dónde mover a ${name}.`, }, common: { settings: 'Configuración', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index bf74d10d8008..33508cc18438 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9159,6 +9159,8 @@ Voici un *reçu test* pour vous montrer comment ça fonctionne :`, emptyMembers: {title: 'Aucun membre dans ce groupe', subtitle: 'Ajoutez un membre ou essayez de modifier le filtre ci-dessus.'}, moveToGroup: 'Déplacer vers le groupe', chooseWhereToMove: ({count}: {count: number}) => `Choisissez où déplacer ${count} ${count === 1 ? 'membre' : 'membres'}.`, + domainGroup: 'Groupe de domaines', + chooseWhereToMoveName: ({name}: {name: string}) => `Choisissez où déplacer ${name}.`, }, common: { settings: 'Paramètres', diff --git a/src/languages/it.ts b/src/languages/it.ts index 5514109962ca..0821036ef57f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -9126,6 +9126,8 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`, emptyMembers: {title: 'Nessun membro in questo gruppo', subtitle: 'Aggiungi un membro o prova a cambiare il filtro qui sopra.'}, moveToGroup: 'Sposta nel gruppo', chooseWhereToMove: ({count}: {count: number}) => `Scegli dove spostare ${count} ${count === 1 ? 'membro' : 'membri'}.`, + domainGroup: 'Gruppo di domini', + chooseWhereToMoveName: ({name}: {name: string}) => `Scegli dove spostare ${name}.`, }, common: { settings: 'Impostazioni', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index ee022d5eeaf7..110cf152935b 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -9012,6 +9012,8 @@ ${reportName} emptyMembers: {title: 'このグループにはメンバーがいません', subtitle: 'メンバーを追加するか、上のフィルターを変更してみてください。'}, moveToGroup: 'グループへ移動', chooseWhereToMove: ({count}: {count: number}) => `${count} ${count === 1 ? 'メンバー' : 'メンバー'} を移動する先を選択してください。`, + domainGroup: 'ドメイングループ', + chooseWhereToMoveName: ({name}: {name: string}) => `${name} をどこに移動するか選択してください。`, }, common: { settings: '設定', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d3854fc85403..a14c59068812 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -9098,6 +9098,8 @@ Hier is een *proefbon* om je te laten zien hoe het werkt:`, emptyMembers: {title: 'Geen leden in deze groep', subtitle: 'Voeg een lid toe of probeer het filter hierboven te wijzigen.'}, moveToGroup: 'Verplaatsen naar groep', chooseWhereToMove: ({count}: {count: number}) => `Kies waar je ${count} ${count === 1 ? 'lid' : 'leden'} naartoe wilt verplaatsen.`, + domainGroup: 'Domeingroep', + chooseWhereToMoveName: ({name}: {name: string}) => `Kies waar je ${name} naartoe wilt verplaatsen.`, }, common: { settings: 'Instellingen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b4b5578e9ec1..eccf0bdd5ed8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -9080,6 +9080,8 @@ Oto *paragon testowy*, żeby pokazać Ci, jak to działa:`, emptyMembers: {title: 'Brak członków w tej grupie', subtitle: 'Dodaj członka lub spróbuj zmienić filtr powyżej.'}, moveToGroup: 'Przenieś do grupy', chooseWhereToMove: ({count}: {count: number}) => `Wybierz, dokąd przenieść ${count} ${count === 1 ? 'członka' : 'członków'}.`, + domainGroup: 'Grupa domen', + chooseWhereToMoveName: ({name}: {name: string}) => `Wybierz, dokąd przenieść ${name}.`, }, common: { settings: 'Ustawienia', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 5720a86a8418..1ffe0fd41d66 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -9088,6 +9088,8 @@ Aqui está um *comprovante de teste* para mostrar como funciona:`, emptyMembers: {title: 'Nenhum membro neste grupo', subtitle: 'Adicione um membro ou tente mudar o filtro acima.'}, moveToGroup: 'Mover para grupo', chooseWhereToMove: ({count}: {count: number}) => `Escolha para onde mover ${count} ${count === 1 ? 'membro' : 'membros'}.`, + domainGroup: 'Grupo de domínio', + chooseWhereToMoveName: ({name}: {name: string}) => `Escolha para onde mover ${name}.`, }, common: { settings: 'Configurações', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c504140ee688..05be5dd1ab27 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8855,6 +8855,8 @@ ${reportName} emptyMembers: {title: '此群组中没有成员', subtitle: '添加成员或尝试更改上方的筛选条件。'}, moveToGroup: '移至群组', chooseWhereToMove: ({count}: {count: number}) => `选择将 ${count} 个 ${count === 1 ? '成员' : '成员'} 移动到哪里。`, + domainGroup: '域名组', + chooseWhereToMoveName: ({name}: {name: string}) => `选择将 ${name} 移动到哪里。`, }, common: { settings: '设置', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8604d51daef1..3f68becb4bae 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -964,6 +964,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_MOVE_TO_GROUP]: () => require('../../../../pages/domain/Members/MoveUserBetweenGroupsPage').default, [SCREENS.DOMAIN.GROUP_DETAILS]: () => require('../../../../pages/domain/Groups/DomainGroupDetailsPage').default, [SCREENS.DOMAIN.GROUP_EDIT_NAME]: () => require('../../../../pages/domain/Groups/DomainGroupEditNamePage').default, }); diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts index e33c6f994028..87676b91647e 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_MOVE_TO_GROUP]: { + path: ROUTES.DOMAIN_MEMBER_MOVE_TO_GROUP.route, + }, [SCREENS.DOMAIN.GROUP_DETAILS]: { path: ROUTES.DOMAIN_GROUP_DETAILS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 54c1f01fe720..8d2e60116f6a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1607,6 +1607,10 @@ type SettingsNavigatorParamList = { [SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP]: { domainAccountID: number; }; + [SCREENS.DOMAIN.MEMBER_MOVE_TO_GROUP]: { + domainAccountID: number; + accountID: number; + }; [SCREENS.DOMAIN.GROUP_DETAILS]: { domainAccountID: number; groupID: string; diff --git a/src/libs/actions/Domain.ts b/src/libs/actions/Domain.ts index 6ce1f2c3570d..39b0ccdebc49 100644 --- a/src/libs/actions/Domain.ts +++ b/src/libs/actions/Domain.ts @@ -1813,6 +1813,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); } @@ -1982,6 +2000,7 @@ export { resetDomainMemberTwoFactorAuth, exportMembersToCSV, changeDomainSecurityGroup, + clearChangeDomainSecurityGroupError, setDomainMembersSelectedForMove, clearDomainMembersSelectedForMove, updateDomainSecurityGroup, diff --git a/src/pages/domain/Members/DomainMemberDetailsPage.tsx b/src/pages/domain/Members/DomainMemberDetailsPage.tsx index 7498cba366c3..66b6f033c972 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_MOVE_TO_GROUP.getRoute(domainAccountID, accountID))} + shouldShowRightIcon + /> + Navigation.navigate(ROUTES.DOMAIN_VACATION_DELEGATE.getRoute(domainAccountID, accountID))} diff --git a/src/pages/domain/Members/MoveUserBetweenGroupsPage.tsx b/src/pages/domain/Members/MoveUserBetweenGroupsPage.tsx new file mode 100644 index 000000000000..a12418fe9624 --- /dev/null +++ b/src/pages/domain/Members/MoveUserBetweenGroupsPage.tsx @@ -0,0 +1,112 @@ +import {domainNameSelector, groupsSelector, selectSecurityGroupForAccount} from '@selectors/Domain'; +import React, {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 Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {changeDomainSecurityGroup} from '@libs/actions/Domain'; +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 {personalDetailsLoginSelector} from '@src/selectors/PersonalDetails'; +import type {Domain} from '@src/types/onyx'; + +type SecurityGroupItem = ListItem & { + value: string; +}; + +type MoveUserBetweenGroupsPageProps = PlatformStackScreenProps; + +function MoveUserBetweenGroupsPage({route}: MoveUserBetweenGroupsPageProps) { + const {domainAccountID, accountID} = route.params; + const styles = useThemeStyles(); + 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 = (domain: OnyxEntry) => selectSecurityGroupForAccount(accountID)(domain); + const [userSecurityGroup] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { + selector: securityGroupSelector, + }); + const [memberLogin] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsLoginSelector(accountID)}); + + 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 || !memberLogin) { + return; + } + + if (selectedGroupId === currentGroupId) { + Navigation.goBack(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, accountID)); + 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)); + }} + /> + {translate('domain.members.chooseWhereToMoveName', {name: memberLogin ?? ''})} + + + data={data} + onSelectRow={handleSelectRow} + ListItem={SingleSelectListItem} + initiallyFocusedItemKey={currentGroupId} + /> + +