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/detail.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Issue Detail view implementation (lit-html based)
|
|
2
2
|
import { html, render } from 'lit-html';
|
|
3
|
+
import { parseView } from '../router.js';
|
|
3
4
|
import { issueDisplayId } from '../utils/issue-id.js';
|
|
5
|
+
import { issueHashFor } from '../utils/issue-url.js';
|
|
4
6
|
import { renderMarkdown } from '../utils/markdown.js';
|
|
5
7
|
import { emojiForPriority } from '../utils/priority-badge.js';
|
|
6
8
|
import { priority_levels } from '../utils/priority.js';
|
|
7
9
|
import { statusLabel } from '../utils/status.js';
|
|
10
|
+
import { showToast } from '../utils/toast.js';
|
|
8
11
|
import { createTypeBadge } from '../utils/type-badge.js';
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -21,6 +24,7 @@ import { createTypeBadge } from '../utils/type-badge.js';
|
|
|
21
24
|
* @property {string} id
|
|
22
25
|
* @property {string} [title]
|
|
23
26
|
* @property {string} [description]
|
|
27
|
+
* @property {string} [design]
|
|
24
28
|
* @property {string} [acceptance]
|
|
25
29
|
* @property {string} [notes]
|
|
26
30
|
* @property {string} [status]
|
|
@@ -58,42 +62,25 @@ export function createDetailView(
|
|
|
58
62
|
/** @type {boolean} */
|
|
59
63
|
let edit_desc = false;
|
|
60
64
|
/** @type {boolean} */
|
|
65
|
+
let edit_design = false;
|
|
66
|
+
/** @type {boolean} */
|
|
67
|
+
let edit_notes = false;
|
|
68
|
+
/** @type {boolean} */
|
|
61
69
|
let edit_accept = false;
|
|
62
70
|
/** @type {boolean} */
|
|
63
71
|
let edit_assignee = false;
|
|
64
72
|
/** @type {string} */
|
|
65
73
|
let new_label_text = '';
|
|
66
74
|
|
|
67
|
-
/**
|
|
68
|
-
* Show a transient toast message.
|
|
69
|
-
* @param {string} text
|
|
70
|
-
*/
|
|
71
|
-
function showToast(text) {
|
|
72
|
-
/** @type {HTMLDivElement} */
|
|
73
|
-
const toast = document.createElement('div');
|
|
74
|
-
toast.className = 'toast';
|
|
75
|
-
toast.textContent = text;
|
|
76
|
-
toast.style.position = 'absolute';
|
|
77
|
-
toast.style.right = '12px';
|
|
78
|
-
toast.style.bottom = '12px';
|
|
79
|
-
toast.style.background = 'rgba(0,0,0,0.8)';
|
|
80
|
-
toast.style.color = '#fff';
|
|
81
|
-
toast.style.padding = '8px 10px';
|
|
82
|
-
toast.style.borderRadius = '4px';
|
|
83
|
-
toast.style.fontSize = '12px';
|
|
84
|
-
mount_element.appendChild(toast);
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
try {
|
|
87
|
-
toast.remove();
|
|
88
|
-
} catch {
|
|
89
|
-
/* ignore */
|
|
90
|
-
}
|
|
91
|
-
}, 2800);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
75
|
/** @param {string} id */
|
|
95
76
|
function issueHref(id) {
|
|
96
|
-
|
|
77
|
+
try {
|
|
78
|
+
/** @type {'issues'|'epics'|'board'} */
|
|
79
|
+
const view = parseView(window.location.hash || '');
|
|
80
|
+
return issueHashFor(view, id);
|
|
81
|
+
} catch {
|
|
82
|
+
return issueHashFor('issues', id);
|
|
83
|
+
}
|
|
97
84
|
}
|
|
98
85
|
|
|
99
86
|
/**
|
|
@@ -159,7 +146,7 @@ export function createDetailView(
|
|
|
159
146
|
current.title = prev;
|
|
160
147
|
edit_title = false;
|
|
161
148
|
doRender();
|
|
162
|
-
showToast('Failed to save title');
|
|
149
|
+
showToast('Failed to save title', 'error');
|
|
163
150
|
} finally {
|
|
164
151
|
pending = false;
|
|
165
152
|
}
|
|
@@ -223,7 +210,7 @@ export function createDetailView(
|
|
|
223
210
|
/** @type {any} */ (current).assignee = prev;
|
|
224
211
|
edit_assignee = false;
|
|
225
212
|
doRender();
|
|
226
|
-
showToast('Failed to update assignee');
|
|
213
|
+
showToast('Failed to update assignee', 'error');
|
|
227
214
|
} finally {
|
|
228
215
|
pending = false;
|
|
229
216
|
}
|
|
@@ -271,7 +258,7 @@ export function createDetailView(
|
|
|
271
258
|
doRender();
|
|
272
259
|
}
|
|
273
260
|
} catch {
|
|
274
|
-
showToast('Failed to add label');
|
|
261
|
+
showToast('Failed to add label', 'error');
|
|
275
262
|
} finally {
|
|
276
263
|
pending = false;
|
|
277
264
|
}
|
|
@@ -294,7 +281,7 @@ export function createDetailView(
|
|
|
294
281
|
doRender();
|
|
295
282
|
}
|
|
296
283
|
} catch {
|
|
297
|
-
showToast('Failed to remove label');
|
|
284
|
+
showToast('Failed to remove label', 'error');
|
|
298
285
|
} finally {
|
|
299
286
|
pending = false;
|
|
300
287
|
}
|
|
@@ -329,7 +316,7 @@ export function createDetailView(
|
|
|
329
316
|
} catch {
|
|
330
317
|
current.status = prev;
|
|
331
318
|
doRender();
|
|
332
|
-
showToast('Failed to update status');
|
|
319
|
+
showToast('Failed to update status', 'error');
|
|
333
320
|
} finally {
|
|
334
321
|
pending = false;
|
|
335
322
|
}
|
|
@@ -364,7 +351,7 @@ export function createDetailView(
|
|
|
364
351
|
} catch {
|
|
365
352
|
current.priority = prev;
|
|
366
353
|
doRender();
|
|
367
|
-
showToast('Failed to update priority');
|
|
354
|
+
showToast('Failed to update priority', 'error');
|
|
368
355
|
} finally {
|
|
369
356
|
pending = false;
|
|
370
357
|
}
|
|
@@ -427,7 +414,7 @@ export function createDetailView(
|
|
|
427
414
|
current.description = prev;
|
|
428
415
|
edit_desc = false;
|
|
429
416
|
doRender();
|
|
430
|
-
showToast('Failed to save description');
|
|
417
|
+
showToast('Failed to save description', 'error');
|
|
431
418
|
} finally {
|
|
432
419
|
pending = false;
|
|
433
420
|
}
|
|
@@ -437,6 +424,150 @@ export function createDetailView(
|
|
|
437
424
|
doRender();
|
|
438
425
|
};
|
|
439
426
|
|
|
427
|
+
// Design inline edit handlers (same UX as Description)
|
|
428
|
+
const onDesignEdit = () => {
|
|
429
|
+
edit_design = true;
|
|
430
|
+
doRender();
|
|
431
|
+
try {
|
|
432
|
+
const ta = /** @type {HTMLTextAreaElement|null} */ (
|
|
433
|
+
mount_element.querySelector('#detail-root .design textarea')
|
|
434
|
+
);
|
|
435
|
+
if (ta) {
|
|
436
|
+
ta.focus();
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
// ignore focus errors
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* @param {KeyboardEvent} ev
|
|
444
|
+
*/
|
|
445
|
+
const onDesignKeydown = (ev) => {
|
|
446
|
+
if (ev.key === 'Escape') {
|
|
447
|
+
edit_design = false;
|
|
448
|
+
doRender();
|
|
449
|
+
} else if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) {
|
|
450
|
+
const btn = /** @type {HTMLButtonElement|null} */ (
|
|
451
|
+
mount_element.querySelector(
|
|
452
|
+
'#detail-root .design .editable-actions button'
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
if (btn) {
|
|
456
|
+
btn.click();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const onDesignSave = async () => {
|
|
461
|
+
if (!current || pending) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
/** @type {HTMLTextAreaElement|null} */
|
|
465
|
+
const ta = /** @type {any} */ (
|
|
466
|
+
mount_element.querySelector('#detail-root .design textarea')
|
|
467
|
+
);
|
|
468
|
+
const prev = current.design || '';
|
|
469
|
+
const next = ta ? ta.value : '';
|
|
470
|
+
if (next === prev) {
|
|
471
|
+
edit_design = false;
|
|
472
|
+
doRender();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
pending = true;
|
|
476
|
+
if (ta) {
|
|
477
|
+
ta.disabled = true;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const updated = await sendFn('edit-text', {
|
|
481
|
+
id: current.id,
|
|
482
|
+
field: 'design',
|
|
483
|
+
value: next
|
|
484
|
+
});
|
|
485
|
+
if (updated && typeof updated === 'object') {
|
|
486
|
+
current = /** @type {IssueDetail} */ (updated);
|
|
487
|
+
edit_design = false;
|
|
488
|
+
doRender();
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
current.design = prev;
|
|
492
|
+
edit_design = false;
|
|
493
|
+
doRender();
|
|
494
|
+
showToast('Failed to save design', 'error');
|
|
495
|
+
} finally {
|
|
496
|
+
pending = false;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const onDesignCancel = () => {
|
|
500
|
+
edit_design = false;
|
|
501
|
+
doRender();
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Notes inline edit handlers
|
|
505
|
+
const onNotesEdit = () => {
|
|
506
|
+
edit_notes = true;
|
|
507
|
+
doRender();
|
|
508
|
+
};
|
|
509
|
+
/**
|
|
510
|
+
* @param {KeyboardEvent} ev
|
|
511
|
+
*/
|
|
512
|
+
const onNotesKeydown = (ev) => {
|
|
513
|
+
if (ev.key === 'Escape') {
|
|
514
|
+
edit_notes = false;
|
|
515
|
+
doRender();
|
|
516
|
+
} else if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) {
|
|
517
|
+
const btn = /** @type {HTMLButtonElement|null} */ (
|
|
518
|
+
mount_element.querySelector(
|
|
519
|
+
'#detail-root .notes .editable-actions button'
|
|
520
|
+
)
|
|
521
|
+
);
|
|
522
|
+
if (btn) {
|
|
523
|
+
btn.click();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
const onNotesSave = async () => {
|
|
528
|
+
if (!current || pending) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
/** @type {HTMLTextAreaElement|null} */
|
|
532
|
+
const ta = /** @type {any} */ (
|
|
533
|
+
mount_element.querySelector('#detail-root .notes textarea')
|
|
534
|
+
);
|
|
535
|
+
const prev = current.notes || '';
|
|
536
|
+
const next = ta ? ta.value : '';
|
|
537
|
+
if (next === prev) {
|
|
538
|
+
edit_notes = false;
|
|
539
|
+
doRender();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
pending = true;
|
|
543
|
+
if (ta) {
|
|
544
|
+
ta.disabled = true;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const updated = await sendFn('edit-text', {
|
|
548
|
+
id: current.id,
|
|
549
|
+
field: 'notes',
|
|
550
|
+
value: next
|
|
551
|
+
});
|
|
552
|
+
if (updated && typeof updated === 'object') {
|
|
553
|
+
current = /** @type {IssueDetail} */ (updated);
|
|
554
|
+
edit_notes = false;
|
|
555
|
+
doRender();
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
current.notes = prev;
|
|
559
|
+
edit_notes = false;
|
|
560
|
+
doRender();
|
|
561
|
+
showToast('Failed to save notes', 'error');
|
|
562
|
+
} finally {
|
|
563
|
+
pending = false;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
const onNotesCancel = () => {
|
|
567
|
+
edit_notes = false;
|
|
568
|
+
doRender();
|
|
569
|
+
};
|
|
570
|
+
|
|
440
571
|
const onAcceptEdit = () => {
|
|
441
572
|
edit_accept = true;
|
|
442
573
|
doRender();
|
|
@@ -448,10 +579,7 @@ export function createDetailView(
|
|
|
448
579
|
if (ev.key === 'Escape') {
|
|
449
580
|
edit_accept = false;
|
|
450
581
|
doRender();
|
|
451
|
-
} else if (
|
|
452
|
-
ev.key === 'Enter' &&
|
|
453
|
-
/** @type {KeyboardEvent} */ (ev).ctrlKey
|
|
454
|
-
) {
|
|
582
|
+
} else if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) {
|
|
455
583
|
const btn = /** @type {HTMLButtonElement|null} */ (
|
|
456
584
|
mount_element.querySelector(
|
|
457
585
|
'#detail-root .acceptance .editable-actions button'
|
|
@@ -496,7 +624,7 @@ export function createDetailView(
|
|
|
496
624
|
current.acceptance = prev;
|
|
497
625
|
edit_accept = false;
|
|
498
626
|
doRender();
|
|
499
|
-
showToast('Failed to save acceptance');
|
|
627
|
+
showToast('Failed to save acceptance', 'error');
|
|
500
628
|
} finally {
|
|
501
629
|
pending = false;
|
|
502
630
|
}
|
|
@@ -524,13 +652,10 @@ export function createDetailView(
|
|
|
524
652
|
: items.map((dep) => {
|
|
525
653
|
const did = dep.id;
|
|
526
654
|
const href = issueHref(did);
|
|
527
|
-
return html
|
|
528
|
-
|
|
655
|
+
return html`<li
|
|
656
|
+
data-href=${href}
|
|
529
657
|
@click=${() => navigateFn(href)}
|
|
530
658
|
>
|
|
531
|
-
<a href=${href} @click=${makeDepLinkClick(href)}
|
|
532
|
-
>${issueDisplayId(did)}</a
|
|
533
|
-
>
|
|
534
659
|
${createTypeBadge(dep.issue_type || '')}
|
|
535
660
|
<span class="text-truncate">${dep.title || ''}</span>
|
|
536
661
|
<button
|
|
@@ -562,14 +687,10 @@ export function createDetailView(
|
|
|
562
687
|
aria-label="Edit title"
|
|
563
688
|
.value=${issue.title || ''}
|
|
564
689
|
@keydown=${onTitleInputKeydown}
|
|
565
|
-
style="width:100%;font-size:inherit;line-height:inherit;"
|
|
566
690
|
/>
|
|
567
|
-
<button
|
|
568
|
-
<button
|
|
569
|
-
Cancel
|
|
570
|
-
</button>
|
|
691
|
+
<button @click=${onTitleSave}>Save</button>
|
|
692
|
+
<button @click=${onTitleCancel}>Cancel</button>
|
|
571
693
|
</h2>
|
|
572
|
-
<span class="mono detail-id">${issueDisplayId(issue.id)}</span>
|
|
573
694
|
</div>`
|
|
574
695
|
: html`<div class="detail-title">
|
|
575
696
|
<h2>
|
|
@@ -583,7 +704,6 @@ export function createDetailView(
|
|
|
583
704
|
>${issue.title || ''}</span
|
|
584
705
|
>
|
|
585
706
|
</h2>
|
|
586
|
-
<span class="mono detail-id">${issueDisplayId(issue.id)}</span>
|
|
587
707
|
</div>`;
|
|
588
708
|
|
|
589
709
|
const status_select = html`<select
|
|
@@ -667,7 +787,7 @@ export function createDetailView(
|
|
|
667
787
|
const accept_block = edit_accept
|
|
668
788
|
? html`<div class="acceptance">
|
|
669
789
|
${acceptance_text.trim().length > 0
|
|
670
|
-
? html`<div class="props-card__title">Acceptance</div>`
|
|
790
|
+
? html`<div class="props-card__title">Acceptance Criteria</div>`
|
|
671
791
|
: ''}
|
|
672
792
|
<textarea
|
|
673
793
|
@keydown=${onAcceptKeydown}
|
|
@@ -681,29 +801,66 @@ export function createDetailView(
|
|
|
681
801
|
</div>
|
|
682
802
|
</div>`
|
|
683
803
|
: html`<div class="acceptance">
|
|
684
|
-
${
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
804
|
+
${(() => {
|
|
805
|
+
const text = acceptance_text;
|
|
806
|
+
const has = text.trim().length > 0;
|
|
807
|
+
return html`${has
|
|
808
|
+
? html`<div class="props-card__title">Acceptance Criteria</div>`
|
|
809
|
+
: ''}
|
|
810
|
+
<div
|
|
811
|
+
class="md editable"
|
|
812
|
+
tabindex="0"
|
|
813
|
+
role="button"
|
|
814
|
+
aria-label="Edit acceptance criteria"
|
|
815
|
+
@click=${onAcceptEdit}
|
|
816
|
+
@keydown=${onAcceptEditableKeydown}
|
|
817
|
+
>
|
|
818
|
+
${has
|
|
819
|
+
? renderMarkdown(text)
|
|
820
|
+
: html`<div class="muted">Add acceptance criteria…</div>`}
|
|
821
|
+
</div>`;
|
|
822
|
+
})()}
|
|
697
823
|
</div>`;
|
|
698
824
|
|
|
699
|
-
// Notes
|
|
825
|
+
// Notes: editable in-place similar to Description
|
|
700
826
|
const notes_text = String(issue.notes || '');
|
|
701
|
-
const notes_block =
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
827
|
+
const notes_block = edit_notes
|
|
828
|
+
? html`<div class="notes">
|
|
829
|
+
${notes_text.trim().length > 0
|
|
830
|
+
? html`<div class="props-card__title">Notes</div>`
|
|
831
|
+
: ''}
|
|
832
|
+
<textarea
|
|
833
|
+
@keydown=${onNotesKeydown}
|
|
834
|
+
.value=${notes_text}
|
|
835
|
+
rows="6"
|
|
836
|
+
style="width:100%"
|
|
837
|
+
></textarea>
|
|
838
|
+
<div class="editable-actions">
|
|
839
|
+
<button @click=${onNotesSave}>Save</button>
|
|
840
|
+
<button @click=${onNotesCancel}>Cancel</button>
|
|
841
|
+
</div>
|
|
842
|
+
</div>`
|
|
843
|
+
: html`<div class="notes">
|
|
844
|
+
${(() => {
|
|
845
|
+
const text = notes_text;
|
|
846
|
+
const has = text.trim().length > 0;
|
|
847
|
+
return html`${has
|
|
848
|
+
? html`<div class="props-card__title">Notes</div>`
|
|
849
|
+
: ''}
|
|
850
|
+
<div
|
|
851
|
+
class="md editable"
|
|
852
|
+
tabindex="0"
|
|
853
|
+
role="button"
|
|
854
|
+
aria-label="Edit notes"
|
|
855
|
+
@click=${onNotesEdit}
|
|
856
|
+
@keydown=${onNotesEditableKeydown}
|
|
857
|
+
>
|
|
858
|
+
${has
|
|
859
|
+
? renderMarkdown(text)
|
|
860
|
+
: html`<div class="muted">Add notes…</div>`}
|
|
861
|
+
</div>`;
|
|
862
|
+
})()}
|
|
863
|
+
</div>`;
|
|
707
864
|
|
|
708
865
|
// Labels section
|
|
709
866
|
const labels = Array.isArray(issue.labels) ? issue.labels : [];
|
|
@@ -729,7 +886,7 @@ export function createDetailView(
|
|
|
729
886
|
<input
|
|
730
887
|
type="text"
|
|
731
888
|
aria-label="Add label"
|
|
732
|
-
placeholder="Add label
|
|
889
|
+
placeholder="Add label"
|
|
733
890
|
.value=${new_label_text}
|
|
734
891
|
@input=${onLabelInput}
|
|
735
892
|
@keydown=${onLabelKeydown}
|
|
@@ -739,12 +896,53 @@ export function createDetailView(
|
|
|
739
896
|
</div>
|
|
740
897
|
</div>`;
|
|
741
898
|
|
|
899
|
+
// Design section block
|
|
900
|
+
const design_text = String(issue.design || '');
|
|
901
|
+
const design_block = edit_design
|
|
902
|
+
? html`<div class="design">
|
|
903
|
+
${design_text.trim().length > 0
|
|
904
|
+
? html`<div class="props-card__title">Design</div>`
|
|
905
|
+
: ''}
|
|
906
|
+
<textarea
|
|
907
|
+
@keydown=${onDesignKeydown}
|
|
908
|
+
.value=${design_text}
|
|
909
|
+
rows="6"
|
|
910
|
+
style="width:100%"
|
|
911
|
+
></textarea>
|
|
912
|
+
<div class="editable-actions">
|
|
913
|
+
<button @click=${onDesignSave}>Save</button>
|
|
914
|
+
<button @click=${onDesignCancel}>Cancel</button>
|
|
915
|
+
</div>
|
|
916
|
+
</div>`
|
|
917
|
+
: html`<div class="design">
|
|
918
|
+
${(() => {
|
|
919
|
+
const text = design_text;
|
|
920
|
+
const has = text.trim().length > 0;
|
|
921
|
+
return html`${has
|
|
922
|
+
? html`<div class="props-card__title">Design</div>`
|
|
923
|
+
: ''}
|
|
924
|
+
<div
|
|
925
|
+
class="md editable"
|
|
926
|
+
tabindex="0"
|
|
927
|
+
role="button"
|
|
928
|
+
aria-label="Edit design"
|
|
929
|
+
@click=${onDesignEdit}
|
|
930
|
+
@keydown=${onDesignEditableKeydown}
|
|
931
|
+
>
|
|
932
|
+
${has
|
|
933
|
+
? renderMarkdown(text)
|
|
934
|
+
: html`<div class="muted">Add design…</div>`}
|
|
935
|
+
</div>`;
|
|
936
|
+
})()}
|
|
937
|
+
</div>`;
|
|
938
|
+
|
|
742
939
|
return html`
|
|
743
940
|
<div class="panel__body" id="detail-root">
|
|
744
941
|
<div style="position:relative">
|
|
745
942
|
<div class="detail-layout">
|
|
746
943
|
<div class="detail-main">
|
|
747
|
-
${title_zone} ${desc_block} ${
|
|
944
|
+
${title_zone} ${desc_block} ${design_block} ${notes_block}
|
|
945
|
+
${accept_block}
|
|
748
946
|
</div>
|
|
749
947
|
<div class="detail-side">
|
|
750
948
|
<div class="props-card">
|
|
@@ -845,22 +1043,6 @@ export function createDetailView(
|
|
|
845
1043
|
// panel header removed for detail view; ID is shown inline with title
|
|
846
1044
|
}
|
|
847
1045
|
|
|
848
|
-
/**
|
|
849
|
-
* Create an anchor click handler for dependency links.
|
|
850
|
-
* @param {string} href
|
|
851
|
-
* @returns {(ev: Event) => void}
|
|
852
|
-
*/
|
|
853
|
-
function makeDepLinkClick(href) {
|
|
854
|
-
return (ev) => {
|
|
855
|
-
ev.preventDefault();
|
|
856
|
-
/** @type {Event} */
|
|
857
|
-
const e = ev;
|
|
858
|
-
// stop bubbling to the li row click
|
|
859
|
-
e.stopPropagation();
|
|
860
|
-
navigateFn(href);
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
|
|
864
1046
|
/**
|
|
865
1047
|
* Create a click handler for the remove button of a dependency row.
|
|
866
1048
|
* @param {string} did
|
|
@@ -961,7 +1143,7 @@ export function createDetailView(
|
|
|
961
1143
|
}
|
|
962
1144
|
}
|
|
963
1145
|
} catch {
|
|
964
|
-
showToast('Failed to add dependency');
|
|
1146
|
+
showToast('Failed to add dependency', 'error');
|
|
965
1147
|
} finally {
|
|
966
1148
|
pending = false;
|
|
967
1149
|
}
|
|
@@ -998,6 +1180,24 @@ export function createDetailView(
|
|
|
998
1180
|
}
|
|
999
1181
|
}
|
|
1000
1182
|
|
|
1183
|
+
/**
|
|
1184
|
+
* @param {KeyboardEvent} ev
|
|
1185
|
+
*/
|
|
1186
|
+
function onNotesEditableKeydown(ev) {
|
|
1187
|
+
if (ev.key === 'Enter') {
|
|
1188
|
+
onNotesEdit();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* @param {KeyboardEvent} ev
|
|
1194
|
+
*/
|
|
1195
|
+
function onDesignEditableKeydown(ev) {
|
|
1196
|
+
if (ev.key === 'Enter') {
|
|
1197
|
+
onDesignEdit();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1001
1201
|
return {
|
|
1002
1202
|
async load(id) {
|
|
1003
1203
|
if (!id) {
|
package/app/views/epics.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html, render } from 'lit-html';
|
|
2
|
-
import {
|
|
2
|
+
import { createIssueIdRenderer } from '../utils/issue-id-renderer.js';
|
|
3
3
|
import { createIssueRowRenderer } from './issue-row.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -62,7 +62,7 @@ export function createEpicsView(mount_element, data, goto_issue) {
|
|
|
62
62
|
tabindex="0"
|
|
63
63
|
aria-expanded=${is_open}
|
|
64
64
|
>
|
|
65
|
-
|
|
65
|
+
${createIssueIdRenderer(id, { class_name: 'mono' })}
|
|
66
66
|
<span class="text-truncate" style="margin-left:8px"
|
|
67
67
|
>${epic.title || '(no title)'}</span
|
|
68
68
|
>
|