Omnichannel Admin rewritten in React (#18438)
Co-authored-by: Martin Schoeler <martin.schoeler@rocket.chat> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>pull/18643/head^2
parent
fe7fe3c462
commit
ed7ff1f01d
@ -1,65 +0,0 @@ |
||||
<template name="livechatBusinessHoursForm"> |
||||
{{#requiresPermission 'view-livechat-business-hours'}} |
||||
<form class="rocket-form" id="businessHoursForm"> |
||||
{{#if timezoneTemplate}} |
||||
{{> Template.dynamic template=timezoneTemplate data=data }} |
||||
{{/if}} |
||||
|
||||
{{#if customFieldsTemplate}} |
||||
{{> Template.dynamic template=customFieldsTemplate data=data }} |
||||
{{/if}} |
||||
|
||||
<!-- days open --> |
||||
<fieldset> |
||||
<legend>{{_ "Open_days_of_the_week"}}</legend> |
||||
{{#each day in days}} |
||||
{{#if open day}} |
||||
<label class="dayOpenCheck"><input type="checkbox" name={{openName day}} checked>{{name |
||||
day}}</label> |
||||
{{else}} |
||||
<label class="dayOpenCheck"><input type="checkbox" name={{openName day}}>{{name day}} |
||||
</label> |
||||
{{/if}} |
||||
{{/each}} |
||||
</fieldset> |
||||
|
||||
<!-- times --> |
||||
<fieldset> |
||||
<legend>{{_ "Hours"}}</legend> |
||||
{{#each day in days}} |
||||
<div class="input-line"> |
||||
<h1><strong>{{name day}}</strong></h1> |
||||
<table style="width:100%;"> |
||||
<tr> |
||||
<td>{{_ "Open"}}:</td> |
||||
<td>{{_ "Close"}}:</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
<div style="margin-right:30px"> |
||||
<input type="time" class="preview-settings rc-input__element" |
||||
name={{startName day}} id={{startName day}} value={{start day}} |
||||
style="width=100px;"> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div style="margin-right:30px"> |
||||
<input type="time" class="preview-settings rc-input__element" |
||||
name={{finishName day}} id={{finishName day}} value={{finish day}} |
||||
style="width=100px;"> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</div> |
||||
{{/each}} |
||||
</fieldset> |
||||
|
||||
|
||||
<div class="rc-button__group"> |
||||
{{#if showBackButton}}<button class="rc-button back" type="button"><i class="icon-left-big"></i><span>{{_ "Back"}}</span></button>{{/if}} |
||||
<button class="rc-button rc-button--primary save"><i class="icon-floppy"></i><span>{{_ "Save"}}</span></button> |
||||
</div> |
||||
</form> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,186 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import toastr from 'toastr'; |
||||
import moment from 'moment'; |
||||
|
||||
import { t, handleError, APIClient } from '../../../../../utils/client'; |
||||
import './livechatBusinessHoursForm.html'; |
||||
import { getCustomFormTemplate } from '../customTemplates/register'; |
||||
import { businessHourManager } from './BusinessHours'; |
||||
|
||||
Template.livechatBusinessHoursForm.helpers({ |
||||
days() { |
||||
return Template.instance().businessHour.get().workHours; |
||||
}, |
||||
startName(day) { |
||||
return `${ day.day }_start`; |
||||
}, |
||||
finishName(day) { |
||||
return `${ day.day }_finish`; |
||||
}, |
||||
openName(day) { |
||||
return `${ day.day }_open`; |
||||
}, |
||||
start(day) { |
||||
return Template.instance().dayVars[day.day].start.get(); |
||||
}, |
||||
finish(day) { |
||||
return Template.instance().dayVars[day.day].finish.get(); |
||||
}, |
||||
name(day) { |
||||
return TAPi18n.__(day.day); |
||||
}, |
||||
open(day) { |
||||
return Template.instance().dayVars[day.day].open.get(); |
||||
}, |
||||
customFieldsTemplate() { |
||||
if (!businessHourManager.showCustomTemplate(Template.instance().businessHour.get())) { |
||||
return; |
||||
} |
||||
return getCustomFormTemplate('livechatBusinessHoursForm'); |
||||
}, |
||||
timezoneTemplate() { |
||||
if (!businessHourManager.showTimezoneTemplate()) { |
||||
return; |
||||
} |
||||
return getCustomFormTemplate('livechatBusinessHoursTimezoneForm'); |
||||
}, |
||||
showBackButton() { |
||||
return businessHourManager.showBackButton(); |
||||
}, |
||||
data() { |
||||
return Template.instance().businessHour; |
||||
}, |
||||
}); |
||||
|
||||
const splitDayAndPeriod = (value) => value.split('_'); |
||||
|
||||
Template.livechatBusinessHoursForm.events({ |
||||
'change .preview-settings, keydown .preview-settings'(e, instance) { |
||||
const [day, period] = splitDayAndPeriod(e.currentTarget.name); |
||||
|
||||
const newTime = moment(e.currentTarget.value, 'HH:mm'); |
||||
|
||||
// check if start and stop do not cross
|
||||
if (period === 'start') { |
||||
if (newTime.isSameOrBefore(moment(instance.dayVars[day].finish.get(), 'HH:mm'))) { |
||||
instance.dayVars[day].start.set(e.currentTarget.value); |
||||
} else { |
||||
e.currentTarget.value = instance.dayVars[day].start.get(); |
||||
} |
||||
} else if (period === 'finish') { |
||||
if (newTime.isSameOrAfter(moment(instance.dayVars[day].start.get(), 'HH:mm'))) { |
||||
instance.dayVars[day].finish.set(e.currentTarget.value); |
||||
} else { |
||||
e.currentTarget.value = instance.dayVars[day].finish.get(); |
||||
} |
||||
} |
||||
}, |
||||
'change .dayOpenCheck input'(e, instance) { |
||||
const [day, period] = splitDayAndPeriod(e.currentTarget.name); |
||||
instance.dayVars[day][period].set(e.target.checked); |
||||
}, |
||||
'change .preview-settings, keyup .preview-settings'(e, instance) { |
||||
let { value } = e.currentTarget; |
||||
if (e.currentTarget.type === 'radio') { |
||||
value = value === 'true'; |
||||
instance[e.currentTarget.name].set(value); |
||||
} |
||||
}, |
||||
|
||||
'click button.back'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
FlowRouter.go('livechat-business-hours'); |
||||
}, |
||||
|
||||
'submit .rocket-form'(e, instance) { |
||||
e.preventDefault(); |
||||
|
||||
// convert all times to utc then update them in db
|
||||
const days = []; |
||||
for (const d in instance.dayVars) { |
||||
if (instance.dayVars.hasOwnProperty(d)) { |
||||
const day = instance.dayVars[d]; |
||||
const start = moment(day.start.get(), 'HH:mm').format('HH:mm'); |
||||
const finish = moment(day.finish.get(), 'HH:mm').format('HH:mm'); |
||||
days.push({ |
||||
day: d, |
||||
start, |
||||
finish, |
||||
open: day.open.get(), |
||||
}); |
||||
} |
||||
} |
||||
|
||||
const businessHourData = { |
||||
...instance.businessHour.get(), |
||||
workHours: days, |
||||
}; |
||||
|
||||
instance.$('.customFormField').each((i, el) => { |
||||
const elField = instance.$(el); |
||||
const name = elField.attr('name'); |
||||
businessHourData[name] = elField.val(); |
||||
}); |
||||
Meteor.call('livechat:saveBusinessHour', businessHourData, function(err /* ,result*/) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(t('Business_hours_updated')); |
||||
FlowRouter.go('livechat-business-hours'); |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
const createDefaultBusinessHour = () => { |
||||
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; |
||||
const closedDays = ['Saturday', 'Sunday']; |
||||
return { |
||||
workHours: days.map((day) => ({ |
||||
day, |
||||
start: '00:00', |
||||
finish: '00:00', |
||||
open: !closedDays.includes(day), |
||||
})), |
||||
}; |
||||
}; |
||||
|
||||
Template.livechatBusinessHoursForm.onCreated(async function() { |
||||
this.dayVars = createDefaultBusinessHour().workHours.reduce((acc, day) => { |
||||
acc[day.day] = { |
||||
start: new ReactiveVar(day.start), |
||||
finish: new ReactiveVar(day.finish), |
||||
open: new ReactiveVar(day.open), |
||||
}; |
||||
return acc; |
||||
}, {}); |
||||
this.businessHour = new ReactiveVar({}); |
||||
|
||||
this.businessHour.set({ |
||||
...createDefaultBusinessHour(), |
||||
}); |
||||
this.autorun(async () => { |
||||
if (FlowRouter.current().route.name.includes('new')) { |
||||
return; |
||||
} |
||||
const id = FlowRouter.getParam('_id'); |
||||
const type = FlowRouter.getParam('type'); |
||||
let url = 'livechat/business-hour'; |
||||
if (id && type) { |
||||
url += `?_id=${ id }&type=${ type }`; |
||||
} |
||||
const { businessHour } = await APIClient.v1.get(url); |
||||
if (!businessHour) { |
||||
return; |
||||
} |
||||
this.businessHour.set(businessHour); |
||||
businessHour.workHours.forEach((d) => { |
||||
this.dayVars[d.day].start.set(moment.utc(d.start.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm')); |
||||
this.dayVars[d.day].finish.set(moment.utc(d.finish.utc.time, 'HH:mm').tz(businessHour.timezone.name).format('HH:mm')); |
||||
this.dayVars[d.day].open.set(d.open); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,5 +0,0 @@ |
||||
<template name="livechatMainBusinessHours"> |
||||
{{#requiresPermission 'view-livechat-business-hours'}} |
||||
{{> Template.dynamic template=getTemplate}} |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,10 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import './livechatMainBusinessHours.html'; |
||||
import { businessHourManager } from './BusinessHours'; |
||||
|
||||
Template.livechatMainBusinessHours.helpers({ |
||||
getTemplate() { |
||||
return businessHourManager.getTemplate(); |
||||
}, |
||||
}); |
||||
@ -1,43 +0,0 @@ |
||||
<template name="livechatIntegrationFacebook"> |
||||
{{#requiresPermission 'view-livechat-facebook'}} |
||||
<form id="profile" autocomplete="off" class="container"> |
||||
<fieldset class="rc-form-legend"> |
||||
<div class="rc-form-group"> |
||||
{{#if enabled}} |
||||
<button class="rc-button rc-button--secondary reload">{{_ "Reload_Pages"}}</button> |
||||
<button class="rc-button rc-button--danger disable">{{_ "Disable"}}</button> |
||||
{{else}} |
||||
<button class="rc-button rc-button--primary enable" {{enableButtonDisabled}}>{{_ "Enable"}}</button> |
||||
{{#unless hasToken}} |
||||
<div class="rc-input__wrapper"> |
||||
<p>{{_ "You_have_to_set_an_API_token_first_in_order_to_use_the_integration"}}</p> |
||||
<p>{{_ "Please_go_to_the_Administration_page_then_Livechat_Facebook"}}</p> |
||||
</div> |
||||
{{/unless}} |
||||
{{/if}} |
||||
</div> |
||||
{{#if isLoading}} |
||||
{{> loading}} |
||||
{{else}} |
||||
{{#each pages}} |
||||
<div class="rc-form-group"> |
||||
<div class="rc-switch"> |
||||
<label class="rc-switch__label" tabindex="-1"> |
||||
<input type="checkbox" class="rc-switch__input" name="subscribe" value="true" {{subscribed}}> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text">{{name}}</span> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
{{#if enabled}} |
||||
<p>{{_ "No_pages_yet_Try_hitting_Reload_Pages_button"}}</p> |
||||
{{/if}} |
||||
{{/each}} |
||||
{{/if}} |
||||
</fieldset> |
||||
</form> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,157 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { modal } from '../../../../../ui-utils'; |
||||
import { t, handleError } from '../../../../../utils'; |
||||
import './livechatIntegrationFacebook.html'; |
||||
|
||||
Template.livechatIntegrationFacebook.helpers({ |
||||
pages() { |
||||
return Template.instance().pages.get(); |
||||
}, |
||||
subscribed() { |
||||
return this.subscribed ? 'checked' : ''; |
||||
}, |
||||
enabled() { |
||||
return Template.instance().enabled.get(); |
||||
}, |
||||
hasToken() { |
||||
return Template.instance().hasToken.get(); |
||||
}, |
||||
enableButtonDisabled() { |
||||
return !Template.instance().hasToken.get() ? 'disabled' : ''; |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().loading.get(); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatIntegrationFacebook.onCreated(function() { |
||||
this.enabled = new ReactiveVar(false); |
||||
this.hasToken = new ReactiveVar(false); |
||||
this.pages = new ReactiveVar([]); |
||||
this.loading = new ReactiveVar(false); |
||||
|
||||
this.autorun(() => { |
||||
if (this.enabled.get()) { |
||||
this.loadPages(); |
||||
} |
||||
}); |
||||
|
||||
this.result = (successFn, errorFn = () => {}) => (error, result) => { |
||||
// fix the state where user it was enabled on admin
|
||||
if (error && error.error) { |
||||
switch (error.error) { |
||||
case 'invalid-facebook-token': |
||||
case 'invalid-instance-url': |
||||
case 'integration-disabled': |
||||
return Meteor.call('livechat:facebook', { action: 'enable' }, this.result(() => { |
||||
this.enabled.set(true); |
||||
this.loadPages(); |
||||
}, () => this.loadPages())); |
||||
} |
||||
} |
||||
|
||||
if (result && result.success === false && (result.type === 'OAuthException' || typeof result.url !== 'undefined')) { |
||||
const oauthWindow = window.open(result.url, 'facebook-integration-oauth', 'width=600,height=400'); |
||||
|
||||
const checkInterval = setInterval(() => { |
||||
if (oauthWindow.closed) { |
||||
clearInterval(checkInterval); |
||||
errorFn(error); |
||||
} |
||||
}, 300); |
||||
return; |
||||
} |
||||
if (error) { |
||||
errorFn(error); |
||||
return modal.open({ |
||||
title: t('Error_loading_pages'), |
||||
text: error.reason, |
||||
type: 'error', |
||||
}); |
||||
} |
||||
successFn(result); |
||||
}; |
||||
|
||||
this.loadPages = () => { |
||||
this.pages.set([]); |
||||
this.loading.set(true); |
||||
Meteor.call('livechat:facebook', { action: 'list-pages' }, this.result((result) => { |
||||
this.pages.set(result.pages); |
||||
this.loading.set(false); |
||||
}, () => this.loading.set(false))); |
||||
}; |
||||
}); |
||||
|
||||
Template.livechatIntegrationFacebook.onRendered(function() { |
||||
this.loading.set(true); |
||||
Meteor.call('livechat:facebook', { action: 'initialState' }, this.result((result) => { |
||||
this.enabled.set(result.enabled); |
||||
this.hasToken.set(result.hasToken); |
||||
this.loading.set(false); |
||||
})); |
||||
}); |
||||
|
||||
Template.livechatIntegrationFacebook.events({ |
||||
'click .reload'(event, instance) { |
||||
event.preventDefault(); |
||||
|
||||
instance.loadPages(); |
||||
}, |
||||
'click .enable'(event, instance) { |
||||
event.preventDefault(); |
||||
|
||||
Meteor.call('livechat:facebook', { action: 'enable' }, instance.result(() => { |
||||
instance.enabled.set(true); |
||||
}, () => instance.enabled.set(true))); |
||||
}, |
||||
'click .disable'(event, instance) { |
||||
event.preventDefault(); |
||||
|
||||
modal.open({ |
||||
title: t('Disable_Facebook_integration'), |
||||
text: t('Are_you_sure_you_want_to_disable_Facebook_integration'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, () => { |
||||
Meteor.call('livechat:facebook', { action: 'disable' }, (err) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
instance.enabled.set(false); |
||||
instance.pages.set([]); |
||||
|
||||
modal.open({ |
||||
title: t('Disabled'), |
||||
text: t('Integration_disabled'), |
||||
type: 'success', |
||||
timer: 2000, |
||||
showConfirmButton: false, |
||||
}); |
||||
}); |
||||
}); |
||||
}, |
||||
'change [name=subscribe]'(event, instance) { |
||||
Meteor.call('livechat:facebook', { |
||||
action: !event.currentTarget.checked ? 'unsubscribe' : 'subscribe', |
||||
page: this.id, |
||||
}, (err, result) => { |
||||
if (result.success) { |
||||
const pages = instance.pages.get(); |
||||
pages.forEach((page) => { |
||||
if (page.id === this.id) { |
||||
page.subscribed = event.currentTarget.checked; |
||||
} |
||||
}); |
||||
instance.pages.set(pages); |
||||
} |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,88 +0,0 @@ |
||||
<template name="livechatIntegrationWebhook"> |
||||
{{#requiresPermission 'view-livechat-webhooks'}} |
||||
<div class="rocket-form"> |
||||
<h2>{{_ "Webhooks"}}</h2> |
||||
<p> |
||||
{{_ "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM"}} |
||||
<a href="https://rocket.chat/docs/administrator-guides/livechat/#integrations">{{_ "Click_here"}}</a> {{_ "to_see_more_details_on_how_to_integrate"}} |
||||
</p> |
||||
|
||||
<form id="integration-form"> |
||||
<div class="input-line"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Webhook_URL"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input type="url" name="webhookUrl" id="webhookUrl" value="{{webhookUrl}}" placeholder="https://yourdomain.com/webhook/entrypoint" class="rc-input__element"> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Secret_token"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input type="text" name="secretToken" id="secretToken" value="{{secretToken}}" |
||||
class="rc-input__element"> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnStart"> |
||||
<input type="checkbox" name="sendOnStart" id="sendOnStart" value="1" checked="{{sendOnStartChecked}}"> |
||||
{{_ "Send_request_on_chat_start"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnClose"> |
||||
<input type="checkbox" name="sendOnClose" id="sendOnClose" value="1" checked="{{sendOnCloseChecked}}"> |
||||
{{_ "Send_request_on_chat_close"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnChatTaken"> |
||||
<input type="checkbox" name="sendOnChatTaken" id="sendOnChatTaken" value="1" checked="{{sendOnChatTakenChecked}}"> |
||||
{{_ "Send_request_on_chat_taken"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnChatQueued"> |
||||
<input type="checkbox" name="sendOnChatQueued" id="sendOnChatQueued" value="1" checked="{{sendOnChatQueuedChecked}}"> |
||||
{{_ "Send_request_on_chat_queued"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnForward"> |
||||
<input type="checkbox" name="sendOnForward" id="sendOnForward" value="1" checked="{{sendOnForwardChecked}}"> |
||||
{{_ "Send_request_on_forwarding"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnOffline"> |
||||
<input type="checkbox" name="sendOnOffline" id="sendOnOffline" value="1" checked="{{sendOnOfflineChecked}}"> |
||||
{{_ "Send_request_on_offline_messages"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnVisitorMessage"> |
||||
<input type="checkbox" name="sendOnVisitorMessage" id="sendOnVisitorMessage" value="1" checked="{{sendOnVisitorMessageChecked}}"> |
||||
{{_ "Send_request_on_visitor_message"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendOnAgentMessage"> |
||||
<input type="checkbox" name="sendOnAgentMessage" id="sendOnAgentMessage" value="1" checked="{{sendOnAgentMessageChecked}}"> |
||||
{{_ "Send_request_on_agent_message"}} |
||||
</label> |
||||
</div> |
||||
<div class="rc-button__group submit"> |
||||
<button class="rc-button rc-button--danger reset-settings" type="button"><i class="icon-ccw"></i>{{_ "Reset"}}</button> |
||||
<button class="rc-button rc-button--secondary test" type="button" disabled="{{disableTest}}">{{_ "Send_Test"}}</button> |
||||
<button class="rc-button rc-button--primary save"><i class="icon-floppy"></i>{{_ "Save"}}</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,146 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import _ from 'underscore'; |
||||
import s from 'underscore.string'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { modal } from '../../../../../ui-utils'; |
||||
import { t, handleError } from '../../../../../utils'; |
||||
import './livechatIntegrationWebhook.html'; |
||||
import { APIClient } from '../../../../../utils/client'; |
||||
|
||||
const getIntegrationSettingById = (settings, id) => settings.find((setting) => setting._id === id); |
||||
|
||||
Template.livechatIntegrationWebhook.helpers({ |
||||
webhookUrl() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhookUrl'); |
||||
return setting && setting.value; |
||||
}, |
||||
secretToken() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_secret_token'); |
||||
return setting && setting.value; |
||||
}, |
||||
disableTest() { |
||||
return Template.instance().disableTest.get(); |
||||
}, |
||||
sendOnStartChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_start'); |
||||
return setting && setting.value; |
||||
}, |
||||
sendOnCloseChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_close'); |
||||
return setting && setting.value; |
||||
}, |
||||
sendOnChatTakenChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_chat_taken'); |
||||
return setting && setting.value; |
||||
}, |
||||
sendOnChatQueuedChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_chat_queued'); |
||||
return setting && setting.value; |
||||
}, |
||||
sendOnForwardChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_forward'); |
||||
return setting && setting.value; |
||||
}, |
||||
sendOnOfflineChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_offline_msg'); |
||||
return setting && setting.value; |
||||
}, |
||||
sendOnVisitorMessageChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_visitor_message'); |
||||
return setting && setting.value; |
||||
}, |
||||
sendOnAgentMessageChecked() { |
||||
const setting = getIntegrationSettingById(Template.instance().settings.get(), 'Livechat_webhook_on_agent_message'); |
||||
return setting && setting.value; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatIntegrationWebhook.onCreated(async function() { |
||||
this.disableTest = new ReactiveVar(true); |
||||
this.settings = new ReactiveVar([]); |
||||
|
||||
this.autorun(() => { |
||||
const webhook = getIntegrationSettingById(this.settings.get(), 'Livechat_webhookUrl'); |
||||
this.disableTest.set(!webhook || _.isEmpty(webhook.value)); |
||||
}); |
||||
const { settings } = await APIClient.v1.get('livechat/integrations.settings'); |
||||
this.settings.set(settings); |
||||
}); |
||||
|
||||
Template.livechatIntegrationWebhook.events({ |
||||
'change #webhookUrl, blur #webhookUrl'(e, instance) { |
||||
const setting = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhookUrl'); |
||||
instance.disableTest.set(!setting || e.currentTarget.value !== setting.value); |
||||
}, |
||||
'click .test'(e, instance) { |
||||
if (!instance.disableTest.get()) { |
||||
Meteor.call('livechat:webhookTest', (err) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
modal.open({ |
||||
title: t('It_works'), |
||||
type: 'success', |
||||
timer: 2000, |
||||
}); |
||||
}); |
||||
} |
||||
}, |
||||
'click .reset-settings'(e, instance) { |
||||
e.preventDefault(); |
||||
|
||||
const webhookUrl = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhookUrl'); |
||||
const secretToken = getIntegrationSettingById(instance.settings.get(), 'Livechat_secret_token'); |
||||
const webhookOnStart = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_start'); |
||||
const webhookOnClose = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_close'); |
||||
const webhookOnChatTaken = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_chat_taken'); |
||||
const webhookOnChatQueued = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_chat_queued'); |
||||
const webhookOnForward = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_forward'); |
||||
const webhookOnOfflineMsg = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_offline_msg'); |
||||
const webhookOnVisitorMessage = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_visitor_message'); |
||||
const webhookOnAgentMessage = getIntegrationSettingById(instance.settings.get(), 'Livechat_webhook_on_agent_message'); |
||||
|
||||
instance.$('#webhookUrl').val(webhookUrl && webhookUrl.value); |
||||
instance.$('#secretToken').val(secretToken && secretToken.value); |
||||
instance.$('#sendOnStart').get(0).checked = webhookOnStart && webhookOnStart.value; |
||||
instance.$('#sendOnClose').get(0).checked = webhookOnClose && webhookOnClose.value; |
||||
instance.$('#sendOnChatTaken').get(0).checked = webhookOnChatTaken && webhookOnChatTaken.value; |
||||
instance.$('#sendOnChatQueued').get(0).checked = webhookOnChatQueued && webhookOnChatQueued.value; |
||||
instance.$('#sendOnForward').get(0).checked = webhookOnForward && webhookOnForward.value; |
||||
instance.$('#sendOnOffline').get(0).checked = webhookOnOfflineMsg && webhookOnOfflineMsg.value; |
||||
instance.$('#sendOnVisitorMessage').get(0).checked = webhookOnVisitorMessage && webhookOnVisitorMessage.value; |
||||
instance.$('#sendOnAgentMessage').get(0).checked = webhookOnAgentMessage && webhookOnAgentMessage.value; |
||||
|
||||
instance.disableTest.set(!webhookUrl || _.isEmpty(webhookUrl.value)); |
||||
}, |
||||
'submit .rocket-form'(e, instance) { |
||||
e.preventDefault(); |
||||
|
||||
const settings = { |
||||
Livechat_webhookUrl: s.trim(instance.$('#webhookUrl').val()), |
||||
Livechat_secret_token: s.trim(instance.$('#secretToken').val()), |
||||
Livechat_webhook_on_start: instance.$('#sendOnStart').get(0).checked, |
||||
Livechat_webhook_on_close: instance.$('#sendOnClose').get(0).checked, |
||||
Livechat_webhook_on_chat_taken: instance.$('#sendOnChatTaken').get(0).checked, |
||||
Livechat_webhook_on_chat_queued: instance.$('#sendOnChatQueued').get(0).checked, |
||||
Livechat_webhook_on_forward: instance.$('#sendOnForward').get(0).checked, |
||||
Livechat_webhook_on_offline_msg: instance.$('#sendOnOffline').get(0).checked, |
||||
Livechat_webhook_on_visitor_message: instance.$('#sendOnVisitorMessage').get(0).checked, |
||||
Livechat_webhook_on_agent_message: instance.$('#sendOnAgentMessage').get(0).checked, |
||||
}; |
||||
Meteor.call('livechat:saveIntegration', settings, (err) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
const savedValues = instance.settings.get().map((setting) => { |
||||
setting.value = settings[setting._id]; |
||||
return setting; |
||||
}); |
||||
instance.settings.set(savedValues); |
||||
toastr.success(t('Saved')); |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,103 +0,0 @@ |
||||
<template name="livechatAgents"> |
||||
{{#requiresPermission 'manage-livechat-agents'}} |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Livechat_Agents"}} |
||||
<div class="content"> |
||||
<form id="form-agent" class="form-inline"> |
||||
<div class="form-group"> |
||||
{{> livechatAutocompleteUser |
||||
onClickTag=onClickTagAgents |
||||
list=selectedAgents |
||||
deleteLastItem=deleteLastAgent |
||||
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 |
||||
}} |
||||
</div> |
||||
<div class="form-group"> |
||||
<button name="add" class="rc-button rc-button--primary add" disabled='{{isloading}}'>{{_ "Add"}}</button> |
||||
</div> |
||||
</form> |
||||
|
||||
<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="agents-filter" type="text" class="rc-input__element" |
||||
placeholder="{{_ "Search"}}" autofocus dir="auto"> |
||||
</div> |
||||
</form> |
||||
<div class="results"> |
||||
{{{_ "Showing_results" agents.length}}} |
||||
</div> |
||||
|
||||
{{#table fixed='true' onScroll=onTableScroll}} |
||||
<thead> |
||||
<tr> |
||||
<th width="30%"><div class="table-fake-th">{{_ "Name"}}</div></th> |
||||
<th width="20%"><div class="table-fake-th">{{_ "Username"}}</div></th> |
||||
<th width="20%"><div class="table-fake-th">{{_ "Email"}}</div></th> |
||||
<th><div class="table-fake-th">{{_ "Status"}}</div></th> |
||||
<th><div class="table-fake-th">{{_ "Service"}}</div></th> |
||||
<th width='40px'><div class="table-fake-th"> </div></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each agents}} |
||||
<tr class="rc-table-tr user-info row-link" data-id="{{_id}}"> |
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-avatar">{{> avatar username=username}}</div> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{name}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{fname}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td>{{username}}</td> |
||||
<td>{{emailAddress}}</td> |
||||
<td>{{status}}</td> |
||||
<td>{{statusService}}</td> |
||||
<td> |
||||
<a href="#remove" class="remove-agent"> |
||||
<i class="icon-trash"></i> |
||||
</a> |
||||
</td> |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
{{/table}} |
||||
</div> |
||||
</div> |
||||
</section> |
||||
{{#with flexData}} |
||||
{{> flexTabBar}} |
||||
{{/with}} |
||||
</div> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,234 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { modal, call, TabBar, RocketChatTabBar } from '../../../../ui-utils'; |
||||
import { t, handleError, APIClient } from '../../../../utils/client'; |
||||
import './livechatAgents.html'; |
||||
|
||||
const loadAgents = async (instance, limit = 50, text) => { |
||||
let baseUrl = `livechat/users/agent?count=${ limit }`; |
||||
|
||||
if (text) { |
||||
baseUrl += `&text=${ encodeURIComponent(text) }`; |
||||
} |
||||
|
||||
const { users } = await APIClient.v1.get(baseUrl); |
||||
instance.agents.set(users); |
||||
instance.ready.set(true); |
||||
}; |
||||
|
||||
const getUsername = (user) => user.username; |
||||
Template.livechatAgents.helpers({ |
||||
exceptionsAgents() { |
||||
const { selectedAgents } = Template.instance(); |
||||
return Template.instance().agents.get() |
||||
.map(getUsername).concat(selectedAgents.get().map(getUsername)); |
||||
}, |
||||
deleteLastAgent() { |
||||
const i = Template.instance(); |
||||
return () => { |
||||
const arr = i.selectedAgents.curValue; |
||||
arr.pop(); |
||||
i.selectedAgents.set(arr); |
||||
}; |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().state.get('loading'); |
||||
}, |
||||
agents() { |
||||
return Template.instance().agents.get(); |
||||
}, |
||||
emailAddress() { |
||||
if (this.emails && this.emails.length > 0) { |
||||
return this.emails[0].address; |
||||
} |
||||
}, |
||||
agentModifier() { |
||||
return (filter, text = '') => { |
||||
const f = filter.get(); |
||||
return `@${ |
||||
f.length === 0 |
||||
? text |
||||
: text.replace( |
||||
new RegExp(filter.get()), |
||||
(part) => `<strong>${ part }</strong>`, |
||||
) |
||||
}`;
|
||||
}; |
||||
}, |
||||
onSelectAgents() { |
||||
return Template.instance().onSelectAgents; |
||||
}, |
||||
selectedAgents() { |
||||
return Template.instance().selectedAgents.get(); |
||||
}, |
||||
onClickTagAgents() { |
||||
return Template.instance().onClickTagAgents; |
||||
}, |
||||
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); |
||||
} |
||||
}; |
||||
}, |
||||
flexData() { |
||||
return { |
||||
tabBar: Template.instance().tabBar, |
||||
data: Template.instance().tabBarData.get(), |
||||
}; |
||||
}, |
||||
statusService() { |
||||
const { status, statusLivechat } = this; |
||||
return statusLivechat === 'available' && status !== 'offline' ? t('Available') : t('Unavailable'); |
||||
}, |
||||
}); |
||||
|
||||
const DEBOUNCE_TIME_FOR_SEARCH_AGENTS_IN_MS = 300; |
||||
|
||||
Template.livechatAgents.events({ |
||||
'click .remove-agent'(e, instance) { |
||||
e.preventDefault(); |
||||
|
||||
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:removeAgent', this.username, async function( |
||||
error, /* , result*/ |
||||
) { |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
|
||||
if (instance.tabBar.getState() === 'opened') { |
||||
instance.tabBar.close(); |
||||
} |
||||
|
||||
await loadAgents(instance); |
||||
|
||||
modal.open({ |
||||
title: t('Removed'), |
||||
text: t('Agent_removed'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
}); |
||||
}, |
||||
); |
||||
}, |
||||
|
||||
async 'submit #form-agent'(e, instance) { |
||||
e.preventDefault(); |
||||
const { selectedAgents, state, limit, filter } = instance; |
||||
|
||||
const users = selectedAgents.get(); |
||||
|
||||
if (!users.length) { |
||||
return; |
||||
} |
||||
|
||||
state.set('loading', true); |
||||
try { |
||||
await Promise.all( |
||||
users.map(({ username }) => call('livechat:addAgent', username)), |
||||
); |
||||
|
||||
await loadAgents(instance, limit.get(), filter.get()); |
||||
selectedAgents.set([]); |
||||
} finally { |
||||
state.set('loading', false); |
||||
} |
||||
}, |
||||
|
||||
'keydown #agents-filter'(e) { |
||||
if (e.which === 13) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
|
||||
'keyup #agents-filter': _.debounce((e, t) => { |
||||
e.preventDefault(); |
||||
t.filter.set(e.currentTarget.value); |
||||
}, DEBOUNCE_TIME_FOR_SEARCH_AGENTS_IN_MS), |
||||
|
||||
'click .user-info'(e, instance) { |
||||
e.preventDefault(); |
||||
instance.tabBarData.set({ |
||||
agentId: this._id, |
||||
onRemoveAgent: () => loadAgents(instance), |
||||
}); |
||||
|
||||
instance.tabBar.setData({ label: t('Agent_Info'), icon: 'livechat' }); |
||||
instance.tabBar.open('livechat-agent-info'); |
||||
}, |
||||
/* |
||||
'click .info-tabs button'(e) { |
||||
e.preventDefault(); |
||||
$('.info-tabs button').removeClass('active'); |
||||
$(e.currentTarget).addClass('active'); |
||||
$('.user-info-content').hide(); |
||||
$($(e.currentTarget).attr('href')).show(); |
||||
}, |
||||
*/ |
||||
}); |
||||
|
||||
Template.livechatAgents.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.selectedAgents = new ReactiveVar([]); |
||||
this.agents = new ReactiveVar([]); |
||||
this.tabBar = new RocketChatTabBar(); |
||||
this.tabBar.showGroup(FlowRouter.current().route.name); |
||||
this.tabBarData = new ReactiveVar(); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['livechat-agent-users'], |
||||
id: 'livechat-agent-info', |
||||
i18nTitle: 'Agent_Info', |
||||
icon: 'livechat', |
||||
template: 'agentInfo', |
||||
order: 1, |
||||
}); |
||||
|
||||
this.onSelectAgents = ({ item: agent }) => { |
||||
this.selectedAgents.set([...this.selectedAgents.curValue, agent]); |
||||
}; |
||||
|
||||
this.onClickTagAgents = ({ username }) => { |
||||
this.selectedAgents.set(this.selectedAgents.curValue.filter((user) => user.username !== username)); |
||||
}; |
||||
|
||||
this.autorun(function() { |
||||
const limit = instance.limit.get(); |
||||
const filter = instance.filter.get(); |
||||
loadAgents(instance, limit, filter); |
||||
}); |
||||
}); |
||||
@ -1,145 +0,0 @@ |
||||
<template name="livechatAppearance"> |
||||
{{#requiresPermission 'view-livechat-appearance'}} |
||||
|
||||
<div class='livechat-content'> |
||||
|
||||
<h2>{{_ "Settings"}}</h2> |
||||
|
||||
<form class="rocket-form"> |
||||
<fieldset> |
||||
<legend>{{_ "Livechat_online"}}</legend> |
||||
<div class="input-line"> |
||||
<label for="title">{{_ "Title"}}</label> |
||||
<input type="text" class="preview-settings rc-input__element" name="title" id="title" value="{{title}}"> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="color">{{_ "Title_bar_color"}}</label> |
||||
<input type="color" class="preview-settings rc-input__element" name="color" id="color" value="{{color}}" /> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="showLimitTextLength">{{_ "Livechat_enable_message_character_limit"}}</label> |
||||
<div class="inline-fields"> |
||||
<input type="radio" class="preview-settings" name="showLimitTextLength" id="showLimitTextLengthFormTrue" checked="{{showLimitTextLengthFormTrueChecked}}" value="true"> |
||||
<label for="showLimitTextLengthFormTrue">{{_ "True"}}</label> |
||||
<input type="radio" class="preview-settings" name="showLimitTextLength" id="showLimitTextLengthFormFalse" checked="{{showLimitTextLengthFormFalseChecked}}" value="false"> |
||||
<label for="showLimitTextLengthFormFalse">{{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="limitTextLength">{{_ "Message_Characther_Limit"}}</label> |
||||
<input type="number" class="preview-settings rc-input__element" name="limitTextLength" id="limitTextLength" value="{{limitTextLength}}"> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="showAgentInfo">{{_ "Show_agent_info"}}</label> |
||||
<div class="inline-fields"> |
||||
<input type="radio" class="preview-settings" name="showAgentInfo" id="showAgentInfoFormTrue" checked="{{showAgentInfoFormTrueChecked}}" value="true"> |
||||
<label for="showAgentInfoFormTrue">{{_ "True"}}</label> |
||||
<input type="radio" class="preview-settings" name="showAgentInfo" id="showAgentInfoFormFalse" checked="{{showAgentInfoFormFalseChecked}}" value="false"> |
||||
<label for="showAgentInfoFormFalse">{{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="showAgentEmail">{{_ "Show_agent_email"}}</label> |
||||
<div class="inline-fields"> |
||||
<input type="radio" class="preview-settings" name="showAgentEmail" id="showAgentEmailFormTrue" checked="{{showAgentEmailFormTrueChecked}}" value="true"> |
||||
<label for="showAgentEmailFormTrue">{{_ "True"}}</label> |
||||
<input type="radio" class="preview-settings" name="showAgentEmail" id="showAgentEmailFormFalse" checked="{{showAgentEmailFormFalseChecked}}" value="false"> |
||||
<label for="showAgentEmailFormFalse">{{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
</fieldset> |
||||
<fieldset> |
||||
<legend>{{_ "Livechat_offline"}}</legend> |
||||
<div class="input-line"> |
||||
<label for="displayOfflineForm">{{_ "Display_offline_form"}}</label> |
||||
<div class="inline-fields"> |
||||
<input type="radio" class="preview-settings" name="displayOfflineForm" id="displayOfflineFormTrue" checked="{{displayOfflineFormTrueChecked}}" value="true"> |
||||
<label for="displayOfflineFormTrue">{{_ "True"}}</label> |
||||
<input type="radio" class="preview-settings" name="displayOfflineForm" id="displayOfflineFormFalse" checked="{{displayOfflineFormFalseChecked}}" value="false"> |
||||
<label for="displayOfflineFormFalse">{{_ "False"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="offlineUnavailableMessage">{{_ "Offline_form_unavailable_message"}}</label> |
||||
<textarea class="preview-settings rc-input__element" name="offlineUnavailableMessage" id="offlineUnavailableMessage">{{offlineUnavailableMessage}}</textarea> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="offlineMessage">{{_ "Offline_message"}}</label> |
||||
<textarea class="preview-settings rc-input__element" name="offlineMessage" id="offlineMessage">{{offlineMessage}}</textarea> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="titleOffline">{{_ "Title_offline"}}</label> |
||||
<input type="text" class="preview-settings rc-input__element" name="titleOffline" id="titleOffline" value="{{titleOffline}}"> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="colorOffline">{{_ "Title_bar_color_offline"}}</label> |
||||
<input type="color" class="preview-settings rc-input__element" name="colorOffline" id="colorOffline" value="{{colorOffline}}" /> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="emailOffline">{{_ "Email_address_to_send_offline_messages"}}</label> |
||||
<input type="text" class="preview-settings rc-input__element" name="emailOffline" id="emailOffline" value="{{emailOffline}}"> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="offlineSuccessMessage">{{_ "Offline_success_message"}}</label> |
||||
<textarea class="preview-settings rc-input__element" name="offlineSuccessMessage" id="offlineSuccessMessage">{{offlineSuccessMessage}}</textarea> |
||||
</div> |
||||
</fieldset> |
||||
<fieldset> |
||||
<legend>{{_ "Livechat_registration_form"}}</legend> |
||||
<!-- |
||||
<label class="rc-switch__label"> |
||||
<input class="rc-switch__input" type="checkbox" name="registrationFormEnabled" checked="{{registrationFormEnabled}}"/> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text"> |
||||
{{_ "Show_preregistration_form"}} |
||||
</span> |
||||
</label> |
||||
--> |
||||
<label class="rc-switch__label"> |
||||
<input class="rc-switch__input js-input-check" type="checkbox" name="registrationFormEnabled" {{registrationFormEnabled}}/> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text">{{_ "Enabled"}}</span> |
||||
</label> |
||||
<label class="rc-switch__label"> |
||||
<input class="rc-switch__input js-input-check" type="checkbox" name="registrationFormNameFieldEnabled" {{registrationFormNameFieldEnabled}}/> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text">{{_ "Show_name_field"}}</span> |
||||
</label> |
||||
<label class="rc-switch__label"> |
||||
<input class="rc-switch__input js-input-check" type="checkbox" name="registrationFormEmailFieldEnabled" {{registrationFormEmailFieldEnabled}}/> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text">{{_ "Show_email_field"}}</span> |
||||
</label> |
||||
<div class="input-line"> |
||||
<label for="registrationFormMessage">{{_ "Livechat_registration_form_message"}}</label> |
||||
<textarea class="preview-settings rc-input__element" name="registrationFormMessage" id="registrationFormMessage">{{registrationFormMessage}}</textarea> |
||||
</div> |
||||
</fieldset> |
||||
<fieldset> |
||||
<legend>{{_ "Conversation_finished"}}</legend> |
||||
<div class="input-line"> |
||||
<label for="conversationFinishedMessage">{{_ "Conversation_finished_message"}}</label> |
||||
<textarea class="preview-settings rc-input__element" name="conversationFinishedMessage" id="conversationFinishedMessage">{{conversationFinishedMessage}}</textarea> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="conversationFinishedText">{{_ "Conversation_finished_text"}}</label> |
||||
<textarea class="preview-settings rc-input__element" name="conversationFinishedText" id="conversationFinishedText">{{conversationFinishedText}}</textarea> |
||||
</div> |
||||
</fieldset> |
||||
<div class="rc-button__group submit"> |
||||
<button class="rc-button rc-button--danger reset-settings" type="button"><i class="icon-ccw"></i>{{_ "Reset"}}</button> |
||||
<button class="rc-button rc-button--primary save"><i class="icon-floppy"></i>{{_ "Save"}}</button> |
||||
</div> |
||||
</form> |
||||
|
||||
</div> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,345 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import s from 'underscore.string'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { t, handleError } from '../../../../utils'; |
||||
import './livechatAppearance.html'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
const getSettingFromAppearance = (instance, settingName) => instance.appearance.get() && instance.appearance.get().find((setting) => setting._id === settingName); |
||||
|
||||
Template.livechatAppearance.helpers({ |
||||
showLimitTextLengthFormTrueChecked() { |
||||
if (Template.instance().showLimitTextLength.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
showLimitTextLengthFormFalseChecked() { |
||||
if (!Template.instance().showLimitTextLength.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
limitTextLength() { |
||||
return Template.instance().limitTextLength.get(); |
||||
}, |
||||
color() { |
||||
return Template.instance().color.get(); |
||||
}, |
||||
showAgentInfo() { |
||||
return Template.instance().showAgentInfo.get(); |
||||
}, |
||||
showAgentEmail() { |
||||
return Template.instance().showAgentEmail.get(); |
||||
}, |
||||
title() { |
||||
return Template.instance().title.get(); |
||||
}, |
||||
colorOffline() { |
||||
return Template.instance().colorOffline.get(); |
||||
}, |
||||
titleOffline() { |
||||
return Template.instance().titleOffline.get(); |
||||
}, |
||||
offlineMessage() { |
||||
return Template.instance().offlineMessage.get(); |
||||
}, |
||||
sampleOfflineMessage() { |
||||
return Template.instance().offlineMessage.get().replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2'); |
||||
}, |
||||
offlineSuccessMessage() { |
||||
return Template.instance().offlineSuccessMessage.get(); |
||||
}, |
||||
sampleOfflineSuccessMessage() { |
||||
return Template.instance().offlineSuccessMessage.get().replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2'); |
||||
}, |
||||
showAgentInfoFormTrueChecked() { |
||||
if (Template.instance().showAgentInfo.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
showAgentInfoFormFalseChecked() { |
||||
if (!Template.instance().showAgentInfo.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
showAgentEmailFormTrueChecked() { |
||||
if (Template.instance().showAgentEmail.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
showAgentEmailFormFalseChecked() { |
||||
if (!Template.instance().showAgentEmail.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
displayOfflineFormTrueChecked() { |
||||
if (Template.instance().displayOfflineForm.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
displayOfflineFormFalseChecked() { |
||||
if (!Template.instance().displayOfflineForm.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
offlineUnavailableMessage() { |
||||
return Template.instance().offlineUnavailableMessage.get(); |
||||
}, |
||||
sampleOfflineUnavailableMessage() { |
||||
return Template.instance().offlineUnavailableMessage.get().replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2'); |
||||
}, |
||||
emailOffline() { |
||||
return Template.instance().offlineEmail.get(); |
||||
}, |
||||
conversationFinishedMessage() { |
||||
return Template.instance().conversationFinishedMessage.get(); |
||||
}, |
||||
conversationFinishedText() { |
||||
return Template.instance().conversationFinishedText.get(); |
||||
}, |
||||
registrationFormEnabled() { |
||||
if (Template.instance().registrationFormEnabled.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
registrationFormNameFieldEnabled() { |
||||
if (Template.instance().registrationFormNameFieldEnabled.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
registrationFormEmailFieldEnabled() { |
||||
if (Template.instance().registrationFormEmailFieldEnabled.get()) { |
||||
return 'checked'; |
||||
} |
||||
}, |
||||
registrationFormMessage() { |
||||
return Template.instance().registrationFormMessage.get(); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatAppearance.onCreated(async function() { |
||||
this.appearance = new ReactiveVar([]); |
||||
this.title = new ReactiveVar(null); |
||||
this.color = new ReactiveVar(null); |
||||
this.showLimitTextLength = new ReactiveVar(null); |
||||
this.limitTextLength = new ReactiveVar(null); |
||||
this.showAgentInfo = new ReactiveVar(null); |
||||
this.showAgentEmail = new ReactiveVar(null); |
||||
this.displayOfflineForm = new ReactiveVar(null); |
||||
this.offlineUnavailableMessage = new ReactiveVar(null); |
||||
this.offlineMessage = new ReactiveVar(null); |
||||
this.offlineSuccessMessage = new ReactiveVar(null); |
||||
this.titleOffline = new ReactiveVar(null); |
||||
this.colorOffline = new ReactiveVar(null); |
||||
this.offlineEmail = new ReactiveVar(null); |
||||
this.conversationFinishedMessage = new ReactiveVar(null); |
||||
this.conversationFinishedText = new ReactiveVar(null); |
||||
this.registrationFormEnabled = new ReactiveVar(null); |
||||
this.registrationFormNameFieldEnabled = new ReactiveVar(null); |
||||
this.registrationFormEmailFieldEnabled = new ReactiveVar(null); |
||||
this.registrationFormMessage = new ReactiveVar(null); |
||||
|
||||
const { appearance } = await APIClient.v1.get('livechat/appearance'); |
||||
this.appearance.set(appearance); |
||||
|
||||
const livechatTitle = getSettingFromAppearance(this, 'Livechat_title'); |
||||
const livechatTitleColor = getSettingFromAppearance(this, 'Livechat_title_color'); |
||||
const livechatShowMessageCharacterLimit = getSettingFromAppearance(this, 'Livechat_enable_message_character_limit'); |
||||
const livechatMessageCharacterLimit = getSettingFromAppearance(this, 'Livechat_message_character_limit'); |
||||
const livechatShowAgentInfo = getSettingFromAppearance(this, 'Livechat_show_agent_info'); |
||||
const livechatShowAgentEmail = getSettingFromAppearance(this, 'Livechat_show_agent_email'); |
||||
const livechatDisplayOfflineForm = getSettingFromAppearance(this, 'Livechat_display_offline_form'); |
||||
const livechatOfflineFormUnavailable = getSettingFromAppearance(this, 'Livechat_offline_form_unavailable'); |
||||
const livechatOfflineMessage = getSettingFromAppearance(this, 'Livechat_offline_message'); |
||||
const livechatOfflineSuccessMessage = getSettingFromAppearance(this, 'Livechat_offline_success_message'); |
||||
const livechatOfflineTitle = getSettingFromAppearance(this, 'Livechat_offline_title'); |
||||
const livechatOfflineTitleColor = getSettingFromAppearance(this, 'Livechat_offline_title_color'); |
||||
const livechatOfflineEmail = getSettingFromAppearance(this, 'Livechat_offline_email'); |
||||
const livechatConversationFinishedMessage = getSettingFromAppearance(this, 'Livechat_conversation_finished_message'); |
||||
const livechatRegistrationFormMessage = getSettingFromAppearance(this, 'Livechat_registration_form_message'); |
||||
const livechatRegistrationForm = getSettingFromAppearance(this, 'Livechat_registration_form'); |
||||
const livechatNameFieldRegistrationForm = getSettingFromAppearance(this, 'Livechat_name_field_registration_form'); |
||||
const livechatEmailFieldRegistrationForm = getSettingFromAppearance(this, 'Livechat_email_field_registration_form'); |
||||
const conversationFinishedText = getSettingFromAppearance(this, 'Livechat_conversation_finished_text'); |
||||
|
||||
this.title.set(livechatTitle && livechatTitle.value); |
||||
this.color.set(livechatTitleColor && livechatTitleColor.value); |
||||
this.showLimitTextLength.set(livechatShowMessageCharacterLimit && livechatShowMessageCharacterLimit.value); |
||||
this.limitTextLength.set(livechatMessageCharacterLimit && livechatMessageCharacterLimit.value); |
||||
this.showAgentInfo.set(livechatShowAgentInfo && livechatShowAgentInfo.value); |
||||
this.showAgentEmail.set(livechatShowAgentEmail && livechatShowAgentEmail.value); |
||||
this.displayOfflineForm.set(livechatDisplayOfflineForm && livechatDisplayOfflineForm.value); |
||||
this.offlineUnavailableMessage.set(livechatOfflineFormUnavailable && livechatOfflineFormUnavailable.value); |
||||
this.offlineMessage.set(livechatOfflineMessage && livechatOfflineMessage.value); |
||||
this.offlineSuccessMessage.set(livechatOfflineSuccessMessage && livechatOfflineSuccessMessage.value); |
||||
this.titleOffline.set(livechatOfflineTitle && livechatOfflineTitle.value); |
||||
this.colorOffline.set(livechatOfflineTitleColor && livechatOfflineTitleColor.value); |
||||
this.offlineEmail.set(livechatOfflineEmail && livechatOfflineEmail.value); |
||||
this.conversationFinishedMessage.set(livechatConversationFinishedMessage && livechatConversationFinishedMessage.value); |
||||
this.registrationFormMessage.set(livechatRegistrationFormMessage && livechatRegistrationFormMessage.value); |
||||
this.registrationFormEnabled.set(livechatRegistrationForm && livechatRegistrationForm.value); |
||||
this.registrationFormNameFieldEnabled.set(livechatNameFieldRegistrationForm && livechatNameFieldRegistrationForm.value); |
||||
this.registrationFormEmailFieldEnabled.set(livechatEmailFieldRegistrationForm && livechatEmailFieldRegistrationForm.value); |
||||
this.conversationFinishedText.set(conversationFinishedText && conversationFinishedText.value); |
||||
}); |
||||
|
||||
Template.livechatAppearance.events({ |
||||
'change .js-input-check'(e, instance) { |
||||
instance[e.currentTarget.name].set(e.currentTarget.checked); |
||||
}, |
||||
'change .preview-settings, keyup .preview-settings'(e, instance) { |
||||
let { value } = e.currentTarget; |
||||
if (e.currentTarget.type === 'radio') { |
||||
value = value === 'true'; |
||||
} |
||||
instance[e.currentTarget.name].set(value); |
||||
}, |
||||
'click .reset-settings'(e, instance) { |
||||
e.preventDefault(); |
||||
|
||||
const settingTitle = getSettingFromAppearance(instance, 'Livechat_title'); |
||||
instance.title.set(settingTitle && settingTitle.value); |
||||
|
||||
const settingTitleColor = getSettingFromAppearance(instance, 'Livechat_title_color'); |
||||
instance.color.set(settingTitleColor && settingTitleColor.value); |
||||
|
||||
const settinglivechatShowMessageCharacterLimit = getSettingFromAppearance(instance, 'Livechat_enable_message_character_limit'); |
||||
instance.showLimitTextLength.set(settinglivechatShowMessageCharacterLimit && settinglivechatShowMessageCharacterLimit.value); |
||||
|
||||
const settinglivechatMessageCharacterLimit = getSettingFromAppearance(instance, 'Livechat_message_character_limit'); |
||||
instance.limitTextLength.set(settinglivechatMessageCharacterLimit && settinglivechatMessageCharacterLimit.value); |
||||
|
||||
const settingShowAgentInfo = getSettingFromAppearance(instance, 'Livechat_show_agent_info'); |
||||
instance.showAgentInfo.set(settingShowAgentInfo && settingShowAgentInfo.value); |
||||
|
||||
const settingShowAgentEmail = getSettingFromAppearance(instance, 'Livechat_show_agent_email'); |
||||
instance.showAgentEmail.set(settingShowAgentEmail && settingShowAgentEmail.value); |
||||
|
||||
const settingDiplayOffline = getSettingFromAppearance(instance, 'Livechat_display_offline_form'); |
||||
instance.displayOfflineForm.set(settingDiplayOffline && settingDiplayOffline.value); |
||||
|
||||
const settingFormUnavailable = getSettingFromAppearance(instance, 'Livechat_offline_form_unavailable'); |
||||
instance.offlineUnavailableMessage.set(settingFormUnavailable && settingFormUnavailable.value); |
||||
|
||||
const settingOfflineMessage = getSettingFromAppearance(instance, 'Livechat_offline_message'); |
||||
instance.offlineMessage.set(settingOfflineMessage && settingOfflineMessage.value); |
||||
|
||||
const settingOfflineSuccess = getSettingFromAppearance(instance, 'Livechat_offline_success_message'); |
||||
instance.offlineSuccessMessage.set(settingOfflineSuccess && settingOfflineSuccess.value); |
||||
|
||||
const settingOfflineTitle = getSettingFromAppearance(instance, 'Livechat_offline_title'); |
||||
instance.titleOffline.set(settingOfflineTitle && settingOfflineTitle.value); |
||||
|
||||
const settingOfflineTitleColor = getSettingFromAppearance(instance, 'Livechat_offline_title_color'); |
||||
instance.colorOffline.set(settingOfflineTitleColor && settingOfflineTitleColor.value); |
||||
|
||||
const settingConversationFinishedMessage = getSettingFromAppearance(instance, 'Livechat_conversation_finished_message'); |
||||
instance.conversationFinishedMessage.set(settingConversationFinishedMessage && settingConversationFinishedMessage.value); |
||||
|
||||
const settingConversationFinishedText = getSettingFromAppearance(instance, 'Livechat_conversation_finished_text'); |
||||
instance.conversationFinishedText.set(settingConversationFinishedText && settingConversationFinishedText.value); |
||||
|
||||
const settingRegistrationFormEnabled = getSettingFromAppearance(instance, 'Livechat_registration_form'); |
||||
instance.registrationFormEnabled.set(settingRegistrationFormEnabled && settingRegistrationFormEnabled.value); |
||||
|
||||
const settingRegistrationFormNameFieldEnabled = getSettingFromAppearance(instance, 'Livechat_name_field_registration_form'); |
||||
instance.registrationFormNameFieldEnabled.set(settingRegistrationFormNameFieldEnabled && settingRegistrationFormNameFieldEnabled.value); |
||||
|
||||
const settingRegistrationFormEmailFieldEnabled = getSettingFromAppearance(instance, 'Livechat_email_field_registration_form'); |
||||
instance.registrationFormEmailFieldEnabled.set(settingRegistrationFormEmailFieldEnabled && settingRegistrationFormEmailFieldEnabled.value); |
||||
|
||||
const settingRegistrationFormMessage = getSettingFromAppearance(instance, 'Livechat_registration_form_message'); |
||||
instance.registrationFormMessage.set(settingRegistrationFormMessage && settingRegistrationFormMessage.value); |
||||
}, |
||||
'submit .rocket-form'(e, instance) { |
||||
e.preventDefault(); |
||||
const settings = [ |
||||
{ |
||||
_id: 'Livechat_title', |
||||
value: s.trim(instance.title.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_title_color', |
||||
value: instance.color.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_enable_message_character_limit', |
||||
value: instance.showLimitTextLength.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_message_character_limit', |
||||
value: parseInt(instance.limitTextLength.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_show_agent_info', |
||||
value: instance.showAgentInfo.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_show_agent_email', |
||||
value: instance.showAgentEmail.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_display_offline_form', |
||||
value: instance.displayOfflineForm.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_offline_form_unavailable', |
||||
value: s.trim(instance.offlineUnavailableMessage.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_offline_message', |
||||
value: s.trim(instance.offlineMessage.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_offline_success_message', |
||||
value: s.trim(instance.offlineSuccessMessage.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_offline_title', |
||||
value: s.trim(instance.titleOffline.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_offline_title_color', |
||||
value: instance.colorOffline.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_offline_email', |
||||
value: instance.$('#emailOffline').val(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_conversation_finished_message', |
||||
value: s.trim(instance.conversationFinishedMessage.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_conversation_finished_text', |
||||
value: s.trim(instance.conversationFinishedText.get()), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_registration_form', |
||||
value: instance.registrationFormEnabled.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_name_field_registration_form', |
||||
value: instance.registrationFormNameFieldEnabled.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_email_field_registration_form', |
||||
value: instance.registrationFormEmailFieldEnabled.get(), |
||||
}, |
||||
{ |
||||
_id: 'Livechat_registration_form_message', |
||||
value: s.trim(instance.registrationFormMessage.get()), |
||||
}, |
||||
]; |
||||
|
||||
Meteor.call('livechat:saveAppearance', settings, (err/* , success*/) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
instance.appearance.set(settings); |
||||
toastr.success(t('Settings_updated')); |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,25 +0,0 @@ |
||||
.rc-table-content { |
||||
& .js-sort { |
||||
cursor: pointer; |
||||
|
||||
&.is-sorting .table-fake-th .rc-icon { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
|
||||
& .table-fake-th { |
||||
color: #444444; |
||||
|
||||
&:hover .rc-icon { |
||||
opacity: 1; |
||||
} |
||||
|
||||
& .rc-icon { |
||||
transition: opacity 0.3s; |
||||
|
||||
opacity: 0; |
||||
|
||||
font-size: 1rem; |
||||
} |
||||
} |
||||
} |
||||
@ -1,196 +0,0 @@ |
||||
<template name="livechatCurrentChats"> |
||||
{{#requiresPermission 'view-livechat-current-chats'}} |
||||
<fieldset> |
||||
<form class="form-inline" id="form-filters" method="post"> |
||||
<div class="livechat-group-filters-wrapper"> |
||||
<div class="livechat-group-filters-container"> |
||||
<div class="livechat-current-chats-standard-filters"> |
||||
<div class="form-group"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Guest"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input type="text" placeholder="{{_ "Name"}}" class="rc-input__element" id="name" name="name"> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="form-group"> |
||||
{{> livechatAutocompleteUser |
||||
onClickTag=onClickTagAgent |
||||
list=selectedAgents |
||||
onSelect=onSelectAgents |
||||
collection='UserAndRoom' |
||||
endpoint='users.autocomplete' |
||||
field='username' |
||||
sort='username' |
||||
label="Served_By" |
||||
placeholder="Served_By" |
||||
name="agent" |
||||
icon="at" |
||||
noMatchTemplate="userSearchEmpty" |
||||
templateItem="popupList_item_default" |
||||
modifier=agentModifier |
||||
showLabel=true |
||||
}} |
||||
</div> |
||||
<div class="form-group"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Status"}}</div> |
||||
<div class="rc-select"> |
||||
<select class="rc-select__element" id="status" name="status"> |
||||
<option class="rc-select__option" value="">{{_ "All"}}</option> |
||||
<option class="rc-select__option" value="opened">{{_ "Opened"}}</option> |
||||
<option class="rc-select__option" value="closed">{{_ "Closed"}}</option> |
||||
</select> |
||||
{{> icon block="rc-select__arrow" icon="arrow-down" }} |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="form-group"> |
||||
{{> livechatAutocompleteUser |
||||
onClickTag=onClickTagDepartment |
||||
list=selectedDepartments |
||||
onSelect=onSelectDepartments |
||||
collection='CachedDepartmentList' |
||||
endpoint='livechat/department.autocomplete' |
||||
field='name' |
||||
sort='name' |
||||
label="Department" |
||||
placeholder="Enter_a_department_name" |
||||
name="department" |
||||
icon="queue" |
||||
noMatchTemplate="userSearchEmpty" |
||||
templateItem="popupList_item_channel" |
||||
template="roomSearch" |
||||
noMatchTemplate="roomSearchEmpty" |
||||
modifier=departmentModifier |
||||
showLabel=true |
||||
}} |
||||
</div> |
||||
<div class="form-group input-daterange"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "From"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input autocomplete="off" type="text" placeholder="{{_ "Date_From"}}" class="rc-input__element" id="from" name="from" > |
||||
</div> |
||||
</label> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "To"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input autocomplete="off" type="text" placeholder="{{_ "Date_to"}}" class="rc-input__element" id="to" name="to"> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<button type="button" class="rc-button rc-button--secondary add-filter-button livechat-current-chats-add-filter-button">{{> icon icon="plus" }}</button> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="livechat-current-chats-custom-filters"> |
||||
{{#each customFilters}} |
||||
<div class="form-group"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{label}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input autocomplete="off" type="text" placeholder="{{label}}" class="rc-input__element" name="custom-field-{{name}}"> |
||||
<a href="#remove" class="remove-livechat-custom-filter" data-name="{{name}}"><i class="icon-trash"></i></a> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
{{/each}} |
||||
{{#each tagFilters}} |
||||
<div class="form-group"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Tags"}}</div> |
||||
<span class="livechat-current-chats-tag-filter-wrapper"> |
||||
{{> livechatRoomTagSelector}} |
||||
<a href="#remove" class="remove-livechat-tags-filter" data-id="{{tagId}}"><i class="icon-trash"></i></a> |
||||
</span> |
||||
</label> |
||||
</div> |
||||
{{/each}} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="livechat-group-filters-buttons"> |
||||
<div class="rc-button__group"> |
||||
<button class="rc-button rc-button--primary">{{_ "Filter"}}</button> |
||||
<button class="livechat-current-chats-extra-actions"> |
||||
{{> icon icon="menu" block="rc-icon--default-size"}} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</fieldset> |
||||
<div class="rc-table-content"> |
||||
{{#table fixed='true' onScroll=onTableScroll onResize=onTableResize onSort=onTableSort}} |
||||
<thead> |
||||
<tr> |
||||
<th class="js-sort {{#if sortBy 'fname'}}is-sorting{{/if}}" data-sort="fname" width="25%"> |
||||
<div class="table-fake-th">{{_ "Name"}}{{> icon icon=(sortIcon 'fname')}}</div> |
||||
</th> |
||||
<th class="js-sort {{#if sortBy 'departmentId'}}is-sorting{{/if}}" data-sort="departmentId" width="15%"> |
||||
<div class="table-fake-th">{{_ "Department"}}{{> icon icon=(sortIcon 'departmentId')}}</div> |
||||
</th> |
||||
<th class="js-sort {{#if sortBy 'servedBy.username'}}is-sorting{{/if}}" data-sort="servedBy.username" width="15%"> |
||||
<div class="table-fake-th">{{_ "Served_By"}}{{> icon icon=(sortIcon 'servedBy.username')}}</div> |
||||
</th> |
||||
<th class="js-sort {{#if sortBy 'ts'}}is-sorting{{/if}}" data-sort="ts" width="15%"> |
||||
<div class="table-fake-th">{{_ "Started_At"}}{{> icon icon=(sortIcon 'ts')}}</div> |
||||
</th> |
||||
|
||||
<th class="js-sort {{#if sortBy 'lm'}}is-sorting{{/if}}" data-sort="lm" width="15%"> |
||||
<div class="table-fake-th">{{_ "Last_Message_At"}}{{> icon icon=(sortIcon 'lm')}}</div> |
||||
</th> |
||||
<th class="js-sort {{#if sortBy 'open'}}is-sorting{{/if}}" data-sort="open" width="10%"> |
||||
<div class="table-fake-th">{{_ "Status"}}{{> icon icon=(sortIcon 'open')}}</div> |
||||
</th> |
||||
|
||||
<th width="5%"><div class="table-fake-th"> </div></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each livechatRoom}} |
||||
<tr class="rc-table-tr manage row-link" data-name="{{latest.name}}"> |
||||
|
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{fname}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td>{{department.name}}</td> |
||||
<td>{{servedBy}}</td> |
||||
<td>{{startedAt}}</td> |
||||
<td>{{lastMessage}}</td> |
||||
<td>{{status}}</td> |
||||
{{#requiresPermission 'remove-closed-livechat-rooms'}} |
||||
{{#if isClosed}} |
||||
<td><a href="#remove" class="remove-livechat-room"><i class="icon-trash"></i></a></td> |
||||
{{else}} |
||||
<td> </td> |
||||
{{/if}} |
||||
{{else}} |
||||
<td> </td> |
||||
{{/requiresPermission}} |
||||
</tr> |
||||
{{/each}} |
||||
{{#if isLoading}} |
||||
<tr class="table-no-click"> |
||||
<td colspan="5" class="table-loading-td">{{> loading}}</td> |
||||
</tr> |
||||
{{/if}} |
||||
</tbody> |
||||
{{/table}} |
||||
</div> |
||||
{{#if hasMore}} |
||||
<div class="rc-button__group"> |
||||
<button class="rc-button rc-button--primary js-load-more">{{_ "Load_more"}}</button> |
||||
</div> |
||||
{{/if}} |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,519 +0,0 @@ |
||||
import 'moment-timezone'; |
||||
import _ from 'underscore'; |
||||
import moment from 'moment'; |
||||
import './livechatCurrentChats.css'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Random } from 'meteor/random'; |
||||
import toastr from 'toastr'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
|
||||
import { modal, call, popover } from '../../../../ui-utils'; |
||||
import { t, handleError, APIClient } from '../../../../utils/client'; |
||||
import { hasRole, hasPermission } from '../../../../authorization'; |
||||
import './livechatCurrentChats.html'; |
||||
|
||||
const ROOMS_COUNT = 50; |
||||
const FILTER_STORE_NAME = 'Filters.LivechatCurrentChats'; |
||||
|
||||
const loadStoredFilters = () => { |
||||
let storedFilters; |
||||
try { |
||||
const storeItem = Meteor._localStorage.getItem(FILTER_STORE_NAME); |
||||
storedFilters = storeItem ? JSON.parse(Meteor._localStorage.getItem(FILTER_STORE_NAME)) : {}; |
||||
} catch (e) { |
||||
storedFilters = {}; |
||||
} |
||||
|
||||
return storedFilters; |
||||
}; |
||||
|
||||
const storeFilters = (filters) => { |
||||
Meteor._localStorage.setItem(FILTER_STORE_NAME, JSON.stringify(filters)); |
||||
}; |
||||
|
||||
const removeStoredFilters = () => { |
||||
Meteor._localStorage.removeItem(FILTER_STORE_NAME); |
||||
}; |
||||
|
||||
Template.livechatCurrentChats.helpers({ |
||||
hasMore() { |
||||
const instance = Template.instance(); |
||||
return instance.total.get() > instance.livechatRooms.get().length; |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().isLoading.get(); |
||||
}, |
||||
livechatRoom() { |
||||
return Template.instance().livechatRooms.get(); |
||||
}, |
||||
startedAt() { |
||||
return moment(this.ts).format('L LTS'); |
||||
}, |
||||
lastMessage() { |
||||
return moment(this.lm).format('L LTS'); |
||||
}, |
||||
servedBy() { |
||||
return this.servedBy && this.servedBy.username; |
||||
}, |
||||
status() { |
||||
return this.open ? t('Opened') : t('Closed'); |
||||
}, |
||||
isClosed() { |
||||
return !this.open; |
||||
}, |
||||
onSelectAgents() { |
||||
return Template.instance().onSelectAgents; |
||||
}, |
||||
agentModifier() { |
||||
return (filter, text = '') => { |
||||
const f = filter.get(); |
||||
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`; |
||||
}; |
||||
}, |
||||
selectedAgents() { |
||||
return Template.instance().selectedAgents.get(); |
||||
}, |
||||
onClickTagAgent() { |
||||
return Template.instance().onClickTagAgent; |
||||
}, |
||||
customFilters() { |
||||
return Template.instance().customFilters.get(); |
||||
}, |
||||
tagFilters() { |
||||
return Template.instance().tagFilters.get(); |
||||
}, |
||||
tagId() { |
||||
return this; |
||||
}, |
||||
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(); |
||||
}, |
||||
onSelectDepartments() { |
||||
return Template.instance().onSelectDepartments; |
||||
}, |
||||
onTableSort() { |
||||
const { sortDirection, sortBy } = Template.instance(); |
||||
return function(type) { |
||||
if (sortBy.get() === type) { |
||||
return sortDirection.set(sortDirection.get() === 'asc' ? 'desc' : 'asc'); |
||||
} |
||||
sortBy.set(type); |
||||
sortDirection.set('asc'); |
||||
}; |
||||
}, |
||||
sortBy(key) { |
||||
return Template.instance().sortBy.get() === key; |
||||
}, |
||||
sortIcon(key) { |
||||
const { sortDirection, sortBy } = Template.instance(); |
||||
return key === sortBy.get() && sortDirection.get() === 'asc' |
||||
? 'sort-up' |
||||
: 'sort-down'; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatCurrentChats.events({ |
||||
'click .row-link'() { |
||||
FlowRouter.go('live', { id: this._id }); |
||||
}, |
||||
'click .js-load-more'(event, instance) { |
||||
instance.offset.set(instance.offset.get() + ROOMS_COUNT); |
||||
}, |
||||
'click .add-filter-button'(event, instance) { |
||||
event.preventDefault(); |
||||
|
||||
const customFields = instance.customFields.get(); |
||||
const filters = instance.customFilters.get(); |
||||
const tagFilters = instance.tagFilters.get(); |
||||
const options = []; |
||||
|
||||
options.push({ |
||||
name: t('Tags'), |
||||
action: () => { |
||||
tagFilters.push(Random.id()); |
||||
instance.tagFilters.set(tagFilters); |
||||
}, |
||||
}); |
||||
|
||||
for (const field of customFields) { |
||||
if (field.visibility !== 'visible') { |
||||
continue; |
||||
} |
||||
if (field.scope !== 'room') { |
||||
continue; |
||||
} |
||||
|
||||
if (filters.find((filter) => filter.name === field._id)) { |
||||
continue; |
||||
} |
||||
|
||||
options.push({ |
||||
name: field.label, |
||||
action: () => { |
||||
filters.push({ |
||||
_id: field._id, |
||||
name: field._id, |
||||
label: field.label, |
||||
}); |
||||
instance.customFilters.set(filters); |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
const config = { |
||||
popoverClass: 'livechat-current-chats-add-filter', |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
items: options, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
currentTarget: event.currentTarget, |
||||
offsetVertical: event.currentTarget.clientHeight, |
||||
}; |
||||
|
||||
popover.open(config); |
||||
}, |
||||
'click .livechat-current-chats-extra-actions'(event, instance) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
const { currentTarget } = event; |
||||
|
||||
const canRemoveAllClosedRooms = hasPermission('remove-closed-livechat-rooms'); |
||||
const allowedDepartments = () => { |
||||
if (hasRole(Meteor.userId(), ['admin', 'livechat-manager'])) { |
||||
return; |
||||
} |
||||
|
||||
const departments = instance.departments.get(); |
||||
return departments && departments.map((d) => d._id); |
||||
}; |
||||
|
||||
const config = { |
||||
popoverClass: 'livechat-current-chats-add-filter', |
||||
columns: [{ |
||||
groups: [ |
||||
{ |
||||
items: [ |
||||
{ |
||||
icon: 'customize', |
||||
name: t('Clear_filters'), |
||||
action: instance.clearFilters, |
||||
}, |
||||
canRemoveAllClosedRooms |
||||
&& { |
||||
icon: 'trash', |
||||
name: t('Delete_all_closed_chats'), |
||||
modifier: 'alert', |
||||
action: () => { |
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, () => { |
||||
Meteor.call('livechat:removeAllClosedRooms', allowedDepartments(), (err, result) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
|
||||
if (result) { |
||||
instance.loadRooms(instance.filter.get(), instance.offset.get()); |
||||
toastr.success(TAPi18n.__('All_closed_chats_have_been_removed')); |
||||
} |
||||
}); |
||||
}); |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}], |
||||
currentTarget, |
||||
offsetVertical: currentTarget.clientHeight, |
||||
}; |
||||
|
||||
popover.open(config); |
||||
}, |
||||
'click .remove-livechat-tags-filter'(event, instance) { |
||||
event.preventDefault(); |
||||
|
||||
const { id } = event.currentTarget.dataset; |
||||
const tagFilters = instance.tagFilters.get(); |
||||
const index = tagFilters.indexOf(id); |
||||
|
||||
if (index >= 0) { |
||||
tagFilters.splice(index, 1); |
||||
} |
||||
instance.tagFilters.set(tagFilters); |
||||
}, |
||||
'click .remove-livechat-custom-filter'(event, instance) { |
||||
event.preventDefault(); |
||||
const fieldName = event.currentTarget.dataset.name; |
||||
|
||||
const filters = instance.customFilters.get(); |
||||
const index = filters.findIndex((filter) => filter.name === fieldName); |
||||
|
||||
if (index >= 0) { |
||||
filters.splice(index, 1); |
||||
} |
||||
|
||||
instance.customFilters.set(filters); |
||||
}, |
||||
'submit form'(event, instance) { |
||||
event.preventDefault(); |
||||
|
||||
const filter = {}; |
||||
$(':input', event.currentTarget).each(function() { |
||||
if (!this.name) { |
||||
return; |
||||
} |
||||
|
||||
const value = $(this).val(); |
||||
if (!value) { |
||||
return; |
||||
} |
||||
|
||||
if (this.name.startsWith('custom-field-')) { |
||||
if (!filter.customFields) { |
||||
filter.customFields = {}; |
||||
} |
||||
|
||||
filter.customFields[this.name.replace('custom-field-', '')] = value; |
||||
return; |
||||
} |
||||
|
||||
if (this.name === 'tags') { |
||||
if (!filter.tags) { |
||||
filter.tags = []; |
||||
} |
||||
|
||||
if (value) { |
||||
filter.tags.push(value); |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
filter[this.name] = value; |
||||
}); |
||||
|
||||
if (!_.isEmpty(filter.from)) { |
||||
filter.from = moment(filter.from, moment.localeData().longDateFormat('L')).toDate(); |
||||
} else { |
||||
delete filter.from; |
||||
} |
||||
|
||||
if (!_.isEmpty(filter.to)) { |
||||
filter.to = moment(filter.to, moment.localeData().longDateFormat('L')).toDate(); |
||||
} else { |
||||
delete filter.to; |
||||
} |
||||
|
||||
const agents = instance.selectedAgents.get(); |
||||
if (agents && agents.length > 0) { |
||||
filter.agents = [agents[0]]; |
||||
} |
||||
|
||||
const departments = instance.selectedDepartments.get(); |
||||
if (departments && departments.length > 0) { |
||||
filter.department = [departments[0]]; |
||||
} |
||||
|
||||
instance.filter.set(filter); |
||||
instance.offset.set(0); |
||||
storeFilters(filter); |
||||
}, |
||||
'click .remove-livechat-room'(event, instance) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
|
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, async (confirmed) => { |
||||
if (!confirmed) { |
||||
return; |
||||
} |
||||
await call('livechat:removeRoom', this._id); |
||||
instance.loadRooms(instance.filter.get(), instance.offset.get()); |
||||
modal.open({ |
||||
title: t('Deleted'), |
||||
text: t('Room_has_been_deleted'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
}); |
||||
}, |
||||
|
||||
'input [id=agent]'(event, template) { |
||||
const input = event.currentTarget; |
||||
if (input.value === '') { |
||||
template.selectedAgents.set([]); |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatCurrentChats.onCreated(async function() { |
||||
this.isLoading = new ReactiveVar(false); |
||||
this.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
this.filter = new ReactiveVar(loadStoredFilters()); |
||||
this.livechatRooms = new ReactiveVar([]); |
||||
this.selectedAgents = new ReactiveVar([]); |
||||
this.customFilters = new ReactiveVar([]); |
||||
this.customFields = new ReactiveVar([]); |
||||
this.tagFilters = new ReactiveVar([]); |
||||
this.selectedDepartments = new ReactiveVar([]); |
||||
this.sortBy = new ReactiveVar('ts'); |
||||
this.sortDirection = new ReactiveVar('desc'); |
||||
|
||||
this.onSelectDepartments = ({ item: department }) => { |
||||
department.text = department.name; |
||||
this.selectedDepartments.set([department]); |
||||
}; |
||||
|
||||
this.onClickTagDepartment = () => { |
||||
this.selectedDepartments.set([]); |
||||
}; |
||||
|
||||
const mountArrayQueryParameters = (label, items, index) => items.reduce((acc, item) => { |
||||
const isTheLastElement = index === items.length - 1; |
||||
acc += `${ label }[]=${ item }${ isTheLastElement ? '' : '&' }`; |
||||
return acc; |
||||
}, ''); |
||||
|
||||
const mountUrlWithParams = (filter, offset, sort) => { |
||||
const { status, agents, department, from, to, tags, customFields, name: roomName } = filter; |
||||
let url = `livechat/rooms?count=${ ROOMS_COUNT }&offset=${ offset }&sort=${ JSON.stringify(sort) }`; |
||||
const dateRange = {}; |
||||
if (status) { |
||||
url += `&open=${ status === 'opened' }`; |
||||
} |
||||
if (department && Array.isArray(department) && department.length) { |
||||
url += `&departmentId=${ department[0]._id }`; |
||||
} |
||||
if (from) { |
||||
dateRange.start = `${ moment(new Date(from)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`; |
||||
} |
||||
if (to) { |
||||
dateRange.end = `${ moment(new Date(to).setHours(23, 59, 59)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`; |
||||
} |
||||
if (tags) { |
||||
url += `&${ mountArrayQueryParameters('tags', tags) }`; |
||||
} |
||||
if (agents && Array.isArray(agents) && agents.length) { |
||||
url += `&${ mountArrayQueryParameters('agents', agents.map((agent) => agent._id)) }`; |
||||
} |
||||
if (customFields) { |
||||
url += `&customFields=${ JSON.stringify(customFields) }`; |
||||
} |
||||
if (Object.keys(dateRange).length) { |
||||
url += `&createdAt=${ JSON.stringify(dateRange) }`; |
||||
} |
||||
if (roomName) { |
||||
url += `&roomName=${ roomName }`; |
||||
} |
||||
return url; |
||||
}; |
||||
|
||||
this.loadDefaultFilters = () => { |
||||
const defaultFilters = this.filter.get(); |
||||
|
||||
Object.keys(defaultFilters).forEach((key) => { |
||||
const value = defaultFilters[key]; |
||||
if (!value) { |
||||
return; |
||||
} |
||||
|
||||
switch (key) { |
||||
case 'agents': |
||||
return this.selectedAgents.set(value); |
||||
case 'department': |
||||
return this.selectedDepartments.set(value); |
||||
case 'from': |
||||
case 'to': |
||||
return $(`#${ key }`).datepicker('setDate', new Date(value)); |
||||
} |
||||
|
||||
$(`#${ key }`).val(value); |
||||
}); |
||||
}; |
||||
|
||||
this.clearFilters = () => { |
||||
removeStoredFilters(); |
||||
$('#form-filters').get(0).reset(); |
||||
this.selectedAgents.set([]); |
||||
this.selectedDepartments.set([]); |
||||
this.filter.set({}); |
||||
}; |
||||
|
||||
this.loadRooms = async (filter, offset, sort) => { |
||||
this.isLoading.set(true); |
||||
const { rooms, total } = await APIClient.v1.get(mountUrlWithParams(filter, offset, sort)); |
||||
this.total.set(total); |
||||
if (offset === 0) { |
||||
this.livechatRooms.set(rooms); |
||||
} else { |
||||
this.livechatRooms.set(this.livechatRooms.get().concat(rooms)); |
||||
} |
||||
this.isLoading.set(false); |
||||
}; |
||||
|
||||
this.onSelectAgents = ({ item: agent }) => { |
||||
this.selectedAgents.set([agent]); |
||||
}; |
||||
|
||||
this.onClickTagAgent = ({ username }) => { |
||||
this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username)); |
||||
}; |
||||
|
||||
this.autorun(async () => { |
||||
const filter = this.filter.get(); |
||||
const offset = this.offset.get(); |
||||
const { sortDirection, sortBy } = Template.instance(); |
||||
this.loadRooms(filter, offset, { [sortBy.get()]: sortDirection.get() === 'asc' ? 1 : -1 }); |
||||
}); |
||||
|
||||
Meteor.call('livechat:getCustomFields', (err, customFields) => { |
||||
if (customFields) { |
||||
this.customFields.set(customFields); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
Template.livechatCurrentChats.onRendered(function() { |
||||
this.$('.input-daterange').datepicker({ |
||||
autoclose: true, |
||||
todayHighlight: true, |
||||
format: moment.localeData().longDateFormat('L').toLowerCase(), |
||||
}); |
||||
|
||||
this.loadDefaultFilters(); |
||||
}); |
||||
@ -1,57 +0,0 @@ |
||||
<template name="livechatCustomFieldForm"> |
||||
{{#requiresPermission 'view-livechat-customfields'}} |
||||
<form id="customField-form" data-id="{{customField._id}}"> |
||||
<div class="rocket-form"> |
||||
{{#if Template.subscriptionsReady}} |
||||
<fieldset> |
||||
<div class="input-line"> |
||||
<label>{{_ "Field"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element custom-field-input" name="field" value="{{customField._id}}" readonly="{{$exists customField._id}}" placeholder="{{_ "Field"}}" /> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Label"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element custom-field-input" name="label" value="{{customField.label}}" placeholder="{{_ "Label"}}" /> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Scope"}}</label> |
||||
<div> |
||||
<select name="scope" class="rc-input__element custom-field-input"> |
||||
<option value="visitor" selected="{{$eq customField.scope 'visitor'}}">{{_ "Visitor"}}</option> |
||||
<option value="room" selected="{{$eq customField.scope 'room'}}">{{_ "Room"}}</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Visibility"}}</label> |
||||
<div> |
||||
<select name="visibility" class="rc-input__element custom-field-input"> |
||||
<option value="visible" selected="{{$eq customField.visibility 'visible'}}">{{_ "Visible"}}</option> |
||||
<option value="hidden" selected="{{$eq customField.visibility 'hidden'}}">{{_ "Hidden"}}</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Validation"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element custom-field-input" name="regexp" value="{{customField.regexp}}" placeholder="{{_ "Regexp_validation"}}" /> |
||||
</div> |
||||
</div> |
||||
{{#if customFieldsTemplate}} |
||||
{{> Template.dynamic template=customFieldsTemplate data=dataContext }} |
||||
{{/if}} |
||||
</fieldset> |
||||
<div class="rc-button__group submit"> |
||||
<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> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,100 +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 toastr from 'toastr'; |
||||
|
||||
import { t, handleError } from '../../../../utils'; |
||||
import { getCustomFormTemplate } from './customTemplates/register'; |
||||
import './livechatCustomFieldForm.html'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
Template.livechatCustomFieldForm.helpers({ |
||||
customField() { |
||||
return Template.instance().customField.get(); |
||||
}, |
||||
|
||||
customFieldsTemplate() { |
||||
return getCustomFormTemplate('livechatCustomFieldsAdditionalForm'); |
||||
}, |
||||
|
||||
dataContext() { |
||||
// To make the dynamic template reactive we need to pass a ReactiveVar through the data property
|
||||
// because only the dynamic template data will be reloaded
|
||||
return Template.instance().localFields; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatCustomFieldForm.events({ |
||||
'submit #customField-form'(e, instance) { |
||||
e.preventDefault(); |
||||
const $btn = instance.$('button.save'); |
||||
|
||||
const _id = $(e.currentTarget).data('id'); |
||||
const field = instance.$('input[name=field]').val(); |
||||
const label = instance.$('input[name=label]').val(); |
||||
const scope = instance.$('select[name=scope]').val(); |
||||
const visibility = instance.$('select[name=visibility]').val(); |
||||
const regexp = instance.$('input[name=regexp]').val(); |
||||
|
||||
if (!/^[0-9a-zA-Z-_]+$/.test(field)) { |
||||
return toastr.error(t('error-invalid-custom-field-name')); |
||||
} |
||||
|
||||
if (label.trim() === '') { |
||||
return toastr.error(t('Please_fill_a_label')); |
||||
} |
||||
|
||||
const oldBtnValue = $btn.html(); |
||||
$btn.html(t('Saving')); |
||||
|
||||
const customFieldData = { |
||||
field, |
||||
label, |
||||
scope: scope.trim(), |
||||
visibility: visibility.trim(), |
||||
regexp: regexp.trim(), |
||||
}; |
||||
|
||||
instance.$('.additional-field').each((i, el) => { |
||||
const elField = instance.$(el); |
||||
const name = elField.attr('name'); |
||||
let value = elField.val(); |
||||
if (['true', 'false'].includes(value) && el.tagName === 'SELECT') { |
||||
value = value === 'true'; |
||||
} |
||||
customFieldData[name] = value; |
||||
}); |
||||
|
||||
Meteor.call('livechat:saveCustomField', _id, customFieldData, function(error) { |
||||
$btn.html(oldBtnValue); |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
|
||||
toastr.success(t('Saved')); |
||||
FlowRouter.go('livechat-customfields'); |
||||
}); |
||||
}, |
||||
|
||||
'click button.back'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
FlowRouter.go('livechat-customfields'); |
||||
}, |
||||
|
||||
'change .custom-field-input'(e, instance) { |
||||
const { target: { name, value } } = e; |
||||
instance.localFields.set({ ...instance.localFields.get(), [name]: value }); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatCustomFieldForm.onCreated(async function() { |
||||
this.customField = new ReactiveVar({}); |
||||
this.localFields = new ReactiveVar({}); |
||||
|
||||
const { customField } = await APIClient.v1.get(`livechat/custom-fields/${ FlowRouter.getParam('_id') }`); |
||||
if (customField) { |
||||
this.customField.set(customField); |
||||
this.localFields.set({ ...customField }); |
||||
} |
||||
}); |
||||
@ -1,37 +0,0 @@ |
||||
<template name="livechatCustomFields"> |
||||
{{#requiresPermission 'view-livechat-customfields'}} |
||||
<div class="rc-table-content"> |
||||
{{#table fixed='true' onScroll=onTableScroll}} |
||||
<thead> |
||||
<tr> |
||||
<th><div class="table-fake-th">{{_ "Field"}}</div></th> |
||||
<th width="25%"><div class="table-fake-th">{{_ "Label"}}</div></th> |
||||
<th width="25%"><div class="table-fake-th">{{_ "Scope"}}</div></th> |
||||
<th width="25%"><div class="table-fake-th">{{_ "Visibility"}}</div></th> |
||||
<th width="40px"> </th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each customFields}} |
||||
<tr class="custom-field-info row-link" data-id="{{_id}}"> |
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{_id}}</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td>{{label}}</td> |
||||
<td>{{scope}}</td> |
||||
<td>{{visibility}}</td> |
||||
<td><a href="#remove" class="remove-custom-field"><i class="icon-trash"></i></a></td> |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
{{/table}} |
||||
</div> |
||||
<div class="rc-button__group"> |
||||
<a href="{{pathFor 'livechat-customfield-new'}}" class="rc-button rc-button--primary">{{_ "New_Custom_Field"}}</a> |
||||
</div> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,83 +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 { modal } from '../../../../ui-utils'; |
||||
import { t, handleError, APIClient } from '../../../../utils/client'; |
||||
import './livechatCustomFields.html'; |
||||
|
||||
const CUSTOM_FIELDS_COUNT = 50; |
||||
|
||||
Template.livechatCustomFields.helpers({ |
||||
customFields() { |
||||
return Template.instance().customFields.get(); |
||||
}, |
||||
onTableScroll() { |
||||
const instance = Template.instance(); |
||||
return function(currentTarget) { |
||||
if (currentTarget.offsetHeight + currentTarget.scrollTop < currentTarget.scrollHeight - 100) { |
||||
return; |
||||
} |
||||
const customFields = instance.customFields.get(); |
||||
if (instance.total.get() <= customFields.length) { |
||||
return; |
||||
} |
||||
return instance.offset.set(instance.offset.get() + CUSTOM_FIELDS_COUNT); |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatCustomFields.events({ |
||||
'click .remove-custom-field'(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:removeCustomField', this._id, function(error/* , result*/) { |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
instance.offset.set(0); |
||||
modal.open({ |
||||
title: t('Removed'), |
||||
text: t('Field_removed'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
}); |
||||
}); |
||||
}, |
||||
|
||||
'click .custom-field-info'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
FlowRouter.go('livechat-customfield-edit', { _id: this._id }); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatCustomFields.onCreated(function() { |
||||
this.customFields = new ReactiveVar([]); |
||||
this.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
|
||||
this.autorun(async () => { |
||||
const offset = this.offset.get(); |
||||
const { customFields, total } = await APIClient.v1.get(`livechat/custom-fields?count=${ CUSTOM_FIELDS_COUNT }&offset=${ offset }`); |
||||
if (offset === 0) { |
||||
this.customFields.set(customFields); |
||||
} else { |
||||
this.customFields.set(this.customFields.get().concat(customFields)); |
||||
} |
||||
this.total.set(total); |
||||
}); |
||||
}); |
||||
@ -1,10 +0,0 @@ |
||||
<template name="livechatInstallation"> |
||||
{{#requiresPermission 'view-livechat-manager'}} |
||||
<p>{{{_ "To_install_RocketChat_Livechat_in_your_website_copy_paste_this_code_above_the_last_body_tag_on_your_site"}}}</p> |
||||
<div class="livechat-code"> |
||||
<textarea class="clipboard" data-clipboard-target=".livechat-code textarea">{{script}}</textarea> |
||||
<button class="rc-button rc-button--primary clipboard" data-clipboard-target=".livechat-code textarea"><i class="icon-docs"></i>{{_ "Copy_to_clipboard"}}</button> |
||||
</div> |
||||
|
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,21 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import s from 'underscore.string'; |
||||
|
||||
import { settings } from '../../../../settings'; |
||||
import './livechatInstallation.html'; |
||||
|
||||
Template.livechatInstallation.helpers({ |
||||
script() { |
||||
const siteUrl = s.rtrim(settings.get('Site_Url'), '/'); |
||||
return `<!-- Start of Rocket.Chat Livechat Script -->
|
||||
<script type="text/javascript"> |
||||
(function(w, d, s, u) { |
||||
w.RocketChat = function(c) { w.RocketChat._.push(c) }; w.RocketChat._ = []; w.RocketChat.url = u; |
||||
var h = d.getElementsByTagName(s)[0], j = d.createElement(s); |
||||
j.async = true; j.src = '${ siteUrl }/livechat/rocketchat-livechat.min.js?_=201903270000'; |
||||
h.parentNode.insertBefore(j, h); |
||||
})(window, document, 'script', '${ siteUrl }/livechat'); |
||||
</script> |
||||
<!-- End of Rocket.Chat Livechat Script -->`;
|
||||
}, |
||||
}); |
||||
@ -1,88 +0,0 @@ |
||||
<template name="livechatManagers"> |
||||
{{#requiresPermission 'manage-livechat-managers'}} |
||||
<form id="form-manager" class="form-inline"> |
||||
<div class="form-group"> |
||||
{{> livechatAutocompleteUser |
||||
onClickTag=onClickTagManagers |
||||
list=selectedManagers |
||||
deleteLastItem=deleteLastManager |
||||
onSelect=onSelectManagers |
||||
collection='UserAndRoom' |
||||
endpoint='users.autocomplete' |
||||
field='username' |
||||
sort='username' |
||||
label="Search_by_username" |
||||
placeholder="Search_by_username" |
||||
name="username" |
||||
exceptions=exceptionsManagers |
||||
icon="at" |
||||
noMatchTemplate="userSearchEmpty" |
||||
templateItem="popupList_item_default" |
||||
modifier=managerModifier |
||||
}} |
||||
</div> |
||||
<div class="form-group"> |
||||
<button name="add" class="rc-button rc-button--primary add" disabled='{{isloading}}'>{{_ "Add"}}</button> |
||||
</div> |
||||
</form> |
||||
<div class="rc-table-content"> |
||||
<form class="search-form" role="form"> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{#if isloading}} |
||||
{{> loading }} |
||||
{{else}} |
||||
{{> icon block="rc-input__icon-svg" icon="magnifier" }} |
||||
{{/if}} |
||||
</div> |
||||
<input id="managers-filter" type="text" class="rc-input__element" |
||||
placeholder="{{_ "Search"}}" autofocus dir="auto"> |
||||
</div> |
||||
</form> |
||||
<div class="results"> |
||||
{{{_ "Showing_results" managers.length}}} |
||||
</div> |
||||
|
||||
{{#table fixed='true' onScroll=onTableScroll}} |
||||
<thead> |
||||
<tr> |
||||
<th><div class="table-fake-th">{{_ "Name"}}</div></th> |
||||
<th width="33%"><div class="table-fake-th">{{_ "Username"}}</div></th> |
||||
<th width="33%"><div class="table-fake-th">{{_ "Email"}}</div></th> |
||||
<td width="40px"> </td> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each managers}} |
||||
<tr class="rc-table-tr user-info row-link" data-id="{{_id}}"> |
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-avatar">{{> avatar username=username}}</div> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{name}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{fname}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td>{{username}}</td> |
||||
<td>{{emailAddress}}</td> |
||||
<td> |
||||
<a href="#remove" class="remove-manager"> |
||||
<i class="icon-trash"></i> |
||||
</a> |
||||
</td> |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
{{/table}} |
||||
</div> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,192 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { modal, call } from '../../../../ui-utils'; |
||||
import { t, handleError } from '../../../../utils'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
import './livechatManagers.html'; |
||||
|
||||
const MANAGERS_COUNT = 50; |
||||
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500; |
||||
|
||||
const getUsername = (user) => user.username; |
||||
Template.livechatManagers.helpers({ |
||||
exceptionsManagers() { |
||||
const { selectedManagers } = Template.instance(); |
||||
return Template.instance() |
||||
.managers |
||||
.get() |
||||
.map(getUsername) |
||||
.concat(selectedManagers.get().map(getUsername)); |
||||
}, |
||||
deleteLastManager() { |
||||
const i = Template.instance(); |
||||
return () => { |
||||
const arr = i.selectedManagers.curValue; |
||||
arr.pop(); |
||||
i.selectedManagers.set(arr); |
||||
}; |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().state.get('loading'); |
||||
}, |
||||
managers() { |
||||
return Template.instance().managers.get(); |
||||
}, |
||||
emailAddress() { |
||||
if (this.emails && this.emails.length > 0) { |
||||
return this.emails[0].address; |
||||
} |
||||
}, |
||||
managerModifier() { |
||||
return (filter, text = '') => { |
||||
const f = filter.get(); |
||||
return `@${ |
||||
f.length === 0 |
||||
? text |
||||
: text.replace( |
||||
new RegExp(filter.get()), |
||||
(part) => `<strong>${ part }</strong>`, |
||||
) |
||||
}`;
|
||||
}; |
||||
}, |
||||
onSelectManagers() { |
||||
return Template.instance().onSelectManagers; |
||||
}, |
||||
selectedManagers() { |
||||
return Template.instance().selectedManagers.get(); |
||||
}, |
||||
onClickTagManagers() { |
||||
return Template.instance().onClickTagManagers; |
||||
}, |
||||
onTableScroll() { |
||||
const instance = Template.instance(); |
||||
return function(currentTarget) { |
||||
if (currentTarget.offsetHeight + currentTarget.scrollTop < currentTarget.scrollHeight - 100) { |
||||
return; |
||||
} |
||||
const managers = instance.managers.get(); |
||||
if (instance.total.get() > managers.length) { |
||||
instance.offset.set(instance.offset.get() + MANAGERS_COUNT); |
||||
} |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatManagers.events({ |
||||
'click .remove-manager'(e, instance) { |
||||
e.preventDefault(); |
||||
|
||||
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:removeManager', this.username, function( |
||||
error, /* , result*/ |
||||
) { |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
instance.loadManagers(instance.filter.get(), 0); |
||||
modal.open({ |
||||
title: t('Removed'), |
||||
text: t('Manager_removed'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
}); |
||||
}, |
||||
); |
||||
}, |
||||
async 'submit #form-manager'(e, instance) { |
||||
e.preventDefault(); |
||||
const { selectedManagers, state } = instance; |
||||
|
||||
const users = selectedManagers.get(); |
||||
|
||||
if (!users.length) { |
||||
return; |
||||
} |
||||
|
||||
state.set('loading', true); |
||||
try { |
||||
await Promise.all( |
||||
users.map(({ username }) => call('livechat:addManager', username)), |
||||
); |
||||
selectedManagers.set([]); |
||||
} finally { |
||||
state.set('loading', false); |
||||
instance.loadManagers(instance.filter.get(), 0); |
||||
} |
||||
}, |
||||
'keydown #managers-filter'(e) { |
||||
if (e.which === 13) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
'keyup #managers-filter': _.debounce((e, t) => { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.filter.set(e.currentTarget.value); |
||||
t.offset.set(0); |
||||
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS), |
||||
}); |
||||
|
||||
Template.livechatManagers.onCreated(function() { |
||||
const instance = this; |
||||
this.offset = new ReactiveVar(0); |
||||
this.filter = new ReactiveVar(''); |
||||
this.state = new ReactiveDict({ |
||||
loading: false, |
||||
}); |
||||
this.total = new ReactiveVar(0); |
||||
this.managers = new ReactiveVar([]); |
||||
this.selectedManagers = new ReactiveVar([]); |
||||
|
||||
this.onSelectManagers = ({ item: manager }) => { |
||||
this.selectedManagers.set([...this.selectedManagers.curValue, manager]); |
||||
}; |
||||
|
||||
this.onClickTagManagers = ({ username }) => { |
||||
this.selectedManagers.set( |
||||
this.selectedManagers.curValue.filter((user) => user.username !== username), |
||||
); |
||||
}; |
||||
|
||||
this.loadManagers = _.debounce(async (filter, offset) => { |
||||
this.state.set('loading', true); |
||||
let url = `livechat/users/manager?count=${ MANAGERS_COUNT }&offset=${ offset }`; |
||||
if (filter) { |
||||
url += `&text=${ encodeURIComponent(filter) }`; |
||||
} |
||||
const { users, total } = await APIClient.v1.get(url); |
||||
this.total.set(total); |
||||
if (offset === 0) { |
||||
this.managers.set(users); |
||||
} else { |
||||
this.managers.set(this.managers.get().concat(users)); |
||||
} |
||||
this.state.set('loading', false); |
||||
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS); |
||||
|
||||
this.autorun(async () => { |
||||
const filter = instance.filter.get(); |
||||
const offset = instance.offset.get(); |
||||
return this.loadManagers(filter, offset); |
||||
}); |
||||
}); |
||||
@ -1,33 +0,0 @@ |
||||
<template name="livechatTriggers"> |
||||
{{#requiresPermission 'view-livechat-triggers'}} |
||||
<div class="rc-table-content"> |
||||
{{#table fixed='true'}} |
||||
<thead> |
||||
<tr> |
||||
<th><div class="table-fake-th">{{_ "Name"}}</div></th> |
||||
<th width="50%"><div class="table-fake-th">{{_ "Description"}}</div></th> |
||||
<th width="20%"><div class="table-fake-th">{{_ "Enabled"}}</div></th> |
||||
<th width="40px"> </th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each triggers}} |
||||
<tr class="trigger-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>{{#if enabled}}{{_ "Yes"}}{{else}}{{_ "No"}}{{/if}}</td> |
||||
<td><a href="#remove" class="remove-trigger"><i class="icon-trash"></i></a></td> |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
{{/table}} |
||||
</div> |
||||
|
||||
<div class="rc-button__group"> |
||||
<a href="{{pathFor 'livechat-trigger-new'}}" class="rc-button rc-button--primary">{{_ "New_Trigger"}}</a> |
||||
</div> |
||||
|
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,62 +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 { modal } from '../../../../ui-utils'; |
||||
import { t, handleError } from '../../../../utils'; |
||||
import './livechatTriggers.html'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
const loadTriggers = async (instance) => { |
||||
const { triggers } = await APIClient.v1.get('livechat/triggers'); |
||||
instance.triggers.set(triggers); |
||||
}; |
||||
|
||||
Template.livechatTriggers.helpers({ |
||||
triggers() { |
||||
return Template.instance().triggers.get(); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggers.events({ |
||||
'click .remove-trigger'(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:removeTrigger', this._id, async function(error/* , result*/) { |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
await loadTriggers(instance); |
||||
modal.open({ |
||||
title: t('Removed'), |
||||
text: t('Trigger_removed'), |
||||
type: 'success', |
||||
timer: 1000, |
||||
showConfirmButton: false, |
||||
}); |
||||
}); |
||||
}); |
||||
}, |
||||
|
||||
'click .trigger-info'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
FlowRouter.go('livechat-trigger-edit', { _id: this._id }); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggers.onCreated(async function() { |
||||
this.triggers = new ReactiveVar([]); |
||||
await loadTriggers(this); |
||||
}); |
||||
@ -1,62 +0,0 @@ |
||||
<template name="livechatTriggersForm"> |
||||
{{#requiresPermission 'view-livechat-triggers'}} |
||||
<form id="trigger-form"> |
||||
<div class="rocket-form"> |
||||
<fieldset> |
||||
<div class="input-line"> |
||||
<label>{{_ "Enabled"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="enabled" value="1" checked="{{$eq enabled true}}" /> {{_ "Yes"}}</label> |
||||
<label><input type="radio" name="enabled" value="0" checked="{{$eq enabled false}}" /> {{_ "No"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Run_only_once_for_each_visitor"}}</label> |
||||
<div> |
||||
<label><input type="radio" name="runOnce" value="1" checked="{{$eq runOnce true}}" /> {{_ "Yes"}}</label> |
||||
<label><input type="radio" name="runOnce" value="0" checked="{{$eq runOnce false}}" /> {{_ "No"}}</label> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Name"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="name" value="{{name}}" /> |
||||
</div> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label>{{_ "Description"}}</label> |
||||
<div> |
||||
<input type="text" class="rc-input__element" name="description" value="{{description}}" /> |
||||
</div> |
||||
</div> |
||||
</fieldset> |
||||
<fieldset> |
||||
<legend>{{_ "Condition"}}</legend> |
||||
<div class="conditions"> |
||||
{{#each conditions}} |
||||
{{> livechatTriggerCondition}} |
||||
{{/each}} |
||||
{{#unless conditions}} |
||||
{{> livechatTriggerCondition}} |
||||
{{/unless}} |
||||
</div> |
||||
</fieldset> |
||||
<fieldset> |
||||
<legend>{{_ "Action"}}</legend> |
||||
<div class="actions"> |
||||
{{#each actions}} |
||||
{{> livechatTriggerAction}} |
||||
{{/each}} |
||||
{{#unless actions}} |
||||
{{> livechatTriggerAction}} |
||||
{{/unless}} |
||||
</div> |
||||
</fieldset> |
||||
<div class="rc-button__group submit"> |
||||
<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> |
||||
</div> |
||||
</form> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,118 +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 toastr from 'toastr'; |
||||
|
||||
import { t, handleError } from '../../../../utils'; |
||||
import './livechatTriggersForm.html'; |
||||
import { APIClient } from '../../../../utils/client'; |
||||
|
||||
Template.livechatTriggersForm.helpers({ |
||||
name() { |
||||
const trigger = Template.instance().trigger.get(); |
||||
return trigger && trigger.name; |
||||
}, |
||||
description() { |
||||
const trigger = Template.instance().trigger.get(); |
||||
return trigger && trigger.description; |
||||
}, |
||||
enabled() { |
||||
const trigger = Template.instance().trigger.get(); |
||||
return trigger && trigger.enabled; |
||||
}, |
||||
runOnce() { |
||||
const trigger = Template.instance().trigger.get(); |
||||
return (trigger && trigger.runOnce) || false; |
||||
}, |
||||
conditions() { |
||||
const trigger = Template.instance().trigger.get(); |
||||
if (!trigger) { |
||||
return []; |
||||
} |
||||
|
||||
return trigger.conditions; |
||||
}, |
||||
actions() { |
||||
const trigger = Template.instance().trigger.get(); |
||||
if (!trigger) { |
||||
return []; |
||||
} |
||||
|
||||
return trigger.actions; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggersForm.events({ |
||||
'submit #trigger-form'(e, instance) { |
||||
e.preventDefault(); |
||||
const $btn = instance.$('button.save'); |
||||
|
||||
const oldBtnValue = $btn.html(); |
||||
$btn.html(t('Saving')); |
||||
|
||||
const data = { |
||||
_id: FlowRouter.getParam('_id'), |
||||
name: instance.$('input[name=name]').val(), |
||||
description: instance.$('input[name=description]').val(), |
||||
enabled: instance.$('input[name=enabled]:checked').val() === '1', |
||||
runOnce: instance.$('input[name=runOnce]:checked').val() === '1', |
||||
conditions: [], |
||||
actions: [], |
||||
}; |
||||
|
||||
$('.each-condition').each(function() { |
||||
data.conditions.push({ |
||||
name: $('.trigger-condition', this).val(), |
||||
value: $(`.${ $('.trigger-condition', this).val() }-value`).val(), |
||||
}); |
||||
}); |
||||
|
||||
$('.each-action').each(function() { |
||||
if ($('.trigger-action', this).val() === 'send-message') { |
||||
const params = { |
||||
sender: $('[name=send-message-sender]', this).val(), |
||||
msg: $('[name=send-message-msg]', this).val(), |
||||
}; |
||||
if (params.sender === 'custom') { |
||||
params.name = $('[name=send-message-name]', this).val(); |
||||
} |
||||
data.actions.push({ |
||||
name: $('.trigger-action', this).val(), |
||||
params, |
||||
}); |
||||
} else { |
||||
data.actions.push({ |
||||
name: $('.trigger-action', this).val(), |
||||
value: $(`.${ $('.trigger-action', this).val() }-value`).val(), |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
Meteor.call('livechat:saveTrigger', data, function(error/* , result*/) { |
||||
$btn.html(oldBtnValue); |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
|
||||
FlowRouter.go('livechat-triggers'); |
||||
|
||||
toastr.success(t('Saved')); |
||||
}); |
||||
}, |
||||
|
||||
'click button.back'(e/* , instance*/) { |
||||
e.preventDefault(); |
||||
FlowRouter.go('livechat-triggers'); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggersForm.onCreated(async function() { |
||||
this.trigger = new ReactiveVar({}); |
||||
const id = FlowRouter.getParam('_id'); |
||||
|
||||
if (id) { |
||||
const { trigger } = await APIClient.v1.get(`livechat/triggers/${ id }`); |
||||
this.trigger.set(trigger); |
||||
} |
||||
}); |
||||
@ -1,20 +0,0 @@ |
||||
<template name="livechatTriggerAction"> |
||||
<div class="input-line each-action"> |
||||
<div class="trigger-option"> |
||||
<select name="action" class="trigger-action rc-input__element"> |
||||
<option value="send-message">{{_ "Send_a_message"}}</option> |
||||
</select> |
||||
</div> |
||||
<div class="trigger-value"> |
||||
<div class="send-message {{hiddenValue 'send-message'}}"> |
||||
<select name="send-message-sender" class="rc-input__element"> |
||||
<option value="">{{_ "Select_an_option"}}</option> |
||||
<option value="queue" selected="{{senderSelected 'queue'}}" disabled="{{disableGetNextAgent}}">{{_ "Impersonate_next_agent_from_queue"}}</option> |
||||
<option value="custom" selected="{{senderSelected 'custom'}}">{{_ "Custom_agent"}}</option> |
||||
</select> |
||||
<input type="text" name="send-message-name" placeholder="{{_ "Name_of_agent"}}" value="{{params.name}}" class="{{showCustomName}} rc-input__element"> |
||||
<textarea name="send-message-msg" placeholder="{{_ "Message"}}" rows="3" class="rc-input__element">{{params.msg}}</textarea> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
@ -1,54 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import './livechatTriggerAction.html'; |
||||
|
||||
Template.livechatTriggerAction.helpers({ |
||||
hiddenValue(current) { |
||||
if (this.name === undefined && Template.instance().firstAction) { |
||||
Template.instance().firstAction = false; |
||||
return ''; |
||||
} if (this.name !== current) { |
||||
return 'hidden'; |
||||
} |
||||
}, |
||||
showCustomName() { |
||||
return Template.instance().sender.get() === 'custom' ? '' : 'hidden'; |
||||
}, |
||||
senderSelected(current) { |
||||
return !!(this.params && this.params.sender === current); |
||||
}, |
||||
disableGetNextAgent() { |
||||
const config = Template.instance().routingConfig.get(); |
||||
return !config.enableTriggerAction; |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggerAction.events({ |
||||
'change .trigger-action'(e, instance) { |
||||
instance.$('.trigger-action-value ').addClass('hidden'); |
||||
instance.$(`.${ e.currentTarget.value }`).removeClass('hidden'); |
||||
}, |
||||
'change [name=send-message-sender]'(e, instance) { |
||||
instance.sender.set(e.currentTarget.value); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggerAction.onCreated(function() { |
||||
this.firstAction = true; |
||||
|
||||
this.sender = new ReactiveVar(''); |
||||
this.routingConfig = new ReactiveVar({}); |
||||
|
||||
Meteor.call('livechat:getRoutingConfig', (err, config) => { |
||||
if (config) { |
||||
this.routingConfig.set(config); |
||||
} |
||||
}); |
||||
|
||||
const data = Template.currentData(); |
||||
if (data && data.name === 'send-message') { |
||||
this.sender.set(data.params.sender); |
||||
} |
||||
}); |
||||
@ -1,18 +0,0 @@ |
||||
<template name="livechatTriggerCondition"> |
||||
<div class="input-line each-condition"> |
||||
<div class="trigger-option"> |
||||
<select name="condition" class="trigger-condition rc-input__element"> |
||||
<option value="page-url" selected="{{conditionSelected 'page-url'}}">{{_ "Visitor_page_URL"}}</option> |
||||
<option value="time-on-site" selected="{{conditionSelected 'time-on-site'}}">{{_ "Visitor_time_on_site"}}</option> |
||||
</select> |
||||
</div> |
||||
<div class="trigger-value"> |
||||
<div class="page-url trigger-condition-value {{hiddenValue 'page-url'}}"> |
||||
<input type="text" name="page-url-value" class="page-url-value rc-input__element" placeholder="{{_ "Enter_a_regex"}}" value="{{valueFor 'page-url'}}"> |
||||
</div> |
||||
<div class="time-on-site trigger-condition-value {{hiddenValue 'time-on-site'}}"> |
||||
<input type="number" name="time-on-site-value" class="time-on-site-value rc-input__element" placeholder="{{_ "Time_in_seconds"}}" value="{{valueFor 'time-on-site'}}"> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
@ -1,34 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import './livechatTriggerCondition.html'; |
||||
|
||||
Template.livechatTriggerCondition.helpers({ |
||||
hiddenValue(current) { |
||||
if (this.name === undefined && Template.instance().firstCondition) { |
||||
Template.instance().firstCondition = false; |
||||
return ''; |
||||
} if (this.name !== current) { |
||||
return 'hidden'; |
||||
} |
||||
}, |
||||
conditionSelected(current) { |
||||
if (this.name === current) { |
||||
return 'selected'; |
||||
} |
||||
}, |
||||
valueFor(condition) { |
||||
if (this.name === condition) { |
||||
return this.value; |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggerCondition.events({ |
||||
'change .trigger-condition'(e, instance) { |
||||
instance.$('.trigger-condition-value ').addClass('hidden'); |
||||
instance.$(`.${ e.currentTarget.value }`).removeClass('hidden'); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatTriggerCondition.onCreated(function() { |
||||
this.firstCondition = true; |
||||
}); |
||||
@ -1,19 +0,0 @@ |
||||
<template name="livechatFlex"> |
||||
<aside class="sidebar-light sidebar--medium" role="navigation"> |
||||
<header class="sidebar-flex__header"> |
||||
<h1 class="sidebar-flex__title">{{_ "Omnichannel"}}</h1> |
||||
{{#unless embeddedVersion}} |
||||
<button class="sidebar-flex__close-button" data-action="close"> |
||||
{{> icon block="sidebar-flex__close-icon" icon="cross"}} |
||||
</button> |
||||
{{/unless}} |
||||
</header> |
||||
<div class="rooms-list {{#if embeddedVersion}}rooms-list--embedded{{/if}}"> |
||||
<ul class="rooms-list__list"> |
||||
{{#each sidebarItems}} |
||||
{{> sidebarItem menuItem title false slug }} |
||||
{{/each}} |
||||
</ul> |
||||
</div> |
||||
</aside> |
||||
</template> |
||||
@ -1,34 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
|
||||
import { SideNav, Layout } from '../../../../ui-utils'; |
||||
import { t } from '../../../../utils'; |
||||
import { hasPermission } from '../../../../authorization'; |
||||
import './livechatFlex.html'; |
||||
import { sidebarItems } from './livechatSideNavItems'; |
||||
|
||||
Template.livechatFlex.helpers({ |
||||
menuItem(name, icon, section) { |
||||
const routeName = FlowRouter.getRouteName(); |
||||
return { |
||||
name: t(name), |
||||
icon, |
||||
pathSection: section, |
||||
darken: true, |
||||
active: section === routeName, |
||||
}; |
||||
}, |
||||
embeddedVersion() { |
||||
return Layout.isEmbedded(); |
||||
}, |
||||
sidebarItems() { |
||||
const items = sidebarItems.get(); |
||||
return items.filter((item) => !item.permission || hasPermission(item.permission)); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechatFlex.events({ |
||||
'click [data-action="close"]'() { |
||||
SideNav.closeFlex(); |
||||
}, |
||||
}); |
||||
@ -1,27 +0,0 @@ |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
|
||||
export const sidebarItems = new ReactiveVar([]); |
||||
export const addSidebarItem = (title, slug, permission) => { |
||||
sidebarItems.set([ |
||||
...sidebarItems.get(), |
||||
{ |
||||
title, |
||||
slug, |
||||
permission, |
||||
}, |
||||
]); |
||||
}; |
||||
|
||||
addSidebarItem('Current_Chats', 'livechat-current-chats', 'view-livechat-current-chats'); |
||||
addSidebarItem('Analytics', 'livechat-analytics', 'view-livechat-analytics'); |
||||
addSidebarItem('Real_Time_Monitoring', 'livechat-real-time-monitoring', 'view-livechat-real-time-monitoring'); |
||||
addSidebarItem('Managers', 'livechat-managers', 'manage-livechat-managers'); |
||||
addSidebarItem('Agents', 'livechat-agents', 'manage-livechat-agents'); |
||||
addSidebarItem('Departments', 'livechat-departments', 'view-livechat-departments'); |
||||
addSidebarItem('Custom_Fields', 'livechat-customfields', 'view-livechat-customfields'); |
||||
addSidebarItem('Livechat_Triggers', 'livechat-triggers', 'view-livechat-triggers'); |
||||
addSidebarItem('Livechat_Installation', 'livechat-installation', 'view-livechat-installation'); |
||||
addSidebarItem('Livechat_Appearance', 'livechat-appearance', 'view-livechat-appearance'); |
||||
addSidebarItem('Webhooks', 'livechat-webhooks', 'view-livechat-webhooks'); |
||||
addSidebarItem('Facebook Messenger', 'livechat-facebook', 'view-livechat-facebook'); |
||||
addSidebarItem('Business_Hours', 'livechat-business-hours', 'view-livechat-business-hours'); |
||||
@ -1,7 +1,7 @@ |
||||
import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import Page from '../components/basic/Page'; |
||||
import Page from './basic/Page'; |
||||
|
||||
function PageSkeleton() { |
||||
return <Page> |
||||
@ -0,0 +1,23 @@ |
||||
import React, { useMemo, useState } from 'react'; |
||||
import { AutoComplete, Option, Options } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import UserAvatar from './avatar/UserAvatar'; |
||||
|
||||
const query = (term = '') => ({ selector: JSON.stringify({ term }) }); |
||||
|
||||
const Avatar = ({ value, ...props }) => <UserAvatar size={Options.AvatarSize} username={value} {...props} />; |
||||
|
||||
export const UserAutoComplete = React.memo((props) => { |
||||
const [filter, setFilter] = useState(''); |
||||
const { data } = useEndpointDataExperimental('users.autocomplete', useMemo(() => query(filter), [filter])); |
||||
const options = useMemo(() => (data && data.items.map((user) => ({ value: user.username, label: user.name }))) || [], [data]); |
||||
return <AutoComplete |
||||
{...props} |
||||
filter={filter} |
||||
setFilter={setFilter} |
||||
renderSelected={({ value, label }) => <><UserAvatar size='x20' username={value} /> {label}</>} |
||||
renderItem={({ value, ...props }) => <Option key={value} {...props} avatar={<Avatar value={value} />} />} |
||||
options={ options } |
||||
/>; |
||||
}); |
||||
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import AutoComplete from './AutoComplete'; |
||||
|
||||
export default { |
||||
title: 'components/basic/AutoComplete', |
||||
component: AutoComplete, |
||||
}; |
||||
|
||||
export const Example = () => <AutoComplete/>; |
||||
@ -1,10 +0,0 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
function ExternalLink({ children, to, ...props }) { |
||||
return <Box is='a' href={to} target='_blank' rel='noopener noreferrer' {...props}> |
||||
{children || to} |
||||
</Box>; |
||||
} |
||||
|
||||
export default ExternalLink; |
||||
@ -0,0 +1,13 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
type ExternalLinkProps = { |
||||
to: string; |
||||
}; |
||||
|
||||
const ExternalLink: FC<ExternalLinkProps> = ({ children, to, ...props }) => |
||||
<Box is='a' href={to} target='_blank' rel='noopener noreferrer' {...props}> |
||||
{children || to} |
||||
</Box>; |
||||
|
||||
export default ExternalLink; |
||||
@ -1,11 +0,0 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
export const ConnectionStatusContext = createContext({ |
||||
connected: true, |
||||
retryCount: 0, |
||||
retryTime: 0, |
||||
status: 'connected', |
||||
reconnect: () => {}, |
||||
}); |
||||
|
||||
export const useConnectionStatus = () => useContext(ConnectionStatusContext); |
||||
@ -0,0 +1,20 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
type ConnectionStatusContextValue = { |
||||
connected: boolean; |
||||
retryCount: number; |
||||
retryTime: number; |
||||
status: 'connected' | 'connecting' | 'failed' | 'waiting' | 'offline'; |
||||
reconnect: () => void; |
||||
}; |
||||
|
||||
export const ConnectionStatusContext = createContext<ConnectionStatusContextValue>({ |
||||
connected: true, |
||||
retryCount: 0, |
||||
retryTime: 0, |
||||
status: 'connected', |
||||
reconnect: () => undefined, |
||||
}); |
||||
|
||||
export const useConnectionStatus = (): ConnectionStatusContextValue => |
||||
useContext(ConnectionStatusContext); |
||||
@ -1,7 +0,0 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
export const CustomSoundContext = createContext({ |
||||
play: () => {}, |
||||
}); |
||||
|
||||
export const useCustomSound = () => useContext(CustomSoundContext); |
||||
@ -0,0 +1,12 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
type CustomSoundContextValue = { |
||||
play: () => void; |
||||
}; |
||||
|
||||
export const CustomSoundContext = createContext<CustomSoundContextValue>({ |
||||
play: () => undefined, |
||||
}); |
||||
|
||||
export const useCustomSound = (): CustomSoundContextValue => |
||||
useContext(CustomSoundContext); |
||||
@ -1,83 +0,0 @@ |
||||
import { createContext, useContext, useMemo } from 'react'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
export const RouterContext = createContext({ |
||||
getRoutePath: () => {}, |
||||
subscribeToRoutePath: () => () => {}, |
||||
getRouteUrl: () => {}, |
||||
subscribeToRouteUrl: () => () => {}, |
||||
pushRoute: () => {}, |
||||
replaceRoute: () => {}, |
||||
getRouteParameter: () => {}, |
||||
subscribeToRouteParameter: () => () => {}, |
||||
getQueryStringParameter: () => {}, |
||||
subscribeToQueryStringParameter: () => () => {}, |
||||
getCurrentRoute: () => {}, |
||||
subscribeToCurrentRoute: () => () => {}, |
||||
}); |
||||
|
||||
export const useRoute = (name) => { |
||||
const { getRoutePath, getRouteUrl, pushRoute, replaceRoute } = useContext(RouterContext); |
||||
|
||||
return useMemo(() => ({ |
||||
getPath: (...args) => getRoutePath(name, ...args), |
||||
getUrl: (...args) => getRouteUrl(name, ...args), |
||||
push: (...args) => pushRoute(name, ...args), |
||||
replace: (...args) => replaceRoute(name, ...args), |
||||
}), [getRoutePath, getRouteUrl, name, pushRoute, replaceRoute]); |
||||
}; |
||||
|
||||
export const useRoutePath = (name, params, queryStringParams) => { |
||||
const { getRoutePath, subscribeToRoutePath } = useContext(RouterContext); |
||||
|
||||
const subscription = useMemo(() => ({ |
||||
getCurrentValue: () => getRoutePath(name, params, queryStringParams), |
||||
subscribe: (callback) => subscribeToRoutePath(name, params, queryStringParams, callback), |
||||
}), [name, params, queryStringParams, getRoutePath, subscribeToRoutePath]); |
||||
|
||||
return useSubscription(subscription); |
||||
}; |
||||
|
||||
export const useRouteUrl = (name, params, queryStringParams) => { |
||||
const { getRouteUrl, subscribeToRouteUrl } = useContext(RouterContext); |
||||
|
||||
const subscription = useMemo(() => ({ |
||||
getCurrentValue: () => getRouteUrl(name, params, queryStringParams), |
||||
subscribe: (callback) => subscribeToRouteUrl(name, params, queryStringParams, callback), |
||||
}), [name, params, queryStringParams, getRouteUrl, subscribeToRouteUrl]); |
||||
|
||||
return useSubscription(subscription); |
||||
}; |
||||
|
||||
export const useRouteParameter = (name) => { |
||||
const { getRouteParameter, subscribeToRouteParameter } = useContext(RouterContext); |
||||
|
||||
const subscription = useMemo(() => ({ |
||||
getCurrentValue: () => getRouteParameter(name), |
||||
subscribe: (callback) => subscribeToRouteParameter(name, callback), |
||||
}), [name, getRouteParameter, subscribeToRouteParameter]); |
||||
|
||||
return useSubscription(subscription); |
||||
}; |
||||
|
||||
export const useQueryStringParameter = (name) => { |
||||
const { getQueryStringParameter, subscribeToQueryStringParameter } = useContext(RouterContext); |
||||
|
||||
const subscription = useMemo(() => ({ |
||||
getCurrentValue: () => getQueryStringParameter(name), |
||||
subscribe: (callback) => subscribeToQueryStringParameter(name, callback), |
||||
}), [name, getQueryStringParameter, subscribeToQueryStringParameter]); |
||||
|
||||
return useSubscription(subscription); |
||||
}; |
||||
|
||||
export const useCurrentRoute = () => { |
||||
const { getCurrentRoute, subscribeToCurrentRoute } = useContext(RouterContext); |
||||
|
||||
const subscription = useMemo(() => ({ |
||||
getCurrentValue: () => getCurrentRoute(), |
||||
subscribe: (callback) => subscribeToCurrentRoute(callback), |
||||
}), [getCurrentRoute, subscribeToCurrentRoute]); |
||||
|
||||
return useSubscription(subscription); |
||||
}; |
||||
@ -0,0 +1,156 @@ |
||||
import { createContext, useContext, useMemo } from 'react'; |
||||
import { useSubscription, Subscription } from 'use-subscription'; |
||||
|
||||
type RouteName = string; |
||||
|
||||
type RouteParameters = Record<string, string>; |
||||
|
||||
type QueryStringParameters = Record<string, string>; |
||||
|
||||
type RouteGroupName = string; |
||||
|
||||
export type RouterContextValue = { |
||||
queryRoutePath: ( |
||||
name: RouteName, |
||||
parameters: RouteParameters | undefined, |
||||
queryStringParameters: QueryStringParameters | undefined, |
||||
) => Subscription<string | undefined>; |
||||
queryRouteUrl: ( |
||||
name: RouteName, |
||||
parameters: RouteParameters | undefined, |
||||
queryStringParameters: QueryStringParameters | undefined, |
||||
) => Subscription<string | undefined>; |
||||
pushRoute: ( |
||||
name: RouteName, |
||||
parameters: RouteParameters | undefined, |
||||
queryStringParameters: QueryStringParameters | undefined, |
||||
) => void; |
||||
replaceRoute: ( |
||||
name: RouteName, |
||||
parameters: RouteParameters | undefined, |
||||
queryStringParameters: QueryStringParameters | undefined, |
||||
) => void; |
||||
queryRouteParameter: (name: string) => Subscription<string | undefined>; |
||||
queryQueryStringParameter: (name: string) => Subscription<string | undefined>; |
||||
queryCurrentRoute: () => Subscription<[ |
||||
RouteName?, |
||||
RouteParameters?, |
||||
QueryStringParameters?, |
||||
RouteGroupName?, |
||||
]>; |
||||
}; |
||||
|
||||
export const RouterContext = createContext<RouterContextValue>({ |
||||
queryRoutePath: () => ({ |
||||
getCurrentValue: (): undefined => undefined, |
||||
subscribe: () => (): void => undefined, |
||||
}), |
||||
queryRouteUrl: () => ({ |
||||
getCurrentValue: (): undefined => undefined, |
||||
subscribe: () => (): void => undefined, |
||||
}), |
||||
pushRoute: () => undefined, |
||||
replaceRoute: () => undefined, |
||||
queryRouteParameter: () => ({ |
||||
getCurrentValue: (): undefined => undefined, |
||||
subscribe: () => (): void => undefined, |
||||
}), |
||||
queryQueryStringParameter: () => ({ |
||||
getCurrentValue: (): undefined => undefined, |
||||
subscribe: () => (): void => undefined, |
||||
}), |
||||
queryCurrentRoute: () => ({ |
||||
getCurrentValue: (): [undefined, {}, {}, undefined] => [undefined, {}, {}, undefined], |
||||
subscribe: () => (): void => undefined, |
||||
}), |
||||
}); |
||||
|
||||
type Route = { |
||||
getPath: (parameters?: RouteParameters, queryStringParameters?: QueryStringParameters) => string | undefined; |
||||
getUrl: (parameters?: RouteParameters, queryStringParameters?: QueryStringParameters) => string | undefined; |
||||
push: (parameters?: RouteParameters, queryStringParameters?: QueryStringParameters) => void; |
||||
replace: (parameters?: RouteParameters, queryStringParameters?: QueryStringParameters) => void; |
||||
} |
||||
|
||||
export const useRoute = (name: string): Route => { |
||||
const { |
||||
queryRoutePath, |
||||
queryRouteUrl, |
||||
pushRoute, |
||||
replaceRoute, |
||||
} = useContext(RouterContext); |
||||
|
||||
return useMemo<Route>(() => ({ |
||||
getPath: (parameters, queryStringParameters): string | undefined => |
||||
queryRoutePath(name, parameters, queryStringParameters).getCurrentValue(), |
||||
getUrl: (parameters, queryStringParameters): ReturnType<Route['getUrl']> => |
||||
queryRouteUrl(name, parameters, queryStringParameters).getCurrentValue(), |
||||
push: (parameters, queryStringParameters): ReturnType<Route['push']> => |
||||
pushRoute(name, parameters, queryStringParameters), |
||||
replace: (parameters, queryStringParameters): ReturnType<Route['replace']> => |
||||
replaceRoute(name, parameters, queryStringParameters), |
||||
}), [queryRoutePath, queryRouteUrl, name, pushRoute, replaceRoute]); |
||||
}; |
||||
|
||||
export const useRoutePath = ( |
||||
name: string, |
||||
parameters?: RouteParameters, |
||||
queryStringParameters?: QueryStringParameters, |
||||
): string | undefined => { |
||||
const { queryRoutePath } = useContext(RouterContext); |
||||
|
||||
return useSubscription( |
||||
useMemo( |
||||
() => queryRoutePath(name, parameters, queryStringParameters), |
||||
[queryRoutePath, name, parameters, queryStringParameters], |
||||
), |
||||
); |
||||
}; |
||||
|
||||
export const useRouteUrl = ( |
||||
name: string, |
||||
parameters?: RouteParameters, |
||||
queryStringParameters?: QueryStringParameters, |
||||
): string | undefined => { |
||||
const { queryRouteUrl } = useContext(RouterContext); |
||||
|
||||
return useSubscription( |
||||
useMemo( |
||||
() => queryRouteUrl(name, parameters, queryStringParameters), |
||||
[queryRouteUrl, name, parameters, queryStringParameters], |
||||
), |
||||
); |
||||
}; |
||||
|
||||
export const useRouteParameter = (name: string): string | undefined => { |
||||
const { queryRouteParameter } = useContext(RouterContext); |
||||
|
||||
return useSubscription( |
||||
useMemo( |
||||
() => queryRouteParameter(name), |
||||
[queryRouteParameter, name], |
||||
), |
||||
); |
||||
}; |
||||
|
||||
export const useQueryStringParameter = (name: string): string | undefined => { |
||||
const { queryQueryStringParameter } = useContext(RouterContext); |
||||
|
||||
return useSubscription( |
||||
useMemo( |
||||
() => queryQueryStringParameter(name), |
||||
[queryQueryStringParameter, name], |
||||
), |
||||
); |
||||
}; |
||||
|
||||
export const useCurrentRoute = (): [RouteName?, RouteParameters?, QueryStringParameters?, RouteGroupName?] => { |
||||
const { queryCurrentRoute } = useContext(RouterContext); |
||||
|
||||
return useSubscription( |
||||
useMemo( |
||||
() => queryCurrentRoute(), |
||||
[queryCurrentRoute], |
||||
), |
||||
); |
||||
}; |
||||
@ -1,5 +0,0 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
export const SidebarContext = createContext([false, () => {}]); |
||||
|
||||
export const useSidebar = () => useContext(SidebarContext); |
||||
@ -0,0 +1,8 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
type SidebarContextValue = [boolean, (open: boolean | ((isOpen: boolean) => boolean)) => void]; |
||||
|
||||
export const SidebarContext = createContext<SidebarContextValue>([false, (): void => undefined]); |
||||
|
||||
export const useSidebar = (): SidebarContextValue => |
||||
useContext(SidebarContext); |
||||
@ -1,7 +0,0 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
export const ToastMessagesContext = createContext({ |
||||
dispatch: () => {}, |
||||
}); |
||||
|
||||
export const useToastMessageDispatch = () => useContext(ToastMessagesContext).dispatch; |
||||
@ -0,0 +1,19 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
export type ToastMessagePayload = { |
||||
type: 'success' | 'info' | 'warning' | 'error'; |
||||
message: string | Error; |
||||
title?: string; |
||||
options?: object; |
||||
}; |
||||
|
||||
type ToastMessagesContextValue = { |
||||
dispatch: (payload: ToastMessagePayload) => void; |
||||
}; |
||||
|
||||
export const ToastMessagesContext = createContext<ToastMessagesContextValue>({ |
||||
dispatch: () => undefined, |
||||
}); |
||||
|
||||
export const useToastMessageDispatch = (): ToastMessagesContextValue['dispatch'] => |
||||
useContext(ToastMessagesContext).dispatch; |
||||
@ -0,0 +1,30 @@ |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
|
||||
import { renderRouteComponent } from '../reactAdapters'; |
||||
|
||||
export const createRouteGroup = (name, prefix, importRouter) => { |
||||
const routeGroup = FlowRouter.group({ |
||||
name, |
||||
prefix, |
||||
}); |
||||
|
||||
const registerRoute = (path, { lazyRouteComponent, props, action, ...options } = {}) => { |
||||
routeGroup.route(path, { |
||||
...options, |
||||
action: (params, queryParams) => { |
||||
if (action) { |
||||
action(params, queryParams); |
||||
return; |
||||
} |
||||
|
||||
renderRouteComponent(importRouter, { |
||||
template: 'main', |
||||
region: 'center', |
||||
propsFn: () => ({ lazyRouteComponent, ...options, params, queryParams, ...props }), |
||||
}); |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
return registerRoute; |
||||
}; |
||||
@ -0,0 +1,4 @@ |
||||
import { useMemo } from 'react'; |
||||
import moment from 'moment-timezone'; |
||||
|
||||
export const useTimezoneNameList = () => useMemo(() => moment.tz.names(), []); |
||||
@ -0,0 +1,23 @@ |
||||
import { Icon, Button, Modal, ButtonGroup } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
|
||||
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='danger' name='modal-warning' size={20}/> |
||||
<Modal.Title>{t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={onCancel}>{t('Cancel')}</Button> |
||||
<Button primary danger onClick={onDelete}>{t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default DeleteWarningModal; |
||||
@ -0,0 +1,22 @@ |
||||
import React, { lazy, useMemo, Suspense, useEffect, FC, ComponentType } from 'react'; |
||||
|
||||
import PageSkeleton from '../components/PageSkeleton'; |
||||
import { SideNav } from '../../app/ui-utils/client'; |
||||
|
||||
type OmnichannelRouterProps = { |
||||
lazyRouteComponent: () => Promise<{ default: ComponentType }>; |
||||
}; |
||||
|
||||
const OmnichannelRouter: FC<OmnichannelRouterProps> = ({ lazyRouteComponent, ...props }) => { |
||||
const LazyRouteComponent = useMemo(() => lazy(lazyRouteComponent), [lazyRouteComponent]); |
||||
useEffect(() => { |
||||
SideNav.setFlex('omnichannelFlex'); |
||||
SideNav.openFlex(() => undefined); |
||||
}, []); |
||||
|
||||
return <Suspense fallback={<PageSkeleton />}> |
||||
<LazyRouteComponent {...props} /> |
||||
</Suspense>; |
||||
}; |
||||
|
||||
export default OmnichannelRouter; |
||||
@ -0,0 +1,26 @@ |
||||
const createFormSubscription = () => { |
||||
let forms = {}; |
||||
let updateCb = () => {}; |
||||
|
||||
const formsSubscription = { |
||||
subscribe: (cb) => { |
||||
updateCb = cb; |
||||
return () => { |
||||
updateCb = () => {}; |
||||
}; |
||||
}, |
||||
getCurrentValue: () => forms, |
||||
}; |
||||
const registerForm = (newForm) => { |
||||
forms = { ...forms, ...newForm }; |
||||
updateCb(); |
||||
}; |
||||
const unregisterForm = (form) => { |
||||
delete forms[form]; |
||||
updateCb(); |
||||
}; |
||||
|
||||
return { registerForm, unregisterForm, formsSubscription }; |
||||
}; |
||||
|
||||
export const { registerForm, unregisterForm, formsSubscription } = createFormSubscription(); |
||||
@ -0,0 +1,154 @@ |
||||
import React, { useMemo, useRef, useState } from 'react'; |
||||
import { Field, TextInput, Button, Margins, Box, MultiSelect, Icon, Select } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
import { UserInfo } from '../../components/basic/UserInfo'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import { FormSkeleton } from './Skeleton'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import { getUserEmailAddress } from '../../helpers/getUserEmailAddress'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { formsSubscription } from '../additionalForms'; |
||||
|
||||
|
||||
export default function AgentEditWithData({ uid, reload }) { |
||||
const t = useTranslation(); |
||||
const { data, state, error } = useEndpointDataExperimental(`livechat/users/agent/${ uid }`); |
||||
const { data: userDepartments, state: userDepartmentsState, error: userDepartmentsError } = useEndpointDataExperimental(`livechat/agents/${ uid }/departments`); |
||||
const { data: availableDepartments, state: availableDepartmentsState, error: availableDepartmentsError } = useEndpointDataExperimental('livechat/department'); |
||||
|
||||
if ([state, availableDepartmentsState, userDepartmentsState].includes(ENDPOINT_STATES.LOADING)) { |
||||
return <FormSkeleton/>; |
||||
} |
||||
|
||||
if (error || userDepartmentsError || availableDepartmentsError) { |
||||
return <Box mbs='x16'>{t('User_not_found')}</Box>; |
||||
} |
||||
|
||||
return <AgentEdit uid={uid} data={data} userDepartments={userDepartments} availableDepartments={availableDepartments} reset={reload}/>; |
||||
} |
||||
|
||||
export function AgentEdit({ data, userDepartments, availableDepartments, uid, reset, ...props }) { |
||||
const t = useTranslation(); |
||||
const agentsRoute = useRoute('omnichannel-agents'); |
||||
const [maxChatUnsaved, setMaxChatUnsaved] = useState(); |
||||
|
||||
const { user } = data || { user: {} }; |
||||
const { |
||||
name, |
||||
username, |
||||
statusLivechat, |
||||
} = user; |
||||
|
||||
const email = getUserEmailAddress(user); |
||||
const options = useMemo(() => (availableDepartments && availableDepartments.departments ? availableDepartments.departments.map(({ _id, name }) => [_id, name || _id]) : []), [availableDepartments]); |
||||
const initialDepartmentValue = useMemo(() => (userDepartments && userDepartments.departments ? userDepartments.departments.map(({ departmentId }) => departmentId) : []), [userDepartments]); |
||||
const eeForms = useSubscription(formsSubscription); |
||||
|
||||
const saveRef = useRef({ values: {}, hasUnsavedChanges: false }); |
||||
|
||||
const onChangeMaxChats = useMutableCallback(({ hasUnsavedChanges, ...value }) => { |
||||
saveRef.current = value; |
||||
|
||||
if (hasUnsavedChanges !== maxChatUnsaved) { |
||||
setMaxChatUnsaved(hasUnsavedChanges); |
||||
} |
||||
}); |
||||
|
||||
const { |
||||
useMaxChatsPerAgent = () => {}, |
||||
} = eeForms; |
||||
|
||||
console.log(eeForms, useMaxChatsPerAgent()); |
||||
|
||||
const { values, handlers, hasUnsavedChanges, commit } = useForm({ departments: initialDepartmentValue, status: statusLivechat, maxChats: 0 }); |
||||
const { |
||||
reset: resetMaxChats, |
||||
commit: commitMaxChats, |
||||
} = saveRef.current; |
||||
|
||||
const { |
||||
handleDepartments, |
||||
handleStatus, |
||||
} = handlers; |
||||
const { |
||||
departments, |
||||
status, |
||||
} = values; |
||||
|
||||
const MaxChats = useMaxChatsPerAgent(); |
||||
|
||||
const saveAgentInfo = useMethod('livechat:saveAgentInfo'); |
||||
const saveAgentStatus = useMethod('livechat:changeLivechatStatus'); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const handleReset = useMutableCallback(() => { |
||||
reset(); |
||||
resetMaxChats(); |
||||
}); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
try { |
||||
await saveAgentInfo(uid, saveRef.current.values, departments); |
||||
await saveAgentStatus({ status, agentId: uid }); |
||||
dispatchToastMessage({ type: 'success', message: t('saved') }); |
||||
agentsRoute.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
console.log(error); |
||||
} |
||||
commit(); |
||||
commitMaxChats(); |
||||
}); |
||||
|
||||
return <VerticalBar.ScrollableContent is='form' { ...props }> |
||||
<UserInfo.Avatar margin='auto' size={'x332'} title={username} username={username}/> |
||||
<Field> |
||||
<Field.Label>{t('Name')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={name} disabled/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Username')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={username} disabled addon={<Icon name='at' size='x20'/>}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Email')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput flexGrow={1} value={email} disabled addon={<Icon name='mail' size='x20'/>}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Departments')}</Field.Label> |
||||
<Field.Row> |
||||
<MultiSelect options={options} value={departments} placeholder={t('Select_an_option')} onChange={handleDepartments} flexGrow={1}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Status')}</Field.Label> |
||||
<Field.Row> |
||||
<Select options={[['available', t('Available')], ['not-available', t('Not_Available')]]} value={status} placeholder={t('Select_an_option')} onChange={handleStatus} flexGrow={1}/> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
{MaxChats && <MaxChats data={user} onChange={onChangeMaxChats}/>} |
||||
|
||||
<Field.Row> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'> |
||||
<Margins inlineEnd='x4'> |
||||
<Button flexGrow={1} type='reset' disabled={!hasUnsavedChanges && !maxChatUnsaved} onClick={handleReset}>{t('Reset')}</Button> |
||||
<Button mie='none' flexGrow={1} disabled={!hasUnsavedChanges && !maxChatUnsaved} onClick={handleSave}>{t('Save')}</Button> |
||||
</Margins> |
||||
</Box> |
||||
</Field.Row> |
||||
</VerticalBar.ScrollableContent>; |
||||
} |
||||
@ -0,0 +1,77 @@ |
||||
import React from 'react'; |
||||
import { Box, Margins, Button, Icon, ButtonGroup } from '@rocket.chat/fuselage'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import { UserInfo } from '../../components/basic/UserInfo'; |
||||
import * as UserStatus from '../../components/basic/UserStatus'; |
||||
import { FormSkeleton } from './Skeleton'; |
||||
import { formsSubscription } from '../additionalForms'; |
||||
|
||||
|
||||
export const AgentInfo = React.memo(function AgentInfo({ |
||||
uid, |
||||
children, |
||||
...props |
||||
}) { |
||||
const t = useTranslation(); |
||||
const { data, state, error } = useEndpointDataExperimental(`livechat/users/agent/${ uid }`); |
||||
const eeForms = useSubscription(formsSubscription); |
||||
|
||||
const { |
||||
useMaxChatsPerAgentDisplay = () => {}, |
||||
} = eeForms; |
||||
|
||||
const MaxChats = useMaxChatsPerAgentDisplay(); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <FormSkeleton/>; |
||||
} |
||||
|
||||
if (error) { |
||||
return <Box mbs='x16'>{t('User_not_found')}</Box>; |
||||
} |
||||
|
||||
const { user } = data || { user: {} }; |
||||
const { |
||||
username, |
||||
statusLivechat, |
||||
} = user; |
||||
|
||||
const status = UserStatus.getStatus(data.status); |
||||
|
||||
return <VerticalBar.ScrollableContent p='x24' {...props}> |
||||
|
||||
<UserInfo.Avatar size={'x332'} username={username}/> |
||||
|
||||
<ButtonGroup mi='neg-x4' flexShrink={0} flexWrap='nowrap' withTruncatedText justifyContent='center' flexShrink={0}> |
||||
{children} |
||||
</ButtonGroup> |
||||
|
||||
<Margins block='x4'> |
||||
<UserInfo.Username name={username} status={status} /> |
||||
|
||||
{statusLivechat && <> |
||||
<UserInfo.Label>{t('Livechat_Status')}</UserInfo.Label> |
||||
<UserInfo.Info>{t(statusLivechat)}</UserInfo.Info> |
||||
</>} |
||||
|
||||
{MaxChats && <MaxChats data={user}/>} |
||||
|
||||
</Margins> |
||||
|
||||
</VerticalBar.ScrollableContent>; |
||||
}); |
||||
|
||||
export const Action = ({ icon, label, ...props }) => ( |
||||
<Button title={label} {...props} mi='x4'> |
||||
<Icon name={icon} size='x20' mie='x4' /> |
||||
{label} |
||||
</Button> |
||||
); |
||||
|
||||
AgentInfo.Action = Action; |
||||
|
||||
export default AgentInfo; |
||||
@ -0,0 +1,72 @@ |
||||
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 { useEndpointAction } from '../../hooks/useEndpointAction'; |
||||
import { GenericTable } from '../../components/GenericTable'; |
||||
import { UserAutoComplete } from '../../components/basic/AutoComplete'; |
||||
|
||||
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 AddAgent({ reload, ...props }) { |
||||
const t = useTranslation(); |
||||
const [username, setUsername] = useState(); |
||||
|
||||
const saveAction = useEndpointAction('POST', 'livechat/users/agent', { username }); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
if (!username) { |
||||
return; |
||||
} |
||||
const result = await saveAction(); |
||||
if (!result.success) { |
||||
return; |
||||
} |
||||
reload(); |
||||
setUsername(); |
||||
}); |
||||
return <Box display='flex' alignItems='center' {...props}> |
||||
<UserAutoComplete value={username} onChange={setUsername}/> |
||||
<Button disabled={!username} onClick={handleSave} mis='x8' primary>{t('Add')}</Button> |
||||
</Box>; |
||||
} |
||||
|
||||
function AgentsPage({ |
||||
data, |
||||
reload, |
||||
header, |
||||
setParams, |
||||
params, |
||||
title, |
||||
renderRow, |
||||
children, |
||||
}) { |
||||
return <Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={title}/> |
||||
<AddAgent reload={reload} pi='x24'/> |
||||
<Page.Content> |
||||
<GenericTable FilterComponent={FilterByText} header={header} renderRow={renderRow} results={data && data.users} total={data && data.total} setParams={setParams} params={params} /> |
||||
</Page.Content> |
||||
</Page> |
||||
{children} |
||||
</Page>; |
||||
} |
||||
|
||||
export default AgentsPage; |
||||
@ -0,0 +1,170 @@ |
||||
|
||||
import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useMemo, useCallback, useState } from 'react'; |
||||
import { Box, 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 AgentsPage from './AgentsPage'; |
||||
import AgentEdit from './AgentEdit'; |
||||
import AgentInfo from './AgentInfo'; |
||||
import UserAvatar from '../../components/basic/avatar/UserAvatar'; |
||||
import { useRouteParameter, useRoute } from '../../contexts/RouterContext'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
|
||||
export function RemoveAgentButton({ _id, reload }) { |
||||
const deleteAction = useEndpointAction('DELETE', `livechat/users/agent/${ _id }`); |
||||
|
||||
const handleRemoveClick = useMutableCallback(async (e) => { |
||||
e.preventDefault(); |
||||
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>; |
||||
} |
||||
|
||||
export function AgentInfoActions({ reload }) { |
||||
const t = useTranslation(); |
||||
const _id = useRouteParameter('id'); |
||||
const agentsRoute = useRoute('omnichannel-agents'); |
||||
const deleteAction = useEndpointAction('DELETE', `livechat/users/agent/${ _id }`); |
||||
|
||||
const handleRemoveClick = useMutableCallback(async () => { |
||||
const result = await deleteAction(); |
||||
if (result.success === true) { |
||||
agentsRoute.push({}); |
||||
reload(); |
||||
} |
||||
}); |
||||
|
||||
const handleEditClick = useMutableCallback(() => agentsRoute.push({ |
||||
context: 'edit', |
||||
id: _id, |
||||
})); |
||||
|
||||
return [ |
||||
<AgentInfo.Action key={t('Remove')} title={t('Remove')} label={t('Remove')} onClick={handleRemoveClick} icon={'trash'} />, |
||||
<AgentInfo.Action key={t('Edit')} title={t('Edit')} label={t('Edit')} onClick={handleEditClick} icon={'edit'} />, |
||||
]; |
||||
} |
||||
|
||||
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 AgentsRoute() { |
||||
const t = useTranslation(); |
||||
const canViewAgents = usePermission('manage-livechat-agents'); |
||||
|
||||
const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 }); |
||||
const [sort, setSort] = useState(['name', 'asc']); |
||||
|
||||
const mediaQuery = useMediaQuery('(min-width: 1024px)'); |
||||
|
||||
const debouncedParams = useDebouncedValue(params, 500); |
||||
const debouncedSort = useDebouncedValue(sort, 500); |
||||
const query = useQuery(debouncedParams, debouncedSort); |
||||
const agentsRoute = useRoute('omnichannel-agents'); |
||||
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) => () => agentsRoute.push({ |
||||
context: 'info', |
||||
id, |
||||
})); |
||||
|
||||
const { data, reload } = useEndpointDataExperimental('livechat/users/agent', query) || {}; |
||||
|
||||
|
||||
const header = useMemo(() => [ |
||||
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w='x200'>{t('Name')}</Th>, |
||||
mediaQuery && <Th key={'username'} direction={sort[1]} active={sort[0] === 'username'} onClick={onHeaderClick} sort='username' w='x140'>{t('Username')}</Th>, |
||||
<Th key={'email'} direction={sort[1]} active={sort[0] === 'emails.adress'} onClick={onHeaderClick} sort='emails.address' w='x120'>{t('Email')}</Th>, |
||||
<Th key={'status'} direction={sort[1]} active={sort[0] === 'status'} onClick={onHeaderClick} sort='status' w='x120'>{t('Livechat_status')}</Th>, |
||||
<Th key={'remove'} w='x40'>{t('Remove')}</Th>, |
||||
].filter(Boolean), [sort, onHeaderClick, t, mediaQuery]); |
||||
|
||||
const renderRow = useCallback(({ emails, _id, username, name, avatarETag, statusLivechat }) => <Table.Row key={_id} tabIndex={0} role='link' onClick={onRowClick(_id)} action qa-user-id={_id}> |
||||
<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> |
||||
{mediaQuery && <Table.Cell> |
||||
<Box fontScale='p2' withTruncatedText color='hint'>{ username }</Box> <Box mi='x4'/> |
||||
</Table.Cell>} |
||||
<Table.Cell withTruncatedText>{emails && emails.length && emails[0].address}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{statusLivechat === 'available' ? t('Available') : t('Not_Available')}</Table.Cell> |
||||
<RemoveAgentButton _id={_id} reload={reload}/> |
||||
</Table.Row>, [mediaQuery, reload, onRowClick, t]); |
||||
|
||||
|
||||
const EditAgentsTab = useCallback(() => { |
||||
if (!context) { |
||||
return ''; |
||||
} |
||||
const handleVerticalBarCloseButtonClick = () => { |
||||
agentsRoute.push({}); |
||||
}; |
||||
|
||||
return <VerticalBar className={'contextual-bar'}> |
||||
<VerticalBar.Header> |
||||
{context === 'edit' && t('Edit_User')} |
||||
{context === 'info' && t('User_Info')} |
||||
<VerticalBar.Close onClick={handleVerticalBarCloseButtonClick} /> |
||||
</VerticalBar.Header> |
||||
|
||||
{context === 'edit' && <AgentEdit uid={id} reload={reload}/>} |
||||
{context === 'info' && <AgentInfo uid={id}><AgentInfoActions id={id} reload={reload} /></AgentInfo>} |
||||
|
||||
</VerticalBar>; |
||||
}, [t, context, id, agentsRoute, reload]); |
||||
|
||||
if (!canViewAgents) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
|
||||
return <AgentsPage |
||||
setParams={setParams} |
||||
params={params} |
||||
onHeaderClick={onHeaderClick} |
||||
data={data} useQuery={useQuery} |
||||
reload={reload} |
||||
header={header} |
||||
renderRow={renderRow} |
||||
title={'Agents'}> |
||||
<EditAgentsTab /> |
||||
</AgentsPage>; |
||||
} |
||||
|
||||
export default AgentsRoute; |
||||
@ -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>; |
||||
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
|
||||
import AppearanceForm from './AppearanceForm'; |
||||
|
||||
export default { |
||||
title: 'omnichannel/AppearanceForm', |
||||
component: AppearanceForm, |
||||
}; |
||||
|
||||
export const Default = () => <AppearanceForm />; |
||||
@ -0,0 +1,251 @@ |
||||
/* eslint-disable @typescript-eslint/camelcase */ |
||||
import React, { FC, FormEvent } from 'react'; |
||||
import { Box, Field, TextInput, ToggleSwitch, Accordion, FieldGroup, InputBox, TextAreaInput, NumberInput } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
type AppearanceFormProps = { |
||||
values: { |
||||
Livechat_title?: string; |
||||
Livechat_title_color?: string; |
||||
Livechat_show_agent_info?: boolean; |
||||
Livechat_show_agent_email?: boolean; |
||||
Livechat_display_offline_form?: boolean; |
||||
Livechat_offline_form_unavailable?: string; |
||||
Livechat_offline_message?: string; |
||||
Livechat_offline_title?: string; |
||||
Livechat_offline_title_color?: string; |
||||
Livechat_offline_email?: string; |
||||
Livechat_offline_success_message?: string; |
||||
Livechat_registration_form?: boolean; |
||||
Livechat_name_field_registration_form?: boolean; |
||||
Livechat_email_field_registration_form?: boolean; |
||||
Livechat_registration_form_message?: string; |
||||
Livechat_conversation_finished_message?: string; |
||||
Livechat_conversation_finished_text?: string; |
||||
Livechat_enable_message_character_limit?: boolean; |
||||
Livechat_message_character_limit?: number; |
||||
}; |
||||
handlers: { |
||||
handleLivechat_title?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_title_color?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_show_agent_info?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_show_agent_email?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_display_offline_form?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_offline_form_unavailable?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_offline_message?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_offline_title?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_offline_title_color?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_offline_email?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_offline_success_message?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_registration_form?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_name_field_registration_form?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_email_field_registration_form?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_registration_form_message?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_conversation_finished_message?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_conversation_finished_text?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_enable_message_character_limit?: (event: FormEvent<HTMLInputElement>) => void; |
||||
handleLivechat_message_character_limit?: (value: number) => void; |
||||
}; |
||||
}; |
||||
|
||||
const AppearanceForm: FC<AppearanceFormProps> = ({ values = {}, handlers = {} }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const { |
||||
Livechat_title, |
||||
Livechat_title_color, |
||||
Livechat_show_agent_info, |
||||
Livechat_show_agent_email, |
||||
Livechat_display_offline_form, |
||||
Livechat_offline_form_unavailable, |
||||
Livechat_offline_message, |
||||
Livechat_offline_title, |
||||
Livechat_offline_title_color, |
||||
Livechat_offline_email, |
||||
Livechat_offline_success_message, |
||||
Livechat_registration_form, |
||||
Livechat_name_field_registration_form, |
||||
Livechat_email_field_registration_form, |
||||
Livechat_registration_form_message, |
||||
Livechat_conversation_finished_message, |
||||
Livechat_conversation_finished_text, |
||||
Livechat_enable_message_character_limit, |
||||
Livechat_message_character_limit, |
||||
} = values; |
||||
|
||||
const { |
||||
handleLivechat_title, |
||||
handleLivechat_title_color, |
||||
handleLivechat_show_agent_info, |
||||
handleLivechat_show_agent_email, |
||||
handleLivechat_display_offline_form, |
||||
handleLivechat_offline_form_unavailable, |
||||
handleLivechat_offline_message, |
||||
handleLivechat_offline_title, |
||||
handleLivechat_offline_title_color, |
||||
handleLivechat_offline_email, |
||||
handleLivechat_offline_success_message, |
||||
handleLivechat_registration_form, |
||||
handleLivechat_name_field_registration_form, |
||||
handleLivechat_email_field_registration_form, |
||||
handleLivechat_registration_form_message, |
||||
handleLivechat_conversation_finished_message, |
||||
handleLivechat_conversation_finished_text, |
||||
handleLivechat_enable_message_character_limit, |
||||
handleLivechat_message_character_limit, |
||||
} = handlers; |
||||
|
||||
const onChangeCharacterLimit = useMutableCallback(({ currentTarget: { value } }) => { |
||||
handleLivechat_message_character_limit && handleLivechat_message_character_limit(Number(value) < 0 ? 0 : value); |
||||
}); |
||||
|
||||
return <Accordion> |
||||
<Accordion.Item defaultExpanded title={t('Livechat_online')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Field.Label>{t('Title')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={Livechat_title} onChange={handleLivechat_title} placeholder={t('Title')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Title_bar_color')}</Field.Label> |
||||
<Field.Row> |
||||
<InputBox type='color' value={Livechat_title_color} onChange={handleLivechat_title_color}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label >{t('Message_Characther_Limit')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={Livechat_enable_message_character_limit} onChange={handleLivechat_enable_message_character_limit}/> |
||||
</Field.Row> |
||||
</Box> |
||||
<Field.Row> |
||||
<NumberInput disabled={!Livechat_enable_message_character_limit} value={Livechat_message_character_limit} onChange={onChangeCharacterLimit}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label >{t('Show_agent_info')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={Livechat_show_agent_info} onChange={handleLivechat_show_agent_info}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label >{t('Show_agent_email')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={Livechat_show_agent_email} onChange={handleLivechat_show_agent_email}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
|
||||
<Accordion.Item title={t('Livechat_offline')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label >{t('Display_offline_form')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={Livechat_display_offline_form} onChange={handleLivechat_display_offline_form}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Offline_form_unavailable_message')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={3} value={Livechat_offline_form_unavailable} onChange={handleLivechat_offline_form_unavailable} placeholder={t('Offline_form_unavailable_message')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Offline_message')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={3} value={Livechat_offline_message} onChange={handleLivechat_offline_message} placeholder={t('Offline_message')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Title_offline')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={Livechat_offline_title} onChange={handleLivechat_offline_title} placeholder={t('Title_offline')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Title_bar_color_offline')}</Field.Label> |
||||
<Field.Row> |
||||
<InputBox type='color' value={Livechat_offline_title_color} onChange={handleLivechat_offline_title_color}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Email_address_to_send_offline_messages')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={Livechat_offline_email} onChange={handleLivechat_offline_email} placeholder={t('Email_address_to_send_offline_messages')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Offline_success_message')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={3} value={Livechat_offline_success_message} onChange={handleLivechat_offline_success_message} placeholder={t('Offline_success_message')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
|
||||
<Accordion.Item title={t('Livechat_registration_form')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label >{t('Enabled')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={Livechat_registration_form} onChange={handleLivechat_registration_form}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label >{t('Show_name_field')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={Livechat_name_field_registration_form} onChange={handleLivechat_name_field_registration_form}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label >{t('Show_email_field')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch checked={Livechat_email_field_registration_form} onChange={handleLivechat_email_field_registration_form}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Livechat_registration_form_message')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={3} value={Livechat_registration_form_message} onChange={handleLivechat_registration_form_message} placeholder={t('Offline_message')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
<Accordion.Item title={t('Conversation_finished')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Field.Label>{t('Conversation_finished_message')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={3} value={Livechat_conversation_finished_message} onChange={handleLivechat_conversation_finished_message} placeholder={t('Offline_message')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Conversation_finished_text')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput rows={3} value={Livechat_conversation_finished_text} onChange={handleLivechat_conversation_finished_text} placeholder={t('Offline_message')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
</Accordion>; |
||||
}; |
||||
|
||||
export default AppearanceForm; |
||||
@ -0,0 +1,128 @@ |
||||
import React, { FC } from 'react'; |
||||
import { Callout, ButtonGroup, Button, Icon, Box } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import Page from '../../components/basic/Page'; |
||||
import AppearanceForm from './AppearanceForm'; |
||||
import PageSkeleton from '../../components/PageSkeleton'; |
||||
import NotAuthorizedPage from '../../components/NotAuthorizedPage'; |
||||
import { ISetting } from '../../../definition/ISetting'; |
||||
|
||||
type LivechatAppearanceEndpointData = { |
||||
success: boolean; |
||||
appearance: ISetting[]; |
||||
}; |
||||
|
||||
type LivechatAppearanceSettings = { |
||||
Livechat_title: string; |
||||
Livechat_title_color: string; |
||||
Livechat_show_agent_info: boolean; |
||||
Livechat_show_agent_email: boolean; |
||||
Livechat_display_offline_form: boolean; |
||||
Livechat_offline_form_unavailable: string; |
||||
Livechat_offline_message: string; |
||||
Livechat_offline_title: string; |
||||
Livechat_offline_title_color: string; |
||||
Livechat_offline_email: string; |
||||
Livechat_offline_success_message: string; |
||||
Livechat_registration_form: boolean; |
||||
Livechat_name_field_registration_form: boolean; |
||||
Livechat_email_field_registration_form: boolean; |
||||
Livechat_registration_form_message: string; |
||||
Livechat_conversation_finished_message: string; |
||||
Livechat_conversation_finished_text: string; |
||||
Livechat_enable_message_character_limit: boolean; |
||||
Livechat_message_character_limit: number; |
||||
}; |
||||
|
||||
type AppearanceSettings = Partial<LivechatAppearanceSettings>; |
||||
|
||||
const reduceAppearance = (settings: LivechatAppearanceEndpointData['appearance']): AppearanceSettings => |
||||
settings.reduce<Partial<LivechatAppearanceSettings>>((acc, { _id, value }) => { |
||||
acc = { ...acc, [_id]: value }; |
||||
return acc; |
||||
}, {}); |
||||
|
||||
const AppearancePageContainer: FC = () => { |
||||
const t = useTranslation(); |
||||
|
||||
const { data, state, error } = useEndpointDataExperimental<LivechatAppearanceEndpointData>('livechat/appearance'); |
||||
|
||||
const canViewAppearance = usePermission('view-livechat-appearance'); |
||||
|
||||
if (!canViewAppearance) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <PageSkeleton />; |
||||
} |
||||
|
||||
if (!data || !data.success || !data.appearance || error) { |
||||
return <Page> |
||||
<Page.Header title={t('Edit_Custom_Field')} /> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Callout type='danger'> |
||||
{t('Error')} |
||||
</Callout> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
} |
||||
|
||||
return <AppearancePage settings={data.appearance}/>; |
||||
}; |
||||
|
||||
type AppearancePageProps = { |
||||
settings: LivechatAppearanceEndpointData['appearance']; |
||||
}; |
||||
|
||||
const AppearancePage: FC<AppearancePageProps> = ({ settings }) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const save: (settings: Pick<ISetting, '_id' | 'value'>[]) => Promise<void> = useMethod('livechat:saveAppearance'); |
||||
|
||||
const { values, handlers, commit, reset, hasUnsavedChanges } = useForm(reduceAppearance(settings)); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
const mappedAppearance = Object.entries(values).map(([_id, value]) => ({ _id, value })); |
||||
|
||||
try { |
||||
await save(mappedAppearance); |
||||
dispatchToastMessage({ type: 'success', message: t('Settings_updated') }); |
||||
commit(); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'success', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleResetButtonClick = (): void => { |
||||
reset(); |
||||
}; |
||||
|
||||
return <Page> |
||||
<Page.Header title={t('Appearance')}> |
||||
<ButtonGroup align='end'> |
||||
<Button onClick={handleResetButtonClick}> |
||||
<Icon size='x16' name='back'/>{t('Back')} |
||||
</Button> |
||||
<Button primary onClick={handleSave} disabled={!hasUnsavedChanges}> |
||||
{t('Save')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Box maxWidth='x600' w='full' alignSelf='center'> |
||||
<AppearanceForm values={values} handlers={handlers}/> |
||||
</Box> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
}; |
||||
|
||||
export default AppearancePageContainer; |
||||
@ -0,0 +1,37 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Field, MultiSelect } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import TimeRangeFieldsAssembler from './TimeRangeFieldsAssembler'; |
||||
|
||||
export const DAYS_OF_WEEK = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; |
||||
|
||||
const BusinessHoursForm = ({ values, handlers, className }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const daysOptions = useMemo(() => DAYS_OF_WEEK.map((day) => [day, t(day)]), [t]); |
||||
|
||||
const { |
||||
daysOpen, |
||||
daysTime, |
||||
} = values; |
||||
|
||||
const { |
||||
handleDaysOpen, |
||||
handleDaysTime, |
||||
} = handlers; |
||||
|
||||
return <> |
||||
<Field className={className}> |
||||
<Field.Label> |
||||
{t('Open_days_of_the_week')} |
||||
</Field.Label> |
||||
<Field.Row> |
||||
<MultiSelect options={daysOptions} onChange={handleDaysOpen} value={daysOpen} placeholder={t('Select_an_option')} w='full'/> |
||||
</Field.Row> |
||||
</Field> |
||||
<TimeRangeFieldsAssembler onChange={handleDaysTime} daysOpen={daysOpen} daysTime={daysTime} className={className}/> |
||||
</>; |
||||
}; |
||||
|
||||
export default BusinessHoursForm; |
||||
@ -0,0 +1,25 @@ |
||||
import React from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
|
||||
import BusinessHoursForm from './BusinessHoursForm'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
|
||||
export default { |
||||
title: 'omnichannel/businessHours', |
||||
component: BusinessHoursForm, |
||||
}; |
||||
|
||||
export const Default = () => { |
||||
const { values, handlers } = useForm({ |
||||
daysOpen: ['Monday', 'Tuesday', 'Saturday'], |
||||
daysTime: { |
||||
Monday: { start: '00:00', finish: '08:00' }, |
||||
Tuesday: { start: '00:00', finish: '08:00' }, |
||||
Saturday: { start: '00:00', finish: '08:00' }, |
||||
}, |
||||
}); |
||||
|
||||
return <Box maxWidth='x600' alignSelf='center' w='full' m='x24'> |
||||
<BusinessHoursForm values={values} handlers={handlers}/> |
||||
</Box>; |
||||
}; |
||||
@ -0,0 +1,56 @@ |
||||
import React from 'react'; |
||||
import { FieldGroup, Box } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
import BusinessHourForm from './BusinessHoursForm'; |
||||
import { formsSubscription } from '../additionalForms'; |
||||
import { useReactiveValue } from '../../hooks/useReactiveValue'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import { businessHourManager } from '../../../app/livechat/client/views/app/business-hours/BusinessHours'; |
||||
|
||||
const useChangeHandler = (name, ref) => useMutableCallback((val) => { |
||||
ref.current[name] = { ...ref.current[name], ...val }; |
||||
}); |
||||
|
||||
const getInitalData = ({ workHours }) => ({ |
||||
daysOpen: workHours.filter(({ open }) => !!open).map(({ day }) => day), |
||||
daysTime: workHours.reduce((acc, { day, start: { time: start }, finish: { time: finish } }) => { |
||||
acc = { ...acc, [day]: { start, finish } }; |
||||
return acc; |
||||
}, {}), |
||||
}); |
||||
|
||||
const cleanFunc = () => {}; |
||||
|
||||
const BusinessHoursFormContainer = ({ data, saveRef }) => { |
||||
const forms = useSubscription(formsSubscription); |
||||
|
||||
const { |
||||
useBusinessHoursTimeZone = cleanFunc, |
||||
useBusinessHoursMultiple = cleanFunc, |
||||
} = forms; |
||||
|
||||
const TimezoneForm = useBusinessHoursTimeZone(); |
||||
const MultipleBHForm = useBusinessHoursMultiple(); |
||||
|
||||
const showTimezone = useReactiveValue(useMutableCallback(() => businessHourManager.showTimezoneTemplate())); |
||||
const showMultipleBHForm = useReactiveValue(useMutableCallback(() => businessHourManager.showCustomTemplate(data))); |
||||
|
||||
const onChangeTimezone = useChangeHandler('timezone', saveRef); |
||||
const onChangeMultipleBHForm = useChangeHandler('multiple', saveRef); |
||||
|
||||
const { values, handlers } = useForm(getInitalData(data)); |
||||
|
||||
saveRef.current.form = values; |
||||
|
||||
return <Box maxWidth='600px' w='full' alignSelf='center'> |
||||
<FieldGroup> |
||||
{showMultipleBHForm && MultipleBHForm && <MultipleBHForm onChange={onChangeMultipleBHForm} data={data}/>} |
||||
{showTimezone && TimezoneForm && <TimezoneForm onChange={onChangeTimezone} data={data?.timezone?.name ?? data?.timezoneName}/>} |
||||
<BusinessHourForm values={values} handlers={handlers}/> |
||||
</FieldGroup> |
||||
</Box>; |
||||
}; |
||||
|
||||
export default BusinessHoursFormContainer; |
||||
@ -0,0 +1,36 @@ |
||||
import React, { lazy, useMemo } from 'react'; |
||||
import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
const BusinessHoursPage = () => { |
||||
const t = useTranslation(); |
||||
|
||||
const router = useRoute('omnichannel-businessHours'); |
||||
|
||||
const Table = useMemo(() => lazy(() => import('../../../ee/client/omnichannel/BusinessHoursTable')), []); |
||||
|
||||
const handleNew = useMutableCallback(() => { |
||||
router.push({ |
||||
context: 'new', |
||||
}); |
||||
}); |
||||
|
||||
return <Page> |
||||
<Page.Header title={t('Business_Hours')}> |
||||
<ButtonGroup> |
||||
<Button small square onClick={handleNew}> |
||||
<Icon name='plus'/> |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Table /> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
}; |
||||
|
||||
export default BusinessHoursPage; |
||||
@ -0,0 +1,41 @@ |
||||
import React, { useEffect } from 'react'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import EditBusinessHoursPage from './EditBusinessHoursPage'; |
||||
import NewBusinessHoursPage from './NewBusinessHoursPage'; |
||||
import BusinessHoursPage from './BusinessHoursPage'; |
||||
import { useRoute, useRouteParameter } from '../../contexts/RouterContext'; |
||||
import { useReactiveValue } from '../../hooks/useReactiveValue'; |
||||
import { businessHourManager } from '../../../app/livechat/client/views/app/business-hours/BusinessHours'; |
||||
|
||||
export const useIsSingleBusinessHours = () => useReactiveValue(useMutableCallback(() => businessHourManager.getTemplate())) === 'livechatBusinessHoursForm'; |
||||
|
||||
const BusinessHoursRouter = () => { |
||||
const context = useRouteParameter('context'); |
||||
const id = useRouteParameter('id'); |
||||
const type = useRouteParameter('type'); |
||||
const isSingleBH = useIsSingleBusinessHours(); |
||||
|
||||
const router = useRoute('omnichannel-businessHours'); |
||||
|
||||
useEffect(() => { |
||||
if (isSingleBH && (context !== 'edit' || type !== 'default')) { |
||||
router.push({ |
||||
context: 'edit', |
||||
type: 'default', |
||||
}); |
||||
} |
||||
}, [context, isSingleBH, router, type]); |
||||
|
||||
if ((context === 'edit' && type) || (isSingleBH && (context !== 'edit' || type !== 'default'))) { |
||||
return <EditBusinessHoursPage type={type} id={id}/>; |
||||
} |
||||
|
||||
if (context === 'new') { |
||||
return <NewBusinessHoursPage/>; |
||||
} |
||||
|
||||
return <BusinessHoursPage />; |
||||
}; |
||||
|
||||
export default BusinessHoursRouter; |
||||
@ -0,0 +1,122 @@ |
||||
import React, { useRef, useMemo } from 'react'; |
||||
import { Button, ButtonGroup, Callout } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import BusinessHoursFormContainer from './BusinessHoursFormContainer'; |
||||
import Page from '../../components/basic/Page'; |
||||
import PageSkeleton from '../../components/PageSkeleton'; |
||||
import { useIsSingleBusinessHours } from './BusinessHoursRouter'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import { mapBusinessHoursForm } from './mapBusinessHoursForm'; |
||||
|
||||
const EditBusinessHoursPage = ({ id, type }) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const isSingleBH = useIsSingleBusinessHours(); |
||||
|
||||
const { data, state } = useEndpointDataExperimental('livechat/business-hour', useMemo(() => ({ _id: id, type }), [id, type])); |
||||
|
||||
const saveData = useRef({ form: {} }); |
||||
|
||||
const save = useMethod('livechat:saveBusinessHour'); |
||||
const deleteBH = useMethod('livechat:removeBusinessHour'); |
||||
|
||||
const router = useRoute('omnichannel-businessHours'); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
if (state !== ENDPOINT_STATES.DONE || !data.success) { |
||||
return; |
||||
} |
||||
|
||||
const { current: { |
||||
form, |
||||
multiple: { departments, ...multiple } = {}, |
||||
timezone: { name: timezoneName } = {}, |
||||
} } = saveData; |
||||
|
||||
if (data.businessHour.type !== 'default' && multiple.name === '') { |
||||
return dispatchToastMessage({ type: 'error', message: t('error-the-field-is-required', { field: t('Name') }) }); |
||||
} |
||||
|
||||
const mappedForm = mapBusinessHoursForm(form, data.businessHour); |
||||
|
||||
const departmentsToApplyBusinessHour = departments?.join(',') || ''; |
||||
|
||||
try { |
||||
const payload = { |
||||
...data.businessHour, |
||||
...multiple, |
||||
departmentsToApplyBusinessHour: departmentsToApplyBusinessHour ?? '', |
||||
timezoneName: timezoneName || data.businessHour.timezone.name, |
||||
workHours: mappedForm, |
||||
}; |
||||
|
||||
await save(payload); |
||||
dispatchToastMessage({ type: 'success', message: t('Business_hours_updated') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleDelete = useMutableCallback(async () => { |
||||
if (type !== 'custom') { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
await deleteBH(id, type); |
||||
dispatchToastMessage({ type: 'success', message: t('Business_Hour_Removed') }); |
||||
router.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleReturn = useMutableCallback(() => { |
||||
router.push({}); |
||||
}); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <PageSkeleton />; |
||||
} |
||||
|
||||
if (state === ENDPOINT_STATES.ERROR || (ENDPOINT_STATES.DONE && !data.businessHour)) { |
||||
return <Page> |
||||
<Page.Header title={t('Business_Hours')}> |
||||
<Button onClick={handleReturn}> |
||||
{t('Back')} |
||||
</Button> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<Callout type='danger'> |
||||
{t('Error')} |
||||
</Callout> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
} |
||||
|
||||
return <Page> |
||||
<Page.Header title={t('Business_Hours')}> |
||||
<ButtonGroup> |
||||
{!isSingleBH && <Button onClick={handleReturn}> |
||||
{t('Back')} |
||||
</Button>} |
||||
{type === 'custom' && <Button primary danger onClick={handleDelete}> |
||||
{t('Delete')} |
||||
</Button>} |
||||
<Button primary onClick={handleSave}> |
||||
{t('Save')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<BusinessHoursFormContainer data={data.businessHour} saveRef={saveData}/> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
}; |
||||
|
||||
export default EditBusinessHoursPage; |
||||
@ -0,0 +1,97 @@ |
||||
import React, { useRef } from 'react'; |
||||
import { Button, ButtonGroup } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import BusinessHoursFormContainer from './BusinessHoursFormContainer'; |
||||
import Page from '../../components/basic/Page'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { mapBusinessHoursForm } from './mapBusinessHoursForm'; |
||||
import { DAYS_OF_WEEK } from './BusinessHoursForm'; |
||||
|
||||
const closedDays = ['Saturday', 'Sunday']; |
||||
const createDefaultBusinessHours = () => ({ |
||||
name: '', |
||||
workHours: DAYS_OF_WEEK.map((day) => ({ |
||||
day, |
||||
start: { |
||||
time: '00:00', |
||||
}, |
||||
finish: { |
||||
time: '00:00', |
||||
}, |
||||
open: !closedDays.includes(day), |
||||
})), |
||||
departments: [], |
||||
timezoneName: 'America/Sao_Paulo', |
||||
departmentsToApplyBusinessHour: '', |
||||
}); |
||||
|
||||
const defaultBusinessHour = createDefaultBusinessHours(); |
||||
|
||||
const NewBusinessHoursPage = () => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const saveData = useRef({ form: {} }); |
||||
|
||||
const save = useMethod('livechat:saveBusinessHour'); |
||||
const router = useRoute('omnichannel-businessHours'); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
const { current: { |
||||
form, |
||||
multiple: { departments, ...multiple } = {}, |
||||
timezone: { name: timezoneName } = {}, |
||||
} } = saveData; |
||||
|
||||
if (multiple.name === '') { |
||||
return dispatchToastMessage({ type: 'error', message: t('error-the-field-is-required', { field: t('Name') }) }); |
||||
} |
||||
|
||||
const mappedForm = mapBusinessHoursForm(form, defaultBusinessHour); |
||||
|
||||
const departmentsToApplyBusinessHour = departments?.join(',') || ''; |
||||
|
||||
try { |
||||
const payload = { |
||||
...defaultBusinessHour, |
||||
...multiple, |
||||
...departmentsToApplyBusinessHour && { departmentsToApplyBusinessHour }, |
||||
timezoneName, |
||||
workHours: mappedForm, |
||||
type: 'custom', |
||||
}; |
||||
|
||||
await save(payload); |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
router.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleReturn = useMutableCallback(() => { |
||||
router.push({}); |
||||
}); |
||||
|
||||
return <Page> |
||||
<Page.Header title={t('Business_Hours')}> |
||||
<ButtonGroup> |
||||
<Button onClick={handleReturn}> |
||||
{t('Back')} |
||||
</Button> |
||||
<Button primary onClick={handleSave}> |
||||
{t('Save')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<BusinessHoursFormContainer data={defaultBusinessHour} saveRef={saveData}/> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
}; |
||||
|
||||
export default NewBusinessHoursPage; |
||||
@ -0,0 +1,29 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Field } from '@rocket.chat/fuselage'; |
||||
import { useStableArray } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import TimeRangeInput from './TimeRangeInput'; |
||||
import { DAYS_OF_WEEK } from './BusinessHoursForm'; |
||||
|
||||
const TimeRangeFieldsAssembler = ({ onChange, daysOpen, daysTime, className }) => { |
||||
const t = useTranslation(); |
||||
const handleChange = (day) => (start, finish) => onChange({ ...daysTime, [day]: { start, finish } }); |
||||
|
||||
const stableDaysOpen = useStableArray(daysOpen); |
||||
const daysList = useMemo(() => DAYS_OF_WEEK.filter((day) => stableDaysOpen.includes(day)), [stableDaysOpen]); |
||||
|
||||
return <> |
||||
{daysList.map((day) => |
||||
<Field className={className} key={day}> |
||||
<Field.Label> |
||||
{t(day)} |
||||
</Field.Label> |
||||
<Field.Row> |
||||
<TimeRangeInput onChange={handleChange(day)} start={daysTime[day]?.start} finish={daysTime[day]?.finish}/> |
||||
</Field.Row> |
||||
</Field>)} |
||||
</>; |
||||
}; |
||||
|
||||
export default TimeRangeFieldsAssembler; |
||||
@ -0,0 +1,43 @@ |
||||
import React, { useState } from 'react'; |
||||
import { Box, InputBox } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
const TimeRangeInput = ({ onChange, start: startDefault, finish: finishDefault }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const [start, setStart] = useState(startDefault); |
||||
const [finish, setFinish] = useState(finishDefault); |
||||
|
||||
const handleChangeFrom = useMutableCallback(({ currentTarget: { value } }) => { |
||||
setStart(value); |
||||
onChange(value, finish); |
||||
}); |
||||
|
||||
const handleChangeTo = useMutableCallback(({ currentTarget: { value } }) => { |
||||
setFinish(value); |
||||
onChange(start, value); |
||||
}); |
||||
|
||||
return <> |
||||
<Box display='flex' flexDirection='column' flexGrow={1} mie='x2'> |
||||
{t('Open')}: |
||||
<InputBox |
||||
type='time' |
||||
value={start} |
||||
onChange={handleChangeFrom} |
||||
/> |
||||
</Box> |
||||
<Box display='flex' flexDirection='column' flexGrow={1} mis='x2'> |
||||
{t('Close')}: |
||||
<InputBox |
||||
type='time' |
||||
value={finish} |
||||
onChange={handleChangeTo} |
||||
/> |
||||
</Box> |
||||
</>; |
||||
}; |
||||
|
||||
export default TimeRangeInput; |
||||
@ -0,0 +1,13 @@ |
||||
export const mapBusinessHoursForm = (formData, data) => { |
||||
const { daysOpen, daysTime } = formData; |
||||
|
||||
return data.workHours?.map((day) => { |
||||
const { day: currentDay, start: { time: start }, finish: { time: finish } } = day; |
||||
const open = daysOpen.includes(currentDay); |
||||
if (daysTime[currentDay]) { |
||||
const { start, finish } = daysTime[currentDay]; |
||||
return { day: currentDay, start, finish, open }; |
||||
} |
||||
return { day: currentDay, start, finish, open }; |
||||
}); |
||||
}; |
||||
@ -0,0 +1,174 @@ |
||||
import React, { useEffect, useMemo } from 'react'; |
||||
import { TextInput, Box, Icon, MultiSelect, Select, InputBox, Menu } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import moment from 'moment'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
import { formsSubscription } from '../additionalForms'; |
||||
import Page from '../../components/basic/Page'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import { GenericTable } from '../../components/GenericTable'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
|
||||
|
||||
// moment(new Date(from)).utc().format('YYYY-MM-DDTHH:mm:ss')
|
||||
// guest: '', servedBy: '', status: '', department: '', from: '', to: ''
|
||||
const Label = (props) => <Box fontScale='p2' color='default' {...props} />; |
||||
|
||||
const RemoveAllClosed = ({ handleClearFilters, handleRemoveClosed, ...props }) => { |
||||
const t = useTranslation(); |
||||
const canRemove = usePermission('remove-closed-livechat-rooms'); |
||||
|
||||
const menuOptions = { |
||||
clearFilters: { |
||||
label: <Box> |
||||
<Icon name='refresh' size='x16' marginInlineEnd='x4' />{t('Clear_filters')} |
||||
</Box>, |
||||
action: handleClearFilters, |
||||
}, |
||||
...canRemove && { |
||||
removeClosed: { |
||||
label: <Box color='danger'> |
||||
<Icon name='trash' size='x16' marginInlineEnd='x4' />{t('Delete_all_closed_chats')} |
||||
</Box>, |
||||
action: handleRemoveClosed, |
||||
}, |
||||
}, |
||||
}; |
||||
return <Menu alignSelf='flex-end' small={false} square options={menuOptions} placement='bottom-start' {...props}/>; |
||||
}; |
||||
|
||||
|
||||
const FilterByText = ({ setFilter, reload, ...props }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const { data: departments } = useEndpointDataExperimental('livechat/department') || {}; |
||||
const { data: agents } = useEndpointDataExperimental('livechat/users/agent'); |
||||
|
||||
const depOptions = useMemo(() => (departments && departments.departments ? departments.departments.map(({ _id, name }) => [_id, name || _id]) : []), [departments]); |
||||
const agentOptions = useMemo(() => (agents && agents.users ? agents.users.map(({ _id, username }) => [_id, username || _id]) : []), [agents]); |
||||
const statusOptions = [['all', t('All')], ['closed', t('Closed')], ['opened', t('Open')]]; |
||||
|
||||
useEffect(() => { |
||||
!depOptions.find((dep) => dep[0] === 'all') && depOptions.unshift(['all', t('All')]); |
||||
}, [depOptions, t]); |
||||
|
||||
const { values, handlers, reset } = useForm({ guest: '', servedBy: [], status: 'all', department: 'all', from: '', to: '', tags: [] }); |
||||
const { |
||||
handleGuest, |
||||
handleServedBy, |
||||
handleStatus, |
||||
handleDepartment, |
||||
handleFrom, |
||||
handleTo, |
||||
handleTags, |
||||
} = handlers; |
||||
const { |
||||
guest, |
||||
servedBy, |
||||
status, |
||||
department, |
||||
from, |
||||
to, |
||||
tags, |
||||
} = values; |
||||
|
||||
const forms = useSubscription(formsSubscription); |
||||
|
||||
const { |
||||
useCurrentChatTags = () => {}, |
||||
} = forms; |
||||
|
||||
const Tags = useCurrentChatTags(); |
||||
|
||||
|
||||
const onSubmit = useMutableCallback((e) => e.preventDefault()); |
||||
|
||||
useEffect(() => { |
||||
setFilter({ |
||||
guest, |
||||
servedBy, |
||||
status, |
||||
department, |
||||
from: from && moment(new Date(from)).utc().format('YYYY-MM-DDTHH:mm:ss'), |
||||
to: to && moment(new Date(to)).utc().format('YYYY-MM-DDTHH:mm:ss'), |
||||
tags, |
||||
}); |
||||
}, [setFilter, guest, servedBy, status, department, from, to, tags]); |
||||
|
||||
const handleClearFilters = useMutableCallback(() => { |
||||
reset(); |
||||
}); |
||||
|
||||
const removeClosedChats = useMethod('livechat:removeAllClosedRooms'); |
||||
|
||||
const handleRemoveClosed = useMutableCallback(async () => { |
||||
await removeClosedChats(); |
||||
reload(); |
||||
}); |
||||
|
||||
return <Box mb='x16' is='form' onSubmit={onSubmit} display='flex' flexDirection='column' {...props}> |
||||
<Box display='flex' flexDirection='row' flexWrap='wrap' {...props}> |
||||
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'> |
||||
<Label mb='x4' >{t('Guest')}:</Label> |
||||
<TextInput flexShrink={0} placeholder={t('Guest')} onChange={handleGuest} value={guest} /> |
||||
</Box> |
||||
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'> |
||||
<Label mb='x4'>{t('Served_By')}:</Label> |
||||
<MultiSelect flexShrink={0} options={agentOptions} value={servedBy} onChange={handleServedBy} placeholder={t('Served_By')}/> |
||||
</Box> |
||||
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'> |
||||
<Label mb='x4'>{t('Department')}:</Label> |
||||
<Select flexShrink={0} options={depOptions} value={department} onChange={handleDepartment} placeholder={t('Department')}/> |
||||
</Box> |
||||
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'> |
||||
<Label mb='x4'>{t('Status')}:</Label> |
||||
<Select flexShrink={0} options={statusOptions} value={status} onChange={handleStatus} placeholder={t('Status')}/> |
||||
</Box> |
||||
<Box display='flex' mie='x8' flexGrow={0} flexDirection='column'> |
||||
<Label mb='x4'>{t('From')}:</Label> |
||||
<InputBox type='date' flexShrink={0} placeholder={t('From')} onChange={handleFrom} value={from} /> |
||||
</Box> |
||||
<Box display='flex' mie='x8' flexGrow={0} flexDirection='column'> |
||||
<Label mb='x4'>{t('To')}:</Label> |
||||
<InputBox type='date' flexShrink={0} placeholder={t('To')} onChange={handleTo} value={to} /> |
||||
</Box> |
||||
|
||||
<RemoveAllClosed handleClearFilters={handleClearFilters} handleRemoveClosed={handleRemoveClosed}/> |
||||
</Box> |
||||
{Tags && <Box display='flex' flexDirection='row' marginBlockStart='x8' {...props}> |
||||
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'> |
||||
<Label mb='x4'>{t('Tags')}:</Label> |
||||
<Tags value={tags} handler={handleTags} /> |
||||
</Box> |
||||
</Box>} |
||||
</Box>; |
||||
}; |
||||
|
||||
|
||||
function CurrentChatsPage({ |
||||
data, |
||||
header, |
||||
setParams, |
||||
params, |
||||
title, |
||||
renderRow, |
||||
departments, |
||||
reload, |
||||
children, |
||||
}) { |
||||
return <Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={title} /> |
||||
<Page.Content> |
||||
<GenericTable FilterComponent={FilterByText} header={header} renderRow={renderRow} results={data && data.rooms} departments={departments} total={data && data.total} setParams={setParams} params={params} reload={reload}/> |
||||
</Page.Content> |
||||
</Page> |
||||
{children} |
||||
</Page>; |
||||
} |
||||
|
||||
export default CurrentChatsPage; |
||||
@ -0,0 +1,137 @@ |
||||
|
||||
|
||||
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useMemo, useCallback, useState } from 'react'; |
||||
import { Table, Icon } from '@rocket.chat/fuselage'; |
||||
import moment from 'moment'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
|
||||
|
||||
import { Th } from '../../components/GenericTable'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import NotAuthorizedPage from '../../components/NotAuthorizedPage'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import CurrentChatsPage from './CurrentChatsPage'; |
||||
|
||||
export function RemoveCurrentChatButton({ _id, reload }) { |
||||
const removeCurrentChat = useMethod('livechat:removeCurrentChat'); |
||||
const currentChatsRoute = useRoute('omnichannel-currentChats'); |
||||
|
||||
|
||||
const handleRemoveClick = useMutableCallback(async (e) => { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
try { |
||||
await removeCurrentChat(_id); |
||||
} catch (error) { |
||||
console.log(error); |
||||
} |
||||
currentChatsRoute.push({}); |
||||
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 = ({ guest, servedBy, department, status, from, to, tags, itemsPerPage, current }, [column, direction]) => useMemo(() => { |
||||
const query = { |
||||
roomName: guest, |
||||
sort: JSON.stringify({ [column]: sortDir(direction), usernames: column === 'name' ? sortDir(direction) : undefined }), |
||||
...itemsPerPage && { count: itemsPerPage }, |
||||
...current && { offset: current }, |
||||
}; |
||||
|
||||
if (from && to) { |
||||
query.createdAt = JSON.stringify({ start: from, end: to }); |
||||
} |
||||
if (status !== 'all') { |
||||
query.open = status === 'open'; |
||||
} |
||||
if (servedBy && servedBy.length > 0) { |
||||
query.agents = servedBy; |
||||
} |
||||
if (department && department.length > 0) { |
||||
if (department !== 'all') { |
||||
query.departmentId = department; |
||||
} |
||||
} |
||||
if (tags && tags.length > 0) { |
||||
query.tags = tags; |
||||
} |
||||
|
||||
return query; |
||||
}, [guest, column, direction, itemsPerPage, current, from, to, status, servedBy, department, tags]); |
||||
|
||||
function CurrentChatsRoute() { |
||||
const t = useTranslation(); |
||||
const canViewCurrentChats = usePermission('view-livechat-current-chats'); |
||||
|
||||
const [params, setParams] = useState({ fname: '', servedBy: [], status: '', department: '', from: '', to: '', 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 livechatRoomRoute = useRoute('live/: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) => { |
||||
FlowRouter.go('live', { id: _id }); |
||||
// routing this way causes a 404 that only goes away with a refresh, need to fix in review
|
||||
// livechatRoomRoute.push({ id: _id });
|
||||
}); |
||||
|
||||
const { data, reload } = useEndpointDataExperimental('livechat/rooms', query) || {}; |
||||
const { data: departments } = useEndpointDataExperimental('livechat/department', query) || {}; |
||||
|
||||
const header = useMemo(() => [ |
||||
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w='x120'>{t('Name')}</Th>, |
||||
<Th key={'departmentId'} direction={sort[1]} active={sort[0] === 'departmentId'} onClick={onHeaderClick} sort='departmentId' w='x200'>{t('Department')}</Th>, |
||||
<Th key={'servedBy'} direction={sort[1]} active={sort[0] === 'servedBy'} onClick={onHeaderClick} sort='servedBy' w='x120'>{t('Served_by')}</Th>, |
||||
<Th key={'ts'} direction={sort[1]} active={sort[0] === 'ts'} onClick={onHeaderClick} sort='ts' w='x120'>{t('Started_at')}</Th>, |
||||
<Th key={'lm'} direction={sort[1]} active={sort[0] === 'lm'} onClick={onHeaderClick} sort='visibility' w='x120'>{t('Last_message')}</Th>, |
||||
<Th key={'status'} direction={sort[1]} active={sort[0] === 'status'} onClick={onHeaderClick} sort='status' w='x120'>{t('Status')}</Th>, |
||||
].filter(Boolean), [sort, onHeaderClick, t]); |
||||
|
||||
const renderRow = useCallback(({ _id, fname, servedBy, ts, lm, department, open }) => <Table.Row key={_id} tabIndex={0} role='link' onClick={() => onRowClick(_id)} action qa-user-id={_id}> |
||||
<Table.Cell withTruncatedText>{fname}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{department ? department.name : ''}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{servedBy && servedBy.username}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{moment(ts).format('L LTS')}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{moment(lm).format('L LTS')}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{open ? t('Open') : t('Closed')}</Table.Cell> |
||||
</Table.Row>, [onRowClick, t]); |
||||
|
||||
if (!canViewCurrentChats) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
|
||||
return <CurrentChatsPage |
||||
setParams={setParams} |
||||
params={params} |
||||
onHeaderClick={onHeaderClick} |
||||
data={data} useQuery={useQuery} |
||||
reload={reload} |
||||
header={header} |
||||
renderRow={renderRow} |
||||
departments={departments} |
||||
title={'Current Chats'}> |
||||
</CurrentChatsPage>; |
||||
} |
||||
|
||||
export default CurrentChatsRoute; |
||||
@ -0,0 +1,66 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Box, Field, TextInput, ToggleSwitch, Select } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
const CustomFieldsForm = ({ values = {}, handlers = {}, className }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const { |
||||
field, |
||||
label, |
||||
scope, |
||||
visibility, |
||||
regexp, |
||||
} = values; |
||||
|
||||
const { |
||||
handleField, |
||||
handleLabel, |
||||
handleScope, |
||||
handleVisibility, |
||||
handleRegexp, |
||||
} = handlers; |
||||
|
||||
const scopeOptions = useMemo(() => [ |
||||
['visitor', t('Visitor')], |
||||
['room', t('Room')], |
||||
], [t]); |
||||
|
||||
return <> |
||||
<Field className={className}> |
||||
<Field.Label>{t('Field')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={field} onChange={handleField} placeholder={t('Field')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field className={className}> |
||||
<Field.Label>{t('Label')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={label} onChange={handleLabel} placeholder={t('Label')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field className={className}> |
||||
<Field.Label>{t('Scope')}</Field.Label> |
||||
<Field.Row> |
||||
<Select options={scopeOptions} value={scope} onChange={handleScope}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field className={className}> |
||||
<Box display='flex' flexDirection='row'> |
||||
<Field.Label htmlFor='visible'>{t('Visible')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch id='visible' checked={visibility} onChange={handleVisibility}/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field className={className}> |
||||
<Field.Label>{t('Validation')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={regexp} onChange={handleRegexp} placeholder={t('Label')}/> |
||||
</Field.Row> |
||||
</Field> |
||||
</>; |
||||
}; |
||||
|
||||
export default CustomFieldsForm; |
||||
@ -0,0 +1,24 @@ |
||||
import React from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
|
||||
import CustomFieldsForm from './CustomFieldsForm'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
|
||||
export default { |
||||
title: 'omnichannel/customFields/NewCustomFieldsForm', |
||||
component: CustomFieldsForm, |
||||
}; |
||||
|
||||
export const Default = () => { |
||||
const { values, handlers } = useForm({ |
||||
field: '', |
||||
label: '', |
||||
scope: 'visitor', |
||||
visibility: true, |
||||
regexp: '', |
||||
}); |
||||
|
||||
return <Box maxWidth='x600' alignSelf='center' w='full' m='x24'> |
||||
<CustomFieldsForm values={values} handlers={handlers} /> |
||||
</Box>; |
||||
}; |
||||
@ -0,0 +1,29 @@ |
||||
import React from 'react'; |
||||
import { Button, Icon } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import CustomFieldsTable from './CustomFieldsTable'; |
||||
|
||||
const CustomFieldsPage = () => { |
||||
const t = useTranslation(); |
||||
|
||||
const router = useRoute('omnichannel-customfields'); |
||||
|
||||
const onAddNew = useMutableCallback(() => router.push({ context: 'new' })); |
||||
|
||||
return <Page> |
||||
<Page.Header title={t('Custom_Fields')}> |
||||
<Button small onClick={onAddNew}> |
||||
<Icon name='plus' size='x16'/> |
||||
</Button> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<CustomFieldsTable /> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page>; |
||||
}; |
||||
|
||||
export default CustomFieldsPage; |
||||
@ -0,0 +1,27 @@ |
||||
import React from 'react'; |
||||
|
||||
import { useRouteParameter } from '../../contexts/RouterContext'; |
||||
import CustomFieldsPage from './CustomFieldsPage'; |
||||
import NewCustomFieldsPage from './NewCustomFieldsPage'; |
||||
import EditCustomFieldsPage from './EditCustomFieldsPage'; |
||||
|
||||
|
||||
const CustomFieldsRouter = () => { |
||||
const context = useRouteParameter('context'); |
||||
|
||||
if (!context) { |
||||
return <CustomFieldsPage />; |
||||
} |
||||
|
||||
if (context === 'new') { |
||||
return <NewCustomFieldsPage />; |
||||
} |
||||
|
||||
if (context === 'edit') { |
||||
return <EditCustomFieldsPage />; |
||||
} |
||||
|
||||
return undefined; |
||||
}; |
||||
|
||||
export default CustomFieldsRouter; |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue