[IMPROVE] Replace livechat:queue subscription (#15612)

* Replace livechat:queue subscription

* Refactor livechat queue screen

* Fix some style stuff.

* Rename old class name.

* Display the user avatar on the livechat queue list.
pull/15940/head
Marcos Spessatto Defendi 6 years ago committed by Renato Becker
parent 7b76bf362d
commit accb833be6
  1. 3
      app/livechat/client/collections/LivechatQueueUser.js
  2. 15
      app/livechat/client/stylesheets/livechat.less
  3. 6
      app/livechat/client/views/app/livechatCurrentChats.html
  4. 136
      app/livechat/client/views/app/livechatQueue.html
  5. 116
      app/livechat/client/views/app/livechatQueue.js
  6. 23
      app/livechat/imports/server/rest/queue.js
  7. 1
      app/livechat/server/api.js
  8. 25
      app/livechat/server/api/lib/queue.js
  9. 20
      app/livechat/server/publications/livechatQueue.js
  10. 9
      app/models/server/raw/LivechatDepartmentAgents.js
  11. 94
      app/models/server/raw/LivechatRooms.js
  12. 1
      packages/rocketchat-i18n/i18n/en.i18n.json
  13. 1
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  14. 46
      tests/end-to-end/api/livechat/queue.js

@ -1,3 +0,0 @@
import { Mongo } from 'meteor/mongo';
export const LivechatQueueUser = new Mongo.Collection('livechatQueueUser');

@ -532,19 +532,15 @@
}
}
.livechat-current-chats-add-filter-button {
margin-top: 17px;
}
.livechat-current-chats-filters-wrapper {
.livechat-group-filters-wrapper {
display: flex;
}
.livechat-current-chats-filters {
.livechat-group-filters-container {
flex: 8 0;
}
.livechat-current-chats-filters-buttons {
.livechat-group-filters-buttons {
flex: 0 0 auto;
margin-top: 26px;
padding-left: 30px;
@ -562,3 +558,8 @@
margin-bottom: auto;
}
}
.livechat-current-chats-add-filter-button {
margin-top: 17px;
}

@ -2,8 +2,8 @@
{{#requiresPermission 'view-livechat-current-chats'}}
<fieldset>
<form class="form-inline" method="post">
<div class="livechat-current-chats-filters-wrapper">
<div class="livechat-current-chats-filters">
<div class="livechat-group-filters-wrapper">
<div class="livechat-group-filters-container">
<div class="livechat-current-chats-standard-filters">
<div class="form-group">
<label class="rc-input__label">
@ -105,7 +105,7 @@
</div>
</div>
<div class="livechat-current-chats-filters-buttons">
<div class="livechat-group-filters-buttons">
<div class="rc-button__group">
<button class="rc-button rc-button--primary">{{_ "Filter"}}</button>
{{#if hasPopoverPermissions}}

@ -1,39 +1,113 @@
<template name="livechatQueue">
{{#if hasPermission}}
{{#each departments}}
<p class="queue-department">
{{_ "Department"}}: <strong>{{name}}</strong>
( <label for="show-offline-{{_id}}">{{_ "show_offline_users"}}</label>
<input type="checkbox" class="show-offline" id="show-offline-{{_id}}">)
</p>
<fieldset>
<form class="form-inline" method="post">
<div class="livechat-group-filters-wrapper">
<div class="livechat-group-filters-container">
<div class="livechat-queue-standard-filters">
<div class="form-group">
{{> livechatAutocompleteUser
onClickTag=onClickTagAgent
list=selectedAgents
onSelect=onSelectAgents
collection='UserAndRoom'
subscription='userAutocomplete'
field='username'
sort='username'
label="Served_By"
placeholder="Served_By"
name="agent"
icon="at"
noMatchTemplate="userSearchEmpty"
templateItem="popupList_item_default"
modifier=agentModifier
showLabel=true
}}
</div>
<div class="form-group">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Status"}}</div>
<div class="rc-select">
<select class="rc-select__element" name="agentStatus">
<option class="rc-select__option" value="">{{_ "Online"}}</option>
<option class="rc-select__option" value="offline">{{_ "Include_Offline_Agents"}}</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
<div class="form-group">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Department"}}</div>
<div class="rc-select">
<select class="rc-select__element" name="department">
<option class="rc-select__option" value="">{{_ "Select_a_department"}}</option>
{{#each departments}}
<option class="rc-select__option" value="{{_id}}">{{name}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
</div>
</div>
<div class="rocket-form">
<div class="list">
<table>
<thead>
<tr>
<th width="70%">{{_ "Agent"}}</th>
<th width="15%" class="text-right">{{_ "Count"}}</th>
<th width="15%" class="text-right">{{_ "Order"}}</th>
</tr>
</thead>
<tbody>
{{#each users}}
<tr>
<td>{{username}}</td>
<td class="text-right">{{count}}</td>
<td class="text-right">{{order}}</td>
</tr>
{{else}}
<tr>
<td colspan="3">{{_ "Nobody_available"}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="livechat-group-filters-buttons">
<div class="rc-button__group">
<button class="rc-button rc-button--primary">{{_ "Filter"}}</button>
</div>
</div>
</div>
{{/each}}
</form>
</fieldset>
<div class="rc-table-content">
{{#table fixed='true' onScroll=onTableScroll onResize=onTableResize onSort=onTableSort}}
<thead>
<tr>
<th width="25%">
<div class="table-fake-th">{{_ "Served_By"}}</div>
</th>
<th width="25%">
<div class="table-fake-th">{{_ "Department"}}</div>
</th>
<th width="25%">
<div class="table-fake-th">{{_ "Total"}}</div>
</th>
<th width="25%">
<div class="table-fake-th">{{_ "Status"}}</div>
</th>
</tr>
</thead>
<tbody>
{{#each queue}}
<tr class="rc-table-tr manage row-link" data-name="{{user.username}}">
<td>
<div class="rc-table-wrapper">
<div class="rc-table-avatar user-image status-{{status}}">{{> avatar username=user.username}}</div>
<div class="rc-table-info">
<span class="rc-table-title">{{user.username}}</span>
</div>
</div>
</td>
<td>{{department.name}}</td>
<td>{{chats}}</td>
<td>{{user.status}}</td>
</tr>
{{/each}}
{{#if isLoading}}
<tr class="table-no-click">
<td colspan="5" class="table-loading-td">{{> loading}}</td>
</tr>
{{/if}}
</tbody>
{{/table}}
</div>
{{#if hasMore}}
<div class="rc-button__group">
<button class="rc-button rc-button--primary js-load-more">{{_ "Load_more"}}</button>
</div>
{{/if}}
{{else}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{/if}}

@ -5,45 +5,45 @@ import { Template } from 'meteor/templating';
import { settings } from '../../../../settings';
import { hasPermission } from '../../../../authorization';
import { Users } from '../../../../models';
import { LivechatQueueUser } from '../../collections/LivechatQueueUser';
import './livechatQueue.html';
import { APIClient } from '../../../../utils/client';
const QUEUE_COUNT = 50;
Template.livechatQueue.helpers({
departments() {
return Template.instance().departments.get().filter((department) => department.enabled === true);
},
users() {
const users = [];
const showOffline = Template.instance().showOffline.get();
LivechatQueueUser.find({
departmentId: this._id,
}, {
sort: {
count: 1,
order: 1,
username: 1,
},
}).forEach((user) => {
const options = { fields: { _id: 1 } };
const userFilter = { _id: user.agentId, status: { $ne: 'offline' } };
const agent = Template.instance().agents.get().find((agent) => agent._id === user.agentId && agent.statusLivechat === 'available');
if (showOffline[this._id] || (Meteor.users.findOne(userFilter, options) && agent)) {
users.push(user);
}
});
return users;
onSelectAgents() {
return Template.instance().onSelectAgents;
},
agentModifier() {
return (filter, text = '') => {
const f = filter.get();
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`;
};
},
selectedAgents() {
return Template.instance().selectedAgents.get();
},
onClickTagAgent() {
return Template.instance().onClickTagAgent;
},
queue() {
return Template.instance().queue.get();
},
isLoading() {
return Template.instance().isLoading.get();
},
hasPermission() {
const user = Users.findOne(Meteor.userId(), { fields: { statusLivechat: 1 } });
return hasPermission(Meteor.userId(), 'view-livechat-queue') || (user.statusLivechat === 'available' && settings.get('Livechat_show_queue_list_link'));
},
hasMore() {
const instance = Template.instance();
const queue = instance.queue.get();
return instance.total.get() > queue.length;
},
});
Template.livechatQueue.events({
@ -54,17 +54,67 @@ Template.livechatQueue.events({
instance.showOffline.set(showOffline);
},
'submit form'(event, instance) {
event.preventDefault();
instance.queue.set([]);
instance.offset.set(0);
const filter = {};
$(':input', event.currentTarget).each(function() {
if (!this.name) {
return;
}
const value = $(this).val();
filter[this.name] = value;
});
const agents = instance.selectedAgents.get();
if (agents && agents.length > 0) {
filter.agent = agents[0]._id;
}
instance.filter.set(filter);
},
'click .js-load-more'(event, instance) {
instance.offset.set(instance.offset.get() + QUEUE_COUNT);
},
});
Template.livechatQueue.onCreated(async function() {
this.showOffline = new ReactiveVar({});
this.agents = new ReactiveVar([]);
this.selectedAgents = new ReactiveVar([]);
this.departments = new ReactiveVar([]);
this.limit = new ReactiveVar(20);
this.filter = new ReactiveVar({});
this.queue = new ReactiveVar([]);
this.isLoading = new ReactiveVar(true);
this.offset = new ReactiveVar(0);
this.total = new ReactiveVar(0);
this.subscribe('livechat:queue');
const { users } = await APIClient.v1.get('livechat/users/agent');
const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}');
this.onSelectAgents = ({ item: agent }) => {
this.selectedAgents.set([agent]);
};
this.agents.set(users);
this.onClickTagAgent = ({ username }) => {
this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username));
};
this.autorun(async () => {
this.isLoading.set(true);
const filter = this.filter.get();
const offset = this.offset.get();
let query = `includeOfflineAgents=${ filter.agentStatus === 'offline' }`;
if (filter.agent) {
query += `&agentId=${ filter.agent }`;
}
if (filter.department) {
query += `&departmentId=${ filter.department }`;
}
const { queue, total } = await APIClient.v1.get(`livechat/queue?${ query }&count=${ QUEUE_COUNT }&offset=${ offset }`);
this.total.set(total);
this.queue.set(this.queue.get().concat(queue));
this.isLoading.set(false);
});
const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}');
this.departments.set(departments);
});

@ -0,0 +1,23 @@
import { API } from '../../../../api';
import { findQueueMetrics } from '../../../server/api/lib/queue';
API.v1.addRoute('livechat/queue', { authRequired: true }, {
get() {
const { offset, count } = this.getPaginationItems();
const { sort } = this.parseJsonQuery();
const { agentId, includeOfflineAgents, departmentId } = this.requestParams();
const users = Promise.await(findQueueMetrics({
userId: this.userId,
agentId,
includeOfflineAgents: includeOfflineAgents === 'true',
departmentId,
pagination: {
offset,
count,
sort,
},
}));
return API.v1.success(users);
},
});

@ -11,3 +11,4 @@ import '../imports/server/rest/triggers.js';
import '../imports/server/rest/integrations.js';
import '../imports/server/rest/messages.js';
import '../imports/server/rest/visitors.js';
import '../imports/server/rest/queue.js';

@ -0,0 +1,25 @@
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
import { LivechatRooms } from '../../../../models/server/raw';
export async function findQueueMetrics({ userId, agentId, includeOfflineAgents, departmentId, pagination: { offset, count, sort } }) {
if (!await hasPermissionAsync(userId, 'view-l-room')) {
throw new Error('error-not-authorized');
}
const queue = await LivechatRooms.getQueueMetrics({ departmentId,
agentId,
includeOfflineAgents,
options: {
sort: sort || { chats: 1 },
offset,
count,
} });
const total = (await LivechatRooms.getQueueMetrics({ departmentId, agentId, includeOfflineAgents })).length;
return {
queue,
count: queue.length,
offset,
total,
};
}

@ -4,6 +4,7 @@ import { hasPermission } from '../../../authorization';
import { LivechatDepartmentAgents } from '../../../models';
Meteor.publish('livechat:queue', function() {
console.warn('The publication "livechat:queue" is deprecated and will be removed after version v3.0.0');
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
}
@ -11,24 +12,6 @@ Meteor.publish('livechat:queue', function() {
if (!hasPermission(this.userId, 'view-l-room')) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
}
// let sort = { count: 1, sort: 1, username: 1 };
// let onlineUsers = {};
// let handleUsers = RocketChat.models.Users.findOnlineAgents().observeChanges({
// added(id, fields) {
// onlineUsers[fields.username] = 1;
// // this.added('livechatQueueUser', id, fields);
// },
// changed(id, fields) {
// onlineUsers[fields.username] = 1;
// // this.changed('livechatQueueUser', id, fields);
// },
// removed(id) {
// this.removed('livechatQueueUser', id);
// }
// });
const self = this;
const handleDepts = LivechatDepartmentAgents.findUsersInQueue().observeChanges({
@ -46,7 +29,6 @@ Meteor.publish('livechat:queue', function() {
this.ready();
this.onStop(() => {
// handleUsers.stop();
handleDepts.stop();
});
});

@ -1,5 +1,14 @@
import { BaseRaw } from './BaseRaw';
export class LivechatDepartmentAgentsRaw extends BaseRaw {
findUsersInQueue(usersList, options) {
const query = {};
if (Array.isArray(usersList) && usersList.length) {
query.username = {
$in: usersList,
};
}
return this.find(query, options);
}
}

@ -1,5 +1,97 @@
import { BaseRaw } from './BaseRaw';
export class LivechatRoomsRaw extends BaseRaw {
getQueueMetrics({ departmentId, agentId, includeOfflineAgents, options = {} }) {
const match = { $match: { t: 'l', open: true, servedBy: { $exists: true } } };
const matchUsers = { $match: {} };
if (departmentId) {
match.$match.departmentId = departmentId;
}
if (agentId) {
matchUsers.$match['user._id'] = agentId;
}
if (!includeOfflineAgents) {
matchUsers.$match['user.status'] = { $ne: 'offline' };
matchUsers.$match['user.statusLivechat'] = { $eq: 'available' };
}
const departmentsLookup = {
$lookup: {
from: 'rocketchat_livechat_department',
localField: 'departmentId',
foreignField: '_id',
as: 'departments',
},
};
const departmentsUnwind = {
$unwind: {
path: '$departments',
preserveNullAndEmptyArrays: true,
},
};
const departmentsGroup = {
$group: {
_id: {
departmentId: '$departmentId',
name: '$departments.name',
room: '$$ROOT',
},
},
};
const usersLookup = {
$lookup: {
from: 'users',
localField: '_id.room.servedBy._id',
foreignField: '_id',
as: 'user',
},
};
const usersUnwind = {
$unwind: {
path: '$user',
preserveNullAndEmptyArrays: true,
},
};
const usersGroup = {
$group: {
_id: {
userId: '$user._id',
username: '$user.username',
status: '$user.status',
departmentId: '$_id.departmentId',
departmentName: '$_id.name',
},
chats: { $sum: 1 },
},
};
const project = {
$project: {
_id: 0,
user: {
_id: '$_id.userId',
username: '$_id.username',
status: '$_id.status',
},
department: {
_id: { $ifNull: ['$_id.departmentId', null] },
name: { $ifNull: ['$_id.departmentName', null] },
},
chats: 1,
},
};
const firstParams = [match, departmentsLookup, departmentsUnwind, departmentsGroup, usersLookup, usersUnwind];
if (Object.keys(matchUsers.$match)) {
firstParams.push(matchUsers);
}
const params = [...firstParams, usersGroup, project];
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
}

@ -1668,6 +1668,7 @@
"Inclusive": "Inclusive",
"Incoming_Livechats": "Incoming Livechats",
"Incoming_WebHook": "Incoming WebHook",
"Include_Offline_Agents": "Include offline agents",
"Industry": "Industry",
"Info": "Info",
"initials_avatar": "Initials Avatar",

@ -1565,6 +1565,7 @@
"Inclusive": "Inclusive",
"Incoming_Livechats": "Livechats entrantes",
"Incoming_WebHook": "WebHook entrante",
"Include_Offline_Agents": "Incluir agentes offline",
"Industry": "Indústria",
"Info": "Informações",
"initials_avatar": "Iniciais Avatar",

@ -0,0 +1,46 @@
import { getCredentials, api, request, credentials } from '../../../data/api-data.js';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
describe('LIVECHAT - Queue', function() {
this.retries(0);
before((done) => getCredentials(done));
before((done) => {
updateSetting('Livechat_enabled', true).then(done);
});
describe('livechat/queue', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-l-room', [])
.then(() => {
request.get(api('livechat/queue'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('error-not-authorized');
})
.end(done);
});
});
it('should return an array of queued metrics', (done) => {
updatePermission('view-l-room', ['admin'])
.then(() => {
request.get(api('livechat/queue'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.queue).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
})
.end(done);
});
});
});
});
Loading…
Cancel
Save