From 08a087e28eff4fb0a937e775f0475cd317fb31ef Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Thu, 20 Jan 2022 14:16:07 -0300 Subject: [PATCH] [IMPROVE] Rewrite Omnichannel Queue Page to React(#24176) Co-authored-by: Kevin Aleman --- app/livechat/client/route.js | 11 -- .../views/app/livechatAutocompleteUser.html | 23 ---- .../views/app/livechatAutocompleteUser.js | 118 ----------------- .../client/views/app/livechatQueue.html | 114 ---------------- .../client/views/app/livechatQueue.js | 125 ------------------ app/livechat/client/views/regular.js | 2 - client/contexts/OmnichannelContext.ts | 2 - client/sidebar/sections/Omnichannel.js | 22 ++- client/startup/routes.tsx | 10 ++ .../omnichannel/queueList/QueueListFilter.tsx | 66 +++++++++ .../omnichannel/queueList/QueueListPage.tsx | 48 +++++++ .../omnichannel/queueList/hooks/useQuery.ts | 54 ++++++++ client/views/omnichannel/queueList/index.tsx | 107 +++++++++++++++ definition/rest/v1/omnichannel.ts | 21 +++ 14 files changed, 322 insertions(+), 401 deletions(-) delete mode 100644 app/livechat/client/views/app/livechatAutocompleteUser.html delete mode 100644 app/livechat/client/views/app/livechatAutocompleteUser.js delete mode 100644 app/livechat/client/views/app/livechatQueue.html delete mode 100644 app/livechat/client/views/app/livechatQueue.js create mode 100644 client/views/omnichannel/queueList/QueueListFilter.tsx create mode 100644 client/views/omnichannel/queueList/QueueListPage.tsx create mode 100644 client/views/omnichannel/queueList/hooks/useQuery.ts create mode 100644 client/views/omnichannel/queueList/index.tsx diff --git a/app/livechat/client/route.js b/app/livechat/client/route.js index 9f82e38fe7f..c7370c0e60e 100644 --- a/app/livechat/client/route.js +++ b/app/livechat/client/route.js @@ -59,14 +59,3 @@ AccountBox.addRoute( livechatManagerRoutes, load, ); - -AccountBox.addRoute( - { - name: 'livechat-queue', - path: '/livechat-queue', - i18nPageTitle: 'Livechat_Queue', - pageTemplate: 'livechatQueue', - }, - null, - load, -); diff --git a/app/livechat/client/views/app/livechatAutocompleteUser.html b/app/livechat/client/views/app/livechatAutocompleteUser.html deleted file mode 100644 index 6b7fab27c15..00000000000 --- a/app/livechat/client/views/app/livechatAutocompleteUser.html +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/app/livechat/client/views/app/livechatAutocompleteUser.js b/app/livechat/client/views/app/livechatAutocompleteUser.js deleted file mode 100644 index 8329d37b75b..00000000000 --- a/app/livechat/client/views/app/livechatAutocompleteUser.js +++ /dev/null @@ -1,118 +0,0 @@ -import { Blaze } from 'meteor/blaze'; -import { Template } from 'meteor/templating'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { AutoComplete } from '../../../../meteor-autocomplete/client'; -import './livechatAutocompleteUser.html'; - -Template.livechatAutocompleteUser.helpers({ - list() { - return this.list; - }, - items() { - return Template.instance().ac.filteredList(); - }, - config() { - const { filter } = Template.instance(); - const { noMatchTemplate, templateItem, modifier } = Template.instance().data; - return { - filter: filter.get(), - template_item: templateItem, - noMatchTemplate, - modifier(text) { - return modifier(filter, text); - }, - }; - }, - label() { - const instance = Template.instance(); - return instance.showLabel && instance.label; - }, - autocomplete(key) { - const instance = Template.instance(); - const param = instance.ac[key]; - return typeof param === 'function' ? param.apply(instance.ac) : param; - }, -}); - -Template.livechatAutocompleteUser.events({ - 'input input'(e, t) { - const input = e.target; - const position = input.selectionEnd || input.selectionStart; - const { length } = input.value; - document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length); - t.filter.set(input.value); - }, - 'click .rc-popup-list__item'(e, t) { - t.ac.onItemClick(this, e); - }, - 'keydown input'(e, t) { - t.ac.onKeyDown(e); - if ([8, 46].includes(e.keyCode) && e.target.value === '') { - const { deleteLastItem } = t; - return deleteLastItem && deleteLastItem(); - } - }, - 'keyup input'(e, t) { - t.ac.onKeyUp(e); - }, - 'focus input'(e, t) { - t.ac.onFocus(e); - }, - 'blur input'(e, t) { - t.ac.onBlur(e); - }, - 'click .rc-tags__tag'({ target }, t) { - const { onClickTag } = t; - return onClickTag && onClickTag(Blaze.getData(target)); - }, -}); - -Template.livechatAutocompleteUser.onRendered(function () { - const { name } = this.data; - - this.ac.element = this.firstNode.querySelector(`[name=${name}]`); - this.ac.$element = $(this.ac.element); - this.deleteLastItem = this.data.deleteLastItem; -}); - -Template.livechatAutocompleteUser.onCreated(function () { - this.filter = new ReactiveVar(''); - this.selected = new ReactiveVar([]); - this.onClickTag = this.data.onClickTag; - this.showLabel = this.data.showLabel; - this.label = this.data.label; - - const filter = {}; - this.autorun(() => { - const { exceptions, conditions } = Template.currentData(); - filter.exceptions = exceptions; - filter.conditions = conditions; - }); - - const { collection, endpoint, field, sort, onSelect, selector = (match) => ({ term: match }) } = this.data; - this.ac = new AutoComplete({ - selector: { - anchor: '.rc-input__label', - item: '.rc-popup-list__item', - container: '.rc-popup-list__list', - }, - onSelect, - position: 'fixed', - limit: 10, - inputDelay: 300, - rules: [ - { - filter, - collection, - endpoint, - field, - matchAll: true, - doNotChangeWidth: false, - selector, - sort, - }, - ], - }); - this.ac.tmplInst = this; -}); diff --git a/app/livechat/client/views/app/livechatQueue.html b/app/livechat/client/views/app/livechatQueue.html deleted file mode 100644 index 2a3f926611e..00000000000 --- a/app/livechat/client/views/app/livechatQueue.html +++ /dev/null @@ -1,114 +0,0 @@ - diff --git a/app/livechat/client/views/app/livechatQueue.js b/app/livechat/client/views/app/livechatQueue.js deleted file mode 100644 index 3be18821907..00000000000 --- a/app/livechat/client/views/app/livechatQueue.js +++ /dev/null @@ -1,125 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; - -import { settings } from '../../../../settings'; -import { hasPermission } from '../../../../authorization'; -import { Users } from '../../../../models'; -import './livechatQueue.html'; -import { APIClient } from '../../../../utils/client'; - -const QUEUE_COUNT = 50; - -Template.livechatQueue.helpers({ - departments() { - return Template.instance() - .departments.get() - .filter((department) => department.enabled === true); - }, - onSelectAgents() { - return Template.instance().onSelectAgents; - }, - agentModifier() { - return (filter, text = '') => { - const f = filter.get(); - return `@${f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `${part}`)}`; - }; - }, - selectedAgents() { - return Template.instance().selectedAgents.get(); - }, - onClickTagAgent() { - return Template.instance().onClickTagAgent; - }, - queue() { - return Template.instance().queue.get(); - }, - isLoading() { - return Template.instance().isLoading.get(); - }, - hasPermission() { - const user = Users.findOne(Meteor.userId(), { fields: { statusLivechat: 1 } }); - return ( - hasPermission(Meteor.userId(), 'view-livechat-queue') || - (user.statusLivechat === 'available' && settings.get('Livechat_show_queue_list_link')) - ); - }, - hasMore() { - const instance = Template.instance(); - const queue = instance.queue.get(); - return instance.total.get() > queue.length; - }, -}); - -Template.livechatQueue.events({ - 'click .show-offline'(event, instance) { - const showOffline = instance.showOffline.get(); - - showOffline[this._id] = event.currentTarget.checked; - - instance.showOffline.set(showOffline); - }, - 'submit form'(event, instance) { - event.preventDefault(); - instance.queue.set([]); - instance.offset.set(0); - - const filter = {}; - $(':input', event.currentTarget).each(function () { - if (!this.name) { - return; - } - - const value = $(this).val(); - - filter[this.name] = value; - }); - const agents = instance.selectedAgents.get(); - if (agents && agents.length > 0) { - filter.agent = agents[0]._id; - } - instance.filter.set(filter); - }, - 'click .js-load-more'(event, instance) { - instance.offset.set(instance.offset.get() + QUEUE_COUNT); - }, -}); - -Template.livechatQueue.onCreated(async function () { - this.selectedAgents = new ReactiveVar([]); - this.departments = new ReactiveVar([]); - this.limit = new ReactiveVar(20); - this.filter = new ReactiveVar({}); - this.queue = new ReactiveVar([]); - this.isLoading = new ReactiveVar(true); - this.offset = new ReactiveVar(0); - this.total = new ReactiveVar(0); - - this.onSelectAgents = ({ item: agent }) => { - this.selectedAgents.set([agent]); - }; - - this.onClickTagAgent = ({ username }) => { - this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username)); - }; - - this.autorun(async () => { - this.isLoading.set(true); - const filter = this.filter.get(); - const offset = this.offset.get(); - let query = `includeOfflineAgents=${filter.agentStatus === 'offline'}`; - if (filter.agent) { - query += `&agentId=${filter.agent}`; - } - if (filter.department) { - query += `&departmentId=${filter.department}`; - } - const { queue, total } = await APIClient.v1.get(`livechat/queue?${query}&count=${QUEUE_COUNT}&offset=${offset}`); - this.total.set(total); - this.queue.set(this.queue.get().concat(queue)); - this.isLoading.set(false); - }); - - const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}'); - this.departments.set(departments); -}); diff --git a/app/livechat/client/views/regular.js b/app/livechat/client/views/regular.js index b32606792eb..17d05a4362a 100644 --- a/app/livechat/client/views/regular.js +++ b/app/livechat/client/views/regular.js @@ -1,5 +1,3 @@ -import './app/livechatAutocompleteUser'; -import './app/livechatQueue'; import './app/livechatReadOnly'; import './app/livechatNotSubscribed.html'; import './app/livechatRoomTagSelector'; diff --git a/client/contexts/OmnichannelContext.ts b/client/contexts/OmnichannelContext.ts index 18a985d75c5..1cd06f5c7b0 100644 --- a/client/contexts/OmnichannelContext.ts +++ b/client/contexts/OmnichannelContext.ts @@ -22,6 +22,4 @@ export const useOmnichannelShowQueueLink = (): boolean => useOmnichannel().showO export const useOmnichannelRouteConfig = (): OmichannelRoutingConfig | undefined => useOmnichannel().routeConfig; export const useOmnichannelAgentAvailable = (): boolean => useOmnichannel().agentAvailable; export const useQueuedInquiries = (): Inquiries => useOmnichannel().inquiries; -export const useOmnichannelQueueLink = (): string => '/livechat-queue'; -export const useOmnichannelDirectoryLink = (): string => '/omnichannel-directory'; export const useOmnichannelEnabled = (): boolean => useOmnichannel().enabled; diff --git a/client/sidebar/sections/Omnichannel.js b/client/sidebar/sections/Omnichannel.js index 09a0ac31edd..b81d8d1b811 100644 --- a/client/sidebar/sections/Omnichannel.js +++ b/client/sidebar/sections/Omnichannel.js @@ -4,7 +4,7 @@ import React, { memo } from 'react'; import { hasPermission } from '../../../app/authorization/client'; import { useLayout } from '../../contexts/LayoutContext'; -import { useOmnichannelShowQueueLink, useOmnichannelAgentAvailable, useOmnichannelQueueLink } from '../../contexts/OmnichannelContext'; +import { useOmnichannelShowQueueLink, useOmnichannelAgentAvailable } from '../../contexts/OmnichannelContext'; import { useRoute } from '../../contexts/RouterContext'; import { useMethod } from '../../contexts/ServerContext'; import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; @@ -15,9 +15,9 @@ const OmnichannelSection = (props) => { const t = useTranslation(); const agentAvailable = useOmnichannelAgentAvailable(); const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); - const queueLink = useOmnichannelQueueLink(); const { sidebar } = useLayout(); const directoryRoute = useRoute('omnichannel-directory'); + const queueListRoute = useRoute('livechat-queue'); const dispatchToastMessage = useToastMessageDispatch(); const icon = { @@ -40,18 +40,28 @@ const OmnichannelSection = (props) => { } }); - const handleDirectory = useMutableCallback(() => { + const handleRoute = useMutableCallback((route) => { sidebar.toggle(); - directoryRoute.push({}); + + switch (route) { + case 'directory': + directoryRoute.push({}); + break; + case 'queue': + queueListRoute.push({}); + break; + } }); return ( {t('Omnichannel')} - {showOmnichannelQueueLink && } + {showOmnichannelQueueLink && handleRoute('queue')} />} - {hasPermission(['view-omnichannel-contact-center']) && } + {hasPermission(['view-omnichannel-contact-center']) && ( + handleRoute('directory')} /> + )} ); diff --git a/client/startup/routes.tsx b/client/startup/routes.tsx index c8c565a4dbe..c51bd89f3ee 100644 --- a/client/startup/routes.tsx +++ b/client/startup/routes.tsx @@ -136,6 +136,16 @@ FlowRouter.route('/omnichannel-directory/:page?/:bar?/:id?/:tab?/:context?', { }, }); +FlowRouter.route('/livechat-queue', { + name: 'livechat-queue', + action: () => { + const OmnichannelQueueList = createTemplateForComponent('QueueList', () => import('../views/omnichannel/queueList'), { + attachment: 'at-parent', + }); + appLayout.renderMainLayout({ center: OmnichannelQueueList }); + }, +}); + FlowRouter.route('/account/:group?', { name: 'account', action: () => { diff --git a/client/views/omnichannel/queueList/QueueListFilter.tsx b/client/views/omnichannel/queueList/QueueListFilter.tsx new file mode 100644 index 00000000000..2d8b3fbcdc2 --- /dev/null +++ b/client/views/omnichannel/queueList/QueueListFilter.tsx @@ -0,0 +1,66 @@ +import { Box, Select, Label } from '@rocket.chat/fuselage'; +import { useMutableCallback, useLocalStorage } from '@rocket.chat/fuselage-hooks'; +import React, { Dispatch, FC, SetStateAction, useEffect } from 'react'; + +import AutoCompleteAgent from '../../../components/AutoCompleteAgent'; +import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment'; +import { useTranslation } from '../../../contexts/TranslationContext'; + +type QueueListFilterPropsType = FC<{ + setFilter: Dispatch>; +}>; + +export const QueueListFilter: QueueListFilterPropsType = ({ setFilter, ...props }) => { + const t = useTranslation(); + + const statusOptions: [string, string][] = [ + ['online', t('Online')], + ['offline', t('Include_Offline_Agents')], + ]; + + const [servedBy, setServedBy] = useLocalStorage('servedBy', 'all'); + const [status, setStatus] = useLocalStorage('status', 'online'); + const [department, setDepartment] = useLocalStorage<{ label: string; value: string }>('department', { value: 'all', label: t('All') }); + + const handleServedBy = useMutableCallback((e) => setServedBy(e)); + const handleStatus = useMutableCallback((e) => setStatus(e)); + const handleDepartment = useMutableCallback((e) => setDepartment(e)); + + const onSubmit = useMutableCallback((e) => e.preventDefault()); + + useEffect(() => { + const filters = { status } as { + status: string; + servedBy?: string; + departmentId?: string; + }; + + if (servedBy !== 'all') { + filters.servedBy = servedBy; + } + if (department?.value && department.value !== 'all') { + filters.departmentId = department.value; + } + + setFilter(filters); + }, [setFilter, servedBy, status, department]); + + return ( + + + + + + + + +