From 985b0254e58c186a044f482fa9212c63b08e435f Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 13 Jan 2023 15:42:28 -0300 Subject: [PATCH 01/43] Move save department functionality to EE --- .../imports/server/rest/departments.ts | 5 +- .../app/livechat/server/lib/Livechat.js | 68 --------------- .../livechat/server/methods/saveDepartment.js | 6 +- .../server/lib/LivechatEnterprise.js | 87 +++++++++++++++++-- .../server/models/raw/LivechatDepartment.ts | 4 + .../src/models/ILivechatDepartmentModel.ts | 1 + 6 files changed, 92 insertions(+), 79 deletions(-) diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 3a76c7ce06fa..5e9913b72d81 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -12,6 +12,7 @@ import { findDepartmentsBetweenIds, findDepartmentAgents, } from '../../../server/api/lib/departments'; +import { LivechatEnterprise } from '../../../../../ee/app/livechat-enterprise/server/lib/LivechatEnterprise'; API.v1.addRoute( 'livechat/department', @@ -54,7 +55,7 @@ API.v1.addRoute( }); const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; - const department = Livechat.saveDepartment(null, this.bodyParams.department, agents); + const department = await LivechatEnterprise.saveDepartment(null, this.bodyParams.department, agents); if (department) { return API.v1.success({ @@ -117,7 +118,7 @@ API.v1.addRoute( let success; if (permissionToSave) { - success = Livechat.saveDepartment(_id, department); + success = await LivechatEnterprise.saveDepartment(_id, department); } if (success && agents && permissionToAddAgents) { diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 47ae2de29551..ea1825c43ae8 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1043,74 +1043,6 @@ export const Livechat = { return updateDepartmentAgents(_id, departmentAgents, department.enabled); }, - saveDepartment(_id, departmentData, departmentAgents) { - check(_id, Match.Maybe(String)); - - const defaultValidations = { - enabled: Boolean, - name: String, - description: Match.Optional(String), - showOnRegistration: Boolean, - email: String, - showOnOfflineForm: Boolean, - requestTagBeforeClosingChat: Match.Optional(Boolean), - chatClosingTags: Match.Optional([String]), - fallbackForwardDepartment: Match.Optional(String), - }; - - // The Livechat Form department support addition/custom fields, so those fields need to be added before validating - Object.keys(departmentData).forEach((field) => { - if (!defaultValidations.hasOwnProperty(field)) { - defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); - } - }); - - check(departmentData, defaultValidations); - check( - departmentAgents, - Match.Maybe({ - upsert: Match.Maybe(Array), - remove: Match.Maybe(Array), - }), - ); - - const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; - if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { - throw new Meteor.Error( - 'error-validating-department-chat-closing-tags', - 'At least one closing tag is required when the department requires tag(s) on closing conversations.', - { method: 'livechat:saveDepartment' }, - ); - } - - if (_id) { - const department = LivechatDepartment.findOneById(_id); - if (!department) { - throw new Meteor.Error('error-department-not-found', 'Department not found', { - method: 'livechat:saveDepartment', - }); - } - } - - if (fallbackForwardDepartment === _id) { - throw new Meteor.Error( - 'error-fallback-department-circular', - 'Cannot save department. Circular reference between fallback department and department', - ); - } - - if (fallbackForwardDepartment && !LivechatDepartment.findOneById(fallbackForwardDepartment)) { - throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { method: 'livechat:saveDepartment' }); - } - - const departmentDB = LivechatDepartment.createOrUpdateDepartment(_id, departmentData); - if (departmentDB && departmentAgents) { - updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); - } - - return departmentDB; - }, - saveAgentInfo(_id, agentData, agentDepartments) { check(_id, Match.Maybe(String)); check(agentData, Object); diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.js b/apps/meteor/app/livechat/server/methods/saveDepartment.js index 581f2a7c0651..6b2cb4d0449f 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.js +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.js @@ -1,16 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { Livechat } from '../lib/Livechat'; +import { LivechatEnterprise } from '../../../../ee/app/livechat-enterprise/server/lib/LivechatEnterprise'; Meteor.methods({ - 'livechat:saveDepartment'(_id, departmentData, departmentAgents) { + async 'livechat:saveDepartment'(_id, departmentData, departmentAgents) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-departments')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveDepartment', }); } - return Livechat.saveDepartment(_id, departmentData, { upsert: departmentAgents }); + return LivechatEnterprise.saveDepartment(_id, departmentData, { upsert: departmentAgents }); }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 9107d8d69d77..e2256377c88d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { LivechatInquiry, Users, LivechatRooms } from '@rocket.chat/models'; +import { LivechatInquiry, Users, LivechatRooms, LivechatDepartment as LivechatDepartmentRaw } from '@rocket.chat/models'; -import LivechatUnit from '../../../models/server/models/LivechatUnit'; -import LivechatTag from '../../../models/server/models/LivechatTag'; -import { Messages } from '../../../../../app/models/server'; -import LivechatPriority from '../../../models/server/models/LivechatPriority'; +import { hasLicense } from '../../../license/server/license'; +import { updateDepartmentAgents } from '../../../../../app/livechat/server/lib/Helper'; +import { Messages, LivechatDepartment } from '../../../../../app/models/server'; import { addUserRoles } from '../../../../../server/lib/roles/addUserRoles'; import { removeUserFromRoles } from '../../../../../server/lib/roles/removeUserFromRoles'; import { @@ -20,7 +19,7 @@ import { settings } from '../../../../../app/settings/server'; import { logger, queueLogger } from './logger'; import { callbacks } from '../../../../../lib/callbacks'; import { AutoCloseOnHoldScheduler } from './AutoCloseOnHoldScheduler'; -import { LivechatUnitMonitors } from '../../../models/server'; +import { LivechatPriority, LivechatTag, LivechatUnit, LivechatUnitMonitors } from '../../../models/server'; export const LivechatEnterprise = { async addMonitor(username) { @@ -215,6 +214,82 @@ export const LivechatEnterprise = { await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); await LivechatRooms.unsetOnHoldAndPredictedVisitorAbandonmentByRoomId(roomId); }, + + async saveDepartment(_id, departmentData, departmentAgents) { + check(_id, Match.Maybe(String)); + + const department = _id && (await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } })); + + if (!hasLicense('livechat-enterprise')) { + const totalDepartments = await LivechatDepartmentRaw.countTotal(); + if (!department && totalDepartments >= 1) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'livechat:saveDepartment', + }); + } + } + + const defaultValidations = { + enabled: Boolean, + name: String, + description: Match.Optional(String), + showOnRegistration: Boolean, + email: String, + showOnOfflineForm: Boolean, + requestTagBeforeClosingChat: Match.Optional(Boolean), + chatClosingTags: Match.Optional([String]), + fallbackForwardDepartment: Match.Optional(String), + }; + + // The Livechat Form department support addition/custom fields, so those fields need to be added before validating + Object.keys(departmentData).forEach((field) => { + if (!defaultValidations.hasOwnProperty(field)) { + defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); + } + }); + + check(departmentData, defaultValidations); + check( + departmentAgents, + Match.Maybe({ + upsert: Match.Maybe(Array), + remove: Match.Maybe(Array), + }), + ); + + const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; + if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { + throw new Meteor.Error( + 'error-validating-department-chat-closing-tags', + 'At least one closing tag is required when the department requires tag(s) on closing conversations.', + { method: 'livechat:saveDepartment' }, + ); + } + + if (_id && !department) { + throw new Meteor.Error('error-department-not-found', 'Department not found', { + method: 'livechat:saveDepartment', + }); + } + + if (fallbackForwardDepartment === _id) { + throw new Meteor.Error( + 'error-fallback-department-circular', + 'Cannot save department. Circular reference between fallback department and department', + ); + } + + if (fallbackForwardDepartment && !LivechatDepartment.findOneById(fallbackForwardDepartment)) { + throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { method: 'livechat:saveDepartment' }); + } + + const departmentDB = LivechatDepartment.createOrUpdateDepartment(_id, departmentData); + if (departmentDB && departmentAgents) { + updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); + } + + return departmentDB; + }, }; const DEFAULT_RACE_TIMEOUT = 5000; diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts index ee03b2ab31a8..8bb11fdfb28b 100644 --- a/apps/meteor/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/server/models/raw/LivechatDepartment.ts @@ -10,6 +10,10 @@ export class LivechatDepartmentRaw extends BaseRaw im super(db, 'livechat_department', trash); } + countTotal(): Promise { + return this.col.countDocuments(); + } + findInIds(departmentsIds: string[], options: FindOptions): FindCursor { const query = { _id: { $in: departmentsIds } }; return this.find(query, options); diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index b75f93007982..3ae3911f691c 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -4,6 +4,7 @@ import type { ILivechatDepartmentRecord } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; export interface ILivechatDepartmentModel extends IBaseModel { + countTotal(): Promise; findInIds(departmentsIds: string[], options: FindOptions): FindCursor; findByNameRegexWithExceptionsAndConditions( searchTerm: string, From 4e24aba64da06b832a85a1d82d21440dce572815 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 13 Jan 2023 19:50:15 -0300 Subject: [PATCH 02/43] [FE] Move Departments to EE --- .../hooks/useMultipleDepartmentsAvailable.ts | 12 ++++ .../hooks/useOmnichannelDepartments.ts | 13 +++++ .../modals/EnterpriseDepartmentsModal.tsx | 56 +++++++++++++++++++ .../departments/DepartmentsRoute.js | 26 +++++++-- .../omnichannel/departments/NewDepartment.tsx | 27 +++++++++ .../departments/UpgradeDepartments.tsx | 14 +++++ .../rocketchat-i18n/i18n/en.i18n.json | 7 +++ apps/meteor/public/images/departments.svg | 9 +++ packages/rest-typings/src/v1/omnichannel.ts | 2 +- 9 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts create mode 100644 apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts create mode 100644 apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx create mode 100644 apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx create mode 100644 apps/meteor/client/views/omnichannel/departments/UpgradeDepartments.tsx create mode 100644 apps/meteor/public/images/departments.svg diff --git a/apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts b/apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts new file mode 100644 index 000000000000..28d948ed23ed --- /dev/null +++ b/apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts @@ -0,0 +1,12 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; + +export const useMultipleDepartmentsAvailable = (): boolean | string => { + const getDepartments = useEndpoint('GET', '/v1/livechat/department'); + const hasLicense = useHasLicenseModule('livechat-enterprise'); + + const { data } = useQuery(['getDepartments'], async () => getDepartments()); + return data?.total < 1 || hasLicense; +}; diff --git a/apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts b/apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts new file mode 100644 index 000000000000..70abd49a4e57 --- /dev/null +++ b/apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts @@ -0,0 +1,13 @@ +import type { ILivechatDepartment, LivechatDepartmentProps } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +export const useOmnichannelDepartments = ( + query?: LivechatDepartmentProps, +): { departments: ILivechatDepartment[] | []; refetch: () => void } => { + const getDepartments = useEndpoint('GET', '/v1/livechat/department'); + + const { data, refetch } = useQuery(['getDepartments', query], async () => getDepartments(query)); + + return { data, refetch }; +}; diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx new file mode 100644 index 000000000000..d5bd144c475f --- /dev/null +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -0,0 +1,56 @@ +import { Button, Modal, Box } from '@rocket.chat/fuselage'; +import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { useUpgradeTabParams } from '../../../views/hooks/useUpgradeTabParams'; + +const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): ReactElement => { + const t = useTranslation(); + const upgradeRoute = useRoute('upgrade'); + const departmentsRoute = useRoute('omnichannel-departments'); + const { tabType, trialEndDate } = useUpgradeTabParams(); + + const upgradeNowClick = (): void => { + upgradeRoute.push({ type: tabType }, trialEndDate ? { trialEndDate } : undefined); + closeModal(); + }; + + const onClose = (): void => { + departmentsRoute.push({}); + closeModal(); + }; + + return ( + <> + + + + {t('Enterprise_capability')} + {t('Departments')} + + + + + + + {t('Enterprise_Departments_title')} + + {t('Enterprise_Departments_description')} + + + + + + + + + + ); +}; + +export default EnterpriseDepartmentsModal; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js index 38d5f392ccfe..c082c4e67a98 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js @@ -4,11 +4,12 @@ import { useRouteParameter, useRoute, usePermission, useTranslation } from '@roc import React, { useMemo, useCallback, useState } from 'react'; import GenericTable from '../../../components/GenericTable'; -import { useEndpointData } from '../../../hooks/useEndpointData'; +import { useMultipleDepartmentsAvailable } from '../../../components/Omnichannel/hooks/useMultipleDepartmentsAvailable'; +import { useOmnichannelDepartments } from '../../../components/Omnichannel/hooks/useOmnichannelDepartments'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import DepartmentsPage from './DepartmentsPage'; -import EditDepartment from './EditDepartment'; import EditDepartmentWithData from './EditDepartmentWithData'; +import NewDepartment from './NewDepartment'; import RemoveDepartmentButton from './RemoveDepartmentButton'; const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1); @@ -44,7 +45,6 @@ function DepartmentsRoute() { const debouncedParams = useDebouncedValue(params, 500); const debouncedSort = useDebouncedValue(sort, 500); const onlyMyDepartments = true; - const query = useQuery(debouncedParams, debouncedSort, onlyMyDepartments); const departmentsRoute = useRoute('omnichannel-departments'); const context = useRouteParameter('context'); const id = useRouteParameter('id'); @@ -67,7 +67,21 @@ function DepartmentsRoute() { }), ); - const { value: data = {}, reload } = useEndpointData('/v1/livechat/department', { params: query }); + const { data, refetch } = useOmnichannelDepartments({ + onlyMyDepartments, + text: debouncedParams.text, + offset: debouncedParams.current, + count: debouncedParams.itemsPerPage, + sort: JSON.stringify({ + [debouncedSort.column]: sortDir(debouncedSort.direction), + usernames: debouncedSort.column === 'name' ? sortDir(debouncedSort.direction) : undefined, + }), + fields: JSON.stringify({ name: 1, username: 1, emails: 1, avatarETag: 1 }), + }); + + const reload = useCallback(() => refetch(), [refetch]); + + // const { value: data = {}, reload } = getDepartments('/v1/livechat/department', { params: query }); const header = useMemo( () => @@ -125,7 +139,7 @@ function DepartmentsRoute() { {canRemoveDepartments && } ), - [canRemoveDepartments, onRowClick, t, reload], + [canRemoveDepartments, onRowClick, reload, t], ); if (!canViewDepartments) { @@ -133,7 +147,7 @@ function DepartmentsRoute() { } if (context === 'new') { - return ; + return ; } if (context === 'edit') { diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx new file mode 100644 index 000000000000..9fa1ea1c331a --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { useMultipleDepartmentsAvailable } from '../../../components/Omnichannel/hooks/useMultipleDepartmentsAvailable'; +import PageSkeleton from '../../../components/PageSkeleton'; +import EditDepartment from './EditDepartment'; +import UpgradeDepartments from './UpgradeDepartments'; + +type NewDepartmentProps = { + id: string; + reload: () => void; +}; + +const NewDepartment = ({ id, reload }: NewDepartmentProps) => { + const isMultipleDepartmentsAvailable = useMultipleDepartmentsAvailable(); + const t = useTranslation(); + + if (isMultipleDepartmentsAvailable === 'loading' || undefined) { + return ; + } + if (!isMultipleDepartmentsAvailable) { + return ; + } + return ; +}; + +export default NewDepartment; diff --git a/apps/meteor/client/views/omnichannel/departments/UpgradeDepartments.tsx b/apps/meteor/client/views/omnichannel/departments/UpgradeDepartments.tsx new file mode 100644 index 000000000000..641c70d3b7fd --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/UpgradeDepartments.tsx @@ -0,0 +1,14 @@ +import { useSetModal } from '@rocket.chat/ui-contexts'; +import React, { useEffect } from 'react'; + +import EnterpriseDepartmentsModal from '../../../components/Omnichannel/modals/EnterpriseDepartmentsModal'; +import PageSkeleton from '../../../components/PageSkeleton'; + +const UpgradeDepartments = () => { + const setModal = useSetModal(); + + useEffect(() => setModal( setModal(null)} />), [setModal]); + return ; +}; + +export default UpgradeDepartments; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 692385d35b24..a9ba07dd3c90 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1835,7 +1835,10 @@ "Enter_to": "Enter to", "Enter_your_E2E_password": "Enter your E2E password", "Enterprise": "Enterprise", + "Enterprise_capability": "Enterprise capability", "Enterprise_capabilities": "Enterprise capabilities", + "Enterprise_Departments_title": "Assign customers to queues and improve agent productivity", + "Enterprise_Departments_description": "Departments allow you to customize how conversations get assigned to agents by setting up multiple queues and defining a better routing mechanism.", "Enterprise_Description": "Manually update your Enterprise license.", "Enterprise_License": "Enterprise License", "Enterprise_License_Description": "If your workspace is registered and license is provided by Rocket.Chat Cloud you don't need to manually update the license here.", @@ -4494,6 +4497,8 @@ "Start_call": "Start call", "Start_Chat": "Start Chat", "Start_conference_call": "Start conference call", + "Start_free_trial": "Start free trial", + "Start_of_conversation": "Start of conversation", "Start_OTR": "Start OTR", "Start_video_call": "Start video call", @@ -4586,6 +4591,7 @@ "Take_rocket_chat_with_you_with_mobile_applications": "Take Rocket.Chat with you with mobile applications.", "Taken_at": "Taken at", "Talk_Time": "Talk Time", + "Talk_to_sales": "Talk to sales", "Talk_to_your_workspace_administrator_about_enabling_video_conferencing": "Talk to your workspace administrator about enabling video conferencing", "Target user not allowed to receive messages": "Target user not allowed to receive messages", "TargetRoom": "Target Room", @@ -4943,6 +4949,7 @@ "Update_to_version": "Update to __version__", "Update_your_RocketChat": "Update your Rocket.Chat", "Updated_at": "Updated at", + "Upgrade_now": "Upgrade now", "Upgrade_tab_connection_error_description": "Looks like you have no internet connection. This may be because your workspace is installed on a fully-secured air-gapped server", "Upgrade_tab_connection_error_restore": "Restore your connection to learn about features you are missing out on.", "Upgrade_tab_go_fully_featured": "Go fully featured", diff --git a/apps/meteor/public/images/departments.svg b/apps/meteor/public/images/departments.svg new file mode 100644 index 000000000000..48823720e129 --- /dev/null +++ b/apps/meteor/public/images/departments.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index c1fa4a1f2aea..88f81e3f1574 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -465,7 +465,7 @@ const LivechatTagsListSchema = { export const isLivechatTagsListProps = ajv.compile(LivechatTagsListSchema); -type LivechatDepartmentProps = PaginatedRequest<{ +export type LivechatDepartmentProps = PaginatedRequest<{ text: string; onlyMyDepartments?: booleanString; enabled?: booleanString; From d61b21971388dd6045546175ef4f75fe290b9c01 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 13 Jan 2023 20:51:48 -0300 Subject: [PATCH 03/43] Fix weird caching issue --- .../hooks/useMultipleDepartmentsAvailable.ts | 12 ----------- .../departments/DepartmentsRoute.js | 13 ++++++++---- .../omnichannel/departments/NewDepartment.tsx | 21 ++++++++++++++----- .../departments/RemoveDepartmentButton.js | 3 ++- 4 files changed, 27 insertions(+), 22 deletions(-) delete mode 100644 apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts diff --git a/apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts b/apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts deleted file mode 100644 index 28d948ed23ed..000000000000 --- a/apps/meteor/client/components/Omnichannel/hooks/useMultipleDepartmentsAvailable.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; - -import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; - -export const useMultipleDepartmentsAvailable = (): boolean | string => { - const getDepartments = useEndpoint('GET', '/v1/livechat/department'); - const hasLicense = useHasLicenseModule('livechat-enterprise'); - - const { data } = useQuery(['getDepartments'], async () => getDepartments()); - return data?.total < 1 || hasLicense; -}; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js index c082c4e67a98..57300fb9c5d0 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js @@ -1,10 +1,9 @@ import { Table } from '@rocket.chat/fuselage'; import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRouteParameter, useRoute, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useMemo, useCallback, useState, useRef } from 'react'; import GenericTable from '../../../components/GenericTable'; -import { useMultipleDepartmentsAvailable } from '../../../components/Omnichannel/hooks/useMultipleDepartmentsAvailable'; import { useOmnichannelDepartments } from '../../../components/Omnichannel/hooks/useOmnichannelDepartments'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import DepartmentsPage from './DepartmentsPage'; @@ -49,6 +48,12 @@ function DepartmentsRoute() { const context = useRouteParameter('context'); const id = useRouteParameter('id'); + const refetchRef = useRef(() => undefined); + + const handleRefetch = useCallback(() => { + refetchRef.current(); + }, [refetchRef]); + const onHeaderClick = useMutableCallback((id) => { const [sortBy, sortDirection] = sort; @@ -136,7 +141,7 @@ function DepartmentsRoute() { {numAgents || '0'} {enabled ? t('Yes') : t('No')} {showOnRegistration ? t('Yes') : t('No')} - {canRemoveDepartments && } + {canRemoveDepartments && } ), [canRemoveDepartments, onRowClick, reload, t], @@ -147,7 +152,7 @@ function DepartmentsRoute() { } if (context === 'new') { - return ; + return ; } if (context === 'edit') { diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 9fa1ea1c331a..b0ece8025b25 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -1,7 +1,9 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import type { MutableRefObject } from 'react'; +import React, { useEffect } from 'react'; -import { useMultipleDepartmentsAvailable } from '../../../components/Omnichannel/hooks/useMultipleDepartmentsAvailable'; +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import PageSkeleton from '../../../components/PageSkeleton'; import EditDepartment from './EditDepartment'; import UpgradeDepartments from './UpgradeDepartments'; @@ -9,10 +11,19 @@ import UpgradeDepartments from './UpgradeDepartments'; type NewDepartmentProps = { id: string; reload: () => void; + refetchRef: MutableRefObject<() => void>; }; -const NewDepartment = ({ id, reload }: NewDepartmentProps) => { - const isMultipleDepartmentsAvailable = useMultipleDepartmentsAvailable(); +const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { + const getDepartments = useEndpoint('GET', '/v1/livechat/department'); + const hasLicense = useHasLicenseModule('livechat-enterprise'); + const { data, refetch } = useQuery(['getDepartments'], async () => getDepartments()); + + useEffect(() => { + refetchRef.current = refetch; + }, [refetchRef, refetch]); + + const isMultipleDepartmentsAvailable = data ? data?.total < 1 || hasLicense : 'loading'; const t = useTranslation(); if (isMultipleDepartmentsAvailable === 'loading' || undefined) { diff --git a/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js b/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js index 8d1f5b02cff3..06b16a57ebc8 100644 --- a/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js +++ b/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js @@ -6,7 +6,7 @@ import React from 'react'; import GenericModal from '../../../components/GenericModal'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; -function RemoveDepartmentButton({ _id, reload }) { +function RemoveDepartmentButton({ _id, reload, refetch }) { const deleteAction = useEndpointAction('DELETE', '/v1/livechat/department/:_id', { keys: { _id } }); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); @@ -16,6 +16,7 @@ function RemoveDepartmentButton({ _id, reload }) { const result = await deleteAction(); if (result.success === true) { reload(); + refetch(); } }); From 984da48682b4951a3afe16c795cac1028db1ddd2 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Jan 2023 10:04:08 -0300 Subject: [PATCH 04/43] Fix TS & Better error message --- .../hooks/useOmnichannelDepartments.ts | 13 ------------ .../modals/EnterpriseDepartmentsModal.tsx | 2 +- .../departments/DepartmentsRoute.js | 20 ++++++++++--------- .../omnichannel/departments/NewDepartment.tsx | 3 ++- .../server/lib/LivechatEnterprise.js | 2 +- packages/rest-typings/src/v1/omnichannel.ts | 2 +- 6 files changed, 16 insertions(+), 26 deletions(-) delete mode 100644 apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts diff --git a/apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts b/apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts deleted file mode 100644 index 70abd49a4e57..000000000000 --- a/apps/meteor/client/components/Omnichannel/hooks/useOmnichannelDepartments.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ILivechatDepartment, LivechatDepartmentProps } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; - -export const useOmnichannelDepartments = ( - query?: LivechatDepartmentProps, -): { departments: ILivechatDepartment[] | []; refetch: () => void } => { - const getDepartments = useEndpoint('GET', '/v1/livechat/department'); - - const { data, refetch } = useQuery(['getDepartments', query], async () => getDepartments(query)); - - return { data, refetch }; -}; diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index d5bd144c475f..d5148b78701f 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -12,7 +12,7 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): const { tabType, trialEndDate } = useUpgradeTabParams(); const upgradeNowClick = (): void => { - upgradeRoute.push({ type: tabType }, trialEndDate ? { trialEndDate } : undefined); + tabType && upgradeRoute.push({ type: tabType }, trialEndDate ? { trialEndDate } : undefined); closeModal(); }; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js index 57300fb9c5d0..8c2f617777e8 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js @@ -1,10 +1,10 @@ import { Table } from '@rocket.chat/fuselage'; import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useRouteParameter, useRoute, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouteParameter, useRoute, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import React, { useMemo, useCallback, useState, useRef } from 'react'; import GenericTable from '../../../components/GenericTable'; -import { useOmnichannelDepartments } from '../../../components/Omnichannel/hooks/useOmnichannelDepartments'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import DepartmentsPage from './DepartmentsPage'; import EditDepartmentWithData from './EditDepartmentWithData'; @@ -13,7 +13,7 @@ import RemoveDepartmentButton from './RemoveDepartmentButton'; const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1); -const useQuery = ({ text, itemsPerPage, current }, [column, direction], onlyMyDepartments) => +const useDepartmentsQuery = ({ text, itemsPerPage, current }, [column, direction], onlyMyDepartments) => useMemo( () => ({ fields: JSON.stringify({ name: 1, username: 1, emails: 1, avatarETag: 1 }), @@ -72,7 +72,7 @@ function DepartmentsRoute() { }), ); - const { data, refetch } = useOmnichannelDepartments({ + const query = { onlyMyDepartments, text: debouncedParams.text, offset: debouncedParams.current, @@ -82,11 +82,13 @@ function DepartmentsRoute() { usernames: debouncedSort.column === 'name' ? sortDir(debouncedSort.direction) : undefined, }), fields: JSON.stringify({ name: 1, username: 1, emails: 1, avatarETag: 1 }), - }); + }; - const reload = useCallback(() => refetch(), [refetch]); + const getDepartments = useEndpoint('GET', '/v1/livechat/department'); - // const { value: data = {}, reload } = getDepartments('/v1/livechat/department', { params: query }); + const { data, refetch } = useQuery(['getDepartments', query], async () => getDepartments(query)); + + const reload = useCallback(() => refetch(), [refetch]); const header = useMemo( () => @@ -144,7 +146,7 @@ function DepartmentsRoute() { {canRemoveDepartments && } ), - [canRemoveDepartments, onRowClick, reload, t], + [canRemoveDepartments, handleRefetch, onRowClick, reload, t], ); if (!canViewDepartments) { @@ -165,7 +167,7 @@ function DepartmentsRoute() { params={params} onHeaderClick={onHeaderClick} data={data} - useQuery={useQuery} + useQuery={useDepartmentsQuery} reload={reload} header={header} renderRow={renderRow} diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index b0ece8025b25..a1edd95179bc 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -32,7 +32,8 @@ const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { if (!isMultipleDepartmentsAvailable) { return ; } - return ; + // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS + return ; }; export default NewDepartment; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index e2256377c88d..c7538af8e049 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -223,7 +223,7 @@ export const LivechatEnterprise = { if (!hasLicense('livechat-enterprise')) { const totalDepartments = await LivechatDepartmentRaw.countTotal(); if (!department && totalDepartments >= 1) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { + throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { method: 'livechat:saveDepartment', }); } diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 88f81e3f1574..622505ca4c9a 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -2755,7 +2755,7 @@ export type OmnichannelEndpoints = { GET: () => ILivechatTag | null; }; '/v1/livechat/department': { - GET: (params: LivechatDepartmentProps) => PaginatedResult<{ + GET: (params?: LivechatDepartmentProps) => PaginatedResult<{ departments: ILivechatDepartment[]; }>; POST: (params: { department: Partial; agents: string[] }) => { From 88c6dd5573027428315da7a11768586a01921ac3 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Jan 2023 15:29:41 -0300 Subject: [PATCH 05/43] Tests and small changes --- .../modals/EnterpriseDepartmentsModal.tsx | 41 ++++++++++++++----- .../e2e/omnichannel-departaments.spec.ts | 8 ++++ .../page-objects/omnichannel-departments.ts | 16 ++++++++ .../tests/end-to-end/api/livechat/00-rooms.ts | 3 +- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index d5148b78701f..6ab167b4ca50 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -1,8 +1,10 @@ import { Button, Modal, Box } from '@rocket.chat/fuselage'; +import { useOutsideClick } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useRef } from 'react'; +import { hasPermission } from '../../../../app/authorization/client'; import { useUpgradeTabParams } from '../../../views/hooks/useUpgradeTabParams'; const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): ReactElement => { @@ -10,7 +12,7 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): const upgradeRoute = useRoute('upgrade'); const departmentsRoute = useRoute('omnichannel-departments'); const { tabType, trialEndDate } = useUpgradeTabParams(); - + const ref = useRef(null); const upgradeNowClick = (): void => { tabType && upgradeRoute.push({ type: tabType }, trialEndDate ? { trialEndDate } : undefined); closeModal(); @@ -21,9 +23,11 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): closeModal(); }; + useOutsideClick([ref], onClose); + return ( <> - + {t('Enterprise_capability')} @@ -39,14 +43,29 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): {t('Enterprise_Departments_description')} - - - - + {hasPermission('view-statistics') ? ( + <> + + + + + + ) : ( + <> + {/* */} + + Talk to your workspace admin about enabling departments. + + + {/* */} + + )} diff --git a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts index 1075e3825dbc..29ec37cf8c68 100644 --- a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts @@ -32,6 +32,14 @@ test.describe.serial('omnichannel-departments', () => { await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); }); + test('expect to not be possible adding a second department ', async () => { + await poOmnichannelDepartments.btnNew.click(); + + await expect(poOmnichannelDepartments.upgradeDepartmentsModal).toBeVisible(); + + await poOmnichannelDepartments.btnUpgradeDepartmentsModalClose.click(); + }); + test('expect update department name', async () => { await poOmnichannelDepartments.inputSearch.fill(departmentName); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts index 2f61adc89d48..3b6aa6bcd15c 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts @@ -63,4 +63,20 @@ export class OmnichannelDepartments { get btnModalConfirmDelete() { return this.page.locator('#modal-root .rcx-modal .rcx-modal__footer .rcx-button--danger'); } + + get upgradeDepartmentsModal() { + return this.page.locator('[data-qa-id="enterprise-departments-modal"]'); + } + + get btnUpgradeDepartmentsModalClose() { + return this.page.locator('#modal-root .rcx-modal .rcx-modal__header .rcx-button--small-square'); + } + + get btnUpgradeDepartmentsModalTalkToSales() { + return this.page.locator('[data-qa-id="talk-to-sales"]'); + } + + get btnUpgradeDepartmentsModalUpgrade() { + return this.page.locator('[data-qa-id="upgrade-now"]'); + } } diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 7072a6a668a3..9ce9fb8e6f2e 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -512,8 +512,7 @@ describe('LIVECHAT - rooms', function () { expect(lastMessage?.transferData?.scope).to.be.equal('agent'); expect(lastMessage?.transferData?.transferredTo?.username).to.be.equal(forwardChatToUser.username); }); - - it('should return a success message when transferred successfully to a department', async () => { + (IS_EE ? it : it.skip)('should return a success message when transferred successfully to a department', async () => { const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent(); const { department: forwardToDepartment } = await createDepartmentWithAnOnlineAgent(); From 085c803313f4b4076c63b3b82661322f14d3eba2 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Jan 2023 16:00:09 -0300 Subject: [PATCH 06/43] Better wording --- .../Omnichannel/modals/EnterpriseDepartmentsModal.tsx | 8 ++++++-- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index 6ab167b4ca50..1a0cf92b1804 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -40,7 +40,9 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): {t('Enterprise_Departments_title')} - {t('Enterprise_Departments_description')} + {tabType === 'go-fully-featured' || tabType === 'go-fully-featured-registered' || tabType === 'upgrade-your-plan' + ? t('Enterprise_Departments_description_upgrade') + : t('Enterprise_Departments_description_free_trial')} {hasPermission('view-statistics') ? ( @@ -50,7 +52,9 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): {t('Talk_to_sales')} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index a9ba07dd3c90..982b8b2877da 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1838,7 +1838,8 @@ "Enterprise_capability": "Enterprise capability", "Enterprise_capabilities": "Enterprise capabilities", "Enterprise_Departments_title": "Assign customers to queues and improve agent productivity", - "Enterprise_Departments_description": "Departments allow you to customize how conversations get assigned to agents by setting up multiple queues and defining a better routing mechanism.", + "Enterprise_Departments_description_upgrade": "Workspaces on Community Edition can create just one department. Upgrade to Enterprise to remove limits and supercharge your workspace.", + "Enterprise_Departments_description_free_trial": "Workspaces on Community Edition can create just one department. Start a free Enterprise trial to remove these limits today!", "Enterprise_Description": "Manually update your Enterprise license.", "Enterprise_License": "Enterprise License", "Enterprise_License_Description": "If your workspace is registered and license is provided by Rocket.Chat Cloud you don't need to manually update the license here.", @@ -1944,6 +1945,7 @@ "error-invalid-webhook-response": "The webhook URL responded with a status other than 200", "error-license-user-limit-reached": "The maximum number of users has been reached.", "error-logged-user-not-in-room": "You are not in the room `%s`", + "error-max-departments-number-reached": "You reached the maximum number of departments allowed by your license. Contact sale@rocket.chat for a new license.", "error-max-guests-number-reached": "You reached the maximum number of guest users allowed by your license. Contact sale@rocket.chat for a new license.", "error-max-number-simultaneous-chats-reached": "The maximum number of simultaneous chats per agent has been reached.", "error-message-deleting-blocked": "Message deleting is blocked", @@ -4949,7 +4951,6 @@ "Update_to_version": "Update to __version__", "Update_your_RocketChat": "Update your Rocket.Chat", "Updated_at": "Updated at", - "Upgrade_now": "Upgrade now", "Upgrade_tab_connection_error_description": "Looks like you have no internet connection. This may be because your workspace is installed on a fully-secured air-gapped server", "Upgrade_tab_connection_error_restore": "Restore your connection to learn about features you are missing out on.", "Upgrade_tab_go_fully_featured": "Go fully featured", From 317bd1270bfe0816fd3aa405f806abddfffb68d3 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Jan 2023 17:19:35 -0300 Subject: [PATCH 07/43] Fix reviews & ee tests --- .../modals/EnterpriseDepartmentsModal.tsx | 38 ++++++++----------- .../omnichannel/departments/NewDepartment.tsx | 4 +- .../e2e/omnichannel-departaments.spec.ts | 3 ++ 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index 1a0cf92b1804..f5cc7cbc4a8e 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -46,29 +46,23 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): {hasPermission('view-statistics') ? ( - <> - - - - - + + + + ) : ( - <> - {/* */} - - Talk to your workspace admin about enabling departments. - - - {/* */} - + + Talk to your workspace admin about enabling departments. + + )} diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index a1edd95179bc..8de263cc87f9 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -17,13 +17,13 @@ type NewDepartmentProps = { const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { const getDepartments = useEndpoint('GET', '/v1/livechat/department'); const hasLicense = useHasLicenseModule('livechat-enterprise'); - const { data, refetch } = useQuery(['getDepartments'], async () => getDepartments()); + const { data, refetch, isLoading } = useQuery(['getDepartments'], async () => getDepartments()); useEffect(() => { refetchRef.current = refetch; }, [refetchRef, refetch]); - const isMultipleDepartmentsAvailable = data ? data?.total < 1 || hasLicense : 'loading'; + const isMultipleDepartmentsAvailable = hasLicense || (!isLoading && data?.total !== undefined && data.total < 1); const t = useTranslation(); if (isMultipleDepartmentsAvailable === 'loading' || undefined) { diff --git a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts index 29ec37cf8c68..0b4e63e5f051 100644 --- a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts @@ -3,6 +3,7 @@ import type { Page } from '@playwright/test'; import { test, expect } from './utils/test'; import { OmnichannelDepartments } from './page-objects'; +import { IS_EE } from './config/constants'; test.use({ storageState: 'admin-session.json' }); @@ -33,6 +34,8 @@ test.describe.serial('omnichannel-departments', () => { }); test('expect to not be possible adding a second department ', async () => { + test.skip(!IS_EE, 'Enterprise Only'); + await poOmnichannelDepartments.btnNew.click(); await expect(poOmnichannelDepartments.upgradeDepartmentsModal).toBeVisible(); From df4d8e7be8ba3472325acbf22b5362c6033fab34 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Jan 2023 17:46:09 -0300 Subject: [PATCH 08/43] fix review again --- .../client/views/omnichannel/departments/NewDepartment.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 8de263cc87f9..9dc86518162b 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -23,13 +23,12 @@ const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { refetchRef.current = refetch; }, [refetchRef, refetch]); - const isMultipleDepartmentsAvailable = hasLicense || (!isLoading && data?.total !== undefined && data.total < 1); const t = useTranslation(); - if (isMultipleDepartmentsAvailable === 'loading' || undefined) { + if (isLoading || hasLicense === 'loading') { return ; } - if (!isMultipleDepartmentsAvailable) { + if (hasLicense === false || (data?.total !== undefined && data.total < 1)) { return ; } // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS From 050b0c2668a76244207b4c7bf783a02a1ef51f30 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Jan 2023 18:53:55 -0300 Subject: [PATCH 09/43] Fix reviews --- .../Omnichannel/modals/EnterpriseDepartmentsModal.tsx | 4 ++-- apps/meteor/tests/e2e/omnichannel-departaments.spec.ts | 2 +- apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts | 2 +- packages/rest-typings/src/v1/omnichannel.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index f5cc7cbc4a8e..13d0da8f1310 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -33,7 +33,7 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): {t('Enterprise_capability')} {t('Departments')} - + @@ -59,7 +59,7 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): ) : ( Talk to your workspace admin about enabling departments. - diff --git a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts index 0b4e63e5f051..139fc55e1a6d 100644 --- a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts @@ -34,7 +34,7 @@ test.describe.serial('omnichannel-departments', () => { }); test('expect to not be possible adding a second department ', async () => { - test.skip(!IS_EE, 'Enterprise Only'); + test.skip(IS_EE, 'Community Edition Only'); await poOmnichannelDepartments.btnNew.click(); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts index 3b6aa6bcd15c..1d09b71e64a5 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts @@ -69,7 +69,7 @@ export class OmnichannelDepartments { } get btnUpgradeDepartmentsModalClose() { - return this.page.locator('#modal-root .rcx-modal .rcx-modal__header .rcx-button--small-square'); + return this.page.locator('[data-qa="modal-close"]'); } get btnUpgradeDepartmentsModalTalkToSales() { diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 622505ca4c9a..379b08ee4c76 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -465,7 +465,7 @@ const LivechatTagsListSchema = { export const isLivechatTagsListProps = ajv.compile(LivechatTagsListSchema); -export type LivechatDepartmentProps = PaginatedRequest<{ +type LivechatDepartmentProps = PaginatedRequest<{ text: string; onlyMyDepartments?: booleanString; enabled?: booleanString; From 1919d1839e3a3a0736bad7c3c9a5c8839e665a37 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Jan 2023 19:32:25 -0300 Subject: [PATCH 10/43] reviews --- .../client/views/omnichannel/departments/NewDepartment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 9dc86518162b..1c5e82c43da5 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -28,7 +28,7 @@ const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { if (isLoading || hasLicense === 'loading') { return ; } - if (hasLicense === false || (data?.total !== undefined && data.total < 1)) { + if (!hasLicense || data?.total === 0) { return ; } // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS From f89b05db1d49811d672ad8e0f4e0eda66fe8bb12 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 17 Jan 2023 10:44:31 -0300 Subject: [PATCH 11/43] Fix wrong conditional --- .../client/views/omnichannel/departments/NewDepartment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 1c5e82c43da5..76361535a6d4 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -28,7 +28,7 @@ const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { if (isLoading || hasLicense === 'loading') { return ; } - if (!hasLicense || data?.total === 0) { + if (!hasLicense && data?.total === 0) { return ; } // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS From f8f1e656ec0af0b17f405ebcb404201cd5b683cb Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 19 Jan 2023 13:55:48 -0300 Subject: [PATCH 12/43] Fix tests --- apps/meteor/tests/data/livechat/rooms.ts | 20 +++++++++-- .../end-to-end/api/livechat/10-departments.ts | 33 ++++++++++++++----- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 6cc320949dc5..45702f00fd2a 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -79,12 +79,12 @@ export const fetchInquiry = (roomId: string): Promise => { }); }; -export const createDepartment = (agents?: { agentId: string }[]): Promise => { +export const createDepartment = (agents?: { agentId: string }[], name?: string): Promise => { return new Promise((resolve, reject) => { request .post(api('livechat/department')) .set(credentials) - .send({ department: { name: `Department ${Date.now()}`, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'a@b.com' }, agents }) + .send({ department: { name: name || `Department ${Date.now()}`, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'a@b.com' }, agents }) .end((err: Error, res: DummyResponse) => { if (err) { return reject(err); @@ -94,6 +94,22 @@ export const createDepartment = (agents?: { agentId: string }[]): Promise => { + return new Promise((resolve, reject) => { + request + .delete(api(`livechat/department/${departmentId}`)) + .set(credentials) + .send() + .expect(200) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(res.body.user); + }); + }); +} + export const createAgent = (overrideUsername?: string): Promise => new Promise((resolve, reject) => { request diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index df182eee81c7..8401804e2255 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -5,7 +5,8 @@ import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -import { makeAgentAvailable, createAgent, createDepartment } from '../../../data/livechat/rooms'; +import { makeAgentAvailable, createAgent, createDepartment, deleteDepartment } from '../../../data/livechat/rooms'; +import { IS_EE } from '../../../e2e/config/constants'; describe('LIVECHAT - Departments', function () { before((done) => getCredentials(done)); @@ -51,7 +52,9 @@ describe('LIVECHAT - Departments', function () { request .post(api('livechat/department')) .set(credentials) - .send({ department: { name: 'Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' } }) + .send({ + department: { name: 'TestUnauthorized', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + }) .expect('Content-Type', 'application/json') .expect(403) .end(done); @@ -91,6 +94,7 @@ describe('LIVECHAT - Departments', function () { expect(res.body.department).to.have.property('enabled', true); expect(res.body.department).to.have.property('showOnOfflineForm', true); expect(res.body.department).to.have.property('showOnRegistration', true); + deleteDepartment(res.body.department._id); }) .end(done); }); @@ -147,6 +151,7 @@ describe('LIVECHAT - Departments', function () { expect(res.body.department).to.have.property('showOnOfflineForm', dep.showOnOfflineForm); expect(res.body.department).to.have.property('showOnRegistration', dep.showOnRegistration); expect(res.body.department).to.have.property('email', dep.email); + deleteDepartment(res.body.department._id); }) .end(done); }); @@ -240,6 +245,7 @@ describe('LIVECHAT - Departments', function () { it('should return a list of departments that match selector.term', (done) => { updatePermission('view-livechat-departments', ['admin']) .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => createDepartment(undefined, 'test')) .then(() => { request .get(api('livechat/department.autocomplete')) @@ -254,25 +260,29 @@ describe('LIVECHAT - Departments', function () { expect(res.body.items).to.have.length.of.at.least(1); expect(res.body.items[0]).to.have.property('_id'); expect(res.body.items[0]).to.have.property('name'); + deleteDepartment(res.body.items[0]._id); }) .end(done); }); }); - - it('should return a list of departments excluding the ids on selector.exceptions', (done) => { - let dep: ILivechatDepartment; + (IS_EE ? it : it.skip)('should return a list of departments excluding the ids on selector.exceptions', (done) => { + let dep1: ILivechatDepartment; + let dep2: ILivechatDepartment; updatePermission('view-livechat-departments', ['admin']) .then(() => updatePermission('view-l-room', ['admin'])) .then(() => createDepartment()) .then((department: ILivechatDepartment) => { - dep = department; + dep1 = department; + }).then(() => createDepartment()) + .then((department: ILivechatDepartment) => { + dep2 = department; }) .then(() => { request .get(api('livechat/department.autocomplete')) .set(credentials) - .query({ selector: `{"exceptions":["${dep._id}"]}` }) + .query({ selector: `{"exceptions":["${dep1._id}"]}` }) .expect('Content-Type', 'application/json') .expect(200) .expect((res: Response) => { @@ -282,7 +292,9 @@ describe('LIVECHAT - Departments', function () { expect(res.body.items).to.have.length.of.at.least(1); expect(res.body.items[0]).to.have.property('_id'); expect(res.body.items[0]).to.have.property('name'); - expect(res.body.items.every((department: ILivechatDepartment) => department._id !== dep._id)).to.be.true; + expect(res.body.items.every((department: ILivechatDepartment) => department._id !== dep1._id)).to.be.true; + deleteDepartment(dep1._id); + deleteDepartment(dep2._id); }) .end(done); }); @@ -389,6 +401,7 @@ describe('LIVECHAT - Departments', function () { expect(res.body.agents).to.be.an('array'); expect(res.body.agents).to.have.lengthOf(0); expect(res.body.total).to.be.equal(0); + deleteDepartment(dep._id); }) .end(done); }); @@ -414,6 +427,7 @@ describe('LIVECHAT - Departments', function () { expect(res.body.agents[0]).to.have.property('departmentId', dep._id); expect(res.body.agents[0]).to.have.property('departmentEnabled', true); expect(res.body.count).to.be.equal(1); + deleteDepartment(dep._id); }) .end(done); }); @@ -465,6 +479,7 @@ describe('LIVECHAT - Departments', function () { .expect((res: Response) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', "Match error: Missing key 'upsert'"); + deleteDepartment(dep._id); }) .end(done); }); @@ -484,6 +499,7 @@ describe('LIVECHAT - Departments', function () { .expect((res: Response) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', "Match error: Missing key 'agentId' in field upsert[0]"); + deleteDepartment(dep._id); }) .end(done); }); @@ -502,6 +518,7 @@ describe('LIVECHAT - Departments', function () { .expect(200) .expect((res: Response) => { expect(res.body).to.have.property('success', true); + deleteDepartment(dep._id); }) .end(done); }); From fca41f8c4d2e8aeb6c560f4120171e5cfe14dca7 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 19 Jan 2023 14:22:05 -0300 Subject: [PATCH 13/43] Lint --- apps/meteor/tests/end-to-end/api/livechat/10-departments.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 8401804e2255..0e68a4a28323 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -274,7 +274,8 @@ describe('LIVECHAT - Departments', function () { .then(() => createDepartment()) .then((department: ILivechatDepartment) => { dep1 = department; - }).then(() => createDepartment()) + }) + .then(() => createDepartment()) .then((department: ILivechatDepartment) => { dep2 = department; }) From a7a1c3d376de0df587d33b7a81353d439b81934d Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 20 Jan 2023 11:35:15 -0300 Subject: [PATCH 14/43] Move department tests to ee --- apps/meteor/tests/e2e/omnichannel-departaments.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts index 139fc55e1a6d..ed1d03c69ccb 100644 --- a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts @@ -8,6 +8,7 @@ import { IS_EE } from './config/constants'; test.use({ storageState: 'admin-session.json' }); test.describe.serial('omnichannel-departments', () => { + test.skip(!IS_EE, 'Enterprise Edition Only'); let poOmnichannelDepartments: OmnichannelDepartments; let departmentName: string; @@ -33,7 +34,7 @@ test.describe.serial('omnichannel-departments', () => { await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); }); - test('expect to not be possible adding a second department ', async () => { + test.skip('expect to not be possible adding a second department ', async () => { test.skip(IS_EE, 'Community Edition Only'); await poOmnichannelDepartments.btnNew.click(); From 61ad52df1503beb6d4016a70b4d22c7c88412c80 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 20 Jan 2023 14:15:19 -0300 Subject: [PATCH 15/43] Update NewDepartment.tsx --- .../client/views/omnichannel/departments/NewDepartment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 76361535a6d4..379407cbab40 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -28,7 +28,7 @@ const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { if (isLoading || hasLicense === 'loading') { return ; } - if (!hasLicense && data?.total === 0) { + if (!hasLicense && data?.total >= 1) { return ; } // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS From e8502c83b27c4684cdd2be648b71dc7946a314ab Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 20 Jan 2023 14:35:10 -0300 Subject: [PATCH 16/43] =?UTF-8?q?I=20love=20typescript=20=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/views/omnichannel/departments/NewDepartment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 379407cbab40..0748fbf25dc7 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -28,7 +28,7 @@ const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { if (isLoading || hasLicense === 'loading') { return ; } - if (!hasLicense && data?.total >= 1) { + if (!hasLicense && data?.total && data.total >= 1) { return ; } // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS From aa408afe55f3d50fcf2da6f4d474214d24534b16 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 23 Jan 2023 10:34:49 -0300 Subject: [PATCH 17/43] Update apps/meteor/tests/data/livechat/rooms.ts Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> --- apps/meteor/tests/data/livechat/rooms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 45702f00fd2a..d90d22fec090 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -105,7 +105,7 @@ export const deleteDepartment = (departmentId: string): Promise => { if (err) { return reject(err); } - resolve(res.body.user); + resolve(); }); }); } From 1ef34e96446ed032d11bbd47160e79f05eedeec6 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 26 Jan 2023 17:41:11 -0300 Subject: [PATCH 18/43] Fix TS --- apps/meteor/tests/data/livechat/rooms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index d90d22fec090..b17bcb48b6a0 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -105,7 +105,7 @@ export const deleteDepartment = (departmentId: string): Promise => { if (err) { return reject(err); } - resolve(); + resolve(res.body); }); }); } From f5cab82b5b0b1746e674f48e3c052a110fda04ef Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 31 Jan 2023 15:57:03 -0300 Subject: [PATCH 19/43] add jsdoc --- .../server/lib/LivechatEnterprise.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index c7538af8e049..4ae1a4c7951b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -215,6 +215,23 @@ export const LivechatEnterprise = { await LivechatRooms.unsetOnHoldAndPredictedVisitorAbandonmentByRoomId(roomId); }, + /** + * @param {import('mongodb').Filter} fields + * @param {string} _id - The department id + * @param {{ + * enabled: boolean, + * name: string, + * description?: string, + * showOnRegistration: boolean, + * email: string, + * showOnOfflineForm: boolean, + * requestTagBeforeClosingChat?: boolean, + * chatClosingTags?: string, + * fallbackForwardDepartment?: string, + * }} departmentData - The department id + * @param {{upsert?: string[], remove?: string[]}} departmentAgents - The department agents + */ + async saveDepartment(_id, departmentData, departmentAgents) { check(_id, Match.Maybe(String)); From 8ad8a06b91ae41239b30f9630ab9914a916e235e Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 31 Jan 2023 15:59:13 -0300 Subject: [PATCH 20/43] Remove extra skip --- apps/meteor/tests/e2e/omnichannel-departaments.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts index ed1d03c69ccb..08f031cd39d5 100644 --- a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts @@ -34,7 +34,7 @@ test.describe.serial('omnichannel-departments', () => { await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); }); - test.skip('expect to not be possible adding a second department ', async () => { + test('expect to not be possible adding a second department ', async () => { test.skip(IS_EE, 'Community Edition Only'); await poOmnichannelDepartments.btnNew.click(); From 6f9c1f2055dcef5443a457aa51847e99ee306e84 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 31 Jan 2023 16:00:16 -0300 Subject: [PATCH 21/43] remove wrong field --- .../ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 4ae1a4c7951b..a057728842e3 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -216,7 +216,6 @@ export const LivechatEnterprise = { }, /** - * @param {import('mongodb').Filter} fields * @param {string} _id - The department id * @param {{ * enabled: boolean, From dbdfffde04fe77c90fa4385463e54f3ece8f4ba6 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 31 Jan 2023 16:51:22 -0300 Subject: [PATCH 22/43] TS fixes --- .../server/lib/LivechatEnterprise.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index a057728842e3..7fbe1f387a27 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -216,19 +216,9 @@ export const LivechatEnterprise = { }, /** - * @param {string} _id - The department id - * @param {{ - * enabled: boolean, - * name: string, - * description?: string, - * showOnRegistration: boolean, - * email: string, - * showOnOfflineForm: boolean, - * requestTagBeforeClosingChat?: boolean, - * chatClosingTags?: string, - * fallbackForwardDepartment?: string, - * }} departmentData - The department id - * @param {{upsert?: string[], remove?: string[]}} departmentAgents - The department agents + * @param {string|null} _id - The department id + * @param {Partial} departmentData + * @param {{upsert?: string[], remove: string[]}} [departmentAgents] - The department agents */ async saveDepartment(_id, departmentData, departmentAgents) { From 55457ec52cc2f426c4d07d22fa359aa0c4c6a219 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 31 Jan 2023 17:33:21 -0300 Subject: [PATCH 23/43] Fix TS --- .../ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 7fbe1f387a27..dade3638e667 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -218,7 +218,7 @@ export const LivechatEnterprise = { /** * @param {string|null} _id - The department id * @param {Partial} departmentData - * @param {{upsert?: string[], remove: string[]}} [departmentAgents] - The department agents + * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }[]}} [departmentAgents] - The department agents */ async saveDepartment(_id, departmentData, departmentAgents) { From 5f783b50a0a9fad03981839fed5ad155e77911fc Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Wed, 1 Feb 2023 10:06:49 -0300 Subject: [PATCH 24/43] Move some tests to EE for now --- apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index bd90b644a44c..51300079aa14 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -1447,7 +1447,8 @@ describe('LIVECHAT - rooms', function () { }); }); - describe('it should mark room as unread when a new message arrives and the config is activated', () => { + // TODO: Implement proper department data cleanup after each test to run in CE + (IS_EE ? describe : describe.skip)('it should mark room as unread when a new message arrives and the config is activated', () => { let room: IOmnichannelRoom; let visitor: ILivechatVisitor; let totalMessagesSent = 0; @@ -1476,7 +1477,7 @@ describe('LIVECHAT - rooms', function () { }); }); - describe('it should NOT mark room as unread when a new message arrives and the config is deactivated', () => { + (IS_EE ? describe : describe.skip)('it should NOT mark room as unread when a new message arrives and the config is deactivated', () => { let room: IOmnichannelRoom; let visitor: ILivechatVisitor; let totalMessagesSent = 0; From 8535bd7705e7da886e17b16aa42720286d825c3d Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Fri, 3 Feb 2023 10:59:05 -0300 Subject: [PATCH 25/43] Use correct image --- apps/meteor/public/images/departments.svg | 396 +++++++++++++++++++++- 1 file changed, 392 insertions(+), 4 deletions(-) diff --git a/apps/meteor/public/images/departments.svg b/apps/meteor/public/images/departments.svg index 48823720e129..b43b90caed8e 100644 --- a/apps/meteor/public/images/departments.svg +++ b/apps/meteor/public/images/departments.svg @@ -1,9 +1,397 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 56be13dd2ddb20874564505a25baf52db16d9966 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Fri, 3 Feb 2023 11:46:56 -0300 Subject: [PATCH 26/43] Fix reviews on test --- apps/meteor/tests/end-to-end/api/livechat/10-departments.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 0e68a4a28323..47ebb33a5f69 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -265,7 +265,10 @@ describe('LIVECHAT - Departments', function () { .end(done); }); }); - (IS_EE ? it : it.skip)('should return a list of departments excluding the ids on selector.exceptions', (done) => { + it('should return a list of departments excluding the ids on selector.exceptions', function (done) { + if (!IS_EE) { + this.skip(); + } let dep1: ILivechatDepartment; let dep2: ILivechatDepartment; From dfa629801dd3ba87603dc86a193322393ea57162 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Mon, 6 Feb 2023 15:05:05 -0300 Subject: [PATCH 27/43] Reviews --- .../modals/EnterpriseDepartmentsModal.tsx | 78 +++++++++---------- .../departments/DepartmentsRoute.js | 24 +++--- .../omnichannel/departments/NewDepartment.tsx | 12 +-- .../departments/RemoveDepartmentButton.js | 4 +- .../server/lib/LivechatEnterprise.js | 1 - 5 files changed, 55 insertions(+), 64 deletions(-) diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index 13d0da8f1310..099d5e6ea7be 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -26,47 +26,45 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): useOutsideClick([ref], onClose); return ( - <> - - - - {t('Enterprise_capability')} - {t('Departments')} - - - - - - - {t('Enterprise_Departments_title')} + + + + {t('Enterprise_capability')} + {t('Departments')} + + + + + + + {t('Enterprise_Departments_title')} + + {tabType === 'go-fully-featured' || tabType === 'go-fully-featured-registered' || tabType === 'upgrade-your-plan' + ? t('Enterprise_Departments_description_upgrade') + : t('Enterprise_Departments_description_free_trial')} + + + {hasPermission('view-statistics') ? ( + + + + + ) : ( + + Talk to your workspace admin about enabling departments. + - {tabType === 'go-fully-featured' || tabType === 'go-fully-featured-registered' || tabType === 'upgrade-your-plan' - ? t('Enterprise_Departments_description_upgrade') - : t('Enterprise_Departments_description_free_trial')} - - - {hasPermission('view-statistics') ? ( - - - - - ) : ( - - Talk to your workspace admin about enabling departments. - - - )} - - - + )} + + ); }; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js index 8c2f617777e8..1f9e546dc004 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js @@ -1,8 +1,8 @@ import { Table } from '@rocket.chat/fuselage'; import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRouteParameter, useRoute, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import React, { useMemo, useCallback, useState, useRef } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import React, { useMemo, useCallback, useState } from 'react'; import GenericTable from '../../../components/GenericTable'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; @@ -13,7 +13,7 @@ import RemoveDepartmentButton from './RemoveDepartmentButton'; const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1); -const useDepartmentsQuery = ({ text, itemsPerPage, current }, [column, direction], onlyMyDepartments) => +const useDepartmentsParams = ({ text, itemsPerPage, current }, [column, direction], onlyMyDepartments) => useMemo( () => ({ fields: JSON.stringify({ name: 1, username: 1, emails: 1, avatarETag: 1 }), @@ -48,11 +48,11 @@ function DepartmentsRoute() { const context = useRouteParameter('context'); const id = useRouteParameter('id'); - const refetchRef = useRef(() => undefined); + const queryClient = useQueryClient(); - const handleRefetch = useCallback(() => { - refetchRef.current(); - }, [refetchRef]); + const onRefetch = useCallback(() => { + queryClient.invalidateQueries(['omnichannel', 'departments']); + }, [queryClient]); const onHeaderClick = useMutableCallback((id) => { const [sortBy, sortDirection] = sort; @@ -86,7 +86,7 @@ function DepartmentsRoute() { const getDepartments = useEndpoint('GET', '/v1/livechat/department'); - const { data, refetch } = useQuery(['getDepartments', query], async () => getDepartments(query)); + const { data, refetch } = useQuery(['omnichannel', 'departments', query], async () => getDepartments(query)); const reload = useCallback(() => refetch(), [refetch]); @@ -143,10 +143,10 @@ function DepartmentsRoute() { {numAgents || '0'} {enabled ? t('Yes') : t('No')} {showOnRegistration ? t('Yes') : t('No')} - {canRemoveDepartments && } + {canRemoveDepartments && } ), - [canRemoveDepartments, handleRefetch, onRowClick, reload, t], + [canRemoveDepartments, onRefetch, onRowClick, reload, t], ); if (!canViewDepartments) { @@ -154,7 +154,7 @@ function DepartmentsRoute() { } if (context === 'new') { - return ; + return ; } if (context === 'edit') { @@ -167,7 +167,7 @@ function DepartmentsRoute() { params={params} onHeaderClick={onHeaderClick} data={data} - useQuery={useDepartmentsQuery} + useQuery={useDepartmentsParams} reload={reload} header={header} renderRow={renderRow} diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 0748fbf25dc7..83ff7e8151a7 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -1,7 +1,6 @@ import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { MutableRefObject } from 'react'; -import React, { useEffect } from 'react'; +import React from 'react'; import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import PageSkeleton from '../../../components/PageSkeleton'; @@ -11,17 +10,12 @@ import UpgradeDepartments from './UpgradeDepartments'; type NewDepartmentProps = { id: string; reload: () => void; - refetchRef: MutableRefObject<() => void>; }; -const NewDepartment = ({ id, reload, refetchRef }: NewDepartmentProps) => { +const NewDepartment = ({ id, reload }: NewDepartmentProps) => { const getDepartments = useEndpoint('GET', '/v1/livechat/department'); const hasLicense = useHasLicenseModule('livechat-enterprise'); - const { data, refetch, isLoading } = useQuery(['getDepartments'], async () => getDepartments()); - - useEffect(() => { - refetchRef.current = refetch; - }, [refetchRef, refetch]); + const { data, isLoading } = useQuery(['omnichannel', 'departments', 'new'], async () => getDepartments()); const t = useTranslation(); diff --git a/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js b/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js index 06b16a57ebc8..96dc70f03a8a 100644 --- a/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js +++ b/apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js @@ -6,7 +6,7 @@ import React from 'react'; import GenericModal from '../../../components/GenericModal'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; -function RemoveDepartmentButton({ _id, reload, refetch }) { +function RemoveDepartmentButton({ _id, reload, onRefetch }) { const deleteAction = useEndpointAction('DELETE', '/v1/livechat/department/:_id', { keys: { _id } }); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); @@ -16,7 +16,7 @@ function RemoveDepartmentButton({ _id, reload, refetch }) { const result = await deleteAction(); if (result.success === true) { reload(); - refetch(); + onRefetch(); } }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index dade3638e667..307d03763531 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -220,7 +220,6 @@ export const LivechatEnterprise = { * @param {Partial} departmentData * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }[]}} [departmentAgents] - The department agents */ - async saveDepartment(_id, departmentData, departmentAgents) { check(_id, Match.Maybe(String)); From 94a81ab5ce21a91c7da81a580a14fbfca7417b12 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Mon, 6 Feb 2023 15:49:36 -0300 Subject: [PATCH 28/43] Review --- .../views/omnichannel/departments/NewDepartment.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index 83ff7e8151a7..221d04946581 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -15,14 +15,21 @@ type NewDepartmentProps = { const NewDepartment = ({ id, reload }: NewDepartmentProps) => { const getDepartments = useEndpoint('GET', '/v1/livechat/department'); const hasLicense = useHasLicenseModule('livechat-enterprise'); - const { data, isLoading } = useQuery(['omnichannel', 'departments', 'new'], async () => getDepartments()); + const { data: isDepartmentCreationAvailable, isLoading } = useQuery( + ['omnichannel', 'departments', 'creation-enabled', { hasLicense }], + async () => { + if (hasLicense === true) return true; + const departments = await getDepartments(); + return departments.total < 1; + }, + ); const t = useTranslation(); if (isLoading || hasLicense === 'loading') { return ; } - if (!hasLicense && data?.total && data.total >= 1) { + if (isDepartmentCreationAvailable === false) { return ; } // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS From dd7712999061a9e3ebc8521a4472a32bfef828bb Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Mon, 6 Feb 2023 17:11:40 -0300 Subject: [PATCH 29/43] update wording --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index ae1d9a1d1d55..645a660a3074 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1838,7 +1838,7 @@ "Enterprise_capabilities": "Enterprise capabilities", "Enterprise_Departments_title": "Assign customers to queues and improve agent productivity", "Enterprise_Departments_description_upgrade": "Workspaces on Community Edition can create just one department. Upgrade to Enterprise to remove limits and supercharge your workspace.", - "Enterprise_Departments_description_free_trial": "Workspaces on Community Edition can create just one department. Start a free Enterprise trial to remove these limits today!", + "Enterprise_Departments_description_free_trial": "Workspaces on Community Edition can create one department. Start a free Enterprise trial to create multiple departments today!", "Enterprise_Description": "Manually update your Enterprise license.", "Enterprise_License": "Enterprise License", "Enterprise_License_Description": "If your workspace is registered and license is provided by Rocket.Chat Cloud you don't need to manually update the license here.", From 6ea66592022f3e12cfb65f7bedd893e838668dcb Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 7 Feb 2023 14:15:19 -0600 Subject: [PATCH 30/43] Update raw model --- .../server/lib/LivechatEnterprise.js | 6 +++--- .../server/models/raw/LivechatDepartment.ts | 21 +++++++++++++++++++ .../models/raw/LivechatDepartmentAgents.ts | 6 +++++- .../models/ILivechatDepartmentAgentsModel.ts | 3 ++- .../src/models/ILivechatDepartmentModel.ts | 1 + 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 307d03763531..62e874f37457 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -4,7 +4,7 @@ import { LivechatInquiry, Users, LivechatRooms, LivechatDepartment as LivechatDe import { hasLicense } from '../../../license/server/license'; import { updateDepartmentAgents } from '../../../../../app/livechat/server/lib/Helper'; -import { Messages, LivechatDepartment } from '../../../../../app/models/server'; +import { Messages } from '../../../../../app/models/server'; import { addUserRoles } from '../../../../../server/lib/roles/addUserRoles'; import { removeUserFromRoles } from '../../../../../server/lib/roles/removeUserFromRoles'; import { @@ -284,11 +284,11 @@ export const LivechatEnterprise = { ); } - if (fallbackForwardDepartment && !LivechatDepartment.findOneById(fallbackForwardDepartment)) { + if (fallbackForwardDepartment && !(await LivechatDepartmentRaw.findOneById(fallbackForwardDepartment))) { throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { method: 'livechat:saveDepartment' }); } - const departmentDB = LivechatDepartment.createOrUpdateDepartment(_id, departmentData); + const departmentDB = await LivechatDepartmentRaw.createOrUpdateDepartment(_id, departmentData); if (departmentDB && departmentAgents) { updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); } diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts index 8bb11fdfb28b..2b2a595c86a6 100644 --- a/apps/meteor/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/server/models/raw/LivechatDepartment.ts @@ -2,6 +2,7 @@ import type { ILivechatDepartmentRecord, RocketChatRecordDeleted } from '@rocket import type { ILivechatDepartmentModel } from '@rocket.chat/model-typings'; import type { Collection, FindCursor, Db, Filter, FindOptions, UpdateResult, Document } from 'mongodb'; import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { LivechatDepartmentAgents } from '@rocket.chat/models'; import { BaseRaw } from './BaseRaw'; @@ -114,4 +115,24 @@ export class LivechatDepartmentRaw extends BaseRaw im return this.updateMany(query, update); } + + async createOrUpdateDepartment(_id: string, data: ILivechatDepartmentRecord): Promise { + const current = await this.findOneById(_id); + + const record = { + ...data, + }; + + if (_id) { + await this.updateOne({ _id }, { $set: record }); + } else { + _id = (await this.insertOne(record)).insertedId; + } + + if (current?.enabled !== data.enabled) { + await LivechatDepartmentAgents.setDepartmentEnabledByDepartmentId(_id, data.enabled); + } + + return Object.assign(record, { _id }); + } } diff --git a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts index a55dea5601e1..63d9bca827e5 100644 --- a/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts +++ b/apps/meteor/server/models/raw/LivechatDepartmentAgents.ts @@ -1,6 +1,6 @@ import type { ILivechatDepartmentAgents, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { FindPaginated, ILivechatDepartmentAgentsModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, Filter, FindOptions } from 'mongodb'; +import type { Collection, FindCursor, Db, Filter, FindOptions, Document, UpdateResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -105,4 +105,8 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw { + return this.updateMany({ departmentId }, { $set: { departmentEnabled } }); + } } diff --git a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts index 05963ea57396..da8843d0cde3 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts @@ -1,4 +1,4 @@ -import type { FindCursor, FindOptions } from 'mongodb'; +import type { FindCursor, FindOptions, Document, UpdateResult } from 'mongodb'; import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -58,4 +58,5 @@ export interface ILivechatDepartmentAgentsModel extends IBaseModel): FindCursor; findAgentsByAgentIdAndBusinessHourId(_agentId: string, _businessHourId: string): []; + setDepartmentEnabledByDepartmentId(departmentId: string, departmentEnabled: boolean): Promise; } diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index 3ae3911f691c..24525d6a3944 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -31,4 +31,5 @@ export interface ILivechatDepartmentModel extends IBaseModel; removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId: string): Promise; + createOrUpdateDepartment(_id: string, data: ILivechatDepartmentRecord): Promise; } From d39c13d8729568986c8bbf313f4dafcdc2b63286 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 7 Feb 2023 17:31:58 -0300 Subject: [PATCH 31/43] lint --- apps/meteor/tests/end-to-end/api/livechat/10-departments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 7572c44c2c7a..5bee491b5aef 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -12,7 +12,7 @@ import { createVisitor, createLivechatRoom, getLivechatRoomInfo, - deleteDepartment, + deleteDepartment, } from '../../../data/livechat/rooms'; import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department'; import { IS_EE } from '../../../e2e/config/constants'; From 0d3df9a41337c1e7c7a2aa48ad0ae145e13b5c39 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 7 Feb 2023 18:32:22 -0300 Subject: [PATCH 32/43] Remove empty line --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index f3a440ba1e9d..b3cb74c7c64b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -4501,7 +4501,6 @@ "Start_Chat": "Start Chat", "Start_conference_call": "Start conference call", "Start_free_trial": "Start free trial", - "Start_of_conversation": "Start of conversation", "Start_OTR": "Start OTR", "Start_video_call": "Start video call", From ce5dab8caeedbf868e96109c1c18762771eb5726 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Wed, 8 Feb 2023 10:53:28 -0300 Subject: [PATCH 33/43] Small wording & link changes to modal --- .../modals/EnterpriseDepartmentsModal.tsx | 27 ++++++++++++++----- .../rocketchat-i18n/i18n/en.i18n.json | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index 099d5e6ea7be..de1a606fdfd3 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -23,6 +23,15 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): closeModal(); }; + const tabTypeIsUpgradeYourPlan = + tabType === 'go-fully-featured' || tabType === 'go-fully-featured-registered' || tabType === 'upgrade-your-plan'; + + const talkToExpertLink = + 'https://www.rocket.chat/sales-contact?utm_source=rocketchat_app&utm_medium=multiple_queues&utm_campaign=in_product_ctas'; + + const freeTrialLink = + 'https://www.rocket.chat/trial-saas?utm_source=rocketchat_app&utm_medium=multiple_queues&utm_campaign=in_product_ctas'; + useOutsideClick([ref], onClose); return ( @@ -46,14 +55,18 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): {hasPermission('view-statistics') ? ( - - ) : ( diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index b3cb74c7c64b..16594c47a064 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -4593,7 +4593,7 @@ "Take_rocket_chat_with_you_with_mobile_applications": "Take Rocket.Chat with you with mobile applications.", "Taken_at": "Taken at", "Talk_Time": "Talk Time", - "Talk_to_sales": "Talk to sales", + "Talk_to_an_expert": "Talk to an expert", "Talk_to_your_workspace_administrator_about_enabling_video_conferencing": "Talk to your workspace administrator about enabling video conferencing", "Target user not allowed to receive messages": "Target user not allowed to receive messages", "TargetRoom": "Target Room", From c77addbd7fb6b424ee0fe6b09a76b988dc336179 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 9 Feb 2023 19:44:27 -0300 Subject: [PATCH 34/43] [NEW] Department Archivation (#27966) * [NEW] Department Archivation * Some TS fixes * noice * Fix Reviews * Remove log * Lint * fix ts error on endp * Add archivation tests * Fix TS * Fix TS again * Move tests to ee * wtf * hehehe * fix lint * Lint :) * Fix additional properties issue * Fix small issues and tests --------- Co-authored-by: Kevin Aleman --- .../imports/server/rest/departments.ts | 107 ++++- .../livechat/server/api/lib/departments.ts | 43 +- apps/meteor/app/livechat/server/config.ts | 9 + .../app/livechat/server/lib/Livechat.js | 68 +++- .../components/AutoCompleteDepartment.tsx | 5 +- .../AutoCompleteDepartmentMultiple.js | 7 +- .../Omnichannel/hooks/useDepartmentsList.ts | 14 +- .../views/hooks/useDepartmentsByUnitsList.ts | 8 +- .../views/omnichannel/agents/AgentEdit.tsx | 17 +- .../omnichannel/agents/AgentEditWithData.tsx | 32 +- .../omnichannel/currentChats/FilterByText.tsx | 2 +- .../ArchivedDepartmentsPageWithData.tsx | 48 +++ .../departments/ArchivedItemMenu.tsx | 63 +++ .../departments/DepartmentItemMenu.tsx | 71 ++++ .../departments/DepartmentsPage.js | 45 -- .../departments/DepartmentsPageWithData.tsx | 48 +++ .../departments/DepartmentsRoute.js | 179 -------- .../departments/DepartmentsRoute.tsx | 70 ++++ .../departments/DepartmentsTable.tsx | 88 ++++ .../omnichannel/departments/EditDepartment.js | 3 +- .../departments/EditDepartmentWithData.js | 11 +- .../omnichannel/departments/NewDepartment.tsx | 24 +- .../PermanentDepartmentRemovalModal.tsx | 57 +++ .../departments/RemoveDepartmentButton.js | 45 -- .../server/lib/LivechatEnterprise.js | 12 +- .../ee/client/omnichannel/tags/TagEdit.js | 2 +- .../ee/client/omnichannel/tags/TagNew.js | 30 -- .../ee/client/omnichannel/tags/TagsRoute.js | 7 +- .../ee/client/omnichannel/units/UnitNew.js | 41 -- .../rocketchat-i18n/i18n/en.i18n.json | 7 + .../server/models/raw/LivechatDepartment.ts | 8 + .../e2e/omnichannel-departaments.spec.ts | 21 +- .../page-objects/omnichannel-departments.ts | 16 +- .../end-to-end/api/livechat/10-departments.ts | 385 +++++++++--------- .../core-typings/src/ILivechatDepartment.ts | 1 + packages/rest-typings/src/v1/omnichannel.ts | 36 +- 36 files changed, 1006 insertions(+), 624 deletions(-) create mode 100644 apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx create mode 100644 apps/meteor/client/views/omnichannel/departments/ArchivedItemMenu.tsx create mode 100644 apps/meteor/client/views/omnichannel/departments/DepartmentItemMenu.tsx delete mode 100644 apps/meteor/client/views/omnichannel/departments/DepartmentsPage.js create mode 100644 apps/meteor/client/views/omnichannel/departments/DepartmentsPageWithData.tsx delete mode 100644 apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.js create mode 100644 apps/meteor/client/views/omnichannel/departments/DepartmentsRoute.tsx create mode 100644 apps/meteor/client/views/omnichannel/departments/DepartmentsTable.tsx create mode 100644 apps/meteor/client/views/omnichannel/departments/PermanentDepartmentRemovalModal.tsx delete mode 100644 apps/meteor/client/views/omnichannel/departments/RemoveDepartmentButton.js delete mode 100644 apps/meteor/ee/client/omnichannel/tags/TagNew.js delete mode 100644 apps/meteor/ee/client/omnichannel/units/UnitNew.js diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 77d96f553be0..ffb7a342fe5f 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -1,9 +1,9 @@ import { isGETLivechatDepartmentProps, isPOSTLivechatDepartmentProps } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; +import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models'; import { API } from '../../../../api/server'; import { hasPermission } from '../../../../authorization/server'; -import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../models/server'; import { Livechat } from '../../../server/lib/Livechat'; import { findDepartments, @@ -11,6 +11,7 @@ import { findDepartmentsToAutocomplete, findDepartmentsBetweenIds, findDepartmentAgents, + findArchivedDepartments, } from '../../../server/api/lib/departments'; import { LivechatEnterprise } from '../../../../../ee/app/livechat-enterprise/server/lib/LivechatEnterprise'; import { DepartmentHelper } from '../../../server/lib/Departments'; @@ -30,12 +31,13 @@ API.v1.addRoute( const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); - const { text, enabled, onlyMyDepartments, excludeDepartmentId } = this.queryParams; + const { text, enabled, onlyMyDepartments, excludeDepartmentId, showArchived } = this.queryParams; const { departments, total } = await findDepartments({ userId: this.userId, text, enabled: enabled === 'true', + showArchived: showArchived === 'true', onlyMyDepartments: onlyMyDepartments === 'true', excludeDepartmentId, pagination: { @@ -61,7 +63,7 @@ API.v1.addRoute( if (department) { return API.v1.success({ department, - agents: LivechatDepartmentAgents.find({ departmentId: department._id }).fetch(), + agents: await LivechatDepartmentAgents.find({ departmentId: department._id }).toArray(), }); } @@ -128,8 +130,8 @@ API.v1.addRoute( if (success) { return API.v1.success({ - department: LivechatDepartment.findOneById(_id), - agents: LivechatDepartmentAgents.find({ departmentId: _id }).fetch(), + department: await LivechatDepartment.findOneById(_id), + agents: await LivechatDepartmentAgents.find({ departmentId: _id }).toArray(), }); } @@ -147,12 +149,79 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'livechat/departments/archived', + { + authRequired: true, + validateParams: { GET: isGETLivechatDepartmentProps }, + permissionsRequired: { + GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' }, + }, + }, + { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + const { text, onlyMyDepartments, excludeDepartmentId } = this.queryParams; + + const { departments, total } = await findArchivedDepartments({ + userId: this.userId, + text, + onlyMyDepartments: onlyMyDepartments === 'true', + excludeDepartmentId, + pagination: { + offset, + count, + sort: sort as any, + }, + }); + + return API.v1.success({ departments, count: departments.length, offset, total }); + }, + }, +); + +API.v1.addRoute( + 'livechat/department/:_id/archive', + { + authRequired: true, + permissionsRequired: ['manage-livechat-departments'], + }, + { + async post() { + if (await Livechat.archiveDepartment(this.urlParams._id)) { + return API.v1.success(); + } + + return API.v1.failure(); + }, + }, +); + +API.v1.addRoute( + 'livechat/department/:_id/unarchive', + { + authRequired: true, + permissionsRequired: ['manage-livechat-departments'], + }, + { + async post() { + if (await Livechat.unarchiveDepartment(this.urlParams._id)) { + return API.v1.success(); + } + + return API.v1.failure(); + }, + }, +); + API.v1.addRoute( 'livechat/department.autocomplete', { authRequired: true, permissionsRequired: { GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' } } }, { async get() { - const { selector, onlyMyDepartments } = this.queryParams; + const { selector, onlyMyDepartments, showArchived } = this.queryParams; if (!selector) { return API.v1.failure("The 'selector' param is required"); } @@ -162,6 +231,7 @@ API.v1.addRoute( uid: this.userId, selector: JSON.parse(selector), onlyMyDepartments: onlyMyDepartments === 'true', + showArchived: showArchived === 'true', }), ); }, @@ -169,7 +239,7 @@ API.v1.addRoute( ); API.v1.addRoute( - 'livechat/department/:departmentId/agents', + 'livechat/department/:_id/agents', { authRequired: true, permissionsRequired: { @@ -180,7 +250,7 @@ API.v1.addRoute( { async get() { check(this.urlParams, { - departmentId: String, + _id: String, }); const { offset, count } = this.getPaginationItems(); @@ -188,7 +258,7 @@ API.v1.addRoute( const agents = await findDepartmentAgents({ userId: this.userId, - departmentId: this.urlParams.departmentId, + departmentId: this.urlParams._id, pagination: { offset, count, @@ -200,7 +270,7 @@ API.v1.addRoute( }, async post() { check(this.urlParams, { - departmentId: String, + _id: String, }); check( @@ -210,7 +280,7 @@ API.v1.addRoute( remove: Array, }), ); - Livechat.saveDepartmentAgents(this.urlParams.departmentId, this.bodyParams); + Livechat.saveDepartmentAgents(this.urlParams._id, this.bodyParams); return API.v1.success(); }, @@ -240,3 +310,18 @@ API.v1.addRoute( }, }, ); +API.v1.addRoute( + 'livechat/department/isDepartmentCreationAvailable', + { + authRequired: true, + permissionsRequired: { + GET: { permissions: ['view-livechat-departments', 'manage-livechat-departments'], operation: 'hasAny' }, + }, + }, + { + async get() { + const isDepartmentCreationAvailable = await LivechatEnterprise.isDepartmentCreationAvailable(); + return API.v1.success({ isDepartmentCreationAvailable }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/lib/departments.ts b/apps/meteor/app/livechat/server/api/lib/departments.ts index f4d43735915f..9f8b1240ccf1 100644 --- a/apps/meteor/app/livechat/server/api/lib/departments.ts +++ b/apps/meteor/app/livechat/server/api/lib/departments.ts @@ -14,6 +14,7 @@ type FindDepartmentParams = { text?: string; enabled?: boolean; excludeDepartmentId?: string; + showArchived?: boolean; } & Pagination; type FindDepartmentByIdParams = { userId: string; @@ -29,6 +30,7 @@ type FindDepartmentToAutocompleteParams = { term: string; }; onlyMyDepartments?: boolean; + showArchived?: boolean; }; type FindDepartmentAgentsParams = { userId: string; @@ -41,10 +43,12 @@ export async function findDepartments({ text, enabled, excludeDepartmentId, + showArchived = false, pagination: { offset, count, sort }, }: FindDepartmentParams): Promise> { let query = { $or: [{ type: { $eq: 'd' } }, { type: { $exists: false } }], + ...(!showArchived && { archived: { $ne: !showArchived } }), ...(enabled && { enabled: Boolean(enabled) }), ...(text && { name: new RegExp(escapeRegExp(text), 'i') }), ...(excludeDepartmentId && { _id: { $ne: excludeDepartmentId } }), @@ -70,6 +74,40 @@ export async function findDepartments({ }; } +export async function findArchivedDepartments({ + userId, + onlyMyDepartments = false, + text, + excludeDepartmentId, + pagination: { offset, count, sort }, +}: FindDepartmentParams): Promise> { + let query = { + $or: [{ type: { $eq: 'd' } }, { type: { $exists: false } }], + archived: { $eq: true }, + ...(text && { name: new RegExp(escapeRegExp(text), 'i') }), + ...(excludeDepartmentId && { _id: { $ne: excludeDepartmentId } }), + }; + + if (onlyMyDepartments) { + query = callbacks.run('livechat.applyDepartmentRestrictions', query, { userId }); + } + + const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const [departments, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + departments, + count: departments.length, + offset, + total, + }; +} + export async function findDepartmentById({ userId, departmentId, @@ -102,6 +140,7 @@ export async function findDepartmentsToAutocomplete({ uid, selector, onlyMyDepartments = false, + showArchived = false, }: FindDepartmentToAutocompleteParams): Promise<{ items: ILivechatDepartmentRecord[] }> { const { exceptions = [] } = selector; let { conditions = {} } = selector; @@ -110,7 +149,9 @@ export async function findDepartmentsToAutocomplete({ conditions = callbacks.run('livechat.applyDepartmentRestrictions', conditions, { userId: uid }); } - const items = await LivechatDepartment.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditions, { + const conditionsWithArchived = { archived: { $ne: !showArchived }, ...conditions }; + + const items = await LivechatDepartment.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditionsWithArchived, { projection: { _id: 1, name: 1, diff --git a/apps/meteor/app/livechat/server/config.ts b/apps/meteor/app/livechat/server/config.ts index a815b0e21068..01e14dbcfead 100644 --- a/apps/meteor/app/livechat/server/config.ts +++ b/apps/meteor/app/livechat/server/config.ts @@ -413,6 +413,15 @@ Meteor.startup(function () { enableQuery: [{ _id: 'Livechat_enable_transcript', value: true }, omnichannelEnabledQuery], }); + this.add('Omnichannel_enable_department_removal', false, { + type: 'boolean', + group: 'Omnichannel', + public: true, + i18nLabel: 'Omnichannel_enable_department_removal', + alert: 'Omnichannel_enable_department_removal_alert', + enableQuery: omnichannelEnabledQuery, + }); + this.add('Livechat_registration_form_message', '', { type: 'string', group: 'Omnichannel', diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index bcf7f490ab06..3e628793cf24 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -9,7 +9,13 @@ import _ from 'underscore'; import s from 'underscore.string'; import moment from 'moment-timezone'; import UAParser from 'ua-parser-js'; -import { Users as UsersRaw, LivechatVisitors, LivechatCustomField, Settings } from '@rocket.chat/models'; +import { + Users as UsersRaw, + LivechatVisitors, + LivechatCustomField, + Settings, + LivechatDepartment as LivechatDepartmentRaw, +} from '@rocket.chat/models'; import { VideoConf, api } from '@rocket.chat/core-services'; import { QueueManager } from './QueueManager'; @@ -1061,6 +1067,66 @@ export const Livechat = { return true; }, + removeDepartment(_id) { + check(_id, String); + + const departmentRemovalEnabled = settings.get('Omnichannel_enable_department_removal'); + + if (!departmentRemovalEnabled) { + throw new Meteor.Error('department-removal-disabled', 'Department removal is disabled', { + method: 'livechat:removeDepartment', + }); + } + + const department = LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found', 'Department not found', { + method: 'livechat:removeDepartment', + }); + } + const ret = LivechatDepartment.removeById(_id); + const agentsIds = LivechatDepartmentAgents.findByDepartmentId(_id) + .fetch() + .map((agent) => agent.agentId); + LivechatDepartmentAgents.removeByDepartmentId(_id); + LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id); + if (ret) { + Meteor.defer(() => { + callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); + }); + } + return ret; + }, + + async unarchiveDepartment(_id) { + check(_id, String); + + const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found', 'Department not found', { + method: 'livechat:removeDepartment', + }); + } + + return LivechatDepartmentRaw.unarchiveDepartment(_id); + }, + + async archiveDepartment(_id) { + check(_id, String); + + const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found', 'Department not found', { + method: 'livechat:removeDepartment', + }); + } + + return LivechatDepartmentRaw.archiveDepartment(_id); + }, + showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; diff --git a/apps/meteor/client/components/AutoCompleteDepartment.tsx b/apps/meteor/client/components/AutoCompleteDepartment.tsx index 4bbb37b9049b..9d19d11e8225 100644 --- a/apps/meteor/client/components/AutoCompleteDepartment.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartment.tsx @@ -15,6 +15,7 @@ type AutoCompleteDepartmentProps = { onlyMyDepartments?: boolean; haveAll?: boolean; haveNone?: boolean; + showArchived?: boolean; }; const AutoCompleteDepartment = ({ @@ -24,6 +25,7 @@ const AutoCompleteDepartment = ({ onChange, haveAll, haveNone, + showArchived = false, }: AutoCompleteDepartmentProps): ReactElement | null => { const t = useTranslation(); const [departmentsFilter, setDepartmentsFilter] = useState(''); @@ -38,8 +40,9 @@ const AutoCompleteDepartment = ({ haveAll, haveNone, excludeDepartmentId, + showArchived, }), - [debouncedDepartmentsFilter, onlyMyDepartments, haveAll, haveNone, excludeDepartmentId], + [debouncedDepartmentsFilter, onlyMyDepartments, haveAll, haveNone, excludeDepartmentId, showArchived], ), ); diff --git a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.js b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.js index 0ccbab0e0c21..c4820235d923 100644 --- a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.js +++ b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.js @@ -8,7 +8,7 @@ import { AsyncStatePhase } from '../hooks/useAsyncState'; import { useDepartmentsList } from './Omnichannel/hooks/useDepartmentsList'; const AutoCompleteDepartmentMultiple = (props) => { - const { value, onlyMyDepartments = false, onChange = () => {} } = props; + const { value, onlyMyDepartments = false, showArchived = false, onChange = () => {} } = props; const t = useTranslation(); const [departmentsFilter, setDepartmentsFilter] = useState(''); @@ -16,7 +16,10 @@ const AutoCompleteDepartmentMultiple = (props) => { const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList( - useMemo(() => ({ filter: debouncedDepartmentsFilter, onlyMyDepartments }), [debouncedDepartmentsFilter, onlyMyDepartments]), + useMemo( + () => ({ filter: debouncedDepartmentsFilter, onlyMyDepartments, ...(showArchived && { showArchived: 'true' }) }), + [debouncedDepartmentsFilter, onlyMyDepartments, showArchived], + ), ); const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts index 0c9752891ab8..980e110d05d7 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -14,6 +14,7 @@ type DepartmentsListOptions = { haveNone?: boolean; excludeDepartmentId?: string; enabled?: boolean; + showArchived?: boolean; }; export const useDepartmentsList = ( @@ -44,6 +45,7 @@ export const useDepartmentsList = ( sort: `{ "name": 1 }`, excludeDepartmentId: options.excludeDepartmentId, enabled: options.enabled ? 'true' : 'false', + showArchived: options.showArchived ? 'true' : 'false', }); const items = departments @@ -54,6 +56,9 @@ export const useDepartmentsList = ( return true; }) .map((department: any) => { + if (department.archived) { + department.name = `${department.name} [${t('Archived')}]`; + } department._updatedAt = new Date(department._updatedAt); department.label = department.name; department.value = { value: department._id, label: department.name }; @@ -81,13 +86,14 @@ export const useDepartmentsList = ( }, [ getDepartments, - options.departmentId, - options.filter, - options.haveAll, options.onlyMyDepartments, - options.haveNone, + options.filter, options.excludeDepartmentId, options.enabled, + options.showArchived, + options.haveAll, + options.haveNone, + options.departmentId, t, ], ); diff --git a/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts b/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts index ceaa600c1ee2..de6ea0dd0706 100644 --- a/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts +++ b/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts @@ -1,5 +1,5 @@ import type { ILivechatDepartmentRecord } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useState } from 'react'; import { useScrollableRecordList } from '../../hooks/lists/useScrollableRecordList'; @@ -19,6 +19,7 @@ export const useDepartmentsByUnitsList = ( reload: () => void; loadMoreItems: (start: number, end: number) => void; } => { + const t = useTranslation(); const [itemsList, setItemsList] = useState(() => new RecordList()); const reload = useCallback(() => setItemsList(new RecordList()), []); @@ -38,6 +39,9 @@ export const useDepartmentsByUnitsList = ( return { items: departments.map((department: any) => { + if (department.archived) { + department.name = `${department.name} [${t('Archived')}]`; + } department._updatedAt = new Date(department._updatedAt); department.label = department.name; department.value = { value: department._id, label: department.name }; @@ -46,7 +50,7 @@ export const useDepartmentsByUnitsList = ( itemCount: total, }; }, - [getDepartments, options.filter], + [getDepartments, options.filter, t], ); const { loadMoreItems, initialItemCount } = useScrollableRecordList(itemsList, fetchData, 25); diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx index ac88511f6e66..d9c1321e93c6 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx @@ -21,7 +21,7 @@ type dataType = { type AgentEditProps = { data: dataType; userDepartments: { departments: Pick[] }; - availableDepartments: { departments: Pick[] }; + availableDepartments: { departments: Pick[] }; uid: string; reset: () => void; }; @@ -37,11 +37,16 @@ const AgentEdit: FC = ({ data, userDepartments, availableDepartm const email = getUserEmailAddress(user); - const options: [string, string][] = useMemo( - () => - availableDepartments?.departments ? availableDepartments.departments.map(({ _id, name }) => (name ? [_id, name] : [_id, _id])) : [], - [availableDepartments], - ); + const options: [string, string][] = useMemo(() => { + const archivedDepartment = (name: string, archived?: boolean) => (archived ? `${name} [${t('Archived')}]` : name); + + return availableDepartments?.departments + ? availableDepartments.departments.map(({ _id, name, archived }) => + name ? [_id, archivedDepartment(name, archived)] : [_id, archivedDepartment(_id, archived)], + ) + : []; + }, [availableDepartments.departments, t]); + const initialDepartmentValue = useMemo( () => (userDepartments.departments ? userDepartments.departments.map(({ departmentId }) => departmentId) : []), [userDepartments], diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx index 92513109dedb..c483b7b80884 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEditWithData.tsx @@ -1,11 +1,10 @@ import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; import { FormSkeleton } from '../../../components/Skeleton'; -import { AsyncStatePhase } from '../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../hooks/useEndpointData'; import AgentEdit from './AgentEdit'; type AgentEditWithDataProps = { @@ -15,23 +14,26 @@ type AgentEditWithDataProps = { const AgentEditWithData = ({ uid, reload }: AgentEditWithDataProps): ReactElement => { const t = useTranslation(); - const { value: data, phase: state, error } = useEndpointData('/v1/livechat/users/agent/:_id', { keys: { _id: uid } }); + const getDepartments = useEndpoint('GET', '/v1/livechat/department'); + + const getAgent = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: uid }); + + const getAgentDepartments = useEndpoint('GET', '/v1/livechat/agents/:agentId/departments', { agentId: uid }); + + const { data, isLoading: state, error } = useQuery(['getAgent'], async () => getAgent()); const { - value: userDepartments, - phase: userDepartmentsState, + data: userDepartments, + isLoading: userDepartmentsState, error: userDepartmentsError, - } = useEndpointData('/v1/livechat/agents/:agentId/departments', { keys: { agentId: uid } }); + } = useQuery(['getAgentDepartments'], async () => getAgentDepartments()); + const { - value: availableDepartments, - phase: availableDepartmentsState, + data: availableDepartments, + isLoading: availableDepartmentsState, error: availableDepartmentsError, - } = useEndpointData('/v1/livechat/department'); + } = useQuery(['getDepartments'], async () => getDepartments({ showArchived: 'true' })); - if ( - [state, availableDepartmentsState, userDepartmentsState].includes(AsyncStatePhase.LOADING) || - !userDepartments || - !availableDepartments - ) { + if (state || availableDepartmentsState || userDepartmentsState || !userDepartments || !availableDepartments) { return ; } diff --git a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx index b9dda4652951..19fc232d8d28 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx @@ -150,7 +150,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, customFields, setCu - + {EETagsComponent && ( diff --git a/apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx b/apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx new file mode 100644 index 000000000000..8164a112d16a --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx @@ -0,0 +1,48 @@ +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import type { ReactElement } from 'react'; +import React, { useMemo, useState } from 'react'; + +import FilterByText from '../../../components/FilterByText'; +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../../components/GenericTable/hooks/useSort'; +import ArchivedItemMenu from './ArchivedItemMenu'; +import DepartmentsTable from './DepartmentsTable'; + +const ArchivedDepartmentsPageWithData = (): ReactElement => { + const [text, setText] = useState(''); + + const pagination = usePagination(); + const sort = useSort<'name' | 'email' | 'active'>('name'); + + const query = useDebouncedValue( + useMemo(() => { + return { + onlyMyDepartments: 'true' as const, + text, + sort: JSON.stringify({ [sort.sortBy]: sort.sortDirection === 'asc' ? 1 : -1 }), + ...(pagination.current && { offset: pagination.current }), + ...(pagination.itemsPerPage && { count: pagination.itemsPerPage }), + fields: JSON.stringify({ name: 1, username: 1, emails: 1, avatarETag: 1 }), + }; + }, [pagination, sort.sortBy, sort.sortDirection, text]), + 500, + ); + + const getArchivedDepartments = useEndpoint('GET', '/v1/livechat/departments/archived'); + + const { data, isLoading } = useQuery(['omnichannel', 'departments', 'archived', query], async () => getArchivedDepartments(query)); + + const removeButton = (dep: Omit) => ; + + return ( + <> + setText(text)} /> + + + ); +}; + +export default ArchivedDepartmentsPageWithData; diff --git a/apps/meteor/client/views/omnichannel/departments/ArchivedItemMenu.tsx b/apps/meteor/client/views/omnichannel/departments/ArchivedItemMenu.tsx new file mode 100644 index 000000000000..1eb6fd7d69b5 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/ArchivedItemMenu.tsx @@ -0,0 +1,63 @@ +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import { Menu, Option } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint, useSetModal, useToastMessageDispatch, useTranslation, useSetting } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; +import type { ReactElement } from 'react'; +import React, { useCallback } from 'react'; + +import PermanentDepartmentRemovalModal from './PermanentDepartmentRemovalModal'; + +const ArchivedItemMenu = ({ dep }: { dep: Omit }): ReactElement => { + const unarchiveDepartment = useEndpoint('POST', '/v1/livechat/department/:_id/unarchive', { _id: dep._id }); + + const t = useTranslation(); + const setModal = useSetModal(); + const dispatchToast = useToastMessageDispatch(); + const departmentRemovalEnabled = useSetting('Omnichannel_enable_department_removal'); + + const queryClient = useQueryClient(); + + const handlePageDepartmentsReload = useCallback(async () => { + await queryClient.refetchQueries(['omnichannel', 'departments', 'archived']); + }, [queryClient]); + + const handleUnarchiveDepartment = useMutableCallback(async () => { + await unarchiveDepartment(); + handlePageDepartmentsReload(); + dispatchToast({ type: 'success', message: t('Department_unarchived') }); + }); + + const handlePermanentDepartmentRemoval = useMutableCallback(() => { + setModal( + setModal(undefined)} + name={dep.name} + />, + ); + }); + + const menuOptions = { + unarchive: { + label: { label: t('Unarchive'), icon: 'undo' }, + action: (): Promise => handleUnarchiveDepartment(), + }, + + ...(departmentRemovalEnabled === true && { + delete: { + label: { label: t('Delete'), icon: 'trash' }, + action: (): void => handlePermanentDepartmentRemoval(), + }, + }), + }; + return ( +