From 72c610156c2eede3902721b336a2405cb797f206 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 15 Oct 2024 12:23:51 -0300 Subject: [PATCH] refactor: unified users page header content into a single component (#33498) --- .../views/admin/users/AdminUsersPage.tsx | 26 +---- .../users/SeatsCapUsage/SeatsCapUsage.tsx | 10 +- ...UserPageHeaderContentWithSeatsCap.spec.tsx | 24 ----- .../users/UsersPageHeaderContent.spec.tsx | 96 +++++++++++++++++++ ...eatsCap.tsx => UsersPageHeaderContent.tsx} | 50 +++++----- 5 files changed, 137 insertions(+), 69 deletions(-) delete mode 100644 apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.spec.tsx create mode 100644 apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx rename apps/meteor/client/views/admin/users/{UserPageHeaderContentWithSeatsCap.tsx => UsersPageHeaderContent.tsx} (53%) diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 78950c0fe22a..14757e3710b0 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -1,9 +1,9 @@ import type { LicenseInfo } from '@rocket.chat/core-typings'; -import { Button, ButtonGroup, Callout, ContextualbarIcon, Icon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage'; +import { Callout, ContextualbarIcon, Icon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { OptionProp } from '@rocket.chat/ui-client'; import { ExternalLink } from '@rocket.chat/ui-client'; -import { usePermission, useRouteParameter, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useRouteParameter, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react'; @@ -28,7 +28,7 @@ import AdminUserForm from './AdminUserForm'; import AdminUserFormWithData from './AdminUserFormWithData'; import AdminUserInfoWithData from './AdminUserInfoWithData'; import AdminUserUpgrade from './AdminUserUpgrade'; -import UserPageHeaderContentWithSeatsCap from './UserPageHeaderContentWithSeatsCap'; +import UsersPageHeaderContent from './UsersPageHeaderContent'; import UsersTable from './UsersTable'; import useFilteredUsers from './hooks/useFilteredUsers'; import usePendingUsersCount from './hooks/usePendingUsersCount'; @@ -56,9 +56,6 @@ const AdminUsersPage = (): ReactElement => { const context = useRouteParameter('context'); const id = useRouteParameter('id'); - const canCreateUser = usePermission('create-user'); - const canBulkCreateUser = usePermission('bulk-register-user'); - const isCreateUserDisabled = useShouldPreventAction('activeUsers'); const getRoles = useEndpoint('GET', '/v1/roles.list'); @@ -111,22 +108,7 @@ const AdminUsersPage = (): ReactElement => { - {seatsCap && seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY ? ( - - ) : ( - - {canBulkCreateUser && ( - - )} - {canCreateUser && ( - - )} - - )} + {preventAction?.includes('activeUsers') && ( diff --git a/apps/meteor/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx b/apps/meteor/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx index 5a3f1bdd299a..4aadae11946e 100644 --- a/apps/meteor/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx +++ b/apps/meteor/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx @@ -14,7 +14,15 @@ const SeatsCapUsage = ({ limit, members }: SeatsCapUsageProps): ReactElement => const percentage = Math.max(0, Math.min((100 / limit) * members, 100)); const seatsLeft = Math.max(0, limit - members); - return ; + return ( + + ); }; export default SeatsCapUsage; diff --git a/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.spec.tsx b/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.spec.tsx deleted file mode 100644 index b11ca46ce40d..000000000000 --- a/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.spec.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { mockAppRoot } from '@rocket.chat/mock-providers'; -import { render, screen } from '@testing-library/react'; -import React from 'react'; -import '@testing-library/jest-dom'; - -import UserPageHeaderContent from './UserPageHeaderContentWithSeatsCap'; - -it('should render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is enabled', async () => { - render(, { - legacyRoot: true, - wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(), - }); - - expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeEnabled(); -}); - -it('should not render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is disabled', async () => { - render(, { - legacyRoot: true, - wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', false).build(), - }); - - expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument(); -}); diff --git a/apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx new file mode 100644 index 000000000000..f4691eb4dd69 --- /dev/null +++ b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx @@ -0,0 +1,96 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import '@testing-library/jest-dom'; + +import UsersPageHeaderContent from './UsersPageHeaderContent'; + +it('should render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is enabled', async () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(), + }); + + expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeEnabled(); +}); + +it('should not render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is disabled', async () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', false).build(), + }); + + expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument(); +}); + +it('should show "Invite" button if has build-register-user permission', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().withPermission('bulk-register-user').build(), + }); + + expect(screen.getByRole('button', { name: 'Invite' })).toBeInTheDocument(); +}); + +it('should hide "Invite" button if user doesnt have build-register-user permission', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().build(), + }); + + expect(screen.queryByRole('button', { name: 'Invite' })).not.toBeInTheDocument(); +}); + +it('should show "New User" button if has create-user permission', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().withPermission('create-user').build(), + }); + + expect(screen.getByRole('button', { name: 'New_user' })).toBeInTheDocument(); +}); + +it('should hide "New User" button if user doesnt have create-user permission', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().withPermission('create-user').build(), + }); + + expect(screen.getByRole('button', { name: 'New_user' })).toBeInTheDocument(); +}); + +it('should show "Buy more seats" button if seats caps is exceeded', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().build(), + }); + + expect(screen.getByRole('link', { name: 'Buy_more_seats' })).toBeInTheDocument(); +}); + +it('should hide "Buy more seats" button if seats caps is within limits', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().build(), + }); + + expect(screen.queryByRole('link', { name: 'Buy_more_seats' })).not.toBeInTheDocument(); +}); + +it('should show seats available progress bar', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().build(), + }); + + expect(screen.getByTestId('seats-cap-progress-bar')).toBeInTheDocument(); +}); + +it('should hide seats available progress bar if theres no limit', () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().build(), + }); + + expect(screen.queryByTestId('seats-cap-progress-bar')).not.toBeInTheDocument(); +}); diff --git a/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx similarity index 53% rename from apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx rename to apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx index a023f5229816..e6794a9f98f6 100644 --- a/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx +++ b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx @@ -1,28 +1,25 @@ import { Button, ButtonGroup, Margins } from '@rocket.chat/fuselage'; -import { useSetModal, useTranslation, useRouter, useSetting } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; +import { usePermission, useRouter, useSetModal, useSetting } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useExternalLink } from '../../../hooks/useExternalLink'; import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl'; import SeatsCapUsage from './SeatsCapUsage'; +import type { SeatCapProps } from './useSeatsCap'; import AssignExtensionModal from './voip/AssignExtensionModal'; -type UserPageHeaderContentWithSeatsCapProps = { - activeUsers: number; - maxActiveUsers: number; +type UsersPageHeaderContentProps = { isSeatsCapExceeded: boolean; + seatsCap?: Omit; }; -const UserPageHeaderContentWithSeatsCap = ({ - isSeatsCapExceeded, - activeUsers, - maxActiveUsers, -}: UserPageHeaderContentWithSeatsCapProps): ReactElement => { - const t = useTranslation(); +const UsersPageHeaderContent = ({ isSeatsCapExceeded, seatsCap }: UsersPageHeaderContentProps) => { + const { t } = useTranslation(); const router = useRouter(); const setModal = useSetModal(); - + const canCreateUser = usePermission('create-user'); + const canBulkCreateUser = usePermission('bulk-register-user'); const canRegisterExtension = useSetting('VoIP_TeamCollab_Enabled'); const manageSubscriptionUrl = useCheckoutUrl()({ target: 'user-page', action: 'buy_more' }); @@ -38,21 +35,30 @@ const UserPageHeaderContentWithSeatsCap = ({ return ( <> - - - + {seatsCap && seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY && ( + + + + )} {canRegisterExtension && ( )} - - + + {canBulkCreateUser && ( + + )} + + {canCreateUser && ( + + )} + {isSeatsCapExceeded && (