first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+268
View File
@@ -0,0 +1,268 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder audiences
*
* @module core_reportbuilder/audience
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import 'core/inplace_editable';
import Templates from 'core/templates';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import {getString} from 'core/str';
import DynamicForm from 'core_form/dynamicform';
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;
/**
* Add audience card
*
* @param {String} className
* @param {String} title
*/
const addAudienceCard = (className, title) => {
const pendingPromise = new Pending('core_reportbuilder/audience:add');
const audiencesContainer = document.querySelector(reportSelectors.regions.audiencesContainer);
const audienceCardLength = audiencesContainer.querySelectorAll(reportSelectors.regions.audienceCard).length;
const params = {
classname: className,
reportid: reportId,
showormessage: (audienceCardLength > 0),
title: title,
};
// Load audience card fragment, render and then initialise the form within.
loadFragment('core_reportbuilder', 'audience_form', contextId, params)
.then((html, js) => {
const audienceCard = Templates.appendNodeContents(audiencesContainer, html, js)[0];
const audienceEmptyMessage = audiencesContainer.querySelector(reportSelectors.regions.audienceEmptyMessage);
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);
})
.then(addToast)
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
};
/**
* Edit audience card
*
* @param {Element} audienceCard
*/
const editAudienceCard = audienceCard => {
const pendingPromise = new Pending('core_reportbuilder/audience:edit');
// Load audience form with data for editing, then toggle visible controls in the card.
const audienceForm = initAudienceCardForm(audienceCard);
audienceForm.load({id: audienceCard.dataset.audienceId})
.then(() => {
const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);
const audienceDescription = audienceCard.querySelector(reportSelectors.regions.audienceDescription);
const audienceEdit = audienceCard.querySelector(reportSelectors.actions.audienceEdit);
audienceFormContainer.classList.remove('hidden');
audienceDescription.classList.add('hidden');
audienceEdit.disabled = true;
return pendingPromise.resolve();
})
.catch(Notification.exception);
};
/**
* Initialise dynamic form within given audience card
*
* @param {Element} audienceCard
* @return {DynamicForm}
*/
const initAudienceCardForm = audienceCard => {
const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);
const audienceForm = new DynamicForm(audienceFormContainer, '\\core_reportbuilder\\form\\audience');
// After submitting the form, update the card instance and description properties.
audienceForm.addEventListener(audienceForm.events.FORM_SUBMITTED, data => {
const audienceHeading = audienceCard.querySelector(reportSelectors.regions.audienceHeading);
const audienceDescription = audienceCard.querySelector(reportSelectors.regions.audienceDescription);
audienceCard.dataset.audienceId = data.detail.instanceid;
audienceHeading.innerHTML = data.detail.heading;
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.
audienceForm.addEventListener(audienceForm.events.FORM_CANCELLED, () => {
if (audienceCard.dataset.audienceId > 0) {
closeAudienceCardForm(audienceCard);
} else {
removeAudienceCard(audienceCard);
}
});
return audienceForm;
};
/**
* Delete audience card
*
* @param {Element} audienceDelete
*/
const deleteAudienceCard = audienceDelete => {
const audienceCard = audienceDelete.closest(reportSelectors.regions.audienceCard);
const {audienceId, audienceTitle, audienceEditWarning = false} = audienceCard.dataset;
// The edit warning indicates the audience is in use in a report schedule.
const audienceDeleteConfirmation = audienceEditWarning ? 'audienceusedbyschedule' : 'deleteaudienceconfirm';
Notification.saveCancelPromise(
getString('deleteaudience', 'core_reportbuilder', audienceTitle),
getString(audienceDeleteConfirmation, 'core_reportbuilder', audienceTitle),
getString('delete', 'core'),
{triggerElement: audienceDelete}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/audience:delete');
return deleteAudience(reportId, audienceId)
.then(() => addToast(getString('audiencedeleted', 'core_reportbuilder', audienceTitle)))
.then(() => {
removeAudienceCard(audienceCard);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}).catch(() => {
return;
});
};
/**
* Close audience card form
*
* @param {Element} audienceCard
*/
const closeAudienceCardForm = audienceCard => {
// Remove the [data-region="audience-form-container"] (with all the event listeners attached to it), and create it again.
const audienceFormContainer = audienceCard.querySelector(reportSelectors.regions.audienceFormContainer);
const NewAudienceFormContainer = audienceFormContainer.cloneNode(false);
audienceCard.querySelector(reportSelectors.regions.audienceForm).replaceChild(NewAudienceFormContainer, audienceFormContainer);
// Show the description container and enable the action buttons.
audienceCard.querySelector(reportSelectors.regions.audienceDescription).classList.remove('hidden');
audienceCard.querySelector(reportSelectors.actions.audienceEdit).disabled = false;
audienceCard.querySelector(reportSelectors.actions.audienceDelete).disabled = false;
};
/**
* Remove audience card
*
* @param {Element} audienceCard
*/
const removeAudienceCard = audienceCard => {
audienceCard.remove();
const audiencesContainer = document.querySelector(reportSelectors.regions.audiencesContainer);
const audienceCards = audiencesContainer.querySelectorAll(reportSelectors.regions.audienceCard);
// Show message if there are no cards remaining, ensure first card's separator is not present.
if (audienceCards.length === 0) {
const audienceEmptyMessage = document.querySelector(reportSelectors.regions.audienceEmptyMessage);
audienceEmptyMessage.classList.remove('hidden');
} else {
const audienceFirstCardSeparator = audienceCards[0].querySelector('.audience-separator');
audienceFirstCardSeparator?.remove();
}
};
let initialized = false;
/**
* Initialise audiences tab.
*
* @param {Number} id
* @param {Number} contextid
*/
export const init = (id, contextid) => {
prefetchStrings('core_reportbuilder', [
'audienceadded',
'audiencedeleted',
'audiencesaved',
'audienceusedbyschedule',
'deleteaudience',
'deleteaudienceconfirm',
]);
prefetchStrings('core', [
'delete',
]);
reportId = id;
contextId = contextid;
if (initialized) {
// We already added the event listeners (can be called multiple times by mustache template).
return;
}
document.addEventListener('click', event => {
// Add instance.
const audienceAdd = event.target.closest(reportSelectors.actions.audienceAdd);
if (audienceAdd) {
event.preventDefault();
addAudienceCard(audienceAdd.dataset.uniqueIdentifier, audienceAdd.dataset.name);
}
// Edit instance.
const audienceEdit = event.target.closest(reportSelectors.actions.audienceEdit);
if (audienceEdit) {
const audienceEditCard = audienceEdit.closest(reportSelectors.regions.audienceCard);
event.preventDefault();
editAudienceCard(audienceEditCard);
}
// Delete instance.
const audienceDelete = event.target.closest(reportSelectors.actions.audienceDelete);
if (audienceDelete) {
event.preventDefault();
deleteAudienceCard(audienceDelete);
}
});
initialized = true;
};
+103
View File
@@ -0,0 +1,103 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder editor
*
* @module core_reportbuilder/editor
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import $ from 'jquery';
import 'core/inplace_editable';
import {addIconToContainer} from 'core/loadingicon';
import Notification from 'core/notification';
import Pending from 'core/pending';
import Templates from 'core/templates';
import {getString} from 'core/str';
import {add as addToast} from 'core/toast';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {init as columnsEditorInit} from 'core_reportbuilder/local/editor/columns';
import {init as conditionsEditorInit} from 'core_reportbuilder/local/editor/conditions';
import {init as filtersEditorInit} from 'core_reportbuilder/local/editor/filters';
import {init as sortingEditorInit} from 'core_reportbuilder/local/editor/sorting';
import {init as cardviewEditorInit} from 'core_reportbuilder/local/editor/card_view';
import {getReport} from 'core_reportbuilder/local/repository/reports';
import {createReportModal} from 'core_reportbuilder/local/repository/modals';
let initialized = false;
/**
* Initialise editor and all it's modules
*/
export const init = () => {
columnsEditorInit(initialized);
conditionsEditorInit(initialized);
filtersEditorInit(initialized);
sortingEditorInit(initialized);
cardviewEditorInit(initialized);
// Ensure we only add our listeners once (can be called multiple times by mustache template).
if (initialized) {
return;
}
// Add event handlers to generic report editor elements.
document.addEventListener('click', event => {
// Toggle between edit and preview mode.
const toggleEditViewMode = event.target.closest(reportSelectors.actions.toggleEditPreview);
if (toggleEditViewMode) {
event.preventDefault();
const reportElement = event.target.closest(reportSelectors.regions.report);
const pendingPromise = new Pending('core_reportbuilder/reports:get');
const toggledEditMode = toggleEditViewMode.dataset.editMode !== "1";
addIconToContainer(toggleEditViewMode)
.then(() => getReport(reportElement.dataset.reportId, toggledEditMode))
.then(response => Promise.all([
$.parseHTML(response.javascript, null, true).map(node => node.innerHTML).join("\n"),
Templates.renderForPromise('core_reportbuilder/local/dynamictabs/editor', response),
]))
.then(([responseJs, {html, js}]) => Templates.replaceNode(reportElement, html, js + responseJs))
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
}
// Edit report details modal.
const reportEdit = event.target.closest(reportSelectors.actions.reportEdit);
if (reportEdit) {
event.preventDefault();
const reportModal = createReportModal(event.target, getString('editreportdetails', 'core_reportbuilder'),
reportEdit.dataset.reportId);
reportModal.addEventListener(reportModal.events.FORM_SUBMITTED, () => {
getString('reportupdated', 'core_reportbuilder')
.then(addToast)
.then(() => {
return window.location.reload();
})
.catch(Notification.exception);
});
reportModal.show();
}
});
initialized = true;
};
+111
View File
@@ -0,0 +1,111 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder filter management
*
* @module core_reportbuilder/filters
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {dispatchEvent} from 'core/event_dispatcher';
import {loadFragment} from 'core/fragment';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {getString} from 'core/str';
import Templates from 'core/templates';
import {add as addToast} from 'core/toast';
import DynamicForm from 'core_form/dynamicform';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {resetFilters} from 'core_reportbuilder/local/repository/filters';
/**
* Update filter button text to indicate applied filter count
*
* @param {Element} reportElement
* @param {Number} filterCount
*/
const setFilterButtonCount = async(reportElement, filterCount) => {
const filterButtonLabel = reportElement.querySelector(reportSelectors.regions.filterButtonLabel);
if (filterCount > 0) {
filterButtonLabel.textContent = await getString('filtersappliedx', 'core_reportbuilder', filterCount);
} else {
filterButtonLabel.textContent = await getString('filters', 'moodle');
}
};
/**
* Initialise module for given report
*
* @method
* @param {Number} reportId
* @param {Number} contextId
*/
export const init = (reportId, contextId) => {
const reportElement = document.querySelector(reportSelectors.forReport(reportId));
const filterFormContainer = reportElement.querySelector(reportSelectors.regions.filtersForm);
// Ensure we only add our listeners once (can be called multiple times by mustache template).
if (filterFormContainer.dataset.initialized) {
return;
}
filterFormContainer.dataset.initialized = true;
const filterForm = new DynamicForm(filterFormContainer, '\\core_reportbuilder\\form\\filter');
// Submit report filters.
filterForm.addEventListener(filterForm.events.FORM_SUBMITTED, event => {
event.preventDefault();
// After the form has been submitted, we should trigger report table reload.
dispatchEvent(reportEvents.tableReload, {}, reportElement);
setFilterButtonCount(reportElement, event.detail);
getString('filtersapplied', 'core_reportbuilder')
.then(addToast)
.catch(Notification.exception);
});
// Reset report filters.
filterForm.addEventListener(filterForm.events.NOSUBMIT_BUTTON_PRESSED, event => {
event.preventDefault();
const pendingPromise = new Pending('core_reportbuilder/filters:reset');
const reportParameters = reportElement.dataset.parameter;
resetFilters(reportId, reportParameters)
.then(() => getString('filtersreset', 'core_reportbuilder'))
.then(addToast)
.then(() => loadFragment('core_reportbuilder', 'filters_form', contextId, {
reportid: reportId,
parameters: reportParameters,
}))
.then((html, js) => {
Templates.replaceNodeContents(filterFormContainer, html, js);
dispatchEvent(reportEvents.tableReload, {}, reportElement);
setFilterButtonCount(reportElement, 0);
return pendingPromise.resolve();
})
.catch(Notification.exception);
});
// Modify "region-main" overflow for big filter forms.
document.querySelector('#region-main').style.overflowX = "visible";
};
@@ -0,0 +1,71 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder card view editor
*
* @module core_reportbuilder/local/editor/card_view
* @copyright 2021 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import DynamicForm from 'core_form/dynamicform';
import {add as addToast} from 'core/toast';
import {getString} from "core/str";
import {subscribe as subscribe} from 'core/pubsub';
import Notification from 'core/notification';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
let cardViewForm = null;
/**
* Initialise card view form, must be called on each init because the form container is re-created when switching editor modes
*/
const initCardViewForm = () => {
const cardViewFormContainer = document.querySelector(reportSelectors.regions.settingsCardView);
if (!cardViewFormContainer) {
return;
}
cardViewForm = new DynamicForm(cardViewFormContainer, '\\core_reportbuilder\\form\\card_view');
cardViewForm.addEventListener(cardViewForm.events.FORM_SUBMITTED, (event) => {
event.preventDefault();
getString('cardviewsettingssaved', 'core_reportbuilder')
.then(addToast)
.catch(Notification.exception);
});
};
/**
* Initialise module
*
* @param {Boolean} initialized Ensure we only add our listeners once
*/
export const init = (initialized) => {
initCardViewForm();
if (initialized) {
return;
}
// Update form each time a column is added or removed to the custom report.
subscribe(reportEvents.publish.reportColumnsUpdated, () => {
const reportElement = document.querySelector(reportSelectors.regions.report);
cardViewForm.load({reportid: reportElement.dataset.reportId});
});
};
@@ -0,0 +1,189 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder columns editor
*
* @module core_reportbuilder/local/editor/columns
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import $ from 'jquery';
import {dispatchEvent} from 'core/event_dispatcher';
import 'core/inplace_editable';
import {eventTypes as inplaceEditableEvents} from 'core/local/inplace_editable/events';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import {publish} from 'core/pubsub';
import SortableList from 'core/sortable_list';
import {getString} from 'core/str';
import {add as addToast} from 'core/toast';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {addColumn, deleteColumn, reorderColumn} from 'core_reportbuilder/local/repository/columns';
import {getColumnSorting} from 'core_reportbuilder/local/repository/sorting';
/**
* Initialise module, prefetch all required strings
*
* @param {Boolean} initialized Ensure we only add our listeners once
*/
export const init = initialized => {
prefetchStrings('core_reportbuilder', [
'columnadded',
'columnaggregated',
'columndeleted',
'columnmoved',
'deletecolumn',
'deletecolumnconfirm',
]);
prefetchStrings('core', [
'delete',
]);
if (initialized) {
return;
}
document.addEventListener('click', event => {
// Add column to report.
const reportAddColumn = event.target.closest(reportSelectors.actions.reportAddColumn);
if (reportAddColumn) {
event.preventDefault();
const pendingPromise = new Pending('core_reportbuilder/columns:add');
const reportElement = reportAddColumn.closest(reportSelectors.regions.report);
addColumn(reportElement.dataset.reportId, reportAddColumn.dataset.uniqueIdentifier)
.then(data => publish(reportEvents.publish.reportColumnsUpdated, data))
.then(() => getString('columnadded', 'core_reportbuilder', reportAddColumn.dataset.name))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
// Remove column from report.
const reportRemoveColumn = event.target.closest(reportSelectors.actions.reportRemoveColumn);
if (reportRemoveColumn) {
event.preventDefault();
const reportElement = reportRemoveColumn.closest(reportSelectors.regions.report);
const columnHeader = reportRemoveColumn.closest(reportSelectors.regions.columnHeader);
const columnName = columnHeader.dataset.columnName;
Notification.saveCancelPromise(
getString('deletecolumn', 'core_reportbuilder', columnName),
getString('deletecolumnconfirm', 'core_reportbuilder', columnName),
getString('delete', 'core'),
{triggerElement: reportRemoveColumn}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/columns:remove');
return deleteColumn(reportElement.dataset.reportId, columnHeader.dataset.columnId)
.then(data => publish(reportEvents.publish.reportColumnsUpdated, data))
.then(() => addToast(getString('columndeleted', 'core_reportbuilder', columnName)))
.then(() => {
dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}).catch(() => {
return;
});
}
});
// Initialize sortable list to handle column moving (note JQuery dependency, see MDL-72293 for resolution).
var columnSortableList = new SortableList(`${reportSelectors.regions.reportTable} thead tr`, {isHorizontal: true});
columnSortableList.getElementName = element => Promise.resolve(element.data('columnName'));
$(document).on(SortableList.EVENTS.DRAG, `${reportSelectors.regions.report} th[data-column-id]`, (event, info) => {
const reportElement = event.target.closest(reportSelectors.regions.report);
const columnPosition = info.element.data('columnPosition');
const targetColumnPosition = info.targetNextElement.data('columnPosition');
$(reportElement).find('tbody tr').each(function() {
const cell = $(this).children(`td.c${columnPosition - 1}`)[0];
if (targetColumnPosition) {
var beforeCell = $(this).children(`td.c${targetColumnPosition - 1}`)[0];
this.insertBefore(cell, beforeCell);
} else {
this.appendChild(cell);
}
});
});
$(document).on(SortableList.EVENTS.DROP, `${reportSelectors.regions.report} th[data-column-id]`, (event, info) => {
if (info.positionChanged) {
const pendingPromise = new Pending('core_reportbuilder/columns:reorder');
const reportElement = event.target.closest(reportSelectors.regions.report);
const columnId = info.element.data('columnId');
const columnName = info.element.data('columnName');
const columnPosition = info.element.data('columnPosition');
// Select target position, if moving to the end then count number of element siblings.
let targetColumnPosition = info.targetNextElement.data('columnPosition') || info.element.siblings().length + 2;
if (targetColumnPosition > columnPosition) {
targetColumnPosition--;
}
// Re-order column, giving drop event transition time to finish.
const reorderPromise = reorderColumn(reportElement.dataset.reportId, columnId, targetColumnPosition);
Promise.all([reorderPromise, new Promise(resolve => setTimeout(resolve, 1000))])
.then(() => getString('columnmoved', 'core_reportbuilder', columnName))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
});
// Initialize inplace editable listeners for column aggregation.
document.addEventListener(inplaceEditableEvents.elementUpdated, event => {
const columnAggregation = event.target.closest('[data-itemtype="columnaggregation"]');
if (columnAggregation) {
const pendingPromise = new Pending('core_reportbuilder/columns:aggregate');
const reportElement = columnAggregation.closest(reportSelectors.regions.report);
const columnHeader = columnAggregation.closest(reportSelectors.regions.columnHeader);
getString('columnaggregated', 'core_reportbuilder', columnHeader.dataset.columnName)
.then(addToast)
.then(() => {
// Pass preserveTriggerElement parameter so columnAggregationLink will be focused after the report reload.
const columnAggregationLink = `[data-itemtype="columnaggregation"][data-itemid="`
+ `${columnAggregation.dataset.itemid}"] > a`;
// Now reload the table, and notify listeners that columns have been updated.
dispatchEvent(reportEvents.tableReload, {preserveTriggerElement: columnAggregationLink}, reportElement);
return getColumnSorting(reportElement.dataset.reportId);
})
.then(data => publish(reportEvents.publish.reportColumnsUpdated, data))
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
}
});
};
@@ -0,0 +1,244 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder conditions editor
*
* @module core_reportbuilder/local/editor/conditions
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import $ from 'jquery';
import {dispatchEvent} from 'core/event_dispatcher';
import AutoComplete from 'core/form-autocomplete';
import 'core/inplace_editable';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import SortableList from 'core/sortable_list';
import {getString} from 'core/str';
import Templates from 'core/templates';
import {add as addToast} from 'core/toast';
import DynamicForm from 'core_form/dynamicform';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {addCondition, deleteCondition, reorderCondition, resetConditions} from 'core_reportbuilder/local/repository/conditions';
/**
* Reload conditions settings region
*
* @param {Element} reportElement
* @param {Object} templateContext
* @return {Promise}
*/
const reloadSettingsConditionsRegion = (reportElement, templateContext) => {
const pendingPromise = new Pending('core_reportbuilder/conditions:reload');
const settingsConditionsRegion = reportElement.querySelector(reportSelectors.regions.settingsConditions);
return Templates.renderForPromise('core_reportbuilder/local/settings/conditions', {conditions: templateContext})
.then(({html, js}) => {
const conditionsjs = $.parseHTML(templateContext.javascript, null, true).map(node => node.innerHTML).join("\n");
Templates.replaceNode(settingsConditionsRegion, html, js + conditionsjs);
initConditionsForm();
// Re-focus the add condition element after reloading the region.
const reportAddCondition = reportElement.querySelector(reportSelectors.actions.reportAddCondition);
reportAddCondition?.focus();
return pendingPromise.resolve();
});
};
/**
* Initialise conditions form, must be called on each init because the form container is re-created when switching editor modes
*/
const initConditionsForm = () => {
const reportElement = document.querySelector(reportSelectors.regions.report);
// Enhance condition selector.
const reportAddCondition = reportElement.querySelector(reportSelectors.actions.reportAddCondition);
AutoComplete.enhanceField(reportAddCondition, false, '', getString('selectacondition', 'core_reportbuilder'))
.catch(Notification.exception);
// Handle dynamic conditions form.
const conditionFormContainer = reportElement.querySelector(reportSelectors.regions.settingsConditions);
if (!conditionFormContainer) {
return;
}
const conditionForm = new DynamicForm(conditionFormContainer, '\\core_reportbuilder\\form\\condition');
// Submit report conditions.
conditionForm.addEventListener(conditionForm.events.FORM_SUBMITTED, event => {
event.preventDefault();
getString('conditionsapplied', 'core_reportbuilder')
.then(addToast)
.catch(Notification.exception);
// After the form has been submitted, we should trigger report table reload.
dispatchEvent(reportEvents.tableReload, {}, reportElement);
});
// Reset report conditions.
conditionForm.addEventListener(conditionForm.events.NOSUBMIT_BUTTON_PRESSED, event => {
event.preventDefault();
Notification.saveCancelPromise(
getString('resetconditions', 'core_reportbuilder'),
getString('resetconditionsconfirm', 'core_reportbuilder'),
getString('resetall', 'core_reportbuilder'),
{triggerElement: event.detail}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/conditions:reset');
return resetConditions(reportElement.dataset.reportId)
.then(data => reloadSettingsConditionsRegion(reportElement, data))
.then(() => addToast(getString('conditionsreset', 'core_reportbuilder')))
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}).catch(() => {
return;
});
});
};
/**
* Initialise module, prefetch all required strings
*
* @param {Boolean} initialized Ensure we only add our listeners once
*/
export const init = initialized => {
prefetchStrings('core_reportbuilder', [
'conditionadded',
'conditiondeleted',
'conditionmoved',
'conditionsapplied',
'conditionsreset',
'deletecondition',
'deleteconditionconfirm',
'resetall',
'resetconditions',
'resetconditionsconfirm',
'selectacondition',
]);
prefetchStrings('core', [
'delete',
]);
initConditionsForm();
if (initialized) {
return;
}
// Add condition to report.
document.addEventListener('change', event => {
const reportAddCondition = event.target.closest(reportSelectors.actions.reportAddCondition);
if (reportAddCondition) {
event.preventDefault();
// Check if dropdown is closed with no condition selected.
if (reportAddCondition.value === "" || reportAddCondition.value === "0") {
return;
}
const reportElement = reportAddCondition.closest(reportSelectors.regions.report);
const pendingPromise = new Pending('core_reportbuilder/conditions:add');
addCondition(reportElement.dataset.reportId, reportAddCondition.value)
.then(data => reloadSettingsConditionsRegion(reportElement, data))
.then(() => getString('conditionadded', 'core_reportbuilder',
reportAddCondition.options[reportAddCondition.selectedIndex].text))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
});
document.addEventListener('click', event => {
// Remove condition from report.
const reportRemoveCondition = event.target.closest(reportSelectors.actions.reportRemoveCondition);
if (reportRemoveCondition) {
event.preventDefault();
const reportElement = reportRemoveCondition.closest(reportSelectors.regions.report);
const conditionContainer = reportRemoveCondition.closest(reportSelectors.regions.activeCondition);
const conditionName = conditionContainer.dataset.conditionName;
Notification.saveCancelPromise(
getString('deletecondition', 'core_reportbuilder', conditionName),
getString('deleteconditionconfirm', 'core_reportbuilder', conditionName),
getString('delete', 'core'),
{triggerElement: reportRemoveCondition}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/conditions:remove');
return deleteCondition(reportElement.dataset.reportId, conditionContainer.dataset.conditionId)
.then(data => reloadSettingsConditionsRegion(reportElement, data))
.then(() => addToast(getString('conditiondeleted', 'core_reportbuilder', conditionName)))
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}).catch(() => {
return;
});
}
});
// Initialize sortable list to handle active conditions moving (note JQuery dependency, see MDL-72293 for resolution).
var activeConditionsSortableList = new SortableList(`${reportSelectors.regions.activeConditions}`,
{isHorizontal: false});
activeConditionsSortableList.getElementName = element => Promise.resolve(element.data('conditionName'));
$(document).on(SortableList.EVENTS.DROP, reportSelectors.regions.activeCondition, (event, info) => {
if (info.positionChanged) {
const pendingPromise = new Pending('core_reportbuilder/conditions:reorder');
const reportElement = event.target.closest(reportSelectors.regions.report);
const conditionId = info.element.data('conditionId');
const conditionPosition = info.element.data('conditionPosition');
// Select target position, if moving to the end then count number of element siblings.
let targetConditionPosition = info.targetNextElement.data('conditionPosition') || info.element.siblings().length + 2;
if (targetConditionPosition > conditionPosition) {
targetConditionPosition--;
}
// Re-order condition, giving drop event transition time to finish.
const reorderPromise = reorderCondition(reportElement.dataset.reportId, conditionId, targetConditionPosition);
Promise.all([reorderPromise, new Promise(resolve => setTimeout(resolve, 1000))])
.then(([data]) => reloadSettingsConditionsRegion(reportElement, data))
.then(() => getString('conditionmoved', 'core_reportbuilder', info.element.data('conditionName')))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
});
};
@@ -0,0 +1,181 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder filters editor
*
* @module core_reportbuilder/local/editor/filters
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import $ from 'jquery';
import AutoComplete from 'core/form-autocomplete';
import 'core/inplace_editable';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import SortableList from 'core/sortable_list';
import {getString} from 'core/str';
import Templates from 'core/templates';
import {add as addToast} from 'core/toast';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {addFilter, deleteFilter, reorderFilter} from 'core_reportbuilder/local/repository/filters';
/**
* Reload filters settings region
*
* @param {Element} reportElement
* @param {Object} templateContext
* @return {Promise}
*/
const reloadSettingsFiltersRegion = (reportElement, templateContext) => {
const pendingPromise = new Pending('core_reportbuilder/filters:reload');
const settingsFiltersRegion = reportElement.querySelector(reportSelectors.regions.settingsFilters);
return Templates.renderForPromise('core_reportbuilder/local/settings/filters', {filters: templateContext})
.then(({html, js}) => {
Templates.replaceNode(settingsFiltersRegion, html, js);
initFiltersForm();
// Re-focus the add filter element after reloading the region.
const reportAddFilter = reportElement.querySelector(reportSelectors.actions.reportAddFilter);
reportAddFilter?.focus();
return pendingPromise.resolve();
});
};
/**
* Initialise filters form, must be called on each init because the form container is re-created when switching editor modes
*/
const initFiltersForm = () => {
const reportElement = document.querySelector(reportSelectors.regions.report);
// Enhance filter selector.
const reportAddFilter = reportElement.querySelector(reportSelectors.actions.reportAddFilter);
AutoComplete.enhanceField(reportAddFilter, false, '', getString('selectafilter', 'core_reportbuilder'))
.catch(Notification.exception);
};
/**
* Initialise module, prefetch all required strings
*
* @param {Boolean} initialized Ensure we only add our listeners once
*/
export const init = initialized => {
prefetchStrings('core_reportbuilder', [
'deletefilter',
'deletefilterconfirm',
'filteradded',
'filterdeleted',
'filtermoved',
'selectafilter',
]);
prefetchStrings('core', [
'delete',
]);
initFiltersForm();
if (initialized) {
return;
}
// Add filter to report.
document.addEventListener('change', event => {
const reportAddFilter = event.target.closest(reportSelectors.actions.reportAddFilter);
if (reportAddFilter) {
event.preventDefault();
// Check if dropdown is closed with no filter selected.
if (reportAddFilter.value === "" || reportAddFilter.value === "0") {
return;
}
const reportElement = reportAddFilter.closest(reportSelectors.regions.report);
const pendingPromise = new Pending('core_reportbuilder/filters:add');
addFilter(reportElement.dataset.reportId, reportAddFilter.value)
.then(data => reloadSettingsFiltersRegion(reportElement, data))
.then(() => getString('filteradded', 'core_reportbuilder',
reportAddFilter.options[reportAddFilter.selectedIndex].text))
.then(addToast)
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
}
});
document.addEventListener('click', event => {
// Remove filter from report.
const reportRemoveFilter = event.target.closest(reportSelectors.actions.reportRemoveFilter);
if (reportRemoveFilter) {
event.preventDefault();
const reportElement = reportRemoveFilter.closest(reportSelectors.regions.report);
const filterContainer = reportRemoveFilter.closest(reportSelectors.regions.activeFilter);
const filterName = filterContainer.dataset.filterName;
Notification.saveCancelPromise(
getString('deletefilter', 'core_reportbuilder', filterName),
getString('deletefilterconfirm', 'core_reportbuilder', filterName),
getString('delete', 'core'),
{triggerElement: reportRemoveFilter}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/filters:remove');
return deleteFilter(reportElement.dataset.reportId, filterContainer.dataset.filterId)
.then(data => reloadSettingsFiltersRegion(reportElement, data))
.then(() => addToast(getString('filterdeleted', 'core_reportbuilder', filterName)))
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
}).catch(() => {
return;
});
}
});
// Initialize sortable list to handle active filters moving (note JQuery dependency, see MDL-72293 for resolution).
var activeFiltersSortableList = new SortableList(`${reportSelectors.regions.activeFilters} ul`, {isHorizontal: false});
activeFiltersSortableList.getElementName = element => Promise.resolve(element.data('filterName'));
$(document).on(SortableList.EVENTS.DROP, `${reportSelectors.regions.report} li[data-filter-id]`, (event, info) => {
if (info.positionChanged) {
const pendingPromise = new Pending('core_reportbuilder/filters:reorder');
const reportElement = event.target.closest(reportSelectors.regions.report);
const filterId = info.element.data('filterId');
const filterPosition = info.element.data('filterPosition');
// Select target position, if moving to the end then count number of element siblings.
let targetFilterPosition = info.targetNextElement.data('filterPosition') || info.element.siblings().length + 2;
if (targetFilterPosition > filterPosition) {
targetFilterPosition--;
}
// Re-order filter, giving drop event transition time to finish.
const reorderPromise = reorderFilter(reportElement.dataset.reportId, filterId, targetFilterPosition);
Promise.all([reorderPromise, new Promise(resolve => setTimeout(resolve, 1000))])
.then(([data]) => reloadSettingsFiltersRegion(reportElement, data))
.then(() => getString('filtermoved', 'core_reportbuilder', info.element.data('filterName')))
.then(addToast)
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
}
});
};
@@ -0,0 +1,182 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder columns sorting editor
*
* @module core_reportbuilder/local/editor/sorting
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import $ from 'jquery';
import 'core/inplace_editable';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {subscribe} from 'core/pubsub';
import SortableList from 'core/sortable_list';
import {getString} from 'core/str';
import {add as addToast} from 'core/toast';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {reorderColumnSorting, toggleColumnSorting} from 'core_reportbuilder/local/repository/sorting';
import Templates from 'core/templates';
import {dispatchEvent} from 'core/event_dispatcher';
import * as reportEvents from 'core_reportbuilder/local/events';
// These constants match PHP consts SORT_ASC, SORT_DESC.
const SORTORDER = {
ASCENDING: 4,
DESCENDING: 3,
};
/**
* Reload sorting settings region
*
* @param {Object} context
* @return {Promise}
*/
const reloadSettingsSortingRegion = context => {
const pendingPromise = new Pending('core_reportbuilder/sorting:reload');
const settingsSortingRegion = document.querySelector(reportSelectors.regions.settingsSorting);
return Templates.renderForPromise('core_reportbuilder/local/settings/sorting', {sorting: context})
.then(({html, js}) => {
Templates.replaceNode(settingsSortingRegion, html, js);
return pendingPromise.resolve();
});
};
/**
* Updates column sorting
*
* @param {Element} reportElement
* @param {Element} element
* @param {Number} sortenabled
* @param {Number} sortdirection
* @return {Promise}
*/
const updateSorting = (reportElement, element, sortenabled, sortdirection) => {
const reportId = reportElement.dataset.reportId;
const listElement = element.closest('li');
const columnId = listElement.dataset.columnSortId;
const columnName = listElement.dataset.columnSortName;
return toggleColumnSorting(reportId, columnId, sortenabled, sortdirection)
.then(reloadSettingsSortingRegion)
.then(() => getString('columnsortupdated', 'core_reportbuilder', columnName))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return null;
});
};
/**
* Initialise module
*
* @param {Boolean} initialized Ensure we only add our listeners once
*/
export const init = (initialized) => {
if (initialized) {
return;
}
// Update sorting region each time report columns are updated (added or removed).
subscribe(reportEvents.publish.reportColumnsUpdated, data => reloadSettingsSortingRegion(data)
.catch(Notification.exception)
);
document.addEventListener('click', event => {
// Enable/disable sorting on columns.
const toggleSorting = event.target.closest(reportSelectors.actions.reportToggleColumnSort);
if (toggleSorting) {
event.preventDefault();
const pendingPromise = new Pending('core_reportbuilder/sorting:toggle');
const reportElement = toggleSorting.closest(reportSelectors.regions.report);
const sortdirection = parseInt(toggleSorting.closest('li').dataset.columnSortDirection);
updateSorting(reportElement, toggleSorting, toggleSorting.checked, sortdirection)
.then(() => {
// Re-focus the toggle sorting element after reloading the region.
const toggleSortingElement = document.getElementById(toggleSorting.id);
toggleSortingElement?.focus();
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
// Change column sort direction.
const toggleSortDirection = event.target.closest(reportSelectors.actions.reportToggleColumnSortDirection);
if (toggleSortDirection) {
event.preventDefault();
const pendingPromise = new Pending('core_reportbuilder/sorting:direction');
const reportElement = toggleSortDirection.closest(reportSelectors.regions.report);
const listElement = toggleSortDirection.closest('li');
const toggleSorting = listElement.querySelector(reportSelectors.actions.reportToggleColumnSort);
let sortdirection = parseInt(listElement.dataset.columnSortDirection);
if (sortdirection === SORTORDER.ASCENDING) {
sortdirection = SORTORDER.DESCENDING;
} else if (sortdirection === SORTORDER.DESCENDING) {
sortdirection = SORTORDER.ASCENDING;
}
updateSorting(reportElement, toggleSortDirection, toggleSorting.checked, sortdirection)
.then(() => {
// Re-focus the toggle sort direction element after reloading the region.
const toggleSortDirectionElement = document.getElementById(toggleSortDirection.id);
toggleSortDirectionElement?.focus();
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
});
// Initialize sortable list to handle column sorting moving (note JQuery dependency, see MDL-72293 for resolution).
var columnsSortingSortableList = new SortableList(`${reportSelectors.regions.settingsSorting} ul`, {isHorizontal: false});
columnsSortingSortableList.getElementName = element => Promise.resolve(element.data('columnSortName'));
$(document).on(SortableList.EVENTS.DROP, `${reportSelectors.regions.report} li[data-column-sort-id]`, (event, info) => {
if (info.positionChanged) {
const pendingPromise = new Pending('core_reportbuilder/sorting:reorder');
const reportElement = event.target.closest(reportSelectors.regions.report);
const columnId = info.element.data('columnSortId');
const columnPosition = info.element.data('columnSortPosition');
// Select target position, if moving to the end then count number of element siblings.
let targetColumnSortPosition = info.targetNextElement.data('columnSortPosition') || info.element.siblings().length + 2;
if (targetColumnSortPosition > columnPosition) {
targetColumnSortPosition--;
}
// Re-order column sorting, giving drop event transition time to finish.
const reorderPromise = reorderColumnSorting(reportElement.dataset.reportId, columnId, targetColumnSortPosition);
Promise.all([reorderPromise, new Promise(resolve => setTimeout(resolve, 1000))])
.then(([data]) => reloadSettingsSortingRegion(data))
.then(() => getString('columnsortupdated', 'core_reportbuilder', info.element.data('columnSortName')))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
});
};
+50
View File
@@ -0,0 +1,50 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder events
*
* @module core_reportbuilder/local/events
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Events for the Report builder subsystem
*
* @constant
* @property {String} tableReload See {@link event:tableReload}
*/
export default {
/**
* Trigger table reloading
*
* @event tableReload
* @type {CustomEvent}
* @property {object} detail
* @property {Boolean} detail.preservePagination Whether current pagination should be preserved (default false)
* @property {String} detail.preserveTriggerElement Element selector that should be focused after table reload (default null)
*
* @example <caption>Triggering table reload</caption>
* import {dispatchEvent} from 'core/event_dispatcher';
* import * as reportEvents from 'core_reportbuilder/local/events';
*
* dispatchEvent(reportEvents.tableReload, {}, document.querySelector(...));
*/
tableReload: 'core_reportbuilder_table_reload',
publish: {
reportColumnsUpdated: 'core_reportbuilder_report_columns_updated',
},
};
@@ -0,0 +1,40 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle audiences AJAX requests
*
* @module core_reportbuilder/local/repository/audiences
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Remove audience from given report
*
* @param {Number} reportId
* @param {Number} instanceId
* @return {Promise}
*/
export const deleteAudience = (reportId, instanceId) => {
const request = {
methodname: 'core_reportbuilder_audiences_delete',
args: {reportid: reportId, instanceid: instanceId}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,73 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle column AJAX requests
*
* @module core_reportbuilder/local/repository/columns
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Add column to given report
*
* @param {Number} reportId
* @param {String} uniqueIdentifier
* @return {Promise}
*/
export const addColumn = (reportId, uniqueIdentifier) => {
const request = {
methodname: 'core_reportbuilder_columns_add',
args: {reportid: reportId, uniqueidentifier: uniqueIdentifier}
};
return Ajax.call([request])[0];
};
/**
* Remove column from given report
*
* @param {Number} reportId
* @param {Number} columnId
* @return {Promise}
*/
export const deleteColumn = (reportId, columnId) => {
const request = {
methodname: 'core_reportbuilder_columns_delete',
args: {reportid: reportId, columnid: columnId}
};
return Ajax.call([request])[0];
};
/**
* Re-order column within a report
*
* @param {Number} reportId
* @param {Number} columnId
* @param {Number} position
* @return {Promise}
*/
export const reorderColumn = (reportId, columnId, position) => {
const request = {
methodname: 'core_reportbuilder_columns_reorder',
args: {reportid: reportId, columnid: columnId, position: position}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,88 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle condition AJAX requests
*
* @module core_reportbuilder/local/repository/conditions
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Reset all conditions for given report
*
* @param {Number} reportId
* @return {Promise}
*/
export const resetConditions = reportId => {
const request = {
methodname: 'core_reportbuilder_conditions_reset',
args: {reportid: reportId}
};
return Ajax.call([request])[0];
};
/**
* Add condition to given report
*
* @param {Number} reportId
* @param {String} uniqueIdentifier
* @return {Promise}
*/
export const addCondition = (reportId, uniqueIdentifier) => {
const request = {
methodname: 'core_reportbuilder_conditions_add',
args: {reportid: reportId, uniqueidentifier: uniqueIdentifier}
};
return Ajax.call([request])[0];
};
/**
* Remove condition from given report
*
* @param {Number} reportId
* @param {Number} conditionId
* @return {Promise}
*/
export const deleteCondition = (reportId, conditionId) => {
const request = {
methodname: 'core_reportbuilder_conditions_delete',
args: {reportid: reportId, conditionid: conditionId}
};
return Ajax.call([request])[0];
};
/**
* Reorder a condition in a given report
*
* @param {Number} reportId
* @param {Number} conditionId
* @param {Number} position
* @return {Promise}
*/
export const reorderCondition = (reportId, conditionId, position) => {
const request = {
methodname: 'core_reportbuilder_conditions_reorder',
args: {reportid: reportId, conditionid: conditionId, position: position}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,108 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle filter AJAX requests
*
* @module core_reportbuilder/local/repository/filters
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Reset all filters for given report
*
* @method
* @param {Number} reportId
* @param {String} reportParameters
* @return {Promise}
*/
export const resetFilters = (reportId, reportParameters) => {
const request = {
methodname: 'core_reportbuilder_filters_reset',
args: {reportid: reportId, parameters: reportParameters}
};
return Ajax.call([request])[0];
};
/**
* Set filter values for given report
*
* @method
* @param {Number} reportId
* @param {String} reportParameters
* @param {String} filterValues
* @return {Promise}
*/
export const setFilters = (reportId, reportParameters, filterValues) => {
const request = {
methodname: 'core_reportbuilder_set_filters',
args: {reportid: reportId, parameters: reportParameters, values: filterValues}
};
return Ajax.call([request])[0];
};
/**
* Add a filter to the given report
*
* @param {Number} reportId
* @param {String} uniqueIdentifier
* @return {Promise}
*/
export const addFilter = (reportId, uniqueIdentifier) => {
const request = {
methodname: 'core_reportbuilder_filters_add',
args: {reportid: reportId, uniqueidentifier: uniqueIdentifier}
};
return Ajax.call([request])[0];
};
/**
* Remove filter from given report
*
* @param {Number} reportId
* @param {Number} filterId
* @return {Promise}
*/
export const deleteFilter = (reportId, filterId) => {
const request = {
methodname: 'core_reportbuilder_filters_delete',
args: {reportid: reportId, filterid: filterId}
};
return Ajax.call([request])[0];
};
/**
* Reorder a filter in a given report
*
* @param {Number} reportId
* @param {Number} filterId
* @param {Number} position
* @return {Promise}
*/
export const reorderFilter = (reportId, filterId, position) => {
const request = {
methodname: 'core_reportbuilder_filters_reorder',
args: {reportid: reportId, filterid: filterId, position: position}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,76 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle modal form requests
*
* @module core_reportbuilder/local/repository/modals
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ModalForm from 'core_form/modalform';
import {getString} from 'core/str';
/**
* Return modal instance
*
* @param {EventTarget} triggerElement
* @param {Promise} modalTitle
* @param {String} formClass
* @param {Object} formArgs
* @return {ModalForm}
*/
const createModalForm = (triggerElement, modalTitle, formClass, formArgs) => {
return new ModalForm({
modalConfig: {
title: modalTitle,
},
formClass: formClass,
args: formArgs,
saveButtonText: getString('save', 'moodle'),
returnFocus: triggerElement,
});
};
/**
* Return report modal instance
*
* @param {EventTarget} triggerElement
* @param {Promise} modalTitle
* @param {Number} reportId
* @return {ModalForm}
*/
export const createReportModal = (triggerElement, modalTitle, reportId = 0) => {
return createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\form\\report', {
id: reportId,
});
};
/**
* Return schedule modal instance
*
* @param {EventTarget} triggerElement
* @param {Promise} modalTitle
* @param {Number} reportId
* @param {Number} scheduleId
* @return {ModalForm}
*/
export const createScheduleModal = (triggerElement, modalTitle, reportId, scheduleId = 0) => {
return createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\form\\schedule', {
reportid: reportId,
id: scheduleId,
});
};
@@ -0,0 +1,56 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle report AJAX requests
*
* @module core_reportbuilder/local/repository/reports
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Delete given report
*
* @param {Number} reportId
* @return {Promise}
*/
export const deleteReport = reportId => {
const request = {
methodname: 'core_reportbuilder_reports_delete',
args: {reportid: reportId}
};
return Ajax.call([request])[0];
};
/**
* Get report content
*
* @param {Number} reportId
* @param {Boolean} editMode
* @param {Number} [pageSize=0]
* @return {Promise}
*/
export const getReport = (reportId, editMode, pageSize = 0) => {
const request = {
methodname: 'core_reportbuilder_reports_get',
args: {reportid: reportId, editmode: editMode, pagesize: pageSize}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,76 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle schedule AJAX requests
*
* @module core_reportbuilder/local/repository/schedules
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Delete schedule
*
* @method
* @param {Number} reportId
* @param {Number} scheduleId
* @return {Promise}
*/
export const deleteSchedule = (reportId, scheduleId) => {
const request = {
methodname: 'core_reportbuilder_schedules_delete',
args: {reportid: reportId, scheduleid: scheduleId}
};
return Ajax.call([request])[0];
};
/**
* Send schedule
*
* @method
* @param {Number} reportId
* @param {Number} scheduleId
* @return {Promise}
*/
export const sendSchedule = (reportId, scheduleId) => {
const request = {
methodname: 'core_reportbuilder_schedules_send',
args: {reportid: reportId, scheduleid: scheduleId}
};
return Ajax.call([request])[0];
};
/**
* Toggle schedule enabled
*
* @method
* @param {Number} reportId
* @param {Number} scheduleId
* @param {Boolean} scheduleEnabled
* @return {Promise}
*/
export const toggleSchedule = (reportId, scheduleId, scheduleEnabled) => {
const request = {
methodname: 'core_reportbuilder_schedules_toggle',
args: {reportid: reportId, scheduleid: scheduleId, enabled: scheduleEnabled}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,74 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle column sorting AJAX requests
*
* @module core_reportbuilder/local/repository/sorting
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Retrieve column sorting
*
* @param {Number} reportId
* @return {Promise}
*/
export const getColumnSorting = reportId => {
const request = {
methodname: 'core_reportbuilder_columns_sort_get',
args: {reportid: reportId}
};
return Ajax.call([request])[0];
};
/**
* Re-order sort column position
*
* @param {Number} reportId
* @param {Number} columnId
* @param {Number} position
* @return {Promise}
*/
export const reorderColumnSorting = (reportId, columnId, position) => {
const request = {
methodname: 'core_reportbuilder_columns_sort_reorder',
args: {reportid: reportId, columnid: columnId, position: position}
};
return Ajax.call([request])[0];
};
/**
* Enables/disabled sorting on column
*
* @param {Number} reportId
* @param {Number} columnId
* @param {Boolean} enabled
* @param {Number} direction
* @return {Promise}
*/
export const toggleColumnSorting = (reportId, columnId, enabled, direction) => {
const request = {
methodname: 'core_reportbuilder_columns_sort_toggle',
args: {reportid: reportId, columnid: columnId, enabled: enabled, direction: direction}
};
return Ajax.call([request])[0];
};
+95
View File
@@ -0,0 +1,95 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder selectors
*
* @module core_reportbuilder/local/selectors
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Selectors for the Report builder subsystem
*
* @property {Object} regions
* @property {String} regions.systemReport System report page region
* @property {String} regions.filterButtonLabel Filters form toggle region
* @property {String} regions.filtersForm Filters form page region
*/
const SELECTORS = {
regions: {
report: '[data-region="core_reportbuilder/report"]',
reportTable: '[data-region="reportbuilder-table"]',
columnHeader: '[data-region="column-header"]',
filterButtonLabel: '[data-region="filter-button-label"]',
filtersForm: '[data-region="filters-form"]',
sidebarMenu: '[data-region="sidebar-menu"]',
sidebarCard: '[data-region="sidebar-card"]',
sidebarItem: '[data-region="sidebar-item"]',
settingsConditions: '[data-region="settings-conditions"]',
activeConditions: '[data-region="active-conditions"]',
activeCondition: '[data-region="active-condition"]',
settingsFilters: '[data-region="settings-filters"]',
activeFilters: '[data-region="active-filters"]',
activeFilter: '[data-region="active-filter"]',
settingsSorting: '[data-region="settings-sorting"]',
audiencesContainer: '[data-region="audiences"]',
audienceFormContainer: '[data-region="audience-form-container"]',
audienceCard: '[data-region="audience-card"]',
audienceHeading: '[data-region="audience-heading"]',
audienceForm: '[data-region="audience-form"]',
audienceEmptyMessage: '[data-region=no-instances-message]',
audienceDescription: '[data-region=audience-description]',
audienceNotSavedLabel: '[data-region=audience-not-saved]',
settingsCardView: '[data-region="settings-cardview"]',
},
actions: {
reportActionPopup: '[data-action="report-action-popup"]',
reportCreate: '[data-action="report-create"]',
reportEdit: '[data-action="report-edit"]',
reportDelete: '[data-action="report-delete"]',
reportAddColumn: '[data-action="report-add-column"]',
reportRemoveColumn: '[data-action="report-remove-column"]',
reportAddCondition: '[data-action="report-add-condition"]',
reportRemoveCondition: '[data-action="report-remove-condition"]',
reportAddFilter: '[data-action="report-add-filter"]',
reportRemoveFilter: '[data-action="report-remove-filter"]',
reportToggleColumnSort: '[data-action="report-toggle-column-sorting"]',
reportToggleColumnSortDirection: '[data-action="report-toggle-sort-direction"]',
sidebarSearch: '[data-action="sidebar-search"]',
toggleEditPreview: '[data-action="toggle-edit-preview"]',
audienceAdd: '[data-action="add-audience"]',
audienceEdit: '[data-action="edit-audience"]',
audienceDelete: '[data-action="delete-audience"]',
toggleCardView: '[data-action="toggle-card"]',
scheduleCreate: '[data-action="schedule-create"]',
scheduleToggle: '[data-action="schedule-toggle"]',
scheduleEdit: '[data-action="schedule-edit"]',
scheduleSend: '[data-action="schedule-send"]',
scheduleDelete: '[data-action="schedule-delete"]',
},
};
/**
* Selector for given report
*
* @method forReport
* @param {Number} reportId
* @return {String}
*/
SELECTORS.forReport = reportId => `${SELECTORS.regions.report}[data-report-id="${reportId}"]`;
export default SELECTORS;
+107
View File
@@ -0,0 +1,107 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder report management
*
* @module core_reportbuilder/report
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Notification from 'core/notification';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {setPageNumber, refreshTableContent} from 'core_table/dynamic';
import * as tableSelectors from 'core_table/local/dynamic/selectors';
const CLASSES = {
COLLAPSED: 'collapsed',
EXPANDED: 'show',
ICONUP: 'fa-angle-up',
ICONDOWN: 'fa-angle-down'
};
let initialized = false;
/**
* Initialise module for given report
*
* @method
*/
export const init = () => {
if (initialized) {
// We already added the event listeners (can be called multiple times by mustache template).
return;
}
// Listen for the table reload event.
document.addEventListener(reportEvents.tableReload, async(event) => {
const reportElement = event.target.closest(reportSelectors.regions.report);
if (reportElement === null) {
return;
}
const tableRoot = reportElement.querySelector(tableSelectors.main.region);
const pageNumber = event.detail?.preservePagination ? null : 1;
await setPageNumber(tableRoot, pageNumber, false)
.then(refreshTableContent)
.then(() => {
// TODO: Refactor this after MDL-73130 lands.
const preserveTriggerElement = event.detail?.preserveTriggerElement;
if (preserveTriggerElement) {
reportElement.querySelector(preserveTriggerElement)?.focus();
}
return;
})
.catch(Notification.exception);
});
// Listen for trigger popup events.
document.addEventListener('click', event => {
const reportActionPopup = event.target.closest(reportSelectors.actions.reportActionPopup);
if (reportActionPopup === null) {
return;
}
event.preventDefault();
const popupAction = JSON.parse(reportActionPopup.dataset.popupAction);
window.openpopup(event, popupAction.jsfunctionargs);
});
// Listen for card view toggle events.
document.addEventListener('click', (event) => {
const toggleCard = event.target.closest(reportSelectors.actions.toggleCardView);
if (toggleCard) {
const tableCard = toggleCard.closest('tr');
const toggleIcon = toggleCard.querySelector('i');
event.preventDefault();
if (toggleCard.classList.contains(CLASSES.COLLAPSED)) {
tableCard.classList.add(CLASSES.EXPANDED);
toggleIcon.classList.replace(CLASSES.ICONDOWN, CLASSES.ICONUP);
toggleCard.classList.remove(CLASSES.COLLAPSED);
toggleCard.setAttribute('aria-expanded', "true");
} else {
tableCard.classList.remove(CLASSES.EXPANDED);
toggleIcon.classList.replace(CLASSES.ICONUP, CLASSES.ICONDOWN);
toggleCard.classList.add(CLASSES.COLLAPSED);
toggleCard.removeAttribute('aria-expanded');
}
}
});
initialized = true;
};
+119
View File
@@ -0,0 +1,119 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder reports list management
*
* @module core_reportbuilder/reports_list
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import {dispatchEvent} from 'core/event_dispatcher';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import {getString} from 'core/str';
import {add as addToast} from 'core/toast';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {deleteReport} from 'core_reportbuilder/local/repository/reports';
import {createReportModal} from 'core_reportbuilder/local/repository/modals';
/**
* Initialise module
*/
export const init = () => {
prefetchStrings('core_reportbuilder', [
'deletereport',
'deletereportconfirm',
'editreportdetails',
'newreport',
'reportdeleted',
'reportupdated',
]);
prefetchStrings('core', [
'delete',
]);
document.addEventListener('click', event => {
const reportCreate = event.target.closest(reportSelectors.actions.reportCreate);
if (reportCreate) {
event.preventDefault();
// Redirect user to editing interface for the report after submission.
const reportModal = createReportModal(event.target, getString('newreport', 'core_reportbuilder'));
reportModal.addEventListener(reportModal.events.FORM_SUBMITTED, event => {
window.location.href = event.detail;
});
reportModal.show();
}
const reportEdit = event.target.closest(reportSelectors.actions.reportEdit);
if (reportEdit) {
event.preventDefault();
// Reload current report page after submission.
// Use triggerElement to return focus to the action menu toggle.
const triggerElement = reportEdit.closest('.dropdown').querySelector('.dropdown-toggle');
const reportModal = createReportModal(triggerElement, getString('editreportdetails', 'core_reportbuilder'),
reportEdit.dataset.reportId);
reportModal.addEventListener(reportModal.events.FORM_SUBMITTED, () => {
const reportElement = event.target.closest(reportSelectors.regions.report);
getString('reportupdated', 'core_reportbuilder')
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);
return;
})
.catch(Notification.exception);
});
reportModal.show();
}
const reportDelete = event.target.closest(reportSelectors.actions.reportDelete);
if (reportDelete) {
event.preventDefault();
// Use triggerElement to return focus to the action menu toggle.
const triggerElement = reportDelete.closest('.dropdown').querySelector('.dropdown-toggle');
Notification.saveCancelPromise(
getString('deletereport', 'core_reportbuilder'),
getString('deletereportconfirm', 'core_reportbuilder', reportDelete.dataset.reportName),
getString('delete', 'core'),
{triggerElement}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/reports:delete');
const reportElement = event.target.closest(reportSelectors.regions.report);
return deleteReport(reportDelete.dataset.reportId)
.then(() => addToast(getString('reportdeleted', 'core_reportbuilder')))
.then(() => {
dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}).catch(() => {
return;
});
}
});
};
+194
View File
@@ -0,0 +1,194 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder audiences
*
* @module core_reportbuilder/schedules
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import {dispatchEvent} from 'core/event_dispatcher';
import 'core/inplace_editable';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import {getString} from 'core/str';
import {add as addToast} from 'core/toast';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {createScheduleModal} from 'core_reportbuilder/local/repository/modals';
import {deleteSchedule, sendSchedule, toggleSchedule} from 'core_reportbuilder/local/repository/schedules';
let initialized = false;
/**
* Initialise schedules tab
*
* @param {Number} reportId
*/
export const init = reportId => {
prefetchStrings('core_reportbuilder', [
'deleteschedule',
'deletescheduleconfirm',
'disableschedule',
'editscheduledetails',
'enableschedule',
'newschedule',
'schedulecreated',
'scheduledeleted',
'schedulesent',
'scheduleupdated',
'sendschedule',
'sendscheduleconfirm',
]);
prefetchStrings('core', [
'confirm',
'delete',
]);
if (initialized) {
// We already added the event listeners (can be called multiple times by mustache template).
return;
}
document.addEventListener('click', event => {
// Create schedule.
const scheduleCreate = event.target.closest(reportSelectors.actions.scheduleCreate);
if (scheduleCreate) {
event.preventDefault();
const scheduleModal = createScheduleModal(event.target, getString('newschedule', 'core_reportbuilder'), reportId);
scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED, () => {
getString('schedulecreated', 'core_reportbuilder')
.then(addToast)
.then(() => {
const reportElement = document.querySelector(reportSelectors.regions.report);
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return;
})
.catch(Notification.exception);
});
scheduleModal.show();
}
// Toggle schedule.
const scheduleToggle = event.target.closest(reportSelectors.actions.scheduleToggle);
if (scheduleToggle) {
const pendingPromise = new Pending('core_reportbuilder/schedules:toggle');
const scheduleStateToggle = +!Number(scheduleToggle.dataset.state);
toggleSchedule(reportId, scheduleToggle.dataset.id, scheduleStateToggle)
.then(() => {
const tableRow = scheduleToggle.closest('tr');
tableRow.classList.toggle('text-muted');
scheduleToggle.dataset.state = scheduleStateToggle;
const stringKey = scheduleStateToggle ? 'disableschedule' : 'enableschedule';
return getString(stringKey, 'core_reportbuilder');
})
.then(toggleLabel => {
const labelContainer = scheduleToggle.parentElement.querySelector(`label[for="${scheduleToggle.id}"] > span`);
labelContainer.innerHTML = toggleLabel;
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
// Edit schedule.
const scheduleEdit = event.target.closest(reportSelectors.actions.scheduleEdit);
if (scheduleEdit) {
event.preventDefault();
// Use triggerElement to return focus to the action menu toggle.
const triggerElement = scheduleEdit.closest('.dropdown').querySelector('.dropdown-toggle');
const scheduleModal = createScheduleModal(triggerElement, getString('editscheduledetails', 'core_reportbuilder'),
reportId, scheduleEdit.dataset.scheduleId);
scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED, () => {
getString('scheduleupdated', 'core_reportbuilder')
.then(addToast)
.then(() => {
const reportElement = scheduleEdit.closest(reportSelectors.regions.report);
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return;
})
.catch(Notification.exception);
});
scheduleModal.show();
}
// Send schedule.
const scheduleSend = event.target.closest(reportSelectors.actions.scheduleSend);
if (scheduleSend) {
event.preventDefault();
// Use triggerElement to return focus to the action menu toggle.
const triggerElement = scheduleSend.closest('.dropdown').querySelector('.dropdown-toggle');
Notification.saveCancelPromise(
getString('sendschedule', 'core_reportbuilder'),
getString('sendscheduleconfirm', 'core_reportbuilder', scheduleSend.dataset.scheduleName),
getString('confirm', 'core'),
{triggerElement}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/schedules:send');
return sendSchedule(reportId, scheduleSend.dataset.scheduleId)
.then(addToast(getString('schedulesent', 'core_reportbuilder')))
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
}).catch(() => {
return;
});
}
// Delete schedule.
const scheduleDelete = event.target.closest(reportSelectors.actions.scheduleDelete);
if (scheduleDelete) {
event.preventDefault();
// Use triggerElement to return focus to the action menu toggle.
const triggerElement = scheduleDelete.closest('.dropdown').querySelector('.dropdown-toggle');
Notification.saveCancelPromise(
getString('deleteschedule', 'core_reportbuilder'),
getString('deletescheduleconfirm', 'core_reportbuilder', scheduleDelete.dataset.scheduleName),
getString('delete', 'core'),
{triggerElement}
).then(() => {
const pendingPromise = new Pending('core_reportbuilder/schedules:delete');
return deleteSchedule(reportId, scheduleDelete.dataset.scheduleId)
.then(addToast(getString('scheduledeleted', 'core_reportbuilder')))
.then(() => {
const reportElement = scheduleDelete.closest(reportSelectors.regions.report);
dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}).catch(() => {
return;
});
}
});
initialized = true;
};
+101
View File
@@ -0,0 +1,101 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder sidebar component
*
* @module core_reportbuilder/sidebar
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Pending from 'core/pending';
import {debounce} from 'core/utils';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
const DEBOUNCE_TIMER = 250;
const CLASSES = {
EXPANDED: 'show',
COLLAPSED: 'collapsed',
HIDE: 'd-none',
};
/**
* Initialise module
*
* @param {Event} event
* @param {Element} sidebarMenu
*/
const sidebarCardFilter = (event, sidebarMenu) => {
const pendingPromise = new Pending('core_reportbuilder/sidebar:cardFilter');
const sidebarCards = sidebarMenu.querySelectorAll(reportSelectors.regions.sidebarCard);
const sidebarItems = sidebarMenu.querySelectorAll(reportSelectors.regions.sidebarItem);
const searchTerm = event.target.value.toLowerCase();
// Toggle items according to match against search term.
sidebarItems.forEach(item => {
const itemContent = item.textContent.toLowerCase();
item.classList.toggle(CLASSES.HIDE, !itemContent.includes(searchTerm));
});
// Toggle cards according to whether they have any visible items.
sidebarCards.forEach(card => {
const visibleItems = card.querySelectorAll(`${reportSelectors.regions.sidebarItem}:not(.${CLASSES.HIDE})`);
card.classList.toggle(CLASSES.HIDE, !visibleItems.length);
expandCard(card);
});
pendingPromise.resolve();
};
/**
* Show a collapsed card.
* This function simulates the behaviour of JQuery show method on a collapsible element.
*
* @param {Element} card
*/
const expandCard = (card) => {
let cardButton = card.querySelector('[data-toggle="collapse"]');
if (cardButton.classList.contains(CLASSES.COLLAPSED)) {
cardButton.classList.remove(CLASSES.COLLAPSED);
cardButton.setAttribute('aria-expanded', "true");
let cardContent = card.querySelector(cardButton.dataset.target);
cardContent.classList.add(CLASSES.EXPANDED);
}
};
/**
* Initialise module
*
* @param {string} selectorId
*/
export const init = (selectorId) => {
const sidebarMenu = document.querySelector(selectorId + reportSelectors.regions.sidebarMenu);
const sidebarSearch = sidebarMenu.querySelector(reportSelectors.actions.sidebarSearch);
// Debounce the event listener to allow the user to finish typing.
const sidebarSearchDebounce = debounce(sidebarCardFilter, DEBOUNCE_TIMER);
sidebarSearch.addEventListener('keyup', event => {
const pendingPromise = new Pending('core_reportbuilder/sidebar:keyup');
sidebarSearchDebounce(event, sidebarMenu);
setTimeout(() => {
pendingPromise.resolve();
}, DEBOUNCE_TIMER);
});
};