diff --git a/www/admin/inner.js b/www/admin/inner.js index 341b9a93a..93bc13f21 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -967,20 +967,393 @@ define([ Messages.broadcast_noFallback = "Don't fallback to a default language"; var onRefreshBroadcast = Util.mkEvent(); - var getBroadcastForm = function ($form, key) { - $form.empty(); + 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 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'); + } + }; + + // Remove a textarea + var removeLang = function (l) { + $container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove(); + checkFallbackBtn(); + }; + + // 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(); + }; + + // 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 getData = function () { - return 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(''); + }); + }; + + // "Don't fallback to a default language" button + $noFallbackBtn.click(function () { + $container.find('.cp-checkmark input').prop('checked', false); + $noFallbackBtn.css('visibility', 'hidden'); + }); + + // 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())); + } + }); + + // 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 + }; + }; + + // 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 + ]) + ]); + + }; + 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(''); }; - var reset = function () {}; + $(button).off('click').click(function () { + var data = broadcast.getData(); + var val = $input.val() || ''; + + // 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); } + + var url = data ? data.url : val; + + 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(); + }); + + }); + $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); + + // Empty history + if (!msgs.length) { + $table.append(h('tr.empty', h('td', Messages.broadcast_empty))); + } + + // Build the table + msgs.forEach(function (data) { + var el = common.mailbox.createElement(data); + var t = Util.find(data, ['content', 'msg', 'type']); + + // 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; + } + + // 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), + ]); + + // 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); + } + } + if (t === 'BROADCAST_VERSION') { + $(deleteBtn).prop('disabled', 'disabled').text(Messages.expired); + } + + // "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(); + }); + + $table.append(tr); + }); + + // Clear all button: remove all the messages and bump lastBroadcastHash + var clearAll = h('button.btn.btn-danger', Messages.broadcast_clear); + UI.confirmButton(clearAll, { + 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(); + }); + */ + }); + }); + + $form.append([ + table, + msgs.length ? clearAll : undefined + ]); + }); + }); + + }; + + var getBroadcastForm = function ($form, key) { + $form.empty(); var button = h('button.btn.btn-primary', Messages.support_formButton); var $button = $(button); var send = function (_cb) { var cb = Util.once(_cb || function () {}); - var data = getData(); + var data = broadcast.getData(); if (data === false) { cb('NODATA'); return void UI.warn(Messages.error); @@ -995,12 +1368,23 @@ define([ return UI.warn(Messages.error); } + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_LAST_BROADCAST_HASH', [data.hash]] + }, function (e) { + if (e) { + UI.warn(Messages.error); console.error(e); + return; + } + // On success, reload the "delete" tab + onRefreshBroadcast.fire(); + }); // Only print success if there is no callback if (!_cb) { UI.log(Messages.saved); // Clear the UI - reset(); + broadcast.reset(); onRefreshBroadcast.fire(); } }); @@ -1011,7 +1395,7 @@ define([ }); var onPreview = function (l) { - var data = getData(); + var data = broadcast.getData(); if (data === false) { return void UI.warn(Messages.error); } var msg = { uid: Util.uid(), @@ -1035,391 +1419,9 @@ define([ }); - if (key === 'custom') { - (function () { - // 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 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'); - } - }; - - // Remove a textarea - var removeLang = function (l) { - $container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove(); - checkFallbackBtn(); - }; - - // 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(); - }; - - // 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]; - }); - - // 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 - }; - }; - // Clear all the textarea when sent - reset = function () { - $container.find('.cp-broadcast-lang textarea').each(function (i, el) { - $(el).val(''); - }); - }; - - // "Don't fallback to a default language" button - $noFallbackBtn.click(function () { - $container.find('.cp-checkmark input').prop('checked', false); - $noFallbackBtn.css('visibility', 'hidden'); - }); - - // 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 - ]) - ]); - })(); - return; - } - if (key === 'maintenance') { - (function () { - // 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())); - } - }); - - // Extract form data - 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 - }; - }; - - // Clear when sent - 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 - ]) - ]); - })(); - return; - } - if (key === 'version') { - (function () { - // 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 - getData = function () { - return { - reload: $checkbox.is(':checked') - }; - }; - reset = function () { - $checkbox[0].checked = false; - }; - $form.append([ - $cbox[0], - h('br'), - h('div.cp-broadcast-form-submit', [ - button, - preview - ]) - ]); - })(); - return; - } - if (key === 'survey') { - (function () { - // New survey message - // TODO send different URLs for other languages? - var label = h('label', Messages.broadcast_surveyURL); - var input = h('input'); - var $input = $(input); - getData = function () { - var url = $input.val(); - if (!Util.isValidURL(url)) { - console.error('Invalid URL'); - return false; - } - return { - url: url - }; - }; - reset = function () { - $input.val(''); - }; - $(button).off('click').click(function () { - var data = getData(); - var val = $input.val() || ''; - - // 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); } - - var url = data ? data.url : val; - - sFrameChan.query('Q_ADMIN_RPC', { - cmd: 'ADMIN_DECREE', - data: ['SET_SURVEY_URL', [url]] - }, function (e) { - console.error(e); - if (e) { - UI.warn(Messages.error); console.error(e); - return; - } - if (!url) { return; } - send(); - }); - - }); - $form.append([ - label, - input, - h('br'), - h('div.cp-broadcast-form-submit', [ - button, - preview - ]) - ]); - })(); - return; - } - - if (key === 'delete') { - // Delete form - 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); - - // Empty history - if (!msgs.length) { - $table.append(h('tr.empty', h('td', Messages.broadcast_empty))); - } - - // Build the table - msgs.forEach(function (data) { - var el = common.mailbox.createElement(data); - var t = Util.find(data, ['content', 'msg', 'type']); - - // 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; - } - - // 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), - ]); - - // 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); - } - } - if (t === 'BROADCAST_VERSION') { - $(deleteBtn).prop('disabled', 'disabled').text(Messages.expired); - } - - // "Delete this message" button - UI.confirmButton(deleteBtn, { - classes: 'btn-danger', - multiple: true - }, function () { - getData = function () { - if (!uid) { return false; } - return { uid: uid }; - }; - reset = function () { - $(deleteBtn).prop('disabled', 'disabled').text(Messages.deleted); - }; - send(); - }); - - $table.append(tr); - }); - - // Clear all button: remove all the messages and bump lastBroadcastHash - var clearAll = h('button.btn.btn-danger', Messages.broadcast_clear); - UI.confirmButton(clearAll, { - classes: 'btn-danger', - multiple: true - }, function () { - getData = function () { - return { all: true }; - }; - 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(); - }); - }); - }); - - $form.append([ - table, - msgs.length ? clearAll : undefined - ]); - }); - }); - return; - } - + var handler = broadcast.handlers[key]; + if (!handler) { return; } // XXX + handler($form, button, send, preview, onPreview); }; create['broadcast'] = function () { var key = 'broadcast';