agkan 2.12.0 → 2.13.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.
Files changed (108) hide show
  1. package/dist/board/boardConfig.d.ts +3 -0
  2. package/dist/board/boardConfig.d.ts.map +1 -1
  3. package/dist/board/boardConfig.js +7 -1
  4. package/dist/board/boardConfig.js.map +1 -1
  5. package/dist/board/boardRenderer.d.ts +1 -1
  6. package/dist/board/boardRenderer.d.ts.map +1 -1
  7. package/dist/board/boardRenderer.js +12 -8
  8. package/dist/board/boardRenderer.js.map +1 -1
  9. package/dist/board/boardRoutes.d.ts.map +1 -1
  10. package/dist/board/boardRoutes.js +13 -1
  11. package/dist/board/boardRoutes.js.map +1 -1
  12. package/dist/board/boardStyles.d.ts +1 -1
  13. package/dist/board/boardStyles.d.ts.map +1 -1
  14. package/dist/board/boardStyles.js +119 -78
  15. package/dist/board/boardStyles.js.map +1 -1
  16. package/dist/board/client/board.js +518 -317
  17. package/dist/cli/commands/block/add.d.ts.map +1 -1
  18. package/dist/cli/commands/block/add.js +156 -110
  19. package/dist/cli/commands/block/add.js.map +1 -1
  20. package/dist/cli/commands/block/list.d.ts.map +1 -1
  21. package/dist/cli/commands/block/list.js +2 -3
  22. package/dist/cli/commands/block/list.js.map +1 -1
  23. package/dist/cli/commands/block/remove.d.ts.map +1 -1
  24. package/dist/cli/commands/block/remove.js +2 -3
  25. package/dist/cli/commands/block/remove.js.map +1 -1
  26. package/dist/cli/commands/comment/add.d.ts.map +1 -1
  27. package/dist/cli/commands/comment/add.js +2 -3
  28. package/dist/cli/commands/comment/add.js.map +1 -1
  29. package/dist/cli/commands/comment/delete.js +2 -2
  30. package/dist/cli/commands/comment/delete.js.map +1 -1
  31. package/dist/cli/commands/comment/list.d.ts.map +1 -1
  32. package/dist/cli/commands/comment/list.js +2 -3
  33. package/dist/cli/commands/comment/list.js.map +1 -1
  34. package/dist/cli/commands/meta/delete.d.ts.map +1 -1
  35. package/dist/cli/commands/meta/delete.js +2 -3
  36. package/dist/cli/commands/meta/delete.js.map +1 -1
  37. package/dist/cli/commands/meta/get.d.ts.map +1 -1
  38. package/dist/cli/commands/meta/get.js +2 -3
  39. package/dist/cli/commands/meta/get.js.map +1 -1
  40. package/dist/cli/commands/meta/list.d.ts.map +1 -1
  41. package/dist/cli/commands/meta/list.js +2 -3
  42. package/dist/cli/commands/meta/list.js.map +1 -1
  43. package/dist/cli/commands/meta/set.d.ts.map +1 -1
  44. package/dist/cli/commands/meta/set.js +2 -3
  45. package/dist/cli/commands/meta/set.js.map +1 -1
  46. package/dist/cli/commands/tag/add.js +2 -2
  47. package/dist/cli/commands/tag/add.js.map +1 -1
  48. package/dist/cli/commands/tag/attach.d.ts.map +1 -1
  49. package/dist/cli/commands/tag/attach.js +2 -4
  50. package/dist/cli/commands/tag/attach.js.map +1 -1
  51. package/dist/cli/commands/tag/delete.js +2 -2
  52. package/dist/cli/commands/tag/delete.js.map +1 -1
  53. package/dist/cli/commands/tag/detach.d.ts.map +1 -1
  54. package/dist/cli/commands/tag/detach.js +2 -4
  55. package/dist/cli/commands/tag/detach.js.map +1 -1
  56. package/dist/cli/commands/tag/list.d.ts.map +1 -1
  57. package/dist/cli/commands/tag/list.js +2 -3
  58. package/dist/cli/commands/tag/list.js.map +1 -1
  59. package/dist/cli/commands/tag/rename.js +2 -2
  60. package/dist/cli/commands/tag/rename.js.map +1 -1
  61. package/dist/cli/commands/tag/show.d.ts.map +1 -1
  62. package/dist/cli/commands/tag/show.js +2 -3
  63. package/dist/cli/commands/tag/show.js.map +1 -1
  64. package/dist/cli/commands/task/add.d.ts.map +1 -1
  65. package/dist/cli/commands/task/add.js +3 -3
  66. package/dist/cli/commands/task/add.js.map +1 -1
  67. package/dist/cli/commands/task/count.js +2 -2
  68. package/dist/cli/commands/task/count.js.map +1 -1
  69. package/dist/cli/commands/task/delete.js +2 -2
  70. package/dist/cli/commands/task/delete.js.map +1 -1
  71. package/dist/cli/commands/task/find.d.ts.map +1 -1
  72. package/dist/cli/commands/task/find.js +2 -3
  73. package/dist/cli/commands/task/find.js.map +1 -1
  74. package/dist/cli/commands/task/get.d.ts.map +1 -1
  75. package/dist/cli/commands/task/get.js +204 -209
  76. package/dist/cli/commands/task/get.js.map +1 -1
  77. package/dist/cli/commands/task/list.d.ts.map +1 -1
  78. package/dist/cli/commands/task/list.js +324 -161
  79. package/dist/cli/commands/task/list.js.map +1 -1
  80. package/dist/cli/commands/task/purge.js +2 -2
  81. package/dist/cli/commands/task/purge.js.map +1 -1
  82. package/dist/cli/commands/task/update-helpers.d.ts +42 -0
  83. package/dist/cli/commands/task/update-helpers.d.ts.map +1 -0
  84. package/dist/cli/commands/task/update-helpers.js +154 -0
  85. package/dist/cli/commands/task/update-helpers.js.map +1 -0
  86. package/dist/cli/commands/task/update-parent.js +2 -2
  87. package/dist/cli/commands/task/update-parent.js.map +1 -1
  88. package/dist/cli/commands/task/update.d.ts.map +1 -1
  89. package/dist/cli/commands/task/update.js +81 -196
  90. package/dist/cli/commands/task/update.js.map +1 -1
  91. package/dist/cli/utils/error-handler.js +3 -3
  92. package/dist/cli/utils/error-handler.js.map +1 -1
  93. package/dist/cli/utils/output-formatter.d.ts +14 -0
  94. package/dist/cli/utils/output-formatter.d.ts.map +1 -1
  95. package/dist/cli/utils/output-formatter.js +36 -0
  96. package/dist/cli/utils/output-formatter.js.map +1 -1
  97. package/dist/cli/utils/service-container.d.ts +25 -0
  98. package/dist/cli/utils/service-container.d.ts.map +1 -0
  99. package/dist/cli/utils/service-container.js +26 -0
  100. package/dist/cli/utils/service-container.js.map +1 -0
  101. package/dist/services/TaskService.d.ts +14 -0
  102. package/dist/services/TaskService.d.ts.map +1 -1
  103. package/dist/services/TaskService.js +48 -29
  104. package/dist/services/TaskService.js.map +1 -1
  105. package/dist/utils/input-validators.d.ts.map +1 -1
  106. package/dist/utils/input-validators.js +64 -69
  107. package/dist/utils/input-validators.js.map +1 -1
  108. package/package.json +4 -3
