Refactor: Omnichannel departments (#18920)
Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat> Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>pull/19181/head
parent
f9b88c6217
commit
379048074a
@ -1,223 +0,0 @@ |
||||
<template name="livechatDepartmentForm"> |
||||
{{#requiresPermission 'view-livechat-departments'}} |
||||
<div class="main-content-flex"> |
||||
<section class="page-container flex-tab-main-content"> |
||||
{{> header sectionName=i18nPageTitle}} |
||||
<div class="content"> |
||||
<form id="department-form" data-id="{{department._id}}"> |
||||
<div class="rocket-form"> |
||||
{{#if Template.subscriptionsReady}} |
||||
<fieldset> |
||||
{{#requiresPermission 'manage-livechat-departments'}} |
||||
<div class="input-line"> |
||||
<label>{{_ "Enabled"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="enabled" value="1" checked="{{$eq department.enabled true}}" /> {{_ "Yes"}}</label> |
||||
<label><input type="radio" name="enabled" value="0" checked="{{$eq department.enabled false}}" /> {{_ "No"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Name"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="name" value="{{department.name}}" placeholder="{{_ "Name"}}" /> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Description"}}</label> |
||||
<div> |
||||
<textarea name="description" class="rc-input__element" rows="6">{{department.description}}</textarea> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Show_on_registration_page"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="showOnRegistration" value="1" checked="{{showOnRegistration true}}" /> {{_ "Yes"}}</label> |
||||
<label><input type="radio" name="showOnRegistration" value="0" checked="{{showOnRegistration false}}" /> {{_ "No"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Email"}}</label> |
||||
<div> |
||||
<input type="email" class="rc-input__element" name="email" value="{{department.email}}" placeholder="{{_ "Email"}}" /> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Show_on_offline_page"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="showOnOfflineForm" value="1" checked="{{showOnOfflineForm true}}" /> {{_ "Yes"}}</label> |
||||
<label><input type="radio" name="showOnOfflineForm" value="0" checked="{{showOnOfflineForm false}}" /> {{_ "No"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
{{> livechatAutocompleteUser |
||||
onClickTag=onClickTagOfflineMessageChannel |
||||
list=selectedOfflineMessageChannel |
||||
onSelect=onSelectOfflineMessageChannel |
||||
collection='CachedChannelList' |
||||
endpoint='rooms.autocomplete.channelAndPrivate' |
||||
field='name' |
||||
sort='name' |
||||
label="Livechat_DepartmentOfflineMessageToChannel" |
||||
placeholder="Channel_name" |
||||
name="offlineMessageChannelName" |
||||
noMatchTemplate="roomSearchEmpty" |
||||
templateItem="popupList_item_channel" |
||||
modifier=offlineMessageChannelModifier |
||||
showLabel=true |
||||
selector=channelSelector |
||||
}} |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Request_tag_before_closing_chat"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="requestTagBeforeClosingChat" value="1" checked="{{$eq requestTagBeforeClosingChat true}}" /> {{_ "Yes"}}</label> |
||||
<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}} |
||||
|
||||
{{#requiresPermission 'add-livechat-department-agents'}} |
||||
<hr /> |
||||
{{else}} |
||||
{{/requiresPermission}} |
||||
{{else}} |
||||
<legend>{{department.name}}</legend> |
||||
{{/requiresPermission}} |
||||
|
||||
{{#requiresPermission 'add-livechat-department-agents'}} |
||||
<h2>{{_ "Agents"}}</h2> |
||||
|
||||
<fieldset> |
||||
<label>{{_ "Add_agent"}}</label> |
||||
<div class="input-line form-inline"> |
||||
<div class="form-group"> |
||||
{{> livechatAutocompleteUser |
||||
onClickTag=onClickTagAgents |
||||
list=selectedAgents |
||||
onSelect=onSelectAgents |
||||
collection='UserAndRoom' |
||||
endpoint='users.autocomplete' |
||||
field='username' |
||||
sort='username' |
||||
label="Search_by_username" |
||||
placeholder="Search_by_username" |
||||
name="username" |
||||
exceptions=exceptionsAgents |
||||
icon="at" |
||||
noMatchTemplate="userSearchEmpty" |
||||
templateItem="popupList_item_default" |
||||
modifier=agentModifier |
||||
conditions=agentConditions |
||||
}} |
||||
</div> |
||||
<div class="form-group"> |
||||
<button name="add" class="rc-button rc-button--primary add-agent">{{_ "Add"}}</button> |
||||
</div> |
||||
</div> |
||||
</fieldset> |
||||
|
||||
<fieldset> |
||||
<legend>{{_ "Selected_agents"}}</legend> |
||||
|
||||
<div class="rc-table-content"> |
||||
{{#table fixed='true' onScroll=onTableScroll }} |
||||
<thead> |
||||
<tr> |
||||
<th width="25%"><div class="table-fake-th">{{_ "Username"}}</div></th> |
||||
<th><div class="table-fake-th">{{_ "Count"}}</div></th> |
||||
<th><div class="table-fake-th">{{_ "Order"}}</div></th> |
||||
<th width="40px"> </th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each departmentAgents}} |
||||
<tr class="agent-info"> |
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{username}}</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td><input type="text" class="count" name="count" value="{{count}}" size="3"></td> |
||||
<td><input type="text" class="order" name="order" value="{{order}}" size="3"></td> |
||||
<td><a href="#remove" class="remove-agent"><i class="icon-trash"></i></a></td> |
||||
</tr> |
||||
{{else}} |
||||
<tr> |
||||
<td colspan="4">{{_ "There_are_no_agents_added_to_this_department_yet"}}</td> |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
{{/table}} |
||||
</div> |
||||
|
||||
</fieldset> |
||||
{{else}} |
||||
{{/requiresPermission}} |
||||
</fieldset> |
||||
<div class="rc-button__group"> |
||||
<button class="rc-button back" type="button"><i class="icon-left-big"></i><span>{{_ "Back"}}</span></button> |
||||
<button class="rc-button rc-button--primary save"><i class="icon-floppy"></i><span>{{_ "Save"}}</span></button> |
||||
</div> |
||||
{{else}} |
||||
{{> loading}} |
||||
{{/if}} |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</section> |
||||
{{#if tabBarVisible}} |
||||
{{#with flexData}} |
||||
{{> flexTabBar}} |
||||
{{/with}} |
||||
{{/if}} |
||||
</div> |
||||
{{/requiresPermission}} |
||||
</template> |
@ -1,367 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import _ from 'underscore'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { TabBar, RocketChatTabBar } from '../../../../ui-utils'; |
||||
import { t, handleError } from '../../../../utils'; |
||||
import { hasPermission } from '../../../../authorization'; |
||||
import { getCustomFormTemplate } from './customTemplates/register'; |
||||
import './livechatDepartmentForm.html'; |
||||
import { APIClient, roomTypes } from '../../../../utils/client'; |
||||
|
||||
const LIST_SIZE = 50; |
||||
|
||||
const saveDepartmentsAgents = async (_id, instance) => { |
||||
const upsert = [...instance.agentsToUpsert.values()]; |
||||
const remove = [...instance.agentsToRemove.values()]; |
||||
if (!upsert.length && !remove.length) { |
||||
return; |
||||
} |
||||
return APIClient.v1.post(`livechat/department/${ _id }/agents`, { |
||||
upsert, |
||||
remove, |
||||
}); |
||||
}; |
||||
|
||||
Template.livechatDepartmentForm.helpers({ |
||||
department() { |
||||
return Template.instance().department.get(); |
||||
}, |
||||
agents() { |
||||
return Template.instance().department && !_.isEmpty(Template.instance().department.get()) ? Template.instance().department.get().agents : []; |
||||
}, |
||||
departmentAgents() { |
||||
return _.sortBy(Template.instance().departmentAgents.get(), 'username'); |
||||
}, |
||||
showOnRegistration(value) { |
||||
const department = Template.instance().department.get(); |
||||
return department.showOnRegistration === value || (department.showOnRegistration === undefined && value === true); |
||||
}, |
||||
showOnOfflineForm(value) { |
||||
const department = Template.instance().department.get(); |
||||
return department.showOnOfflineForm === value || (department.showOnOfflineForm === undefined && value === true); |
||||
}, |
||||
requestTagBeforeClosingChat() { |
||||
const department = Template.instance().department.get(); |
||||
return !!(department && department.requestTagBeforeClosingChat); |
||||
}, |
||||
customFieldsTemplate() { |
||||
return getCustomFormTemplate('livechatDepartmentForm'); |
||||
}, |
||||
data() { |
||||
return { id: FlowRouter.getParam('_id') }; |
||||
}, |
||||
exceptionsAgents() { |
||||
return _.pluck(Template.instance().departmentAgents.get(), 'username'); |
||||
}, |
||||
agentModifier() { |
||||
return (filter, text = '') => { |
||||
const f = filter.get(); |
||||
return `@${ |
||||
f.length === 0 |
||||
? text |
||||
: text.replace( |
||||
new RegExp(filter.get()), |
||||
(part) => `<strong>${ part }</strong>`, |
||||
) |
||||
}`;
|
||||
}; |
||||
}, |
||||
agentConditions() { |
||||
return { roles: 'livechat-agent' }; |
||||
}, |
||||
onSelectAgents() { |
||||
return Template.instance().onSelectAgents; |
||||
}, |
||||
selectedAgents() { |
||||
return Template.instance().selectedAgents.get(); |
||||
}, |
||||
onClickTagAgents() { |
||||
return Template.instance().onClickTagAgents; |
||||
}, |
||||
flexData() { |
||||
return { |
||||
tabBar: Template.instance().tabBar, |
||||
data: Template.instance().tabBarData.get(), |
||||
}; |
||||
}, |
||||
tabBarVisible() { |
||||
return Object.values(TabBar.buttons.get()) |
||||
.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; |
||||
}, |
||||
onTableScroll() { |
||||
const instance = Template.instance(); |
||||
return function(currentTarget) { |
||||
if (currentTarget.offsetHeight + currentTarget.scrollTop < currentTarget.scrollHeight - 100) { |
||||
return; |
||||
} |
||||
const agents = instance.departmentAgents.get(); |
||||
if (instance.total.get() > agents.length) { |
||||
instance.offset.set(instance.offset.get() + LIST_SIZE); |
||||
} |
||||
}; |
||||
}, |
||||
onClickTagOfflineMessageChannel() { |
||||
return Template.instance().onClickTagOfflineMessageChannel; |
||||
}, |
||||
selectedOfflineMessageChannel() { |
||||
return Template.instance().offlineMessageChannel.get(); |
||||
}, |
||||
onSelectOfflineMessageChannel() { |
||||
return Template.instance().onSelectOfflineMessageChannel; |
||||
}, |
||||
offlineMessageChannelModifier() { |
||||
return (filter, text = '') => { |
||||
const f = filter.get(); |
||||
return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`; |
||||
}; |
||||
}, |
||||
channelSelector() { |
||||
return (expression) => ({ name: expression }); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatDepartmentForm.events({ |
||||
'submit #department-form'(e, instance) { |
||||
e.preventDefault(); |
||||
const $btn = instance.$('button.save'); |
||||
|
||||
let departmentData; |
||||
|
||||
const _id = $(e.currentTarget).data('id'); |
||||
|
||||
if (hasPermission('manage-livechat-departments')) { |
||||
const enabled = instance.$('input[name=enabled]:checked').val(); |
||||
const name = instance.$('input[name=name]').val(); |
||||
const description = instance.$('textarea[name=description]').val(); |
||||
const showOnRegistration = instance.$('input[name=showOnRegistration]:checked').val(); |
||||
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(); |
||||
const [offlineMessageChannel] = instance.offlineMessageChannel.get(); |
||||
const offlineMessageChannelName = (offlineMessageChannel && roomTypes.getRoomName(offlineMessageChannel.t, offlineMessageChannel)) || ''; |
||||
|
||||
if (enabled !== '1' && enabled !== '0') { |
||||
return toastr.error(t('Please_select_enabled_yes_or_no')); |
||||
} |
||||
|
||||
if (name.trim() === '') { |
||||
return toastr.error(t('Please_fill_a_name')); |
||||
} |
||||
|
||||
if (email.trim() === '' && showOnOfflineForm === '1') { |
||||
return toastr.error(t('Please_fill_an_email')); |
||||
} |
||||
|
||||
departmentData = { |
||||
enabled: enabled === '1', |
||||
name: name.trim(), |
||||
description: description.trim(), |
||||
showOnRegistration: showOnRegistration === '1', |
||||
showOnOfflineForm: showOnOfflineForm === '1', |
||||
requestTagBeforeClosingChat: requestTagBeforeClosingChat === '1', |
||||
email: email.trim(), |
||||
chatClosingTags, |
||||
offlineMessageChannelName, |
||||
}; |
||||
} |
||||
|
||||
const oldBtnValue = $btn.html(); |
||||
$btn.html(t('Saving')); |
||||
|
||||
instance.$('.customFormField').each((i, el) => { |
||||
const elField = instance.$(el); |
||||
const name = elField.attr('name'); |
||||
departmentData[name] = elField.val(); |
||||
}); |
||||
|
||||
if (hasPermission('manage-livechat-departments')) { |
||||
Meteor.call('livechat:saveDepartment', _id, departmentData, [], async function(err, result) { |
||||
$btn.html(oldBtnValue); |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
|
||||
await saveDepartmentsAgents(result._id, instance); |
||||
toastr.success(t('Saved')); |
||||
FlowRouter.go('livechat-departments'); |
||||
}); |
||||
} else if (hasPermission('add-livechat-department-agents')) { |
||||
saveDepartmentsAgents(_id, instance); |
||||
} else { |
||||
throw new Error(t('error-not-authorized')); |
||||
} |
||||
}, |
||||
|
||||
'click .add-agent'(e, instance) { |
||||
e.preventDefault(); |
||||
|
||||
const users = instance.selectedAgents.get(); |
||||
users.forEach(async (user) => { |
||||
const { _id, username } = user; |
||||
|
||||
const departmentAgents = instance.departmentAgents.get(); |
||||
if (departmentAgents.find(({ agentId }) => agentId === _id)) { |
||||
return toastr.error(t('This_agent_was_already_selected')); |
||||
} |
||||
const newAgent = _.clone(user); |
||||
newAgent.agentId = _id; |
||||
delete newAgent._id; |
||||
if (instance.agentsToRemove.has(newAgent.agentId)) { |
||||
instance.agentsToRemove.delete(newAgent.agentId); |
||||
} |
||||
instance.agentsToUpsert.set(newAgent.agentId, { ...newAgent, count: 0, order: 0 }); |
||||
departmentAgents.push(newAgent); |
||||
instance.departmentAgents.set(departmentAgents); |
||||
instance.selectedAgents.set(instance.selectedAgents.get().filter((user) => user.username !== username)); |
||||
}); |
||||
}, |
||||
|
||||
'click button.back'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
FlowRouter.go('livechat-departments'); |
||||
}, |
||||
|
||||
'click .remove-agent'(e, instance) { |
||||
e.preventDefault(); |
||||
if (instance.agentsToUpsert.has(this.agentId)) { |
||||
instance.agentsToUpsert.delete(this.agentId); |
||||
} |
||||
instance.agentsToRemove.set(this.agentId, this); |
||||
|
||||
instance.departmentAgents.set(instance.departmentAgents.get().filter((agent) => agent.agentId !== this.agentId)); |
||||
}, |
||||
|
||||
'keyup .count'(event, instance) { |
||||
const agent = instance.agentsToUpsert.get(this.agentId) || this; |
||||
instance.agentsToUpsert.set(this.agentId, { ...agent, count: parseInt(event.currentTarget.value) || 0 }); |
||||
}, |
||||
|
||||
'keyup .order'(event, instance) { |
||||
const agent = instance.agentsToUpsert.get(this.agentId) || this; |
||||
instance.agentsToUpsert.set(this.agentId, { ...agent, order: parseInt(event.currentTarget.value) || 0 }); |
||||
}, |
||||
|
||||
'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() { |
||||
this.agentsToUpsert = new Map(); |
||||
this.agentsToRemove = new Map(); |
||||
this.department = new ReactiveVar({ enabled: true }); |
||||
this.departmentAgents = new ReactiveVar([]); |
||||
this.selectedAgents = new ReactiveVar([]); |
||||
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.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
this.offlineMessageChannel = new ReactiveVar([]); |
||||
|
||||
|
||||
this.onClickTagOfflineMessageChannel = () => { |
||||
this.offlineMessageChannel.set([]); |
||||
}; |
||||
|
||||
this.onSelectOfflineMessageChannel = async ({ item }) => { |
||||
const { room } = await APIClient.v1.get(`rooms.info?roomId=${ item._id }`); |
||||
room.text = room.name; |
||||
this.offlineMessageChannel.set([room]); |
||||
}; |
||||
this.onSelectAgents = ({ item: agent }) => { |
||||
this.selectedAgents.set([agent]); |
||||
}; |
||||
|
||||
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 offset = this.offset.get(); |
||||
const { agents, total } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }/agents?count=${ LIST_SIZE }&offset=${ offset }`); |
||||
this.total.set(total); |
||||
if (offset === 0) { |
||||
this.departmentAgents.set(agents); |
||||
} else { |
||||
this.departmentAgents.set(this.departmentAgents.get().concat(agents)); |
||||
} |
||||
}); |
||||
|
||||
this.autorun(async () => { |
||||
const id = FlowRouter.getParam('_id'); |
||||
if (id) { |
||||
const { department } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }?includeAgents=false`); |
||||
this.department.set(department); |
||||
this.chatClosingTags.set((department && department.chatClosingTags) || []); |
||||
this.loadAvailableTags(id); |
||||
} |
||||
}); |
||||
|
||||
this.autorun(async () => { |
||||
const department = this.department.get(); |
||||
let offlineChannel = []; |
||||
if (department?.offlineMessageChannelName) { |
||||
const { room } = await APIClient.v1.get(`rooms.info?roomName=${ department?.offlineMessageChannelName }`); |
||||
if (room) { |
||||
room.text = room.name; |
||||
offlineChannel = [{ ...room }]; |
||||
} |
||||
} |
||||
this.offlineMessageChannel.set(offlineChannel); |
||||
}); |
||||
}); |
@ -1,59 +0,0 @@ |
||||
<template name="livechatDepartments"> |
||||
{{#requiresPermission 'view-livechat-departments'}} |
||||
<div class="rc-table-content"> |
||||
<form class="search-form" role="form"> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{#if isReady}} |
||||
{{> icon block="rc-input__icon-svg" icon="magnifier" }} |
||||
{{else}} |
||||
{{> loading }} |
||||
{{/if}} |
||||
</div> |
||||
<input id="departments-filter" type="text" class="rc-input__element" |
||||
placeholder="{{_ "Search"}}" autofocus dir="auto"> |
||||
</div> |
||||
</form> |
||||
<div class="results"> |
||||
{{{_ "Showing_results" departments.length}}} |
||||
</div> |
||||
|
||||
{{#table fixed='true' onScroll=onTableScroll}} |
||||
<thead> |
||||
<tr> |
||||
<th><div class="table-fake-th">{{_ "Name"}}</div></th> |
||||
<th width="30%"><div class="table-fake-th">{{_ "Description"}}</div></th> |
||||
<th><div class="table-fake-th">{{_ "Num_Agents"}}</div></th> |
||||
<th><div class="table-fake-th">{{_ "Enabled"}}</div></th> |
||||
<th width="20%"><div class="table-fake-th">{{_ "Show_on_registration_page"}}</div></th> |
||||
<th width='40px'><div class="table-fake-th"> </div></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each departments}} |
||||
<tr class="department-info row-link" data-id="{{_id}}"> |
||||
<td><div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"><span class="rc-table-title">{{name}}</span></div></div></td> |
||||
<td>{{description}}</td> |
||||
<td>{{numAgents}}</td> |
||||
<td>{{#if enabled}}{{_ "Yes"}}{{else}}{{_ "No"}}{{/if}}</td> |
||||
<td>{{#if showOnRegistration}}{{_ "Yes"}}{{else}}{{_ "No"}}{{/if}}</td> |
||||
<td> |
||||
{{#requiresPermission 'manage-livechat-departments'}} |
||||
<a href="#remove" class="remove-department"><i class="icon-trash"></i></a> |
||||
{{else}} |
||||
{{/requiresPermission}} |
||||
</td> |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
{{/table}} |
||||
</div> |
||||
{{#requiresPermission 'manage-livechat-departments'}} |
||||
<div class="rc-button__group"> |
||||
<a href="{{pathFor 'livechat-department-new'}}" class="rc-button rc-button--primary">{{_ "New_Department"}}</a> |
||||
</div> |
||||
{{else}} |
||||
{{/requiresPermission}} |
||||
{{/requiresPermission}} |
||||
</template> |
@ -1,111 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { modal } from '../../../../ui-utils'; |
||||
import { t, handleError } from '../../../../utils'; |
||||
import './livechatDepartments.html'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
Template.livechatDepartments.helpers({ |
||||
departments() { |
||||
return Template.instance().departments.get(); |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().state.get('loading'); |
||||
}, |
||||
isReady() { |
||||
const instance = Template.instance(); |
||||
return instance.ready && instance.ready.get(); |
||||
}, |
||||
onTableScroll() { |
||||
const instance = Template.instance(); |
||||
return function(currentTarget) { |
||||
if ( |
||||
currentTarget.offsetHeight + currentTarget.scrollTop |
||||
>= currentTarget.scrollHeight - 100 |
||||
) { |
||||
return instance.limit.set(instance.limit.get() + 50); |
||||
} |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
const DEBOUNCE_TIME_FOR_SEARCH_DEPARTMENTS_IN_MS = 300; |
||||
|
||||
Template.livechatDepartments.events({ |
||||
'click .remove-department'(e, instance) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
|
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, () => { |
||||
Meteor.call('livechat:removeDepartment', this._id, (error/* , result*/) => { |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
instance.departments.set(instance.departments.curValue.filter((department) => department._id !== this._id)); |
||||
modal.open({ |
||||
title: t('Removed'), |
||||
text: t('Department_removed'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
}); |
||||
}); |
||||
}, |
||||
|
||||
'click .department-info'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
FlowRouter.go('livechat-department-edit', { _id: this._id }); |
||||
}, |
||||
|
||||
'keydown #departments-filter'(e) { |
||||
if (e.which === 13) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
'keyup #departments-filter': _.debounce((e, t) => { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.filter.set(e.currentTarget.value); |
||||
}, DEBOUNCE_TIME_FOR_SEARCH_DEPARTMENTS_IN_MS), |
||||
}); |
||||
|
||||
Template.livechatDepartments.onCreated(function() { |
||||
const instance = this; |
||||
this.limit = new ReactiveVar(50); |
||||
this.filter = new ReactiveVar(''); |
||||
this.state = new ReactiveDict({ |
||||
loading: false, |
||||
}); |
||||
this.ready = new ReactiveVar(true); |
||||
this.departments = new ReactiveVar([]); |
||||
|
||||
this.autorun(async function() { |
||||
const limit = instance.limit.get(); |
||||
const filter = instance.filter.get(); |
||||
let baseUrl = `livechat/department?count=${ limit }`; |
||||
|
||||
if (filter) { |
||||
baseUrl += `&text=${ encodeURIComponent(filter) }`; |
||||
} |
||||
|
||||
const { departments } = await APIClient.v1.get(baseUrl); |
||||
instance.departments.set(departments); |
||||
instance.ready.set(true); |
||||
}); |
||||
}); |
@ -0,0 +1,15 @@ |
||||
import { useEffect, useRef } from 'react'; |
||||
|
||||
export const useComponentDidUpdate = ( |
||||
effect, |
||||
dependencies = [], |
||||
) => { |
||||
const hasMounted = useRef(false); |
||||
useEffect(() => { |
||||
if (!hasMounted.current) { |
||||
hasMounted.current = true; |
||||
return; |
||||
} |
||||
effect(); |
||||
}, dependencies); |
||||
}; |
@ -0,0 +1,310 @@ |
||||
/* eslint-disable complexity */ |
||||
import React, { useMemo, useState, useRef } from 'react'; |
||||
import { FieldGroup, Field, TextInput, Chip, SelectFiltered, Box, Icon, Divider, ToggleSwitch, TextAreaInput, ButtonGroup, Button } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useEndpointAction } from '../../hooks/useEndpointAction'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import { FormSkeleton } from './Skeleton'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import Page from '../../components/basic/Page'; |
||||
import DepartmentsAgentsTable from './DepartmentsAgentsTable'; |
||||
import { formsSubscription } from '../additionalForms'; |
||||
import { useComponentDidUpdate } from '../../hooks/useComponentDidUpdate'; |
||||
|
||||
|
||||
export default function EditDepartmentWithData({ id, reload, title }) { |
||||
const t = useTranslation(); |
||||
const { data, state, error } = useEndpointDataExperimental(`livechat/department/${ id }`) || {}; |
||||
|
||||
if ([state].includes(ENDPOINT_STATES.LOADING)) { |
||||
return <FormSkeleton/>; |
||||
} |
||||
|
||||
if (error) { |
||||
return <Box mbs='x16'>{t('User_not_found')}</Box>; |
||||
} |
||||
return <EditDepartment id={id} data={data} reload={reload} title={title}/>; |
||||
} |
||||
|
||||
const useQuery = ({ name }) => useMemo(() => ({ selector: JSON.stringify({ name }) }), [name]); |
||||
|
||||
export function EditDepartment({ data, id, title, reload }) { |
||||
const t = useTranslation(); |
||||
const agentsRoute = useRoute('omnichannel-departments'); |
||||
const eeForms = useSubscription(formsSubscription); |
||||
const initialAgents = useRef((data && data.agents) || []); |
||||
|
||||
const router = useRoute('omnichannel-departments'); |
||||
|
||||
const { |
||||
useEeNumberInput = () => {}, |
||||
useEeTextInput = () => {}, |
||||
useEeTextAreaInput = () => {}, |
||||
useDepartmentForwarding = () => {}, |
||||
useDepartmentBusinessHours = () => {}, |
||||
} = eeForms; |
||||
|
||||
const MaxChats = useEeNumberInput(); |
||||
const VisitorInactivity = useEeNumberInput(); |
||||
const WaitingQueueMessageInput = useEeTextAreaInput(); |
||||
const AbandonedMessageInput = useEeTextInput(); |
||||
const DepartmentForwarding = useDepartmentForwarding(); |
||||
const DepartmentBusinessHours = useDepartmentBusinessHours(); |
||||
const [agentList, setAgentList] = useState([]); |
||||
|
||||
const { department } = data || { department: {} }; |
||||
|
||||
const [tags, setTags] = useState((department && department.chatClosingTags) || []); |
||||
const [tagsText, setTagsText] = useState(); |
||||
|
||||
const { values, handlers } = useForm({ |
||||
name: (department && department.name) || '', |
||||
email: (department && department.email) || '', |
||||
description: (department && department.description) || '', |
||||
enabled: !!(department && department.enabled), |
||||
maxNumberSimultaneousChat: (department && department.maxNumberSimultaneousChat) || undefined, |
||||
showOnRegistration: !!(department && department.showOnRegistration), |
||||
showOnOfflineForm: !!(department && department.showOnOfflineForm), |
||||
abandonedRoomsCloseCustomMessage: (department && department.abandonedRoomsCloseCustomMessage) || '', |
||||
requestTagBeforeClosingChat: (department && department.requestTagBeforeClosingChat) || false, |
||||
offlineMessageChannelName: (department && department.offlineMessageChannelName) || '', |
||||
visitorInactivityTimeoutInSeconds: (department && department.visitorInactivityTimeoutInSeconds) || undefined, |
||||
waitingQueueMessage: (department && department.waitingQueueMessage) || '', |
||||
departmentsAllowedToForward: (department && department.departmentsAllowedToForward) || [], |
||||
}); |
||||
const { |
||||
handleName, |
||||
handleEmail, |
||||
handleDescription, |
||||
handleEnabled, |
||||
handleMaxNumberSimultaneousChat, |
||||
handleShowOnRegistration, |
||||
handleShowOnOfflineForm, |
||||
handleAbandonedRoomsCloseCustomMessage, |
||||
handleRequestTagBeforeClosingChat, |
||||
handleOfflineMessageChannelName, |
||||
handleVisitorInactivityTimeoutInSeconds, |
||||
handleWaitingQueueMessage, |
||||
handleDepartmentsAllowedToForward, |
||||
} = handlers; |
||||
const { |
||||
name, |
||||
email, |
||||
description, |
||||
enabled, |
||||
maxNumberSimultaneousChat, |
||||
showOnRegistration, |
||||
showOnOfflineForm, |
||||
abandonedRoomsCloseCustomMessage, |
||||
requestTagBeforeClosingChat, |
||||
offlineMessageChannelName, |
||||
visitorInactivityTimeoutInSeconds, |
||||
waitingQueueMessage, |
||||
departmentsAllowedToForward, |
||||
} = values; |
||||
|
||||
const handleTagChipClick = (tag) => () => { |
||||
setTags((tags) => tags.filter((_tag) => _tag !== tag)); |
||||
}; |
||||
|
||||
const handleTagTextSubmit = useMutableCallback(() => { |
||||
if (!tags.includes(tagsText)) { |
||||
setTags([...tags, tagsText]); |
||||
setTagsText(''); |
||||
} |
||||
}); |
||||
|
||||
const handleTagTextChange = useMutableCallback((e) => { |
||||
setTagsText(e.target.value); |
||||
}); |
||||
|
||||
const query = useQuery({ offlineMessageChannelName }); |
||||
|
||||
const { data: autoCompleteChannels } = useEndpointDataExperimental('rooms.autocomplete.channelAndPrivate', query) || {}; |
||||
|
||||
const channelOpts = useMemo(() => (autoCompleteChannels && autoCompleteChannels.items ? autoCompleteChannels.items.map(({ name }) => [name, name]) : []), [autoCompleteChannels]); |
||||
|
||||
const saveDepartmentInfo = useMethod('livechat:saveDepartment'); |
||||
const saveDepartmentAgentsInfoOnEdit = useEndpointAction('POST', `livechat/department/${ id }/agents`); |
||||
|
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [nameError, setNameError] = useState(); |
||||
const [emailError, setEmailError] = useState(); |
||||
const [tagError, setTagError] = useState(); |
||||
|
||||
|
||||
useComponentDidUpdate(() => setNameError(!name ? t('The_field_is_required', 'name') : ''), [t, name]); |
||||
useComponentDidUpdate(() => setEmailError(!email ? t('The_field_is_required', 'email') : ''), [t, email]); |
||||
useComponentDidUpdate(() => setTagError(requestTagBeforeClosingChat && (!tags || tags.length === 0) ? t('The_field_is_required', 'name') : ''), [t, tags]); |
||||
|
||||
const handleSubmit = useMutableCallback(async (e) => { |
||||
e.preventDefault(); |
||||
let error = false; |
||||
if (!name) { |
||||
setNameError(t('The_field_is_required', 'name')); |
||||
error = true; |
||||
} |
||||
if (!email) { |
||||
setEmailError(t('The_field_is_required', 'email')); |
||||
error = true; |
||||
} |
||||
if (requestTagBeforeClosingChat && (!tags || tags.length === 0)) { |
||||
setTagError(t('The_field_is_required', 'tags')); |
||||
error = true; |
||||
} |
||||
|
||||
if (error) { |
||||
return; |
||||
} |
||||
|
||||
const payload = { |
||||
enabled, |
||||
name, |
||||
description, |
||||
showOnRegistration, |
||||
showOnOfflineForm, |
||||
requestTagBeforeClosingChat, |
||||
email, |
||||
chatClosingTags: tags, |
||||
offlineMessageChannelName, |
||||
maxNumberSimultaneousChat, |
||||
visitorInactivityTimeoutInSeconds, |
||||
abandonedRoomsCloseCustomMessage, |
||||
waitingQueueMessage, |
||||
departmentsAllowedToForward: departmentsAllowedToForward && departmentsAllowedToForward[0], |
||||
}; |
||||
|
||||
const agentListPayload = { |
||||
upsert: agentList.filter((agent) => !initialAgents.current.some((initialAgent) => initialAgent._id === agent._id |
||||
&& agent.count === initialAgent.count |
||||
&& agent.order === initialAgent.order, |
||||
)), |
||||
remove: initialAgents.current.filter((initialAgent) => !agentList.some((agent) => initialAgent._id === agent._id)), |
||||
}; |
||||
|
||||
try { |
||||
if (id) { |
||||
await saveDepartmentInfo(id, payload, []); |
||||
await saveDepartmentAgentsInfoOnEdit(agentListPayload); |
||||
} else { |
||||
await saveDepartmentInfo(id, payload, agentList); |
||||
} |
||||
dispatchToastMessage({ type: 'success', message: t('saved') }); |
||||
reload(); |
||||
agentsRoute.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleReturn = useMutableCallback(() => { |
||||
router.push({}); |
||||
}); |
||||
|
||||
const invalidForm = !name || !email || (requestTagBeforeClosingChat && (!tags || tags.length === 0)); |
||||
|
||||
const formId = useUniqueId(); |
||||
|
||||
return <Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={title}> |
||||
<ButtonGroup> |
||||
<Button onClick={handleReturn}>{t('Back')}</Button> |
||||
<Button type='submit' form={formId} primary disabled={invalidForm}>{t('Save')}</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<FieldGroup w='full' alignSelf='center' maxWidth='x600' id={formId} is='form' autoComplete='off' onSubmit={handleSubmit}> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label>{t('Enabled')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} checked={enabled} onChange={handleEnabled} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Name')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} error={nameError} value={name} onChange={handleName} placeholder={t('Name')} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Description')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput flexGrow={1} value={description} onChange={handleDescription} placeholder={t('Description')} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label>{t('Show_on_registration_page')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} checked={showOnRegistration} onChange={handleShowOnRegistration} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Email')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} error={emailError} value={email} addon={<Icon name='mail' size='x20'/>} onChange={handleEmail} placeholder={t('Email')} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label>{t('Show_on_offline_page')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} checked={showOnOfflineForm} onChange={handleShowOnOfflineForm} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Livechat_DepartmentOfflineMessageToChannel')}</Field.Label> |
||||
<Field.Row> |
||||
<SelectFiltered flexGrow={1} options={channelOpts} value={offlineMessageChannelName} onChange={handleOfflineMessageChannelName} placeholder={t('Channel_name')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
{MaxChats && <MaxChats value={maxNumberSimultaneousChat} handler={handleMaxNumberSimultaneousChat} label={'Max_number_of_chats_per_agent'} placeholder='Max_number_of_chats_per_agent_description' />} |
||||
{VisitorInactivity && <VisitorInactivity value={visitorInactivityTimeoutInSeconds} handler={handleVisitorInactivityTimeoutInSeconds} label={'How_long_to_wait_to_consider_visitor_abandonment_in_seconds'} placeholder='Number_in_seconds' />} |
||||
{AbandonedMessageInput && <AbandonedMessageInput value={abandonedRoomsCloseCustomMessage} handler={handleAbandonedRoomsCloseCustomMessage} label={'Livechat_abandoned_rooms_closed_custom_message'} placeholder='Enter_a_custom_message' />} |
||||
{WaitingQueueMessageInput && <WaitingQueueMessageInput value={waitingQueueMessage} handler={handleWaitingQueueMessage} label={'Waiting_queue_message'} />} |
||||
{DepartmentForwarding && <DepartmentForwarding value={departmentsAllowedToForward} handler={handleDepartmentsAllowedToForward} label={'List_of_departments_for_forward_description'} placeholder='Enter_a_department_name' />} |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label>{t('Request_tag_before_closing_chat')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} checked={requestTagBeforeClosingChat} onChange={handleRequestTagBeforeClosingChat} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
{requestTagBeforeClosingChat && <Field> |
||||
<Field.Label alignSelf='stretch'>{t('Conversation_closing_tags')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput error={tagError} value={tagsText} onChange={handleTagTextChange} placeholder={t('Enter_a_tag')} /> |
||||
<Button mis='x8' title={t('add')} onClick={handleTagTextSubmit}> |
||||
{t('Add')} |
||||
</Button> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Conversation_closing_tags_description')}</Field.Hint> |
||||
{tags && tags.length > 0 && <Field.Row justifyContent='flex-start'> |
||||
{tags.map((tag, i) => <Chip key={i} onClick={handleTagChipClick(tag)} mie='x8'>{tag}</Chip>)} |
||||
</Field.Row>} |
||||
</Field>} |
||||
{DepartmentBusinessHours && <DepartmentBusinessHours bhId={department && department.businessHourId}/>} |
||||
<Divider mb='x16' /> |
||||
<Field> |
||||
<Field.Label mb='x4'>{t('Agents')}:</Field.Label> |
||||
<DepartmentsAgentsTable agents={data && data.agents} setAgentListFinal={setAgentList}/> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
</Page>; |
||||
} |
@ -0,0 +1,147 @@ |
||||
|
||||
import { useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useState, useEffect } from 'react'; |
||||
import { Box, Table, Icon, Button, NumberInput } from '@rocket.chat/fuselage'; |
||||
|
||||
import { Th, GenericTable } from '../../components/GenericTable'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointAction } from '../../hooks/useEndpointAction'; |
||||
import UserAvatar from '../../components/basic/avatar/UserAvatar'; |
||||
import DeleteWarningModal from '../../components/DeleteWarningModal'; |
||||
import { useSetModal } from '../../contexts/ModalContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { AutoCompleteAgent } from '../../components/basic/AutoCompleteAgent'; |
||||
|
||||
function AddAgent({ agentList, setAgentList, ...props }) { |
||||
const t = useTranslation(); |
||||
const [userId, setUserId] = useState(); |
||||
const getAgent = useEndpointAction('GET', `livechat/users/agent/${ userId }`); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const handleAgent = useMutableCallback((e) => setUserId(e)); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
if (!userId) { |
||||
return; |
||||
} |
||||
const { user } = await getAgent(); |
||||
|
||||
if (agentList.filter((e) => e.agentId === user._id).length === 0) { |
||||
setAgentList([{ ...user, agentId: user._id }, ...agentList]); |
||||
setUserId(); |
||||
} else { |
||||
dispatchToastMessage({ type: 'error', message: t('This_agent_was_already_selected') }); |
||||
} |
||||
}); |
||||
return <Box display='flex' alignItems='center' {...props}> |
||||
<AutoCompleteAgent empty value={userId} onChange={handleAgent}/> |
||||
<Button disabled={!userId} onClick={handleSave} mis='x8' primary>{t('Add')}</Button> |
||||
</Box>; |
||||
} |
||||
|
||||
export function RemoveAgentButton({ agentId, setAgentList, agentList }) { |
||||
const setModal = useSetModal(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const t = useTranslation(); |
||||
|
||||
const handleDelete = useMutableCallback((e) => { |
||||
e.stopPropagation(); |
||||
const onDeleteAgent = async () => { |
||||
const newList = agentList.filter((listItem) => listItem.agentId !== agentId); |
||||
setAgentList(newList); |
||||
dispatchToastMessage({ type: 'success', message: t('Agent_removed') }); |
||||
setModal(); |
||||
}; |
||||
|
||||
setModal(<DeleteWarningModal onDelete={onDeleteAgent} onCancel={() => setModal()}/>); |
||||
}); |
||||
|
||||
return <Button small ghost title={t('Remove')} onClick={handleDelete}><Icon name='trash' size='x16'/></Button>; |
||||
} |
||||
|
||||
export function Count({ agentId, setAgentList, agentList }) { |
||||
const t = useTranslation(); |
||||
const [agentCount, setAgentCount] = useState(agentList.find((agent) => agent.agentId === agentId).count || 0); |
||||
|
||||
const handleCount = useMutableCallback(async (e) => { |
||||
const countValue = Number(e.currentTarget.value); |
||||
setAgentCount(countValue); |
||||
setAgentList(agentList.map((agent) => { |
||||
if (agent.agentId === agentId) { |
||||
agent.count = countValue; |
||||
} |
||||
return agent; |
||||
})); |
||||
}); |
||||
|
||||
return <Box display='flex'><NumberInput flexShrink={1} key={`${ agentId }-count`} title={t('Count')} value={agentCount} onChange={handleCount} /></Box>; |
||||
} |
||||
|
||||
export function Order({ agentId, setAgentList, agentList }) { |
||||
const t = useTranslation(); |
||||
const [agentOrder, setAgentOrder] = useState(agentList.find((agent) => agent.agentId === agentId).order || 0); |
||||
|
||||
const handleOrder = useMutableCallback(async (e) => { |
||||
const orderValue = Number(e.currentTarget.value); |
||||
setAgentOrder(orderValue); |
||||
setAgentList(agentList.map((agent) => { |
||||
if (agent.agentId === agentId) { |
||||
agent.order = orderValue; |
||||
} |
||||
return agent; |
||||
})); |
||||
}); |
||||
|
||||
return <Box display='flex'><NumberInput flexShrink={1} key={`${ agentId }-order`} title={t('Order')} value={agentOrder} onChange={handleOrder} /></Box>; |
||||
} |
||||
|
||||
const AgentRow = React.memo(({ agentId, username, name, avatarETag, mediaQuery, agentList, setAgentList }) => <Table.Row key={agentId} tabIndex={0} role='link' action qa-user-id={agentId}> |
||||
<Table.Cell withTruncatedText> |
||||
<Box display='flex' alignItems='center'> |
||||
<UserAvatar size={mediaQuery ? 'x28' : 'x40'} title={username} username={username} etag={avatarETag}/> |
||||
<Box display='flex' withTruncatedText mi='x8'> |
||||
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText> |
||||
<Box fontScale='p2' withTruncatedText color='default'>{name || username}</Box> |
||||
{!mediaQuery && name && <Box fontScale='p1' color='hint' withTruncatedText> {`@${ username }`} </Box>} |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
</Table.Cell> |
||||
<Table.Cell fontScale='p1' color='hint' withTruncatedText> |
||||
<Count agentId={agentId} agentList={agentList} setAgentList={setAgentList}/> |
||||
</Table.Cell> |
||||
<Table.Cell fontScale='p1' color='hint' withTruncatedText> |
||||
<Order agentId={agentId} agentList={agentList} setAgentList={setAgentList}/> |
||||
</Table.Cell> |
||||
<Table.Cell fontScale='p1' color='hint'> |
||||
<RemoveAgentButton agentId={agentId} agentList={agentList} setAgentList={setAgentList}/> |
||||
</Table.Cell> |
||||
</Table.Row>); |
||||
|
||||
function DepartmentsAgentsTable({ agents, setAgentListFinal }) { |
||||
const t = useTranslation(); |
||||
const [agentList, setAgentList] = useState((agents && JSON.parse(JSON.stringify(agents))) || []); |
||||
|
||||
useEffect(() => setAgentListFinal(agentList), [agentList, setAgentListFinal]); |
||||
|
||||
const mediaQuery = useMediaQuery('(min-width: 1024px)'); |
||||
|
||||
return <> |
||||
<AddAgent agentList={agentList} setAgentList={setAgentList}/> |
||||
<GenericTable |
||||
header={<> |
||||
<Th key={'name'} w='x200'>{t('Name')}</Th> |
||||
<Th key={'Count'} w='x140'>{t('Count')}</Th> |
||||
<Th key={'Order'} w='x120'>{t('Order')}</Th> |
||||
<Th key={'remove'} w='x40'>{t('Remove')}</Th> |
||||
</>} |
||||
results={agentList} |
||||
total={agentList?.length} |
||||
pi='x24' |
||||
> |
||||
{(props) => <AgentRow key={props._id} mediaQuery={mediaQuery} agentList={agentList} setAgentList={setAgentList} {...props}/>} |
||||
</GenericTable> |
||||
</>; |
||||
} |
||||
|
||||
export default DepartmentsAgentsTable; |
@ -0,0 +1,55 @@ |
||||
import React, { useState, useEffect } from 'react'; |
||||
import { TextInput, Button, Box, Icon } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { GenericTable } from '../../components/GenericTable'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
|
||||
|
||||
const FilterByText = ({ setFilter, ...props }) => { |
||||
const t = useTranslation(); |
||||
const [text, setText] = useState(''); |
||||
|
||||
const handleChange = useMutableCallback((event) => setText(event.currentTarget.value)); |
||||
const onSubmit = useMutableCallback((e) => e.preventDefault()); |
||||
|
||||
useEffect(() => { |
||||
setFilter({ text }); |
||||
}, [setFilter, text]); |
||||
return <Box mb='x16' is='form' onSubmit={onSubmit} display='flex' flexDirection='column' {...props}> |
||||
<TextInput flexShrink={0} placeholder={t('Search')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} /> |
||||
</Box>; |
||||
}; |
||||
|
||||
function DepartmentsPage({ |
||||
data, |
||||
header, |
||||
setParams, |
||||
params, |
||||
title, |
||||
renderRow, |
||||
children, |
||||
}) { |
||||
const departmentsRoute = useRoute('omnichannel-departments'); |
||||
|
||||
const onAddNew = useMutableCallback(() => departmentsRoute.push({ |
||||
context: 'new', |
||||
})); |
||||
return <Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={title}> |
||||
<Button small onClick={onAddNew}> |
||||
<Icon name='plus' size='x16'/> |
||||
</Button> |
||||
</Page.Header> |
||||
<Page.Content> |
||||
<GenericTable FilterComponent={FilterByText} header={header} renderRow={renderRow} results={data && data.departments} total={data && data.total} setParams={setParams} params={params} /> |
||||
</Page.Content> |
||||
</Page> |
||||
{children} |
||||
</Page>; |
||||
} |
||||
|
||||
export default DepartmentsPage; |
@ -0,0 +1,114 @@ |
||||
|
||||
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useMemo, useCallback, useState } from 'react'; |
||||
import { Table, Icon } from '@rocket.chat/fuselage'; |
||||
|
||||
import { Th } from '../../components/GenericTable'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import { useEndpointAction } from '../../hooks/useEndpointAction'; |
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import NotAuthorizedPage from '../../components/NotAuthorizedPage'; |
||||
import DepartmentsPage from './DepartmentsPage'; |
||||
import EditDepartmentWithData from './DepartmentEdit'; |
||||
import { useRouteParameter, useRoute } from '../../contexts/RouterContext'; |
||||
|
||||
export function RemoveDepartmentButton({ _id, reload }) { |
||||
const deleteAction = useEndpointAction('DELETE', `livechat/department/${ _id }`); |
||||
|
||||
const handleRemoveClick = useMutableCallback(async (e) => { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
const result = await deleteAction(); |
||||
if (result.success === true) { |
||||
reload(); |
||||
} |
||||
}); |
||||
|
||||
return <Table.Cell fontScale='p1' color='hint' onClick={handleRemoveClick} withTruncatedText><Icon name='trash' size='x20'/></Table.Cell>; |
||||
} |
||||
|
||||
const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1); |
||||
|
||||
const useQuery = ({ text, itemsPerPage, current }, [column, direction]) => useMemo(() => ({ |
||||
fields: JSON.stringify({ name: 1, username: 1, emails: 1, avatarETag: 1 }), |
||||
text, |
||||
sort: JSON.stringify({ [column]: sortDir(direction), usernames: column === 'name' ? sortDir(direction) : undefined }), |
||||
...itemsPerPage && { count: itemsPerPage }, |
||||
...current && { offset: current }, |
||||
}), [text, itemsPerPage, current, column, direction]); |
||||
|
||||
function DepartmentsRoute() { |
||||
const t = useTranslation(); |
||||
const canViewDepartments = usePermission('manage-livechat-departments'); |
||||
|
||||
const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 }); |
||||
const [sort, setSort] = useState(['name', 'asc']); |
||||
|
||||
const debouncedParams = useDebouncedValue(params, 500); |
||||
const debouncedSort = useDebouncedValue(sort, 500); |
||||
const query = useQuery(debouncedParams, debouncedSort); |
||||
const departmentsRoute = useRoute('omnichannel-departments'); |
||||
const context = useRouteParameter('context'); |
||||
const id = useRouteParameter('id'); |
||||
|
||||
const onHeaderClick = useMutableCallback((id) => { |
||||
const [sortBy, sortDirection] = sort; |
||||
|
||||
if (sortBy === id) { |
||||
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']); |
||||
return; |
||||
} |
||||
setSort([id, 'asc']); |
||||
}); |
||||
|
||||
const onRowClick = useMutableCallback((id) => () => departmentsRoute.push({ |
||||
context: 'edit', |
||||
id, |
||||
})); |
||||
|
||||
const { data, reload } = useEndpointDataExperimental('livechat/department', query) || {}; |
||||
|
||||
const header = useMemo(() => [ |
||||
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w='x200'>{t('Name')}</Th>, |
||||
<Th key={'description'} direction={sort[1]} active={sort[0] === 'description'} onClick={onHeaderClick} sort='description' w='x140'>{t('Description')}</Th>, |
||||
<Th key={'numAgents'} direction={sort[1]} active={sort[0] === 'numAgents'} onClick={onHeaderClick} sort='numAgents' w='x120'>{t('Num_Agents')}</Th>, |
||||
<Th key={'enabled'} direction={sort[1]} active={sort[0] === 'enabled'} onClick={onHeaderClick} sort='enabled' w='x120'>{t('Enabled')}</Th>, |
||||
<Th key={'showOnRegistration'} direction={sort[1]} active={sort[0] === 'showOnRegistration'} onClick={onHeaderClick} sort='status' w='x120'>{t('Show_on_registration_page')}</Th>, |
||||
<Th key={'remove'} w='x40'>{t('Remove')}</Th>, |
||||
].filter(Boolean), [sort, onHeaderClick, t]); |
||||
|
||||
const renderRow = useCallback(({ name, _id, description, numAgents, enabled, showOnRegistration }) => <Table.Row key={_id} tabIndex={0} role='link' onClick={onRowClick(_id)} action qa-user-id={_id}> |
||||
<Table.Cell withTruncatedText>{name}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{description}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{numAgents || '0'}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{enabled ? t('Yes') : t('No')}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{showOnRegistration ? t('Yes') : t('No')}</Table.Cell> |
||||
<RemoveDepartmentButton _id={_id} reload={reload}/> |
||||
</Table.Row>, [onRowClick, t, reload]); |
||||
|
||||
if (!canViewDepartments) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
if (context === 'edit' || context === 'new') { |
||||
return <EditDepartmentWithData |
||||
reload={reload} |
||||
id={id} |
||||
title={context === 'edit' ? t('Edit_Department') : t('New_Department')} />; |
||||
} |
||||
|
||||
|
||||
return <DepartmentsPage |
||||
setParams={setParams} |
||||
params={params} |
||||
onHeaderClick={onHeaderClick} |
||||
data={data} useQuery={useQuery} |
||||
reload={reload} |
||||
header={header} |
||||
renderRow={renderRow} |
||||
title={'Departments'}> |
||||
</DepartmentsPage>; |
||||
} |
||||
|
||||
export default DepartmentsRoute; |
@ -0,0 +1,11 @@ |
||||
import React from 'react'; |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
export const FormSkeleton = (props) => <Box w='full' pb='x24' {...props}> |
||||
<Skeleton mbe='x8' /> |
||||
<Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
<Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
</Box>; |
@ -1,65 +0,0 @@ |
||||
<template name="livechatDepartmentCustomFieldsForm" args="deptId"> |
||||
<div class="input-line"> |
||||
<label>{{_ "Max_number_of_chats_per_agent_description"}}</label> |
||||
<div> |
||||
<input type="number" class="rc-input__element customFormField" name="maxNumberSimultaneousChat" |
||||
value="{{department.maxNumberSimultaneousChat}}" |
||||
placeholder="{{_ "Max_number_of_chats_per_agent"}}"/> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "How_long_to_wait_to_consider_visitor_abandonment"}}</label> |
||||
<div> |
||||
<input type="number" class="rc-input__element customFormField" name="visitorInactivityTimeoutInSeconds" |
||||
value="{{department.visitorInactivityTimeoutInSeconds}}" placeholder="{{_ "Number_in_seconds"}}"/> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Livechat_abandoned_rooms_closed_custom_message"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element customFormField" name="abandonedRoomsCloseCustomMessage" |
||||
value="{{department.abandonedRoomsCloseCustomMessage}}" |
||||
placeholder="{{_ "Enter_a_custom_message"}}"/> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Waiting_queue_message"}}</label> |
||||
<div> |
||||
<textarea class="rc-input__element customFormField" name="waitingQueueMessage" |
||||
rows="4">{{department.waitingQueueMessage}}</textarea> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
{{> livechatAutocompleteUser |
||||
onClickTag=onClickTagDepartment |
||||
list=selectedDepartments |
||||
onSelect=onSelectDepartments |
||||
collection='CachedDepartmentList' |
||||
endpoint='livechat/department.autocomplete' |
||||
field='name' |
||||
sort='name' |
||||
label="List_of_departments_for_forward" |
||||
placeholder="Enter_a_department_name" |
||||
name="department" |
||||
icon="queue" |
||||
noMatchTemplate="userSearchEmpty" |
||||
templateItem="popupList_item_channel" |
||||
template="roomSearch" |
||||
noMatchTemplate="roomSearchEmpty" |
||||
modifier=departmentModifier |
||||
showLabel=true |
||||
exceptions=exceptionsDepartments |
||||
}} |
||||
<input type="hidden" class="customFormField" name="departmentsAllowedToForward" |
||||
value="{{selectedDepartmentsIds}}"/> |
||||
<div> |
||||
<small class="secondary-font-color">{{{_ "List_of_departments_for_forward_description"}}}</small> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Business_Hour"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="businessHour" value="{{businessHourName}}" disabled/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -1,72 +0,0 @@ |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { APIClient, mountArrayQueryParameters } from '../../../../../../../app/utils/client'; |
||||
import './livechatDepartmentCustomFieldsForm.html'; |
||||
import { LivechatBusinessHourTypes } from '../../../../../../../definition/ILivechatBusinessHour'; |
||||
|
||||
Template.livechatDepartmentCustomFieldsForm.helpers({ |
||||
department() { |
||||
return Template.instance().department.get(); |
||||
}, |
||||
departmentModifier() { |
||||
return (filter, text = '') => { |
||||
const f = filter.get(); |
||||
return `${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`; |
||||
}; |
||||
}, |
||||
onClickTagDepartment() { |
||||
return Template.instance().onClickTagDepartment; |
||||
}, |
||||
selectedDepartments() { |
||||
return Template.instance().selectedDepartments.get(); |
||||
}, |
||||
selectedDepartmentsIds() { |
||||
return Template.instance().selectedDepartments.get().map((dept) => dept._id); |
||||
}, |
||||
onSelectDepartments() { |
||||
return Template.instance().onSelectDepartments; |
||||
}, |
||||
exceptionsDepartments() { |
||||
const department = Template.instance().department.get(); |
||||
return [department && department._id, ...Template.instance().selectedDepartments.get().map((dept) => dept._id)]; |
||||
}, |
||||
businessHourName() { |
||||
const businessHour = Template.instance().businessHour.get(); |
||||
return businessHour?.name; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatDepartmentCustomFieldsForm.onCreated(function() { |
||||
this.businessHour = new ReactiveVar({}); |
||||
this.selectedDepartments = new ReactiveVar([]); |
||||
const { id: _id, department: contextDepartment } = this.data; |
||||
|
||||
this.department = new ReactiveVar(contextDepartment); |
||||
this.onSelectDepartments = ({ item: department }) => { |
||||
department.text = department.name; |
||||
this.selectedDepartments.set(this.selectedDepartments.get().concat(department)); |
||||
}; |
||||
|
||||
this.onClickTagDepartment = (department) => { |
||||
this.selectedDepartments.set(this.selectedDepartments.get().filter((dept) => dept._id !== department._id)); |
||||
}; |
||||
|
||||
if (!contextDepartment && _id) { |
||||
this.autorun(async () => { |
||||
const { department } = await APIClient.v1.get(`livechat/department/${ _id }?includeAgents=false`); |
||||
if (department.departmentsAllowedToForward) { |
||||
const { departments } = await APIClient.v1.get(`livechat/department.listByIds?${ mountArrayQueryParameters('ids', department.departmentsAllowedToForward) }&fields=${ JSON.stringify({ fields: { name: 1 } }) }`); |
||||
this.selectedDepartments.set(departments.map((dept) => ({ |
||||
_id: dept._id, |
||||
text: dept.name, |
||||
}))); |
||||
} |
||||
if (department.businessHourId) { |
||||
const { businessHour } = await APIClient.v1.get(`livechat/business-hour?_id=${ department.businessHourId }&type=${ LivechatBusinessHourTypes.CUSTOM }`); |
||||
this.businessHour.set(businessHour); |
||||
} |
||||
this.department.set(department); |
||||
}); |
||||
} |
||||
}); |
@ -0,0 +1,22 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Field, TextInput } from '@rocket.chat/fuselage'; |
||||
// import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
|
||||
import { useTranslation } from '../../../../client/contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental } from '../../../../client/hooks/useEndpointDataExperimental'; |
||||
|
||||
export const DepartmentBusinessHours = ({ bhId }) => { |
||||
const t = useTranslation(); |
||||
const { data } = useEndpointDataExperimental('livechat/business-hour', useMemo(() => ({ _id: bhId, type: 'custom' }), [bhId])); |
||||
|
||||
const name = data && data.businessHour && data.businessHour.name; |
||||
|
||||
return <Field mbe='x16'> |
||||
<Field.Label>{t('Business_Hour')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput disabled value={name || ''}/> |
||||
</Field.Row> |
||||
</Field>; |
||||
}; |
||||
|
||||
export default DepartmentBusinessHours; |
@ -0,0 +1,23 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Field, MultiSelectFiltered } from '@rocket.chat/fuselage'; |
||||
// import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
|
||||
import { useTranslation } from '../../../../client/contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental } from '../../../../client/hooks/useEndpointDataExperimental'; |
||||
|
||||
export const DepartmentForwarding = ({ value, handler, label, placeholder }) => { |
||||
const t = useTranslation(); |
||||
const { data } = useEndpointDataExperimental('livechat/department'); |
||||
|
||||
const options = useMemo(() => (data && [...data.departments.map((department) => [department._id, department.name])]) || [], [data]); |
||||
|
||||
return <Field mbe='x16'> |
||||
<Field.Label>{t(label)}</Field.Label> |
||||
<Field.Row> |
||||
<MultiSelectFiltered value={value} options={options} onChange={handler} disabled={!options} placeholder={t(placeholder)} flexGrow={1} /> |
||||
</Field.Row> |
||||
<Field.Hint>{t('List_of_departments_for_forward_description')}</Field.Hint> |
||||
</Field>; |
||||
}; |
||||
|
||||
export default DepartmentForwarding; |
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
import { NumberInput, Field } from '@rocket.chat/fuselage'; |
||||
// import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
|
||||
import { useTranslation } from '../../../../client/contexts/TranslationContext'; |
||||
|
||||
export const EeNumberInput = ({ value, handler, label, placeholder }) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Field mbe='x16'> |
||||
<Field.Label>{t(label)}</Field.Label> |
||||
<Field.Row> |
||||
<NumberInput value={value} onChange={handler} flexGrow={1} placeholder={t(placeholder)}/> |
||||
</Field.Row> |
||||
</Field>; |
||||
}; |
||||
|
||||
export default EeNumberInput; |
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
import { TextAreaInput, Field } from '@rocket.chat/fuselage'; |
||||
// import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
|
||||
import { useTranslation } from '../../../../client/contexts/TranslationContext'; |
||||
|
||||
export const EeTextAreaInput = ({ value, handler, label, placeholder }) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Field mbe='x16'> |
||||
<Field.Label>{t(label)}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput flexGrow={1} value={value} onChange={handler} placeholder={t(placeholder)} /> |
||||
</Field.Row> |
||||
</Field>; |
||||
}; |
||||
|
||||
export default EeTextAreaInput; |
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
import { TextInput, Field } from '@rocket.chat/fuselage'; |
||||
// import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
|
||||
import { useTranslation } from '../../../../client/contexts/TranslationContext'; |
||||
|
||||
export const EeTextInput = ({ value, handler, label, placeholder }) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Field mbe='x16'> |
||||
<Field.Label>{t(label)}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={value} onChange={handler} placeholder={t(placeholder)} /> |
||||
</Field.Row> |
||||
</Field>; |
||||
}; |
||||
|
||||
export default EeTextInput; |
Loading…
Reference in new issue