MDL-66893 mod_forum: update navigation bar in grader UI
[moodle.git] / mod / forum / amd / src / local / grades / grader.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * This module will tie together all of the different calls the gradable module will make.
18  *
19  * @module     mod_forum/local/grades/grader
20  * @package    mod_forum
21  * @copyright  2019 Mathew May <mathew.solutions>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 import Templates from 'core/templates';
25 import Selectors from './local/grader/selectors';
26 import getUserPicker from './local/grader/user_picker';
27 import {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';
28 import getGradingPanelFunctions from './local/grader/gradingpanel';
29 import {add as addToast} from 'core/toast';
30 import {get_string as getString} from 'core/str';
31 import {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';
32 import {addIconToContainerWithPromise} from 'core/loadingicon';
34 const templateNames = {
35     grader: {
36         app: 'mod_forum/local/grades/grader',
37         gradingPanel: {
38             error: 'mod_forum/local/grades/local/grader/gradingpanel/error',
39         },
40     },
41 };
43 /**
44  * Helper function that replaces the user picker placeholder with what we get back from the user picker class.
45  *
46  * @param {HTMLElement} root
47  * @param {String} html
48  */
49 const displayUserPicker = (root, html) => {
50     const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);
51     Templates.replaceNodeContents(pickerRegion, html, '');
52 };
54 /**
55  * To be removed, this is now done as a part of Templates.renderForPromise()
56  *
57  * @param {String} html
58  * @param {String} js
59  * @return {[*, *]}
60  */
61 const fetchContentFromRender = (html, js) => {
62     return [html, js];
63 };
65 /**
66  * Here we build the function that is passed to the user picker that'll handle updating the user content area
67  * of the grading interface.
68  *
69  * @param {HTMLElement} root
70  * @param {Function} getContentForUser
71  * @param {Function} getGradeForUser
72  * @return {Function}
73  */
74 const getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser) => {
75     return async(user) => {
76         const spinner = addIconToContainerWithPromise(root);
77         const [
78             [html, js],
79             userGrade,
80         ] = await Promise.all([
81             getContentForUser(user.id).then(fetchContentFromRender),
82             getGradeForUser(user.id),
83         ]);
84         Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);
86         const [
87             gradingPanelHtml,
88             gradingPanelJS
89         ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);
90         const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);
91         const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);
92         Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);
93         panelContainer.scrollTop = 0;
94         spinner.resolve();
95     };
96 };
98 /**
99  * Add click handlers to the buttons in the header of the grading interface.
100  *
101  * @param {HTMLElement} graderLayout
102  * @param {Object} userPicker
103  * @param {Function} saveGradeFunction
104  */
105 const registerEventListeners = (graderLayout, userPicker, saveGradeFunction) => {
106     const graderContainer = graderLayout.getContainer();
107     graderContainer.addEventListener('click', (e) => {
108         if (e.target.closest(Selectors.buttons.toggleFullscreen)) {
109             e.stopImmediatePropagation();
110             e.preventDefault();
111             graderLayout.toggleFullscreen();
113             return;
114         }
116         if (e.target.closest(Selectors.buttons.closeGrader)) {
117             e.stopImmediatePropagation();
118             e.preventDefault();
120             graderLayout.close();
122             return;
123         }
125         if (e.target.closest(Selectors.buttons.saveGrade)) {
126             saveGradeFunction(userPicker.currentUser);
127         }
128     });
129 };
131 /**
132  * Get the function used to save a user grade.
133  *
134  * @param {HTMLElement} root The container for the grader
135  * @param {Function} setGradeForUser The function that will be called.
136  * @return {Function}
137  */
138 const getSaveUserGradeFunction = (root, setGradeForUser) => {
139     return async(user) => {
140         try {
141             root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';
142             const result = await setGradeForUser(user.id, root.querySelector(Selectors.regions.gradingPanel));
143             if (result.success) {
144                 addToast(await getString('grades:gradesavedfor', 'mod_forum', user));
145             }
146             if (result.failed) {
147                 displayGradingError(root, user, result.error);
148             }
150             return result;
151         } catch (err) {
152             displayGradingError(root, user, err);
154             return failedUpdate(err);
155         }
156     };
157 };
159 /**
160  * Display a grading error, typically from a failed save.
161  *
162  * @param {HTMLElement} root The container for the grader
163  * @param {Object} user The user who was errored
164  * @param {Object} err The details of the error
165  */
166 const displayGradingError = async(root, user, err) => {
167     const [
168         {html, js},
169         errorString
170     ] = await Promise.all([
171         Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),
172         await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),
173     ]);
175     Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);
176     addToast(errorString);
177 };
179 /**
180  * Launch the grader interface with the specified parameters.
181  *
182  * @param {Function} getListOfUsers A function to get the list of users
183  * @param {Function} getContentForUser A function to get the content for a specific user
184  * @param {Function} getGradeForUser A function get the grade details for a specific user
185  * @param {Function} setGradeForUser A function to set the grade for a specific user
186  */
187 export const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {
188     initialUserId = null, moduleName, courseName, courseUrl
189 } = {}) => {
191     // We need all of these functions to be executed in series, if one step runs before another the interface
192     // will not work.
193     const [
194         graderLayout,
195         {html, js},
196         userList,
197     ] = await Promise.all([
198         createFullScreenWindow({fullscreen: false, showLoader: false}),
199         Templates.renderForPromise(templateNames.grader.app, {
200             moduleName,
201             courseName,
202             courseUrl,
203             drawer: {show: true}
204         }),
205         getListOfUsers(),
206     ]);
207     const graderContainer = graderLayout.getContainer();
209     const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);
211     Templates.replaceNodeContents(graderContainer, html, js);
212     const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser);
214     // Fetch the userpicker for display.
215     const userPicker = await getUserPicker(
216         userList,
217         updateUserContent,
218         saveGradeFunction,
219         {
220             initialUserId,
221         },
222     );
224     // Register all event listeners.
225     registerEventListeners(graderLayout, userPicker, saveGradeFunction);
227     // Display the newly created user picker.
228     displayUserPicker(graderContainer, userPicker.rootNode);
229 };
231 export {getGradingPanelFunctions};