feat: add sortable column headers to focus page

Click any column header to sort ascending/descending. Supports sorting by
order, done status, critical flag, due date, priority, title, domain, area,
project, and estimated time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 18:44:49 +00:00
parent d93893d5c6
commit 30cff10150

View File

@@ -27,28 +27,44 @@
<col style="width:18px"><col style="width:24px"><col style="width:20px"><col style="width:18px"><col style="width:74px"><col style="width:10px">
<col><col style="width:100px"><col style="width:110px"><col style="width:120px"><col style="width:50px"><col style="width:24px">
</colgroup>
<thead>
<tr style="border-bottom:2px solid var(--border);background:var(--surface2);">
<th class="focus-sort-head" data-col="0" style="padding:3px 2px;font-size:0.7rem;font-weight:600;color:var(--muted);text-align:center;cursor:pointer;" title="Sort by order">#</th>
<th style="padding:3px 1px;"></th>
<th class="focus-sort-head" data-col="2" style="padding:3px 1px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by done">Done</th>
<th class="focus-sort-head" data-col="3" style="padding:3px 1px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;text-align:center;" title="Sort by critical">!</th>
<th class="focus-sort-head" data-col="4" style="padding:3px 3px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by due date">Due</th>
<th class="focus-sort-head" data-col="5" style="padding:3px 1px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by priority">P</th>
<th class="focus-sort-head" data-col="6" style="padding:3px 3px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by title">Title</th>
<th class="focus-sort-head" data-col="7" style="padding:3px 3px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by domain">Domain</th>
<th class="focus-sort-head" data-col="8" style="padding:3px 3px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by area">Area</th>
<th class="focus-sort-head" data-col="9" style="padding:3px 3px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;" title="Sort by project">Project</th>
<th class="focus-sort-head" data-col="10" style="padding:3px 3px;font-size:0.7rem;font-weight:600;color:var(--muted);cursor:pointer;text-align:right;" title="Sort by estimate">Est</th>
<th style="padding:3px 1px;"></th>
</tr>
</thead>
<tbody class="focus-drag-group">
{% for item in items %}
<tr class="focus-drag-row" draggable="true" data-id="{{ item.id }}" style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}">
<td class="focus-row-num" style="padding:1px 2px;vertical-align:middle;text-align:center;color:var(--muted);font-size:0.72rem;font-weight:600;">{{ loop.index }}</td>
<td class="focus-row-num" data-sort="{{ '%04d'|format(loop.index) }}" style="padding:1px 2px;vertical-align:middle;text-align:center;color:var(--muted);font-size:0.72rem;font-weight:600;">{{ loop.index }}</td>
<td style="padding:1px 1px;vertical-align:middle;">{% with reorder_url="/focus/reorder", item_id=item.id %}{% include 'partials/reorder_arrows.html' %}{% endwith %}</td>
<td style="padding:1px 1px;vertical-align:middle;">
<td data-sort="{{ '1' if item.completed else '0' }}" style="padding:1px 1px;vertical-align:middle;">
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
<div class="row-check"><input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()"><label for="f-{{ item.id }}"></label></div>
</form>
</td>
<td style="padding:1px 1px;vertical-align:middle;text-align:center;">
<td data-sort="{{ '1' if item.critical else '0' }}" style="padding:1px 1px;vertical-align:middle;text-align:center;">
<form action="/focus/{{ item.id }}/toggle-critical" method="post" style="display:inline">
<button type="submit" class="btn-critical-toggle" title="Toggle critical" style="background:none;border:none;cursor:pointer;padding:0;font-size:0.8rem;line-height:1;{{ 'color:var(--red);' if item.critical else 'color:var(--border);' }}">&#9873;</button>
</form>
</td>
<td style="padding:1px 3px;vertical-align:middle;color:var(--muted);font-size:0.78rem;white-space:nowrap;">{{ item.due_date or '' }}</td>
<td style="padding:1px 1px;vertical-align:middle;">{% if item.task_id %}<span class="priority-dot priority-{{ item.priority }}"></span>{% elif item.list_item_id %}<span style="color:var(--muted);font-size:0.85rem;">&#9776;</span>{% else %}<span style="color:var(--muted);font-size:0.85rem;">&#9679;</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;{{ 'text-decoration:line-through;' if item.completed }}">{% if item.task_id %}{% if item.task_title %}<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.task_title }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% else %}<a href="/focus/{{ item.id }}" class="focus-title">{{ item.title }}</a>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.domain_name %}<span class="row-domain-tag" style="background:{{ (item.domain_color or 'var(--accent)') }}22;color:{{ item.domain_color or 'var(--accent)' }}">{{ item.domain_name }}</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.area_name %}<span class="row-tag">{{ item.area_name }}</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.project_name %}<span class="row-tag" style="background:var(--accent-soft);color:var(--accent)">{{ item.project_name }}</span>{% elif item.list_item_id and item.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ item.list_name }}</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;text-align:right;color:var(--muted);font-size:0.78rem;">{{ '~%smin'|format(item.estimated_minutes) if item.estimated_minutes else '' }}</td>
<td data-sort="{{ item.due_date or '9999-99-99' }}" style="padding:1px 3px;vertical-align:middle;color:var(--muted);font-size:0.78rem;white-space:nowrap;">{{ item.due_date or '' }}</td>
<td data-sort="{{ item.priority if item.task_id and item.priority else '9' }}" style="padding:1px 1px;vertical-align:middle;">{% if item.task_id %}<span class="priority-dot priority-{{ item.priority }}"></span>{% elif item.list_item_id %}<span style="color:var(--muted);font-size:0.85rem;">&#9776;</span>{% else %}<span style="color:var(--muted);font-size:0.85rem;">&#9679;</span>{% endif %}</td>
<td data-sort="{{ (item.task_title or item.list_item_content or item.title or '')|lower }}" style="padding:1px 3px;vertical-align:middle;{{ 'text-decoration:line-through;' if item.completed }}">{% if item.task_id %}{% if item.task_title %}<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.task_title }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% else %}<a href="/focus/{{ item.id }}" class="focus-title">{{ item.title }}</a>{% endif %}</td>
<td data-sort="{{ (item.domain_name or '')|lower }}" style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.domain_name %}<span class="row-domain-tag" style="background:{{ (item.domain_color or 'var(--accent)') }}22;color:{{ item.domain_color or 'var(--accent)' }}">{{ item.domain_name }}</span>{% endif %}</td>
<td data-sort="{{ (item.area_name or '')|lower }}" style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.area_name %}<span class="row-tag">{{ item.area_name }}</span>{% endif %}</td>
<td data-sort="{{ (item.project_name or item.list_name or '')|lower }}" style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.project_name %}<span class="row-tag" style="background:var(--accent-soft);color:var(--accent)">{{ item.project_name }}</span>{% elif item.list_item_id and item.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ item.list_name }}</span>{% endif %}</td>
<td data-sort="{{ '%05d'|format(item.estimated_minutes or 0) }}" style="padding:1px 3px;vertical-align:middle;text-align:right;color:var(--muted);font-size:0.78rem;">{{ '~%smin'|format(item.estimated_minutes) if item.estimated_minutes else '' }}</td>
<td style="padding:1px 1px;vertical-align:middle;">
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">&times;</button>
@@ -257,6 +273,47 @@
renumberFocusRows();
});
});
// Column sorting
var sortCol = -1;
var sortAsc = true;
document.querySelectorAll('.focus-sort-head').forEach(function(th) {
th.addEventListener('click', function() {
var col = parseInt(th.dataset.col);
if (sortCol === col) {
sortAsc = !sortAsc;
} else {
sortCol = col;
sortAsc = true;
}
// Update header indicators
document.querySelectorAll('.focus-sort-head').forEach(function(h) {
h.style.color = 'var(--muted)';
var arrow = h.querySelector('.sort-arrow');
if (arrow) arrow.remove();
});
th.style.color = 'var(--text)';
var indicator = document.createElement('span');
indicator.className = 'sort-arrow';
indicator.textContent = sortAsc ? ' \u25B2' : ' \u25BC';
indicator.style.fontSize = '0.6rem';
th.appendChild(indicator);
var tbody = document.querySelector('.focus-drag-group');
var rows = Array.from(tbody.querySelectorAll('.focus-drag-row'));
rows.sort(function(a, b) {
var aVal = a.cells[col].getAttribute('data-sort') || '';
var bVal = b.cells[col].getAttribute('data-sort') || '';
// Critical and Done: sort descending by default (critical/done first)
var defaultDesc = (col === 3);
var eff = defaultDesc ? !sortAsc : sortAsc;
var cmp = aVal.localeCompare(bVal, undefined, {numeric: true});
return eff ? cmp : -cmp;
});
rows.forEach(function(r) { tbody.appendChild(r); });
renumberFocusRows();
});
});
})();
</script>
{% endblock %}