Skip to content

Commit

Permalink
MDL-76848 core_courseformat: bulk availability
Browse files Browse the repository at this point in the history
  • Loading branch information
ferranrecio committed Feb 17, 2023
1 parent b1ad848 commit e680289
Show file tree
Hide file tree
Showing 22 changed files with 625 additions and 10 deletions.
2 changes: 1 addition & 1 deletion course/format/amd/build/local/content/actions.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion course/format/amd/build/local/content/actions.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion course/format/amd/build/local/courseeditor/exporter.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

122 changes: 121 additions & 1 deletion course/format/amd/src/local/content/actions.js
Expand Up @@ -31,7 +31,7 @@ import ModalEvents from 'core/modal_events';
import Templates from 'core/templates';
import {prefetchStrings} from 'core/prefetch';
import {get_string as getString} from 'core/str';
import {getList} from 'core/normalise';
import {getList, getFirst} from 'core/normalise';
import * as CourseEvents from 'core_course/events';
import Pending from 'core/pending';
import ContentTree from 'core_courseformat/local/courseeditor/contenttree';
Expand Down Expand Up @@ -72,6 +72,8 @@ export default class extends BaseComponent {
CONTENTTREE: `#destination-selector`,
ACTIONMENU: `.action-menu`,
ACTIONMENUTOGGLER: `[data-toggle="dropdown"]`,
// Availability modal selectors.
OPTIONSRADIO: `[type='radio']`,
};
// Component css classes.
this.classes = {
Expand Down Expand Up @@ -174,6 +176,31 @@ export default class extends BaseComponent {
this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);
}

/**
* Return the ids represented by this element.
*
* Depending on the dataset attributes the action could represent a single id
* or a bulk actions with all the current selected ids.
*
* @param {HTMLElement} target
* @returns {Number[]} array of Ids
*/
_getTargetIds(target) {
let ids = [];
if (target?.dataset?.id) {
ids.push(target.dataset.id);
}
const bulkType = target?.dataset?.bulk;
if (!bulkType) {
return ids;
}
const bulk = this.reactive.get('bulk');
if (bulk.enabled && bulk.selectedType === bulkType) {
ids = [...ids, ...bulk.selection];
}
return ids;
}

/**
* Handle a move section request.
*
Expand Down Expand Up @@ -498,6 +525,99 @@ export default class extends BaseComponent {
);
}

/**
* Handle a cm availability change request.
*
* @param {Element} target the dispatch action element
*/
async _requestCmAvailability(target) {
const cmIds = this._getTargetIds(target);
if (cmIds.length == 0) {
return;
}
// Show the availability modal to decide which action to trigger.
const exporter = this.reactive.getExporter();
const data = {
allowstealth: exporter.canUseStealth(this.reactive.state, cmIds),
};
const modalParams = {
title: getString('availability', 'core'),
body: Templates.render('core_courseformat/local/content/cm/availabilitymodal', data),
saveButtonText: getString('apply', 'core'),
type: ModalFactory.types.SAVE_CANCEL,
};
const modal = await this._modalBodyRenderedPromise(modalParams);

this._setupMutationRadioButtonModal(modal, cmIds);
}

/**
* Handle a section availability change request.
*
* @param {Element} target the dispatch action element
*/
async _requestSectionAvailability(target) {
const sectionIds = this._getTargetIds(target);
if (sectionIds.length == 0) {
return;
}
// Show the availability modal to decide which action to trigger.
const modalParams = {
title: getString('availability', 'core'),
body: Templates.render('core_courseformat/local/content/section/availabilitymodal', []),
saveButtonText: getString('apply', 'core'),
type: ModalFactory.types.SAVE_CANCEL,
};
const modal = await this._modalBodyRenderedPromise(modalParams);

this._setupMutationRadioButtonModal(modal, sectionIds);
}

/**
* Add events to a mutation selector radio buttons modal.
* @param {Modal} modal
* @param {Number[]} ids the section or cm ids to apply the mutation
*/
_setupMutationRadioButtonModal(modal, ids) {
// The save button is not enabled until the user selects an option.
modal.setButtonDisabled('save', true);

const submitFunction = (radio) => {
const mutation = radio?.value;
if (!mutation) {
return false;
}
this.reactive.dispatch(mutation, ids);
return true;
};

const modalBody = getFirst(modal.getBody());
const radioOptions = modalBody.querySelectorAll(this.selectors.OPTIONSRADIO);
radioOptions.forEach(radio => {
radio.addEventListener('change', () => {
modal.setButtonDisabled('save', false);
});
radio.parentNode.addEventListener('click', () => {
radio.checked = true;
modal.setButtonDisabled('save', false);
});
radio.parentNode.addEventListener('dblclick', dbClickEvent => {
if (submitFunction(radio)) {
dbClickEvent.preventDefault();
modal.destroy();
}
});
});

modal.getRoot().on(
ModalEvents.save,
() => {
const radio = modalBody.querySelector(`${this.selectors.OPTIONSRADIO}:checked`);
submitFunction(radio);
}
);
}

