diff --git a/packages/enketo-express/locales/src/en/translation.json b/packages/enketo-express/locales/src/en/translation.json index cf9c8493..962da4dd 100644 --- a/packages/enketo-express/locales/src/en/translation.json +++ b/packages/enketo-express/locales/src/en/translation.json @@ -272,7 +272,7 @@ "btn": "Finish Later" }, "done": { - "btn": "I'm done" + "btn": "I'm Done" } }, "geopicker": { diff --git a/packages/enketo-express/public/js/src/module/calculate.js b/packages/enketo-express/public/js/src/module/calculate.js index 2b8de62b..6c918a79 100644 --- a/packages/enketo-express/public/js/src/module/calculate.js +++ b/packages/enketo-express/public/js/src/module/calculate.js @@ -14,6 +14,15 @@ calculationModule._updateCalc = function (control, props, emptyNonRelevant) { !control.matches('[readonly]') ); + if (emptyNonRelevant) { + // We're temporarily disabling this cache because it is causing this bug + // https://github.com/OpenClinica/enketo-express-oc/issues/730 + // which we don't quite understand yet, but need to fix urgently. + // A proper fix would have to be created in enketo/enketo after which we should remove this clause + // (and improve performance). + this.preInitRelevance = new WeakMap(); + } + return this._originalUpdateCalc(control, props, emptyNonRelevant); }; diff --git a/packages/enketo-express/public/js/src/module/controller-webform-oc.js b/packages/enketo-express/public/js/src/module/controller-webform-oc.js index f4805cd9..cd94320b 100644 --- a/packages/enketo-express/public/js/src/module/controller-webform-oc.js +++ b/packages/enketo-express/public/js/src/module/controller-webform-oc.js @@ -153,7 +153,13 @@ function init(formEl, data, loadErrors = []) { ); // For Participant empty-form view in order to show Close button on all pages - if (settings.participant && settings.type !== 'edit') { + // add settings.reasonForChange condition to prevent Close button show on all pages + // for non Participant empty-form that does not have calculate items + if ( + settings.participant && + settings.reasonForChange && + settings.type !== 'edit' + ) { form.view.html.classList.add('empty-untouched'); } diff --git a/packages/enketo-express/public/js/src/module/form.js b/packages/enketo-express/public/js/src/module/form.js index 060555a7..64e73019 100644 --- a/packages/enketo-express/public/js/src/module/form.js +++ b/packages/enketo-express/public/js/src/module/form.js @@ -1,6 +1,8 @@ // Extend the Enketo Core Form class, and expose it for local testing. import { Form, FormModel } from 'enketo-core'; import events from 'enketo-core/src/js/event'; +import { scrollIntoViewIfNeeded } from 'enketo-core/src/js/dom-utils'; +import $ from 'jquery'; import config from 'enketo/config'; import gui from './gui'; import settings from './settings'; @@ -421,5 +423,50 @@ Form.prototype.updateValidityInUi = function (control, result) { } }; +Form.prototype.goToTarget = function (target, options = {}) { + if (target) { + if (this.pages.active && !options.isPageFlip) { + // Flip to page + this.pages.flipToPageContaining($(target)); + } + // check if the target has a form control + if (target.closest('.calculation, .setvalue, .setgeopoint')) { + // It is up to the apps to decide what to do with this event. + target.dispatchEvent(events.GoToInvisible()); + } + // check if the nearest question or group is irrelevant after page flip + if (target.closest('.or-branch.disabled')) { + // It is up to the apps to decide what to do with this event. + target.dispatchEvent(events.GoToIrrelevant()); + } + + // Focus on the first non .ignore form control which is not currently readonly. + // If the element is hidden (e.g. because it's been replaced by a widget), + // the focus event will not fire, so we also trigger an applyfocus event that widgets can listen for. + let selector; + if (target.closest('.question')) { + selector = + 'input:not(.ignore):not([readonly]), textarea:not(.ignore):not([readonly]), select:not(.ignore):not([readonly])'; + } else { + // For repeat DOM, prevent focus on DN dome when all element is readonly or ignore(#733) + selector = + '.question:not(.or-appearance-dn) input:not(.ignore):not([readonly]), .question:not(.or-appearance-dn) textarea:not(.ignore):not([readonly]), .question:not(.or-appearance-dn) select:not(.ignore):not([readonly])'; + } + + const input = target.querySelector(selector); + + if (input != null) { + input.focus(); + input.dispatchEvent(events.ApplyFocus()); + } + + // Scroll to element if needed. This will generally be a noop unless no + // focusable control was found (e.g. readonly question in pages mode). + scrollIntoViewIfNeeded(target); + } + + return !!target; +}; + /* eslint import/prefer-default-export: "off" */ export { Form }; diff --git a/packages/enketo-express/test/client/dn-widget.spec.js b/packages/enketo-express/test/client/dn-widget.spec.js index 5b41de50..7c01d05c 100644 --- a/packages/enketo-express/test/client/dn-widget.spec.js +++ b/packages/enketo-express/test/client/dn-widget.spec.js @@ -345,11 +345,11 @@ describe('DN object', () => { ['{"queries":[], "logs":[{"type": "comment"}]}', ''], // first has system user [ - '{"queries":[], "logs":[{"type": "comment", "status": "updated", "user": "root", "date_time":2}]}', + '{"queries":[], "logs":[{"type": "comment", "status": "updated", "user": "system", "date_time":2}]}', '', ], [ - '{"queries":[{"type": "comment", "status": "updated", "user": "root", "date_time":2}], "logs":[]}', + '{"queries":[{"type": "comment", "status": "updated", "user": "system", "date_time":2}], "logs":[]}', '', ], // first has empty user @@ -372,20 +372,20 @@ describe('DN object', () => { ], // first root ignored, next is taken [ - '{"queries":[], "logs":[{"type": "audit", "user": "root", "date_time":2},{"type": "comment", "user": "jen", "date_time":1}]}', + '{"queries":[], "logs":[{"type": "audit", "user": "system", "date_time":2},{"type": "comment", "user": "jen", "date_time":1}]}', 'jen', ], [ - '{"queries":[{"type": "audit", "user": "root", "date_time":2},{"type": "comment", "user": "jen", "date_time":1}], "logs":[]}', + '{"queries":[{"type": "audit", "user": "system", "date_time":2},{"type": "comment", "user": "jen", "date_time":1}], "logs":[]}', 'jen', ], // same, but switched order (same date_time) to test ordering [ - '{"queries":[], "logs":[{"type": "comment", "user": "jen", "date_time":1}, {"type": "audit", "user": "root", "date_time":2}]}', + '{"queries":[], "logs":[{"type": "comment", "user": "jen", "date_time":1}, {"type": "audit", "user": "system", "date_time":2}]}', 'jen', ], [ - '{"queries":[{"type": "comment", "user": "jen", "date_time":1}, {"type": "audit", "user": "root", "date_time":2}], "logs":[]}', + '{"queries":[{"type": "comment", "user": "jen", "date_time":1}, {"type": "audit", "user": "system", "date_time":2}], "logs":[]}', 'jen', ], ].forEach((test) => { diff --git a/packages/enketo-express/widget/discrepancy-note/dn-widget.js b/packages/enketo-express/widget/discrepancy-note/dn-widget.js index 94df0566..8fffefd8 100644 --- a/packages/enketo-express/widget/discrepancy-note/dn-widget.js +++ b/packages/enketo-express/widget/discrepancy-note/dn-widget.js @@ -12,7 +12,7 @@ import reasons from '../../public/js/src/module/reasons'; let currentUser; let users; let annotationIconDataUri; -const SYSTEM_USER = 'root'; +const SYSTEM_USER = 'system'; const pad2 = (x) => (x < 10 ? `0${x}` : x); @@ -404,7 +404,7 @@ class Comment extends Widget { : t('widget.dn.fileremoved'); } - this._addAudit(comment, '', false); + this._addAudit(this._decodeHtml(comment), '', false); if (settings.reasonForChange && !this.linkedQuestionReadonly) { const reasonQuestion = reasons.addField(this.linkedQuestion); @@ -1602,6 +1602,20 @@ class Comment extends Widget { ); } + _decodeHtml(str) { + return str.replace( + /(&)|(<)|(>)|(')|(")/g, + (tag) => + ({ + '&': '&', + '<': '<', + '>': '>', + ''': "'", + '"': '"', + })[tag] + ); + } + _getHistoryRow(item, options = {}) { const types = { comment: ' ',