diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfb93bf..bd2f6d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changes
+## v0.4.2
+
+* Make the task-state columns (Queued, Active, Scheduled, Error) sortable.
+ Clicking a column header cycles its sort through descending, ascending, and
+ the original order, with a `↓`/`↑` arrow showing the active direction.
+ Grouped rows keep their children attached and sorted under their parent.
+
## v0.4.1
* First release with automated releases.
\ No newline at end of file
diff --git a/tasktiger_admin/__init__.py b/tasktiger_admin/__init__.py
index 0ea0b01..1adf5e7 100644
--- a/tasktiger_admin/__init__.py
+++ b/tasktiger_admin/__init__.py
@@ -2,7 +2,7 @@
from .views import TaskTigerView
-__version__ = "0.4.1"
+__version__ = "0.4.2"
__all__ = ["TaskTigerView", "tasktiger_admin"]
tasktiger_admin = Blueprint(
diff --git a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html
index 7ca1c6b..f37b56e 100644
--- a/tasktiger_admin/templates/tasktiger_admin/tasktiger.html
+++ b/tasktiger_admin/templates/tasktiger_admin/tasktiger.html
@@ -15,10 +15,10 @@
TaskTiger
|
- Queued |
- Active |
- Scheduled |
- Error |
+
+
+
+
@@ -70,6 +70,102 @@ TaskTiger
});
});
+// Capture the original row order once so the "none" sort mode can restore it.
+const searchableBody = document.querySelector('tbody.searchable');
+Array.from(searchableBody.rows).forEach((row, index) => {
+ row.dataset.originalIndex = index;
+});
+
+// Sorting state for the task-state columns.
+let sortColumn = null;
+let sortDirection = null;
+
+document.querySelectorAll('.sort-header').forEach(header => {
+ header.addEventListener('click', function() {
+ const column = parseInt(header.dataset.col, 10);
+ if (sortColumn === column) {
+ // Cycle the active column: desc -> asc -> none.
+ if (sortDirection === 'desc') {
+ sortDirection = 'asc';
+ } else if (sortDirection === 'asc') {
+ sortColumn = null;
+ sortDirection = null;
+ }
+ } else {
+ // Move the sort to a new column, starting descending.
+ sortColumn = column;
+ sortDirection = 'desc';
+ }
+ applySort();
+ updateArrows();
+ });
+});
+
+// Return true if the row is a top-level row (its first cell is a ).
+function isTopLevelRow(row) {
+ return row.cells[0].tagName === 'TH';
+}
+
+// Parse the numeric value of a row's cell at the active sort column.
+function cellValue(row) {
+ return parseInt(row.cells[sortColumn].textContent.trim(), 10) || 0;
+}
+
+function applySort() {
+ const rows = Array.from(searchableBody.rows);
+
+ // Partition rows into blocks. A block begins at a top-level row and
+ // includes all following child rows up to the next top-level row.
+ const blocks = [];
+ let currentBlock = null;
+ rows.forEach(row => {
+ if (isTopLevelRow(row)) {
+ currentBlock = { header: row, children: [] };
+ blocks.push(currentBlock);
+ } else if (currentBlock) {
+ currentBlock.children.push(row);
+ }
+ });
+
+ const originalIndex = row =>
+ parseInt(row.dataset.originalIndex, 10);
+
+ // Comparator for the current direction, with a stable tie-break on the
+ // original index so equal values keep a deterministic order.
+ const compare = (a, b) => {
+ if (sortDirection === null) {
+ return originalIndex(a) - originalIndex(b);
+ }
+ const delta = cellValue(a) - cellValue(b);
+ const ordered = sortDirection === 'asc' ? delta : -delta;
+ return ordered !== 0 ? ordered : originalIndex(a) - originalIndex(b);
+ };
+
+ blocks.sort((a, b) => compare(a.header, b.header));
+ blocks.forEach(block => block.children.sort(compare));
+
+ // Re-append rows in the new order. appendChild moves nodes, preserving
+ // each row's inline display so collapse/filter visibility is kept.
+ blocks.forEach(block => {
+ searchableBody.appendChild(block.header);
+ block.children.forEach(child => searchableBody.appendChild(child));
+ });
+}
+
+function updateArrows() {
+ document.querySelectorAll('.sort-header').forEach(header => {
+ const arrow = header.querySelector('.sort-arrow');
+ const column = parseInt(header.dataset.col, 10);
+ if (column === sortColumn && sortDirection === 'desc') {
+ arrow.textContent = '↓';
+ } else if (column === sortColumn && sortDirection === 'asc') {
+ arrow.textContent = '↑';
+ } else {
+ arrow.textContent = '';
+ }
+ });
+}
+
function filterRows(value) {
const regex = new RegExp(value, 'i');
const rows = document.querySelectorAll('.searchable tr');
|