diff --git a/app/livechat/client/index.js b/app/livechat/client/index.js index 4df9fcd92ed..26013edf70a 100644 --- a/app/livechat/client/index.js +++ b/app/livechat/client/index.js @@ -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'; diff --git a/app/livechat/client/views/app/dialog/closeRoom.html b/app/livechat/client/views/app/dialog/closeRoom.html new file mode 100644 index 00000000000..0cb20ad48d8 --- /dev/null +++ b/app/livechat/client/views/app/dialog/closeRoom.html @@ -0,0 +1,72 @@ + diff --git a/app/livechat/client/views/app/dialog/closeRoom.js b/app/livechat/client/views/app/dialog/closeRoom.js new file mode 100644 index 00000000000..2dac3bc7956 --- /dev/null +++ b/app/livechat/client/views/app/dialog/closeRoom.js @@ -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); + }); +}); diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 24de8917bce..d33cbfc0c05 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -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, }); }, diff --git a/app/livechat/server/hooks/beforeCloseRoom.js b/app/livechat/server/hooks/beforeCloseRoom.js index de4f13a7f8c..6c819c23e2b 100644 --- a/app/livechat/server/hooks/beforeCloseRoom.js +++ b/app/livechat/server/hooks/beforeCloseRoom.js @@ -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'); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 323dd80b8a1..1438e24b940 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -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; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 4f48e53d8e2..ef3de482664 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -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", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 9792f3c1f07..22ed0b99fea 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -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",