From fee546ee9d35b0f6701b49f2c937e10b656a3411 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 9 Jul 2021 18:12:04 +0530 Subject: [PATCH 001/183] describe the premium support ticket policy on the support page --- www/support/app-support.less | 4 ++++ www/support/inner.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/www/support/app-support.less b/www/support/app-support.less index 0aa4c6b61..3d3066bcc 100644 --- a/www/support/app-support.less +++ b/www/support/app-support.less @@ -34,5 +34,9 @@ color: @cryptpad_color_link; text-decoration: underline; } + // add some whitespace to improve readability a bit + br { + margin: 5px; + } } diff --git a/www/support/inner.js b/www/support/inner.js index 6d9e5eae8..ebfe3bcfc 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -166,12 +166,29 @@ define([ return $div; }; + Messages.support_premiumPriority = "Premium users help support improvements to CryptPad's usability and benefit from prioritized responses to their support tickets."; // XXX + Messages.support_premiumLink = 'View subscription options.'; // XXX + // Create a new tickets create['form'] = function () { var key = 'form'; var $div = makeBlock(key, true); // Msg.support_formHint, .support_formTitle, .support_formButton Pages.documentationLink($div.find('a')[0], 'https://docs.cryptpad.fr/en/user_guide/index.html'); + var accountsLink = h('a', { + href: Pages.accounts.upgradeURL, + }, Messages.support_premiumLink,); + + var premium = h("div.alert.alert-info", [ + Messages.support_premiumPriority, + ' ', + accountsLink, + ]); + + if (Pages.areSubscriptionsAllowed()) { + $div.find('.cp-sidebarlayout-description').append(premium); + } + var form = APP.support.makeForm(); var id = Util.uid(); From d22c4a6d687c64ddfe31b8fbbf129dd57574cbf2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 9 Jul 2021 18:33:14 +0530 Subject: [PATCH 002/183] extra styles for support page enhancements --- www/support/app-support.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/support/app-support.less b/www/support/app-support.less index 3d3066bcc..d4719956c 100644 --- a/www/support/app-support.less +++ b/www/support/app-support.less @@ -34,6 +34,11 @@ color: @cryptpad_color_link; text-decoration: underline; } + .alert-info { + font-size: 16px; + margin-top: 15px; + margin-bottom: 15px; + } // add some whitespace to improve readability a bit br { margin: 5px; From 6a4ef02c413767330333298f3c5e434c7772a39a Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 08:13:06 +0530 Subject: [PATCH 003/183] remove some dead toolbar code --- www/common/toolbar.js | 24 ------------------------ www/file/inner.js | 1 - 2 files changed, 25 deletions(-) diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 47c0b578e..8bd0fcc32 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -53,7 +53,6 @@ MessengerUI, Messages, Pages) { var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown'; var USERNAME_CLS = Bar.constants.username = 'cp-toolbar-user-name'; /*var READONLY_CLS = */Bar.constants.readonly = 'cp-toolbar-readonly'; - var USERBUTTON_CLS = Bar.constants.changeUsername = "cp-toolbar-user-rename"; // Create the toolbar element @@ -1029,36 +1028,13 @@ MessengerUI, Messages, Pages) { var userMenuCfg = { $initBlock: $userAdmin, }; - if (!config.hideDisplayName) { - $.extend(true, userMenuCfg, { - displayNameCls: USERNAME_CLS, - changeNameButtonCls: USERBUTTON_CLS, - }); - } if (config.readOnly !== 1) { userMenuCfg.displayName = 1; userMenuCfg.displayChangeName = 1; } - /*if (config.displayed.indexOf('userlist') !== -1) { - userMenuCfg.displayChangeName = 0; - }*/ Common.createUserAdminMenu(userMenuCfg); $userAdmin.find('> button').attr('title', Messages.userAccountButton); - var $userButton = toolbar.$userNameButton = $userAdmin.find('a.' + USERBUTTON_CLS); - $userButton.click(function (e) { - e.preventDefault(); - e.stopPropagation(); - var myData = metadataMgr.getUserData(); - var lastName = myData.name; - UI.prompt(Messages.changeNamePrompt, lastName || '', function (newName) { - if (newName === null && typeof(lastName) === "string") { return; } - if (newName === null) { newName = ''; } - else { Feedback.send('NAME_CHANGED'); } - setDisplayName(newName); - }); - }); - return $userAdmin; }; diff --git a/www/file/inner.js b/www/file/inner.js index d69420f7b..2be2759db 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -62,7 +62,6 @@ define([ } var configTb = { displayed: displayed, - //hideDisplayName: true, $container: $bar, metadataMgr: metadataMgr, sfCommon: common, From 8d5221e6e8a047555b307b846a8f419a8f4b7a8b Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 09:56:48 +0530 Subject: [PATCH 004/183] include bar graphs for multi* form answers --- www/form/app-form.less | 29 ++++++++++++++++++---- www/form/inner.js | 55 +++++++----------------------------------- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 06b1c331e..781b8161d 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -532,8 +532,30 @@ background: @cp_form-bg2; &:not(:last-child) { margin-bottom: 1px; } } - .cp-form-results-type-radio { + + .cp-form-results-cell() { + border: 1px solid @cp_form-border; + display: table-cell; + padding: 5px 10px; + background: @cp_form-bg2; + } + + .cp-form-results-type-multiradio-data { + .cp-mr-q { + font-weight: bold; + padding: 5px 10px; + .cp-form-results-cell(); + } + &:not(:first-child) { + .cp-mr-q { + margin-top: 15px; + } + } + } + + .cp-form-results-type-radio { display: table; + width: 100%; .cp-form-results-type-multiradio-data { display: flex; flex-flow: column; @@ -542,10 +564,7 @@ display: table-row; border: 1px solid @cp_form-border; & > span { - border: 1px solid @cp_form-border; - display: table-cell; - padding: 5px 10px; - background: @cp_form-bg2; + .cp-form-results-cell(); &.cp-value { min-width: 200px; } diff --git a/www/form/inner.js b/www/form/inner.js index 089db8b57..ad89ada26 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1255,7 +1255,7 @@ define([ }; }, - printResults: function (answers, uid, form) { + printResults: function (answers, uid, form, content) { // results multiradio var structure = form[uid]; if (!structure) { return; } @@ -1263,6 +1263,7 @@ define([ var results = []; var empty = 0; var count = {}; + var showBars = Boolean(content); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1293,34 +1294,13 @@ define([ return h('div.cp-form-results-type-radio-data', [ h('span.cp-value', res), h('span.cp-count', itemCount), - //barGraphic((itemCount / max) * 100) + showBars? barGraphic((itemCount / max)): undefined, ]); }); results.push(h('div.cp-form-results-type-multiradio-data', [ h('span.cp-mr-q', q), h('span.cp-mr-value', values) ])); - return; -/* - var table = Charts.table([ - h('caption', { - style: 'color: var(--msg-color)', - }, q), - h('tbody', Object.keys(c).map(function (res) { - return Charts.row(res, c[res] / max, c[res]); - })), - ], [ - 'charts-css', - 'bar', - 'show-heading', - 'show-data-on-hover', - 'show-labels', - ]); - - results.push(h('div.cp-form-results-type-multiradio-data', { - style: 'width: 100%', - }, table)); -*/ }); results.push(getEmpty(empty)); @@ -1509,7 +1489,7 @@ define([ }; }, - printResults: function (answers, uid, form) { + printResults: function (answers, uid, form, content ) { // results multicheckbox var structure = form[uid]; if (!structure) { return; } @@ -1517,6 +1497,7 @@ define([ var results = []; var empty = 0; var count = {}; + var showBars = Boolean(content); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1544,35 +1525,17 @@ define([ var c = count[q_uid]; var values = Object.keys(c).map(function (res) { + var val = c[res]; return h('div.cp-form-results-type-radio-data', [ h('span.cp-value', res), - h('span.cp-count', c[res]) + h('span.cp-count', val), + showBars? barGraphic(val / max) : undefined, ]); }); results.push(h('div.cp-form-results-type-multiradio-data', [ h('span.cp-mr-q', q), - h('span.cp-mr-value', values) + h('span.cp-mr-value', values), ])); -/* - var table = Charts.table([ - h('caption', { - style: 'color: var(--msg-color)', - }, q), - h('tbody', Object.keys(c).map(function (res) { - return Charts.row(res, c[res] / max, c[res]); - })), - ], [ - 'charts-css', - 'bar', - 'show-heading', - 'show-data-on-hover', - 'show-labels', - ]); - - results.push(h('div.cp-form-results-type-multiradio-data', { - style: 'width: 100%', - }, table)); -*/ }); results.push(getEmpty(empty)); From 8215bf1c495ed1f4b2a9aa56c95f33797801cbef Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 09:58:03 +0530 Subject: [PATCH 005/183] add draft changelog update --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f86b192..185a9ae29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 4.11.0 (WIP) + +## Goals + +## Update notes + +## Features + +* unify unregistered/non-registered/anonymous terminology as 'guest' +* prompt users that need support to subscribe +* include bar graphs for multiple-answer form questions + +## Bug fixes + + # 4.10.0 ## Goals From 16f59792528db01b898efc1bf0b9575e8d3a84ef Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 11:09:14 +0530 Subject: [PATCH 006/183] present premium support notice as a banner --- www/support/inner.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/www/support/inner.js b/www/support/inner.js index ebfe3bcfc..a193a2f77 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -45,6 +45,7 @@ define([ 'cp-support-list', ], 'new': [ // Msg.support_cat_new + 'cp-support-subscribe', 'cp-support-language', 'cp-support-form', ], @@ -166,8 +167,25 @@ define([ return $div; }; - Messages.support_premiumPriority = "Premium users help support improvements to CryptPad's usability and benefit from prioritized responses to their support tickets."; // XXX - Messages.support_premiumLink = 'View subscription options.'; // XXX + create['subscribe'] = function () { + if (!Pages.areSubscriptionsAllowed()) { return; } + var url = Pages.accounts.upgradeURL; + var accountsLink = h('a', { + href: url, + }, Messages.support_premiumLink); + $(accountsLink).click(function (ev) { + ev.preventDefault(); + common.openURL(url); + }); + + return $(h('div.cp-support-subscribe.cp-sidebarlayout-element', [ + h('div.alert.alert-info', [ + Messages.support_premiumPriority, + ' ', + accountsLink, + ]), + ])); + }; // Create a new tickets create['form'] = function () { @@ -175,20 +193,6 @@ define([ var $div = makeBlock(key, true); // Msg.support_formHint, .support_formTitle, .support_formButton Pages.documentationLink($div.find('a')[0], 'https://docs.cryptpad.fr/en/user_guide/index.html'); - var accountsLink = h('a', { - href: Pages.accounts.upgradeURL, - }, Messages.support_premiumLink,); - - var premium = h("div.alert.alert-info", [ - Messages.support_premiumPriority, - ' ', - accountsLink, - ]); - - if (Pages.areSubscriptionsAllowed()) { - $div.find('.cp-sidebarlayout-description').append(premium); - } - var form = APP.support.makeForm(); var id = Util.uid(); From cf7593553bbe8ed980611c29830cb1c44e1d4001 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 14:46:56 +0530 Subject: [PATCH 007/183] polish and re-enable form response messages --- www/form/app-form.less | 9 ++++++++- www/form/inner.js | 24 +++++++++++++++++------- www/form/main.js | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 781b8161d..53acb92af 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -440,7 +440,6 @@ } } .cp-form-edit-block { - button.btn-secondary { margin-left: 30px; margin-bottom: 5px; @@ -487,6 +486,9 @@ p:last-child { margin-bottom: 0; } + * { + max-width: 100%; + } } .cp-form-creator-results-controls { @@ -832,6 +834,11 @@ color: @cp_form-poll-yes-color; } } + .cp-form-response-modal { + .CodeMirror { + border: 1px solid @cp_forms-border; + } + } .charts_main(); } diff --git a/www/form/inner.js b/www/form/inner.js index ad89ada26..ec329830f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2702,7 +2702,6 @@ define([ var responseMsg = h('div.cp-form-response-msg-container'); var $responseMsg = $(responseMsg); var refreshResponse = function () { - if (true) { return; } // XXX 4.11.0 $responseMsg.empty(); Messages.form_updateMsg = "Update response message"; // XXX 4.11.0 Messages.form_addMsg = "Add response message"; // XXX 4.11.0 @@ -2713,12 +2712,23 @@ define([ var editor; if (!APP.responseModal) { var t = h('textarea'); + var p = h('p', Messages.form_responseMsg); var div = h('div', [ - h('p', Messages.form_responseMsg), - t + p, + h('div.cp-form-response-modal', t), ]); - var cm = SFCodeMirror.create("gfm", CMeditor, t); + var cm = window.my_cm = SFCodeMirror.create("gfm", CMeditor, t); editor = APP.responseEditor = cm.editor; + var markdownTb = APP.common.createMarkdownToolbar(editor, { + embed: function (mt) { + editor.focus(); + editor.replaceSelection($(mt)[0].outerHTML); + } + }); + $(markdownTb.toolbar).insertAfter($(p)); + $(markdownTb.toolbar).show(); + + cm.configureTheme(APP.common, function () {}); editor.setOption('lineNumbers', true); editor.setOption('lineWrapping', true); editor.setOption('styleActiveLine', true); @@ -2735,7 +2745,7 @@ define([ name: Messages.settings_save, onClick: function () { var v = editor.getValue(); - content.answers.msg = v.trim(0, 2000); // XXX 4.11.0 max length? + content.answers.msg = v.slice(0, 2000); // XXX 4.11.0 max length? framework.localChange(); framework._.cpNfInner.chainpad.onSettle(function () { UI.log(Messages.saved); @@ -2761,9 +2771,9 @@ define([ } UI.openCustomModal(APP.responseModal); }); - // $responseMsg.append(btn); // XXX 4.11.0 + $responseMsg.append(btn); }; - //refreshResponse(); + refreshResponse(); // Allow anonymous answers var privacyContainer = h('div.cp-form-privacy-container'); diff --git a/www/form/main.js b/www/form/main.js index b82397caf..9a017fbe1 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -176,7 +176,7 @@ define([ validateKey: keys.secondaryValidateKey, owners: [myKeys.edPublic], crypto: crypto, - //Cache: Utils.Cache // XXX 4.11.0 + //Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers }; var results = {}; config.onError = function (info) { From fe256e8282fc982125a9c3691c03888bdcec1627 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 14:55:31 +0530 Subject: [PATCH 008/183] give mobile users a way to escape from the error screen (aside from ESC) --- www/common/sframe-common.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index c05bdfe3f..206137c86 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -921,9 +921,10 @@ define([ }); ctx.sframeChan.on('EV_WORKER_TIMEOUT', function () { - UI.errorLoadingScreen(Messages.timeoutError, false, function () { // XXX 4.11.0 mobile users can't necessarily hit 'ESC' as this message suggests. provice a click option - funcs.gotoURL(''); - }); + var message = UI.setHTML(h('span'), Messages.timeoutError); + var cb = Util.once(function () { funcs.gotoURL(''); }); + $(message).find('em').on('touchend', cb); + UI.errorLoadingScreen(message, false, cb); }); ctx.sframeChan.on('EV_CHROME_68', function () { From 15b935eadc765debd7afebaef512b3005d662854 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 15:07:19 +0530 Subject: [PATCH 009/183] change an XXX to a FIXME --- www/common/outer/cache-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/outer/cache-store.js b/www/common/outer/cache-store.js index d1c3795c0..17fea6b16 100644 --- a/www/common/outer/cache-store.js +++ b/www/common/outer/cache-store.js @@ -97,7 +97,7 @@ define([ var checkCheckpoints = function (array) { if (!Array.isArray(array)) { return; } // Keep the last 100 messages - if (array.length > 100) { // XXX 4.11.0 + if (array.length > 100) { // FIXME this behaviour is only valid for chainpad-style documents array.splice(0, array.length - 100); } // Remove every message before the first checkpoint From aa84b625c7cb3363ac38c09ce79c0bc6898e1c19 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 19 Aug 2021 12:56:44 +0200 Subject: [PATCH 010/183] Improve forms participant view --- www/common/sframe-common-outer.js | 17 +++++++++++++++ www/common/toolbar.js | 4 +++- www/form/app-form.less | 20 +++++++++++++++++ www/form/inner.js | 36 +++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 575971368..5223898d1 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -618,6 +618,7 @@ define([ prefersDriveRedirect: Utils.LocalStore.getDriveRedirectPreference(), isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, + canEdit: hashes && hashes.editHash, oldVersionHash: parsed.hashData && parsed.hashData.version < 2, // password isHistoryVersion: parsed.hashData && parsed.hashData.versionHash, notifications: notifs, @@ -1749,6 +1750,22 @@ define([ }); }); + sframeChan.on('Q_COPY_VIEW_URL', function (data, cb) { + require(['/common/clipboard.js'], function (Clipboard) { + var url = window.location.origin + + Utils.Hash.hashToHref(hashes.viewHash, 'form'); + var success = Clipboard.copy(url); + cb(success); + }); + }); + sframeChan.on('EV_OPEN_VIEW_URL', function () { + var url = Utils.Hash.hashToHref(hashes.viewHash, 'form'); + var a = window.open(url); + if (!a) { + sframeChan.event('EV_POPUP_BLOCKED'); + } + }); + if (cfg.messaging) { sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) { Cryptpad.universal.execCommand({ diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 8bd0fcc32..31a40ffeb 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -64,7 +64,9 @@ MessengerUI, Messages, Pages) { if (!config.$container) { return; } var $container = config.$container; - var isEmbed = Bar.isEmbed = config.metadataMgr.getPrivateData().isEmbed; + var priv = config.metadataMgr.getPrivateData(); + var isEmbed = Bar.isEmbed = priv.isEmbed || + (priv.app === 'form' && priv.readOnly && !priv.form_auditorHash); if (isEmbed) { $container.hide(); } diff --git a/www/form/app-form.less b/www/form/app-form.less index 53acb92af..340cd8878 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -78,6 +78,26 @@ display: flex; } + .cp-form-view-title { + margin-bottom: 20px; + } + div.cp-form-view-logo { + align-items: center; + justify-content: center; + max-height: 140px; + font-size: 50px; + display: flex; + font-family: "IBM Plex Mono"; + .tools_unselectable(); + color: @cp_sidebar-hint; + padding: 20px; + cursor: pointer; + img { + max-height: 100%; + margin-right: 20px; + } + } + div.cp-form-creator-container { display: flex; flex: 1; diff --git a/www/form/inner.js b/www/form/inner.js index ec329830f..b829919ec 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1,6 +1,7 @@ define([ 'jquery', 'json.sortify', + '/api/config', '/bower_components/chainpad-crypto/crypto.js', '/common/sframe-app-framework.js', '/common/toolbar.js', @@ -42,6 +43,7 @@ define([ ], function ( $, Sortify, + ApiConfig, Crypto, Framework, Toolbar, @@ -2608,7 +2610,24 @@ define([ } // In view mode, add "Submit" and "reset" buttons + // Embed mode is enforced so we add the title at the top and a CryptPad logo + // at the bottom + var title = framework._.title.title || framework._.title.defaultTitle; + $container.prepend(h('h1.cp-form-view-title', title)); + $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); + + var logo = h('div.cp-form-view-logo', [ + h('img', { + src:'/customize/CryptPad_logo_grey.svg?'+ApiConfig.requireConf.urlArgs, + alt:'CryptPad_logo' + }), + h('span', 'CryptPad') + ]); + $(logo).click(function () { + framework._.sfCommon.gotoURL('/drive/'); + }); + $container.append(logo); if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); } @@ -2672,6 +2691,22 @@ define([ } var makeFormSettings = function () { + Messages.form_preview = "Preview participant page"; // XXX + Messages.form_geturl = "Copy participant link"; // XXX + var previewBtn = h('button.btn.btn-primary', Messages.form_preview); + var participantBtn = h('button.btn.btn-primary', Messages.form_geturl); + var preview = h('div.cp-forms-results-participant', [previewBtn, participantBtn]); + $(previewBtn).click(function () { + sframeChan.event('EV_OPEN_VIEW_URL'); + }); + $(participantBtn).click(function () { + sframeChan.query('Q_COPY_VIEW_URL', null, function (err, success) { + if (success) { return void UI.log(Messages.shareSuccess); } + UI.warn(Messages.error); + }); + }); + + // Private / public status var resultsType = h('div.cp-form-results-type-container'); var $results = $(resultsType); @@ -2873,6 +2908,7 @@ define([ //evOnChange.reg(refreshResponse); return [ + preview, endDateContainer, privacyContainer, resultsType, From e414524eab6bfc3f06ed3cd705d67f01c9880d27 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 16:17:59 +0530 Subject: [PATCH 011/183] refactor form results display * improve table component reusability for the rest of the platform * display count of empty results at the top of each section * remove unused styles * fix incorrect methods for counting empty answers for multi-checkbox questions --- customize.dist/src/less2/include/charts.less | 52 ++++++++ www/form/app-form.less | 64 +--------- www/form/inner.js | 120 ++++++++++--------- 3 files changed, 117 insertions(+), 119 deletions(-) diff --git a/customize.dist/src/less2/include/charts.less b/customize.dist/src/less2/include/charts.less index c8b983707..ea402094f 100644 --- a/customize.dist/src/less2/include/charts.less +++ b/customize.dist/src/less2/include/charts.less @@ -50,4 +50,56 @@ } } } + + .cp-charts-cell { + border: 1px solid @cp_form-border; + display: table-cell; + padding: 5px 10px; + background: @cp_form-bg2; + } + + .cp-form-results-type-radio { + .cp-form-results-type-multiradio-data { + display: flex; + flex-flow: column; + } + .cp-form-results-type-radio-data { + display: table-row; + border: 1px solid @cp_form-border; + & > span { + .cp-charts-cell(); + } + } + } + + .cp-charts.cp-bar-table, .cp-charts.cp-text-table { + display: table; + width: 100%; + .cp-charts-row { + display: table-row; + border: 1px solid @cp_form-border; + &.full { + display: flex; + flex-flow: column; + } + + & > span { + .cp-charts-cell(); + display: table-cell; + &.cp-value { + min-width: 200px; + } + &.cp-bar-container { + width: 99%; + padding: 0px; + position: relative; + .cp-bar { + position: absolute; + background: @cryptpad_color_brand; + height: 100%; + } + } + } + } + } } diff --git a/www/form/app-form.less b/www/form/app-form.less index 53acb92af..5ef211201 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -519,69 +519,9 @@ i { margin-right: 5px; } background: fade(@cryptpad_text_col, 15%); } - .cp-form-results-type-text { - max-height: 300px; + .cp-form-results-contained { + max-height: 350px; // enough for 10 table entries overflow: auto; - .cp-form-results-type-text-data { - padding: 5px 10px; - background: @cp_form-bg2; - &:not(:last-child) { margin-bottom: 1px; } - } - } - .cp-form-results-type-textarea-data { - white-space: pre-wrap; - padding: 5px 10px; - background: @cp_form-bg2; - &:not(:last-child) { margin-bottom: 1px; } - } - - .cp-form-results-cell() { - border: 1px solid @cp_form-border; - display: table-cell; - padding: 5px 10px; - background: @cp_form-bg2; - } - - .cp-form-results-type-multiradio-data { - .cp-mr-q { - font-weight: bold; - padding: 5px 10px; - .cp-form-results-cell(); - } - &:not(:first-child) { - .cp-mr-q { - margin-top: 15px; - } - } - } - - .cp-form-results-type-radio { - display: table; - width: 100%; - .cp-form-results-type-multiradio-data { - display: flex; - flex-flow: column; - } - .cp-form-results-type-radio-data { - display: table-row; - border: 1px solid @cp_form-border; - & > span { - .cp-form-results-cell(); - &.cp-value { - min-width: 200px; - } - &.cp-bar-container { - width: 99%; - padding: 0px; - position: relative; - .cp-bar { - position: absolute; - background: @cryptpad_color_brand; - height: 100%; - } - } - } - } } .cp-form-individual { background: @cp_form-bg1; diff --git a/www/form/inner.js b/www/form/inner.js index ec329830f..cb85ae29c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -803,10 +803,31 @@ define([ return total; }; - var getEmpty = function (empty) { // TODO don't include this in the scrollable area - if (empty) { - return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); - } + var multiAnswerSubHeading = function (content) { + return h('span.cp-charts-row', h('td.cp-charts-cell', { + colspan: 3, + style: 'font-weight: bold;', + }, content)); + }; + + var getEmpty = function (empty) { + if (!empty) { return; } + var msg = UI.setHTML(h('span.cp-form-results-empty-text'), Messages._getKey('form_notAnswered', [empty])); + return multiAnswerSubHeading(msg); + }; + + var barGraphic = function (itemScale) { + return h('span.cp-bar-container', h('div.cp-bar', { + style: 'width: ' + (itemScale * 100) + '%', + }, ' ')); + }; + + var barRow = function (value, count, max, showBar) { + return h('div.cp-charts-row', [ + h('span.cp-value', value), + h('span.cp-count', count), + showBar? barGraphic((count / max)): undefined, + ]); }; var findItem = function (items, uid) { @@ -957,27 +978,21 @@ define([ return Array.isArray(A)? Math.max.apply(null, A): NaN; }; - var barGraphic = function (itemScale) { - return h('span.cp-bar-container', h('div.cp-bar', { - style: 'width: ' + (itemScale * 100) + '%', - }, ' ')); - }; - var renderTally = function (tally, empty, showBar) { var rows = []; var counts = Util.values(tally); var max = arrayMax(counts); + if (empty) { rows.push(getEmpty(empty)); } Object.keys(tally).forEach(function (value) { var itemCount = tally[value]; var itemScale = (itemCount / max); - rows.push(h('div.cp-form-results-type-radio-data', [ + rows.push(h('div.cp-charts-row', [ h('span.cp-value', value), h('span.cp-count', itemCount), showBar? barGraphic(itemScale): undefined, ])); }); - if (empty) { rows.push(getEmpty(empty)); } return rows; }; @@ -1015,34 +1030,35 @@ define([ reset: function () { $tag.val(''); } }; }, - printResults: function (answers, uid) { + printResults: function (answers, uid) { // results text var results = []; var empty = 0; var tally = {}; + var isEmpty = function (answer) { + console.error("EMPTY?", JSON.stringify(answer)); + return !answer || !answer.trim(); + }; + Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !answer.trim()) { return empty++; } + if (isEmpty(answer)) { return empty++; } Util.inc(tally, answer); }); //var counts = Util.values(tally); //var max = arrayMax(counts); //if (max < 2) { // there are no duplicates, so just return text + results.push(getEmpty(empty)); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; if (!answer || !answer.trim()) { return empty++; } - results.push(h('div.cp-form-results-type-text-data', answer)); + results.push(h('div.cp-charts-row', h('span.cp-value', answer))); }); - results.push(getEmpty(empty)); - return h('div.cp-form-results-type-text', results); + return h('div.cp-form-results-contained', h('div.cp-charts.cp-text-table', results)); //} -/* - var rendered = renderTally(tally, empty); - return h('div.cp-form-results-type-text', rendered); -*/ }, icon: h('i.cptools.cptools-form-text') }, @@ -1104,11 +1120,11 @@ define([ var obj = answers[author]; var answer = obj.msg[uid]; if (!answer || !answer.trim()) { return empty++; } - results.push(h('div.cp-form-results-type-textarea-data', answer)); + results.push(h('div.cp-charts-row', h('span.cp-value', answer))); }); - results.push(getEmpty(empty)); + results.unshift(getEmpty(empty)); - return h('div.cp-form-results-type-text', results); + return h('div.cp-form-results-contained', h('div.cp-charts.cp-text-table', results)); }, icon: h('i.cptools.cptools-form-paragraph') }, @@ -1179,7 +1195,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-radio') }, @@ -1285,26 +1301,17 @@ define([ max = arrayMax(counts); }); + results.push(getEmpty(empty)); count_keys.forEach(function (q_uid) { var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - var values = Object.keys(c).map(function (res) { - var itemCount = c[res]; - return h('div.cp-form-results-type-radio-data', [ - h('span.cp-value', res), - h('span.cp-count', itemCount), - showBars? barGraphic((itemCount / max)): undefined, - ]); + results.push(multiAnswerSubHeading(q)); + Object.keys(c).forEach(function (res) { + results.push(barRow(res, c[res], max, showBars)); }); - results.push(h('div.cp-form-results-type-multiradio-data', [ - h('span.cp-mr-q', q), - h('span.cp-mr-value', values) - ])); }); - results.push(getEmpty(empty)); - return h('div.cp-form-results-type-radio', results); + return h('div.cp-charts.cp-bar-table', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1401,7 +1408,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-check') }, @@ -1498,10 +1505,19 @@ define([ var empty = 0; var count = {}; var showBars = Boolean(content); + + var isEmpty = function (answer) { + if (!answer) { return true; } + return !Object.keys(answer).some(function (k) { + var A = answer[k]; + return Array.isArray(A) && A.length; + }); + }; + Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !Object.keys(answer).length) { return empty++; } + if (isEmpty(answer)) { return empty++; } Object.keys(answer).forEach(function (q_uid) { var c = count[q_uid] = count[q_uid] || {}; var res = answer[q_uid]; @@ -1520,26 +1536,17 @@ define([ max = arrayMax(counts); }); + results.push(getEmpty(empty)); count_keys.forEach(function (q_uid) { var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - var values = Object.keys(c).map(function (res) { - var val = c[res]; - return h('div.cp-form-results-type-radio-data', [ - h('span.cp-value', res), - h('span.cp-count', val), - showBars? barGraphic(val / max) : undefined, - ]); + results.push(multiAnswerSubHeading(q)); + Object.keys(c).forEach(function (res) { + results.push(barRow(res, c[res], max, showBars)); }); - results.push(h('div.cp-form-results-type-multiradio-data', [ - h('span.cp-mr-q', q), - h('span.cp-mr-value', values), - ])); }); - results.push(getEmpty(empty)); - return h('div.cp-form-results-type-radio', results); + return h('div.cp-charts.cp-bar-table', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1658,7 +1665,6 @@ define([ // results sort var opts = form[uid].opts || TYPES.sort.defaultOpts; var l = (opts.values || []).length; - //var results = []; var empty = 0; var count = {}; var showBars = Boolean(content); @@ -1673,7 +1679,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-ordered') }, From f537d8f65864ffb552c872ffbeb54a25e1226717 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 19 Aug 2021 14:44:14 +0200 Subject: [PATCH 012/183] Improve form editor UX --- www/form/app-form.less | 8 ++++++ www/form/inner.js | 64 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 340cd8878..a0d0bdd65 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -313,6 +313,14 @@ margin-bottom: 20px; } + .cp-form-preview { + color: @cp_sidebar-hint; + margin-bottom: 10px; + padding: 0; + text-align: center; + font-weight: bold; + } + .cp-form-block-drag-handle { display: flex; flex-flow: column; diff --git a/www/form/inner.js b/www/form/inner.js index b829919ec..bf2228e7b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1115,6 +1115,7 @@ define([ icon: h('i.cptools.cptools-form-paragraph') }, radio: { + compatible: ['radio', 'checkbox', 'sort'], defaultOpts: { values: [1,2].map(function (i) { return Messages._getKey('form_defaultOption', [i]); @@ -1186,6 +1187,7 @@ define([ icon: h('i.cptools.cptools-form-list-radio') }, multiradio: { + compatible: ['multiradio', 'multicheck'], defaultOpts: { items: [1,2].map(function (i) { return { @@ -1325,6 +1327,7 @@ define([ icon: h('i.cptools.cptools-form-grid-radio') }, checkbox: { + compatible: ['radio', 'checkbox', 'sort'], defaultOpts: { max: 3, values: [1, 2, 3].map(function (i) { @@ -1341,6 +1344,7 @@ define([ $(cbox).find('input').data('val', data); return cbox; }); + if (!opts.max) { opts.max = TYPES.checkbox.defaultOpts.max; } var tag = h('div', [ h('div.cp-form-max-options', Messages._getKey('form_maxOptions', [opts.max])), h('div.radio-group.cp-form-type-checkbox', els) @@ -1408,6 +1412,7 @@ define([ icon: h('i.cptools.cptools-form-list-check') }, multicheck: { + compatible: ['multiradio', 'multicheck'], defaultOpts: { max: 3, items: [1,2].map(function (i) { @@ -1437,6 +1442,8 @@ define([ return h('div.radio-group', {'data-uid':name}, els); }); + if (!opts.max) { opts.max = TYPES.multicheck.defaultOpts.max; } + lines.forEach(function (l) { $(l).find('input').on('change', function () { var selected = $(l).find('input:checked').length; @@ -1560,6 +1567,7 @@ define([ icon: h('i.cptools.cptools-form-grid-check') }, sort: { + compatible: ['radio', 'checkbox', 'sort'], defaultOpts: { values: [1,2].map(function (i) { return Messages._getKey('form_defaultOption', [i]); @@ -2389,6 +2397,9 @@ define([ APP.formBlocks.push(data); + Messages.form_preview = "Preview:"; // XXX + var previewDiv = h('div.cp-form-preview', Messages.form_preview); + if (editable) { // Drag handle dragHandle = h('span.cp-form-block-drag-handle', [ @@ -2446,6 +2457,7 @@ define([ // Delete question var edit = h('span'); + var changeType; var del = h('button.btn.btn-danger-alt', [ h('i.fa.fa-trash-o'), h('span', Messages.form_delete) @@ -2475,6 +2487,7 @@ define([ $(editContainer).empty(); $(editButtons).show(); $(data.tag).show(); + $(previewDiv).show(); return; } $(editContainer).empty(); @@ -2492,6 +2505,7 @@ define([ }; var onEdit = function (tmp) { data.editing = true; + $(previewDiv).hide(); $(data.tag).hide(); $(editContainer).append(data.edit(onSave, tmp, framework)); $(editButtons).hide(); @@ -2506,10 +2520,57 @@ define([ onEdit(temp[uid]); }); } + + Messages.form_changeType = "Change type"; // XXX + Messages.form_changeTypeConfirm = "Select the new type of this question and click OK."; // XXX + if (Array.isArray(model.compatible)) { + changeType = h('button.btn.btn-secondary', [ + h('i.fa.fa-question'), + h('span', Messages.form_changeType) + ]); + $(changeType).click(function () { + var name = Util.uid(); + var els = model.compatible.map(function (data, i) { + var text = Messages['form_type_'+data]; + if (!text) { return; } + var radio = UI.createRadio(name, 'cp-form-changetype-'+i, + text, data===type, { mark: { tabindex:1 } }); + $(radio).find('input').data('val', data); + return radio; + }); + var tag = h('div.radio-group', els); + var changeTypeContent = [ + h('p', Messages.form_changeTypeConfirm), + tag + ]; + UI.confirm(changeTypeContent, function (yes) { + if (!yes) { return; } + var res; + els.some(function (el) { + var $i = $(el).find('input'); + if (Util.isChecked($i)) { + res = $i.data('val'); + return true; + } + }); + if (res === type || !TYPES[res]) { return; } + model = TYPES[res]; + type = res; + if (!data) { data = {}; } + block.type = res; + framework.localChange(); + var $oldTag = $(data.tag); + framework._.cpNfInner.chainpad.onSettle(function () { + data = model.get(block.opts, _answers, null, evOnChange); + $oldTag.before(data.tag).remove(); + }); + }); + }); + } } editButtons = h('div.cp-form-edit-buttons-container', [ - edit, del + edit, changeType, del ]); } var editableCls = editable ? ".editable" : ""; @@ -2519,6 +2580,7 @@ define([ APP.isEditor ? dragHandle : undefined, isStatic ? undefined : q, h('div.cp-form-block-content', [ + isStatic || !APP.isEditor ? undefined : previewDiv, data.tag, editButtons ]), From bfdcf4ec0ce38f9e2930a8d988cc038a4f6f4df9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 18:20:14 +0530 Subject: [PATCH 013/183] fix user/display name rendering which I accidentally broke in the user admin menu --- www/common/toolbar.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 8bd0fcc32..d103fe410 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -53,6 +53,7 @@ MessengerUI, Messages, Pages) { var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown'; var USERNAME_CLS = Bar.constants.username = 'cp-toolbar-user-name'; /*var READONLY_CLS = */Bar.constants.readonly = 'cp-toolbar-readonly'; + var USERBUTTON_CLS = Bar.constants.changeUsername = "cp-toolbar-user-rename"; // Create the toolbar element @@ -1028,6 +1029,12 @@ MessengerUI, Messages, Pages) { var userMenuCfg = { $initBlock: $userAdmin, }; + if (!config.hideDisplayName) { + $.extend(true, userMenuCfg, { + displayNameCls: USERNAME_CLS, + changeNameButtonCls: USERBUTTON_CLS, + }); + } if (config.readOnly !== 1) { userMenuCfg.displayName = 1; userMenuCfg.displayChangeName = 1; From eafe27ffb46a00fa16e4ea9369c0b51e58b30dee Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 18:21:48 +0530 Subject: [PATCH 014/183] reuse cp-charts to visualize server task running time as bar charts --- www/admin/app-admin.less | 24 +++++++++++++++++++ www/admin/inner.js | 52 +++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index 227792540..f66eb75f7 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -1,12 +1,14 @@ @import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less'; @import (reference) '../../customize/src/less2/include/support.less'; +@import (reference) '../../customize/src/less2/include/charts.less'; &.cp-app-admin { .framework_min_main(); .sidebar-layout_main(); .support_main(); + .charts_main(); .cp-hidden { display: none !important; @@ -294,5 +296,27 @@ } } } + span.cp-bar.profiling-percentage { + text-align: center; + padding: 5px; + } + span.profiling-label { + position: absolute; + z-index: 1; + width: 100%; + text-align: center; + padding: 5px; + } + #profiling-chart { + .cp-bar-container { + max-width: 400px; + } + } + .width-constrained { + max-width: 800px; + } + .cp-charts-row.heading { + font-weight: bold; + } } diff --git a/www/admin/inner.js b/www/admin/inner.js index e8d567d41..63c516614 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -1673,34 +1673,51 @@ define([ var $div = makeBlock('performance-profiling'); // Msg.admin_performanceProfilingHint, .admin_performanceProfilingTitle var onRefresh = function () { - var body = h('tbody'); + var createBody = function () { + return h('div#profiling-chart.cp-charts.cp-bar-table', [ + h('span.cp-charts-row.heading', [ + h('span', Messages.admin_performanceKeyHeading), + h('span', Messages.admin_performanceTimeHeading), + h('span', Messages.admin_performancePercentHeading), + //h('span', ''), //Messages.admin_performancePercentHeading), + ]), + ]); + }; - var table = h('table#cp-performance-table', [ - h('thead', [ - h('th', Messages.admin_performanceKeyHeading), - h('th', Messages.admin_performanceTimeHeading), - h('th', Messages.admin_performancePercentHeading), - ]), - body, - ]); - var appendRow = function (key, time, percent) { - console.log("[%s] %ss running time (%s%)", key, time, percent); - body.appendChild(h('tr', [ key, time, percent ].map(function (x) { - return h('td', x); - }))); + var body = createBody(); + var appendRow = function (key, time, percent, scaled) { + //console.log("[%s] %ss running time (%s%)", key, time, percent); + body.appendChild(h('span.cp-charts-row', [ + h('span', key), + h('span', time), + //h('span', percent), + h('span.cp-bar-container', [ + h('span.cp-bar.profiling-percentage', { + style: 'width: ' + scaled + '%', + }, ' ' ), + h('span.profiling-label', percent + '%'), + ]), + ])); }; var process = function (_o) { + $('#profiling-chart').remove(); + body = createBody(); var o = _o[0]; var sorted = Object.keys(o).sort(function (a, b) { if (o[b] - o[a] <= 0) { return -1; } return 1; }); + + var values = sorted.map(function (k) { return o[k]; }); var total = 0; - sorted.forEach(function (k) { total += o[k]; }); + values.forEach(function (value) { total += value; }); + var max = Math.max.apply(null, values); + sorted.forEach(function (k) { var percent = Math.floor((o[k] / total) * 1000) / 10; - appendRow(k, o[k], percent); + appendRow(k, o[k], percent, (o[k] / max) * 100); }); + $div.append(h('div.width-constrained', body)); }; sFrameChan.query('Q_ADMIN_RPC', { @@ -1710,10 +1727,7 @@ define([ UI.warn(Messages.error); return void console.error(e, data); } - //console.info(data); - $div.find("table").remove(); process(data); - $div.append(table); }); }; From 7bbe9059a1be0acbc9d727494e9a23dda16ccd04 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 19 Aug 2021 15:23:49 +0200 Subject: [PATCH 015/183] Tell users when a form has already been submitted --- www/form/inner.js | 22 ++++++++++++++++++---- www/form/main.js | 6 ++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index bf2228e7b..2f717cb05 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1388,6 +1388,10 @@ define([ $el.prop('checked', true); } }); + var selected = $tag.find('input:checked').length; + if (selected >= opts.max) { + $tag.find('input:not(:checked)').attr('disabled', 'disabled'); + } } }; @@ -2671,14 +2675,22 @@ define([ return; } + // If the form is already submitted, show an info message + Messages.form_alreadyAnswered = "You've submitted answers to this form on {0}"; // XXX + if (answers) { + $container.prepend(h('div.alert.alert-info', + Messages._getKey('form_alreadyAnswered', [ + new Date(answers._time).toLocaleString()]))); + // XXX make the page read-only? + } + // In view mode, add "Submit" and "reset" buttons + $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); + // Embed mode is enforced so we add the title at the top and a CryptPad logo // at the bottom var title = framework._.title.title || framework._.title.defaultTitle; $container.prepend(h('h1.cp-form-view-title', title)); - - $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); - var logo = h('div.cp-form-view-logo', [ h('img', { src:'/customize/CryptPad_logo_grey.svg?'+ApiConfig.requireConf.urlArgs, @@ -2690,6 +2702,7 @@ define([ framework._.sfCommon.gotoURL('/drive/'); }); $container.append(logo); + if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); } @@ -3156,7 +3169,7 @@ define([ } // If the results are public and there is at least one doodle, fetch the results now - if (content.answers.privateKey && Object.keys(content.form).some(function (uid) { + if (0 && content.answers.privateKey && Object.keys(content.form).some(function (uid) { return content.form[uid].type === "poll"; })) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { @@ -3186,6 +3199,7 @@ define([ var myAnswersObj = answers[curve1] || answers[curve2] || undefined; if (myAnswersObj) { myAnswers = myAnswersObj.msg; + myAnswers._time = myAnswersObj.time; } } // If we have a non-anon answer, we can't answer anonymously later diff --git a/www/form/main.js b/www/form/main.js index 9a017fbe1..4c0831fe9 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -272,8 +272,10 @@ define([ my_private: Nacl.util.decodeBase64(myKeys.curvePrivate), their_public: Nacl.util.decodeBase64(data.publicKey) }); - res.content._isAnon = answer.anonymous; - cb(JSON.parse(res.content)); + var parsed = JSON.parse(res.content); + parsed._isAnon = answer.anonymous; + parsed._time = messages[0].time; + cb(parsed); }); }); From 82101bcb9b16ee2a7c187f4eab310bc8789ed358 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 21:16:49 +0530 Subject: [PATCH 016/183] use two characters for the default avatar --- customize.dist/src/less2/include/avatar.less | 2 +- customize.dist/src/less2/include/toolbar.less | 2 +- www/common/inner/common-mediatag.js | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 725c7748f..02abc4050 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -4,7 +4,7 @@ @width: 30px ) { @avatar-width: @width; - @avatar-font-size: @width / 1.2; + @avatar-font-size: @width / 1.8; } .avatar_main(@width: 30px) { --LessLoader_require: LessLoader_currentFile(); diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index b0f9b5e42..28b513095 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -855,7 +855,7 @@ span { text-align: center; width: 100%; - font-size: 48px; + font-size: 40px; display: inline-flex; justify-content: center; align-items: center; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 1d88e1029..03d85fab0 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -82,7 +82,16 @@ define([ MT.displayAvatar = function (common, $container, href, name, _cb) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { - var text = Util.getFirstCharacter(name || Messages.anonymous); + name = (name || "").trim() || Messages.anonymous; + var parts = name.split(/\s+/); + var text; + if (parts.length > 1) { + text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); + } else { + text = Util.getFirstCharacter(name); + text += Util.getFirstCharacter(name.replace(text, '')); + } + var $avatar = $('', {'class': 'cp-avatar-default'}).text(text); $container.append($avatar); if (cb) { cb(); } From b8c847bccef0001553cfc09724f38939dd8cd74c Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 22:25:51 +0530 Subject: [PATCH 017/183] prototype animal avatars for guests that haven't set a custom name --- www/common/inner/common-mediatag.js | 34 ++++++++++++++++++++++++++--- www/common/toolbar.js | 5 +++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 03d85fab0..2dc912e02 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -79,21 +79,48 @@ define([ }); }; - MT.displayAvatar = function (common, $container, href, name, _cb) { + // https://emojipedia.org/nature/ + var ANIMALS = [ '🙈', '🦀', '🐞', '🦋', '🐬', '🐋', '🐢', '🦉', '🦆', '🐧', '🦡', '🦘', '🦨', '🦦', '🦥', '🐼', '🐻', '🦝', '🦄', '🐄', '🐷', '🐐', '🦙', '🦒', '🐘', '🦏', '🐁', '🐹', '🐰', '🦫', '🦔', '🐨']; + + var getRandomAnimal = function () { + return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; + }; + + var getPseudorandomAnimal = function (seed) { + if (typeof(seed) !== 'string') { return getRandomAnimal(); } + seed = seed.replace(/\D/g, '').slice(0, 10); + seed = parseInt(seed); + if (!seed) { return getRandomAnimal(); } + return ANIMALS[seed % ANIMALS.length]; + }; + + MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { + if (avatars[uid]) { + var nodes = $.parseHTML(avatars[uid]); + var $el = $(nodes[0]); + $container.append($el); + return void cb($el); + } + var animal = false; + name = (name || "").trim() || Messages.anonymous; var parts = name.split(/\s+/); var text; - if (parts.length > 1) { + if (name === Messages.anonymous) { + text = getPseudorandomAnimal(uid); + animal = true; + } else if (parts.length > 1) { text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); } else { text = Util.getFirstCharacter(name); text += Util.getFirstCharacter(name.replace(text, '')); } - var $avatar = $('', {'class': 'cp-avatar-default'}).text(text); + var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); $container.append($avatar); + avatars[uid] = $avatar[0].outerHTML; if (cb) { cb(); } }; if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol @@ -106,6 +133,7 @@ define([ return void cb($el); } + var centerImage = function ($img, $image) { var img = $image[0]; var w = img.width; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index d103fe410..791857049 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -361,9 +361,10 @@ MessengerUI, Messages, Pages) { Common.openURL(origin+'/profile/#' + data.profile); }); } - Common.displayAvatar($span, data.avatar, name, function () { + console.error("AVATAR", $span, data.uid); + Common.displayAvatar($span, data.avatar, name, function () { // XXX pass a little more info so we can display better (pseudo-random) defaults $span.append($rightCol); - }); + }, data.uid); $span.data('uid', data.uid); $editUsersList.append($span); }); From 904e06091da39b6f56ef88382091e2411a30ff0f Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 11:29:16 +0200 Subject: [PATCH 018/183] Fix calendar .ics import (#784) --- www/calendar/export.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/www/calendar/export.js b/www/calendar/export.js index 64775762b..5967e2027 100644 --- a/www/calendar/export.js +++ b/www/calendar/export.js @@ -123,6 +123,7 @@ define([ var jcalData = ICAL.parse(content); vcalendar = new ICAL.Component(jcalData); } catch (e) { + console.error(e); return void cb(e); } @@ -147,6 +148,18 @@ define([ var isAllDay = false; var start = ev.getFirstPropertyValue('dtstart'); var end = ev.getFirstPropertyValue('dtend'); + var duration = ev.getFirstPropertyValue('duration'); + if (!end && !duration) { + if (start.isDate) { + end = start.clone(); + end.adjust(1); // Add one day + } else { + end = start.clone(); + } + } else if (!end) { + end = start.clone(); + end.addDuration(duration); + } if (start.isDate && end.isDate) { isAllDay = true; start = String(start); @@ -175,7 +188,7 @@ define([ hidden.push(al.toString()); } var trigger = al.getFirstPropertyValue('trigger'); - var minutes = -trigger.toSeconds() / 60; + var minutes = trigger ? (-trigger.toSeconds() / 60) : 0; if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); } }); From c5e6ca646eb0ab2868f3c2c31602537e6cbfed19 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 15:57:14 +0530 Subject: [PATCH 019/183] adjust animal avatar caching system and adjust size in the toolbar --- customize.dist/src/less2/include/toolbar.less | 3 +++ www/common/inner/common-mediatag.js | 21 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 28b513095..d8029f0d2 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -200,6 +200,9 @@ .avatar_main(30px); .cp-avatar-default, media-tag { margin-right: 5px; + &.animal { + font-size: 20px; + } } &.cp-userlist-clickable { cursor: pointer; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 2dc912e02..ece9e3024 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -80,7 +80,7 @@ define([ }; // https://emojipedia.org/nature/ - var ANIMALS = [ '🙈', '🦀', '🐞', '🦋', '🐬', '🐋', '🐢', '🦉', '🦆', '🐧', '🦡', '🦘', '🦨', '🦦', '🦥', '🐼', '🐻', '🦝', '🦄', '🐄', '🐷', '🐐', '🦙', '🦒', '🐘', '🦏', '🐁', '🐹', '🐰', '🦫', '🦔', '🐨']; + var ANIMALS = '🙈 🦀 🐞 🦋 🐬 🐋 🐢 🦉 🦆 🐧 🦡 🦘 🦨 🦦 🦥 🐼 🐻 🦝 🦓 🐄 🐷 🐐 🦙 🦒 🐘 🦏 🐁 🐹 🐰 🦫 🦔 🐨 🐱 🐺 👺 👹 👽 👾 🤖'.split(/\s+/); var getRandomAnimal = function () { return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; @@ -94,14 +94,13 @@ define([ return ANIMALS[seed % ANIMALS.length]; }; + var animal_avatars = {}; MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { - if (avatars[uid]) { - var nodes = $.parseHTML(avatars[uid]); - var $el = $(nodes[0]); - $container.append($el); - return void cb($el); + var animal_avatar; + if (uid && animal_avatars[uid]) { + animal_avatar = animal_avatars[uid] } var animal = false; @@ -109,7 +108,11 @@ define([ var parts = name.split(/\s+/); var text; if (name === Messages.anonymous) { - text = getPseudorandomAnimal(uid); + if (animal_avatar) { + text = animal_avatar; + } else { + text = animal_avatar = getPseudorandomAnimal(uid); + } animal = true; } else if (parts.length > 1) { text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); @@ -120,7 +123,9 @@ define([ var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); $container.append($avatar); - avatars[uid] = $avatar[0].outerHTML; + if (uid && animal) { + animal_avatars[uid] = animal_avatar; + } if (cb) { cb(); } }; if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol From ff1c4c9a65971033f085957ea4c4c8c6f3786005 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 16:02:00 +0530 Subject: [PATCH 020/183] update the calendar list when calendar removal is successful also fall back to team.name when team.displayName is not available --- www/calendar/inner.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 21cb12789..a1fd8c8e8 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -566,7 +566,7 @@ define([ attributes: { 'class': 'fa fa-trash-o', }, - content: h('span', Messages.kanban_delete), + content: h('span', Messages.kanban_delete), // XXX delete key is misleading... "Remove" ? `poll_remove`, `fc_remove` action: function (e) { e.stopPropagation(); var cal = APP.calendars[id]; @@ -586,8 +586,9 @@ define([ }, function (err) { if (err) { console.error(err); - UI.warn(Messages.error); + return void UI.warn(Messages.error); } + renderCalendar(); }); }); } @@ -722,7 +723,7 @@ define([ if (!calendars.length) { return; } var team = privateData.teams[teamId]; var avatar = h('span.cp-avatar'); - common.displayAvatar($(avatar), team.avatar, team.displayName); + common.displayAvatar($(avatar), team.avatar, team.displayName || team.name); APP.$calendars.append(h('div.cp-calendar-team', [ avatar, h('span.cp-name', {title: team.name}, team.name) From f0fad8e95c005474f8a9f248d2140ed1e60ea3be Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 16:03:59 +0530 Subject: [PATCH 021/183] avoid unnecessary use of scrollbars on sidebar apps (settings, admin, support, ...) --- customize.dist/src/less2/include/sidebar-layout.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index ef93eae4a..1b0c75932 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -69,7 +69,7 @@ background: @cp_sidebar-right-bg; color: @cp_sidebar-right-fg; overflow: auto; - padding-bottom: 200px; + //padding-bottom: 200px; // XXX what was the intent behind this? // Following rules are only in settings .cp-sidebarlayout-element { From 925872679a55913db286ea9afa6b1a70ba887948 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 13:22:06 +0200 Subject: [PATCH 022/183] New page when form responses have been submitted --- www/form/app-form.less | 16 ++++ www/form/inner.js | 162 ++++++++++++++++++++++++++++++++++------- www/form/main.js | 1 + 3 files changed, 154 insertions(+), 25 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index a0d0bdd65..ef6d9b839 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -127,6 +127,10 @@ & > div:not(:last-child) { margin-bottom: 20px; } + .cp-forms-results-participant { + display: flex; + flex-flow: column; + } } div.cp-form-filler-container { width: 300px; @@ -313,6 +317,12 @@ margin-bottom: 20px; } + .cp-form-disabled { + .cp-form-poll-choice, .cp-form-type-sort { + cursor: not-allowed !important; + } + } + .cp-form-preview { color: @cp_sidebar-hint; margin-bottom: 10px; @@ -502,6 +512,12 @@ } } } + div.cp-form-creator-answered { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + } div.cp-form-creator-results { display: flex; flex-flow: column; diff --git a/www/form/inner.js b/www/form/inner.js index 2f717cb05..f1cb5adfd 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -68,6 +68,7 @@ define([ ) { var APP = window.APP = { + blocks: {} }; var is24h = UIElements.is24h(); @@ -1009,6 +1010,10 @@ define([ return $tag.val(); }, setValue: function (val) { $tag.val(val); }, + setEditable: function (state) { + if (state) { $tag.removeAttr('disabled'); } + else { $tag.attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editTextOptions(v, setCursorGetter, cb, tmp); @@ -1091,6 +1096,10 @@ define([ $text.val(val); updateChar(); }, + setEditable: function (state) { + if (state) { $(tag).removeAttr('disabled'); } + else { $(tag).attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editTextOptions(v, setCursorGetter, cb, tmp); @@ -1151,6 +1160,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { $(tag).find('input').removeAttr('disabled'); } + else { $(tag).find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1222,7 +1235,8 @@ define([ var tag = h('div.radio-group.cp-form-type-multiradio', lines); var cursorGetter; var setCursorGetter = function (f) { cursorGetter = f; }; - $(tag).find('input[type="radio"]').on('change', function () { + var $tag = $(tag); + $tag.find('input[type="radio"]').on('change', function () { evOnChange.fire(); }); return { @@ -1242,6 +1256,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { $tag.find('input').removeAttr('disabled'); } + else { $tag.find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1350,13 +1368,16 @@ define([ h('div.radio-group.cp-form-type-checkbox', els) ]); var $tag = $(tag); - $tag.find('input').on('change', function () { + var checkDisabled = function () { var selected = $tag.find('input:checked').length; if (selected >= opts.max) { $tag.find('input:not(:checked)').attr('disabled', 'disabled'); } else { $tag.find('input').removeAttr('disabled'); } + }; + $tag.find('input').on('change', function () { + checkDisabled(); evOnChange.fire(); }); var cursorGetter; @@ -1374,6 +1395,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { checkDisabled(); } + else { $tag.find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1388,10 +1413,7 @@ define([ $el.prop('checked', true); } }); - var selected = $tag.find('input:checked').length; - if (selected >= opts.max) { - $tag.find('input:not(:checked)').attr('disabled', 'disabled'); - } + checkDisabled(); } }; @@ -1448,14 +1470,18 @@ define([ if (!opts.max) { opts.max = TYPES.multicheck.defaultOpts.max; } + var checkDisabled = function (l) { + var selected = $(l).find('input:checked').length; + if (selected >= opts.max) { + $(l).find('input:not(:checked)').attr('disabled', 'disabled'); + } else { + $(l).find('input').removeAttr('disabled'); + } + }; + lines.forEach(function (l) { $(l).find('input').on('change', function () { - var selected = $(l).find('input:checked').length; - if (selected >= opts.max) { - $(l).find('input:not(:checked)').attr('disabled', 'disabled'); - } else { - $(l).find('input').removeAttr('disabled'); - } + checkDisabled(l); evOnChange.fire(); }); }); @@ -1484,6 +1510,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { lines.forEach(checkDisabled); } + else { $(tag).find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1498,6 +1528,7 @@ define([ $(el).prop('checked', true); }); }); + lines.forEach(checkDisabled); } }; @@ -1633,10 +1664,6 @@ define([ } } }); - - $(tag).find('input[type="radio"]').on('change', function () { - evOnChange.fire(); - }); return { tag: tag, getValue: function () { @@ -1653,6 +1680,10 @@ define([ sortable.sort(toSort); reorder(true); }, + setEditable: function (state) { + sortable.options.disabled = !state; + $(tag).toggleClass('cp-form-disabled', !state); + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1704,6 +1735,7 @@ define([ var lines = makePollTable(answers, opts, false); + var disabled = false; // Add form var addLine = opts.values.map(function (data) { var cell = h('div.cp-poll-cell.cp-form-poll-choice', [ @@ -1716,6 +1748,7 @@ define([ var val = 0; $c.attr('data-value', val); $c.click(function () { + if (disabled) { return; } val = (val+1)%3; $c.attr('data-value', val); evOnChange.fire(); @@ -1768,6 +1801,10 @@ define([ reset: function () { $tag.find('.cp-form-poll-choice').attr('data-value', 0); }, + setEditable: function (state) { + disabled = !state; + $tag.toggleClass('cp-form-disabled', disabled); + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -2092,6 +2129,43 @@ define([ framework._.toolbar.$bottomL.append($res); }; + Messages.form_alreadyAnswered = "You've responded to this form on {0}"; // XXX + Messages.form_editAnswer = "Edit my responses"; // XXX + Messages.form_viewAnswer = "View my responses"; // XXX + var showAnsweredPage = function (framework, content, answers) { + var $formContainer = $('div.cp-form-creator-content').hide(); + var $container = $('div.cp-form-creator-answered').empty().css('display', ''); + + var viewOnly = content.answers.cantEdit; + var action = h('button.btn.btn-primary', [ + viewOnly ? h('i.fa.fa-bar-chart') : h('i.fa.fa-pencil'), + h('span', viewOnly ? Messages.form_viewAnswer : Messages.form_editAnswer) + ]); + + $(action).click(function () { + $formContainer.css('display', ''); + $container.hide(); + if (viewOnly) { + $formContainer.find('.cp-form-send-container .cp-open').hide(); + Object.keys(APP.blocks).forEach(function (uid) { + var b = APP.blocks[uid]; + if (!b.setEditable) { return; } + b.setEditable(false); + }); + } + }); + + if (answers._time) { APP.lastAnswerTime = answers._time; } + + var title = framework._.title.title || framework._.title.defaultTitle; + $container.append(h('div.cp-form-submit-success', [ + h('h3.cp-form-view-title', title), + h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ + new Date(APP.lastAnswerTime).toLocaleString()])), + action + ])); + }; + var getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; @@ -2180,8 +2254,9 @@ define([ addResultsButton(framework, content); } $send.removeAttr('disabled'); - UI.alert(Messages.form_sent); + //UI.alert(Messages.form_sent); // XXX not needed anymore? $send.text(Messages.form_update); + showAnsweredPage(framework, content, { '_time': +new Date() }); }); }); @@ -2378,7 +2453,7 @@ define([ name = user.name; } - var data = model.get(block.opts, _answers, name, evOnChange); + var data = APP.blocks[uid] = model.get(block.opts, _answers, name, evOnChange); if (!data) { return; } data.uid = uid; if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } @@ -2502,7 +2577,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); _answers = getBlockAnswers(APP.answers, uid); - data = model.get(newOpts, _answers, null, evOnChange); + data = APP.blocks[uid] = model.get(newOpts, _answers, null, evOnChange); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -2565,7 +2640,7 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { - data = model.get(block.opts, _answers, null, evOnChange); + data = APP.blocks[uid] = model.get(block.opts, _answers, null, evOnChange); $oldTag.before(data.tag).remove(); }); }); @@ -2676,12 +2751,11 @@ define([ } // If the form is already submitted, show an info message - Messages.form_alreadyAnswered = "You've submitted answers to this form on {0}"; // XXX if (answers) { + showAnsweredPage(framework, content, answers); $container.prepend(h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ - new Date(answers._time).toLocaleString()]))); - // XXX make the page read-only? + new Date(answers._time || APP.lastAnswerTime).toLocaleString()]))); } // In view mode, add "Submit" and "reset" buttons @@ -2705,8 +2779,8 @@ define([ if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); - } - }; + } +}; var getTempFields = function () { if (!Array.isArray(APP.formBlocks)) { return; } @@ -2916,6 +2990,38 @@ define([ }; refreshPrivacy(); + // Allow responses edition + Messages.form_editable = "Allow users to edit their responses"; // XXX + var editableContainer = h('div.cp-form-editable-container'); + var $editable = $(editableContainer); + var refreshEditable = function () { + $editable.empty(); + var editable = !content.answers.cantEdit; + var radioOn = UI.createRadio('cp-form-editable', 'cp-form-editable-on', + Messages.form_anonymous_on, Boolean(editable), { + input: { value: 1 }, + mark: { tabindex:1 } + }); + var radioOff = UI.createRadio('cp-form-editable', 'cp-form-editable-off', + Messages.form_anonymous_off, !editable, { + input: { value: 0 }, + mark: { tabindex:1 } + }); + var radioContainer = h('div.cp-form-editable-radio', [radioOn, radioOff]); + $(radioContainer).find('input[type="radio"]').on('change', function() { + var val = $('input:radio[name="cp-form-editable"]:checked').val(); + val = Number(val) || 0; + content.answers.cantEdit = !val; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + }); + }); + $editable.append(h('div.cp-form-status', Messages.form_editable)); + $editable.append(h('div.cp-form-actions', radioContainer)); + }; + refreshEditable(); + // End date / Closed state var endDateContainer = h('div.cp-form-status-container'); var $endDate = $(endDateContainer); @@ -2979,6 +3085,7 @@ define([ evOnChange.reg(refreshPublic); evOnChange.reg(refreshPrivacy); + evOnChange.reg(refreshEditable); evOnChange.reg(refreshEndDate); //evOnChange.reg(refreshResponse); @@ -2986,6 +3093,7 @@ define([ preview, endDateContainer, privacyContainer, + editableContainer, resultsType, responseMsg ]; @@ -3027,10 +3135,14 @@ define([ var contentContainer = h('div.cp-form-creator-content'); var resultsContainer = h('div.cp-form-creator-results'); + var answeredContainer = h('div.cp-form-creator-answered', { + style: 'display: none;' + }); var div = h('div.cp-form-creator-container', [ controlContainer, contentContainer, resultsContainer, + answeredContainer, fillerContainer ]); return div; diff --git a/www/form/main.js b/www/form/main.js index 4c0831fe9..58030f35c 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -266,6 +266,7 @@ define([ if (obj && obj.error) { return void cb(obj); } var messages = obj.messages; if (!messages.length) { return void cb(); } + if (obj.lastKnownHash !== answer.hash) { return void cb(); } var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, { validateKey: data.validateKey, ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate), From 2df0f6df9b111736b6264747509272d9b69bb3ae Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 14:45:05 +0200 Subject: [PATCH 023/183] Disable answer edition in forms --- www/form/inner.js | 7 +++++-- www/form/main.js | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f1cb5adfd..1973619db 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3244,7 +3244,8 @@ define([ channel: content.answers.channel, validateKey: content.answers.validateKey, publicKey: content.answers.publicKey, - privateKey: key + privateKey: key, + cantEdit: content.answers.cantEdit }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } @@ -3263,7 +3264,8 @@ define([ sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, validateKey: content.answers.validateKey, - publicKey: content.answers.publicKey + publicKey: content.answers.publicKey, + cantEdit: content.answers.cantEdit }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } @@ -3289,6 +3291,7 @@ define([ validateKey: content.answers.validateKey, publicKey: content.answers.publicKey, privateKey: content.answers.privateKey, + cantEdit: content.answers.cantEdit }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } diff --git a/www/form/main.js b/www/form/main.js index 58030f35c..7d81b1268 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -221,6 +221,8 @@ define([ delete results[parsed._proof.key]; } } + // XXX If "allow edition" is disabled, don't override here? + // if (data.cantEdit && results[senderCurve]) { return; } results[senderCurve] = { msg: parsed, hash: hash, From 5d04cd0f4ff461ae9fbd165c4728786b463ae3d8 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 14:56:28 +0200 Subject: [PATCH 024/183] Forms code improvements --- www/form/inner.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 1973619db..6fbd495b1 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2147,11 +2147,12 @@ define([ $container.hide(); if (viewOnly) { $formContainer.find('.cp-form-send-container .cp-open').hide(); - Object.keys(APP.blocks).forEach(function (uid) { - var b = APP.blocks[uid]; - if (!b.setEditable) { return; } - b.setEditable(false); - }); + if (Array.isArray(APP.formBlocks)) { + APP.formBlocks.forEach(function (b) { + if (!b.setEditable) { return; } + b.setEditable(false); + }); + } } }); @@ -2453,7 +2454,7 @@ define([ name = user.name; } - var data = APP.blocks[uid] = model.get(block.opts, _answers, name, evOnChange); + var data = model.get(block.opts, _answers, name, evOnChange); if (!data) { return; } data.uid = uid; if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } @@ -2577,7 +2578,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); _answers = getBlockAnswers(APP.answers, uid); - data = APP.blocks[uid] = model.get(newOpts, _answers, null, evOnChange); + data = model.get(newOpts, _answers, null, evOnChange); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -2640,7 +2641,7 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { - data = APP.blocks[uid] = model.get(block.opts, _answers, null, evOnChange); + data = model.get(block.opts, _answers, null, evOnChange); $oldTag.before(data.tag).remove(); }); }); From 583060d130613d8e3c5f613bf1864a8d933dbbd2 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 17:41:38 +0200 Subject: [PATCH 025/183] Required questions --- www/form/app-form.less | 19 ++++- www/form/inner.js | 154 +++++++++++++++++++++++++++++++++++------ 2 files changed, 149 insertions(+), 24 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index ef6d9b839..441982b5f 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -356,10 +356,25 @@ .cp-form-block-question { margin-bottom: 5px; + display: flex; .cp-form-block-question-number { font-weight: bold; margin-right: 10px; } + .cp-form-block-question-text { + flex: 1; + } + .cp-form-required-tag { + background: fade(@cryptpad_text_col, 15%); + padding: 5px; + margin-top: -10px; + margin-right: -10px; + &.cp-is-empty { + padding: 3px; + border: 2px solid @cryptpad_color_red; + color: @cp_form-invalid; + } + } } .cp-form-block-content { overflow-x: auto; @@ -548,8 +563,8 @@ .cp-form-creator-results-content { padding-bottom: 100px; .cp-form-block { - background: @cp_form-bg1; - padding: 10px; + background: @cp_form-bg1; + padding: 10px; } } .cp-form-block-question { diff --git a/www/form/inner.js b/www/form/inner.js index 6fbd495b1..f50d4266f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1004,6 +1004,7 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { return !$tag.val().trim(); }, getValue: function () { //var invalid = $tag.is(':invalid'); //if (invalid) { return; } @@ -1091,6 +1092,7 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { return !$text.val().trim(); }, getValue: function () { return $text.val().slice(0, opts.maxLength); }, setValue: function (val) { $text.val(val); @@ -1148,6 +1150,7 @@ define([ }); return { tag: tag, + isEmpty: function () { return !this.getValue(); }, getValue: function () { var res; els.some(function (el) { @@ -1241,6 +1244,12 @@ define([ }); return { tag: tag, + isEmpty: function () { + var v = this.getValue(); + return !Object.keys(v).length || Object.keys(v).some(function (uid) { + return !v[uid]; + }); + }, getValue: function () { var res = {}; var l = lines.slice(1); @@ -1384,6 +1393,10 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { + var v = this.getValue(); + return !v.length; + }, getValue: function () { var res = []; els.forEach(function (el) { @@ -1495,6 +1508,12 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { + var v = this.getValue(); + return Object.keys(v).some(function (uid) { + return !v[uid].length; + }); + }, getValue: function () { var res = {}; var l = lines.slice(1); @@ -1659,13 +1678,14 @@ define([ forceFallback: true, store: { set: function () { - evOnChange.fire(); reorder(); + evOnChange.fire(); } } }); return { tag: tag, + isEmpty: function () { return !this.getValue(); }, getValue: function () { if (!sorted) { return; } return sortable.toArray().map(function (id) { @@ -2215,6 +2235,7 @@ define([ if (typeof(data.reset) === "function") { data.reset(); } }); $(reset).attr('disabled', 'disabled'); + evOnChange.fire(); }); var $send = $(send).click(function () { $send.attr('disabled', 'disabled'); @@ -2266,6 +2287,9 @@ define([ reset = undefined; } + Messages.form_requiredWarning = "These questions need an answer:"; // XXX + var errors = h('div.cp-form-invalid-warning'); + var $errors = $(errors); var invalid = h('div.cp-form-invalid-warning'); var $invalid = $(invalid); if (evOnChange) { @@ -2274,7 +2298,28 @@ define([ priv = metadataMgr.getPrivateData(); origin = priv.origin; } - evOnChange.reg(function () { + + var gotoQuestion = function (el) { + var $el = $(el).closest('.cp-form-block'); + var number = $el.find('.cp-form-block-question-number').text(); + var a = h('a', { + href: origin + '#' + Messages._getKey('form_invalidQuestion', [number]) + }, Messages._getKey('form_invalidQuestion', [number])); + $(a).click(function (e) { + e.preventDefault(); + if (!$el.is(':visible')) { + var pages = $el.closest('.cp-form-page').index(); + if (APP.refreshPage) { APP.refreshPage(pages + 1); } + } + $el[0].scrollIntoView(); + }); + return h('li', a); + }; + + if (APP.checkInvalidEvt) { evOnChange.unreg(APP.checkInvalidEvt); } + if (APP.checkErrorEvt) { evOnChange.unreg(APP.checkErrorEvt); } + // Check invalid inputs + APP.checkInvalidEvt = function () { var $container = $('div.cp-form-creator-content'); var $inputs = $container.find('input:invalid'); if (!$inputs.length) { @@ -2284,21 +2329,7 @@ define([ $send.text(update ? Messages.form_updateWarning : Messages.form_submitWarning); var lis = []; $inputs.each(function (i, el) { - var $el = $(el).closest('.cp-form-block'); - var number = $el.find('.cp-form-block-question-number').text(); - var a = h('a', { - href: origin + '#' + Messages._getKey('form_invalidQuestion', [number]) - }, Messages._getKey('form_invalidQuestion', [number])); - $(a).click(function (e) { - e.preventDefault(); - if (!$el.is(':visible')) { - var pages = $el.closest('.cp-form-page').index(); - if (APP.refreshPage) { APP.refreshPage(pages + 1); } - } - $el[0].scrollIntoView(); - }); - var li = h('li', a); - lis.push(li); + lis.push(gotoQuestion(el)); }); var list = h('ul', lis); var content = [ @@ -2306,12 +2337,47 @@ define([ list ]; $invalid.empty().append(content); - }); + }; + // Check empty required questions + APP.checkErrorEvt = function () { + if (!Array.isArray(APP.formBlocks)) { return; } + var form = content.form; + var errorBlocks = APP.formBlocks.filter(function (data) { + var uid = data.uid; + var block = form[uid]; + if (!data.isEmpty) { return; } + if (!block) { return; } + if (!block.opts || !block.opts.required) { return; } + console.error(data.getValue()); + var isEmpty = data.isEmpty(); + var $el = $(data.tag).closest('.cp-form-block'); + $el.find('.cp-form-required-tag').toggleClass('cp-is-empty', isEmpty); + return isEmpty; + }); + if (!errorBlocks.length) { + $send.removeAttr('disabled'); + return void $errors.empty(); + } + $send.attr('disabled', 'disabled'); + var lis = []; + errorBlocks.forEach(function (data) { + lis.push(gotoQuestion(data.tag)); + }); + var list = h('ul', lis); + var divContent = [ + h('span', Messages.form_requiredWarning), + list + ]; + $errors.empty().append(divContent); + }; + evOnChange.reg(APP.checkInvalidEvt); + evOnChange.reg(APP.checkErrorEvt); evOnChange.fire(true); } return h('div.cp-form-send-container', [ invalid, + errors, cbox ? h('div.cp-form-anon-answer', [ cbox, anonName @@ -2465,10 +2531,17 @@ define([ } + Messages.form_required = "Required"; // XXX + var requiredTag; + if (block.opts && block.opts.required) { + requiredTag = h('span.cp-form-required-tag', Messages.form_required); + } + var dragHandle; var q = h('div.cp-form-block-question', [ h('span.cp-form-block-question-number', (n++)+'.'), - h('span', block.q || Messages.form_default) + h('span.cp-form-block-question-text', block.q || Messages.form_default), + requiredTag ]); // Static blocks don't have questions ("q" is not used) so we can decrement n if (isStatic) { n--; } @@ -2480,6 +2553,38 @@ define([ Messages.form_preview = "Preview:"; // XXX var previewDiv = h('div.cp-form-preview', Messages.form_preview); + Messages.form_required_on = "Required answer"; + Messages.form_required_off = "Optional answer"; + // Required radio displayed only for types that have an "isEmpty" function + var requiredDiv; + if (APP.isEditor && !isStatic && data.isEmpty) { + if (!block.opts) { block.opts = {}; } + var isRequired = Boolean(block.opts.required); + var radioOn = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-on', + Messages.form_required_on, isRequired, { + input: { value: 1 }, + mark: { tabindex:1 } + }); + var radioOff = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-off', + Messages.form_required_off, !isRequired, { + input: { value: 0 }, + mark: { tabindex:1 } + }); + var radioContainer = h('div.cp-form-required-radio', [radioOn, radioOff]); + requiredDiv = h('div.cp-form-required', [ + radioContainer + ]); + $(radioContainer).find('input[type="radio"]').on('change', function() { + var val = $('input:radio[name="cp-form-required-'+uid+'"]:checked').val(); + val = Number(val) || 0; + block.opts.required = Boolean(val); + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + }); + }); + } + if (editable) { // Drag handle dragHandle = h('span.cp-form-block-drag-handle', [ @@ -2568,6 +2673,7 @@ define([ $(editButtons).show(); $(data.tag).show(); $(previewDiv).show(); + $(requiredDiv).show(); return; } $(editContainer).empty(); @@ -2576,6 +2682,8 @@ define([ var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { $(editButtons).show(); + $(previewDiv).show(); + $(requiredDiv).show(); UI.log(Messages.saved); _answers = getBlockAnswers(APP.answers, uid); data = model.get(newOpts, _answers, null, evOnChange); @@ -2585,6 +2693,7 @@ define([ }; var onEdit = function (tmp) { data.editing = true; + $(requiredDiv).hide(); $(previewDiv).hide(); $(data.tag).hide(); $(editContainer).append(data.edit(onSave, tmp, framework)); @@ -2660,7 +2769,8 @@ define([ APP.isEditor ? dragHandle : undefined, isStatic ? undefined : q, h('div.cp-form-block-content', [ - isStatic || !APP.isEditor ? undefined : previewDiv, + APP.isEditor && !isStatic ? requiredDiv : undefined, + APP.isEditor && !isStatic ? previewDiv : undefined, data.tag, editButtons ]), @@ -2780,8 +2890,8 @@ define([ if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); - } -}; + } + }; var getTempFields = function () { if (!Array.isArray(APP.formBlocks)) { return; } From 0c4405d43f43e9f831396a60c8c8472f0154b024 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 11:55:34 +0200 Subject: [PATCH 026/183] Fix textarea issues in forms --- www/form/inner.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f50d4266f..19e42e131 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1059,7 +1059,7 @@ define([ maxLength: 1000 }, get: function (opts, a, n, evOnChange) { - if (!opts) { opts = TYPES.textarea.defaultOpts; } + if (!opts || typeof(opts.maxLength) === "undefined") { opts = TYPES.textarea.defaultOpts; } var text = h('textarea', {maxlength: opts.maxLength}); var $text = $(text); var charCount = h('div.cp-form-type-textarea-charcount'); @@ -1099,8 +1099,8 @@ define([ updateChar(); }, setEditable: function (state) { - if (state) { $(tag).removeAttr('disabled'); } - else { $(tag).attr('disabled', 'disabled'); } + if (state) { $(tag).find('textarea').removeAttr('disabled'); } + else { $(tag).find('textarea').attr('disabled', 'disabled'); } }, edit: function (cb, tmp) { var v = Util.clone(opts); @@ -2348,7 +2348,6 @@ define([ if (!data.isEmpty) { return; } if (!block) { return; } if (!block.opts || !block.opts.required) { return; } - console.error(data.getValue()); var isEmpty = data.isEmpty(); var $el = $(data.tag).closest('.cp-form-block'); $el.find('.cp-form-required-tag').toggleClass('cp-is-empty', isEmpty); From aef1b22291d36510ec31c755d8565671442be101 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 16:32:25 +0530 Subject: [PATCH 027/183] console.error instead of throwing when unregistering handlers --- www/common/common-util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/common-util.js b/www/common/common-util.js index 3d2a8d0a4..fbed064dc 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -75,7 +75,9 @@ handlers.push(cb); }, unreg: function (cb) { - if (handlers.indexOf(cb) === -1) { throw new Error("Not registered"); } + if (handlers.indexOf(cb) === -1) { + return void console.error("event handler was already unregistered"); + } handlers.splice(handlers.indexOf(cb), 1); }, fire: function () { From 46e545a976ef9312f9aa0018aaca7dca614cedf1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 16:34:51 +0530 Subject: [PATCH 028/183] lint compliance --- www/common/inner/common-mediatag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index ece9e3024..3d4e82caf 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -100,7 +100,7 @@ define([ var displayDefault = function () { var animal_avatar; if (uid && animal_avatars[uid]) { - animal_avatar = animal_avatars[uid] + animal_avatar = animal_avatars[uid]; } var animal = false; From 9af99c1b8a057dac1dc947b4b7f80eb2c2069786 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 14:32:57 +0200 Subject: [PATCH 029/183] Fix tabindex issues in forms --- www/form/inner.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 19e42e131..c797786fa 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1138,7 +1138,7 @@ define([ var name = Util.uid(); var els = opts.values.map(function (data, i) { var radio = UI.createRadio(name, 'cp-form-'+name+'-'+i, - data, false, { mark: { tabindex:1 } }); + data, false, {}); $(radio).find('input').data('val', data); return radio; }); @@ -1223,7 +1223,7 @@ define([ var item = itemData.v; var els = opts.values.map(function (data, i) { var radio = UI.createRadio(name, 'cp-form-'+name+'-'+i, - '', false, { mark: { tabindex:1 } }); + '', false, {}); $(radio).find('input').data('uid', name); $(radio).find('input').data('val', data); return radio; @@ -1367,7 +1367,7 @@ define([ var name = Util.uid(); var els = opts.values.map(function (data, i) { var cbox = UI.createCheckbox('cp-form-'+name+'-'+i, - data, false, { mark: { tabindex:1 } }); + data, false, {}); $(cbox).find('input').data('val', data); return cbox; }); @@ -1472,7 +1472,7 @@ define([ var item = itemData.v; var els = opts.values.map(function (data, i) { var cbox = UI.createCheckbox('cp-form-'+name+'-'+i, - '', false, { mark: { tabindex:1 } }); + '', false, {}); $(cbox).find('input').data('uid', name); $(cbox).find('input').data('val', data); return cbox; @@ -2206,7 +2206,7 @@ define([ var cbox; var anonName, $anonName; cbox = UI.createCheckbox('cp-form-anonymous', - Messages.form_anonymousBox, true, { mark: { tabindex:1 } }); + Messages.form_anonymousBox, true, {}); var $anonBox = $(cbox).find('input'); if (loggedIn) { if (!content.answers.anonymous || APP.cantAnon) { @@ -2562,12 +2562,10 @@ define([ var radioOn = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-on', Messages.form_required_on, isRequired, { input: { value: 1 }, - mark: { tabindex:1 } }); var radioOff = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-off', Messages.form_required_off, !isRequired, { input: { value: 0 }, - mark: { tabindex:1 } }); var radioContainer = h('div.cp-form-required-radio', [radioOn, radioOff]); requiredDiv = h('div.cp-form-required', [ @@ -2722,7 +2720,7 @@ define([ var text = Messages['form_type_'+data]; if (!text) { return; } var radio = UI.createRadio(name, 'cp-form-changetype-'+i, - text, data===type, { mark: { tabindex:1 } }); + text, data===type, {}); $(radio).find('input').data('val', data); return radio; }); @@ -3078,12 +3076,10 @@ define([ var radioOn = UI.createRadio('cp-form-privacy', 'cp-form-privacy-on', Messages.form_anonymous_on, Boolean(anonymous), { input: { value: 1 }, - mark: { tabindex:1 } }); var radioOff = UI.createRadio('cp-form-privacy', 'cp-form-privacy-off', Messages.form_anonymous_off, !anonymous, { input: { value: 0 }, - mark: { tabindex:1 } }); var radioContainer = h('div.cp-form-privacy-radio', [radioOn, radioOff]); $(radioContainer).find('input[type="radio"]').on('change', function() { @@ -3110,12 +3106,10 @@ define([ var radioOn = UI.createRadio('cp-form-editable', 'cp-form-editable-on', Messages.form_anonymous_on, Boolean(editable), { input: { value: 1 }, - mark: { tabindex:1 } }); var radioOff = UI.createRadio('cp-form-editable', 'cp-form-editable-off', Messages.form_anonymous_off, !editable, { input: { value: 0 }, - mark: { tabindex:1 } }); var radioContainer = h('div.cp-form-editable-radio', [radioOn, radioOff]); $(radioContainer).find('input[type="radio"]').on('change', function() { From 4fe19c1ea4f67c41ab4d5fc34df4ac0da47b6dec Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 18:16:32 +0530 Subject: [PATCH 030/183] remove unnecessary example cards from default kanban board --- www/kanban/inner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 38e933cf1..585c7c285 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -563,12 +563,12 @@ define([ "12": { "id": 12, "title": Messages.kanban_working, - "item": [3, 4] + "item": [], }, "13": { "id": 13, "title": Messages.kanban_done, - "item": [5, 6] + "item": [], } }, items: items From 9f52ec8dc713043cac8a41edc34acbeadb060658 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 18:19:35 +0530 Subject: [PATCH 031/183] add new translation check to find duplicates and move all translation scripts into a dedicated folder --- package.json | 4 +- .../find-duplicate-translations.js | 55 +++++++++++++++++++ .../{ => translations}/lint-translations.js | 0 .../{ => translations}/unused-translations.js | 0 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 scripts/translations/find-duplicate-translations.js rename scripts/{ => translations}/lint-translations.js (100%) rename scripts/{ => translations}/unused-translations.js (100%) diff --git a/package.json b/package.json index a844167f5..2d1c3f626 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "lint:js": "jshint --config .jshintrc --exclude-path .jshintignore .", "lint:server": "jshint --config .jshintrc lib", "lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/", - "lint:translations": "node ./scripts/lint-translations.js", - "unused-translations": "node ./scripts/unused-translations.js", + "lint:translations": "node ./scripts/translations/lint-translations.js", + "unused-translations": "node ./scripts/translations/unused-translations.js", "test": "node scripts/TestSelenium.js", "test-rpc": "cd scripts/tests && node test-rpc", "template": "cd customize.dist/src && for page in ../index.html ../privacy.html ../terms.html ../contact.html ../what-is-cryptpad.html ../features.html ../../www/login/index.html ../../www/register/index.html ../../www/user/index.html;do echo $page; cp template.html $page; done;", diff --git a/scripts/translations/find-duplicate-translations.js b/scripts/translations/find-duplicate-translations.js new file mode 100644 index 000000000..f8b25497c --- /dev/null +++ b/scripts/translations/find-duplicate-translations.js @@ -0,0 +1,55 @@ +var Util = require("../lib/common-util"); +var EN = Util.clone(require("../www/common/translations/messages.json")); +var FR = Util.clone(require("../www/common/translations/messages.fr.json")); +var DE = Util.clone(require("../www/common/translations/messages.de.json")); +var JP = Util.clone(require("../www/common/translations/messages.ja.json")); + +var keys = Object.keys(EN); + +var duplicates = {}; +var addIfAbsent = function (A, e) { + if (A.includes(e)) { return; } + A.push(e); +}; +var markDuplicate = function (value, key1, key2) { + //console.log("[%s] === [%s] (%s)", key1, key2, value); + if (!Array.isArray(duplicates[value])) { + duplicates[value] = []; + } + addIfAbsent(duplicates[value], key1); + addIfAbsent(duplicates[value], key2); +}; + +keys.forEach(function (key) { + var value = EN[key]; + + //var duplicates = []; + keys.forEach(function (key2) { + if (key === key2) { return; } + var value2 = EN[key2]; + if (value === value2) { + markDuplicate(value, key, key2); + } + }); +}); + +// indicate which strings are duplicated and could potentially be changed to use one key +Object.keys(duplicates).forEach(function (val) { + console.log('\"%s\" => %s', val, JSON.stringify(duplicates[val])); +}); + +// TODO iterate over all languages and + +// 1) check whether the same mapping exists across languages +// ie. English has "Open" (verb) and "Open" (adjective) +// while French has "Ouvrir" and "Ouvert(s)" +// such keys should not be simplified/deduplicated + + + +// find instances where +// one of the duplicated keys is not translated +// perhaps we could automatically use the translated one everywhere +// and improve the completeness of translations + + diff --git a/scripts/lint-translations.js b/scripts/translations/lint-translations.js similarity index 100% rename from scripts/lint-translations.js rename to scripts/translations/lint-translations.js diff --git a/scripts/unused-translations.js b/scripts/translations/unused-translations.js similarity index 100% rename from scripts/unused-translations.js rename to scripts/translations/unused-translations.js From ead5ad217c25e76b70c1c69fe60fbccf90d48a70 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 14:53:24 +0200 Subject: [PATCH 032/183] Use placeholders when editing form questions --- www/form/inner.js | 52 +++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index c797786fa..ed1d77cdd 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -189,7 +189,7 @@ define([ saveAndCancel ]; }; - var editOptions = function (v, setCursorGetter, cb, tmp) { + var editOptions = function (v, isDefaultOpts, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), h('span', Messages.form_add_option) @@ -254,8 +254,16 @@ define([ // Show existing options var $add, $addItem; var addMultiple; - var getOption = function (val, isItem, uid) { + var getOption = function (val, placeholder, isItem, uid) { var input = h('input', {value:val}); + if (placeholder) { + input.placeholder = val; + input.value = ''; + $(input).change(function () { + input.placeholder = ''; + $(input).off(change); + }); + } if (uid) { $(input).data('uid', uid); } // If the input is a date, initialize flatpickr @@ -316,7 +324,7 @@ define([ }); return el; }; - var inputs = v.values.map(function (val) { return getOption(val, false); }); + var inputs = v.values.map(function (val) { return getOption(val, isDefaultOpts, false); }); inputs.push(add); var container = h('div.cp-form-edit-block', inputs); @@ -332,7 +340,7 @@ define([ var containerItems; if (v.items) { var inputsItems = v.items.map(function (itemData) { - return getOption(itemData.v, true, itemData.uid); + return getOption(itemData.v, isDefaultOpts, true, itemData.uid); }); inputsItems.push(addItem); containerItems = h('div.cp-form-edit-block', inputsItems); @@ -385,7 +393,7 @@ define([ }); $(addMultipleButton).click(function () { multiplePickr.selectedDates.some(function (date) { - $add.before(getOption(date, false)); + $add.before(getOption(date, false, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { @@ -440,7 +448,7 @@ define([ // "Add option" button handler $add = $(add).click(function () { var txt = v.type ? '' : Messages.form_newOption; - $add.before(getOption(txt, false)); + $add.before(getOption(txt, true, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { $add.hide(); } @@ -448,7 +456,7 @@ define([ // If multiline block, handle "Add item" button $addItem = $(addItem).click(function () { - $addItem.before(getOption(Messages.form_newItem, true, Util.uid())); + $addItem.before(getOption(Messages.form_newItem, true, true, Util.uid())); if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } }); if ($container.find('input').length >= MAX_OPTIONS) { $add.hide(); } @@ -460,12 +468,13 @@ define([ var active = document.activeElement; var cursor = {}; $container.find('input').each(function (i, el) { + var val = $(el).val() || el.placeholder || ''; if (el === active && !el._flatpickr) { - cursor.el= $(el).val(); + cursor.el = val; cursor.start = el.selectionStart; cursor.end = el.selectionEnd; } - values.push($(el).val()); + values.push(val); }); if (v.type === "day") { var dayPickr = $(calendarView).find('input')[0]._flatpickr; @@ -492,9 +501,10 @@ define([ cursor.start = el.selectionStart; cursor.end = el.selectionEnd; } + var val = $(el).val() || el.placeholder || ''; items.push({ uid: $(el).data('uid'), - v: $(el).val() + v: val }); }); _content.items = items; @@ -517,7 +527,7 @@ define([ }); } else { $container.find('input').each(function (i, el) { - var val = $(el).val().trim(); + var val = ($(el).val() || el.placeholder || '').trim(); if (v.type === "day" || v.type === "time") { var f = el._flatpickr; if (f && f.selectedDates && f.selectedDates.length) { @@ -538,7 +548,7 @@ define([ if (v.items) { var items = []; $(containerItems).find('input').each(function (i, el) { - var val = $(el).val().trim(); + var val = ($(el).val() || el.placeholder || '').trim(); var uid = $(el).data('uid'); if (!items.some(function (i) { return i.uid === uid; })) { items.push({ @@ -1133,6 +1143,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.radio.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var name = Util.uid(); @@ -1169,7 +1180,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1216,6 +1227,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.multiradio.defaultOpts; } if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; } var lines = opts.items.map(function (itemData) { @@ -1271,7 +1283,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1362,6 +1374,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.checkbox.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var name = Util.uid(); @@ -1414,7 +1427,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1465,6 +1478,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.multicheck.defaultOpts; } if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; } var lines = opts.items.map(function (itemData) { @@ -1535,7 +1549,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1628,6 +1642,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.sort.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var map = {}; @@ -1706,7 +1721,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1750,6 +1765,7 @@ define([ }) }, get: function (opts, answers, username, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } @@ -1827,7 +1843,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (res) { From 228a44e36cb15eca9e5b09f78dbbb9bf352c347f Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 14:56:45 +0200 Subject: [PATCH 033/183] Fix form question disappearing --- www/form/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index ed1d77cdd..973b9e89c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2573,7 +2573,7 @@ define([ // Required radio displayed only for types that have an "isEmpty" function var requiredDiv; if (APP.isEditor && !isStatic && data.isEmpty) { - if (!block.opts) { block.opts = {}; } + if (!block.opts) { block.opts = TYPES[type].defaultOpts; } var isRequired = Boolean(block.opts.required); var radioOn = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-on', Messages.form_required_on, isRequired, { From e33179df87a51af62f674d88d1fccf939dcd06a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 23 Aug 2021 16:57:49 +0100 Subject: [PATCH 034/183] Style and align form editor buttons for preview and link --- www/form/app-form.less | 5 ++++- www/form/inner.js | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 441982b5f..86bd9a2ed 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -120,7 +120,6 @@ } .cp-form-creator-settings { - padding: 30px; .cp-form-actions { margin-top: 5px; } @@ -130,6 +129,9 @@ .cp-forms-results-participant { display: flex; flex-flow: column; + button { + margin-bottom: 20px; + } } } div.cp-form-filler-container { @@ -139,6 +141,7 @@ } div.cp-form-creator-control { padding: 10px; + margin-top: 10px; display: flex; flex-flow: column; width: 300px; diff --git a/www/form/inner.js b/www/form/inner.js index 19e42e131..8e3fb8596 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2950,10 +2950,16 @@ define([ } var makeFormSettings = function () { - Messages.form_preview = "Preview participant page"; // XXX - Messages.form_geturl = "Copy participant link"; // XXX - var previewBtn = h('button.btn.btn-primary', Messages.form_preview); - var participantBtn = h('button.btn.btn-primary', Messages.form_geturl); + Messages.form_preview = "Preview form"; // XXX + Messages.form_geturl = "Copy link"; // XXX + var previewBtn = h('button.btn.btn-primary', [ + h('i.fa.fa-eye'), + Messages.form_preview + ]); + var participantBtn = h('button.btn.btn-primary',[ + h('i.fa.fa-link'), + Messages.form_geturl + ]); var preview = h('div.cp-forms-results-participant', [previewBtn, participantBtn]); $(previewBtn).click(function () { sframeChan.event('EV_OPEN_VIEW_URL'); From 7db03efdc509be9aba6606b6a5c19aae66efed92 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 18:08:32 +0200 Subject: [PATCH 035/183] Fix type errors when changing question type in forms --- www/form/inner.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 973b9e89c..150e80207 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1204,7 +1204,7 @@ define([ Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !answer.trim()) { return empty++; } + if (!answer || !answer.trim || !answer.trim()) { return empty++; } Util.inc(count, answer); }); @@ -1315,7 +1315,7 @@ define([ Object.keys(answer).forEach(function (q_uid) { var c = count[q_uid] = count[q_uid] || {}; var res = answer[q_uid]; - if (!res || !res.trim()) { return; } + if (!res || !res.trim || !res.trim()) { return; } Util.inc(c, res); }); }); @@ -1452,6 +1452,7 @@ define([ Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; + if (answer && typeof(answer) === "string") { answer = [answer]; } if (!Array.isArray(answer) || !answer.length) { return empty++; } answer.forEach(function (val) { Util.inc(count, val); @@ -1582,6 +1583,7 @@ define([ Object.keys(answer).forEach(function (q_uid) { var c = count[q_uid] = count[q_uid] || {}; var res = answer[q_uid]; + if (res && typeof(res) === "string") { res = [res]; } if (!Array.isArray(res) || !res.length) { return; } res.forEach(function (v) { Util.inc(c, v); @@ -1745,6 +1747,7 @@ define([ Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; + if (answer && typeof(answer) === "string") { answer = [answer]; } if (!Array.isArray(answer) || !answer.length) { return empty++; } answer.forEach(function (el, i) { var score = l - i; @@ -1769,6 +1772,7 @@ define([ if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } + if (APP.isEditor) { answers = {}; } var lines = makePollTable(answers, opts, false); var disabled = false; From 4e013520c5eb82c5d0cc70a82c331204984903ff Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 18:14:26 +0200 Subject: [PATCH 036/183] Fix forms issues --- www/form/inner.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 150e80207..d16a2b474 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1271,6 +1271,7 @@ define([ $el.find('input').each(function (i, input) { var $i = $(input); if (res[uid]) { return; } + res[uid] = undefined; if (Util.isChecked($i)) { res[uid] = $i.data('val'); } }); }); @@ -1420,7 +1421,10 @@ define([ }); return res; }, - reset: function () { $(tag).find('input').removeAttr('checked'); }, + reset: function () { + $(tag).find('input').removeAttr('checked'); + checkDisabled(); + }, setEditable: function (state) { if (state) { checkDisabled(); } else { $tag.find('input').attr('disabled', 'disabled'); } @@ -1543,7 +1547,10 @@ define([ }); return res; }, - reset: function () { $(tag).find('input').removeAttr('checked'); }, + reset: function () { + $(tag).find('input').removeAttr('checked'); + lines.forEach(checkDisabled); + }, setEditable: function (state) { if (state) { lines.forEach(checkDisabled); } else { $(tag).find('input').attr('disabled', 'disabled'); } From 5a8104e793fdd49bb1910dabb3920387338c49ce Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 18:16:53 +0200 Subject: [PATCH 037/183] lint compliance --- www/form/inner.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index d16a2b474..4120df1df 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -259,9 +259,9 @@ define([ if (placeholder) { input.placeholder = val; input.value = ''; - $(input).change(function () { - input.placeholder = ''; - $(input).off(change); + $(input).on('keypress', function () { + $(input).removeAttr('placeholder'); + $(input).off('keypress'); }); } if (uid) { $(input).data('uid', uid); } From e7eb79c4bc50998b003d396bd2ac63ec0569cefe Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 24 Aug 2021 11:23:15 +0200 Subject: [PATCH 038/183] More privacy settings for forms --- www/form/inner.js | 121 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 28 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 4120df1df..b4dd66ffb 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2223,6 +2223,10 @@ define([ }); return results; }; + + Messages.form_anonAnswer = "All answers to this form are anonymous"; // XXX + Messages.form_authAnswer = "You can't answer anonymously to this form"; // XXX + var makeFormControls = function (framework, content, update, evOnChange) { var loggedIn = framework._.sfCommon.isLoggedIn(); var metadataMgr = framework._.cpNfInner.metadataMgr; @@ -2234,23 +2238,45 @@ define([ var anonName, $anonName; cbox = UI.createCheckbox('cp-form-anonymous', Messages.form_anonymousBox, true, {}); - var $anonBox = $(cbox).find('input'); - if (loggedIn) { - if (!content.answers.anonymous || APP.cantAnon) { - $(cbox).hide().find('input').attr('disabled', 'disabled').prop('checked', false); + var $cbox = $(cbox); + var $anonBox = $cbox.find('input'); + if (content.answers.makeAnonymous) { + // If we make all answers anonymous, hide the checkbox and display a message + $cbox.hide(); + $anonBox.attr('disabled', 'disabled').prop('checked', true); + setTimeout(function () { + // We need to wait for cbox to be added into the DOM before using .after() + $cbox.after(h('div.alert.alert-info', Messages.form_anonAnswer)); + }); + } else if (content.answers.anonymous) { + // Answers aren't anonymous and guests are allowed + // Guests can set a username and logged in users can answer anonymously + if (!loggedIn) { + anonName = h('div.cp-form-anon-answer-input', [ + Messages.form_answerAs, + h('input', { + value: user.name || '', + placeholder: Messages.form_anonName + }) + ]); + $anonName = $(anonName).hide(); + $anonBox.on('change', function () { + if (Util.isChecked($anonBox)) { $anonName.hide(); } + else { $anonName.show(); } + }); + } else if (APP.cantAnon) { + // You've already answered with your credentials + $cbox.hide(); + $anonBox.attr('disabled', 'disabled').prop('checked', false); } } else { - anonName = h('div.cp-form-anon-answer-input', [ - Messages.form_answerAs, - h('input', { - value: user.name || '', - placeholder: Messages.form_anonName - }) - ]); - $anonName = $(anonName).hide(); - $anonBox.on('change', function () { - if (Util.isChecked($anonBox)) { $anonName.hide(); } - else { $anonName.show(); } + // Answers don't have to be anonymous and only logged in users can answer + // ==> they have to answer with their keys so we know their name too + $cbox.hide(); + $anonBox.attr('disabled', 'disabled').prop('checked', false); + setTimeout(function () { + // We need to wait for cbox to be added into the DOM before using .after() + $cbox.after(h('div.alert.alert-info', Messages.form_authAnswer)); }); } @@ -2270,7 +2296,7 @@ define([ if (!results) { return; } var user = metadataMgr.getUserData(); - if (!Util.isChecked($anonBox)) { + if (!Util.isChecked($anonBox) && !content.answers.makeAnonymous) { results._userdata = loggedIn ? { avatar: user.avatar, name: user.name, @@ -2286,7 +2312,8 @@ define([ sframeChan.query('Q_FORM_SUBMIT', { mailbox: content.answers, results: results, - anonymous: !loggedIn || Util.isChecked($(cbox).find('input')) + anonymous: content.answers.makeAnonymous || !loggedIn + || (Util.isChecked($anonBox) && !APP.cantAnon) // use ephemeral keys }, function (err, data) { $send.attr('disabled', 'disabled'); if (err || (data && data.error)) { @@ -2305,6 +2332,7 @@ define([ $send.removeAttr('disabled'); //UI.alert(Messages.form_sent); // XXX not needed anymore? $send.text(Messages.form_update); + APP.hasAnswered = true; showAnsweredPage(framework, content, { '_time': +new Date() }); }); }); @@ -2343,10 +2371,8 @@ define([ return h('li', a); }; - if (APP.checkInvalidEvt) { evOnChange.unreg(APP.checkInvalidEvt); } - if (APP.checkErrorEvt) { evOnChange.unreg(APP.checkErrorEvt); } // Check invalid inputs - APP.checkInvalidEvt = function () { + evOnChange.reg(function () { var $container = $('div.cp-form-creator-content'); var $inputs = $container.find('input:invalid'); if (!$inputs.length) { @@ -2364,9 +2390,9 @@ define([ list ]; $invalid.empty().append(content); - }; + }); // Check empty required questions - APP.checkErrorEvt = function () { + evOnChange.reg(function () { if (!Array.isArray(APP.formBlocks)) { return; } var form = content.form; var errorBlocks = APP.formBlocks.filter(function (data) { @@ -2395,9 +2421,7 @@ define([ list ]; $errors.empty().append(divContent); - }; - evOnChange.reg(APP.checkInvalidEvt); - evOnChange.reg(APP.checkErrorEvt); + }); evOnChange.fire(true); } @@ -2886,7 +2910,7 @@ define([ } // If the form is already submitted, show an info message - if (answers) { + if (APP.hasAnswered) { showAnsweredPage(framework, content, answers); $container.prepend(h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ @@ -2896,6 +2920,20 @@ define([ // In view mode, add "Submit" and "reset" buttons $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); + // In view mode, tell the user and answers are forced to be anonymous or authenticated + if (!APP.isEditor) { + var infoTxt; + var loggedIn = framework._.sfCommon.isLoggedIn(); + if (content.answers.makeAnonymous) { + infoTxt = Messages.form_anonAnswer; + } else if (!content.answers.anonymous && loggedIn) { + infoTxt = Messages.form_authAnswer; + } + if (infoTxt) { + $container.prepend(h('div.alert.alert-info', infoTxt)); + } + } + // Embed mode is enforced so we add the title at the top and a CryptPad logo // at the bottom var title = framework._.title.title || framework._.title.defaultTitle; @@ -3094,7 +3132,30 @@ define([ }; refreshResponse(); - // Allow anonymous answers + // Make answers anonymous + Messages.form_makeAnon = "Make all answers anonymous"; // XXX + var anonContainer = h('div.cp-form-anon-container'); + var $anon = $(anonContainer); + var refreshAnon = function () { + $anon.empty(); + var anonymous = content.answers.makeAnonymous; + var cbox = UI.createCheckbox('cp-form-make-anon', + Messages.form_makeAnon, anonymous, {}); + var radioContainer = h('div.cp-form-anon-radio', [cbox]); + var $r = $(radioContainer).find('input').on('change', function() { + var val = Util.isChecked($r); + content.answers.makeAnonymous = val; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + }); + }); + $anon.append(h('div.cp-form-actions', radioContainer)); + }; + refreshAnon(); + + // XXX UPDATE KEYS "form_anonyous_on", "form_anonymous_off" and "form_anonymous" + // Allow guest(anonymous) answers var privacyContainer = h('div.cp-form-privacy-container'); var $privacy = $(privacyContainer); var refreshPrivacy = function () { @@ -3216,6 +3277,7 @@ define([ evOnChange.reg(refreshPublic); evOnChange.reg(refreshPrivacy); + evOnChange.reg(refreshAnon); evOnChange.reg(refreshEditable); evOnChange.reg(refreshEndDate); //evOnChange.reg(refreshResponse); @@ -3224,6 +3286,7 @@ define([ preview, endDateContainer, privacyContainer, + anonContainer, editableContainer, resultsType, responseMsg @@ -3414,7 +3477,7 @@ define([ } // If the results are public and there is at least one doodle, fetch the results now - if (0 && content.answers.privateKey && Object.keys(content.form).some(function (uid) { + if (content.answers.privateKey && Object.keys(content.form).some(function (uid) { return content.form[uid].type === "poll"; })) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { @@ -3444,6 +3507,7 @@ define([ if (answers) { var myAnswersObj = answers[curve1] || answers[curve2] || undefined; if (myAnswersObj) { + APP.hasAnswered = true; myAnswers = myAnswersObj.msg; myAnswers._time = myAnswersObj.time; } @@ -3478,6 +3542,7 @@ define([ var answers; if (obj && !obj.error) { answers = obj; + APP.hasAnswered = true; // If we have a non-anon answer, we can't answer anonymously later if (!obj._isAnon) { APP.cantAnon = true; } From 83a1a2a337b81a299d255d7983dbcdafa169d330 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 24 Aug 2021 13:24:06 +0200 Subject: [PATCH 039/183] Improve Poll template for forms --- www/common/common-ui-elements.js | 7 ++++--- www/form/inner.js | 10 +++++----- www/form/templates.js | 25 ++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 8315768f1..d8b399851 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -649,7 +649,7 @@ define([ if (!AppConfig.enableTemplates) { return; } if (!common.isLoggedIn()) { return; } button = $('