agkan 2.14.1 → 2.15.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.ja.md +13 -13
- package/README.md +13 -13
- package/dist/board/boardFavicon.d.ts +2 -0
- package/dist/board/boardFavicon.d.ts.map +1 -0
- package/dist/board/boardFavicon.js +5 -0
- package/dist/board/boardFavicon.js.map +1 -0
- package/dist/board/boardRenderer.d.ts +22 -6
- package/dist/board/boardRenderer.d.ts.map +1 -1
- package/dist/board/boardRenderer.js +40 -24
- package/dist/board/boardRenderer.js.map +1 -1
- package/dist/board/boardRoutes.d.ts +2 -2
- package/dist/board/boardRoutes.d.ts.map +1 -1
- package/dist/board/boardRoutes.js +18 -3
- package/dist/board/boardRoutes.js.map +1 -1
- package/dist/board/boardStyles.d.ts +1 -1
- package/dist/board/boardStyles.d.ts.map +1 -1
- package/dist/board/boardStyles.js +15 -7
- package/dist/board/boardStyles.js.map +1 -1
- package/dist/board/client/board.js +590 -207
- package/dist/board/server.d.ts +2 -2
- package/dist/board/server.d.ts.map +1 -1
- package/dist/board/server.js +5 -5
- package/dist/board/server.js.map +1 -1
- package/dist/cli/commands/agent-guide.d.ts.map +1 -1
- package/dist/cli/commands/agent-guide.js +6 -0
- package/dist/cli/commands/agent-guide.js.map +1 -1
- package/dist/cli/commands/board.d.ts.map +1 -1
- package/dist/cli/commands/board.js +202 -15
- package/dist/cli/commands/board.js.map +1 -1
- package/dist/cli/commands/ps.d.ts +7 -0
- package/dist/cli/commands/ps.d.ts.map +1 -0
- package/dist/cli/commands/ps.js +83 -0
- package/dist/cli/commands/ps.js.map +1 -0
- package/dist/cli/commands/task/add.js +1 -1
- package/dist/cli/commands/task/add.js.map +1 -1
- package/dist/cli/commands/task/copy.d.ts +6 -0
- package/dist/cli/commands/task/copy.d.ts.map +1 -0
- package/dist/cli/commands/task/copy.js +118 -0
- package/dist/cli/commands/task/copy.js.map +1 -0
- package/dist/cli/commands/task/list.d.ts.map +1 -1
- package/dist/cli/commands/task/list.js +37 -17
- package/dist/cli/commands/task/list.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/board-daemon.d.ts +7 -0
- package/dist/cli/utils/board-daemon.d.ts.map +1 -0
- package/dist/cli/utils/board-daemon.js +77 -0
- package/dist/cli/utils/board-daemon.js.map +1 -0
- package/dist/db/adapters/sqlite-storage-backend.d.ts +26 -0
- package/dist/db/adapters/sqlite-storage-backend.d.ts.map +1 -0
- package/dist/db/adapters/sqlite-storage-backend.js +447 -0
- package/dist/db/adapters/sqlite-storage-backend.js.map +1 -0
- package/dist/db/connection.d.ts +14 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +28 -2
- package/dist/db/connection.js.map +1 -1
- package/dist/db/migrations/20260328000000_initial_schema.d.ts +3 -0
- package/dist/db/migrations/20260328000000_initial_schema.d.ts.map +1 -0
- package/dist/db/migrations/20260328000000_initial_schema.js +218 -0
- package/dist/db/migrations/20260328000000_initial_schema.js.map +1 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.d.ts +3 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.d.ts.map +1 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.js +7 -0
- package/dist/db/migrations/20260329000000_add_session_id_to_task_run_logs.js.map +1 -0
- package/dist/db/migrations/index.d.ts +4 -0
- package/dist/db/migrations/index.d.ts.map +1 -0
- package/dist/db/migrations/index.js +18 -0
- package/dist/db/migrations/index.js.map +1 -0
- package/dist/db/migrations/types.d.ts +17 -0
- package/dist/db/migrations/types.d.ts.map +1 -0
- package/dist/db/migrations/types.js +3 -0
- package/dist/db/migrations/types.js.map +1 -0
- package/dist/db/reset.d.ts.map +1 -1
- package/dist/db/reset.js +8 -3
- package/dist/db/reset.js.map +1 -1
- package/dist/db/schema.d.ts +4 -4
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +22 -207
- package/dist/db/schema.js.map +1 -1
- package/dist/db/types/repository.d.ts +192 -0
- package/dist/db/types/repository.d.ts.map +1 -0
- package/dist/db/types/repository.js +15 -0
- package/dist/db/types/repository.js.map +1 -0
- package/dist/models/Attachment.d.ts +25 -0
- package/dist/models/Attachment.d.ts.map +1 -0
- package/dist/models/Attachment.js +7 -0
- package/dist/models/Attachment.js.map +1 -0
- package/dist/services/AttachmentService.d.ts +62 -0
- package/dist/services/AttachmentService.d.ts.map +1 -0
- package/dist/services/AttachmentService.js +95 -0
- package/dist/services/AttachmentService.js.map +1 -0
- package/dist/services/ClaudeProcessService.d.ts +100 -0
- package/dist/services/ClaudeProcessService.d.ts.map +1 -0
- package/dist/services/ClaudeProcessService.js +278 -0
- package/dist/services/ClaudeProcessService.js.map +1 -0
- package/dist/services/CommentService.d.ts +3 -3
- package/dist/services/CommentService.d.ts.map +1 -1
- package/dist/services/CommentService.js +10 -70
- package/dist/services/CommentService.js.map +1 -1
- package/dist/services/ExportImportService.d.ts +3 -3
- package/dist/services/ExportImportService.d.ts.map +1 -1
- package/dist/services/ExportImportService.js +12 -16
- package/dist/services/ExportImportService.js.map +1 -1
- package/dist/services/MetadataService.d.ts +3 -3
- package/dist/services/MetadataService.d.ts.map +1 -1
- package/dist/services/MetadataService.js +9 -69
- package/dist/services/MetadataService.js.map +1 -1
- package/dist/services/TagService.d.ts +3 -3
- package/dist/services/TagService.d.ts.map +1 -1
- package/dist/services/TagService.js +9 -35
- package/dist/services/TagService.js.map +1 -1
- package/dist/services/TaskBlockService.d.ts +3 -3
- package/dist/services/TaskBlockService.d.ts.map +1 -1
- package/dist/services/TaskBlockService.js +9 -36
- package/dist/services/TaskBlockService.js.map +1 -1
- package/dist/services/TaskService.d.ts +3 -23
- package/dist/services/TaskService.d.ts.map +1 -1
- package/dist/services/TaskService.js +34 -186
- package/dist/services/TaskService.js.map +1 -1
- package/dist/services/TaskTagService.d.ts +3 -3
- package/dist/services/TaskTagService.d.ts.map +1 -1
- package/dist/services/TaskTagService.js +19 -83
- package/dist/services/TaskTagService.js.map +1 -1
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +12 -5
- package/dist/board/boardScript.d.ts +0 -2
- package/dist/board/boardScript.d.ts.map +0 -1
- package/dist/board/boardScript.js +0 -1202
- package/dist/board/boardScript.js.map +0 -1
- package/dist/services/ProcessService.d.ts +0 -54
- package/dist/services/ProcessService.d.ts.map +0 -1
- package/dist/services/ProcessService.js +0 -147
- package/dist/services/ProcessService.js.map +0 -1
- package/dist/services/TmuxService.d.ts +0 -2
- package/dist/services/TmuxService.d.ts.map +0 -1
- package/dist/services/TmuxService.js +0 -7
- package/dist/services/TmuxService.js.map +0 -1
|
@@ -31,8 +31,23 @@
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// src/board/client/dragDrop.ts
|
|
34
|
+
var _redrawDependencies = null;
|
|
35
|
+
function registerDependencyRedrawCallback(callback) {
|
|
36
|
+
_redrawDependencies = callback;
|
|
37
|
+
}
|
|
34
38
|
var draggedCard = null;
|
|
35
39
|
var sourceBody = null;
|
|
40
|
+
var _dragMouseX = 0;
|
|
41
|
+
var _dragMouseY = 0;
|
|
42
|
+
var _dragOffsetX = 0;
|
|
43
|
+
var _dragOffsetY = 0;
|
|
44
|
+
function getDraggedCardVirtualRect() {
|
|
45
|
+
if (!draggedCard) return null;
|
|
46
|
+
const rect = draggedCard.getBoundingClientRect();
|
|
47
|
+
const left = _dragMouseX - _dragOffsetX;
|
|
48
|
+
const top = _dragMouseY - _dragOffsetY;
|
|
49
|
+
return new DOMRect(left, top, rect.width, rect.height);
|
|
50
|
+
}
|
|
36
51
|
function updateCount(status) {
|
|
37
52
|
const col = document.querySelector(`.column[data-status="${status}"]`);
|
|
38
53
|
if (!col) return;
|
|
@@ -62,6 +77,9 @@
|
|
|
62
77
|
body: JSON.stringify({ status: newStatus })
|
|
63
78
|
});
|
|
64
79
|
if (!res.ok) throw new Error("Server error");
|
|
80
|
+
if (_redrawDependencies) {
|
|
81
|
+
_redrawDependencies();
|
|
82
|
+
}
|
|
65
83
|
} catch {
|
|
66
84
|
if (prevBody && draggedCard) {
|
|
67
85
|
prevBody.appendChild(draggedCard);
|
|
@@ -72,17 +90,33 @@
|
|
|
72
90
|
showToast();
|
|
73
91
|
}
|
|
74
92
|
}
|
|
93
|
+
var _documentDragOverListener = null;
|
|
75
94
|
function attachDragListeners(card) {
|
|
76
95
|
card.addEventListener("dragstart", (e) => {
|
|
77
96
|
draggedCard = card;
|
|
78
97
|
sourceBody = card.parentElement;
|
|
79
98
|
card.classList.add("dragging");
|
|
80
99
|
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
|
100
|
+
const rect = card.getBoundingClientRect();
|
|
101
|
+
_dragOffsetX = e.clientX - rect.left;
|
|
102
|
+
_dragOffsetY = e.clientY - rect.top;
|
|
103
|
+
_dragMouseX = e.clientX;
|
|
104
|
+
_dragMouseY = e.clientY;
|
|
105
|
+
_documentDragOverListener = (ev) => {
|
|
106
|
+
_dragMouseX = ev.clientX;
|
|
107
|
+
_dragMouseY = ev.clientY;
|
|
108
|
+
if (_redrawDependencies) _redrawDependencies();
|
|
109
|
+
};
|
|
110
|
+
document.addEventListener("dragover", _documentDragOverListener);
|
|
81
111
|
});
|
|
82
112
|
card.addEventListener("dragend", () => {
|
|
83
113
|
card.classList.remove("dragging");
|
|
84
114
|
draggedCard = null;
|
|
85
115
|
sourceBody = null;
|
|
116
|
+
if (_documentDragOverListener) {
|
|
117
|
+
document.removeEventListener("dragover", _documentDragOverListener);
|
|
118
|
+
_documentDragOverListener = null;
|
|
119
|
+
}
|
|
86
120
|
});
|
|
87
121
|
}
|
|
88
122
|
function initDragDrop() {
|
|
@@ -219,14 +253,43 @@
|
|
|
219
253
|
});
|
|
220
254
|
input.placeholder = currentTags.length === 0 ? "Add tags..." : "";
|
|
221
255
|
}
|
|
256
|
+
async function createAndAddTag(name) {
|
|
257
|
+
const detailTaskId2 = _getDetailTaskId ? _getDetailTaskId() : null;
|
|
258
|
+
try {
|
|
259
|
+
const createRes = await fetch("/api/tags", {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: { "Content-Type": "application/json" },
|
|
262
|
+
body: JSON.stringify({ name })
|
|
263
|
+
});
|
|
264
|
+
if (!createRes.ok) throw new Error("Server error");
|
|
265
|
+
const newTag = await createRes.json();
|
|
266
|
+
allAvailableTags.push(newTag);
|
|
267
|
+
const taskRes = await fetch("/api/tasks/" + detailTaskId2 + "/tags", {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: { "Content-Type": "application/json" },
|
|
270
|
+
body: JSON.stringify({ tagId: newTag.id })
|
|
271
|
+
});
|
|
272
|
+
if (!taskRes.ok) throw new Error("Server error");
|
|
273
|
+
currentTags.push(newTag);
|
|
274
|
+
input.value = "";
|
|
275
|
+
inputValue = "";
|
|
276
|
+
renderPills();
|
|
277
|
+
renderDropdown();
|
|
278
|
+
} catch {
|
|
279
|
+
showToast("Failed to create tag");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
222
282
|
function renderDropdown() {
|
|
223
283
|
const filtered = getFilteredTags();
|
|
224
284
|
dropdown.innerHTML = "";
|
|
225
285
|
focusedOptionIndex = -1;
|
|
226
|
-
|
|
286
|
+
const hasInput = inputValue.trim() !== "";
|
|
287
|
+
const exactMatch = hasInput && allAvailableTags.some((t) => t.name.toLowerCase() === inputValue.trim().toLowerCase());
|
|
288
|
+
const showCreate = hasInput && !exactMatch;
|
|
289
|
+
if (filtered.length === 0 && !showCreate) {
|
|
227
290
|
const noOpt = document.createElement("div");
|
|
228
291
|
noOpt.className = "tag-select-no-options";
|
|
229
|
-
noOpt.textContent =
|
|
292
|
+
noOpt.textContent = hasInput ? "No matching tags" : "No tags available";
|
|
230
293
|
dropdown.appendChild(noOpt);
|
|
231
294
|
} else {
|
|
232
295
|
filtered.forEach((t, i) => {
|
|
@@ -241,6 +304,18 @@
|
|
|
241
304
|
});
|
|
242
305
|
dropdown.appendChild(opt);
|
|
243
306
|
});
|
|
307
|
+
if (showCreate) {
|
|
308
|
+
const createOpt = document.createElement("div");
|
|
309
|
+
createOpt.className = "tag-select-option tag-select-create-option";
|
|
310
|
+
createOpt.dataset.create = "true";
|
|
311
|
+
createOpt.textContent = `Create "${inputValue.trim()}"`;
|
|
312
|
+
createOpt.addEventListener("mouseover", () => setFocusedOption(filtered.length));
|
|
313
|
+
createOpt.addEventListener("mousedown", async (e) => {
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
await createAndAddTag(inputValue.trim());
|
|
316
|
+
});
|
|
317
|
+
dropdown.appendChild(createOpt);
|
|
318
|
+
}
|
|
244
319
|
}
|
|
245
320
|
}
|
|
246
321
|
function setFocusedOption(index) {
|
|
@@ -296,6 +371,8 @@
|
|
|
296
371
|
e.preventDefault();
|
|
297
372
|
if (focusedOptionIndex >= 0 && filtered[focusedOptionIndex]) {
|
|
298
373
|
await addTag(String(filtered[focusedOptionIndex].id));
|
|
374
|
+
} else if (focusedOptionIndex >= 0 && inputValue.trim()) {
|
|
375
|
+
await createAndAddTag(inputValue.trim());
|
|
299
376
|
}
|
|
300
377
|
} else if (e.key === "Escape") {
|
|
301
378
|
closeDropdown();
|
|
@@ -347,6 +424,7 @@
|
|
|
347
424
|
var _showUpdateWarning = null;
|
|
348
425
|
var _getDetailTaskId2 = null;
|
|
349
426
|
var _setActiveCard = null;
|
|
427
|
+
var _redrawDependencies2 = null;
|
|
350
428
|
function registerDetailPanelCallbacks(callbacks) {
|
|
351
429
|
_openTaskDetail = callbacks.openTaskDetail;
|
|
352
430
|
_renderDetailPanel = callbacks.renderDetailPanel;
|
|
@@ -354,6 +432,9 @@
|
|
|
354
432
|
_getDetailTaskId2 = callbacks.getDetailTaskId;
|
|
355
433
|
_setActiveCard = callbacks.setActiveCard;
|
|
356
434
|
}
|
|
435
|
+
function registerDependencyRedrawCallback2(callback) {
|
|
436
|
+
_redrawDependencies2 = callback;
|
|
437
|
+
}
|
|
357
438
|
function attachCardListeners(body) {
|
|
358
439
|
body.querySelectorAll(".card").forEach((card) => {
|
|
359
440
|
attachDragListeners(card);
|
|
@@ -447,6 +528,9 @@
|
|
|
447
528
|
if (detailTaskId2 !== null && _setActiveCard) {
|
|
448
529
|
_setActiveCard(detailTaskId2);
|
|
449
530
|
}
|
|
531
|
+
if (_redrawDependencies2) {
|
|
532
|
+
_redrawDependencies2();
|
|
533
|
+
}
|
|
450
534
|
if (detailTaskId2 !== null) {
|
|
451
535
|
await refreshOpenDetailPanel(detailTaskId2);
|
|
452
536
|
}
|
|
@@ -512,10 +596,13 @@
|
|
|
512
596
|
const filtered = getFilteredAddTags();
|
|
513
597
|
dropdown.innerHTML = "";
|
|
514
598
|
tagFocusedIndex = -1;
|
|
515
|
-
|
|
599
|
+
const hasInput = tagInputValue.trim() !== "";
|
|
600
|
+
const exactMatch = hasInput && allAvailableTags.some((t) => t.name.toLowerCase() === tagInputValue.trim().toLowerCase());
|
|
601
|
+
const showCreate = hasInput && !exactMatch;
|
|
602
|
+
if (filtered.length === 0 && !showCreate) {
|
|
516
603
|
const noOpt = document.createElement("div");
|
|
517
604
|
noOpt.className = "tag-select-no-options";
|
|
518
|
-
noOpt.textContent =
|
|
605
|
+
noOpt.textContent = hasInput ? "No matching tags" : "No tags available";
|
|
519
606
|
dropdown.appendChild(noOpt);
|
|
520
607
|
} else {
|
|
521
608
|
filtered.forEach((t, i) => {
|
|
@@ -530,6 +617,19 @@
|
|
|
530
617
|
});
|
|
531
618
|
dropdown.appendChild(opt);
|
|
532
619
|
});
|
|
620
|
+
if (showCreate) {
|
|
621
|
+
const createOpt = document.createElement("div");
|
|
622
|
+
createOpt.className = "tag-select-option tag-select-create-option";
|
|
623
|
+
createOpt.dataset.create = "true";
|
|
624
|
+
createOpt.textContent = `Create "${tagInputValue.trim()}"`;
|
|
625
|
+
createOpt.addEventListener("mouseover", () => setAddTagFocused(dropdown, filtered.length));
|
|
626
|
+
createOpt.addEventListener("mousedown", async (e) => {
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
const input = document.getElementById("add-tag-input");
|
|
629
|
+
await createAndSelectAddTag(tagInputValue.trim(), dropdown, input);
|
|
630
|
+
});
|
|
631
|
+
dropdown.appendChild(createOpt);
|
|
632
|
+
}
|
|
533
633
|
}
|
|
534
634
|
}
|
|
535
635
|
function setAddTagFocused(dropdown, index) {
|
|
@@ -547,6 +647,21 @@
|
|
|
547
647
|
renderAddTagPills(control, input);
|
|
548
648
|
renderAddTagDropdown(dropdown);
|
|
549
649
|
}
|
|
650
|
+
async function createAndSelectAddTag(name, dropdown, input) {
|
|
651
|
+
try {
|
|
652
|
+
const res = await fetch("/api/tags", {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: { "Content-Type": "application/json" },
|
|
655
|
+
body: JSON.stringify({ name })
|
|
656
|
+
});
|
|
657
|
+
if (!res.ok) throw new Error("Server error");
|
|
658
|
+
const newTag = await res.json();
|
|
659
|
+
allAvailableTags.push(newTag);
|
|
660
|
+
selectAddTag(newTag.id, dropdown, input);
|
|
661
|
+
} catch {
|
|
662
|
+
showToast("Failed to create tag");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
550
665
|
function initAddTagSelector() {
|
|
551
666
|
const control = document.getElementById("add-tag-select-control");
|
|
552
667
|
const dropdown = document.getElementById("add-tag-select-dropdown");
|
|
@@ -574,7 +689,7 @@
|
|
|
574
689
|
renderAddTagDropdown(dropdown);
|
|
575
690
|
if (!dropdown.classList.contains("open")) dropdown.classList.add("open");
|
|
576
691
|
});
|
|
577
|
-
input.addEventListener("keydown", (e) => {
|
|
692
|
+
input.addEventListener("keydown", async (e) => {
|
|
578
693
|
const filtered = getFilteredAddTags();
|
|
579
694
|
const opts = dropdown.querySelectorAll(".tag-select-option");
|
|
580
695
|
if (e.key === "ArrowDown") {
|
|
@@ -587,6 +702,8 @@
|
|
|
587
702
|
e.preventDefault();
|
|
588
703
|
if (tagFocusedIndex >= 0 && filtered[tagFocusedIndex]) {
|
|
589
704
|
selectAddTag(filtered[tagFocusedIndex].id, dropdown, input);
|
|
705
|
+
} else if (tagFocusedIndex >= 0 && tagInputValue.trim()) {
|
|
706
|
+
await createAndSelectAddTag(tagInputValue.trim(), dropdown, input);
|
|
590
707
|
}
|
|
591
708
|
} else if (e.key === "Escape") {
|
|
592
709
|
dropdown.classList.remove("open");
|
|
@@ -636,7 +753,7 @@
|
|
|
636
753
|
function resetAddModal(elements) {
|
|
637
754
|
elements.addTitle.value = "";
|
|
638
755
|
elements.addBody.value = "";
|
|
639
|
-
elements.addPriority.value = "";
|
|
756
|
+
elements.addPriority.value = "medium";
|
|
640
757
|
selectedTags = [];
|
|
641
758
|
tagInputValue = "";
|
|
642
759
|
tagFocusedIndex = -1;
|
|
@@ -764,6 +881,220 @@
|
|
|
764
881
|
});
|
|
765
882
|
}
|
|
766
883
|
|
|
884
|
+
// src/board/client/detailPanelApi.ts
|
|
885
|
+
var PANEL_MIN_WIDTH = 280;
|
|
886
|
+
var PANEL_MAX_WIDTH = 800;
|
|
887
|
+
var PANEL_DEFAULT_WIDTH = 400;
|
|
888
|
+
async function fetchComments(taskId) {
|
|
889
|
+
const res = await fetch("/api/tasks/" + taskId + "/comments");
|
|
890
|
+
if (!res.ok) throw new Error("Server error");
|
|
891
|
+
const data = await res.json();
|
|
892
|
+
return data.comments || [];
|
|
893
|
+
}
|
|
894
|
+
async function patchComment(commentId, content) {
|
|
895
|
+
const res = await fetch("/api/comments/" + commentId, {
|
|
896
|
+
method: "PATCH",
|
|
897
|
+
headers: { "Content-Type": "application/json" },
|
|
898
|
+
body: JSON.stringify({ content })
|
|
899
|
+
});
|
|
900
|
+
if (!res.ok) throw new Error("Server error");
|
|
901
|
+
}
|
|
902
|
+
async function deleteCommentRequest(commentId) {
|
|
903
|
+
const res = await fetch("/api/comments/" + commentId, { method: "DELETE" });
|
|
904
|
+
if (!res.ok) throw new Error("Server error");
|
|
905
|
+
}
|
|
906
|
+
async function postComment(taskId, content) {
|
|
907
|
+
const res = await fetch("/api/tasks/" + taskId + "/comments", {
|
|
908
|
+
method: "POST",
|
|
909
|
+
headers: { "Content-Type": "application/json" },
|
|
910
|
+
body: JSON.stringify({ content })
|
|
911
|
+
});
|
|
912
|
+
if (!res.ok) throw new Error("Server error");
|
|
913
|
+
}
|
|
914
|
+
async function fetchTaskDetail(taskId) {
|
|
915
|
+
const res = await fetch("/api/tasks/" + taskId);
|
|
916
|
+
if (!res.ok) throw new Error("Server error");
|
|
917
|
+
return res.json();
|
|
918
|
+
}
|
|
919
|
+
async function patchTask(taskId, fields) {
|
|
920
|
+
const res = await fetch("/api/tasks/" + taskId, {
|
|
921
|
+
method: "PATCH",
|
|
922
|
+
headers: { "Content-Type": "application/json" },
|
|
923
|
+
body: JSON.stringify(fields)
|
|
924
|
+
});
|
|
925
|
+
if (!res.ok) throw new Error("Server error");
|
|
926
|
+
return fetchTaskDetail(taskId);
|
|
927
|
+
}
|
|
928
|
+
async function syncTimestampAfterSave() {
|
|
929
|
+
try {
|
|
930
|
+
const tsRes = await fetch("/api/board/updated-at");
|
|
931
|
+
if (tsRes.ok) {
|
|
932
|
+
const tsData = await tsRes.json();
|
|
933
|
+
setLastUpdatedAt(tsData.updatedAt);
|
|
934
|
+
}
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async function fetchPanelWidthFromConfig() {
|
|
939
|
+
let targetWidth = PANEL_DEFAULT_WIDTH;
|
|
940
|
+
try {
|
|
941
|
+
const res = await fetch("/api/config");
|
|
942
|
+
if (res.ok) {
|
|
943
|
+
const data = await res.json();
|
|
944
|
+
const savedWidth = data && data.board && data.board.detailPaneWidth;
|
|
945
|
+
if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
|
|
946
|
+
targetWidth = savedWidth;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
return targetWidth;
|
|
952
|
+
}
|
|
953
|
+
function savePanelWidthToConfig(width) {
|
|
954
|
+
fetch("/api/config", {
|
|
955
|
+
method: "PUT",
|
|
956
|
+
headers: { "Content-Type": "application/json" },
|
|
957
|
+
body: JSON.stringify({ board: { detailPaneWidth: width } })
|
|
958
|
+
}).catch(function() {
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/board/client/detailPanelHtml.ts
|
|
963
|
+
function renderCommentItemHtml(comment, taskId) {
|
|
964
|
+
const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
|
|
965
|
+
const dateRel = relativeTime(comment.created_at);
|
|
966
|
+
const dateAbs = escapeHtmlClient(comment.created_at);
|
|
967
|
+
const contentText = escapeHtmlClient(comment.content);
|
|
968
|
+
let html = '<div class="comment-item" data-comment-id="' + comment.id + '">';
|
|
969
|
+
html += '<div class="comment-meta">';
|
|
970
|
+
html += '<span class="comment-author">' + authorText + "</span>";
|
|
971
|
+
html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
|
|
972
|
+
html += '<span class="comment-actions">';
|
|
973
|
+
html += '<button class="comment-action-btn" title="Edit" data-action="start-comment-edit" data-comment-id="' + comment.id + '">✎</button>';
|
|
974
|
+
html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">🗑</button>';
|
|
975
|
+
html += "</span></div>";
|
|
976
|
+
html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
|
|
977
|
+
html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
|
|
978
|
+
html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
|
|
979
|
+
html += '<div class="comment-edit-actions">';
|
|
980
|
+
html += '<button class="comment-btn" data-action="save-comment-edit" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">Save</button>';
|
|
981
|
+
html += '<button class="comment-btn" data-action="cancel-comment-edit" data-comment-id="' + comment.id + '">Cancel</button>';
|
|
982
|
+
html += "</div></div></div>";
|
|
983
|
+
return html;
|
|
984
|
+
}
|
|
985
|
+
function renderAddCommentFormHtml(taskId) {
|
|
986
|
+
let html = '<button class="add-comment-trigger" id="add-comment-trigger" data-action="open-add-comment">+ Add comment...</button>';
|
|
987
|
+
html += '<div class="add-comment-form" id="add-comment-form">';
|
|
988
|
+
html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
|
|
989
|
+
html += "<div>";
|
|
990
|
+
html += '<button class="add-comment-submit" data-action="submit-comment" data-task-id="' + taskId + '">Add Comment</button>';
|
|
991
|
+
html += '<button class="add-comment-cancel" data-action="close-add-comment">Cancel</button>';
|
|
992
|
+
html += "</div></div>";
|
|
993
|
+
return html;
|
|
994
|
+
}
|
|
995
|
+
function renderStatusField(currentStatus, allStatuses, statusLabels) {
|
|
996
|
+
let html = '<div class="detail-field">';
|
|
997
|
+
html += '<div class="detail-field-label">Status</div>';
|
|
998
|
+
html += '<select id="detail-edit-status" class="detail-edit-select">';
|
|
999
|
+
allStatuses.forEach((s) => {
|
|
1000
|
+
const selected = s === currentStatus ? " selected" : "";
|
|
1001
|
+
html += '<option value="' + s + '"' + selected + ">" + statusLabels[s] + "</option>";
|
|
1002
|
+
});
|
|
1003
|
+
html += "</select></div>";
|
|
1004
|
+
return html;
|
|
1005
|
+
}
|
|
1006
|
+
function renderPriorityField(currentPriority, allPriorities) {
|
|
1007
|
+
let html = '<div class="detail-field">';
|
|
1008
|
+
html += '<div class="detail-field-label">Priority</div>';
|
|
1009
|
+
html += '<select id="detail-edit-priority" class="detail-edit-select">';
|
|
1010
|
+
html += '<option value="">None</option>';
|
|
1011
|
+
allPriorities.forEach((p) => {
|
|
1012
|
+
const selected = currentPriority === p ? " selected" : "";
|
|
1013
|
+
html += '<option value="' + p + '"' + selected + ">" + p.charAt(0).toUpperCase() + p.slice(1) + "</option>";
|
|
1014
|
+
});
|
|
1015
|
+
html += "</select></div>";
|
|
1016
|
+
return html;
|
|
1017
|
+
}
|
|
1018
|
+
function renderRelationsHtml(parent, blockedBy, blocking) {
|
|
1019
|
+
let html = '<div class="detail-relations">';
|
|
1020
|
+
if (parent) {
|
|
1021
|
+
html += '<div class="detail-relation-row">';
|
|
1022
|
+
html += '<span class="detail-relation-label">Parent</span>';
|
|
1023
|
+
html += '<div class="detail-relation-ids"><span class="detail-relation-id detail-relation-link" data-task-id="' + parent.id + '">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
|
|
1024
|
+
html += "</div>";
|
|
1025
|
+
}
|
|
1026
|
+
if (blockedBy.length > 0) {
|
|
1027
|
+
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
|
|
1028
|
+
html += '<div class="detail-relation-ids">';
|
|
1029
|
+
blockedBy.forEach((t) => {
|
|
1030
|
+
html += '<span class="detail-relation-id detail-relation-link" data-task-id="' + t.id + '">#' + t.id + "</span>";
|
|
1031
|
+
});
|
|
1032
|
+
html += "</div></div>";
|
|
1033
|
+
}
|
|
1034
|
+
if (blocking.length > 0) {
|
|
1035
|
+
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
|
|
1036
|
+
html += '<div class="detail-relation-ids">';
|
|
1037
|
+
blocking.forEach((t) => {
|
|
1038
|
+
html += '<span class="detail-relation-id detail-relation-link" data-task-id="' + t.id + '">#' + t.id + "</span>";
|
|
1039
|
+
});
|
|
1040
|
+
html += "</div></div>";
|
|
1041
|
+
}
|
|
1042
|
+
html += "</div>";
|
|
1043
|
+
return html;
|
|
1044
|
+
}
|
|
1045
|
+
function renderMetadataTable(metadata) {
|
|
1046
|
+
if (metadata.length === 0) return "";
|
|
1047
|
+
let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
|
|
1048
|
+
html += '<table class="detail-meta-table">';
|
|
1049
|
+
metadata.forEach((m) => {
|
|
1050
|
+
html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
|
|
1051
|
+
});
|
|
1052
|
+
html += "</table></div>";
|
|
1053
|
+
return html;
|
|
1054
|
+
}
|
|
1055
|
+
function renderEditableTextFields(task) {
|
|
1056
|
+
let html = '<div class="detail-field"><div class="detail-field-label">Title</div>';
|
|
1057
|
+
html += '<input id="detail-edit-title" class="detail-edit-input" type="text" value="' + escapeHtmlClient(task.title) + '">';
|
|
1058
|
+
html += "</div>";
|
|
1059
|
+
html += '<div class="detail-field description-field-wrapper"><div class="detail-field-label">Description</div>';
|
|
1060
|
+
html += '<textarea id="detail-edit-body" class="detail-edit-textarea">' + escapeHtmlClient(task.body || "") + "</textarea>";
|
|
1061
|
+
html += "</div>";
|
|
1062
|
+
return html;
|
|
1063
|
+
}
|
|
1064
|
+
function renderDetailPanelHtml(data) {
|
|
1065
|
+
const task = data.task;
|
|
1066
|
+
const metadata = data.metadata || [];
|
|
1067
|
+
const blockedBy = data.blockedBy || [];
|
|
1068
|
+
const blocking = data.blocking || [];
|
|
1069
|
+
const parent = data.parent || null;
|
|
1070
|
+
const win = window;
|
|
1071
|
+
const allStatuses = win.allStatuses;
|
|
1072
|
+
const statusLabels = win.statusLabels;
|
|
1073
|
+
const allPriorities = win.allPriorities;
|
|
1074
|
+
let html = "";
|
|
1075
|
+
html += renderStatusField(task.status, allStatuses, statusLabels);
|
|
1076
|
+
html += renderPriorityField(task.priority, allPriorities);
|
|
1077
|
+
html += '<div class="detail-field"><div class="detail-field-label">Tags</div>';
|
|
1078
|
+
html += '<div id="detail-tags-container"></div></div>';
|
|
1079
|
+
const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
|
|
1080
|
+
if (hasRelations) {
|
|
1081
|
+
html += renderRelationsHtml(parent, blockedBy, blocking);
|
|
1082
|
+
}
|
|
1083
|
+
html += renderMetadataTable(metadata);
|
|
1084
|
+
html += renderEditableTextFields(task);
|
|
1085
|
+
return html;
|
|
1086
|
+
}
|
|
1087
|
+
function buildDetailPanelHtml() {
|
|
1088
|
+
return '<div class="detail-panel" id="detail-panel"><div class="detail-panel-resize-handle" id="detail-panel-resize-handle"></div><div class="detail-panel-header"><h2 id="detail-panel-title">Task Detail</h2><button class="detail-panel-copy-id" id="detail-panel-copy-id" title="Copy task ID">⎘</button><button class="detail-panel-close" id="detail-panel-close" title="Close">×</button></div><div class="detail-tabs" id="detail-tabs"><button class="detail-tab active" data-tab="details">Details</button><button class="detail-tab" data-tab="comments" id="detail-tab-comments">Comments</button></div><div class="detail-panel-body" id="detail-panel-body"><div class="detail-tab-content active" id="detail-tab-content-details"></div><div class="detail-tab-content" id="detail-tab-content-comments"></div></div><div class="detail-panel-footer" id="detail-panel-footer"><button id="detail-save-btn">Save</button></div></div>';
|
|
1089
|
+
}
|
|
1090
|
+
function autoResizeTextarea(el) {
|
|
1091
|
+
const scrollContainer = el.closest(".detail-tab-content");
|
|
1092
|
+
const scrollTop = scrollContainer?.scrollTop ?? 0;
|
|
1093
|
+
el.style.height = "auto";
|
|
1094
|
+
el.style.height = el.scrollHeight + "px";
|
|
1095
|
+
if (scrollContainer) scrollContainer.scrollTop = scrollTop;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
767
1098
|
// src/board/client/detailPanel.ts
|
|
768
1099
|
var detailTaskId = null;
|
|
769
1100
|
var lastTab = "details";
|
|
@@ -802,10 +1133,7 @@
|
|
|
802
1133
|
const pane = document.getElementById("detail-tab-content-comments");
|
|
803
1134
|
if (!pane) return;
|
|
804
1135
|
try {
|
|
805
|
-
const
|
|
806
|
-
if (!res.ok) throw new Error("Server error");
|
|
807
|
-
const data = await res.json();
|
|
808
|
-
const comments = data.comments || [];
|
|
1136
|
+
const comments = await fetchComments(taskId);
|
|
809
1137
|
if (tabBtn) tabBtn.textContent = "Comments (" + comments.length + ")";
|
|
810
1138
|
renderComments(taskId, comments);
|
|
811
1139
|
} catch (err) {
|
|
@@ -813,38 +1141,6 @@
|
|
|
813
1141
|
if (pane) pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load comments</div>';
|
|
814
1142
|
}
|
|
815
1143
|
}
|
|
816
|
-
function renderCommentItemHtml(comment, taskId) {
|
|
817
|
-
const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
|
|
818
|
-
const dateRel = relativeTime(comment.created_at);
|
|
819
|
-
const dateAbs = escapeHtmlClient(comment.created_at);
|
|
820
|
-
const contentText = escapeHtmlClient(comment.content);
|
|
821
|
-
let html = '<div class="comment-item" data-comment-id="' + comment.id + '">';
|
|
822
|
-
html += '<div class="comment-meta">';
|
|
823
|
-
html += '<span class="comment-author">' + authorText + "</span>";
|
|
824
|
-
html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
|
|
825
|
-
html += '<span class="comment-actions">';
|
|
826
|
-
html += '<button class="comment-action-btn" title="Edit" data-action="start-comment-edit" data-comment-id="' + comment.id + '">✎</button>';
|
|
827
|
-
html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">🗑</button>';
|
|
828
|
-
html += "</span></div>";
|
|
829
|
-
html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
|
|
830
|
-
html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
|
|
831
|
-
html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
|
|
832
|
-
html += '<div class="comment-edit-actions">';
|
|
833
|
-
html += '<button class="comment-btn" data-action="save-comment-edit" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">Save</button>';
|
|
834
|
-
html += '<button class="comment-btn" data-action="cancel-comment-edit" data-comment-id="' + comment.id + '">Cancel</button>';
|
|
835
|
-
html += "</div></div></div>";
|
|
836
|
-
return html;
|
|
837
|
-
}
|
|
838
|
-
function renderAddCommentFormHtml(taskId) {
|
|
839
|
-
let html = '<button class="add-comment-trigger" id="add-comment-trigger" data-action="open-add-comment">+ Add comment...</button>';
|
|
840
|
-
html += '<div class="add-comment-form" id="add-comment-form">';
|
|
841
|
-
html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
|
|
842
|
-
html += "<div>";
|
|
843
|
-
html += '<button class="add-comment-submit" data-action="submit-comment" data-task-id="' + taskId + '">Add Comment</button>';
|
|
844
|
-
html += '<button class="add-comment-cancel" data-action="close-add-comment">Cancel</button>';
|
|
845
|
-
html += "</div></div>";
|
|
846
|
-
return html;
|
|
847
|
-
}
|
|
848
1144
|
function renderComments(taskId, comments) {
|
|
849
1145
|
const pane = document.getElementById("detail-tab-content-comments");
|
|
850
1146
|
if (!pane) return;
|
|
@@ -903,12 +1199,7 @@
|
|
|
903
1199
|
return;
|
|
904
1200
|
}
|
|
905
1201
|
try {
|
|
906
|
-
|
|
907
|
-
method: "PATCH",
|
|
908
|
-
headers: { "Content-Type": "application/json" },
|
|
909
|
-
body: JSON.stringify({ content })
|
|
910
|
-
});
|
|
911
|
-
if (!res.ok) throw new Error("Server error");
|
|
1202
|
+
await patchComment(commentId, content);
|
|
912
1203
|
await loadComments(taskId);
|
|
913
1204
|
} catch {
|
|
914
1205
|
showToast("Failed to update comment");
|
|
@@ -917,8 +1208,7 @@
|
|
|
917
1208
|
async function deleteComment(commentId, taskId) {
|
|
918
1209
|
if (!confirm("Delete this comment?")) return;
|
|
919
1210
|
try {
|
|
920
|
-
|
|
921
|
-
if (!res.ok) throw new Error("Server error");
|
|
1211
|
+
await deleteCommentRequest(commentId);
|
|
922
1212
|
await loadComments(taskId);
|
|
923
1213
|
} catch {
|
|
924
1214
|
showToast("Failed to delete comment");
|
|
@@ -933,12 +1223,7 @@
|
|
|
933
1223
|
return;
|
|
934
1224
|
}
|
|
935
1225
|
try {
|
|
936
|
-
|
|
937
|
-
method: "POST",
|
|
938
|
-
headers: { "Content-Type": "application/json" },
|
|
939
|
-
body: JSON.stringify({ content })
|
|
940
|
-
});
|
|
941
|
-
if (!res.ok) throw new Error("Server error");
|
|
1226
|
+
await postComment(taskId, content);
|
|
942
1227
|
await loadComments(taskId);
|
|
943
1228
|
} catch {
|
|
944
1229
|
showToast("Failed to add comment");
|
|
@@ -977,103 +1262,6 @@
|
|
|
977
1262
|
const taskId = target.dataset.taskId ? Number(target.dataset.taskId) : NaN;
|
|
978
1263
|
dispatchCommentAction(action, commentId, taskId);
|
|
979
1264
|
}
|
|
980
|
-
function renderStatusField(currentStatus, allStatuses, statusLabels) {
|
|
981
|
-
let html = '<div class="detail-field">';
|
|
982
|
-
html += '<div class="detail-field-label">Status</div>';
|
|
983
|
-
html += '<select id="detail-edit-status" class="detail-edit-select">';
|
|
984
|
-
allStatuses.forEach((s) => {
|
|
985
|
-
const selected = s === currentStatus ? " selected" : "";
|
|
986
|
-
html += '<option value="' + s + '"' + selected + ">" + statusLabels[s] + "</option>";
|
|
987
|
-
});
|
|
988
|
-
html += "</select></div>";
|
|
989
|
-
return html;
|
|
990
|
-
}
|
|
991
|
-
function renderPriorityField(currentPriority, allPriorities) {
|
|
992
|
-
let html = '<div class="detail-field">';
|
|
993
|
-
html += '<div class="detail-field-label">Priority</div>';
|
|
994
|
-
html += '<select id="detail-edit-priority" class="detail-edit-select">';
|
|
995
|
-
html += '<option value="">None</option>';
|
|
996
|
-
allPriorities.forEach((p) => {
|
|
997
|
-
const selected = currentPriority === p ? " selected" : "";
|
|
998
|
-
html += '<option value="' + p + '"' + selected + ">" + p.charAt(0).toUpperCase() + p.slice(1) + "</option>";
|
|
999
|
-
});
|
|
1000
|
-
html += "</select></div>";
|
|
1001
|
-
return html;
|
|
1002
|
-
}
|
|
1003
|
-
function renderRelationsHtml(parent, blockedBy, blocking) {
|
|
1004
|
-
let html = '<div class="detail-relations">';
|
|
1005
|
-
if (parent) {
|
|
1006
|
-
html += '<div class="detail-relation-row">';
|
|
1007
|
-
html += '<span class="detail-relation-label">Parent</span>';
|
|
1008
|
-
html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
|
|
1009
|
-
html += "</div>";
|
|
1010
|
-
}
|
|
1011
|
-
if (blockedBy.length > 0) {
|
|
1012
|
-
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
|
|
1013
|
-
html += '<div class="detail-relation-ids">';
|
|
1014
|
-
blockedBy.forEach((t) => {
|
|
1015
|
-
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
1016
|
-
});
|
|
1017
|
-
html += "</div></div>";
|
|
1018
|
-
}
|
|
1019
|
-
if (blocking.length > 0) {
|
|
1020
|
-
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
|
|
1021
|
-
html += '<div class="detail-relation-ids">';
|
|
1022
|
-
blocking.forEach((t) => {
|
|
1023
|
-
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
1024
|
-
});
|
|
1025
|
-
html += "</div></div>";
|
|
1026
|
-
}
|
|
1027
|
-
html += "</div>";
|
|
1028
|
-
return html;
|
|
1029
|
-
}
|
|
1030
|
-
function autoResizeTextarea(el) {
|
|
1031
|
-
el.style.height = "auto";
|
|
1032
|
-
el.style.height = el.scrollHeight + "px";
|
|
1033
|
-
}
|
|
1034
|
-
function renderMetadataTable(metadata) {
|
|
1035
|
-
const otherMeta = metadata.filter((m) => m.key !== "priority");
|
|
1036
|
-
if (otherMeta.length === 0) return "";
|
|
1037
|
-
let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
|
|
1038
|
-
html += '<table class="detail-meta-table">';
|
|
1039
|
-
otherMeta.forEach((m) => {
|
|
1040
|
-
html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
|
|
1041
|
-
});
|
|
1042
|
-
html += "</table></div>";
|
|
1043
|
-
return html;
|
|
1044
|
-
}
|
|
1045
|
-
function renderEditableTextFields(task) {
|
|
1046
|
-
let html = '<div class="detail-field"><div class="detail-field-label">Title</div>';
|
|
1047
|
-
html += '<input id="detail-edit-title" class="detail-edit-input" type="text" value="' + escapeHtmlClient(task.title) + '">';
|
|
1048
|
-
html += "</div>";
|
|
1049
|
-
html += '<div class="detail-field description-field-wrapper"><div class="detail-field-label">Description</div>';
|
|
1050
|
-
html += '<textarea id="detail-edit-body" class="detail-edit-textarea">' + escapeHtmlClient(task.body || "") + "</textarea>";
|
|
1051
|
-
html += "</div>";
|
|
1052
|
-
return html;
|
|
1053
|
-
}
|
|
1054
|
-
function renderDetailPanelHtml(data) {
|
|
1055
|
-
const task = data.task;
|
|
1056
|
-
const metadata = data.metadata || [];
|
|
1057
|
-
const blockedBy = data.blockedBy || [];
|
|
1058
|
-
const blocking = data.blocking || [];
|
|
1059
|
-
const parent = data.parent || null;
|
|
1060
|
-
const win = window;
|
|
1061
|
-
const allStatuses = win.allStatuses;
|
|
1062
|
-
const statusLabels = win.statusLabels;
|
|
1063
|
-
const allPriorities = win.allPriorities;
|
|
1064
|
-
let html = "";
|
|
1065
|
-
html += renderStatusField(task.status, allStatuses, statusLabels);
|
|
1066
|
-
html += renderPriorityField(task.priority, allPriorities);
|
|
1067
|
-
html += '<div class="detail-field"><div class="detail-field-label">Tags</div>';
|
|
1068
|
-
html += '<div id="detail-tags-container"></div></div>';
|
|
1069
|
-
const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
|
|
1070
|
-
if (hasRelations) {
|
|
1071
|
-
html += renderRelationsHtml(parent, blockedBy, blocking);
|
|
1072
|
-
}
|
|
1073
|
-
html += renderMetadataTable(metadata);
|
|
1074
|
-
html += renderEditableTextFields(task);
|
|
1075
|
-
return html;
|
|
1076
|
-
}
|
|
1077
1265
|
function renderDetailPanel(data) {
|
|
1078
1266
|
document.getElementById("detail-panel-update-warning")?.remove();
|
|
1079
1267
|
const detailPanelTitle = document.getElementById("detail-panel-title");
|
|
@@ -1085,6 +1273,13 @@
|
|
|
1085
1273
|
if (detailsPane) {
|
|
1086
1274
|
detailsPane.innerHTML = renderDetailPanelHtml(data);
|
|
1087
1275
|
detailsPane.style.padding = "20px";
|
|
1276
|
+
detailsPane.querySelectorAll(".detail-relation-link[data-task-id]").forEach((el) => {
|
|
1277
|
+
el.style.cursor = "pointer";
|
|
1278
|
+
el.addEventListener("click", () => {
|
|
1279
|
+
const tid = el.dataset.taskId;
|
|
1280
|
+
if (tid) void openTaskDetail(tid);
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1088
1283
|
}
|
|
1089
1284
|
const footer = document.getElementById("detail-panel-footer");
|
|
1090
1285
|
if (footer) {
|
|
@@ -1100,7 +1295,8 @@
|
|
|
1100
1295
|
autoResizeTextarea(textarea);
|
|
1101
1296
|
});
|
|
1102
1297
|
}
|
|
1103
|
-
|
|
1298
|
+
renderTagsSection([...tags]);
|
|
1299
|
+
loadAllTags().catch((err) => {
|
|
1104
1300
|
console.error("[agkan] renderDetailPanel tags failed", err);
|
|
1105
1301
|
});
|
|
1106
1302
|
loadComments(task.id);
|
|
@@ -1110,9 +1306,7 @@
|
|
|
1110
1306
|
const detailPanel = document.getElementById("detail-panel");
|
|
1111
1307
|
const PANEL_DEFAULT_WIDTH2 = 400;
|
|
1112
1308
|
try {
|
|
1113
|
-
const
|
|
1114
|
-
if (!res.ok) throw new Error("Server error");
|
|
1115
|
-
const data = await res.json();
|
|
1309
|
+
const data = await fetchTaskDetail(taskId);
|
|
1116
1310
|
renderDetailPanel(data);
|
|
1117
1311
|
setActiveCard(Number(taskId));
|
|
1118
1312
|
if (!detailPanel.classList.contains("open")) {
|
|
@@ -1148,9 +1342,8 @@
|
|
|
1148
1342
|
reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
|
|
1149
1343
|
reloadBtn.addEventListener("click", async () => {
|
|
1150
1344
|
try {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const taskData = await taskRes.json();
|
|
1345
|
+
if (detailTaskId !== null) {
|
|
1346
|
+
const taskData = await fetchTaskDetail(detailTaskId);
|
|
1154
1347
|
renderDetailPanel(taskData);
|
|
1155
1348
|
}
|
|
1156
1349
|
} catch {
|
|
@@ -1175,24 +1368,13 @@
|
|
|
1175
1368
|
priority: priorityEl ? priorityEl.value || null : null
|
|
1176
1369
|
};
|
|
1177
1370
|
}
|
|
1178
|
-
async function patchAndReloadDetail(taskId, fields) {
|
|
1179
|
-
const res = await fetch("/api/tasks/" + taskId, {
|
|
1180
|
-
method: "PATCH",
|
|
1181
|
-
headers: { "Content-Type": "application/json" },
|
|
1182
|
-
body: JSON.stringify(fields)
|
|
1183
|
-
});
|
|
1184
|
-
if (!res.ok) throw new Error("Server error");
|
|
1185
|
-
const getRes = await fetch("/api/tasks/" + taskId);
|
|
1186
|
-
if (!getRes.ok) throw new Error("Failed to fetch updated task");
|
|
1187
|
-
const data = await getRes.json();
|
|
1188
|
-
renderDetailPanel(data);
|
|
1189
|
-
}
|
|
1190
1371
|
async function saveDetailTask() {
|
|
1191
1372
|
if (detailTaskId === null) return;
|
|
1192
1373
|
const fields = collectEditedTaskFields();
|
|
1193
1374
|
if (!fields) return;
|
|
1194
1375
|
try {
|
|
1195
|
-
await
|
|
1376
|
+
const data = await patchTask(detailTaskId, fields);
|
|
1377
|
+
renderDetailPanel(data);
|
|
1196
1378
|
showToast("Task saved successfully");
|
|
1197
1379
|
await syncTimestampAfterSave();
|
|
1198
1380
|
refreshBoardCards();
|
|
@@ -1200,32 +1382,8 @@
|
|
|
1200
1382
|
showToast("Failed to update task");
|
|
1201
1383
|
}
|
|
1202
1384
|
}
|
|
1203
|
-
async function syncTimestampAfterSave() {
|
|
1204
|
-
try {
|
|
1205
|
-
const tsRes = await fetch("/api/board/updated-at");
|
|
1206
|
-
if (tsRes.ok) {
|
|
1207
|
-
const tsData = await tsRes.json();
|
|
1208
|
-
setLastUpdatedAt(tsData.updatedAt);
|
|
1209
|
-
}
|
|
1210
|
-
} catch {
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
var PANEL_MIN_WIDTH = 280;
|
|
1214
|
-
var PANEL_MAX_WIDTH = 800;
|
|
1215
|
-
var PANEL_DEFAULT_WIDTH = 400;
|
|
1216
1385
|
async function initPanelWidthFromConfig(detailPanel) {
|
|
1217
|
-
|
|
1218
|
-
try {
|
|
1219
|
-
const res = await fetch("/api/config");
|
|
1220
|
-
if (res.ok) {
|
|
1221
|
-
const data = await res.json();
|
|
1222
|
-
const savedWidth = data && data.board && data.board.detailPaneWidth;
|
|
1223
|
-
if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
|
|
1224
|
-
targetWidth = savedWidth;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
} catch {
|
|
1228
|
-
}
|
|
1386
|
+
const targetWidth = await fetchPanelWidthFromConfig();
|
|
1229
1387
|
detailPanel.dataset.preferredWidth = String(targetWidth);
|
|
1230
1388
|
}
|
|
1231
1389
|
function attachResizeMousedown(resizeHandle, detailPanel) {
|
|
@@ -1250,12 +1408,7 @@
|
|
|
1250
1408
|
detailPanel.style.transition = "";
|
|
1251
1409
|
const currentWidth = detailPanel.offsetWidth;
|
|
1252
1410
|
detailPanel.dataset.preferredWidth = String(currentWidth);
|
|
1253
|
-
|
|
1254
|
-
method: "PUT",
|
|
1255
|
-
headers: { "Content-Type": "application/json" },
|
|
1256
|
-
body: JSON.stringify({ board: { detailPaneWidth: currentWidth } })
|
|
1257
|
-
}).catch(function() {
|
|
1258
|
-
});
|
|
1411
|
+
savePanelWidthToConfig(currentWidth);
|
|
1259
1412
|
document.removeEventListener("mousemove", onMouseMove);
|
|
1260
1413
|
document.removeEventListener("mouseup", onMouseUp);
|
|
1261
1414
|
}
|
|
@@ -1268,14 +1421,17 @@
|
|
|
1268
1421
|
initPanelWidthFromConfig(detailPanel);
|
|
1269
1422
|
attachResizeMousedown(resizeHandle, detailPanel);
|
|
1270
1423
|
}
|
|
1271
|
-
function buildDetailPanelHtml() {
|
|
1272
|
-
return '<div class="detail-panel" id="detail-panel"><div class="detail-panel-resize-handle" id="detail-panel-resize-handle"></div><div class="detail-panel-header"><h2 id="detail-panel-title">Task Detail</h2><button class="detail-panel-close" id="detail-panel-close" title="Close">×</button></div><div class="detail-tabs" id="detail-tabs"><button class="detail-tab active" data-tab="details">Details</button><button class="detail-tab" data-tab="comments" id="detail-tab-comments">Comments</button></div><div class="detail-panel-body" id="detail-panel-body"><div class="detail-tab-content active" id="detail-tab-content-details"></div><div class="detail-tab-content" id="detail-tab-content-comments"></div></div><div class="detail-panel-footer" id="detail-panel-footer"><button id="detail-save-btn">Save</button></div></div>';
|
|
1273
|
-
}
|
|
1274
1424
|
function initDetailPanel() {
|
|
1275
1425
|
const boardContainer = document.querySelector(".board-container");
|
|
1276
1426
|
boardContainer.insertAdjacentHTML("beforeend", buildDetailPanelHtml());
|
|
1277
1427
|
const detailPanel = document.getElementById("detail-panel");
|
|
1278
1428
|
document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
|
|
1429
|
+
document.getElementById("detail-panel-copy-id")?.addEventListener("click", () => {
|
|
1430
|
+
if (detailTaskId === null) return;
|
|
1431
|
+
navigator.clipboard.writeText(String(detailTaskId)).then(() => {
|
|
1432
|
+
showToast("Copied task ID: " + detailTaskId);
|
|
1433
|
+
});
|
|
1434
|
+
});
|
|
1279
1435
|
document.addEventListener("keydown", (e) => {
|
|
1280
1436
|
if (e.key === "Escape" && detailPanel.classList.contains("open")) {
|
|
1281
1437
|
closeDetailPanel();
|
|
@@ -1687,6 +1843,232 @@
|
|
|
1687
1843
|
initDarkMode();
|
|
1688
1844
|
}
|
|
1689
1845
|
|
|
1846
|
+
// src/board/client/dependencyVisualization.ts
|
|
1847
|
+
var isDependencyVisible = false;
|
|
1848
|
+
var arrowMarkers = /* @__PURE__ */ new Map();
|
|
1849
|
+
var scrollListener = null;
|
|
1850
|
+
var columnScrollListener = null;
|
|
1851
|
+
var resizeListener = null;
|
|
1852
|
+
function getOrCreateArrowMarker(svg, color) {
|
|
1853
|
+
const markerId = `arrow-${color.substring(1)}`;
|
|
1854
|
+
if (!arrowMarkers.has(markerId)) {
|
|
1855
|
+
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
1856
|
+
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
|
|
1857
|
+
marker.setAttribute("id", markerId);
|
|
1858
|
+
marker.setAttribute("markerWidth", "10");
|
|
1859
|
+
marker.setAttribute("markerHeight", "10");
|
|
1860
|
+
marker.setAttribute("refX", "8");
|
|
1861
|
+
marker.setAttribute("refY", "3");
|
|
1862
|
+
marker.setAttribute("orient", "auto");
|
|
1863
|
+
marker.setAttribute("markerUnits", "strokeWidth");
|
|
1864
|
+
const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
1865
|
+
polygon.setAttribute("points", "0 0, 10 3, 0 6");
|
|
1866
|
+
polygon.setAttribute("fill", color);
|
|
1867
|
+
marker.appendChild(polygon);
|
|
1868
|
+
defs.appendChild(marker);
|
|
1869
|
+
svg.appendChild(defs);
|
|
1870
|
+
arrowMarkers.set(markerId, { svg });
|
|
1871
|
+
}
|
|
1872
|
+
return markerId;
|
|
1873
|
+
}
|
|
1874
|
+
function drawBezierLine(svg, x1, y1, x2, y2, color, isHovered) {
|
|
1875
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1876
|
+
const dx = Math.abs(x2 - x1);
|
|
1877
|
+
const isSameSide = dx < 10;
|
|
1878
|
+
let cp1x;
|
|
1879
|
+
let cp2x;
|
|
1880
|
+
if (isSameSide) {
|
|
1881
|
+
const cpOffset = 80;
|
|
1882
|
+
cp1x = x1 + cpOffset;
|
|
1883
|
+
cp2x = x2 + cpOffset;
|
|
1884
|
+
} else {
|
|
1885
|
+
const cpOffset = Math.max(dx * 0.5, 60);
|
|
1886
|
+
cp1x = x1 < x2 ? x1 + cpOffset : x1 - cpOffset;
|
|
1887
|
+
cp2x = x1 < x2 ? x2 - cpOffset : x2 + cpOffset;
|
|
1888
|
+
}
|
|
1889
|
+
const pathData = `M ${x1} ${y1} C ${cp1x} ${y1}, ${cp2x} ${y2}, ${x2} ${y2}`;
|
|
1890
|
+
path.setAttribute("d", pathData);
|
|
1891
|
+
path.setAttribute("stroke", color);
|
|
1892
|
+
path.setAttribute("stroke-width", isHovered ? "2.5" : "1.5");
|
|
1893
|
+
path.setAttribute("fill", "none");
|
|
1894
|
+
path.setAttribute("stroke-linecap", "round");
|
|
1895
|
+
path.setAttribute("marker-end", `url(#${getOrCreateArrowMarker(svg, color)})`);
|
|
1896
|
+
path.setAttribute("class", "dependency-line");
|
|
1897
|
+
return path;
|
|
1898
|
+
}
|
|
1899
|
+
function getCardRect(card) {
|
|
1900
|
+
if (card === draggedCard) {
|
|
1901
|
+
return getDraggedCardVirtualRect() ?? card.getBoundingClientRect();
|
|
1902
|
+
}
|
|
1903
|
+
return card.getBoundingClientRect();
|
|
1904
|
+
}
|
|
1905
|
+
function getCardEdgePoints(fromCard, toCard, boardRect) {
|
|
1906
|
+
const fromRect = getCardRect(fromCard);
|
|
1907
|
+
const toRect = getCardRect(toCard);
|
|
1908
|
+
const fromCenterX = fromRect.left + fromRect.width / 2;
|
|
1909
|
+
const toCenterX = toRect.left + toRect.width / 2;
|
|
1910
|
+
const columnThreshold = 50;
|
|
1911
|
+
let fromX;
|
|
1912
|
+
let toX;
|
|
1913
|
+
if (Math.abs(fromCenterX - toCenterX) < columnThreshold) {
|
|
1914
|
+
fromX = fromRect.right - boardRect.left;
|
|
1915
|
+
toX = toRect.right - boardRect.left;
|
|
1916
|
+
} else if (fromCenterX <= toCenterX) {
|
|
1917
|
+
fromX = fromRect.right - boardRect.left;
|
|
1918
|
+
toX = toRect.left - boardRect.left;
|
|
1919
|
+
} else {
|
|
1920
|
+
fromX = fromRect.left - boardRect.left;
|
|
1921
|
+
toX = toRect.right - boardRect.left;
|
|
1922
|
+
}
|
|
1923
|
+
return {
|
|
1924
|
+
x1: fromX,
|
|
1925
|
+
y1: fromRect.top - boardRect.top + fromRect.height / 2,
|
|
1926
|
+
x2: toX,
|
|
1927
|
+
y2: toRect.top - boardRect.top + toRect.height / 2
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
function createSVGOverlay() {
|
|
1931
|
+
const boardContainer = document.querySelector(".board-container");
|
|
1932
|
+
const existing = boardContainer.querySelector("#dependency-svg");
|
|
1933
|
+
if (existing) {
|
|
1934
|
+
existing.remove();
|
|
1935
|
+
}
|
|
1936
|
+
arrowMarkers.clear();
|
|
1937
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1938
|
+
svg.setAttribute("id", "dependency-svg");
|
|
1939
|
+
svg.setAttribute("width", "100%");
|
|
1940
|
+
svg.setAttribute("height", "100%");
|
|
1941
|
+
svg.setAttribute("viewBox", `0 0 ${boardContainer.offsetWidth} ${boardContainer.offsetHeight}`);
|
|
1942
|
+
svg.style.position = "absolute";
|
|
1943
|
+
svg.style.top = "0";
|
|
1944
|
+
svg.style.left = "0";
|
|
1945
|
+
svg.style.pointerEvents = "none";
|
|
1946
|
+
svg.style.zIndex = "5";
|
|
1947
|
+
boardContainer.style.position = "relative";
|
|
1948
|
+
boardContainer.appendChild(svg);
|
|
1949
|
+
return svg;
|
|
1950
|
+
}
|
|
1951
|
+
function redrawDependencies() {
|
|
1952
|
+
if (!isDependencyVisible) return;
|
|
1953
|
+
const svg = createSVGOverlay();
|
|
1954
|
+
const boardContainer = document.querySelector(".board-container");
|
|
1955
|
+
svg.querySelectorAll(".dependency-line").forEach((line) => line.remove());
|
|
1956
|
+
const cards = Array.from(document.querySelectorAll(".card"));
|
|
1957
|
+
const cardMap = /* @__PURE__ */ new Map();
|
|
1958
|
+
cards.forEach((card) => {
|
|
1959
|
+
const id = Number(card.getAttribute("data-id"));
|
|
1960
|
+
cardMap.set(id, card);
|
|
1961
|
+
});
|
|
1962
|
+
const hoveredCard = document.querySelector(".card:hover");
|
|
1963
|
+
const hoveredCardId = hoveredCard ? Number(hoveredCard.getAttribute("data-id")) : null;
|
|
1964
|
+
const hoveredBlockedBySet = /* @__PURE__ */ new Set();
|
|
1965
|
+
const hoveredBlockingSet = /* @__PURE__ */ new Set();
|
|
1966
|
+
if (hoveredCardId) {
|
|
1967
|
+
const hoveredElement = cardMap.get(hoveredCardId);
|
|
1968
|
+
if (hoveredElement) {
|
|
1969
|
+
const blockedBy = hoveredElement.getAttribute("data-blocked-by");
|
|
1970
|
+
const blocking = hoveredElement.getAttribute("data-blocking");
|
|
1971
|
+
if (blockedBy) {
|
|
1972
|
+
blockedBy.split(",").forEach((id) => {
|
|
1973
|
+
const numId = Number(id.trim());
|
|
1974
|
+
if (!isNaN(numId)) hoveredBlockedBySet.add(numId);
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
if (blocking) {
|
|
1978
|
+
blocking.split(",").forEach((id) => {
|
|
1979
|
+
const numId = Number(id.trim());
|
|
1980
|
+
if (!isNaN(numId)) hoveredBlockingSet.add(numId);
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const boardRect = boardContainer.getBoundingClientRect();
|
|
1986
|
+
cards.forEach((card) => {
|
|
1987
|
+
const cardId = Number(card.getAttribute("data-id"));
|
|
1988
|
+
const blockedByStr = card.getAttribute("data-blocked-by");
|
|
1989
|
+
const blockingStr = card.getAttribute("data-blocking");
|
|
1990
|
+
if (!blockedByStr && !blockingStr) return;
|
|
1991
|
+
const isHovered = cardId === hoveredCardId || hoveredBlockedBySet.has(cardId) || hoveredBlockingSet.has(cardId);
|
|
1992
|
+
if (blockingStr) {
|
|
1993
|
+
const blockingIds = blockingStr.split(",").map((s) => Number(s.trim()));
|
|
1994
|
+
blockingIds.forEach((blockedId) => {
|
|
1995
|
+
const blockedCard = cardMap.get(blockedId);
|
|
1996
|
+
if (blockedCard) {
|
|
1997
|
+
const { x1, y1, x2, y2 } = getCardEdgePoints(card, blockedCard, boardRect);
|
|
1998
|
+
const color = isHovered || hoveredBlockedBySet.has(blockedId) ? "#ef4444" : "#cbd5e1";
|
|
1999
|
+
const line = drawBezierLine(svg, x1, y1, x2, y2, color, isHovered);
|
|
2000
|
+
svg.appendChild(line);
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
svg.setAttribute("viewBox", `0 0 ${boardContainer.offsetWidth} ${boardContainer.offsetHeight}`);
|
|
2006
|
+
}
|
|
2007
|
+
function handleCardHoverEvents() {
|
|
2008
|
+
const cards = document.querySelectorAll(".card");
|
|
2009
|
+
cards.forEach((card) => {
|
|
2010
|
+
card.addEventListener("mouseenter", () => {
|
|
2011
|
+
redrawDependencies();
|
|
2012
|
+
});
|
|
2013
|
+
card.addEventListener("mouseleave", () => {
|
|
2014
|
+
redrawDependencies();
|
|
2015
|
+
});
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
function initDependencyVisualization() {
|
|
2019
|
+
const toggleBtn = document.getElementById("dependency-toggle");
|
|
2020
|
+
if (!toggleBtn) return;
|
|
2021
|
+
const redrawIfVisible = () => {
|
|
2022
|
+
if (isDependencyVisible) {
|
|
2023
|
+
handleCardHoverEvents();
|
|
2024
|
+
redrawDependencies();
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
registerDependencyRedrawCallback2(redrawIfVisible);
|
|
2028
|
+
registerDependencyRedrawCallback(redrawIfVisible);
|
|
2029
|
+
toggleBtn.addEventListener("click", () => {
|
|
2030
|
+
isDependencyVisible = !isDependencyVisible;
|
|
2031
|
+
if (isDependencyVisible) {
|
|
2032
|
+
toggleBtn.classList.add("active");
|
|
2033
|
+
redrawDependencies();
|
|
2034
|
+
handleCardHoverEvents();
|
|
2035
|
+
const board = document.querySelector(".board");
|
|
2036
|
+
const boardContainer = document.querySelector(".board-container");
|
|
2037
|
+
if (board) {
|
|
2038
|
+
scrollListener = () => redrawDependencies();
|
|
2039
|
+
board.addEventListener("scroll", scrollListener, { passive: true });
|
|
2040
|
+
}
|
|
2041
|
+
columnScrollListener = () => redrawDependencies();
|
|
2042
|
+
document.querySelectorAll(".column-body").forEach((col) => {
|
|
2043
|
+
col.addEventListener("scroll", columnScrollListener, { passive: true });
|
|
2044
|
+
});
|
|
2045
|
+
if (boardContainer) {
|
|
2046
|
+
resizeListener = () => redrawDependencies();
|
|
2047
|
+
window.addEventListener("resize", resizeListener, { passive: true });
|
|
2048
|
+
}
|
|
2049
|
+
} else {
|
|
2050
|
+
toggleBtn.classList.remove("active");
|
|
2051
|
+
const svg = document.querySelector("#dependency-svg");
|
|
2052
|
+
if (svg) svg.remove();
|
|
2053
|
+
const board = document.querySelector(".board");
|
|
2054
|
+
if (board && scrollListener) {
|
|
2055
|
+
board.removeEventListener("scroll", scrollListener);
|
|
2056
|
+
scrollListener = null;
|
|
2057
|
+
}
|
|
2058
|
+
if (columnScrollListener) {
|
|
2059
|
+
document.querySelectorAll(".column-body").forEach((col) => {
|
|
2060
|
+
col.removeEventListener("scroll", columnScrollListener);
|
|
2061
|
+
});
|
|
2062
|
+
columnScrollListener = null;
|
|
2063
|
+
}
|
|
2064
|
+
if (resizeListener) {
|
|
2065
|
+
window.removeEventListener("resize", resizeListener);
|
|
2066
|
+
resizeListener = null;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1690
2072
|
// src/board/client/main.ts
|
|
1691
2073
|
initDragDrop();
|
|
1692
2074
|
initAutoScroll();
|
|
@@ -1696,4 +2078,5 @@
|
|
|
1696
2078
|
initBoardPolling();
|
|
1697
2079
|
initFilters();
|
|
1698
2080
|
initBurgerMenu();
|
|
2081
|
+
initDependencyVisualization();
|
|
1699
2082
|
})();
|