@beastmode-develeap/beastmode 0.1.212 → 0.1.214
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/dist/web/board.html +303 -33
- package/dist/web/build-commit.txt +1 -1
- package/dist/web/build-stamp.txt +1 -1
- package/package.json +1 -1
package/dist/web/board.html
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
17
|
<!--BOARD_DATA-->
|
|
18
|
-
<script>window.__BUILD_STAMP__ = "20260509-
|
|
18
|
+
<script>window.__BUILD_STAMP__ = "20260509-230834-09eb714";</script>
|
|
19
19
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
20
20
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
21
21
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -1688,6 +1688,29 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
1688
1688
|
color: #f97316;
|
|
1689
1689
|
border: 1px solid rgba(249, 115, 22, 0.4);
|
|
1690
1690
|
}
|
|
1691
|
+
.badge-superseded {
|
|
1692
|
+
background: rgba(100, 116, 139, 0.2);
|
|
1693
|
+
color: #94a3b8;
|
|
1694
|
+
border: 1px solid rgba(100, 116, 139, 0.4);
|
|
1695
|
+
text-decoration: line-through;
|
|
1696
|
+
}
|
|
1697
|
+
.kanban-card[data-overlay="superseded"] {
|
|
1698
|
+
opacity: 0.6;
|
|
1699
|
+
}
|
|
1700
|
+
.kanban-card[data-overlay="superseded"] .card-title {
|
|
1701
|
+
text-decoration: line-through;
|
|
1702
|
+
color: var(--text-muted);
|
|
1703
|
+
}
|
|
1704
|
+
.badge-successor {
|
|
1705
|
+
background: rgba(100, 116, 139, 0.15);
|
|
1706
|
+
color: #94a3b8;
|
|
1707
|
+
cursor: pointer;
|
|
1708
|
+
text-decoration: none;
|
|
1709
|
+
}
|
|
1710
|
+
.badge-successor:hover {
|
|
1711
|
+
background: rgba(100, 116, 139, 0.3);
|
|
1712
|
+
color: #cbd5e1;
|
|
1713
|
+
}
|
|
1691
1714
|
|
|
1692
1715
|
/* Card-level visual treatment for overlay statuses */
|
|
1693
1716
|
.kanban-card[data-overlay="stuck"] {
|
|
@@ -2161,6 +2184,95 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
2161
2184
|
.filter-toggle:hover { border-color: var(--text-muted); color: var(--text); }
|
|
2162
2185
|
.filter-toggle.active { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); }
|
|
2163
2186
|
|
|
2187
|
+
/* Sort toggle button — mirrors filter-toggle */
|
|
2188
|
+
.sort-toggle {
|
|
2189
|
+
display: inline-flex;
|
|
2190
|
+
align-items: center;
|
|
2191
|
+
gap: 6px;
|
|
2192
|
+
padding: 0 14px;
|
|
2193
|
+
height: 36px;
|
|
2194
|
+
background: var(--bg-card);
|
|
2195
|
+
border: 1px solid var(--border);
|
|
2196
|
+
border-radius: var(--radius-sm);
|
|
2197
|
+
color: var(--text-secondary);
|
|
2198
|
+
font-size: 13px;
|
|
2199
|
+
font-family: var(--font-sans);
|
|
2200
|
+
cursor: pointer;
|
|
2201
|
+
transition: all 0.15s;
|
|
2202
|
+
white-space: nowrap;
|
|
2203
|
+
position: relative;
|
|
2204
|
+
}
|
|
2205
|
+
.sort-toggle:hover { border-color: var(--text-muted); color: var(--text); }
|
|
2206
|
+
.sort-toggle.active { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); }
|
|
2207
|
+
|
|
2208
|
+
.sort-active-label {
|
|
2209
|
+
font-size: 11px;
|
|
2210
|
+
font-weight: 600;
|
|
2211
|
+
color: var(--accent);
|
|
2212
|
+
margin-left: 2px;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
/* Sort dropdown */
|
|
2216
|
+
.sort-dropdown {
|
|
2217
|
+
position: absolute;
|
|
2218
|
+
top: 100%;
|
|
2219
|
+
right: 0;
|
|
2220
|
+
margin-top: 4px;
|
|
2221
|
+
background: var(--bg-card);
|
|
2222
|
+
border: 1px solid var(--border);
|
|
2223
|
+
border-radius: var(--radius-sm);
|
|
2224
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
2225
|
+
z-index: 100;
|
|
2226
|
+
min-width: 180px;
|
|
2227
|
+
padding: 4px;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
.sort-options {
|
|
2231
|
+
display: flex;
|
|
2232
|
+
flex-direction: column;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
.sort-option {
|
|
2236
|
+
display: flex;
|
|
2237
|
+
align-items: center;
|
|
2238
|
+
justify-content: space-between;
|
|
2239
|
+
padding: 8px 12px;
|
|
2240
|
+
background: none;
|
|
2241
|
+
border: none;
|
|
2242
|
+
color: var(--text-secondary);
|
|
2243
|
+
font-size: 13px;
|
|
2244
|
+
font-family: var(--font-sans);
|
|
2245
|
+
cursor: pointer;
|
|
2246
|
+
border-radius: var(--radius-sm);
|
|
2247
|
+
transition: background 0.1s;
|
|
2248
|
+
width: 100%;
|
|
2249
|
+
text-align: left;
|
|
2250
|
+
}
|
|
2251
|
+
.sort-option:hover { background: var(--bg-hover); color: var(--text); }
|
|
2252
|
+
.sort-option.active { color: var(--accent); font-weight: 600; }
|
|
2253
|
+
|
|
2254
|
+
.sort-dir-icon {
|
|
2255
|
+
font-size: 14px;
|
|
2256
|
+
font-weight: 700;
|
|
2257
|
+
color: var(--accent);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
.sort-clear {
|
|
2261
|
+
display: block;
|
|
2262
|
+
width: 100%;
|
|
2263
|
+
padding: 8px 12px;
|
|
2264
|
+
background: none;
|
|
2265
|
+
border: none;
|
|
2266
|
+
border-top: 1px solid var(--border);
|
|
2267
|
+
color: var(--text-muted);
|
|
2268
|
+
font-size: 12px;
|
|
2269
|
+
font-family: var(--font-sans);
|
|
2270
|
+
cursor: pointer;
|
|
2271
|
+
text-align: center;
|
|
2272
|
+
margin-top: 4px;
|
|
2273
|
+
}
|
|
2274
|
+
.sort-clear:hover { color: var(--danger); }
|
|
2275
|
+
|
|
2164
2276
|
.view-toggle {
|
|
2165
2277
|
display: inline-flex;
|
|
2166
2278
|
border: 1px solid var(--border);
|
|
@@ -3829,6 +3941,7 @@ function Icon({ name, size = 18, className = '' }) {
|
|
|
3829
3941
|
lightbulb: html`<path d="M8 1a5 5 0 00-3 9c.5.6.8 1.2 1 1.8V13h4v-1.2c.2-.6.5-1.2 1-1.8A5 5 0 008 1z"/><line x1="6" y1="13.5" x2="10" y2="13.5"/><line x1="6.5" y1="15" x2="9.5" y2="15"/>`,
|
|
3830
3942
|
help: html`<circle cx="8" cy="8" r="6.5"/><path d="M6 6.5a2 2 0 013.7 1c0 1.5-1.7 1.5-1.7 3"/><circle cx="8" cy="12" r="0.5" fill="currentColor" stroke="none"/>`,
|
|
3831
3943
|
filter: html`<polygon points="1.5,2 14.5,2 9.5,8.5 9.5,13 6.5,14.5 6.5,8.5"/>`,
|
|
3944
|
+
'arrow-up-down': html`<path d="M4 6l4-4 4 4"/><path d="M4 10l4 4 4-4"/>`,
|
|
3832
3945
|
costs: html`<circle cx="8" cy="8" r="6.5"/><path d="M8 3.5v1m0 7v1M6 10.5c0 .8.9 1.5 2 1.5s2-.7 2-1.5S9.1 9 8 9s-2-.7-2-1.5S6.9 6 8 6s2 .7 2 1.5"/>`,
|
|
3833
3946
|
};
|
|
3834
3947
|
|
|
@@ -4382,7 +4495,9 @@ function isOverlayStatus(status) {
|
|
|
4382
4495
|
const overlays = (typeof PIPELINE_CONFIG !== 'undefined' && PIPELINE_CONFIG && PIPELINE_CONFIG.overlay_statuses)
|
|
4383
4496
|
? PIPELINE_CONFIG.overlay_statuses
|
|
4384
4497
|
: ['Stuck', 'Awaiting Input'];
|
|
4385
|
-
|
|
4498
|
+
if (overlays.includes(status)) return true;
|
|
4499
|
+
if (status === 'Superseded') return true;
|
|
4500
|
+
return false;
|
|
4386
4501
|
}
|
|
4387
4502
|
|
|
4388
4503
|
function overlayBadgeClass(status) {
|
|
@@ -4390,6 +4505,7 @@ function overlayBadgeClass(status) {
|
|
|
4390
4505
|
const s = status.toLowerCase();
|
|
4391
4506
|
if (s === 'stuck') return 'badge-overlay-stuck';
|
|
4392
4507
|
if (s === 'awaiting input') return 'badge-overlay-awaiting';
|
|
4508
|
+
if (s === 'superseded') return 'badge-superseded';
|
|
4393
4509
|
return '';
|
|
4394
4510
|
}
|
|
4395
4511
|
|
|
@@ -4398,6 +4514,7 @@ function overlayBadgeLabel(status) {
|
|
|
4398
4514
|
const s = status.toLowerCase();
|
|
4399
4515
|
if (s === 'stuck') return '\u26A0 Stuck';
|
|
4400
4516
|
if (s === 'awaiting input') return '\u23F3 Awaiting Input';
|
|
4517
|
+
if (s === 'superseded') return 'Superseded';
|
|
4401
4518
|
return status;
|
|
4402
4519
|
}
|
|
4403
4520
|
|
|
@@ -4406,6 +4523,7 @@ function overlayKey(status) {
|
|
|
4406
4523
|
const s = status.toLowerCase();
|
|
4407
4524
|
if (s === 'stuck') return 'stuck';
|
|
4408
4525
|
if (s === 'awaiting input') return 'awaiting-input';
|
|
4526
|
+
if (s === 'superseded') return 'superseded';
|
|
4409
4527
|
return null;
|
|
4410
4528
|
}
|
|
4411
4529
|
|
|
@@ -4414,6 +4532,7 @@ function overlayTooltip(status) {
|
|
|
4414
4532
|
const s = status.toLowerCase();
|
|
4415
4533
|
if (s === 'stuck') return 'Task is stuck after max retries. Comment \'reset\' to restart or investigate the failure.';
|
|
4416
4534
|
if (s === 'awaiting input') return 'BeastMode needs your answer to a question before continuing. Check the task updates.';
|
|
4535
|
+
if (s === 'superseded') return 'This item has been superseded by another item. It is terminal and cannot be reactivated.';
|
|
4417
4536
|
return '';
|
|
4418
4537
|
}
|
|
4419
4538
|
|
|
@@ -4566,6 +4685,32 @@ function partitionByEpic(columnItems, allItems) {
|
|
|
4566
4685
|
groups[epicId].epicName = groups[epicId].epic.name;
|
|
4567
4686
|
groups[epicId].epicStatus = groups[epicId].epic.status;
|
|
4568
4687
|
}
|
|
4688
|
+
const allChildren = allItems.filter(i => String(i.parent_epic) === String(epicId));
|
|
4689
|
+
let doneCount = 0;
|
|
4690
|
+
let supersededCount = 0;
|
|
4691
|
+
let activeCount = 0;
|
|
4692
|
+
let mostCommonSuccessor = null;
|
|
4693
|
+
const successorCounts = {};
|
|
4694
|
+
for (const child of allChildren) {
|
|
4695
|
+
if (child.status === 'Done') {
|
|
4696
|
+
doneCount++;
|
|
4697
|
+
} else if (child.status === 'Superseded') {
|
|
4698
|
+
supersededCount++;
|
|
4699
|
+
const sby = child.extra && child.extra.superseded_by;
|
|
4700
|
+
if (sby) {
|
|
4701
|
+
successorCounts[sby] = (successorCounts[sby] || 0) + 1;
|
|
4702
|
+
}
|
|
4703
|
+
} else {
|
|
4704
|
+
activeCount++;
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
if (supersededCount > 0) {
|
|
4708
|
+
let maxCount = 0;
|
|
4709
|
+
for (const [id, count] of Object.entries(successorCounts)) {
|
|
4710
|
+
if (count > maxCount) { maxCount = count; mostCommonSuccessor = id; }
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
groups[epicId].breakdown = { doneCount, supersededCount, activeCount, mostCommonSuccessor };
|
|
4569
4714
|
}
|
|
4570
4715
|
|
|
4571
4716
|
const sortedGroupKeys = Object.keys(groups).sort((a, b) => {
|
|
@@ -5419,7 +5564,23 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject, all
|
|
|
5419
5564
|
<aside class="detail-sidebar open" ref=${sidebarRef}>
|
|
5420
5565
|
<div class="detail-resize-handle" onMouseDown=${onResizeStart}></div>
|
|
5421
5566
|
<div class="detail-header">
|
|
5422
|
-
<h3>${item.name || item.title}</h3>
|
|
5567
|
+
<h3 style=${item.status === 'Superseded' ? 'text-decoration:line-through;color:var(--text-muted);' : ''}>${item.name || item.title}</h3>
|
|
5568
|
+
${item.status === 'Superseded' && html`
|
|
5569
|
+
<div class="detail-superseded-info" data-testid="sidebar-superseded-info"
|
|
5570
|
+
style="display:flex;align-items:center;gap:8px;padding:4px 0 8px;font-size:13px;color:var(--text-muted);">
|
|
5571
|
+
<span class="badge-superseded" style="font-size:11px;padding:2px 8px;border-radius:4px;">Superseded</span>
|
|
5572
|
+
${item.extra && item.extra.superseded_by && html`
|
|
5573
|
+
<span>\u2192</span>
|
|
5574
|
+
<a href="#" data-testid="sidebar-successor-link"
|
|
5575
|
+
style="color:var(--accent);text-decoration:none;font-family:var(--font-mono);font-size:12px;"
|
|
5576
|
+
onClick=${(e) => {
|
|
5577
|
+
e.preventDefault();
|
|
5578
|
+
const target = allItems && allItems.find ? allItems.find(i => String(i.id) === String(item.extra.superseded_by)) : null;
|
|
5579
|
+
if (target && onSelectItem) onSelectItem(target);
|
|
5580
|
+
}}>#${item.extra.superseded_by}</a>
|
|
5581
|
+
`}
|
|
5582
|
+
</div>
|
|
5583
|
+
`}
|
|
5423
5584
|
<button class="detail-close" onClick=${onClose}>\u00d7</button>
|
|
5424
5585
|
</div>
|
|
5425
5586
|
<div class="detail-meta">
|
|
@@ -5824,8 +5985,7 @@ function PipelineView({
|
|
|
5824
5985
|
selectedProject,
|
|
5825
5986
|
setSelectedItem,
|
|
5826
5987
|
deleteItem,
|
|
5827
|
-
|
|
5828
|
-
cycleSort,
|
|
5988
|
+
globalSort,
|
|
5829
5989
|
sortColumnItems,
|
|
5830
5990
|
costsByItem,
|
|
5831
5991
|
envVerifyByItem,
|
|
@@ -5849,6 +6009,15 @@ function PipelineView({
|
|
|
5849
6009
|
<div class="card-title" style="cursor:pointer;" onClick=${() => setSelectedItem(item)} title=${item.name || item.title}>${item.name || item.title}</div>
|
|
5850
6010
|
<div class="card-footer">
|
|
5851
6011
|
${isOverlayStatus(item.status) && html`<span class=${'card-badge badge-overlay ' + overlayBadgeClass(item.status)} title=${overlayTooltip(item.status)}>${overlayBadgeLabel(item.status)}</span>`}
|
|
6012
|
+
${item.extra && item.extra.superseded_by && html`<a class="card-badge badge-successor"
|
|
6013
|
+
href="#" data-testid="card-successor-link"
|
|
6014
|
+
title=${'Superseded by #' + item.extra.superseded_by}
|
|
6015
|
+
onClick=${(e) => {
|
|
6016
|
+
e.preventDefault();
|
|
6017
|
+
e.stopPropagation();
|
|
6018
|
+
const target = filteredItems.find(i => String(i.id) === String(item.extra.superseded_by));
|
|
6019
|
+
if (target) setSelectedItem(target);
|
|
6020
|
+
}}>→ #${item.extra.superseded_by}</a>`}
|
|
5852
6021
|
${!isParentEpic && item.parent_epic && html`<span class="card-badge badge-epic">epic:${item.parent_epic}</span>`}
|
|
5853
6022
|
${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
|
|
5854
6023
|
${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
|
|
@@ -5915,8 +6084,7 @@ function PipelineView({
|
|
|
5915
6084
|
${columns.map(colId => {
|
|
5916
6085
|
const meta = getColumnMeta(colId);
|
|
5917
6086
|
const rawColItems = laneItemList.filter(item => getItemColumn(item, columns) === colId);
|
|
5918
|
-
const colItems = sortColumnItems(rawColItems,
|
|
5919
|
-
const sortMode = columnSorts[colId] || '';
|
|
6087
|
+
const colItems = sortColumnItems(rawColItems, globalSort);
|
|
5920
6088
|
return html`
|
|
5921
6089
|
<div class="pipeline-column"
|
|
5922
6090
|
data-testid=${'pipeline-col-' + colId}
|
|
@@ -5927,9 +6095,8 @@ function PipelineView({
|
|
|
5927
6095
|
onDrop=${e => onDrop(e, colId)}>
|
|
5928
6096
|
<div class="pipeline-column-header"
|
|
5929
6097
|
style=${'--col-color: ' + meta.color}
|
|
5930
|
-
title=${meta.tooltip}
|
|
5931
|
-
|
|
5932
|
-
<span>${meta.label}${sortMode && html`<span class="sort-indicator">${sortMode === 'priority' ? '\u25BC' : '\u25B2'}</span>`}</span>
|
|
6098
|
+
title=${meta.tooltip}>
|
|
6099
|
+
<span>${meta.label}</span>
|
|
5933
6100
|
<span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
|
|
5934
6101
|
</div>
|
|
5935
6102
|
<div class="pipeline-column-items">
|
|
@@ -5957,6 +6124,19 @@ function PipelineView({
|
|
|
5957
6124
|
<span class="epic-group-color" style="background: var(--accent)"></span>
|
|
5958
6125
|
<span class="epic-group-name">${group.epicName}</span>
|
|
5959
6126
|
<span class="epic-group-count">${totalCount}</span>
|
|
6127
|
+
${(() => {
|
|
6128
|
+
const bd = group.breakdown;
|
|
6129
|
+
if (!bd || (bd.doneCount === 0 && bd.supersededCount === 0)) return null;
|
|
6130
|
+
const parts = [];
|
|
6131
|
+
if (bd.doneCount > 0) parts.push(bd.doneCount + ' Done');
|
|
6132
|
+
if (bd.supersededCount > 0) parts.push(bd.supersededCount + ' Superseded');
|
|
6133
|
+
if (bd.activeCount > 0) parts.push(bd.activeCount + ' Active');
|
|
6134
|
+
return html`<span class="epic-group-breakdown" data-testid="epic-group-breakdown"
|
|
6135
|
+
style="font-size:10px;color:var(--text-muted);margin-left:4px;">(${parts.join(', ')}${bd.mostCommonSuccessor ? html` → <a href="#" data-testid="epic-breakdown-successor-link" style="color:var(--accent);text-decoration:none;" onClick=${(e) => {
|
|
6136
|
+
e.preventDefault();
|
|
6137
|
+
e.stopPropagation();
|
|
6138
|
+
}}>#${bd.mostCommonSuccessor}</a>` : ''})</span>`;
|
|
6139
|
+
})()}
|
|
5960
6140
|
</div>
|
|
5961
6141
|
<div class="epic-group-body" id=${egBodyId}>
|
|
5962
6142
|
${group.epic && renderCard(group.epic, meta.status, true)}
|
|
@@ -6167,7 +6347,14 @@ function BoardPage({ selectedProject }) {
|
|
|
6167
6347
|
const allKeys = ((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).map(s => s.key);
|
|
6168
6348
|
return new Set(allKeys);
|
|
6169
6349
|
});
|
|
6170
|
-
const [
|
|
6350
|
+
const [globalSort, setGlobalSort] = useState(() => {
|
|
6351
|
+
try {
|
|
6352
|
+
const saved = localStorage.getItem('beastmode-sort-mode');
|
|
6353
|
+
if (saved) return JSON.parse(saved);
|
|
6354
|
+
} catch {}
|
|
6355
|
+
return { field: '', direction: 'asc' };
|
|
6356
|
+
});
|
|
6357
|
+
const [sortOpen, setSortOpen] = useState(false);
|
|
6171
6358
|
const [epicCollapseKey, setEpicCollapseKey] = useState(0);
|
|
6172
6359
|
const [costsByItem, setCostsByItem] = useState({});
|
|
6173
6360
|
const [envVerifyByItem, setEnvVerifyByItem] = useState({});
|
|
@@ -6606,6 +6793,25 @@ function BoardPage({ selectedProject }) {
|
|
|
6606
6793
|
} catch {}
|
|
6607
6794
|
}, [activeSwimlanesSet]);
|
|
6608
6795
|
|
|
6796
|
+
// Persist sort preference to localStorage whenever it changes.
|
|
6797
|
+
useEffect(() => {
|
|
6798
|
+
localStorage.setItem('beastmode-sort-mode', JSON.stringify(globalSort));
|
|
6799
|
+
}, [globalSort]);
|
|
6800
|
+
|
|
6801
|
+
// Close sort dropdown when clicking outside.
|
|
6802
|
+
useEffect(() => {
|
|
6803
|
+
if (!sortOpen) return;
|
|
6804
|
+
const handler = (e) => {
|
|
6805
|
+
const dropdown = document.querySelector('.sort-dropdown');
|
|
6806
|
+
const toggle = document.querySelector('[data-testid="sort-toggle"]');
|
|
6807
|
+
if (dropdown && !dropdown.contains(e.target) && toggle && !toggle.contains(e.target)) {
|
|
6808
|
+
setSortOpen(false);
|
|
6809
|
+
}
|
|
6810
|
+
};
|
|
6811
|
+
document.addEventListener('click', handler, true);
|
|
6812
|
+
return () => document.removeEventListener('click', handler, true);
|
|
6813
|
+
}, [sortOpen]);
|
|
6814
|
+
|
|
6609
6815
|
// Expose swimlane state for external scenario verification.
|
|
6610
6816
|
useEffect(() => {
|
|
6611
6817
|
const swimlanes = (window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || [];
|
|
@@ -6687,28 +6893,32 @@ function BoardPage({ selectedProject }) {
|
|
|
6687
6893
|
const activeFilterCount = _baseFilterCount + (_swimlaneFilterActive ? 1 : 0);
|
|
6688
6894
|
|
|
6689
6895
|
// ── Column sorting ──
|
|
6896
|
+
const SORT_LABELS = {
|
|
6897
|
+
priority: 'Priority',
|
|
6898
|
+
name: 'Name',
|
|
6899
|
+
created: 'Created',
|
|
6900
|
+
updated: 'Updated',
|
|
6901
|
+
status_age: 'Time in Status',
|
|
6902
|
+
};
|
|
6690
6903
|
const PRIORITY_ORDER = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, '': 4 };
|
|
6691
|
-
const sortColumnItems = (colItems,
|
|
6692
|
-
if (!
|
|
6904
|
+
const sortColumnItems = (colItems, sort) => {
|
|
6905
|
+
if (!sort || !sort.field) return colItems;
|
|
6906
|
+
const dir = sort.direction === 'desc' ? -1 : 1;
|
|
6693
6907
|
const sorted = [...colItems];
|
|
6694
|
-
if (
|
|
6695
|
-
sorted.sort((a, b) => (PRIORITY_ORDER[a.priority || ''] ?? 4) - (PRIORITY_ORDER[b.priority || ''] ?? 4));
|
|
6696
|
-
} else if (
|
|
6697
|
-
sorted.sort((a, b) => (a.name || a.title || '').localeCompare(b.name || b.title || ''));
|
|
6908
|
+
if (sort.field === 'priority') {
|
|
6909
|
+
sorted.sort((a, b) => dir * ((PRIORITY_ORDER[a.priority || ''] ?? 4) - (PRIORITY_ORDER[b.priority || ''] ?? 4)));
|
|
6910
|
+
} else if (sort.field === 'name') {
|
|
6911
|
+
sorted.sort((a, b) => dir * (a.name || a.title || '').localeCompare(b.name || b.title || ''));
|
|
6912
|
+
} else if (sort.field === 'created') {
|
|
6913
|
+
sorted.sort((a, b) => dir * ((a.created_at || '').localeCompare(b.created_at || '')));
|
|
6914
|
+
} else if (sort.field === 'updated') {
|
|
6915
|
+
sorted.sort((a, b) => dir * ((a.updated_at || '').localeCompare(b.updated_at || '')));
|
|
6916
|
+
} else if (sort.field === 'status_age') {
|
|
6917
|
+
sorted.sort((a, b) => dir * ((a.status_changed_at || a.updated_at || '').localeCompare(b.status_changed_at || b.updated_at || '')));
|
|
6698
6918
|
}
|
|
6699
6919
|
return sorted;
|
|
6700
6920
|
};
|
|
6701
6921
|
|
|
6702
|
-
const cycleSort = (colId) => {
|
|
6703
|
-
setColumnSorts(prev => {
|
|
6704
|
-
const current = prev[colId] || '';
|
|
6705
|
-
const next = current === '' ? 'priority' : current === 'priority' ? 'name' : '';
|
|
6706
|
-
const copy = { ...prev };
|
|
6707
|
-
if (next) copy[colId] = next; else delete copy[colId];
|
|
6708
|
-
return copy;
|
|
6709
|
-
});
|
|
6710
|
-
};
|
|
6711
|
-
|
|
6712
6922
|
// Unique project IDs and parent epics for filter dropdowns
|
|
6713
6923
|
const uniqueProjects = [...new Set(items.map(i => i.project_id).filter(Boolean))];
|
|
6714
6924
|
const uniqueEpics = [...new Set(items.map(i => i.parent_epic).filter(Boolean))].map(String);
|
|
@@ -6794,6 +7004,46 @@ function BoardPage({ selectedProject }) {
|
|
|
6794
7004
|
${activeFilterCount > 0 && html`<span class="filter-active-count">${activeFilterCount}</span>`}
|
|
6795
7005
|
</button>
|
|
6796
7006
|
|
|
7007
|
+
<div style="position: relative;">
|
|
7008
|
+
<button class=${'sort-toggle' + (sortOpen || globalSort.field ? ' active' : '')}
|
|
7009
|
+
onClick=${() => setSortOpen(v => !v)}
|
|
7010
|
+
data-testid="sort-toggle">
|
|
7011
|
+
<${Icon} name="arrow-up-down" size=${13} />
|
|
7012
|
+
Sort
|
|
7013
|
+
${globalSort.field && html`<span class="sort-active-label">${SORT_LABELS[globalSort.field]}</span>`}
|
|
7014
|
+
</button>
|
|
7015
|
+
${sortOpen && html`
|
|
7016
|
+
<div class="sort-dropdown" data-testid="sort-dropdown">
|
|
7017
|
+
<div class="sort-options">
|
|
7018
|
+
${Object.entries(SORT_LABELS).map(([key, label]) => html`
|
|
7019
|
+
<button key=${key}
|
|
7020
|
+
class=${'sort-option' + (globalSort.field === key ? ' active' : '')}
|
|
7021
|
+
data-testid=${'sort-option-' + key}
|
|
7022
|
+
onClick=${() => {
|
|
7023
|
+
if (globalSort.field === key) {
|
|
7024
|
+
setGlobalSort(s => ({ ...s, direction: s.direction === 'asc' ? 'desc' : 'asc' }));
|
|
7025
|
+
} else {
|
|
7026
|
+
const defaultDir = (key === 'created' || key === 'updated') ? 'desc' : 'asc';
|
|
7027
|
+
setGlobalSort({ field: key, direction: defaultDir });
|
|
7028
|
+
}
|
|
7029
|
+
}}>
|
|
7030
|
+
<span>${label}</span>
|
|
7031
|
+
${globalSort.field === key && html`
|
|
7032
|
+
<span class="sort-dir-icon">${globalSort.direction === 'asc' ? '↑' : '↓'}</span>
|
|
7033
|
+
`}
|
|
7034
|
+
</button>
|
|
7035
|
+
`)}
|
|
7036
|
+
</div>
|
|
7037
|
+
${globalSort.field && html`
|
|
7038
|
+
<button class="sort-clear" data-testid="sort-clear"
|
|
7039
|
+
onClick=${() => { setGlobalSort({ field: '', direction: 'asc' }); setSortOpen(false); }}>
|
|
7040
|
+
Clear sort
|
|
7041
|
+
</button>
|
|
7042
|
+
`}
|
|
7043
|
+
</div>
|
|
7044
|
+
`}
|
|
7045
|
+
</div>
|
|
7046
|
+
|
|
6797
7047
|
<button class="btn btn-primary" onClick=${() => setShowCreateDialog(true)}>
|
|
6798
7048
|
<${Icon} name="plus" size=${14} />
|
|
6799
7049
|
New Task
|
|
@@ -6891,8 +7141,7 @@ function BoardPage({ selectedProject }) {
|
|
|
6891
7141
|
selectedProject=${selectedProject}
|
|
6892
7142
|
setSelectedItem=${setSelectedItem}
|
|
6893
7143
|
deleteItem=${deleteItem}
|
|
6894
|
-
|
|
6895
|
-
cycleSort=${cycleSort}
|
|
7144
|
+
globalSort=${globalSort}
|
|
6896
7145
|
sortColumnItems=${sortColumnItems}
|
|
6897
7146
|
costsByItem=${costsByItem}
|
|
6898
7147
|
envVerifyByItem=${envVerifyByItem}
|
|
@@ -6927,15 +7176,14 @@ function BoardPage({ selectedProject }) {
|
|
|
6927
7176
|
if (col.also && col.also.includes(i.status)) return true;
|
|
6928
7177
|
return false;
|
|
6929
7178
|
});
|
|
6930
|
-
const colItems = sortColumnItems(rawColItems,
|
|
6931
|
-
const sortMode = columnSorts[col.id] || '';
|
|
7179
|
+
const colItems = sortColumnItems(rawColItems, globalSort);
|
|
6932
7180
|
return html`
|
|
6933
7181
|
<div class="kanban-column" key=${col.id}
|
|
6934
7182
|
onDragOver=${e => onDragOver(e, col.id)}
|
|
6935
7183
|
onDragLeave=${onDragLeave}
|
|
6936
7184
|
onDrop=${e => onDrop(e, col.id)}>
|
|
6937
|
-
<div class="kanban-column-header" style=${'--col-color: ' + col.color} title=${col.tooltip}
|
|
6938
|
-
<span>${col.label}
|
|
7185
|
+
<div class="kanban-column-header" style=${'--col-color: ' + col.color} title=${col.tooltip}>
|
|
7186
|
+
<span>${col.label}</span>
|
|
6939
7187
|
<span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
|
|
6940
7188
|
</div>
|
|
6941
7189
|
<div class="kanban-items">
|
|
@@ -6957,6 +7205,15 @@ function BoardPage({ selectedProject }) {
|
|
|
6957
7205
|
<div class="card-title" style="cursor:pointer;" onClick=${() => setSelectedItem(item)} title=${item.name || item.title}>${item.name || item.title}</div>
|
|
6958
7206
|
<div class="card-footer">
|
|
6959
7207
|
${isOverlayStatus(item.status) && html`<span class=${'card-badge badge-overlay ' + overlayBadgeClass(item.status)} title=${overlayTooltip(item.status)}>${overlayBadgeLabel(item.status)}</span>`}
|
|
7208
|
+
${item.extra && item.extra.superseded_by && html`<a class="card-badge badge-successor"
|
|
7209
|
+
href="#" data-testid="card-successor-link"
|
|
7210
|
+
title=${'Superseded by #' + item.extra.superseded_by}
|
|
7211
|
+
onClick=${(e) => {
|
|
7212
|
+
e.preventDefault();
|
|
7213
|
+
e.stopPropagation();
|
|
7214
|
+
const target = swimlaneFilteredItems.find(i => String(i.id) === String(item.extra.superseded_by));
|
|
7215
|
+
if (target) setSelectedItem(target);
|
|
7216
|
+
}}>→ #${item.extra.superseded_by}</a>`}
|
|
6960
7217
|
${!isParentEpic && item.parent_epic && html`<span class="card-badge badge-epic">epic:${item.parent_epic}</span>`}
|
|
6961
7218
|
${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
|
|
6962
7219
|
${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
|
|
@@ -7006,6 +7263,19 @@ function BoardPage({ selectedProject }) {
|
|
|
7006
7263
|
<span class="epic-group-color" style="background: var(--accent)"></span>
|
|
7007
7264
|
<span class="epic-group-name">${group.epicName}</span>
|
|
7008
7265
|
<span class="epic-group-count">${totalCount}</span>
|
|
7266
|
+
${(() => {
|
|
7267
|
+
const bd = group.breakdown;
|
|
7268
|
+
if (!bd || (bd.doneCount === 0 && bd.supersededCount === 0)) return null;
|
|
7269
|
+
const parts = [];
|
|
7270
|
+
if (bd.doneCount > 0) parts.push(bd.doneCount + ' Done');
|
|
7271
|
+
if (bd.supersededCount > 0) parts.push(bd.supersededCount + ' Superseded');
|
|
7272
|
+
if (bd.activeCount > 0) parts.push(bd.activeCount + ' Active');
|
|
7273
|
+
return html`<span class="epic-group-breakdown" data-testid="epic-group-breakdown"
|
|
7274
|
+
style="font-size:10px;color:var(--text-muted);margin-left:4px;">(${parts.join(', ')}${bd.mostCommonSuccessor ? html` → <a href="#" data-testid="epic-breakdown-successor-link" style="color:var(--accent);text-decoration:none;" onClick=${(e) => {
|
|
7275
|
+
e.preventDefault();
|
|
7276
|
+
e.stopPropagation();
|
|
7277
|
+
}}>#${bd.mostCommonSuccessor}</a>` : ''})</span>`;
|
|
7278
|
+
})()}
|
|
7009
7279
|
</div>
|
|
7010
7280
|
<div class="epic-group-body" id=${bodyId}>
|
|
7011
7281
|
${group.epic && renderCard(group.epic, true)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
09eb71470e076891f6ed9304e081cc9372bad7fc
|
package/dist/web/build-stamp.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
20260509-
|
|
1
|
+
20260509-230834-09eb714
|