diff --git a/.upgradenotes/MDL-87690-2026012701000221.yml b/.upgradenotes/MDL-87690-2026012701000221.yml
new file mode 100644
index 00000000000..7ef9d34817b
--- /dev/null
+++ b/.upgradenotes/MDL-87690-2026012701000221.yml
@@ -0,0 +1,15 @@
+issueNumber: MDL-87690
+notes:
+ core_customfield:
+ - message: >-
+ A new WebService `core_customfield_convert_category` has been added.
+ It allows the conversion of any entity custom field category to a shared category.
+ type: improved
+ - message: >-
+ The WebService `core_customfield_reload_template` now returns a new
+ parameter "canconvert".
+ type: changed
+ - message: >-
+ The Javascript module `core_customfield/repository/toggle_shared` has been deprecated.
+ Please, use `core_customfield/repository` instead.
+ type: deprecated
diff --git a/public/customfield/amd/build/form.min.js b/public/customfield/amd/build/form.min.js
index bae1ff17a59..6940d3404c3 100644
--- a/public/customfield/amd/build/form.min.js
+++ b/public/customfield/amd/build/form.min.js
@@ -1,10 +1,10 @@
-define("core_customfield/form",["exports","core/inplace_editable","core/ajax","core/str","core_form/modalform","core/notification","core/pending","core/sortable_list","core/templates","jquery"],(function(_exports,_inplace_editable,_ajax,_str,_modalform,_notification,_pending,_sortable_list,_templates,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("core_customfield/form",["exports","core/inplace_editable","core/ajax","core/str","core/toast","core_customfield/repository","core_form/modalform","core/notification","core/pending","core/sortable_list","core/templates","jquery"],(function(_exports,_inplace_editable,_ajax,_str,_toast,Repository,_modalform,_notification,_pending,_sortable_list,_templates,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
/**
* Custom Field interaction management for Moodle.
*
* @module core_customfield/form
* @copyright 2018 Toni Barbera
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates),_jquery=_interopRequireDefault(_jquery);const confirmDelete=(id,type,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:confirmDelete");(0,_str.getStrings)([{key:"confirm"},{key:"confirmdelete"+type,component:"core_customfield"},{key:"yes"},{key:"no"}]).then((strings=>_notification.default.confirm(strings[0],strings[1],strings[2],strings[3],(function(){const pendingDeletePromise=new _pending.default("core_customfield/form:confirmDelete");(0,_ajax.call)([{methodname:"field"===type?"core_customfield_delete_field":"core_customfield_delete_category",args:{id:id}},{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[1].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then(pendingDeletePromise.resolve).catch(_notification.default.exception)})))).then(pendingPromise.resolve).catch(_notification.default.exception)},getCategoryNameFor=nodeElement=>nodeElement.closest("[data-category-id]").attr("data-category-name");_exports.init=()=>{const rootNode=document.querySelector("#customfield_catlist"),component=rootNode.dataset.component,area=rootNode.dataset.area,itemid=rootNode.dataset.itemid;rootNode.addEventListener("click",(e=>{const roleHolder=e.target.closest("[data-role]");if(roleHolder)return"deletefield"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"field",component,area,itemid)):"deletecategory"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"category",component,area,itemid)):"addnewcategory"===roleHolder.dataset.role?(e.preventDefault(),void((component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewCategory");(0,_ajax.call)([{methodname:"core_customfield_create_category",args:{component:component,area:area,itemid:itemid}},{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[1].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})(component,area,itemid)):"addfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewField"),returnFocus=element.closest(".action-menu").querySelector(".dropdown-toggle"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{categoryid:element.getAttribute("data-categoryid"),type:element.getAttribute("data-type")},modalConfig:{title:(0,_str.getString)("addingnewcustomfield","core_customfield",element.getAttribute("data-typename"))},returnFocus:returnFocus});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");(0,_ajax.call)([{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[0].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):"editfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:editField"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{id:element.getAttribute("data-id")},modalConfig:{title:(0,_str.getString)("editingfield","core_customfield",element.getAttribute("data-name"))},returnFocus:element});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");(0,_ajax.call)([{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[0].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):void 0})),(rootNode=>{new _sortable_list.default("#customfield_catlist .categorieslist",{moveHandlerSelector:".movecategory [data-drag-type=move]"}).getElementName=nodeElement=>Promise.resolve(getCategoryNameFor(nodeElement)),(0,_jquery.default)("[data-category-id]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:categoryid:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_category",args:{id:info.element.data("category-id"),beforeid:info.targetNextElement.data("category-id")}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),new _sortable_list.default("#customfield_catlist .fieldslist tbody",{moveHandlerSelector:".movefield [data-drag-type=move]"}).getDestinationName=(parentElement,afterElement)=>afterElement.length?afterElement.attr("data-field-name")?(0,_str.getString)("afterfield","customfield",afterElement.attr("data-field-name")):Promise.resolve(""):(0,_str.getString)("totopofcategory","customfield",getCategoryNameFor(parentElement)),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_field",args:{id:info.element.data("field-id"),beforeid:info.targetNextElement.data("field-id"),categoryid:Number(info.targetList.closest("[data-category-id]").attr("data-category-id"))}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DRAG,(evt=>{var pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drag");evt.stopPropagation(),_templates.default.render("core_customfield/nofields",{}).then((html=>{rootNode.querySelectorAll(".categorieslist > *").forEach((category=>{const fields=category.querySelectorAll(".field:not(.sortable-list-is-dragged)"),noFields=category.querySelector(".nofields");fields.length||noFields?fields.length&&noFields&&noFields.remove():category.querySelector("tbody").innerHTML=html}))})).then(pendingPromise.resolve).catch(_notification.default.exception)})),(0,_jquery.default)("[data-category-id], [data-field-name]").on(_sortable_list.default.EVENTS.DRAGSTART,((evt,info)=>{setTimeout((()=>{(0,_jquery.default)(".sortable-list-is-dragged").width(info.element.width())}),501)}))})(rootNode)}}));
+ */(Repository),_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates),_jquery=_interopRequireDefault(_jquery);const confirmDelete=(id,type,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:confirmDelete");(0,_str.getStrings)([{key:"confirm"},{key:"confirmdelete"+type,component:"core_customfield"},{key:"yes"},{key:"no"}]).then((strings=>_notification.default.confirm(strings[0],strings[1],strings[2],strings[3],(function(){const pendingDeletePromise=new _pending.default("core_customfield/form:confirmDelete");(0,_ajax.call)([{methodname:"field"===type?"core_customfield_delete_field":"core_customfield_delete_category",args:{id:id}}])[0].then((()=>Repository.reloadTemplate(component,area,itemid))).then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then(pendingDeletePromise.resolve).catch(_notification.default.exception)})))).then(pendingPromise.resolve).catch(_notification.default.exception)},handleConversion=(categoryId,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:handleConversion");Repository.convertCategory(categoryId,component,area,itemid).then((()=>Repository.toggleCategory(categoryId,component,area,itemid,!0))).then((()=>Repository.reloadTemplate(component,area,itemid))).then((response=>_templates.default.renderForPromise("core_customfield/list",response))).then((_ref=>{let{html:html,js:js}=_ref;const listPage=document.querySelector('[data-region="list-page"]');return _templates.default.replaceNode(listPage,html,js)})).then((()=>(0,_toast.add)((0,_str.getString)("categoryconverted","core_customfield"),{type:"success"}))).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)},getCategoryNameFor=nodeElement=>nodeElement.closest("[data-category-id]").attr("data-category-name");_exports.init=()=>{const rootNode=document.querySelector("#customfield_catlist"),component=rootNode.dataset.component,area=rootNode.dataset.area,itemid=rootNode.dataset.itemid;rootNode.addEventListener("click",(e=>{const roleHolder=e.target.closest("[data-role]");if(roleHolder)return"deletefield"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"field",component,area,itemid)):"deletecategory"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"category",component,area,itemid)):"convertcategory"===roleHolder.dataset.role?(e.preventDefault(),void((categoryId,component,area,itemid)=>{(0,_str.getStrings)([{key:"convertcategorytitle",component:"core_customfield"},{key:"convertcategoryconfirm",component:"core_customfield"},{key:"proceed"},{key:"cancel"}]).then((strings=>_notification.default.confirm(strings[0],strings[1],strings[2],strings[3],(function(){handleConversion(categoryId,component,area,itemid)})))).catch(_notification.default.exception)})(roleHolder.dataset.id,component,area,itemid)):"hasduplicatecustomfield"===roleHolder.dataset.role?(e.preventDefault(),void(0,_str.getStrings)([{key:"conversionerror",component:"core_customfield"},{key:"sharedcustomfieldalreadyexists",component:"core_customfield"},{key:"closebuttontitle"}]).then((strings=>_notification.default.alert(strings[0],strings[1],strings[2]))).catch(_notification.default.exception)):"addnewcategory"===roleHolder.dataset.role?(e.preventDefault(),void((component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewCategory");(0,_ajax.call)([{methodname:"core_customfield_create_category",args:{component:component,area:area,itemid:itemid}}])[0].then((()=>Repository.reloadTemplate(component,area,itemid))).then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})(component,area,itemid)):"addfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewField"),returnFocus=element.closest(".action-menu").querySelector(".dropdown-toggle"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{categoryid:element.getAttribute("data-categoryid"),type:element.getAttribute("data-type")},modalConfig:{title:(0,_str.getString)("addingnewcustomfield","core_customfield",element.getAttribute("data-typename"))},returnFocus:returnFocus});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");Repository.reloadTemplate(component,area,itemid).then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):"editfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:editField"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{id:element.getAttribute("data-id")},modalConfig:{title:(0,_str.getString)("editingfield","core_customfield",element.getAttribute("data-name"))},returnFocus:element});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");Repository.reloadTemplate(component,area,itemid).then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):void 0})),(rootNode=>{new _sortable_list.default("#customfield_catlist .categorieslist",{moveHandlerSelector:".movecategory [data-drag-type=move]"}).getElementName=nodeElement=>Promise.resolve(getCategoryNameFor(nodeElement)),(0,_jquery.default)("[data-category-id]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:categoryid:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_category",args:{id:info.element.data("category-id"),beforeid:info.targetNextElement.data("category-id")}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),new _sortable_list.default("#customfield_catlist .fieldslist tbody",{moveHandlerSelector:".movefield [data-drag-type=move]"}).getDestinationName=(parentElement,afterElement)=>afterElement.length?afterElement.attr("data-field-name")?(0,_str.getString)("afterfield","customfield",afterElement.attr("data-field-name")):Promise.resolve(""):(0,_str.getString)("totopofcategory","customfield",getCategoryNameFor(parentElement)),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_field",args:{id:info.element.data("field-id"),beforeid:info.targetNextElement.data("field-id"),categoryid:Number(info.targetList.closest("[data-category-id]").attr("data-category-id"))}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DRAG,(evt=>{var pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drag");evt.stopPropagation(),_templates.default.render("core_customfield/nofields",{}).then((html=>{rootNode.querySelectorAll(".categorieslist > *").forEach((category=>{const fields=category.querySelectorAll(".field:not(.sortable-list-is-dragged)"),noFields=category.querySelector(".nofields");fields.length||noFields?fields.length&&noFields&&noFields.remove():category.querySelector("tbody").innerHTML=html}))})).then(pendingPromise.resolve).catch(_notification.default.exception)})),(0,_jquery.default)("[data-category-id], [data-field-name]").on(_sortable_list.default.EVENTS.DRAGSTART,((evt,info)=>{setTimeout((()=>{(0,_jquery.default)(".sortable-list-is-dragged").width(info.element.width())}),501)}))})(rootNode)}}));
//# sourceMappingURL=form.min.js.map
\ No newline at end of file
diff --git a/public/customfield/amd/build/form.min.js.map b/public/customfield/amd/build/form.min.js.map
index 1d5b299f8c5..ba577e658c4 100644
--- a/public/customfield/amd/build/form.min.js.map
+++ b/public/customfield/amd/build/form.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"form.min.js","sources":["../src/form.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 * Custom Field interaction management for Moodle.\n *\n * @module core_customfield/form\n * @copyright 2018 Toni Barbera\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport 'core/inplace_editable';\nimport {call as fetchMany} from 'core/ajax';\nimport {\n getString,\n getStrings,\n} from 'core/str';\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport SortableList from 'core/sortable_list';\nimport Templates from 'core/templates';\nimport jQuery from 'jquery';\n\n/**\n * Display confirmation dialogue\n *\n * @param {Number} id\n * @param {String} type\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst confirmDelete = (id, type, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:confirmDelete');\n\n getStrings([\n {'key': 'confirm'},\n {'key': 'confirmdelete' + type, component: 'core_customfield'},\n {'key': 'yes'},\n {'key': 'no'},\n ])\n .then(strings => {\n return Notification.confirm(strings[0], strings[1], strings[2], strings[3], function() {\n const pendingDeletePromise = new Pending('core_customfield/form:confirmDelete');\n fetchMany([\n {\n methodname: (type === 'field') ? 'core_customfield_delete_field' : 'core_customfield_delete_category',\n args: {id},\n },\n {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}\n ])[1]\n .then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(pendingDeletePromise.resolve)\n .catch(Notification.exception);\n });\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n};\n\n\n/**\n * Creates a new custom fields category with default name and updates the list\n *\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewCategory = (component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewCategory');\n const promises = fetchMany([\n {methodname: 'core_customfield_create_category', args: {component, area, itemid}},\n {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}\n ]);\n\n promises[1].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n};\n\n/**\n * Create new custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewField');\n\n const returnFocus = element.closest(\".action-menu\").querySelector(\".dropdown-toggle\");\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n categoryid: element.getAttribute('data-categoryid'),\n type: element.getAttribute('data-type'),\n },\n modalConfig: {\n title: getString('addingnewcustomfield', 'core_customfield', element.getAttribute('data-typename')),\n },\n returnFocus,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n const promises = fetchMany([\n {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}\n ]);\n\n promises[0].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Edit custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst editField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:editField');\n\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n id: element.getAttribute('data-id'),\n },\n modalConfig: {\n title: getString('editingfield', 'core_customfield', element.getAttribute('data-name')),\n },\n returnFocus: element,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n const promises = fetchMany([\n {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}\n ]);\n\n promises[0].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Fetch the category name from an inplace editable, given a child node of that field.\n *\n * @param {NodeElement} nodeElement\n * @returns {String}\n */\nconst getCategoryNameFor = nodeElement => nodeElement\n .closest('[data-category-id]')\n .attr('data-category-name');\n\nconst setupSortableLists = rootNode => {\n // Sort category.\n const sortCat = new SortableList(\n '#customfield_catlist .categorieslist',\n {\n moveHandlerSelector: '.movecategory [data-drag-type=move]',\n }\n );\n sortCat.getElementName = nodeElement => Promise.resolve(getCategoryNameFor(nodeElement));\n\n // Note: The sortable list currently uses jQuery events.\n jQuery('[data-category-id]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:categoryid:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_category',\n args: {\n id: info.element.data('category-id'),\n beforeid: info.targetNextElement.data('category-id')\n }\n\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n // Sort fields.\n var sort = new SortableList(\n '#customfield_catlist .fieldslist tbody',\n {\n moveHandlerSelector: '.movefield [data-drag-type=move]',\n }\n );\n\n sort.getDestinationName = (parentElement, afterElement) => {\n if (!afterElement.length) {\n return getString('totopofcategory', 'customfield', getCategoryNameFor(parentElement));\n } else if (afterElement.attr('data-field-name')) {\n return getString('afterfield', 'customfield', afterElement.attr('data-field-name'));\n } else {\n return Promise.resolve('');\n }\n };\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_field',\n args: {\n id: info.element.data('field-id'),\n beforeid: info.targetNextElement.data('field-id'),\n categoryid: Number(info.targetList.closest('[data-category-id]').attr('data-category-id'))\n },\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DRAG, evt => {\n var pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drag');\n\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n\n // Refreshing fields tables.\n Templates.render('core_customfield/nofields', {})\n .then(html => {\n rootNode.querySelectorAll('.categorieslist > *')\n .forEach(category => {\n const fields = category.querySelectorAll('.field:not(.sortable-list-is-dragged)');\n const noFields = category.querySelector('.nofields');\n\n if (!fields.length && !noFields) {\n category.querySelector('tbody').innerHTML = html;\n } else if (fields.length && noFields) {\n noFields.remove();\n }\n });\n return;\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n });\n\n jQuery('[data-category-id], [data-field-name]').on(SortableList.EVENTS.DRAGSTART, (evt, info) => {\n setTimeout(() => {\n jQuery('.sortable-list-is-dragged').width(info.element.width());\n }, 501);\n });\n};\n\n/**\n * Initialise the custom fields manager.\n */\nexport const init = () => {\n const rootNode = document.querySelector('#customfield_catlist');\n\n const component = rootNode.dataset.component;\n const area = rootNode.dataset.area;\n const itemid = rootNode.dataset.itemid;\n\n rootNode.addEventListener('click', e => {\n const roleHolder = e.target.closest('[data-role]');\n if (!roleHolder) {\n return;\n }\n\n if (roleHolder.dataset.role === 'deletefield') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'field', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'deletecategory') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'category', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'addnewcategory') {\n e.preventDefault();\n createNewCategory(component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'addfield') {\n e.preventDefault();\n createNewField(roleHolder, component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'editfield') {\n e.preventDefault();\n editField(roleHolder, component, area, itemid);\n\n return;\n }\n });\n\n setupSortableLists(rootNode, component, area, itemid);\n};\n"],"names":["confirmDelete","id","type","component","area","itemid","pendingPromise","Pending","then","strings","Notification","confirm","pendingDeletePromise","methodname","args","response","Templates","render","html","js","replaceNode","resolve","catch","exception","getCategoryNameFor","nodeElement","closest","attr","rootNode","document","querySelector","dataset","addEventListener","e","roleHolder","target","role","preventDefault","createNewCategory","element","returnFocus","form","ModalForm","formClass","categoryid","getAttribute","modalConfig","title","events","FORM_SUBMITTED","pendingCreatedPromise","window","location","reload","show","createNewField","editField","SortableList","moveHandlerSelector","getElementName","Promise","on","EVENTS","DROP","evt","info","positionChanged","data","beforeid","targetNextElement","stopPropagation","getDestinationName","parentElement","afterElement","length","Number","targetList","DRAG","querySelectorAll","forEach","category","fields","noFields","remove","innerHTML","DRAGSTART","setTimeout","width","setupSortableLists"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"form.min.js","sources":["../src/form.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 * Custom Field interaction management for Moodle.\n *\n * @module core_customfield/form\n * @copyright 2018 Toni Barbera\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport 'core/inplace_editable';\nimport {call as fetchMany} from 'core/ajax';\nimport {\n getString,\n getStrings,\n} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport * as Repository from 'core_customfield/repository';\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport SortableList from 'core/sortable_list';\nimport Templates from 'core/templates';\nimport jQuery from 'jquery';\n\n/**\n * Display confirmation dialogue\n *\n * @param {Number} id\n * @param {String} type\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst confirmDelete = (id, type, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:confirmDelete');\n\n getStrings([\n {'key': 'confirm'},\n {'key': 'confirmdelete' + type, component: 'core_customfield'},\n {'key': 'yes'},\n {'key': 'no'},\n ])\n .then(strings => {\n return Notification.confirm(strings[0], strings[1], strings[2], strings[3], function() {\n const pendingDeletePromise = new Pending('core_customfield/form:confirmDelete');\n fetchMany([\n {\n methodname: (type === 'field') ? 'core_customfield_delete_field' : 'core_customfield_delete_category',\n args: {id},\n },\n ])[0]\n .then(() => Repository.reloadTemplate(component, area, itemid))\n .then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(pendingDeletePromise.resolve)\n .catch(Notification.exception);\n });\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n};\n\n/**\n * Display a confirmation modal to convert an entity custom field category to a shared category.\n *\n * @param {Number} categoryId\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst confirmConvert = (categoryId, component, area, itemid) => {\n getStrings([\n {'key': 'convertcategorytitle', component: 'core_customfield'},\n {'key': 'convertcategoryconfirm', component: 'core_customfield'},\n {'key': 'proceed'},\n {'key': 'cancel'},\n ])\n .then(strings => {\n return Notification.confirm(strings[0], strings[1], strings[2], strings[3], function() {\n handleConversion(categoryId, component, area, itemid);\n });\n })\n .catch(Notification.exception);\n};\n\n/**\n * Handle the conversion of an entity custom field category to a shared category.\n *\n * @param {Number} categoryId\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst handleConversion = (categoryId, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:handleConversion');\n\n Repository.convertCategory(categoryId, component, area, itemid)\n .then(() => Repository.toggleCategory(categoryId, component, area, itemid, true))\n .then(() => Repository.reloadTemplate(component, area, itemid))\n .then((response) => Templates.renderForPromise('core_customfield/list', response))\n .then(({html, js}) => {\n const listPage = document.querySelector('[data-region=\"list-page\"]');\n return Templates.replaceNode(listPage, html, js);\n })\n .then(() => addToast(getString('categoryconverted', 'core_customfield'), {type: 'success'}))\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n};\n\n/**\n * Display an alert when conversion of a category to a shared category is blocked due to duplicate names.\n */\nconst alertDuplicateField = () => {\n getStrings([\n {'key': 'conversionerror', component: 'core_customfield'},\n {'key': 'sharedcustomfieldalreadyexists', component: 'core_customfield'},\n {'key': 'closebuttontitle'},\n ])\n .then(strings => {\n return Notification.alert(strings[0], strings[1], strings[2]);\n })\n .catch(Notification.exception);\n};\n\n/**\n * Creates a new custom fields category with default name and updates the list\n *\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewCategory = (component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewCategory');\n const promises = fetchMany([\n {methodname: 'core_customfield_create_category', args: {component, area, itemid}},\n ]);\n\n promises[0].then(() => Repository.reloadTemplate(component, area, itemid))\n .then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n};\n\n/**\n * Create new custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewField');\n\n const returnFocus = element.closest(\".action-menu\").querySelector(\".dropdown-toggle\");\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n categoryid: element.getAttribute('data-categoryid'),\n type: element.getAttribute('data-type'),\n },\n modalConfig: {\n title: getString('addingnewcustomfield', 'core_customfield', element.getAttribute('data-typename')),\n },\n returnFocus,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n\n Repository.reloadTemplate(component, area, itemid)\n .then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Edit custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst editField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:editField');\n\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n id: element.getAttribute('data-id'),\n },\n modalConfig: {\n title: getString('editingfield', 'core_customfield', element.getAttribute('data-name')),\n },\n returnFocus: element,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n\n Repository.reloadTemplate(component, area, itemid)\n .then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Fetch the category name from an inplace editable, given a child node of that field.\n *\n * @param {NodeElement} nodeElement\n * @returns {String}\n */\nconst getCategoryNameFor = nodeElement => nodeElement\n .closest('[data-category-id]')\n .attr('data-category-name');\n\nconst setupSortableLists = rootNode => {\n // Sort category.\n const sortCat = new SortableList(\n '#customfield_catlist .categorieslist',\n {\n moveHandlerSelector: '.movecategory [data-drag-type=move]',\n }\n );\n sortCat.getElementName = nodeElement => Promise.resolve(getCategoryNameFor(nodeElement));\n\n // Note: The sortable list currently uses jQuery events.\n jQuery('[data-category-id]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:categoryid:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_category',\n args: {\n id: info.element.data('category-id'),\n beforeid: info.targetNextElement.data('category-id')\n }\n\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n // Sort fields.\n var sort = new SortableList(\n '#customfield_catlist .fieldslist tbody',\n {\n moveHandlerSelector: '.movefield [data-drag-type=move]',\n }\n );\n\n sort.getDestinationName = (parentElement, afterElement) => {\n if (!afterElement.length) {\n return getString('totopofcategory', 'customfield', getCategoryNameFor(parentElement));\n } else if (afterElement.attr('data-field-name')) {\n return getString('afterfield', 'customfield', afterElement.attr('data-field-name'));\n } else {\n return Promise.resolve('');\n }\n };\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_field',\n args: {\n id: info.element.data('field-id'),\n beforeid: info.targetNextElement.data('field-id'),\n categoryid: Number(info.targetList.closest('[data-category-id]').attr('data-category-id'))\n },\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DRAG, evt => {\n var pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drag');\n\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n\n // Refreshing fields tables.\n Templates.render('core_customfield/nofields', {})\n .then(html => {\n rootNode.querySelectorAll('.categorieslist > *')\n .forEach(category => {\n const fields = category.querySelectorAll('.field:not(.sortable-list-is-dragged)');\n const noFields = category.querySelector('.nofields');\n\n if (!fields.length && !noFields) {\n category.querySelector('tbody').innerHTML = html;\n } else if (fields.length && noFields) {\n noFields.remove();\n }\n });\n return;\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n });\n\n jQuery('[data-category-id], [data-field-name]').on(SortableList.EVENTS.DRAGSTART, (evt, info) => {\n setTimeout(() => {\n jQuery('.sortable-list-is-dragged').width(info.element.width());\n }, 501);\n });\n};\n\n/**\n * Initialise the custom fields manager.\n */\nexport const init = () => {\n const rootNode = document.querySelector('#customfield_catlist');\n\n const component = rootNode.dataset.component;\n const area = rootNode.dataset.area;\n const itemid = rootNode.dataset.itemid;\n\n rootNode.addEventListener('click', e => {\n const roleHolder = e.target.closest('[data-role]');\n if (!roleHolder) {\n return;\n }\n\n if (roleHolder.dataset.role === 'deletefield') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'field', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'deletecategory') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'category', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'convertcategory') {\n e.preventDefault();\n\n confirmConvert(roleHolder.dataset.id, component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'hasduplicatecustomfield') {\n e.preventDefault();\n\n alertDuplicateField();\n return;\n }\n\n if (roleHolder.dataset.role === 'addnewcategory') {\n e.preventDefault();\n createNewCategory(component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'addfield') {\n e.preventDefault();\n createNewField(roleHolder, component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'editfield') {\n e.preventDefault();\n editField(roleHolder, component, area, itemid);\n\n return;\n }\n });\n\n setupSortableLists(rootNode, component, area, itemid);\n};\n"],"names":["confirmDelete","id","type","component","area","itemid","pendingPromise","Pending","then","strings","Notification","confirm","pendingDeletePromise","methodname","args","Repository","reloadTemplate","response","Templates","render","html","js","replaceNode","resolve","catch","exception","handleConversion","categoryId","convertCategory","toggleCategory","renderForPromise","_ref","listPage","document","querySelector","getCategoryNameFor","nodeElement","closest","attr","rootNode","dataset","addEventListener","e","roleHolder","target","role","preventDefault","confirmConvert","alert","createNewCategory","element","returnFocus","form","ModalForm","formClass","categoryid","getAttribute","modalConfig","title","events","FORM_SUBMITTED","pendingCreatedPromise","window","location","reload","show","createNewField","editField","SortableList","moveHandlerSelector","getElementName","Promise","on","EVENTS","DROP","evt","info","positionChanged","data","beforeid","targetNextElement","stopPropagation","getDestinationName","parentElement","afterElement","length","Number","targetList","DRAG","querySelectorAll","forEach","category","fields","noFields","remove","innerHTML","DRAGSTART","setTimeout","width","setupSortableLists"],"mappings":"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"}
\ No newline at end of file
diff --git a/public/customfield/amd/build/repository.min.js b/public/customfield/amd/build/repository.min.js
new file mode 100644
index 00000000000..4bf905a8780
--- /dev/null
+++ b/public/customfield/amd/build/repository.min.js
@@ -0,0 +1,3 @@
+define("core_customfield/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.toggleCategory=_exports.reloadTemplate=_exports.convertCategory=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.convertCategory=(categoryId,component,area,itemid)=>{const request={methodname:"core_customfield_convert_category",args:{categoryid:categoryId,component:component,area:area,itemid:itemid}};return _ajax.default.call([request])[0]};_exports.reloadTemplate=(component,area,itemid)=>{const request={methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}};return _ajax.default.call([request])[0]};_exports.toggleCategory=(categoryId,component,area,itemid,state)=>{const request={methodname:"core_customfield_toggle_shared",args:{categoryid:categoryId,component:component,area:area,itemid:itemid,state:state}};return _ajax.default.call([request])[0]}}));
+
+//# sourceMappingURL=repository.min.js.map
\ No newline at end of file
diff --git a/public/customfield/amd/build/repository.min.js.map b/public/customfield/amd/build/repository.min.js.map
new file mode 100644
index 00000000000..8d2bf7e8589
--- /dev/null
+++ b/public/customfield/amd/build/repository.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"repository.min.js","sources":["../src/repository.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 * Module to handle custom fields AJAX requests.\n *\n * @module core_customfield/repository\n * @copyright 2026 Yerai Rodríguez \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport Ajax from 'core/ajax';\n\n/**\n * Convert an entity custom field category to a shared category.\n *\n * @param {Number} categoryId\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n * @return {Promise}\n */\nexport const convertCategory = (categoryId, component, area, itemid) => {\n const request = {\n methodname: 'core_customfield_convert_category',\n args: {categoryid: categoryId, component: component, area: area, itemid: itemid}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Reload custom fields list.\n *\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n * @return {Promise}\n */\nexport const reloadTemplate = (component, area, itemid) => {\n const request = {\n methodname: 'core_customfield_reload_template',\n args: {component: component, area: area, itemid: itemid}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Toggle shared category enabled.\n *\n * @param {Number} categoryId\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n * @param {Boolean} state\n * @return {Promise}\n */\nexport const toggleCategory = (categoryId, component, area, itemid, state) => {\n const request = {\n methodname: 'core_customfield_toggle_shared',\n args: {categoryid: categoryId, component: component, area: area, itemid: itemid, state: state}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["categoryId","component","area","itemid","request","methodname","args","categoryid","Ajax","call","state"],"mappings":"oTAoC+B,CAACA,WAAYC,UAAWC,KAAMC,gBACnDC,QAAU,CACZC,WAAY,oCACZC,KAAM,CAACC,WAAYP,WAAYC,UAAWA,UAAWC,KAAMA,KAAMC,OAAQA,gBAGtEK,cAAKC,KAAK,CAACL,UAAU,4BAWF,CAACH,UAAWC,KAAMC,gBACtCC,QAAU,CACZC,WAAY,mCACZC,KAAM,CAACL,UAAWA,UAAWC,KAAMA,KAAMC,OAAQA,gBAG9CK,cAAKC,KAAK,CAACL,UAAU,4BAaF,CAACJ,WAAYC,UAAWC,KAAMC,OAAQO,eAC1DN,QAAU,CACZC,WAAY,iCACZC,KAAM,CAACC,WAAYP,WAAYC,UAAWA,UAAWC,KAAMA,KAAMC,OAAQA,OAAQO,MAAOA,eAGrFF,cAAKC,KAAK,CAACL,UAAU"}
\ No newline at end of file
diff --git a/public/customfield/amd/build/repository/toggle_shared.min.js b/public/customfield/amd/build/repository/toggle_shared.min.js
index 331b1b46a5e..10d41affe87 100644
--- a/public/customfield/amd/build/repository/toggle_shared.min.js
+++ b/public/customfield/amd/build/repository/toggle_shared.min.js
@@ -5,6 +5,7 @@ define("core_customfield/repository/toggle_shared",["exports","core/ajax"],(func
* @module core_customfield/repository/toggle_shared
* @copyright 2025 David Carrillo
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 5.1 - please use core_customfield/repository instead.
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.toggleCategory=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.toggleCategory=(categoryId,component,area,itemid,state)=>{const request={methodname:"core_customfield_toggle_shared",args:{categoryid:categoryId,component:component,area:area,itemid:itemid,state:state}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=toggle_shared.min.js.map
\ No newline at end of file
diff --git a/public/customfield/amd/build/repository/toggle_shared.min.js.map b/public/customfield/amd/build/repository/toggle_shared.min.js.map
index 9a4559b7c0c..5a4738fe88e 100644
--- a/public/customfield/amd/build/repository/toggle_shared.min.js.map
+++ b/public/customfield/amd/build/repository/toggle_shared.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"toggle_shared.min.js","sources":["../../src/repository/toggle_shared.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 * Module to handle toggle shared category AJAX requests\n *\n * @module core_customfield/repository/toggle_shared\n * @copyright 2025 David Carrillo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Toggle shared category enabled\n *\n * @method\n * @param {Number} categoryId\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n * @param {Boolean} state\n * @return {Promise}\n */\nexport const toggleCategory = (categoryId, component, area, itemid, state) => {\n const request = {\n methodname: 'core_customfield_toggle_shared',\n args: {categoryid: categoryId, component: component, area: area, itemid: itemid, state: state}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["categoryId","component","area","itemid","state","request","methodname","args","categoryid","Ajax","call"],"mappings":";;;;;;;wKAoC8B,CAACA,WAAYC,UAAWC,KAAMC,OAAQC,eAC1DC,QAAU,CACZC,WAAY,iCACZC,KAAM,CAACC,WAAYR,WAAYC,UAAWA,UAAWC,KAAMA,KAAMC,OAAQA,OAAQC,MAAOA,eAGrFK,cAAKC,KAAK,CAACL,UAAU"}
\ No newline at end of file
+{"version":3,"file":"toggle_shared.min.js","sources":["../../src/repository/toggle_shared.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 * Module to handle toggle shared category AJAX requests\n *\n * @module core_customfield/repository/toggle_shared\n * @copyright 2025 David Carrillo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @deprecated since Moodle 5.1 - please use core_customfield/repository instead.\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Toggle shared category enabled\n *\n * @method\n * @param {Number} categoryId\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n * @param {Boolean} state\n * @return {Promise}\n */\nexport const toggleCategory = (categoryId, component, area, itemid, state) => {\n const request = {\n methodname: 'core_customfield_toggle_shared',\n args: {categoryid: categoryId, component: component, area: area, itemid: itemid, state: state}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["categoryId","component","area","itemid","state","request","methodname","args","categoryid","Ajax","call"],"mappings":";;;;;;;;wKAqC8B,CAACA,WAAYC,UAAWC,KAAMC,OAAQC,eAC1DC,QAAU,CACZC,WAAY,iCACZC,KAAM,CAACC,WAAYR,WAAYC,UAAWA,UAAWC,KAAMA,KAAMC,OAAQA,OAAQC,MAAOA,eAGrFK,cAAKC,KAAK,CAACL,UAAU"}
\ No newline at end of file
diff --git a/public/customfield/amd/build/toggle_shared.min.js b/public/customfield/amd/build/toggle_shared.min.js
index 0bfa577552d..b160c28b7e7 100644
--- a/public/customfield/amd/build/toggle_shared.min.js
+++ b/public/customfield/amd/build/toggle_shared.min.js
@@ -1,3 +1,3 @@
-define("core_customfield/toggle_shared",["exports","core/inplace_editable","core/notification","core/pending","core_customfield/repository/toggle_shared"],(function(_exports,_inplace_editable,_notification,_pending,_toggle_shared){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);let initialized=!1;_exports.init=()=>{initialized||(document.addEventListener("click",(event=>{const sharedToggle=event.target.closest('[data-action="shared-toggle"]');if(sharedToggle){const pendingPromise=new _pending.default("core_customfield/shared:toggle"),categoryId=sharedToggle.dataset.id,component=sharedToggle.dataset.component,area=sharedToggle.dataset.area,itemid=sharedToggle.dataset.itemid,sharedStateToggle=+!Number(sharedToggle.dataset.state);(0,_toggle_shared.toggleCategory)(categoryId,component,area,itemid,sharedStateToggle).then((()=>(sharedToggle.dataset.state=sharedStateToggle,pendingPromise.resolve()))).catch(_notification.default.exception)}})),initialized=!0)}}));
+define("core_customfield/toggle_shared",["exports","core/inplace_editable","core/notification","core/pending","core_customfield/repository"],(function(_exports,_inplace_editable,_notification,_pending,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);let initialized=!1;_exports.init=()=>{initialized||(document.addEventListener("click",(event=>{const sharedToggle=event.target.closest('[data-action="shared-toggle"]');if(sharedToggle){const pendingPromise=new _pending.default("core_customfield/shared:toggle"),categoryId=sharedToggle.dataset.id,component=sharedToggle.dataset.component,area=sharedToggle.dataset.area,itemid=sharedToggle.dataset.itemid,sharedStateToggle=+!Number(sharedToggle.dataset.state);(0,_repository.toggleCategory)(categoryId,component,area,itemid,sharedStateToggle).then((()=>(sharedToggle.dataset.state=sharedStateToggle,pendingPromise.resolve()))).catch(_notification.default.exception)}})),initialized=!0)}}));
//# sourceMappingURL=toggle_shared.min.js.map
\ No newline at end of file
diff --git a/public/customfield/amd/build/toggle_shared.min.js.map b/public/customfield/amd/build/toggle_shared.min.js.map
index 829a187418b..e8bec2bef51 100644
--- a/public/customfield/amd/build/toggle_shared.min.js.map
+++ b/public/customfield/amd/build/toggle_shared.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"toggle_shared.min.js","sources":["../src/toggle_shared.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 * Custom fields shared category toggle\n *\n * @module core_customfield/toggle_shared\n * @copyright 2025 David Carrillo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {toggleCategory} from 'core_customfield/repository/toggle_shared';\n\nlet initialized = false;\n\n/**\n * Initialise toggle\n */\nexport const init = () => {\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n document.addEventListener('click', event => {\n // Toggle shared category.\n const sharedToggle = event.target.closest('[data-action=\"shared-toggle\"]');\n if (sharedToggle) {\n const pendingPromise = new Pending('core_customfield/shared:toggle');\n const categoryId = sharedToggle.dataset.id;\n const component = sharedToggle.dataset.component;\n const area = sharedToggle.dataset.area;\n const itemid = sharedToggle.dataset.itemid;\n const sharedStateToggle = +!Number(sharedToggle.dataset.state);\n\n toggleCategory(categoryId, component, area, itemid, sharedStateToggle)\n .then(() => {\n sharedToggle.dataset.state = sharedStateToggle;\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }\n });\n\n initialized = true;\n};\n"],"names":["initialized","document","addEventListener","event","sharedToggle","target","closest","pendingPromise","Pending","categoryId","dataset","id","component","area","itemid","sharedStateToggle","Number","state","then","resolve","catch","Notification","exception"],"mappings":"weA8BIA,aAAc,gBAKE,KACZA,cAKJC,SAASC,iBAAiB,SAASC,cAEzBC,aAAeD,MAAME,OAAOC,QAAQ,oCACtCF,aAAc,OACRG,eAAiB,IAAIC,iBAAQ,kCAC7BC,WAAaL,aAAaM,QAAQC,GAClCC,UAAYR,aAAaM,QAAQE,UACjCC,KAAOT,aAAaM,QAAQG,KAC5BC,OAASV,aAAaM,QAAQI,OAC9BC,oBAAsBC,OAAOZ,aAAaM,QAAQO,yCAEzCR,WAAYG,UAAWC,KAAMC,OAAQC,mBAC/CG,MAAK,KACFd,aAAaM,QAAQO,MAAQF,kBACtBR,eAAeY,aAEzBC,MAAMC,sBAAaC,eAIhCtB,aAAc"}
\ No newline at end of file
+{"version":3,"file":"toggle_shared.min.js","sources":["../src/toggle_shared.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 * Custom fields shared category toggle\n *\n * @module core_customfield/toggle_shared\n * @copyright 2025 David Carrillo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {toggleCategory} from 'core_customfield/repository';\n\nlet initialized = false;\n\n/**\n * Initialise toggle\n */\nexport const init = () => {\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n document.addEventListener('click', event => {\n // Toggle shared category.\n const sharedToggle = event.target.closest('[data-action=\"shared-toggle\"]');\n if (sharedToggle) {\n const pendingPromise = new Pending('core_customfield/shared:toggle');\n const categoryId = sharedToggle.dataset.id;\n const component = sharedToggle.dataset.component;\n const area = sharedToggle.dataset.area;\n const itemid = sharedToggle.dataset.itemid;\n const sharedStateToggle = +!Number(sharedToggle.dataset.state);\n\n toggleCategory(categoryId, component, area, itemid, sharedStateToggle)\n .then(() => {\n sharedToggle.dataset.state = sharedStateToggle;\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }\n });\n\n initialized = true;\n};\n"],"names":["initialized","document","addEventListener","event","sharedToggle","target","closest","pendingPromise","Pending","categoryId","dataset","id","component","area","itemid","sharedStateToggle","Number","state","then","resolve","catch","Notification","exception"],"mappings":"udA8BIA,aAAc,gBAKE,KACZA,cAKJC,SAASC,iBAAiB,SAASC,cAEzBC,aAAeD,MAAME,OAAOC,QAAQ,oCACtCF,aAAc,OACRG,eAAiB,IAAIC,iBAAQ,kCAC7BC,WAAaL,aAAaM,QAAQC,GAClCC,UAAYR,aAAaM,QAAQE,UACjCC,KAAOT,aAAaM,QAAQG,KAC5BC,OAASV,aAAaM,QAAQI,OAC9BC,oBAAsBC,OAAOZ,aAAaM,QAAQO,sCAEzCR,WAAYG,UAAWC,KAAMC,OAAQC,mBAC/CG,MAAK,KACFd,aAAaM,QAAQO,MAAQF,kBACtBR,eAAeY,aAEzBC,MAAMC,sBAAaC,eAIhCtB,aAAc"}
\ No newline at end of file
diff --git a/public/customfield/amd/src/form.js b/public/customfield/amd/src/form.js
index dbd461a4ca6..76c80d86d6a 100644
--- a/public/customfield/amd/src/form.js
+++ b/public/customfield/amd/src/form.js
@@ -27,6 +27,8 @@ import {
getString,
getStrings,
} from 'core/str';
+import {add as addToast} from 'core/toast';
+import * as Repository from 'core_customfield/repository';
import ModalForm from 'core_form/modalform';
import Notification from 'core/notification';
import Pending from 'core/pending';
@@ -60,8 +62,8 @@ const confirmDelete = (id, type, component, area, itemid) => {
methodname: (type === 'field') ? 'core_customfield_delete_field' : 'core_customfield_delete_category',
args: {id},
},
- {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}
- ])[1]
+ ])[0]
+ .then(() => Repository.reloadTemplate(component, area, itemid))
.then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(pendingDeletePromise.resolve)
@@ -72,6 +74,67 @@ const confirmDelete = (id, type, component, area, itemid) => {
.catch(Notification.exception);
};
+/**
+ * Display a confirmation modal to convert an entity custom field category to a shared category.
+ *
+ * @param {Number} categoryId
+ * @param {String} component
+ * @param {String} area
+ * @param {Number} itemid
+ */
+const confirmConvert = (categoryId, component, area, itemid) => {
+ getStrings([
+ {'key': 'convertcategorytitle', component: 'core_customfield'},
+ {'key': 'convertcategoryconfirm', component: 'core_customfield'},
+ {'key': 'proceed'},
+ {'key': 'cancel'},
+ ])
+ .then(strings => {
+ return Notification.confirm(strings[0], strings[1], strings[2], strings[3], function() {
+ handleConversion(categoryId, component, area, itemid);
+ });
+ })
+ .catch(Notification.exception);
+};
+
+/**
+ * Handle the conversion of an entity custom field category to a shared category.
+ *
+ * @param {Number} categoryId
+ * @param {String} component
+ * @param {String} area
+ * @param {Number} itemid
+ */
+const handleConversion = (categoryId, component, area, itemid) => {
+ const pendingPromise = new Pending('core_customfield/form:handleConversion');
+
+ Repository.convertCategory(categoryId, component, area, itemid)
+ .then(() => Repository.toggleCategory(categoryId, component, area, itemid, true))
+ .then(() => Repository.reloadTemplate(component, area, itemid))
+ .then((response) => Templates.renderForPromise('core_customfield/list', response))
+ .then(({html, js}) => {
+ const listPage = document.querySelector('[data-region="list-page"]');
+ return Templates.replaceNode(listPage, html, js);
+ })
+ .then(() => addToast(getString('categoryconverted', 'core_customfield'), {type: 'success'}))
+ .then(() => pendingPromise.resolve())
+ .catch(Notification.exception);
+};
+
+/**
+ * Display an alert when conversion of a category to a shared category is blocked due to duplicate names.
+ */
+const alertDuplicateField = () => {
+ getStrings([
+ {'key': 'conversionerror', component: 'core_customfield'},
+ {'key': 'sharedcustomfieldalreadyexists', component: 'core_customfield'},
+ {'key': 'closebuttontitle'},
+ ])
+ .then(strings => {
+ return Notification.alert(strings[0], strings[1], strings[2]);
+ })
+ .catch(Notification.exception);
+};
/**
* Creates a new custom fields category with default name and updates the list
@@ -84,10 +147,10 @@ const createNewCategory = (component, area, itemid) => {
const pendingPromise = new Pending('core_customfield/form:createNewCategory');
const promises = fetchMany([
{methodname: 'core_customfield_create_category', args: {component, area, itemid}},
- {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}
]);
- promises[1].then(response => Templates.render('core_customfield/list', response))
+ promises[0].then(() => Repository.reloadTemplate(component, area, itemid))
+ .then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(() => pendingPromise.resolve())
.catch(Notification.exception);
@@ -119,11 +182,9 @@ const createNewField = (element, component, area, itemid) => {
form.addEventListener(form.events.FORM_SUBMITTED, () => {
const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');
- const promises = fetchMany([
- {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}
- ]);
- promises[0].then(response => Templates.render('core_customfield/list', response))
+ Repository.reloadTemplate(component, area, itemid)
+ .then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(() => pendingCreatedPromise.resolve())
.catch(() => window.location.reload());
@@ -158,11 +219,9 @@ const editField = (element, component, area, itemid) => {
form.addEventListener(form.events.FORM_SUBMITTED, () => {
const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');
- const promises = fetchMany([
- {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}
- ]);
- promises[0].then(response => Templates.render('core_customfield/list', response))
+ Repository.reloadTemplate(component, area, itemid)
+ .then(response => Templates.render('core_customfield/list', response))
.then((html, js) => Templates.replaceNode(jQuery('[data-region="list-page"]'), html, js))
.then(() => pendingCreatedPromise.resolve())
.catch(() => window.location.reload());
@@ -308,6 +367,20 @@ export const init = () => {
return;
}
+ if (roleHolder.dataset.role === 'convertcategory') {
+ e.preventDefault();
+
+ confirmConvert(roleHolder.dataset.id, component, area, itemid);
+ return;
+ }
+
+ if (roleHolder.dataset.role === 'hasduplicatecustomfield') {
+ e.preventDefault();
+
+ alertDuplicateField();
+ return;
+ }
+
if (roleHolder.dataset.role === 'addnewcategory') {
e.preventDefault();
createNewCategory(component, area, itemid);
diff --git a/public/customfield/amd/src/repository.js b/public/customfield/amd/src/repository.js
new file mode 100644
index 00000000000..03573f5e288
--- /dev/null
+++ b/public/customfield/amd/src/repository.js
@@ -0,0 +1,80 @@
+// 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 .
+
+/**
+ * Module to handle custom fields AJAX requests.
+ *
+ * @module core_customfield/repository
+ * @copyright 2026 Yerai Rodríguez
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+"use strict";
+
+import Ajax from 'core/ajax';
+
+/**
+ * Convert an entity custom field category to a shared category.
+ *
+ * @param {Number} categoryId
+ * @param {String} component
+ * @param {String} area
+ * @param {Number} itemid
+ * @return {Promise}
+ */
+export const convertCategory = (categoryId, component, area, itemid) => {
+ const request = {
+ methodname: 'core_customfield_convert_category',
+ args: {categoryid: categoryId, component: component, area: area, itemid: itemid}
+ };
+
+ return Ajax.call([request])[0];
+};
+
+/**
+ * Reload custom fields list.
+ *
+ * @param {String} component
+ * @param {String} area
+ * @param {Number} itemid
+ * @return {Promise}
+ */
+export const reloadTemplate = (component, area, itemid) => {
+ const request = {
+ methodname: 'core_customfield_reload_template',
+ args: {component: component, area: area, itemid: itemid}
+ };
+
+ return Ajax.call([request])[0];
+};
+
+/**
+ * Toggle shared category enabled.
+ *
+ * @param {Number} categoryId
+ * @param {String} component
+ * @param {String} area
+ * @param {Number} itemid
+ * @param {Boolean} state
+ * @return {Promise}
+ */
+export const toggleCategory = (categoryId, component, area, itemid, state) => {
+ const request = {
+ methodname: 'core_customfield_toggle_shared',
+ args: {categoryid: categoryId, component: component, area: area, itemid: itemid, state: state}
+ };
+
+ return Ajax.call([request])[0];
+};
diff --git a/public/customfield/amd/src/repository/toggle_shared.js b/public/customfield/amd/src/repository/toggle_shared.js
index 809a05d630b..b94ee3dd121 100644
--- a/public/customfield/amd/src/repository/toggle_shared.js
+++ b/public/customfield/amd/src/repository/toggle_shared.js
@@ -19,6 +19,7 @@
* @module core_customfield/repository/toggle_shared
* @copyright 2025 David Carrillo
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 5.1 - please use core_customfield/repository instead.
*/
import Ajax from 'core/ajax';
diff --git a/public/customfield/amd/src/toggle_shared.js b/public/customfield/amd/src/toggle_shared.js
index 734773c6403..89479f9dd9a 100644
--- a/public/customfield/amd/src/toggle_shared.js
+++ b/public/customfield/amd/src/toggle_shared.js
@@ -26,7 +26,7 @@
import 'core/inplace_editable';
import Notification from 'core/notification';
import Pending from 'core/pending';
-import {toggleCategory} from 'core_customfield/repository/toggle_shared';
+import {toggleCategory} from 'core_customfield/repository';
let initialized = false;
diff --git a/public/customfield/classes/external/convert_category.php b/public/customfield/classes/external/convert_category.php
new file mode 100644
index 00000000000..6f131a24115
--- /dev/null
+++ b/public/customfield/classes/external/convert_category.php
@@ -0,0 +1,116 @@
+.
+
+declare(strict_types=1);
+
+namespace core_customfield\external;
+
+use core_customfield\api;
+use core_customfield\customfield\shared_handler;
+use core_customfield\handler;
+use core_customfield\category;
+use core_external\external_api;
+use core_external\external_value;
+use core_external\external_function_parameters;
+
+/**
+ * External method for converting any entity custom field category to a shared category.
+ *
+ * @package core_customfield
+ * @copyright 2026 Yerai Rodríguez
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class convert_category extends external_api {
+ /**
+ * External method parameters
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters([
+ 'categoryid' => new external_value(PARAM_INT, 'Category ID'),
+ 'component' => new external_value(PARAM_COMPONENT, 'Component'),
+ 'area' => new external_value(PARAM_AREA, 'Area'),
+ 'itemid' => new external_value(PARAM_INT, 'Item ID'),
+ ]);
+ }
+
+ /**
+ * External method execution
+ *
+ * @param int $categoryid
+ * @param string $component
+ * @param string $area
+ * @param int $itemid
+ * @return bool
+ */
+ public static function execute(int $categoryid, string $component, string $area, int $itemid): bool {
+ [
+ 'categoryid' => $categoryid,
+ 'component' => $component,
+ 'area' => $area,
+ 'itemid' => $itemid,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'categoryid' => $categoryid,
+ 'component' => $component,
+ 'area' => $area,
+ 'itemid' => $itemid,
+ ]);
+
+ // Validate context.
+ $context = \core\context\system::instance();
+ self::validate_context($context);
+
+ $handler = handler::get_handler($component, $area, $itemid);
+ $sharedhandler = shared_handler::get_handler('core_customfield', 'shared');
+ if (!$handler->can_configure() || !$sharedhandler->can_configure()) {
+ throw new \core\exception\access_denied_exception('You do not have permission to convert this category to shared.');
+ }
+
+ // Make sure no other shared custom fields with the same short name exist.
+ $categorieswithfields = $handler->get_categories_with_fields();
+ $convertingcategory = $handler->get_category_from_array($categorieswithfields, $categoryid, $component, $area, $itemid);
+ if (!$convertingcategory) {
+ throw new \core\exception\invalid_parameter_exception('Custom field category with ID ' . $categoryid .
+ ' does not exist for the specified component, area and itemid.');
+ }
+ foreach ($convertingcategory->get_fields() as $field) {
+ if (!api::is_shortname_unique($handler, $field->get('shortname'), $field->get('id'))) {
+ throw new \core\exception\moodle_exception('sharedcustomfieldalreadyexists', 'core_customfield');
+ }
+ }
+
+ // Convert category to shared.
+ $record = new category($categoryid);
+ $record->set_many([
+ 'component' => 'core_customfield',
+ 'area' => 'shared',
+ 'shared' => 1,
+ ]);
+ $record->save();
+
+ return true;
+ }
+
+ /**
+ * External method return value
+ *
+ * @return external_value
+ */
+ public static function execute_returns(): external_value {
+ return new external_value(PARAM_BOOL);
+ }
+}
diff --git a/public/customfield/classes/output/management.php b/public/customfield/classes/output/management.php
index f37e1e6de09..01698b7c291 100644
--- a/public/customfield/classes/output/management.php
+++ b/public/customfield/classes/output/management.php
@@ -25,6 +25,7 @@
namespace core_customfield\output;
use core_customfield\api;
+use core_customfield\customfield\shared_handler;
use core_customfield\handler;
use core_customfield\shared;
use renderable;
@@ -122,6 +123,10 @@ class management implements renderable, templatable {
]);
}
+ $sharedhandler = shared_handler::get_handler('core_customfield', 'shared');
+ $categoryarray['canconvert'] = $sharedhandler->can_configure() && !$category->get('shared');
+ $categoryarray['hasduplicatecustomfield'] = false;
+
$categoryarray['fields'] = array();
foreach ($category->get_fields() as $field) {
@@ -135,6 +140,11 @@ class management implements renderable, templatable {
$categoryarray['canedit'] = $canedit;
$categoryarray['fields'][] = $fieldarray;
+
+ if ($categoryarray['canconvert'] && !$categoryarray['hasduplicatecustomfield']) {
+ $fieldisunique = api::is_shortname_unique($this->handler, $fieldarray['shortname'], $fieldarray['id']);
+ $categoryarray['hasduplicatecustomfield'] = !$fieldisunique;
+ }
if ($canedit) {
$movablefieldscount++;
}
diff --git a/public/customfield/externallib.php b/public/customfield/externallib.php
index 2f5d8da28fa..a049823f5aa 100644
--- a/public/customfield/externallib.php
+++ b/public/customfield/externallib.php
@@ -122,6 +122,8 @@ class core_customfield_external extends external_api {
'nameeditable' => new external_value(PARAM_RAW, 'inplace editable name'),
'addfieldmenu' => new external_value(PARAM_RAW, 'addfieldmenu'),
'canedit' => new external_value(PARAM_BOOL, 'can edit'),
+ 'canconvert' => new external_value(PARAM_BOOL, 'can convert to shared category'),
+ 'hasduplicatecustomfield' => new external_value(PARAM_BOOL, 'category has a duplicate field name?'),
'fields' => new external_multiple_structure(
new external_single_structure(
array(
diff --git a/public/customfield/templates/list.mustache b/public/customfield/templates/list.mustache
index 5dd2a156cd8..55e6a05b1fd 100644
--- a/public/customfield/templates/list.mustache
+++ b/public/customfield/templates/list.mustache
@@ -46,6 +46,8 @@
"nameeditable": "Other fields",
"addfieldmenu": "Add field",
"canedit": true,
+ "canconvert": true,
+ "hasduplicatecustomfield": false,
"fields": [
{ "id": 0, "name": "Field name", "shortname": "shortname", "type": "Text" },
{ "id": 0, "name": "Another field", "shortname": "checkme", "type": "Checkbox" }
@@ -89,7 +91,21 @@
{{/canmovecategories}}
{{{nameeditable}}}
{{#canedit}}
-