trekoon 0.2.8 → 0.3.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/README.md +14 -14
- package/docs/commands.md +9 -11
- package/docs/quickstart.md +10 -12
- package/package.json +23 -1
- package/src/board/assets/app.js +469 -1377
- package/src/board/assets/components/ClampedText.js +1 -1
- package/src/board/assets/components/Component.js +271 -0
- package/src/board/assets/components/ConfirmDialog.js +81 -0
- package/src/board/assets/components/EpicRow.js +43 -26
- package/src/board/assets/components/EpicsOverview.js +52 -11
- package/src/board/assets/components/Inspector.js +335 -0
- package/src/board/assets/components/Notice.js +87 -0
- package/src/board/assets/components/SubtaskModal.js +100 -0
- package/src/board/assets/components/TaskCard.js +82 -0
- package/src/board/assets/components/TaskModal.js +99 -0
- package/src/board/assets/components/TopBar.js +167 -0
- package/src/board/assets/components/Workspace.js +319 -0
- package/src/board/assets/components/assetMap.js +29 -14
- package/src/board/assets/components/helpers.js +261 -0
- package/src/board/assets/fonts/inter-latin.woff2 +0 -0
- package/src/board/assets/fonts/material-symbols-rounded.woff2 +0 -0
- package/src/board/assets/index.html +20 -57
- package/src/board/assets/main.js +2 -18
- package/src/board/assets/runtime/clipboard.js +34 -0
- package/src/board/assets/runtime/delegation.js +342 -0
- package/src/board/assets/state/actions.js +204 -16
- package/src/board/assets/state/api.js +201 -46
- package/src/board/assets/state/store.js +418 -117
- package/src/board/assets/state/url.js +184 -0
- package/src/board/assets/state/utils.js +222 -0
- package/src/board/assets/styles/board.css +933 -129
- package/src/board/assets/styles/fonts.css +22 -0
- package/src/board/routes.ts +15 -6
- package/src/board/server.ts +1 -0
- package/src/board/assets/components/AppShell.js +0 -17
- package/src/board/assets/components/BoardTopbar.js +0 -78
- package/src/board/assets/components/WorkspaceHeader.js +0 -70
- package/src/board/assets/utils/dom.js +0 -308
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event delegation system for the board runtime.
|
|
3
|
+
*
|
|
4
|
+
* Attaches a single listener per event type on the root element.
|
|
5
|
+
* Uses event.target.closest() to match data-attributes, so dynamically
|
|
6
|
+
* rendered content is handled automatically without rebinding.
|
|
7
|
+
*
|
|
8
|
+
* @param {HTMLElement} rootElement Mount root for delegated listeners.
|
|
9
|
+
* @param {object} actions Callback map the delegation dispatches into.
|
|
10
|
+
* @returns {() => void} Teardown function that removes every listener.
|
|
11
|
+
*/
|
|
12
|
+
export function createDelegation(rootElement, actions) {
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Click delegation
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function handleClick(event) {
|
|
17
|
+
const { target } = event;
|
|
18
|
+
|
|
19
|
+
// -- Destructive / mutation buttons (most specific first) -----------------
|
|
20
|
+
|
|
21
|
+
const deleteSubtaskEl = target.closest("[data-delete-subtask]");
|
|
22
|
+
if (deleteSubtaskEl) {
|
|
23
|
+
if (actions.isMutating()) return;
|
|
24
|
+
const subtaskId = deleteSubtaskEl.dataset.deleteSubtask;
|
|
25
|
+
if (subtaskId) actions.deleteSubtask(subtaskId, deleteSubtaskEl);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const removeDependencyEl = target.closest("[data-remove-dependency-source]");
|
|
30
|
+
if (removeDependencyEl) {
|
|
31
|
+
if (actions.isMutating()) return;
|
|
32
|
+
const sourceId = removeDependencyEl.dataset.removeDependencySource;
|
|
33
|
+
const dependsOnId = removeDependencyEl.dataset.removeDependencyTarget;
|
|
34
|
+
actions.removeDependency(sourceId, dependsOnId, removeDependencyEl);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// -- Disclosure openers ---------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const openSubtaskEl = target.closest("[data-open-subtask]");
|
|
41
|
+
if (openSubtaskEl) {
|
|
42
|
+
actions.openSubtask(openSubtaskEl.dataset.openSubtask || null, openSubtaskEl);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const copyEpicIdEl = target.closest("[data-copy-epic-id]");
|
|
47
|
+
if (copyEpicIdEl) {
|
|
48
|
+
actions.copyEpicId(copyEpicIdEl.dataset.copyEpicId || null);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// -- Backdrop-style close handlers ----------------------------------------
|
|
53
|
+
// Only close when the click lands directly on the backdrop element itself,
|
|
54
|
+
// not on any child content rendered inside the overlay.
|
|
55
|
+
|
|
56
|
+
const closeSubtaskEl = target.closest("[data-close-subtask]");
|
|
57
|
+
if (closeSubtaskEl) {
|
|
58
|
+
if (
|
|
59
|
+
closeSubtaskEl.classList.contains("board-modal-backdrop") &&
|
|
60
|
+
target !== closeSubtaskEl
|
|
61
|
+
) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
actions.closeSubtask();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const closeTaskEl = target.closest("[data-close-task]");
|
|
69
|
+
if (closeTaskEl) {
|
|
70
|
+
if (
|
|
71
|
+
closeTaskEl.classList.contains("board-task-modal-backdrop") &&
|
|
72
|
+
target !== closeTaskEl
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
actions.closeTask();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const closeConfirmEl = target.closest("[data-close-confirm]");
|
|
81
|
+
if (closeConfirmEl) {
|
|
82
|
+
if (
|
|
83
|
+
closeConfirmEl.classList.contains("board-confirm-backdrop") &&
|
|
84
|
+
target !== closeConfirmEl
|
|
85
|
+
) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
actions.cancelDelete();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// -- Navigation -----------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
const navEl = target.closest("[data-nav]");
|
|
95
|
+
if (navEl) {
|
|
96
|
+
if (navEl.dataset.nav === "epics") actions.showEpics();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const navBoardEl = target.closest("[data-nav-board]");
|
|
101
|
+
if (navBoardEl) {
|
|
102
|
+
actions.showBoard();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const navDetailEl = target.closest("[data-nav-detail]");
|
|
107
|
+
if (navDetailEl) {
|
|
108
|
+
actions.scrollToDetail();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -- Notes panel toggle ---------------------------------------------------
|
|
113
|
+
|
|
114
|
+
const toggleNotesEl = target.closest("[data-toggle-notes]");
|
|
115
|
+
if (toggleNotesEl) {
|
|
116
|
+
actions.toggleNotesPanel();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// -- View switching -------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const viewEl = target.closest("[data-view]");
|
|
123
|
+
if (viewEl) {
|
|
124
|
+
actions.setView(viewEl.dataset.view);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// -- Generic actions (toggle-theme, confirm-delete, cancel-delete) --------
|
|
129
|
+
|
|
130
|
+
const actionEl = target.closest("[data-action]");
|
|
131
|
+
if (actionEl) {
|
|
132
|
+
const action = actionEl.dataset.action;
|
|
133
|
+
if (action === "toggle-theme") actions.toggleTheme();
|
|
134
|
+
if (action === "confirm-delete") actions.confirmDelete();
|
|
135
|
+
if (action === "cancel-delete") actions.cancelDelete();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// -- Epic selection -------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
const openEpicEl = target.closest("[data-open-epic]");
|
|
142
|
+
if (openEpicEl) {
|
|
143
|
+
actions.openEpic(openEpicEl.dataset.openEpic || null);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// -- Task selection (broadest — checked last) -----------------------------
|
|
148
|
+
|
|
149
|
+
const taskEl = target.closest("[data-task-id]");
|
|
150
|
+
if (taskEl) {
|
|
151
|
+
actions.selectTask(taskEl.dataset.taskId, taskEl);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Input delegation (#board-search-input)
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
function handleInput(event) {
|
|
160
|
+
if (event.target.id === "board-search-input") {
|
|
161
|
+
actions.updateSearch(event.target.value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Change delegation (#board-epic-select)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
function handleChange(event) {
|
|
169
|
+
if (event.target.id === "board-epic-select") {
|
|
170
|
+
actions.selectEpic(event.target.value || null);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const epicStatusForm = event.target.closest("[data-epic-status-form]");
|
|
175
|
+
if (epicStatusForm) {
|
|
176
|
+
if (actions.isMutating()) return;
|
|
177
|
+
actions.changeEpicStatus(epicStatusForm.dataset.epicStatusForm, event.target.value);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const bulkStatusForm = event.target.closest("[data-bulk-status-form]");
|
|
182
|
+
if (bulkStatusForm) {
|
|
183
|
+
if (actions.isMutating()) return;
|
|
184
|
+
const newStatus = event.target.value;
|
|
185
|
+
if (newStatus) {
|
|
186
|
+
actions.bulkSetStatus(bulkStatusForm.dataset.bulkStatusForm, newStatus);
|
|
187
|
+
event.target.value = "";
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Submit delegation (data-task-form, data-subtask-form, etc.)
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
function handleSubmit(event) {
|
|
197
|
+
const form = event.target;
|
|
198
|
+
|
|
199
|
+
const taskForm = form.closest("[data-task-form]");
|
|
200
|
+
if (taskForm) {
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
if (actions.isMutating()) return;
|
|
203
|
+
actions.submitTaskForm(taskForm.dataset.taskForm, new FormData(taskForm));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const subtaskForm = form.closest("[data-subtask-form]");
|
|
208
|
+
if (subtaskForm) {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
if (actions.isMutating()) return;
|
|
211
|
+
actions.submitSubtaskForm(
|
|
212
|
+
subtaskForm.dataset.subtaskForm,
|
|
213
|
+
new FormData(subtaskForm),
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const createSubtaskForm = form.closest("[data-create-subtask-form]");
|
|
219
|
+
if (createSubtaskForm) {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
if (actions.isMutating()) return;
|
|
222
|
+
actions.submitCreateSubtask(
|
|
223
|
+
createSubtaskForm.dataset.createSubtaskForm,
|
|
224
|
+
new FormData(createSubtaskForm),
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const dependencyForm = form.closest("[data-dependency-form]");
|
|
230
|
+
if (dependencyForm) {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
if (actions.isMutating()) return;
|
|
233
|
+
actions.addDependency(
|
|
234
|
+
dependencyForm.dataset.dependencyForm,
|
|
235
|
+
new FormData(dependencyForm),
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Keyboard shortcuts (window-level)
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
function handleKeydown(event) {
|
|
245
|
+
if (event.defaultPrevented) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
actions.handleKeydown(event);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handleDelegatedKeydown(event) {
|
|
252
|
+
if (event.defaultPrevented) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
257
|
+
if (!target) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (target.closest("[data-copy-epic-id]")) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const openEpicEl = target.closest("[data-open-epic]");
|
|
266
|
+
if (!openEpicEl) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
actions.openEpic(openEpicEl.dataset.openEpic || null);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Drag-and-drop delegation
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
function handleDragstart(event) {
|
|
280
|
+
const draggable = event.target.closest("[data-draggable-task]");
|
|
281
|
+
if (!draggable) return;
|
|
282
|
+
|
|
283
|
+
if (actions.isMutating()) {
|
|
284
|
+
event.preventDefault();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const taskId = draggable.dataset.taskId;
|
|
289
|
+
if (!taskId) return;
|
|
290
|
+
|
|
291
|
+
event.dataTransfer?.setData("text/task-id", taskId);
|
|
292
|
+
event.dataTransfer?.setData("text/plain", taskId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function handleDragover(event) {
|
|
296
|
+
if (event.target.closest("[data-drop-status]")) {
|
|
297
|
+
event.preventDefault();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function handleDrop(event) {
|
|
302
|
+
const column = event.target.closest("[data-drop-status]");
|
|
303
|
+
if (!column) return;
|
|
304
|
+
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
if (actions.isMutating()) return;
|
|
307
|
+
|
|
308
|
+
const taskId =
|
|
309
|
+
event.dataTransfer?.getData("text/task-id") ||
|
|
310
|
+
event.dataTransfer?.getData("text/plain");
|
|
311
|
+
const nextStatus = column.dataset.dropStatus;
|
|
312
|
+
actions.dropTaskStatus(taskId, nextStatus);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Attach
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
rootElement.addEventListener("click", handleClick);
|
|
319
|
+
rootElement.addEventListener("input", handleInput);
|
|
320
|
+
rootElement.addEventListener("change", handleChange);
|
|
321
|
+
rootElement.addEventListener("submit", handleSubmit);
|
|
322
|
+
rootElement.addEventListener("dragstart", handleDragstart);
|
|
323
|
+
rootElement.addEventListener("dragover", handleDragover);
|
|
324
|
+
rootElement.addEventListener("drop", handleDrop);
|
|
325
|
+
rootElement.addEventListener("keydown", handleDelegatedKeydown);
|
|
326
|
+
window.addEventListener("keydown", handleKeydown);
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Teardown
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
return function teardown() {
|
|
332
|
+
rootElement.removeEventListener("click", handleClick);
|
|
333
|
+
rootElement.removeEventListener("input", handleInput);
|
|
334
|
+
rootElement.removeEventListener("change", handleChange);
|
|
335
|
+
rootElement.removeEventListener("submit", handleSubmit);
|
|
336
|
+
rootElement.removeEventListener("dragstart", handleDragstart);
|
|
337
|
+
rootElement.removeEventListener("dragover", handleDragover);
|
|
338
|
+
rootElement.removeEventListener("drop", handleDrop);
|
|
339
|
+
rootElement.removeEventListener("keydown", handleDelegatedKeydown);
|
|
340
|
+
window.removeEventListener("keydown", handleKeydown);
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { copyTextToClipboard } from "../runtime/clipboard.js";
|
|
2
|
+
import { orderEpicsNewestFirst } from "./store.js";
|
|
3
|
+
|
|
1
4
|
function cloneSnapshot(snapshot) {
|
|
2
5
|
if (typeof structuredClone === "function") {
|
|
3
6
|
return structuredClone(snapshot);
|
|
@@ -38,6 +41,40 @@ export function updateSubtaskInSnapshot(snapshot, subtaskId, updates, normalizeS
|
|
|
38
41
|
return normalizeSnapshot(nextSnapshot);
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
export function cascadeEpicStatusInSnapshot(snapshot, epicId, status, normalizeSnapshot) {
|
|
45
|
+
const nextSnapshot = cloneSnapshot(snapshot);
|
|
46
|
+
const epic = nextSnapshot.epics.find((candidate) => candidate.id === epicId);
|
|
47
|
+
if (!epic) {
|
|
48
|
+
return snapshot;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const updatedAt = Date.now();
|
|
52
|
+
epic.status = status;
|
|
53
|
+
epic.updatedAt = updatedAt;
|
|
54
|
+
|
|
55
|
+
const taskIds = new Set();
|
|
56
|
+
for (const task of nextSnapshot.tasks) {
|
|
57
|
+
if (task.epicId !== epicId) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
task.status = status;
|
|
62
|
+
task.updatedAt = updatedAt;
|
|
63
|
+
taskIds.add(task.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const subtask of nextSnapshot.subtasks) {
|
|
67
|
+
if (!taskIds.has(subtask.taskId)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
subtask.status = status;
|
|
72
|
+
subtask.updatedAt = updatedAt;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return normalizeSnapshot(nextSnapshot);
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
export function addDependencyInSnapshot(snapshot, sourceId, dependsOnId, normalizeSnapshot) {
|
|
42
79
|
const nextSnapshot = cloneSnapshot(snapshot);
|
|
43
80
|
const duplicate = normalizeArray(nextSnapshot.dependencies).some(
|
|
@@ -98,6 +135,8 @@ export function createBoardActions(options) {
|
|
|
98
135
|
applyTheme,
|
|
99
136
|
closeTopmostDisclosure,
|
|
100
137
|
dismissSearch,
|
|
138
|
+
hasOpenOverlay,
|
|
139
|
+
closeActiveOverlay,
|
|
101
140
|
focusSearch,
|
|
102
141
|
focusTaskDetail,
|
|
103
142
|
searchFocusKeys,
|
|
@@ -117,6 +156,80 @@ export function createBoardActions(options) {
|
|
|
117
156
|
};
|
|
118
157
|
|
|
119
158
|
let searchTimer = null;
|
|
159
|
+
let pendingSearchValue = null;
|
|
160
|
+
let copyFeedbackTimer = null;
|
|
161
|
+
|
|
162
|
+
const syncSearchInputToState = () => {
|
|
163
|
+
const input = document.querySelector("#board-search-input");
|
|
164
|
+
if (input instanceof HTMLInputElement) {
|
|
165
|
+
input.value = store.search;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const cancelPendingSearch = (options = {}) => {
|
|
170
|
+
const { syncInput = true } = options;
|
|
171
|
+
pendingSearchValue = null;
|
|
172
|
+
if (searchTimer !== null) {
|
|
173
|
+
clearTimeout(searchTimer);
|
|
174
|
+
searchTimer = null;
|
|
175
|
+
}
|
|
176
|
+
if (syncInput) {
|
|
177
|
+
syncSearchInputToState();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const focusSearchInput = () => {
|
|
182
|
+
const input = document.querySelector("#board-search-input");
|
|
183
|
+
if (input instanceof HTMLInputElement) {
|
|
184
|
+
input.focus({ preventScroll: true });
|
|
185
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const shouldRefocusSearchInput = () => document.activeElement?.id === "board-search-input";
|
|
190
|
+
|
|
191
|
+
const clearCopyFeedback = ({ rerenderBoard = true } = {}) => {
|
|
192
|
+
if (copyFeedbackTimer !== null) {
|
|
193
|
+
clearTimeout(copyFeedbackTimer);
|
|
194
|
+
copyFeedbackTimer = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!store.copyFeedback) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
store.copyFeedback = null;
|
|
202
|
+
if (rerenderBoard) {
|
|
203
|
+
rerender();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const scheduleCopyFeedbackClear = (epicId) => {
|
|
208
|
+
if (copyFeedbackTimer !== null) {
|
|
209
|
+
clearTimeout(copyFeedbackTimer);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
copyFeedbackTimer = setTimeout(() => {
|
|
213
|
+
copyFeedbackTimer = null;
|
|
214
|
+
if (store.copyFeedback?.epicId !== epicId) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
store.copyFeedback = null;
|
|
219
|
+
rerender();
|
|
220
|
+
}, 1800);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const commitSearch = (nextSearch, options = {}) => {
|
|
224
|
+
const { focusInput = false } = options;
|
|
225
|
+
cancelPendingSearch({ syncInput: false });
|
|
226
|
+
syncState({ search: nextSearch });
|
|
227
|
+
persist();
|
|
228
|
+
rerender({ preserveFocus: false });
|
|
229
|
+
if (focusInput) {
|
|
230
|
+
focusSearchInput();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
120
233
|
|
|
121
234
|
return {
|
|
122
235
|
toggleTheme() {
|
|
@@ -124,23 +237,26 @@ export function createBoardActions(options) {
|
|
|
124
237
|
applyTheme(store.theme);
|
|
125
238
|
rerender();
|
|
126
239
|
},
|
|
240
|
+
toggleNotesPanel() {
|
|
241
|
+
store.notesPanelOpen = !store.notesPanelOpen;
|
|
242
|
+
persist();
|
|
243
|
+
rerender();
|
|
244
|
+
},
|
|
127
245
|
updateSearch(value) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
246
|
+
const nextSearch = typeof value === "string" ? value : "";
|
|
247
|
+
cancelPendingSearch({ syncInput: false });
|
|
248
|
+
pendingSearchValue = nextSearch;
|
|
132
249
|
searchTimer = setTimeout(() => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
persist();
|
|
136
|
-
rerender({ preserveFocus: false });
|
|
137
|
-
const input = document.querySelector("#board-search-input");
|
|
138
|
-
if (input instanceof HTMLInputElement) {
|
|
139
|
-
input.focus({ preventScroll: true });
|
|
140
|
-
input.setSelectionRange(input.value.length, input.value.length);
|
|
250
|
+
if (pendingSearchValue !== nextSearch) {
|
|
251
|
+
return;
|
|
141
252
|
}
|
|
253
|
+
commitSearch(nextSearch, { focusInput: shouldRefocusSearchInput() });
|
|
142
254
|
}, 180);
|
|
143
255
|
},
|
|
256
|
+
clearSearch() {
|
|
257
|
+
commitSearch("");
|
|
258
|
+
},
|
|
259
|
+
cancelPendingSearch,
|
|
144
260
|
openEpic(epicId) {
|
|
145
261
|
transition({
|
|
146
262
|
screen: "tasks",
|
|
@@ -165,7 +281,11 @@ export function createBoardActions(options) {
|
|
|
165
281
|
});
|
|
166
282
|
},
|
|
167
283
|
showBoard() {
|
|
168
|
-
const
|
|
284
|
+
const boardState = getBoardState();
|
|
285
|
+
const fallbackEpicId = boardState.selectedEpicId
|
|
286
|
+
|| boardState.visibleEpics[0]?.id
|
|
287
|
+
|| orderEpicsNewestFirst(store.snapshot.epics)[0]?.id
|
|
288
|
+
|| null;
|
|
169
289
|
if (!fallbackEpicId) {
|
|
170
290
|
return;
|
|
171
291
|
}
|
|
@@ -180,6 +300,37 @@ export function createBoardActions(options) {
|
|
|
180
300
|
setView(view) {
|
|
181
301
|
transition({ view });
|
|
182
302
|
},
|
|
303
|
+
async copyEpicId(epicId) {
|
|
304
|
+
const normalizedEpicId = typeof epicId === "string" ? epicId.trim() : "";
|
|
305
|
+
|
|
306
|
+
if (!normalizedEpicId) {
|
|
307
|
+
clearCopyFeedback({ rerenderBoard: false });
|
|
308
|
+
store.notice = {
|
|
309
|
+
type: "error",
|
|
310
|
+
title: "Copy failed",
|
|
311
|
+
message: "Epic UUID is unavailable.",
|
|
312
|
+
};
|
|
313
|
+
rerender();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
await copyTextToClipboard(normalizedEpicId);
|
|
319
|
+
store.copyFeedback = {
|
|
320
|
+
epicId: normalizedEpicId,
|
|
321
|
+
};
|
|
322
|
+
scheduleCopyFeedbackClear(normalizedEpicId);
|
|
323
|
+
} catch {
|
|
324
|
+
clearCopyFeedback({ rerenderBoard: false });
|
|
325
|
+
store.notice = {
|
|
326
|
+
type: "error",
|
|
327
|
+
title: "Copy failed",
|
|
328
|
+
message: "Could not copy the epic UUID.",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
rerender();
|
|
333
|
+
},
|
|
183
334
|
selectTask(taskId) {
|
|
184
335
|
const task = getTaskById(taskId);
|
|
185
336
|
if (!task) {
|
|
@@ -261,6 +412,20 @@ export function createBoardActions(options) {
|
|
|
261
412
|
transition({ selectedTaskId: taskId }, { rerenderBoard: false });
|
|
262
413
|
api.patchTask(taskId, { status: nextStatus }, (snapshot) => updateTaskInSnapshot(snapshot, taskId, { status: nextStatus }, normalizeSnapshot));
|
|
263
414
|
},
|
|
415
|
+
changeEpicStatus(epicId, newStatus) {
|
|
416
|
+
const normalizedStatus = normalizeStatus(newStatus);
|
|
417
|
+
api.patchEpic(epicId, { status: normalizedStatus }, (snapshot) => {
|
|
418
|
+
const epic = snapshot.epics.find(e => e.id === epicId);
|
|
419
|
+
if (epic) epic.status = normalizedStatus;
|
|
420
|
+
return snapshot;
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
bulkSetStatus(epicId, newStatus) {
|
|
424
|
+
const normalizedStatus = normalizeStatus(newStatus);
|
|
425
|
+
api.cascadeEpicStatus(epicId, normalizedStatus, (snapshot) =>
|
|
426
|
+
cascadeEpicStatusInSnapshot(snapshot, epicId, normalizedStatus, normalizeSnapshot),
|
|
427
|
+
);
|
|
428
|
+
},
|
|
264
429
|
handleKeydown(event) {
|
|
265
430
|
const boardState = getBoardState();
|
|
266
431
|
const activeElement = document.activeElement;
|
|
@@ -276,23 +441,42 @@ export function createBoardActions(options) {
|
|
|
276
441
|
}
|
|
277
442
|
|
|
278
443
|
if (event.key === "Escape") {
|
|
444
|
+
if (activeElement?.id === "board-search-input" && pendingSearchValue !== null) {
|
|
445
|
+
event.preventDefault();
|
|
446
|
+
activeElement.value = "";
|
|
447
|
+
this.clearSearch();
|
|
448
|
+
activeElement.blur();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
279
452
|
if (closeTopmostDisclosure?.(boardState, activeElement)) {
|
|
280
453
|
event.preventDefault();
|
|
281
454
|
return;
|
|
282
455
|
}
|
|
283
456
|
|
|
457
|
+
if (dismissSearch?.(boardState, activeElement)) {
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (hasOpenOverlay?.()) {
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
closeActiveOverlay?.();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
284
468
|
if (boardState.selectedSubtaskId) {
|
|
285
469
|
event.preventDefault();
|
|
286
470
|
this.closeSubtask();
|
|
287
471
|
} else if (boardState.selectedTaskId) {
|
|
288
472
|
event.preventDefault();
|
|
289
473
|
this.closeTask();
|
|
290
|
-
} else if (dismissSearch?.(boardState, activeElement)) {
|
|
291
|
-
event.preventDefault();
|
|
292
|
-
return;
|
|
293
474
|
} else if (boardState.screen === "tasks") {
|
|
294
475
|
event.preventDefault();
|
|
295
476
|
this.showEpics();
|
|
477
|
+
} else if (store.copyFeedback) {
|
|
478
|
+
event.preventDefault();
|
|
479
|
+
clearCopyFeedback();
|
|
296
480
|
} else if (store.notice) {
|
|
297
481
|
event.preventDefault();
|
|
298
482
|
store.notice = null;
|
|
@@ -301,6 +485,10 @@ export function createBoardActions(options) {
|
|
|
301
485
|
return;
|
|
302
486
|
}
|
|
303
487
|
|
|
488
|
+
if (hasOpenOverlay?.()) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
304
492
|
if (boardState.screen !== "tasks" || isTypingTarget || visibleTasks.length === 0) {
|
|
305
493
|
return;
|
|
306
494
|
}
|