\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_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {\n const args = {\n action,\n courseid: courseId,\n ids,\n };\n if (targetSectionId) {\n args.targetsectionid = targetSectionId;\n }\n if (targetCmId) {\n args.targetcmid = targetCmId;\n }\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args,\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Execute a basic section state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} sectionIds the section ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _sectionBasicAction(stateManager, action, sectionIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n sectionIds,\n targetSectionId,\n targetCmId\n );\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Execute a basic course module state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} cmIds the cm ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _cmBasicAction(stateManager, action, cmIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n cmIds,\n targetSectionId,\n targetCmId\n );\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmIds, false);\n }\n\n /**\n * Mutation module initialize.\n *\n * The reactive instance will execute this method when addMutations or setMutation is invoked.\n *\n * @param {StateManager} stateManager the state manager\n */\n init(stateManager) {\n // Add a method to prepare the fields when some update is comming from the server.\n stateManager.addUpdateTypes({\n prepareFields: this._prepareFields,\n });\n }\n\n /**\n * Add default values to state elements.\n *\n * This method is called every time a webservice returns a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n _prepareFields(stateManager, updateName, fields) {\n // Any update should unlock the element.\n fields.locked = false;\n return fields;\n }\n\n /**\n * Hides sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionHide(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_hide', sectionIds);\n }\n\n /**\n * Show sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionShow(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_show', sectionIds);\n }\n\n /**\n * Show cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmShow(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_show', cmIds);\n }\n\n /**\n * Hide cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmHide(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_hide', cmIds);\n }\n\n /**\n * Stealth cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmStealth(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_stealth', cmIds);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * Note that one of targetSectionId or targetCmId should be provided in order to identify the\n * new location:\n * - targetCmId: the activities will be located avobe the target cm. The targetSectionId\n * value will be ignored in this case.\n * - targetSectionId: the activities will be appended to the section. In this case\n * targetSectionId should not be present.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmids the list of cm ids to move\n * @param {number} targetSectionId the target section id\n * @param {number} targetCmId the target course module id\n */\n async cmMove(stateManager, cmids, targetSectionId, targetCmId) {\n if (!targetSectionId && !targetCmId) {\n throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);\n }\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmids, true);\n const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMove(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMove requires targetSectionId`);\n }\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Add a new section to a specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} targetSectionId optional the target section id\n */\n async addSection(stateManager, targetSectionId) {\n if (!targetSectionId) {\n targetSectionId = 0;\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_add', course.id, [], targetSectionId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Delete sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of course modules ids\n */\n async sectionDelete(stateManager, sectionIds) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_delete', course.id, sectionIds);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Mark or unmark course modules as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} dragValue the new dragging value\n */\n cmDrag(stateManager, cmIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'cm', cmIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course sections as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} dragValue the new dragging value\n */\n sectionDrag(stateManager, sectionIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'section', sectionIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course modules as complete.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} complete the new completion value\n */\n cmCompletion(stateManager, cmIds, complete) {\n const newValue = (complete) ? 1 : 0;\n this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue);\n }\n\n /**\n * Move cms to the right: indent = 1.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveRight(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveright', cmIds);\n }\n\n /**\n * Move cms to the left: indent = 0.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveLeft(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveleft', cmIds);\n }\n\n /**\n * Lock or unlock course modules.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} lockValue the new locked value\n */\n cmLock(stateManager, cmIds, lockValue) {\n this._setElementsValue(stateManager, 'cm', cmIds, 'locked', lockValue);\n }\n\n /**\n * Lock or unlock course sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} lockValue the new locked value\n */\n sectionLock(stateManager, sectionIds, lockValue) {\n this._setElementsValue(stateManager, 'section', sectionIds, 'locked', lockValue);\n }\n\n _setElementsValue(stateManager, name, ids, fieldName, newValue) {\n stateManager.setReadOnly(false);\n ids.forEach((id) => {\n const element = stateManager.get(name, id);\n if (element) {\n element[fieldName] = newValue;\n }\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Set the page current item.\n *\n * Only one element of the course state can be the page item at a time.\n *\n * There are several actions that can alter the page current item. For example, when the user is in an activity\n * page, the page item is always the activity one. However, in a course page, when the user scrolls to an element,\n * this element get the page item.\n *\n * If the page item is static means that it is not meant to change. This is important because\n * static page items has some special logic. For example, if a cm is the static page item\n * and it is inside a collapsed section, the course index will expand the section to make it visible.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {String|undefined} type the element type (section or cm). Undefined will remove the current page item.\n * @param {Number|undefined} id the element id\n * @param {boolean|undefined} isStatic if the page item is static\n */\n setPageItem(stateManager, type, id, isStatic) {\n let newPageItem;\n if (type !== undefined) {\n newPageItem = stateManager.get(type, id);\n if (!newPageItem) {\n return;\n }\n }\n stateManager.setReadOnly(false);\n // Remove the current page item.\n const course = stateManager.get('course');\n course.pageItem = null;\n // Save the new page item.\n if (newPageItem) {\n course.pageItem = {\n id,\n type,\n sectionId: (type == 'section') ? newPageItem.id : newPageItem.sectionid,\n isStatic,\n };\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Unlock all course elements.\n *\n * @param {StateManager} stateManager the current state manager\n */\n unlockAll(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.section.forEach((section) => {\n section.locked = false;\n });\n state.cm.forEach((cm) => {\n cm.locked = false;\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Update the course index collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionIndexCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Update the course content collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionContentCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Private batch update for a section preference attribute.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {string} preferenceName the preference name\n * @param {array} sectionIds the affected section ids\n * @param {boolean} preferenceValue the new preferenceValue value\n * @return {Number[]|null} sections ids with the preference value true or null if no update is required\n */\n _updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) {\n stateManager.setReadOnly(false);\n const affectedSections = new Set();\n // Check if we need to update preferences.\n sectionIds.forEach(sectionId => {\n const section = stateManager.get('section', sectionId);\n if (section === undefined) {\n return null;\n }\n const newValue = preferenceValue ?? section[preferenceName];\n if (section[preferenceName] != newValue) {\n section[preferenceName] = newValue;\n affectedSections.add(section.id);\n }\n });\n stateManager.setReadOnly(true);\n if (affectedSections.size == 0) {\n return null;\n }\n // Get all collapsed section ids.\n const collapsedSectionIds = [];\n const state = stateManager.state;\n state.section.forEach(section => {\n if (section[preferenceName]) {\n collapsedSectionIds.push(section.id);\n }\n });\n return collapsedSectionIds;\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 this.cmLock(stateManager, cmids, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_state', course.id, cmids);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\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 this.sectionLock(stateManager, sectionIds, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_state', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Get the full updated state data of the course.\n *\n * @param {StateManager} stateManager the current state\n */\n async courseState(stateManager) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('course_state', course.id);\n stateManager.processUpdates(updates);\n }\n\n}\n"],"names":["action","courseId","ids","targetSectionId","targetCmId","args","courseid","targetsectionid","targetcmid","ajaxresult","ajax","call","methodname","JSON","parse","stateManager","sectionIds","course","get","sectionLock","updates","this","_callEditWebservice","id","processUpdates","cmIds","cmLock","init","addUpdateTypes","prepareFields","_prepareFields","updateName","fields","locked","_sectionBasicAction","_cmBasicAction","cmids","Error","cmDrag","dragValue","setPageItem","_setElementsValue","sectionDrag","cmCompletion","complete","newValue","lockValue","name","fieldName","setReadOnly","forEach","element","type","isStatic","newPageItem","undefined","pageItem","sectionId","sectionid","unlockAll","state","section","cm","collapsed","collapsedIds","_updateStateSectionPreference","preferenceName","preferenceValue","affectedSections","Set","add","size","collapsedSectionIds","push"],"mappings":";;;;;;;;iMAuC8BA,OAAQC,SAAUC,IAAKC,gBAAiBC,kBACxDC,KAAO,CACTL,OAAAA,OACAM,SAAUL,SACVC,IAAAA,KAEAC,kBACAE,KAAKE,gBAAkBJ,iBAEvBC,aACAC,KAAKG,WAAaJ,gBAElBK,iBAAmBC,cAAKC,KAAK,CAAC,CAC9BC,WAAY,kCACZP,KAAAA,QACA,UACGQ,KAAKC,MAAML,sCAWIM,aAAcf,OAAQgB,WAAYb,gBAAiBC,kBACnEa,OAASF,aAAaG,IAAI,eAC3BC,YAAYJ,aAAcC,YAAY,SACrCI,cAAgBC,KAAKC,oBACvBtB,OACAiB,OAAOM,GACPP,WACAb,gBACAC,YAEJW,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,wBAW1BD,aAAcf,OAAQyB,MAAOtB,gBAAiBC,kBACzDa,OAASF,aAAaG,IAAI,eAC3BQ,OAAOX,aAAcU,OAAO,SAC3BL,cAAgBC,KAAKC,oBACvBtB,OACAiB,OAAOM,GACPE,MACAtB,gBACAC,YAEJW,aAAaS,eAAeJ,cACvBM,OAAOX,aAAcU,OAAO,GAUrCE,KAAKZ,cAEDA,aAAaa,eAAe,CACxBC,cAAeR,KAAKS,iBAc5BA,eAAef,aAAcgB,WAAYC,eAErCA,OAAOC,QAAS,EACTD,yBAQOjB,aAAcC,kBACtBK,KAAKa,oBAAoBnB,aAAc,eAAgBC,8BAQ/CD,aAAcC,kBACtBK,KAAKa,oBAAoBnB,aAAc,eAAgBC,yBAQpDD,aAAcU,aACjBJ,KAAKc,eAAepB,aAAc,UAAWU,oBAQ1CV,aAAcU,aACjBJ,KAAKc,eAAepB,aAAc,UAAWU,uBAQvCV,aAAcU,aACpBJ,KAAKc,eAAepB,aAAc,aAAcU,oBAkB7CV,aAAcqB,MAAOjC,gBAAiBC,gBAC1CD,kBAAoBC,iBACf,IAAIiC,sEAERpB,OAASF,aAAaG,IAAI,eAC3BQ,OAAOX,aAAcqB,OAAO,SAC3BhB,cAAgBC,KAAKC,oBAAoB,UAAWL,OAAOM,GAAIa,MAAOjC,gBAAiBC,YAC7FW,aAAaS,eAAeJ,cACvBM,OAAOX,aAAcqB,OAAO,qBAUnBrB,aAAcC,WAAYb,qBACnCA,sBACK,IAAIkC,6DAERpB,OAASF,aAAaG,IAAI,eAC3BC,YAAYJ,aAAcC,YAAY,SACrCI,cAAgBC,KAAKC,oBAAoB,eAAgBL,OAAOM,GAAIP,WAAYb,iBACtFY,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,oBAS9BD,aAAcZ,iBACtBA,kBACDA,gBAAkB,SAEhBc,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,cAAeL,OAAOM,GAAI,GAAIpB,iBAC7EY,aAAaS,eAAeJ,6BASZL,aAAcC,kBACxBC,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,iBAAkBL,OAAOM,GAAIP,YAC5ED,aAAaS,eAAeJ,SAUhCkB,OAAOvB,aAAcU,MAAOc,gBACnBC,YAAYzB,mBACZ0B,kBAAkB1B,aAAc,KAAMU,MAAO,WAAYc,WAUlEG,YAAY3B,aAAcC,WAAYuB,gBAC7BC,YAAYzB,mBACZ0B,kBAAkB1B,aAAc,UAAWC,WAAY,WAAYuB,WAU5EI,aAAa5B,aAAcU,MAAOmB,gBACxBC,SAAYD,SAAY,EAAI,OAC7BH,kBAAkB1B,aAAc,KAAMU,MAAO,kBAAmBoB,4BAQvD9B,aAAcU,aACtBJ,KAAKc,eAAepB,aAAc,eAAgBU,wBAQ3CV,aAAcU,aACrBJ,KAAKc,eAAepB,aAAc,cAAeU,OAU3DC,OAAOX,aAAcU,MAAOqB,gBACnBL,kBAAkB1B,aAAc,KAAMU,MAAO,SAAUqB,WAUhE3B,YAAYJ,aAAcC,WAAY8B,gBAC7BL,kBAAkB1B,aAAc,UAAWC,WAAY,SAAU8B,WAG1EL,kBAAkB1B,aAAcgC,KAAM7C,IAAK8C,UAAWH,UAClD9B,aAAakC,aAAY,GACzB/C,IAAIgD,SAAS3B,WACH4B,QAAUpC,aAAaG,IAAI6B,KAAMxB,IACnC4B,UACAA,QAAQH,WAAaH,aAG7B9B,aAAakC,aAAY,GAqB7BT,YAAYzB,aAAcqC,KAAM7B,GAAI8B,cAC5BC,oBACSC,IAATH,OACAE,YAAcvC,aAAaG,IAAIkC,KAAM7B,KAChC+B,oBAITvC,aAAakC,aAAY,SAEnBhC,OAASF,aAAaG,IAAI,UAChCD,OAAOuC,SAAW,KAEdF,cACArC,OAAOuC,SAAW,CACdjC,GAAAA,GACA6B,KAAAA,KACAK,UAAoB,WAARL,KAAqBE,YAAY/B,GAAK+B,YAAYI,UAC9DL,SAAAA,WAGRtC,aAAakC,aAAY,GAQ7BU,UAAU5C,oBACA6C,MAAQ7C,aAAa6C,MAC3B7C,aAAakC,aAAY,GACzBW,MAAMC,QAAQX,SAASW,UACnBA,QAAQ5B,QAAS,KAErB2B,MAAME,GAAGZ,SAASY,KACdA,GAAG7B,QAAS,KAEhBlB,aAAakC,aAAY,+BAUDlC,aAAcC,WAAY+C,iBAC5CC,aAAe3C,KAAK4C,8BAA8BlD,aAAc,iBAAkBC,WAAY+C,eAC/FC,0BAGC/C,OAASF,aAAaG,IAAI,gBAC1BG,KAAKC,oBAAoB,0BAA2BL,OAAOM,GAAIyC,4CAU3CjD,aAAcC,WAAY+C,iBAC9CC,aAAe3C,KAAK4C,8BAA8BlD,aAAc,mBAAoBC,WAAY+C,eACjGC,0BAGC/C,OAASF,aAAaG,IAAI,gBAC1BG,KAAKC,oBAAoB,4BAA6BL,OAAOM,GAAIyC,cAY3EC,8BAA8BlD,aAAcmD,eAAgBlD,WAAYmD,iBACpEpD,aAAakC,aAAY,SACnBmB,iBAAmB,IAAIC,OAE7BrD,WAAWkC,SAAQO,kBACTI,QAAU9C,aAAaG,IAAI,UAAWuC,mBAC5BF,IAAZM,eACO,WAELhB,SAAWsB,MAAAA,gBAAAA,gBAAmBN,QAAQK,gBACxCL,QAAQK,iBAAmBrB,WAC3BgB,QAAQK,gBAAkBrB,SAC1BuB,iBAAiBE,IAAIT,QAAQtC,QAGrCR,aAAakC,aAAY,GACI,GAAzBmB,iBAAiBG,YACV,WAGLC,oBAAsB,UACdzD,aAAa6C,MACrBC,QAAQX,SAAQW,UACdA,QAAQK,iBACRM,oBAAoBC,KAAKZ,QAAQtC,OAGlCiD,kCAUGzD,aAAcqB,YACnBV,OAAOX,aAAcqB,OAAO,SAC3BnB,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,WAAYL,OAAOM,GAAIa,OACtErB,aAAaS,eAAeJ,cACvBM,OAAOX,aAAcqB,OAAO,sBAUlBrB,aAAcC,iBACxBG,YAAYJ,aAAcC,YAAY,SACrCC,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,gBAAiBL,OAAOM,GAAIP,YAC3ED,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,qBAQ7BD,oBACRE,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,eAAgBL,OAAOM,IACtER,aAAaS,eAAeJ"}
\ No newline at end of file
diff --git a/course/format/amd/src/local/content.js b/course/format/amd/src/local/content.js
index 16338bb3372..ab75072e1e9 100644
--- a/course/format/amd/src/local/content.js
+++ b/course/format/amd/src/local/content.js
@@ -221,6 +221,7 @@ export default class Component extends BaseComponent {
// State changes that require to reload some course modules.
{watch: `cm.visible:updated`, handler: this._reloadCm},
{watch: `cm.stealth:updated`, handler: this._reloadCm},
+ {watch: `cm.indent:updated`, handler: this._reloadCm},
// Update section number and title.
{watch: `section.number:updated`, handler: this._refreshSectionNumber},
// Collapse and expand sections.
diff --git a/course/format/amd/src/local/content/actions.js b/course/format/amd/src/local/content/actions.js
index 437b3dbf741..eb04a6a5f0c 100644
--- a/course/format/amd/src/local/content/actions.js
+++ b/course/format/amd/src/local/content/actions.js
@@ -50,6 +50,8 @@ const directMutations = {
cmHide: 'cmHide',
cmShow: 'cmShow',
cmStealth: 'cmStealth',
+ cmMoveRight: 'cmMoveRight',
+ cmMoveLeft: 'cmMoveLeft',
};
export default class extends BaseComponent {
diff --git a/course/format/amd/src/local/courseeditor/mutations.js b/course/format/amd/src/local/courseeditor/mutations.js
index 4cc3d73e576..59617627f30 100644
--- a/course/format/amd/src/local/courseeditor/mutations.js
+++ b/course/format/amd/src/local/courseeditor/mutations.js
@@ -282,6 +282,24 @@ export default class {
this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue);
}
+ /**
+ * Move cms to the right: indent = 1.
+ * @param {StateManager} stateManager the current state manager
+ * @param {array} cmIds the list of cm ids
+ */
+ async cmMoveRight(stateManager, cmIds) {
+ await this._cmBasicAction(stateManager, 'cm_moveright', cmIds);
+ }
+
+ /**
+ * Move cms to the left: indent = 0.
+ * @param {StateManager} stateManager the current state manager
+ * @param {array} cmIds the list of cm ids
+ */
+ async cmMoveLeft(stateManager, cmIds) {
+ await this._cmBasicAction(stateManager, 'cm_moveleft', cmIds);
+ }
+
/**
* Lock or unlock course modules.
*
diff --git a/course/format/classes/base.php b/course/format/classes/base.php
index 92aa28d0863..30e7f300858 100644
--- a/course/format/classes/base.php
+++ b/course/format/classes/base.php
@@ -456,14 +456,6 @@ abstract class base {
/**
* Returns true if this course format uses activity indentation.
*
- * Indentation is not supported by core formats anymore and may be deprecated in the future.
- * This method will keep a default return "true" for legacy reasons but new formats should override
- * it with a return false to prevent future deprecations.
- *
- * A message in a bottle: if indentation is finally deprecated, both behat steps i_indent_right_activity
- * and i_indent_left_activity should be removed as well. Right now no core behat uses them but indentation
- * is not officially deprecated so they are still available for the contrib formats.
- *
* @return bool if the course format uses indentation.
*/
public function uses_indentation(): bool {
diff --git a/course/format/classes/output/local/content/section/cmitem.php b/course/format/classes/output/local/content/section/cmitem.php
index 995b324f8ce..a740748672f 100644
--- a/course/format/classes/output/local/content/section/cmitem.php
+++ b/course/format/classes/output/local/content/section/cmitem.php
@@ -106,6 +106,7 @@ class cmitem implements named_templatable, renderable {
'extraclasses' => $mod->extraclasses,
'cmformat' => $item->export_for_template($output),
'hasinfo' => $hasinfo,
+ 'indent' => $mod->indent,
];
}
}
diff --git a/course/format/classes/output/local/state/cm.php b/course/format/classes/output/local/state/cm.php
index 99b5a493f4f..99458d0141f 100644
--- a/course/format/classes/output/local/state/cm.php
+++ b/course/format/classes/output/local/state/cm.php
@@ -90,6 +90,7 @@ class cm implements renderable {
'sectionnumber' => $section->section,
'uservisible' => $cm->uservisible,
'hascmrestrictions' => $this->get_has_restrictions(),
+ 'indent' => $cm->indent,
'module' => $cm->modname,
'plugin' => 'mod_' . $cm->modname,
];
diff --git a/course/format/classes/stateactions.php b/course/format/classes/stateactions.php
index a94ad2ff7d5..0f1a6ffc362 100644
--- a/course/format/classes/stateactions.php
+++ b/course/format/classes/stateactions.php
@@ -422,6 +422,79 @@ class stateactions {
}
}
+ /**
+ * Move course cms to the right. Indent = 1.
+ *
+ * @param stateupdates $updates the affected course elements track
+ * @param stdClass $course the course object
+ * @param int[] $ids cm ids
+ * @param int $targetsectionid not used
+ * @param int $targetcmid not used
+ */
+ public function cm_moveright(
+ stateupdates $updates,
+ stdClass $course,
+ array $ids = [],
+ ?int $targetsectionid = null,
+ ?int $targetcmid = null
+ ): void {
+ $this->set_cm_indentation($updates, $course, $ids, 1);
+ }
+
+ /**
+ * Move course cms to the left. Indent = 0.
+ *
+ * @param stateupdates $updates the affected course elements track
+ * @param stdClass $course the course object
+ * @param int[] $ids cm ids
+ * @param int $targetsectionid not used
+ * @param int $targetcmid not used
+ */
+ public function cm_moveleft(
+ stateupdates $updates,
+ stdClass $course,
+ array $ids = [],
+ ?int $targetsectionid = null,
+ ?int $targetcmid = null
+ ): void {
+ $this->set_cm_indentation($updates, $course, $ids, 0);
+ }
+
+ /**
+ * Internal method to define the cm indentation level.
+ *
+ * @param stateupdates $updates the affected course elements track
+ * @param stdClass $course the course object
+ * @param int[] $ids cm ids
+ * @param int $indent new value for indentation
+ */
+ protected function set_cm_indentation(
+ stateupdates $updates,
+ stdClass $course,
+ array $ids,
+ int $indent
+ ): void {
+ global $DB;
+
+ $this->validate_cms($course, $ids, __FUNCTION__);
+
+ // Check capabilities on every activity context.
+ foreach ($ids as $cmid) {
+ $modcontext = context_module::instance($cmid);
+ require_capability('moodle/course:manageactivities', $modcontext);
+ }
+ $modinfo = get_fast_modinfo($course);
+ $cms = $this->get_cm_info($modinfo, $ids);
+ list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED);
+ $DB->set_field_select('course_modules', 'indent', $indent, "id $insql", $inparams);
+ rebuild_course_cache($course->id, false, true);
+ foreach ($cms as $cm) {
+ $modcontext = context_module::instance($cm->id);
+ course_module_updated::create_from_cm($cm, $modcontext)->trigger();
+ $updates->add_cm_put($cm->id);
+ }
+ }
+
/**
* Extract several cm_info from the course_modinfo.
*
diff --git a/course/format/templates/local/content/cm.mustache b/course/format/templates/local/content/cm.mustache
index 017fd65e8ca..ee82ad698ed 100644
--- a/course/format/templates/local/content/cm.mustache
+++ b/course/format/templates/local/content/cm.mustache
@@ -53,7 +53,8 @@
}
]
},
- "modstealth": true
+ "modstealth": true,
+ "indent": 1
}
}}
basic_state_text(
+ 'cm_moveright',
+ $role,
+ ['cm0', 'cm1', 'cm2', 'cm3'],
+ $expectedexception,
+ 4,
+ null,
+ null,
+ null,
+ null,
+ 'indent',
+ 1
+ );
+ }
+
+ /**
+ * Test for cm_moveleft
+ *
+ * @covers ::cm_moveleft
+ * @dataProvider basic_role_provider
+ * @param string $role the user role
+ * @param bool $expectedexception if it will expect an exception.
+ */
+ public function test_cm_moveleft(
+ string $role = 'editingteacher',
+ bool $expectedexception = false
+ ): void {
+ $this->basic_state_text(
+ 'cm_moveleft',
+ $role,
+ ['cm0', 'cm1', 'cm2', 'cm3'],
+ $expectedexception,
+ 4,
+ null,
+ null,
+ null,
+ null,
+ 'indent',
+ 0
+ );
+ }
}
diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php
index efda54a16a5..a2f5843ef3e 100644
--- a/course/format/topics/lib.php
+++ b/course/format/topics/lib.php
@@ -50,10 +50,6 @@ class format_topics extends core_courseformat\base {
return true;
}
- public function uses_indentation(): bool {
- return false;
- }
-
/**
* Returns the display name of the given section that the course prefers.
*
diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php
index fd265ad81fe..88d8638dd59 100644
--- a/course/format/weeks/lib.php
+++ b/course/format/weeks/lib.php
@@ -49,10 +49,6 @@ class format_weeks extends core_courseformat\base {
return true;
}
- public function uses_indentation(): bool {
- return false;
- }
-
/**
* Generate the title for this section page
* @return string the page title
diff --git a/course/lib.php b/course/lib.php
index 529a01855c5..50602113e76 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -1702,7 +1702,9 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
if ($hasmanageactivities && $indent >= 0) {
$indentlimits = new stdClass();
$indentlimits->min = 0;
- $indentlimits->max = 16;
+ // Legacy indentation could continue using a limit of 16,
+ // but components based formats will be forced to use one level indentation only.
+ $indentlimits->max = ($usecomponents) ? 1 : 16;
if (right_to_left()) { // Exchange arrows on RTL
$rightarrow = 't/left';
$leftarrow = 't/right';
@@ -1717,11 +1719,16 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
$enabledclass = '';
}
$actions['moveright'] = new action_menu_link_secondary(
- new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
- new pix_icon($rightarrow, '', 'moodle', array('class' => 'iconsmall')),
+ new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '1']),
+ new pix_icon($rightarrow, '', 'moodle', ['class' => 'iconsmall']),
$str->moveright,
- array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright',
- 'data-keepopen' => true, 'data-sectionreturn' => $sr)
+ [
+ 'class' => 'editing_moveright ' . $enabledclass,
+ 'data-action' => ($usecomponents) ? 'cmMoveRight' : 'moveright',
+ 'data-keepopen' => true,
+ 'data-sectionreturn' => $sr,
+ 'data-id' => $mod->id,
+ ]
);
if ($indent <= $indentlimits->min) {
@@ -1730,11 +1737,16 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
$enabledclass = '';
}
$actions['moveleft'] = new action_menu_link_secondary(
- new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
- new pix_icon($leftarrow, '', 'moodle', array('class' => 'iconsmall')),
+ new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '0']),
+ new pix_icon($leftarrow, '', 'moodle', ['class' => 'iconsmall']),
$str->moveleft,
- array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft',
- 'data-keepopen' => true, 'data-sectionreturn' => $sr)
+ [
+ 'class' => 'editing_moveleft ' . $enabledclass,
+ 'data-action' => ($usecomponents) ? 'cmMoveLeft' : 'moveleft',
+ 'data-keepopen' => true,
+ 'data-sectionreturn' => $sr,
+ 'data-id' => $mod->id,
+ ]
);
}
diff --git a/course/tests/behat/course_controls.feature b/course/tests/behat/course_controls.feature
index 23d5dc6336d..fddf59a476c 100644
--- a/course/tests/behat/course_controls.feature
+++ b/course/tests/behat/course_controls.feature
@@ -145,3 +145,36 @@ Feature: Course activity controls works as expected
| weeks | 0 | "General" | should | should | "8 January - 14 January" |
| weeks | 1 | "1 January - 7 January" | should not | should not | "8 January - 14 January" |
| weeks | 1 | "General" | should | should not | "8 January - 14 January" |
+
+ @javascript
+ Scenario Outline: Indentation should allow one level only
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | format | coursedisplay | numsections | startdate |
+ | Course 1 | C1 | | | 5 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | forum | Test forum name | Test forum description | C1 | forum1 |
+ When I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+ And I open "Test forum name" actions menu
+ Then "Move right" "link" should be visible
+ And "Move left" "link" should not be visible
+ And I click on "Move right" "link" in the "Test forum name" activity
+ And I open "Test forum name" actions menu
+ And "Move right" "link" should not be visible
+ And "Move left" "link" should be visible
+ And I click on "Move left" "link" in the "Test forum name" activity
+ And I open "Test forum name" actions menu
+ And "Move right" "link" should be visible
+ And "Move left" "link" should not be visible
+
+ Examples:
+ | courseformat |
+ | topics |
+ | weeks |
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index a53d13d9ac2..3d8a07a0210 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -18,6 +18,8 @@ information provided here is intended especially for developers.
needs to be rendered as is and not whitened out, the `.nofilter` CSS class needs to be applied to the icon.
* New moodle_url::export_params_for_template() returns the params as an array of key => value pairs. You should use this when
you want to pass URL params to a template. An example can be found in single button.
+* Course module indentation has been recovered for course format using components via Move right/left feature.
+ Course formats using components will be allowed to use one level indentation only.
=== 4.1.2 ===
diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss
index 763bd685657..7c5f561766a 100644
--- a/theme/boost/scss/moodle/course.scss
+++ b/theme/boost/scss/moodle/course.scss
@@ -109,6 +109,19 @@ body:not(.editing) .sitetopic ul.section {
a.stealth:hover {
color: lighten($link-color, 25%) !important; /* stylelint-disable-line declaration-no-important */
}
+
+ &.indented {
+ .activity-item {
+ border: 0;
+ margin-left: map-get($spacers, 3);
+ }
+ }
+ &.indented + .indented {
+ .activity-item {
+ border-top: $border-width solid $border-color;
+ border-radius: unset;
+ }
+ }
}
.label {
diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css
index 644d4c9863f..a19589f8032 100644
--- a/theme/boost/style/moodle.css
+++ b/theme/boost/style/moodle.css
@@ -13994,6 +13994,12 @@ body:not(.editing) .sitetopic ul.section {
.section .activity a.stealth:hover {
color: #5babf2 !important;
/* stylelint-disable-line declaration-no-important */ }
+ .section .activity.indented .activity-item {
+ border: 0;
+ margin-left: 1rem; }
+ .section .activity.indented + .indented .activity-item {
+ border-top: 1px solid #dee2e6;
+ border-radius: unset; }
.section .label .contentwithoutlink,
.section .label .activityinstance {
diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css
index 96df00d6496..d2a8e3ab15b 100644
--- a/theme/classic/style/moodle.css
+++ b/theme/classic/style/moodle.css
@@ -13994,6 +13994,12 @@ body:not(.editing) .sitetopic ul.section {
.section .activity a.stealth:hover {
color: #5babf2 !important;
/* stylelint-disable-line declaration-no-important */ }
+ .section .activity.indented .activity-item {
+ border: 0;
+ margin-left: 1rem; }
+ .section .activity.indented + .indented .activity-item {
+ border-top: 1px solid #dee2e6;
+ border-radius: unset; }
.section .label .contentwithoutlink,
.section .label .activityinstance {