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
gabriellsh 5 years ago committed by GitHub
parent fe7fe3c462
commit ed7ff1f01d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .eslintrc
  2. 2
      .storybook/main.js
  3. 1
      app/api/server/v1/users.js
  4. 1
      app/livechat/client/index.js
  5. 129
      app/livechat/client/route.js
  6. 4
      app/livechat/client/ui.js
  7. 15
      app/livechat/client/views/admin.js
  8. 65
      app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.html
  9. 186
      app/livechat/client/views/app/business-hours/livechatBusinessHoursForm.js
  10. 5
      app/livechat/client/views/app/business-hours/livechatMainBusinessHours.html
  11. 10
      app/livechat/client/views/app/business-hours/livechatMainBusinessHours.js
  12. 43
      app/livechat/client/views/app/integrations/livechatIntegrationFacebook.html
  13. 157
      app/livechat/client/views/app/integrations/livechatIntegrationFacebook.js
  14. 88
      app/livechat/client/views/app/integrations/livechatIntegrationWebhook.html
  15. 146
      app/livechat/client/views/app/integrations/livechatIntegrationWebhook.js
  16. 103
      app/livechat/client/views/app/livechatAgents.html
  17. 234
      app/livechat/client/views/app/livechatAgents.js
  18. 145
      app/livechat/client/views/app/livechatAppearance.html
  19. 345
      app/livechat/client/views/app/livechatAppearance.js
  20. 25
      app/livechat/client/views/app/livechatCurrentChats.css
  21. 196
      app/livechat/client/views/app/livechatCurrentChats.html
  22. 519
      app/livechat/client/views/app/livechatCurrentChats.js
  23. 57
      app/livechat/client/views/app/livechatCustomFieldForm.html
  24. 100
      app/livechat/client/views/app/livechatCustomFieldForm.js
  25. 37
      app/livechat/client/views/app/livechatCustomFields.html
  26. 83
      app/livechat/client/views/app/livechatCustomFields.js
  27. 10
      app/livechat/client/views/app/livechatInstallation.html
  28. 21
      app/livechat/client/views/app/livechatInstallation.js
  29. 88
      app/livechat/client/views/app/livechatManagers.html
  30. 192
      app/livechat/client/views/app/livechatManagers.js
  31. 33
      app/livechat/client/views/app/livechatTriggers.html
  32. 62
      app/livechat/client/views/app/livechatTriggers.js
  33. 62
      app/livechat/client/views/app/livechatTriggersForm.html
  34. 118
      app/livechat/client/views/app/livechatTriggersForm.js
  35. 20
      app/livechat/client/views/app/triggers/livechatTriggerAction.html
  36. 54
      app/livechat/client/views/app/triggers/livechatTriggerAction.js
  37. 18
      app/livechat/client/views/app/triggers/livechatTriggerCondition.html
  38. 34
      app/livechat/client/views/app/triggers/livechatTriggerCondition.js
  39. 19
      app/livechat/client/views/sideNav/livechatFlex.html
  40. 34
      app/livechat/client/views/sideNav/livechatFlex.js
  41. 27
      app/livechat/client/views/sideNav/livechatSideNavItems.js
  42. 21
      app/ui-sidenav/client/sidebarHeader.js
  43. 8
      client/account/AccountSidebar.js
  44. 2
      client/admin/AdministrationRouter.js
  45. 2
      client/admin/apps/AppsRoute.js
  46. 26
      client/admin/routes.js
  47. 8
      client/admin/sidebar/AdminSidebar.js
  48. 4
      client/admin/users/EditUser.js
  49. 3
      client/components/GenericTable.js
  50. 2
      client/components/PageSkeleton.js
  51. 23
      client/components/basic/AutoComplete.js
  52. 10
      client/components/basic/AutoComplete.stories.js
  53. 10
      client/components/basic/ExternalLink.js
  54. 13
      client/components/basic/ExternalLink.tsx
  55. 52
      client/components/basic/Page.tsx
  56. 2
      client/components/basic/Sidebar.js
  57. 30
      client/components/basic/TextCopy.js
  58. 8
      client/components/basic/UserInfo.js
  59. 11
      client/contexts/ConnectionStatusContext.js
  60. 20
      client/contexts/ConnectionStatusContext.ts
  61. 7
      client/contexts/CustomSoundContext.js
  62. 12
      client/contexts/CustomSoundContext.ts
  63. 83
      client/contexts/RouterContext.js
  64. 156
      client/contexts/RouterContext.ts
  65. 18
      client/contexts/ServerContext.ts
  66. 5
      client/contexts/SidebarContext.js
  67. 8
      client/contexts/SidebarContext.ts
  68. 7
      client/contexts/ToastMessagesContext.js
  69. 19
      client/contexts/ToastMessagesContext.ts
  70. 30
      client/helpers/createRouteGroup.js
  71. 42
      client/hooks/useEndpointDataExperimental.ts
  72. 25
      client/hooks/useForm.ts
  73. 4
      client/hooks/useTimezoneNameList.js
  74. 23
      client/omnichannel/DeleteWarningModal.js
  75. 22
      client/omnichannel/OmnichannelRouter.tsx
  76. 26
      client/omnichannel/additionalForms.js
  77. 154
      client/omnichannel/agents/AgentEdit.js
  78. 77
      client/omnichannel/agents/AgentInfo.js
  79. 72
      client/omnichannel/agents/AgentsPage.js
  80. 170
      client/omnichannel/agents/AgentsRoute.js
  81. 11
      client/omnichannel/agents/Skeleton.js
  82. 10
      client/omnichannel/appearance/AppearanceForm.stories.js
  83. 251
      client/omnichannel/appearance/AppearanceForm.tsx
  84. 128
      client/omnichannel/appearance/AppearancePage.tsx
  85. 37
      client/omnichannel/businessHours/BusinessHoursForm.js
  86. 25
      client/omnichannel/businessHours/BusinessHoursForm.stories.js
  87. 56
      client/omnichannel/businessHours/BusinessHoursFormContainer.js
  88. 36
      client/omnichannel/businessHours/BusinessHoursPage.js
  89. 41
      client/omnichannel/businessHours/BusinessHoursRouter.js
  90. 122
      client/omnichannel/businessHours/EditBusinessHoursPage.js
  91. 97
      client/omnichannel/businessHours/NewBusinessHoursPage.js
  92. 29
      client/omnichannel/businessHours/TimeRangeFieldsAssembler.js
  93. 43
      client/omnichannel/businessHours/TimeRangeInput.js
  94. 13
      client/omnichannel/businessHours/mapBusinessHoursForm.js
  95. 174
      client/omnichannel/currentChats/CurrentChatsPage.js
  96. 137
      client/omnichannel/currentChats/CurrentChatsRoute.js
  97. 66
      client/omnichannel/customFields/CustomFieldsForm.js
  98. 24
      client/omnichannel/customFields/CustomFieldsForm.stories.js
  99. 29
      client/omnichannel/customFields/CustomFieldsPage.js
  100. 27
      client/omnichannel/customFields/CustomFieldsRouter.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -73,12 +73,15 @@
