From 89796cb920bbd12fd123466f212d472642234fb6 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Mon, 16 Dec 2019 16:55:11 -0300 Subject: [PATCH] [IMPROVE] Replace livechat:rooms publication by REST (#15968) * Replace livechat:rooms publication by REST * Improve livechat rooms endpoint to return department data * remove log * Fix load rooms * Fix lint * Fix wrong end date * Fix wrong var name --- .../client/collections/LivechatRoom.js | 3 - .../views/app/livechatCurrentChats.html | 6 +- .../client/views/app/livechatCurrentChats.js | 91 ++++++++++++++++--- app/livechat/imports/server/rest/rooms.js | 19 ++-- app/livechat/server/api/lib/livechat.js | 55 ----------- app/livechat/server/api/lib/rooms.js | 53 +++++++++++ .../server/publications/livechatRooms.js | 1 + app/models/server/raw/LivechatRooms.js | 73 +++++++++++++++ server/stream/rooms/index.js | 3 +- tests/end-to-end/api/livechat/00-rooms.js | 14 ++- 10 files changed, 227 insertions(+), 91 deletions(-) delete mode 100644 app/livechat/client/collections/LivechatRoom.js create mode 100644 app/livechat/server/api/lib/rooms.js diff --git a/app/livechat/client/collections/LivechatRoom.js b/app/livechat/client/collections/LivechatRoom.js deleted file mode 100644 index 739aff33fba..00000000000 --- a/app/livechat/client/collections/LivechatRoom.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const LivechatRoom = new Mongo.Collection('livechatRoom'); diff --git a/app/livechat/client/views/app/livechatCurrentChats.html b/app/livechat/client/views/app/livechatCurrentChats.html index 064a61f7fa6..b0c9434df38 100644 --- a/app/livechat/client/views/app/livechatCurrentChats.html +++ b/app/livechat/client/views/app/livechatCurrentChats.html @@ -158,7 +158,7 @@ - {{lookupDepartment.name}} + {{department.name}} {{servedBy}} {{startedAt}} {{lastMessage}} @@ -174,11 +174,11 @@ {{/requiresPermission}} {{/each}} - {{#unless isReady}} + {{#if isLoading}} {{> loading}} - {{/unless}} + {{/if}} {{/table}} diff --git a/app/livechat/client/views/app/livechatCurrentChats.js b/app/livechat/client/views/app/livechatCurrentChats.js index 17988b1ded0..fedb8f9f0f6 100644 --- a/app/livechat/client/views/app/livechatCurrentChats.js +++ b/app/livechat/client/views/app/livechatCurrentChats.js @@ -1,3 +1,4 @@ +import 'moment-timezone'; import _ from 'underscore'; import moment from 'moment'; import { ReactiveVar } from 'meteor/reactive-var'; @@ -10,19 +11,21 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { modal, call, popover } from '../../../../ui-utils'; import { t, handleError, APIClient } from '../../../../utils/client'; -import { LivechatRoom } from '../../collections/LivechatRoom'; import { hasRole, hasPermission, hasAtLeastOnePermission } from '../../../../authorization'; import './livechatCurrentChats.html'; +const ROOMS_COUNT = 50; + Template.livechatCurrentChats.helpers({ hasMore() { - return Template.instance().ready.get() && LivechatRoom.find({ t: 'l' }, { sort: { ts: -1 } }).count() === Template.instance().limit.get(); + const instance = Template.instance(); + return instance.total.get() > instance.livechatRooms.get().length; }, - isReady() { - return Template.instance().ready.get(); + isLoading() { + return Template.instance().isLoading.get(); }, livechatRoom() { - return Template.instance().livechatRoom.get(); + return Template.instance().livechatRooms.get(); }, startedAt() { return moment(this.ts).format('L LTS'); @@ -76,7 +79,7 @@ Template.livechatCurrentChats.events({ FlowRouter.go('live', { id: this._id }); }, 'click .js-load-more'(event, instance) { - instance.limit.set(instance.limit.get() + 20); + instance.offset.set(instance.offset.get() + ROOMS_COUNT); }, 'click .add-filter-button'(event, instance) { event.preventDefault(); @@ -179,6 +182,7 @@ Template.livechatCurrentChats.events({ } if (result) { + instance.loadRooms(instance.filter.get(), instance.offset.get()); toastr.success(TAPi18n.__('All_closed_chats_have_been_removed')); } }); @@ -230,6 +234,9 @@ Template.livechatCurrentChats.events({ } const value = $(this).val(); + if (!value) { + return; + } if (this.name.startsWith('custom-field-')) { if (!filter.customFields) { @@ -269,13 +276,13 @@ Template.livechatCurrentChats.events({ const agents = instance.selectedAgents.get(); if (agents && agents.length > 0) { - filter.agent = agents[0]._id; + filter.agents = [agents[0]._id]; } instance.filter.set(filter); - instance.limit.set(20); + instance.offset.set(0); }, - 'click .remove-livechat-room'(event) { + 'click .remove-livechat-room'(event, instance) { event.preventDefault(); event.stopPropagation(); @@ -293,6 +300,7 @@ Template.livechatCurrentChats.events({ return; } await call('livechat:removeRoom', this._id); + instance.loadRooms(instance.filter.get(), instance.offset.get()); modal.open({ title: t('Deleted'), text: t('Room_has_been_deleted'), @@ -312,16 +320,69 @@ Template.livechatCurrentChats.events({ }); Template.livechatCurrentChats.onCreated(async function() { - this.ready = new ReactiveVar(false); - this.limit = new ReactiveVar(20); + this.isLoading = new ReactiveVar(false); + this.offset = new ReactiveVar(0); + this.total = new ReactiveVar(0); this.filter = new ReactiveVar({}); - this.livechatRoom = new ReactiveVar([]); + this.livechatRooms = new ReactiveVar([]); this.selectedAgents = new ReactiveVar([]); this.customFilters = new ReactiveVar([]); this.customFields = new ReactiveVar([]); this.tagFilters = new ReactiveVar([]); this.departments = new ReactiveVar([]); + const mountArrayQueryParameters = (label, items, index) => items.reduce((acc, item) => { + const isTheLastElement = index === items.length - 1; + acc += `${ label }[]=${ item }${ isTheLastElement ? '' : '&' }`; + return acc; + }, ''); + + const mountUrlWithParams = (filter, offset) => { + const { status, agents, department, from, to, tags, customFields, name: roomName } = filter; + let url = `livechat/rooms?count=${ ROOMS_COUNT }&offset=${ offset }`; + const dateRange = {}; + if (status) { + url += `&open=${ status === 'opened' }`; + } + if (department) { + url += `&departmentId=${ department }`; + } + if (from) { + dateRange.start = `${ moment(new Date(from)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`; + } + if (to) { + dateRange.end = `${ moment(new Date(to).setHours(23, 59, 59)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`; + } + if (tags) { + url += `&${ mountArrayQueryParameters('tags', tags) }`; + } + if (agents && Array.isArray(agents) && agents.length) { + url += `&${ mountArrayQueryParameters('agents', agents) }`; + } + if (customFields) { + url += `&customFields=${ JSON.stringify(customFields) }`; + } + if (Object.keys(dateRange).length) { + url += `&createdAt=${ JSON.stringify(dateRange) }`; + } + if (roomName) { + url += `&roomName=${ roomName }`; + } + return url; + }; + + this.loadRooms = async (filter, offset) => { + this.isLoading.set(true); + const { rooms, total } = await APIClient.v1.get(mountUrlWithParams(filter, offset)); + this.total.set(total); + if (offset === 0) { + this.livechatRooms.set(rooms); + } else { + this.livechatRooms.set(this.livechatRooms.get().concat(rooms)); + } + this.isLoading.set(false); + }; + this.onSelectAgents = ({ item: agent }) => { this.selectedAgents.set([agent]); }; @@ -331,7 +392,9 @@ Template.livechatCurrentChats.onCreated(async function() { }; this.autorun(async () => { - this.ready.set(this.subscribe('livechat:rooms', this.filter.get(), 0, this.limit.get()).ready()); + const filter = this.filter.get(); + const offset = this.offset.get(); + this.loadRooms(filter, offset); }); const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}'); @@ -342,8 +405,6 @@ Template.livechatCurrentChats.onCreated(async function() { this.customFields.set(customFields); } }); - - this.livechatRoom.set(LivechatRoom.find({ t: 'l' }, { sort: { ts: -1 } })); }); Template.livechatCurrentChats.onRendered(function() { diff --git a/app/livechat/imports/server/rest/rooms.js b/app/livechat/imports/server/rest/rooms.js index 7c66c1d4788..905f8ff2b26 100644 --- a/app/livechat/imports/server/rest/rooms.js +++ b/app/livechat/imports/server/rest/rooms.js @@ -2,7 +2,7 @@ import { Match, check } from 'meteor/check'; import { hasPermission } from '../../../../authorization/server'; import { API } from '../../../../api'; -import { findRooms } from '../../../server/api/lib/livechat'; +import { findRooms } from '../../../server/api/lib/rooms'; API.v1.addRoute('livechat/rooms', { authRequired: true }, { get() { @@ -12,12 +12,14 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, { } const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); - const { agents, departmentId, open, tags } = this.requestParams(); + const { agents, departmentId, open, tags, roomName } = this.requestParams(); let { createdAt, customFields, closedAt } = this.requestParams(); check(agents, Match.Maybe([String])); + check(roomName, Match.Maybe(String)); check(departmentId, Match.Maybe(String)); check(open, Match.Maybe(String)); check(tags, Match.Maybe([String])); + if (createdAt) { createdAt = JSON.parse(createdAt); } @@ -27,8 +29,10 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, { if (customFields) { customFields = JSON.parse(customFields); } - const { rooms, total } = findRooms({ + + return API.v1.success(Promise.await(findRooms({ agents, + roomName, departmentId, open: open && open === 'true', createdAt, @@ -36,14 +40,7 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, { tags, customFields, options: { offset, count, sort, fields }, - }); - - return API.v1.success({ - rooms, - count: rooms.length, - offset, - total, - }); + }))); } catch (e) { return API.v1.failure(e); } diff --git a/app/livechat/server/api/lib/livechat.js b/app/livechat/server/api/lib/livechat.js index bc0d7f109dd..ccaebb40379 100644 --- a/app/livechat/server/api/lib/livechat.js +++ b/app/livechat/server/api/lib/livechat.js @@ -148,58 +148,3 @@ export function settings() { export async function getExtraConfigInfo(room) { return callbacks.run('livechat.onLoadConfigApi', room); } - -export function findRooms({ - agents, - departmentId, - open, - createdAt, - closedAt, - tags, - customFields, - options = {}, -}) { - const query = { t: 'l' }; - if (agents) { - query.$or = [{ 'servedBy._id': { $in: agents } }, { 'servedBy.username': { $in: agents } }]; - } - if (departmentId) { - query.departmentId = departmentId; - } - if (open !== undefined) { - query.open = { $exists: open }; - } - if (createdAt) { - query.ts = {}; - if (createdAt.start) { - query.ts.$gte = new Date(createdAt.start); - } - if (createdAt.end) { - query.ts.$lte = new Date(createdAt.end); - } - } - if (closedAt) { - query.closedAt = {}; - if (closedAt.start) { - query.closedAt.$gte = new Date(closedAt.start); - } - if (closedAt.end) { - query.closedAt.$lte = new Date(closedAt.end); - } - } - if (tags) { - query.tags = { $in: tags }; - } - if (customFields) { - query.$and = Object.keys(customFields).map((key) => ({ [`livechatData.${ key }`]: customFields[key] })); - } - return { - rooms: LivechatRooms.find(query, { - sort: options.sort || { ts: 1 }, - skip: options.offset, - limit: options.count, - fields: options.fields, - }).fetch(), - total: LivechatRooms.find(query).count(), - }; -} diff --git a/app/livechat/server/api/lib/rooms.js b/app/livechat/server/api/lib/rooms.js new file mode 100644 index 00000000000..985f12bf987 --- /dev/null +++ b/app/livechat/server/api/lib/rooms.js @@ -0,0 +1,53 @@ +import { LivechatRooms } from '../../../../models/server/raw'; + +export async function findRooms({ + agents, + roomName, + departmentId, + open, + createdAt, + closedAt, + tags, + customFields, + options: { + offset, + count, + fields, + sort, + }, +}) { + const total = (await LivechatRooms.findRoomsWithCriteria({ + agents, + roomName, + departmentId, + open, + createdAt, + closedAt, + tags, + customFields, + })).length; + + const rooms = await LivechatRooms.findRoomsWithCriteria({ + agents, + roomName, + departmentId, + open, + createdAt, + closedAt, + tags, + customFields, + options: { + sort: sort || { ts: 1 }, + offset, + count, + fields, + }, + }); + + return { + rooms, + count: rooms.length, + offset, + total, + }; +} diff --git a/app/livechat/server/publications/livechatRooms.js b/app/livechat/server/publications/livechatRooms.js index 1009cf649d6..1b14da103c7 100644 --- a/app/livechat/server/publications/livechatRooms.js +++ b/app/livechat/server/publications/livechatRooms.js @@ -17,6 +17,7 @@ const userCanAccessRoom = ({ _id }) => { }; Meteor.publish('livechat:rooms', function(filter = {}, offset = 0, limit = 20) { + console.warn('The publication "livechat:rooms" is deprecated and will be removed after version v4.0.0'); if (!this.userId) { return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:rooms' })); } diff --git a/app/models/server/raw/LivechatRooms.js b/app/models/server/raw/LivechatRooms.js index b29b854a650..624a244ecea 100644 --- a/app/models/server/raw/LivechatRooms.js +++ b/app/models/server/raw/LivechatRooms.js @@ -103,4 +103,77 @@ export class LivechatRoomsRaw extends BaseRaw { return this.find(query, options); } + + findRoomsWithCriteria({ agents, roomName, departmentId, open, createdAt, closedAt, tags, customFields, options = {} }) { + const match = { + $match: { + t: 'l', + }, + }; + if (agents) { + match.$match.$or = [{ 'servedBy._id': { $in: agents } }, { 'servedBy.username': { $in: agents } }]; + } + if (roomName) { + match.$match.fname = new RegExp(roomName, 'i'); + } + if (departmentId) { + match.$match.departmentId = departmentId; + } + if (open !== undefined) { + match.$match.open = { $exists: open }; + } + if (createdAt) { + match.$match.ts = {}; + if (createdAt.start) { + match.$match.ts.$gte = new Date(createdAt.start); + } + if (createdAt.end) { + match.$match.ts.$lte = new Date(createdAt.end); + } + } + if (closedAt) { + match.$match.closedAt = {}; + if (closedAt.start) { + match.$match.closedAt.$gte = new Date(closedAt.start); + } + if (closedAt.end) { + match.$match.closedAt.$lte = new Date(closedAt.end); + } + } + if (tags) { + match.$match.tags = { $in: tags }; + } + if (customFields) { + match.$match.$and = Object.keys(customFields).map((key) => ({ [`livechatData.${ key }`]: new RegExp(customFields[key], 'i') })); + } + const firstParams = [match]; + if (options.offset) { + firstParams.push({ $skip: options.offset }); + } + if (options.count) { + firstParams.push({ $limit: options.count }); + } + if (options.sort) { + firstParams.push({ $sort: { name: 1 } }); + } + const lookup = { + $lookup: { + from: 'rocketchat_livechat_department', + localField: 'departmentId', + foreignField: '_id', + as: 'department', + }, + }; + const unwind = { + $unwind: { + path: '$department', + preserveNullAndEmptyArrays: true, + }, + }; + const params = [...firstParams, lookup, unwind]; + if (options.fields) { + params.push({ $project: options.fields }); + } + return this.col.aggregate(params).toArray(); + } } diff --git a/server/stream/rooms/index.js b/server/stream/rooms/index.js index c601d0dc75f..c65df05ea63 100644 --- a/server/stream/rooms/index.js +++ b/server/stream/rooms/index.js @@ -12,7 +12,6 @@ roomDataStream.allowWrite('none'); roomDataStream.allowRead(function(rid) { try { const room = Meteor.call('canAccessRoom', rid, this.userId); - if (!room) { return false; } @@ -28,7 +27,7 @@ roomDataStream.allowRead(function(rid) { }); export function emitRoomDataEvent(id, data) { - if (!data) { + if (!data || !data.t) { return; } diff --git a/tests/end-to-end/api/livechat/00-rooms.js b/tests/end-to-end/api/livechat/00-rooms.js index af15cb39bae..dc163d4c841 100644 --- a/tests/end-to-end/api/livechat/00-rooms.js +++ b/tests/end-to-end/api/livechat/00-rooms.js @@ -12,7 +12,7 @@ describe('LIVECHAT - rooms', function() { createAgent() .then(() => createVisitor()) .then((visitor) => createLivechatRoom(visitor.token)) - .then(done); + .then(() => done()); }); }); @@ -42,6 +42,16 @@ describe('LIVECHAT - rooms', function() { .end(done); }); }); + it('should return an error when the "roomName" query parameter is not valid', (done) => { + request.get(api('livechat/rooms?roomName[]=invalid')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }) + .end(done); + }); it('should return an error when the "departmentId" query parameter is not valid', (done) => { request.get(api('livechat/rooms?departmentId[]=marcos')) .set(credentials) @@ -119,7 +129,7 @@ describe('LIVECHAT - rooms', function() { it('should return an array of rooms when the query params is all valid', (done) => { request.get(api(`livechat/rooms?agents[]=teste&departamentId=123&open=true&createdAt={"start": "2018-01-26T00:11:22.345Z", "end": "2018-01-26T00:11:22.345Z"} &closedAt={"start": "2018-01-26T00:11:22.345Z", "end": "2018-01-26T00:11:22.345Z"}&tags[]=rocket - &customFields={"docId": "031041"}&count=3&offset=1&sort={"_updatedAt": 1}&fields={"msgs": 1}`)) + &customFields={"docId": "031041"}&count=3&offset=1&sort={"_updatedAt": 1}&fields={"msgs": 0}&roomName=test`)) .set(credentials) .expect('Content-Type', 'application/json') .expect(200)