From 472c0eedd3186e4aa0569060e9d6344b48a5e006 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Tue, 2 Jul 2024 15:49:29 +0100 Subject: [PATCH] WIP --- .../amd/build/category.min.js | 2 +- .../amd/build/category.min.js.map | 2 +- .../amd/build/categorymanager.min.js | 2 +- .../amd/build/categorymanager.min.js.map | 2 +- .../bank/managecategories/amd/src/category.js | 107 ++++++++- .../amd/src/categorymanager.js | 86 ++++--- .../classes/external/move_category.php | 227 ++++++++++++++++++ .../classes/output/categories.php | 3 + .../classes/question_category_object.php | 4 +- .../bank/managecategories/db/services.php | 8 + .../templates/categories.mustache | 3 +- .../managecategories/templates/item.mustache | 4 +- .../tests/behat/question_categories.feature | 14 +- question/bank/managecategories/version.php | 2 +- 14 files changed, 403 insertions(+), 63 deletions(-) create mode 100644 question/bank/managecategories/classes/external/move_category.php diff --git a/question/bank/managecategories/amd/build/category.min.js b/question/bank/managecategories/amd/build/category.min.js index e4d245f7a5a04..497a18d8ca5c2 100644 --- a/question/bank/managecategories/amd/build/category.min.js +++ b/question/bank/managecategories/amd/build/category.min.js @@ -1,3 +1,3 @@ -define("qbank_managecategories/category",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={},this.dragdrop=new _reactive.DragDrop(this),this.dragdrop.setDraggable(!0),window.console.log(this.dragdrop)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}getDraggableData(){return{id:this.element.dataset.categoryid}}validateDropData(dropData){return window.console.log(dropData),!0}dragStart(){window.console.log("dragging!!!")}dragEnd(){window.console.log("drag ended!!!")}drop(dropData){window.console.log("dropped!!!"),window.console.log(dropData)}}return _exports.default=_default,_exports.default})); +define("qbank_managecategories/category",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={CATEGORY_LIST:".qbank_managecategories-categorylist",LIST_ITEM:".qbank_managecategories-item[data-categoryid]"}}stateReady(){this.dragdrop=new _reactive.DragDrop(this)}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}getDraggableData(){return{id:this.element.dataset.categoryid}}validateDropData(){return!0}drop(dropData,event){var _precedingSibling;const dropTarget=event.target.closest(this.selectors.LIST_ITEM);if(!dropTarget)return;if(!document.getElementById("category-".concat(dropData.id)))return;const insertBefore=this.getInsertBefore(event,dropTarget),targetParentId=dropTarget.dataset.parent,parentList=dropTarget.closest(this.selectors.CATEGORY_LIST);let precedingSibling;precedingSibling=insertBefore&&dropTarget===parentList.firstElementChild?null:insertBefore?dropTarget.previousElementSibling:dropTarget,_categorymanager.categorymanager.setCatOrder(dropData.id,targetParentId,null===(_precedingSibling=precedingSibling)||void 0===_precedingSibling?void 0:_precedingSibling.dataset.categoryid)}getInsertBefore(event,dropTarget){return event.clientY-dropTarget.getBoundingClientRect().top [data-sortorder="'.concat(element.sortorder-1,'"]')),nextSibling=null===(_previousSibling=previousSibling)||void 0===_previousSibling?void 0:_previousSibling.nextElementSibling);(newParent!==this.element.parentElement||nextSibling!==this.element)&&(nextSibling?newParent.insertBefore(this.element,nextSibling):newParent.appendChild(this.element)),this.element.dataset.sortorder=element.sortorder}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=category.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/category.min.js.map b/question/bank/managecategories/amd/build/category.min.js.map index 2f5d5e47c11fe..ec8d03f8cb301 100644 --- a/question/bank/managecategories/amd/build/category.min.js.map +++ b/question/bank/managecategories/amd/build/category.min.js.map @@ -1 +1 @@ -{"version":3,"file":"category.min.js","sources":["../src/category.js"],"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 * The category component.\n *\n * @module qbank_managecategories/category\n * @class qbank_managecategories/category\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {};\n this.dragdrop = new DragDrop(this);\n this.dragdrop.setDraggable(true);\n window.console.log(this.dragdrop);\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n selectors,\n reactive: categorymanager,\n });\n }\n\n getDraggableData() {\n return {\n id: this.element.dataset.categoryid\n };\n }\n\n validateDropData(dropData) {\n window.console.log(dropData);\n return true;\n }\n\n dragStart() {\n window.console.log('dragging!!!');\n }\n\n dragEnd() {\n window.console.log('drag ended!!!');\n }\n\n drop(dropData) {\n window.console.log('dropped!!!');\n window.console.log(dropData);\n }\n}"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","dragdrop","DragDrop","this","setDraggable","window","console","log","target","document","querySelector","reactive","categorymanager","getDraggableData","dataset","categoryid","validateDropData","dropData","dragStart","dragEnd","drop"],"mappings":"oQAyB6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,QACZC,SAAW,IAAIC,mBAASC,WACxBF,SAASG,cAAa,GAC3BC,OAAOC,QAAQC,IAAIJ,KAAKF,sBAUhBO,OAAQR,kBACT,IAAIG,KAAK,CACZL,QAASW,SAASC,cAAcF,QAChCR,UAAAA,UACAW,SAAUC,mCAIlBC,yBACW,CACHd,GAAII,KAAKL,QAAQgB,QAAQC,YAIjCC,iBAAiBC,iBACbZ,OAAOC,QAAQC,IAAIU,WACZ,EAGXC,YACIb,OAAOC,QAAQC,IAAI,eAGvBY,UACId,OAAOC,QAAQC,IAAI,iBAGvBa,KAAKH,UACDZ,OAAOC,QAAQC,IAAI,cACnBF,OAAOC,QAAQC,IAAIU"} \ No newline at end of file +{"version":3,"file":"category.min.js","sources":["../src/category.js"],"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 * The category component.\n *\n * @module qbank_managecategories/category\n * @class qbank_managecategories/category\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n CATEGORY_LIST: '.qbank_managecategories-categorylist',\n LIST_ITEM: '.qbank_managecategories-item[data-categoryid]',\n };\n }\n\n stateReady() {\n this.dragdrop = new DragDrop(this);\n }\n\n destroy() {\n // The draggable element must be unregistered.\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n selectors,\n reactive: categorymanager,\n });\n }\n\n getDraggableData() {\n return {\n id: this.element.dataset.categoryid\n };\n }\n\n validateDropData() {\n return true;\n }\n\n drop(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.LIST_ITEM);\n\n if (!dropTarget) {\n return;\n }\n\n const source = document.getElementById(`category-${dropData.id}`);\n\n if (!source) {\n return;\n }\n\n const insertBefore = this.getInsertBefore(event, dropTarget);\n const targetParentId = dropTarget.dataset.parent;\n const parentList = dropTarget.closest(this.selectors.CATEGORY_LIST);\n let precedingSibling;\n\n if (insertBefore && dropTarget === parentList.firstElementChild) {\n // Dropped at the top of the list.\n precedingSibling = null;\n } else {\n precedingSibling = insertBefore ? dropTarget.previousElementSibling : dropTarget;\n }\n\n // Insert the category after the target category\n categorymanager.setCatOrder(dropData.id, targetParentId, precedingSibling?.dataset.categoryid);\n }\n\n getInsertBefore(event, dropTarget) {\n // Get the current mouse position within the drop target\n const mouseY = event.clientY - dropTarget.getBoundingClientRect().top;\n\n // Get the height of the drop target\n const targetHeight = dropTarget.clientHeight;\n\n // Check if the mouse is over the top half of the drop target\n return mouseY < targetHeight / 2;\n }\n\n getWatchers() {\n return [\n {watch: `categories[${this.element.dataset.categoryid}]:updated`, handler: this.updatePosition},\n ];\n }\n\n updateSortorder({element}) {\n this.element.dataset.sortorder = element.sortorder;\n }\n\n updatePosition({element}) {\n let newParent;\n if (parseInt(this.element.dataset.parent) !== element.parent) {\n newParent = document.querySelector(`ul[data-categoryid=\"${element.parent}\"]`);\n this.element.dataset.parent = element.parent;\n } else {\n newParent = this.element.parentElement;\n }\n\n let previousSibling;\n let nextSibling;\n if (element.sortorder === 0 && newParent.firstElementChild) {\n // Move to the top of the list.\n nextSibling = newParent.firstElementChild;\n } else {\n // Move later in the list.\n previousSibling = newParent.querySelector(`:scope > [data-sortorder=\"${element.sortorder - 1}\"]`);\n nextSibling = previousSibling?.nextElementSibling;\n }\n\n // Check if this has actually moved, or if it's just having its sortorder updated due to another element moving.\n const moved = (newParent !== this.element.parentElement || nextSibling !== this.element);\n\n if (moved) {\n if (nextSibling) {\n // Move to the specified position in the list.\n newParent.insertBefore(this.element, nextSibling);\n } else {\n // Move to the end of the list (may also be the top of the list is empty).\n newParent.appendChild(this.element);\n }\n }\n this.element.dataset.sortorder = element.sortorder;\n }\n\n}"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","CATEGORY_LIST","LIST_ITEM","stateReady","dragdrop","DragDrop","this","destroy","undefined","unregister","target","document","querySelector","reactive","categorymanager","getDraggableData","dataset","categoryid","validateDropData","drop","dropData","event","dropTarget","closest","getElementById","insertBefore","getInsertBefore","targetParentId","parent","parentList","precedingSibling","firstElementChild","previousElementSibling","setCatOrder","_precedingSibling","clientY","getBoundingClientRect","top","clientHeight","getWatchers","watch","handler","updatePosition","updateSortorder","sortorder","newParent","previousSibling","nextSibling","parseInt","parentElement","_previousSibling","nextElementSibling","appendChild"],"mappings":"oQAyB6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,cAAe,uCACfC,UAAW,iDAInBC,kBACSC,SAAW,IAAIC,mBAASC,MAGjCC,eAE0BC,IAAlBF,KAAKF,eACAA,SAASK,yBAWVC,OAAQV,kBACT,IAAIM,KAAK,CACZR,QAASa,SAASC,cAAcF,QAChCV,UAAAA,UACAa,SAAUC,mCAIlBC,yBACW,CACHhB,GAAIO,KAAKR,QAAQkB,QAAQC,YAIjCC,0BACW,EAGXC,KAAKC,SAAUC,mCACLC,WAAaD,MAAMX,OAAOa,QAAQjB,KAAKN,UAAUE,eAElDoB,sBAIUX,SAASa,kCAA2BJ,SAASrB,kBAMtD0B,aAAenB,KAAKoB,gBAAgBL,MAAOC,YAC3CK,eAAiBL,WAAWN,QAAQY,OACpCC,WAAaP,WAAWC,QAAQjB,KAAKN,UAAUC,mBACjD6B,iBAIAA,iBAFAL,cAAgBH,aAAeO,WAAWE,kBAEvB,KAEAN,aAAeH,WAAWU,uBAAyBV,4CAI1DW,YAAYb,SAASrB,GAAI4B,yCAAgBG,qDAAAI,kBAAkBlB,QAAQC,YAGvFS,gBAAgBL,MAAOC,mBAEJD,MAAMc,QAAUb,WAAWc,wBAAwBC,IAG7Cf,WAAWgB,aAGD,EAGnCC,oBACW,CACH,CAACC,2BAAqBlC,KAAKR,QAAQkB,QAAQC,wBAAuBwB,QAASnC,KAAKoC,iBAIxFC,0BAAgB7C,QAACA,mBACRA,QAAQkB,QAAQ4B,UAAY9C,QAAQ8C,UAG7CF,0BACQG,UAQAC,gBACAC,aAVOjD,QAACA,qCAERkD,SAAS1C,KAAKR,QAAQkB,QAAQY,UAAY9B,QAAQ8B,QAClDiB,UAAYlC,SAASC,4CAAqCd,QAAQ8B,mBAC7D9B,QAAQkB,QAAQY,OAAS9B,QAAQ8B,QAEtCiB,UAAYvC,KAAKR,QAAQmD,cAKH,IAAtBnD,QAAQ8C,WAAmBC,UAAUd,mBAErCgB,YAAcF,UAAUd,mBAGxBe,gBAAkBD,UAAUjC,kDAA2Cd,QAAQ8C,UAAY,SAC3FG,qCAAcD,mDAAAI,iBAAiBC,qBAIpBN,YAAcvC,KAAKR,QAAQmD,eAAiBF,cAAgBzC,KAAKR,WAGxEiD,YAEAF,UAAUpB,aAAanB,KAAKR,QAASiD,aAGrCF,UAAUO,YAAY9C,KAAKR,eAG9BA,QAAQkB,QAAQ4B,UAAY9C,QAAQ8C"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/categorymanager.min.js b/question/bank/managecategories/amd/build/categorymanager.min.js index 0d9df698d8b15..923d45e6c32c3 100644 --- a/question/bank/managecategories/amd/build/categorymanager.min.js +++ b/question/bank/managecategories/amd/build/categorymanager.min.js @@ -1,3 +1,3 @@ -define("qbank_managecategories/categorymanager",["exports","core/reactive","qbank_managecategories/mutations","qbank_managecategories/events"],(function(_exports,_reactive,_mutations,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.categorymanager=void 0;const SELECTORS_CATEGORY_LIST=".qbank_managecategories-categorylist",SELECTORS_CONTEXT=".qbank_managecategories-categorylist[data-contextid]";class CategoryManager extends _reactive.Reactive{static getCategoryState(item){const categories=[];return item.children&&item.children.forEach((category=>{let child={categoryid:category.dataset.categoryid,categoryname:category.dataset.categoryname,categories:null,firstchild:category===item.children[0]};const childList=category.querySelector(SELECTORS_CATEGORY_LIST);childList&&(child.categories=this.getCategoryState(childList)),categories.push(child)})),categories}}const categorymanager=new CategoryManager({name:"qtype_managecategories_categorymanager",eventName:_events.eventTypes.qbankManagecategoriesStateUpdated,eventDispatch:_events.notifyQbankManagecategoriesStateUpdated,mutations:_mutations.mutations});_exports.categorymanager=categorymanager;_exports.init=()=>{const state={contexts:[]};document.querySelectorAll(SELECTORS_CONTEXT).forEach((context=>{const stateContext={id:context.dataset.contextid,contextname:context.dataset.contextname,categories:[],hascategories:!1};stateContext.categories=CategoryManager.getCategoryState(context),stateContext.hascategories=stateContext.categories.length>0,state.contexts.push(stateContext)})),categorymanager.setInitialState(state)}})); +define("qbank_managecategories/categorymanager",["exports","core/reactive","qbank_managecategories/mutations","qbank_managecategories/events","core/ajax","core/notification"],(function(_exports,_reactive,_mutations,_events,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.categorymanager=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);const SELECTORS_LIST_ITEM=".qbank_managecategories-item[data-categoryid]",SELECTORS_MODULE_ROOT="#categoriesrendered";class CategoryManager extends _reactive.Reactive{setCatOrder(categoryId,targetParentId){let precedingSiblingId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const call={methodname:"qbank_managecategories_move_category",args:{pagecontextid:this.state.page.contextid,categoryid:categoryId,targetparentid:targetParentId,precedingsiblingid:precedingSiblingId}};_ajax.default.call([call])[0].then((stateUpdates=>{this.stateManager.processUpdates(stateUpdates)})).catch((error=>{var _document$getElements;_notification.default.addNotification({message:error.message,type:"error"}),null===(_document$getElements=document.getElementsByClassName("alert-danger")[0])||void 0===_document$getElements||_document$getElements.scrollIntoView()}))}}const categorymanager=new CategoryManager({name:"qtype_managecategories_categorymanager",eventName:_events.eventTypes.qbankManagecategoriesStateUpdated,eventDispatch:_events.notifyQbankManagecategoriesStateUpdated,mutations:_mutations.mutations});_exports.categorymanager=categorymanager;_exports.init=()=>{(async reactive=>{const stateData={page:{contextid:document.querySelector(SELECTORS_MODULE_ROOT).dataset.contextid},categories:[]};document.querySelectorAll(SELECTORS_LIST_ITEM).forEach((item=>{const category={id:item.dataset.categoryid,name:item.dataset.categoryname,parent:item.dataset.parent,contextid:item.dataset.contextid,sortorder:item.dataset.sortorder};stateData.categories.push(category)})),reactive.setInitialState(stateData)})(categorymanager)}})); //# sourceMappingURL=categorymanager.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/categorymanager.min.js.map b/question/bank/managecategories/amd/build/categorymanager.min.js.map index 2b83a87fc58af..557837d8b48f6 100644 --- a/question/bank/managecategories/amd/build/categorymanager.min.js.map +++ b/question/bank/managecategories/amd/build/categorymanager.min.js.map @@ -1 +1 @@ -{"version":3,"file":"categorymanager.min.js","sources":["../src/categorymanager.js"],"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 * Reactive module for category manager\n *\n * @module qbank_managecategories/categorymanager\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qbank_managecategories/mutations';\nimport {eventTypes, notifyQbankManagecategoriesStateUpdated} from 'qbank_managecategories/events';\n\nconst SELECTORS = {\n CATEGORY_LIST: '.qbank_managecategories-categorylist',\n CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',\n};\n\nclass CategoryManager extends Reactive {\n\n static getCategoryState(item) {\n const categories = [];\n if (item.children) {\n item.children.forEach(category => {\n let child = {\n categoryid: category.dataset.categoryid,\n categoryname: category.dataset.categoryname,\n categories: null,\n firstchild: category === item.children[0],\n };\n\n const childList = category.querySelector(SELECTORS.CATEGORY_LIST);\n if (childList) {\n child.categories = this.getCategoryState(childList);\n }\n categories.push(child);\n });\n }\n return categories;\n }\n}\n\n// The reactive instance requires an event (eventNamer and eventDispatch method)\nexport const categorymanager = new CategoryManager({\n name: 'qtype_managecategories_categorymanager',\n eventName: eventTypes.qbankManagecategoriesStateUpdated,\n eventDispatch: notifyQbankManagecategoriesStateUpdated,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n const state = {\n contexts: [],\n };\n const contexts = document.querySelectorAll(SELECTORS.CONTEXT);\n contexts.forEach(context => {\n const stateContext = {\n id: context.dataset.contextid,\n contextname: context.dataset.contextname,\n categories: [],\n hascategories: false,\n };\n stateContext.categories = CategoryManager.getCategoryState(context);\n stateContext.hascategories = stateContext.categories.length > 0;\n state.contexts.push(stateContext);\n });\n categorymanager.setInitialState(state);\n};\n"],"names":["SELECTORS","CategoryManager","Reactive","item","categories","children","forEach","category","child","categoryid","dataset","categoryname","firstchild","childList","querySelector","this","getCategoryState","push","categorymanager","name","eventName","eventTypes","qbankManagecategoriesStateUpdated","eventDispatch","notifyQbankManagecategoriesStateUpdated","mutations","state","contexts","document","querySelectorAll","context","stateContext","id","contextid","contextname","hascategories","length","setInitialState"],"mappings":"4SAyBMA,wBACa,uCADbA,kBAEO,6DAGPC,wBAAwBC,2CAEFC,YACdC,WAAa,UACfD,KAAKE,UACLF,KAAKE,SAASC,SAAQC,eACdC,MAAQ,CACRC,WAAYF,SAASG,QAAQD,WAC7BE,aAAcJ,SAASG,QAAQC,aAC/BP,WAAY,KACZQ,WAAYL,WAAaJ,KAAKE,SAAS,UAGrCQ,UAAYN,SAASO,cAAcd,yBACrCa,YACAL,MAAMJ,WAAaW,KAAKC,iBAAiBH,YAE7CT,WAAWa,KAAKT,UAGjBJ,kBAKFc,gBAAkB,IAAIjB,gBAAgB,CAC/CkB,KAAM,yCACNC,UAAWC,mBAAWC,kCACtBC,cAAeC,gDACfC,UAAAA,8EAMgB,WACVC,MAAQ,CACVC,SAAU,IAEGC,SAASC,iBAAiB7B,mBAClCM,SAAQwB,gBACPC,aAAe,CACjBC,GAAIF,QAAQpB,QAAQuB,UACpBC,YAAaJ,QAAQpB,QAAQwB,YAC7B9B,WAAY,GACZ+B,eAAe,GAEnBJ,aAAa3B,WAAaH,gBAAgBe,iBAAiBc,SAC3DC,aAAaI,cAAgBJ,aAAa3B,WAAWgC,OAAS,EAC9DV,MAAMC,SAASV,KAAKc,iBAExBb,gBAAgBmB,gBAAgBX"} \ No newline at end of file +{"version":3,"file":"categorymanager.min.js","sources":["../src/categorymanager.js"],"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 * Reactive module for category manager\n *\n * @module qbank_managecategories/categorymanager\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qbank_managecategories/mutations';\nimport {eventTypes, notifyQbankManagecategoriesStateUpdated} from 'qbank_managecategories/events';\nimport Ajax from \"core/ajax\";\nimport Notification from \"core/notification\";\n\nconst SELECTORS = {\n CATEGORY_LIST: '.qbank_managecategories-categorylist',\n CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',\n LIST_ITEM: '.qbank_managecategories-item[data-categoryid]',\n MODULE_ROOT: '#categoriesrendered',\n};\n\nconst loadState = async (reactive) => {\n const rootElement = document.querySelector(SELECTORS.MODULE_ROOT);\n const stateData = {\n page: {\n contextid: rootElement.dataset.contextid,\n },\n categories: [],\n };\n const listItems = document.querySelectorAll(SELECTORS.LIST_ITEM);\n listItems.forEach(item => {\n const category = {\n id: item.dataset.categoryid,\n name: item.dataset.categoryname,\n parent: item.dataset.parent,\n contextid: item.dataset.contextid,\n sortorder: item.dataset.sortorder,\n };\n stateData.categories.push(category);\n });\n reactive.setInitialState(stateData);\n};\n\nclass CategoryManager extends Reactive {\n setCatOrder(\n categoryId,\n targetParentId,\n precedingSiblingId = null,\n ) {\n const call = {\n methodname: 'qbank_managecategories_move_category',\n args: {\n pagecontextid: this.state.page.contextid,\n categoryid: categoryId,\n targetparentid: targetParentId,\n precedingsiblingid: precedingSiblingId,\n }\n };\n Ajax.call([call])[0]\n .then((stateUpdates) => {\n this.stateManager.processUpdates(stateUpdates);\n })\n .catch(error => {\n Notification.addNotification({\n message: error.message,\n type: 'error',\n });\n document.getElementsByClassName('alert-danger')[0]?.scrollIntoView();\n });\n }\n}\n\n// The reactive instance requires an event (eventNamer and eventDispatch method)\nexport const categorymanager = new CategoryManager({\n name: 'qtype_managecategories_categorymanager',\n eventName: eventTypes.qbankManagecategoriesStateUpdated,\n eventDispatch: notifyQbankManagecategoriesStateUpdated,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n loadState(categorymanager);\n};\n"],"names":["SELECTORS","CategoryManager","Reactive","setCatOrder","categoryId","targetParentId","precedingSiblingId","call","methodname","args","pagecontextid","this","state","page","contextid","categoryid","targetparentid","precedingsiblingid","then","stateUpdates","stateManager","processUpdates","catch","error","addNotification","message","type","document","getElementsByClassName","scrollIntoView","categorymanager","name","eventName","eventTypes","qbankManagecategoriesStateUpdated","eventDispatch","notifyQbankManagecategoriesStateUpdated","mutations","async","stateData","querySelector","dataset","categories","querySelectorAll","forEach","item","category","id","categoryname","parent","sortorder","push","reactive","setInitialState","loadState"],"mappings":"0gBA2BMA,oBAGS,gDAHTA,sBAIW,4BAyBXC,wBAAwBC,mBAC1BC,YACIC,WACAC,oBACAC,0EAAqB,WAEfC,KAAO,CACTC,WAAY,uCACZC,KAAM,CACFC,cAAeC,KAAKC,MAAMC,KAAKC,UAC/BC,WAAYX,WACZY,eAAgBX,eAChBY,mBAAoBX,mCAGvBC,KAAK,CAACA,OAAO,GACbW,MAAMC,oBACEC,aAAaC,eAAeF,iBAEpCG,OAAMC,wDACUC,gBAAgB,CACzBC,QAASF,MAAME,QACfC,KAAM,wCAEVC,SAASC,uBAAuB,gBAAgB,2DAAIC,2BAMvDC,gBAAkB,IAAI7B,gBAAgB,CAC/C8B,KAAM,yCACNC,UAAWC,mBAAWC,kCACtBC,cAAeC,gDACfC,UAAAA,8EAMgB,KA9DFC,OAAAA,iBAERC,UAAY,CACd1B,KAAM,CACFC,UAHYa,SAASa,cAAcxC,uBAGZyC,QAAQ3B,WAEnC4B,WAAY,IAEEf,SAASgB,iBAAiB3C,qBAClC4C,SAAQC,aACRC,SAAW,CACbC,GAAIF,KAAKJ,QAAQ1B,WACjBgB,KAAMc,KAAKJ,QAAQO,aACnBC,OAAQJ,KAAKJ,QAAQQ,OACrBnC,UAAW+B,KAAKJ,QAAQ3B,UACxBoC,UAAWL,KAAKJ,QAAQS,WAE5BX,UAAUG,WAAWS,KAAKL,aAE9BM,SAASC,gBAAgBd,YA4CzBe,CAAUxB"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/src/category.js b/question/bank/managecategories/amd/src/category.js index b1a9705d6373d..db8a28a78a16a 100644 --- a/question/bank/managecategories/amd/src/category.js +++ b/question/bank/managecategories/amd/src/category.js @@ -27,10 +27,21 @@ export default class extends BaseComponent { create(descriptor) { this.name = descriptor.element.id; - this.selectors = {}; + this.selectors = { + CATEGORY_LIST: '.qbank_managecategories-categorylist', + LIST_ITEM: '.qbank_managecategories-item[data-categoryid]', + }; + } + + stateReady() { this.dragdrop = new DragDrop(this); - this.dragdrop.setDraggable(true); - window.console.log(this.dragdrop); + } + + destroy() { + // The draggable element must be unregistered. + if (this.dragdrop !== undefined) { + this.dragdrop.unregister(); + } } /** @@ -54,21 +65,93 @@ export default class extends BaseComponent { }; } - validateDropData(dropData) { - window.console.log(dropData); + validateDropData() { return true; } - dragStart() { - window.console.log('dragging!!!'); + drop(dropData, event) { + const dropTarget = event.target.closest(this.selectors.LIST_ITEM); + + if (!dropTarget) { + return; + } + + const source = document.getElementById(`category-${dropData.id}`); + + if (!source) { + return; + } + + const insertBefore = this.getInsertBefore(event, dropTarget); + const targetParentId = dropTarget.dataset.parent; + const parentList = dropTarget.closest(this.selectors.CATEGORY_LIST); + let precedingSibling; + + if (insertBefore && dropTarget === parentList.firstElementChild) { + // Dropped at the top of the list. + precedingSibling = null; + } else { + precedingSibling = insertBefore ? dropTarget.previousElementSibling : dropTarget; + } + + // Insert the category after the target category + categorymanager.setCatOrder(dropData.id, targetParentId, precedingSibling?.dataset.categoryid); } - dragEnd() { - window.console.log('drag ended!!!'); + getInsertBefore(event, dropTarget) { + // Get the current mouse position within the drop target + const mouseY = event.clientY - dropTarget.getBoundingClientRect().top; + + // Get the height of the drop target + const targetHeight = dropTarget.clientHeight; + + // Check if the mouse is over the top half of the drop target + return mouseY < targetHeight / 2; + } + + getWatchers() { + return [ + {watch: `categories[${this.element.dataset.categoryid}]:updated`, handler: this.updatePosition}, + ]; + } + + updateSortorder({element}) { + this.element.dataset.sortorder = element.sortorder; } - drop(dropData) { - window.console.log('dropped!!!'); - window.console.log(dropData); + updatePosition({element}) { + let newParent; + if (parseInt(this.element.dataset.parent) !== element.parent) { + newParent = document.querySelector(`ul[data-categoryid="${element.parent}"]`); + this.element.dataset.parent = element.parent; + } else { + newParent = this.element.parentElement; + } + + let previousSibling; + let nextSibling; + if (element.sortorder === 0 && newParent.firstElementChild) { + // Move to the top of the list. + nextSibling = newParent.firstElementChild; + } else { + // Move later in the list. + previousSibling = newParent.querySelector(`:scope > [data-sortorder="${element.sortorder - 1}"]`); + nextSibling = previousSibling?.nextElementSibling; + } + + // Check if this has actually moved, or if it's just having its sortorder updated due to another element moving. + const moved = (newParent !== this.element.parentElement || nextSibling !== this.element); + + if (moved) { + if (nextSibling) { + // Move to the specified position in the list. + newParent.insertBefore(this.element, nextSibling); + } else { + // Move to the end of the list (may also be the top of the list is empty). + newParent.appendChild(this.element); + } + } + this.element.dataset.sortorder = element.sortorder; } + } \ No newline at end of file diff --git a/question/bank/managecategories/amd/src/categorymanager.js b/question/bank/managecategories/amd/src/categorymanager.js index ea393453ec522..e87f6a23f9e3e 100644 --- a/question/bank/managecategories/amd/src/categorymanager.js +++ b/question/bank/managecategories/amd/src/categorymanager.js @@ -22,33 +22,64 @@ import {Reactive} from 'core/reactive'; import {mutations} from 'qbank_managecategories/mutations'; import {eventTypes, notifyQbankManagecategoriesStateUpdated} from 'qbank_managecategories/events'; +import Ajax from "core/ajax"; +import Notification from "core/notification"; const SELECTORS = { CATEGORY_LIST: '.qbank_managecategories-categorylist', CONTEXT: '.qbank_managecategories-categorylist[data-contextid]', + LIST_ITEM: '.qbank_managecategories-item[data-categoryid]', + MODULE_ROOT: '#categoriesrendered', }; -class CategoryManager extends Reactive { - - static getCategoryState(item) { - const categories = []; - if (item.children) { - item.children.forEach(category => { - let child = { - categoryid: category.dataset.categoryid, - categoryname: category.dataset.categoryname, - categories: null, - firstchild: category === item.children[0], - }; +const loadState = async (reactive) => { + const rootElement = document.querySelector(SELECTORS.MODULE_ROOT); + const stateData = { + page: { + contextid: rootElement.dataset.contextid, + }, + categories: [], + }; + const listItems = document.querySelectorAll(SELECTORS.LIST_ITEM); + listItems.forEach(item => { + const category = { + id: item.dataset.categoryid, + name: item.dataset.categoryname, + parent: item.dataset.parent, + contextid: item.dataset.contextid, + sortorder: item.dataset.sortorder, + }; + stateData.categories.push(category); + }); + reactive.setInitialState(stateData); +}; - const childList = category.querySelector(SELECTORS.CATEGORY_LIST); - if (childList) { - child.categories = this.getCategoryState(childList); - } - categories.push(child); +class CategoryManager extends Reactive { + setCatOrder( + categoryId, + targetParentId, + precedingSiblingId = null, + ) { + const call = { + methodname: 'qbank_managecategories_move_category', + args: { + pagecontextid: this.state.page.contextid, + categoryid: categoryId, + targetparentid: targetParentId, + precedingsiblingid: precedingSiblingId, + } + }; + Ajax.call([call])[0] + .then((stateUpdates) => { + this.stateManager.processUpdates(stateUpdates); + }) + .catch(error => { + Notification.addNotification({ + message: error.message, + type: 'error', + }); + document.getElementsByClassName('alert-danger')[0]?.scrollIntoView(); }); - } - return categories; } } @@ -64,20 +95,5 @@ export const categorymanager = new CategoryManager({ * Load the initial state. */ export const init = () => { - const state = { - contexts: [], - }; - const contexts = document.querySelectorAll(SELECTORS.CONTEXT); - contexts.forEach(context => { - const stateContext = { - id: context.dataset.contextid, - contextname: context.dataset.contextname, - categories: [], - hascategories: false, - }; - stateContext.categories = CategoryManager.getCategoryState(context); - stateContext.hascategories = stateContext.categories.length > 0; - state.contexts.push(stateContext); - }); - categorymanager.setInitialState(state); + loadState(categorymanager); }; diff --git a/question/bank/managecategories/classes/external/move_category.php b/question/bank/managecategories/classes/external/move_category.php new file mode 100644 index 0000000000000..5da3cdaede05d --- /dev/null +++ b/question/bank/managecategories/classes/external/move_category.php @@ -0,0 +1,227 @@ +. + +namespace qbank_managecategories\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use moodle_exception; +use context; + +/** + * External class used for category reordering. + * + * @package qbank_managecategories + * @category external + * @copyright 2024 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class move_category extends external_api { + /** + * Describes the parameters for update_category_order webservice. + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'pagecontextid' => new external_value(PARAM_INT, 'The context of the current page.'), + 'categoryid' => new external_value(PARAM_INT, 'Category being moved'), + 'targetparentid' => new external_value(PARAM_INT, 'The ID of the parent category to move to.'), + 'precedingsiblingid' => new external_value( + PARAM_INT, 'The ID of the preceding category. Null if this is being moved to top of its parent', + VALUE_OPTIONAL, + ), + ]); + } + + /** + * Move category to new location. + * + * @param int $pagecontextid ID of the context of the current page. + * @param int $categoryid ID of the category to move. + * @param int $targetparentid The ID of the parent category to move to. + * @param ?int $precedingsiblingid The ID of the preceding category. Null if this is being moved to top of its parent. + * @return array Reactive state updates representing the changes made to the categories. + */ + public static function execute( + int $pagecontextid, + int $categoryid, + int $targetparentid, + ?int $precedingsiblingid = null + ): array { + // Update category location. + global $DB, $CFG; + + require_once($CFG->libdir . '/questionlib.php'); + + $context = context::instance_by_id($pagecontextid); + self::validate_context($context); + + $origincategory = $DB->get_record('question_categories', ['id' => $categoryid], '*', MUST_EXIST); + $targetparent = $DB->get_record('question_categories', ['id' => $targetparentid], '*', MUST_EXIST); + if ($precedingsiblingid) { + $precedingsibling = $DB->get_record('question_categories', ['id' => $precedingsiblingid], '*', MUST_EXIST); + } + + // Check permission for original and destination contexts. + require_capability('moodle/question:managecategory', context::instance_by_id($origincategory->contextid)); + + if ($origincategory->contextid != $targetparent->contextid) { + require_capability('moodle/question:managecategory', context::instance_by_id($targetparent->contextid)); + } + + $originstateupdate = (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $origincategory->id, + ] + ]; + $stateupdates = []; + + $transaction = $DB->start_delegated_transaction(); + + // Set new parent. + if ($origincategory->parent !== $targetparent->id) { + $DB->set_field( + 'question_categories', + 'parent', + $targetparent->id, + ['id' => $origincategory->id], + ); + $originstateupdate->fields->parent = $targetparent->id; + } + + // Change to the same context. + if ($origincategory->contextid !== $targetparent->contextid) { + // Check for duplicate idnumber. + if (!is_null($origincategory->idnumber)) { + $duplicateidnumber = $DB->record_exists('question_categories', [ + 'idnumber' => $origincategory->idnumber, + 'contextid' => $targetparent->contextid, + ]); + if ($duplicateidnumber) { + throw new moodle_exception('idnumberexists', 'qbank_managecategories'); + } + } + + $DB->set_field( + 'question_categories', + 'contextid', + $targetparent->contextid, + ['id' => $origincategory->id], + ); + // Make change to sub categories. + \question_move_category_to_context( + $origincategory->id, + $origincategory->contextid, + $targetparent->contextid + ); + $originstateupdate->fields->context = $targetparent->contextid; + } + + // Update sort order. + if ($precedingsiblingid) { + $sortorder = $precedingsibling->sortorder + 1; + } else { + $sortorder = 0; + } + $DB->set_field('question_categories', 'sortorder', $sortorder, ['id' => $origincategory->id]); + $originstateupdate->fields->sortorder = $sortorder; + + // Get other categories which are after the new position, and update their sortorder. + $params = [ + 'parent' => $targetparent->id, + 'sortorder' => $sortorder, + 'origincategoryid' => $origincategory->id, + ]; + $select = " + parent = :parent + AND id <> :origincategoryid + AND sortorder >= :sortorder"; + $sort = "sortorder ASC"; + $toupdatesortorder = $DB->get_records_select('question_categories', $select, $params, $sort); + foreach ($toupdatesortorder as $category) { + $DB->set_field('question_categories', 'sortorder', ++$sortorder, ['id' => $category->id]); + $stateupdates[] = (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $category->id, + 'sortorder' => $sortorder, + ] + ]; + } + + if (isset($originstateupdate->fields->parent)) { + // If the category has moved parent, re-order the original siblings to fill the gap. + $originsortorder = $origincategory->sortorder; + $params = [ + 'parent' => $origincategory->parent, + 'sortorder' => $originsortorder, + ]; + $select = "parent = :parent AND sortorder >= :sortorder"; + $sort = "sortorder ASC"; + $originsiblings = $DB->get_records_select('question_categories', $select, $params, $sort); + foreach ($originsiblings as $category) { + $DB->set_field('question_categories', 'sortorder', $originsortorder, ['id' => $category->id]); + $stateupdates[] = (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $category->id, + 'sortorder' => $originsortorder, + ] + ]; + $originsortorder++; + } + } + + $transaction->allow_commit(); + + array_unshift($stateupdates, $originstateupdate); + + return $stateupdates; + } + + /** + * Returns description of method result value. + * + * @return external_multiple_structure + */ + public static function execute_returns(): external_multiple_structure { + return new external_multiple_structure( + new external_single_structure( + [ + 'name' => new external_value(PARAM_ALPHA, 'State object name'), + 'action' => new external_value(PARAM_ALPHA, 'State update type'), + 'fields' => new external_single_structure( + [ + 'id' => new external_value(PARAM_INT, 'The ID of the category that was updated.'), + 'sortorder' => new external_value(PARAM_INT, 'The new sortorder'), + 'parent' => new external_value(PARAM_INT, 'The ID of the new parent category.', VALUE_OPTIONAL), + 'context' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL), + ] + ), + ] + ), + 'Category state updates', + ); + } +} diff --git a/question/bank/managecategories/classes/output/categories.php b/question/bank/managecategories/classes/output/categories.php index 8bdd8f5c11e94..79f9549f9812b 100644 --- a/question/bank/managecategories/classes/output/categories.php +++ b/question/bank/managecategories/classes/output/categories.php @@ -72,6 +72,7 @@ public function export_for_template(renderer_base $output): array { 'contextname' => $contextname, 'heading' => $heading, 'items' => $itemstab['items'], + 'categoryid' => $list->categoryid, ]; } } @@ -243,6 +244,8 @@ public function item_data(stdClass $list, stdClass $category, context $context, 'iconright' => $iconright, 'haschildren' => !empty($children), 'children' => $children, + 'parent' => $category->parent, + 'sortorder' => $category->sortorder, ]; return $itemdata; } diff --git a/question/bank/managecategories/classes/question_category_object.php b/question/bank/managecategories/classes/question_category_object.php index fdebaa0e2e519..9655be68b7971 100644 --- a/question/bank/managecategories/classes/question_category_object.php +++ b/question/bank/managecategories/classes/question_category_object.php @@ -126,13 +126,15 @@ public function initialize( $cmid, $courseid, ): void { - + global $DB; foreach ($contexts as $context) { $items = helper::get_categories_for_contexts($context->id); $items = helper::create_ordered_tree($items); + $categoryid = $DB->get_field('question_categories', 'id', ['contextid' => $context->id, 'parent' => 0]); $this->editlists[$context->id] = (object)[ 'items' => $items, 'context' => $context, + 'categoryid' => $categoryid, ]; } diff --git a/question/bank/managecategories/db/services.php b/question/bank/managecategories/db/services.php index 9781824dfb8cf..0f3d91f413eaa 100644 --- a/question/bank/managecategories/db/services.php +++ b/question/bank/managecategories/db/services.php @@ -42,4 +42,12 @@ 'capabilities' => 'moodle/question:managecategory', 'ajax' => true, ], + + 'qbank_managecategories_move_category' => [ + 'classname' => 'qbank_managecategories\external\move_category', + 'description' => 'Move a question category', + 'type' => 'write', + 'capabilities' => 'moodle/question:managecategory', + 'ajax' => true, + ], ]; diff --git a/question/bank/managecategories/templates/categories.mustache b/question/bank/managecategories/templates/categories.mustache index 0b5f9578018fb..c7742d84d1cbd 100644 --- a/question/bank/managecategories/templates/categories.mustache +++ b/question/bank/managecategories/templates/categories.mustache @@ -52,7 +52,8 @@ {{#categoriesrendered}}

{{heading}}

-
    +
      {{#items}} {{> qbank_managecategories/item }} {{/items}} diff --git a/question/bank/managecategories/templates/item.mustache b/question/bank/managecategories/templates/item.mustache index b853176b6afce..8bd320b34871a 100644 --- a/question/bank/managecategories/templates/item.mustache +++ b/question/bank/managecategories/templates/item.mustache @@ -48,7 +48,7 @@ } }}
    • + data-contextid="{{{contextid}}}" data-categoryname="{{categoryname}}" data-parent="{{parent}}" data-sortorder="{{sortorder}}">
      @@ -90,7 +90,7 @@
      {{#haschildren}}
      -
        +
          {{#children}} {{> qbank_managecategories/item }} {{/children}} diff --git a/question/bank/managecategories/tests/behat/question_categories.feature b/question/bank/managecategories/tests/behat/question_categories.feature index f76e2ed3e4204..f18c796d9e708 100644 --- a/question/bank/managecategories/tests/behat/question_categories.feature +++ b/question/bank/managecategories/tests/behat/question_categories.feature @@ -15,13 +15,13 @@ Feature: A teacher can put questions in categories in the question bank | user | course | role | | teacher1 | C1 | editingteacher | And the following "question categories" exist: - | contextlevel | reference | questioncategory | name | - | Course | C1 | Top | top | - | Course | C1 | top | Default for C1 | - | Course | C1 | Default for C1 | Subcategory | - | Course | C1 | Default for C1 | Another subcat | - | Course | C1 | top | Used category | - | Course | C1 | top | Default & testing | + | contextlevel | reference | questioncategory | name | sortorder | + | Course | C1 | Top | top | 0 | + | Course | C1 | top | Default for C1 | 0 | + | Course | C1 | Default for C1 | Subcategory | 0 | + | Course | C1 | Default for C1 | Another subcat | 1 | + | Course | C1 | top | Used category | 1 | + | Course | C1 | top | Default & testing | 2 | And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Used category | essay | Test question to be moved | Write about whatever you want | diff --git a/question/bank/managecategories/version.php b/question/bank/managecategories/version.php index e4fa826185d7f..ac0e786956da5 100644 --- a/question/bank/managecategories/version.php +++ b/question/bank/managecategories/version.php @@ -26,6 +26,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'qbank_managecategories'; -$plugin->version = 2024060300; +$plugin->version = 2024070200; $plugin->requires = 2024041600; $plugin->maturity = MATURITY_STABLE;