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.
- package/dist/board/boardConfig.d.ts +3 -0
- package/dist/board/boardConfig.d.ts.map +1 -1
- package/dist/board/boardConfig.js +7 -1
- package/dist/board/boardConfig.js.map +1 -1
- package/dist/board/boardRenderer.d.ts +1 -1
- package/dist/board/boardRenderer.d.ts.map +1 -1
- package/dist/board/boardRenderer.js +12 -8
- package/dist/board/boardRenderer.js.map +1 -1
- package/dist/board/boardRoutes.d.ts.map +1 -1
- package/dist/board/boardRoutes.js +13 -1
- 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 +119 -78
- package/dist/board/boardStyles.js.map +1 -1
- package/dist/board/client/board.js +518 -317
- package/dist/cli/commands/block/add.d.ts.map +1 -1
- package/dist/cli/commands/block/add.js +156 -110
- package/dist/cli/commands/block/add.js.map +1 -1
- package/dist/cli/commands/block/list.d.ts.map +1 -1
- package/dist/cli/commands/block/list.js +2 -3
- package/dist/cli/commands/block/list.js.map +1 -1
- package/dist/cli/commands/block/remove.d.ts.map +1 -1
- package/dist/cli/commands/block/remove.js +2 -3
- package/dist/cli/commands/block/remove.js.map +1 -1
- package/dist/cli/commands/comment/add.d.ts.map +1 -1
- package/dist/cli/commands/comment/add.js +2 -3
- package/dist/cli/commands/comment/add.js.map +1 -1
- package/dist/cli/commands/comment/delete.js +2 -2
- package/dist/cli/commands/comment/delete.js.map +1 -1
- package/dist/cli/commands/comment/list.d.ts.map +1 -1
- package/dist/cli/commands/comment/list.js +2 -3
- package/dist/cli/commands/comment/list.js.map +1 -1
- package/dist/cli/commands/meta/delete.d.ts.map +1 -1
- package/dist/cli/commands/meta/delete.js +2 -3
- package/dist/cli/commands/meta/delete.js.map +1 -1
- package/dist/cli/commands/meta/get.d.ts.map +1 -1
- package/dist/cli/commands/meta/get.js +2 -3
- package/dist/cli/commands/meta/get.js.map +1 -1
- package/dist/cli/commands/meta/list.d.ts.map +1 -1
- package/dist/cli/commands/meta/list.js +2 -3
- package/dist/cli/commands/meta/list.js.map +1 -1
- package/dist/cli/commands/meta/set.d.ts.map +1 -1
- package/dist/cli/commands/meta/set.js +2 -3
- package/dist/cli/commands/meta/set.js.map +1 -1
- package/dist/cli/commands/tag/add.js +2 -2
- package/dist/cli/commands/tag/add.js.map +1 -1
- package/dist/cli/commands/tag/attach.d.ts.map +1 -1
- package/dist/cli/commands/tag/attach.js +2 -4
- package/dist/cli/commands/tag/attach.js.map +1 -1
- package/dist/cli/commands/tag/delete.js +2 -2
- package/dist/cli/commands/tag/delete.js.map +1 -1
- package/dist/cli/commands/tag/detach.d.ts.map +1 -1
- package/dist/cli/commands/tag/detach.js +2 -4
- package/dist/cli/commands/tag/detach.js.map +1 -1
- package/dist/cli/commands/tag/list.d.ts.map +1 -1
- package/dist/cli/commands/tag/list.js +2 -3
- package/dist/cli/commands/tag/list.js.map +1 -1
- package/dist/cli/commands/tag/rename.js +2 -2
- package/dist/cli/commands/tag/rename.js.map +1 -1
- package/dist/cli/commands/tag/show.d.ts.map +1 -1
- package/dist/cli/commands/tag/show.js +2 -3
- package/dist/cli/commands/tag/show.js.map +1 -1
- package/dist/cli/commands/task/add.d.ts.map +1 -1
- package/dist/cli/commands/task/add.js +3 -3
- package/dist/cli/commands/task/add.js.map +1 -1
- package/dist/cli/commands/task/count.js +2 -2
- package/dist/cli/commands/task/count.js.map +1 -1
- package/dist/cli/commands/task/delete.js +2 -2
- package/dist/cli/commands/task/delete.js.map +1 -1
- package/dist/cli/commands/task/find.d.ts.map +1 -1
- package/dist/cli/commands/task/find.js +2 -3
- package/dist/cli/commands/task/find.js.map +1 -1
- package/dist/cli/commands/task/get.d.ts.map +1 -1
- package/dist/cli/commands/task/get.js +204 -209
- package/dist/cli/commands/task/get.js.map +1 -1
- package/dist/cli/commands/task/list.d.ts.map +1 -1
- package/dist/cli/commands/task/list.js +324 -161
- package/dist/cli/commands/task/list.js.map +1 -1
- package/dist/cli/commands/task/purge.js +2 -2
- package/dist/cli/commands/task/purge.js.map +1 -1
- package/dist/cli/commands/task/update-helpers.d.ts +42 -0
- package/dist/cli/commands/task/update-helpers.d.ts.map +1 -0
- package/dist/cli/commands/task/update-helpers.js +154 -0
- package/dist/cli/commands/task/update-helpers.js.map +1 -0
- package/dist/cli/commands/task/update-parent.js +2 -2
- package/dist/cli/commands/task/update-parent.js.map +1 -1
- package/dist/cli/commands/task/update.d.ts.map +1 -1
- package/dist/cli/commands/task/update.js +81 -196
- package/dist/cli/commands/task/update.js.map +1 -1
- package/dist/cli/utils/error-handler.js +3 -3
- package/dist/cli/utils/error-handler.js.map +1 -1
- package/dist/cli/utils/output-formatter.d.ts +14 -0
- package/dist/cli/utils/output-formatter.d.ts.map +1 -1
- package/dist/cli/utils/output-formatter.js +36 -0
- package/dist/cli/utils/output-formatter.js.map +1 -1
- package/dist/cli/utils/service-container.d.ts +25 -0
- package/dist/cli/utils/service-container.d.ts.map +1 -0
- package/dist/cli/utils/service-container.js +26 -0
- package/dist/cli/utils/service-container.js.map +1 -0
- package/dist/services/TaskService.d.ts +14 -0
- package/dist/services/TaskService.d.ts.map +1 -1
- package/dist/services/TaskService.js +48 -29
- package/dist/services/TaskService.js.map +1 -1
- package/dist/utils/input-validators.d.ts.map +1 -1
- package/dist/utils/input-validators.js +64 -69
- package/dist/utils/input-validators.js.map +1 -1
- 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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 + '">✎</button>';
|
|
594
|
+
html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">🗑</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(
|
|
567
|
-
|
|
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 + ')">✎</button>';
|
|
577
|
-
html += '<button class="comment-action-btn danger" title="Delete" onclick="deleteComment(' + comment.id + "," + taskId + ')">🗑</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 +=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
700
|
-
const selected = s ===
|
|
701
|
-
html += '<option value="' + s + '"' + selected + ">" +
|
|
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
|
|
705
|
-
|
|
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
|
-
|
|
710
|
-
const 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
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
html
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
html += '<div class="detail-
|
|
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
|
-
|
|
749
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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) + " · 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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
|
841
|
-
if (
|
|
842
|
-
const
|
|
843
|
-
|
|
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
|
-
|
|
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(
|
|
862
|
-
const delta = startX -
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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">×</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
|
|
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",
|
|
1106
|
-
|
|
1107
|
-
|
|
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();
|