beads-ui 0.1.1 → 0.2.0
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/CHANGES.md +27 -1
- package/README.md +39 -45
- package/app/data/providers.js +57 -26
- package/app/index.html +8 -0
- package/app/main.js +179 -33
- package/app/protocol.md +3 -4
- package/app/router.js +45 -9
- package/app/state.js +27 -11
- package/app/styles.css +170 -6
- package/app/utils/issue-id-renderer.js +71 -0
- package/app/utils/issue-url.js +9 -0
- package/app/utils/toast.js +35 -0
- package/app/views/board.js +347 -17
- package/app/views/detail.js +292 -92
- package/app/views/epics.js +2 -2
- package/app/views/issue-dialog.js +170 -0
- package/app/views/issue-row.js +9 -8
- package/app/views/list.js +85 -11
- package/app/views/new-issue-dialog.js +352 -0
- package/app/ws.js +30 -0
- package/docs/architecture.md +1 -1
- package/package.json +17 -1
- package/server/cli/commands.js +11 -3
- package/server/cli/index.js +35 -4
- package/server/cli/usage.js +1 -1
- package/server/watcher.js +3 -3
- package/server/ws.js +39 -19
- package/.beads/issues.jsonl +0 -107
- package/.editorconfig +0 -10
- package/.eslintrc.json +0 -36
- package/.github/workflows/ci.yml +0 -38
- package/.prettierignore +0 -5
- package/AGENTS.md +0 -85
- package/app/data/providers.test.js +0 -126
- package/app/main.board-switch.test.js +0 -94
- package/app/main.deep-link.test.js +0 -64
- package/app/main.live-updates.test.js +0 -229
- package/app/main.test.js +0 -17
- package/app/main.theme.test.js +0 -41
- package/app/main.view-sync.test.js +0 -54
- package/app/protocol.test.js +0 -57
- package/app/router.test.js +0 -34
- package/app/state.test.js +0 -21
- package/app/utils/markdown.test.js +0 -103
- package/app/utils/type-badge.test.js +0 -30
- package/app/views/board.test.js +0 -184
- package/app/views/detail.acceptance-notes.test.js +0 -67
- package/app/views/detail.assignee.test.js +0 -161
- package/app/views/detail.deps.test.js +0 -97
- package/app/views/detail.edits.test.js +0 -146
- package/app/views/detail.labels.test.js +0 -73
- package/app/views/detail.priority.test.js +0 -86
- package/app/views/detail.test.js +0 -188
- package/app/views/detail.ui47.test.js +0 -78
- package/app/views/epics.test.js +0 -283
- package/app/views/list.inline-edits.test.js +0 -84
- package/app/views/list.test.js +0 -479
- package/app/views/nav.test.js +0 -43
- package/app/ws.test.js +0 -168
- package/docs/quickstart.md +0 -142
- package/eslint.config.js +0 -59
- package/media/bdui-board.png +0 -0
- package/media/bdui-epics.png +0 -0
- package/media/bdui-issues.png +0 -0
- package/prettier.config.js +0 -13
- package/server/app.test.js +0 -29
- package/server/bd.test.js +0 -93
- package/server/cli/cli.test.js +0 -109
- package/server/cli/commands.integration.test.js +0 -155
- package/server/cli/commands.unit.test.js +0 -94
- package/server/cli/open.test.js +0 -26
- package/server/db.test.js +0 -70
- package/server/protocol.test.js +0 -87
- package/server/watcher.test.js +0 -100
- package/server/ws.handlers.test.js +0 -174
- package/server/ws.labels.test.js +0 -95
- package/server/ws.mutations.test.js +0 -261
- package/server/ws.subscriptions.test.js +0 -116
- package/server/ws.test.js +0 -52
- package/test/setup-vitest.js +0 -12
- package/tsconfig.json +0 -23
- package/vitest.config.mjs +0 -14
package/app/views/board.js
CHANGED
|
@@ -1,40 +1,76 @@
|
|
|
1
1
|
import { html, render } from 'lit-html';
|
|
2
|
-
import {
|
|
2
|
+
import { createIssueIdRenderer } from '../utils/issue-id-renderer.js';
|
|
3
3
|
import { createPriorityBadge } from '../utils/priority-badge.js';
|
|
4
4
|
import { createTypeBadge } from '../utils/type-badge.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @typedef {{
|
|
7
|
+
* @typedef {{
|
|
8
|
+
* id: string,
|
|
9
|
+
* title?: string,
|
|
10
|
+
* status?: 'open'|'in_progress'|'closed',
|
|
11
|
+
* priority?: number,
|
|
12
|
+
* issue_type?: string,
|
|
13
|
+
* updated_at?: string,
|
|
14
|
+
* closed_at?: string
|
|
15
|
+
* }} IssueLite
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
|
-
* Create the Board view with
|
|
19
|
+
* Create the Board view with Open + Blocked stacked, then Ready, In progress, Closed.
|
|
12
20
|
* Data providers are expected to return raw arrays; this view applies sorting.
|
|
13
21
|
*
|
|
14
22
|
* Sorting rules:
|
|
15
|
-
* - Open: updated_at desc
|
|
23
|
+
* - Open: priority asc, then updated_at desc when present
|
|
16
24
|
* - Ready: priority asc, then updated_at desc when present
|
|
17
25
|
* - In progress: updated_at desc
|
|
18
|
-
* - Closed:
|
|
26
|
+
* - Closed: closed_at desc (fallback to updated_at)
|
|
19
27
|
* @param {HTMLElement} mount_element
|
|
20
|
-
* @param {{ getOpen: () => Promise<any[]>, getReady: () => Promise<any[]>, getInProgress: () => Promise<any[]>, getClosed: (limit?: number) => Promise<any[]> }} data
|
|
21
|
-
* @param {(id: string) => void}
|
|
28
|
+
* @param {{ getOpen: () => Promise<any[]>, getReady: () => Promise<any[]>, getBlocked?: () => Promise<any[]>, getInProgress: () => Promise<any[]>, getClosed: (limit?: number) => Promise<any[]> }} data
|
|
29
|
+
* @param {(id: string) => void} gotoIssue - Navigate to issue detail.
|
|
30
|
+
* @param {{ getState: () => any, setState: (patch: any) => void, subscribe?: (fn: (s:any)=>void)=>()=>void }} [store]
|
|
22
31
|
* @returns {{ load: () => Promise<void>, clear: () => void }}
|
|
23
32
|
*/
|
|
24
|
-
export function createBoardView(mount_element, data,
|
|
33
|
+
export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
25
34
|
/** @type {IssueLite[]} */
|
|
26
35
|
let list_open = [];
|
|
27
36
|
/** @type {IssueLite[]} */
|
|
28
37
|
let list_ready = [];
|
|
29
38
|
/** @type {IssueLite[]} */
|
|
39
|
+
let list_blocked = [];
|
|
40
|
+
/** @type {IssueLite[]} */
|
|
30
41
|
let list_in_progress = [];
|
|
31
42
|
/** @type {IssueLite[]} */
|
|
32
43
|
let list_closed = [];
|
|
44
|
+
/** @type {IssueLite[]} */
|
|
45
|
+
let list_closed_raw = [];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Closed column filter mode.
|
|
49
|
+
* 'today' → items with closed_at since local day start
|
|
50
|
+
* '3' → last 3 days; '7' → last 7 days
|
|
51
|
+
* @type {'today'|'3'|'7'}
|
|
52
|
+
*/
|
|
53
|
+
let closed_filter_mode = 'today';
|
|
54
|
+
if (store) {
|
|
55
|
+
try {
|
|
56
|
+
const s = store.getState();
|
|
57
|
+
const cf =
|
|
58
|
+
s && s.board ? String(s.board.closed_filter || 'today') : 'today';
|
|
59
|
+
if (cf === 'today' || cf === '3' || cf === '7') {
|
|
60
|
+
closed_filter_mode = /** @type {any} */ (cf);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore store init errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
33
66
|
|
|
34
67
|
function template() {
|
|
35
68
|
return html`
|
|
36
69
|
<div class="panel__body board-root">
|
|
37
|
-
|
|
70
|
+
<div class="board-stack-2">
|
|
71
|
+
${columnTemplate('Open', 'open-col', list_open)}
|
|
72
|
+
${columnTemplate('Blocked', 'blocked-col', list_blocked)}
|
|
73
|
+
</div>
|
|
38
74
|
${columnTemplate('Ready', 'ready-col', list_ready)}
|
|
39
75
|
${columnTemplate('In Progress', 'in-progress-col', list_in_progress)}
|
|
40
76
|
${columnTemplate('Closed', 'closed-col', list_closed)}
|
|
@@ -50,10 +86,42 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
50
86
|
function columnTemplate(title, id, items) {
|
|
51
87
|
return html`
|
|
52
88
|
<section class="board-column" id=${id}>
|
|
53
|
-
<header
|
|
54
|
-
|
|
89
|
+
<header
|
|
90
|
+
class="board-column__header"
|
|
91
|
+
id=${id + '-header'}
|
|
92
|
+
role="heading"
|
|
93
|
+
aria-level="2"
|
|
94
|
+
>
|
|
95
|
+
<span>${title}</span>
|
|
96
|
+
${id === 'closed-col'
|
|
97
|
+
? html`<label class="board-closed-filter">
|
|
98
|
+
<span class="visually-hidden">Filter closed issues</span>
|
|
99
|
+
<select
|
|
100
|
+
id="closed-filter"
|
|
101
|
+
aria-label="Filter closed issues"
|
|
102
|
+
@change=${onClosedFilterChange}
|
|
103
|
+
>
|
|
104
|
+
<option
|
|
105
|
+
value="today"
|
|
106
|
+
?selected=${closed_filter_mode === 'today'}
|
|
107
|
+
>
|
|
108
|
+
Today
|
|
109
|
+
</option>
|
|
110
|
+
<option value="3" ?selected=${closed_filter_mode === '3'}>
|
|
111
|
+
Last 3 days
|
|
112
|
+
</option>
|
|
113
|
+
<option value="7" ?selected=${closed_filter_mode === '7'}>
|
|
114
|
+
Last 7 days
|
|
115
|
+
</option>
|
|
116
|
+
</select>
|
|
117
|
+
</label>`
|
|
118
|
+
: ''}
|
|
55
119
|
</header>
|
|
56
|
-
<div
|
|
120
|
+
<div
|
|
121
|
+
class="board-column__body"
|
|
122
|
+
role="list"
|
|
123
|
+
aria-labelledby=${id + '-header'}
|
|
124
|
+
>
|
|
57
125
|
${items.map((it) => cardTemplate(it))}
|
|
58
126
|
</div>
|
|
59
127
|
</section>
|
|
@@ -68,7 +136,9 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
68
136
|
<article
|
|
69
137
|
class="board-card"
|
|
70
138
|
data-issue-id=${it.id}
|
|
71
|
-
|
|
139
|
+
role="listitem"
|
|
140
|
+
tabindex="-1"
|
|
141
|
+
@click=${() => gotoIssue(it.id)}
|
|
72
142
|
>
|
|
73
143
|
<div class="board-card__title text-truncate">
|
|
74
144
|
${it.title || '(no title)'}
|
|
@@ -76,7 +146,7 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
76
146
|
<div class="board-card__meta">
|
|
77
147
|
${createTypeBadge(/** @type {any} */ (it).issue_type)}
|
|
78
148
|
${createPriorityBadge(/** @type {any} */ (it).priority)}
|
|
79
|
-
|
|
149
|
+
${createIssueIdRenderer(it.id, { class_name: 'mono' })}
|
|
80
150
|
</div>
|
|
81
151
|
</article>
|
|
82
152
|
`;
|
|
@@ -84,6 +154,174 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
84
154
|
|
|
85
155
|
function doRender() {
|
|
86
156
|
render(template(), mount_element);
|
|
157
|
+
postRenderEnhance();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Enhance rendered board with a11y and keyboard navigation.
|
|
162
|
+
* - Roving tabindex per column (first card tabbable)
|
|
163
|
+
* - ArrowUp/ArrowDown within column
|
|
164
|
+
* - ArrowLeft/ArrowRight to adjacent non-empty column (focus top card)
|
|
165
|
+
* - Enter/Space to open details for focused card
|
|
166
|
+
*/
|
|
167
|
+
function postRenderEnhance() {
|
|
168
|
+
try {
|
|
169
|
+
/** @type {HTMLElement[]} */
|
|
170
|
+
const columns = Array.from(
|
|
171
|
+
mount_element.querySelectorAll('.board-column')
|
|
172
|
+
);
|
|
173
|
+
for (const col of columns) {
|
|
174
|
+
/** @type {HTMLElement|null} */
|
|
175
|
+
const body = /** @type {any} */ (
|
|
176
|
+
col.querySelector('.board-column__body')
|
|
177
|
+
);
|
|
178
|
+
if (!body) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
/** @type {HTMLElement[]} */
|
|
182
|
+
const cards = Array.from(body.querySelectorAll('.board-card'));
|
|
183
|
+
// Assign aria-label using column header for screen readers
|
|
184
|
+
const header = /** @type {HTMLElement|null} */ (
|
|
185
|
+
col.querySelector('.board-column__header')
|
|
186
|
+
);
|
|
187
|
+
const col_name = header ? header.textContent?.trim() || '' : '';
|
|
188
|
+
for (const card of cards) {
|
|
189
|
+
const title_el = /** @type {HTMLElement|null} */ (
|
|
190
|
+
card.querySelector('.board-card__title')
|
|
191
|
+
);
|
|
192
|
+
const t = title_el ? title_el.textContent?.trim() || '' : '';
|
|
193
|
+
card.setAttribute(
|
|
194
|
+
'aria-label',
|
|
195
|
+
`Issue ${t || '(no title)'} — Column ${col_name}`
|
|
196
|
+
);
|
|
197
|
+
// Default roving setup
|
|
198
|
+
card.tabIndex = -1;
|
|
199
|
+
}
|
|
200
|
+
if (cards.length > 0) {
|
|
201
|
+
cards[0].tabIndex = 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// non-fatal
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Delegate keyboard handling from mount_element
|
|
210
|
+
mount_element.addEventListener('keydown', (ev) => {
|
|
211
|
+
/** @type {HTMLElement} */
|
|
212
|
+
const target = /** @type {any} */ (ev.target);
|
|
213
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Do not intercept keys inside editable controls
|
|
217
|
+
const tag = String(target.tagName || '').toLowerCase();
|
|
218
|
+
if (
|
|
219
|
+
tag === 'input' ||
|
|
220
|
+
tag === 'textarea' ||
|
|
221
|
+
tag === 'select' ||
|
|
222
|
+
/** @type {any} */ (target).isContentEditable === true
|
|
223
|
+
) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const card = target.closest('.board-card');
|
|
227
|
+
if (!card) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const key = String(ev.key || '');
|
|
231
|
+
if (key === 'Enter' || key === ' ') {
|
|
232
|
+
ev.preventDefault();
|
|
233
|
+
const id = /** @type {HTMLElement} */ (card).getAttribute(
|
|
234
|
+
'data-issue-id'
|
|
235
|
+
);
|
|
236
|
+
if (id) {
|
|
237
|
+
gotoIssue(id);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
key !== 'ArrowUp' &&
|
|
243
|
+
key !== 'ArrowDown' &&
|
|
244
|
+
key !== 'ArrowLeft' &&
|
|
245
|
+
key !== 'ArrowRight'
|
|
246
|
+
) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
ev.preventDefault();
|
|
250
|
+
// Column context
|
|
251
|
+
const col = /** @type {HTMLElement|null} */ (card.closest('.board-column'));
|
|
252
|
+
if (!col) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const body = /** @type {HTMLElement|null} */ (
|
|
256
|
+
col.querySelector('.board-column__body')
|
|
257
|
+
);
|
|
258
|
+
if (!body) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
/** @type {HTMLElement[]} */
|
|
262
|
+
const cards = Array.from(body.querySelectorAll('.board-card'));
|
|
263
|
+
const idx = cards.indexOf(/** @type {HTMLElement} */ (card));
|
|
264
|
+
if (idx === -1) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (key === 'ArrowDown' && idx < cards.length - 1) {
|
|
268
|
+
moveFocus(cards[idx], cards[idx + 1]);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (key === 'ArrowUp' && idx > 0) {
|
|
272
|
+
moveFocus(cards[idx], cards[idx - 1]);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (key === 'ArrowRight' || key === 'ArrowLeft') {
|
|
276
|
+
// Find adjacent column with at least one card
|
|
277
|
+
/** @type {HTMLElement[]} */
|
|
278
|
+
const cols = Array.from(mount_element.querySelectorAll('.board-column'));
|
|
279
|
+
const col_idx = cols.indexOf(col);
|
|
280
|
+
if (col_idx === -1) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const dir = key === 'ArrowRight' ? 1 : -1;
|
|
284
|
+
let next_idx = col_idx + dir;
|
|
285
|
+
/** @type {HTMLElement|null} */
|
|
286
|
+
let target_col = null;
|
|
287
|
+
while (next_idx >= 0 && next_idx < cols.length) {
|
|
288
|
+
const candidate = cols[next_idx];
|
|
289
|
+
const c_body = /** @type {HTMLElement|null} */ (
|
|
290
|
+
candidate.querySelector('.board-column__body')
|
|
291
|
+
);
|
|
292
|
+
const c_cards = c_body
|
|
293
|
+
? Array.from(c_body.querySelectorAll('.board-card'))
|
|
294
|
+
: [];
|
|
295
|
+
if (c_cards.length > 0) {
|
|
296
|
+
target_col = candidate;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
next_idx += dir;
|
|
300
|
+
}
|
|
301
|
+
if (target_col) {
|
|
302
|
+
const first = /** @type {HTMLElement|null} */ (
|
|
303
|
+
target_col.querySelector('.board-column__body .board-card')
|
|
304
|
+
);
|
|
305
|
+
if (first) {
|
|
306
|
+
moveFocus(/** @type {HTMLElement} */ (card), first);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @param {HTMLElement} from
|
|
315
|
+
* @param {HTMLElement} to
|
|
316
|
+
*/
|
|
317
|
+
function moveFocus(from, to) {
|
|
318
|
+
try {
|
|
319
|
+
from.tabIndex = -1;
|
|
320
|
+
to.tabIndex = 0;
|
|
321
|
+
to.focus();
|
|
322
|
+
} catch {
|
|
323
|
+
// ignore focus errors
|
|
324
|
+
}
|
|
87
325
|
}
|
|
88
326
|
|
|
89
327
|
/**
|
|
@@ -116,6 +354,77 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
116
354
|
});
|
|
117
355
|
}
|
|
118
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Sort by closed_at desc with updated_at fallback.
|
|
359
|
+
* @param {IssueLite[]} arr
|
|
360
|
+
*/
|
|
361
|
+
function sortByClosedDesc(arr) {
|
|
362
|
+
arr.sort((a, b) => {
|
|
363
|
+
const ca = a.closed_at || a.updated_at || '';
|
|
364
|
+
const cb = b.closed_at || b.updated_at || '';
|
|
365
|
+
return ca < cb ? 1 : ca > cb ? -1 : 0;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Recompute closed list from raw using the current filter and sort.
|
|
371
|
+
*/
|
|
372
|
+
function applyClosedFilter() {
|
|
373
|
+
/** @type {IssueLite[]} */
|
|
374
|
+
let items = Array.isArray(list_closed_raw) ? [...list_closed_raw] : [];
|
|
375
|
+
const now = new Date();
|
|
376
|
+
/** @type {number} */
|
|
377
|
+
let since_ts = 0;
|
|
378
|
+
if (closed_filter_mode === 'today') {
|
|
379
|
+
const start = new Date(
|
|
380
|
+
now.getFullYear(),
|
|
381
|
+
now.getMonth(),
|
|
382
|
+
now.getDate(),
|
|
383
|
+
0,
|
|
384
|
+
0,
|
|
385
|
+
0,
|
|
386
|
+
0
|
|
387
|
+
);
|
|
388
|
+
since_ts = start.getTime();
|
|
389
|
+
} else if (closed_filter_mode === '3') {
|
|
390
|
+
since_ts = now.getTime() - 3 * 24 * 60 * 60 * 1000;
|
|
391
|
+
} else if (closed_filter_mode === '7') {
|
|
392
|
+
since_ts = now.getTime() - 7 * 24 * 60 * 60 * 1000;
|
|
393
|
+
}
|
|
394
|
+
items = items.filter((it) => {
|
|
395
|
+
const s = it.closed_at || '';
|
|
396
|
+
if (!s || isNaN(Date.parse(s))) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
const t = Date.parse(s);
|
|
400
|
+
return t >= since_ts;
|
|
401
|
+
});
|
|
402
|
+
sortByClosedDesc(items);
|
|
403
|
+
list_closed = items;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {Event} ev
|
|
408
|
+
*/
|
|
409
|
+
function onClosedFilterChange(ev) {
|
|
410
|
+
try {
|
|
411
|
+
const el = /** @type {HTMLSelectElement} */ (ev.target);
|
|
412
|
+
const v = String(el.value || 'today');
|
|
413
|
+
closed_filter_mode = v === '3' || v === '7' ? v : 'today';
|
|
414
|
+
if (store) {
|
|
415
|
+
try {
|
|
416
|
+
store.setState({ board: { closed_filter: closed_filter_mode } });
|
|
417
|
+
} catch {
|
|
418
|
+
// ignore store errors
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
applyClosedFilter();
|
|
422
|
+
doRender();
|
|
423
|
+
} catch {
|
|
424
|
+
// ignore
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
119
428
|
return {
|
|
120
429
|
async load() {
|
|
121
430
|
/** @type {IssueLite[]} */
|
|
@@ -123,6 +432,8 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
123
432
|
/** @type {IssueLite[]} */
|
|
124
433
|
let r = [];
|
|
125
434
|
/** @type {IssueLite[]} */
|
|
435
|
+
let b = [];
|
|
436
|
+
/** @type {IssueLite[]} */
|
|
126
437
|
let p = [];
|
|
127
438
|
/** @type {IssueLite[]} */
|
|
128
439
|
let c = [];
|
|
@@ -136,6 +447,13 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
136
447
|
} catch {
|
|
137
448
|
r = [];
|
|
138
449
|
}
|
|
450
|
+
try {
|
|
451
|
+
// getBlocked is optional for backward compatibility in tests
|
|
452
|
+
const fn = /** @type {any} */ (data).getBlocked;
|
|
453
|
+
b = typeof fn === 'function' ? /** @type {any} */ (await fn()) : [];
|
|
454
|
+
} catch {
|
|
455
|
+
b = [];
|
|
456
|
+
}
|
|
139
457
|
try {
|
|
140
458
|
p = /** @type {any} */ (await data.getInProgress());
|
|
141
459
|
} catch {
|
|
@@ -154,6 +472,13 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
154
472
|
o = o.filter((it) => !ready_ids.has(it.id));
|
|
155
473
|
}
|
|
156
474
|
|
|
475
|
+
// Remove items from Open that are blocked (UI-121)
|
|
476
|
+
if (o.length > 0 && b.length > 0) {
|
|
477
|
+
/** @type {Set<string>} */
|
|
478
|
+
const blocked_ids = new Set(b.map((it) => it.id));
|
|
479
|
+
o = o.filter((it) => !blocked_ids.has(it.id));
|
|
480
|
+
}
|
|
481
|
+
|
|
157
482
|
// Remove items from Ready that are already In Progress by id
|
|
158
483
|
if (r.length > 0 && p.length > 0) {
|
|
159
484
|
/** @type {Set<string>} */
|
|
@@ -161,21 +486,26 @@ export function createBoardView(mount_element, data, goto_issue) {
|
|
|
161
486
|
r = r.filter((it) => !in_progress_ids.has(it.id));
|
|
162
487
|
}
|
|
163
488
|
|
|
164
|
-
|
|
489
|
+
// UI-119: Open sorted like Ready (priority asc, then updated desc)
|
|
490
|
+
sortReady(o);
|
|
165
491
|
sortReady(r);
|
|
492
|
+
sortReady(b);
|
|
166
493
|
sortByUpdatedDesc(p);
|
|
167
|
-
|
|
494
|
+
// Closed handled separately to use closed_at and filtering
|
|
168
495
|
|
|
169
496
|
list_open = o;
|
|
170
497
|
list_ready = r;
|
|
498
|
+
list_blocked = b;
|
|
171
499
|
list_in_progress = p;
|
|
172
|
-
|
|
500
|
+
list_closed_raw = c;
|
|
501
|
+
applyClosedFilter();
|
|
173
502
|
doRender();
|
|
174
503
|
},
|
|
175
504
|
clear() {
|
|
176
505
|
mount_element.replaceChildren();
|
|
177
506
|
list_open = [];
|
|
178
507
|
list_ready = [];
|
|
508
|
+
list_blocked = [];
|
|
179
509
|
list_in_progress = [];
|
|
180
510
|
list_closed = [];
|
|
181
511
|
}
|