MDL-74054 qbank_columnsortorder: Progressively enhance question bank actions

This modifies the question_data fragment used by the filter code to make its
parameters closer to the URL parameters of the question/edit.php page. This
Allows us to progressively enhance the add, remove and reset actions on the
question bank page, using this same fragment to reload the question table after
each change. This re-uses the same actions.js module used for enhancing these
actions on the qbank_columnsortorder admin screen.
This commit is contained in:
Mark Johnson 2023-09-05 10:31:10 +01:00 committed by Andrew Nicols
parent 3685a3355f
commit 63894ec2fe
No known key found for this signature in database
GPG Key ID: 6D1E3157C8CFBF14
14 changed files with 106 additions and 83 deletions

View File

@ -2549,15 +2549,22 @@ function mod_quiz_output_fragment_question_data(array $args): string {
$thispageurl = new \moodle_url('/mod/quiz/edit.php', ['cmid' => $cmid]);
$thiscontext = \context_module::instance($cmid);
$contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
$defaultcategory = question_make_default_categories($contexts->all());
$params['cat'] = implode(',', [$defaultcategory->id, $defaultcategory->contextid]);
$course = get_course($params['courseid']);
[, $cm] = get_module_from_cmid($cmid);
$params['tabname'] = 'questions';
// Custom question bank View.
$viewclass = clean_param($args['view'], PARAM_NOTAGS);
$questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams);
// Question table.
return $questionbank->display_questions_table();
$questionbank->add_standard_search_conditions();
ob_start();
$questionbank->display_question_list();
return ob_get_clean();
}
/**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -70,7 +70,9 @@ export const init = (
const filterSet = document.querySelector(`#${filterRegionId}`);
const filterCondition = {
const viewData = {
view: view,
cmid: cmid,
cat: defaultcategoryid,
courseid: defaultcourseid,
filter: {},
@ -78,12 +80,14 @@ export const init = (
qpage: 0,
qperpage: perpage,
sortdata: {},
tabname: 'questions',
extraparams: extraparams,
lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null,
};
let sortData = {};
const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;
if (defaultSort) {
filterCondition.sortData = JSON.parse(defaultSort);
sortData = JSON.parse(defaultSort);
}
/**
@ -97,25 +101,20 @@ export const init = (
// use the default category filter condition.
if (filterdata) {
// Main join types.
filterCondition.jointype = parseInt(filterSet.dataset.filterverb, 10);
viewData.jointype = parseInt(filterSet.dataset.filterverb, 10);
delete filterdata.jointype;
// Retrieve filter info.
filterCondition.filter = filterdata;
viewData.filter = filterdata;
if (Object.keys(filterdata).length !== 0) {
if (!isNaN(filterCondition.jointype)) {
filterdata.jointype = filterCondition.jointype;
if (!isNaN(viewData.jointype)) {
filterdata.jointype = viewData.jointype;
}
updateUrlParams(filterdata);
}
}
// Load questions for first page.
const viewData = {
view: view,
cmid: cmid,
filtercondition: JSON.stringify(filterCondition),
extraparams: extraparams,
lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null
};
viewData.filter = JSON.stringify(filterdata);
viewData.sortdata = JSON.stringify(sortData);
Fragment.loadFragment(component, callback, contextId, viewData)
// Render questions for first page and pagination.
.then((questionhtml, jsfooter) => {
@ -126,7 +125,7 @@ export const init = (
if (jsfooter === undefined) {
jsfooter = '';
}
Templates.replaceNodeContents(questionscontainer, questionhtml, jsfooter);
Templates.replaceNode(questionscontainer, questionhtml, jsfooter);
// Resolve filter promise.
if (pendingPromise) {
pendingPromise.resolve();
@ -187,15 +186,15 @@ export const init = (
const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);
if (sortableLink) {
e.preventDefault();
const oldSort = filterCondition.sortdata;
filterCondition.sortdata = {};
filterCondition.sortdata[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
const oldSort = sortData;
sortData = {};
sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
for (const sortname in oldSort) {
if (sortname !== sortableLink.dataset.sortname) {
filterCondition.sortdata[sortname] = oldSort[sortname];
sortData[sortname] = oldSort[sortname];
}
}
filterCondition.qpage = 0;
viewData.qpage = 0;
coreFilter.updateTableFromFilter();
}
if (paginationLink) {
@ -203,7 +202,7 @@ export const init = (
const paginationURL = new URL(paginationLink.getAttribute("href"));
const qpage = paginationURL.searchParams.get('qpage');
if (paginationURL.search !== null) {
filterCondition.qpage = qpage;
viewData.qpage = qpage;
coreFilter.updateTableFromFilter();
}
}

View File

@ -6,6 +6,6 @@ define("qbank_columnsortorder/actions",["exports","core/sortable_list","jquery",
* @copyright 2023 onwards Catalyst IT Europe Ltd
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupSortableLists=_exports.setupActionButtons=_exports.getColumnOrder=_exports.SELECTORS=void 0,_sortable_list=_interopRequireDefault(_sortable_list),_jquery=_interopRequireDefault(_jquery),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}(repository),_notification=_interopRequireDefault(_notification),_fragment=_interopRequireDefault(_fragment),_templates=_interopRequireDefault(_templates);const SELECTORS={columnList:".qbank-column-list",sortableColumn:".qbank-sortable-column",removeLink:"[data-action=remove]",moveHandler:"[data-drag-type=move]",addColumn:".addcolumn",addLink:"[data-action=add]",actionLink:".action-link"};_exports.SELECTORS=SELECTORS;_exports.setupSortableLists=function(listRoot){let vertical=arguments.length>1&&void 0!==arguments[1]&&arguments[1],global=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const sortableList=new _sortable_list.default(listRoot,{moveHandlerSelector:SELECTORS.moveHandler,isHorizontal:!vertical});sortableList.getElementName=element=>Promise.resolve(element.data("name"));const sortableColumns=(0,_jquery.default)(SELECTORS.sortableColumn);return sortableColumns.on(_sortable_list.default.EVENTS.DROP,(()=>{repository.setColumnbankOrder(getColumnOrder(listRoot),global).catch(_notification.default.exception),listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach((item=>item.classList.remove("active")))})),sortableColumns.on(_sortable_list.default.EVENTS.DRAGSTART,(event=>{event.currentTarget.classList.add("active")})),sortableColumns};_exports.setupActionButtons=function(uiRoot){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];uiRoot.addEventListener("click",(async e=>{const actionLink=e.target.closest(SELECTORS.actionLink);if(actionLink)try{e.preventDefault();const action=actionLink.dataset.action;if("add"===action||"remove"===action){const hiddenColumns=[],addColumnList=document.querySelector(SELECTORS.addColumn);addColumnList&&addColumnList.querySelectorAll(SELECTORS.addLink).forEach((item=>{"add"===action&&item===actionLink||hiddenColumns.push(item.dataset.column)})),"remove"===action&&hiddenColumns.push(actionLink.dataset.column),await repository.setHiddenColumns(hiddenColumns,global)}else"reset"===action&&await Promise.all([repository.setColumnbankOrder([],global),repository.setHiddenColumns([],global),repository.setColumnSize("",global)]);const fragmentData=uiRoot.dataset;_fragment.default.loadFragment(fragmentData.component,fragmentData.callback,fragmentData.contextid).then(((html,js)=>_templates.default.replaceNode(uiRoot,html,js))).catch(_notification.default.exception)}catch(ex){await _notification.default.exception(ex)}}))};const getColumnOrder=listRoot=>{const columns=Array.from(listRoot.querySelectorAll("[data-columnid]")).map((column=>column.dataset.columnid));return columns.filter(((value,index)=>columns.indexOf(value)===index))};_exports.getColumnOrder=getColumnOrder}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupSortableLists=_exports.setupActionButtons=_exports.getColumnOrder=_exports.SELECTORS=void 0,_sortable_list=_interopRequireDefault(_sortable_list),_jquery=_interopRequireDefault(_jquery),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}(repository),_notification=_interopRequireDefault(_notification),_fragment=_interopRequireDefault(_fragment),_templates=_interopRequireDefault(_templates);const SELECTORS={columnList:".qbank-column-list",sortableColumn:".qbank-sortable-column",removeLink:"[data-action=remove]",moveHandler:"[data-drag-type=move]",addColumn:".addcolumn",addLink:"[data-action=add]",actionLink:".action-link"};_exports.SELECTORS=SELECTORS;_exports.setupSortableLists=function(listRoot){let vertical=arguments.length>1&&void 0!==arguments[1]&&arguments[1],global=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const sortableList=new _sortable_list.default(listRoot,{moveHandlerSelector:SELECTORS.moveHandler,isHorizontal:!vertical});sortableList.getElementName=element=>Promise.resolve(element.data("name"));const sortableColumns=(0,_jquery.default)(SELECTORS.sortableColumn);return sortableColumns.on(_sortable_list.default.EVENTS.DROP,(()=>{repository.setColumnbankOrder(getColumnOrder(listRoot),global).catch(_notification.default.exception),listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach((item=>item.classList.remove("active")))})),sortableColumns.on(_sortable_list.default.EVENTS.DRAGSTART,(event=>{event.currentTarget.classList.add("active")})),sortableColumns};_exports.setupActionButtons=function(uiRoot){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];uiRoot.addEventListener("click",(async e=>{const actionLink=e.target.closest(SELECTORS.actionLink);if(actionLink)try{e.preventDefault();const action=actionLink.dataset.action;if("add"===action||"remove"===action){const hiddenColumns=[],addColumnList=document.querySelector(SELECTORS.addColumn);addColumnList&&addColumnList.querySelectorAll(SELECTORS.addLink).forEach((item=>{"add"===action&&item===actionLink||hiddenColumns.push(item.dataset.column)})),"remove"===action&&hiddenColumns.push(actionLink.dataset.column),await repository.setHiddenColumns(hiddenColumns,global)}else"reset"===action&&await Promise.all([repository.setColumnbankOrder([],global),repository.setHiddenColumns([],global),repository.setColumnSize("",global)]);const fragmentData=uiRoot.dataset,actionUrl=new URL(actionLink.href),returnUrl=new URL(actionUrl.searchParams.get("returnurl").replaceAll("&amp;","&")),viewData={},sortData={};returnUrl&&returnUrl.searchParams.forEach(((value,key)=>{const sortItem=key.match(/sortdata\[([^\]]+)\]/);sortItem?sortData[sortItem.pop()]=value:viewData[key]=value})),viewData.sortdata=JSON.stringify(sortData),_fragment.default.loadFragment(fragmentData.component,fragmentData.callback,fragmentData.contextid,viewData).then(((html,js)=>_templates.default.replaceNode(uiRoot,html,js))).catch(_notification.default.exception)}catch(ex){await _notification.default.exception(ex)}}))};const getColumnOrder=listRoot=>{const columns=Array.from(listRoot.querySelectorAll("[data-columnid]")).map((column=>column.dataset.columnid));return columns.filter(((value,index)=>columns.indexOf(value)===index))};_exports.getColumnOrder=getColumnOrder}));
//# sourceMappingURL=actions.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -109,8 +109,26 @@ export const setupActionButtons = (uiRoot, global = false) => {
]);
}
const fragmentData = uiRoot.dataset;
const actionUrl = new URL(actionLink.href);
const returnUrl = new URL(actionUrl.searchParams.get('returnurl').replaceAll('&amp;', '&'));
const viewData = {};
const sortData = {};
if (returnUrl) {
returnUrl.searchParams.forEach((value, key) => {
// Match keys like 'sortdata[fieldname]' and convert them to an array,
// because the fragment API doesn't like non-alphanum argument keys.
const sortItem = key.match(/sortdata\[([^\]]+)\]/);
if (sortItem) {
// The item returned by sortItem.pop() is the contents of the matching group, the field name.
sortData[sortItem.pop()] = value;
} else {
viewData[key] = value;
}
});
}
viewData.sortdata = JSON.stringify(sortData);
// We have to use then() there, as loadFragment doesn't appear to work with await.
Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid)
Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid, viewData)
.then((html, js) => {
return Templates.replaceNode(uiRoot, html, js);
})

View File

@ -349,7 +349,7 @@ const reorderColumns = event => {
* column sorting, then enable the move and resize modals to be triggered from menu actions.
*/
export const init = async() => {
const uiRoot = document.querySelector('.questionbankwindow');
const uiRoot = document.getElementById('questionscontainer');
await addHandleContainers(uiRoot);
setUpMoveHandles(uiRoot.querySelectorAll(SELECTORS.moveAction));
setUpResizeHandles(uiRoot);
@ -364,4 +364,5 @@ export const init = async() => {
});
setUpMoveActions(uiRoot);
setUpResizeActions(uiRoot);
actions.setupActionButtons(uiRoot);
};

