agkan 2.12.2 → 2.14.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 +33 -5
- package/dist/board/boardRenderer.js.map +1 -1
- package/dist/board/boardRoutes.d.ts +1 -1
- package/dist/board/boardRoutes.d.ts.map +1 -1
- package/dist/board/boardRoutes.js +76 -5
- 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 +135 -83
- package/dist/board/boardStyles.js.map +1 -1
- package/dist/board/client/board.js +901 -369
- 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/export.d.ts +7 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +30 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/import.d.ts +7 -0
- package/dist/cli/commands/import.d.ts.map +1 -0
- package/dist/cli/commands/import.js +44 -0
- package/dist/cli/commands/import.js.map +1 -0
- 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/index.js +6 -0
- package/dist/cli/index.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/ExportImportService.d.ts +84 -0
- package/dist/services/ExportImportService.d.ts.map +1 -0
- package/dist/services/ExportImportService.js +222 -0
- package/dist/services/ExportImportService.js.map +1 -0
- package/dist/services/ProcessService.d.ts +54 -0
- package/dist/services/ProcessService.d.ts.map +1 -0
- package/dist/services/ProcessService.js +147 -0
- package/dist/services/ProcessService.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/services/TmuxService.d.ts +2 -0
- package/dist/services/TmuxService.d.ts.map +1 -0
- package/dist/services/TmuxService.js +7 -0
- package/dist/services/TmuxService.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -1
- package/dist/services/index.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 +3 -3
|
@@ -149,106 +149,6 @@
|
|
|
149
149
|
document.addEventListener("dragend", stopAutoScroll);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// src/board/client/addTaskModal.ts
|
|
153
|
-
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");
|
|
159
|
-
document.querySelectorAll(".add-btn").forEach((btn) => {
|
|
160
|
-
btn.addEventListener("click", (e) => {
|
|
161
|
-
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();
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
document.getElementById("add-cancel")?.addEventListener("click", () => {
|
|
171
|
-
addModal.classList.remove("show");
|
|
172
|
-
});
|
|
173
|
-
addModal.addEventListener("click", (e) => {
|
|
174
|
-
if (e.target === addModal) addModal.classList.remove("show");
|
|
175
|
-
});
|
|
176
|
-
addTitle.addEventListener("keydown", (e) => {
|
|
177
|
-
if (e.key === "Enter" && !e.isComposing) {
|
|
178
|
-
e.preventDefault();
|
|
179
|
-
document.getElementById("add-submit").click();
|
|
180
|
-
}
|
|
181
|
-
});
|
|
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
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/board/client/contextMenu.ts
|
|
210
|
-
function initContextMenu() {
|
|
211
|
-
const ctxMenu = document.getElementById("context-menu");
|
|
212
|
-
let ctxTargetCard = null;
|
|
213
|
-
document.addEventListener("contextmenu", (e) => {
|
|
214
|
-
const card = e.target.closest(".card");
|
|
215
|
-
if (!card) {
|
|
216
|
-
ctxMenu.style.display = "none";
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
ctxTargetCard = card;
|
|
221
|
-
ctxMenu.style.left = e.clientX + "px";
|
|
222
|
-
ctxMenu.style.top = e.clientY + "px";
|
|
223
|
-
ctxMenu.style.display = "block";
|
|
224
|
-
});
|
|
225
|
-
document.addEventListener("click", (e) => {
|
|
226
|
-
if (!e.target.closest("#context-menu")) {
|
|
227
|
-
ctxMenu.style.display = "none";
|
|
228
|
-
ctxTargetCard = null;
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
document.getElementById("ctx-delete")?.addEventListener("click", async (e) => {
|
|
232
|
-
e.stopPropagation();
|
|
233
|
-
ctxMenu.style.display = "none";
|
|
234
|
-
if (!ctxTargetCard) return;
|
|
235
|
-
const card = ctxTargetCard;
|
|
236
|
-
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
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
152
|
// src/board/client/tags.ts
|
|
253
153
|
var allAvailableTags = [];
|
|
254
154
|
var _getDetailTaskId = null;
|
|
@@ -425,7 +325,7 @@
|
|
|
425
325
|
function setLastUpdatedAt(val) {
|
|
426
326
|
lastUpdatedAt = val;
|
|
427
327
|
}
|
|
428
|
-
var activeFilters = { tagIds: [], priorities: [], assignee: "" };
|
|
328
|
+
var activeFilters = { tagIds: [], priorities: [], assignee: "", searchText: "" };
|
|
429
329
|
function buildFilterParams() {
|
|
430
330
|
const params = new URLSearchParams();
|
|
431
331
|
if (activeFilters.priorities.length > 0) {
|
|
@@ -437,17 +337,103 @@
|
|
|
437
337
|
if (activeFilters.assignee) {
|
|
438
338
|
params.set("assignee", activeFilters.assignee);
|
|
439
339
|
}
|
|
340
|
+
if (activeFilters.searchText) {
|
|
341
|
+
params.set("search", activeFilters.searchText);
|
|
342
|
+
}
|
|
440
343
|
return params;
|
|
441
344
|
}
|
|
442
345
|
var _openTaskDetail = null;
|
|
443
346
|
var _renderDetailPanel = null;
|
|
444
347
|
var _showUpdateWarning = null;
|
|
445
348
|
var _getDetailTaskId2 = null;
|
|
349
|
+
var _setActiveCard = null;
|
|
446
350
|
function registerDetailPanelCallbacks(callbacks) {
|
|
447
351
|
_openTaskDetail = callbacks.openTaskDetail;
|
|
448
352
|
_renderDetailPanel = callbacks.renderDetailPanel;
|
|
449
353
|
_showUpdateWarning = callbacks.showUpdateWarning;
|
|
450
354
|
_getDetailTaskId2 = callbacks.getDetailTaskId;
|
|
355
|
+
_setActiveCard = callbacks.setActiveCard;
|
|
356
|
+
}
|
|
357
|
+
function attachCardListeners(body) {
|
|
358
|
+
body.querySelectorAll(".card").forEach((card) => {
|
|
359
|
+
attachDragListeners(card);
|
|
360
|
+
card.addEventListener("click", async (e) => {
|
|
361
|
+
if (e.defaultPrevented) return;
|
|
362
|
+
if (_openTaskDetail) await _openTaskDetail(card.dataset.id);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function applyIncrementalCardUpdate(body, newHtml) {
|
|
367
|
+
const template = document.createElement("div");
|
|
368
|
+
template.innerHTML = newHtml;
|
|
369
|
+
const newCards = Array.from(template.querySelectorAll(".card"));
|
|
370
|
+
const existingCards = /* @__PURE__ */ new Map();
|
|
371
|
+
body.querySelectorAll(".card").forEach((card) => {
|
|
372
|
+
const id = card.dataset.id;
|
|
373
|
+
if (id) existingCards.set(id, card);
|
|
374
|
+
});
|
|
375
|
+
const newCardIds = /* @__PURE__ */ new Set();
|
|
376
|
+
newCards.forEach((card) => {
|
|
377
|
+
const id = card.dataset.id;
|
|
378
|
+
if (id) newCardIds.add(id);
|
|
379
|
+
});
|
|
380
|
+
existingCards.forEach((card, id) => {
|
|
381
|
+
if (!newCardIds.has(id)) {
|
|
382
|
+
card.remove();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
newCards.forEach((newCard, index) => {
|
|
386
|
+
const id = newCard.dataset.id;
|
|
387
|
+
const newUpdatedAt = newCard.dataset.updatedAt;
|
|
388
|
+
const existing = id ? existingCards.get(id) : void 0;
|
|
389
|
+
if (existing) {
|
|
390
|
+
const existingUpdatedAt = existing.dataset.updatedAt;
|
|
391
|
+
let activeCard;
|
|
392
|
+
if (newUpdatedAt !== existingUpdatedAt) {
|
|
393
|
+
existing.replaceWith(newCard);
|
|
394
|
+
activeCard = newCard;
|
|
395
|
+
} else {
|
|
396
|
+
activeCard = existing;
|
|
397
|
+
}
|
|
398
|
+
const currentChild = body.children[index];
|
|
399
|
+
if (currentChild !== activeCard) {
|
|
400
|
+
body.insertBefore(activeCard, currentChild || null);
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
const currentChild = body.children[index];
|
|
404
|
+
body.insertBefore(newCard, currentChild || null);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function updateColumnHtml(col) {
|
|
409
|
+
const body = document.getElementById("col-" + col.status);
|
|
410
|
+
if (!body) return;
|
|
411
|
+
applyIncrementalCardUpdate(body, col.html);
|
|
412
|
+
const colEl = body.closest(".column");
|
|
413
|
+
if (colEl) {
|
|
414
|
+
const countEl = colEl.querySelector(".column-count");
|
|
415
|
+
if (countEl) countEl.textContent = String(col.count);
|
|
416
|
+
}
|
|
417
|
+
attachCardListeners(body);
|
|
418
|
+
attachAutoScrollToBody(body);
|
|
419
|
+
}
|
|
420
|
+
function isEditingDetailPanel() {
|
|
421
|
+
const editableFields = ["detail-edit-title", "detail-edit-body", "detail-edit-status", "detail-edit-priority"];
|
|
422
|
+
return editableFields.some((id) => document.activeElement && document.activeElement.id === id);
|
|
423
|
+
}
|
|
424
|
+
async function refreshOpenDetailPanel(detailTaskId2) {
|
|
425
|
+
if (isEditingDetailPanel()) {
|
|
426
|
+
if (_showUpdateWarning) _showUpdateWarning();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const taskRes = await fetch("/api/tasks/" + detailTaskId2);
|
|
431
|
+
if (taskRes.ok) {
|
|
432
|
+
const taskData = await taskRes.json();
|
|
433
|
+
if (_renderDetailPanel) _renderDetailPanel(taskData);
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
451
437
|
}
|
|
452
438
|
async function refreshBoardCards() {
|
|
453
439
|
const filterParams = buildFilterParams();
|
|
@@ -456,41 +442,13 @@
|
|
|
456
442
|
const res = await fetch(url);
|
|
457
443
|
if (!res.ok) return;
|
|
458
444
|
const data = await res.json();
|
|
459
|
-
|
|
460
|
-
columns.forEach((col) => {
|
|
461
|
-
const body = document.getElementById("col-" + col.status);
|
|
462
|
-
if (!body) return;
|
|
463
|
-
body.innerHTML = col.html;
|
|
464
|
-
const colEl = body.closest(".column");
|
|
465
|
-
if (colEl) {
|
|
466
|
-
const countEl = colEl.querySelector(".column-count");
|
|
467
|
-
if (countEl) countEl.textContent = String(col.count);
|
|
468
|
-
}
|
|
469
|
-
body.querySelectorAll(".card").forEach((card) => {
|
|
470
|
-
attachDragListeners(card);
|
|
471
|
-
card.addEventListener("click", async (e) => {
|
|
472
|
-
if (e.defaultPrevented) return;
|
|
473
|
-
if (_openTaskDetail) await _openTaskDetail(card.dataset.id);
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
attachAutoScrollToBody(body);
|
|
477
|
-
});
|
|
445
|
+
data.columns.forEach(updateColumnHtml);
|
|
478
446
|
const detailTaskId2 = _getDetailTaskId2 ? _getDetailTaskId2() : null;
|
|
447
|
+
if (detailTaskId2 !== null && _setActiveCard) {
|
|
448
|
+
_setActiveCard(detailTaskId2);
|
|
449
|
+
}
|
|
479
450
|
if (detailTaskId2 !== null) {
|
|
480
|
-
|
|
481
|
-
const isEditing = editableFields.some((id) => document.activeElement && document.activeElement.id === id);
|
|
482
|
-
if (isEditing) {
|
|
483
|
-
if (_showUpdateWarning) _showUpdateWarning();
|
|
484
|
-
} else {
|
|
485
|
-
try {
|
|
486
|
-
const taskRes = await fetch("/api/tasks/" + detailTaskId2);
|
|
487
|
-
if (taskRes.ok) {
|
|
488
|
-
const taskData = await taskRes.json();
|
|
489
|
-
if (_renderDetailPanel) _renderDetailPanel(taskData);
|
|
490
|
-
}
|
|
491
|
-
} catch {
|
|
492
|
-
}
|
|
493
|
-
}
|
|
451
|
+
await refreshOpenDetailPanel(detailTaskId2);
|
|
494
452
|
}
|
|
495
453
|
} catch {
|
|
496
454
|
}
|
|
@@ -502,16 +460,11 @@
|
|
|
502
460
|
if (!res.ok) return;
|
|
503
461
|
const data = await res.json();
|
|
504
462
|
const ts = data.updatedAt;
|
|
505
|
-
const detailPanel = document.getElementById("detail-panel");
|
|
506
463
|
if (lastUpdatedAt === null) {
|
|
507
464
|
lastUpdatedAt = ts;
|
|
508
465
|
} else if (ts !== lastUpdatedAt) {
|
|
509
466
|
lastUpdatedAt = ts;
|
|
510
|
-
|
|
511
|
-
await refreshBoardCards();
|
|
512
|
-
} else {
|
|
513
|
-
location.reload();
|
|
514
|
-
}
|
|
467
|
+
await refreshBoardCards();
|
|
515
468
|
}
|
|
516
469
|
} catch {
|
|
517
470
|
}
|
|
@@ -521,16 +474,316 @@
|
|
|
521
474
|
pollBoardUpdates();
|
|
522
475
|
}
|
|
523
476
|
|
|
477
|
+
// src/board/client/addTaskModal.ts
|
|
478
|
+
var selectedTags = [];
|
|
479
|
+
var tagInputValue = "";
|
|
480
|
+
var tagFocusedIndex = -1;
|
|
481
|
+
function getFilteredAddTags() {
|
|
482
|
+
const selectedIds = new Set(selectedTags.map((t) => t.id));
|
|
483
|
+
const available = allAvailableTags.filter((t) => !selectedIds.has(t.id));
|
|
484
|
+
if (!tagInputValue.trim()) return available;
|
|
485
|
+
const q = tagInputValue.toLowerCase();
|
|
486
|
+
return available.filter((t) => t.name.toLowerCase().includes(q));
|
|
487
|
+
}
|
|
488
|
+
function renderAddTagPills(control, input) {
|
|
489
|
+
control.querySelectorAll(".tag-pill").forEach((p) => p.remove());
|
|
490
|
+
selectedTags.forEach((t) => {
|
|
491
|
+
const pill = document.createElement("span");
|
|
492
|
+
pill.className = "tag-pill";
|
|
493
|
+
pill.dataset.tagId = String(t.id);
|
|
494
|
+
const label = document.createTextNode(t.name);
|
|
495
|
+
const removeBtn = document.createElement("button");
|
|
496
|
+
removeBtn.className = "tag-pill-remove";
|
|
497
|
+
removeBtn.title = "Remove tag";
|
|
498
|
+
removeBtn.innerHTML = "×";
|
|
499
|
+
removeBtn.addEventListener("click", (e) => {
|
|
500
|
+
e.stopPropagation();
|
|
501
|
+
const idx = selectedTags.findIndex((x) => x.id === t.id);
|
|
502
|
+
if (idx !== -1) selectedTags.splice(idx, 1);
|
|
503
|
+
renderAddTagPills(control, input);
|
|
504
|
+
});
|
|
505
|
+
pill.appendChild(label);
|
|
506
|
+
pill.appendChild(removeBtn);
|
|
507
|
+
control.insertBefore(pill, input);
|
|
508
|
+
});
|
|
509
|
+
input.placeholder = selectedTags.length === 0 ? "Add tags..." : "";
|
|
510
|
+
}
|
|
511
|
+
function renderAddTagDropdown(dropdown) {
|
|
512
|
+
const filtered = getFilteredAddTags();
|
|
513
|
+
dropdown.innerHTML = "";
|
|
514
|
+
tagFocusedIndex = -1;
|
|
515
|
+
if (filtered.length === 0) {
|
|
516
|
+
const noOpt = document.createElement("div");
|
|
517
|
+
noOpt.className = "tag-select-no-options";
|
|
518
|
+
noOpt.textContent = tagInputValue ? "No matching tags" : "No tags available";
|
|
519
|
+
dropdown.appendChild(noOpt);
|
|
520
|
+
} else {
|
|
521
|
+
filtered.forEach((t, i) => {
|
|
522
|
+
const opt = document.createElement("div");
|
|
523
|
+
opt.className = "tag-select-option";
|
|
524
|
+
opt.dataset.tagId = String(t.id);
|
|
525
|
+
opt.textContent = t.name;
|
|
526
|
+
opt.addEventListener("mouseover", () => setAddTagFocused(dropdown, i));
|
|
527
|
+
opt.addEventListener("mousedown", (e) => {
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
selectAddTag(t.id, dropdown, document.getElementById("add-tag-input"));
|
|
530
|
+
});
|
|
531
|
+
dropdown.appendChild(opt);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function setAddTagFocused(dropdown, index) {
|
|
536
|
+
const opts = dropdown.querySelectorAll(".tag-select-option");
|
|
537
|
+
opts.forEach((o, i) => o.classList.toggle("focused", i === index));
|
|
538
|
+
tagFocusedIndex = index;
|
|
539
|
+
}
|
|
540
|
+
function selectAddTag(tagId, dropdown, input) {
|
|
541
|
+
const tag = allAvailableTags.find((t) => t.id === tagId);
|
|
542
|
+
if (!tag) return;
|
|
543
|
+
selectedTags.push(tag);
|
|
544
|
+
input.value = "";
|
|
545
|
+
tagInputValue = "";
|
|
546
|
+
const control = document.getElementById("add-tag-select-control");
|
|
547
|
+
renderAddTagPills(control, input);
|
|
548
|
+
renderAddTagDropdown(dropdown);
|
|
549
|
+
}
|
|
550
|
+
function initAddTagSelector() {
|
|
551
|
+
const control = document.getElementById("add-tag-select-control");
|
|
552
|
+
const dropdown = document.getElementById("add-tag-select-dropdown");
|
|
553
|
+
if (!control || !dropdown) return;
|
|
554
|
+
const input = document.createElement("input");
|
|
555
|
+
input.className = "tag-select-input";
|
|
556
|
+
input.id = "add-tag-input";
|
|
557
|
+
input.type = "text";
|
|
558
|
+
input.autocomplete = "off";
|
|
559
|
+
control.appendChild(input);
|
|
560
|
+
control.addEventListener("click", () => input.focus());
|
|
561
|
+
input.addEventListener("focus", () => {
|
|
562
|
+
renderAddTagDropdown(dropdown);
|
|
563
|
+
dropdown.classList.add("open");
|
|
564
|
+
});
|
|
565
|
+
input.addEventListener(
|
|
566
|
+
"blur",
|
|
567
|
+
() => setTimeout(() => {
|
|
568
|
+
dropdown.classList.remove("open");
|
|
569
|
+
tagFocusedIndex = -1;
|
|
570
|
+
}, 150)
|
|
571
|
+
);
|
|
572
|
+
input.addEventListener("input", () => {
|
|
573
|
+
tagInputValue = input.value;
|
|
574
|
+
renderAddTagDropdown(dropdown);
|
|
575
|
+
if (!dropdown.classList.contains("open")) dropdown.classList.add("open");
|
|
576
|
+
});
|
|
577
|
+
input.addEventListener("keydown", (e) => {
|
|
578
|
+
const filtered = getFilteredAddTags();
|
|
579
|
+
const opts = dropdown.querySelectorAll(".tag-select-option");
|
|
580
|
+
if (e.key === "ArrowDown") {
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
setAddTagFocused(dropdown, Math.min(tagFocusedIndex + 1, opts.length - 1));
|
|
583
|
+
} else if (e.key === "ArrowUp") {
|
|
584
|
+
e.preventDefault();
|
|
585
|
+
setAddTagFocused(dropdown, Math.max(tagFocusedIndex - 1, 0));
|
|
586
|
+
} else if (e.key === "Enter") {
|
|
587
|
+
e.preventDefault();
|
|
588
|
+
if (tagFocusedIndex >= 0 && filtered[tagFocusedIndex]) {
|
|
589
|
+
selectAddTag(filtered[tagFocusedIndex].id, dropdown, input);
|
|
590
|
+
}
|
|
591
|
+
} else if (e.key === "Escape") {
|
|
592
|
+
dropdown.classList.remove("open");
|
|
593
|
+
input.blur();
|
|
594
|
+
} else if (e.key === "Backspace" && input.value === "" && selectedTags.length > 0) {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
selectedTags.splice(selectedTags.length - 1, 1);
|
|
597
|
+
renderAddTagPills(control, input);
|
|
598
|
+
renderAddTagDropdown(dropdown);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function addMetadataRow(container) {
|
|
603
|
+
const row = document.createElement("div");
|
|
604
|
+
row.className = "metadata-row";
|
|
605
|
+
const keyInput = document.createElement("input");
|
|
606
|
+
keyInput.type = "text";
|
|
607
|
+
keyInput.className = "metadata-row-key";
|
|
608
|
+
keyInput.placeholder = "Key";
|
|
609
|
+
const valueInput = document.createElement("input");
|
|
610
|
+
valueInput.type = "text";
|
|
611
|
+
valueInput.className = "metadata-row-value";
|
|
612
|
+
valueInput.placeholder = "Value";
|
|
613
|
+
const removeBtn = document.createElement("button");
|
|
614
|
+
removeBtn.type = "button";
|
|
615
|
+
removeBtn.className = "metadata-row-remove";
|
|
616
|
+
removeBtn.title = "Remove";
|
|
617
|
+
removeBtn.innerHTML = "×";
|
|
618
|
+
removeBtn.addEventListener("click", () => {
|
|
619
|
+
row.remove();
|
|
620
|
+
});
|
|
621
|
+
row.appendChild(keyInput);
|
|
622
|
+
row.appendChild(valueInput);
|
|
623
|
+
row.appendChild(removeBtn);
|
|
624
|
+
container.appendChild(row);
|
|
625
|
+
}
|
|
626
|
+
function collectMetadata(container) {
|
|
627
|
+
const rows = container.querySelectorAll(".metadata-row");
|
|
628
|
+
const result = [];
|
|
629
|
+
rows.forEach((row) => {
|
|
630
|
+
const key = (row.querySelector(".metadata-row-key")?.value ?? "").trim();
|
|
631
|
+
const value = (row.querySelector(".metadata-row-value")?.value ?? "").trim();
|
|
632
|
+
if (key) result.push({ key, value });
|
|
633
|
+
});
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
function resetAddModal(elements) {
|
|
637
|
+
elements.addTitle.value = "";
|
|
638
|
+
elements.addBody.value = "";
|
|
639
|
+
elements.addPriority.value = "";
|
|
640
|
+
selectedTags = [];
|
|
641
|
+
tagInputValue = "";
|
|
642
|
+
tagFocusedIndex = -1;
|
|
643
|
+
const control = document.getElementById("add-tag-select-control");
|
|
644
|
+
const input = document.getElementById("add-tag-input");
|
|
645
|
+
if (control && input) {
|
|
646
|
+
renderAddTagPills(control, input);
|
|
647
|
+
}
|
|
648
|
+
elements.addMetadataRows.innerHTML = "";
|
|
649
|
+
}
|
|
650
|
+
function openAddModal(elements, status) {
|
|
651
|
+
elements.addStatus.value = status;
|
|
652
|
+
resetAddModal(elements);
|
|
653
|
+
elements.addModal.classList.add("show");
|
|
654
|
+
elements.addTitle.focus();
|
|
655
|
+
}
|
|
656
|
+
async function submitAddTask(elements) {
|
|
657
|
+
const title = elements.addTitle.value.trim();
|
|
658
|
+
if (!title) {
|
|
659
|
+
elements.addTitle.focus();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const status = elements.addStatus.value;
|
|
663
|
+
elements.addModal.classList.remove("show");
|
|
664
|
+
const tags = selectedTags.map((t) => t.id);
|
|
665
|
+
const metadata = collectMetadata(elements.addMetadataRows);
|
|
666
|
+
try {
|
|
667
|
+
const res = await fetch("/api/tasks", {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: { "Content-Type": "application/json" },
|
|
670
|
+
body: JSON.stringify({
|
|
671
|
+
title,
|
|
672
|
+
body: elements.addBody.value.trim() || null,
|
|
673
|
+
status,
|
|
674
|
+
priority: elements.addPriority.value || null,
|
|
675
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
676
|
+
metadata: metadata.length > 0 ? metadata : void 0
|
|
677
|
+
})
|
|
678
|
+
});
|
|
679
|
+
if (!res.ok) throw new Error("Server error");
|
|
680
|
+
await refreshBoardCards();
|
|
681
|
+
} catch {
|
|
682
|
+
showToast("Failed to add task");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function initAddTaskModal() {
|
|
686
|
+
const elements = {
|
|
687
|
+
addModal: document.getElementById("add-modal"),
|
|
688
|
+
addTitle: document.getElementById("add-title"),
|
|
689
|
+
addBody: document.getElementById("add-body"),
|
|
690
|
+
addPriority: document.getElementById("add-priority"),
|
|
691
|
+
addStatus: document.getElementById("add-status"),
|
|
692
|
+
addTagControl: document.getElementById("add-tag-select-control"),
|
|
693
|
+
addTagDropdown: document.getElementById("add-tag-select-dropdown"),
|
|
694
|
+
addMetadataRows: document.getElementById("add-metadata-rows")
|
|
695
|
+
};
|
|
696
|
+
initAddTagSelector();
|
|
697
|
+
document.querySelectorAll(".add-btn").forEach((btn) => {
|
|
698
|
+
btn.addEventListener("click", (e) => {
|
|
699
|
+
e.stopPropagation();
|
|
700
|
+
openAddModal(elements, btn.dataset.status);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
document.getElementById("add-cancel")?.addEventListener("click", () => {
|
|
704
|
+
elements.addModal.classList.remove("show");
|
|
705
|
+
});
|
|
706
|
+
elements.addModal.addEventListener("click", (e) => {
|
|
707
|
+
if (e.target === elements.addModal) elements.addModal.classList.remove("show");
|
|
708
|
+
});
|
|
709
|
+
elements.addTitle.addEventListener("keydown", (e) => {
|
|
710
|
+
if (e.key === "Enter" && !e.isComposing) {
|
|
711
|
+
e.preventDefault();
|
|
712
|
+
document.getElementById("add-submit").click();
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
document.getElementById("add-metadata-add-row")?.addEventListener("click", () => {
|
|
716
|
+
addMetadataRow(elements.addMetadataRows);
|
|
717
|
+
});
|
|
718
|
+
document.getElementById("add-submit")?.addEventListener("click", () => submitAddTask(elements));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/board/client/contextMenu.ts
|
|
722
|
+
async function deleteCard(card) {
|
|
723
|
+
const taskId = card.dataset.id;
|
|
724
|
+
const status = card.dataset.status;
|
|
725
|
+
if (!confirm("Delete task #" + taskId + "?")) return;
|
|
726
|
+
card.remove();
|
|
727
|
+
updateCount(status);
|
|
728
|
+
try {
|
|
729
|
+
const res = await fetch("/api/tasks/" + taskId, { method: "DELETE" });
|
|
730
|
+
if (!res.ok) throw new Error("Server error");
|
|
731
|
+
} catch {
|
|
732
|
+
location.reload();
|
|
733
|
+
showToast("Failed to delete task");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function initContextMenu() {
|
|
737
|
+
const ctxMenu = document.getElementById("context-menu");
|
|
738
|
+
let ctxTargetCard = null;
|
|
739
|
+
document.addEventListener("contextmenu", (e) => {
|
|
740
|
+
const card = e.target.closest(".card");
|
|
741
|
+
if (!card) {
|
|
742
|
+
ctxMenu.style.display = "none";
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
e.preventDefault();
|
|
746
|
+
ctxTargetCard = card;
|
|
747
|
+
ctxMenu.style.left = e.clientX + "px";
|
|
748
|
+
ctxMenu.style.top = e.clientY + "px";
|
|
749
|
+
ctxMenu.style.display = "block";
|
|
750
|
+
});
|
|
751
|
+
document.addEventListener("click", (e) => {
|
|
752
|
+
if (!e.target.closest("#context-menu")) {
|
|
753
|
+
ctxMenu.style.display = "none";
|
|
754
|
+
ctxTargetCard = null;
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
document.getElementById("ctx-delete")?.addEventListener("click", async (e) => {
|
|
758
|
+
e.stopPropagation();
|
|
759
|
+
ctxMenu.style.display = "none";
|
|
760
|
+
if (!ctxTargetCard) return;
|
|
761
|
+
const card = ctxTargetCard;
|
|
762
|
+
ctxTargetCard = null;
|
|
763
|
+
await deleteCard(card);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
524
767
|
// src/board/client/detailPanel.ts
|
|
525
768
|
var detailTaskId = null;
|
|
526
769
|
var lastTab = "details";
|
|
527
770
|
function getDetailTaskId() {
|
|
528
771
|
return detailTaskId;
|
|
529
772
|
}
|
|
773
|
+
function setActiveCard(taskId) {
|
|
774
|
+
document.querySelectorAll(".card.active").forEach((card) => {
|
|
775
|
+
card.classList.remove("active");
|
|
776
|
+
});
|
|
777
|
+
if (taskId !== null) {
|
|
778
|
+
const card = document.querySelector('.card[data-id="' + taskId + '"]');
|
|
779
|
+
if (card) card.classList.add("active");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
530
782
|
function closeDetailPanel() {
|
|
531
783
|
const detailPanel = document.getElementById("detail-panel");
|
|
532
784
|
detailPanel.classList.remove("open");
|
|
533
785
|
detailPanel.style.width = "";
|
|
786
|
+
setActiveCard(null);
|
|
534
787
|
detailTaskId = null;
|
|
535
788
|
}
|
|
536
789
|
function switchTab(tabName) {
|
|
@@ -560,44 +813,56 @@
|
|
|
560
813
|
if (pane) pane.innerHTML = '<div style="padding:20px;font-size:12px;color:#94a3b8;">Failed to load comments</div>';
|
|
561
814
|
}
|
|
562
815
|
}
|
|
816
|
+
function renderCommentItemHtml(comment, taskId) {
|
|
817
|
+
const authorText = comment.author ? escapeHtmlClient(comment.author) : "Anonymous";
|
|
818
|
+
const dateRel = relativeTime(comment.created_at);
|
|
819
|
+
const dateAbs = escapeHtmlClient(comment.created_at);
|
|
820
|
+
const contentText = escapeHtmlClient(comment.content);
|
|
821
|
+
let html = '<div class="comment-item" data-comment-id="' + comment.id + '">';
|
|
822
|
+
html += '<div class="comment-meta">';
|
|
823
|
+
html += '<span class="comment-author">' + authorText + "</span>";
|
|
824
|
+
html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
|
|
825
|
+
html += '<span class="comment-actions">';
|
|
826
|
+
html += '<button class="comment-action-btn" title="Edit" data-action="start-comment-edit" data-comment-id="' + comment.id + '">✎</button>';
|
|
827
|
+
html += '<button class="comment-action-btn danger" title="Delete" data-action="delete-comment" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">🗑</button>';
|
|
828
|
+
html += "</span></div>";
|
|
829
|
+
html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
|
|
830
|
+
html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
|
|
831
|
+
html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
|
|
832
|
+
html += '<div class="comment-edit-actions">';
|
|
833
|
+
html += '<button class="comment-btn" data-action="save-comment-edit" data-comment-id="' + comment.id + '" data-task-id="' + taskId + '">Save</button>';
|
|
834
|
+
html += '<button class="comment-btn" data-action="cancel-comment-edit" data-comment-id="' + comment.id + '">Cancel</button>';
|
|
835
|
+
html += "</div></div></div>";
|
|
836
|
+
return html;
|
|
837
|
+
}
|
|
838
|
+
function renderAddCommentFormHtml(taskId) {
|
|
839
|
+
let html = '<button class="add-comment-trigger" id="add-comment-trigger" data-action="open-add-comment">+ Add comment...</button>';
|
|
840
|
+
html += '<div class="add-comment-form" id="add-comment-form">';
|
|
841
|
+
html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
|
|
842
|
+
html += "<div>";
|
|
843
|
+
html += '<button class="add-comment-submit" data-action="submit-comment" data-task-id="' + taskId + '">Add Comment</button>';
|
|
844
|
+
html += '<button class="add-comment-cancel" data-action="close-add-comment">Cancel</button>';
|
|
845
|
+
html += "</div></div>";
|
|
846
|
+
return html;
|
|
847
|
+
}
|
|
563
848
|
function renderComments(taskId, comments) {
|
|
564
849
|
const pane = document.getElementById("detail-tab-content-comments");
|
|
565
850
|
if (!pane) return;
|
|
566
851
|
pane.style.padding = "16px 20px";
|
|
567
852
|
let html = "";
|
|
568
|
-
comments.forEach(
|
|
569
|
-
|
|
570
|
-
const dateRel = relativeTime(comment.created_at);
|
|
571
|
-
const dateAbs = escapeHtmlClient(comment.created_at);
|
|
572
|
-
const contentText = escapeHtmlClient(comment.content);
|
|
573
|
-
html += '<div class="comment-item" data-comment-id="' + comment.id + '">';
|
|
574
|
-
html += '<div class="comment-meta">';
|
|
575
|
-
html += '<span class="comment-author">' + authorText + "</span>";
|
|
576
|
-
html += '<span class="comment-date" title="' + dateAbs + '">' + dateRel + "</span>";
|
|
577
|
-
html += '<span class="comment-actions">';
|
|
578
|
-
html += '<button class="comment-action-btn" title="Edit" onclick="startCommentEdit(' + comment.id + ')">✎</button>';
|
|
579
|
-
html += '<button class="comment-action-btn danger" title="Delete" onclick="deleteComment(' + comment.id + "," + taskId + ')">🗑</button>';
|
|
580
|
-
html += "</span>";
|
|
581
|
-
html += "</div>";
|
|
582
|
-
html += '<div class="comment-content" id="comment-content-' + comment.id + '">' + contentText + "</div>";
|
|
583
|
-
html += '<div id="comment-edit-' + comment.id + '" style="display:none;">';
|
|
584
|
-
html += '<textarea class="comment-edit-area" id="comment-edit-area-' + comment.id + '">' + contentText + "</textarea>";
|
|
585
|
-
html += '<div class="comment-edit-actions">';
|
|
586
|
-
html += '<button class="comment-btn" onclick="saveCommentEdit(' + comment.id + "," + taskId + ')">Save</button>';
|
|
587
|
-
html += '<button class="comment-btn" onclick="cancelCommentEdit(' + comment.id + ')">Cancel</button>';
|
|
588
|
-
html += "</div></div>";
|
|
589
|
-
html += "</div>";
|
|
853
|
+
comments.forEach((comment) => {
|
|
854
|
+
html += renderCommentItemHtml(comment, taskId);
|
|
590
855
|
});
|
|
591
|
-
html +=
|
|
592
|
-
html += '<div class="add-comment-form" id="add-comment-form">';
|
|
593
|
-
html += '<textarea class="add-comment-textarea" id="add-comment-text" placeholder="Write a comment..."></textarea>';
|
|
594
|
-
html += "<div>";
|
|
595
|
-
html += '<button class="add-comment-submit" onclick="submitComment(' + taskId + ')">Add Comment</button>';
|
|
596
|
-
html += '<button class="add-comment-cancel" onclick="closeAddCommentForm()">Cancel</button>';
|
|
597
|
-
html += "</div></div>";
|
|
856
|
+
html += renderAddCommentFormHtml(taskId);
|
|
598
857
|
pane.innerHTML = html;
|
|
858
|
+
const paneEl = pane;
|
|
859
|
+
if (paneEl._commentActionHandler) {
|
|
860
|
+
paneEl.removeEventListener("click", paneEl._commentActionHandler);
|
|
861
|
+
}
|
|
862
|
+
paneEl._commentActionHandler = handleCommentAction;
|
|
863
|
+
paneEl.addEventListener("click", paneEl._commentActionHandler);
|
|
599
864
|
}
|
|
600
|
-
|
|
865
|
+
function openAddCommentForm() {
|
|
601
866
|
const trigger = document.getElementById("add-comment-trigger");
|
|
602
867
|
const form = document.getElementById("add-comment-form");
|
|
603
868
|
if (trigger) trigger.style.display = "none";
|
|
@@ -605,8 +870,8 @@
|
|
|
605
870
|
form.classList.add("open");
|
|
606
871
|
form.querySelector("textarea").focus();
|
|
607
872
|
}
|
|
608
|
-
}
|
|
609
|
-
|
|
873
|
+
}
|
|
874
|
+
function closeAddCommentForm() {
|
|
610
875
|
const trigger = document.getElementById("add-comment-trigger");
|
|
611
876
|
const form = document.getElementById("add-comment-form");
|
|
612
877
|
if (trigger) trigger.style.display = "";
|
|
@@ -614,22 +879,22 @@
|
|
|
614
879
|
form.classList.remove("open");
|
|
615
880
|
form.querySelector("textarea").value = "";
|
|
616
881
|
}
|
|
617
|
-
}
|
|
618
|
-
|
|
882
|
+
}
|
|
883
|
+
function startCommentEdit(commentId) {
|
|
619
884
|
const contentEl = document.getElementById("comment-content-" + commentId);
|
|
620
885
|
const editWrapper = document.getElementById("comment-edit-" + commentId);
|
|
621
886
|
if (contentEl) contentEl.style.display = "none";
|
|
622
887
|
if (editWrapper) editWrapper.style.display = "block";
|
|
623
888
|
const area = document.getElementById("comment-edit-area-" + commentId);
|
|
624
889
|
if (area) area.focus();
|
|
625
|
-
}
|
|
626
|
-
|
|
890
|
+
}
|
|
891
|
+
function cancelCommentEdit(commentId) {
|
|
627
892
|
const contentEl = document.getElementById("comment-content-" + commentId);
|
|
628
893
|
const editWrapper = document.getElementById("comment-edit-" + commentId);
|
|
629
894
|
if (contentEl) contentEl.style.display = "";
|
|
630
895
|
if (editWrapper) editWrapper.style.display = "none";
|
|
631
|
-
}
|
|
632
|
-
|
|
896
|
+
}
|
|
897
|
+
async function saveCommentEdit(commentId, taskId) {
|
|
633
898
|
const area = document.getElementById("comment-edit-area-" + commentId);
|
|
634
899
|
if (!area) return;
|
|
635
900
|
const content = area.value.trim();
|
|
@@ -648,8 +913,8 @@
|
|
|
648
913
|
} catch {
|
|
649
914
|
showToast("Failed to update comment");
|
|
650
915
|
}
|
|
651
|
-
}
|
|
652
|
-
|
|
916
|
+
}
|
|
917
|
+
async function deleteComment(commentId, taskId) {
|
|
653
918
|
if (!confirm("Delete this comment?")) return;
|
|
654
919
|
try {
|
|
655
920
|
const res = await fetch("/api/comments/" + commentId, { method: "DELETE" });
|
|
@@ -658,8 +923,8 @@
|
|
|
658
923
|
} catch {
|
|
659
924
|
showToast("Failed to delete comment");
|
|
660
925
|
}
|
|
661
|
-
}
|
|
662
|
-
|
|
926
|
+
}
|
|
927
|
+
async function submitComment(taskId) {
|
|
663
928
|
const textarea = document.getElementById("add-comment-text");
|
|
664
929
|
if (!textarea) return;
|
|
665
930
|
const content = textarea.value.trim();
|
|
@@ -678,99 +943,161 @@
|
|
|
678
943
|
} catch {
|
|
679
944
|
showToast("Failed to add comment");
|
|
680
945
|
}
|
|
681
|
-
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
946
|
+
}
|
|
947
|
+
function dispatchCommentAction(action, commentId, taskId) {
|
|
948
|
+
switch (action) {
|
|
949
|
+
case "open-add-comment":
|
|
950
|
+
openAddCommentForm();
|
|
951
|
+
break;
|
|
952
|
+
case "close-add-comment":
|
|
953
|
+
closeAddCommentForm();
|
|
954
|
+
break;
|
|
955
|
+
case "start-comment-edit":
|
|
956
|
+
startCommentEdit(commentId);
|
|
957
|
+
break;
|
|
958
|
+
case "cancel-comment-edit":
|
|
959
|
+
cancelCommentEdit(commentId);
|
|
960
|
+
break;
|
|
961
|
+
case "save-comment-edit":
|
|
962
|
+
void saveCommentEdit(commentId, taskId);
|
|
963
|
+
break;
|
|
964
|
+
case "delete-comment":
|
|
965
|
+
void deleteComment(commentId, taskId);
|
|
966
|
+
break;
|
|
967
|
+
case "submit-comment":
|
|
968
|
+
void submitComment(taskId);
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function handleCommentAction(e) {
|
|
973
|
+
const target = e.target.closest("[data-action]");
|
|
974
|
+
if (!target) return;
|
|
975
|
+
const action = target.dataset.action ?? "";
|
|
976
|
+
const commentId = target.dataset.commentId ? Number(target.dataset.commentId) : NaN;
|
|
977
|
+
const taskId = target.dataset.taskId ? Number(target.dataset.taskId) : NaN;
|
|
978
|
+
dispatchCommentAction(action, commentId, taskId);
|
|
979
|
+
}
|
|
980
|
+
function renderStatusField(currentStatus, allStatuses, statusLabels) {
|
|
981
|
+
let html = '<div class="detail-field">';
|
|
699
982
|
html += '<div class="detail-field-label">Status</div>';
|
|
700
983
|
html += '<select id="detail-edit-status" class="detail-edit-select">';
|
|
701
|
-
|
|
702
|
-
const selected = s ===
|
|
703
|
-
html += '<option value="' + s + '"' + selected + ">" +
|
|
984
|
+
allStatuses.forEach((s) => {
|
|
985
|
+
const selected = s === currentStatus ? " selected" : "";
|
|
986
|
+
html += '<option value="' + s + '"' + selected + ">" + statusLabels[s] + "</option>";
|
|
704
987
|
});
|
|
705
|
-
html += "</select>";
|
|
706
|
-
html
|
|
707
|
-
|
|
988
|
+
html += "</select></div>";
|
|
989
|
+
return html;
|
|
990
|
+
}
|
|
991
|
+
function renderPriorityField(currentPriority, allPriorities) {
|
|
992
|
+
let html = '<div class="detail-field">';
|
|
708
993
|
html += '<div class="detail-field-label">Priority</div>';
|
|
709
994
|
html += '<select id="detail-edit-priority" class="detail-edit-select">';
|
|
710
995
|
html += '<option value="">None</option>';
|
|
711
|
-
|
|
712
|
-
const selected =
|
|
996
|
+
allPriorities.forEach((p) => {
|
|
997
|
+
const selected = currentPriority === p ? " selected" : "";
|
|
713
998
|
html += '<option value="' + p + '"' + selected + ">" + p.charAt(0).toUpperCase() + p.slice(1) + "</option>";
|
|
714
999
|
});
|
|
715
|
-
html += "</select>";
|
|
716
|
-
html
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
html
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
html += '<div class="detail-
|
|
724
|
-
if (parent) {
|
|
725
|
-
html += '<div class="detail-relation-row">';
|
|
726
|
-
html += '<span class="detail-relation-label">Parent</span>';
|
|
727
|
-
html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
|
|
728
|
-
html += "</div>";
|
|
729
|
-
}
|
|
730
|
-
if (blockedBy.length > 0) {
|
|
731
|
-
html += '<div class="detail-relation-row">';
|
|
732
|
-
html += '<span class="detail-relation-label">Blocked by</span>';
|
|
733
|
-
html += '<div class="detail-relation-ids">';
|
|
734
|
-
blockedBy.forEach((t) => {
|
|
735
|
-
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
736
|
-
});
|
|
737
|
-
html += "</div></div>";
|
|
738
|
-
}
|
|
739
|
-
if (blocking.length > 0) {
|
|
740
|
-
html += '<div class="detail-relation-row">';
|
|
741
|
-
html += '<span class="detail-relation-label">Blocking</span>';
|
|
742
|
-
html += '<div class="detail-relation-ids">';
|
|
743
|
-
blocking.forEach((t) => {
|
|
744
|
-
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
745
|
-
});
|
|
746
|
-
html += "</div></div>";
|
|
747
|
-
}
|
|
1000
|
+
html += "</select></div>";
|
|
1001
|
+
return html;
|
|
1002
|
+
}
|
|
1003
|
+
function renderRelationsHtml(parent, blockedBy, blocking) {
|
|
1004
|
+
let html = '<div class="detail-relations">';
|
|
1005
|
+
if (parent) {
|
|
1006
|
+
html += '<div class="detail-relation-row">';
|
|
1007
|
+
html += '<span class="detail-relation-label">Parent</span>';
|
|
1008
|
+
html += '<div class="detail-relation-ids"><span class="detail-relation-id">#' + parent.id + " " + escapeHtmlClient(parent.title) + "</span></div>";
|
|
748
1009
|
html += "</div>";
|
|
749
1010
|
}
|
|
750
|
-
|
|
751
|
-
|
|
1011
|
+
if (blockedBy.length > 0) {
|
|
1012
|
+
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocked by</span>';
|
|
1013
|
+
html += '<div class="detail-relation-ids">';
|
|
1014
|
+
blockedBy.forEach((t) => {
|
|
1015
|
+
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
1016
|
+
});
|
|
1017
|
+
html += "</div></div>";
|
|
1018
|
+
}
|
|
1019
|
+
if (blocking.length > 0) {
|
|
1020
|
+
html += '<div class="detail-relation-row"><span class="detail-relation-label">Blocking</span>';
|
|
1021
|
+
html += '<div class="detail-relation-ids">';
|
|
1022
|
+
blocking.forEach((t) => {
|
|
1023
|
+
html += '<span class="detail-relation-id">#' + t.id + "</span>";
|
|
1024
|
+
});
|
|
1025
|
+
html += "</div></div>";
|
|
1026
|
+
}
|
|
1027
|
+
html += "</div>";
|
|
1028
|
+
return html;
|
|
1029
|
+
}
|
|
1030
|
+
function autoResizeTextarea(el) {
|
|
1031
|
+
el.style.height = "auto";
|
|
1032
|
+
el.style.height = el.scrollHeight + "px";
|
|
1033
|
+
}
|
|
1034
|
+
function renderMetadataTable(metadata) {
|
|
1035
|
+
const otherMeta = metadata.filter((m) => m.key !== "priority");
|
|
1036
|
+
if (otherMeta.length === 0) return "";
|
|
1037
|
+
let html = '<div class="detail-field"><div class="detail-field-label">Metadata</div>';
|
|
1038
|
+
html += '<table class="detail-meta-table">';
|
|
1039
|
+
otherMeta.forEach((m) => {
|
|
1040
|
+
html += "<tr><td>" + escapeHtmlClient(m.key) + "</td><td>" + escapeHtmlClient(m.value) + "</td></tr>";
|
|
1041
|
+
});
|
|
1042
|
+
html += "</table></div>";
|
|
1043
|
+
return html;
|
|
1044
|
+
}
|
|
1045
|
+
function renderEditableTextFields(task) {
|
|
1046
|
+
let html = '<div class="detail-field"><div class="detail-field-label">Title</div>';
|
|
752
1047
|
html += '<input id="detail-edit-title" class="detail-edit-input" type="text" value="' + escapeHtmlClient(task.title) + '">';
|
|
753
1048
|
html += "</div>";
|
|
754
|
-
html += '<div class="detail-field description-field-wrapper">';
|
|
755
|
-
html += '<div class="detail-field-label">Description</div>';
|
|
1049
|
+
html += '<div class="detail-field description-field-wrapper"><div class="detail-field-label">Description</div>';
|
|
756
1050
|
html += '<textarea id="detail-edit-body" class="detail-edit-textarea">' + escapeHtmlClient(task.body || "") + "</textarea>";
|
|
757
1051
|
html += "</div>";
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1052
|
+
return html;
|
|
1053
|
+
}
|
|
1054
|
+
function renderDetailPanelHtml(data) {
|
|
1055
|
+
const task = data.task;
|
|
1056
|
+
const metadata = data.metadata || [];
|
|
1057
|
+
const blockedBy = data.blockedBy || [];
|
|
1058
|
+
const blocking = data.blocking || [];
|
|
1059
|
+
const parent = data.parent || null;
|
|
1060
|
+
const win = window;
|
|
1061
|
+
const allStatuses = win.allStatuses;
|
|
1062
|
+
const statusLabels = win.statusLabels;
|
|
1063
|
+
const allPriorities = win.allPriorities;
|
|
1064
|
+
let html = "";
|
|
1065
|
+
html += renderStatusField(task.status, allStatuses, statusLabels);
|
|
1066
|
+
html += renderPriorityField(task.priority, allPriorities);
|
|
1067
|
+
html += '<div class="detail-field"><div class="detail-field-label">Tags</div>';
|
|
1068
|
+
html += '<div id="detail-tags-container"></div></div>';
|
|
1069
|
+
const hasRelations = parent || blockedBy.length > 0 || blocking.length > 0;
|
|
1070
|
+
if (hasRelations) {
|
|
1071
|
+
html += renderRelationsHtml(parent, blockedBy, blocking);
|
|
767
1072
|
}
|
|
768
|
-
html +=
|
|
1073
|
+
html += renderMetadataTable(metadata);
|
|
1074
|
+
html += renderEditableTextFields(task);
|
|
1075
|
+
return html;
|
|
1076
|
+
}
|
|
1077
|
+
function renderDetailPanel(data) {
|
|
1078
|
+
document.getElementById("detail-panel-update-warning")?.remove();
|
|
1079
|
+
const detailPanelTitle = document.getElementById("detail-panel-title");
|
|
1080
|
+
const task = data.task;
|
|
1081
|
+
const tags = data.tags || [];
|
|
1082
|
+
detailTaskId = task.id;
|
|
1083
|
+
detailPanelTitle.textContent = "#" + task.id;
|
|
769
1084
|
const detailsPane = document.getElementById("detail-tab-content-details");
|
|
770
1085
|
if (detailsPane) {
|
|
771
|
-
detailsPane.innerHTML =
|
|
1086
|
+
detailsPane.innerHTML = renderDetailPanelHtml(data);
|
|
772
1087
|
detailsPane.style.padding = "20px";
|
|
773
1088
|
}
|
|
1089
|
+
const footer = document.getElementById("detail-panel-footer");
|
|
1090
|
+
if (footer) {
|
|
1091
|
+
footer.innerHTML = '<span class="detail-footer-timestamp">created ' + relativeTime(task.created_at) + " · updated " + relativeTime(task.updated_at) + '</span><button id="detail-save-btn">Save</button>';
|
|
1092
|
+
document.getElementById("detail-save-btn")?.addEventListener("click", saveDetailTask);
|
|
1093
|
+
}
|
|
1094
|
+
const textarea = document.getElementById("detail-edit-body");
|
|
1095
|
+
if (textarea) {
|
|
1096
|
+
autoResizeTextarea(textarea);
|
|
1097
|
+
textarea.addEventListener("input", () => {
|
|
1098
|
+
autoResizeTextarea(textarea);
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
774
1101
|
loadAllTags().then(() => renderTagsSection([...tags])).catch((err) => {
|
|
775
1102
|
console.error("[agkan] renderDetailPanel tags failed", err);
|
|
776
1103
|
});
|
|
@@ -779,14 +1106,15 @@
|
|
|
779
1106
|
}
|
|
780
1107
|
async function openTaskDetail(taskId) {
|
|
781
1108
|
const detailPanel = document.getElementById("detail-panel");
|
|
782
|
-
const
|
|
1109
|
+
const PANEL_DEFAULT_WIDTH2 = 400;
|
|
783
1110
|
try {
|
|
784
1111
|
const res = await fetch("/api/tasks/" + taskId);
|
|
785
1112
|
if (!res.ok) throw new Error("Server error");
|
|
786
1113
|
const data = await res.json();
|
|
787
1114
|
renderDetailPanel(data);
|
|
1115
|
+
setActiveCard(Number(taskId));
|
|
788
1116
|
if (!detailPanel.classList.contains("open")) {
|
|
789
|
-
const preferredWidth = detailPanel.dataset.preferredWidth || String(
|
|
1117
|
+
const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH2);
|
|
790
1118
|
detailPanel.style.width = preferredWidth + "px";
|
|
791
1119
|
detailPanel.classList.add("open");
|
|
792
1120
|
}
|
|
@@ -805,55 +1133,100 @@
|
|
|
805
1133
|
const msgSpan = document.createElement("span");
|
|
806
1134
|
msgSpan.style.cssText = "flex: 1;";
|
|
807
1135
|
msgSpan.textContent = "This task has been updated in the database. Save or discard your changes to see the latest version.";
|
|
808
|
-
const reloadBtn =
|
|
809
|
-
reloadBtn.title = "Reload latest data";
|
|
810
|
-
reloadBtn.textContent = "\u21BA";
|
|
811
|
-
reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
|
|
812
|
-
reloadBtn.addEventListener("click", async () => {
|
|
813
|
-
try {
|
|
814
|
-
const taskRes = await fetch("/api/tasks/" + detailTaskId);
|
|
815
|
-
if (taskRes.ok) {
|
|
816
|
-
const taskData = await taskRes.json();
|
|
817
|
-
renderDetailPanel(taskData);
|
|
818
|
-
}
|
|
819
|
-
} catch {
|
|
820
|
-
}
|
|
821
|
-
});
|
|
1136
|
+
const reloadBtn = buildUpdateWarningReloadBtn();
|
|
822
1137
|
warningEl.appendChild(msgSpan);
|
|
823
1138
|
warningEl.appendChild(reloadBtn);
|
|
824
1139
|
detailPanelBody.insertBefore(warningEl, detailPanelBody.firstChild);
|
|
825
1140
|
}
|
|
826
1141
|
}
|
|
827
|
-
function
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
document.getElementById("detail-tabs")?.addEventListener("click", (e) => {
|
|
834
|
-
const btn = e.target.closest(".detail-tab");
|
|
835
|
-
if (!btn) return;
|
|
836
|
-
switchTab(btn.dataset.tab);
|
|
837
|
-
});
|
|
838
|
-
const resizeHandle = document.getElementById("detail-panel-resize-handle");
|
|
839
|
-
const PANEL_MIN_WIDTH = 280;
|
|
840
|
-
const PANEL_MAX_WIDTH = 800;
|
|
841
|
-
const PANEL_DEFAULT_WIDTH = 400;
|
|
842
|
-
(async function initPanelWidth() {
|
|
843
|
-
let targetWidth = PANEL_DEFAULT_WIDTH;
|
|
1142
|
+
function buildUpdateWarningReloadBtn() {
|
|
1143
|
+
const reloadBtn = document.createElement("button");
|
|
1144
|
+
reloadBtn.title = "Reload latest data";
|
|
1145
|
+
reloadBtn.textContent = "\u21BA";
|
|
1146
|
+
reloadBtn.style.cssText = "background: none; border: none; cursor: pointer; font-size: 1.1em; color: red; padding: 0 2px; line-height: 1; flex-shrink: 0;";
|
|
1147
|
+
reloadBtn.addEventListener("click", async () => {
|
|
844
1148
|
try {
|
|
845
|
-
const
|
|
846
|
-
if (
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
|
|
850
|
-
targetWidth = savedWidth;
|
|
851
|
-
}
|
|
1149
|
+
const taskRes = await fetch("/api/tasks/" + detailTaskId);
|
|
1150
|
+
if (taskRes.ok) {
|
|
1151
|
+
const taskData = await taskRes.json();
|
|
1152
|
+
renderDetailPanel(taskData);
|
|
852
1153
|
}
|
|
853
1154
|
} catch {
|
|
854
1155
|
}
|
|
855
|
-
|
|
856
|
-
|
|
1156
|
+
});
|
|
1157
|
+
return reloadBtn;
|
|
1158
|
+
}
|
|
1159
|
+
function collectEditedTaskFields() {
|
|
1160
|
+
const titleInput = document.getElementById("detail-edit-title");
|
|
1161
|
+
const title = titleInput ? titleInput.value.trim() : "";
|
|
1162
|
+
if (!title) {
|
|
1163
|
+
if (titleInput) titleInput.focus();
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
const bodyEl = document.getElementById("detail-edit-body");
|
|
1167
|
+
const statusEl = document.getElementById("detail-edit-status");
|
|
1168
|
+
const priorityEl = document.getElementById("detail-edit-priority");
|
|
1169
|
+
return {
|
|
1170
|
+
title,
|
|
1171
|
+
body: bodyEl ? bodyEl.value.trim() || null : null,
|
|
1172
|
+
status: statusEl ? statusEl.value : void 0,
|
|
1173
|
+
priority: priorityEl ? priorityEl.value || null : null
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
async function patchAndReloadDetail(taskId, fields) {
|
|
1177
|
+
const res = await fetch("/api/tasks/" + taskId, {
|
|
1178
|
+
method: "PATCH",
|
|
1179
|
+
headers: { "Content-Type": "application/json" },
|
|
1180
|
+
body: JSON.stringify(fields)
|
|
1181
|
+
});
|
|
1182
|
+
if (!res.ok) throw new Error("Server error");
|
|
1183
|
+
const getRes = await fetch("/api/tasks/" + taskId);
|
|
1184
|
+
if (!getRes.ok) throw new Error("Failed to fetch updated task");
|
|
1185
|
+
const data = await getRes.json();
|
|
1186
|
+
renderDetailPanel(data);
|
|
1187
|
+
}
|
|
1188
|
+
async function saveDetailTask() {
|
|
1189
|
+
if (detailTaskId === null) return;
|
|
1190
|
+
const fields = collectEditedTaskFields();
|
|
1191
|
+
if (!fields) return;
|
|
1192
|
+
try {
|
|
1193
|
+
await patchAndReloadDetail(detailTaskId, fields);
|
|
1194
|
+
showToast("Task saved successfully");
|
|
1195
|
+
await syncTimestampAfterSave();
|
|
1196
|
+
refreshBoardCards();
|
|
1197
|
+
} catch {
|
|
1198
|
+
showToast("Failed to update task");
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async function syncTimestampAfterSave() {
|
|
1202
|
+
try {
|
|
1203
|
+
const tsRes = await fetch("/api/board/updated-at");
|
|
1204
|
+
if (tsRes.ok) {
|
|
1205
|
+
const tsData = await tsRes.json();
|
|
1206
|
+
setLastUpdatedAt(tsData.updatedAt);
|
|
1207
|
+
}
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
var PANEL_MIN_WIDTH = 280;
|
|
1212
|
+
var PANEL_MAX_WIDTH = 800;
|
|
1213
|
+
var PANEL_DEFAULT_WIDTH = 400;
|
|
1214
|
+
async function initPanelWidthFromConfig(detailPanel) {
|
|
1215
|
+
let targetWidth = PANEL_DEFAULT_WIDTH;
|
|
1216
|
+
try {
|
|
1217
|
+
const res = await fetch("/api/config");
|
|
1218
|
+
if (res.ok) {
|
|
1219
|
+
const data = await res.json();
|
|
1220
|
+
const savedWidth = data && data.board && data.board.detailPaneWidth;
|
|
1221
|
+
if (typeof savedWidth === "number" && savedWidth >= PANEL_MIN_WIDTH && savedWidth <= PANEL_MAX_WIDTH) {
|
|
1222
|
+
targetWidth = savedWidth;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
detailPanel.dataset.preferredWidth = String(targetWidth);
|
|
1228
|
+
}
|
|
1229
|
+
function attachResizeMousedown(resizeHandle, detailPanel) {
|
|
857
1230
|
resizeHandle.addEventListener("mousedown", function(e) {
|
|
858
1231
|
e.preventDefault();
|
|
859
1232
|
if (!detailPanel.classList.contains("open")) return;
|
|
@@ -863,8 +1236,8 @@
|
|
|
863
1236
|
document.body.style.userSelect = "none";
|
|
864
1237
|
document.body.style.cursor = "col-resize";
|
|
865
1238
|
detailPanel.style.transition = "none";
|
|
866
|
-
function onMouseMove(
|
|
867
|
-
const delta = startX -
|
|
1239
|
+
function onMouseMove(ev) {
|
|
1240
|
+
const delta = startX - ev.clientX;
|
|
868
1241
|
const newWidth = Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, startWidth + delta));
|
|
869
1242
|
detailPanel.style.width = newWidth + "px";
|
|
870
1243
|
}
|
|
@@ -887,47 +1260,31 @@
|
|
|
887
1260
|
document.addEventListener("mousemove", onMouseMove);
|
|
888
1261
|
document.addEventListener("mouseup", onMouseUp);
|
|
889
1262
|
});
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
body: bodyEl ? bodyEl.value.trim() || null : null,
|
|
908
|
-
status: statusEl ? statusEl.value : void 0,
|
|
909
|
-
priority: priorityEl ? priorityEl.value || null : null
|
|
910
|
-
})
|
|
911
|
-
});
|
|
912
|
-
if (!res.ok) throw new Error("Server error");
|
|
913
|
-
const getRes = await fetch("/api/tasks/" + detailTaskId);
|
|
914
|
-
if (!getRes.ok) throw new Error("Failed to fetch updated task");
|
|
915
|
-
const data = await getRes.json();
|
|
916
|
-
renderDetailPanel(data);
|
|
917
|
-
showToast("Task saved successfully");
|
|
918
|
-
try {
|
|
919
|
-
const tsRes = await fetch("/api/board/updated-at");
|
|
920
|
-
if (tsRes.ok) {
|
|
921
|
-
const tsData = await tsRes.json();
|
|
922
|
-
setLastUpdatedAt(tsData.updatedAt);
|
|
923
|
-
}
|
|
924
|
-
} catch {
|
|
925
|
-
}
|
|
926
|
-
refreshBoardCards();
|
|
927
|
-
} catch {
|
|
928
|
-
showToast("Failed to update task");
|
|
1263
|
+
}
|
|
1264
|
+
function initPanelResize(detailPanel) {
|
|
1265
|
+
const resizeHandle = document.getElementById("detail-panel-resize-handle");
|
|
1266
|
+
initPanelWidthFromConfig(detailPanel);
|
|
1267
|
+
attachResizeMousedown(resizeHandle, detailPanel);
|
|
1268
|
+
}
|
|
1269
|
+
function buildDetailPanelHtml() {
|
|
1270
|
+
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>';
|
|
1271
|
+
}
|
|
1272
|
+
function initDetailPanel() {
|
|
1273
|
+
const boardContainer = document.querySelector(".board-container");
|
|
1274
|
+
boardContainer.insertAdjacentHTML("beforeend", buildDetailPanelHtml());
|
|
1275
|
+
const detailPanel = document.getElementById("detail-panel");
|
|
1276
|
+
document.getElementById("detail-panel-close")?.addEventListener("click", closeDetailPanel);
|
|
1277
|
+
document.addEventListener("keydown", (e) => {
|
|
1278
|
+
if (e.key === "Escape" && detailPanel.classList.contains("open")) {
|
|
1279
|
+
closeDetailPanel();
|
|
929
1280
|
}
|
|
930
1281
|
});
|
|
1282
|
+
document.getElementById("detail-tabs")?.addEventListener("click", (e) => {
|
|
1283
|
+
const btn = e.target.closest(".detail-tab");
|
|
1284
|
+
if (!btn) return;
|
|
1285
|
+
switchTab(btn.dataset.tab);
|
|
1286
|
+
});
|
|
1287
|
+
initPanelResize(detailPanel);
|
|
931
1288
|
document.querySelectorAll(".card").forEach((card) => {
|
|
932
1289
|
card.addEventListener("click", async (e) => {
|
|
933
1290
|
if (e.defaultPrevented) return;
|
|
@@ -938,14 +1295,15 @@
|
|
|
938
1295
|
openTaskDetail,
|
|
939
1296
|
renderDetailPanel,
|
|
940
1297
|
showUpdateWarning,
|
|
941
|
-
getDetailTaskId
|
|
1298
|
+
getDetailTaskId,
|
|
1299
|
+
setActiveCard
|
|
942
1300
|
});
|
|
943
1301
|
registerGetDetailTaskId(getDetailTaskId);
|
|
944
1302
|
}
|
|
945
1303
|
|
|
946
1304
|
// src/board/client/filters.ts
|
|
947
1305
|
function isFiltersActive() {
|
|
948
|
-
return activeFilters.priorities.length > 0 || activeFilters.tagIds.length > 0 || activeFilters.assignee !== "";
|
|
1306
|
+
return activeFilters.priorities.length > 0 || activeFilters.tagIds.length > 0 || activeFilters.assignee !== "" || activeFilters.searchText !== "";
|
|
949
1307
|
}
|
|
950
1308
|
function applyFilters() {
|
|
951
1309
|
const clearBtn = document.getElementById("filter-clear");
|
|
@@ -998,6 +1356,17 @@
|
|
|
998
1356
|
applyFilters();
|
|
999
1357
|
});
|
|
1000
1358
|
});
|
|
1359
|
+
const searchInput = document.getElementById("filter-search");
|
|
1360
|
+
let searchTimer = null;
|
|
1361
|
+
if (searchInput) {
|
|
1362
|
+
searchInput.addEventListener("input", () => {
|
|
1363
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
1364
|
+
searchTimer = setTimeout(() => {
|
|
1365
|
+
activeFilters.searchText = searchInput.value.trim();
|
|
1366
|
+
applyFilters();
|
|
1367
|
+
}, 300);
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1001
1370
|
const assigneeInput = document.getElementById("filter-assignee");
|
|
1002
1371
|
let assigneeTimer = null;
|
|
1003
1372
|
if (assigneeInput) {
|
|
@@ -1015,7 +1384,9 @@
|
|
|
1015
1384
|
activeFilters.tagIds = [];
|
|
1016
1385
|
activeFilters.priorities = [];
|
|
1017
1386
|
activeFilters.assignee = "";
|
|
1387
|
+
activeFilters.searchText = "";
|
|
1018
1388
|
document.querySelectorAll(".filter-priority-btn").forEach((btn) => btn.classList.remove("active"));
|
|
1389
|
+
if (searchInput) searchInput.value = "";
|
|
1019
1390
|
if (assigneeInput) assigneeInput.value = "";
|
|
1020
1391
|
renderFilterTagPills();
|
|
1021
1392
|
applyFilters();
|
|
@@ -1082,10 +1453,71 @@
|
|
|
1082
1453
|
});
|
|
1083
1454
|
}
|
|
1084
1455
|
|
|
1456
|
+
// src/board/client/darkMode.ts
|
|
1457
|
+
function applyTheme(preference) {
|
|
1458
|
+
if (preference === "dark") {
|
|
1459
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
1460
|
+
} else if (preference === "light") {
|
|
1461
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
1462
|
+
} else {
|
|
1463
|
+
document.documentElement.removeAttribute("data-theme");
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async function persistThemeToServer(theme) {
|
|
1467
|
+
try {
|
|
1468
|
+
await fetch("/api/config", {
|
|
1469
|
+
method: "PUT",
|
|
1470
|
+
headers: { "Content-Type": "application/json" },
|
|
1471
|
+
body: JSON.stringify({ board: { theme } })
|
|
1472
|
+
});
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
function getActivePreference() {
|
|
1477
|
+
const ssrTheme = document.documentElement.getAttribute("data-theme");
|
|
1478
|
+
if (ssrTheme === "dark" || ssrTheme === "light") return ssrTheme;
|
|
1479
|
+
return "system";
|
|
1480
|
+
}
|
|
1481
|
+
function updateCheckmarks(active) {
|
|
1482
|
+
const items = {
|
|
1483
|
+
dark: "burger-theme-dark",
|
|
1484
|
+
light: "burger-theme-light",
|
|
1485
|
+
system: "burger-theme-system"
|
|
1486
|
+
};
|
|
1487
|
+
for (const [pref, id] of Object.entries(items)) {
|
|
1488
|
+
const el = document.getElementById(id);
|
|
1489
|
+
if (!el) continue;
|
|
1490
|
+
if (pref === active) {
|
|
1491
|
+
if (!el.textContent?.startsWith("\u2713 ")) {
|
|
1492
|
+
el.textContent = "\u2713 " + el.textContent?.replace(/^\u2713 /, "");
|
|
1493
|
+
}
|
|
1494
|
+
} else {
|
|
1495
|
+
el.textContent = el.textContent?.replace(/^\u2713 /, "") ?? el.textContent;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function initDarkMode() {
|
|
1500
|
+
const activePreference = getActivePreference();
|
|
1501
|
+
updateCheckmarks(activePreference);
|
|
1502
|
+
document.getElementById("burger-theme-dark")?.addEventListener("click", () => {
|
|
1503
|
+
applyTheme("dark");
|
|
1504
|
+
updateCheckmarks("dark");
|
|
1505
|
+
void persistThemeToServer("dark");
|
|
1506
|
+
});
|
|
1507
|
+
document.getElementById("burger-theme-light")?.addEventListener("click", () => {
|
|
1508
|
+
applyTheme("light");
|
|
1509
|
+
updateCheckmarks("light");
|
|
1510
|
+
void persistThemeToServer("light");
|
|
1511
|
+
});
|
|
1512
|
+
document.getElementById("burger-theme-system")?.addEventListener("click", () => {
|
|
1513
|
+
applyTheme("system");
|
|
1514
|
+
updateCheckmarks("system");
|
|
1515
|
+
void persistThemeToServer("system");
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1085
1519
|
// src/board/client/burgerMenu.ts
|
|
1086
|
-
function
|
|
1087
|
-
const burgerBtn = document.getElementById("burger-menu-btn");
|
|
1088
|
-
const burgerDropdown = document.getElementById("burger-menu-dropdown");
|
|
1520
|
+
function initBurgerToggle(burgerBtn, burgerDropdown) {
|
|
1089
1521
|
burgerBtn.addEventListener("click", (e) => {
|
|
1090
1522
|
e.stopPropagation();
|
|
1091
1523
|
burgerDropdown.classList.toggle("open");
|
|
@@ -1095,6 +1527,21 @@
|
|
|
1095
1527
|
burgerDropdown.classList.remove("open");
|
|
1096
1528
|
}
|
|
1097
1529
|
});
|
|
1530
|
+
}
|
|
1531
|
+
async function executePurge() {
|
|
1532
|
+
try {
|
|
1533
|
+
const res = await fetch("/api/tasks/purge", {
|
|
1534
|
+
method: "POST",
|
|
1535
|
+
headers: { "Content-Type": "application/json" },
|
|
1536
|
+
body: JSON.stringify({})
|
|
1537
|
+
});
|
|
1538
|
+
if (res.ok) {
|
|
1539
|
+
await refreshBoardCards();
|
|
1540
|
+
}
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function initPurgeModal(burgerDropdown) {
|
|
1098
1545
|
const purgeModal = document.getElementById("purge-confirm-modal");
|
|
1099
1546
|
const purgeConfirmBtn = document.getElementById("purge-confirm-btn");
|
|
1100
1547
|
const purgeCancelBtn = document.getElementById("purge-cancel-btn");
|
|
@@ -1107,32 +1554,12 @@
|
|
|
1107
1554
|
purgeCancelBtn.addEventListener("click", () => {
|
|
1108
1555
|
purgeModal.classList.remove("show");
|
|
1109
1556
|
});
|
|
1110
|
-
purgeConfirmBtn.addEventListener("click",
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
try {
|
|
1114
|
-
const res = await fetch("/api/tasks/purge", {
|
|
1115
|
-
method: "POST",
|
|
1116
|
-
headers: { "Content-Type": "application/json" },
|
|
1117
|
-
body: JSON.stringify({})
|
|
1118
|
-
});
|
|
1119
|
-
const data = await res.json();
|
|
1120
|
-
if (res.ok) {
|
|
1121
|
-
purgeResultEl.textContent = "Purged " + data.count + " task(s).";
|
|
1122
|
-
setTimeout(() => {
|
|
1123
|
-
purgeModal.classList.remove("show");
|
|
1124
|
-
}, 1500);
|
|
1125
|
-
location.reload();
|
|
1126
|
-
} else {
|
|
1127
|
-
purgeResultEl.textContent = "Error: " + (data.error || "Unknown error");
|
|
1128
|
-
}
|
|
1129
|
-
} catch {
|
|
1130
|
-
purgeResultEl.textContent = "Failed to purge tasks.";
|
|
1131
|
-
} finally {
|
|
1132
|
-
purgeConfirmBtn.disabled = false;
|
|
1133
|
-
purgeConfirmBtn.textContent = "Purge";
|
|
1134
|
-
}
|
|
1557
|
+
purgeConfirmBtn.addEventListener("click", () => {
|
|
1558
|
+
purgeModal.classList.remove("show");
|
|
1559
|
+
void executePurge();
|
|
1135
1560
|
});
|
|
1561
|
+
}
|
|
1562
|
+
function initVersionModal(burgerDropdown) {
|
|
1136
1563
|
const versionModal = document.getElementById("version-info-modal");
|
|
1137
1564
|
const versionCloseBtn = document.getElementById("version-info-close");
|
|
1138
1565
|
const versionTextEl = document.getElementById("version-info-text");
|
|
@@ -1152,6 +1579,111 @@
|
|
|
1152
1579
|
versionModal.classList.remove("show");
|
|
1153
1580
|
});
|
|
1154
1581
|
}
|
|
1582
|
+
function initExportModal(burgerDropdown) {
|
|
1583
|
+
document.getElementById("burger-export-tasks")?.addEventListener("click", () => {
|
|
1584
|
+
burgerDropdown.classList.remove("open");
|
|
1585
|
+
const a = document.createElement("a");
|
|
1586
|
+
a.href = "/api/export";
|
|
1587
|
+
a.download = "";
|
|
1588
|
+
document.body.appendChild(a);
|
|
1589
|
+
a.click();
|
|
1590
|
+
document.body.removeChild(a);
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function initImportModal(burgerDropdown) {
|
|
1594
|
+
const importModal = document.getElementById("import-modal");
|
|
1595
|
+
const importCancelBtn = document.getElementById("import-cancel-btn");
|
|
1596
|
+
const importConfirmBtn = document.getElementById("import-confirm-btn");
|
|
1597
|
+
const importResultEl = document.getElementById("import-result");
|
|
1598
|
+
const importDropZone = document.getElementById("import-drop-zone");
|
|
1599
|
+
const importFileInput = document.getElementById("import-file-input");
|
|
1600
|
+
if (!importModal || !importCancelBtn || !importConfirmBtn || !importResultEl || !importDropZone || !importFileInput) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const safeImportModal = importModal;
|
|
1604
|
+
const safeImportCancelBtn = importCancelBtn;
|
|
1605
|
+
const safeImportConfirmBtn = importConfirmBtn;
|
|
1606
|
+
const safeImportResultEl = importResultEl;
|
|
1607
|
+
const safeImportDropZone = importDropZone;
|
|
1608
|
+
const safeImportFileInput = importFileInput;
|
|
1609
|
+
let selectedFile = null;
|
|
1610
|
+
function setFile(file) {
|
|
1611
|
+
selectedFile = file;
|
|
1612
|
+
safeImportResultEl.textContent = `Selected: ${file.name}`;
|
|
1613
|
+
safeImportResultEl.style.color = "#64748b";
|
|
1614
|
+
safeImportConfirmBtn.disabled = false;
|
|
1615
|
+
}
|
|
1616
|
+
document.getElementById("burger-import-tasks")?.addEventListener("click", () => {
|
|
1617
|
+
burgerDropdown.classList.remove("open");
|
|
1618
|
+
selectedFile = null;
|
|
1619
|
+
safeImportResultEl.textContent = "";
|
|
1620
|
+
safeImportConfirmBtn.disabled = true;
|
|
1621
|
+
safeImportFileInput.value = "";
|
|
1622
|
+
safeImportModal.classList.add("show");
|
|
1623
|
+
});
|
|
1624
|
+
safeImportCancelBtn.addEventListener("click", () => {
|
|
1625
|
+
safeImportModal.classList.remove("show");
|
|
1626
|
+
});
|
|
1627
|
+
safeImportFileInput.addEventListener("change", () => {
|
|
1628
|
+
const file = safeImportFileInput.files?.[0];
|
|
1629
|
+
if (file) setFile(file);
|
|
1630
|
+
});
|
|
1631
|
+
safeImportDropZone.addEventListener("dragover", (e) => {
|
|
1632
|
+
e.preventDefault();
|
|
1633
|
+
safeImportDropZone.style.borderColor = "#3b82f6";
|
|
1634
|
+
});
|
|
1635
|
+
safeImportDropZone.addEventListener("dragleave", () => {
|
|
1636
|
+
safeImportDropZone.style.borderColor = "#94a3b8";
|
|
1637
|
+
});
|
|
1638
|
+
safeImportDropZone.addEventListener("drop", (e) => {
|
|
1639
|
+
e.preventDefault();
|
|
1640
|
+
safeImportDropZone.style.borderColor = "#94a3b8";
|
|
1641
|
+
const file = e.dataTransfer?.files?.[0];
|
|
1642
|
+
if (file) setFile(file);
|
|
1643
|
+
});
|
|
1644
|
+
safeImportConfirmBtn.addEventListener("click", async () => {
|
|
1645
|
+
if (!selectedFile) return;
|
|
1646
|
+
safeImportConfirmBtn.disabled = true;
|
|
1647
|
+
safeImportConfirmBtn.textContent = "Importing...";
|
|
1648
|
+
try {
|
|
1649
|
+
const text = await selectedFile.text();
|
|
1650
|
+
const data = JSON.parse(text);
|
|
1651
|
+
const res = await fetch("/api/import", {
|
|
1652
|
+
method: "POST",
|
|
1653
|
+
headers: { "Content-Type": "application/json" },
|
|
1654
|
+
body: JSON.stringify(data)
|
|
1655
|
+
});
|
|
1656
|
+
const result = await res.json();
|
|
1657
|
+
if (res.ok) {
|
|
1658
|
+
safeImportResultEl.textContent = `Imported ${result.importedCount} task(s) successfully.`;
|
|
1659
|
+
safeImportResultEl.style.color = "#16a34a";
|
|
1660
|
+
setTimeout(() => {
|
|
1661
|
+
safeImportModal.classList.remove("show");
|
|
1662
|
+
location.reload();
|
|
1663
|
+
}, 1500);
|
|
1664
|
+
} else {
|
|
1665
|
+
safeImportResultEl.textContent = "Error: " + (result.error || "Unknown error");
|
|
1666
|
+
safeImportResultEl.style.color = "#dc2626";
|
|
1667
|
+
}
|
|
1668
|
+
} catch {
|
|
1669
|
+
safeImportResultEl.textContent = "Failed to import tasks. Invalid JSON file.";
|
|
1670
|
+
safeImportResultEl.style.color = "#dc2626";
|
|
1671
|
+
} finally {
|
|
1672
|
+
safeImportConfirmBtn.disabled = false;
|
|
1673
|
+
safeImportConfirmBtn.textContent = "Import";
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
function initBurgerMenu() {
|
|
1678
|
+
const burgerBtn = document.getElementById("burger-menu-btn");
|
|
1679
|
+
const burgerDropdown = document.getElementById("burger-menu-dropdown");
|
|
1680
|
+
initBurgerToggle(burgerBtn, burgerDropdown);
|
|
1681
|
+
initPurgeModal(burgerDropdown);
|
|
1682
|
+
initExportModal(burgerDropdown);
|
|
1683
|
+
initImportModal(burgerDropdown);
|
|
1684
|
+
initVersionModal(burgerDropdown);
|
|
1685
|
+
initDarkMode();
|
|
1686
|
+
}
|
|
1155
1687
|
|
|
1156
1688
|
// src/board/client/main.ts
|
|
1157
1689
|
initDragDrop();
|