From 5a2624c47236f5ee3530543884ac0a1a5c787330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Thu, 4 Nov 2021 12:09:30 +0100 Subject: [PATCH] MDL-72952 reportbuilder: UX behaviour improvements in audiences - Show a toast notification when saving an audience - Add form change checker when adding a new audience to prevent user from navigating away if it is not saved - Remove expand/collapse animation in audience sidebar to be consistent with editor Co-authored-By: Paul Holden --- lang/en/reportbuilder.php | 1 + reportbuilder/amd/build/audience.min.js | 2 +- reportbuilder/amd/build/audience.min.js.map | 2 +- reportbuilder/amd/src/audience.js | 8 +- .../templates/local/audience/form.mustache | 16 +++- .../templates/local/settings/area.mustache | 2 +- reportbuilder/templates/toggle_card.mustache | 2 +- reportbuilder/tests/behat/audience.feature | 80 ++++++++++++++----- theme/boost/scss/moodle/reportbuilder.scss | 8 -- theme/boost/style/moodle.css | 11 --- theme/classic/style/moodle.css | 11 --- 11 files changed, 86 insertions(+), 57 deletions(-) diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index 472c2e68b73..c90564fc0d8 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -47,6 +47,7 @@ $string['audienceadded'] = 'Added audience \'{$a}\''; $string['audiencedeleted'] = 'Deleted audience \'{$a}\''; $string['audiencemultiselectpostfix'] = '{$a->elements} plus {$a->morecount} more'; $string['audiencenotsaved'] = 'Audience not saved'; +$string['audiencesaved'] = 'Audience saved'; $string['columnadded'] = 'Added column \'{$a}\''; $string['columnaggregated'] = 'Aggregated column \'{$a}\''; $string['columndeleted'] = 'Deleted column \'{$a}\''; diff --git a/reportbuilder/amd/build/audience.min.js b/reportbuilder/amd/build/audience.min.js index 91bd4970aaa..3fc500e7c7d 100644 --- a/reportbuilder/amd/build/audience.min.js +++ b/reportbuilder/amd/build/audience.min.js @@ -1,2 +1,2 @@ -function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_reportbuilder/audience",["exports","core/templates","core/notification","core/pending","core/str","core_form/dynamicform","core/toast","core_reportbuilder/local/repository/audiences","core_reportbuilder/local/selectors","core/fragment"],function(a,b,c,d,e,f,g,h,i,j){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=m(b);c=m(c);d=m(d);f=m(f);i=l(i);function k(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;k=function(){return a};return a}function l(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=k();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function m(a){return a&&a.__esModule?a:{default:a}}function n(a,b){return s(a)||r(a,b)||p(a,b)||o()}function o(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function p(a,b){if(!a)return;if("string"==typeof a)return q(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return q(a,b)}function q(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Report builder audiences\n *\n * @module core_reportbuilder/audience\n * @copyright 2021 David Matamoros \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_string as getString, get_strings as getStrings} from 'core/str';\nimport DynamicForm from 'core_form/dynamicform';\nimport {add as addToast} from 'core/toast';\nimport {deleteAudience} from 'core_reportbuilder/local/repository/audiences';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {loadFragment} from 'core/fragment';\n\nlet reportId = 0;\nlet contextId = 0;\n\n/**\n * Add audience card\n *\n * @param {String} className\n * @param {String} title\n */\nconst addAudienceCard = (className, title) => {\n const pendingPromise = new Pending('core_reportbuilder/audience:add');\n\n const audiencesContainer = document.querySelector(reportSelectors.regions.audiencesContainer);\n const audienceCardLength = audiencesContainer.querySelectorAll(reportSelectors.regions.audienceCard).length;\n\n const params = {\n classname: className,\n reportid: reportId,\n showormessage: (audienceCardLength > 0),\n title: title,\n };\n\n // Load audience card fragment, render and then initialise the form within.\n loadFragment('core_reportbuilder', 'audience_form', contextId, params)\n .then((html, js) => {\n const audienceCard = Templates.appendNodeContents(audiencesContainer, html, js)[0];\n const audienceEmptyMessage = audiencesContainer.querySelector(reportSelectors.regions.audienceEmptyMessage);\n\n initAudienceCardForm(audienceCard);\n audienceEmptyMessage.classList.add('hidden');\n\n return getString('audienceadded', 'core_reportbuilder', title);\n })\n .then(addToast)\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n};\n\n/**\n * Edit audience card\n *\n * @param {Element} audienceCard\n */\nconst editAudienceCard = audienceCard => {\n const pendingPromise = new Pending('core_reportbuilder/audience:edit');\n\n const audienceForm = initAudienceCardForm(audienceCard);\n const audienceFormData = {\n reportid: reportId,\n id: audienceCard.dataset.instanceid,\n classname: audienceCard.dataset.classname\n };\n\n // Load audience form with data for editing, then toggle visible controls in the card.\n audienceForm.load(audienceFormData)\n .then(() => {\n const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);\n const audienceDescription = audienceCard.querySelector(reportSelectors.regions.audienceDescription);\n const audienceEdit = audienceCard.querySelector(reportSelectors.actions.audienceEdit);\n\n audienceFormContainer.classList.remove('hidden');\n audienceDescription.classList.add('hidden');\n audienceEdit.disabled = true;\n\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n};\n\n/**\n * Initialise dynamic form within given audience card\n *\n * @param {Element} audienceCard\n * @return {DynamicForm}\n */\nconst initAudienceCardForm = audienceCard => {\n const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);\n const audienceForm = new DynamicForm(audienceFormContainer, '\\\\core_reportbuilder\\\\form\\\\audience');\n\n // After submitting the form, update the card instance and description properties.\n audienceForm.addEventListener(audienceForm.events.FORM_SUBMITTED, data => {\n const audienceDescription = audienceCard.querySelector(reportSelectors.regions.audienceDescription);\n\n audienceCard.dataset.instanceid = data.detail.instanceid;\n audienceDescription.innerHTML = data.detail.description;\n\n closeAudienceCardForm(audienceCard);\n });\n\n // If cancelling the form, close the card or remove it if it was never created.\n audienceForm.addEventListener(audienceForm.events.FORM_CANCELLED, () => {\n if (audienceCard.dataset.instanceid > 0) {\n closeAudienceCardForm(audienceCard);\n } else {\n removeAudienceCard(audienceCard);\n }\n });\n\n return audienceForm;\n};\n\n/**\n * Delete audience card\n *\n * @param {Element} audienceCard\n */\nconst deleteAudienceCard = audienceCard => {\n const audienceTitle = audienceCard.dataset.title;\n\n getStrings([\n {key: 'deleteaudience', component: 'core_reportbuilder', param: audienceTitle},\n {key: 'deleteaudienceconfirm', component: 'core_reportbuilder', param: audienceTitle},\n {key: 'delete', component: 'moodle'},\n ]).then(([confirmTitle, confirmText, confirmButton]) => {\n Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => {\n const pendingPromise = new Pending('core_reportbuilder/audience:delete');\n\n deleteAudience(reportId, audienceCard.dataset.instanceid)\n .then(() => getString('audiencedeleted', 'core_reportbuilder', audienceTitle))\n .then(addToast)\n .then(() => {\n removeAudienceCard(audienceCard);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Close audience card form\n *\n * @param {Element} audienceCard\n */\nconst closeAudienceCardForm = audienceCard => {\n // Remove the [data-region=\"audience-form-container\"] (with all the event listeners attached to it), and create it again.\n const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);\n const NewAudienceFormContainer = audienceFormContainer.cloneNode(false);\n audienceCard.querySelector(reportSelectors.regions.audienceForm).replaceChild(NewAudienceFormContainer, audienceFormContainer);\n // Show the description container and enable the action buttons.\n audienceCard.querySelector(reportSelectors.regions.audienceDescription).classList.remove('hidden');\n audienceCard.querySelector(reportSelectors.actions.audienceEdit).disabled = false;\n audienceCard.querySelector(reportSelectors.actions.audienceDelete).disabled = false;\n};\n\n/**\n * Remove audience card\n *\n * @param {Element} audienceCard\n */\nconst removeAudienceCard = audienceCard => {\n audienceCard.remove();\n\n const audiencesContainer = document.querySelector(reportSelectors.regions.audiencesContainer);\n const audienceCards = audiencesContainer.querySelectorAll(reportSelectors.regions.audienceCard);\n\n // Show message if there are no cards remaining, ensure first card's separator is not present.\n if (audienceCards.length === 0) {\n const audienceEmptyMessage = document.querySelector(reportSelectors.regions.audienceEmptyMessage);\n audienceEmptyMessage.classList.remove('hidden');\n } else {\n const audienceFirstCardSeparator = audienceCards[0].querySelector('.audience-separator');\n audienceFirstCardSeparator?.remove();\n }\n};\n\nlet initialized = false;\n\n/**\n * Initialise audiences tab.\n *\n * @param {Number} id\n * @param {Number} contextid\n */\nexport const init = (id, contextid) => {\n reportId = id;\n contextId = contextid;\n\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n document.addEventListener('click', event => {\n\n // Add instance.\n const audienceAdd = event.target.closest(reportSelectors.actions.audienceAdd);\n if (audienceAdd) {\n event.preventDefault();\n addAudienceCard(audienceAdd.dataset.uniqueIdentifier, audienceAdd.dataset.name);\n }\n\n // Edit instance.\n const audienceEdit = event.target.closest(reportSelectors.actions.audienceEdit);\n if (audienceEdit) {\n const audienceEditCard = audienceEdit.closest(reportSelectors.regions.audienceCard);\n\n event.preventDefault();\n editAudienceCard(audienceEditCard);\n }\n\n // Delete instance.\n const audienceDelete = event.target.closest(reportSelectors.actions.audienceDelete);\n if (audienceDelete) {\n const audienceDeleteCard = audienceDelete.closest(reportSelectors.regions.audienceCard);\n\n event.preventDefault();\n deleteAudienceCard(audienceDeleteCard);\n }\n });\n\n initialized = true;\n};\n"],"file":"audience.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/audience.js"],"names":["reportId","contextId","addAudienceCard","className","title","pendingPromise","Pending","audiencesContainer","document","querySelector","reportSelectors","regions","audienceCardLength","querySelectorAll","audienceCard","length","params","classname","reportid","showormessage","then","html","js","Templates","appendNodeContents","audienceEmptyMessage","audienceForm","initAudienceCardForm","getFormNode","classList","add","addToast","resolve","catch","Notification","exception","editAudienceCard","audienceFormData","id","dataset","instanceid","load","audienceFormContainer","audienceDescription","audienceEdit","actions","remove","disabled","DynamicForm","addEventListener","events","FORM_SUBMITTED","data","detail","innerHTML","description","closeAudienceCardForm","FORM_CANCELLED","removeAudienceCard","deleteAudienceCard","audienceTitle","key","component","param","confirmTitle","confirmText","confirmButton","confirm","NewAudienceFormContainer","cloneNode","replaceChild","audienceDelete","audienceCards","audienceFirstCardSeparator","initialized","init","contextid","event","audienceAdd","target","closest","preventDefault","uniqueIdentifier","name","audienceEditCard","audienceDeleteCard"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Report builder audiences\n *\n * @module core_reportbuilder/audience\n * @copyright 2021 David Matamoros \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_string as getString, get_strings as getStrings} from 'core/str';\nimport DynamicForm from 'core_form/dynamicform';\nimport {add as addToast} from 'core/toast';\nimport {deleteAudience} from 'core_reportbuilder/local/repository/audiences';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {loadFragment} from 'core/fragment';\nimport {markFormAsDirty} from 'core_form/changechecker';\n\nlet reportId = 0;\nlet contextId = 0;\n\n/**\n * Add audience card\n *\n * @param {String} className\n * @param {String} title\n */\nconst addAudienceCard = (className, title) => {\n const pendingPromise = new Pending('core_reportbuilder/audience:add');\n\n const audiencesContainer = document.querySelector(reportSelectors.regions.audiencesContainer);\n const audienceCardLength = audiencesContainer.querySelectorAll(reportSelectors.regions.audienceCard).length;\n\n const params = {\n classname: className,\n reportid: reportId,\n showormessage: (audienceCardLength > 0),\n title: title,\n };\n\n // Load audience card fragment, render and then initialise the form within.\n loadFragment('core_reportbuilder', 'audience_form', contextId, params)\n .then((html, js) => {\n const audienceCard = Templates.appendNodeContents(audiencesContainer, html, js)[0];\n const audienceEmptyMessage = audiencesContainer.querySelector(reportSelectors.regions.audienceEmptyMessage);\n\n const audienceForm = initAudienceCardForm(audienceCard);\n // Mark as dirty new audience form created to prevent users leaving the page without saving it.\n markFormAsDirty(audienceForm.getFormNode());\n audienceEmptyMessage.classList.add('hidden');\n\n return getString('audienceadded', 'core_reportbuilder', title);\n })\n .then(addToast)\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n};\n\n/**\n * Edit audience card\n *\n * @param {Element} audienceCard\n */\nconst editAudienceCard = audienceCard => {\n const pendingPromise = new Pending('core_reportbuilder/audience:edit');\n\n const audienceForm = initAudienceCardForm(audienceCard);\n const audienceFormData = {\n reportid: reportId,\n id: audienceCard.dataset.instanceid,\n classname: audienceCard.dataset.classname\n };\n\n // Load audience form with data for editing, then toggle visible controls in the card.\n audienceForm.load(audienceFormData)\n .then(() => {\n const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);\n const audienceDescription = audienceCard.querySelector(reportSelectors.regions.audienceDescription);\n const audienceEdit = audienceCard.querySelector(reportSelectors.actions.audienceEdit);\n\n audienceFormContainer.classList.remove('hidden');\n audienceDescription.classList.add('hidden');\n audienceEdit.disabled = true;\n\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n};\n\n/**\n * Initialise dynamic form within given audience card\n *\n * @param {Element} audienceCard\n * @return {DynamicForm}\n */\nconst initAudienceCardForm = audienceCard => {\n const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);\n const audienceForm = new DynamicForm(audienceFormContainer, '\\\\core_reportbuilder\\\\form\\\\audience');\n\n // After submitting the form, update the card instance and description properties.\n audienceForm.addEventListener(audienceForm.events.FORM_SUBMITTED, data => {\n const audienceDescription = audienceCard.querySelector(reportSelectors.regions.audienceDescription);\n\n audienceCard.dataset.instanceid = data.detail.instanceid;\n audienceDescription.innerHTML = data.detail.description;\n\n closeAudienceCardForm(audienceCard);\n\n return getString('audiencesaved', 'core_reportbuilder')\n .then(addToast);\n });\n\n // If cancelling the form, close the card or remove it if it was never created.\n audienceForm.addEventListener(audienceForm.events.FORM_CANCELLED, () => {\n if (audienceCard.dataset.instanceid > 0) {\n closeAudienceCardForm(audienceCard);\n } else {\n removeAudienceCard(audienceCard);\n }\n });\n\n return audienceForm;\n};\n\n/**\n * Delete audience card\n *\n * @param {Element} audienceCard\n */\nconst deleteAudienceCard = audienceCard => {\n const audienceTitle = audienceCard.dataset.title;\n\n getStrings([\n {key: 'deleteaudience', component: 'core_reportbuilder', param: audienceTitle},\n {key: 'deleteaudienceconfirm', component: 'core_reportbuilder', param: audienceTitle},\n {key: 'delete', component: 'moodle'},\n ]).then(([confirmTitle, confirmText, confirmButton]) => {\n Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => {\n const pendingPromise = new Pending('core_reportbuilder/audience:delete');\n\n deleteAudience(reportId, audienceCard.dataset.instanceid)\n .then(() => getString('audiencedeleted', 'core_reportbuilder', audienceTitle))\n .then(addToast)\n .then(() => {\n removeAudienceCard(audienceCard);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Close audience card form\n *\n * @param {Element} audienceCard\n */\nconst closeAudienceCardForm = audienceCard => {\n // Remove the [data-region=\"audience-form-container\"] (with all the event listeners attached to it), and create it again.\n const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);\n const NewAudienceFormContainer = audienceFormContainer.cloneNode(false);\n audienceCard.querySelector(reportSelectors.regions.audienceForm).replaceChild(NewAudienceFormContainer, audienceFormContainer);\n // Show the description container and enable the action buttons.\n audienceCard.querySelector(reportSelectors.regions.audienceDescription).classList.remove('hidden');\n audienceCard.querySelector(reportSelectors.actions.audienceEdit).disabled = false;\n audienceCard.querySelector(reportSelectors.actions.audienceDelete).disabled = false;\n};\n\n/**\n * Remove audience card\n *\n * @param {Element} audienceCard\n */\nconst removeAudienceCard = audienceCard => {\n audienceCard.remove();\n\n const audiencesContainer = document.querySelector(reportSelectors.regions.audiencesContainer);\n const audienceCards = audiencesContainer.querySelectorAll(reportSelectors.regions.audienceCard);\n\n // Show message if there are no cards remaining, ensure first card's separator is not present.\n if (audienceCards.length === 0) {\n const audienceEmptyMessage = document.querySelector(reportSelectors.regions.audienceEmptyMessage);\n audienceEmptyMessage.classList.remove('hidden');\n } else {\n const audienceFirstCardSeparator = audienceCards[0].querySelector('.audience-separator');\n audienceFirstCardSeparator?.remove();\n }\n};\n\nlet initialized = false;\n\n/**\n * Initialise audiences tab.\n *\n * @param {Number} id\n * @param {Number} contextid\n */\nexport const init = (id, contextid) => {\n reportId = id;\n contextId = contextid;\n\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n document.addEventListener('click', event => {\n\n // Add instance.\n const audienceAdd = event.target.closest(reportSelectors.actions.audienceAdd);\n if (audienceAdd) {\n event.preventDefault();\n addAudienceCard(audienceAdd.dataset.uniqueIdentifier, audienceAdd.dataset.name);\n }\n\n // Edit instance.\n const audienceEdit = event.target.closest(reportSelectors.actions.audienceEdit);\n if (audienceEdit) {\n const audienceEditCard = audienceEdit.closest(reportSelectors.regions.audienceCard);\n\n event.preventDefault();\n editAudienceCard(audienceEditCard);\n }\n\n // Delete instance.\n const audienceDelete = event.target.closest(reportSelectors.actions.audienceDelete);\n if (audienceDelete) {\n const audienceDeleteCard = audienceDelete.closest(reportSelectors.regions.audienceCard);\n\n event.preventDefault();\n deleteAudienceCard(audienceDeleteCard);\n }\n });\n\n initialized = true;\n};\n"],"file":"audience.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/src/audience.js b/reportbuilder/amd/src/audience.js index 79e3ba9a3ac..73ec20d3cc7 100644 --- a/reportbuilder/amd/src/audience.js +++ b/reportbuilder/amd/src/audience.js @@ -32,6 +32,7 @@ import {add as addToast} from 'core/toast'; import {deleteAudience} from 'core_reportbuilder/local/repository/audiences'; import * as reportSelectors from 'core_reportbuilder/local/selectors'; import {loadFragment} from 'core/fragment'; +import {markFormAsDirty} from 'core_form/changechecker'; let reportId = 0; let contextId = 0; @@ -61,7 +62,9 @@ const addAudienceCard = (className, title) => { const audienceCard = Templates.appendNodeContents(audiencesContainer, html, js)[0]; const audienceEmptyMessage = audiencesContainer.querySelector(reportSelectors.regions.audienceEmptyMessage); - initAudienceCardForm(audienceCard); + const audienceForm = initAudienceCardForm(audienceCard); + // Mark as dirty new audience form created to prevent users leaving the page without saving it. + markFormAsDirty(audienceForm.getFormNode()); audienceEmptyMessage.classList.add('hidden'); return getString('audienceadded', 'core_reportbuilder', title); @@ -120,6 +123,9 @@ const initAudienceCardForm = audienceCard => { audienceDescription.innerHTML = data.detail.description; closeAudienceCardForm(audienceCard); + + return getString('audiencesaved', 'core_reportbuilder') + .then(addToast); }); // If cancelling the form, close the card or remove it if it was never created. diff --git a/reportbuilder/templates/local/audience/form.mustache b/reportbuilder/templates/local/audience/form.mustache index 538f851a486..e34ee43ebe2 100644 --- a/reportbuilder/templates/local/audience/form.mustache +++ b/reportbuilder/templates/local/audience/form.mustache @@ -45,13 +45,21 @@ {{#canedit}} - {{/canedit}} {{#candelete}} - {{/candelete}} diff --git a/reportbuilder/templates/local/settings/area.mustache b/reportbuilder/templates/local/settings/area.mustache index 2ff5afcd0c1..92d755e4e40 100644 --- a/reportbuilder/templates/local/settings/area.mustache +++ b/reportbuilder/templates/local/settings/area.mustache @@ -72,7 +72,7 @@ } }} -