diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index 9e2c574aa..ab29b56fa 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -9,12 +9,14 @@ * If you want to check all the configurable values, you can open the internal configuration file but you should not change it directly (/common/application_config_internal.js) */ + define(['/common/application_config_internal.js'], function (AppConfig) { + // Example: If you want to remove the survey link in the menu: // AppConfig.surveyURL = ""; // To inform users of the support ticket panel which languages your admins speak: //AppConfig.supportLanguages = [ 'en', 'fr' ]; - + return AppConfig; }); diff --git a/customize.dist/messages.js b/customize.dist/messages.js index b527d24e0..b644fd936 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -132,6 +132,28 @@ define(req, function(AppConfig, Default, Language) { } }; +Messages.install_token = "Install token"; +Messages.install_header = "Installation"; +Messages.install_instance = "Create the first admin account, then proceed to customize this instance"; +Messages.install_launch = "Instance setup"; +Messages.install_notes = ``; + +Messages.admin_appSelection = 'App configuration'; +Messages.admin_appsTitle = "Instance applications"; +Messages.admin_appsHint = "Choose which apps to enable on this instance."; +Messages.admin_cat_apps = "Applications"; + +Messages.onboarding_save_error = "Some options could not be saved properly. Please visit the administration panel to check the values."; +Messages.onboarding_upload = "Select logo"; +Messages.admin_onboardingNameTitle = 'Welcome to your CryptPad instance'; +Messages.admin_onboardingNameHint = 'Please choose a title, description, accent color and logo (all are optional)'; +Messages.admin_onboardingOptionsTitle = "Instance options"; +Messages.admin_onboardingOptionsHint = "Please select the apropriate option for your instance. These settings can be changed later in the admin panel."; +Messages.admin_onboardingNamePlaceholder = 'Instance title'; +Messages.admin_onboardingDescPlaceholder = 'Instance description text'; return Messages; diff --git a/customize.dist/pages/install.js b/customize.dist/pages/install.js index d6e8a1713..018d221b9 100644 --- a/customize.dist/pages/install.js +++ b/customize.dist/pages/install.js @@ -18,8 +18,6 @@ define([ return; } -Msg.install_token = "Install token"; - document.title = Msg.install_header; var frame = function (content) { @@ -27,8 +25,7 @@ Msg.install_token = "Install token"; h('div#cp-main', [ //Pages.infopageTopbar(), h('div.container.cp-container', [ - //h('div.row.cp-page-title', h('h1', Msg.install_header)), - h('div.row.cp-page-title', h('h1', Msg.register_header)), + h('div.row.cp-page-title', h('h1', Msg.install_header)), ].concat(content)), Pages.infopageFooter(), ]), @@ -39,17 +36,12 @@ Msg.install_token = "Install token"; h('div.row.cp-register-det', [ h('div#data.hidden.col-md-6', [ h('h2', Msg.register_notes_title), - //Pages.setHTML(h('div.cp-register-notes'), Msg.install_notes) - Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes) + Pages.setHTML(h('div.cp-register-notes'), Msg.install_notes) ]), h('div.cp-reg-form.col-md-6', [ h('div#userForm.form-group.hidden', [ h('div.cp-register-instance', [ - Msg._getKey('register_instance', [ Pages.Instance.name ]), - /*h('br'), - h('a', { - href: '/features.html' - }, Msg.register_whyRegister)*/ + Msg.install_instance, ]), h('input.form-control#installtoken', { type: 'text', @@ -75,7 +67,7 @@ Msg.install_token = "Install token"; /*h('div.checkbox-container', [ UI.createCheckbox('import-recent', Msg.register_importRecent, true) ]),*/ - h('button#register', Msg.login_register) + h('button#register', Msg.install_launch) ]) ]), ]) diff --git a/customize.dist/src/less2/include/admin.less b/customize.dist/src/less2/include/admin.less new file mode 100644 index 000000000..13fe5360e --- /dev/null +++ b/customize.dist/src/less2/include/admin.less @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +@import (reference) "./colortheme-all.less"; +@import (reference) "./forms.less"; +@import (reference) './icon-colors.less'; + +.admin_main() { + --LessLoader_require: LessLoader_currentFile(); +} +& { + // XXX Instance accent color presets + @palette-colors: + @cryptpad_color_brand, + #ffee00, + #FF0073; + + div.cp-palette-container { + + .cp-palette-nocolor { + display: none; + } + .instance-colors(@palette-colors; @index) when (@index > 0){ + // loop through the @colors + .instance-colors(@palette-colors; (@index - 1)); + + @color: extract(@palette-colors, @index); + // make a numbered class selector for each color + .cp-palette-color@{index}{ + background-color: @color !important; + color: contrast(@color, @cryptpad_color_grey_800, @cryptpad_color_grey_200) !important; + } + } + .instance-colors(@palette-colors; length(@palette-colors)); + } + + .cp-admin-customize-apps-grid, .cp-admin-customize-options-grid { + display: grid; + gap: 0.5rem; + } + + .cp-admin-customize-apps-grid { + grid-template-columns: 1fr 1fr 1fr; + .cp-appblock { + padding: 0.5rem; + border-radius: @variables_radius; + font-size: 1.2em; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; + .iconColors_main(); + &:hover { + cursor: pointer; + } + i.cp-icon { + font-size: 2.8rem; + } + .cp-app-name { + flex-grow: 1; + } + } + .cp-inactive-app { + background-color: transparent; + opacity: 0.75; + .cp-on-enabled { + visibility: hidden; + } + } + .cp-active-app { + background-color: fade(@cryptpad_text_col, 10%); + .cp-on-enabled { + visibility: visible; + } + } + } + + .cp-admin-customize-options-grid { + grid-template-columns: 1fr 1fr; + .cp-optionblock { + padding: 0.5rem; + border-radius: @variables_radius; + background-color: fade(@cryptpad_text_col, 10%); + align-self: start; + .cp-checkmark-label { + font-weight: bold; + } + .cp-option-hint { + margin-left: 30px; + display: inline-block; + } + } + } +} diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 3c018e365..9f0ec1c15 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -119,6 +119,31 @@ } } + // The following palette container is just for the UI components + // The specific colors you want to show have to be defined in your app + // using the classes .cp-palette-nocolor .cp-palette-color1 .cp-palette-color2 etc. + div.cp-palette-container { + display: flex; + justify-content: space-between; + .cp-palette-color { + display: inline-block; + border-radius: 50%; + height: 30px; + width: 30px; + text-align: center; + line-height: 30px; + color: @cp_kanban-fg; + border: 1px solid fade(@cp_kanban-fg, 40%); + &.fa-check { // tick on selected color + color: @cryptpad_text_col; + } + outline: none; + &:focus{ + outline: @cryptpad_color_brand solid 2px; + } + } + } + button.btn { background-color: @cp_buttons-cancel; box-sizing: border-box; diff --git a/lib/api.js b/lib/api.js index 220e954d8..b3beddf3d 100644 --- a/lib/api.js +++ b/lib/api.js @@ -10,6 +10,7 @@ const nThen = require("nthen"); const Fs = require("fs"); const Path = require("path"); const Nacl = require("tweetnacl/nacl-fast"); +const Hash = require('./common-hash'); module.exports.create = function (Env) { var log = Env.Log; @@ -25,6 +26,39 @@ nThen(function (w) { console.error(err); } })); +}).nThen(function (w) { + let admins = Env.admins || []; + + // If we don't have any admin on this instance, print an onboarding link + if (Array.isArray(admins) && admins.length) { return; } + let token = Env.installToken; + let printLink = () => { + let url = `${Env.httpUnsafeOrigin}/install/#${token}`; + console.log('============================='); + console.log('Create your first admin account and customize your instance by visiting'); + console.log(url); + console.log('============================='); + + }; + + // If we already have a token, print it + if (token) { return void printLink(); } + + // Otherwise create a new token + let decreeName = Path.join(Env.paths.decree, 'decree.ndjson'); + token = Hash.createChannelId() + Hash.createChannelId(); + let decree = ["ADD_INSTALL_TOKEN",[token],"",+new Date()]; + Fs.appendFile(decreeName, JSON.stringify(decree) + '\n', w(function (err) { + if (err) { console.log(err); return; } + printLink(); + })); +}).nThen(function (w) { + if (!Env.admins.length) { + Env.Log.info('NO_ADMIN_CONFIGURED', { + message: `Your instance is not correctly configured for production usage. Review its checkup page for more information.`, + details: new URL('/checkup/', Env.httpUnsafeOrigin).href, + }); + } }).nThen(function (w) { // we assume the server has generated a secret used to validate JWT tokens if (typeof(Env.bearerSecret) === 'string') { return; } diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index bce956147..4f5f8af6e 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -466,6 +466,8 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) { // CryptPad_AsyncStore.rpc.send('ADMIN', ['INSTANCE_STATUS], console.log) var instanceStatus = function (Env, Server, cb) { cb(void 0, { + + appsToDisable: Env.appsToDisable, restrictRegistration: Env.restrictRegistration, restrictSsoRegistration: Env.restrictSsoRegistration, dontStoreSSOUsers: Env.dontStoreSSOUsers, diff --git a/lib/common-hash.js b/lib/common-hash.js new file mode 100644 index 000000000..7a3beddab --- /dev/null +++ b/lib/common-hash.js @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +module.exports = require("../www/common/common-hash"); + diff --git a/lib/decrees.js b/lib/decrees.js index 317d492df..6eea60250 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -220,6 +220,15 @@ commands.SET_SUPPORT_MAILBOX = makeGenericSetter('supportMailbox', function (arg return args_isString(args) && Core.isValidPublicKey(args[0]); }); // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SUPPORT_KEYS', ["Tdz6+fE9N9XXBY93rW5qeNa/k27yd40c0vq7EJyt7jA=", "Tdz6+fE9N9XXBY93rW5qeNa/k27yd40c0vq7EJyt7jA="]]], console.log) + + +commands.DISABLE_APPS = function (Env, args) { + if (!Array.isArray(args)) { throw new Error("INVALID_ARGS"); } + const appsToDisable = args; + Env.appsToDisable = appsToDisable; + return true; +}; + commands.SET_SUPPORT_KEYS = function (Env, args) { const curvePublic = args[0]; // Support mailbox key const edPublic = args[1]; // Support pin log @@ -232,7 +241,7 @@ commands.SET_SUPPORT_KEYS = function (Env, args) { Env.supportMailboxKey = curvePublic; Env.supportPinKey = edPublic; return true; -}; + }; // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_PURPOSE', ["development"]]], console.log) commands.SET_INSTANCE_PURPOSE = makeGenericSetter('instancePurpose', args_isString); @@ -290,7 +299,7 @@ var args_isMaintenance = function (args) { // whenever that happens we can relax validation a bit to support more formats var makeBroadcastSetter = function (attr, validation) { return function (Env, args) { - if ((validation && !validation(args)) && !args_isString(args)) { + if ((validation && !validation(args)) && !args_isString(args)) { throw new Error('INVALID_ARGS'); } var str = args[0]; diff --git a/lib/http-worker.js b/lib/http-worker.js index f84658a34..227122765 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -590,6 +590,7 @@ var serveConfig = makeRouteCache(function () { maxUploadSize: Env.maxUploadSize, premiumUploadSize: Env.premiumUploadSize, restrictRegistration: Env.restrictRegistration, + appsToDisable: Env.appsToDisable, restrictSsoRegistration: Env.restrictSsoRegistration, httpSafeOrigin: Env.httpSafeOrigin, enableEmbedding: Env.enableEmbedding, diff --git a/lib/stats.js b/lib/stats.js index d67a2ee71..e3c8e502d 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -82,7 +82,6 @@ Stats.instanceData = function (Env) { // check how many instances provide stats before we put more work into it data.providesAggregateStatistics = true; } - return data; }; diff --git a/scripts/install.js b/scripts/install.js index 86a0d0b75..4fb06a4ed 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -40,5 +40,4 @@ nThen(function (w) { console.log(token); var url = config.httpUnsafeOrigin + '/install/'; console.log(`Please visit ${url} to create your first admin user`); - }); diff --git a/server.js b/server.js index e84bec70b..f240b1cd9 100644 --- a/server.js +++ b/server.js @@ -68,13 +68,6 @@ nThen(function (w) { Env.Log.info("WEBSERVER_LISTENING", { origin: url, }); - - if (!Env.admins.length) { - Env.Log.info('NO_ADMIN_CONFIGURED', { - message: `Your instance is not correctly configured for production usage. Review its checkup page for more information.`, - details: new URL('/checkup/', Env.httpUnsafeOrigin).href, - }); - } } catch (err) { Env.Log.error("INVALID_ORIGIN", { httpUnsafeOrigin: Env.httpUnsafeOrigin, diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index 2e49e3dec..fbaaf4b5e 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -9,12 +9,14 @@ @import (reference) "../../customize/src/less2/include/creation.less"; @import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/export.less'; +@import (reference) '../../customize/src/less2/include/admin.less'; &.cp-app-admin { .framework_min_main(); .sidebar-layout_main(); .limit-bar_main(); .creation_main(); + .admin_main(); display: flex; flex-flow: column; @@ -32,6 +34,16 @@ border-radius: 5px; background-color: @cryptpad_color_brand; } + input.cp-admin-color-picker { + vertical-align: middle; + } + .cp-palette-container { + display: inline-flex; + width: ~"calc(100% - 5rem)"; + padding-left: 0.5rem; + vertical-align: middle; + } + .cp-admin-color-preview { & > div { margin-top: @sidebar_base-margin; diff --git a/www/admin/inner.js b/www/admin/inner.js index 595dbf4dd..bbb92d3db 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -5,6 +5,7 @@ define([ 'jquery', '/common/toolbar.js', + '/common/pad-types.js', '/components/nthen/index.js', '/common/sframe-common.js', '/common/common-interface.js', @@ -17,11 +18,11 @@ define([ '/common/hyperscript.js', '/common/clipboard.js', 'json.sortify', - '/customize/application_config.js', '/api/config', '/api/instance', '/lib/datepicker/flatpickr.js', - '/common/hyperscript.js', + '/install/onboardscreen.js', + 'css!/lib/datepicker/flatpickr.min.css', 'css!/components/bootstrap/dist/css/bootstrap.min.css', 'css!/components/components-font-awesome/css/font-awesome.min.css', @@ -29,6 +30,7 @@ define([ ], function( $, Toolbar, + PadTypes, nThen, SFCommon, UI, @@ -41,11 +43,12 @@ define([ h, Clipboard, Sortify, - AppConfig, ApiConfig, Instance, - Flatpickr + Flatpickr, + Onboarding, ) { + var APP = window.APP = {}; var Nacl = window.nacl; @@ -90,6 +93,12 @@ define([ 'forcemfa', ] }, + 'apps': { // Msg.admin_cat_apps + icon: 'fa fa-wrench', + content: [ + 'apps', + ] + }, 'users' : { // Msg.admin_cat_users icon : 'fa fa-address-card-o', content : [ @@ -160,7 +169,7 @@ define([ } }; - const blocks = sidebar.blocks; + const blocks = Sidebar.blocks('admin'); const flushCache = (cb) => { cb = cb || function () {}; @@ -190,6 +199,7 @@ define([ // Msg.admin_flushCacheHint, .admin_flushCacheTitle, .admin_flushCacheButton sidebar.addItem('flush-cache', function (cb) { + const blocks = Sidebar.blocks('admin'); var button = blocks.activeButton('primary', '', Messages.admin_flushCacheButton, done => { flushCache(function (e, data) { @@ -609,7 +619,6 @@ define([ UI.log(Messages._getKey('ui_saved', [Messages.admin_emailTitle])); }); }); - var nav = blocks.nav([button]); var form = blocks.form([ @@ -621,6 +630,35 @@ define([ cb(form); }); + sidebar.addItem('apps', function (cb) { + const appsToDisable = ApiConfig.appsToDisable || []; + const grid = Onboarding.createAppsGrid(appsToDisable); + + var save = blocks.activeButton('primary', '', Messages.settings_save, function (done) { + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['DISABLE_APPS', appsToDisable] + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + done(false); + return; + } + flushCache(); + done(true); + UI.log(Messages._getKey('ui_saved', [Messages.admin_appSelection])); + }); + }); + + let form = blocks.form([ + grid + ], blocks.nav([save])); + + cb(form); + }); + + sidebar.addItem('instance-info-notice', function(cb){ var key = 'instance-info-notice'; var notice = blocks.alert('info', key, [Messages.admin_infoNotice1, ' ', Messages.admin_infoNotice2]); @@ -905,7 +943,7 @@ define([ setColor(color, done); }); - let $input = $(input).on('change', () => { + let onColorPicked = () => { require(['/lib/less.min.js'], (Less) => { let color = $input.val(); let lColor = Less.color(color.slice(1)); @@ -925,7 +963,8 @@ define([ $preview.find('.cp-admin-color-preview-dark a').attr('style', `color: ${lightColor} !important`); $preview.find('.cp-admin-color-preview-light a').attr('style', `color: ${color} !important`); }); - }); + }; + let $input = $(input).on('change', onColorPicked).addClass('cp-admin-color-picker'); UI.confirmButton($remove, { classes: 'btn-danger', @@ -935,9 +974,19 @@ define([ setColor('', () => {}); }); + var colors = UIElements.makePalette(8, (color, $color) => { + // onselect + let rgb = $color.css('background-color'); + let hex = Util.rgbToHex(rgb); + $input.val(hex); + onColorPicked(); + }); + var $colors = $(colors); + + $(label).append(colors); let form = blocks.form([ labelCurrent, - label + label, ], blocks.nav([btn, remove, btn.spinner])); cb([form, labelPreview]); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 26584da30..d4f9ea7c5 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -1119,6 +1119,21 @@ define([ } }; + UI.getNewIcon = function (type) { + var icon = h('i.fa.fa-file-text-o'); + + if (AppConfig.applicationsIcon && AppConfig.applicationsIcon[type]) { + var icon = AppConfig.applicationsIcon[type]; + var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa'; + if (type === 'fileupload') { type = 'file'; } + if (type === 'folderupload') { type = 'file'; } + if (type === 'link') { type = 'drive'; } + var appClass = ' cp-icon cp-icon-color-'+type; + icon = h('i', {'class': font + ' ' + icon + appClass}); + } + + return icon; + }; var $defaultIcon = $('', {"class": "fa fa-file-text-o"}); UI.getIcon = function (type) { var $icon = $defaultIcon.clone(); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 8fc65140c..4326cd534 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -4297,5 +4297,76 @@ define([ return UI.errorLoadingScreen(msg, false, false); }; + UIElements.makePalette = (maxColors, onSelect) => { + let palette = ['']; + for (var i=1; i<=maxColors; i++) { palette.push('color'+i); } + + let offline = false; + let selectedColor = ''; + let container = h('div.cp-palette-container'); + let $container = $(container); + + var all = []; + palette.forEach(function (color, i) { + var $color = $(h('button.cp-palette-color.fa')); + all.push($color); + $color.addClass('cp-palette-'+(color || 'nocolor')); + $color.keydown(function (e) { + if (e.which === 13) { + e.stopPropagation(); + e.preventDefault(); + $color.click(); + } + }); + $color.click(function () { + if (offline) { return; } + if (color === selectedColor) { return; } + selectedColor = color; + $container.find('.cp-palette-color').removeClass('fa-check'); + $color.addClass('fa-check'); + onSelect(color, $color); + }).appendTo($container); + $color.keydown(e => { + if (e.which === 37) { + e.preventDefault(); + if (i === 0) { + all[all.length - 1].focus(); + } else { + all[i - 1].focus(); + } + } + if (e.which === 39) { + e.preventDefault(); + if (i === (all.length - 1)) { + all[0].focus(); + } else { + all[i + 1].focus(); + } + } + if (e.which === 9) { + if (e.shiftKey) { + all[0].focus(); + return; + } + all[all.length - 1].focus(); + } + }); + }); + + container.disable = state => { + offline = !!state; + }; + container.getValue = () => { + return selectedColor; + }; + container.setValue = color => { + $container.find('.cp-palette-color').removeClass('fa-check'); + let $color = $container.find('.cp-palette-'+(color || 'nocolor')); + $color.addClass('fa-check'); + selectedColor = color; + }; + return container; + }; + return UIElements; }); diff --git a/www/common/common-util.js b/www/common/common-util.js index fd96f12c4..5faa9171e 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -606,6 +606,9 @@ parseInt(h.slice(4,6), 16), ]; }; + Util.rgbToHex = function (rgb) { + return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/).slice(1).map(n => parseInt(n, 10).toString(16).padStart(2, '0')).join('')}`; + }; Util.isSmallScreen = function () { return window.innerHeight < 800 || window.innerWidth < 800; diff --git a/www/common/inner/sidebar-layout.js b/www/common/inner/sidebar-layout.js index f6adcb66c..9df1ac220 100644 --- a/www/common/inner/sidebar-layout.js +++ b/www/common/inner/sidebar-layout.js @@ -22,17 +22,14 @@ define([ h ) { const Sidebar = {}; + const keyToCamlCase = (key) => { + return key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); + }; - Sidebar.create = function (common, app, $container) { - const $leftside = $(h('div#cp-sidebarlayout-leftside')).appendTo($container); - const $rightside = $(h('div#cp-sidebarlayout-rightside')).appendTo($container); - const sidebar = { - $leftside, - $rightside - }; - const items = {}; + Sidebar.blocks = function (app) { + + let blocks = {}; - let blocks = sidebar.blocks = {}; blocks.labelledInput = (label, input, inputBlock) => { let uid = Util.uid(); let id = `cp-${app}-item-${uid}`; @@ -109,7 +106,7 @@ define([ element ]); }; - blocks.pre = (value) => { + blocks.pre = (value) => { return h('pre', value); }; @@ -220,9 +217,7 @@ define([ return button; }; - const keyToCamlCase = (key) => { - return key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); - }; + blocks.activeCheckbox = (data) => { const state = data.getState(); const key = data.key; @@ -246,6 +241,20 @@ define([ return box; }; + return blocks; + + }; + + Sidebar.create = function (common, app, $container) { + const $leftside = $(h('div#cp-sidebarlayout-leftside')).appendTo($container); + const $rightside = $(h('div#cp-sidebarlayout-rightside')).appendTo($container); + const sidebar = { + $leftside, + $rightside + }; + const items = {}; + sidebar.blocks = Sidebar.blocks(app); + sidebar.addItem = (key, get, options) => { const safeKey = keyToCamlCase(key); get((content) => { @@ -274,6 +283,7 @@ define([ sidebar.addCheckboxItem = (data) => { const key = data.key; + let blocks = sidebar.blocks; let box = blocks.activeCheckbox(data); sidebar.addItem(key, function (cb) { cb(box); diff --git a/www/common/pad-types.js b/www/common/pad-types.js index 86390e885..78adb56df 100644 --- a/www/common/pad-types.js +++ b/www/common/pad-types.js @@ -12,12 +12,22 @@ define([ OOCurrentVersion.currentVersion, ); - let availableTypes = AppConfig.availablePadTypes.filter( - (t) => ooEnabled || !OO_APPS.includes(t), + let availablePadTypes = AppConfig.availablePadTypes.filter( + (t) => ooEnabled || !OO_APPS.includes(t) ); + let availableTypes; + if (ApiConfig.appsToDisable) { + availableTypes = availablePadTypes.filter(value => !ApiConfig.appsToDisable.includes(value)); + } else { + availableTypes = availablePadTypes; + } + + var appsToSelect = availablePadTypes.filter(value => !['drive', 'teams', 'file', 'contacts', 'convert'].includes(value)); + return { availableTypes, + appsToSelect, isAvailable: function (type) { return availableTypes.includes(type); diff --git a/www/install/main.js b/www/install/main.js index fffb7343d..b7735a321 100644 --- a/www/install/main.js +++ b/www/install/main.js @@ -16,13 +16,17 @@ define([ '/common/hyperscript.js', '/customize/pages.js', '/common/rpc.js', + 'onboardscreen.js', + '/common/inner/sidebar-layout.js', + 'less!/install/onboarding.less', 'css!/components/components-font-awesome/css/font-awesome.min.css', -], function ($, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages, Rpc) { +], function ($, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages, Rpc, OnboardScreen, Sidebar) { if (window.top !== window) { return; } var Messages = Cryptpad.Messages; $(function () { - if (LocalStore.isLoggedIn()) { + + if (LocalStore.isLoggedIn() && !localStorage.CP_dev) { // already logged in, redirect to drive document.location.href = '/drive/'; return; @@ -55,10 +59,14 @@ define([ } } + var showTitleScreen = function (sendAdminDecree, sendAdminRpc) { + OnboardScreen.create(sendAdminDecree, sendAdminRpc); + }; + var registerClick = function () { var uname = $uname.val().trim(); - // trim whitespace surrounding the username since it is otherwise included in key derivation - // most people won't realize that its presence is significant + // trim whitespace surrounding the username since it is otherwise included in key derivation + // most people won't realize that its presence is significant $uname.val(uname); var passwd = $passwd.val(); @@ -112,6 +120,27 @@ define([ return void UI.alert(Messages.register_mustAcceptTerms); } + let startOnboarding = function (network, proxy) { + Rpc.create(network, proxy.edPrivate, proxy.edPublic, function (e, rpc) { + if (e) { + // TODO: handle error + return; + } + + let sendAdminDecree = function (command, data, callback) { + var params = ['ADMIN_DECREE', [command, data]]; + rpc.send('ADMIN', params, callback); + }; + + let sendAdminRpc = function (command, data, callback) { + var params = [command, data]; + rpc.send('ADMIN', params, callback); + }; + + showTitleScreen(sendAdminDecree, sendAdminRpc); + }); + }; + setTimeout(function () { var span = h('span', [ h('h2', [ @@ -145,7 +174,7 @@ define([ edPublic: proxy.edPublic }, function (e) { if (e) { UI.alert(Messages.error); return console.error(e); } - window.location.href = '/drive/'; + startOnboarding(data.network, proxy); }); }); diff --git a/www/install/onboarding.less b/www/install/onboarding.less new file mode 100644 index 000000000..d0f2c33ac --- /dev/null +++ b/www/install/onboarding.less @@ -0,0 +1,119 @@ +@import (reference) "../../customize/src/less2/include/browser.less"; +@import (reference) "../../customize/src/less2/include/framework.less"; +@import (reference) "../../customize/src/less2/include/tools.less"; +@import (reference) "../../customize/src/less2/include/markdown.less"; +@import (reference) "../../customize/src/less2/include/avatar.less"; +@import (reference) '../../customize/src/less2/include/admin.less'; +@import (reference) '../../customize/src/less2/include/icon-colors.less'; + +&.cp-page-install { + .admin_main(); + div.cp-palette-container { + max-width: 400px; + } + #cp-onboarding { + position: fixed; + z-index: 1000; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + background-color: @cp_loading-bg; + color: @cp_loading-fg; + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + .cp-onboarding-box { + width: 850px; + background-color: @cp_loading-progress-bg; + border-radius: @variables_radius_L; + box-shadow: 0px 0px 10px 0px @cp_shadow-color; + padding: 30px 110px 20px 110px; + } + .cp-onboarding-logo-input { + display: none; + } + .cp-onboardscreen-logo { + min-width: 200px; + max-width: 200px; + nav { + display: flex; + margin-top: 0.5rem; + justify-content: center; + } + } + } +} + +.configscreen { + //width: 70%; + //height: 75%; + background-color: @cp_loading-progress-bg; + border-radius: @variables_radius_L; + box-shadow: 0px 0px 10px 0px @cp_shadow-color; + padding: 30px 110px 20px 110px; +} + +.cp-onboardscreen-screentitle { + text-align: center; + font-size: 1rem; + margin-bottom: 1rem; + h1.cp-onboardscreen-title { + font-family: "IBM Plex Mono"; + color: @cryptpad_color_link; + font-size: 1.5rem; + font-weight: 500; + } +} + +.cp-instance-form { + display: flex; + flex-direction: row; + gap: 1rem; + input, textarea { + border: 0px transparent !important; + } + .cp-onboardscreen-logo { + .cp-sidebar-form { + background-color: @cp_forms-bg; + border-radius: @variables_radius; + padding: 0.5rem; + .cp-admin-customize-logo { + img { + max-width: 100%; + } + } + } + } + .cp-instance-text-form { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; + .cp-onboardscreen-name { + input { + font-family: "IBM Plex Mono"; + font-size: 1.2em; + } + } + .cp-onboardscreen-colorpick { + div.cp-palette-container { + display: flex; + justify-content: flex-start; + .cp-palette-color { + margin-right: 1rem; + } + } + } + } +} + + +nav.cp-onboardscreen-nav { + width: 100%; + display: flex; + align-items: flex-end; + justify-content: space-between; + margin-top: 1rem; +} \ No newline at end of file diff --git a/www/install/onboardscreen.js b/www/install/onboardscreen.js new file mode 100644 index 000000000..6272da418 --- /dev/null +++ b/www/install/onboardscreen.js @@ -0,0 +1,498 @@ +define([ + 'jquery', + '/common/inner/sidebar-layout.js', + '/customize/messages.js', + '/customize/application_config.js', + '/common/hyperscript.js', + '/common/common-interface.js', + '/common/common-util.js', + '/common/common-ui-elements.js', + '/common/pad-types.js', + '/components/nthen/index.js', + + 'css!/components/bootstrap/dist/css/bootstrap.min.css', + 'css!/components/components-font-awesome/css/font-awesome.min.css', + +], function( + $, + Sidebar, + Messages, + AppConfig, + h, + UI, + Util, + UIElements, + PadTypes, + nThen +) { + + let pages = []; + const gotoPage = function (Env, page) { + if (typeof(page) !== "number" || !page) { page = 0; } + if (page > (pages.length - 1)) { page = pages.length - 1; } + + var nextPageFunction = pages[page]; + var nextPageForm = nextPageFunction(Env); + let frame = h('div.cp-onboarding-box', nextPageForm); + Env.overlay.empty().append(frame); + }; + const blocks = Sidebar.blocks('admin'); + + var flushCache = (Env, cb) => { + const { sendAdminRpc } = Env; + sendAdminRpc('FLUSH_CACHE', {}, function (e, response) { + if (e || response.error) { + console.error(e || response.error); + } + cb(); + }); + }; + + var selections = { + title: '', + description: '', + logoURL: '', + color: '', + appsToDisable: [], + mfa: false, + closeRegistration: false + }; + + const saveAndRedirect = (Env) => { + const { sendAdminDecree, sendAdminRpc } = Env; + + let hasError = false; + let error = () => { + hasError = true; + }; + nThen(waitFor => { + var name = selections.title; + if (!name) { return; } + sendAdminDecree('SET_INSTANCE_NAME', [name], waitFor((e, response) => { + if (e || response.error) { + console.error(e || response.error); + return void error(); + } + })); + }).nThen(waitFor => { + var description = selections.description; + if (!description) { return; } + sendAdminDecree('SET_INSTANCE_DESCRIPTION', [ + description + ], waitFor((e, response) => { + if (e || response.error) { + console.error(e || response.error); + return void error(); + } + + })); + }).nThen(waitFor => { + var dataURL = selections.logoURL; + if (!dataURL) { return; } + sendAdminRpc('UPLOAD_LOGO', {dataURL}, waitFor(function (e, response) { + if (e || response.error) { + console.error(e || response.error); + return void error(); + } + })); + }).nThen(waitFor => { + var color = selections.color; + if (!color) { return; } + sendAdminRpc('CHANGE_COLOR', {color}, waitFor(function (e, response) { + if (e || response.error) { + console.error(e || response.error); + return void error(); + } + })); + }).nThen(waitFor => { + var apps = selections.appsToDisable; + if (!apps.length) { return; } + sendAdminDecree('DISABLE_APPS', apps, waitFor(function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + return; + } + })); + }).nThen(waitFor => { + if (!selections.mfa) { return; } + sendAdminDecree('ENFORCE_MFA', [selections.mfa], waitFor((e, response) => { + if (e || response.error) { + UI.warn(Messages.error); + return; + } + })); + }).nThen(waitFor => { + if (!selections.closeRegistration) { return; } + sendAdminDecree('RESTRICT_REGISTRATION', [ + selections.closeRegistration + ], waitFor(function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + return; + } + })); + }).nThen(() => { + flushCache(Env, function () { + if (hasError) { + UI.alert(Messages.onboarding_save_error, function () { + document.location.href = '/drive/'; + }) + return; + } + document.location.href = '/drive/'; + }); + }); + }; + + const titleConfig = function (Env) { + let titleInput = blocks.input({ + type: 'text', + value: selections.title, + placeholder: Messages.admin_onboardingNamePlaceholder, + 'aria-labelledby': 'cp-admin-name' + }); + let description = blocks.textarea({ + placeholder: Messages.admin_onboardingDescPlaceholder, + 'aria-labelledby': 'cp-admin-description' + }, selections.description); + $(description).addClass('cp-onboardscreen-desc'); + + let dataURL = selections.logoURL; + var getLogoBlock = function() { + let inputLogo = blocks.input({ + type: 'file', + accept: 'image/*', + 'aria-labelledby': 'cp-admin-logo' + }); + + var currentContainer = blocks.block([], 'cp-admin-customize-logo'); + + var upload = blocks.button('secondary', '', Messages.onboarding_upload); + + let formLogo = blocks.form([ + currentContainer, + blocks.block(inputLogo, 'cp-onboarding-logo-input'), + blocks.nav([upload]) + ]); + + let $button = $(upload); + + let state = false; + let redraw = () => { + var current = h('img', {src: dataURL || '/api/logo?'+(+new Date())}); + $(currentContainer).empty().append(current); + $button.removeAttr('disabled'); + state = !!dataURL; + if (dataURL) { + $button.text(Messages.admin_logoRemoveButton); + $button.removeClass('btn-secondary').addClass('btn-danger-alt'); + } else { + $button.text(Messages.onboarding_upload); + $button.removeClass('btn-danger-alt').addClass('btn-secondary'); + } + }; + redraw(); + + let $input = $(inputLogo); + $input.on('change', function () { + let files = inputLogo.files; + if (files.length !== 1) { + UI.warn(Messages.error); + return; + } + let reader = new FileReader(); + reader.onloadend = function () { + dataURL = this.result; + redraw(dataURL); + }; + reader.readAsDataURL(files[0]); + }); + Util.onClickEnter($button, function () { + $button.attr('disabled', 'disabled'); + if (!state) { + return void $input.click(); + } + dataURL = ''; + redraw(); + }); + + return formLogo; + }; + + var getColorBlock = function () { + + // XXX Number of accent color presets + var colors = UIElements.makePalette(5, (color, $color) => { + let rgb = $color.css('background-color'); + let hex = Util.rgbToHex(rgb); + if (hex) { + selections.color = color ? hex : ''; + selections.colorId = color; + } + }); + if (selections.colorId) { + colors.setValue(selections.colorId); + } + var $colors = $(colors).attr('id', 'cp-install-color'); + var content = h('div.cp-onboardscreen-colorpick', [ + h('label', {for:'cp-install-color'}, Messages.kanban_color), + colors + ]); + + return content; + }; + + var button = blocks.activeButton('primary', '', Messages.continue, function (done) { + selections.title = $(titleInput).val() || ''; + selections.description = $(description).val() || ''; + if (dataURL) { + selections.logoURL = dataURL; + } + + gotoPage(Env, 1); + }); + + var titleBlock = h('div.cp-onboardscreen-name', titleInput); + var descriptionBlock = h('div', description); + var logoBlock = h('div.cp-onboardscreen-logo', getLogoBlock()); + var colorBlock = h('div.cp-onboardscreen-color', getColorBlock()); + + var screenTitle = h('div.cp-onboardscreen-screentitle'); + $(screenTitle).append(('div.cp-onboardscreen-maintitle', [ + h('h1.cp-onboardscreen-title', Messages.admin_onboardingNameTitle), + h('span', Messages.admin_onboardingNameHint) + ])); + var nav = blocks.nav([h('span'), button]); + + $(button).addClass('cp-onboardscreen-save'); + $(nav).addClass('cp-onboardscreen-nav'); + + var textForm = h('div.cp-instance-text-form', [ + titleBlock, + descriptionBlock, + colorBlock, + ]); + + var instanceForm = h('div.cp-instance-form', [logoBlock, textForm]); + + var form = blocks.form([ + screenTitle, + instanceForm + ], nav); + + $(form).addClass('cp-onboardscreen-form'); + + return form; + + }; + + const createAppsGrid = appsToDisable => { + const grid = blocks.block([], 'cp-admin-customize-apps-grid'); + const $grid = $(grid); + const allApps = PadTypes.appsToSelect; + + let select = function (app, $app) { + if (appsToDisable.indexOf(app) === -1) { + appsToDisable.push(app); + $app.toggleClass('cp-inactive-app', true); + $app.toggleClass('cp-active-app', false); + } else { + appsToDisable.splice(appsToDisable.indexOf(app), 1); + $app.toggleClass('cp-inactive-app', false); + $app.toggleClass('cp-active-app', true); + } + }; + + allApps.forEach(app => { + let name = Messages.type[app] || app; + let icon = UI.getNewIcon(app); + let appBlock = h('div.cp-appblock', + {tabindex:0, role:"button"}, + [ + icon, + h('span.cp-app-name', name), + h('i.fa.fa-check.cp-on-enabled') + ]); + let $app = $(appBlock).appendTo($grid); + if (appsToDisable.includes(app)) { + $app.addClass('cp-inactive-app'); + } else { + $app.addClass('cp-active-app'); + } + $app.on('click', () => select(app, $app)); + }); + + return grid; + }; + + const appConfig = function (Env) { + const appsToDisable = selections.appsToDisable; + const grid = createAppsGrid(appsToDisable); + + + var save = blocks.activeButton('primary', '', Messages.continue, function (done) { + gotoPage(Env, 2); + }); + + var prev = blocks.activeButton('secondary', '', Messages.form_backButton, function () { + gotoPage(Env, 0); + }); + + var screenTitle = h('div.cp-onboardscreen-screentitle'); + $(screenTitle).append(h('div.cp-onboardscreen-maintitle', h('h1.cp-onboardscreen-title', Messages.admin_appsTitle), h('span', Messages.admin_appsHint))); + $(save).addClass('cp-onboardscreen-save'); + $(prev).addClass('cp-onboardscreen-prev'); + var nav = blocks.nav([prev, save]); + $(nav).addClass('cp-onboardscreen-nav'); + let form = blocks.form([ + screenTitle, + grid + ], nav); + + $(form).addClass('cp-onboardscreen-form'); + + return form; + }; + + const mfaRegistrationScreen = function (Env) { + var restrict = blocks.activeCheckbox({ + key: 'registration', + getState: function () { + return selections.closeRegistration; + }, + label: 'registration', + query: function (val, setState) { + selections.closeRegistration = val; + setState(val) + }, + }); + + var forceMFA = blocks.activeCheckbox({ + key: 'forcemfa', + getState: function () { + return selections.mfa; + }, + label: 'forcemfa', + query: function (val, setState) { + selections.mfa = val; + setState(val) + }, + }); + + + let mfaOption = h('div.cp-optionblock', [ + forceMFA, + h('span.cp-option-hint', Messages.admin_forcemfaHint) + ]); + let registrationOption = h('div.cp-optionblock', [ + restrict, + h('span.cp-option-hint', Messages.admin_registrationHint) + ]); + const grid = blocks.block([ + mfaOption, + registrationOption + ], 'cp-admin-customize-options-grid'); + + var save = blocks.activeButton('primary', '', Messages.settings_save, function () { + + saveAndRedirect(Env); + }); + + var prev = blocks.activeButton('secondary', '', Messages.form_backButton, function () { + gotoPage(Env, 1); + }); + + var screenTitle = h('div.cp-onboardscreen-screentitle'); + $(screenTitle).append(h('div.cp-onboardscreen-maintitle', h('h1.cp-onboardscreen-title', Messages.admin_onboardingOptionsTitle), h('span', Messages.admin_onboardingOptionsHint))); + $(save).addClass('cp-onboardscreen-save'); + $(prev).addClass('cp-onboardscreen-prev'); + var nav = blocks.nav([prev, save]); + $(nav).addClass('cp-onboardscreen-nav'); + + var form = blocks.form([ + screenTitle, + grid], nav + ); + + $(form).addClass('cp-onboardscreen-form'); + + return form; + }; + + pages = [ + titleConfig, + appConfig, + mfaRegistrationScreen + ]; + const create = (sendAdminDecree, sendAdminRpc) => { + let Env = { + sendAdminDecree, + sendAdminRpc + }; + Env.overlay = $(h('div#cp-onboarding')); + gotoPage(Env, 0); + $('body').append(Env.overlay); + }; + + // XXX test functions to remove + window.CP_onboarding_test = () => { + create(() => {}, () => {}); + }; + window.CP_onboarding_test_full = () => { + require([ + '/common/cryptpad-common.js', + '/common/outer/local-store.js', + '/common/outer/login-block.js', + '/common/rpc.js', + '/common/common-constants.js', + '/common/cryptget.js', + ], function (CryptPad, LocalStore, Block, Rpc, Constants, CryptGet) { + var blockHash = LocalStore.getBlockHash(); + if (!blockHash) { return void console.error('NOT LOGGED IN'); } + var parsed = Block.parseBlockHash(blockHash); + var sessionToken = LocalStore.getSessionToken() || undefined; + let userHash; + nThen(w => { + // Get user keys + Util.getBlock(parsed.href, { + bearer: sessionToken + }, w((err, response) => { + if (err) { + w.abort(); + return void console.error('Please login in and try again'); + } + response.arrayBuffer().then(w(arraybuffer => { + arraybuffer = new Uint8Array(arraybuffer); + var block_info = Block.decrypt(arraybuffer, parsed.keys); + userHash = block_info[Constants.userHashKey]; + })); + })); + }).nThen(() => { + // Make RPC + if (!userHash) { return void console.error('AUTH FAILED'); } + CryptPad.makeNetwork((err, network) => { + if (err) { return void console.error(err); } + CryptGet.get(userHash, (err, val) => { + let p = Util.tryParse(val); + Rpc.create(network, p.edPrivate, p.edPublic, function (e, rpc) { + let sendAdminDecree = function (command, data, callback) { + var params = ['ADMIN_DECREE', [command, data]]; + rpc.send('ADMIN', params, callback); + }; + + let sendAdminRpc = function (command, data, callback) { + var params = [command, data]; + rpc.send('ADMIN', params, callback); + }; + + create(sendAdminDecree, sendAdminRpc); + }); + }, {network: network}); + }); + }); + }); + }; + return { create, createAppsGrid }; + +}); + diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index f871964da..ffe0a0da8 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -63,11 +63,13 @@ .kanban-colors(@kanban-colors; (@index - 1)); @color: extract(@kanban-colors, @index); // make a numbered class selector for each color - .cp-kanban-palette-color@{index}{ - background-color: @color !important; + .cp-palette-color@{index}{ &.cp-kanban-palette-board { background-color: @color !important; } + } + .cp-kanban-palette-color@{index}{ + background-color: @color !important; &.kanban-board-inner { background-color: fade(@color, 50%) !important; } @@ -81,10 +83,12 @@ // .cp-kanban-card-color@{index}{ // background-color: @color !important; // } - .cp-kanban-palette-color@{index}{ + .cp-palette-color@{index}{ &.cp-kanban-palette-card { background-color: @color !important; } + } + .cp-kanban-palette-color@{index}{ &.kanban-item { background-color: @color !important; } @@ -140,31 +144,7 @@ } margin-bottom: 15px; } - #cp-kanban-edit-colors { - display: flex; - justify-content: space-between; - .cp-kanban-palette { - display: inline-block; - border-radius: 50%; - height: 30px; - width: 30px; - text-align: center; - line-height: 30px; - color: @cp_kanban-fg; - border: 1px solid fade(@cp_kanban-fg, 40%); - outline: none; - // tick on selected color - &.cp-kanban-palette-card.fa-check { - color: @cryptpad_text_col; - } - &.cp-kanban-palette-board.fa-check { - color: @cryptpad_text_col_inv; - } - &:focus{ - outline: @cryptpad_color_brand solid 2px; - } - } - } + #cp-kanban-edit-tags { .tokenfield { margin: 0; diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 6b2519c95..5ac017b8d 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -185,6 +185,11 @@ define([ update(); }; + var colors = UIElements.makePalette(8, color => { + dataObject.color = color; + commit(); + }); + var conflicts, conflictContainer, titleInput, tagsDiv, colors, text; var content = h('div', [ conflictContainer = h('div#cp-kanban-edit-conflicts', [ @@ -200,7 +205,7 @@ define([ h('label', {for:'cp-kanban-edit-tags'}, Messages.fm_tagsName), tagsDiv = h('div#cp-kanban-edit-tags'), h('label', {for:'cp-kanban-edit-color'}, Messages.kanban_color), - colors = h('div#cp-kanban-edit-colors'), + colors, ]); var $tags = $(tagsDiv); @@ -362,11 +367,9 @@ define([ // Colors var $colors = $(colors); - var palette = ['']; - for (var i=1; i<=8; i++) { palette.push('color'+i); } var selectedColor = ''; var resetThemeClass = function () { - $colors.find('.cp-kanban-palette').each(function (i, el) { + $colors.find('.cp-palette-color').each(function (i, el) { var $c = $(el); $c.removeClass('cp-kanban-palette-card'); $c.removeClass('cp-kanban-palette-board'); @@ -377,38 +380,13 @@ define([ } }); }; - palette.forEach(function (color) { - var $color = $(h('button.cp-kanban-palette.fa')); - $color.addClass('cp-kanban-palette-'+(color || 'nocolor')); - $color.keydown(function (e) { - if (e.which === 13) { - e.stopPropagation(); - e.preventDefault(); - $color.click(); - } - }); - $color.click(function () { - if (offline) { return; } - if (color === selectedColor) { return; } - selectedColor = color; - $colors.find('.cp-kanban-palette').removeClass('fa-check'); - var $col = $colors.find('.cp-kanban-palette-'+(color || 'nocolor')); - $col.addClass('fa-check'); - - dataObject.color = color; - commit(); - }).appendTo($colors); - }); var color = { getValue: function () { - return selectedColor; + return colors.getValue(); }, setValue: function (color) { resetThemeClass(); - $colors.find('.cp-kanban-palette').removeClass('fa-check'); - var $col = $colors.find('.cp-kanban-palette-'+(color || 'nocolor')); - $col.addClass('fa-check'); - selectedColor = color; + colors.setValue(color); } }; @@ -460,6 +438,7 @@ define([ $modal.find('nav button.danger').prop('disabled', unlocked ? '' : 'disabled'); offline = !unlocked; + palette.disable(offline); }); diff --git a/www/moderation/inner.js b/www/moderation/inner.js index 4fd9fa99f..a93dc5e98 100644 --- a/www/moderation/inner.js +++ b/www/moderation/inner.js @@ -56,7 +56,7 @@ define([ var andThen = function (common, $container, linkedTicket) { const sidebar = Sidebar.create(common, 'support', $container); - const blocks = sidebar.blocks; + const blocks = sidebar.blocks(); APP.recorded = {}; APP.allTags = []; APP.openTicketCategory = Util.mkEvent();