/**
* Disable all add sections actions.
*
Expand Down
14 changes: 14 additions & 0 deletions course/format/amd/src/local/courseeditor/exporter.js
Expand Up @@ -223,4 +223,18 @@ export default class {
});
return items;
}

/**
* Check is some activities of a list can be stealth.
*
* @param {Object} state the current state.
* @param {Number[]} cmIds the module ids to check
* @returns {Boolean} if any of the activities can be stealth.
*/
canUseStealth(state, cmIds) {
return cmIds.some(cmId => {
const cminfo = state.cm.get(cmId);
return cminfo?.allowstealth ?? false;
});
}
}
2 changes: 2 additions & 0 deletions course/format/amd/src/local/courseeditor/mutations.js
Expand Up @@ -74,6 +74,7 @@ export default class {
targetSectionId,
targetCmId
);
this.bulkReset(stateManager);
stateManager.processUpdates(updates);
this.sectionLock(stateManager, sectionIds, false);
}
Expand All @@ -96,6 +97,7 @@ export default class {
targetSectionId,
targetCmId
);
this.bulkReset(stateManager);
stateManager.processUpdates(updates);
this.cmLock(stateManager, cmIds, false);
}
Expand Down
32 changes: 32 additions & 0 deletions course/format/classes/output/local/content/bulkedittools.php
Expand Up @@ -82,7 +82,23 @@ protected function get_toolbar_actions(): array {
* @return array of edit control items
*/
protected function cm_control_items(): array {
global $USER;
$format = $this->format;
$context = $format->get_context();
$user = $USER;

$controls = [];

if (has_capability('moodle/course:activityvisibility', $context, $user)) {
$controls['availability'] = [
'icon' => 't/show',
'action' => 'cmAvailability',
'name' => get_string('availability'),
'title' => get_string('cmavailability', 'core_courseformat'),
'bulk' => 'cm',
];
}

return $controls;
}

Expand All @@ -95,7 +111,23 @@ protected function cm_control_items(): array {
* @return array of edit control items
*/
protected function section_control_items(): array {
global $USER;
$format = $this->format;
$context = $format->get_context();
$user = $USER;

$controls = [];

if (has_capability('moodle/course:sectionvisibility', $context, $user)) {
$controls['availability'] = [
'icon' => 't/show',
'action' => 'sectionAvailability',
'name' => get_string('availability'),
'title' => get_string('sectionavailability', 'core_courseformat'),
'bulk' => 'section',
];
}

return $controls;
}
}
4 changes: 3 additions & 1 deletion course/format/classes/output/local/state/cm.php
Expand Up @@ -72,7 +72,7 @@ public function __construct(course_format $format, section_info $section, cm_inf
* @return stdClass data context for a mustache template
*/
public function export_for_template(renderer_base $output): stdClass {
global $USER;
global $CFG, $USER;

$format = $this->format;
$section = $this->section;
Expand Down Expand Up @@ -114,6 +114,8 @@ public function export_for_template(renderer_base $output): stdClass {
$data->completionstate = $completiondata->completionstate;
}

$data->allowstealth = !empty($CFG->allowstealth) && $format->allow_stealth_module_visibility($cm, $section);

return $data;
}

Expand Down
@@ -0,0 +1,96 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_courseformat/local/content/cm/availabilitymodal
Displays the activity availability modal form.
Example context (json):
{
"allowstealth": true
}

}}
<div class="d-flex flex-column p-3">
<form>
<div class="d-flex flex-row align-items-start py-3 border-bottom">
<div class="icon-box mx-2">
{{#pix}} t/hide, core {{/pix}}
</div>
<input
class="mt-2 mx-2"
type="radio"
id="showRadio"
name="option"
value="cmShow"
aria-describedby="showRadio_help"
>
<div class="w-100">
<label class="mb-1" for="showRadio">
{{#str}} availability_show, core_courseformat {{/str}}
</label>
<div id="showRadio_help" class="small text-muted">
{{#str}} availability_show_help, core_courseformat {{/str}}
</div>
</div>
</div>
<div class="d-flex flex-row align-items-start py-3 {{#allowstealth}} border-bottom {{/allowstealth}}">
<div class="icon-box mx-2">
{{#pix}} t/show, core {{/pix}}
</div>
<input
class="mt-2 mx-2"
type="radio"
id="hideRadio"
name="option"
value="cmHide"
aria-describedby="hideRadio_help"
>
<div class="w-100">
<label class="mt-1" for="hideRadio">
{{#str}} availability_hide, core_courseformat {{/str}}
</label>
<div id="hideRadio_help" class="small text-muted">
{{#str}} availability_hide_help, core_courseformat {{/str}}
</div>
</div>
</div>
{{#allowstealth}}
<div class="d-flex flex-row align-items-start py-3">
<div class="icon-box mx-2">
{{#pix}} t/stealth, core {{/pix}}
</div>
<input
class="mt-2 mx-2"
type="radio"
id="stealthRadio"
name="option"
value="cmStealth"
aria-describedby="stealthRadio_help"
>
<div class="w-100">
<label class="mt-1" for="stealthRadio">
{{#str}} availability_stealth, core_courseformat {{/str}}
</label>
<div id="stealthRadio_help" class="small text-muted">
{{#str}} availability_stealth_help, core_courseformat {{/str}}
</div>
</div>
</div>
{{/allowstealth}}
</form>
</div>

0 comments on commit e680289

Please sign in to comment.