From a7bee82a1b4474f26030fed4f338f7ae4a51d6d7 Mon Sep 17 00:00:00 2001
From: Hardik Bhatia <98163873+hardikbhatia777@users.noreply.github.com>
Date: Tue, 5 Dec 2023 18:13:40 +0530
Subject: [PATCH 01/21] i18n: Fixed all German translation handlebar errors
(#31139)
---
apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json
index 9faa71e252d0..6601535bc40b 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json
@@ -3208,7 +3208,7 @@
"Message_has_been_edited": "Die Nachricht wurde bearbeitet",
"Message_has_been_edited_at": "Die Nachricht wurde am {{date}} bearbeitet",
"Message_has_been_edited_by": "Die Nachricht wurde editiert von {{username}}",
- "Message_has_been_edited_by_at": "Die Nachricht wurde bearbeitet von __Benutzername__ am __datum__",
+ "Message_has_been_edited_by_at": "Die Nachricht wurde bearbeitet von {{username}} am {{date}}",
"Message_has_been_pinned": "Nachricht wurde angeheftet",
"Message_has_been_starred": "Nachricht wurde als Favorit gekennzeichnet",
"Message_has_been_unpinned": "Nachricht wurde entpinnt",
@@ -3332,7 +3332,7 @@
"Moderation_Delete_message": "Nachricht löschen",
"Moderation_Dismiss_and_delete": "Ablehnen und löschen",
"Moderation_Delete_this_message": "Diese Nachricht löschen",
- "Moderation_Message_context_header": "Nachricht(en) von __displayName__",
+ "Moderation_Message_context_header": "Nachricht(s) gemeldet",
"Moderation_Action_View_reports": "Gemeldete Nachrichten anzeigen",
"Moderation_Deactivate_User": "Nutzer deaktivieren",
"Moderation_Delete_all_messages": "Alle Nachrichten löschen",
@@ -4999,7 +4999,7 @@
"User_Settings": "Kontoeinstellungen",
"User_started_a_new_conversation": "{{username}} hat ein neues Gespräch begonnen",
"User_unmuted_by": "Benutzer {{user_unmuted}} wurde das Chatten von {{user_by}} wieder erlaubt",
- "User_has_been_unmuted": "__user_muted__ nicht mehr stummgeschaltet",
+ "User_has_been_unmuted": "{{user_unmuted}} nicht mehr stummgeschaltet",
"User_unmuted_in_room": "Dem Benutzer wurde das Chatten wieder erlaubt",
"User_updated_successfully": "Benutzer wurde erfolgreich aktualisiert",
"User_uploaded_a_file_on_channel": "{{username}} hat eine Datei in {{channel}} hochgeladen",
From 200b1fea6370e69ba9e589ff92d73e7111d3e9ef Mon Sep 17 00:00:00 2001
From: Matheus Barbosa Silva
<36537004+matheusbsilva137@users.noreply.github.com>
Date: Tue, 5 Dec 2023 12:52:29 -0300
Subject: [PATCH 02/21] fix: New `custom-roles` license module isn't properly
checked (#31153)
---
.changeset/fresh-radios-whisper.md | 5 +++++
.../client/views/admin/permissions/EditRolePage.tsx | 2 +-
.../views/admin/permissions/EditRolePageWithData.tsx | 8 ++++----
.../views/admin/permissions/PermissionsContextBar.tsx | 9 ++++-----
apps/meteor/ee/server/api/roles.ts | 4 ++--
apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 2 +-
6 files changed, 17 insertions(+), 13 deletions(-)
create mode 100644 .changeset/fresh-radios-whisper.md
diff --git a/.changeset/fresh-radios-whisper.md b/.changeset/fresh-radios-whisper.md
new file mode 100644
index 000000000000..cba234524dce
--- /dev/null
+++ b/.changeset/fresh-radios-whisper.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixed issue with the new `custom-roles` license module not being checked throughout the application
diff --git a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx
index b7948d571254..8980d64d17e8 100644
--- a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx
+++ b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx
@@ -74,7 +74,7 @@ const EditRolePage = ({ role, isEnterprise }: { role?: IRole; isEnterprise: bool
}
};
- const deleteRoleMessage = isEnterprise ? t('Delete_Role_Warning') : t('Delete_Role_Warning_Community_Edition');
+ const deleteRoleMessage = isEnterprise ? t('Delete_Role_Warning') : t('Delete_Role_Warning_Not_Enterprise');
setModal(
{t('error-invalid-role')};
}
- if (isLoading || !data) {
+ if (hasCustomRolesModule === 'loading') {
return ;
}
- return ;
+ return ;
};
export default EditRolePageWithData;
diff --git a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx
index b472e57d7eae..767593067547 100644
--- a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx
+++ b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx
@@ -3,8 +3,8 @@ import { useRouteParameter, useRoute, useTranslation, useSetModal } from '@rocke
import type { ReactElement } from 'react';
import React, { useEffect } from 'react';
+import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule';
import { Contextualbar, ContextualbarHeader, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar';
-import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
import CustomRoleUpsellModal from './CustomRoleUpsellModal';
import EditRolePageWithData from './EditRolePageWithData';
@@ -14,21 +14,20 @@ const PermissionsContextBar = (): ReactElement | null => {
const context = useRouteParameter('context');
const router = useRoute('admin-permissions');
const setModal = useSetModal();
- const { data } = useIsEnterprise();
- const isEnterprise = !!data?.isEnterprise;
+ const hasCustomRolesModule = useHasLicenseModule('custom-roles') === true;
const handleCloseContextualbar = useMutableCallback(() => {
router.push({});
});
useEffect(() => {
- if (context !== 'new' || isEnterprise) {
+ if (context !== 'new' || hasCustomRolesModule) {
return;
}
setModal( setModal(null)} />);
handleCloseContextualbar();
- }, [context, isEnterprise, handleCloseContextualbar, setModal]);
+ }, [context, hasCustomRolesModule, handleCloseContextualbar, setModal]);
return (
(context && (
diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts
index c10c32c3ee1a..7a71613b3548 100644
--- a/apps/meteor/ee/server/api/roles.ts
+++ b/apps/meteor/ee/server/api/roles.ts
@@ -96,7 +96,7 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
- if (!License.hasValidLicense()) {
+ if (!License.hasModule('custom-roles')) {
throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature');
}
@@ -154,7 +154,7 @@ API.v1.addRoute(
const role = await Roles.findOne(roleId);
- if (!License.hasValidLicense() && !role?.protected) {
+ if (!License.hasModule('custom-roles') && !role?.protected) {
throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature');
}
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index 62b87f4a75d6..6898eb26e4a1 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -1561,7 +1561,7 @@
"Delete_message": "Delete message",
"Delete_my_account": "Delete my account",
"Delete_Role_Warning": "This cannot be undone",
- "Delete_Role_Warning_Community_Edition": "This cannot be undone. Note that it's not possible to create new custom roles in a Community workspace",
+ "Delete_Role_Warning_Not_Enterprise": "This cannot be undone. You won't be able to create a new custom role, since that feature is no longer available for your current plan.",
"Delete_Room_Warning": "Deleting a room will delete all messages posted within the room. This cannot be undone.",
"Delete_User_Warning": "Deleting a user will delete all messages from that user as well. This cannot be undone.",
"Delete_User_Warning_Delete": "Deleting a user will delete all messages from that user as well. This cannot be undone.",
From e6323bedb5f1895c306f9b1a4a5b49927fbe4227 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?=
<60678893+juliajforesti@users.noreply.github.com>
Date: Tue, 5 Dec 2023 13:24:11 -0300
Subject: [PATCH 03/21] chore: ImageGallery tweaks (#31159)
---
.../components/ImageGallery/ImageGallery.tsx | 15 +++++++++++++--
.../ImageGallery/ImageGalleryLoader.tsx | 4 ++--
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx
index 2946e3e692a2..0675532432aa 100644
--- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx
+++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx
@@ -1,5 +1,5 @@
import { css } from '@rocket.chat/css-in-js';
-import { Box, IconButton, Throbber } from '@rocket.chat/fuselage';
+import { Box, IconButton, Palette, Throbber } from '@rocket.chat/fuselage';
import React, { useRef, useState } from 'react';
import { FocusScope } from 'react-aria';
import { createPortal } from 'react-dom';
@@ -83,6 +83,17 @@ const swiperStyle = css`
right: 10px;
left: auto;
}
+
+ .rcx-lazy-preloader {
+ position: absolute;
+ z-index: -1;
+ left: 50%;
+ top: 50%;
+
+ transform: translate(-50%, -50%);
+
+ color: ${Palette.text['font-pure-white']};
+ }
`;
const ImageGallery = () => {
@@ -122,7 +133,7 @@ const ImageGallery = () => {
-
+
diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx
index a495f345194e..131b82780f22 100644
--- a/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx
+++ b/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx
@@ -12,9 +12,9 @@ const closeButtonStyle = css`
const ImageGalleryLoader = ({ onClose }: { onClose: () => void }) =>
createPortal(
-
+
-
+
,
document.body,
);
From 0681c455fc95a9584d446b0ac50580a21735c706 Mon Sep 17 00:00:00 2001
From: Douglas Fabris
Date: Tue, 5 Dec 2023 16:39:37 -0300
Subject: [PATCH 04/21] chore: Replace `useForm` in favor of RHF on Omnichannel
`AgentsEdit` (#30789)
Co-authored-by: Martin Schoeler
---
.../views/omnichannel/additionalForms.tsx | 4 +-
.../views/omnichannel/agents/AgentEdit.tsx | 341 ++++++++++--------
.../omnichannel/agents/AgentEditWithData.tsx | 40 +-
.../views/omnichannel/agents/AgentInfo.tsx | 100 +++--
.../omnichannel/agents/AgentInfoActions.tsx | 56 ---
.../views/omnichannel/agents/AgentsPage.tsx | 15 +-
.../views/omnichannel/agents/AgentsTab.tsx | 44 ---
.../agents/AgentsTable/AgentsTable.tsx | 14 +-
.../agents/AgentsTable/AgentsTableRow.tsx | 44 +--
.../agents/AgentsTable/RemoveAgentButton.tsx | 53 ---
.../agents/hooks/useRemoveAgent.tsx | 36 ++
.../additionalForms/MaxChatsPerAgent.tsx | 31 +-
.../MaxChatsPerAgentContainer.js | 23 --
...Display.js => MaxChatsPerAgentDisplay.tsx} | 6 +-
packages/core-typings/src/ILivechatAgent.ts | 3 +-
15 files changed, 359 insertions(+), 451 deletions(-)
delete mode 100644 apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx
delete mode 100644 apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx
delete mode 100644 apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx
create mode 100644 apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx
delete mode 100644 apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js
rename apps/meteor/ee/client/omnichannel/additionalForms/{MaxChatsPerAgentDisplay.js => MaxChatsPerAgentDisplay.tsx} (77%)
diff --git a/apps/meteor/client/views/omnichannel/additionalForms.tsx b/apps/meteor/client/views/omnichannel/additionalForms.tsx
index 152245df90be..16f6c9bcb440 100644
--- a/apps/meteor/client/views/omnichannel/additionalForms.tsx
+++ b/apps/meteor/client/views/omnichannel/additionalForms.tsx
@@ -7,14 +7,14 @@ import DepartmentForwarding from '../../../ee/client/omnichannel/additionalForms
import EeNumberInput from '../../../ee/client/omnichannel/additionalForms/EeNumberInput';
import EeTextAreaInput from '../../../ee/client/omnichannel/additionalForms/EeTextAreaInput';
import EeTextInput from '../../../ee/client/omnichannel/additionalForms/EeTextInput';
-import MaxChatsPerAgentContainer from '../../../ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer';
+import MaxChatsPerAgent from '../../../ee/client/omnichannel/additionalForms/MaxChatsPerAgent';
import MaxChatsPerAgentDisplay from '../../../ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay';
import PrioritiesSelect from '../../../ee/client/omnichannel/additionalForms/PrioritiesSelect';
import SlaPoliciesSelect from '../../../ee/client/omnichannel/additionalForms/SlaPoliciesSelect';
export {
CustomFieldsAdditionalForm,
- MaxChatsPerAgentContainer,
+ MaxChatsPerAgent,
MaxChatsPerAgentDisplay,
EeNumberInput,
EeTextAreaInput,
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx
index ef397d8ba7c0..e23baf346f7f 100644
--- a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx
+++ b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx
@@ -2,6 +2,7 @@ import type { ILivechatAgent, ILivechatDepartment, ILivechatDepartmentAgents } f
import {
Field,
FieldLabel,
+ FieldGroup,
FieldRow,
TextInput,
Button,
@@ -12,199 +13,229 @@ import {
ContextualbarFooter,
ButtonGroup,
} from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useToastMessageDispatch, useRoute, useSetting, useMethod, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
-import type { FC } from 'react';
-import React, { useMemo, useRef, useState } from 'react';
+import type { SelectOption } from '@rocket.chat/fuselage';
+import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks';
+import { useToastMessageDispatch, useSetting, useMethod, useTranslation, useEndpoint, useRouter } from '@rocket.chat/ui-contexts';
+import { useQueryClient } from '@tanstack/react-query';
+import React, { useMemo } from 'react';
+import { useForm, Controller, FormProvider } from 'react-hook-form';
import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress';
-import { ContextualbarScrollableContent } from '../../../components/Contextualbar';
+import {
+ Contextualbar,
+ ContextualbarTitle,
+ ContextualbarClose,
+ ContextualbarHeader,
+ ContextualbarScrollableContent,
+} from '../../../components/Contextualbar';
import UserInfo from '../../../components/UserInfo';
-import { useForm } from '../../../hooks/useForm';
-import { MaxChatsPerAgentContainer } from '../additionalForms';
-
-// TODO: TYPE:
-// Department
-
-type dataType = {
- user: Pick;
-};
+import { MaxChatsPerAgent } from '../additionalForms';
type AgentEditProps = {
- data: dataType;
- userDepartments: { departments: Pick[] };
- availableDepartments: { departments: Pick[] };
- uid: string;
- reset: () => void;
+ agentData: Pick;
+ userDepartments: Pick[];
+ availableDepartments: Pick[];
};
-const AgentEdit: FC = ({ data, userDepartments, availableDepartments, uid, reset, ...props }) => {
+const AgentEdit = ({ agentData, userDepartments, availableDepartments }: AgentEditProps) => {
const t = useTranslation();
- const agentsRoute = useRoute('omnichannel-agents');
- const [maxChatUnsaved, setMaxChatUnsaved] = useState();
+ const router = useRouter();
+ const queryClient = useQueryClient();
+
const voipEnabled = useSetting('VoIP_Enabled');
+ const dispatchToastMessage = useToastMessageDispatch();
- const { user } = data || { user: {} };
- const { name, username, statusLivechat } = user;
+ const { name, username, livechat, statusLivechat } = agentData;
- const email = getUserEmailAddress(user);
+ const email = getUserEmailAddress(agentData);
- const options: [string, string][] = useMemo(() => {
+ const departmentsOptions: SelectOption[] = useMemo(() => {
const archivedDepartment = (name: string, archived?: boolean) => (archived ? `${name} [${t('Archived')}]` : name);
- return availableDepartments?.departments
- ? availableDepartments.departments.map(({ _id, name, archived }) =>
- name ? [_id, archivedDepartment(name, archived)] : [_id, archivedDepartment(_id, archived)],
- )
- : [];
- }, [availableDepartments.departments, t]);
-
- const initialDepartmentValue = useMemo(
- () => (userDepartments.departments ? userDepartments.departments.map(({ departmentId }) => departmentId) : []),
- [userDepartments],
+ return (
+ availableDepartments.map(({ _id, name, archived }) =>
+ name ? [_id, archivedDepartment(name, archived)] : [_id, archivedDepartment(_id, archived)],
+ ) || []
+ );
+ }, [availableDepartments, t]);
+
+ const statusOptions: SelectOption[] = useMemo(
+ () => [
+ ['available', t('Available')],
+ ['not-available', t('Not_Available')],
+ ],
+ [t],
);
- const saveRef = useRef({
- values: {},
- hasUnsavedChanges: false,
- reset: () => undefined,
- commit: () => undefined,
+ const initialDepartmentValue = useMemo(() => userDepartments.map(({ departmentId }) => departmentId) || [], [userDepartments]);
+
+ const methods = useForm({
+ values: {
+ name,
+ username,
+ email,
+ departments: initialDepartmentValue,
+ status: statusLivechat,
+ maxNumberSimultaneousChat: livechat?.maxNumberSimultaneousChat || 0,
+ voipExtension: '',
+ },
});
- const { reset: resetMaxChats, commit: commitMaxChats } = saveRef.current;
-
- const onChangeMaxChats = useMutableCallback(({ hasUnsavedChanges, ...value }) => {
- saveRef.current = value;
-
- if (hasUnsavedChanges !== maxChatUnsaved) {
- setMaxChatUnsaved(hasUnsavedChanges);
- }
- });
-
- const { values, handlers, hasUnsavedChanges, commit } = useForm({
- departments: initialDepartmentValue,
- status: statusLivechat,
- maxChats: 0,
- voipExtension: '',
- });
-
- const { handleDepartments, handleStatus, handleVoipExtension } = handlers;
- const { departments, status, voipExtension } = values as {
- departments: string[];
- status: ILivechatAgent['statusLivechat'];
- voipExtension: string;
- };
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isDirty },
+ } = methods;
const saveAgentInfo = useMethod('livechat:saveAgentInfo');
const saveAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status');
- const dispatchToastMessage = useToastMessageDispatch();
-
- const handleReset = useMutableCallback(() => {
- reset();
- resetMaxChats();
- });
-
- const handleSave = useMutableCallback(async () => {
+ const handleSave = useMutableCallback(async ({ status, departments, ...data }) => {
try {
- await saveAgentStatus({ status, agentId: uid });
- await saveAgentInfo(uid, saveRef.current.values, departments);
+ await saveAgentStatus({ agentId: agentData._id, status });
+ await saveAgentInfo(agentData._id, data, departments);
dispatchToastMessage({ type: 'success', message: t('Success') });
- agentsRoute.push({});
- reset();
+ router.navigate('/omnichannel/agents');
+ queryClient.invalidateQueries(['livechat-agents']);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
- commit();
- commitMaxChats();
});
+ const formId = useUniqueId();
+ const nameField = useUniqueId();
+ const usernameField = useUniqueId();
+ const emailField = useUniqueId();
+ const departmentsField = useUniqueId();
+ const statusField = useUniqueId();
+ const voipExtensionField = useUniqueId();
+
return (
- <>
-
- {username && (
-
-
-
- )}
-
- {t('Name')}
-
-
-
-
-
- {t('Username')}
-
- } />
-
-
-
- {t('Email')}
-
- } />
-
-
-
- {t('Departments')}
-
-
-
-
-
- {t('Status')}
-
-
-
-
-
- {voipEnabled && (
-
- {t('VoIP_Extension')}
-
-
-
-
- )}
+
+
+ {t('Edit_User')}
+ router.navigate('/omnichannel/agents')} />
+
+
+
+
+
-
-
- >
+
);
};
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx
index 543d1d7bab93..df092de6f5eb 100644
--- a/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx
+++ b/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx
@@ -1,3 +1,4 @@
+import type { ILivechatAgent } from '@rocket.chat/core-typings';
import { Box } from '@rocket.chat/fuselage';
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
@@ -7,41 +8,42 @@ import React from 'react';
import { FormSkeleton } from '../../../components/Skeleton';
import AgentEdit from './AgentEdit';
-type AgentEditWithDataProps = {
- uid: string;
- reload: () => void;
-};
-
-const AgentEditWithData = ({ uid, reload }: AgentEditWithDataProps): ReactElement => {
+const AgentEditWithData = ({ uid }: { uid: ILivechatAgent['_id'] }): ReactElement => {
const t = useTranslation();
- const getDepartments = useEndpoint('GET', '/v1/livechat/department');
-
- const getAgent = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: uid });
+ const getAvailableDepartments = useEndpoint('GET', '/v1/livechat/department');
+ const getAgentById = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: uid });
const getAgentDepartments = useEndpoint('GET', '/v1/livechat/agents/:agentId/departments', { agentId: uid });
- const { data, isInitialLoading: isLoading, error } = useQuery(['getAgent'], async () => getAgent());
+ const { data, isLoading, error } = useQuery(['livechat-getAgentById', uid], async () => getAgentById(), { refetchOnWindowFocus: false });
+
const {
- data: userDepartments,
- isLoading: isUserDepartmentsLoading,
- error: userDepartmentsError,
- } = useQuery({ queryKey: ['getAgentDepartments'], queryFn: async () => getAgentDepartments(), cacheTime: 0 });
+ data: agentDepartments,
+ isLoading: agentDepartmentsLoading,
+ error: agentsDepartmentsError,
+ } = useQuery(['livechat-getAgentDepartments', uid], async () => getAgentDepartments(), { refetchOnWindowFocus: false });
const {
data: availableDepartments,
- isLoading: isAvailableDepartmentsLoading,
+ isLoading: availableDepartmentsLoading,
error: availableDepartmentsError,
- } = useQuery(['getDepartments'], async () => getDepartments({ showArchived: 'true' }));
+ } = useQuery(['livechat-getAvailableDepartments'], async () => getAvailableDepartments({ showArchived: 'true' }));
- if (isLoading || isAvailableDepartmentsLoading || isUserDepartmentsLoading || !userDepartments || !availableDepartments) {
+ if (isLoading || availableDepartmentsLoading || agentDepartmentsLoading || !agentDepartments || !availableDepartments) {
return ;
}
- if (error || userDepartmentsError || availableDepartmentsError || !data || !data.user) {
+ if (error || agentsDepartmentsError || availableDepartmentsError || !data?.user) {
return {t('User_not_found')};
}
- return ;
+ return (
+
+ );
};
export default AgentEditWithData;
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx b/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx
index 5c8765a1b9f1..09723ee31602 100644
--- a/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx
+++ b/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx
@@ -1,63 +1,83 @@
-import { Box, Margins, ButtonGroup } from '@rocket.chat/fuselage';
-import { useTranslation } from '@rocket.chat/ui-contexts';
+import { Box, Margins, ButtonGroup, ContextualbarSkeleton } from '@rocket.chat/fuselage';
+import { useEndpoint, useRouter, useTranslation } from '@rocket.chat/ui-contexts';
+import { useQuery } from '@tanstack/react-query';
import type { HTMLAttributes } from 'react';
-import React, { memo } from 'react';
+import React from 'react';
-import { ContextualbarScrollableContent } from '../../../components/Contextualbar';
-import { FormSkeleton } from '../../../components/Skeleton';
+import {
+ Contextualbar,
+ ContextualbarTitle,
+ ContextualbarClose,
+ ContextualbarHeader,
+ ContextualbarScrollableContent,
+} from '../../../components/Contextualbar';
import UserInfo from '../../../components/UserInfo';
import { UserStatus } from '../../../components/UserStatus';
-import { AsyncStatePhase } from '../../../hooks/useAsyncState';
-import { useEndpointData } from '../../../hooks/useEndpointData';
import { MaxChatsPerAgentDisplay } from '../additionalForms';
+import AgentInfoAction from './AgentInfoAction';
+import { useRemoveAgent } from './hooks/useRemoveAgent';
type AgentInfoProps = {
uid: string;
} & Omit, 'is'>;
-const AgentInfo = memo(function AgentInfo({ uid, children, ...props }) {
+const AgentInfo = ({ uid }: AgentInfoProps) => {
const t = useTranslation();
- const result = useEndpointData('/v1/livechat/users/agent/:_id', { keys: { _id: uid } });
+ const router = useRouter();
+ const getAgentById = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: uid });
+ const { data, isLoading, isError } = useQuery(['livechat-getAgentInfoById', uid], async () => getAgentById(), {
+ refetchOnWindowFocus: false,
+ });
- if (result.phase === AsyncStatePhase.LOADING) {
- return ;
+ const handleDelete = useRemoveAgent(uid);
+
+ if (isLoading) {
+ return ;
}
- if (result.phase === AsyncStatePhase.REJECTED) {
+ if (isError) {
return {t('User_not_found')};
}
- const { user } = result.value;
- const { username, statusLivechat, status: userStatus } = user;
+ const { username, statusLivechat, status: userStatus } = data?.user;
return (
-
- {username && (
-
-
-
- )}
-
-
- {children}
-
-
-
-
- } />
-
-
- {statusLivechat && (
- <>
- {t('Livechat_status')}
- {t(statusLivechat === 'available' ? 'Available' : 'Not_Available')}
- >
+
+
+ {t('User_Info')}
+ router.navigate('/omnichannel/agents')} />
+
+
+ {username && (
+
+
+
)}
-
-
-
-
+
+ router.navigate(`/omnichannel/agents/edit/${uid}`)}
+ icon='edit'
+ />
+
+
+
+
+ } />
+
+ {statusLivechat && (
+ <>
+ {t('Livechat_status')}
+ {t(statusLivechat === 'available' ? 'Available' : 'Not_Available')}
+ >
+ )}
+ {MaxChatsPerAgentDisplay && }
+
+
+
);
-});
+};
export default AgentInfo;
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx b/apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx
deleted file mode 100644
index 3a8422be381d..000000000000
--- a/apps/meteor/client/views/omnichannel/agents/AgentInfoActions.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useSetModal, useToastMessageDispatch, useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-contexts';
-import type { ReactElement } from 'react';
-import React from 'react';
-
-import GenericModal from '../../../components/GenericModal';
-import { useEndpointAction } from '../../../hooks/useEndpointAction';
-import AgentInfoAction from './AgentInfoAction';
-
-const AgentInfoActions = ({ reload }: { reload: () => void }): ReactElement => {
- const t = useTranslation();
- const _id = useRouteParameter('id') ?? '';
- const agentsRoute = useRoute('omnichannel-agents');
- const deleteAction = useEndpointAction('DELETE', '/v1/livechat/users/agent/:_id', { keys: { _id } });
- const setModal = useSetModal();
- const dispatchToastMessage = useToastMessageDispatch();
-
- const handleRemoveClick = useMutableCallback(async () => {
- const result = await deleteAction();
- if (result.success === true) {
- agentsRoute.push({});
- reload();
- }
- });
-
- const handleDelete = useMutableCallback((e) => {
- e.stopPropagation();
- const onDeleteAgent = async (): Promise => {
- try {
- await handleRemoveClick();
- dispatchToastMessage({ type: 'success', message: t('Agent_removed') });
- } catch (error) {
- dispatchToastMessage({ type: 'error', message: error });
- }
- setModal();
- };
-
- setModal( setModal()} confirmText={t('Delete')} />);
- });
-
- const handleEditClick = useMutableCallback(() =>
- agentsRoute.push({
- context: 'edit',
- id: _id,
- }),
- );
-
- return (
- <>
-
-
- >
- );
-};
-
-export default AgentInfoActions;
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx
index 7640077baaba..d276413b9861 100644
--- a/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx
+++ b/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx
@@ -1,24 +1,20 @@
import { usePermission, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
-import React, { useRef, useCallback } from 'react';
+import React from 'react';
import Page from '../../../components/Page';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
-import AgentsTab from './AgentsTab';
+import AgentEditWithData from './AgentEditWithData';
+import AgentInfo from './AgentInfo';
import AgentsTable from './AgentsTable/AgentsTable';
const AgentsPage = (): ReactElement => {
const t = useTranslation();
- const reload = useRef(() => null);
const canViewAgents = usePermission('manage-livechat-agents');
const context = useRouteParameter('context');
const id = useRouteParameter('id');
- const handleReload = useCallback(() => {
- reload.current();
- }, [reload]);
-
if (!canViewAgents) {
return ;
}
@@ -28,10 +24,11 @@ const AgentsPage = (): ReactElement => {
-
+
- {context && id && }
+ {id && context === 'edit' && }
+ {id && context === 'info' && }
);
};
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx
deleted file mode 100644
index 1ba6d7537e9a..000000000000
--- a/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
-import type { ReactElement } from 'react';
-import React, { useCallback } from 'react';
-
-import { Contextualbar, ContextualbarHeader, ContextualbarClose, ContextualbarTitle } from '../../../components/Contextualbar';
-import AgentEditWithData from './AgentEditWithData';
-import AgentInfo from './AgentInfo';
-import AgentInfoActions from './AgentInfoActions';
-
-type AgentsTabProps = {
- reload: () => void;
- context: string;
- id: string;
-};
-
-const AgentsTab = ({ reload, context, id }: AgentsTabProps): ReactElement => {
- const t = useTranslation();
- const agentsRoute = useRoute('omnichannel-agents');
-
- const handleClose = useCallback((): void => {
- agentsRoute.push({});
- }, [agentsRoute]);
-
- return (
-
-
-
- {context === 'edit' && t('Edit_User')}
- {context === 'info' && t('User_Info')}
-
-
-
-
- {context === 'edit' && }
- {context === 'info' && (
-
-
-
- )}
-
- );
-};
-
-export default AgentsTab;
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx
index 215155e97cca..cfabfb91f787 100644
--- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx
+++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx
@@ -2,8 +2,7 @@ import { Pagination } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { hashQueryKey } from '@tanstack/react-query';
-import type { MutableRefObject } from 'react';
-import React, { useMemo, useState, useEffect } from 'react';
+import React, { useMemo, useState } from 'react';
import FilterByText from '../../../../components/FilterByText';
import GenericNoResults from '../../../../components/GenericNoResults/GenericNoResults';
@@ -22,7 +21,7 @@ import AddAgent from './AddAgent';
import AgentsTableRow from './AgentsTableRow';
// TODO: missing error state
-const AgentsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => {
+const AgentsTable = () => {
const t = useTranslation();
const [filter, setFilter] = useState('');
@@ -41,11 +40,6 @@ const AgentsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => {
const [defaultQuery] = useState(hashQueryKey([query]));
const queryHasChanged = defaultQuery !== hashQueryKey([query]);
- useEffect(() => {
- reload.current = refetch;
- }, [reload, refetch]);
- reload.current = refetch;
-
const onHeaderClick = useMutableCallback((id) => {
if (sortBy === id) {
setSort(id, sortDirection === 'asc' ? 'desc' : 'asc');
@@ -100,11 +94,11 @@ const AgentsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => {
)}
{isSuccess && data?.users.length > 0 && (
<>
-
+
{headers}
{data?.users.map((user) => (
-
+
))}
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx
index d65efacbb1c8..1bad8cbcc62e 100644
--- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx
+++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx
@@ -1,17 +1,15 @@
-import { Box } from '@rocket.chat/fuselage';
-import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
-import { useQueryClient } from '@tanstack/react-query';
+import { Box, IconButton } from '@rocket.chat/fuselage';
+import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
-import React, { useCallback } from 'react';
+import React from 'react';
import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable';
import UserAvatar from '../../../../components/avatar/UserAvatar';
-import RemoveAgentButton from './RemoveAgentButton';
+import { useRemoveAgent } from '../hooks/useRemoveAgent';
const AgentsTableRow = ({
- user: { _id, name, username, avatarETag, emails, statusLivechat, departments },
+ user: { _id, name, username, avatarETag, emails, statusLivechat },
mediaQuery,
- reload,
}: {
user: {
_id: string;
@@ -20,30 +18,16 @@ const AgentsTableRow = ({
avatarETag?: string;
emails?: { address: string }[];
statusLivechat: string;
- departments: string[];
};
mediaQuery: boolean;
- reload: () => void;
}): ReactElement => {
const t = useTranslation();
- const agentsRoute = useRoute('omnichannel-agents');
- const queryClient = useQueryClient();
+ const router = useRouter();
- const onRowClick = useCallback(() => {
- agentsRoute.push({
- context: 'info',
- id: _id,
- });
- }, [_id, agentsRoute]);
-
- const onAgentRemoved = useCallback(() => {
- departments.forEach((departmentId) => {
- queryClient.removeQueries(['/v1/livechat/department/:_id', departmentId]);
- });
- }, [queryClient, departments]);
+ const handleDelete = useRemoveAgent(_id);
return (
-
+ router.navigate(`/omnichannel/agents/info/${_id}`)}>
{username && }
@@ -71,7 +55,17 @@ const AgentsTableRow = ({
)}
{emails?.length && emails[0].address}
{statusLivechat === 'available' ? t('Available') : t('Not_Available')}
-
+
+ {
+ e.stopPropagation();
+ handleDelete();
+ }}
+ />
+
);
};
diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx
deleted file mode 100644
index 81956a058f82..000000000000
--- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/RemoveAgentButton.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { IconButton } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
-import type { ReactElement } from 'react';
-import React from 'react';
-
-import GenericModal from '../../../../components/GenericModal';
-import { GenericTableCell } from '../../../../components/GenericTable';
-import { useEndpointAction } from '../../../../hooks/useEndpointAction';
-
-type RemoveAgentButtonProps = {
- _id: string;
- reload: () => void;
- onAgentRemoved?: () => void;
-};
-
-const RemoveAgentButton = ({ _id, reload, onAgentRemoved }: RemoveAgentButtonProps): ReactElement => {
- const deleteAction = useEndpointAction('DELETE', '/v1/livechat/users/agent/:_id', { keys: { _id } });
- const setModal = useSetModal();
- const dispatchToastMessage = useToastMessageDispatch();
- const t = useTranslation();
-
- const handleRemoveClick = useMutableCallback(async () => {
- const result = await deleteAction();
- if (result.success === true) {
- reload();
- onAgentRemoved?.();
- }
- });
-
- const handleDelete = useMutableCallback((e) => {
- e.stopPropagation();
- const onDeleteAgent = async (): Promise => {
- try {
- await handleRemoveClick();
- dispatchToastMessage({ type: 'success', message: t('Agent_removed') });
- } catch (error) {
- dispatchToastMessage({ type: 'error', message: error });
- }
- setModal();
- };
-
- setModal( setModal()} confirmText={t('Delete')} />);
- });
-
- return (
-
-
-
- );
-};
-
-export default RemoveAgentButton;
diff --git a/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx b/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx
new file mode 100644
index 000000000000..206d7aab2668
--- /dev/null
+++ b/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx
@@ -0,0 +1,36 @@
+import type { ILivechatAgent } from '@rocket.chat/core-typings';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import { useSetModal, useToastMessageDispatch, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts';
+import { useQueryClient } from '@tanstack/react-query';
+import React from 'react';
+
+import GenericModal from '../../../../components/GenericModal';
+
+export const useRemoveAgent = (uid: ILivechatAgent['_id']) => {
+ const t = useTranslation();
+ const router = useRouter();
+ const setModal = useSetModal();
+ const queryClient = useQueryClient();
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const deleteAction = useEndpoint('DELETE', '/v1/livechat/users/agent/:_id', { _id: uid });
+
+ const handleDelete = useMutableCallback(() => {
+ const onDeleteAgent = async () => {
+ try {
+ await deleteAction();
+ dispatchToastMessage({ type: 'success', message: t('Agent_removed') });
+ router.navigate('/omnichannel/agents');
+ queryClient.invalidateQueries(['livechat-agents']);
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ } finally {
+ setModal();
+ }
+ };
+
+ setModal( setModal()} confirmText={t('Delete')} />);
+ });
+
+ return handleDelete;
+};
diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx
index 1894b1367c15..75a2656dc4ea 100644
--- a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx
+++ b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx
@@ -1,21 +1,32 @@
import { NumberInput, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage';
+import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
-import type { FC } from 'react';
+import type { ComponentProps } from 'react';
import React from 'react';
+import { useFormContext, Controller } from 'react-hook-form';
-const MaxChatsPerAgent: FC<{
- values: { maxNumberSimultaneousChat: number };
- handlers: { handleMaxNumberSimultaneousChat: () => void };
-}> = ({ values, handlers }) => {
+import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
+
+const MaxChatsPerAgent = ({ className }: { className?: ComponentProps['className'] }) => {
const t = useTranslation();
- const { maxNumberSimultaneousChat } = values;
- const { handleMaxNumberSimultaneousChat } = handlers;
+ const { control } = useFormContext();
+ const hasLicense = useHasLicenseModule('livechat-enterprise');
+
+ const maxChatsField = useUniqueId();
+
+ if (!hasLicense) {
+ return null;
+ }
return (
-
- {t('Max_number_of_chats_per_agent')}
+
+ {t('Max_number_of_chats_per_agent')}
-
+ }
+ />
);
diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js
deleted file mode 100644
index f8e70e020c92..000000000000
--- a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentContainer.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-
-import { useForm } from '../../../../client/hooks/useForm';
-import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
-import MaxChatsPerAgent from './MaxChatsPerAgent';
-
-const MaxChatsPerAgentContainer = ({ data: { livechat: { maxNumberSimultaneousChat = '' } = {} } = {}, onChange }) => {
- const hasLicense = useHasLicenseModule('livechat-enterprise');
-
- const { values, handlers, hasUnsavedChanges, commit, reset } = useForm({
- maxNumberSimultaneousChat,
- });
-
- onChange({ values, hasUnsavedChanges, commit, reset });
-
- if (!hasLicense) {
- return null;
- }
-
- return ;
-};
-
-export default MaxChatsPerAgentContainer;
diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.js b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx
similarity index 77%
rename from apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.js
rename to apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx
index f166678607aa..286b1a344d3b 100644
--- a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.js
+++ b/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx
@@ -4,7 +4,7 @@ import React from 'react';
import UserInfo from '../../../../client/components/UserInfo';
import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
-const MaxChatsPerAgentDisplay = ({ data: { livechat: { maxNumberSimultaneousChat = 0 } = {} } = {} }) => {
+const MaxChatsPerAgentDisplay = ({ maxNumberSimultaneousChat = 0 }) => {
const t = useTranslation();
const hasLicense = useHasLicenseModule('livechat-enterprise');
@@ -12,12 +12,12 @@ const MaxChatsPerAgentDisplay = ({ data: { livechat: { maxNumberSimultaneousChat
return null;
}
- return maxNumberSimultaneousChat ? (
+ return (
<>
{t('Max_number_of_chats_per_agent')}
{maxNumberSimultaneousChat}
>
- ) : null;
+ );
};
export default MaxChatsPerAgentDisplay;
diff --git a/packages/core-typings/src/ILivechatAgent.ts b/packages/core-typings/src/ILivechatAgent.ts
index 68a99c2723d9..f106239400b1 100644
--- a/packages/core-typings/src/ILivechatAgent.ts
+++ b/packages/core-typings/src/ILivechatAgent.ts
@@ -7,12 +7,11 @@ export enum ILivechatAgentStatus {
export interface ILivechatAgent extends IUser {
statusLivechat: ILivechatAgentStatus;
- livechat: {
+ livechat?: {
maxNumberSimultaneousChat: number;
};
livechatCount: number;
lastRoutingTime: Date;
livechatStatusSystemModified?: boolean;
-
openBusinessHours?: string[];
}
From dd5fd6d2c8e9b9a3746dd81580828c6dd64e4f93 Mon Sep 17 00:00:00 2001
From: Douglas Fabris
Date: Tue, 5 Dec 2023 16:40:00 -0300
Subject: [PATCH 05/21] feat: Skip to main content shortcut and
`useDocumentTitle` (#30680)
Co-authored-by: Guilherme Gazzo
---
.changeset/kind-beers-share.md | 7 +++
.../client/components/Page/PageHeader.tsx | 8 +--
apps/meteor/client/sidebar/Item/Condensed.tsx | 2 +-
apps/meteor/client/sidebar/Item/Extended.tsx | 4 +-
apps/meteor/client/sidebar/Item/Medium.tsx | 2 +-
apps/meteor/client/startup/unread.ts | 5 --
.../client/views/directory/DirectoryPage.tsx | 2 +-
.../channels/ChannelsTable/ChannelsTable.tsx | 2 +-
.../tabs/teams/TeamsTable/TeamsTable.tsx | 2 +-
.../tabs/users/UsersTable/UsersTable.tsx | 2 +-
.../views/home/cards/CustomContentCard.tsx | 12 ++--
.../client/views/room/Header/RoomTitle.tsx | 18 +++---
apps/meteor/client/views/root/AppLayout.tsx | 7 ++-
.../views/root/DocumentTitleWrapper.tsx | 55 +++++++++++++++++++
.../root/MainLayout/AccessibilityShortcut.tsx | 33 +++++++++++
.../root/MainLayout/LayoutWithSidebar.tsx | 7 ++-
.../views/root/hooks/useUnreadMessages.ts | 14 +++++
.../rocketchat-i18n/i18n/en.i18n.json | 1 +
apps/meteor/tests/e2e/homepage.spec.ts | 12 ++--
.../src/hooks/useDocumentTitle.spec.ts | 26 +++++++++
.../ui-client/src/hooks/useDocumentTitle.ts | 54 ++++++++++++++++++
packages/ui-client/src/index.ts | 1 +
.../web-ui-registration/src/GuestForm.tsx | 2 +
.../web-ui-registration/src/LoginForm.tsx | 3 +
.../src/RegisterSecretPageRouter.tsx | 5 ++
.../src/ResetPasswordForm.tsx | 3 +
26 files changed, 249 insertions(+), 40 deletions(-)
create mode 100644 .changeset/kind-beers-share.md
create mode 100644 apps/meteor/client/views/root/DocumentTitleWrapper.tsx
create mode 100644 apps/meteor/client/views/root/MainLayout/AccessibilityShortcut.tsx
create mode 100644 apps/meteor/client/views/root/hooks/useUnreadMessages.ts
create mode 100644 packages/ui-client/src/hooks/useDocumentTitle.spec.ts
create mode 100644 packages/ui-client/src/hooks/useDocumentTitle.ts
diff --git a/.changeset/kind-beers-share.md b/.changeset/kind-beers-share.md
new file mode 100644
index 000000000000..b7871e568d10
--- /dev/null
+++ b/.changeset/kind-beers-share.md
@@ -0,0 +1,7 @@
+---
+"@rocket.chat/meteor": minor
+"@rocket.chat/ui-client": minor
+"@rocket.chat/web-ui-registration": minor
+---
+
+feat: Skip to main content shortcut and useDocumentTitle
diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx
index 6725c2da6f5c..25d20381e52e 100644
--- a/apps/meteor/client/components/Page/PageHeader.tsx
+++ b/apps/meteor/client/components/Page/PageHeader.tsx
@@ -1,6 +1,5 @@
import { Box, IconButton } from '@rocket.chat/fuselage';
-import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
-import { HeaderToolbox } from '@rocket.chat/ui-client';
+import { HeaderToolbox, useDocumentTitle } from '@rocket.chat/ui-client';
import { useLayout, useTranslation } from '@rocket.chat/ui-contexts';
import type { FC, ComponentProps, ReactNode } from 'react';
import React, { useContext } from 'react';
@@ -18,12 +17,11 @@ const PageHeader: FC = ({ children = undefined, title, onClickB
const t = useTranslation();
const [border] = useContext(PageContext);
const { isMobile } = useLayout();
- const headerAutoFocus = useAutoFocus();
+
+ useDocumentTitle(typeof title === 'string' ? title : undefined);
return (
= ({ icon, title = '', avatar, actions, href
{badges && {badges}}
{menu && (
- {menuVisibility ? menu() : }
+ {menuVisibility ? menu() : }
)}
diff --git a/apps/meteor/client/sidebar/Item/Extended.tsx b/apps/meteor/client/sidebar/Item/Extended.tsx
index c997a48b5b4a..73493a4aee8f 100644
--- a/apps/meteor/client/sidebar/Item/Extended.tsx
+++ b/apps/meteor/client/sidebar/Item/Extended.tsx
@@ -54,7 +54,7 @@ const Extended: VFC = ({
};
return (
-
+
{avatar && {avatar}}
@@ -72,7 +72,7 @@ const Extended: VFC = ({
{badges}
{menu && (
- {menuVisibility ? menu() : }
+ {menuVisibility ? menu() : }
)}
diff --git a/apps/meteor/client/sidebar/Item/Medium.tsx b/apps/meteor/client/sidebar/Item/Medium.tsx
index 2c97b890988f..6feed3071ffc 100644
--- a/apps/meteor/client/sidebar/Item/Medium.tsx
+++ b/apps/meteor/client/sidebar/Item/Medium.tsx
@@ -42,7 +42,7 @@ const Medium: VFC = ({ icon, title = '', avatar, actions, href, bad
{badges && {badges}}
{menu && (
- {menuVisibility ? menu() : }
+ {menuVisibility ? menu() : }
)}
diff --git a/apps/meteor/client/startup/unread.ts b/apps/meteor/client/startup/unread.ts
index 6c076e4ba107..d9c2a35efab5 100644
--- a/apps/meteor/client/startup/unread.ts
+++ b/apps/meteor/client/startup/unread.ts
@@ -5,7 +5,6 @@ import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { ChatSubscription, ChatRoom } from '../../app/models/client';
-import { settings } from '../../app/settings/client';
import { getUserPreference } from '../../app/utils/client';
import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent';
@@ -78,13 +77,9 @@ Meteor.startup(() => {
const updateFavicon = manageFavicon();
Tracker.autorun(() => {
- const siteName = settings.get('Site_Name') ?? '';
-
const unread = Session.get('unread');
fireGlobalEvent('unread-changed', unread);
updateFavicon(unread);
-
- document.title = unread === '' ? siteName : `(${unread}) ${siteName}`;
});
});
diff --git a/apps/meteor/client/views/directory/DirectoryPage.tsx b/apps/meteor/client/views/directory/DirectoryPage.tsx
index 2bcd32a2a5e8..d260971dde2c 100644
--- a/apps/meteor/client/views/directory/DirectoryPage.tsx
+++ b/apps/meteor/client/views/directory/DirectoryPage.tsx
@@ -56,8 +56,8 @@ const DirectoryPage = (): ReactElement => {
)}
- {tab === 'users' && }
{tab === 'channels' && }
+ {tab === 'users' && }
{tab === 'teams' && }
{federationEnabled && tab === 'external' && }
diff --git a/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx b/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx
index 0d2b9f8db289..8ec510eed76d 100644
--- a/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx
+++ b/apps/meteor/client/views/directory/tabs/channels/ChannelsTable/ChannelsTable.tsx
@@ -96,7 +96,7 @@ const ChannelsTable = () => {
return (
<>
- setText(text)} />
+ setText(text)} />
{isLoading && (
{headers}
diff --git a/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx b/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx
index 160ce8e8cc59..f33b359e4b07 100644
--- a/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx
+++ b/apps/meteor/client/views/directory/tabs/teams/TeamsTable/TeamsTable.tsx
@@ -73,7 +73,7 @@ const TeamsTable = () => {
return (
<>
- setText(text)} />
+ setText(text)} />
{isLoading && (
{headers}
diff --git a/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx
index c67d408aa2a3..5b69a59909ca 100644
--- a/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx
+++ b/apps/meteor/client/views/directory/tabs/users/UsersTable/UsersTable.tsx
@@ -95,7 +95,7 @@ const UsersTable = ({ workspace = 'local' }): ReactElement => {
return (
<>
- setText(text)} />
+ setText(text)} />
{isLoading && (
{headers}
diff --git a/apps/meteor/client/views/home/cards/CustomContentCard.tsx b/apps/meteor/client/views/home/cards/CustomContentCard.tsx
index 44f8c6262406..7683f1e1c300 100644
--- a/apps/meteor/client/views/home/cards/CustomContentCard.tsx
+++ b/apps/meteor/client/views/home/cards/CustomContentCard.tsx
@@ -13,10 +13,10 @@ const CustomContentCard = (): ReactElement | null => {
const { data } = useIsEnterprise();
const isAdmin = useRole('admin');
- const customContentBody = String(useSetting('Layout_Home_Body'));
+ const customContentBody = useSetting('Layout_Home_Body');
const isCustomContentBodyEmpty = customContentBody === '';
- const isCustomContentVisible = Boolean(useSetting('Layout_Home_Custom_Block_Visible'));
- const isCustomContentOnly = Boolean(useSetting('Layout_Custom_Body_Only'));
+ const isCustomContentVisible = useSetting('Layout_Home_Custom_Block_Visible');
+ const isCustomContentOnly = useSetting('Layout_Custom_Body_Only');
const settingsRoute = useRoute('admin-settings');
@@ -55,14 +55,12 @@ const CustomContentCard = (): ReactElement | null => {
return (
-
+
{willNotShowCustomContent ? t('Not_Visible_To_Workspace') : t('Visible_To_Workspace')}
-
- {isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : }
-
+ {isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : }
settingsRoute.push({ group: 'Layout' })} title={t('Layout_Home_Page_Content')}>
diff --git a/apps/meteor/client/views/room/Header/RoomTitle.tsx b/apps/meteor/client/views/room/Header/RoomTitle.tsx
index f1cf95db1eaa..13b628ab3823 100644
--- a/apps/meteor/client/views/room/Header/RoomTitle.tsx
+++ b/apps/meteor/client/views/room/Header/RoomTitle.tsx
@@ -1,5 +1,5 @@
import type { IRoom } from '@rocket.chat/core-typings';
-import { HeaderTitle } from '@rocket.chat/ui-client';
+import { HeaderTitle, useDocumentTitle } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import React from 'react';
@@ -9,11 +9,15 @@ type RoomTitleProps = {
room: IRoom;
};
-const RoomTitle = ({ room }: RoomTitleProps): ReactElement => (
- <>
-
- {room.name}
- >
-);
+const RoomTitle = ({ room }: RoomTitleProps): ReactElement => {
+ useDocumentTitle(room.name, false);
+
+ return (
+ <>
+
+ {room.name}
+ >
+ );
+};
export default RoomTitle;
diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx
index 5bdcd9d6a5b5..68b1c0ec155d 100644
--- a/apps/meteor/client/views/root/AppLayout.tsx
+++ b/apps/meteor/client/views/root/AppLayout.tsx
@@ -4,6 +4,7 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { useAnalytics } from '../../../app/analytics/client/loadScript';
import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking';
import { appLayout } from '../../lib/appLayout';
+import DocumentTitleWrapper from './DocumentTitleWrapper';
import PageLoading from './PageLoading';
import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke';
import { useGoogleTagManager } from './hooks/useGoogleTagManager';
@@ -26,7 +27,11 @@ const AppLayout = () => {
const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot);
- return }>{layout};
+ return (
+ }>
+ {layout}
+
+ );
};
export default AppLayout;
diff --git a/apps/meteor/client/views/root/DocumentTitleWrapper.tsx b/apps/meteor/client/views/root/DocumentTitleWrapper.tsx
new file mode 100644
index 000000000000..fb48e4d90913
--- /dev/null
+++ b/apps/meteor/client/views/root/DocumentTitleWrapper.tsx
@@ -0,0 +1,55 @@
+import { css } from '@rocket.chat/css-in-js';
+import { Box } from '@rocket.chat/fuselage';
+import { useDocumentTitle } from '@rocket.chat/ui-client';
+import { useSetting } from '@rocket.chat/ui-contexts';
+import type { FC } from 'react';
+import React, { useEffect, useCallback } from 'react';
+
+import { useUnreadMessages } from './hooks/useUnreadMessages';
+
+const useRouteTitleFocus = () => {
+ return useCallback((node: HTMLElement | null) => {
+ if (!node) {
+ return;
+ }
+
+ node.focus();
+ }, []);
+};
+
+const DocumentTitleWrapper: FC = ({ children }) => {
+ useDocumentTitle(useSetting('Site_Name') || '', false);
+ const { title, key } = useDocumentTitle(useUnreadMessages(), false);
+
+ const refocusRef = useRouteTitleFocus();
+
+ useEffect(() => {
+ document.title = title;
+ }, [title]);
+
+ return (
+ <>
+
+ {title}
+
+ {children}
+ >
+ );
+};
+
+export default DocumentTitleWrapper;
diff --git a/apps/meteor/client/views/root/MainLayout/AccessibilityShortcut.tsx b/apps/meteor/client/views/root/MainLayout/AccessibilityShortcut.tsx
new file mode 100644
index 000000000000..f82f5d96ec3b
--- /dev/null
+++ b/apps/meteor/client/views/root/MainLayout/AccessibilityShortcut.tsx
@@ -0,0 +1,33 @@
+import { css } from '@rocket.chat/css-in-js';
+import { Button } from '@rocket.chat/fuselage';
+import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
+import React from 'react';
+
+const AccessibilityShortcut = () => {
+ const t = useTranslation();
+ const router = useRouter();
+ const currentRoutePath = router.getLocationPathname();
+
+ const customButtonClass = css`
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ z-index: 99;
+ &:not(:focus) {
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(1px, 1px, 1px, 1px);
+ border: 0;
+ }
+ `;
+
+ return (
+
+ {t('Skip_to_main_content')}
+
+ );
+};
+
+export default AccessibilityShortcut;
diff --git a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx
index 5a5ea0998c67..1edd6966d065 100644
--- a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx
+++ b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx
@@ -6,6 +6,7 @@ import type { ReactElement, ReactNode } from 'react';
import React, { useEffect, useRef } from 'react';
import Sidebar from '../../../sidebar';
+import AccessibilityShortcut from './AccessibilityShortcut';
const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement => {
const { isEmbedded: embeddedLayout } = useLayout();
@@ -46,10 +47,14 @@ const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement
className={[embeddedLayout ? 'embedded-view' : undefined, 'menu-nav'].filter(Boolean).join(' ')}
aria-hidden={Boolean(modal)}
>
+
{!removeSidenav && }
-
+
{children}
diff --git a/apps/meteor/client/views/root/hooks/useUnreadMessages.ts b/apps/meteor/client/views/root/hooks/useUnreadMessages.ts
new file mode 100644
index 000000000000..0f2ee34b8c4c
--- /dev/null
+++ b/apps/meteor/client/views/root/hooks/useUnreadMessages.ts
@@ -0,0 +1,14 @@
+import { useSession, useTranslation } from '@rocket.chat/ui-contexts';
+
+export const useUnreadMessages = (): string | undefined => {
+ const t = useTranslation();
+ const unreadMessages = useSession('unread');
+
+ return (() => {
+ if (unreadMessages === '') {
+ return undefined;
+ }
+
+ return t('unread_messages_counter', { count: unreadMessages });
+ })();
+};
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index 6898eb26e4a1..33df4302103b 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -4775,6 +4775,7 @@
"Size": "Size",
"Skin_tone": "Skin tone",
"Skip": "Skip",
+ "Skip_to_main_content": "Skip to main content",
"SLA_Policy": "SLA Policy",
"SLA_Policies": "SLA Policies",
"SLA_removed": "SLA removed",
diff --git a/apps/meteor/tests/e2e/homepage.spec.ts b/apps/meteor/tests/e2e/homepage.spec.ts
index 4f8f9d09a2f3..89aa905744df 100644
--- a/apps/meteor/tests/e2e/homepage.spec.ts
+++ b/apps/meteor/tests/e2e/homepage.spec.ts
@@ -50,7 +50,7 @@ test.describe.serial('homepage', () => {
test('visibility and button functionality in custom body with empty custom content', async () => {
await test.step('expect default value in custom body', async () => {
await expect(
- adminPage.locator('role=status[name="Admins may insert content html to be rendered in this white space."]'),
+ adminPage.locator('div >> text="Admins may insert content html to be rendered in this white space."'),
).toBeVisible();
});
@@ -60,7 +60,7 @@ test.describe.serial('homepage', () => {
});
await test.step('expect visibility tag to show "not visible"', async () => {
- await expect(adminPage.locator('role=status[name="Not visible to workspace"]')).toBeVisible();
+ await expect(adminPage.locator('span >> text="Not visible to workspace"')).toBeVisible();
});
});
});
@@ -72,7 +72,7 @@ test.describe.serial('homepage', () => {
test('visibility and button functionality in custom body with custom content', async () => {
await test.step('expect custom body to be visible', async () => {
- await expect(adminPage.locator('role=status[name="Hello admin"]')).toBeVisible();
+ await expect(adminPage.locator('div >> text="Hello admin"')).toBeVisible();
});
await test.step('expect correct state for card buttons', async () => {
@@ -101,7 +101,7 @@ test.describe.serial('homepage', () => {
});
await test.step('expect visibility tag to show "visible to workspace"', async () => {
- await expect(adminPage.locator('role=status[name="Visible to workspace"]')).toBeVisible();
+ await expect(adminPage.locator('span >> text="Visible to workspace"')).toBeVisible();
});
});
});
@@ -188,7 +188,7 @@ test.describe.serial('homepage', () => {
});
test('expect custom body to be visible', async () => {
- await expect(regularUserPage.locator('role=status[name="Hello"]')).toBeVisible();
+ await expect(regularUserPage.locator('div >> text="Hello"')).toBeVisible();
});
test.describe('enterprise edition', () => {
@@ -208,7 +208,7 @@ test.describe.serial('homepage', () => {
});
await test.step('expect custom body to be visible', async () => {
- await expect(regularUserPage.locator('role=status[name="Hello"]')).toBeVisible();
+ await expect(regularUserPage.locator('div >> text="Hello"')).toBeVisible();
});
});
});
diff --git a/packages/ui-client/src/hooks/useDocumentTitle.spec.ts b/packages/ui-client/src/hooks/useDocumentTitle.spec.ts
new file mode 100644
index 000000000000..e5df07fb354c
--- /dev/null
+++ b/packages/ui-client/src/hooks/useDocumentTitle.spec.ts
@@ -0,0 +1,26 @@
+import { renderHook } from '@testing-library/react-hooks';
+
+import { useDocumentTitle } from './useDocumentTitle';
+
+const DEFAULT_TITLE = 'Default Title';
+const EXAMPLE_TITLE = 'Example Title';
+
+it('should return the default title', () => {
+ const { result } = renderHook(() => useDocumentTitle(DEFAULT_TITLE));
+
+ expect(result.current.title).toBe(DEFAULT_TITLE);
+});
+
+it('should return the default title and empty key value if refocus param is false', () => {
+ const { result } = renderHook(() => useDocumentTitle(DEFAULT_TITLE, false));
+
+ expect(result.current.title).toBe(DEFAULT_TITLE);
+ expect(result.current.key).toBe('');
+});
+
+it('should return the default title and the example title concatenated', () => {
+ renderHook(() => useDocumentTitle(DEFAULT_TITLE));
+ const { result } = renderHook(() => useDocumentTitle(EXAMPLE_TITLE));
+
+ expect(result.current.title).toBe(`${EXAMPLE_TITLE} - ${DEFAULT_TITLE}`);
+});
diff --git a/packages/ui-client/src/hooks/useDocumentTitle.ts b/packages/ui-client/src/hooks/useDocumentTitle.ts
new file mode 100644
index 000000000000..5c5aca8b2296
--- /dev/null
+++ b/packages/ui-client/src/hooks/useDocumentTitle.ts
@@ -0,0 +1,54 @@
+import { Emitter } from '@rocket.chat/emitter';
+import { useCallback, useEffect } from 'react';
+import { useSyncExternalStore } from 'use-sync-external-store/shim';
+
+const ee = new Emitter<{
+ change: void;
+}>();
+
+const titles = new Set<{
+ title?: string;
+ refocus?: boolean;
+}>();
+
+const useReactiveDocumentTitle = (): string =>
+ useSyncExternalStore(
+ useCallback((callback) => ee.on('change', callback), []),
+ (): string =>
+ Array.from(titles)
+ .reverse()
+ .map(({ title }) => title)
+ .join(' - '),
+ );
+
+const useReactiveDocumentTitleKey = (): string =>
+ useSyncExternalStore(
+ useCallback((callback) => ee.on('change', callback), []),
+ (): string =>
+ Array.from(titles)
+ .filter(({ refocus }) => refocus)
+ .map(({ title }) => title)
+ .join(' - '),
+ );
+
+export const useDocumentTitle = (documentTitle?: string, refocus = true) => {
+ useEffect(() => {
+ const titleObj = {
+ title: documentTitle,
+ refocus,
+ };
+
+ if (titleObj.title) {
+ titles.add(titleObj);
+ }
+
+ ee.emit('change');
+
+ return () => {
+ titles.delete(titleObj);
+ ee.emit('change');
+ };
+ }, [documentTitle, refocus]);
+
+ return { title: useReactiveDocumentTitle(), key: useReactiveDocumentTitleKey() };
+};
diff --git a/packages/ui-client/src/index.ts b/packages/ui-client/src/index.ts
index 0e4454d38dbf..c04e795f6ab5 100644
--- a/packages/ui-client/src/index.ts
+++ b/packages/ui-client/src/index.ts
@@ -1,3 +1,4 @@
export * from './components';
export * from './hooks/useFeaturePreview';
export * from './hooks/useFeaturePreviewList';
+export * from './hooks/useDocumentTitle';
diff --git a/packages/web-ui-registration/src/GuestForm.tsx b/packages/web-ui-registration/src/GuestForm.tsx
index 59df56837d86..d6b5fe15f135 100644
--- a/packages/web-ui-registration/src/GuestForm.tsx
+++ b/packages/web-ui-registration/src/GuestForm.tsx
@@ -1,11 +1,13 @@
import { Button, ButtonGroup } from '@rocket.chat/fuselage';
import { Form } from '@rocket.chat/layout';
+import { useDocumentTitle } from '@rocket.chat/ui-client';
import { useTranslation } from 'react-i18next';
import type { DispatchLoginRouter } from './hooks/useLoginRouter';
const GuestForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRouter }) => {
const { t } = useTranslation();
+ useDocumentTitle(t('registration.component.login'), false);
return (