beads-ui 0.1.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/.beads/issues.jsonl +107 -0
- package/.editorconfig +10 -0
- package/.eslintrc.json +36 -0
- package/.github/workflows/ci.yml +38 -0
- package/.prettierignore +5 -0
- package/AGENTS.md +85 -0
- package/CHANGES.md +5 -0
- package/LICENSE +22 -0
- package/README.md +75 -0
- package/app/data/providers.js +178 -0
- package/app/data/providers.test.js +126 -0
- package/app/index.html +29 -0
- package/app/main.board-switch.test.js +94 -0
- package/app/main.deep-link.test.js +64 -0
- package/app/main.js +280 -0
- package/app/main.live-updates.test.js +229 -0
- package/app/main.test.js +17 -0
- package/app/main.theme.test.js +41 -0
- package/app/main.view-sync.test.js +54 -0
- package/app/protocol.js +200 -0
- package/app/protocol.md +64 -0
- package/app/protocol.test.js +57 -0
- package/app/router.js +78 -0
- package/app/router.test.js +34 -0
- package/app/state.js +87 -0
- package/app/state.test.js +21 -0
- package/app/styles.css +1343 -0
- package/app/utils/issue-id.js +10 -0
- package/app/utils/issue-type.js +27 -0
- package/app/utils/markdown.js +201 -0
- package/app/utils/markdown.test.js +103 -0
- package/app/utils/priority-badge.js +49 -0
- package/app/utils/priority.js +1 -0
- package/app/utils/status-badge.js +33 -0
- package/app/utils/status.js +23 -0
- package/app/utils/type-badge.js +36 -0
- package/app/utils/type-badge.test.js +30 -0
- package/app/views/board.js +183 -0
- package/app/views/board.test.js +184 -0
- package/app/views/detail.acceptance-notes.test.js +67 -0
- package/app/views/detail.assignee.test.js +161 -0
- package/app/views/detail.deps.test.js +97 -0
- package/app/views/detail.edits.test.js +146 -0
- package/app/views/detail.js +1039 -0
- package/app/views/detail.labels.test.js +73 -0
- package/app/views/detail.priority.test.js +86 -0
- package/app/views/detail.test.js +188 -0
- package/app/views/detail.ui47.test.js +78 -0
- package/app/views/epics.js +228 -0
- package/app/views/epics.test.js +283 -0
- package/app/views/issue-row.js +191 -0
- package/app/views/list.inline-edits.test.js +84 -0
- package/app/views/list.js +393 -0
- package/app/views/list.test.js +479 -0
- package/app/views/nav.js +67 -0
- package/app/views/nav.test.js +43 -0
- package/app/ws.js +252 -0
- package/app/ws.test.js +168 -0
- package/bin/bdui.js +18 -0
- package/docs/architecture.md +244 -0
- package/docs/db-watching.md +29 -0
- package/docs/quickstart.md +142 -0
- package/eslint.config.js +59 -0
- package/media/bdui-board.png +0 -0
- package/media/bdui-epics.png +0 -0
- package/media/bdui-issues.png +0 -0
- package/package.json +48 -0
- package/prettier.config.js +13 -0
- package/server/app.js +80 -0
- package/server/app.test.js +29 -0
- package/server/bd.js +125 -0
- package/server/bd.test.js +93 -0
- package/server/cli/cli.test.js +109 -0
- package/server/cli/commands.integration.test.js +155 -0
- package/server/cli/commands.js +91 -0
- package/server/cli/commands.unit.test.js +94 -0
- package/server/cli/daemon.js +239 -0
- package/server/cli/index.js +74 -0
- package/server/cli/open.js +96 -0
- package/server/cli/open.test.js +26 -0
- package/server/cli/usage.js +22 -0
- package/server/config.js +29 -0
- package/server/db.js +100 -0
- package/server/db.test.js +70 -0
- package/server/index.js +29 -0
- package/server/protocol.js +3 -0
- package/server/protocol.test.js +87 -0
- package/server/watcher.js +107 -0
- package/server/watcher.test.js +100 -0
- package/server/ws.handlers.test.js +174 -0
- package/server/ws.js +784 -0
- package/server/ws.labels.test.js +95 -0
- package/server/ws.mutations.test.js +261 -0
- package/server/ws.subscriptions.test.js +116 -0
- package/server/ws.test.js +52 -0
- package/test/setup-vitest.js +12 -0
- package/tsconfig.json +23 -0
- package/vitest.config.mjs +14 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { createDetailView } from './detail.js';
|
|
3
|
+
|
|
4
|
+
/** @type {(impl: (type: string, payload?: unknown) => Promise<any>) => (type: string, payload?: unknown) => Promise<any>} */
|
|
5
|
+
const mockSend = (impl) => vi.fn(impl);
|
|
6
|
+
|
|
7
|
+
describe('views/detail edits', () => {
|
|
8
|
+
test('updates status via dropdown and disables while pending', async () => {
|
|
9
|
+
document.body.innerHTML =
|
|
10
|
+
'<section class="panel"><div id="mount"></div></section>';
|
|
11
|
+
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
12
|
+
|
|
13
|
+
const initial = {
|
|
14
|
+
id: 'UI-7',
|
|
15
|
+
title: 'T',
|
|
16
|
+
description: 'D',
|
|
17
|
+
status: 'open',
|
|
18
|
+
priority: 2
|
|
19
|
+
};
|
|
20
|
+
const updated = { ...initial, status: 'in_progress' };
|
|
21
|
+
|
|
22
|
+
const send = mockSend(async (type, payload) => {
|
|
23
|
+
if (type === 'show-issue') {
|
|
24
|
+
return initial;
|
|
25
|
+
}
|
|
26
|
+
if (type === 'update-status') {
|
|
27
|
+
expect(payload).toEqual({ id: 'UI-7', status: 'in_progress' });
|
|
28
|
+
// simulate server reconcile payload
|
|
29
|
+
return updated;
|
|
30
|
+
}
|
|
31
|
+
throw new Error('Unexpected');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const view = createDetailView(mount, send);
|
|
35
|
+
await view.load('UI-7');
|
|
36
|
+
|
|
37
|
+
const select = /** @type {HTMLSelectElement} */ (
|
|
38
|
+
mount.querySelector('select')
|
|
39
|
+
);
|
|
40
|
+
expect(select.value).toBe('open');
|
|
41
|
+
|
|
42
|
+
// Trigger change
|
|
43
|
+
select.value = 'in_progress';
|
|
44
|
+
const beforeDisabled = select.disabled;
|
|
45
|
+
select.dispatchEvent(new Event('change'));
|
|
46
|
+
// After dispatch, the component sets disabled & will re-render upon reply
|
|
47
|
+
expect(beforeDisabled || select.disabled).toBe(true);
|
|
48
|
+
|
|
49
|
+
// After async flow, DOM should reflect updated status
|
|
50
|
+
await Promise.resolve(); // allow microtasks
|
|
51
|
+
const select2 = /** @type {HTMLSelectElement} */ (
|
|
52
|
+
mount.querySelector('select')
|
|
53
|
+
);
|
|
54
|
+
expect(select2.value).toBe('in_progress');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('saves title and re-renders from reply', async () => {
|
|
58
|
+
document.body.innerHTML =
|
|
59
|
+
'<section class="panel"><div id="mount"></div></section>';
|
|
60
|
+
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
61
|
+
const initial = {
|
|
62
|
+
id: 'UI-8',
|
|
63
|
+
title: 'Old',
|
|
64
|
+
description: '',
|
|
65
|
+
status: 'open',
|
|
66
|
+
priority: 1
|
|
67
|
+
};
|
|
68
|
+
const send = mockSend(async (type, payload) => {
|
|
69
|
+
if (type === 'show-issue') {
|
|
70
|
+
return initial;
|
|
71
|
+
}
|
|
72
|
+
if (type === 'edit-text') {
|
|
73
|
+
const next = { ...initial, title: /** @type {any} */ (payload).value };
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
throw new Error('Unexpected');
|
|
77
|
+
});
|
|
78
|
+
const view = createDetailView(mount, send);
|
|
79
|
+
await view.load('UI-8');
|
|
80
|
+
// Enter edit mode by clicking the span
|
|
81
|
+
const titleSpan = /** @type {HTMLSpanElement} */ (
|
|
82
|
+
mount.querySelector('h2 .editable')
|
|
83
|
+
);
|
|
84
|
+
titleSpan.click();
|
|
85
|
+
const titleInput = /** @type {HTMLInputElement} */ (
|
|
86
|
+
mount.querySelector('h2 input')
|
|
87
|
+
);
|
|
88
|
+
const titleSave = /** @type {HTMLButtonElement} */ (
|
|
89
|
+
mount.querySelector('h2 button')
|
|
90
|
+
);
|
|
91
|
+
titleInput.value = 'New Title';
|
|
92
|
+
titleSave.click();
|
|
93
|
+
await Promise.resolve();
|
|
94
|
+
// After save, returns to read mode with updated text
|
|
95
|
+
const titleSpan2 = /** @type {HTMLSpanElement} */ (
|
|
96
|
+
mount.querySelector('h2 .editable')
|
|
97
|
+
);
|
|
98
|
+
expect(titleSpan2.textContent).toBe('New Title');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('shows toast on description save error and re-enables', async () => {
|
|
102
|
+
vi.useFakeTimers();
|
|
103
|
+
document.body.innerHTML =
|
|
104
|
+
'<section class="panel"><div id="mount"></div></section>';
|
|
105
|
+
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
106
|
+
const initial = {
|
|
107
|
+
id: 'UI-9',
|
|
108
|
+
title: 'T',
|
|
109
|
+
description: 'D',
|
|
110
|
+
status: 'open',
|
|
111
|
+
priority: 2
|
|
112
|
+
};
|
|
113
|
+
const send = mockSend(async (type) => {
|
|
114
|
+
if (type === 'show-issue') {
|
|
115
|
+
return initial;
|
|
116
|
+
}
|
|
117
|
+
if (type === 'edit-text') {
|
|
118
|
+
throw new Error('boom');
|
|
119
|
+
}
|
|
120
|
+
throw new Error('Unexpected');
|
|
121
|
+
});
|
|
122
|
+
const view = createDetailView(mount, send);
|
|
123
|
+
await view.load('UI-9');
|
|
124
|
+
// Enter edit mode
|
|
125
|
+
const md = /** @type {HTMLDivElement} */ (mount.querySelector('.md'));
|
|
126
|
+
md.click();
|
|
127
|
+
const ta = /** @type {HTMLTextAreaElement} */ (
|
|
128
|
+
mount.querySelector('textarea')
|
|
129
|
+
);
|
|
130
|
+
const btn = /** @type {HTMLButtonElement} */ (
|
|
131
|
+
mount.querySelector('.editable-actions button')
|
|
132
|
+
);
|
|
133
|
+
ta.value = 'New D';
|
|
134
|
+
btn.click();
|
|
135
|
+
await Promise.resolve();
|
|
136
|
+
// Toast appears
|
|
137
|
+
const toast = /** @type {HTMLElement} */ (mount.querySelector('.toast'));
|
|
138
|
+
expect(toast).not.toBeNull();
|
|
139
|
+
expect((toast.textContent || '').toLowerCase()).toContain(
|
|
140
|
+
'failed to save description'
|
|
141
|
+
);
|
|
142
|
+
// Auto-dismiss after a while
|
|
143
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
144
|
+
vi.useRealTimers();
|
|
145
|
+
});
|
|
146
|
+
});
|