bosun 0.28.3 → 0.28.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui/tabs/tasks.js CHANGED
@@ -89,6 +89,8 @@ const SORT_OPTIONS = [
89
89
  { value: "title", label: "Title" },
90
90
  ];
91
91
 
92
+ const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, "": 4 };
93
+
92
94
  const SYSTEM_TAGS = new Set([
93
95
  "draft",
94
96
  "todo",
@@ -760,6 +762,8 @@ export function TasksTab() {
760
762
  const [actionsOpen, setActionsOpen] = useState(false);
761
763
  const [exporting, setExporting] = useState(false);
762
764
  const [filtersOpen, setFiltersOpen] = useState(false);
765
+ const [listSortCol, setListSortCol] = useState(""); // active column sort in list mode
766
+ const [listSortDir, setListSortDir] = useState("desc"); // "asc" | "desc"
763
767
  const [isCompact, setIsCompact] = useState(() => {
764
768
  try { return globalThis.matchMedia?.("(max-width: 768px)")?.matches ?? false; }
765
769
  catch { return false; }
@@ -978,6 +982,31 @@ export function TasksTab() {
978
982
  ];
979
983
  }, [tasks]);
980
984
 
985
+ /* ── Client-side table sort (list mode) ── */
986
+ const sortedForTable = useMemo(() => {
987
+ if (!listSortCol) return visible;
988
+ return [...visible].sort((a, b) => {
989
+ let av, bv;
990
+ const dir = listSortDir === "asc" ? 1 : -1;
991
+ if (listSortCol === "priority") {
992
+ av = PRIORITY_ORDER[a.priority || ""] ?? 4;
993
+ bv = PRIORITY_ORDER[b.priority || ""] ?? 4;
994
+ return dir * (av - bv);
995
+ }
996
+ if (listSortCol === "updated") {
997
+ av = a.updated_at ? new Date(a.updated_at).getTime() : 0;
998
+ bv = b.updated_at ? new Date(b.updated_at).getTime() : 0;
999
+ return dir * (av - bv);
1000
+ }
1001
+ if (listSortCol === "status") { av = a.status || ""; bv = b.status || ""; }
1002
+ else if (listSortCol === "title") { av = (a.title || "").toLowerCase(); bv = (b.title || "").toLowerCase(); }
1003
+ else if (listSortCol === "repo") { av = a.repository || a.workspace || ""; bv = b.repository || b.workspace || ""; }
1004
+ else if (listSortCol === "branch") { av = getTaskBaseBranch(a); bv = getTaskBaseBranch(b); }
1005
+ else { return 0; }
1006
+ return dir * String(av).localeCompare(String(bv));
1007
+ });
1008
+ }, [visible, listSortCol, listSortDir]);
1009
+
981
1010
  /* ── Handlers ── */
982
1011
  const handleFilter = async (s) => {
983
1012
  haptic();
@@ -1578,23 +1607,16 @@ export function TasksTab() {
1578
1607
  `}
1579
1608
  </div>
1580
1609
 
1581
- <${Card}
1582
- title="Work Snapshot"
1583
- subtitle=${isKanban ? "Board view · showing all statuses" : "List view · filtered results"}
1584
- className="work-summary-card"
1585
- >
1586
- <div class="stat-strip">
1587
- ${summaryMetrics.map(
1588
- (metric) => html`
1589
- <${StatCard}
1590
- value=${metric.value}
1591
- label=${metric.label}
1592
- color=${metric.color}
1593
- />
1594
- `,
1595
- )}
1596
- </div>
1597
- <//>
1610
+ <div class="snapshot-bar">
1611
+ ${summaryMetrics.map((m) => html`
1612
+ <span key=${m.label} class="snapshot-pill">
1613
+ <span class="snapshot-dot" style="background:${m.color};" />
1614
+ <strong class="snapshot-val">${m.value}</strong>
1615
+ <span class="snapshot-lbl">${m.label}</span>
1616
+ </span>
1617
+ `)}
1618
+ <span class="snapshot-view-tag">${isKanban ? "⬛ Board" : "☰ List"}</span>
1619
+ </div>
1598
1620
 
1599
1621
  <style>
1600
1622
  .actions-btn { display:inline-flex; align-items:center; gap:4px; }
@@ -1617,119 +1639,84 @@ export function TasksTab() {
1617
1639
 
1618
1640
  ${isKanban && html`<${KanbanBoard} onOpenTask=${openDetail} />`}
1619
1641
 
1620
- ${!isKanban &&
1621
- visible.map((task) => {
1622
- const isManual = isTaskManual(task);
1623
- const descriptionLimit = isCompact ? 90 : 120;
1624
- const tags = getTaskTags(task);
1625
- const visibleTags = isCompact ? tags.slice(0, 3) : tags;
1626
- const extraTagCount = tags.length - visibleTags.length;
1627
- return html`
1628
- <div
1629
- key=${task.id}
1630
- class="task-card ${batchMode && selectedIds.has(task.id)
1631
- ? "task-card-selected"
1632
- : ""} task-card-enter"
1633
- data-status=${task.status || ""}
1634
- data-manual=${isManual ? "true" : "false"}
1635
- onClick=${() =>
1636
- batchMode ? toggleSelect(task.id) : openDetail(task.id)}
1637
- >
1638
- ${batchMode &&
1639
- html`
1640
- <input
1641
- type="checkbox"
1642
- checked=${selectedIds.has(task.id)}
1643
- class="task-checkbox"
1644
- onClick=${(e) => {
1645
- e.stopPropagation();
1646
- toggleSelect(task.id);
1647
- }}
1648
- style="accent-color:var(--accent)"
1649
- />
1650
- `}
1651
- <div class="task-card-header">
1652
- <div>
1653
- <div class="task-card-title">${task.title || "(untitled)"}</div>
1654
- <div class="task-card-meta">
1655
- ${task.id}${task.priority
1656
- ? html` ·
1657
- <${Badge}
1658
- status=${task.priority}
1659
- text=${task.priority}
1660
- />`
1661
- : ""}
1662
- ${task.updated_at
1663
- ? html` · ${formatRelative(task.updated_at)}`
1664
- : ""}
1665
- </div>
1666
- </div>
1667
- <div class="task-card-badges">
1668
- ${isManual && html`<${Badge} status="warning" text="manual" />`}
1669
- <${Badge} status=${task.status} text=${task.status} />
1670
- </div>
1671
- </div>
1672
- <div class="meta-text">
1673
- ${task.description
1674
- ? truncate(task.description, descriptionLimit)
1675
- : "No description."}
1676
- </div>
1677
- ${getTaskBaseBranch(task) &&
1678
- html`
1679
- <div class="meta-text">
1680
- Base: <code>${getTaskBaseBranch(task)}</code>
1681
- </div>
1682
- `}
1683
- ${(task.workspace || task.repository) &&
1684
- html`
1685
- <div class="meta-text" style="display:flex;gap:4px;align-items:center;flex-wrap:wrap">
1686
- ${task.workspace && html`<span class="pill" style="font-size:11px">📂 ${task.workspace}</span>`}
1687
- ${task.repository && html`<span class="pill" style="font-size:11px">📁 ${task.repository}</span>`}
1688
- </div>
1689
- `}
1690
- ${tags.length > 0 &&
1691
- html`
1692
- <div class="tag-row">
1693
- ${visibleTags.map(
1694
- (tag) => html`<span class="tag-chip">#${tag}</span>`,
1695
- )}
1696
- ${extraTagCount > 0 &&
1697
- html`
1698
- <span class="tag-chip tag-chip-muted">
1699
- +${extraTagCount}
1700
- </span>
1701
- `}
1702
- </div>
1703
- `}
1704
- ${!batchMode &&
1705
- html`
1706
- <div class="btn-row mt-sm task-card-actions" onClick=${(e) => e.stopPropagation()}>
1707
- ${task.status === "todo" &&
1708
- html`
1709
- <button
1710
- class="btn btn-primary btn-sm"
1711
- onClick=${() => openStartModal(task)}
1642
+ ${!isKanban && visible.length > 0 && html`
1643
+ <div class="task-table-wrap">
1644
+ <table class="task-table">
1645
+ <thead>
1646
+ <tr>
1647
+ ${[
1648
+ { col: "status", label: "Status" },
1649
+ { col: "priority", label: "Pri" },
1650
+ { col: "title", label: "Title", grow: true },
1651
+ { col: "branch", label: "Branch" },
1652
+ { col: "repo", label: "Repo" },
1653
+ { col: "updated", label: "Updated" },
1654
+ ].map(({ col, label, grow }) => {
1655
+ const active = listSortCol === col;
1656
+ const arrow = active ? (listSortDir === "asc" ? "▲" : "") : "⇅";
1657
+ return html`
1658
+ <th
1659
+ key=${col}
1660
+ class="task-th ${active ? "task-th-active" : ""} ${grow ? "task-th-grow" : ""}"
1661
+ onClick=${() => {
1662
+ if (listSortCol === col) {
1663
+ setListSortDir(listSortDir === "asc" ? "desc" : "asc");
1664
+ } else {
1665
+ setListSortCol(col);
1666
+ setListSortDir("desc");
1667
+ }
1668
+ }}
1669
+ >${label} <span class="task-th-arrow">${arrow}</span></th>
1670
+ `;
1671
+ })}
1672
+ </tr>
1673
+ </thead>
1674
+ <tbody>
1675
+ ${sortedForTable.map((task) => {
1676
+ const isManual = isTaskManual(task);
1677
+ const branch = getTaskBaseBranch(task);
1678
+ return html`
1679
+ <tr
1680
+ key=${task.id}
1681
+ class="task-tr ${batchMode && selectedIds.has(task.id) ? "task-tr-selected" : ""}"
1682
+ data-status=${task.status || ""}
1683
+ onClick=${() => batchMode ? toggleSelect(task.id) : openDetail(task.id)}
1712
1684
  >
1713
- Start
1714
- </button>
1715
- `}
1716
- <button
1717
- class="btn btn-secondary btn-sm"
1718
- onClick=${() => handleStatusUpdate(task.id, "inreview")}
1719
- >
1720
- Review
1721
- </button>
1722
- <button
1723
- class="btn btn-ghost btn-sm"
1724
- onClick=${() => handleStatusUpdate(task.id, "done")}
1725
- >
1726
- Done
1727
- </button>
1728
- </div>
1729
- `}
1730
- </div>
1731
- `;
1732
- })}
1685
+ <td class="task-td task-td-status">
1686
+ <${Badge} status=${task.status} text=${task.status} />
1687
+ ${isManual && html`<${Badge} status="warning" text="⚑" />`}
1688
+ </td>
1689
+ <td class="task-td task-td-pri">
1690
+ ${task.priority
1691
+ ? html`<${Badge} status=${task.priority} text=${task.priority} />`
1692
+ : html`<span class="task-td-empty">—</span>`}
1693
+ </td>
1694
+ <td class="task-td task-td-title">
1695
+ <div class="task-td-title-text">${task.title || "(untitled)"}</div>
1696
+ ${task.id && html`<div class="task-td-id">${task.id}</div>`}
1697
+ </td>
1698
+ <td class="task-td task-td-branch">
1699
+ ${branch
1700
+ ? html`<code class="task-td-code">${branch}</code>`
1701
+ : html`<span class="task-td-empty">—</span>`}
1702
+ </td>
1703
+ <td class="task-td task-td-repo">
1704
+ ${(task.repository || task.workspace)
1705
+ ? html`<span>${task.repository || task.workspace}</span>`
1706
+ : html`<span class="task-td-empty">—</span>`}
1707
+ </td>
1708
+ <td class="task-td task-td-updated">
1709
+ ${task.updated_at
1710
+ ? html`<span class="task-td-date">${formatRelative(task.updated_at)}</span>`
1711
+ : html`<span class="task-td-empty">—</span>`}
1712
+ </td>
1713
+ </tr>
1714
+ `;
1715
+ })}
1716
+ </tbody>
1717
+ </table>
1718
+ </div>
1719
+ `}
1733
1720
  ${!isKanban && !visible.length &&
1734
1721
  html`
1735
1722
  <${EmptyState}