MDL-68463 user: Rewrite participant bulk actions in ES
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 24 Apr 2020 01:40:20 +0000 (09:40 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 18 May 2020 12:00:47 +0000 (20:00 +0800)
user/amd/build/local/participants/bulkactions.min.js [new file with mode: 0644]
user/amd/build/local/participants/bulkactions.min.js.map [new file with mode: 0644]
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/build/repository.min.js
user/amd/build/repository.min.js.map
user/amd/src/local/participants/bulkactions.js [new file with mode: 0644]
user/amd/src/participants.js
user/amd/src/repository.js
user/index.php

diff --git a/user/amd/build/local/participants/bulkactions.min.js b/user/amd/build/local/participants/bulkactions.min.js
new file mode 100644 (file)
index 0000000..90f87b3
Binary files /dev/null and b/user/amd/build/local/participants/bulkactions.min.js differ
diff --git a/user/amd/build/local/participants/bulkactions.min.js.map b/user/amd/build/local/participants/bulkactions.min.js.map
new file mode 100644 (file)
index 0000000..c09406f
Binary files /dev/null and b/user/amd/build/local/participants/bulkactions.min.js.map differ
index 56be84f..f433003 100644 (file)
Binary files a/user/amd/build/participants.min.js and b/user/amd/build/participants.min.js differ
index d204dba..d24dd4a 100644 (file)
Binary files a/user/amd/build/participants.min.js.map and b/user/amd/build/participants.min.js.map differ
index 5ff57f0..fec9ab0 100644 (file)
Binary files a/user/amd/build/repository.min.js and b/user/amd/build/repository.min.js differ
index 7d60e60..b117500 100644 (file)
Binary files a/user/amd/build/repository.min.js.map and b/user/amd/build/repository.min.js.map differ
diff --git a/user/amd/src/local/participants/bulkactions.js b/user/amd/src/local/participants/bulkactions.js
new file mode 100644 (file)
index 0000000..3872f71
--- /dev/null
@@ -0,0 +1,191 @@
+// 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/>.
+
+/**
+ * Bulk actions for lists of participants.
+ *
+ * @module     core_user/local/participants/bulkactions
+ * @package    core_user
+ * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import * as Repository from 'core_user/repository';
+import * as Str from 'core/str';
+import ModalEvents from 'core/modal_events';
+import ModalFactory from 'core/modal_factory';
+import Templates from 'core/templates';
+import {add as notifyUser} from 'core/toast';
+
+/**
+ * Show the add note popup
+ *
+ * @param {Number} courseid
+ * @param {Number[]} users
+ * @param {String[]} noteStateNames
+ * @param {HTMLElement} stateHelpIcon
+ * @return {Promise}
+ */
+export const showAddNote = (courseid, users, noteStateNames, stateHelpIcon) => {
+    if (!users.length) {
+        // No users were selected.
+        return Promise.resolve();
+    }
+
+    const states = [];
+    for (let key in noteStateNames) {
+        switch (key) {
+            case 'draft':
+                states.push({value: 'personal', label: noteStateNames[key]});
+                break;
+            case 'public':
+                states.push({value: 'course', label: noteStateNames[key], selected: 1});
+                break;
+            case 'site':
+                states.push({value: key, label: noteStateNames[key]});
+                break;
+        }
+    }
+
+    const context = {
+        stateNames: states,
+        stateHelpIcon: stateHelpIcon.innerHTML,
+    };
+
+    let titlePromise = null;
+    if (users.length === 1) {
+        titlePromise = Str.get_string('addbulknotesingle', 'core_notes');
+    } else {
+        titlePromise = Str.get_string('addbulknote', 'core_notes', users.length);
+    }
+
+    return ModalFactory.create({
+        type: ModalFactory.types.SAVE_CANCEL,
+        body: Templates.render('core_user/add_bulk_note', context),
+        title: titlePromise,
+        buttons: {
+            save: titlePromise,
+        },
+        removeOnClose: true,
+    })
+    .then(modal => {
+        modal.getRoot().on(ModalEvents.save, () => submitAddNote(courseid, users, modal));
+
+        modal.show();
+
+        return modal;
+    });
+};
+
+/**
+ * Add a note to this list of users.
+ *
+ * @param {Number} courseid
+ * @param {Number[]} users
+ * @param {Modal} modal
+ * @return {Promise}
+ */
+const submitAddNote = (courseid, users, modal) => {
+    const text = modal.getRoot().find('form textarea').val();
+    const publishstate = modal.getRoot().find('form select').val();
+
+    const notes = users.map(userid => {
+        return {
+            userid,
+            text,
+            courseid,
+            publishstate,
+        };
+    });
+
+    return Repository.createNotesForUsers(notes)
+    .then(noteIds => {
+        if (noteIds.length === 1) {
+            return Str.get_string('addbulknotedonesingle', 'core_notes');
+        } else {
+            return Str.get_string('addbulknotedone', 'core_notes', noteIds.length);
+        }
+    })
+    .then(msg => notifyUser(msg))
+    .catch(Notification.exception);
+};
+
+/**
+ * Show the send message popup.
+ *
+ * @param {Number[]} users
+ * @return {Promise}
+ */
+export const showSendMessage = users => {
+    if (!users.length) {
+        // Nothing to do.
+        return Promise.resolve();
+    }
+
+    let titlePromise;
+    if (users.length === 1) {
+        titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message');
+    } else {
+        titlePromise = Str.get_string('sendbulkmessage', 'core_message', users.length);
+    }
+
+    return ModalFactory.create({
+        type: ModalFactory.types.SAVE_CANCEL,
+        body: Templates.render('core_user/send_bulk_message', {}),
+        title: titlePromise,
+        buttons: {
+            save: titlePromise,
+        },
+        removeOnClose: true,
+    })
+    .then(modal => {
+        modal.getRoot().on(ModalEvents.save, () => {
+            submitSendMessage(modal, users);
+        });
+
+        modal.show();
+
+        return modal;
+    });
+};
+
+/**
+ * Send a message to these users.
+ *
+ * @param {Modal} modal
+ * @param {Number[]} users
+ * @return {Promise}
+ */
+const submitSendMessage = (modal, users) => {
+    const text = modal.getRoot().find('form textarea').val();
+
+    const messages = users.map(touserid => {
+        return {
+            touserid,
+            text,
+        };
+    });
+
+    return Repository.sendMessagesToUsers(messages)
+    .then(messageIds => {
+        if (messageIds.length == 1) {
+            return Str.get_string('sendbulkmessagesentsingle', 'core_message');
+        } else {
+            return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);
+        }
+    })
+    .then(msg => notifyUser(msg))
+    .catch(Notification.exception);
+};
index d2156d5..eba586a 100644 (file)
  * @copyright  2017 Damyon Wiese
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/templates', 'core/notification', 'core/ajax',
-        'core/custom_interaction_events'],
-        function($, Str, ModalFactory, ModalEvents, Templates, Notification, Ajax, CustomEvents) {
 
-    var SELECTORS = {
-        BULKACTIONSELECT: "#formactionid",
-        BULKUSERCHECKBOXES: "input.usercheckbox",
-        BULKUSERNOSCHECKBOXES: "input.usercheckbox[value='0']",
-        BULKUSERSELECTEDCHECKBOXES: "input.usercheckbox:checked",
-        BULKACTIONFORM: "#participantsform",
-        CHECKALLBUTTON: "#checkall",
-        CHECKALLNOSBUTTON: "#checkallnos"
-    };
+import DynamicTableSelectors from 'core_table/local/dynamic/selectors';
+import ModalEvents from 'core/modal_events';
+import Notification from 'core/notification';
+import CustomEvents from 'core/custom_interaction_events';
+import {showAddNote, showSendMessage} from 'core_user/local/participants/bulkactions';
+
+const Selectors = {
+    bulkActionSelect: "#formactionid",
+    bulkUserCheckBoxes: "input.usercheckbox",
+    bulkUserSelectedCheckBoxes: "input.usercheckbox:checked",
+    checkAllButton: "#checkall",
+    stateHelpIcon: '[data-region="state-help-icon"]',
+    tableForm: uniqueId => `form[data-table-unique-id="${uniqueId}"]`,
+};
+
+export const init = ({
+    uniqueid,
+    noteStateNames = {},
+}) => {
+    const root = document.querySelector(Selectors.tableForm(uniqueid));
+    const getTableFromUniqueId = uniqueId => root.querySelector(DynamicTableSelectors.main.fromRegionId(uniqueId));
 
     /**
-     * Constructor
+     * Private method.
      *
-     * @param {Object} options Object containing options. Contextid is required.
-     * Each call to templates.render gets it's own instance of this class.
-     */
-    var Participants = function(options) {
-
-        this.courseId = options.courseid;
-        this.noteStateNames = options.noteStateNames;
-        this.stateHelpIcon = options.stateHelpIcon;
-
-        this.attachEventListeners();
-    };
-    // Class variables and functions.
-
-    /**
-     * @var {Modal} modal
-     * @private
-     */
-    Participants.prototype.modal = null;
-
-    /**
-     * @var {int} courseId
-     * @private
-     */
-    Participants.prototype.courseId = -1;
-
-    /**
-     * @var {Object} noteStateNames
-     * @private
-     */
-    Participants.prototype.noteStateNames = {};
-
-    /**
-     * @var {String} stateHelpIcon
+     * @method registerEventListeners
      * @private
      */
-    Participants.prototype.stateHelpIcon = "";
+    const registerEventListeners = () => {
+        root.querySelector(Selectors.bulkActionSelect).addEventListener(CustomEvents.events.accessibleChange, e => {
+            const action = e.target.value;
+            const tableRoot = getTableFromUniqueId(uniqueid);
+            const checkboxes = tableRoot.querySelectorAll(Selectors.bulkUserSelectedCheckBoxes);
 
-    /**
-     * Private method
-     *
-     * @method attachEventListeners
-     * @private
-     */
-    Participants.prototype.attachEventListeners = function() {
-        CustomEvents.define(SELECTORS.BULKACTIONSELECT, [CustomEvents.events.accessibleChange]);
-        $(SELECTORS.BULKACTIONSELECT).on(CustomEvents.events.accessibleChange, function(e) {
-            var action = $(e.target).val();
             if (action.indexOf('#') !== -1) {
                 e.preventDefault();
 
-                var ids = [];
-                $(SELECTORS.BULKUSERSELECTEDCHECKBOXES).each(function(index, ele) {
-                    var name = $(ele).attr('name');
-                    var id = name.replace('user', '');
-                    ids.push(id);
+                const ids = [];
+                checkboxes.forEach(checkbox => {
+                    ids.push(checkbox.getAttribute('name').replace('user', ''));
                 });
 
-                if (action == '#messageselect') {
-                    this.showSendMessage(ids).fail(Notification.exception);
-                } else if (action == '#addgroupnote') {
-                    this.showAddNote(ids).fail(Notification.exception);
-                }
-                $(SELECTORS.BULKACTIONSELECT + ' option[value=""]').prop('selected', 'selected');
-            } else if (action !== '') {
-                if ($(SELECTORS.BULKUSERSELECTEDCHECKBOXES).length > 0) {
-                    $(SELECTORS.BULKACTIONFORM).submit();
-                } else {
-                    $(SELECTORS.BULKACTIONSELECT + ' option[value=""]').prop('selected', 'selected');
+                let bulkAction;
+                if (action === '#messageselect') {
+                    bulkAction = showSendMessage(ids);
+                } else if (action === '#addgroupnote') {
+                    bulkAction = showAddNote(
+                        root.dataset.courseId,
+                        ids,
+                        noteStateNames,
+                        root.querySelector(Selectors.stateHelpIcon)
+                    );
                 }
-            }
-        }.bind(this));
 
-        $(SELECTORS.CHECKALLBUTTON).on('click', function() {
-            var showallink = $(this).data('showallink');
-            if (showallink) {
-                window.location = showallink;
+                if (bulkAction) {
+                    bulkAction
+                    .then(modal => {
+                        modal.getRoot().on(ModalEvents.hidden, () => {
+                            // Focus on the action select when the dialog is closed.
+                            const bulkActionSelector = root.querySelector(Selectors.bulkActionSelect);
+                            bulkActionSelector.focus();
+                        });
+
+                        return modal;
+                    })
+                    .catch(Notification.exception);
+                }
+            } else if (action !== '' && checkboxes.length) {
+                e.target.form.submit();
             }
-        });
 
-        $(SELECTORS.CHECKALLNOSBUTTON).on('click', function() {
-            $(SELECTORS.BULKUSERNOSCHECKBOXES).prop('checked', true);
+            resetBulkAction(e.target);
         });
-    };
-
-    /**
-     * Show the add note popup
-     *
-     * @method showAddNote
-     * @private
-     * @param {int[]} users
-     * @return {Promise}
-     */
-    Participants.prototype.showAddNote = function(users) {
-
-        if (users.length == 0) {
-            // Nothing to do.
-            return $.Deferred().resolve().promise();
-        }
-
-        var states = [];
-        for (var key in this.noteStateNames) {
-            switch (key) {
-                case 'draft':
-                    states.push({value: 'personal', label: this.noteStateNames[key]});
-                    break;
-                case 'public':
-                    states.push({value: 'course', label: this.noteStateNames[key], selected: 1});
-                    break;
-                case 'site':
-                    states.push({value: key, label: this.noteStateNames[key]});
-                    break;
-            }
-        }
-
-        var context = {stateNames: states, stateHelpIcon: this.stateHelpIcon};
-        var titlePromise = null;
-        if (users.length == 1) {
-            titlePromise = Str.get_string('addbulknotesingle', 'core_notes');
-        } else {
-            titlePromise = Str.get_string('addbulknote', 'core_notes', users.length);
-        }
 
-        return $.when(
-            ModalFactory.create({
-                type: ModalFactory.types.SAVE_CANCEL,
-                body: Templates.render('core_user/add_bulk_note', context)
-            }),
-            titlePromise
-        ).then(function(modal, title) {
-            // Keep a reference to the modal.
-            this.modal = modal;
-            this.modal.setTitle(title);
-            this.modal.setSaveButtonText(title);
-
-            // We want to focus on the action select when the dialog is closed.
-            this.modal.getRoot().on(ModalEvents.hidden, function() {
-                var notification = $('#user-notifications [role=alert]');
-                if (notification.length) {
-                    notification.focus();
-                } else {
-                    $(SELECTORS.BULKACTIONSELECT).focus();
+        root.addEventListener('click', e => {
+            const checkAllButton = e.target.closest(Selectors.checkAllButton);
+            if (checkAllButton) {
+                const showAllLink = checkAllButton.dataset.showalllink;
+                if (showAllLink) {
+                    window.location = showAllLink;
                 }
-                this.modal.getRoot().remove();
-            }.bind(this));
-
-            this.modal.getRoot().on(ModalEvents.save, this.submitAddNote.bind(this, users));
-
-            this.modal.show();
-
-            return this.modal;
-        }.bind(this));
-    };
-
-    /**
-     * Add a note to this list of users.
-     *
-     * @method submitAddNote
-     * @private
-     * @param {int[]} users
-     * @return {Promise}
-     */
-    Participants.prototype.submitAddNote = function(users) {
-        var noteText = this.modal.getRoot().find('form textarea').val();
-        var publishState = this.modal.getRoot().find('form select').val();
-        var notes = [],
-            i = 0;
-
-        for (i = 0; i < users.length; i++) {
-            notes.push({userid: users[i], text: noteText, courseid: this.courseId, publishstate: publishState});
-        }
-
-        return Ajax.call([{
-            methodname: 'core_notes_create_notes',
-            args: {notes: notes}
-        }])[0].then(function(noteIds) {
-            if (noteIds.length == 1) {
-                return Str.get_string('addbulknotedonesingle', 'core_notes');
-            } else {
-                return Str.get_string('addbulknotedone', 'core_notes', noteIds.length);
             }
-        }).then(function(msg) {
-            Notification.addNotification({
-                message: msg,
-                type: "success"
-            });
-            return true;
-        }).catch(Notification.exception);
-    };
-
-    /**
-     * Show the send message popup.
-     *
-     * @method showSendMessage
-     * @private
-     * @param {int[]} users
-     * @return {Promise}
-     */
-    Participants.prototype.showSendMessage = function(users) {
-
-        if (users.length == 0) {
-            // Nothing to do.
-            return $.Deferred().resolve().promise();
-        }
-        var titlePromise = null;
-        if (users.length == 1) {
-            titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message');
-        } else {
-            titlePromise = Str.get_string('sendbulkmessage', 'core_message', users.length);
-        }
-
-        return $.when(
-            ModalFactory.create({
-                type: ModalFactory.types.SAVE_CANCEL,
-                body: Templates.render('core_user/send_bulk_message', {})
-            }),
-            titlePromise
-        ).then(function(modal, title) {
-            // Keep a reference to the modal.
-            this.modal = modal;
-
-            this.modal.setTitle(title);
-            this.modal.setSaveButtonText(title);
-
-            // We want to focus on the action select when the dialog is closed.
-            this.modal.getRoot().on(ModalEvents.hidden, function() {
-                $(SELECTORS.BULKACTIONSELECT).focus();
-                this.modal.getRoot().remove();
-            }.bind(this));
-
-            this.modal.getRoot().on(ModalEvents.save, this.submitSendMessage.bind(this, users));
-
-            this.modal.show();
-
-            return this.modal;
-        }.bind(this));
+        });
     };
 
-    /**
-     * Send a message to these users.
-     *
-     * @method submitSendMessage
-     * @private
-     * @param {int[]} users
-     * @param {Event} e Form submission event.
-     * @return {Promise}
-     */
-    Participants.prototype.submitSendMessage = function(users) {
-
-        var messageText = this.modal.getRoot().find('form textarea').val();
-
-        var messages = [],
-            i = 0;
-
-        for (i = 0; i < users.length; i++) {
-            messages.push({touserid: users[i], text: messageText});
-        }
-
-        return Ajax.call([{
-            methodname: 'core_message_send_instant_messages',
-            args: {messages: messages}
-        }])[0].then(function(messageIds) {
-            if (messageIds.length == 1) {
-                return Str.get_string('sendbulkmessagesentsingle', 'core_message');
-            } else {
-                return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);
-            }
-        }).then(function(msg) {
-            Notification.addNotification({
-                message: msg,
-                type: "success"
-            });
-            return true;
-        }).catch(Notification.exception);
+    const resetBulkAction = bulkActionSelect => {
+        bulkActionSelect.value = '';
     };
 
-    return /** @alias module:core_user/participants */ {
-        // Public variables and functions.
-
-        /**
-         * Initialise the unified user filter.
-         *
-         * @method init
-         * @param {Object} options - List of options.
-         * @return {Participants}
-         */
-        'init': function(options) {
-            return new Participants(options);
-        }
-    };
-});
+    registerEventListeners();
+};
index 7be9378..c719bc1 100644 (file)
@@ -51,3 +51,19 @@ export const submitUserEnrolmentForm = formdata => {
         },
     }])[0];
 };
+
+export const createNotesForUsers = notes => {
+    return fetchMany([{
+        methodname: 'core_notes_create_notes',
+        args: {
+            notes
+        }
+    }])[0];
+};
+
+export const sendMessagesToUsers = messages => {
+    return fetchMany([{
+        methodname: 'core_message_send_instant_messages',
+        args: {messages}
+    }])[0];
+};
index 92f81e2..4aa5fcb 100644 (file)
@@ -289,7 +289,13 @@ ob_end_clean();
 echo html_writer::tag('p', get_string('participantscount', 'moodle', $participanttable->totalrows));
 
 if ($bulkoperations) {
-    echo '<form action="action_redir.php" method="post" id="participantsform">';
+    echo html_writer::start_tag('form', [
+        'action' => 'action_redir.php',
+        'method' => 'post',
+        'id' => 'participantsform',
+        'data-course-id' => $course->id,
+        'data-table-unique-id' => $participanttable->uniqueid,
+    ]);
     echo '<div>';
     echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
     echo '<input type="hidden" name="returnto" value="'.s($PAGE->url->out(false)).'" />';
@@ -392,13 +398,14 @@ if ($bulkoperations) {
     echo html_writer::tag('div', $label . $select);
 
     echo '<input type="hidden" name="id" value="' . $course->id . '" />';
+    echo '<div class="d-none" data-region="state-help-icon">' . $OUTPUT->help_icon('publishstate', 'notes') . '</div>';
     echo '</div></div></div>';
     echo '</form>';
 
-    $options = new stdClass();
-    $options->courseid = $course->id;
-    $options->noteStateNames = note_get_state_names();
-    $options->stateHelpIcon = $OUTPUT->help_icon('publishstate', 'notes');
+    $options = (object) [
+        'uniqueid' => $participanttable->uniqueid,
+        'noteStateNames' => note_get_state_names(),
+    ];
     $PAGE->requires->js_call_amd('core_user/participants', 'init', [$options]);
 }