"@typescript-eslint"
],
"rules": {
"func-call-spacing": "off",
"jsx-quotes": [
"error",
"prefer-single"
],
"indent": "off",
"no-extra-parens": "off",
"no-spaced-func": "off",
"no-useless-constructor": "off",
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/jsx-no-undef": "error",

@ -2,7 +2,7 @@ module.exports = {
stories: [
'../app/**/*.stories.js',
'../client/**/*.stories.js',
'../ee/app/**/*.stories.js',
'../ee/**/*.stories.js',
],
addons: [
'@storybook/addon-actions',

@ -771,6 +771,7 @@ API.v1.addRoute('users.requestDataDownload', { authRequired: true }, {
API.v1.addRoute('users.autocomplete', { authRequired: true }, {
get() {
const { selector } = this.queryParams;
if (!selector) {
return API.v1.failure('The \'selector\' param is required');
}

@ -7,6 +7,5 @@ import './startup/notifyUnreadRooms';
import './views/app/dialog/closeRoom';
import './stylesheets/livechat.css';
import './views/sideNav/livechat';
import './views/sideNav/livechatFlex';
import './externalFrame';
import './lib/messageTypes';

@ -1,10 +1,11 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { AccountBox } from '../../ui-utils';
import '../../../client/omnichannel/routes';
export const livechatManagerRoutes = FlowRouter.group({
prefix: '/livechat-manager',
name: 'livechat-manager',
prefix: '/omnichannel',
name: 'omnichannel',
});
export const load = () => import('./views/admin');
@ -12,23 +13,15 @@ export const load = () => import('./views/admin');
AccountBox.addRoute({
name: 'livechat-dashboard',
path: '/dashboard',
sideNav: 'livechatFlex',
sideNav: 'omnichannelFlex',
i18nPageTitle: 'Livechat_Dashboard',
pageTemplate: 'livechatDashboard',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-current-chats',
path: '/current',
sideNav: 'livechatFlex',
i18nPageTitle: 'Current_Chats',
pageTemplate: 'livechatCurrentChats',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-analytics',
path: '/analytics',
sideNav: 'livechatFlex',
sideNav: 'omnichannelFlex',
i18nPageTitle: 'Analytics',
pageTemplate: 'livechatAnalytics',
}, livechatManagerRoutes, load);
@ -36,31 +29,15 @@ AccountBox.addRoute({
AccountBox.addRoute({
name: 'livechat-real-time-monitoring',
path: '/real-time-monitoring',
sideNav: 'livechatFlex',
sideNav: 'omnichannelFlex',
i18nPageTitle: 'Real_Time_Monitoring',
pageTemplate: 'livechatRealTimeMonitoring',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-managers',
path: '/managers',
sideNav: 'livechatFlex',
i18nPageTitle: 'Livechat_Managers',
pageTemplate: 'livechatManagers',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-agents',
path: '/agents',
sideNav: 'livechatFlex',
pageTemplate: 'livechatAgents',
customContainer: true,
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-departments',
path: '/departments',
sideNav: 'livechatFlex',
sideNav: 'omnichannelFlex',
i18nPageTitle: 'Departments',
pageTemplate: 'livechatDepartments',
}, livechatManagerRoutes, load);
@ -68,7 +45,7 @@ AccountBox.addRoute({
AccountBox.addRoute({
name: 'livechat-department-edit',
path: '/departments/:_id/edit',
sideNav: 'livechatFlex',
sideNav: 'omnichannelFlex',
i18nPageTitle: 'Edit_Department',
pageTemplate: 'livechatDepartmentForm',
customContainer: true,
@ -77,100 +54,12 @@ AccountBox.addRoute({
AccountBox.addRoute({
name: 'livechat-department-new',
path: '/departments/new',
sideNav: 'livechatFlex',
sideNav: 'omnichannelFlex',
i18nPageTitle: 'New_Department',
pageTemplate: 'livechatDepartmentForm',
customContainer: true,
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-triggers',
path: '/triggers',
sideNav: 'livechatFlex',
i18nPageTitle: 'Triggers',
pageTemplate: 'livechatTriggers',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-trigger-edit',
path: '/triggers/:_id/edit',
sideNav: 'livechatFlex',
i18nPageTitle: 'Edit_Trigger',
pageTemplate: 'livechatTriggersForm',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-trigger-new',
path: '/triggers/new',
sideNav: 'livechatFlex',
i18nPageTitle: 'New_Trigger',
pageTemplate: 'livechatTriggersForm',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-installation',
path: '/installation',
sideNav: 'livechatFlex',
i18nPageTitle: 'Installation',
pageTemplate: 'livechatInstallation',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-appearance',
path: '/appearance',
sideNav: 'livechatFlex',
i18nPageTitle: 'Appearance',
pageTemplate: 'livechatAppearance',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-business-hours',
path: '/businessHours',
sideNav: 'livechatFlex',
i18nPageTitle: 'Business_Hours',
pageTemplate: 'livechatMainBusinessHours',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-customfields',
path: '/customfields',
sideNav: 'livechatFlex',
i18nPageTitle: 'Custom_Fields',
pageTemplate: 'livechatCustomFields',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-customfield-edit',
path: '/customfields/:_id/edit',
sideNav: 'livechatFlex',
i18nPageTitle: 'Edit_Custom_Field',
pageTemplate: 'livechatCustomFieldForm',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-customfield-new',
path: '/customfields/new',
sideNav: 'livechatFlex',
i18nPageTitle: 'New_Custom_Field',
pageTemplate: 'livechatCustomFieldForm',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-webhooks',
path: '/webhooks',
sideNav: 'livechatFlex',
i18nPageTitle: 'Webhooks',
pageTemplate: 'livechatIntegrationWebhook',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-facebook',
path: '/facebook',
sideNav: 'livechatFlex',
i18nPageTitle: 'Facebook Messenger',
pageTemplate: 'livechatIntegrationFacebook',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-queue',
path: '/livechat-queue',

@ -16,8 +16,8 @@ Tracker.autorun((c) => {
AccountBox.addItem({
name: 'Omnichannel',
icon: 'omnichannel',
href: 'livechat-current-chats',
sideNav: 'livechatFlex',
href: '/omnichannel/current',
sideNav: 'omnichannelFlex',
condition: () => settings.get('Livechat_enabled') && hasAllPermission('view-livechat-manager'),
});

@ -2,21 +2,6 @@ import './app/analytics/livechatAnalytics';
import './app/analytics/livechatAnalyticsCustomDaterange';
import './app/analytics/livechatAnalyticsDaterange';
import './app/analytics/livechatRealTimeMonitoring';
import './app/livechatAgents';
import './app/livechatAppearance';
import './app/livechatDashboard.html';
import './app/livechatCurrentChats';
import './app/livechatCustomFields';
import './app/livechatCustomFieldForm';
import './app/livechatDepartmentForm';
import './app/livechatDepartments';
import './app/livechatInstallation';
import './app/livechatTriggers';
import './app/livechatTriggersForm';
import './app/livechatManagers';
import './app/integrations/livechatIntegrationWebhook';
import './app/integrations/livechatIntegrationFacebook';
import './app/triggers/livechatTriggerAction';
import './app/triggers/livechatTriggerCondition';
import './app/business-hours/livechatBusinessHoursForm';
import './app/business-hours/livechatMainBusinessHours';

@ -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">&nbsp;</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">&nbsp;</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>&nbsp;</td>
{{/if}}
{{else}}
<td>&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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');

@ -174,18 +174,17 @@ const toolbarButtons = (/* user */) => [{
items: AccountBox.getItems().map((item) => {
let action;
if (item.href) {
if (item.href || item.sideNav) {
action = () => {
FlowRouter.go(item.href);
popover.close();
};
}
if (item.sideNav) {
action = () => {
SideNav.setFlex(item.sideNav);
SideNav.openFlex();
popover.close();
if (item.href) {
FlowRouter.go(item.href);
popover.close();
}
if (item.sideNav) {
SideNav.setFlex(item.sideNav);
SideNav.openFlex();
popover.close();
}
};
}

@ -22,14 +22,14 @@ export default React.memo(function AccountSidebar() {
SideNav.closeFlex();
}, []);
const currentRoute = useCurrentRoute();
const currentPath = useRoutePath(...currentRoute);
const [currentRouteName, ...rest] = useCurrentRoute();
const currentPath = useRoutePath(currentRouteName, ...rest);
useEffect(() => {
if (currentRoute[0] !== 'account') {
if (currentRouteName !== 'account') {
SideNav.closeFlex();
}
}, [currentRoute, currentPath]);
}, [currentRouteName]);
// TODO: uplift this provider
return <SettingsProvider privileged>

@ -2,7 +2,7 @@ import React, { lazy, useMemo, Suspense } from 'react';
import SettingsProvider from '../providers/SettingsProvider';
import AdministrationLayout from './AdministrationLayout';
import PageSkeleton from './PageSkeleton';
import PageSkeleton from '../components/PageSkeleton';
function AdministrationRouter({ lazyRouteComponent, ...props }) {
const LazyRouteComponent = useMemo(() => lazy(lazyRouteComponent), [lazyRouteComponent]);

@ -4,7 +4,7 @@ import { usePermission } from '../../contexts/AuthorizationContext';
import { useRouteParameter, useRoute, useCurrentRoute } from '../../contexts/RouterContext';
import { useMethod } from '../../contexts/ServerContext';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import PageSkeleton from '../PageSkeleton';
import PageSkeleton from '../../components/PageSkeleton';
import AppDetailsPage from './AppDetailsPage';
import MarketplacePage from './MarketplacePage';
import AppsPage from './AppsPage';

@ -1,30 +1,8 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Meteor } from 'meteor/meteor';
import { renderRouteComponent } from '../reactAdapters';
import { createRouteGroup } from '../helpers/createRouteGroup';
const routeGroup = FlowRouter.group({
name: 'admin',
prefix: '/admin',
});
export const registerAdminRoute = (path, { lazyRouteComponent, props, action, ...options } = {}) => {
routeGroup.route(path, {
...options,
action: (params, queryParams) => {
if (action) {
action(params, queryParams);
return;
}
renderRouteComponent(() => import('./AdministrationRouter'), {
template: 'main',
region: 'center',
propsFn: () => ({ lazyRouteComponent, ...options, params, queryParams, ...props }),
});
},
});
};
export const registerAdminRoute = createRouteGroup('admin', '/admin', () => import('./AdministrationRouter'));
registerAdminRoute('/', {
triggersEnter: [(context, redirect) => {

@ -8,7 +8,6 @@ import { SettingType } from '../../../definition/ISetting';
import { useSettings } from '../../contexts/SettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoutePath, useCurrentRoute } from '../../contexts/RouterContext';
import { useAbsoluteUrl } from '../../contexts/ServerContext';
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import Sidebar from '../../components/basic/Sidebar';
import SettingsProvider from '../../providers/SettingsProvider';
@ -123,14 +122,13 @@ export default React.memo(function AdminSidebar() {
const currentRoute = useCurrentRoute();
const currentPath = useRoutePath(...currentRoute);
const absoluteUrl = useAbsoluteUrl();
const [,,, currentRouteGroupName] = currentRoute;
useEffect(() => {
const { pathname: adminPath } = new URL(absoluteUrl('admin/'));
if (!currentPath.startsWith(adminPath)) {
if (currentRouteGroupName !== 'admin') {
SideNav.closeFlex();
}
}, [absoluteUrl, currentPath]);
}, [currentRouteGroupName]);
// TODO: uplift this provider
return <SettingsProvider privileged>

@ -1,5 +1,5 @@
import React, { useMemo, useState, useCallback } from 'react';
import { Box, Field, Margins, Button } from '@rocket.chat/fuselage';
import { Box, Field, Margins, Button, Callout } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
@ -21,7 +21,7 @@ export function EditUserWithData({ uid, ...props }) {
}
if (error || roleError) {
return <Box mbs='x16' {...props}>{t('User_not_found')}</Box>;
return <Callout m='x16' type='danger'>{t('User_not_found')}</Callout>;
}
return <EditUser data={data.user} roles={roleData.roles} {...props}/>;

@ -45,6 +45,7 @@ export const GenericTable = forwardRef(function GenericTable({
setParams = () => { },
params: paramsDefault = '',
FilterComponent = () => null,
...props
}, ref) {
const t = useTranslation();
@ -70,7 +71,7 @@ export const GenericTable = forwardRef(function GenericTable({
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]);
return <>
<FilterComponent setFilter={setFilter} />
<FilterComponent setFilter={setFilter} { ...props}/>
{results && !results.length
? <Tile fontScale='p1' elevation='0' color='info' textAlign='center'>
{t('No_data_found')}

@ -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,14 +1,19 @@
import { Box, Scrollable } from '@rocket.chat/fuselage';
import { Box, Scrollable, ScrollableProps } from '@rocket.chat/fuselage';
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext, useState, FC, Dispatch, SetStateAction } from 'react';
import { useSidebar } from '../../contexts/SidebarContext';
import BurgerMenuButton from './burger/BurgerMenuButton';
import { useSession } from '../../contexts/SessionContext';
const PageContext = createContext();
type PageContextValue = [
boolean,
Dispatch<SetStateAction<boolean>>,
];
function Page(props) {
const PageContext = createContext<PageContextValue>([false, (): void => undefined]);
const Page: FC = (props) => {
const [border, setBorder] = useState(false);
return <PageContext.Provider value={[border, setBorder]}>
<Box
@ -23,15 +28,19 @@ function Page(props) {
{...props}
/>
</PageContext.Provider>;
}
};
type PageHeaderProps = {
title: string;
};
function PageHeader({ children, title, ...props }) {
const PageHeader: FC<PageHeaderProps> = ({ children = undefined, title, ...props }) => {
const [border] = useContext(PageContext);
const hasBurgerMenuButton = useMediaQuery('(max-width: 780px)');
const [isSidebarOpen, setSidebarOpen] = useSidebar();
const unreadMessagesBadge = useSession('unread');
const handleBurgerMenuButtonClick = () => {
const handleBurgerMenuButtonClick = (): void => {
setSidebarOpen((isSidebarOpen) => !isSidebarOpen);
};
@ -57,7 +66,7 @@ function PageHeader({ children, title, ...props }) {
{children}
</Box>
</Box>;
}
};
const PageContent = React.forwardRef(function PageContent(props, ref) {
return <Box
@ -71,26 +80,29 @@ const PageContent = React.forwardRef(function PageContent(props, ref) {
/>;
});
function PageScrollableContent({ onScrollContent, ...props }) {
return <Scrollable onScrollContent={onScrollContent} >
type PageScrollableContentProps = {
onScrollContent?: ScrollableProps['onScrollContent'];
};
const PageScrollableContent: FC<PageScrollableContentProps> = ({ onScrollContent, ...props }) =>
<Scrollable onScrollContent={onScrollContent} >
<Box p='x16' display='flex' flexDirection='column' flexGrow={1} {...props} />
</Scrollable>;
}
function PageScrollableContentWithShadow({ onScrollContent, ...props }) {
const PageScrollableContentWithShadow: FC<PageScrollableContentProps> = ({ onScrollContent, ...props }) => {
const [, setBorder] = useContext(PageContext);
return <PageScrollableContent
onScrollContent={({ top, ...args }) => {
onScrollContent={({ top, ...args }): void => {
setBorder(!top);
onScrollContent && onScrollContent({ top, ...args });
}}
{ ...props }
/>;
}
};
Page.Header = PageHeader;
Page.Content = PageContent;
Page.ScrollableContent = PageScrollableContent;
Page.ScrollableContentWithShadow = PageScrollableContentWithShadow;
export default Page;
export default Object.assign(Page, {
Header: PageHeader,
Content: PageContent,
ScrollableContent: PageScrollableContent,
ScrollableContentWithShadow: PageScrollableContentWithShadow,
});

@ -15,7 +15,7 @@ const Content = ({ children, ...props }) => <Scrollable {...props}>
</Box>
</Scrollable>;
const Header = ({ title, onClose, children, ...props }) => <Box is='header' display='flex' flexDirection='column' pb='x16' {...props}>
const Header = ({ title, onClose, children = undefined, ...props }) => <Box is='header' display='flex' flexDirection='column' pb='x16' {...props}>
{(title || onClose) && <Box display='flex' flexDirection='row' alignItems='center' pi='x24' justifyContent='space-between' flexGrow={1}>
{title && <Box color='neutral-800' fontSize='p1' fontWeight='p1' flexShrink={1} withTruncatedText>{title}</Box>}
{onClose && <Button square small ghost onClick={onClose}><Icon name='cross' size='x20'/></Button>}

@ -1,15 +1,25 @@
import { Box, Icon, Button, Scrollable } from '@rocket.chat/fuselage';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
const TextCopy = ({ text, wordBreak = 'break-all', ...props }) => {
const Wrapper = (text) => <Box
fontFamily='mono'
alignSelf='center'
fontScale='p1'
style={{ wordBreak: 'break-all' }}
mie='x4'
flexGrow={1}
maxHeight='x108'
>
{text}
</Box>;
const TextCopy = ({ text, wrapper = Wrapper, ...props }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const style = useMemo(() => ({ wordBreak }), [wordBreak]);
const onClick = useCallback(() => {
try {
navigator.clipboard.writeText(text);
@ -31,17 +41,7 @@ const TextCopy = ({ text, wordBreak = 'break-all', ...props }) => {
{...props}
>
<Scrollable vertical>
<Box
fontFamily='mono'
alignSelf='center'
fontScale='p1'
style={style}
mie='x4'
flexGrow={1}
maxHeight='x108'
>
{text}
</Box>
{ wrapper(text) }
</Scrollable>
<Button ghost square small flexShrink={0} onClick={onClick} title={t('Copy')}>
<Icon name='copy' size='x20' />

@ -17,6 +17,8 @@ const wordBreak = css`
`;
const Info = ({ className, ...props }) => <UserCard.Info className={[className, wordBreak]} flexShrink={0} {...props}/>;
const Avatar = ({ username, ...props }) => <UserAvatar title={username} username={username} {...props}/>;
const Username = ({ username, status, ...props }) => <UserCard.Username name={username} status={status} {...props}/>;
export const UserInfo = React.memo(function UserInfo({
username,
@ -43,7 +45,7 @@ export const UserInfo = React.memo(function UserInfo({
return <VerticalBar.ScrollableContent p='x24' {...props}>
<UserAvatar margin='auto' size={'x332'} title={username} username={username}/>
<Avatar margin='auto' size={'x332'} username={username}/>
{actions}
@ -121,5 +123,9 @@ export const Action = ({ icon, label, ...props }) => (
);
UserInfo.Action = Action;
UserInfo.Avatar = Avatar;
UserInfo.Info = Info;
UserInfo.Label = Label;
UserInfo.Username = Username;
export default UserInfo;

@ -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],
),
);
};

@ -6,12 +6,12 @@ interface IServerStream {
}
type ServerContextValue = {
info: object;
info: {};
absoluteUrl: (path: string) => string;
callMethod: (methodName: string, ...args: any[]) => Promise<any>;
callEndpoint: (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string, ...args: any[]) => Promise<any>;
uploadToEndpoint: (endpoint: string) => Promise<void>;
getStream: (streamName: string, options?: object) => IServerStream;
getStream: (streamName: string, options?: {}) => IServerStream;
};
export const ServerContext = createContext<ServerContextValue>({
@ -26,26 +26,26 @@ export const ServerContext = createContext<ServerContextValue>({
}),
});
export const useServerInformation = (): object => useContext(ServerContext).info;
export const useServerInformation = (): {} => useContext(ServerContext).info;
export const useAbsoluteUrl = (): ((path: string) => string) => useContext(ServerContext).absoluteUrl;
export const useMethod = (methodName: string): (...args: any[]) => Promise<any> => {
const { callMethod } = useContext(ServerContext);
return useCallback((...args) => callMethod(methodName, ...args), [callMethod, methodName]);
return useCallback((...args: any[]) => callMethod(methodName, ...args), [callMethod, methodName]);
};
export const useEndpoint = (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string): (...args: any[]) => Promise<any> => {
const { callEndpoint } = useContext(ServerContext);
return useCallback((...args) => callEndpoint(httpMethod, endpoint, ...args), [callEndpoint, httpMethod, endpoint]);
return useCallback((...args: any[]) => callEndpoint(httpMethod, endpoint, ...args), [callEndpoint, httpMethod, endpoint]);
};
export const useUpload = (endpoint: string): () => Promise<void> => {
const { uploadToEndpoint } = useContext(ServerContext);
return useCallback((...args) => uploadToEndpoint(endpoint, ...args), [endpoint, uploadToEndpoint]);
return useCallback(() => uploadToEndpoint(endpoint), [endpoint, uploadToEndpoint]);
};
export const useStream = (streamName: string, options?: object): IServerStream => {
export const useStream = (streamName: string, options?: {}): IServerStream => {
const { getStream } = useContext(ServerContext);
return useMemo(() => getStream(streamName, options), [getStream, streamName, options]);
};
@ -57,7 +57,7 @@ export enum AsyncState {
}
export const useMethodData = <T>(methodName: string, args: any[] = []): [T | null, AsyncState, () => void] => {
const getData = useMethod(methodName);
const getData: (...args: unknown[]) => Promise<T> = useMethod(methodName);
const [[data, state], updateState] = useState<[T | null, AsyncState]>([null, AsyncState.LOADING]);
const isMountedRef = useRef(true);
@ -85,7 +85,7 @@ export const useMethodData = <T>(methodName: string, args: any[] = []): [T | nul
updateState(([data]) => [data, AsyncState.ERROR]);
console.error(error);
});
}, [getData, ...args]);
}, [getData, args]);
useEffect(() => {
fetchData();

@ -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;
};

@ -3,22 +3,34 @@ import { useEffect, useState } from 'react';
import { useEndpoint } from '../contexts/ServerContext';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
export const ENDPOINT_STATES = {
LOADING: 'LOADING',
DONE: 'DONE',
ERROR: 'ERROR',
export enum ENDPOINT_STATES {
LOADING = 'LOADING',
DONE = 'DONE',
ERROR = 'ERROR',
}
type EndpointState<T> = {
data: T | null;
state: ENDPOINT_STATES;
error?: Error;
delaying?: boolean;
reload?: () => Promise<void>;
};
const defaultState = { data: null, state: ENDPOINT_STATES.LOADING };
const defaultParams = {};
const defaultOptions = {};
type EndpointOptions = {
delayTimeout?: number;
};
const defaultState: EndpointState<any> = { data: null, state: ENDPOINT_STATES.LOADING };
const defaultParams: Record<string, unknown> = {};
const defaultOptions: EndpointOptions = {};
export const useEndpointDataExperimental = (
endpoint,
export const useEndpointDataExperimental = <T>(
endpoint: string,
params = defaultParams,
{ delayTimeout = 1000 } = defaultOptions,
) => {
const [data, setData] = useState(defaultState);
): EndpointState<T> => {
const [data, setData] = useState<EndpointState<T>>(defaultState);
const getData = useEndpoint('GET', endpoint);
const dispatchToastMessage = useToastMessageDispatch();
@ -26,13 +38,13 @@ export const useEndpointDataExperimental = (
useEffect(() => {
let mounted = true;
const fetchData = async () => {
const fetchData = async (): Promise<void> => {
const timer = setTimeout(() => {
if (!mounted) {
return;
}
setData({ delaying: true, state: ENDPOINT_STATES.LOADING, reload: fetchData });
setData({ data: null, delaying: true, state: ENDPOINT_STATES.LOADING, reload: fetchData });
}, delayTimeout);
try {
@ -54,7 +66,7 @@ export const useEndpointDataExperimental = (
return;
}
setData({ error, state: ENDPOINT_STATES.ERROR, reload: fetchData });
setData({ data: null, error, state: ENDPOINT_STATES.ERROR, reload: fetchData });
dispatchToastMessage({ type: 'error', message: error });
} finally {
clearTimeout(timer);
@ -63,7 +75,7 @@ export const useEndpointDataExperimental = (
fetchData();
return () => {
return (): void => {
mounted = false;
};
}, [delayTimeout, dispatchToastMessage, getData, params]);

@ -152,18 +152,19 @@ export const useForm = (
dispatch(formReset());
}, []);
const handlers = useMemo(() => state.fields.reduce((handlers, { name, initialValue }) => ({
...handlers,
[`handle${ capitalize(name) }`]: (eventOrValue: ChangeEvent | unknown): void => {
const newValue = getValue(eventOrValue);
dispatch(valueChanged(name, newValue));
onChange({
initialValue,
value: newValue,
key: name,
});
},
}), {}), [onChange, state.fields]);
const handlers = useMemo<Record<string, (eventOrValue: ChangeEvent | unknown) => void>>(
() => state.fields.reduce((handlers, { name, initialValue }) => ({
...handlers,
[`handle${ capitalize(name) }`]: (eventOrValue: ChangeEvent | unknown): void => {
const newValue = getValue(eventOrValue);
dispatch(valueChanged(name, newValue));
onChange({
initialValue,
value: newValue,
key: name,
});
},
}), {}), [onChange, state.fields]);
return {
handlers,

@ -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…
Cancel
Save