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}} - + {{/hasduplicatecustomfield}} + {{#hasduplicatecustomfield}} + + {{/hasduplicatecustomfield}} + {{/canconvert}} + {{/canedit}} diff --git a/public/customfield/tests/behat/convert_category.feature b/public/customfield/tests/behat/convert_category.feature new file mode 100644 index 00000000000..eee993eaef9 --- /dev/null +++ b/public/customfield/tests/behat/convert_category.feature @@ -0,0 +1,39 @@ +@core @core_course @core_customfield @javascript +Feature: Managers can convert categories for any entity custom field category to shared custom field + In order to use custom fields across the site + As a manager + I need to convert any entity custom field's categories to shared custom fields + + Background: + Given the following "custom field categories" exist: + | name | component | area | itemid | + | Category for course 1 | core_course | course | 0 | + | Category for course 2 | core_course | course | 0 | + | Shared category | core_customfield | shared | 0 | + And the following "custom fields" exist: + | name | category | type | shortname | description | + | Field 1 | Category for course 1 | text | f1 | d1 | + | Field 2 | Category for course 1 | text | f2 | d2 | + | Field 3 | Category for course 2 | text | shf1 | d3 | + | Field 4 | Shared category | text | shf1 | shd1 | + + Scenario: Convert a course custom field category to shared custom fields + Given I log in as "admin" + When I navigate to "Courses > Default settings > Course custom fields" in site administration + And I click on "Convert to shared custom fields" "button" in the "Category for course 1" "core_customfield > Category" + And I should see "Are you sure you want to convert this category" in the "Convert to shared custom fields?" "dialogue" + And I click on "Proceed" "button" in the "Convert to shared custom fields?" "dialogue" + Then "The category has been successfully converted to shared custom fields" "toast_message" should be visible + And "Enable Category for course 1" "checkbox" should exist in the "Category for course 1" "core_customfield > Category" + And I reload the page + And I navigate to "Custom fields > Shared custom fields" in site administration + And "Category for course 1" "core_customfield > Category" should exist + And "Convert to shared custom fields" "button" should not exist + + Scenario: Cannot convert category with duplicate custom field short names in shared category + Given I log in as "admin" + When I navigate to "Courses > Default settings > Course custom fields" in site administration + And I click on "Convert to shared custom fields" "button" in the "Category for course 2" "core_customfield > Category" + Then I should see "One or more short names in this category already exist as shared custom fields." in the "Cannot convert to shared custom fields" "dialogue" + And I click on "Close" "button" in the "Cannot convert to shared custom fields" "dialogue" + And "Enable Category for course 2" "checkbox" should not exist in the "Category for course 2" "core_customfield > Category" diff --git a/public/customfield/tests/external/convert_category_test.php b/public/customfield/tests/external/convert_category_test.php new file mode 100644 index 00000000000..a227489a0bd --- /dev/null +++ b/public/customfield/tests/external/convert_category_test.php @@ -0,0 +1,154 @@ +. + +declare(strict_types=1); + +namespace core_customfield\external; + +use core_customfield\category; +use core_external\external_api; + +/** + * Unit tests for custom field convert_category external method + * + * @package core_customfield + * @covers \core_customfield\external\convert_category + * @copyright 2026 Yerai Rodríguez + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class convert_category_test extends \core_external\tests\externallib_testcase { + /** + * Test execute + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $category = $this->getDataGenerator()->create_custom_field_category([ + 'component' => 'core_course', + 'area' => 'course', + ]); + $this->getDataGenerator()->create_custom_field([ + 'shortname' => 'customfield1', + 'type' => 'text', + 'categoryid' => $category->get('id'), + ]); + + $result = convert_category::execute( + categoryid: $category->get('id'), + component: 'core_course', + area: 'course', + itemid: 0, + ); + + $result = external_api::clean_returnvalue(convert_category::execute_returns(), $result); + $this->assertTrue($result); + + // Assert category is now shared. + $categoryrecord = category::get_record(['id' => $category->get('id')]); + $this->assertEquals($category->get('id'), $categoryrecord->get('id')); + $this->assertEquals('core_customfield', $categoryrecord->get('component')); + $this->assertEquals('shared', $categoryrecord->get('area')); + $this->assertEquals(0, $categoryrecord->get('itemid')); + $this->assertEquals(1, $categoryrecord->get('shared')); + } + + /** + * Test execute with category not found + */ + public function test_execute_category_not_found(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->expectException(\core\exception\invalid_parameter_exception::class); + $this->expectExceptionMessage( + 'Custom field category with ID 999 does not exist for the specified component, area and itemid.' + ); + convert_category::execute( + categoryid: 999, + component: 'core_course', + area: 'course', + itemid: 0, + ); + } + + /** + * Test execute with no permission + */ + public function test_execute_no_permission(): void { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + + $category = $this->getDataGenerator()->create_custom_field_category([ + 'component' => 'core_course', + 'area' => 'course', + ]); + $this->getDataGenerator()->create_custom_field([ + 'shortname' => 'customfield1', + 'type' => 'text', + 'categoryid' => $category->get('id'), + ]); + + $this->setUser($user); + + $this->expectException(\core\exception\access_denied_exception::class); + $this->expectExceptionMessage('You do not have permission to convert this category to shared.'); + convert_category::execute( + categoryid: $category->get('id'), + component: 'core_course', + area: 'course', + itemid: 0, + ); + } + + /** + * Test execute with existing custom field in shared category + */ + public function test_execute_existing_customfield(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $category = $this->getDataGenerator()->create_custom_field_category([ + 'component' => 'core_course', + 'area' => 'course', + ]); + $this->getDataGenerator()->create_custom_field([ + 'shortname' => 'customfield1', + 'type' => 'text', + 'categoryid' => $category->get('id'), + ]); + + $sharedcategory = $this->getDataGenerator()->create_custom_field_category([ + 'component' => 'core_customfield', + 'area' => 'shared', + ]); + $this->getDataGenerator()->create_custom_field([ + 'shortname' => 'customfield1', + 'type' => 'text', + 'categoryid' => $sharedcategory->get('id'), + ]); + + $this->expectException(\core\exception\moodle_exception::class); + $this->expectExceptionMessage(get_string('sharedcustomfieldalreadyexists', 'core_customfield')); + convert_category::execute( + categoryid: $category->get('id'), + component: 'core_course', + area: 'course', + itemid: 0, + ); + } +} diff --git a/public/lang/en/customfield.php b/public/lang/en/customfield.php index 3a3f85be531..cf2ff545ca3 100644 --- a/public/lang/en/customfield.php +++ b/public/lang/en/customfield.php @@ -26,12 +26,17 @@ $string['action'] = 'Action'; $string['addingnewcustomfield'] = 'Adding a new {$a}'; $string['addnewcategory'] = 'Add a new category'; $string['afterfield'] = 'After field {$a}'; +$string['categoryconverted'] = 'The category has been successfully converted to shared custom fields'; $string['categorynotfound'] = 'Category not found'; $string['checked'] = 'Checked'; $string['commonsettings'] = 'General'; $string['componentsettings'] = 'Component settings'; $string['confirmdeletecategory'] = 'Are you sure you want to delete this category? All fields inside the category will also be deleted and all data associated with them. This action cannot be undone.'; $string['confirmdeletefield'] = 'Are you sure you want to delete this field and all associated data? This action cannot be undone.'; +$string['conversionerror'] = 'Cannot convert to shared custom fields'; +$string['convertcategory'] = 'Convert to shared custom fields'; +$string['convertcategoryconfirm'] = 'Are you sure you want to convert this category and all its fields to shared custom fields? This will make these fields available to all entities that support custom fields, such as courses, groups, cohorts, etc.

Once converted, the shared category will be enabled by default but can be disabled manually.

This action cannot be undone.'; +$string['convertcategorytitle'] = 'Convert to shared custom fields?'; $string['createnewcustomfield'] = 'Add a new custom field'; $string['customfield'] = 'Custom field'; $string['customfielddata'] = 'Custom fields data'; @@ -96,6 +101,7 @@ $string['privacy:metadata:customfield_shared:itemid'] = 'The ID of the item the $string['privacy:metadata:customfieldpluginsummary'] = 'Fields for various components'; $string['privacy:metadata:filepurpose'] = 'File attached to the custom field data'; $string['sharedcategoryhelptext'] = 'When enabled, the custom fields in this category will be used'; +$string['sharedcustomfieldalreadyexists'] = 'One or more short names in this category already exist as shared custom fields. To proceed with the conversion, please rename the conflicting fields in this category or modify the existing shared custom fields.'; $string['sharedcustomfields'] = 'Shared custom fields'; $string['shortname'] = 'Short name'; $string['shortname_help'] = 'The short name must be unique and can only contain alphanumeric lowercase characters and underscores (_). It is not displayed anywhere on the site, but may be used for synchronisation with external systems or in web services.'; diff --git a/public/lib/db/services.php b/public/lib/db/services.php index 4e8514019dd..174a6838742 100644 --- a/public/lib/db/services.php +++ b/public/lib/db/services.php @@ -2918,6 +2918,12 @@ $functions = array( 'type' => 'write', 'ajax' => true, ), + 'core_customfield_convert_category' => [ + 'classname' => core_customfield\external\convert_category::class, + 'description' => 'Convert an entity custom field category to a shared category.', + 'type' => 'write', + 'ajax' => true, + ], 'core_customfield_move_field' => array( 'classname' => 'core_customfield_external', 'methodname' => 'move_field', diff --git a/public/version.php b/public/version.php index 827e40e7dcb..fa9caadc83c 100644 --- a/public/version.php +++ b/public/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2026022700.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2026022700.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '5.2dev (Build: 20260227)'; // Human-friendly version name