reportbuilder/amd/src/audience.js

// 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;
};