diff --git a/course/format/amd/build/local/content/activity_header.min.js b/course/format/amd/build/local/content/activity_header.min.js
new file mode 100644
index 00000000000..2cc101f8448
--- /dev/null
+++ b/course/format/amd/build/local/content/activity_header.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/content/activity_header",["exports","core/reactive","core_courseformat/courseeditor","core_course/events"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;d=f(d);function e(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;e=function(){return a};return a}function f(a){if(a&&a.__esModule){return a}if(null===a||"object"!==g(a)&&"function"!=typeof a){return{default:a}}var b=e();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in a){if(Object.prototype.hasOwnProperty.call(a,f)){var h=d?Object.getOwnPropertyDescriptor(a,f):null;if(h&&(h.get||h.set)){Object.defineProperty(c,f,h)}else{c[f]=a[f]}}}c.default=a;if(b){b.set(a,c)}return c}function g(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){g=function(a){return typeof a}}else{g=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return g(a)}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c.\n\n/**\n * The activity header component.\n *\n * @module core_courseformat/local/content/activity_header\n * @class core_courseformat/local/content/activity_header\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport * as CourseEvents from 'core_course/events';\n\n// Global page selectors.\nconst SELECTORS = {\n ACTIVITY_HEADER: `[data-for='page-activity-header']`,\n};\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'activity_header';\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target optional altentative DOM main element CSS selector\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n const elementselector = (target) ? target : SELECTORS.ACTIVITY_HEADER;\n return new Component({\n element: document.querySelector(elementselector),\n reactive: getCurrentCourseEditor(),\n selectors\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Capture completion events.\n this.addEventListener(\n this.element,\n CourseEvents.manualCompletionToggled,\n this._completionHandler\n );\n }\n\n /**\n * Activity manual completion listener.\n *\n * @param {Event} event the custom event\n * @param {object} event.detail the event details\n */\n _completionHandler({detail}) {\n if (detail === undefined) {\n return;\n }\n this.reactive.dispatch('cmCompletion', [detail.cmid], detail.completed);\n }\n}\n"],"file":"activity_header.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/src/local/content/activity_header.js b/course/format/amd/src/local/content/activity_header.js
new file mode 100644
index 00000000000..ec5ac52c5f1
--- /dev/null
+++ b/course/format/amd/src/local/content/activity_header.js
@@ -0,0 +1,84 @@
+// 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 .
+
+/**
+ * The activity header component.
+ *
+ * @module core_courseformat/local/content/activity_header
+ * @class core_courseformat/local/content/activity_header
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent} from 'core/reactive';
+import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
+import * as CourseEvents from 'core_course/events';
+
+// Global page selectors.
+const SELECTORS = {
+ ACTIVITY_HEADER: `[data-for='page-activity-header']`,
+};
+
+export default class Component extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'activity_header';
+ }
+
+ /**
+ * Static method to create a component instance form the mustache template.
+ *
+ * @param {string} target optional altentative DOM main element CSS selector
+ * @param {object} selectors optional css selector overrides
+ * @return {Component}
+ */
+ static init(target, selectors) {
+ const elementselector = (target) ? target : SELECTORS.ACTIVITY_HEADER;
+ return new Component({
+ element: document.querySelector(elementselector),
+ reactive: getCurrentCourseEditor(),
+ selectors
+ });
+ }
+
+ /**
+ * Initial state ready method.
+ */
+ stateReady() {
+ // Capture completion events.
+ this.addEventListener(
+ this.element,
+ CourseEvents.manualCompletionToggled,
+ this._completionHandler
+ );
+ }
+
+ /**
+ * Activity manual completion listener.
+ *
+ * @param {Event} event the custom event
+ * @param {object} event.detail the event details
+ */
+ _completionHandler({detail}) {
+ if (detail === undefined) {
+ return;
+ }
+ this.reactive.dispatch('cmCompletion', [detail.cmid], detail.completed);
+ }
+}
diff --git a/course/format/tests/behat/courseindex_completion.feature b/course/format/tests/behat/courseindex_completion.feature
index 8a842c1bcb2..8d10341a7d6 100644
--- a/course/format/tests/behat/courseindex_completion.feature
+++ b/course/format/tests/behat/courseindex_completion.feature
@@ -40,7 +40,7 @@ Feature: Course index completion icons
And "To do" "icon" should exist in the "courseindex-content" "region"
@javascript
- Scenario: Manual completion shoudl update the course index completion
+ Scenario: Manual completion should update the course index completion
Given I am on the "C1" "Course" page logged in as "student1"
And "To do" "icon" should exist in the "courseindex-content" "region"
When I press "Mark as done"
@@ -50,6 +50,17 @@ Feature: Course index completion icons
And I wait until "Mark as done" "button" exists
And "To do" "icon" should exist in the "courseindex-content" "region"
+ @javascript
+ Scenario: Manual completion in an activity page should update the course index
+ Given I am on the "sample1" "Activity" page logged in as "student1"
+ And "To do" "icon" should exist in the "courseindex-content" "region"
+ When I press "Mark as done"
+ And I wait until "Done" "button" exists
+ Then "Done" "icon" should exist in the "courseindex-content" "region"
+ And I press "Done"
+ And I wait until "Mark as done" "button" exists
+ And "To do" "icon" should exist in the "courseindex-content" "region"
+
@javascript
Scenario: Refresh the page should keep the completion consistent
Given I am on the "C1" "Course" page logged in as "student1"
diff --git a/lib/classes/output/activity_header.php b/lib/classes/output/activity_header.php
index af1bd2bf904..7af3340b0da 100644
--- a/lib/classes/output/activity_header.php
+++ b/lib/classes/output/activity_header.php
@@ -174,6 +174,14 @@ class activity_header implements \renderable, \templatable {
$completion = $output->activity_information($this->page->cm, $completiondetails, $activitydates);
}
+ $format = course_get_format($this->page->course);
+ if ($format->supports_components()) {
+ $this->page->requires->js_call_amd(
+ 'core_courseformat/local/content/activity_header',
+ 'init'
+ );
+ }
+
return [
'title' => $this->title,
'description' => $this->description,
diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php
index b9a835cd861..afe8b9842b7 100644
--- a/lib/outputrequirementslib.php
+++ b/lib/outputrequirementslib.php
@@ -1041,11 +1041,12 @@ class page_requirements_manager {
public function js_call_amd($fullmodule, $func = null, $params = array()) {
global $CFG;
- list($component, $module) = explode('/', $fullmodule, 2);
+ $modulepath = explode('/', $fullmodule);
- $component = clean_param($component, PARAM_COMPONENT);
- $module = clean_param($module, PARAM_ALPHANUMEXT);
- $modname = "{$component}/{$module}";
+ $modname = clean_param(array_shift($modulepath), PARAM_COMPONENT);
+ foreach ($modulepath as $module) {
+ $modname .= '/' . clean_param($module, PARAM_ALPHANUMEXT);
+ }
$functioncode = [];
if ($func !== null) {
diff --git a/lib/templates/activity_header.mustache b/lib/templates/activity_header.mustache
index 42339b6748f..149f32adfa9 100644
--- a/lib/templates/activity_header.mustache
+++ b/lib/templates/activity_header.mustache
@@ -70,7 +70,7 @@
{{#title}}
{{{title}}}
{{/title}}
-