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