View File

@ -59,7 +59,7 @@ class column_action_remove extends column_action_base {
'column' => $column->get_column_id(),
'action' => 'remove',
'sesskey' => sesskey(),
'returnurl' => $this->qbank->returnurl,
'returnurl' => new \moodle_url($this->qbank->returnurl),
]);
if ($this->global) {
$actionurl->param('global', $this->global);

View File

@ -37,16 +37,23 @@ class filter_condition_manager {
* @return array the param and extra param
*/
public static function extract_parameters_from_fragment_args(array $args): array {
global $DB;
// Decode query string.
$filtercondition = json_decode($args['filtercondition'], true);
$categories = $DB->get_records('question_categories', ['id' => clean_param($filtercondition['cat'], PARAM_INT)]);
$categories = \qbank_managecategories\helper::question_add_context_in_key($categories);
$category = array_pop($categories);
$filtercondition['cat'] = $category->id;
$extraparams = json_decode($args['extraparams'], true);
$params = [];
if (array_key_exists('filter', $args)) {
$params['filter'] = json_decode($args['filter'], true);
}
if (array_key_exists('cmid', $args)) {
$params['cmid'] = $args['cmid'];
}
if (array_key_exists('courseid', $args)) {
$params['courseid'] = $args['courseid'];
}
$params['jointype'] = $args['jointype'] ?? condition::JOINTYPE_DEFAULT;
$params['qpage'] = $args['qpage'] ?? 0;
$params['qperpage'] = $args['qperpage'] ?? 100;
$params['sortdata'] = json_decode($args['sortdata'] ?? '', true);
$extraparams = json_decode($args['extraparams'] ?? '', true);
return [$filtercondition, $extraparams];
return [$params, $extraparams];
}
/**

View File

@ -201,6 +201,9 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused =
// Category list page.
$params['cpage'] = optional_param('cpage', null, PARAM_INT);
// Sort data.
$params['sortdata'] = optional_param_array('sortdata', [], PARAM_INT);
$PAGE->set_pagelayout('admin');
return question_build_edit_resources($edittab, $baseurl, $params);
@ -247,7 +250,7 @@ function question_build_edit_resources($edittab, $baseurl, $params,
$thispageurl->remove_all_params(); // We are going to explicity add back everything important - this avoids unwanted params from being retained.
$cleanparams = [
'qsorts' => [],
'sortdata' => [],
'filter' => null
];
$paramtypes = [
@ -258,7 +261,6 @@ function question_build_edit_resources($edittab, $baseurl, $params,
'category' => PARAM_SEQUENCE,
'qperpage' => PARAM_INT,
'cpage' => PARAM_INT,
'qbshowtext' => PARAM_INT,
];
foreach ($paramtypes as $name => $type) {
@ -276,6 +278,10 @@ function question_build_edit_resources($edittab, $baseurl, $params,
$cleanparams['filter'] = $params['filter'];
}
if (isset($params['sortdata'])) {
$cleanparams['sortdata'] = clean_param_array($params['sortdata'], PARAM_INT);
}
$cmid = $cleanparams['cmid'];
$courseid = $cleanparams['courseid'];
$qpage = $cleanparams['qpage'] ?: -1;
@ -283,7 +289,6 @@ function question_build_edit_resources($edittab, $baseurl, $params,
$category = $cleanparams['category'] ?: 0;
$qperpage = $cleanparams['qperpage'];
$cpage = $cleanparams['cpage'] ?: 1;
$qsorts = $cleanparams['qsorts'];
if (is_null($cmid) && is_null($courseid)) {
throw new \moodle_exception('Must provide a cmid or courseid');
@ -293,16 +298,21 @@ function question_build_edit_resources($edittab, $baseurl, $params,
list($module, $cm) = get_module_from_cmid($cmid);
$courseid = $cm->course;
$thispageurl->params(compact('cmid'));
require_login($courseid, false, $cm);
$thiscontext = context_module::instance($cmid);
} else {
$module = null;
$cm = null;
$thispageurl->params(compact('courseid'));
require_login($courseid, false);
$thiscontext = context_course::instance($courseid);
}
if (defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
// For AJAX, we don't need to set up the course page for output.
require_login();
} else {
require_login($courseid, false, $cm);
}
if ($thiscontext){
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
$contexts->require_one_edit_tab_cap($edittab);
@ -330,19 +340,6 @@ function question_build_edit_resources($edittab, $baseurl, $params,
navigation_node::override_active_url($thispageurl);
}
// This need to occur after the override_active_url call above because
// these values change on the page request causing the URLs to mismatch
// when trying to work out the active node.
for ($i = 1; $i <= core_question\local\bank\view::MAX_SORTS; $i++) {
$param = 'qbs' . $i;
if (isset($params[$param])) {
$value = clean_param($params[$param], PARAM_TEXT);
} else {
break;
}
$thispageurl->param($param, $value);
}
if ($pagevars['qpage'] > -1) {
$thispageurl->param('qpage', $pagevars['qpage']);
} else {
@ -387,7 +384,7 @@ function question_build_edit_resources($edittab, $baseurl, $params,
$pagevars['tabname'] = $edittab;
// Sort parameters.
$pagevars['sortdata'] = optional_param_array('sortdata', [], PARAM_INT);
$pagevars['sortdata'] = $cleanparams['sortdata'];
foreach ($pagevars['sortdata'] as $sortname => $sortorder) {
$thispageurl->param('sortdata[' . $sortname . ']', $sortorder);
}

View File

@ -117,39 +117,33 @@ function core_question_output_fragment_tags_form($args) {
* @return array|string
*/
function core_question_output_fragment_question_data(array $args): string {
global $PAGE;
if (empty($args)) {
return '';
}
[$params, $extraparams] = \core_question\local\bank\filter_condition_manager::extract_parameters_from_fragment_args($args);
$thiscontext = \context_course::instance($params['courseid']);
$contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
$contexts->require_one_edit_tab_cap($params['tabname']);
$course = get_course($params['courseid']);
[
$thispageurl,
$contexts,
,
$cm,
,
$pagevars
] = question_build_edit_resources('questions', '/question/edit.php', $params);
if (is_null($cm)) {
$course = get_course(clean_param($args['courseid'], PARAM_INT));
} else {
$course = get_course($cm->course);
}
$viewclass = empty($args['view']) ? \core_question\local\bank\view::class : clean_param($args['view'], PARAM_NOTAGS);
$cmid = clean_param($args['cmid'], PARAM_INT);
[, $cm] = empty($cmid) ? [null, null] : get_module_from_cmid($cmid);
$nodeparent = $PAGE->settingsnav->find('questionbank', \navigation_node::TYPE_CONTAINER);
$thispageurl = new \moodle_url($nodeparent->action);
if ($cm) {
$thispageurl->param('cmid', $cm->id);
} else {
$thispageurl->param('courseid', $params['courseid']);
}
if (!empty($args['filtercondition'])) {
$thispageurl->param('filter', $args['filtercondition']);
}
if (!empty($args['lastchanged'])) {
$thispageurl->param('lastchanged', $args['lastchanged']);
$thispageurl->param('lastchanged', clean_param($args['lastchanged'], PARAM_INT));
}
if (!empty($params['sortdata'])) {
foreach ($params['sortdata'] as $sortname => $sortorder) {
$thispageurl->param('sortdata[' . $sortname . ']', $sortorder);
}
}
$questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams);
// This is highly suspicious, but it is the same approach taken in /question/edit.php. See MDL-79281.
$thispageurl->param('deleteall', 1);
$questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $pagevars, $extraparams);
$questionbank->add_standard_search_conditions();
ob_start();
$questionbank->display_question_list();