Skip to content

Commit 259be97

Browse files
committed
Merge branch 'dev' of https://github.com/maths/moodle-qtype_stack into iss1171
2 parents 961d172 + e386e2e commit 259be97

30 files changed

Lines changed: 2189 additions & 767 deletions

.github/workflows/moodle-ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
moodle-branch: 'MOODLE_502_STABLE'
4242
database: 'pgsql'
4343
maxima: 'SBCL'
44-
moodle-app: false
44+
moodle-app: true
4545
- php: '8.2'
4646
moodle-branch: 'MOODLE_500_STABLE'
4747
database: 'pgsql'
@@ -58,7 +58,7 @@ jobs:
5858
moodle-branch: 'MOODLE_402_STABLE'
5959
database: 'pgsql'
6060
maxima: 'GCL'
61-
moodle-app: false
61+
moodle-app: true
6262

6363
steps:
6464
- name: Install Maxima (${{ matrix.maxima }})
@@ -267,4 +267,7 @@ jobs:
267267
- name: Behat features
268268
if: ${{ always() }}
269269
run: moodle-plugin-ci behat --profile chrome --auto-rerun 12
270+
env:
271+
# April 2026 - Remove after next Moodle App release
272+
MOODLE_BEHAT_SELENIUM_IMAGE: selenium/standalone-chrome:145.0-20260222
270273

amd/build/dashboard.min.js

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

amd/build/dashboard.min.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

amd/src/dashboard.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// This file is part of Moodle - http://moodle.org/
2+
//
3+
// Moodle is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// Moodle is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
15+
16+
/**
17+
* A javascript module to handle some UI on the STACK dashboard.
18+
*
19+
* @module qtype_stack/dashboard
20+
* @copyright 2026 The University of Edinburgh
21+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22+
*/
23+
define([], function () {
24+
25+
/**
26+
* Sets up.
27+
*
28+
*/
29+
function init() {
30+
// Add simple client-side sorting for the first two columns.
31+
const variantsTable = document.getElementById('deployed-variants-table');
32+
if (!variantsTable) {
33+
return;
34+
}
35+
const sortableHeaders = variantsTable.querySelectorAll('.stack-sortable-header');
36+
const toggleVariantsButton = variantsTable.querySelector('.stack-toggle-variants-btn');
37+
38+
/**
39+
* Update arrow icons showing direction of sort
40+
*
41+
* @param {element} activeHeader Header element to update
42+
* @param {boolean} isAsc is column sorted in ascending order?
43+
*/
44+
function setHeaderState(activeHeader, isAsc) {
45+
sortableHeaders.forEach((header) => {
46+
const arrowIcon = header.querySelector('.stack-sort-arrow i');
47+
if (header === activeHeader) {
48+
header.setAttribute('aria-sort', isAsc ? 'ascending' : 'descending');
49+
header.dataset.sortDirection = isAsc ? 'asc' : 'desc';
50+
if (arrowIcon) {
51+
arrowIcon.classList.remove('fa-sort', 'fa-sort-up', 'fa-sort-down', 'text-muted');
52+
arrowIcon.classList.add(isAsc ? 'fa-sort-up' : 'fa-sort-down');
53+
}
54+
} else {
55+
header.setAttribute('aria-sort', 'none');
56+
delete header.dataset.sortDirection;
57+
if (arrowIcon) {
58+
arrowIcon.classList.remove('fa-sort-up', 'fa-sort-down');
59+
arrowIcon.classList.add('fa-sort', 'text-muted');
60+
}
61+
}
62+
});
63+
}
64+
65+
/**
66+
* Get test content of index cell or row.
67+
*
68+
* @param {int} row
69+
* @param {int} index
70+
*/
71+
function getCellValue(row, index) {
72+
const cell = row.cells[index];
73+
return cell ? cell.textContent.trim() : '';
74+
}
75+
76+
/**
77+
* Sort table by a column.
78+
*
79+
* @param {element} header The header element mof the column.
80+
*/
81+
function sortByHeader(header) {
82+
const columnIndex = Number(header.dataset.sortIndex);
83+
const sortType = header.dataset.sortType;
84+
const currentDirection = header.dataset.sortDirection || 'none';
85+
// Flip current direction.
86+
const isAsc = currentDirection !== 'asc';
87+
const direction = isAsc ? 1 : -1;
88+
// Filters out header row.
89+
const rows = Array.from(variantsTable.querySelectorAll('tr')).filter((row) => row.querySelector('td'));
90+
91+
rows.sort((a, b) => {
92+
const av = getCellValue(a, columnIndex);
93+
const bv = getCellValue(b, columnIndex);
94+
95+
if (sortType === 'number') {
96+
const an = Number(av);
97+
const bn = Number(bv);
98+
const aIsNum = !Number.isNaN(an);
99+
const bIsNum = !Number.isNaN(bn);
100+
if (aIsNum && bIsNum) {
101+
return (an - bn) * direction;
102+
}
103+
return av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' }) * direction;
104+
}
105+
106+
return av.localeCompare(bv, undefined, { sensitivity: 'base' }) * direction;
107+
});
108+
109+
rows.forEach((row) => variantsTable.appendChild(row));
110+
setHeaderState(header, isAsc);
111+
}
112+
113+
// Add event listeners to headers.
114+
sortableHeaders.forEach((header) => {
115+
header.style.cursor = 'pointer';
116+
header.addEventListener('click', () => sortByHeader(header));
117+
header.addEventListener('keydown', (event) => {
118+
if (event.key === 'Enter' || event.key === ' ') {
119+
event.preventDefault();
120+
sortByHeader(header);
121+
}
122+
});
123+
});
124+
125+
// Assume question note starts in ascending order and show that state initially.
126+
const defaultSortedHeader = variantsTable.querySelector('.stack-sortable-header[data-sort-index="1"]');
127+
if (defaultSortedHeader) {
128+
setHeaderState(defaultSortedHeader, true);
129+
}
130+
131+
// Prevent toggle button sorting column. Add event listener to toggle selection boxes.
132+
if (toggleVariantsButton) {
133+
toggleVariantsButton.addEventListener('click', (event) => {
134+
event.preventDefault();
135+
event.stopPropagation();
136+
const checkboxes = Array.from(variantsTable.querySelectorAll('input[type="checkbox"][name^="selectvariant-"]'));
137+
checkboxes.forEach((checkbox) => {
138+
checkbox.checked = !checkbox.checked;
139+
});
140+
});
141+
}
142+
143+
const warningTab = document.getElementById('warning-tab');
144+
if (document.querySelectorAll('.var-pane-fail').length) {
145+
const varTab = document.getElementById('variants-pane-warning');
146+
varTab.style.display = 'inline';
147+
warningTab.style.display = 'inline';
148+
}
149+
if (document.querySelectorAll('.test-pane-fail').length) {
150+
const testTab = document.getElementById('test-pane-warning');
151+
testTab.style.display = 'inline';
152+
warningTab.style.display = 'inline';
153+
}
154+
}
155+
156+
/** Export our entry point. */
157+
return {
158+
init: init
159+
};
160+
});

0 commit comments

Comments
 (0)