Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion tasktiger_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from .views import TaskTigerView

__version__ = "0.4.1"
__version__ = "0.4.2"
__all__ = ["TaskTigerView", "tasktiger_admin"]

tasktiger_admin = Blueprint(
Expand Down
104 changes: 100 additions & 4 deletions tasktiger_admin/templates/tasktiger_admin/tasktiger.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ <h2>TaskTiger</h2>
<thead>
<tr>
<th></th>
<th>Queued</th>
<th>Active</th>
<th>Scheduled</th>
<th>Error</th>
<th class="sort-header" data-col="1" style="cursor: pointer;">Queued <span class="sort-arrow"></span></th>
<th class="sort-header" data-col="2" style="cursor: pointer;">Active <span class="sort-arrow"></span></th>
<th class="sort-header" data-col="3" style="cursor: pointer;">Scheduled <span class="sort-arrow"></span></th>
<th class="sort-header" data-col="4" style="cursor: pointer;">Error <span class="sort-arrow"></span></th>
</tr>
</thead>
<tbody class="searchable">
Expand Down Expand Up @@ -70,6 +70,102 @@ <h2>TaskTiger</h2>
});
});

// 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 <th>).
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');
Expand Down
Loading