diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less
index 24bb09c11..3cb9da2d2 100644
--- a/customize.dist/src/less2/include/toolbar.less
+++ b/customize.dist/src/less2/include/toolbar.less
@@ -400,7 +400,7 @@
button {
.toolbar_button;
- &.cp-notifications-bell {
+ &.cp-notifications-bell, &.cp-maintenance-wrench {
color: @cryptpad_text_col;
}
}
@@ -506,7 +506,7 @@
}
.cp-toolbar-user {
height: @toolbar_line-height;
- .cp-toolbar-notifications {
+ .cp-toolbar-notifications, .cp-toolbar-maintenance {
height: @toolbar_line-height;
width: @toolbar_line-height;
margin-left: 0;
@@ -709,7 +709,7 @@
height: 43px;
}
}
- .cp-toolbar-link, .cp-toolbar-notifications {
+ .cp-toolbar-link, .cp-toolbar-notifications, .cp-toolbar-maintenance {
line-height: @toolbar_top-height;
width: @toolbar_top-height;
height: @toolbar_top-height;
@@ -717,7 +717,7 @@
box-sizing: border-box;
display: inline-block;
}
- .cp-toolbar-notifications {
+ .cp-toolbar-notifications, .cp-toolbar-maintenance {
text-align: center;
font-size: 32px;
margin-left: 10px;
diff --git a/lib/decrees.js b/lib/decrees.js
index 7d2a8e037..0b5e8572e 100644
--- a/lib/decrees.js
+++ b/lib/decrees.js
@@ -128,10 +128,13 @@ commands.SET_ACCOUNT_RETENTION_TIME = makeIntegerSetter('accountRetentionTime');
var args_isString = function (args) {
return Array.isArray(args) && typeof(args[0]) === "string";
};
+var args_isMaintenance = function (args) {
+ return Array.isArray(args) && args[0] && args[0].end && args[0].start;
+};
var makeBroadcastSetter = function (attr) {
return function (Env, args) {
- if (!args_isString(args)) {
+ if (!args_isString(args) && !args_isMaintenance(args)) {
throw new Error('INVALID_ARGS');
}
var str = args[0];
@@ -148,6 +151,10 @@ commands.SET_LAST_BROADCAST_HASH = makeBroadcastSetter('lastBroadcastHash');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SURVEY_URL', [url]]], console.log)
commands.SET_SURVEY_URL = makeBroadcastSetter('surveyURL');
+// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [{start: +Date, end: +Date}]]], console.log)
+// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAINTENANCE', [""]]], console.log)
+commands.SET_MAINTENANCE = makeBroadcastSetter('maintenance');
+
var Quota = require("./commands/quota");
var Keys = require("./keys");
var Util = require("./common-util");
diff --git a/lib/env.js b/lib/env.js
index 1a26fa0f9..b879d102f 100644
--- a/lib/env.js
+++ b/lib/env.js
@@ -70,6 +70,7 @@ module.exports.create = function (config) {
// /api/broadcast
lastBroadcastHash: '',
surveyURL: undefined,
+ maintenance: undefined,
netfluxUsers: {},
diff --git a/server.js b/server.js
index 74f72bdb2..7f4c1fcbf 100644
--- a/server.js
+++ b/server.js
@@ -279,11 +279,16 @@ var serveBroadcast = (function () {
};
var template = function (host) {
+ var maintenance = Env.maintenance;
+ if (maintenance && maintenance.end && maintenance.end < (+new Date())) {
+ maintenance = undefined;
+ }
return [
'define(function(){',
'return ' + JSON.stringify({
lastBroadcastHash: Env.lastBroadcastHash,
- surveyURL: Env.surveyURL
+ surveyURL: Env.surveyURL,
+ maintenance: maintenance
}, null, '\t'),
'});'
].join(';\n')
diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less
index ba35e8f15..11ef0c634 100644
--- a/www/admin/app-admin.less
+++ b/www/admin/app-admin.less
@@ -200,10 +200,20 @@
}
.cp-admin-broadcast-form {
- margin-top: 30px;
input.flatpickr-input {
width: 307.875px !important; // same width as flatpickr calendar
}
+ .cp-broadcast-active {
+ display: flex;
+ flex-flow: column;
+ align-items: start;
+ padding: 10px;
+ background-color: @cp_sidebar-left-bg;
+ color: @cp_sidebar-left-fg;
+ p {
+ margin: 0;
+ }
+ }
.cp-broadcast-form-submit {
margin-top: 30px;
button {
diff --git a/www/admin/inner.js b/www/admin/inner.js
index aaeeee924..73f836a9f 100644
--- a/www/admin/inner.js
+++ b/www/admin/inner.js
@@ -73,8 +73,9 @@ define([
'cp-admin-support-list',
'cp-admin-support-init'
],
- 'broadcast': [ // Msg.admin_cat_support
- 'cp-admin-broadcast-delete',
+ 'broadcast': [ // Msg.admin_cat_broadcast
+ 'cp-admin-maintenance',
+ 'cp-admin-survey',
'cp-admin-broadcast',
],
'performance': [ // Msg.admin_cat_performance
@@ -941,546 +942,532 @@ define([
};
Messages.admin_cat_broadcast = "Broadcast"; // XXX
- // Messages.admin_broadcastHint // XXX
- // Messages.admin_broadcastTitle // XXX
-
- //Messages.admin_broadcastDeleteHint // XXX
- //Messages.admin_broadcastDeleteTitle // XXX
-
- Messages.broadcast_new = "New message";
- Messages.broadcast_maintenance = 'maintenance';// XXX
- Messages.broadcast_survey = 'survey'; // XXX
- Messages.broadcast_version = 'version'; // XXX
- Messages.broadcast_custom = 'custom'; // XXX
- Messages.broadcast_delete = 'delete'; // XXX
- Messages.broadcast_newVersionReload = 'Force a worker reload on all clients'; // XXX
- Messages.broadcast_surveyURL = 'Survey URL';
- Messages.broadcast_translations = 'Translations';
- Messages.broadcast_defaultLanguage = 'Fallback to this language (optional)';
+
+ Messages.admin_maintenanceTitle = "Maintenance"; // XXX
+ Messages.admin_maintenanceHint = "Plan, remove or update a maintenance. You can only have one active maintenance at a time."; // XXX
+ Messages.admin_maintenanceButton = "Plan maintenance"; // XXX
+ Messages.admin_maintenanceCancel = "Cancel planned maintenance"; // XXX
Messages.broadcast_start = 'Start time';
Messages.broadcast_end = 'End time';
+
+
+ Messages.admin_surveyTitle = "Survey"; // XXX
+ Messages.admin_surveyHint = "Add, update or remove the active survey accessible from the user menu"; // XXX
+ Messages.admin_surveyButton = "Apply survey"; // XXX
+ Messages.admin_surveyCancel = "Cancel active survey"; // XXX
+ Messages.admin_surveyActive = "View the active survey"; // XXX
+ Messages.broadcast_surveyURL = 'Survey URL';
+
+ Messages.admin_broadcastTitle = "Broadcast a message"; // XXX
+ Messages.admin_broadcastHint = "Send a message to all the existing and future users as a notification"; // XXX
+ Messages.admin_broadcastButton = "Send"; // XXX
+ Messages.admin_broadcastActive = "Active message"; // XXX
+ Messages.admin_broadcastCancel = "Delete active message"; // XXX
+ Messages.broadcast_translations = 'Translations';
+ Messages.broadcast_defaultLanguage = 'Fallback to this language';
Messages.broadcast_preview = "Preview in a fake notification";
- Messages.broadcast_deleteBtn = "Delete for all";
- Messages.broadcast_clear = "Clear all for everybody";
- Messages.expired = "Expired";
- Messages.broadcast_empty = "No active message";
- Messages.broadcast_noFallback = "Don't fallback to a default language";
-
- var onRefreshBroadcast = Util.mkEvent();
- var broadcast = {
- getData: function () { return false; },
- reset: function () {},
- handlers: {}
- };
- broadcast.handlers.custom = function ($form, button, send, preview, onPreview) {
- // Custom message
- var container = h('div.cp-broadcast-container');
- var $container = $(container);
- var languages = Messages._languages;
- var keys = Object.keys(languages).sort();
-
- // Always keep the textarea ordered by language code
- var reorder = function () {
- $container.find('.cp-broadcast-lang').each(function (i, el) {
- var $el = $(el);
- var l = $el.attr('data-lang');
- $el.css('order', keys.indexOf(l));
+
+ var getApi = function (cb) {
+ return function () {
+ require(['/api/broadcast?'+ (+new Date())], function (Broadcast) {
+ cb(Broadcast);
});
};
+ };
- var noFallbackBtn = h('button.btn.btn-secondary.cp-broadcast-preview',
- Messages.broadcast_noFallback);
- var $noFallbackBtn = $(noFallbackBtn);
- var checkFallbackBtn = function () {
- var hasDefault = $container.find('.cp-broadcast-lang .cp-checkmark input:checked').length;
- if (hasDefault) {
- $noFallbackBtn.css('visibility', '');
- } else {
- $noFallbackBtn.css('visibility', 'hidden');
- }
- };
+ // Update the lastBroadcastHash in /api/broadcast if we can do it.
+ // To do so, find the last "BROADCAST_CUSTOM" in the current history and use the previous
+ // message's hash.
+ // If the last BROADCAST_CUSTOM has been deleted by an admin, we can use the most recent
+ // message's hash.
+ var checkLastBroadcastHash = function () {
+ var deleted = [];
- // Remove a textarea
- var removeLang = function (l) {
- $container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove();
- checkFallbackBtn();
- };
+ require(['/api/broadcast?'+ (+new Date())], function (BCast) {
+ var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
+ common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
+ if (e) { return void console.error(e); }
- // Add a textarea
- var addLang = function (l) {
- if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; }
- var preview = h('button.btn.btn-secondary', Messages.broadcast_preview);
- $(preview).click(function () {
- onPreview(l);
- });
- var bcastDefault = Messages.broadcast_defaultLanguage;
- var first = !$container.find('.cp-broadcast-lang').length;
- var radio = UI.createRadio('broadcastDefault', null, bcastDefault, first, {
- 'data-lang': l,
- label: {class: 'noTitle'}
- });
- $(radio).find('input').on('change', function () {
- checkFallbackBtn();
- });
- $container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [
- h('h4', languages[l]),
- h('label', Messages.kanban_body),
- h('textarea'),
- radio,
- preview
- ]));
- checkFallbackBtn();
- reorder();
- };
+ // No history, nothing to change
+ if (!Array.isArray(msgs)) { return; }
+ if (!msgs.length) { return; }
- // Checkboxes to select translations
- var boxes = keys.map(function (l) {
- var $cbox = $(UI.createCheckbox('cp-broadcast-custom-lang-'+l,
- languages[l], false, { label: { class: 'noTitle' } }));
- var $check = $cbox.find('input').on('change', function () {
- var c = $check.is(':checked');
- if (c) { return void addLang(l); }
- removeLang(l);
- });
- if (l === 'en') {
- setTimeout(function () {
- $check.click();
- });
- }
- return $cbox[0];
- });
+ var lastHash;
+ var next = false;
- // Extract form data
- broadcast.getData = function () {
- var map = {};
- var defaultLanguage;
- var error = false;
- $container.find('.cp-broadcast-lang').each(function (i, el) {
- var $el = $(el);
- var l = $el.attr('data-lang');
- if (!l) { error = true; return; }
- var text = $el.find('textarea').val();
- if (!text.trim()) { error = true; return; }
- if ($el.find('.cp-checkmark input').is(':checked')) {
- defaultLanguage = l;
- }
- map[l] = text;
- });
- if (!Object.keys(map).length) {
- console.error('You must select at least one language');
- return false;
- }
- if (error) {
- console.error('One of the selected languages has no data');
- return false;
- }
- return {
- defaultLanguage: defaultLanguage,
- content: map
- };
- };
- // Clear all the textarea when sent
- broadcast.reset = function () {
- $container.find('.cp-broadcast-lang textarea').each(function (i, el) {
- $(el).val('');
- });
- };
+ // Start from the most recent messages until you find a CUSTOM message and
+ // check if it has been deleted
+ msgs.reverse().some(function (data) {
+ var c = data.content;
- // "Don't fallback to a default language" button
- $noFallbackBtn.click(function () {
- $container.find('.cp-checkmark input').prop('checked', false);
- $noFallbackBtn.css('visibility', 'hidden');
- });
+ // This is the hash we want to keep
+ if (next) {
+ if (!c || !c.hash) { return; }
+ lastHash = c.hash;
+ next = false;
+ return true;
+ }
- // Make the form
- $form.append([
- h('label', Messages.broadcast_translations),
- h('div.cp-broadcast-languages', boxes),
- container,
- h('div.cp-broadcast-form-submit', [
- noFallbackBtn,
- h('br'),
- button
- ])
- ]);
- };
- broadcast.handlers.maintenance = function ($form, button, send, preview) {
- // Maintenance message
-
- // Start and end date pickers
- var start = h('input');
- var end = h('input');
- var $start = $(start);
- var $end = $(end);
- var endPickr = Flatpickr(end, {
- enableTime: true,
- minDate: new Date()
- });
- Flatpickr(start, {
- enableTime: true,
- minDate: new Date(),
- onChange: function () {
- endPickr.set('minDate', new Date($start.val()));
- }
- });
+ // initialize with the most recent hash
+ if (!lastHash && c && c.hash) { lastHash = c.hash; }
- // Extract form data
- broadcast.getData = function () {
- var start = +new Date($start.val());
- var end = +new Date($end.val());
- if (isNaN(start) || isNaN(end)) {
- console.error('Invalid dates');
- return false;
- }
- return {
- start: start,
- end: end
- };
- };
+ var msg = c && c.msg;
+ if (!msg) { return; }
- // Clear when sent
- broadcast.reset = function () {
- $start.val('');
- $end.val('');
- };
- $form.append([
- h('label', Messages.broadcast_start),
- start,
- h('label', Messages.broadcast_end),
- end,
- h('br'),
- h('div.cp-broadcast-form-submit', [
- button,
- preview
- ])
- ]);
+ // Remember all deleted messages
+ if (msg.type === "BROADCAST_DELETE") {
+ deleted.push(Util.find(msg, ['content', 'uid']));
+ }
- };
- broadcast.handlers.version = function ($form, button, send, preview) {
- // New version available message
-
- // This checkbox can be used to trigger a fake "reconnect" event on the clients
- // so that they can check api/config and reload the worker in case of a new version
- var $cbox = $(UI.createCheckbox('cp-admin-version-reload',
- Messages.broadcast_newVersionReload,
- false, { label: { class: 'noTitle' } }));
- var $checkbox = $cbox.find('input');
-
- // Extract the data and make the form
- broadcast.getData = function () {
- return {
- reload: $checkbox.is(':checked')
- };
- };
- broadcast.reset = function () {
- $checkbox[0].checked = false;
- };
- $form.append([
- $cbox[0],
- h('br'),
- h('div.cp-broadcast-form-submit', [
- button,
- preview
- ])
- ]);
- };
- broadcast.handlers.survey = function ($form, button, send, preview) {
- // New survey message
- // TODO send different URLs for other languages?
- var label = h('label', Messages.broadcast_surveyURL);
- var input = h('input');
- var $input = $(input);
- broadcast.getData = function () {
- var url = $input.val();
- if (!Util.isValidURL(url)) {
- console.error('Invalid URL');
- return false;
- }
- return {
- url: url
- };
- };
- broadcast.reset = function () {
- $input.val('');
- };
- $(button).off('click').click(function () {
- var data = broadcast.getData();
- var val = $input.val() || '';
+ // Only check custom messages
+ if (msg.type !== "BROADCAST_CUSTOM") { return; }
- // Invalid url: abort
- // NOTE: empty strings are allowed to remove a surveyURL from the decrees
- // XXX usability...
- if (!data && val) { return void UI.warn(Messages.error); }
+ // If the most recent CUSTOM message has been deleted, it means we don't
+ // need to keep any message and we can continue with lastHash as the most
+ // recent broadcast message.
+ if (deleted.indexOf(msg.uid) !== -1) { return true; }
- var url = data ? data.url : val;
+ // We just found the oldest message we want to keep, move one iteration
+ // further into the loop to get the next message's hash.
+ // If this is the end of the loop, don't bump lastBroadcastHash at all.
+ next = true;
+ });
- sFrameChan.query('Q_ADMIN_RPC', {
- cmd: 'ADMIN_DECREE',
- data: ['SET_SURVEY_URL', [url]]
- }, function (e) {
- if (e) {
- UI.warn(Messages.error); console.error(e);
- return;
- }
- if (!url) { return; }
- send();
- });
+ // If we don't have to bump our lastBroadcastHash, abort
+ if (next) { return; }
+ // Otherwise, bump to lastHash
+ console.warn('Updating last broadcast hash to', lastHash);
+ sFrameChan.query('Q_ADMIN_RPC', {
+ cmd: 'ADMIN_DECREE',
+ data: ['SET_LAST_BROADCAST_HASH', [lastHash]]
+ }, function (e) {
+ if (e) {
+ console.error(e);
+ return;
+ }
+ console.log('lastBroadcastHash updated');
+ });
+ });
});
- $form.append([
- label,
- input,
- h('br'),
- h('div.cp-broadcast-form-submit', [
- button,
- preview
- ])
- ]);
+
};
- broadcast.handlers.delete = function ($form, button, send) {
- require(['/api/broadcast?'+ (+new Date())], function (BCast) {
- // Always display the messages from the instance "lastBroadcastHash"
- var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
- common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
- var table = h('table.cp-broadcast-delete');
- var $table = $(table);
+ create['broadcast'] = function () {
+ var key = 'broadcast';
+ var $div = makeBlock(key);
+
+ var form = h('div.cp-admin-broadcast-form');
+ var $form = $(form).appendTo($div);
- // Empty history
- if (!msgs.length) {
- $table.append(h('tr.empty', h('td', Messages.broadcast_empty)));
- }
+ var refresh = getApi(function (Broadcast) {
+ var button = h('button.btn.btn-primary', Messages.admin_broadcastButton);
+ var $button = $(button);
+ var removeButton = h('button.btn.btn-danger', Messages.admin_broadcastCancel);
+ var active = h('div.cp-broadcast-active', h('p', Messages.admin_broadcastActive));
+ var $active = $(active);
+ var activeUid;
+ var deleted = [];
+
+ // Render active message (if there is one)
+ require(['/api/broadcast?'+ (+new Date())], function (BCast) {
+ var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
+ common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
+ if (e) { return void console.error(e); }
+ if (!Array.isArray(msgs)) { return; }
+ if (!msgs.length) {
+ $active.hide();
+ }
+ msgs.reverse().some(function (data) {
+ var c = data.content;
+ var msg = c && c.msg;
+ if (!msg) { return; }
+ if (msg.type === "BROADCAST_DELETE") {
+ deleted.push(Util.find(msg, ['content', 'uid']));
+ }
+ if (msg.type !== "BROADCAST_CUSTOM") { return; }
+ if (deleted.indexOf(msg.uid) !== -1) { return true; }
+
+ // We found an active custom message, show it
+ var el = common.mailbox.createElement(data);
+ var table = h('table.cp-broadcast-delete');
+ var $table = $(table);
+ var uid = Util.find(data, ['content', 'msg', 'uid']);
+ var time = Util.find(data, ['content', 'msg', 'content', 'time']);
+ var tr = h('tr', { 'data-uid': uid }, [
+ h('td', 'ID: '+uid),
+ h('td', new Date(time || 0).toLocaleString()),
+ h('td', el),
+ h('td.delete', removeButton),
+ ]);
+ $table.append(tr);
+ $active.append(table);
+ activeUid = uid;
- // Build the table
- msgs.forEach(function (data) {
- var el = common.mailbox.createElement(data);
- var t = Util.find(data, ['content', 'msg', 'type']);
+ return true;
+ });
+ if (!activeUid) { $active.hide(); }
+ });
+ });
- // A "DELETE" message is here to disable a previous line
- if (t === 'BROADCAST_DELETE') {
- var _uid = Util.find(data, ['content', 'msg', 'content', 'uid']);
- var $button = $table.find('[data-uid="'+_uid+'"] td.delete button');
- $button.prop('disabled', 'disabled').text(Messages.deleted);
- return;
+ // Custom message
+ var container = h('div.cp-broadcast-container');
+ var $container = $(container);
+ var languages = Messages._languages;
+ var keys = Object.keys(languages).sort();
+
+ // Always keep the textarea ordered by language code
+ var reorder = function () {
+ $container.find('.cp-broadcast-lang').each(function (i, el) {
+ var $el = $(el);
+ var l = $el.attr('data-lang');
+ $el.css('order', keys.indexOf(l));
+ });
+ };
+
+ // Remove a textarea
+ var removeLang = function (l) {
+ $container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove();
+
+ var hasDefault = $container.find('.cp-broadcast-lang .cp-checkmark input:checked').length;
+ if (!hasDefault) {
+ $container.find('.cp-broadcast-lang').first().find('.cp-checkmark input').prop('checked', 'checked');
}
+ };
- // Make the line
- var uid = Util.find(data, ['content', 'msg', 'uid']);
- var time = Util.find(data, ['content', 'msg', 'content', 'time']);
- var deleteBtn = h('button.btn.btn-danger', Messages.broadcast_deleteBtn);
- var tr = h('tr', { 'data-uid': uid }, [
- h('td', 'ID: '+uid),
- h('td', new Date(time || 0).toLocaleString()),
- h('td', el),
- h('td.delete', deleteBtn),
- ]);
+ var getData = function () { return false; };
+ var onPreview = function (l) {
+ var data = getData();
+ if (data === false) { return void UI.warn(Messages.error); }
- // Auto-expire maintenance and survey messages
- if (t === 'BROADCAST_MAINTENANCE') {
- var end = Util.find(data, ['content', 'msg', 'content', 'end']);
- if (end < +new Date()) {
- $(deleteBtn).prop('disabled', 'disabled').text(Messages.expired);
+ var msg = {
+ uid: Util.uid(),
+ type: 'BROADCAST_CUSTOM',
+ content: data
+ };
+ common.mailbox.onMessage({
+ lang: l,
+ type: 'broadcast',
+ content: {
+ msg: msg,
+ hash: 'LOCAL|' + JSON.stringify(msg).slice(0,58)
}
+ }, function () {
+ UI.log(Messages.saved);
+ });
+ };
+
+ // Add a textarea
+ var addLang = function (l) {
+ if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; }
+ var preview = h('button.btn.btn-secondary', Messages.broadcast_preview);
+ $(preview).click(function () {
+ onPreview(l);
+ });
+ var bcastDefault = Messages.broadcast_defaultLanguage;
+ var first = !$container.find('.cp-broadcast-lang').length;
+ var radio = UI.createRadio('broadcastDefault', null, bcastDefault, first, {
+ 'data-lang': l,
+ label: {class: 'noTitle'}
+ });
+ $container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [
+ h('h4', languages[l]),
+ h('label', Messages.kanban_body),
+ h('textarea'),
+ radio,
+ preview
+ ]));
+ reorder();
+ };
+
+ // Checkboxes to select translations
+ var boxes = keys.map(function (l) {
+ var $cbox = $(UI.createCheckbox('cp-broadcast-custom-lang-'+l,
+ languages[l], false, { label: { class: 'noTitle' } }));
+ var $check = $cbox.find('input').on('change', function () {
+ var c = $check.is(':checked');
+ if (c) { return void addLang(l); }
+ removeLang(l);
+ });
+ if (l === 'en') {
+ setTimeout(function () {
+ $check.click();
+ });
}
- if (t === 'BROADCAST_VERSION') {
- $(deleteBtn).prop('disabled', 'disabled').text(Messages.expired);
+ return $cbox[0];
+ });
+
+ // Extract form data
+ getData = function () {
+ var map = {};
+ var defaultLanguage;
+ var error = false;
+ $container.find('.cp-broadcast-lang').each(function (i, el) {
+ var $el = $(el);
+ var l = $el.attr('data-lang');
+ if (!l) { error = true; return; }
+ var text = $el.find('textarea').val();
+ if (!text.trim()) { error = true; return; }
+ if ($el.find('.cp-checkmark input').is(':checked')) {
+ defaultLanguage = l;
+ }
+ map[l] = text;
+ });
+ if (!Object.keys(map).length) {
+ console.error('You must select at least one language');
+ return false;
+ }
+ if (error) {
+ console.error('One of the selected languages has no data');
+ return false;
}
+ return {
+ defaultLanguage: defaultLanguage,
+ content: map
+ };
+ };
- // "Delete this message" button
- UI.confirmButton(deleteBtn, {
- classes: 'btn-danger',
- multiple: true
- }, function () {
- broadcast.getData = function () {
- if (!uid) { return false; }
- return { uid: uid };
- };
- broadcast.reset = function () {
- $(deleteBtn).prop('disabled', 'disabled').text(Messages.deleted);
- };
- send();
+ var send = function (data) {
+ $button.prop('disabled', 'disabled');
+ data.time = +new Date();
+ common.mailbox.sendTo('BROADCAST_CUSTOM', data, {}, function (err, data) {
+ if (err) {
+ $button.prop('disabled', '');
+ console.error(err);
+ return UI.warn(Messages.error);
+ }
+ UI.log(Messages.saved);
+ refresh();
+
+ checkLastBroadcastHash();
});
+ };
- $table.append(tr);
+ $button.click(function () {
+ var data = getData();
+ if (data === false) { return void UI.warn(Messages.error); }
+ send(data);
});
- // Clear all button: remove all the messages and bump lastBroadcastHash
- var clearAll = h('button.btn.btn-danger', Messages.broadcast_clear);
- UI.confirmButton(clearAll, {
+ UI.confirmButton(removeButton, {
classes: 'btn-danger',
- multiple: true
}, function () {
- broadcast.getData = function () {
- return { all: true };
- };
- broadcast.reset = function () {};
-
- // Send a message to all users telling them to wipe the broadcast mailbox
- // and on success, send an admin decree to update /api/broadcast
- send(function (err, obj) {
- if (err) { return; }
- if (!obj || !obj.hash) { return; }
- /*
- sFrameChan.query('Q_ADMIN_RPC', {
- cmd: 'ADMIN_DECREE',
- data: ['SET_LAST_BROADCAST_HASH', [obj.hash]]
- }, function (e) {
- if (e) {
- UI.warn(Messages.error); console.error(e);
- return;
- }
- // On success, reload the "delete" tab
- onRefreshBroadcast.fire();
- });
- */
+ if (!activeUid) { return; }
+ common.mailbox.sendTo('BROADCAST_DELETE', {
+ uid: activeUid
+ }, {}, function (err, data) {
+ if (err) { return UI.warn(Messages.error); }
+ UI.log(Messages.saved);
+ refresh();
+ checkLastBroadcastHash();
});
});
- $form.append([
- table,
- msgs.length ? clearAll : undefined
+ // Make the form
+ $form.empty().append([
+ active,
+ h('label', Messages.broadcast_translations),
+ h('div.cp-broadcast-languages', boxes),
+ container,
+ h('div.cp-broadcast-form-submit', [
+ h('br'),
+ button
+ ])
]);
});
- });
+ refresh();
+ return $div;
};
- var getBroadcastForm = function ($form, key) {
- $form.empty();
+ create['maintenance'] = function () {
+ var key = 'maintenance';
+ var $div = makeBlock(key);
- var button = h('button.btn.btn-primary', Messages.support_formButton);
- var $button = $(button);
+ var form = h('div.cp-admin-broadcast-form');
+ var $form = $(form).appendTo($div);
- var send = function (_cb) {
- var cb = Util.once(_cb || function () {});
- var data = broadcast.getData();
- if (data === false) {
- cb('NODATA');
- return void UI.warn(Messages.error);
+ var refresh = getApi(function (Broadcast) {
+ var button = h('button.btn.btn-primary', Messages.admin_maintenanceButton);
+ var $button = $(button);
+ var removeButton = h('button.btn.btn-danger', Messages.admin_maintenanceCancel);
+ var active;
+
+ if (Broadcast && Broadcast.maintenance) {
+ var m = Broadcast.maintenance;
+ if (m.start && m.end && m.end >= (+new Date())) {
+ active = h('div.cp-broadcast-active', [
+ UI.setHTML(h('p'), Messages._getKey('broadcast_maintenance', [
+ new Date(m.start).toLocaleString(),
+ new Date(m.end).toLocaleString(),
+ ])),
+ removeButton
+ ]);
+ }
}
- $button.prop('disabled', 'disabled');
- data.time = +new Date();
- common.mailbox.sendTo('BROADCAST_'+key.toUpperCase(), data, {}, function (err, data) {
- $button.prop('disabled', '');
- cb(err, data);
- if (err) {
- console.error(err);
- return UI.warn(Messages.error);
+
+ // Start and end date pickers
+ var start = h('input');
+ var end = h('input');
+ var $start = $(start);
+ var $end = $(end);
+ var endPickr = Flatpickr(end, {
+ enableTime: true,
+ minDate: new Date()
+ });
+ Flatpickr(start, {
+ enableTime: true,
+ minDate: new Date(),
+ onChange: function () {
+ endPickr.set('minDate', new Date($start.val()));
}
+ });
+ // Extract form data
+ var getData = function () {
+ var start = +new Date($start.val());
+ var end = +new Date($end.val());
+ if (isNaN(start) || isNaN(end)) {
+ console.error('Invalid dates');
+ return false;
+ }
+ return {
+ start: start,
+ end: end
+ };
+ };
+
+ var send = function (data) {
+ $button.prop('disabled', 'disabled');
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
- data: ['SET_LAST_BROADCAST_HASH', [data.hash]]
+ data: ['SET_MAINTENANCE', [data]]
}, function (e) {
if (e) {
UI.warn(Messages.error); console.error(e);
+ $button.prop('disabled', '');
return;
}
- // On success, reload the "delete" tab
- onRefreshBroadcast.fire();
+ // Maintenance applied, send notification
+ common.mailbox.sendTo('BROADCAST_MAINTENANCE', {}, {}, function (err, data) {
+ refresh();
+ checkLastBroadcastHash();
+ });
});
- // Only print success if there is no callback
- if (!_cb) {
- UI.log(Messages.saved);
- // Clear the UI
- broadcast.reset();
- onRefreshBroadcast.fire();
- }
- });
- };
-
- $button.click(function () {
- send();
- });
-
- var onPreview = function (l) {
- var data = broadcast.getData();
- if (data === false) { return void UI.warn(Messages.error); }
- var msg = {
- uid: Util.uid(),
- type: 'BROADCAST_'+key.toUpperCase(),
- content: data
};
- common.mailbox.onMessage({
- lang: l,
- type: 'broadcast',
- content: {
- msg: msg,
- hash: 'LOCAL|' + JSON.stringify(msg).slice(0,58)
- }
+ $button.click(function () {
+ var data = getData();
+ if (data === false) { return void UI.warn(Messages.error); }
+ send(data);
+ });
+ UI.confirmButton(removeButton, {
+ classes: 'btn-danger',
}, function () {
- UI.log(Messages.saved);
+ send("");
});
- };
- var preview = h('button.cp-broadcast-preview.btn.btn-secondary', Messages.broadcast_preview);
- $(preview).click(function () {
- onPreview();
- });
+ $form.empty().append([
+ active,
+ h('label', Messages.broadcast_start),
+ start,
+ h('label', Messages.broadcast_end),
+ end,
+ h('br'),
+ h('div.cp-broadcast-form-submit', [
+ button
+ ])
+ ]);
+ });
+ refresh();
- var handler = broadcast.handlers[key];
- if (!handler) { return; } // XXX
- handler($form, button, send, preview, onPreview);
+ return $div;
};
- create['broadcast'] = function () {
- var key = 'broadcast';
+ create['survey'] = function () {
+ var key = 'survey';
var $div = makeBlock(key);
var form = h('div.cp-admin-broadcast-form');
- var $select = $(h('div.cp-dropdown-container')).appendTo($div);
var $form = $(form).appendTo($div);
- var categories = [
- 'maintenance',
- 'survey',
- 'version',
- 'custom',
- ];
-
- // The "version" message only works if the instance is using a manual /api/config
- // This is a custom setup for which our team won't provide support and is NOT
- // recommended unless you know exactly what you're doing.
- if (!AppConfig.customApiConfig) { categories.splice(2,1); }
+ var refresh = getApi(function (Broadcast) {
+ var button = h('button.btn.btn-primary', Messages.admin_surveyButton);
+ var $button = $(button);
+ var removeButton = h('button.btn.btn-danger', Messages.admin_surveyCancel);
+ var active;
+
+ if (Broadcast && Broadcast.surveyURL) {
+ var a = h('a', {href: Broadcast.surveyURL}, Messages.admin_surveyActive);
+ $(a).click(function (e) {
+ e.preventDefault();
+ common.openUnsafeURL(Broadcast.surveyURL);
+ });
+ active = h('div.cp-broadcast-active', [
+ h('p', a),
+ removeButton
+ ]);
+ }
- categories = categories.map(function (key) {
- return {
- tag: 'a',
- content: h('span', Messages['broadcast_'+key]),
- action: function () {
- getBroadcastForm($form, key);
+ // Survey form
+ var label = h('label', Messages.broadcast_surveyURL);
+ var input = h('input');
+ var $input = $(input);
+
+ // Extract form data
+ var getData = function () {
+ var url = $input.val();
+ if (!Util.isValidURL(url)) {
+ console.error('Invalid URL');
+ return false;
}
+ return url;
};
- });
- var dropdownCfg = {
- text: Messages.broadcast_new,
- angleDown: 1,
- options: categories,
- container: $select,
- isSelect: true,
- buttonCls: 'btn btn-default'
- };
- UIElements.createDropdown(dropdownCfg);
-
- return $div;
- };
+ var send = function (data) {
+ $button.prop('disabled', 'disabled');
+ sFrameChan.query('Q_ADMIN_RPC', {
+ cmd: 'ADMIN_DECREE',
+ data: ['SET_SURVEY_URL', [data]]
+ }, function (e) {
+ if (e) {
+ $button.prop('disabled', '');
+ UI.warn(Messages.error); console.error(e);
+ return;
+ }
+ // Maintenance applied, send notification
+ common.mailbox.sendTo('BROADCAST_SURVEY', {}, {}, function (err, data) {
+ refresh();
+ checkLastBroadcastHash();
+ });
+ });
- create['broadcast-delete'] = function () {
- var key = 'broadcast-delete';
- var $div = makeBlock(key);
+ };
+ $button.click(function () {
+ var data = getData();
+ if (data === false) { return void UI.warn(Messages.error); }
+ send(data);
+ });
+ UI.confirmButton(removeButton, {
+ classes: 'btn-danger',
+ }, function () {
+ send("");
+ });
- var form = h('div.cp-admin-broadcast-form');
- var $form = $(form).appendTo($div);
- getBroadcastForm($form, 'delete');
- onRefreshBroadcast.reg(function () {
- getBroadcastForm($form, 'delete');
+ $form.empty().append([
+ active,
+ label,
+ input,
+ h('br'),
+ h('div.cp-broadcast-form-submit', [
+ button
+ ])
+ ]);
});
+ refresh();
+
return $div;
};
-
-
var onRefreshPerformance = Util.mkEvent();
create['refresh-performance'] = function () {
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index aa4e50a2e..92ecb4b4e 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -1,6 +1,7 @@
define([
'jquery',
'/api/config',
+ '/api/broadcast',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-language.js',
@@ -17,7 +18,7 @@ define([
'/common/visible.js',
'css!/customize/fonts/cptools/style.css',
-], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, Clipboard,
+], function ($, Config, Broadcast, Util, Hash, Language, UI, Constants, Feedback, h, Clipboard,
Messages, AppConfig, Pages, NThen, InviteInner, Visible) {
var UIElements = {};
var urlArgs = Config.requireConf.urlArgs;
@@ -1762,23 +1763,20 @@ define([
});
}
- // XXX Admin panel overrides AppConfig
// If you set "" in the admin panel, it will remove the AppConfig survey
- var surveyURL = typeof(Config.surveyURL) !== "undefined" ? Config.surveyURL
+ var surveyURL = typeof(Broadcast.surveyURL) !== "undefined" ? Broadcast.surveyURL
: AppConfig.surveyURL;
- if (surveyURL) {
- options.push({
- tag: 'a',
- attributes: {
- 'class': 'cp-toolbar-survey fa fa-graduation-cap'
- },
- content: h('span', Messages.survey),
- action: function () {
- Common.openUnsafeURL(surveyURL);
- Feedback.send('SURVEY_CLICKED');
- },
- });
- }
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-toolbar-survey fa fa-graduation-cap'
+ },
+ content: h('span', Messages.survey),
+ action: function () {
+ Common.openUnsafeURL(surveyURL);
+ Feedback.send('SURVEY_CLICKED');
+ },
+ });
options.push({ tag: 'hr' });
// Add login or logout button depending on the current status
@@ -1845,6 +1843,24 @@ define([
};
var $userAdmin = UIElements.createDropdown(dropdownConfigUser);
+ var $survey = $userAdmin.find('.cp-toolbar-survey');
+ if (!surveyURL) { $survey.hide(); }
+ Common.makeUniversal('broadcast', {
+ onEvent: function (obj) {
+ var cmd = obj.ev;
+ if (cmd !== "SURVEY") { return; }
+ var url = obj.data;
+ if (url === surveyURL) { return; }
+ if (url && !Util.isValidURL(url)) { return; }
+ surveyURL = url;
+ if (!url) {
+ $survey.hide();
+ return;
+ }
+ $survey.show();
+ }
+ });
+
/*
// Uncomment these lines to have a language selector in the admin menu
// FIXME clicking on the inner menu hides the outer one
diff --git a/www/common/notifications.js b/www/common/notifications.js
index 41cfd075e..0804bacbf 100644
--- a/www/common/notifications.js
+++ b/www/common/notifications.js
@@ -422,32 +422,6 @@ define([
}
};
- Messages.broadcast_newMaintenance = "A maintenance is planned between {0} and {1}"; // XXX
- handlers['BROADCAST_MAINTENANCE'] = function (common, data) {
- var content = data.content;
- var msg = content.msg.content;
- content.getFormatText = function () {
- return Messages._getKey('broadcast_newMaintenance', [
- new Date(msg.start).toLocaleString(),
- new Date(msg.end).toLocaleString(),
- ]);
- };
- if (!content.archived) {
- content.dismissHandler = defaultDismiss(common, data);
- }
- };
-
- Messages.broadcast_newVersion = "A new version is available. Reload the page to discover the new features!"; // XXX
- handlers['BROADCAST_VERSION'] = function (common, data) {
- var content = data.content;
- content.getFormatText = function () {
- return Messages.broadcast_newVersion;
- };
- if (!content.archived) {
- content.dismissHandler = defaultDismiss(common, data);
- }
- };
-
Messages.broadcast_newCustom = "Message from the administrators"; // XXX
handlers['BROADCAST_CUSTOM'] = function (common, data) {
var content = data.content;
diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js
index dcf176218..3ee629b6d 100644
--- a/www/common/outer/async-store.js
+++ b/www/common/outer/async-store.js
@@ -626,6 +626,33 @@ define([
cb(JSON.parse(JSON.stringify(metadata)));
};
+ Store.onMaintenanceUpdate = function (uid) {
+ // use uid in /api/broadcast so that all connected users will use the same cached
+ // version on the server
+ require(['/api/broadcast?'+uid], function (Broadcast) {
+ broadcast([], 'UNIVERSAL_EVENT', {
+ type: 'broadcast',
+ data: {
+ ev: 'MAINTENANCE',
+ data: Broadcast.maintenance
+ }
+ });
+ });
+ };
+ Store.onSurveyUpdate = function (uid) {
+ // use uid in /api/broadcast so that all connected users will use the same cached
+ // version on the server
+ require(['/api/broadcast?'+uid], function (Broadcast) {
+ broadcast([], 'UNIVERSAL_EVENT', {
+ type: 'broadcast',
+ data: {
+ ev: 'SURVEY',
+ data: Broadcast.surveyURL
+ }
+ });
+ });
+ };
+
var makePad = function (href, roHref, title) {
var now = +new Date();
return {
diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js
index 33c87617b..7751fd5d7 100644
--- a/www/common/outer/mailbox-handlers.js
+++ b/www/common/outer/mailbox-handlers.js
@@ -690,74 +690,47 @@ define([
var broadcasts = {};
handlers['BROADCAST_MAINTENANCE'] = function (ctx, box, data, cb) {
var msg = data.msg;
- var content = msg.content;
- if (content.end < (+new Date())) {
- // Expired maintenance: dismiss
- return void cb(true);
- }
- var uid = msg.uid;
- broadcasts[uid] = {
- type: box.type,
- hash: data.hash
- };
- cb(false);
- };
- handlers['BROADCAST_VERSION'] = function (ctx, box, data, cb) {
- var msg = data.msg;
- var content = msg.content;
- if (!box.ready) {
- // This is an old version message: dismiss
- return void cb(true);
- }
- if (content.reload) {
- // We're going to force a disconnect, dismiss
- ctx.Store.newVersionReload();
- return; // This message will be removed when reloading the worker
- }
var uid = msg.uid;
- broadcasts[uid] = {
- type: box.type,
- hash: data.hash
- };
- cb(false);
+ ctx.Store.onMaintenanceUpdate(uid);
+ cb(true);
};
+ var activeSurvey;
handlers['BROADCAST_SURVEY'] = function (ctx, box, data, cb) {
var msg = data.msg;
var uid = msg.uid;
- broadcasts[uid] = {
+ var old = activeSurvey;
+ activeSurvey = {
type: box.type,
hash: data.hash
};
- cb(false);
+ ctx.Store.onSurveyUpdate(uid);
+ cb(false, old);
};
+ var activeCustom
handlers['BROADCAST_CUSTOM'] = function (ctx, box, data, cb) {
var msg = data.msg;
var uid = msg.uid;
- broadcasts[uid] = {
+ var old = activeCustom;
+ activeCustom = {
+ uid: uid,
type: box.type,
hash: data.hash
};
- cb(false);
+ cb(false, old);
};
handlers['BROADCAST_DELETE'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
- // If this is a "clear all" message, empty the box and update lkh
- if (content.all) {
- // 3rd argument of callback: clear the mailbox
- return void cb(null, null, true);
- }
-
var uid = content.uid; // uid of the message to delete
- if (!broadcasts[uid]) {
- // We don't have this message in memory, nothing to delete
- return void cb(true);
+ if (activeCustom && activeCustom.uid === uid) {
+ // We have the message in memory, remove it and don't keep the DELETE msg
+ cb(true, activeCustom);
+ activeCustom = undefined;
+ return;
}
-
- // We have the message in memory, remove it and don't keep the DELETE msg
- cb(true, broadcasts[uid]);
- delete broadcasts[uid];
+ // We don't have this message in memory, nothing to delete
+ cb(true);
};
return {
diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js
index 4ab2b6f72..58396b1e9 100644
--- a/www/common/sframe-common-mailbox.js
+++ b/www/common/sframe-common-mailbox.js
@@ -219,8 +219,8 @@ define([
var historyState = false;
var onHistory = function () {};
- mailbox.getMoreHistory = function (type, count, lastKnownHash, cb) {
- if (historyState) { return void cb("ALREADY_CALLED"); }
+ mailbox.getMoreHistory = function (type, count, lastKnownHash, cb) {
+ if (type !== "broadcast" && historyState) { return void cb("ALREADY_CALLED"); }
historyState = true;
var txid = Util.uid();
execCommand('LOAD_HISTORY', {
diff --git a/www/common/toolbar.js b/www/common/toolbar.js
index 172568f44..d59c9afa6 100644
--- a/www/common/toolbar.js
+++ b/www/common/toolbar.js
@@ -2,6 +2,7 @@ define([
'jquery',
'/customize/application_config.js',
'/api/config',
+ '/api/broadcast',
'/common/common-ui-elements.js',
'/common/common-interface.js',
'/common/common-hash.js',
@@ -11,7 +12,7 @@ define([
'/common/hyperscript.js',
'/common/messenger-ui.js',
'/customize/messages.js',
-], function ($, Config, ApiConfig, UIElements, UI, Hash, Util, Feedback, MT, h,
+], function ($, Config, ApiConfig, Broadcast, UIElements, UI, Hash, Util, Feedback, MT, h,
MessengerUI, Messages) {
var Common;
@@ -45,6 +46,7 @@ MessengerUI, Messages) {
var TITLE_CLS = Bar.constants.title = "cp-toolbar-title";
var LINK_CLS = Bar.constants.link = "cp-toolbar-link";
var NOTIFICATIONS_CLS = Bar.constants.user = 'cp-toolbar-notifications';
+ var MAINTENANCE_CLS = Bar.constants.user = 'cp-toolbar-maintenance';
// User admin menu
var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown';
@@ -78,6 +80,7 @@ MessengerUI, Messages) {
'class': USER_CLS
}).appendTo($topContainer);
$('', {'class': LIMIT_CLS}).hide().appendTo($userContainer);
+ $('', {'class': MAINTENANCE_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer);
$('', {'class': NOTIFICATIONS_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer);
$('', {'class': USERADMIN_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer);
@@ -1026,6 +1029,42 @@ MessengerUI, Messages) {
return $userAdmin;
};
+ Messages.broadcast_maintenance = "A maintenance is planned between {0} and {1}"; // XXX
+ var createMaintenance = function (toolbar, config) {
+ var $notif = toolbar.$top.find('.'+MAINTENANCE_CLS);
+ var button = h('button.cp-maintenance-wrench.fa.fa-wrench');
+ $notif.append(button);
+
+
+ var m = Broadcast.maintenance;
+ $(button).click(function () {
+ if (!m || !m.start || !m.end) { return; }
+ UI.alert(Messages._getKey('broadcast_maintenance', [
+ new Date(m.start).toLocaleString(),
+ new Date(m.end).toLocaleString(),
+ ]), null, true);
+ });
+
+ Common.makeUniversal('broadcast', {
+ onEvent: function (obj) {
+ var cmd = obj.ev;
+ if (cmd !== "MAINTENANCE") { return; }
+ var data = obj.data;
+ if (!data) {
+ return void $notif.hide();
+ }
+ m = data;
+ $notif.css('display', '');
+ }
+ });
+
+ if (m && m.start && m.end) {
+ $notif.css('display', '');
+ } else {
+ $notif.hide();
+ }
+ };
+
var createNotifications = function (toolbar, config) {
var $notif = toolbar.$top.find('.'+NOTIFICATIONS_CLS).show();
var openNotifsApp = h('div.cp-notifications-gotoapp', h('p', Messages.openNotificationsApp || "Open notifications App"));
@@ -1287,6 +1326,7 @@ MessengerUI, Messages) {
tb['useradmin'] = createUserAdmin;
tb['unpinnedWarning'] = createUnpinnedWarning;
tb['notifications'] = createNotifications;
+ tb['maintenance'] = createMaintenance;
tb['pad'] = function () {
toolbar.$file.show();
@@ -1323,6 +1363,7 @@ MessengerUI, Messages) {
};
addElement(config.displayed, {}, true);
+ addElement(['maintenance'], {}, true);
toolbar['linkToMain'] = createLinkToMain(toolbar, config);