diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bc8af4df56860..86a2615d0b25b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -554,6 +554,15 @@ const ROUTES = { route: 'workspace/:policyID/tags', getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, }, + WORKSPACE_MEMBER_DETAILS: { + route: 'workspace/:policyID/members/:accountID', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo), + }, + WORKSPACE_MEMBER_ROLE_SELECTION: { + route: 'workspace/:policyID/members/:accountID/role-selection', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo), + }, + // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 17073d289f8f7..98eb13b6231ab 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -226,6 +226,8 @@ const SCREENS = { CATEGORY_CREATE: 'Category_Create', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', + MEMBER_DETAILS: 'Workspace_Member_Details', + MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection', }, EDIT_REQUEST: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 1ba613610723f..b5166fb71a910 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1809,6 +1809,9 @@ export default { genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.', removeMembersPrompt: 'Are you sure you want to remove these members?', removeMembersTitle: 'Remove members', + removeMemberButtonTitle: 'Remove from workspace', + removeMemberPrompt: ({memberName}) => `Are you sure you want to remove ${memberName}`, + removeMemberTitle: 'Remove member', makeMember: 'Make member', makeAdmin: 'Make admin', selectAll: 'Select all', diff --git a/src/languages/es.ts b/src/languages/es.ts index acbb703081673..2b2b379040bcb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1833,6 +1833,9 @@ export default { genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor, inténtalo más tarde.', removeMembersPrompt: '¿Estás seguro de que deseas eliminar a estos miembros?', removeMembersTitle: 'Eliminar miembros', + removeMemberButtonTitle: 'Quitar del espacio de trabajo', + removeMemberPrompt: ({memberName}) => `¿Estás seguro de que deseas eliminar a ${memberName}`, + removeMemberTitle: 'Eliminar miembro', makeMember: 'Hacer miembro', makeAdmin: 'Hacer administrador', selectAll: 'Seleccionar todo', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index cee9e31cedea7..978e338796eae 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -251,6 +251,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MEMBER_DETAILS]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 7cca4ed466d46..5bc7d52230a83 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -4,7 +4,7 @@ import SCREENS from '@src/SCREENS'; const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = { [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE], [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT], - [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE], + [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION], [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], }; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index d3eb6d6a4c2b4..b16f751a5efe0 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -280,6 +280,12 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route, }, + [SCREENS.WORKSPACE.MEMBER_DETAILS]: { + path: ROUTES.WORKSPACE_MEMBER_DETAILS.route, + }, + [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: { + path: ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.route, + }, [SCREENS.WORKSPACE.CATEGORY_CREATE]: { path: ROUTES.WORKSPACE_CATEGORY_CREATE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 12b2802cb3faa..ad4c124ca51b2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -208,6 +208,16 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.MEMBER_DETAILS]: { + policyID: string; + accountID: string; + backTo: Routes; + }; + [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: { + policyID: string; + accountID: string; + backTo: Routes; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 100044344d4eb..42f29f885c00e 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -254,6 +254,23 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se [selectedEmployees, addUser, removeUser], ); + /** Opens the member details page */ + const openMemberDetails = useCallback( + (item: MemberOption) => { + if (!isPolicyAdmin) { + Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); + return; + } + + if (!PolicyUtils.isPaidGroupPolicy(policy)) { + return; + } + + Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID, Navigation.getActiveRoute())); + }, + [isPolicyAdmin, policy, route.params.policyID], + ); + /** * Dismisses the errors on one item */ @@ -465,7 +482,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se onPress={inviteUser} text={translate('workspace.invite.member')} icon={Expensicons.Plus} - iconStyles={{transform: [{scale: 0.6}]}} + iconStyles={StyleUtils.getTransformScaleStyle(0.6)} innerStyles={[isSmallScreenWidth && styles.alignItemsCenter]} style={[isSmallScreenWidth && styles.flexGrow1]} /> @@ -525,13 +542,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} headerContent={getHeaderContent()} - onSelectRow={(item) => { - if (!isPolicyAdmin) { - Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); - return; - } - toggleUser(item.accountID); - }} + onSelectRow={openMemberDetails} + onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers))} diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx new file mode 100644 index 0000000000000..d44ff8baa08b8 --- /dev/null +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -0,0 +1,152 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Avatar from '@components/Avatar'; +import Button from '@components/Button'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as UserUtils from '@libs/UserUtils'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; + +type WorkspacePolicyOnyxProps = { + /** Personal details of all users */ + personalDetails: OnyxEntry; +}; + +type WorkspaceMemberDetailsPageProps = WithPolicyAndFullscreenLoadingProps & WorkspacePolicyOnyxProps & StackScreenProps; + +function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, route}: WorkspaceMemberDetailsPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const StyleUtils = useStyleUtils(); + + const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false); + + const accountID = Number(route.params.accountID); + const policyID = route.params.policyID; + const backTo = route.params.backTo ?? ('' as Route); + + const member = policyMembers?.[accountID]; + const details = personalDetails?.[accountID] ?? ({} as PersonalDetails); + const avatar = details.avatar ?? UserUtils.getDefaultAvatar(); + const fallbackIcon = details.fallbackIcon ?? ''; + const displayName = details.displayName ?? ''; + + const askForConfirmationToRemove = () => { + setIsRemoveMemberConfirmModalVisible(true); + }; + + const removeUser = useCallback(() => { + Policy.removeMembers([accountID], route.params.policyID); + setIsRemoveMemberConfirmModalVisible(false); + Navigation.goBack(backTo); + }, [accountID, backTo, route.params.policyID]); + + const navigateToProfile = useCallback(() => { + Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute())); + }, [accountID]); + + const openRoleSelectionModal = useCallback(() => { + Navigation.navigate(ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.getRoute(route.params.policyID, accountID, Navigation.getActiveRoute())); + }, [accountID, route.params.policyID]); + + return ( + + + + Navigation.goBack(backTo)} + /> + + + + + + {Boolean(details.displayName ?? '') && ( + + {displayName} + + )} +