Skip to content

Commit

Permalink
feat: improved the user interface
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksandernsilva committed Sep 18, 2024
1 parent 134f456 commit 7eec447
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ it('should render "Associate Extension" button when VoIP_TeamCollab_Enabled sett
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(),
});

expect(screen.getByRole('button', { name: 'Associate_Extension' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeEnabled();
});

it('should not render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is disabled', async () => {
Expand All @@ -20,5 +20,5 @@ it('should not render "Associate Extension" button when VoIP_TeamCollab_Enabled
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', false).build(),
});

expect(screen.queryByRole('button', { name: 'Associate_Extension' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ const UserPageHeaderContentWithSeatsCap = ({
</Margins>
<ButtonGroup>
{canRegisterExtension && (
<Button primary onClick={(): void => setModal(<AssignExtensionModal onClose={(): void => setModal(null)} />)}>
{t('Associate_Extension')}
<Button icon='phone' onClick={(): void => setModal(<AssignExtensionModal onClose={(): void => setModal(null)} />)}>
{t('Assign_extension')}
</Button>
)}
<Button icon='mail' onClick={handleInviteButtonClick} disabled={isSeatsCapExceeded}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ const UsersTable = ({
),
tab === 'all' && isVoIPEnabled && (
<GenericTableHeaderCell
w='x100'
w='x180'
key='freeSwitchExtension'
direction={sortDirection}
active={sortBy === 'freeSwitchExtension'}
onClick={setSort}
sort='freeSwitchExtension'
>
{t('Voice_Call_Extension')}
{t('Voice_call_extension')}
</GenericTableHeaderCell>
),
<GenericTableHeaderCell key='actions' w={tab === 'pending' ? 'x204' : 'x50'} />,
Expand Down
23 changes: 13 additions & 10 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import { useDeleteUserAction } from '../hooks/useDeleteUserAction';
import { useResetE2EEKeyAction } from '../hooks/useResetE2EEKeyAction';
import { useResetTOTPAction } from '../hooks/useResetTOTPAction';
import { useSendWelcomeEmailMutation } from '../hooks/useSendWelcomeEmailMutation';
import AssignExtensionButton from '../voip/AssignExtensionButton';
import RemoveExtensionButton from '../voip/RemoveExtensionButton';
import { useVoiceCallExtensionAction } from '../hooks/useVoiceCallExtensionAction';

type UsersTableRowProps = {
user: Serialized<DefaultUserInfo>;
Expand All @@ -43,7 +42,7 @@ const UsersTableRow = ({
}: UsersTableRowProps): ReactElement => {
const { t } = useTranslation();

const { _id, emails, username, name, roles, status, active, avatarETag, lastLogin, type, freeSwitchExtension } = user;
const { _id, emails, username = '', name = '', roles, status, active, avatarETag, lastLogin, type, freeSwitchExtension } = user;
const registrationStatusText = useMemo(() => {
const usersExcludedFromPending = ['bot', 'app'];

Expand Down Expand Up @@ -76,10 +75,17 @@ const UsersTableRow = ({
const resetTOTPAction = useResetTOTPAction(userId);
const resetE2EKeyAction = useResetE2EEKeyAction(userId);
const resendWelcomeEmail = useSendWelcomeEmailMutation();
const voiceCallExtensionAction = useVoiceCallExtensionAction({ extension: freeSwitchExtension, username, name });

const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser;
const menuOptions = useMemo(
() => ({
...(voiceCallExtensionAction && {
voiceCallExtensionAction: {
label: { label: voiceCallExtensionAction.label, icon: voiceCallExtensionAction.icon },
action: voiceCallExtensionAction.action,
},
}),
...(isNotPendingDeactivatedNorFederated &&
changeAdminStatusAction && {
makeAdmin: {
Expand Down Expand Up @@ -114,6 +120,7 @@ const UsersTableRow = ({
isNotPendingDeactivatedNorFederated,
resetE2EKeyAction,
resetTOTPAction,
voiceCallExtensionAction,
],
);

Expand Down Expand Up @@ -167,13 +174,9 @@ const UsersTableRow = ({
)}

{tab === 'all' && showVoipExtension && username && (
<>
{freeSwitchExtension ? (
<RemoveExtensionButton username={username} extension={freeSwitchExtension} />
) : (
<AssignExtensionButton username={username} />
)}
</>
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
{freeSwitchExtension || t('Not_assigned')}
</GenericTableCell>
)}

<GenericTableCell
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useSetting } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import type { Action } from '../../../hooks/useActionSpread';
import AssignExtensionModal from '../voip/AssignExtensionModal';
import RemoveExtensionModal from '../voip/RemoveExtensionModal';

type VoiceCallExtensionActionParams = {
name: string;
username: string;
extension?: string;
};

export const useVoiceCallExtensionAction = ({ name, username, extension }: VoiceCallExtensionActionParams): Action | undefined => {
const isVoiceCallEnabled = useSetting('VoIP_TeamCollab_Enabled');
const { t } = useTranslation();
const setModal = useSetModal();

const handleExtensionAssignment = useEffectEvent(() => {
if (extension) {
setModal(<RemoveExtensionModal name={name} username={username} extension={extension} onClose={(): void => setModal(null)} />);
return;
}

setModal(<AssignExtensionModal defaultUsername={username} onClose={(): void => setModal(null)} />);
});

return isVoiceCallEnabled
? {
icon: extension ? 'phone-disabled' : 'phone',
label: extension ? t('Unassign_extension') : t('Assign_extension'),
action: handleExtensionAssignment,
}
: undefined;
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ it('should only enable "Free Extension Numbers" field if username is informed',

const extensionsSelect = screen.getByRole('button', { name: /Select_an_option/i });
expect(extensionsSelect).toHaveClass('disabled');
expect(screen.getByLabelText('User_Without_Extensions')).toBeEnabled();
expect(screen.getByLabelText('User')).toBeEnabled();

screen.getByLabelText('User_Without_Extensions').focus();
screen.getByLabelText('User').focus();
const userOption = await screen.findByRole('option', { name: 'Jane Doe' });
await userEvent.click(userOption);

Expand All @@ -70,7 +70,7 @@ it('should only enable "Associate" button both username and extension is informe

expect(screen.getByRole('button', { name: /Associate/i, hidden: true })).toBeDisabled();

screen.getByLabelText('User_Without_Extensions').focus();
screen.getByLabelText('User').focus();
const userOption = await screen.findByRole('option', { name: 'Jane Doe' });
await userEvent.click(userOption);

Expand All @@ -91,7 +91,7 @@ it('should call onClose when extension is associated', async () => {
wrapper: appRoot.build(),
});

screen.getByLabelText('User_Without_Extensions').focus();
screen.getByLabelText('User').focus();
const userOption = await screen.findByRole('option', { name: 'Jane Doe' });
await userEvent.click(userOption);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ const AssignExtensionModal = ({ defaultExtension, defaultUsername, onClose }: As
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit((data) => handleAssignment.mutateAsync(data))} {...props} />}
>
<Modal.Header>
<Modal.Title id={modalTitleId}>{t('Associate_User_to_Extension')}</Modal.Title>
<Modal.Title id={modalTitleId}>{t('Assign_extension')}</Modal.Title>
<Modal.Close aria-label={t('Close')} onClick={onClose} />
</Modal.Header>
<Modal.Content>
<FieldGroup>
<Field>
<FieldLabel htmlFor={usersWithoutExtensionsId}>{t('User_Without_Extensions')}</FieldLabel>
<FieldLabel htmlFor={usersWithoutExtensionsId}>{t('User')}</FieldLabel>
<FieldRow>
<Controller
control={control}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';

import '@testing-library/jest-dom';
import RemoveExtensionModal from './RemoveExtensionModal';

const appRoot = mockAppRoot().withJohnDoe();

it('should have user and extension informed', async () => {
render(<RemoveExtensionModal name='John Doe' username='john.doe' extension='1000' onClose={() => undefined} />, {
legacyRoot: true,
wrapper: appRoot.build(),
});

expect(screen.getByLabelText('User')).toHaveValue('John Doe');
expect(screen.getByLabelText('Extension')).toHaveValue('1000');
});

it('should call assign endpoint and onClose when extension is removed', async () => {
const closeFn = jest.fn();
const assignFn = jest.fn(() => null);
render(<RemoveExtensionModal name='John Doe' username='john.doe' extension='1000' onClose={closeFn} />, {
legacyRoot: true,
wrapper: appRoot.withEndpoint('POST', '/v1/voip-freeswitch.extension.assign', assignFn).build(),
});

screen.getByRole('button', { name: /Remove/i, hidden: true }).click();

await waitFor(() => expect(assignFn).toHaveBeenCalled());
await waitFor(() => expect(closeFn).toHaveBeenCalled());
});

it('should call onClose when cancel button is clicked', () => {
const closeFn = jest.fn();
render(<RemoveExtensionModal name='John Doe' username='john.doe' extension='1000' onClose={closeFn} />, {
legacyRoot: true,
wrapper: appRoot.build(),
});

screen.getByRole('button', { name: /Cancel/i, hidden: true }).click();
expect(closeFn).toHaveBeenCalled();
});

it('should call onClose when cancel button is clicked', () => {
const closeFn = jest.fn();
render(<RemoveExtensionModal name='John Doe' username='john.doe' extension='1000' onClose={closeFn} />, {
legacyRoot: true,
wrapper: appRoot.build(),
});

screen.getByRole('button', { name: /Close/i, hidden: true }).click();
expect(closeFn).toHaveBeenCalled();
});
81 changes: 81 additions & 0 deletions apps/meteor/client/views/admin/users/voip/RemoveExtensionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Button, Modal, Field, FieldGroup, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useEndpoint, useUser } from '@rocket.chat/ui-contexts';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React from 'react';
import { useTranslation } from 'react-i18next';

type RemoveExtensionModalProps = {
name: string;
extension: string;
username: string;
onClose: () => void;
};

const RemoveExtensionModal = ({ name, extension, username, onClose }: RemoveExtensionModalProps) => {
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const queryClient = useQueryClient();

const loggedUser = useUser();

const removeExtension = useEndpoint('POST', '/v1/voip-freeswitch.extension.assign');

const modalTitleId = useUniqueId();
const userFieldId = useUniqueId();
const freeExtensionNumberId = useUniqueId();

const handleRemoveExtension = useMutation({
mutationFn: (username: string) => removeExtension({ username }),
onSuccess: () => {
dispatchToastMessage({ type: 'success', message: t('Extension_removed') });

queryClient.invalidateQueries(['users.list']);
if (loggedUser?.username === username) {
queryClient.invalidateQueries(['voice-call-client']);
}

onClose();
},
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
onClose();
},
});

return (
<Modal aria-labelledby={modalTitleId}>
<Modal.Header>
<Modal.Title id={modalTitleId}>{t('Remove_extension')}</Modal.Title>
<Modal.Close aria-label={t('Close')} onClick={onClose} />
</Modal.Header>
<Modal.Content>
<FieldGroup>
<Field>
<FieldLabel htmlFor={userFieldId}>{t('User')}</FieldLabel>
<FieldRow>
<TextInput disabled id={userFieldId} value={name} />
</FieldRow>
</Field>

<Field>
<FieldLabel htmlFor={freeExtensionNumberId}>{t('Extension')}</FieldLabel>
<FieldRow>
<TextInput disabled id={freeExtensionNumberId} value={extension} />
</FieldRow>
</Field>
</FieldGroup>
</Modal.Content>
<Modal.Footer>
<Modal.FooterControllers>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button danger onClick={() => handleRemoveExtension.mutate(username)} loading={handleRemoveExtension.isLoading}>
{t('Remove')}
</Button>
</Modal.FooterControllers>
</Modal.Footer>
</Modal>
);
};

export default RemoveExtensionModal;
Loading

0 comments on commit 7eec447

Please sign in to comment.