beads-ui 0.2.0 → 0.3.1
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 +14 -0
- package/README.md +4 -4
- package/app/data/list-selectors.js +103 -0
- package/app/data/providers.js +7 -138
- package/app/data/sort.js +47 -0
- package/app/data/subscription-issue-store.js +161 -0
- package/app/data/subscription-issue-stores.js +128 -0
- package/app/data/subscriptions-store.js +227 -0
- package/app/main.js +346 -66
- package/app/protocol.js +23 -17
- package/app/protocol.md +18 -15
- package/app/router.js +3 -0
- package/app/state.js +2 -0
- package/app/styles.css +222 -197
- package/app/utils/issue-id-renderer.js +2 -1
- package/app/utils/issue-id.js +1 -0
- package/app/utils/issue-type.js +2 -0
- package/app/utils/issue-url.js +1 -0
- package/app/utils/markdown.js +13 -198
- package/app/utils/priority-badge.js +1 -2
- package/app/utils/status-badge.js +1 -1
- package/app/utils/status.js +2 -0
- package/app/utils/toast.js +1 -1
- package/app/utils/type-badge.js +1 -3
- package/app/views/board.js +172 -148
- package/app/views/detail.js +79 -66
- package/app/views/epics.js +127 -74
- package/app/views/issue-dialog.js +9 -15
- package/app/views/issue-row.js +2 -3
- package/app/views/list.js +105 -104
- package/app/views/nav.js +1 -0
- package/app/views/new-issue-dialog.js +30 -34
- package/app/ws.js +10 -10
- package/bin/bdui.js +1 -1
- package/docs/adr/001-push-only-lists.md +134 -0
- package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +200 -0
- package/docs/architecture.md +34 -84
- package/docs/data-exchange-subscription-plan.md +198 -0
- package/docs/db-watching.md +2 -1
- package/docs/migration-v2.md +54 -0
- package/docs/protocol/issues-push-v2.md +179 -0
- package/docs/subscription-issue-store.md +112 -0
- package/package.json +5 -4
- package/server/app.js +2 -0
- package/server/bd.js +4 -2
- package/server/cli/commands.js +5 -2
- package/server/cli/daemon.js +19 -5
- package/server/cli/index.js +2 -2
- package/server/cli/open.js +3 -0
- package/server/cli/usage.js +2 -1
- package/server/config.js +13 -6
- package/server/db.js +3 -1
- package/server/index.js +9 -5
- package/server/list-adapters.js +224 -0
- package/server/subscriptions.js +289 -0
- package/server/validators.js +113 -0
- package/server/watcher.js +8 -8
- package/server/ws.js +457 -229
package/app/views/board.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { html, render } from 'lit-html';
|
|
2
|
+
import { createListSelectors } from '../data/list-selectors.js';
|
|
3
|
+
import { cmpClosedDesc, cmpPriorityThenCreated } from '../data/sort.js';
|
|
2
4
|
import { createIssueIdRenderer } from '../utils/issue-id-renderer.js';
|
|
3
5
|
import { createPriorityBadge } from '../utils/priority-badge.js';
|
|
4
6
|
import { createTypeBadge } from '../utils/type-badge.js';
|
|
@@ -10,29 +12,36 @@ import { createTypeBadge } from '../utils/type-badge.js';
|
|
|
10
12
|
* status?: 'open'|'in_progress'|'closed',
|
|
11
13
|
* priority?: number,
|
|
12
14
|
* issue_type?: string,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* created_at?: number,
|
|
16
|
+
* updated_at?: number,
|
|
17
|
+
* closed_at?: number
|
|
15
18
|
* }} IssueLite
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
|
-
* Create the Board view with
|
|
20
|
-
*
|
|
22
|
+
* Create the Board view with Blocked, Ready, In progress, Closed.
|
|
23
|
+
* Push-only: derives items from per-subscription stores.
|
|
21
24
|
*
|
|
22
25
|
* Sorting rules:
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
25
|
-
*
|
|
26
|
-
* - Closed: closed_at desc (fallback to updated_at)
|
|
26
|
+
* - Ready/Blocked/In progress: priority asc, then created_at asc.
|
|
27
|
+
* - Closed: closed_at desc.
|
|
28
|
+
*
|
|
27
29
|
* @param {HTMLElement} mount_element
|
|
28
|
-
* @param {
|
|
30
|
+
* @param {unknown} _data - Unused (legacy param retained for call-compat)
|
|
29
31
|
* @param {(id: string) => void} gotoIssue - Navigate to issue detail.
|
|
30
32
|
* @param {{ getState: () => any, setState: (patch: any) => void, subscribe?: (fn: (s:any)=>void)=>()=>void }} [store]
|
|
33
|
+
* @param {{ selectors: { getIds: (client_id: string) => string[], count?: (client_id: string) => number } }} [subscriptions]
|
|
34
|
+
* @param {{ snapshotFor?: (client_id: string) => any[], subscribe?: (fn: () => void) => () => void }} [issueStores]
|
|
31
35
|
* @returns {{ load: () => Promise<void>, clear: () => void }}
|
|
32
36
|
*/
|
|
33
|
-
export function createBoardView(
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
export function createBoardView(
|
|
38
|
+
mount_element,
|
|
39
|
+
_data,
|
|
40
|
+
gotoIssue,
|
|
41
|
+
store,
|
|
42
|
+
subscriptions = undefined,
|
|
43
|
+
issueStores = undefined
|
|
44
|
+
) {
|
|
36
45
|
/** @type {IssueLite[]} */
|
|
37
46
|
let list_ready = [];
|
|
38
47
|
/** @type {IssueLite[]} */
|
|
@@ -43,11 +52,14 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
43
52
|
let list_closed = [];
|
|
44
53
|
/** @type {IssueLite[]} */
|
|
45
54
|
let list_closed_raw = [];
|
|
55
|
+
// Centralized selection helpers
|
|
56
|
+
const selectors = issueStores ? createListSelectors(issueStores) : null;
|
|
46
57
|
|
|
47
58
|
/**
|
|
48
59
|
* Closed column filter mode.
|
|
49
60
|
* 'today' → items with closed_at since local day start
|
|
50
61
|
* '3' → last 3 days; '7' → last 7 days
|
|
62
|
+
*
|
|
51
63
|
* @type {'today'|'3'|'7'}
|
|
52
64
|
*/
|
|
53
65
|
let closed_filter_mode = 'today';
|
|
@@ -67,10 +79,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
67
79
|
function template() {
|
|
68
80
|
return html`
|
|
69
81
|
<div class="panel__body board-root">
|
|
70
|
-
|
|
71
|
-
${columnTemplate('Open', 'open-col', list_open)}
|
|
72
|
-
${columnTemplate('Blocked', 'blocked-col', list_blocked)}
|
|
73
|
-
</div>
|
|
82
|
+
${columnTemplate('Blocked', 'blocked-col', list_blocked)}
|
|
74
83
|
${columnTemplate('Ready', 'ready-col', list_ready)}
|
|
75
84
|
${columnTemplate('In Progress', 'in-progress-col', list_in_progress)}
|
|
76
85
|
${columnTemplate('Closed', 'closed-col', list_closed)}
|
|
@@ -144,8 +153,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
144
153
|
${it.title || '(no title)'}
|
|
145
154
|
</div>
|
|
146
155
|
<div class="board-card__meta">
|
|
147
|
-
${createTypeBadge(
|
|
148
|
-
${createPriorityBadge(/** @type {any} */ (it).priority)}
|
|
156
|
+
${createTypeBadge(it.issue_type)} ${createPriorityBadge(it.priority)}
|
|
149
157
|
${createIssueIdRenderer(it.id, { class_name: 'mono' })}
|
|
150
158
|
</div>
|
|
151
159
|
</article>
|
|
@@ -159,10 +167,10 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
159
167
|
|
|
160
168
|
/**
|
|
161
169
|
* 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
|
|
170
|
+
* - Roving tabindex per column (first card tabbable).
|
|
171
|
+
* - ArrowUp/ArrowDown within column.
|
|
172
|
+
* - ArrowLeft/ArrowRight to adjacent non-empty column (focus top card).
|
|
173
|
+
* - Enter/Space to open details for focused card.
|
|
166
174
|
*/
|
|
167
175
|
function postRenderEnhance() {
|
|
168
176
|
try {
|
|
@@ -171,8 +179,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
171
179
|
mount_element.querySelectorAll('.board-column')
|
|
172
180
|
);
|
|
173
181
|
for (const col of columns) {
|
|
174
|
-
/** @type {HTMLElement|null} */
|
|
175
|
-
const body = /** @type {any} */ (
|
|
182
|
+
const body = /** @type {HTMLElement|null} */ (
|
|
176
183
|
col.querySelector('.board-column__body')
|
|
177
184
|
);
|
|
178
185
|
if (!body) {
|
|
@@ -208,8 +215,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
208
215
|
|
|
209
216
|
// Delegate keyboard handling from mount_element
|
|
210
217
|
mount_element.addEventListener('keydown', (ev) => {
|
|
211
|
-
|
|
212
|
-
const target = /** @type {any} */ (ev.target);
|
|
218
|
+
const target = ev.target;
|
|
213
219
|
if (!target || !(target instanceof HTMLElement)) {
|
|
214
220
|
return;
|
|
215
221
|
}
|
|
@@ -219,7 +225,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
219
225
|
tag === 'input' ||
|
|
220
226
|
tag === 'textarea' ||
|
|
221
227
|
tag === 'select' ||
|
|
222
|
-
|
|
228
|
+
target.isContentEditable === true
|
|
223
229
|
) {
|
|
224
230
|
return;
|
|
225
231
|
}
|
|
@@ -230,9 +236,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
230
236
|
const key = String(ev.key || '');
|
|
231
237
|
if (key === 'Enter' || key === ' ') {
|
|
232
238
|
ev.preventDefault();
|
|
233
|
-
const id =
|
|
234
|
-
'data-issue-id'
|
|
235
|
-
);
|
|
239
|
+
const id = card.getAttribute('data-issue-id');
|
|
236
240
|
if (id) {
|
|
237
241
|
gotoIssue(id);
|
|
238
242
|
}
|
|
@@ -252,9 +256,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
252
256
|
if (!col) {
|
|
253
257
|
return;
|
|
254
258
|
}
|
|
255
|
-
const body =
|
|
256
|
-
col.querySelector('.board-column__body')
|
|
257
|
-
);
|
|
259
|
+
const body = col.querySelector('.board-column__body');
|
|
258
260
|
if (!body) {
|
|
259
261
|
return;
|
|
260
262
|
}
|
|
@@ -324,47 +326,7 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
|
|
328
|
-
* Sort helpers.
|
|
329
|
-
*/
|
|
330
|
-
/**
|
|
331
|
-
* @param {IssueLite[]} arr
|
|
332
|
-
*/
|
|
333
|
-
function sortReady(arr) {
|
|
334
|
-
arr.sort((a, b) => {
|
|
335
|
-
const pa = a.priority ?? 2;
|
|
336
|
-
const pb = b.priority ?? 2;
|
|
337
|
-
if (pa !== pb) {
|
|
338
|
-
return pa - pb;
|
|
339
|
-
}
|
|
340
|
-
const ua = a.updated_at || '';
|
|
341
|
-
const ub = b.updated_at || '';
|
|
342
|
-
return ua < ub ? 1 : ua > ub ? -1 : 0;
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* @param {IssueLite[]} arr
|
|
348
|
-
*/
|
|
349
|
-
function sortByUpdatedDesc(arr) {
|
|
350
|
-
arr.sort((a, b) => {
|
|
351
|
-
const ua = a.updated_at || '';
|
|
352
|
-
const ub = b.updated_at || '';
|
|
353
|
-
return ua < ub ? 1 : ua > ub ? -1 : 0;
|
|
354
|
-
});
|
|
355
|
-
}
|
|
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
|
-
}
|
|
329
|
+
// Sort helpers centralized in app/data/sort.js
|
|
368
330
|
|
|
369
331
|
/**
|
|
370
332
|
* Recompute closed list from raw using the current filter and sort.
|
|
@@ -373,7 +335,6 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
373
335
|
/** @type {IssueLite[]} */
|
|
374
336
|
let items = Array.isArray(list_closed_raw) ? [...list_closed_raw] : [];
|
|
375
337
|
const now = new Date();
|
|
376
|
-
/** @type {number} */
|
|
377
338
|
let since_ts = 0;
|
|
378
339
|
if (closed_filter_mode === 'today') {
|
|
379
340
|
const start = new Date(
|
|
@@ -392,14 +353,15 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
392
353
|
since_ts = now.getTime() - 7 * 24 * 60 * 60 * 1000;
|
|
393
354
|
}
|
|
394
355
|
items = items.filter((it) => {
|
|
395
|
-
const s = it.closed_at
|
|
396
|
-
|
|
356
|
+
const s = Number.isFinite(it.closed_at)
|
|
357
|
+
? /** @type {number} */ (it.closed_at)
|
|
358
|
+
: NaN;
|
|
359
|
+
if (!Number.isFinite(s)) {
|
|
397
360
|
return false;
|
|
398
361
|
}
|
|
399
|
-
|
|
400
|
-
return t >= since_ts;
|
|
362
|
+
return s >= since_ts;
|
|
401
363
|
});
|
|
402
|
-
|
|
364
|
+
items.sort(cmpClosedDesc);
|
|
403
365
|
list_closed = items;
|
|
404
366
|
}
|
|
405
367
|
|
|
@@ -425,85 +387,147 @@ export function createBoardView(mount_element, data, gotoIssue, store) {
|
|
|
425
387
|
}
|
|
426
388
|
}
|
|
427
389
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
p = /** @type {any} */ (await data.getInProgress());
|
|
459
|
-
} catch {
|
|
460
|
-
p = [];
|
|
461
|
-
}
|
|
462
|
-
try {
|
|
463
|
-
c = /** @type {any} */ (await data.getClosed());
|
|
464
|
-
} catch {
|
|
465
|
-
c = [];
|
|
466
|
-
}
|
|
390
|
+
/**
|
|
391
|
+
* Compose lists from subscriptions + issues store and render.
|
|
392
|
+
*/
|
|
393
|
+
function refreshFromStores() {
|
|
394
|
+
try {
|
|
395
|
+
if (selectors) {
|
|
396
|
+
const in_progress = selectors.selectBoardColumn(
|
|
397
|
+
'tab:board:in-progress',
|
|
398
|
+
'in_progress'
|
|
399
|
+
);
|
|
400
|
+
const blocked = selectors.selectBoardColumn(
|
|
401
|
+
'tab:board:blocked',
|
|
402
|
+
'blocked'
|
|
403
|
+
);
|
|
404
|
+
const ready_raw = selectors.selectBoardColumn(
|
|
405
|
+
'tab:board:ready',
|
|
406
|
+
'ready'
|
|
407
|
+
);
|
|
408
|
+
const closed = selectors.selectBoardColumn(
|
|
409
|
+
'tab:board:closed',
|
|
410
|
+
'closed'
|
|
411
|
+
);
|
|
467
412
|
|
|
468
|
-
|
|
469
|
-
if (o.length > 0 && r.length > 0) {
|
|
413
|
+
// Ready excludes items that are in progress
|
|
470
414
|
/** @type {Set<string>} */
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
}
|
|
415
|
+
const in_prog_ids = new Set(in_progress.map((i) => i.id));
|
|
416
|
+
const ready = ready_raw.filter((i) => !in_prog_ids.has(i.id));
|
|
474
417
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
o = o.filter((it) => !blocked_ids.has(it.id));
|
|
418
|
+
list_ready = ready;
|
|
419
|
+
list_blocked = blocked;
|
|
420
|
+
list_in_progress = in_progress;
|
|
421
|
+
list_closed_raw = closed;
|
|
480
422
|
}
|
|
423
|
+
applyClosedFilter();
|
|
424
|
+
doRender();
|
|
425
|
+
} catch {
|
|
426
|
+
list_ready = [];
|
|
427
|
+
list_blocked = [];
|
|
428
|
+
list_in_progress = [];
|
|
429
|
+
list_closed = [];
|
|
430
|
+
doRender();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
481
433
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
434
|
+
// Live updates: recompose on issue store envelopes
|
|
435
|
+
if (selectors) {
|
|
436
|
+
selectors.subscribe(() => {
|
|
437
|
+
try {
|
|
438
|
+
refreshFromStores();
|
|
439
|
+
} catch {
|
|
440
|
+
// ignore
|
|
487
441
|
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
488
444
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
//
|
|
445
|
+
return {
|
|
446
|
+
async load() {
|
|
447
|
+
// Compose lists from subscriptions + issues store
|
|
448
|
+
refreshFromStores();
|
|
449
|
+
// If nothing is present yet (e.g., immediately after switching back
|
|
450
|
+
// to the Board and before list-delta arrives), fetch via data layer as
|
|
451
|
+
// a fallback so the board is not empty on initial display.
|
|
452
|
+
try {
|
|
453
|
+
const has_subs = Boolean(subscriptions && subscriptions.selectors);
|
|
454
|
+
/**
|
|
455
|
+
* @param {string} id
|
|
456
|
+
*/
|
|
457
|
+
const cnt = (id) => {
|
|
458
|
+
if (!has_subs || !subscriptions) {
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
const sel = subscriptions.selectors;
|
|
462
|
+
if (typeof sel.count === 'function') {
|
|
463
|
+
return Number(sel.count(id) || 0);
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const arr = sel.getIds(id);
|
|
467
|
+
return Array.isArray(arr) ? arr.length : 0;
|
|
468
|
+
} catch {
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
const total_items =
|
|
473
|
+
cnt('tab:board:ready') +
|
|
474
|
+
cnt('tab:board:blocked') +
|
|
475
|
+
cnt('tab:board:in-progress') +
|
|
476
|
+
cnt('tab:board:closed');
|
|
477
|
+
const data = /** @type {any} */ (_data);
|
|
478
|
+
const can_fetch =
|
|
479
|
+
data &&
|
|
480
|
+
typeof data.getReady === 'function' &&
|
|
481
|
+
typeof data.getBlocked === 'function' &&
|
|
482
|
+
typeof data.getInProgress === 'function' &&
|
|
483
|
+
typeof data.getClosed === 'function';
|
|
484
|
+
if (total_items === 0 && can_fetch) {
|
|
485
|
+
/** @type {[IssueLite[], IssueLite[], IssueLite[], IssueLite[]]} */
|
|
486
|
+
const [ready_raw, blocked_raw, in_prog_raw, closed_raw] =
|
|
487
|
+
await Promise.all([
|
|
488
|
+
data.getReady().catch(() => []),
|
|
489
|
+
data.getBlocked().catch(() => []),
|
|
490
|
+
data.getInProgress().catch(() => []),
|
|
491
|
+
data.getClosed().catch(() => [])
|
|
492
|
+
]);
|
|
493
|
+
// Normalize and map unknowns to IssueLite shape
|
|
494
|
+
/** @type {IssueLite[]} */
|
|
495
|
+
let ready = Array.isArray(ready_raw) ? ready_raw.map((it) => it) : [];
|
|
496
|
+
/** @type {IssueLite[]} */
|
|
497
|
+
const blocked = Array.isArray(blocked_raw)
|
|
498
|
+
? blocked_raw.map((it) => it)
|
|
499
|
+
: [];
|
|
500
|
+
/** @type {IssueLite[]} */
|
|
501
|
+
const in_prog = Array.isArray(in_prog_raw)
|
|
502
|
+
? in_prog_raw.map((it) => it)
|
|
503
|
+
: [];
|
|
504
|
+
/** @type {IssueLite[]} */
|
|
505
|
+
const closed = Array.isArray(closed_raw)
|
|
506
|
+
? closed_raw.map((it) => it)
|
|
507
|
+
: [];
|
|
495
508
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
509
|
+
// Remove items from Ready that are already In Progress
|
|
510
|
+
/** @type {Set<string>} */
|
|
511
|
+
const in_progress_ids = new Set(in_prog.map((i) => i.id));
|
|
512
|
+
ready = ready.filter((i) => !in_progress_ids.has(i.id));
|
|
513
|
+
|
|
514
|
+
// Sort as per column rules
|
|
515
|
+
ready.sort(cmpPriorityThenCreated);
|
|
516
|
+
blocked.sort(cmpPriorityThenCreated);
|
|
517
|
+
in_prog.sort(cmpPriorityThenCreated);
|
|
518
|
+
list_ready = ready;
|
|
519
|
+
list_blocked = blocked;
|
|
520
|
+
list_in_progress = in_prog;
|
|
521
|
+
list_closed_raw = closed;
|
|
522
|
+
applyClosedFilter();
|
|
523
|
+
doRender();
|
|
524
|
+
}
|
|
525
|
+
} catch {
|
|
526
|
+
// ignore fallback errors
|
|
527
|
+
}
|
|
503
528
|
},
|
|
504
529
|
clear() {
|
|
505
530
|
mount_element.replaceChildren();
|
|
506
|
-
list_open = [];
|
|
507
531
|
list_ready = [];
|
|
508
532
|
list_blocked = [];
|
|
509
533
|
list_in_progress = [];
|