[NEW] Add ability to set tags in the Omnichannel room closing dialog (#17254)

pull/17141/head
Renato Becker 6 years ago committed by GitHub
parent 545e03a07d
commit c21bfd442d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/livechat/client/index.js
  2. 72
      app/livechat/client/views/app/dialog/closeRoom.html
  3. 190
      app/livechat/client/views/app/dialog/closeRoom.js
  4. 67
      app/livechat/client/views/app/tabbar/visitorInfo.js
  5. 23
      app/livechat/server/hooks/beforeCloseRoom.js
  6. 3
      app/livechat/server/lib/Livechat.js
  7. 1
      packages/rocketchat-i18n/i18n/en.i18n.json
  8. 1
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json

@ -4,6 +4,7 @@ import './route';
import './ui';
import './hooks/onCreateRoomTabBar';
import './startup/notifyUnreadRooms';
import './views/app/dialog/closeRoom';
import './stylesheets/livechat.css';
import './views/sideNav/livechat';
import './views/sideNav/livechatFlex';

@ -0,0 +1,72 @@
<template name="closeRoom">
<section class="create-channel">
<div class="create-channel__wrapper">
<p class="create-channel__description">{{_ "Close_room_description"}}</p>
<form id="close-room" class="close-room__content">
<div class="create-channel__inputs">
<div class="rc-input {{#if invalidComment}}rc-input--error{{/if}}">
<label class="rc-input__label">
<div class="rc-input__wrapper">
<input name="comment" id="comment" type="text" class="rc-input__element"
placeholder="{{_ "Please_add_a_comment"}}" autofocus>
</div>
</label>
{{#if invalidComment}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{_ "Please_add_a_comment_to_close_the_room"}}</div>
</div>
{{/if}}
</div>
<div class="rc-input {{#if invalidTags}}rc-input--error{{/if}}">
{{#if hasAvailableTags}}
<div class="rc-form-group rc-form-group--small rc-form-group--inline">
<select id="tagSelect" class="rc-input rc-input--small rc-form-item-inline">
<option value="placeholder" disabled selected>{{_ "Select_tag"}}</option>
{{#each availableUserTags}}
<option value="{{_id}}">{{this}}</option>
{{/each}}
</select>
<button id="addTag" class="rc-button rc-button--primary rc-form-item-inline"><i class="icon-plus"></i></button>
</div>
{{else}}
<label class="rc-input__label">
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon icon='edit' }}
</div>
<input id="tagInput" class="rc-input__element" type="text" name="tags" autocomplete="off" placeholder="{{tagsPlaceHolder}}">
</div>
</label>
{{/if}}
{{#if invalidTags}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{_ "error-tags-must-be-assigned-before-closing-chat"}}</div>
</div>
{{/if}}
</div>
<div class="rc-form-group rc-form-group--small">
<ul id="tags" class="chip-container current-room-tags">
{{#each tags}}
<li class="remove-tag" title="{{this}}">
{{#if canRemoveTag availableUserTags this}}
<i class="icon icon-cancel-circled"></i>
{{/if}}
{{this}}
</li>
{{/each}}
</ul>
</div>
</div>
</form>
<div class="rc-input">
<button form="close-room" class="rc-button rc-button--primary js-close-room">{{_ " Confirm "}}</button>
</div>
</div>
</section>
</template>

@ -0,0 +1,190 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { settings } from '../../../../../settings';
import { modal } from '../../../../../ui-utils/client';
import { APIClient, handleError, t } from '../../../../../utils';
import { hasRole } from '../../../../../authorization';
import './closeRoom.html';
const validateRoomComment = (comment) => {
if (!settings.get('Livechat_request_comment_when_closing_conversation')) {
return true;
}
return comment?.length > 0;
};
const validateRoomTags = (tagsRequired, tags) => {
if (!tagsRequired) {
return true;
}
return tags?.length > 0;
};
const checkUserTagPermission = (availableUserTags = [], tag) => {
if (hasRole(Meteor.userId(), ['admin', 'livechat-manager'])) {
return true;
}
return availableUserTags.includes(tag);
};
Template.closeRoom.helpers({
invalidComment() {
return Template.instance().invalidComment.get();
},
tags() {
return Template.instance().tags.get();
},
invalidTags() {
return Template.instance().invalidTags.get();
},
availableUserTags() {
return Template.instance().availableUserTags.get();
},
tagsPlaceHolder() {
let placeholder = TAPi18n.__('Enter_a_tag');
if (!Template.instance().tagsRequired.get()) {
placeholder = placeholder.concat(`(${ TAPi18n.__('Optional') })`);
}
return placeholder;
},
hasAvailableTags() {
const tags = Template.instance().availableTags.get();
return tags?.length > 0;
},
canRemoveTag(availableUserTags, tag) {
return checkUserTagPermission(availableUserTags, tag);
},
});
Template.closeRoom.events({
async 'submit .close-room__content'(e, instance) {
e.preventDefault();
e.stopPropagation();
const comment = instance.$('#comment').val();
instance.invalidComment.set(!validateRoomComment(comment));
if (instance.invalidComment.get()) {
return;
}
const tagsRequired = instance.tagsRequired.get();
const tags = instance.tags.get();
instance.invalidTags.set(!validateRoomTags(tagsRequired, tags));
if (instance.invalidTags.get()) {
return;
}
Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true, tags }, function(error/* , result*/) {
if (error) {
console.log(error);
return handleError(error);
}
modal.open({
title: t('Chat_closed'),
text: t('Chat_closed_successfully'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
});
},
'click .remove-tag'(e, instance) {
e.stopPropagation();
e.preventDefault();
const tag = this.valueOf();
const availableTags = instance.availableTags.get();
const hasAvailableTags = availableTags?.length > 0;
const availableUserTags = instance.availableUserTags.get();
if (hasAvailableTags && !checkUserTagPermission(availableUserTags, tag)) {
return;
}
let tags = instance.tags.get();
tags = tags.filter((el) => el !== tag);
instance.tags.set(tags);
},
'click #addTag'(e, instance) {
e.stopPropagation();
e.preventDefault();
if ($('#tagSelect').find(':selected').is(':disabled')) {
return;
}
const tags = [...instance.tags.get()];
const tagVal = $('#tagSelect').val();
if (tagVal === '' || tags.includes(tagVal)) {
return;
}
tags.push(tagVal);
instance.tags.set(tags);
$('#tagSelect').val('placeholder');
},
'keydown #tagInput'(e, instance) {
if (e.which === 13) {
e.stopPropagation();
e.preventDefault();
const tags = [...instance.tags.get()];
const tagVal = $('#tagInput').val();
if (tagVal === '' || tags.includes(tagVal)) {
return;
}
tags.push(tagVal);
instance.tags.set(tags);
$('#tagInput').val('');
}
},
});
Template.closeRoom.onRendered(function() {
this.find('#comment').focus();
});
Template.closeRoom.onCreated(async function() {
this.tags = new ReactiveVar([]);
this.invalidComment = new ReactiveVar(false);
this.invalidTags = new ReactiveVar(false);
this.tagsRequired = new ReactiveVar(false);
this.availableTags = new ReactiveVar([]);
this.availableUserTags = new ReactiveVar([]);
this.agentDepartments = new ReactiveVar([]);
this.onEnterTag = () => this.invalidTags.set(!validateRoomTags(this.tagsRequired.get(), this.tags.get()));
const { rid } = Template.currentData();
const { room } = await APIClient.v1.get(`rooms.info?roomId=${ rid }`);
this.tags.set(room?.tags || []);
if (room?.departmentId) {
const { department } = await APIClient.v1.get(`livechat/department/${ room.departmentId }?includeAgents=false`);
this.tagsRequired.set(department?.requestTagBeforeClosingChat);
}
const uid = Meteor.userId();
const { departments } = await APIClient.v1.get(`livechat/agents/${ uid }/departments`);
const agentDepartments = departments.map((dept) => dept.departmentId);
this.agentDepartments.set(agentDepartments);
Meteor.call('livechat:getTagsList', (err, tagsList) => {
this.availableTags.set(tagsList);
const isAdmin = hasRole(uid, ['admin', 'livechat-manager']);
const availableTags = tagsList
.filter(({ departments }) => isAdmin || (departments.length === 0 || departments.some((i) => agentDepartments.includes(i))))
.map(({ name }) => name);
this.availableUserTags.set(availableTags);
});
});

@ -1,11 +1,10 @@
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import s from 'underscore.string';
import moment from 'moment';
import UAParser from 'ua-parser-js';
@ -29,6 +28,14 @@ const isSubscribedToRoom = () => {
return subscription !== undefined;
};
const closingDialogRequired = (department) => {
if (settings.get('Livechat_request_comment_when_closing_conversation')) {
return true;
}
return department && department.requestTagBeforeClosingChat;
};
Template.visitorInfo.helpers({
user() {
const user = Template.instance().user.get();
@ -228,46 +235,36 @@ Template.visitorInfo.events({
instance.action.set('edit');
},
'click .close-livechat'(event) {
'click .close-livechat'(event, instance) {
event.preventDefault();
const closeRoom = (comment) => Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true }, function(error/* , result*/) {
if (error) {
return handleError(error);
}
modal.open({
title: t('Chat_closed'),
text: t('Chat_closed_successfully'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
});
if (!settings.get('Livechat_request_comment_when_closing_conversation')) {
if (!closingDialogRequired(instance.department.get())) {
const comment = TAPi18n.__('Chat_closed_by_agent');
return closeRoom(comment);
return Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true }, function(error/* , result*/) {
if (error) {
return handleError(error);
}
modal.open({
title: t('Chat_closed'),
text: t('Chat_closed_successfully'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
});
}
// Setting for Ask_for_conversation_finished_message is set to true
modal.open({
title: t('Closing_chat'),
type: 'input',
inputPlaceholder: t('Please_add_a_comment'),
showCancelButton: true,
closeOnConfirm: false,
}, (inputValue) => {
if (!inputValue) {
modal.showInputError(t('Please_add_a_comment_to_close_the_room'));
return false;
}
if (s.trim(inputValue) === '') {
modal.showInputError(t('Please_add_a_comment_to_close_the_room'));
return false;
}
return closeRoom(inputValue);
modifier: 'modal',
content: 'closeRoom',
data: {
rid: this.rid,
},
confirmOnEnter: false,
showConfirmButton: false,
showCancelButton: false,
});
},

@ -5,32 +5,35 @@ import { LivechatDepartment } from '../../../models';
const concatUnique = (...arrays) => [...new Set([].concat(...arrays.filter(Array.isArray)))];
callbacks.add('livechat.beforeCloseRoom', ({ room, options }) => {
const { departmentId, tags: roomTags } = room;
const normalizeParams = (params, tags = []) => Object.assign(params, { extraData: { tags } });
callbacks.add('livechat.beforeCloseRoom', (originalParams = {}) => {
const { room, options } = originalParams;
const { departmentId, tags: optionsTags } = room;
const { clientAction, tags: oldRoomTags } = options;
const roomTags = concatUnique(oldRoomTags, optionsTags);
if (!departmentId) {
return;
return normalizeParams({ ...originalParams }, roomTags);
}
const department = LivechatDepartment.findOneById(departmentId);
if (!department) {
return;
return normalizeParams({ ...originalParams }, roomTags);
}
const { requestTagBeforeClosingChat, chatClosingTags } = department;
const extraData = {
tags: concatUnique(roomTags, chatClosingTags),
};
const extraRoomTags = concatUnique(roomTags, chatClosingTags);
if (!requestTagBeforeClosingChat) {
return extraData;
return normalizeParams({ ...originalParams }, extraRoomTags);
}
const { clientAction } = options;
const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0);
const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0;
if (!checkRoomTags || !checkDepartmentTags) {
throw new Meteor.Error('error-tags-must-be-assigned-before-closing-chat', 'Tag(s) must be assigned before closing the chat', { method: 'livechat.beforeCloseRoom' });
}
return extraData;
return normalizeParams({ ...originalParams }, extraRoomTags);
}, callbacks.priority.HIGH, 'livechat-before-close-Room');

@ -320,7 +320,8 @@ export const Livechat = {
return false;
}
const extraData = callbacks.run('livechat.beforeCloseRoom', { room, options });
const params = callbacks.run('livechat.beforeCloseRoom', { room, options });
const { extraData } = params;
const now = new Date();
const { _id: rid, servedBy } = room;

@ -692,6 +692,7 @@
"close-others-livechat-room": "Close other Omnichannel Room",
"Cloud_workspace_connected_without_account": "Your workspace is now connected to the Rocket.Chat Cloud. If you would like, you can login to the Rocket.Chat Cloud and associate your workspace with your Cloud account.",
"close-others-livechat-room_description": "Permission to close other Omnichannel rooms",
"Close_room_description" : "You are about to close this chat. Are you sure you want to continue?",
"Closed": "Closed",
"Closed_At": "Closed at",
"Closed_by_visitor": "Closed by visitor",

@ -670,6 +670,7 @@
"close-others-livechat-room": "Sala de Omnichannel fechada",
"Cloud_workspace_connected_without_account": "Seu workspace está agora conectado ao Rocket.Chat Cloud. Se desejar, você pode fazer o login no Rocket.Chat Cloud e associar seu workspace à sua conta do Cloud.",
"close-others-livechat-room_description": "Permissão para fechar outras salas de Omnichannel",
"Close_room_description" : "Você está prestes a fechar este bate-papo. Você tem certeza que quer continuar?",
"Closed": "Fechado",
"Closed_At": "Encerrado em",
"Closed_by_visitor": "Encerrado pelo visitante",

Loading…
Cancel
Save