@@ -150,63 +150,83 @@
150
150
  }
151
151
 
152
152
  // src/board/client/addTaskModal.ts
153
+ function openAddModal(elements, status) {
154
+ elements.addStatus.value = status;
155
+ elements.addTitle.value = "";
156
+ elements.addBody.value = "";
157
+ elements.addPriority.value = "";
158
+ elements.addModal.classList.add("show");
159
+ elements.addTitle.focus();
160
+ }
161
+ async function submitAddTask(elements) {
162
+ const title = elements.addTitle.value.trim();
163
+ if (!title) {
164
+ elements.addTitle.focus();
165
+ return;
166
+ }
167
+ const status = elements.addStatus.value;
168
+ elements.addModal.classList.remove("show");
169
+ try {
170
+ const res = await fetch("/api/tasks", {
171
+ method: "POST",
172
+ headers: { "Content-Type": "application/json" },
173
+ body: JSON.stringify({
174
+ title,
175
+ body: elements.addBody.value.trim() || null,
176
+ status,
177
+ priority: elements.addPriority.value || null
178
+ })
179
+ });
180
+ if (!res.ok) throw new Error("Server error");
181
+ location.reload();
182
+ } catch {
183
+ showToast("Failed to add task");
184
+ }
185
+ }
153
186
  function initAddTaskModal() {
154
- const addModal = document.getElementById("add-modal");
155
- const addTitle = document.getElementById("add-title");
156
- const addBody = document.getElementById("add-body");
157
- const addPriority = document.getElementById("add-priority");
158
- const addStatus = document.getElementById("add-status");
187
+ const elements = {
188
+ addModal: document.getElementById("add-modal"),
189
+ addTitle: document.getElementById("add-title"),
190
+ addBody: document.getElementById("add-body"),
191
+ addPriority: document.getElementById("add-priority"),
192
+ addStatus: document.getElementById("add-status")
193
+ };
159
194
  document.querySelectorAll(".add-btn").forEach((btn) => {
160
195
  btn.addEventListener("click", (e) => {
161
196
  e.stopPropagation();
162
- addStatus.value = btn.dataset.status;
163
- addTitle.value = "";
164
- addBody.value = "";
165
- addPriority.value = "";
166
- addModal.classList.add("show");
167
- addTitle.focus();
197
+ openAddModal(elements, btn.dataset.status);
168
198
  });
169
199
  });
170
200
  document.getElementById("add-cancel")?.addEventListener("click", () => {
171
- addModal.classList.remove("show");
201
+ elements.addModal.classList.remove("show");
172
202
  });
173
- addModal.addEventListener("click", (e) => {
174
- if (e.target === addModal) addModal.classList.remove("show");
203
+ elements.addModal.addEventListener("click", (e) => {
204
+ if (e.target === elements.addModal) elements.addModal.classList.remove("show");
175
205
  });
176
- addTitle.addEventListener("keydown", (e) => {
206
+ elements.addTitle.addEventListener("keydown", (e) => {
177
207
  if (e.key === "Enter" && !e.isComposing) {
178
208
  e.preventDefault();
179
209
  document.getElementById("add-submit").click();
180
210
  }
181
211
  });
182
- document.getElementById("add-submit")?.addEventListener("click", async () => {
183
- const title = addTitle.value.trim();
184
- if (!title) {
185
- addTitle.focus();
186
- return;
187
- }
188
- const status = addStatus.value;
189
- addModal.classList.remove("show");
190
- try {
191
- const res = await fetch("/api/tasks", {
192
- method: "POST",
193
- headers: { "Content-Type": "application/json" },
194
- body: JSON.stringify({
195
- title,
196
- body: addBody.value.trim() || null,
197
- status,
198
- priority: addPriority.value || null
199
- })
200
- });
201
- if (!res.ok) throw new Error("Server error");
202
- location.reload();
203
- } catch {
204
- showToast("Failed to add task");
205
- }
206
- });
212
+ document.getElementById("add-submit")?.addEventListener("click", () => submitAddTask(elements));
207
213
  }
208
214
 
209
215
  // src/board/client/contextMenu.ts
216
+ async function deleteCard(card) {
217
+ const taskId = card.dataset.id;
218
+ const status = card.dataset.status;
219
+ if (!confirm("Delete task #" + taskId + "?")) return;
220
+ card.remove();
221
+ updateCount(status);
222
+ try {
223
+ const res = await fetch("/api/tasks/" + taskId, { method: "DELETE" });
224
+ if (!res.ok) throw new Error("Server error");
225
+ } catch {
226
+ location.reload();
227
+ showToast("Failed to delete task");
228
+ }
229
+ }
210
230
  function initContextMenu() {
211
231
  const ctxMenu = document.getElementById("context-menu");
212
232
  let ctxTargetCard = null;
@@ -234,18 +254,7 @@
234
254
  if (!ctxTargetCard) return;
235
255
  const card = ctxTargetCard;
236
256
  ctxTargetCard = null;
237
- const taskId = card.dataset.id;
238
- const status = card.dataset.status;
239
- if (!confirm("Delete task #" + taskId + "?")) return;
240
- card.remove();
241
- updateCount(status);
242
- try {
243
- const res = await fetch("/api/tasks/" + taskId, { method: "DELETE" });
244
- if (!res.ok) throw new Error("Server error");
245
- } catch {
246
- location.reload();
247
- showToast("Failed to delete task");
248
- }
257
+ await deleteCard(card);
249
258
  });
250
259
  }
251
260
 
@@ -270,6 +279,7 @@
270
279
  container.innerHTML = '<div class="tag-select-wrapper"><div class="tag-select-control" id="tag-select-control"></div><div class="tag-select-dropdown" id="tag-select-dropdown"></div></div>';
271
280
  const control = document.getElementById("tag-select-control");
272
281
  const dropdown = document.getElementById("tag-select-dropdown");
282
+ if (!control || !dropdown) return;
273
283
  let focusedOptionIndex = -1;
274
284
  let inputValue = "";
275
285
  function getFilteredTags() {
@@ -424,7 +434,7 @@
424
434
  function setLastUpdatedAt(val) {
425
435
  lastUpdatedAt = val;
426
436
  }
427
- var activeFilters = { tagIds: [], priorities: [], assignee: "" };
437
+ var activeFilters = { tagIds: [], priorities: [], assignee: "", searchText: "" };
428
438
  function buildFilterParams() {
429
439
  const params = new URLSearchParams();
430
440
  if (activeFilters.priorities.length > 0) {
@@ -436,6 +446,9 @@
436
446
  if (activeFilters.assignee) {
437
447
  params.set("assignee", activeFilters.assignee);
438
448
  }
449
+ if (activeFilters.searchText) {
450
+ params.set("search", activeFilters.searchText);
451
+ }
439
452
  return params;
440
453
  }
441
454
  var _openTaskDetail = null;
@@ -448,6 +461,45 @@
448
461
  _showUpdateWarning = callbacks.showUpdateWarning;
449
462
  _getDetailTaskId2 = callbacks.getDetailTaskId;
450
463
  }
464
+ function attachCardListeners(body) {
465
+ body.querySelectorAll(".card").forEach((card) => {
466
+ attachDragListeners(card);
467
+ card.addEventListener("click", async (e) => {
468
+ if (e.defaultPrevented) return;
469
+ if (_openTaskDetail) await _openTaskDetail(card.dataset.id);
470
+ });
471
+ });
472
+ }
473
+ function updateColumnHtml(col) {
474
+ const body = document.getElementById("col-" + col.status);
475
+ if (!body) return;
476
+ body.innerHTML = col.html;
477
+ const colEl = body.closest(".column");
478
+ if (colEl) {
479
+ const countEl = colEl.querySelector(".column-count");
480
+ if (countEl) countEl.textContent = String(col.count);
481
+ }
482
+ attachCardListeners(body);
483
+ attachAutoScrollToBody(body);
484
+ }
485
+ function isEditingDetailPanel() {
486
+ const editableFields = ["detail-edit-title", "detail-edit-body", "detail-edit-status", "detail-edit-priority"];
487
+ return editableFields.some((id) => document.activeElement && document.activeElement.id === id);
488
+ }
489
+ async function refreshOpenDetailPanel(detailTaskId2) {
490
+ if (isEditingDetailPanel()) {
491
+ if (_showUpdateWarning) _showUpdateWarning();
492
+ return;
493
+ }
494
+ try {
495
+ const taskRes = await fetch("/api/tasks/" + detailTaskId2);
496
+ if (taskRes.ok) {
497
+ const taskData = await taskRes.json();
498
+ if (_renderDetailPanel) _renderDetailPanel(taskData);
499
+ }
500
+ } catch {
501
+ }
502
+ }
451
503
  async function refreshBoardCards() {
452
504
  const filterParams = buildFilterParams();
453
505
  const url = "/api/board/cards" + (filterParams.toString() ? "?" + filterParams.toString() : "");
@@ -455,41 +507,10 @@
455
507
  const res = await fetch(url);
456
508
  if (!res.ok) return;
457
509
  const data = await res.json();
458
- const columns = data.columns;
459
- columns.forEach((col) => {
460
- const body = document.getElementById("col-" + col.status);
461
- if (!body) return;
462
- body.innerHTML = col.html;
463
- const colEl = body.closest(".column");
464
- if (colEl) {
465
- const countEl = colEl.querySelector(".column-count");
466
- if (countEl) countEl.textContent = String(col.count);
467
- }
468
- body.querySelectorAll(".card").forEach((card) => {
469
- attachDragListeners(card);
470
- card.addEventListener("click", async (e) => {
471
- if (e.defaultPrevented) return;
472
- if (_openTaskDetail) await _openTaskDetail(card.dataset.id);
473
- });
474
- });
475
- attachAutoScrollToBody(body);
476
- });
510
+ data.columns.forEach(updateColumnHtml);
477
511
  const detailTaskId2 = _getDetailTaskId2 ? _getDetailTaskId2() : null;
478
512
  if (detailTaskId2 !== null) {
479
- const editableFields = ["detail-edit-title", "detail-edit-body", "detail-edit-status", "detail-edit-priority"];
480
- const isEditing = editableFields.some((id) => document.activeElement && document.activeElement.id === id);
481
- if (isEditing) {
482
- if (_showUpdateWarning) _showUpdateWarning();
483
- } else {
484
- try {
485
- const taskRes = await fetch("/api/tasks/" + detailTaskId2);
486
- if (taskRes.ok) {
487
- const taskData = await taskRes.json();
488
- if (_renderDetailPanel) _renderDetailPanel(taskData);
489
- }
490
- } catch {
491
- }
492
- }
513
+ await refreshOpenDetailPanel(detailTaskId2);
493
514
  }
494
515
  } catch {
495
516
  }
@@ -554,48 +575,61 @@
554
575
  const comments = data.comments || [];
555
576
  if (tabBtn) tabBtn.textContent = "Comments (" + comments.length + ")";
556
577
  renderComments(taskId, comments);
557
- } catch {
578
+ } catch (err) {
579
+ console.error("[agkan] loadComments failed for task", taskId, err);
558
580
  if (pane) pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load comments</div>';
559
581
  }
560
582
  }
583
+ function renderCommentItemHtml(comment, taskId) {
584
+ const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
585
+ const dateRel = relativeTime(comment.created_at);
586
+ const dateAbs = escapeHtmlClient(comment.created_at);
587
+ const contentText = escapeHtmlClient(comment.content);
588
+ let html = '<div class="comment-item" data-comment-id="' + comment.id + '">';
589
+ html += '<div class="comment-meta">';
590
+ html += '<span class="comment-author">' + authorText + "</span>";
591
+ html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
592
+ html += '<span class="comment-actions">';
593
+ html += '<button class="comment-action-btn" title="Edit" data-action="start-comment-edit" data-comment-id="' + comment.id + '">&#9998;</button>';
594
+ html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">&#128465;</button>';
595
+ html += "</span></div>";
596
+ html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
597
+ html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
598
+ html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
599
+ html += '<div class="comment-edit-actions">';
600
+ html += '<button class="comment-btn" data-action="save-comment-edit" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">Save</button>';
601
+ html += '<button class="comment-btn" data-action="cancel-comment-edit" data-comment-id="' + comment.id + '">Cancel</button>';
602
+ html += "</div></div></div>";
603
+ return html;
604
+ }
605
+ function renderAddCommentFormHtml(taskId) {
606
+ let html = '<button class="add-comment-trigger" id="add-comment-trigger" data-action="open-add-comment">+ Add comment...</button>';
607
+ html += '<div class="add-comment-form" id="add-comment-form">';
608
+ html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
609
+ html += "<div>";
610
+ html += '<button class="add-comment-submit" data-action="submit-comment" data-task-id="' + taskId + '">Add Comment</button>';
611
+ html += '<button class="add-comment-cancel" data-action="close-add-comment">Cancel</button>';
612
+ html += "</div></div>";
613
+ return html;
614
+ }
561
615
  function renderComments(taskId, comments) {
562
616
  const pane = document.getElementById("detail-tab-content-comments");
563
617
  if (!pane) return;
564
618
  pane.style.padding = "16px 20px";
565
619
  let html = "";
566
- comments.forEach(function(comment) {
567
- const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
568
- const dateRel = relativeTime(comment.created_at);
569
- const dateAbs = escapeHtmlClient(comment.created_at);
570
- const contentText = escapeHtmlClient(comment.content);
571
- html += '<div class="comment-item" data-comment-id="' + comment.id + '">';
572
- html += '<div class="comment-meta">';
573
- html += '<span class="comment-author">' + authorText + "</span>";
574
- html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
575
- html += '<span class="comment-actions">';
576
- html += '<button class="comment-action-btn" title="Edit" onclick="startCommentEdit(' + comment.id + ')">&#9998;</button>';
577
- html += '<button class="comment-action-btn danger" title="Delete" onclick="deleteComment(' + comment.id + "," + taskId + ')">&#128465;</button>';
578
- html += "</span>";
579
- html += "</div>";
580
- html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
581
- html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
582
- html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
583
- html += '<div class="comment-edit-actions">';
584
- html += '<button class="comment-btn" onclick="saveCommentEdit(' + comment.id + "," + taskId + ')">Save</button>';
585
- html += '<button class="comment-btn" onclick="cancelCommentEdit(' + comment.id + ')">Cancel</button>';
586
- html += "</div></div>";
587
- html += "</div>";
620
+ comments.forEach((comment) => {
621
+ html += renderCommentItemHtml(comment, taskId);
588
622
  });
589
- html += '<button class="add-comment-trigger" id="add-comment-trigger" onclick="openAddCommentForm()">+ Add comment...</button>';
590
- html += '<div class="add-comment-form" id="add-comment-form">';
591
- html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
592
- html += "<div>";
593
- html += '<button class="add-comment-submit" onclick="submitComment(' + taskId + ')">Add Comment</button>';
594
- html += '<button class="add-comment-cancel" onclick="closeAddCommentForm()">Cancel</button>';
595
- html += "</div></div>";
623
+ html += renderAddCommentFormHtml(taskId);
596
624
  pane.innerHTML = html;
625
+ const paneEl = pane;
626
+ if (paneEl._commentActionHandler) {
627
+ paneEl.removeEventListener("click", paneEl._commentActionHandler);
628
+ }
629
+ paneEl._commentActionHandler = handleCommentAction;
630
+ paneEl.addEventListener("click", paneEl._commentActionHandler);
597
631
  }
598
- window.openAddCommentForm = function() {
632
+ function openAddCommentForm() {
599
633
  const trigger = document.getElementById("add-comment-trigger");
600
634
  const form = document.getElementById("add-comment-form");
601
635
  if (trigger) trigger.style.display = "none";
@@ -603,8 +637,8 @@
603
637
  form.classList.add("open");
604
638
  form.querySelector("textarea").focus();
605
639
  }
606
- };
607
- window.closeAddCommentForm = function() {
640
+ }
641
+ function closeAddCommentForm() {
608
642
  const trigger = document.getElementById("add-comment-trigger");
609
643
  const form = document.getElementById("add-comment-form");
610
644
  if (trigger) trigger.style.display = "";
@@ -612,22 +646,22 @@
612
646
  form.classList.remove("open");
613
647
  form.querySelector("textarea").value = "";
614
648
  }
615
- };
616
- window.startCommentEdit = function(commentId) {
649
+ }
650
+ function startCommentEdit(commentId) {
617
651
  const contentEl = document.getElementById("comment-content-" + commentId);
618
652
  const editWrapper = document.getElementById("comment-edit-" + commentId);
619
653
  if (contentEl) contentEl.style.display = "none";
620
654
  if (editWrapper) editWrapper.style.display = "block";
621
655
  const area = document.getElementById("comment-edit-area-" + commentId);
622
656
  if (area) area.focus();
623
- };
624
- window.cancelCommentEdit = function(commentId) {
657
+ }
658
+ function cancelCommentEdit(commentId) {
625
659
  const contentEl = document.getElementById("comment-content-" + commentId);
626
660
  const editWrapper = document.getElementById("comment-edit-" + commentId);
627
661
  if (contentEl) contentEl.style.display = "";
628
662
  if (editWrapper) editWrapper.style.display = "none";
629
- };
630
- window.saveCommentEdit = async function(commentId, taskId) {
663
+ }
664
+ async function saveCommentEdit(commentId, taskId) {
631
665
  const area = document.getElementById("comment-edit-area-" + commentId);
632
666
  if (!area) return;
633
667
  const content = area.value.trim();
@@ -646,8 +680,8 @@
646
680
  } catch {
647
681
  showToast("Failed to update comment");
648
682
  }
649
- };
650
- window.deleteComment = async function(commentId, taskId) {
683
+ }
684
+ async function deleteComment(commentId, taskId) {
651
685
  if (!confirm("Delete this comment?")) return;
652
686
  try {
653
687
  const res = await fetch("/api/comments/" + commentId, { method: "DELETE" });
@@ -656,8 +690,8 @@
656
690
  } catch {
657
691
  showToast("Failed to delete comment");
658
692
  }
659
- };
660
- window.submitComment = async function(taskId) {
693
+ }
694
+ async function submitComment(taskId) {
661
695
  const textarea = document.getElementById("add-comment-text");
662
696
  if (!textarea) return;
663
697
  const content = textarea.value.trim();
@@ -676,117 +710,167 @@
676
710
  } catch {
677
711
  showToast("Failed to add comment");
678
712
  }
679
- };
680
- function renderDetailPanel(data) {
681
- document.getElementById("detail-panel-update-warning")?.remove();
682
- const detailPanelTitle = document.getElementById("detail-panel-title");
683
- const task = data.task;
684
- const tags = data.tags || [];
685
- const metadata = data.metadata || [];
686
- const blockedBy = data.blockedBy || [];
687
- const blocking = data.blocking || [];
688
- const parent = data.parent || null;
689
- detailTaskId = task.id;
690
- detailPanelTitle.textContent = "#" + task.id;
691
- const win = window;
692
- const _allStatuses = win.allStatuses;
693
- const _statusLabels = win.statusLabels;
694
- const _allPriorities = win.allPriorities;
695
- let html = "";
696
- html += '<div class="detail-field">';
713
+ }
714
+ function dispatchCommentAction(action, commentId, taskId) {
715
+ switch (action) {
716
+ case "open-add-comment":
717
+ openAddCommentForm();
718
+ break;
719
+ case "close-add-comment":
720
+ closeAddCommentForm();
721
+ break;
722
+ case "start-comment-edit":
723
+ startCommentEdit(commentId);
724
+ break;
725
+ case "cancel-comment-edit":
726
+ cancelCommentEdit(commentId);
727
+ break;
728
+ case "save-comment-edit":
729
+ void saveCommentEdit(commentId, taskId);
730
+ break;
731
+ case "delete-comment":
732
+ void deleteComment(commentId, taskId);
733
+ break;
734
+ case "submit-comment":
735
+ void submitComment(taskId);
736
+ break;
737
+ }
738
+ }
739
+ function handleCommentAction(e) {
740
+ const target = e.target.closest("[data-action]");
741
+ if (!target) return;
742
+ const action = target.dataset.action ?? "";
743
+ const commentId = target.dataset.commentId ? Number(target.dataset.commentId) : NaN;
744
+ const taskId = target.dataset.taskId ? Number(target.dataset.taskId) : NaN;
745
+ dispatchCommentAction(action, commentId, taskId);
746
+ }
747
+ function renderStatusField(currentStatus, allStatuses, statusLabels) {
748
+ let html = '<div class="detail-field">';
697
749
  html += '<div class="detail-field-label">Status</div>';
698
750
  html += '<select id="detail-edit-status" class="detail-edit-select">';
699
- _allStatuses.forEach((s) => {
700
- const selected = s === task.status ? " selected" : "";
701
- html += '<option value="' + s + '"' + selected + ">" + _statusLabels[s] + "</option>";
751
+ allStatuses.forEach((s) => {
752
+ const selected = s === currentStatus ? " selected" : "";
753
+ html += '<option value="' + s + '"' + selected + ">" + statusLabels[s] + "</option>";
702
754
  });
703
- html += "</select>";
704
- html += "</div>";
705
- html += '<div class="detail-field">';
755
+ html += "</select></div>";
756
+ return html;
757
+ }
758
+ function renderPriorityField(currentPriority, allPriorities) {
759
+ let html = '<div class="detail-field">';
706
760
  html += '<div class="detail-field-label">Priority</div>';
707
761
  html += '<select id="detail-edit-priority" class="detail-edit-select">';
708
762
  html += '<option value="">None</option>';
709
- _allPriorities.forEach((p) => {
710
- const selected = task.priority === p ? " selected" : "";
763
+ allPriorities.forEach((p) => {
764
+ const selected = currentPriority === p ? " selected" : "";
711
765
  html += '<option value="' + p + '"' + selected + ">" + p.charAt(0).toUpperCase() + p.slice(1) + "</option>";
712
766
  });
713
- html += "</select>";
714
- html += "</div>";
715
- html += '<div class="detail-field">';
716
- html += '<div class="detail-field-label">Tags</div>';
717
- html += '<div id="detail-tags-container"></div>';
718
- html += "</div>";
719
- const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
720
- if (hasRelations) {
721
- html += '<div class="detail-relations">';
722
- if (parent) {
723
- html += '<div class="detail-relation-row">';
724
- html += '<span class="detail-relation-label">Parent</span>';
725
- html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
726
- html += "</div>";
727
- }
728
- if (blockedBy.length > 0) {
729
- html += '<div class="detail-relation-row">';
730
- html += '<span class="detail-relation-label">Blocked by</span>';
731
- html += '<div class="detail-relation-ids">';
732
- blockedBy.forEach((t) => {
733
- html += '<span class="detail-relation-id">#' + t.id + "</span>";
734
- });
735
- html += "</div></div>";
736
- }
737
- if (blocking.length > 0) {
738
- html += '<div class="detail-relation-row">';
739
- html += '<span class="detail-relation-label">Blocking</span>';
740
- html += '<div class="detail-relation-ids">';
741
- blocking.forEach((t) => {
742
- html += '<span class="detail-relation-id">#' + t.id + "</span>";
743
- });
744
- html += "</div></div>";
745
- }
767
+ html += "</select></div>";
768
+ return html;
769
+ }
770
+ function renderRelationsHtml(parent, blockedBy, blocking) {
771
+ let html = '<div class="detail-relations">';
772
+ if (parent) {
773
+ html += '<div class="detail-relation-row">';
774
+ html += '<span class="detail-relation-label">Parent</span>';
775
+ html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
746
776
  html += "</div>";
747
777
  }
748
- html += '<div class="detail-field">';
749
- html += '<div class="detail-field-label">Title</div>';
778
+ if (blockedBy.length > 0) {
779
+ html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
780
+ html += '<div class="detail-relation-ids">';
781
+ blockedBy.forEach((t) => {
782
+ html += '<span class="detail-relation-id">#' + t.id + "</span>";
783
+ });
784
+ html += "</div></div>";
785
+ }
786
+ if (blocking.length > 0) {
787
+ html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
788
+ html += '<div class="detail-relation-ids">';
789
+ blocking.forEach((t) => {
790
+ html += '<span class="detail-relation-id">#' + t.id + "</span>";
791
+ });
792
+ html += "</div></div>";
793
+ }
794
+ html += "</div>";
795
+ return html;
796
+ }
797
+ function renderMetadataTable(metadata) {
798
+ const otherMeta = metadata.filter((m) => m.key !== "priority");
799
+ if (otherMeta.length === 0) return "";
800
+ let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
801
+ html += '<table class="detail-meta-table">';
802
+ otherMeta.forEach((m) => {
803
+ html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
804
+ });
805
+ html += "</table></div>";
806
+ return html;
807
+ }
808
+ function renderEditableTextFields(task) {
809
+ let html = '<div class="detail-field"><div class="detail-field-label">Title</div>';
750
810
  html += '<input id="detail-edit-title" class="detail-edit-input" type="text" value="' + escapeHtmlClient(task.title) + '">';
751
811
  html += "</div>";
752
- html += '<div class="detail-field description-field-wrapper">';
753
- html += '<div class="detail-field-label">Description</div>';
812
+ html += '<div class="detail-field description-field-wrapper"><div class="detail-field-label">Description</div>';
754
813
  html += '<textarea id="detail-edit-body" class="detail-edit-textarea">' + escapeHtmlClient(task.body || "") + "</textarea>";
755
814
  html += "</div>";
756
- const otherMeta = metadata.filter((m) => m.key !== "priority");
757
- if (otherMeta.length > 0) {
758
- html += '<div class="detail-field">';
759
- html += '<div class="detail-field-label">Metadata</div>';
760
- html += '<table class="detail-meta-table">';
761
- otherMeta.forEach((m) => {
762
- html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
763
- });
764
- html += "</table></div>";
815
+ return html;
816
+ }
817
+ function renderDetailPanelHtml(data) {
818
+ const task = data.task;
819
+ const metadata = data.metadata || [];
820
+ const blockedBy = data.blockedBy || [];
821
+ const blocking = data.blocking || [];
822
+ const parent = data.parent || null;
823
+ const win = window;
824
+ const allStatuses = win.allStatuses;
825
+ const statusLabels = win.statusLabels;
826
+ const allPriorities = win.allPriorities;
827
+ let html = "";
828
+ html += renderStatusField(task.status, allStatuses, statusLabels);
829
+ html += renderPriorityField(task.priority, allPriorities);
830
+ html += '<div class="detail-field"><div class="detail-field-label">Tags</div>';
831
+ html += '<div id="detail-tags-container"></div></div>';
832
+ const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
833
+ if (hasRelations) {
834
+ html += renderRelationsHtml(parent, blockedBy, blocking);
765
835
  }
836
+ html += renderEditableTextFields(task);
837
+ html += renderMetadataTable(metadata);
766
838
  html += '<div class="detail-timestamp">created ' + relativeTime(task.created_at) + " &middot; updated " + relativeTime(task.updated_at) + "</div>";
839
+ return html;
840
+ }
841
+ function renderDetailPanel(data) {
842
+ document.getElementById("detail-panel-update-warning")?.remove();
843
+ const detailPanelTitle = document.getElementById("detail-panel-title");
844
+ const task = data.task;
845
+ const tags = data.tags || [];
846
+ detailTaskId = task.id;
847
+ detailPanelTitle.textContent = "#" + task.id;
767
848
  const detailsPane = document.getElementById("detail-tab-content-details");
768
849
  if (detailsPane) {
769
- detailsPane.innerHTML = html;
850
+ detailsPane.innerHTML = renderDetailPanelHtml(data);
770
851
  detailsPane.style.padding = "20px";
771
852
  }
772
- loadAllTags().then(() => renderTagsSection([...tags]));
853
+ loadAllTags().then(() => renderTagsSection([...tags])).catch((err) => {
854
+ console.error("[agkan] renderDetailPanel tags failed", err);
855
+ });
773
856
  loadComments(task.id);
774
857
  switchTab(lastTab);
775
858
  }
776
859
  async function openTaskDetail(taskId) {
777
860
  const detailPanel = document.getElementById("detail-panel");
778
- const PANEL_DEFAULT_WIDTH = 400;
861
+ const PANEL_DEFAULT_WIDTH2 = 400;
779
862
  try {
780
863
  const res = await fetch("/api/tasks/" + taskId);
781
864
  if (!res.ok) throw new Error("Server error");
782
865
  const data = await res.json();
783
866
  renderDetailPanel(data);
784
867
  if (!detailPanel.classList.contains("open")) {
785
- const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH);
868
+ const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH2);
786
869
  detailPanel.style.width = preferredWidth + "px";
787
870
  detailPanel.classList.add("open");
788
871
  }
789
- } catch {
872
+ } catch (err) {
873
+ console.error("[agkan] openTaskDetail failed for task", taskId, err);
790
874
  showToast("Failed to load task details");
791
875
  }
792
876
  }
@@ -800,55 +884,100 @@
800
884
  const msgSpan = document.createElement("span");
801
885
  msgSpan.style.cssText = "flex: 1;";
802
886
  msgSpan.textContent = "This task has been updated in the database. Save or discard your changes to see the latest version.";
803
- const reloadBtn = document.createElement("button");
804
- reloadBtn.title = "Reload latest data";
805
- reloadBtn.textContent = "\u21BA";
806
- reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
807
- reloadBtn.addEventListener("click", async () => {
808
- try {
809
- const taskRes = await fetch("/api/tasks/" + detailTaskId);
810
- if (taskRes.ok) {
811
- const taskData = await taskRes.json();
812
- renderDetailPanel(taskData);
813
- }
814
- } catch {
815
- }
816
- });
887
+ const reloadBtn = buildUpdateWarningReloadBtn();
817
888
  warningEl.appendChild(msgSpan);
818
889
  warningEl.appendChild(reloadBtn);
819
890
  detailPanelBody.insertBefore(warningEl, detailPanelBody.firstChild);
820
891
  }
821
892
  }
822
- function initDetailPanel() {
823
- const boardContainer = document.querySelector(".board-container");
824
- const detailPanelHtml = '<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">&times;</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>';
825
- boardContainer.insertAdjacentHTML("beforeend", detailPanelHtml);
826
- const detailPanel = document.getElementById("detail-panel");
827
- document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
828
- document.getElementById("detail-tabs")?.addEventListener("click", (e) => {
829
- const btn = e.target.closest(".detail-tab");
830
- if (!btn) return;
831
- switchTab(btn.dataset.tab);
832
- });
833
- const resizeHandle = document.getElementById("detail-panel-resize-handle");
834
- const PANEL_MIN_WIDTH = 280;
835
- const PANEL_MAX_WIDTH = 800;
836
- const PANEL_DEFAULT_WIDTH = 400;
837
- (async function initPanelWidth() {
838
- let targetWidth = PANEL_DEFAULT_WIDTH;
893
+ function buildUpdateWarningReloadBtn() {
894
+ const reloadBtn = document.createElement("button");
895
+ reloadBtn.title = "Reload latest data";
896
+ reloadBtn.textContent = "\u21BA";
897
+ reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
898
+ reloadBtn.addEventListener("click", async () => {
839
899
  try {
840
- const res = await fetch("/api/config");
841
- if (res.ok) {
842
- const data = await res.json();
843
- const savedWidth = data && data.board && data.board.detailPaneWidth;
844
- if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
845
- targetWidth = savedWidth;
846
- }
900
+ const taskRes = await fetch("/api/tasks/" + detailTaskId);
901
+ if (taskRes.ok) {
902
+ const taskData = await taskRes.json();
903
+ renderDetailPanel(taskData);
847
904
  }
848
905
  } catch {
849
906
  }
850
- detailPanel.dataset.preferredWidth = String(targetWidth);
851
- })();
907
+ });
908
+ return reloadBtn;
909
+ }
910
+ function collectEditedTaskFields() {
911
+ const titleInput = document.getElementById("detail-edit-title");
912
+ const title = titleInput ? titleInput.value.trim() : "";
913
+ if (!title) {
914
+ if (titleInput) titleInput.focus();
915
+ return null;
916
+ }
917
+ const bodyEl = document.getElementById("detail-edit-body");
918
+ const statusEl = document.getElementById("detail-edit-status");
919
+ const priorityEl = document.getElementById("detail-edit-priority");
920
+ return {
921
+ title,
922
+ body: bodyEl ? bodyEl.value.trim() || null : null,
923
+ status: statusEl ? statusEl.value : void 0,
924
+ priority: priorityEl ? priorityEl.value || null : null
925
+ };
926
+ }
927
+ async function patchAndReloadDetail(taskId, fields) {
928
+ const res = await fetch("/api/tasks/" + taskId, {
929
+ method: "PATCH",
930
+ headers: { "Content-Type": "application/json" },
931
+ body: JSON.stringify(fields)
932
+ });
933
+ if (!res.ok) throw new Error("Server error");
934
+ const getRes = await fetch("/api/tasks/" + taskId);
935
+ if (!getRes.ok) throw new Error("Failed to fetch updated task");
936
+ const data = await getRes.json();
937
+ renderDetailPanel(data);
938
+ }
939
+ async function saveDetailTask() {
940
+ if (detailTaskId === null) return;
941
+ const fields = collectEditedTaskFields();
942
+ if (!fields) return;
943
+ try {
944
+ await patchAndReloadDetail(detailTaskId, fields);
945
+ showToast("Task saved successfully");
946
+ await syncTimestampAfterSave();
947
+ refreshBoardCards();
948
+ } catch {
949
+ showToast("Failed to update task");
950
+ }
951
+ }
952
+ async function syncTimestampAfterSave() {
953
+ try {
954
+ const tsRes = await fetch("/api/board/updated-at");
955
+ if (tsRes.ok) {
956
+ const tsData = await tsRes.json();
957
+ setLastUpdatedAt(tsData.updatedAt);
958
+ }
959
+ } catch {
960
+ }
961
+ }
962
+ var PANEL_MIN_WIDTH = 280;
963
+ var PANEL_MAX_WIDTH = 800;
964
+ var PANEL_DEFAULT_WIDTH = 400;
965
+ async function initPanelWidthFromConfig(detailPanel) {
966
+ let targetWidth = PANEL_DEFAULT_WIDTH;
967
+ try {
968
+ const res = await fetch("/api/config");
969
+ if (res.ok) {
970
+ const data = await res.json();
971
+ const savedWidth = data && data.board && data.board.detailPaneWidth;
972
+ if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
973
+ targetWidth = savedWidth;
974
+ }
975
+ }
976
+ } catch {
977
+ }
978
+ detailPanel.dataset.preferredWidth = String(targetWidth);
979
+ }
980
+ function attachResizeMousedown(resizeHandle, detailPanel) {
852
981
  resizeHandle.addEventListener("mousedown", function(e) {
853
982
  e.preventDefault();
854
983
  if (!detailPanel.classList.contains("open")) return;
@@ -858,8 +987,8 @@
858
987
  document.body.style.userSelect = "none";
859
988
  document.body.style.cursor = "col-resize";
860
989
  detailPanel.style.transition = "none";
861
- function onMouseMove(e2) {
862
- const delta = startX - e2.clientX;
990
+ function onMouseMove(ev) {
991
+ const delta = startX - ev.clientX;
863
992
  const newWidth = Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, startWidth + delta));
864
993
  detailPanel.style.width = newWidth + "px";
865
994
  }
@@ -882,47 +1011,32 @@
882
1011
  document.addEventListener("mousemove", onMouseMove);
883
1012
  document.addEventListener("mouseup", onMouseUp);
884
1013
  });
885
- document.getElementById("detail-save-btn")?.addEventListener("click", async () => {
886
- if (detailTaskId === null) return;
887
- const titleInput = document.getElementById("detail-edit-title");
888
- const title = titleInput ? titleInput.value.trim() : "";
889
- if (!title) {
890
- if (titleInput) titleInput.focus();
891
- return;
892
- }
893
- const bodyEl = document.getElementById("detail-edit-body");
894
- const statusEl = document.getElementById("detail-edit-status");
895
- const priorityEl = document.getElementById("detail-edit-priority");
896
- try {
897
- const res = await fetch("/api/tasks/" + detailTaskId, {
898
- method: "PATCH",
899
- headers: { "Content-Type": "application/json" },
900
- body: JSON.stringify({
901
- title,
902
- body: bodyEl ? bodyEl.value.trim() || null : null,
903
- status: statusEl ? statusEl.value : void 0,
904
- priority: priorityEl ? priorityEl.value || null : null
905
- })
906
- });
907
- if (!res.ok) throw new Error("Server error");
908
- const getRes = await fetch("/api/tasks/" + detailTaskId);
909
- if (!getRes.ok) throw new Error("Failed to fetch updated task");
910
- const data = await getRes.json();
911
- renderDetailPanel(data);
912
- showToast("Task saved successfully");
913
- try {
914
- const tsRes = await fetch("/api/board/updated-at");
915
- if (tsRes.ok) {
916
- const tsData = await tsRes.json();
917
- setLastUpdatedAt(tsData.updatedAt);
918
- }
919
- } catch {
920
- }
921
- refreshBoardCards();
922
- } catch {
923
- showToast("Failed to update task");
1014
+ }
1015
+ function initPanelResize(detailPanel) {
1016
+ const resizeHandle = document.getElementById("detail-panel-resize-handle");
1017
+ initPanelWidthFromConfig(detailPanel);
1018
+ attachResizeMousedown(resizeHandle, detailPanel);
1019
+ }
1020
+ function buildDetailPanelHtml() {
1021
+ 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">&times;</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>';
1022
+ }
1023
+ function initDetailPanel() {
1024
+ const boardContainer = document.querySelector(".board-container");
1025
+ boardContainer.insertAdjacentHTML("beforeend", buildDetailPanelHtml());
1026
+ const detailPanel = document.getElementById("detail-panel");
1027
+ document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
1028
+ document.addEventListener("keydown", (e) => {
1029
+ if (e.key === "Escape" && detailPanel.classList.contains("open")) {
1030
+ closeDetailPanel();
924
1031
  }
925
1032
  });
1033
+ document.getElementById("detail-tabs")?.addEventListener("click", (e) => {
1034
+ const btn = e.target.closest(".detail-tab");
1035
+ if (!btn) return;
1036
+ switchTab(btn.dataset.tab);
1037
+ });
1038
+ initPanelResize(detailPanel);
1039
+ document.getElementById("detail-save-btn")?.addEventListener("click", saveDetailTask);
926
1040
  document.querySelectorAll(".card").forEach((card) => {
927
1041
  card.addEventListener("click", async (e) => {
928
1042
  if (e.defaultPrevented) return;
@@ -940,7 +1054,7 @@
940
1054
 
941
1055
  // src/board/client/filters.ts
942
1056
  function isFiltersActive() {
943
- return activeFilters.priorities.length > 0 || activeFilters.tagIds.length > 0 || activeFilters.assignee !== "";
1057
+ return activeFilters.priorities.length > 0 || activeFilters.tagIds.length > 0 || activeFilters.assignee !== "" || activeFilters.searchText !== "";
944
1058
  }
945
1059
  function applyFilters() {
946
1060
  const clearBtn = document.getElementById("filter-clear");
@@ -993,6 +1107,17 @@
993
1107
  applyFilters();
994
1108
  });
995
1109
  });
1110
+ const searchInput = document.getElementById("filter-search");
1111
+ let searchTimer = null;
1112
+ if (searchInput) {
1113
+ searchInput.addEventListener("input", () => {
1114
+ if (searchTimer) clearTimeout(searchTimer);
1115
+ searchTimer = setTimeout(() => {
1116
+ activeFilters.searchText = searchInput.value.trim();
1117
+ applyFilters();
1118
+ }, 300);
1119
+ });
1120
+ }
996
1121
  const assigneeInput = document.getElementById("filter-assignee");
997
1122
  let assigneeTimer = null;
998
1123
  if (assigneeInput) {
@@ -1010,7 +1135,9 @@
1010
1135
  activeFilters.tagIds = [];
1011
1136
  activeFilters.priorities = [];
1012
1137
  activeFilters.assignee = "";
1138
+ activeFilters.searchText = "";
1013
1139
  document.querySelectorAll(".filter-priority-btn").forEach((btn) => btn.classList.remove("active"));
1140
+ if (searchInput) searchInput.value = "";
1014
1141
  if (assigneeInput) assigneeInput.value = "";
1015
1142
  renderFilterTagPills();
1016
1143
  applyFilters();
@@ -1077,10 +1204,71 @@
1077
1204
  });
1078
1205
  }
1079
1206
 
1207
+ // src/board/client/darkMode.ts
1208
+ function applyTheme(preference) {
1209
+ if (preference === "dark") {
1210
+ document.documentElement.setAttribute("data-theme", "dark");
1211
+ } else if (preference === "light") {
1212
+ document.documentElement.setAttribute("data-theme", "light");
1213
+ } else {
1214
+ document.documentElement.removeAttribute("data-theme");
1215
+ }
1216
+ }
1217
+ async function persistThemeToServer(theme) {
1218
+ try {
1219
+ await fetch("/api/config", {
1220
+ method: "PUT",
1221
+ headers: { "Content-Type": "application/json" },
1222
+ body: JSON.stringify({ board: { theme } })
1223
+ });
1224
+ } catch {
1225
+ }
1226
+ }
1227
+ function getActivePreference() {
1228
+ const ssrTheme = document.documentElement.getAttribute("data-theme");
1229
+ if (ssrTheme === "dark" || ssrTheme === "light") return ssrTheme;
1230
+ return "system";
1231
+ }
1232
+ function updateCheckmarks(active) {
1233
+ const items = {
1234
+ dark: "burger-theme-dark",
1235
+ light: "burger-theme-light",
1236
+ system: "burger-theme-system"
1237
+ };
1238
+ for (const [pref, id] of Object.entries(items)) {
1239
+ const el = document.getElementById(id);
1240
+ if (!el) continue;
1241
+ if (pref === active) {
1242
+ if (!el.textContent?.startsWith("\u2713 ")) {
1243
+ el.textContent = "\u2713 " + el.textContent?.replace(/^\u2713 /, "");
1244
+ }
1245
+ } else {
1246
+ el.textContent = el.textContent?.replace(/^\u2713 /, "") ?? el.textContent;
1247
+ }
1248
+ }
1249
+ }
1250
+ function initDarkMode() {
1251
+ const activePreference = getActivePreference();
1252
+ updateCheckmarks(activePreference);
1253
+ document.getElementById("burger-theme-dark")?.addEventListener("click", () => {
1254
+ applyTheme("dark");
1255
+ updateCheckmarks("dark");
1256
+ void persistThemeToServer("dark");
1257
+ });
1258
+ document.getElementById("burger-theme-light")?.addEventListener("click", () => {
1259
+ applyTheme("light");
1260
+ updateCheckmarks("light");
1261
+ void persistThemeToServer("light");
1262
+ });
1263
+ document.getElementById("burger-theme-system")?.addEventListener("click", () => {
1264
+ applyTheme("system");
1265
+ updateCheckmarks("system");
1266
+ void persistThemeToServer("system");
1267
+ });
1268
+ }
1269
+
1080
1270
  // src/board/client/burgerMenu.ts
1081
- function initBurgerMenu() {
1082
- const burgerBtn = document.getElementById("burger-menu-btn");
1083
- const burgerDropdown = document.getElementById("burger-menu-dropdown");
1271
+ function initBurgerToggle(burgerBtn, burgerDropdown) {
1084
1272
  burgerBtn.addEventListener("click", (e) => {
1085
1273
  e.stopPropagation();
1086
1274
  burgerDropdown.classList.toggle("open");
@@ -1090,6 +1278,34 @@
1090
1278
  burgerDropdown.classList.remove("open");
1091
1279
  }
1092
1280
  });
1281
+ }
1282
+ async function executePurge(purgeConfirmBtn, purgeModal, purgeResultEl) {
1283
+ purgeConfirmBtn.disabled = true;
1284
+ purgeConfirmBtn.textContent = "Purging...";
1285
+ try {
1286
+ const res = await fetch("/api/tasks/purge", {
1287
+ method: "POST",
1288
+ headers: { "Content-Type": "application/json" },
1289
+ body: JSON.stringify({})
1290
+ });
1291
+ const data = await res.json();
1292
+ if (res.ok) {
1293
+ purgeResultEl.textContent = "Purged " + data.count + " task(s).";
1294
+ setTimeout(() => {
1295
+ purgeModal.classList.remove("show");
1296
+ }, 1500);
1297
+ location.reload();
1298
+ } else {
1299
+ purgeResultEl.textContent = "Error: " + (data.error || "Unknown error");
1300
+ }
1301
+ } catch {
1302
+ purgeResultEl.textContent = "Failed to purge tasks.";
1303
+ } finally {
1304
+ purgeConfirmBtn.disabled = false;
1305
+ purgeConfirmBtn.textContent = "Purge";
1306
+ }
1307
+ }
1308
+ function initPurgeModal(burgerDropdown) {
1093
1309
  const purgeModal = document.getElementById("purge-confirm-modal");
1094
1310
  const purgeConfirmBtn = document.getElementById("purge-confirm-btn");
1095
1311
  const purgeCancelBtn = document.getElementById("purge-cancel-btn");
@@ -1102,32 +1318,9 @@
1102
1318
  purgeCancelBtn.addEventListener("click", () => {
1103
1319
  purgeModal.classList.remove("show");
1104
1320
  });
1105
- purgeConfirmBtn.addEventListener("click", async () => {
1106
- purgeConfirmBtn.disabled = true;
1107
- purgeConfirmBtn.textContent = "Purging...";
1108
- try {
1109
- const res = await fetch("/api/tasks/purge", {
1110
- method: "POST",
1111
- headers: { "Content-Type": "application/json" },
1112
- body: JSON.stringify({})
1113
- });
1114
- const data = await res.json();
1115
- if (res.ok) {
1116
- purgeResultEl.textContent = "Purged " + data.count + " task(s).";
1117
- setTimeout(() => {
1118
- purgeModal.classList.remove("show");
1119
- }, 1500);
1120
- location.reload();
1121
- } else {
1122
- purgeResultEl.textContent = "Error: " + (data.error || "Unknown error");
1123
- }
1124
- } catch {
1125
- purgeResultEl.textContent = "Failed to purge tasks.";
1126
- } finally {
1127
- purgeConfirmBtn.disabled = false;
1128
- purgeConfirmBtn.textContent = "Purge";
1129
- }
1130
- });
1321
+ purgeConfirmBtn.addEventListener("click", () => executePurge(purgeConfirmBtn, purgeModal, purgeResultEl));
1322
+ }
1323
+ function initVersionModal(burgerDropdown) {
1131
1324
  const versionModal = document.getElementById("version-info-modal");
1132
1325
  const versionCloseBtn = document.getElementById("version-info-close");
1133
1326
  const versionTextEl = document.getElementById("version-info-text");
@@ -1147,6 +1340,14 @@
1147
1340
  versionModal.classList.remove("show");
1148
1341
  });
1149
1342
  }
1343
+ function initBurgerMenu() {
1344
+ const burgerBtn = document.getElementById("burger-menu-btn");
1345
+ const burgerDropdown = document.getElementById("burger-menu-dropdown");
1346
+ initBurgerToggle(burgerBtn, burgerDropdown);
1347
+ initPurgeModal(burgerDropdown);
1348
+ initVersionModal(burgerDropdown);
1349
+ initDarkMode();
1350
+ }
1150
1351
 
1151
1352
  // src/board/client/main.ts
1152
1353
  initDragDrop();