From e6779782dce63c2abc6f9ad9a657416e4e14be8e Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Thu, 1 Apr 2021 15:46:37 +0200 Subject: [PATCH] MDL-71134 course: new course editor reactive module The new course editor for Moodle 4.0 is the first AMD module that will use the new reactive library. This commit creates creates the initial structure of the new course editor frontend. --- course/amd/build/courseeditor.min.js | 2 + course/amd/build/courseeditor.min.js.map | 1 + course/amd/build/events.min.js | 2 +- course/amd/build/events.min.js.map | 2 +- .../local/courseeditor/courseeditor.min.js | 2 + .../courseeditor/courseeditor.min.js.map | 1 + .../build/local/courseeditor/mutations.min.js | 2 + .../local/courseeditor/mutations.min.js.map | 1 + course/amd/src/courseeditor.js | 67 ++++++++++ course/amd/src/events.js | 1 + .../src/local/courseeditor/courseeditor.js | 117 ++++++++++++++++++ .../amd/src/local/courseeditor/mutations.js | 111 +++++++++++++++++ course/lib.php | 5 + 13 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 course/amd/build/courseeditor.min.js create mode 100644 course/amd/build/courseeditor.min.js.map create mode 100644 course/amd/build/local/courseeditor/courseeditor.min.js create mode 100644 course/amd/build/local/courseeditor/courseeditor.min.js.map create mode 100644 course/amd/build/local/courseeditor/mutations.min.js create mode 100644 course/amd/build/local/courseeditor/mutations.min.js.map create mode 100644 course/amd/src/courseeditor.js create mode 100644 course/amd/src/local/courseeditor/courseeditor.js create mode 100644 course/amd/src/local/courseeditor/mutations.js diff --git a/course/amd/build/courseeditor.min.js b/course/amd/build/courseeditor.min.js new file mode 100644 index 00000000000..f0227f5a5d3 --- /dev/null +++ b/course/amd/build/courseeditor.min.js @@ -0,0 +1,2 @@ +define ("core_course/courseeditor",["exports","core_course/local/courseeditor/mutations","core_course/local/courseeditor/courseeditor","core_course/events"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=a.courseEditor=void 0;b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b){if(b===void 0){b=document}b.dispatchEvent(new CustomEvent(d.default.stateChanged,{bubbles:!0,detail:a}))}var g=new c.default({name:"CourseEditor",eventName:d.default.stateChanged,eventDispatch:f,mutations:new b.default});a.courseEditor=g;a.init=function init(a){g.loadCourse(a)}}); +//# sourceMappingURL=courseeditor.min.js.map diff --git a/course/amd/build/courseeditor.min.js.map b/course/amd/build/courseeditor.min.js.map new file mode 100644 index 00000000000..f2bb4faa9a7 --- /dev/null +++ b/course/amd/build/courseeditor.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/courseeditor.js"],"names":["dispatchStateChangedEvent","detail","target","document","dispatchEvent","CustomEvent","events","stateChanged","bubbles","courseEditor","CourseEditor","name","eventName","eventDispatch","mutations","DefaultMutations","init","courseId","loadCourse"],"mappings":"0QAuBA,OACA,OACA,O,mDAYA,QAASA,CAAAA,CAAT,CAAmCC,CAAnC,CAA2CC,CAA3C,CAAmD,CAC/C,GAAIA,CAAM,SAAV,CAA0B,CACtBA,CAAM,CAAGC,QACZ,CACDD,CAAM,CAACE,aAAP,CAAqB,GAAIC,CAAAA,WAAJ,CAAgBC,UAAOC,YAAvB,CAAqC,CACtDC,OAAO,GAD+C,CAEtDP,MAAM,CAAEA,CAF8C,CAArC,CAArB,CAIH,CAKM,GAAMQ,CAAAA,CAAY,CAAG,GAAIC,UAAJ,CAAiB,CACzCC,IAAI,CAAE,cADmC,CAEzCC,SAAS,CAAEN,UAAOC,YAFuB,CAGzCM,aAAa,CAAEb,CAH0B,CAMzCc,SAAS,CAAE,GAAIC,UAN0B,CAAjB,CAArB,C,wBAca,QAAPC,CAAAA,IAAO,CAACC,CAAD,CAAc,CAC9BR,CAAY,CAACS,UAAb,CAAwBD,CAAxB,CACH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core_course/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DefaultMutations from 'core_course/local/courseeditor/mutations';\nimport CourseEditor from 'core_course/local/courseeditor/courseeditor';\nimport events from 'core_course/events';\n\n/**\n * Trigger a state changed event.\n *\n * This function will be moved to core_course/events module\n * when the file is migrated to the new JS events structure proposed in MDL-70990.\n *\n * @method dispatchStateChangedEvent\n * @param {object} detail the full state\n * @param {object} target the custom event target (document if none provided)\n */\nfunction dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(new CustomEvent(events.stateChanged, {\n bubbles: true,\n detail: detail,\n }));\n}\n\n/**\n * This is the courseditor instance all components will register in.\n */\nexport const courseEditor = new CourseEditor({\n name: 'CourseEditor',\n eventName: events.stateChanged,\n eventDispatch: dispatchStateChangedEvent,\n // Mutations can be overridden by the format plugin using setMutations\n // but we need the default one at least.\n mutations: new DefaultMutations(),\n});\n\n/**\n * This method is called only once to load the initial state when the page is ready.\n *\n * @param {int} courseId the current course id\n */\nexport const init = (courseId) => {\n courseEditor.loadCourse(courseId);\n};\n"],"file":"courseeditor.min.js"} \ No newline at end of file diff --git a/course/amd/build/events.min.js b/course/amd/build/events.min.js index f9dac2e1512..a8d0f3b2d93 100644 --- a/course/amd/build/events.min.js +++ b/course/amd/build/events.min.js @@ -1,2 +1,2 @@ -define ("core_course/events",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={favourited:"core_course:favourited",unfavorited:"core_course:unfavorited",manualCompletionToggled:"core_course:manualcompletiontoggled"};return a.default}); +define ("core_course/events",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={favourited:"core_course:favourited",unfavorited:"core_course:unfavorited",manualCompletionToggled:"core_course:manualcompletiontoggled",stateChanged:"core_course:stateChanged"};return a.default}); //# sourceMappingURL=events.min.js.map diff --git a/course/amd/build/events.min.js.map b/course/amd/build/events.min.js.map index 045bcbe5eaa..a06beac3493 100644 --- a/course/amd/build/events.min.js.map +++ b/course/amd/build/events.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/events.js"],"names":["favourited","unfavorited","manualCompletionToggled"],"mappings":"8IAuBe,CACXA,UAAU,CAAE,wBADD,CAEXC,WAAW,CAAE,yBAFF,CAGXC,uBAAuB,CAAE,qCAHd,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the events the course component can trigger.\n *\n * @module core_course/events\n * @package core_course\n * @copyright 2018 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n favourited: 'core_course:favourited',\n unfavorited: 'core_course:unfavorited',\n manualCompletionToggled: 'core_course:manualcompletiontoggled',\n};\n"],"file":"events.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/events.js"],"names":["favourited","unfavorited","manualCompletionToggled","stateChanged"],"mappings":"8IAuBe,CACXA,UAAU,CAAE,wBADD,CAEXC,WAAW,CAAE,yBAFF,CAGXC,uBAAuB,CAAE,qCAHd,CAIXC,YAAY,CAAE,0BAJH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the events the course component can trigger.\n *\n * @module core_course/events\n * @package core_course\n * @copyright 2018 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n favourited: 'core_course:favourited',\n unfavorited: 'core_course:unfavorited',\n manualCompletionToggled: 'core_course:manualcompletiontoggled',\n stateChanged: 'core_course:stateChanged',\n};\n"],"file":"events.min.js"} \ No newline at end of file diff --git a/course/amd/build/local/courseeditor/courseeditor.min.js b/course/amd/build/local/courseeditor/courseeditor.min.js new file mode 100644 index 00000000000..715e2fcb898 --- /dev/null +++ b/course/amd/build/local/courseeditor/courseeditor.min.js @@ -0,0 +1,2 @@ +define ("core_course/local/courseeditor/courseeditor",["exports","core/reactive","core/notification","core/log","core/ajax"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=f(c);d=f(d);e=f(e);function f(a){return a&&a.__esModule?a:{default:a}}function g(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){g=function(a){return typeof a}}else{g=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return g(a)}function h(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function i(a){for(var b=1,c;b.\n\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_course/local/courseeditor/courseeditor\n * @class core_course/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * @param {int} courseId course id\n */\n async loadCourse(courseId) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n this.courseId = courseId;\n\n let stateData;\n\n try {\n stateData = await this.getServerCourseState();\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n // Edit mode is part of the state but it could change over time.\n // Components should use isEditing method to check the editing mode instead.\n this._editing = stateData.course.editmode ?? false;\n\n this.setInitialState(stateData);\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n const courseState = await ajax.call([{\n methodname: 'core_course_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n notification.exception(error);\n }\n }\n}\n"],"file":"courseeditor.min.js"} \ No newline at end of file diff --git a/course/amd/build/local/courseeditor/mutations.min.js b/course/amd/build/local/courseeditor/mutations.min.js new file mode 100644 index 00000000000..c4db9f7fe8d --- /dev/null +++ b/course/amd/build/local/courseeditor/mutations.min.js @@ -0,0 +1,2 @@ +define ("core_course/local/courseeditor/mutations",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function d(a){return function(){var b=this,d=arguments;return new Promise(function(e,f){var i=a.apply(b,d);function g(a){c(i,e,f,g,h,"next",a)}function h(a){c(i,e,f,g,h,"throw",a)}g(void 0)})}}function e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(a,b){for(var c=0,d;c.\n\nimport ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_course/local/courseeditor/mutations\n * @class core_course/local/courseeditor/mutations\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_course_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {int} courseId\n * @param {array} ids\n */\n async _callEditWebservice(action, courseId, ids) {\n let ajaxresult = await ajax.call([{\n methodname: 'core_course_update_course',\n args: {\n action,\n courseid: courseId,\n ids,\n }\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} statemanager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(statemanager, cmids) {\n const state = statemanager.state;\n const updates = await this._callEditWebservice('cm_state', state.course.id, cmids);\n statemanager.setReadOnly(false);\n this._processUpdates(statemanager, updates);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} statemanager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(statemanager, sectionIds) {\n const state = statemanager.state;\n const updates = await this._callEditWebservice('section_state', state.course.id, sectionIds);\n this._processUpdates(statemanager, updates);\n }\n\n /**\n * Helper to propcess both section_state and cm_state action results.\n *\n * @param {StateManager} statemanager the current state\n * @param {Array} updates of updates.\n */\n _processUpdates(statemanager, updates) {\n\n const state = statemanager.state;\n\n statemanager.setReadOnly(false);\n\n // The cm_state and section_state state action returns only updated states. However, most of the time we need this\n // mutation to fix discrepancies between the course content and the course state because core_course_edit_module\n // does not provide enough information to rebuild some state objects. This is the reason why we cannot use\n // the batch method processUpdates as the rest of mutations do.\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n // Compare the action with the current state.\n let current = state[update.name];\n if (current instanceof Map) {\n current = state[update.name].get(update.fields.id);\n }\n if (!current) {\n update.action = 'create';\n }\n\n statemanager.processUpdate(update.name, update.action, update.fields);\n });\n\n statemanager.setReadOnly(true);\n }\n}\n"],"file":"mutations.min.js"} \ No newline at end of file diff --git a/course/amd/src/courseeditor.js b/course/amd/src/courseeditor.js new file mode 100644 index 00000000000..d13a6d40746 --- /dev/null +++ b/course/amd/src/courseeditor.js @@ -0,0 +1,67 @@ +// 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 . + +/** + * Generic reactive module used in the course editor. + * + * @module core_course/courseeditor + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import DefaultMutations from 'core_course/local/courseeditor/mutations'; +import CourseEditor from 'core_course/local/courseeditor/courseeditor'; +import events from 'core_course/events'; + +/** + * Trigger a state changed event. + * + * This function will be moved to core_course/events module + * when the file is migrated to the new JS events structure proposed in MDL-70990. + * + * @method dispatchStateChangedEvent + * @param {object} detail the full state + * @param {object} target the custom event target (document if none provided) + */ +function dispatchStateChangedEvent(detail, target) { + if (target === undefined) { + target = document; + } + target.dispatchEvent(new CustomEvent(events.stateChanged, { + bubbles: true, + detail: detail, + })); +} + +/** + * This is the courseditor instance all components will register in. + */ +export const courseEditor = new CourseEditor({ + name: 'CourseEditor', + eventName: events.stateChanged, + eventDispatch: dispatchStateChangedEvent, + // Mutations can be overridden by the format plugin using setMutations + // but we need the default one at least. + mutations: new DefaultMutations(), +}); + +/** + * This method is called only once to load the initial state when the page is ready. + * + * @param {int} courseId the current course id + */ +export const init = (courseId) => { + courseEditor.loadCourse(courseId); +}; diff --git a/course/amd/src/events.js b/course/amd/src/events.js index ef9d24ed0ae..e0a6ffdb23d 100644 --- a/course/amd/src/events.js +++ b/course/amd/src/events.js @@ -25,4 +25,5 @@ export default { favourited: 'core_course:favourited', unfavorited: 'core_course:unfavorited', manualCompletionToggled: 'core_course:manualcompletiontoggled', + stateChanged: 'core_course:stateChanged', }; diff --git a/course/amd/src/local/courseeditor/courseeditor.js b/course/amd/src/local/courseeditor/courseeditor.js new file mode 100644 index 00000000000..f327a2519a1 --- /dev/null +++ b/course/amd/src/local/courseeditor/courseeditor.js @@ -0,0 +1,117 @@ +// 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 . + +import {Reactive} from 'core/reactive'; +import notification from 'core/notification'; +import log from 'core/log'; +import ajax from 'core/ajax'; + +/** + * Main course editor module. + * + * All formats can register new components on this object to create new reactive + * UI components that watch the current course state. + * + * @module core_course/local/courseeditor/courseeditor + * @class core_course/local/courseeditor/courseeditor + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +export default class extends Reactive { + + /** + * Set up the course editor when the page is ready. + * + * The course can only be loaded once per instance. Otherwise an error is thrown. + * + * @param {int} courseId course id + */ + async loadCourse(courseId) { + + if (this.courseId) { + throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`); + } + + this.courseId = courseId; + + let stateData; + + try { + stateData = await this.getServerCourseState(); + } catch (error) { + log.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"); + log.error(error); + return; + } + + // Edit mode is part of the state but it could change over time. + // Components should use isEditing method to check the editing mode instead. + this._editing = stateData.course.editmode ?? false; + + this.setInitialState(stateData); + } + + /** + * Load the current course state from the server. + * + * @returns {Object} the current course state + */ + async getServerCourseState() { + const courseState = await ajax.call([{ + methodname: 'core_course_get_state', + args: { + courseid: this.courseId, + } + }])[0]; + + const stateData = JSON.parse(courseState); + + return { + course: {}, + section: [], + cm: [], + ...stateData, + }; + } + + /** + * Return the current edit mode. + * + * Components should use this method to check if edit mode is active. + * + * @return {boolean} if edit is enabled + */ + get isEditing() { + return this._editing ?? false; + } + + /** + * Dispatch a change in the state. + * + * Usually reactive modules throw an error directly to the components when something + * goes wrong. However, course editor can directly display a notification. + * + * @method dispatch + * @param {string} actionName the action name (usually the mutation name) + * @param {*} param any number of params the mutation needs. + */ + async dispatch(...args) { + try { + await super.dispatch(...args); + } catch (error) { + notification.exception(error); + } + } +} diff --git a/course/amd/src/local/courseeditor/mutations.js b/course/amd/src/local/courseeditor/mutations.js new file mode 100644 index 00000000000..56b4485140a --- /dev/null +++ b/course/amd/src/local/courseeditor/mutations.js @@ -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 . + +import ajax from 'core/ajax'; + +/** + * Default mutation manager + * + * @module core_course/local/courseeditor/mutations + * @class core_course/local/courseeditor/mutations + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +export default class { + + // All course editor mutations for Moodle 4.0 will be located in this file. + + /** + * Private method to call core_course_update_course webservice. + * + * @method _callEditWebservice + * @param {string} action + * @param {int} courseId + * @param {array} ids + */ + async _callEditWebservice(action, courseId, ids) { + let ajaxresult = await ajax.call([{ + methodname: 'core_course_update_course', + args: { + action, + courseid: courseId, + ids, + } + }])[0]; + return JSON.parse(ajaxresult); + } + + /** + * Get updated state data related to some cm ids. + * + * @method cmState + * @param {StateManager} statemanager the current state + * @param {array} cmids the list of cm ids to update + */ + async cmState(statemanager, cmids) { + const state = statemanager.state; + const updates = await this._callEditWebservice('cm_state', state.course.id, cmids); + statemanager.setReadOnly(false); + this._processUpdates(statemanager, updates); + } + + /** + * Get updated state data related to some section ids. + * + * @method sectionState + * @param {StateManager} statemanager the current state + * @param {array} sectionIds the list of section ids to update + */ + async sectionState(statemanager, sectionIds) { + const state = statemanager.state; + const updates = await this._callEditWebservice('section_state', state.course.id, sectionIds); + this._processUpdates(statemanager, updates); + } + + /** + * Helper to propcess both section_state and cm_state action results. + * + * @param {StateManager} statemanager the current state + * @param {Array} updates of updates. + */ + _processUpdates(statemanager, updates) { + + const state = statemanager.state; + + statemanager.setReadOnly(false); + + // The cm_state and section_state state action returns only updated states. However, most of the time we need this + // mutation to fix discrepancies between the course content and the course state because core_course_edit_module + // does not provide enough information to rebuild some state objects. This is the reason why we cannot use + // the batch method processUpdates as the rest of mutations do. + updates.forEach((update) => { + if (update.name === undefined) { + throw Error('Missing state update name'); + } + // Compare the action with the current state. + let current = state[update.name]; + if (current instanceof Map) { + current = state[update.name].get(update.fields.id); + } + if (!current) { + update.action = 'create'; + } + + statemanager.processUpdate(update.name, update.action, update.fields); + }); + + statemanager.setReadOnly(true); + } +} diff --git a/course/lib.php b/course/lib.php index 63f191c85ae..908651fe31d 100644 --- a/course/lib.php +++ b/course/lib.php @@ -3215,6 +3215,11 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = return false; } + // All the new editor elements will be loaded after the course is presented and + // the initial course state will be generated using core_course_get_state webservice. + $PAGE->requires->js_call_amd('core_course/courseeditor', 'init', [$course->id]); + // TODO: as part of MDL-70907, add a way to indicate the plugin needs the legacy libraries (and get a deprecation message). + if (!$config) { $config = new stdClass(); }