From b8b94c7b00d7fae2c76f4390635dc58906d8d332 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Fri, 30 Jun 2023 17:38:31 -0300 Subject: [PATCH 01/10] fix: Canned Response custom tags break the GUI on enterprise --- .../client/components/Omnichannel/Tags.tsx | 50 ++++++++++--------- apps/meteor/ee/client/hooks/useTagsList.ts | 11 ++-- .../cannedResponses/CannedResponseEdit.tsx | 8 +-- .../modals/CreateCannedResponse/index.tsx | 3 +- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/apps/meteor/client/components/Omnichannel/Tags.tsx b/apps/meteor/client/components/Omnichannel/Tags.tsx index dfa1cb713a5f5..dc0983babe536 100644 --- a/apps/meteor/client/components/Omnichannel/Tags.tsx +++ b/apps/meteor/client/components/Omnichannel/Tags.tsx @@ -3,22 +3,19 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ChangeEvent, ReactElement } from 'react'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useFormsSubscription } from '../../views/omnichannel/additionalForms'; import { FormSkeleton } from './Skeleton'; -const Tags = ({ - tags = [], - handler, - error, - tagRequired, -}: { +type TagsProps = { tags?: string[]; handler: (value: string[]) => void; error?: string; tagRequired?: boolean; -}): ReactElement => { +}; + +const Tags = ({ tags = [], handler, error, tagRequired }: TagsProps): ReactElement => { const t = useTranslation(); const forms = useFormsSubscription() as any; @@ -32,16 +29,20 @@ const Tags = ({ enabled: Boolean(EETagsComponent), }); + const customTags = useMemo(() => { + return tags.filter((tag) => !tagsResult?.tags.find((rtag) => rtag._id === tag)); + }, [tags, tagsResult?.tags]); + const dispatchToastMessage = useToastMessageDispatch(); const [tagValue, handleTagValue] = useState(''); - const [paginatedTagValue, handlePaginatedTagValue] = useState<{ label: string; value: string }[]>(); + + const paginatedTagValue = useMemo(() => tags.map((tag) => ({ label: tag, value: tag })), [tags]); const removeTag = (tagToRemove: string): void => { - if (tags) { - const tagsFiltered = tags.filter((tag: string) => tag !== tagToRemove); - handler(tagsFiltered); - } + if (!tags) return; + + handler(tags.filter((tag) => tag !== tagToRemove)); }; const handleTagTextSubmit = useMutableCallback(() => { @@ -55,7 +56,7 @@ const Tags = ({ return; } - if (tags.includes(tagValue)) { + if (tags.some((tag) => tag === tagValue)) { dispatchToastMessage({ type: 'error', message: t('Tag_already_exists') }); return; } @@ -78,8 +79,7 @@ const Tags = ({ { - handler(tags.map((tag) => tag.label)); - handlePaginatedTagValue(tags); + handler(tags.map((tag) => tag.value)); }} /> @@ -97,16 +97,18 @@ const Tags = ({ {t('Add')} - - - {tags?.map((tag, i) => ( - removeTag(tag)} mie='x8'> - {tag} - - ))} - )} + + {customTags.length > 0 && ( + + {customTags?.map((tag, i) => ( + removeTag(tag)} mie='x8'> + {tag} + + ))} + + )} ); }; diff --git a/apps/meteor/ee/client/hooks/useTagsList.ts b/apps/meteor/ee/client/hooks/useTagsList.ts index 5c887d28d7139..28ee025368bee 100644 --- a/apps/meteor/ee/client/hooks/useTagsList.ts +++ b/apps/meteor/ee/client/hooks/useTagsList.ts @@ -35,12 +35,11 @@ export const useTagsList = ( count: end + start, }); return { - items: tags.map((tag: any) => { - tag._updatedAt = new Date(tag._updatedAt); - tag.label = tag.name; - tag.value = { value: tag._id, label: tag.name }; - return tag; - }), + items: tags.map((tag: any) => ({ + label: tag.name, + value: tag._id, + _updatedAt: new Date(tag._updatedAt), + })), itemCount: total, }; }, diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx b/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx index daac8b0533330..721b78d60dd02 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx +++ b/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx @@ -39,10 +39,7 @@ const CannedResponseEdit: FC<{ _id: data?.cannedResponse ? data.cannedResponse._id : '', shortcut: data ? data.cannedResponse.shortcut : '', text: data ? data.cannedResponse.text : '', - tags: - data?.cannedResponse?.tags && Array.isArray(data.cannedResponse.tags) - ? data.cannedResponse.tags.map((tag) => ({ label: tag, value: tag })) - : [], + tags: data?.cannedResponse?.tags ?? [], scope: data ? data.cannedResponse.scope : 'user', departmentId: data?.cannedResponse?.departmentId ? data.cannedResponse.departmentId : '', }); @@ -100,13 +97,12 @@ const CannedResponseEdit: FC<{ tags: any; departmentId: string; }; - const mappedTags = tags.map((tag: string | { value: string; label: string }) => (typeof tag === 'object' ? tag?.value : tag)); await saveCannedResponse({ ...(_id && { _id }), shortcut, text, scope, - ...(mappedTags.length > 0 && { tags: mappedTags }), + tags, ...(departmentId && { departmentId }), }); dispatchToastMessage({ diff --git a/apps/meteor/ee/client/omnichannel/components/CannedResponse/modals/CreateCannedResponse/index.tsx b/apps/meteor/ee/client/omnichannel/components/CannedResponse/modals/CreateCannedResponse/index.tsx index fc19de9481356..9285bc5ae942a 100644 --- a/apps/meteor/ee/client/omnichannel/components/CannedResponse/modals/CreateCannedResponse/index.tsx +++ b/apps/meteor/ee/client/omnichannel/components/CannedResponse/modals/CreateCannedResponse/index.tsx @@ -77,13 +77,12 @@ const WrapCreateCannedResponseModal: FC<{ data?: any; reloadCannedList?: any }> tags: any; departmentId: string; }; - const mappedTags = tags.map((tag: string | { value: string; label: string }) => (typeof tag === 'object' ? tag?.value : tag)); await saveCannedResponse({ ...(_id && { _id }), shortcut, text, scope, - ...(tags.length > 0 && { tags: mappedTags }), + tags, ...(departmentId && { departmentId }), }); dispatchToastMessage({ From 894b44d1c79bd605a9960b75007a46bc4b516748 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Fri, 30 Jun 2023 17:39:50 -0300 Subject: [PATCH 02/10] chore: changeset --- .changeset/kind-coats-worry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/kind-coats-worry.md diff --git a/.changeset/kind-coats-worry.md b/.changeset/kind-coats-worry.md new file mode 100644 index 0000000000000..81140d95124f0 --- /dev/null +++ b/.changeset/kind-coats-worry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed Canned Response custom tags breaking the GUI on enterprise From 846022674e6036cde58950cfeeb5932c3714b8f0 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 3 Jul 2023 09:55:34 -0600 Subject: [PATCH 03/10] get tags from db --- .../server/lib/canned-responses.js | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js index 26f751fe4e99f..d0aaa20527106 100644 --- a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js @@ -1,12 +1,38 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { LivechatDepartmentAgents, CannedResponse, LivechatUnit } from '@rocket.chat/models'; +import { LivechatDepartmentAgents, CannedResponse, LivechatUnit, LivechatTag } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission'; +const getTagsInformation = async (cannedResponses) => { + return Promise.all( + cannedResponses.map(async (cannedResponse) => { + const { tags } = cannedResponse; + + const serverTags = await LivechatTag.find({ _id: { $in: tags } }, { projection: { _id: 1, name: 1 } }).toArray(); + + // filter out tags that were found + const newTags = tags.reduce((acc, tag) => { + const found = serverTags.find((serverTag) => serverTag._id === tag); + if (found) { + acc.push(found.name); + } else { + acc.push(tag); + } + return acc; + }, []); + + // Known limitation: if a tag was added and removed before this, it will return the tag id instead of the name + cannedResponse.tags = newTags; + + return cannedResponse; + }), + ); +}; + export async function findAllCannedResponses({ userId }) { // If the user is an admin or livechat manager, get his own responses and all responses from all departments if (await hasPermissionAsync(userId, 'view-all-canned-responses')) { - return CannedResponse.find({ + const cannedResponses = await CannedResponse.find({ $or: [ { scope: 'user', @@ -20,14 +46,16 @@ export async function findAllCannedResponses({ userId }) { }, ], }).toArray(); + return getTagsInformation(cannedResponses); } // If the user it not any of the previous roles nor an agent, then get only his own responses if (!(await hasPermissionAsync(userId, 'view-agent-canned-responses'))) { - return CannedResponse.find({ + const cannedResponses = await CannedResponse.find({ scope: 'user', userId, }).toArray(); + return getTagsInformation(cannedResponses); } // Last scenario: user is an agente, so get his own responses and those from the departments he is in @@ -48,7 +76,7 @@ export async function findAllCannedResponses({ userId }) { ...monitoredDepartments.map((department) => department._id), ]; - return CannedResponse.find({ + const cannedResponses = await CannedResponse.find({ $or: [ { scope: 'user', @@ -65,6 +93,8 @@ export async function findAllCannedResponses({ userId }) { }, ], }).toArray(); + + return getTagsInformation(cannedResponses); } export async function findAllCannedResponsesFilter({ userId, shortcut, text, departmentId, scope, createdBy, tags = [], options = {} }) { @@ -76,7 +106,7 @@ export async function findAllCannedResponsesFilter({ userId, shortcut, text, dep agentId: userId, }, { - fields: { + projection: { departmentId: 1, }, }, @@ -153,7 +183,7 @@ export async function findAllCannedResponsesFilter({ userId, shortcut, text, dep }); const [cannedResponses, total] = await Promise.all([cursor.toArray(), totalCount]); return { - cannedResponses, + cannedResponses: await getTagsInformation(cannedResponses), total, }; } From 53b57595e00477df1ef80d93d6cc8479fe74ccd3 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 3 Jul 2023 10:31:32 -0600 Subject: [PATCH 04/10] remove tags when theyre removed from db --- .../api-enterprise/server/lib/canned-responses.js | 4 ++++ .../server/hooks/afterTagRemoved.ts | 14 ++++++++++++++ .../app/livechat-enterprise/server/hooks/index.ts | 1 + .../server/lib/LivechatEnterprise.ts | 2 ++ apps/meteor/ee/server/models/raw/CannedResponse.ts | 12 +++++++++++- apps/meteor/lib/callbacks.ts | 2 ++ .../core-typings/src/IOmnichannelCannedResponse.ts | 2 +- .../src/models/ICannedResponseModel.ts | 3 ++- 8 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTagRemoved.ts diff --git a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js index d0aaa20527106..8ef12b980352b 100644 --- a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js @@ -8,6 +8,10 @@ const getTagsInformation = async (cannedResponses) => { cannedResponses.map(async (cannedResponse) => { const { tags } = cannedResponse; + if (!Array.isArray(tags) || !tags.length) { + return cannedResponse; + } + const serverTags = await LivechatTag.find({ _id: { $in: tags } }, { projection: { _id: 1, name: 1 } }).toArray(); // filter out tags that were found diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTagRemoved.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTagRemoved.ts new file mode 100644 index 0000000000000..451ce972f62ae --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterTagRemoved.ts @@ -0,0 +1,14 @@ +import { CannedResponse } from '@rocket.chat/models'; + +import { callbacks } from '../../../../../lib/callbacks'; + +callbacks.add( + 'livechat.afterTagRemoved', + async (tag) => { + const { _id } = tag; + + await CannedResponse.removeTagFromCannedResponses(_id); + }, + callbacks.priority.MEDIUM, + 'on-tag-removed-remove-references', +); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts index 3eba755b573da..c5bf0a5aa392b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts @@ -25,3 +25,4 @@ import './applySimultaneousChatsRestrictions'; import './afterInquiryQueued'; import './sendPdfTranscriptOnClose'; import './applyRoomRestrictions'; +import './afterTagRemoved'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts index 995ac4f8cabad..94cb249ac5d6a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts @@ -26,6 +26,7 @@ import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingMa import { settings } from '../../../../../app/settings/server'; import { queueLogger } from './logger'; import { getInquirySortMechanismSetting } from '../../../../../app/livechat/server/lib/settings'; +import { callbacks } from '../../../../../lib/callbacks'; export const LivechatEnterprise = { async addMonitor(username: string) { @@ -134,6 +135,7 @@ export const LivechatEnterprise = { throw new Meteor.Error('tag-not-found', 'Tag not found', { method: 'livechat:removeTag' }); } + await callbacks.run('livechat.afterTagRemoved', tag); return LivechatTag.removeById(_id); }, diff --git a/apps/meteor/ee/server/models/raw/CannedResponse.ts b/apps/meteor/ee/server/models/raw/CannedResponse.ts index 5fd3f5c639741..21f2ed1d1a4fe 100644 --- a/apps/meteor/ee/server/models/raw/CannedResponse.ts +++ b/apps/meteor/ee/server/models/raw/CannedResponse.ts @@ -1,6 +1,6 @@ import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; import type { ICannedResponseModel } from '@rocket.chat/model-typings'; -import type { Db, DeleteResult, FindCursor, FindOptions, IndexDescription } from 'mongodb'; +import type { Db, DeleteResult, Document, FindCursor, FindOptions, IndexDescription, UpdateFilter, UpdateResult } from 'mongodb'; import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; @@ -106,4 +106,14 @@ export class CannedResponseRaw extends BaseRaw imple return this.deleteOne(query); } + + removeTagFromCannedResponses(tagId: string): Promise { + const update: UpdateFilter = { + $pull: { + tags: tagId, + }, + }; + + return this.updateMany({}, update); + } } diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index f84b927c89e0f..0cbc7ed1d98ee 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -19,6 +19,7 @@ import type { ILivechatTag, SelectedAgent, InquiryWithAgentInfo, + ILivechatTagRecord, } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; @@ -93,6 +94,7 @@ interface EventLikeCallbackSignatures { 'afterJoinRoom': (user: IUser, room: IRoom) => void; 'beforeCreateRoom': (data: { type: IRoom['t']; extraData: { encrypted: boolean } }) => void; 'afterSaveUser': ({ user, oldUser }: { user: IUser; oldUser: IUser | null }) => void; + 'livechat.afterTagRemoved': (tag: ILivechatTagRecord) => void; } /** diff --git a/packages/core-typings/src/IOmnichannelCannedResponse.ts b/packages/core-typings/src/IOmnichannelCannedResponse.ts index 9887476c8b386..79e6b2b3a6858 100644 --- a/packages/core-typings/src/IOmnichannelCannedResponse.ts +++ b/packages/core-typings/src/IOmnichannelCannedResponse.ts @@ -6,7 +6,7 @@ export interface IOmnichannelCannedResponse extends IRocketChatRecord { shortcut: string; text: string; scope: string; - tags: any; + tags: string[]; // userId is optional, its only required when scope === 'user' userId?: IUser['_id']; departmentId?: ILivechatDepartment['_id']; diff --git a/packages/model-typings/src/models/ICannedResponseModel.ts b/packages/model-typings/src/models/ICannedResponseModel.ts index bd8fd9fb0356e..375ff9cdb03fc 100644 --- a/packages/model-typings/src/models/ICannedResponseModel.ts +++ b/packages/model-typings/src/models/ICannedResponseModel.ts @@ -1,5 +1,5 @@ import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, DeleteResult } from 'mongodb'; +import type { FindOptions, FindCursor, DeleteResult, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -24,4 +24,5 @@ export interface ICannedResponseModel extends IBaseModel, ): Promise>; + removeTagFromCannedResponses(tagId: string): Promise; } From b93a192e36cf0566414e4077d009de1df33a813a Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 3 Jul 2023 10:56:55 -0600 Subject: [PATCH 05/10] tests --- apps/meteor/tests/data/livechat/tags.ts | 23 +++++++++++ .../api/livechat/15-canned-responses.ts | 41 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/data/livechat/tags.ts b/apps/meteor/tests/data/livechat/tags.ts index c8cfbd1d4bed2..c2c8f42a209de 100644 --- a/apps/meteor/tests/data/livechat/tags.ts +++ b/apps/meteor/tests/data/livechat/tags.ts @@ -24,3 +24,26 @@ export const saveTags = (): Promise => { }); }); }; + +export const removeTag = (id: string): Promise => { + return new Promise((resolve, reject) => { + request + .post(methodCall(`livechat:removeTag`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:removeTag', + params: [id], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + } + ); + }); +}; \ No newline at end of file diff --git a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts index fffe2f125d190..82b6eb6feb214 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts @@ -6,6 +6,7 @@ import { getCredentials, api, request, credentials } from '../../../data/api-dat import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { createCannedResponse } from '../../../data/livechat/canned-responses'; import { IS_EE } from '../../../e2e/config/constants'; +import { removeTag, saveTags } from '../../../data/livechat/tags'; (IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Canned responses', function () { this.retries(0); @@ -72,11 +73,11 @@ import { IS_EE } from '../../../e2e/config/constants'; }); it('should return a canned response matching the params provided (tags)', async () => { const response = await createCannedResponse(); - const { body } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': response.tags[0] }).expect(200); + const { body } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': response.tags?.[0] }).expect(200); expect(body).to.have.property('success', true); expect(body.cannedResponses).to.be.an('array').with.lengthOf(1); expect(body.cannedResponses[0]).to.have.property('_id'); - expect(body.cannedResponses[0]).to.have.property('tags').that.is.an('array').which.includes(response.tags[0]); + expect(body.cannedResponses[0]).to.have.property('tags').that.is.an('array').which.includes(response.tags?.[0]); }); it('should return a canned response matching the params provided (text)', async () => { const response = await createCannedResponse(); @@ -129,6 +130,42 @@ import { IS_EE } from '../../../e2e/config/constants'; .send({ shortcut: 'shortcutxx', scope: 'user', tags: ['tag'], text: 'text' }) .expect(400); }); + it('should save a canned response related to an EE tag', async () => { + const tag = await saveTags(); + + const { body } = await request + .post(api('canned-responses')) + .set(credentials) + .send({ shortcut: 'shortcutxxx', scope: 'user', tags: [tag._id], text: 'text' }) + .expect(200); + + expect(body).to.have.property('success', true); + + const { body: getResult } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': tag._id }).expect(200); + + expect(getResult).to.have.property('success', true); + expect(getResult.cannedResponses).to.be.an('array').with.lengthOf(1); + expect(getResult.cannedResponses[0]).to.have.property('_id'); + expect(getResult.cannedResponses[0]).to.have.property('tags').that.is.an('array').which.includes(tag.name); + }); + it('should not include removed tags in the response', async () => { + const tag = await saveTags(); + + const { body } = await request + .post(api('canned-responses')) + .set(credentials) + .send({ shortcut: 'shortcutxxxx', scope: 'user', tags: [tag._id], text: 'text' }) + .expect(200); + + expect(body).to.have.property('success', true); + + await removeTag(tag._id); + + const { body: getResult } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': tag._id }).expect(200); + + expect(getResult).to.have.property('success', true); + expect(getResult.cannedResponses).to.be.an('array').with.lengthOf(0); + }); }); describe('[DELETE] canned-responses', () => { From 1550bd5bc4c2665acccbf2893f2d712247f5ccea Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 3 Jul 2023 12:23:55 -0600 Subject: [PATCH 06/10] ref --- .../server/lib/canned-responses.js | 33 ++----------------- .../ee/app/api-enterprise/server/lib/tags.ts | 33 +++++++++++++++++++ .../ee/server/models/raw/LivechatTag.ts | 8 ++++- .../src/models/ILivechatTagModel.ts | 3 +- 4 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 apps/meteor/ee/app/api-enterprise/server/lib/tags.ts diff --git a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js index 8ef12b980352b..1fd685aa69f1a 100644 --- a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js @@ -1,37 +1,8 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { LivechatDepartmentAgents, CannedResponse, LivechatUnit, LivechatTag } from '@rocket.chat/models'; +import { LivechatDepartmentAgents, CannedResponse, LivechatUnit } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission'; - -const getTagsInformation = async (cannedResponses) => { - return Promise.all( - cannedResponses.map(async (cannedResponse) => { - const { tags } = cannedResponse; - - if (!Array.isArray(tags) || !tags.length) { - return cannedResponse; - } - - const serverTags = await LivechatTag.find({ _id: { $in: tags } }, { projection: { _id: 1, name: 1 } }).toArray(); - - // filter out tags that were found - const newTags = tags.reduce((acc, tag) => { - const found = serverTags.find((serverTag) => serverTag._id === tag); - if (found) { - acc.push(found.name); - } else { - acc.push(tag); - } - return acc; - }, []); - - // Known limitation: if a tag was added and removed before this, it will return the tag id instead of the name - cannedResponse.tags = newTags; - - return cannedResponse; - }), - ); -}; +import { getTagsInformation } from './tags'; export async function findAllCannedResponses({ userId }) { // If the user is an admin or livechat manager, get his own responses and all responses from all departments diff --git a/apps/meteor/ee/app/api-enterprise/server/lib/tags.ts b/apps/meteor/ee/app/api-enterprise/server/lib/tags.ts new file mode 100644 index 0000000000000..c4ffc97da7f84 --- /dev/null +++ b/apps/meteor/ee/app/api-enterprise/server/lib/tags.ts @@ -0,0 +1,33 @@ +import type { ILivechatTag, IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; +import { LivechatTag } from '@rocket.chat/models'; + +const filterTags = (tags: string[], serverTags: ILivechatTag[]) => { + return tags.reduce((acc, tag) => { + const found = serverTags.find((serverTag) => serverTag._id === tag); + if (found) { + acc.push(found.name); + } else { + acc.push(tag); + } + return acc; + }, [] as string[]); +}; + +export const getTagsInformation = async (cannedResponses: IOmnichannelCannedResponse[]) => { + return Promise.all( + cannedResponses.map(async (cannedResponse) => { + const { tags } = cannedResponse; + + if (!Array.isArray(tags) || !tags.length) { + return cannedResponse; + } + + const serverTags = await LivechatTag.findInIds(tags, { projection: { _id: 1, name: 1 } }).toArray(); + + // Known limitation: if a tag was added and removed before this, it will return the tag id instead of the name + cannedResponse.tags = filterTags(tags, serverTags); + + return cannedResponse; + }), + ); +}; diff --git a/apps/meteor/ee/server/models/raw/LivechatTag.ts b/apps/meteor/ee/server/models/raw/LivechatTag.ts index 465ed709b598a..a4314c569f46a 100644 --- a/apps/meteor/ee/server/models/raw/LivechatTag.ts +++ b/apps/meteor/ee/server/models/raw/LivechatTag.ts @@ -1,6 +1,6 @@ import type { ILivechatTag } from '@rocket.chat/core-typings'; import type { ILivechatTagModel } from '@rocket.chat/model-typings'; -import type { Db, DeleteResult, FindOptions, IndexDescription } from 'mongodb'; +import type { Db, DeleteResult, FindCursor, FindOptions, IndexDescription } from 'mongodb'; import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; @@ -26,6 +26,12 @@ export class LivechatTagRaw extends BaseRaw implements ILivechatTa return this.findOne(query, options); } + findInIds(ids: string[], options?: FindOptions): FindCursor { + const query = { _id: { $in: ids } }; + + return this.find(query, options); + } + async createOrUpdateTag( _id: string | undefined, { name, description }: { name: string; description?: string }, diff --git a/packages/model-typings/src/models/ILivechatTagModel.ts b/packages/model-typings/src/models/ILivechatTagModel.ts index 22aa655dbb9e0..270e9ba99e095 100644 --- a/packages/model-typings/src/models/ILivechatTagModel.ts +++ b/packages/model-typings/src/models/ILivechatTagModel.ts @@ -1,10 +1,11 @@ import type { ILivechatTag } from '@rocket.chat/core-typings'; -import type { FindOptions, DeleteResult } from 'mongodb'; +import type { FindOptions, DeleteResult, FindCursor } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; export interface ILivechatTagModel extends IBaseModel { findOneById(_id: string, options?: FindOptions): Promise; + findInIds(ids: string[], options?: FindOptions): FindCursor; createOrUpdateTag( _id: string | undefined, { name, description }: { name: string; description?: string }, From 4bd1be815256f37e73de962980306b7f00b04ed4 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Mon, 3 Jul 2023 16:49:08 -0300 Subject: [PATCH 07/10] fix: showing tag id's in the room info contextual bar --- .../directory/chats/contextualBar/ChatInfo.js | 3 ++- .../directory/chats/hooks/useTagsLabels.tsx | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/client/views/omnichannel/directory/chats/hooks/useTagsLabels.tsx diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js index 7c32188e7e343..386ef76552335 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js @@ -16,6 +16,7 @@ import Label from '../../../components/Label'; import { AgentField, SlaField, ContactField, SourceField } from '../../components'; import PriorityField from '../../components/PriorityField'; import { useOmnichannelRoomInfo } from '../../hooks/useOmnichannelRoomInfo'; +import { useTagsLabels } from '../hooks/useTagsLabels'; import DepartmentField from './DepartmentField'; import VisitorClientInfo from './VisitorClientInfo'; @@ -33,7 +34,6 @@ function ChatInfo({ id, route }) { const { ts, - tags, closedAt, departmentId, v, @@ -49,6 +49,7 @@ function ChatInfo({ id, route }) { queuedAt, } = room || { room: { v: {} } }; + const tags = useTagsLabels(room?.tags); const routePath = useRoute(route || 'omnichannel-directory'); const canViewCustomFields = usePermission('view-livechat-room-customfields'); const subscription = useUserSubscription(id); diff --git a/apps/meteor/client/views/omnichannel/directory/chats/hooks/useTagsLabels.tsx b/apps/meteor/client/views/omnichannel/directory/chats/hooks/useTagsLabels.tsx new file mode 100644 index 0000000000000..f732a78ba3f57 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/directory/chats/hooks/useTagsLabels.tsx @@ -0,0 +1,21 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +// TODO: Remove this hook when the endpoint is changed +// to return labels instead of tag id's +export const useTagsLabels = (initialTags: string[] = []) => { + const getTags = useEndpoint('GET', '/v1/livechat/tags'); + const { data: tagsData, isInitialLoading } = useQuery(['/v1/livechat/tags'], () => getTags({ text: '' }), { + enabled: Boolean(initialTags.length), + }); + + const labels = useMemo(() => { + const { tags = [] } = tagsData || {}; + return tags.reduce>((acc, tag) => ({ ...acc, [tag._id]: tag.name }), {}); + }, [tagsData]); + + return useMemo(() => { + return isInitialLoading ? initialTags : initialTags.map((tag) => labels[tag] || tag); + }, [initialTags, isInitialLoading, labels]); +}; From a3c93c7df061474fc7cde7ab9299f329fc615339 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 3 Jul 2023 15:05:20 -0600 Subject: [PATCH 08/10] fix ts --- apps/meteor/tests/data/livechat/tags.ts | 22 ------------------- .../src/models/ICannedResponseModel.ts | 2 +- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/apps/meteor/tests/data/livechat/tags.ts b/apps/meteor/tests/data/livechat/tags.ts index 0f9077dd22e1c..b12cca1f64ae2 100644 --- a/apps/meteor/tests/data/livechat/tags.ts +++ b/apps/meteor/tests/data/livechat/tags.ts @@ -25,28 +25,6 @@ export const saveTags = (departments: string[] = []): Promise => { }); }; -export const removeTag = (id: string): Promise => { - return new Promise((resolve, reject) => { - request - .post(methodCall(`livechat:removeTag`)) - .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:removeTag', - params: [id], - id: '101', - msg: 'method', - }), - }) - .end((err: Error, res: DummyResponse) => { - if (err) { - return reject(err); - } - resolve(JSON.parse(res.body.message).result); - }); - }); -}; - export const removeTag = (id: string): Promise => { return new Promise((resolve, reject) => { request diff --git a/packages/model-typings/src/models/ICannedResponseModel.ts b/packages/model-typings/src/models/ICannedResponseModel.ts index 375ff9cdb03fc..6cfa582c78259 100644 --- a/packages/model-typings/src/models/ICannedResponseModel.ts +++ b/packages/model-typings/src/models/ICannedResponseModel.ts @@ -1,5 +1,5 @@ import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, DeleteResult, UpdateResult } from 'mongodb'; +import type { FindOptions, FindCursor, DeleteResult, UpdateResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; From 080e6a70264e80d4a51252fb470cdd46b8f9eab3 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 4 Jul 2023 09:31:34 -0300 Subject: [PATCH 09/10] chore: typescript --- apps/meteor/ee/server/models/raw/CannedResponse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/ee/server/models/raw/CannedResponse.ts b/apps/meteor/ee/server/models/raw/CannedResponse.ts index 21f2ed1d1a4fe..e897db928dc27 100644 --- a/apps/meteor/ee/server/models/raw/CannedResponse.ts +++ b/apps/meteor/ee/server/models/raw/CannedResponse.ts @@ -1,6 +1,6 @@ import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; import type { ICannedResponseModel } from '@rocket.chat/model-typings'; -import type { Db, DeleteResult, Document, FindCursor, FindOptions, IndexDescription, UpdateFilter, UpdateResult } from 'mongodb'; +import type { Db, DeleteResult, FindCursor, FindOptions, IndexDescription, UpdateFilter } from 'mongodb'; import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; @@ -107,7 +107,7 @@ export class CannedResponseRaw extends BaseRaw imple return this.deleteOne(query); } - removeTagFromCannedResponses(tagId: string): Promise { + removeTagFromCannedResponses(tagId: string) { const update: UpdateFilter = { $pull: { tags: tagId, From 6d4e6d1c80ab5ab7bd96e4267a135dec3488199f Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 6 Jul 2023 13:57:45 -0300 Subject: [PATCH 10/10] fix: missing _id on the useTagsList --- apps/meteor/ee/client/hooks/useTagsList.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/meteor/ee/client/hooks/useTagsList.ts b/apps/meteor/ee/client/hooks/useTagsList.ts index 664f463e43f95..229e495342e14 100644 --- a/apps/meteor/ee/client/hooks/useTagsList.ts +++ b/apps/meteor/ee/client/hooks/useTagsList.ts @@ -36,8 +36,10 @@ export const useTagsList = ( count: end + start, ...(options.department && { department: options.department }), }); + return { items: tags.map((tag: any) => ({ + _id: tag._id, label: tag.name, value: tag._id, _updatedAt: new Date(tag._updatedAt),