| 1 | efrain | 1 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 2 | //
 | 
        
           |  |  | 3 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 4 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 5 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 6 | // (at your option) any later version.
 | 
        
           |  |  | 7 | //
 | 
        
           |  |  | 8 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 11 | // GNU General Public License for more details.
 | 
        
           |  |  | 12 | //
 | 
        
           |  |  | 13 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 14 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 15 |   | 
        
           |  |  | 16 | /**
 | 
        
           |  |  | 17 |  * Report builder conditions editor
 | 
        
           |  |  | 18 |  *
 | 
        
           |  |  | 19 |  * @module      core_reportbuilder/local/editor/conditions
 | 
        
           |  |  | 20 |  * @copyright   2021 Paul Holden <paulh@moodle.com>
 | 
        
           |  |  | 21 |  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 22 |  */
 | 
        
           |  |  | 23 |   | 
        
           |  |  | 24 | "use strict";
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 | import $ from 'jquery';
 | 
        
           |  |  | 27 | import {dispatchEvent} from 'core/event_dispatcher';
 | 
        
           |  |  | 28 | import AutoComplete from 'core/form-autocomplete';
 | 
        
           |  |  | 29 | import 'core/inplace_editable';
 | 
        
           |  |  | 30 | import Notification from 'core/notification';
 | 
        
           |  |  | 31 | import Pending from 'core/pending';
 | 
        
           |  |  | 32 | import {prefetchStrings} from 'core/prefetch';
 | 
        
           |  |  | 33 | import SortableList from 'core/sortable_list';
 | 
        
           |  |  | 34 | import {getString} from 'core/str';
 | 
        
           |  |  | 35 | import Templates from 'core/templates';
 | 
        
           |  |  | 36 | import {add as addToast} from 'core/toast';
 | 
        
           |  |  | 37 | import DynamicForm from 'core_form/dynamicform';
 | 
        
           |  |  | 38 | import * as reportEvents from 'core_reportbuilder/local/events';
 | 
        
           |  |  | 39 | import * as reportSelectors from 'core_reportbuilder/local/selectors';
 | 
        
           |  |  | 40 | import {addCondition, deleteCondition, reorderCondition, resetConditions} from 'core_reportbuilder/local/repository/conditions';
 | 
        
           |  |  | 41 |   | 
        
           |  |  | 42 | /**
 | 
        
           |  |  | 43 |  * Reload conditions settings region
 | 
        
           |  |  | 44 |  *
 | 
        
           |  |  | 45 |  * @param {Element} reportElement
 | 
        
           |  |  | 46 |  * @param {Object} templateContext
 | 
        
           |  |  | 47 |  * @return {Promise}
 | 
        
           |  |  | 48 |  */
 | 
        
           |  |  | 49 | const reloadSettingsConditionsRegion = (reportElement, templateContext) => {
 | 
        
           |  |  | 50 |     const pendingPromise = new Pending('core_reportbuilder/conditions:reload');
 | 
        
           |  |  | 51 |     const settingsConditionsRegion = reportElement.querySelector(reportSelectors.regions.settingsConditions);
 | 
        
           |  |  | 52 |   | 
        
           |  |  | 53 |     return Templates.renderForPromise('core_reportbuilder/local/settings/conditions', {conditions: templateContext})
 | 
        
           |  |  | 54 |         .then(({html, js}) => {
 | 
        
           |  |  | 55 |             const conditionsjs = $.parseHTML(templateContext.javascript, null, true).map(node => node.innerHTML).join("\n");
 | 
        
           |  |  | 56 |             Templates.replaceNode(settingsConditionsRegion, html, js + conditionsjs);
 | 
        
           |  |  | 57 |   | 
        
           |  |  | 58 |             initConditionsForm();
 | 
        
           |  |  | 59 |   | 
        
           |  |  | 60 |             // Re-focus the add condition element after reloading the region.
 | 
        
           |  |  | 61 |             const reportAddCondition = reportElement.querySelector(reportSelectors.actions.reportAddCondition);
 | 
        
           |  |  | 62 |             reportAddCondition?.focus();
 | 
        
           |  |  | 63 |   | 
        
           |  |  | 64 |             return pendingPromise.resolve();
 | 
        
           |  |  | 65 |         });
 | 
        
           |  |  | 66 | };
 | 
        
           |  |  | 67 |   | 
        
           |  |  | 68 | /**
 | 
        
           |  |  | 69 |  * Initialise conditions form, must be called on each init because the form container is re-created when switching editor modes
 | 
        
           |  |  | 70 |  */
 | 
        
           |  |  | 71 | const initConditionsForm = () => {
 | 
        
           |  |  | 72 |     const reportElement = document.querySelector(reportSelectors.regions.report);
 | 
        
           |  |  | 73 |   | 
        
           |  |  | 74 |     // Enhance condition selector.
 | 
        
           |  |  | 75 |     const reportAddCondition = reportElement.querySelector(reportSelectors.actions.reportAddCondition);
 | 
        
           |  |  | 76 |     AutoComplete.enhanceField(reportAddCondition, false, '', getString('selectacondition', 'core_reportbuilder'))
 | 
        
           |  |  | 77 |         .catch(Notification.exception);
 | 
        
           |  |  | 78 |   | 
        
           |  |  | 79 |     // Handle dynamic conditions form.
 | 
        
           |  |  | 80 |     const conditionFormContainer = reportElement.querySelector(reportSelectors.regions.settingsConditions);
 | 
        
           |  |  | 81 |     if (!conditionFormContainer) {
 | 
        
           |  |  | 82 |         return;
 | 
        
           |  |  | 83 |     }
 | 
        
           |  |  | 84 |     const conditionForm = new DynamicForm(conditionFormContainer, '\\core_reportbuilder\\form\\condition');
 | 
        
           |  |  | 85 |   | 
        
           |  |  | 86 |     // Submit report conditions.
 | 
        
           |  |  | 87 |     conditionForm.addEventListener(conditionForm.events.FORM_SUBMITTED, event => {
 | 
        
           |  |  | 88 |         event.preventDefault();
 | 
        
           |  |  | 89 |   | 
        
           |  |  | 90 |         getString('conditionsapplied', 'core_reportbuilder')
 | 
        
           |  |  | 91 |             .then(addToast)
 | 
        
           |  |  | 92 |             .catch(Notification.exception);
 | 
        
           |  |  | 93 |   | 
        
           |  |  | 94 |         // After the form has been submitted, we should trigger report table reload.
 | 
        
           |  |  | 95 |         dispatchEvent(reportEvents.tableReload, {}, reportElement);
 | 
        
           |  |  | 96 |     });
 | 
        
           |  |  | 97 |   | 
        
           |  |  | 98 |     // Reset report conditions.
 | 
        
           |  |  | 99 |     conditionForm.addEventListener(conditionForm.events.NOSUBMIT_BUTTON_PRESSED, event => {
 | 
        
           |  |  | 100 |         event.preventDefault();
 | 
        
           |  |  | 101 |   | 
        
           |  |  | 102 |         Notification.saveCancelPromise(
 | 
        
           |  |  | 103 |             getString('resetconditions', 'core_reportbuilder'),
 | 
        
           |  |  | 104 |             getString('resetconditionsconfirm', 'core_reportbuilder'),
 | 
        
           |  |  | 105 |             getString('resetall', 'core_reportbuilder'),
 | 
        
           |  |  | 106 |             {triggerElement: event.detail}
 | 
        
           |  |  | 107 |         ).then(() => {
 | 
        
           |  |  | 108 |             const pendingPromise = new Pending('core_reportbuilder/conditions:reset');
 | 
        
           |  |  | 109 |   | 
        
           |  |  | 110 |             return resetConditions(reportElement.dataset.reportId)
 | 
        
           |  |  | 111 |                 .then(data => reloadSettingsConditionsRegion(reportElement, data))
 | 
        
           |  |  | 112 |                 .then(() => addToast(getString('conditionsreset', 'core_reportbuilder')))
 | 
        
           |  |  | 113 |                 .then(() => {
 | 
        
           |  |  | 114 |                     dispatchEvent(reportEvents.tableReload, {}, reportElement);
 | 
        
           |  |  | 115 |                     return pendingPromise.resolve();
 | 
        
           |  |  | 116 |                 })
 | 
        
           |  |  | 117 |                 .catch(Notification.exception);
 | 
        
           |  |  | 118 |         }).catch(() => {
 | 
        
           |  |  | 119 |             return;
 | 
        
           |  |  | 120 |         });
 | 
        
           |  |  | 121 |     });
 | 
        
           |  |  | 122 | };
 | 
        
           |  |  | 123 |   | 
        
           |  |  | 124 | /**
 | 
        
           |  |  | 125 |  * Initialise module, prefetch all required strings
 | 
        
           |  |  | 126 |  *
 | 
        
           |  |  | 127 |  * @param {Boolean} initialized Ensure we only add our listeners once
 | 
        
           |  |  | 128 |  */
 | 
        
           |  |  | 129 | export const init = initialized => {
 | 
        
           |  |  | 130 |     prefetchStrings('core_reportbuilder', [
 | 
        
           |  |  | 131 |         'conditionadded',
 | 
        
           |  |  | 132 |         'conditiondeleted',
 | 
        
           |  |  | 133 |         'conditionmoved',
 | 
        
           |  |  | 134 |         'conditionsapplied',
 | 
        
           |  |  | 135 |         'conditionsreset',
 | 
        
           |  |  | 136 |         'deletecondition',
 | 
        
           |  |  | 137 |         'deleteconditionconfirm',
 | 
        
           |  |  | 138 |         'resetall',
 | 
        
           |  |  | 139 |         'resetconditions',
 | 
        
           |  |  | 140 |         'resetconditionsconfirm',
 | 
        
           |  |  | 141 |         'selectacondition',
 | 
        
           |  |  | 142 |     ]);
 | 
        
           |  |  | 143 |   | 
        
           |  |  | 144 |     prefetchStrings('core', [
 | 
        
           |  |  | 145 |         'delete',
 | 
        
           |  |  | 146 |     ]);
 | 
        
           |  |  | 147 |   | 
        
           |  |  | 148 |     initConditionsForm();
 | 
        
           |  |  | 149 |     if (initialized) {
 | 
        
           |  |  | 150 |         return;
 | 
        
           |  |  | 151 |     }
 | 
        
           |  |  | 152 |   | 
        
           |  |  | 153 |     // Add condition to report.
 | 
        
           |  |  | 154 |     document.addEventListener('change', event => {
 | 
        
           |  |  | 155 |         const reportAddCondition = event.target.closest(reportSelectors.actions.reportAddCondition);
 | 
        
           |  |  | 156 |         if (reportAddCondition) {
 | 
        
           |  |  | 157 |             event.preventDefault();
 | 
        
           |  |  | 158 |   | 
        
           |  |  | 159 |             // Check if dropdown is closed with no condition selected.
 | 
        
           |  |  | 160 |             if (reportAddCondition.value === "" || reportAddCondition.value === "0") {
 | 
        
           |  |  | 161 |                 return;
 | 
        
           |  |  | 162 |             }
 | 
        
           |  |  | 163 |   | 
        
           |  |  | 164 |             const reportElement = reportAddCondition.closest(reportSelectors.regions.report);
 | 
        
           |  |  | 165 |             const pendingPromise = new Pending('core_reportbuilder/conditions:add');
 | 
        
           |  |  | 166 |   | 
        
           |  |  | 167 |             addCondition(reportElement.dataset.reportId, reportAddCondition.value)
 | 
        
           |  |  | 168 |                 .then(data => reloadSettingsConditionsRegion(reportElement, data))
 | 
        
           |  |  | 169 |                 .then(() => getString('conditionadded', 'core_reportbuilder',
 | 
        
           |  |  | 170 |                     reportAddCondition.options[reportAddCondition.selectedIndex].text))
 | 
        
           |  |  | 171 |                 .then(addToast)
 | 
        
           |  |  | 172 |                 .then(() => {
 | 
        
           |  |  | 173 |                     dispatchEvent(reportEvents.tableReload, {}, reportElement);
 | 
        
           |  |  | 174 |                     return pendingPromise.resolve();
 | 
        
           |  |  | 175 |                 })
 | 
        
           |  |  | 176 |                 .catch(Notification.exception);
 | 
        
           |  |  | 177 |         }
 | 
        
           |  |  | 178 |     });
 | 
        
           |  |  | 179 |   | 
        
           |  |  | 180 |     document.addEventListener('click', event => {
 | 
        
           |  |  | 181 |   | 
        
           |  |  | 182 |         // Remove condition from report.
 | 
        
           |  |  | 183 |         const reportRemoveCondition = event.target.closest(reportSelectors.actions.reportRemoveCondition);
 | 
        
           |  |  | 184 |         if (reportRemoveCondition) {
 | 
        
           |  |  | 185 |             event.preventDefault();
 | 
        
           |  |  | 186 |   | 
        
           |  |  | 187 |             const reportElement = reportRemoveCondition.closest(reportSelectors.regions.report);
 | 
        
           |  |  | 188 |             const conditionContainer = reportRemoveCondition.closest(reportSelectors.regions.activeCondition);
 | 
        
           |  |  | 189 |             const conditionName = conditionContainer.dataset.conditionName;
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |             Notification.saveCancelPromise(
 | 
        
           |  |  | 192 |                 getString('deletecondition', 'core_reportbuilder', conditionName),
 | 
        
           |  |  | 193 |                 getString('deleteconditionconfirm', 'core_reportbuilder', conditionName),
 | 
        
           |  |  | 194 |                 getString('delete', 'core'),
 | 
        
           |  |  | 195 |                 {triggerElement: reportRemoveCondition}
 | 
        
           |  |  | 196 |             ).then(() => {
 | 
        
           |  |  | 197 |                 const pendingPromise = new Pending('core_reportbuilder/conditions:remove');
 | 
        
           |  |  | 198 |   | 
        
           |  |  | 199 |                 return deleteCondition(reportElement.dataset.reportId, conditionContainer.dataset.conditionId)
 | 
        
           |  |  | 200 |                     .then(data => reloadSettingsConditionsRegion(reportElement, data))
 | 
        
           |  |  | 201 |                     .then(() => addToast(getString('conditiondeleted', 'core_reportbuilder', conditionName)))
 | 
        
           |  |  | 202 |                     .then(() => {
 | 
        
           |  |  | 203 |                         dispatchEvent(reportEvents.tableReload, {}, reportElement);
 | 
        
           |  |  | 204 |                         return pendingPromise.resolve();
 | 
        
           |  |  | 205 |                     })
 | 
        
           |  |  | 206 |                     .catch(Notification.exception);
 | 
        
           |  |  | 207 |             }).catch(() => {
 | 
        
           |  |  | 208 |                 return;
 | 
        
           |  |  | 209 |             });
 | 
        
           |  |  | 210 |         }
 | 
        
           |  |  | 211 |     });
 | 
        
           |  |  | 212 |   | 
        
           | 1441 | ariadna | 213 |     // Initialize sortable list to handle active conditions moving.
 | 
        
           |  |  | 214 |     const activeConditionsSelector = reportSelectors.regions.activeConditions;
 | 
        
           |  |  | 215 |     const activeConditionsSortableList = new SortableList(activeConditionsSelector, {isHorizontal: false});
 | 
        
           | 1 | efrain | 216 |     activeConditionsSortableList.getElementName = element => Promise.resolve(element.data('conditionName'));
 | 
        
           |  |  | 217 |   | 
        
           | 1441 | ariadna | 218 |     document.addEventListener(SortableList.EVENTS.elementDrop, event => {
 | 
        
           |  |  | 219 |         const reportOrderCondition = event.target.closest(`${activeConditionsSelector} ${reportSelectors.regions.activeCondition}`);
 | 
        
           |  |  | 220 |         if (reportOrderCondition && event.detail.positionChanged) {
 | 
        
           | 1 | efrain | 221 |             const pendingPromise = new Pending('core_reportbuilder/conditions:reorder');
 | 
        
           |  |  | 222 |   | 
        
           | 1441 | ariadna | 223 |             const reportElement = reportOrderCondition.closest(reportSelectors.regions.report);
 | 
        
           |  |  | 224 |             const {conditionId, conditionPosition, conditionName} = reportOrderCondition.dataset;
 | 
        
           |  |  | 225 |   | 
        
           | 1 | efrain | 226 |             // Select target position, if moving to the end then count number of element siblings.
 | 
        
           | 1441 | ariadna | 227 |             let targetConditionPosition = event.detail.targetNextElement.data('conditionPosition')
 | 
        
           |  |  | 228 |                 || event.detail.element.siblings().length + 2;
 | 
        
           | 1 | efrain | 229 |             if (targetConditionPosition > conditionPosition) {
 | 
        
           |  |  | 230 |                 targetConditionPosition--;
 | 
        
           |  |  | 231 |             }
 | 
        
           |  |  | 232 |   | 
        
           |  |  | 233 |             // Re-order condition, giving drop event transition time to finish.
 | 
        
           |  |  | 234 |             const reorderPromise = reorderCondition(reportElement.dataset.reportId, conditionId, targetConditionPosition);
 | 
        
           |  |  | 235 |             Promise.all([reorderPromise, new Promise(resolve => setTimeout(resolve, 1000))])
 | 
        
           |  |  | 236 |                 .then(([data]) => reloadSettingsConditionsRegion(reportElement, data))
 | 
        
           | 1441 | ariadna | 237 |                 .then(() => getString('conditionmoved', 'core_reportbuilder', conditionName))
 | 
        
           | 1 | efrain | 238 |                 .then(addToast)
 | 
        
           |  |  | 239 |                 .then(() => {
 | 
        
           |  |  | 240 |                     dispatchEvent(reportEvents.tableReload, {}, reportElement);
 | 
        
           |  |  | 241 |                     return pendingPromise.resolve();
 | 
        
           |  |  | 242 |                 })
 | 
        
           |  |  | 243 |                 .catch(Notification.exception);
 | 
        
           |  |  | 244 |         }
 | 
        
           |  |  | 245 |     });
 | 
        
           |  |  | 246 | };
 |