From 2c4cb3519244c3aabc25b312c8b093d4719b11a2 Mon Sep 17 00:00:00 2001
From: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat>
Date: Wed, 4 May 2022 14:47:26 -0300
Subject: [PATCH] Chore: Rewrite some Omnichannel files to TypeScript (#25359)

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
---
 .../imports/server/rest/departments.ts        |  19 +--
 .../Omnichannel/{Tags.js => Tags.tsx}         |  58 ++++---
 .../Omnichannel/modals/CloseChatModal.js      | 101 ------------
 .../Omnichannel/modals/CloseChatModal.tsx     | 125 +++++++++++++++
 .../Omnichannel/modals/CloseChatModalData.js  |  17 --
 .../Omnichannel/modals/CloseChatModalData.tsx |  44 ++++++
 .../Omnichannel/modals/ForwardChatModal.js    | 138 -----------------
 .../Omnichannel/modals/ForwardChatModal.tsx   | 146 ++++++++++++++++++
 .../omnichannel/currentChats/FilterByText.tsx |   6 +-
 .../directory/chats/contextualBar/RoomEdit.js |   8 +-
 .../QuickActions/hooks/useQuickActions.tsx    |   2 +-
 .../rocketchat-i18n/i18n/en.i18n.json         |   1 +
 packages/rest-typings/src/v1/omnichannel.ts   |   7 +-
 13 files changed, 372 insertions(+), 300 deletions(-)
 rename apps/meteor/client/components/Omnichannel/{Tags.js => Tags.tsx} (59%)
 delete mode 100644 apps/meteor/client/components/Omnichannel/modals/CloseChatModal.js
 create mode 100644 apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx
 delete mode 100644 apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.js
 create mode 100644 apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx
 delete mode 100644 apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.js
 create mode 100644 apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx

diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts
index 80b03eb209b..ab8ee8a66e4 100644
--- a/apps/meteor/app/livechat/imports/server/rest/departments.ts
+++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts
@@ -74,28 +74,25 @@ API.v1.addRoute(
 	'livechat/department/:_id',
 	{ authRequired: true },
 	{
-		get() {
+		async get() {
 			check(this.urlParams, {
 				_id: String,
 			});
 
 			const { onlyMyDepartments } = this.queryParams;
 
-			const { department, agents } = Promise.await(
-				findDepartmentById({
-					userId: this.userId,
-					departmentId: this.urlParams._id,
-					includeAgents: this.queryParams.includeAgents && this.queryParams.includeAgents === 'true',
-					onlyMyDepartments: onlyMyDepartments === 'true',
-				}),
-			);
+			const { department, agents } = await findDepartmentById({
+				userId: this.userId,
+				departmentId: this.urlParams._id,
+				includeAgents: this.queryParams.includeAgents && this.queryParams.includeAgents === 'true',
+				onlyMyDepartments: onlyMyDepartments === 'true',
+			});
 
 			// TODO: return 404 when department is not found
 			// Currently, FE relies on the fact that this endpoint returns an empty payload
 			// to show the "new" view. Returning 404 breaks it
-			const result = { department, agents };
 
-			return API.v1.success(result);
+			return API.v1.success({ department, agents });
 		},
 		put() {
 			const permissionToSave = hasPermission(this.userId, 'manage-livechat-departments');
diff --git a/apps/meteor/client/components/Omnichannel/Tags.js b/apps/meteor/client/components/Omnichannel/Tags.tsx
similarity index 59%
rename from apps/meteor/client/components/Omnichannel/Tags.js
rename to apps/meteor/client/components/Omnichannel/Tags.tsx
index 736827496f4..1d315ead091 100644
--- a/apps/meteor/client/components/Omnichannel/Tags.js
+++ b/apps/meteor/client/components/Omnichannel/Tags.tsx
@@ -1,6 +1,6 @@
 import { Field, TextInput, Chip, Button } from '@rocket.chat/fuselage';
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { useState } from 'react';
+import React, { ChangeEvent, ReactElement, useState } from 'react';
 import { useSubscription } from 'use-subscription';
 
 import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
@@ -10,30 +10,48 @@ import { useEndpointData } from '../../hooks/useEndpointData';
 import { formsSubscription } from '../../views/omnichannel/additionalForms';
 import { FormSkeleton } from './Skeleton';
 
-const Tags = ({ tags = [], handler = () => {}, error = '', tagRequired = false }) => {
-	const { value: tagsResult = [], phase: stateTags } = useEndpointData('livechat/tags.list');
+const Tags = ({
+	tags,
+	handler,
+	error,
+	tagRequired,
+}: {
+	tags?: string[];
+	handler: (value: string[]) => void;
+	error?: string;
+	tagRequired?: boolean;
+}): ReactElement => {
 	const t = useTranslation();
-	const forms = useSubscription(formsSubscription);
+	const forms = useSubscription<any>(formsSubscription);
 
-	const { useCurrentChatTags = () => {} } = forms;
-	const Tags = useCurrentChatTags();
+	const { value: tagsResult, phase: stateTags } = useEndpointData('livechat/tags.list');
+
+	const { useCurrentChatTags } = forms;
+	const EETagsComponent = useCurrentChatTags();
 
 	const dispatchToastMessage = useToastMessageDispatch();
 
 	const [tagValue, handleTagValue] = useState('');
-	const [paginatedTagValue, handlePaginatedTagValue] = useState(tags);
+	const [paginatedTagValue, handlePaginatedTagValue] = useState<{ label: string; value: string }[]>();
 
-	const removeTag = (tag) => {
-		const tagsFiltered = tags.filter((tagArray) => tagArray !== tag);
-		handler(tagsFiltered);
+	const removeTag = (tagToRemove: string): void => {
+		if (tags) {
+			const tagsFiltered = tags.filter((tag: string) => tag !== tagToRemove);
+			handler(tagsFiltered);
+		}
 	};
 
 	const handleTagTextSubmit = useMutableCallback(() => {
+		if (!tags) {
+			return;
+		}
+
 		if (!tagValue || tagValue.trim() === '') {
 			dispatchToastMessage({ type: 'error', message: t('Enter_a_tag') });
 			handleTagValue('');
 			return;
 		}
+
 		if (tags.includes(tagValue)) {
 			dispatchToastMessage({ type: 'error', message: t('Tag_already_exists') });
 			return;
@@ -46,18 +64,16 @@ const Tags = ({ tags = [], handler = () => {}, error = '', tagRequired = false }
 		return <FormSkeleton />;
 	}
 
-	const { tags: tagsList } = tagsResult;
-
 	return (
 		<>
 			<Field.Label required={tagRequired} mb='x4'>
 				{t('Tags')}
 			</Field.Label>
-			{Tags && tagsList && tagsList.length > 0 ? (
+			{EETagsComponent && tagsResult?.tags && tagsResult?.tags.length ? (
 				<Field.Row>
-					<Tags
+					<EETagsComponent
 						value={paginatedTagValue}
-						handler={(tags) => {
+						handler={(tags: { label: string; value: string }[]): void => {
 							handler(tags.map((tag) => tag.label));
 							handlePaginatedTagValue(tags);
 						}}
@@ -68,19 +84,19 @@ const Tags = ({ tags = [], handler = () => {}, error = '', tagRequired = false }
 					<Field.Row>
 						<TextInput
 							error={error}
-							value={tagValue?.value ? tagValue.value : tagValue}
-							onChange={(event) => handleTagValue(event.target.value)}
+							value={tagValue}
+							onChange={({ currentTarget }: ChangeEvent<HTMLInputElement>): void => handleTagValue(currentTarget.value)}
 							flexGrow={1}
 							placeholder={t('Enter_a_tag')}
 						/>
-						<Button disabled={!tagValue} mis='x8' title={t('add')} onClick={handleTagTextSubmit}>
+						<Button disabled={!tagValue} mis='x8' title={t('Add')} onClick={handleTagTextSubmit}>
 							{t('Add')}
 						</Button>
 					</Field.Row>
 					<Field.Row justifyContent='flex-start'>
-						{tags.map((tag, i) => (
-							<Chip key={i} onClick={() => removeTag(tag)} mie='x8'>
-								{tag?.value ? tag.value : tag}
+						{tags?.map((tag, i) => (
+							<Chip key={i} onClick={(): void => removeTag(tag)} mie='x8'>
+								{tag}
 							</Chip>
 						))}
 					</Field.Row>
diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.js b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.js
deleted file mode 100644
index e303a0fca03..00000000000
--- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import { Field, Button, TextInput, Icon, ButtonGroup, Modal, Box } from '@rocket.chat/fuselage';
-import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
-import React, { useCallback, useState, useMemo, useEffect } from 'react';
-
-import { useSetting } from '../../../contexts/SettingsContext';
-import { useTranslation } from '../../../contexts/TranslationContext';
-import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate';
-import { useForm } from '../../../hooks/useForm';
-import GenericModal from '../../GenericModal';
-import Tags from '../Tags';
-
-const CloseChatModal = ({ department = {}, onCancel, onConfirm }) => {
-	const t = useTranslation();
-
-	const inputRef = useAutoFocus(true);
-
-	const { values, handlers } = useForm({ comment: '', tags: [] });
-
-	const commentRequired = useSetting('Livechat_request_comment_when_closing_conversation');
-
-	const { comment, tags } = values;
-	const { handleComment, handleTags } = handlers;
-	const [commentError, setCommentError] = useState('');
-	const [tagError, setTagError] = useState('');
-	const [tagRequired, setTagRequired] = useState(false);
-
-	const handleConfirm = useCallback(() => {
-		onConfirm(comment, tags);
-	}, [comment, onConfirm, tags]);
-
-	useComponentDidUpdate(() => {
-		setCommentError(!comment && commentRequired ? t('The_field_is_required', t('Comment')) : '');
-	}, [commentRequired, comment, t]);
-
-	const canConfirm = useMemo(() => {
-		const canConfirmTag = !tagError && (tagRequired ? tags.length > 0 : true);
-		const canConfirmComment = !commentError && (commentRequired ? !!comment : true);
-		return canConfirmTag && canConfirmComment;
-	}, [comment, commentError, commentRequired, tagError, tagRequired, tags.length]);
-
-	useEffect(() => {
-		department?.requestTagBeforeClosingChat && setTagRequired(true);
-		setTagError(tagRequired && (!tags || tags.length === 0) ? t('error-tags-must-be-assigned-before-closing-chat') : '');
-	}, [department, tagRequired, t, tags]);
-
-	if (!commentRequired && !tagRequired) {
-		return (
-			<GenericModal
-				variant='warning'
-				title={t('Are_you_sure_you_want_to_close_this_chat')}
-				onConfirm={handleConfirm}
-				onCancel={onCancel}
-				onClose={onCancel}
-				confirmText={t('Confirm')}
-			></GenericModal>
-		);
-	}
-
-	return (
-		<Modal>
-			<Modal.Header>
-				<Icon name='baloon-close-top-right' size={20} />
-				<Modal.Title>{t('Closing_chat')}</Modal.Title>
-				<Modal.Close onClick={onCancel} />
-			</Modal.Header>
-			<Modal.Content fontScale='p2'>
-				<Box color='neutral-600'>{t('Close_room_description')}</Box>
-				<Field marginBlock='x15'>
-					<Field.Label required={commentRequired}>{t('Comment')}</Field.Label>
-					<Field.Row>
-						<TextInput
-							ref={inputRef}
-							error={commentError}
-							flexGrow={1}
-							value={comment}
-							onChange={handleComment}
-							placeholder={t('Please_add_a_comment')}
-						/>
-					</Field.Row>
-					<Field.Error>{commentError}</Field.Error>
-				</Field>
-				{Tags && (
-					<Field>
-						<Tags tagRequired={tagRequired} tags={tags} handler={handleTags} error={tagError} />
-						<Field.Error>{tagError}</Field.Error>
-					</Field>
-				)}
-			</Modal.Content>
-			<Modal.Footer>
-				<ButtonGroup align='end'>
-					<Button onClick={onCancel}>{t('Cancel')}</Button>
-					<Button disabled={!canConfirm} primary onClick={handleConfirm}>
-						{t('Confirm')}
-					</Button>
-				</ButtonGroup>
-			</Modal.Footer>
-		</Modal>
-	);
-};
-
-export default CloseChatModal;
diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx
new file mode 100644
index 00000000000..4493b19ea2d
--- /dev/null
+++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx
@@ -0,0 +1,125 @@
+import { ILivechatDepartment } from '@rocket.chat/core-typings';
+import { Field, Button, TextInput, Icon, ButtonGroup, Modal, Box } from '@rocket.chat/fuselage';
+import React, { useCallback, useState, useEffect, ReactElement, useMemo } from 'react';
+import { useForm } from 'react-hook-form';
+
+import { useSetting } from '../../../contexts/SettingsContext';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import GenericModal from '../../GenericModal';
+import Tags from '../Tags';
+
+const CloseChatModal = ({
+	department,
+	onCancel,
+	onConfirm,
+}: {
+	department?: ILivechatDepartment | null;
+	onCancel: () => void;
+	onConfirm: (comment?: string, tags?: string[]) => Promise<void>;
+}): ReactElement => {
+	const t = useTranslation();
+
+	const {
+		formState: { errors },
+		handleSubmit,
+		register,
+		setError,
+		setFocus,
+		setValue,
+		watch,
+	} = useForm();
+
+	const commentRequired = useSetting('Livechat_request_comment_when_closing_conversation') as boolean;
+	const [tagRequired, setTagRequired] = useState(false);
+
+	const tags = watch('tags');
+	const comment = watch('comment');
+
+	const handleTags = (value: string[]): void => {
+		setValue('tags', value);
+	};
+
+	const onSubmit = useCallback(
+		({ comment, tags }): void => {
+			if (!comment && commentRequired) {
+				setError('comment', { type: 'custom', message: t('The_field_is_required', t('Comment')) });
+			}
+
+			if (!tags?.length && tagRequired) {
+				setError('tags', { type: 'custom', message: t('error-tags-must-be-assigned-before-closing-chat') });
+			}
+
+			if (!errors.comment || errors.tags) {
+				onConfirm(comment, tags);
+			}
+		},
+		[commentRequired, tagRequired, errors, setError, t, onConfirm],
+	);
+
+	const cannotSubmit = useMemo(() => {
+		const cannotSendTag = (tagRequired && !tags?.length) || errors.tags;
+		const cannotSendComment = (commentRequired && !comment) || errors.comment;
+		return cannotSendTag || cannotSendComment;
+	}, [comment, commentRequired, errors, tagRequired, tags]);
+
+	useEffect(() => {
+		if (department?.requestTagBeforeClosingChat) {
+			setTagRequired(true);
+		}
+	}, [department]);
+
+	useEffect(() => {
+		if (commentRequired) {
+			setFocus('comment');
+		}
+	}, [commentRequired, setFocus]);
+
+	useEffect(() => {
+		if (tagRequired) {
+			register('tags');
+		}
+	}, [register, tagRequired]);
+
+	return commentRequired || tagRequired ? (
+		<Modal is='form' onSubmit={handleSubmit(onSubmit)}>
+			<Modal.Header>
+				<Icon name='baloon-close-top-right' size={20} />
+				<Modal.Title>{t('Closing_chat')}</Modal.Title>
+				<Modal.Close onClick={onCancel} />
+			</Modal.Header>
+			<Modal.Content fontScale='p2'>
+				<Box color='neutral-600'>{t('Close_room_description')}</Box>
+				<Field marginBlock='x15'>
+					<Field.Label required={commentRequired}>{t('Comment')}</Field.Label>
+					<Field.Row>
+						<TextInput {...register('comment')} error={errors.comment} flexGrow={1} placeholder={t('Please_add_a_comment')} />
+					</Field.Row>
+					<Field.Error>{errors.comment?.message}</Field.Error>
+				</Field>
+				<Field>
+					<Tags tagRequired={tagRequired} tags={tags} handler={handleTags} />
+					<Field.Error>{errors.tags?.message}</Field.Error>
+				</Field>
+			</Modal.Content>
+			<Modal.Footer>
+				<ButtonGroup align='end'>
+					<Button onClick={onCancel}>{t('Cancel')}</Button>
+					<Button type='submit' disabled={cannotSubmit} primary>
+						{t('Confirm')}
+					</Button>
+				</ButtonGroup>
+			</Modal.Footer>
+		</Modal>
+	) : (
+		<GenericModal
+			variant='warning'
+			title={t('Are_you_sure_you_want_to_close_this_chat')}
+			onConfirm={onConfirm}
+			onCancel={onCancel}
+			onClose={onCancel}
+			confirmText={t('Confirm')}
+		></GenericModal>
+	);
+};
+
+export default CloseChatModal;
diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.js b/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.js
deleted file mode 100644
index 4e6b9ca7695..00000000000
--- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-
-import { AsyncStatePhase } from '../../../hooks/useAsyncState';
-import { useEndpointData } from '../../../hooks/useEndpointData';
-import { FormSkeleton } from '../Skeleton';
-import CloseChatModal from './CloseChatModal';
-
-const CloseChatModalData = ({ departmentId, onCancel, onConfirm }) => {
-	const { value: data, phase: state } = useEndpointData(`livechat/department/${departmentId}?includeAgents=false`);
-	if ([state].includes(AsyncStatePhase.LOADING)) {
-		return <FormSkeleton />;
-	}
-	const { department } = data || {};
-	return <CloseChatModal onCancel={onCancel} onConfirm={onConfirm} department={department} />;
-};
-
-export default CloseChatModalData;
diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx
new file mode 100644
index 00000000000..97893173188
--- /dev/null
+++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx
@@ -0,0 +1,44 @@
+import { ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
+import React, { ReactElement } from 'react';
+
+import { AsyncStatePhase } from '../../../hooks/useAsyncState';
+import { useEndpointData } from '../../../hooks/useEndpointData';
+import { FormSkeleton } from '../Skeleton';
+import CloseChatModal from './CloseChatModal';
+
+const CloseChatModalData = ({
+	departmentId,
+	onCancel,
+	onConfirm,
+}: {
+	departmentId: ILivechatDepartment['_id'];
+	onCancel: () => void;
+	onConfirm: (comment?: string, tags?: string[]) => Promise<void>;
+}): ReactElement => {
+	const { value: data, phase: state } = useEndpointData(`livechat/department/${departmentId}`);
+
+	if ([state].includes(AsyncStatePhase.LOADING)) {
+		return <FormSkeleton />;
+	}
+
+	// TODO: chapter day: fix issue with rest typing
+	// TODO: This is necessary because of a weird problem
+	// There is an endpoint livechat/department/${departmentId}/agents
+	// that is causing the problem. type A | type B | undefined
+
+	return (
+		<CloseChatModal
+			onCancel={onCancel}
+			onConfirm={onConfirm}
+			department={
+				(
+					data as {
+						department: ILivechatDepartment | null;
+						agents?: ILivechatDepartmentAgents[];
+					}
+				).department
+			}
+		/>
+	);
+};
+export default CloseChatModalData;
diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.js b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.js
deleted file mode 100644
index ea151f7a483..00000000000
--- a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import { Field, Button, TextAreaInput, Icon, ButtonGroup, Modal, Box, PaginatedSelectFiltered } from '@rocket.chat/fuselage';
-import { useMutableCallback, useAutoFocus, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
-import React, { useEffect, useMemo, useState } from 'react';
-
-import { useEndpoint } from '../../../contexts/ServerContext';
-import { useTranslation } from '../../../contexts/TranslationContext';
-import { useRecordList } from '../../../hooks/lists/useRecordList';
-import { AsyncStatePhase } from '../../../hooks/useAsyncState';
-import { useForm } from '../../../hooks/useForm';
-import UserAutoComplete from '../../UserAutoComplete';
-import { useDepartmentsList } from '../hooks/useDepartmentsList';
-import ModalSeparator from './ModalSeparator';
-
-const ForwardChatModal = ({ onForward, onCancel, room, ...props }) => {
-	const t = useTranslation();
-
-	const inputRef = useAutoFocus(true);
-
-	const { values, handlers } = useForm({
-		username: '',
-		comment: '',
-		department: {},
-	});
-	const { username, comment, department } = values;
-	const [userId, setUserId] = useState('');
-
-	const { handleUsername, handleComment, handleDepartment } = handlers;
-	const getUserData = useEndpoint('GET', `users.info?username=${username}`);
-
-	const [departmentsFilter, setDepartmentsFilter] = useState('');
-
-	const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500);
-
-	const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList(
-		useMemo(() => ({ filter: debouncedDepartmentsFilter, enabled: true }), [debouncedDepartmentsFilter]),
-	);
-
-	const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList);
-
-	const handleSend = useMutableCallback(() => {
-		onForward(department?.value, userId, comment);
-	}, [onForward, department.value, userId, comment]);
-
-	const onChangeUsername = useMutableCallback((username) => {
-		handleUsername(username);
-	});
-
-	useEffect(() => {
-		if (!username) {
-			return;
-		}
-		const fetchData = async () => {
-			const { user } = await getUserData();
-			setUserId(user._id);
-		};
-		fetchData();
-		// eslint-disable-next-line react-hooks/exhaustive-deps
-	}, [username]);
-
-	const canForward = department || username;
-
-	const departments = departmentsItems;
-
-	const hasDepartments = departments && departments.length > 0;
-
-	const { servedBy: { _id: agentId } = {} } = room || {};
-
-	const _id = agentId && { $ne: agentId };
-
-	const conditions = { _id, status: { $ne: 'offline' }, statusLivechat: 'available' };
-
-	return (
-		<Modal {...props}>
-			<Modal.Header>
-				<Icon name='baloon-arrow-top-right' size={20} />
-				<Modal.Title>{t('Forward_chat')}</Modal.Title>
-				<Modal.Close onClick={onCancel} />
-			</Modal.Header>
-			<Modal.Content fontScale='p2'>
-				<Field mbe={'x30'}>
-					<Field.Label>{t('Forward_to_department')}</Field.Label>
-					<Field.Row>
-						<PaginatedSelectFiltered
-							withTitle
-							filter={departmentsFilter}
-							setFilter={setDepartmentsFilter}
-							options={departmentsItems}
-							value={department}
-							maxWidth='100%'
-							placeholder={t('Select_an_option')}
-							onChange={handleDepartment}
-							flexGrow={1}
-							endReached={
-								departmentsPhase === AsyncStatePhase.LOADING
-									? () => {}
-									: (start) => loadMoreDepartments(start, Math.min(50, departmentsTotal))
-							}
-						/>
-					</Field.Row>
-				</Field>
-				<ModalSeparator text={t('or')} />
-				<Field mbs={hasDepartments && 'x30'}>
-					<Field.Label>{t('Forward_to_user')}</Field.Label>
-					<Field.Row>
-						<UserAutoComplete
-							conditions={conditions}
-							flexGrow={1}
-							value={username}
-							onChange={onChangeUsername}
-							placeholder={t('Username')}
-						/>
-					</Field.Row>
-				</Field>
-				<Field marginBlock='x15'>
-					<Field.Label>
-						{t('Leave_a_comment')}{' '}
-						<Box is='span' color='neutral-600'>
-							({t('Optional')})
-						</Box>
-					</Field.Label>
-					<Field.Row>
-						<TextAreaInput ref={inputRef} rows={8} flexGrow={1} value={comment} onChange={handleComment} />
-					</Field.Row>
-				</Field>
-			</Modal.Content>
-			<Modal.Footer>
-				<ButtonGroup align='end'>
-					<Button onClick={onCancel}>{t('Cancel')}</Button>
-					<Button disabled={!canForward} primary onClick={handleSend}>
-						{t('Forward')}
-					</Button>
-				</ButtonGroup>
-			</Modal.Footer>
-		</Modal>
-	);
-};
-
-export default ForwardChatModal;
diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx
new file mode 100644
index 00000000000..1697b0b4c31
--- /dev/null
+++ b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx
@@ -0,0 +1,146 @@
+import { IOmnichannelRoom } from '@rocket.chat/core-typings';
+import { Field, Button, TextAreaInput, Icon, ButtonGroup, Modal, Box, PaginatedSelectFiltered } from '@rocket.chat/fuselage';
+import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
+import { useForm } from 'react-hook-form';
+
+import { useEndpoint } from '../../../contexts/ServerContext';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { useRecordList } from '../../../hooks/lists/useRecordList';
+import { AsyncStatePhase } from '../../../hooks/useAsyncState';
+import UserAutoComplete from '../../UserAutoComplete';
+import { useDepartmentsList } from '../hooks/useDepartmentsList';
+import ModalSeparator from './ModalSeparator';
+
+const ForwardChatModal = ({
+	onForward,
+	onCancel,
+	room,
+	...props
+}: {
+	onForward: (departmentId?: string, userId?: string, comment?: string) => Promise<void>;
+	onCancel: () => void;
+	room: IOmnichannelRoom;
+}): ReactElement => {
+	const t = useTranslation();
+	const getUserData = useEndpoint('GET', 'users.info');
+
+	const { getValues, handleSubmit, register, setFocus, setValue, watch } = useForm();
+
+	useEffect(() => {
+		setFocus('comment');
+	}, [setFocus]);
+
+	const department = watch('department');
+	const username = watch('username');
+
+	const [departmentsFilter, setDepartmentsFilter] = useState<string | number | undefined>('');
+	const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500);
+
+	const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList(
+		useMemo(() => ({ filter: debouncedDepartmentsFilter as string, enabled: true }), [debouncedDepartmentsFilter]),
+	);
+	const { phase: departmentsPhase, items: departments, itemCount: departmentsTotal } = useRecordList(departmentsList);
+	const hasDepartments = useMemo(() => departments && departments.length > 0, [departments]);
+
+	const _id = { $ne: room.servedBy?._id };
+	const conditions = { _id, status: { $ne: 'offline' }, statusLivechat: 'available' };
+
+	const endReached = useCallback(
+		(start) => {
+			if (departmentsPhase === AsyncStatePhase.LOADING) {
+				loadMoreDepartments(start, Math.min(50, departmentsTotal));
+			}
+		},
+		[departmentsPhase, departmentsTotal, loadMoreDepartments],
+	);
+
+	const onSubmit = useCallback(
+		async ({ department: departmentId, username, comment }) => {
+			let uid;
+
+			if (username) {
+				const { user } = await getUserData({ userName: username });
+				uid = user?._id;
+			}
+
+			onForward(departmentId, uid, comment);
+		},
+		[getUserData, onForward],
+	);
+
+	useEffect(() => {
+		register('department');
+		register('username');
+	}, [register]);
+
+	return (
+		<Modal {...props} is='form' onSubmit={handleSubmit(onSubmit)}>
+			<Modal.Header>
+				<Icon name='baloon-arrow-top-right' size={20} />
+				<Modal.Title>{t('Forward_chat')}</Modal.Title>
+				<Modal.Close onClick={onCancel} />
+			</Modal.Header>
+			<Modal.Content fontScale='p2'>
+				<Field mbe={'x30'}>
+					<Field.Label>{t('Forward_to_department')}</Field.Label>
+					<Field.Row>
+						{
+							// TODO: Definitions on fuselage are incorrect, need to be fixed!
+							// @ts-ignore-next-line
+							<PaginatedSelectFiltered
+								withTitle
+								filter={departmentsFilter as string}
+								setFilter={setDepartmentsFilter}
+								options={departments.map(({ _id, name }) => ({ value: _id, label: name }))}
+								maxWidth='100%'
+								placeholder={t('Select_an_option')}
+								onChange={(value: string): void => {
+									setValue('department', value);
+								}}
+								flexGrow={1}
+								endReached={endReached}
+							/>
+						}
+					</Field.Row>
+				</Field>
+				<ModalSeparator text={t('or')} />
+				<Field {...(hasDepartments && { mbs: 'x30' })}>
+					<Field.Label>{t('Forward_to_user')}</Field.Label>
+					<Field.Row>
+						<UserAutoComplete
+							conditions={conditions}
+							flexGrow={1}
+							placeholder={t('Username')}
+							onChange={(value: string): void => {
+								setValue('username', value);
+							}}
+							value={getValues().username}
+						/>
+					</Field.Row>
+				</Field>
+				<Field marginBlock='x15'>
+					<Field.Label>
+						{t('Leave_a_comment')}{' '}
+						<Box is='span' color='neutral-600'>
+							({t('Optional')})
+						</Box>
+					</Field.Label>
+					<Field.Row>
+						<TextAreaInput {...register('comment')} rows={8} flexGrow={1} />
+					</Field.Row>
+				</Field>
+			</Modal.Content>
+			<Modal.Footer>
+				<ButtonGroup align='end'>
+					<Button onClick={onCancel}>{t('Cancel')}</Button>
+					<Button type='submit' disabled={!username && !department} primary>
+						{t('Forward')}
+					</Button>
+				</ButtonGroup>
+			</Modal.Footer>
+		</Modal>
+	);
+};
+
+export default ForwardChatModal;
diff --git a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx
index 3f892c7aeeb..640c45749c1 100644
--- a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx
+++ b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx
@@ -71,7 +71,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => {
 
 	const { useCurrentChatTags = (): void => undefined } = forms;
 
-	const Tags = useCurrentChatTags();
+	const EETagsComponent = useCurrentChatTags();
 
 	const onSubmit = useMutableCallback((e) => e.preventDefault());
 	const reducer = function (acc: any, curr: string): any {
@@ -151,11 +151,11 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => {
 					<AutoCompleteDepartment haveAll value={department} onChange={handleDepartment} label={t('All')} onlyMyDepartments />
 				</Box>
 			</Box>
-			{Tags && (
+			{EETagsComponent && (
 				<Box display='flex' flexDirection='row' marginBlockStart='x8' {...props}>
 					<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'>
 						<Label mb='x4'>{t('Tags')}</Label>
-						<Tags value={tags} handler={handleTags} />
+						<EETagsComponent value={tags} handler={handleTags} />
 					</Box>
 				</Box>
 			)}
diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js
index a39afc72d41..42fac968ef3 100644
--- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js
+++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js
@@ -143,11 +143,9 @@ function RoomEdit({ room, visitor, reload, reloadInfo, close }) {
 						<TextInput flexGrow={1} value={topic} onChange={handleTopic} />
 					</Field.Row>
 				</Field>
-				{Tags && (
-					<Field>
-						<Tags tags={tags} handler={handleTags} />
-					</Field>
-				)}
+				<Field>
+					<Tags tags={tags} handler={handleTags} />
+				</Field>
 				{PrioritiesSelect && priorities && priorities.length > 0 && (
 					<PrioritiesSelect value={priorityId} label={t('Priority')} options={priorities} handler={handlePriorityId} />
 				)}
diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx
index 5001355f6d1..c89c24a23d0 100644
--- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx
+++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx
@@ -178,7 +178,7 @@ export const useQuickActions = (
 	const closeChat = useMethod('livechat:closeRoom');
 
 	const handleClose = useCallback(
-		async (comment: string, tags: string[]) => {
+		async (comment?: string, tags?: string[]) => {
 			try {
 				await closeChat(rid, comment, { clientAction: true, tags });
 				closeModal();
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index dfc300434ec..977c1a4fdfa 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -3334,6 +3334,7 @@
   "Opened": "Opened",
   "Opened_in_a_new_window": "Opened in a new window.",
   "Opens_a_channel_group_or_direct_message": "Opens a channel, group or direct message",
+  "Optional": "Optional",
   "optional": "optional",
   "Options": "Options",
   "or": "or",
diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts
index 1f5757eca9a..44bbfe86064 100644
--- a/packages/rest-typings/src/v1/omnichannel.ts
+++ b/packages/rest-typings/src/v1/omnichannel.ts
@@ -2,6 +2,7 @@ import type {
 	IOmnichannelCannedResponse,
 	ILivechatAgent,
 	ILivechatDepartment,
+	ILivechatDepartmentRecord,
 	ILivechatDepartmentAgents,
 	ILivechatMonitor,
 	ILivechatTag,
@@ -67,12 +68,12 @@ export type OmnichannelEndpoints = {
 	};
 	'livechat/department/:_id': {
 		GET: (params: { onlyMyDepartments?: booleanString; includeAgents?: booleanString }) => {
-			department: ILivechatDepartment | null;
-			agents?: any[];
+			department: ILivechatDepartmentRecord | null;
+			agents?: ILivechatDepartmentAgents[];
 		};
 		PUT: (params: { department: Partial<ILivechatDepartment>[]; agents: any[] }) => {
 			department: ILivechatDepartment;
-			agents: any[];
+			agents: ILivechatDepartmentAgents[];
 		};
 		DELETE: () => void;
 	};
-- 
GitLab