From 379048074a6afabed15f72cd595be183ba02b28d Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 8 Oct 2020 12:30:29 -0300 Subject: [PATCH] Refactor: Omnichannel departments (#18920) Co-authored-by: Guilherme Gazzo Co-authored-by: Gabriel Henriques Co-authored-by: Guilherme Gazzo --- app/livechat/client/views/admin.js | 2 - .../views/app/livechatDepartmentForm.html | 223 ----------- .../views/app/livechatDepartmentForm.js | 367 ------------------ .../client/views/app/livechatDepartments.html | 59 --- .../client/views/app/livechatDepartments.js | 111 ------ client/channel/UserCard/index.js | 17 +- client/components/GenericTable.js | 2 +- client/components/basic/AutoCompleteAgent.js | 5 +- client/hooks/useComponentDidUpdate.js | 15 + .../omnichannel/departments/DepartmentEdit.js | 310 +++++++++++++++ .../departments/DepartmentsAgentsTable.js | 147 +++++++ .../departments/DepartmentsPage.js | 55 +++ .../departments/DepartmentsRoute.js | 114 ++++++ client/omnichannel/departments/Skeleton.js | 11 + client/omnichannel/routes.js | 5 + .../livechatDepartmentCustomFieldsForm.html | 65 ---- .../livechatDepartmentCustomFieldsForm.js | 72 ---- .../views/app/registerCustomTemplates.js | 1 - .../DepartmentBusinessHours.js | 22 ++ .../additionalForms/DepartmentForwarding.js | 23 ++ .../additionalForms/EeNumberInput.js | 18 + .../additionalForms/EeTextAreaInput.js | 18 + .../additionalForms/EeTextInput.js | 18 + .../omnichannel/additionalForms/register.js | 5 + packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 25 files changed, 768 insertions(+), 918 deletions(-) delete mode 100644 app/livechat/client/views/app/livechatDepartmentForm.html delete mode 100644 app/livechat/client/views/app/livechatDepartmentForm.js delete mode 100644 app/livechat/client/views/app/livechatDepartments.html delete mode 100644 app/livechat/client/views/app/livechatDepartments.js create mode 100644 client/hooks/useComponentDidUpdate.js create mode 100644 client/omnichannel/departments/DepartmentEdit.js create mode 100644 client/omnichannel/departments/DepartmentsAgentsTable.js create mode 100644 client/omnichannel/departments/DepartmentsPage.js create mode 100644 client/omnichannel/departments/DepartmentsRoute.js create mode 100644 client/omnichannel/departments/Skeleton.js delete mode 100644 ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html delete mode 100644 ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js create mode 100644 ee/client/omnichannel/additionalForms/DepartmentBusinessHours.js create mode 100644 ee/client/omnichannel/additionalForms/DepartmentForwarding.js create mode 100644 ee/client/omnichannel/additionalForms/EeNumberInput.js create mode 100644 ee/client/omnichannel/additionalForms/EeTextAreaInput.js create mode 100644 ee/client/omnichannel/additionalForms/EeTextInput.js diff --git a/app/livechat/client/views/admin.js b/app/livechat/client/views/admin.js index 78a7e39c94d..71d284db226 100644 --- a/app/livechat/client/views/admin.js +++ b/app/livechat/client/views/admin.js @@ -1,3 +1 @@ import './app/livechatDashboard.html'; -import './app/livechatDepartmentForm'; -import './app/livechatDepartments'; diff --git a/app/livechat/client/views/app/livechatDepartmentForm.html b/app/livechat/client/views/app/livechatDepartmentForm.html deleted file mode 100644 index d846f4e129c..00000000000 --- a/app/livechat/client/views/app/livechatDepartmentForm.html +++ /dev/null @@ -1,223 +0,0 @@ - diff --git a/app/livechat/client/views/app/livechatDepartmentForm.js b/app/livechat/client/views/app/livechatDepartmentForm.js deleted file mode 100644 index cb4c8f5e0cb..00000000000 --- a/app/livechat/client/views/app/livechatDepartmentForm.js +++ /dev/null @@ -1,367 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Template } from 'meteor/templating'; -import _ from 'underscore'; -import toastr from 'toastr'; - -import { TabBar, RocketChatTabBar } from '../../../../ui-utils'; -import { t, handleError } from '../../../../utils'; -import { hasPermission } from '../../../../authorization'; -import { getCustomFormTemplate } from './customTemplates/register'; -import './livechatDepartmentForm.html'; -import { APIClient, roomTypes } from '../../../../utils/client'; - -const LIST_SIZE = 50; - -const saveDepartmentsAgents = async (_id, instance) => { - const upsert = [...instance.agentsToUpsert.values()]; - const remove = [...instance.agentsToRemove.values()]; - if (!upsert.length && !remove.length) { - return; - } - return APIClient.v1.post(`livechat/department/${ _id }/agents`, { - upsert, - remove, - }); -}; - -Template.livechatDepartmentForm.helpers({ - department() { - return Template.instance().department.get(); - }, - agents() { - return Template.instance().department && !_.isEmpty(Template.instance().department.get()) ? Template.instance().department.get().agents : []; - }, - departmentAgents() { - return _.sortBy(Template.instance().departmentAgents.get(), 'username'); - }, - showOnRegistration(value) { - const department = Template.instance().department.get(); - return department.showOnRegistration === value || (department.showOnRegistration === undefined && value === true); - }, - showOnOfflineForm(value) { - const department = Template.instance().department.get(); - return department.showOnOfflineForm === value || (department.showOnOfflineForm === undefined && value === true); - }, - requestTagBeforeClosingChat() { - const department = Template.instance().department.get(); - return !!(department && department.requestTagBeforeClosingChat); - }, - customFieldsTemplate() { - return getCustomFormTemplate('livechatDepartmentForm'); - }, - data() { - return { id: FlowRouter.getParam('_id') }; - }, - exceptionsAgents() { - return _.pluck(Template.instance().departmentAgents.get(), 'username'); - }, - agentModifier() { - return (filter, text = '') => { - const f = filter.get(); - return `@${ - f.length === 0 - ? text - : text.replace( - new RegExp(filter.get()), - (part) => `${ part }`, - ) - }`; - }; - }, - agentConditions() { - return { roles: 'livechat-agent' }; - }, - onSelectAgents() { - return Template.instance().onSelectAgents; - }, - selectedAgents() { - return Template.instance().selectedAgents.get(); - }, - onClickTagAgents() { - return Template.instance().onClickTagAgents; - }, - flexData() { - return { - tabBar: Template.instance().tabBar, - data: Template.instance().tabBarData.get(), - }; - }, - tabBarVisible() { - return Object.values(TabBar.buttons.get()) - .some((button) => button.groups - .some((group) => group.startsWith('livechat-department'))); - }, - chatClosingTags() { - return Template.instance().chatClosingTags.get(); - }, - availableDepartmentTags() { - return Template.instance().availableDepartmentTags.get(); - }, - hasAvailableTags() { - return [...Template.instance().availableTags.get()].length > 0; - }, - hasChatClosingTags() { - return [...Template.instance().chatClosingTags.get()].length > 0; - }, - onTableScroll() { - const instance = Template.instance(); - return function(currentTarget) { - if (currentTarget.offsetHeight + currentTarget.scrollTop < currentTarget.scrollHeight - 100) { - return; - } - const agents = instance.departmentAgents.get(); - if (instance.total.get() > agents.length) { - instance.offset.set(instance.offset.get() + LIST_SIZE); - } - }; - }, - onClickTagOfflineMessageChannel() { - return Template.instance().onClickTagOfflineMessageChannel; - }, - selectedOfflineMessageChannel() { - return Template.instance().offlineMessageChannel.get(); - }, - onSelectOfflineMessageChannel() { - return Template.instance().onSelectOfflineMessageChannel; - }, - offlineMessageChannelModifier() { - return (filter, text = '') => { - const f = filter.get(); - return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `${ part }`) }`; - }; - }, - channelSelector() { - return (expression) => ({ name: expression }); - }, -}); - -Template.livechatDepartmentForm.events({ - 'submit #department-form'(e, instance) { - e.preventDefault(); - const $btn = instance.$('button.save'); - - let departmentData; - - const _id = $(e.currentTarget).data('id'); - - if (hasPermission('manage-livechat-departments')) { - const enabled = instance.$('input[name=enabled]:checked').val(); - const name = instance.$('input[name=name]').val(); - const description = instance.$('textarea[name=description]').val(); - const showOnRegistration = instance.$('input[name=showOnRegistration]:checked').val(); - const email = instance.$('input[name=email]').val(); - const showOnOfflineForm = instance.$('input[name=showOnOfflineForm]:checked').val(); - const requestTagBeforeClosingChat = instance.$('input[name=requestTagBeforeClosingChat]:checked').val(); - const chatClosingTags = instance.chatClosingTags.get(); - const [offlineMessageChannel] = instance.offlineMessageChannel.get(); - const offlineMessageChannelName = (offlineMessageChannel && roomTypes.getRoomName(offlineMessageChannel.t, offlineMessageChannel)) || ''; - - if (enabled !== '1' && enabled !== '0') { - return toastr.error(t('Please_select_enabled_yes_or_no')); - } - - if (name.trim() === '') { - return toastr.error(t('Please_fill_a_name')); - } - - if (email.trim() === '' && showOnOfflineForm === '1') { - return toastr.error(t('Please_fill_an_email')); - } - - departmentData = { - enabled: enabled === '1', - name: name.trim(), - description: description.trim(), - showOnRegistration: showOnRegistration === '1', - showOnOfflineForm: showOnOfflineForm === '1', - requestTagBeforeClosingChat: requestTagBeforeClosingChat === '1', - email: email.trim(), - chatClosingTags, - offlineMessageChannelName, - }; - } - - const oldBtnValue = $btn.html(); - $btn.html(t('Saving')); - - instance.$('.customFormField').each((i, el) => { - const elField = instance.$(el); - const name = elField.attr('name'); - departmentData[name] = elField.val(); - }); - - if (hasPermission('manage-livechat-departments')) { - Meteor.call('livechat:saveDepartment', _id, departmentData, [], async function(err, result) { - $btn.html(oldBtnValue); - if (err) { - return handleError(err); - } - - await saveDepartmentsAgents(result._id, instance); - toastr.success(t('Saved')); - FlowRouter.go('livechat-departments'); - }); - } else if (hasPermission('add-livechat-department-agents')) { - saveDepartmentsAgents(_id, instance); - } else { - throw new Error(t('error-not-authorized')); - } - }, - - 'click .add-agent'(e, instance) { - e.preventDefault(); - - const users = instance.selectedAgents.get(); - users.forEach(async (user) => { - const { _id, username } = user; - - const departmentAgents = instance.departmentAgents.get(); - if (departmentAgents.find(({ agentId }) => agentId === _id)) { - return toastr.error(t('This_agent_was_already_selected')); - } - const newAgent = _.clone(user); - newAgent.agentId = _id; - delete newAgent._id; - if (instance.agentsToRemove.has(newAgent.agentId)) { - instance.agentsToRemove.delete(newAgent.agentId); - } - instance.agentsToUpsert.set(newAgent.agentId, { ...newAgent, count: 0, order: 0 }); - departmentAgents.push(newAgent); - instance.departmentAgents.set(departmentAgents); - instance.selectedAgents.set(instance.selectedAgents.get().filter((user) => user.username !== username)); - }); - }, - - 'click button.back'(e/* , instance*/) { - e.preventDefault(); - FlowRouter.go('livechat-departments'); - }, - - 'click .remove-agent'(e, instance) { - e.preventDefault(); - if (instance.agentsToUpsert.has(this.agentId)) { - instance.agentsToUpsert.delete(this.agentId); - } - instance.agentsToRemove.set(this.agentId, this); - - instance.departmentAgents.set(instance.departmentAgents.get().filter((agent) => agent.agentId !== this.agentId)); - }, - - 'keyup .count'(event, instance) { - const agent = instance.agentsToUpsert.get(this.agentId) || this; - instance.agentsToUpsert.set(this.agentId, { ...agent, count: parseInt(event.currentTarget.value) || 0 }); - }, - - 'keyup .order'(event, instance) { - const agent = instance.agentsToUpsert.get(this.agentId) || this; - instance.agentsToUpsert.set(this.agentId, { ...agent, order: parseInt(event.currentTarget.value) || 0 }); - }, - - 'click #addTag'(e, instance) { - e.stopPropagation(); - e.preventDefault(); - - const isSelect = [...instance.availableTags.get()].length > 0; - const elId = isSelect ? '#tagSelect' : '#tagInput'; - const elDefault = isSelect ? 'placeholder' : ''; - - const tag = $(elId).val(); - const chatClosingTags = [...instance.chatClosingTags.get()]; - if (tag === '' || chatClosingTags.indexOf(tag) > -1) { - return; - } - - chatClosingTags.push(tag); - instance.chatClosingTags.set(chatClosingTags); - $(elId).val(elDefault); - }, - - 'click .remove-tag'(e, instance) { - e.stopPropagation(); - e.preventDefault(); - - const chatClosingTags = [...instance.chatClosingTags.get()].filter((el) => el !== this.valueOf()); - instance.chatClosingTags.set(chatClosingTags); - }, -}); - -Template.livechatDepartmentForm.onCreated(async function() { - this.agentsToUpsert = new Map(); - this.agentsToRemove = new Map(); - this.department = new ReactiveVar({ enabled: true }); - this.departmentAgents = new ReactiveVar([]); - this.selectedAgents = new ReactiveVar([]); - this.tabBar = new RocketChatTabBar(); - this.tabBar.showGroup(FlowRouter.current().route.name); - this.tabBarData = new ReactiveVar(); - this.chatClosingTags = new ReactiveVar([]); - this.availableTags = new ReactiveVar([]); - this.availableDepartmentTags = new ReactiveVar([]); - this.offset = new ReactiveVar(0); - this.total = new ReactiveVar(0); - this.offlineMessageChannel = new ReactiveVar([]); - - - this.onClickTagOfflineMessageChannel = () => { - this.offlineMessageChannel.set([]); - }; - - this.onSelectOfflineMessageChannel = async ({ item }) => { - const { room } = await APIClient.v1.get(`rooms.info?roomId=${ item._id }`); - room.text = room.name; - this.offlineMessageChannel.set([room]); - }; - this.onSelectAgents = ({ item: agent }) => { - this.selectedAgents.set([agent]); - }; - - this.onClickTagAgents = ({ username }) => { - this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username)); - }; - - this.loadAvailableTags = (departmentId) => { - Meteor.call('livechat:getTagsList', (err, tagsList) => { - this.availableTags.set(tagsList || []); - const tags = this.availableTags.get(); - const availableTags = tags - .filter(({ departments }) => departments.length === 0 || departments.indexOf(departmentId) > -1) - .map(({ name }) => name); - this.availableDepartmentTags.set(availableTags); - }); - }; - this.autorun(async () => { - const offset = this.offset.get(); - const { agents, total } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }/agents?count=${ LIST_SIZE }&offset=${ offset }`); - this.total.set(total); - if (offset === 0) { - this.departmentAgents.set(agents); - } else { - this.departmentAgents.set(this.departmentAgents.get().concat(agents)); - } - }); - - this.autorun(async () => { - const id = FlowRouter.getParam('_id'); - if (id) { - const { department } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }?includeAgents=false`); - this.department.set(department); - this.chatClosingTags.set((department && department.chatClosingTags) || []); - this.loadAvailableTags(id); - } - }); - - this.autorun(async () => { - const department = this.department.get(); - let offlineChannel = []; - if (department?.offlineMessageChannelName) { - const { room } = await APIClient.v1.get(`rooms.info?roomName=${ department?.offlineMessageChannelName }`); - if (room) { - room.text = room.name; - offlineChannel = [{ ...room }]; - } - } - this.offlineMessageChannel.set(offlineChannel); - }); -}); diff --git a/app/livechat/client/views/app/livechatDepartments.html b/app/livechat/client/views/app/livechatDepartments.html deleted file mode 100644 index cb3963f6076..00000000000 --- a/app/livechat/client/views/app/livechatDepartments.html +++ /dev/null @@ -1,59 +0,0 @@ - diff --git a/app/livechat/client/views/app/livechatDepartments.js b/app/livechat/client/views/app/livechatDepartments.js deleted file mode 100644 index 97b50f75160..00000000000 --- a/app/livechat/client/views/app/livechatDepartments.js +++ /dev/null @@ -1,111 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Template } from 'meteor/templating'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { ReactiveDict } from 'meteor/reactive-dict'; -import _ from 'underscore'; - -import { modal } from '../../../../ui-utils'; -import { t, handleError } from '../../../../utils'; -import './livechatDepartments.html'; -import { APIClient } from '../../../../utils/client'; - -Template.livechatDepartments.helpers({ - departments() { - return Template.instance().departments.get(); - }, - isLoading() { - return Template.instance().state.get('loading'); - }, - isReady() { - const instance = Template.instance(); - return instance.ready && instance.ready.get(); - }, - onTableScroll() { - const instance = Template.instance(); - return function(currentTarget) { - if ( - currentTarget.offsetHeight + currentTarget.scrollTop - >= currentTarget.scrollHeight - 100 - ) { - return instance.limit.set(instance.limit.get() + 50); - } - }; - }, -}); - -const DEBOUNCE_TIME_FOR_SEARCH_DEPARTMENTS_IN_MS = 300; - -Template.livechatDepartments.events({ - 'click .remove-department'(e, instance) { - e.preventDefault(); - e.stopPropagation(); - - modal.open({ - title: t('Are_you_sure'), - type: 'warning', - showCancelButton: true, - confirmButtonColor: '#DD6B55', - confirmButtonText: t('Yes'), - cancelButtonText: t('Cancel'), - closeOnConfirm: false, - html: false, - }, () => { - Meteor.call('livechat:removeDepartment', this._id, (error/* , result*/) => { - if (error) { - return handleError(error); - } - instance.departments.set(instance.departments.curValue.filter((department) => department._id !== this._id)); - modal.open({ - title: t('Removed'), - text: t('Department_removed'), - type: 'success', - timer: 1000, - showConfirmButton: false, - }); - }); - }); - }, - - 'click .department-info'(e/* , instance*/) { - e.preventDefault(); - FlowRouter.go('livechat-department-edit', { _id: this._id }); - }, - - 'keydown #departments-filter'(e) { - if (e.which === 13) { - e.stopPropagation(); - e.preventDefault(); - } - }, - 'keyup #departments-filter': _.debounce((e, t) => { - e.stopPropagation(); - e.preventDefault(); - t.filter.set(e.currentTarget.value); - }, DEBOUNCE_TIME_FOR_SEARCH_DEPARTMENTS_IN_MS), -}); - -Template.livechatDepartments.onCreated(function() { - const instance = this; - this.limit = new ReactiveVar(50); - this.filter = new ReactiveVar(''); - this.state = new ReactiveDict({ - loading: false, - }); - this.ready = new ReactiveVar(true); - this.departments = new ReactiveVar([]); - - this.autorun(async function() { - const limit = instance.limit.get(); - const filter = instance.filter.get(); - let baseUrl = `livechat/department?count=${ limit }`; - - if (filter) { - baseUrl += `&text=${ encodeURIComponent(filter) }`; - } - - const { departments } = await APIClient.v1.get(baseUrl); - instance.departments.set(departments); - instance.ready.set(true); - }); -}); diff --git a/client/channel/UserCard/index.js b/client/channel/UserCard/index.js index dca8b8bfb65..aa496ef6acb 100644 --- a/client/channel/UserCard/index.js +++ b/client/channel/UserCard/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { PositionAnimated, AnimatedVisibility, Menu, Option } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -10,22 +10,9 @@ import { Backdrop } from '../../components/basic/Backdrop'; import * as UserStatus from '../../components/basic/UserStatus'; import { LocalTime } from '../../components/basic/UTCClock'; import { useUserInfoActions, useUserInfoActionsSpread } from '../hooks/useUserInfoActions'; +import { useComponentDidUpdate } from '../../hooks/useComponentDidUpdate'; import { useCurrentRoute } from '../../contexts/RouterContext'; -export const useComponentDidUpdate = ( - effect, - dependencies = [], -) => { - const hasMounted = useRef(false); - useEffect(() => { - if (!hasMounted.current) { - hasMounted.current = true; - return; - } - effect(); - }, dependencies); -}; - const UserCardWithData = ({ username, onClose, target, open, rid }) => { const ref = useRef(target); diff --git a/client/components/GenericTable.js b/client/components/GenericTable.js index 6e2fa6844f6..efd134ed07b 100644 --- a/client/components/GenericTable.js +++ b/client/components/GenericTable.js @@ -90,7 +90,7 @@ export const GenericTable = forwardRef(function GenericTable({ {RenderRow && ( results ? results.map((props, index) => ) - : + : )} {children && (results ? results.map(children) : )} diff --git a/client/components/basic/AutoCompleteAgent.js b/client/components/basic/AutoCompleteAgent.js index 5e42fdd0df5..c070dda62b4 100644 --- a/client/components/basic/AutoCompleteAgent.js +++ b/client/components/basic/AutoCompleteAgent.js @@ -9,7 +9,8 @@ export const AutoCompleteAgent = React.memo((props) => { const [filter, setFilter] = useState(''); const { data } = useEndpointDataExperimental('livechat/users/agent', useMemo(() => ({ text: filter }), [filter])); - const options = useMemo(() => (data && [{ value: 'all', label: t('All') }, ...data.users.map((user) => ({ value: user._id, label: user.name }))]) || [{ value: 'all', label: t('All') }], [data, t]); + const options = useMemo(() => (data && [...data.users.map((user) => ({ value: user._id, label: user.name }))]) || [], [data]); + const optionsWithAll = useMemo(() => (data && [{ value: 'all', label: t('All') }, ...data.users.map((user) => ({ value: user._id, label: user.name }))]) || [{ value: 'all', label: t('All') }], [data, t]); return { setFilter={setFilter} renderSelected={({ label }) => <>{label}} renderItem={({ value, ...props }) =>