Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Add default chat closing tags in Omnichannel departments #16859

Merged
merged 5 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions app/livechat/client/views/app/livechatDepartmentForm.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,50 @@
<label><input type="radio" name="requestTagBeforeClosingChat" value="0" checked="{{$eq requestTagBeforeClosingChat false}}" /> {{_ "No"}}</label>
</div>
</div>
<label>{{_ "Conversation_closing_tags"}}</label>
<div class="input-line form-inline">
<div class="form-group">
{{#if hasAvailableTags}}
<div class="rc-input__wrapper">
<select id="tagSelect" class="rc-input rc-input__element rc-input--small rc-form-item-inline">
<option value="placeholder" disabled selected>{{_ "Select_tag"}}</option>
{{#each availableDepartmentTags}}
<option value="{{_id}}">{{this}}</option>
{{/each}}
</select>
</div>
{{else}}
<div class="rc-input" id="add-tag-input">
<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="{{_"Enter_a_tag"}}">
</div>
</label>
</div>
{{/if}}
</div>
<div class="form-group">
<button id="addTag" name="addTag" class="rc-button rc-button--primary add-tag">{{_ "Add"}}</button>
</div>
<div>
<small class="secondary-font-color">{{{_ "Conversation_closing_tags_description"}}}</small>
</div>
</div>
{{#if hasChatClosingTags}}
<div class="input-line">
<ul id="tags" class="chip-container department-fallback-tags">
{{#each chatClosingTags}}
<li class="remove-tag" title="{{this}}">
<i class="icon icon-cancel-circled"></i>
{{this}}
</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#if customFieldsTemplate}}
{{> Template.dynamic template=customFieldsTemplate data=data }}
{{/if}}
Expand Down
60 changes: 58 additions & 2 deletions app/livechat/client/views/app/livechatDepartmentForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ Template.livechatDepartmentForm.helpers({
.some((button) => button.groups
.some((group) => group.startsWith('livechat-department')));
},
chatClosingTags() {
return Template.instance().chatClosingTags.get();
},
availableDepartmentTags() {
return Template.instance().availableDepartmentTags.get();
},
hasAvailableTags() {
return [...Template.instance().availableTags.get()].length > 0;
},
hasChatClosingTags() {
return [...Template.instance().chatClosingTags.get()].length > 0;
},
});

Template.livechatDepartmentForm.events({
Expand All @@ -98,7 +110,7 @@ Template.livechatDepartmentForm.events({
const email = instance.$('input[name=email]').val();
const showOnOfflineForm = instance.$('input[name=showOnOfflineForm]:checked').val();
const requestTagBeforeClosingChat = instance.$('input[name=requestTagBeforeClosingChat]:checked').val();

const chatClosingTags = instance.chatClosingTags.get();
if (enabled !== '1' && enabled !== '0') {
return toastr.error(t('Please_select_enabled_yes_or_no'));
}
Expand All @@ -119,6 +131,7 @@ Template.livechatDepartmentForm.events({
showOnOfflineForm: showOnOfflineForm === '1',
requestTagBeforeClosingChat: requestTagBeforeClosingChat === '1',
email: email.trim(),
chatClosingTags,
};
}

Expand Down Expand Up @@ -190,6 +203,33 @@ Template.livechatDepartmentForm.events({

instance.departmentAgents.set(instance.departmentAgents.get().filter((agent) => agent.agentId !== this.agentId));
},

'click #addTag'(e, instance) {
e.stopPropagation();
e.preventDefault();

const isSelect = [...instance.availableTags.get()].length > 0;
const elId = isSelect ? '#tagSelect' : '#tagInput';
const elDefault = isSelect ? 'placeholder' : '';

const tag = $(elId).val();
const chatClosingTags = [...instance.chatClosingTags.get()];
if (tag === '' || chatClosingTags.indexOf(tag) > -1) {
return;
}

chatClosingTags.push(tag);
instance.chatClosingTags.set(chatClosingTags);
$(elId).val(elDefault);
},

'click .remove-tag'(e, instance) {
e.stopPropagation();
e.preventDefault();

const chatClosingTags = [...instance.chatClosingTags.get()].filter((el) => el !== this.valueOf());
instance.chatClosingTags.set(chatClosingTags);
},
});

Template.livechatDepartmentForm.onCreated(async function() {
Expand All @@ -199,21 +239,37 @@ Template.livechatDepartmentForm.onCreated(async function() {
this.tabBar = new RocketChatTabBar();
this.tabBar.showGroup(FlowRouter.current().route.name);
this.tabBarData = new ReactiveVar();
this.chatClosingTags = new ReactiveVar([]);
this.availableTags = new ReactiveVar([]);
this.availableDepartmentTags = new ReactiveVar([]);

this.onSelectAgents = ({ item: agent }) => {
this.selectedAgents.set([agent]);
};

this.onClickTagAgent = ({ username }) => {
this.onClickTagAgents = ({ username }) => {
this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username));
};

this.loadAvailableTags = (departmentId) => {
Meteor.call('livechat:getTagsList', (err, tagsList) => {
this.availableTags.set(tagsList || []);
const tags = this.availableTags.get();
const availableTags = tags
.filter(({ departments }) => departments.length === 0 || departments.indexOf(departmentId) > -1)
.map(({ name }) => name);
this.availableDepartmentTags.set(availableTags);
});
};

this.autorun(async () => {
const id = FlowRouter.getParam('_id');
if (id) {
const { department, agents } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }`);
this.department.set(department);
this.departmentAgents.set(agents);
this.chatClosingTags.set((department && department.chatClosingTags) || []);
this.loadAvailableTags(id);
}
});
});
2 changes: 1 addition & 1 deletion app/livechat/client/views/app/tabbar/visitorInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ Template.visitorInfo.events({
'click .close-livechat'(event) {
event.preventDefault();

const closeRoom = (comment) => Meteor.call('livechat:closeRoom', this.rid, comment, function(error/* , result*/) {
const closeRoom = (comment) => Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true }, function(error/* , result*/) {
if (error) {
return handleError(error);
}
Expand Down
30 changes: 22 additions & 8 deletions app/livechat/server/hooks/beforeCloseRoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,34 @@ import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../callbacks';
import { LivechatDepartment } from '../../../models';

callbacks.add('livechat.beforeCloseRoom', (room) => {
const { departmentId } = room;
const concatUnique = (...arrays) => [...new Set([].concat(...arrays.filter(Array.isArray)))];

callbacks.add('livechat.beforeCloseRoom', ({ room, options }) => {
const { departmentId, tags: roomTags } = room;
if (!departmentId) {
return room;
return;
}

const department = LivechatDepartment.findOneById(departmentId);
if (!department || !department.requestTagBeforeClosingChat) {
return room;
if (!department) {
return;
}

const { requestTagBeforeClosingChat, chatClosingTags } = department;
const extraData = {
tags: concatUnique(roomTags, chatClosingTags),
};

if (!requestTagBeforeClosingChat) {
return extraData;
}

if (room.tags && room.tags.length > 0) {
return room;
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' });
}

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;
}, callbacks.priority.HIGH, 'livechat-before-close-Room');
12 changes: 10 additions & 2 deletions app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,18 +314,19 @@ export const Livechat = {
return ret;
},

closeRoom({ user, visitor, room, comment }) {
closeRoom({ user, visitor, room, comment, options = {} }) {
if (!room || room.t !== 'l' || !room.open) {
return false;
}

callbacks.run('livechat.beforeCloseRoom', room);
const extraData = callbacks.run('livechat.beforeCloseRoom', { room, options });

const now = new Date();

const closeData = {
closedAt: now,
chatDuration: (now.getTime() - room.ts) / 1000,
...extraData,
};

if (user) {
Expand Down Expand Up @@ -807,6 +808,8 @@ export const Livechat = {
showOnRegistration: Boolean,
email: String,
showOnOfflineForm: Boolean,
requestTagBeforeClosingChat: Match.Optional(Boolean),
chatClosingTags: Match.Optional([String]),
};

// The Livechat Form department support addition/custom fields, so those fields need to be added before validating
Expand All @@ -825,6 +828,11 @@ export const Livechat = {
}),
]));

const { requestTagBeforeClosingChat, chatClosingTags } = 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) {
Expand Down
3 changes: 2 additions & 1 deletion app/livechat/server/methods/closeRoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Subscriptions, LivechatRooms } from '../../../models';
import { Livechat } from '../lib/Livechat';

Meteor.methods({
'livechat:closeRoom'(roomId, comment) {
'livechat:closeRoom'(roomId, comment, options = {}) {
const userId = Meteor.userId();
if (!userId || !hasPermission(userId, 'close-livechat-room')) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:closeRoom' });
Expand All @@ -31,6 +31,7 @@ Meteor.methods({
user,
room: LivechatRooms.findOneById(roomId),
comment,
options,
});
},
});
11 changes: 7 additions & 4 deletions app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,16 +421,19 @@ export class LivechatRooms extends Base {


closeByRoomId(roomId, closeInfo) {
const { closer, closedBy, closedAt, chatDuration, ...extraData } = closeInfo;

return this.update({
_id: roomId,
t: 'l',
}, {
$set: {
closer: closeInfo.closer,
closedBy: closeInfo.closedBy,
closedAt: closeInfo.closedAt,
'metrics.chatDuration': closeInfo.chatDuration,
closer,
closedBy,
closedAt,
'metrics.chatDuration': chatDuration,
'v.status': 'offline',
...extraData,
},
$unset: {
open: 1,
Expand Down
3 changes: 3 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,8 @@
"Conversation": "Conversation",
"Conversations": "Conversations",
"Conversation_closed": "Conversation closed: __comment__.",
"Conversation_closing_tags": "Conversation closing tags",
"Conversation_closing_tags_description": "Closing tags will be automatically assigned to conversations at closing.",
"Conversation_finished": "Conversation Finished",
"Conversation_finished_message": "Conversation Finished Message",
"Conversation_finished_text": "Conversation Finished Text",
Expand Down Expand Up @@ -1402,6 +1404,7 @@
"error-logged-user-not-in-room": "You are not in the room `%s`",
"error-user-registration-disabled": "User registration is disabled",
"error-user-registration-secret": "User registration is only allowed via Secret URL",
"error-validating-department-chat-closing-tags": "At least one closing tag is required when the department requires tag(s) on closing conversations.",
"error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.",
"error-starring-message": "Message could not be stared",
"Error_404": "Error:404",
Expand Down
3 changes: 3 additions & 0 deletions packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@
"Conversation": "Conversa",
"Conversations": "Conversas",
"Conversation_closed": "Chat encerrado: __comment__.",
"Conversation_closing_tags": "Tags de encerramento de conversa",
"Conversation_closing_tags_description": "As tags de encerramento serão automaticamente atribuídas as conversas no seu encerramento.",
"Conversation_finished": "Conversa concluída",
"Conversation_finished_message": "Mensagem de conversa concluída",
"Conversation_finished_text": "Texto de conversa concluída",
Expand Down Expand Up @@ -1303,6 +1305,7 @@
"error-logged-user-not-in-room": "Você não está na sala `%s`",
"error-user-registration-disabled": "O registro do usuário está desativado",
"error-user-registration-secret": "O registro de usuário é permitido somente via URL secreta",
"error-validating-department-chat-closing-tags": "Pelo menos uma tag de encerramento é necessária quando o departamento exige tags no encerramento de conversas.",
"error-you-are-last-owner": "Você é o último proprietário da sala. Por favor defina um novo proprietário antes de sair.",
"Error_404": "Erro 404",
"Error_changing_password": "Erro ao alterar senha",
Expand Down