yarlo-plugin-board 0.2.0 → 0.2.1
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-server.d.ts +8 -0
- package/dist/board-server.d.ts.map +1 -1
- package/dist/board-server.js +76 -12
- package/dist/board-server.js.map +1 -1
- package/dist/board-ui.html +681 -5
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/source/board-server.test.ts +164 -0
- package/source/board-server.ts +97 -15
- package/source/board-ui.html +681 -5
- package/source/index.ts +1 -1
- package/tsconfig.json +2 -1
- package/vitest.config.ts +14 -0
package/dist/board-ui.html
CHANGED
|
@@ -132,12 +132,33 @@
|
|
|
132
132
|
.card:active { cursor: grabbing; }
|
|
133
133
|
.card:hover { border-color: var(--accent-dim); }
|
|
134
134
|
.card.dragging { opacity: 0.45; }
|
|
135
|
+
.card-head {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: flex-start;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
gap: 0.5rem;
|
|
140
|
+
margin-bottom: 0.45rem;
|
|
141
|
+
}
|
|
135
142
|
.card-title {
|
|
136
143
|
font-size: 0.9rem;
|
|
137
144
|
font-weight: 600;
|
|
138
145
|
line-height: 1.35;
|
|
139
|
-
margin: 0
|
|
146
|
+
margin: 0;
|
|
147
|
+
flex: 1;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
}
|
|
150
|
+
button.card-open {
|
|
151
|
+
font-family: inherit;
|
|
152
|
+
font-size: 0.7rem;
|
|
153
|
+
padding: 0.25rem 0.5rem;
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
border: 1px solid var(--border);
|
|
156
|
+
background: var(--bg);
|
|
157
|
+
color: var(--accent);
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
flex-shrink: 0;
|
|
140
160
|
}
|
|
161
|
+
button.card-open:hover { border-color: var(--accent-dim); }
|
|
141
162
|
.badges {
|
|
142
163
|
display: flex;
|
|
143
164
|
flex-wrap: wrap;
|
|
@@ -158,6 +179,160 @@
|
|
|
158
179
|
text-align: center;
|
|
159
180
|
color: var(--muted);
|
|
160
181
|
}
|
|
182
|
+
.overlay {
|
|
183
|
+
position: fixed;
|
|
184
|
+
inset: 0;
|
|
185
|
+
background: rgba(0, 0, 0, 0.55);
|
|
186
|
+
z-index: 100;
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: flex-start;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
padding: 1.5rem;
|
|
191
|
+
overflow-y: auto;
|
|
192
|
+
}
|
|
193
|
+
.overlay[hidden] { display: none !important; }
|
|
194
|
+
.panel {
|
|
195
|
+
width: min(560px, 100%);
|
|
196
|
+
max-height: calc(100vh - 3rem);
|
|
197
|
+
margin-top: 0.5rem;
|
|
198
|
+
background: var(--surface);
|
|
199
|
+
border: 1px solid var(--border);
|
|
200
|
+
border-radius: 12px;
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.35);
|
|
204
|
+
}
|
|
205
|
+
.panel-header {
|
|
206
|
+
padding: 1rem 1.25rem;
|
|
207
|
+
border-bottom: 1px solid var(--border);
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: space-between;
|
|
211
|
+
gap: 0.75rem;
|
|
212
|
+
}
|
|
213
|
+
.panel-header h2 {
|
|
214
|
+
margin: 0;
|
|
215
|
+
font-size: 1rem;
|
|
216
|
+
font-weight: 700;
|
|
217
|
+
}
|
|
218
|
+
.panel-body {
|
|
219
|
+
padding: 1rem 1.25rem;
|
|
220
|
+
overflow-y: auto;
|
|
221
|
+
flex: 1;
|
|
222
|
+
}
|
|
223
|
+
.panel-footer {
|
|
224
|
+
padding: 0.85rem 1.25rem;
|
|
225
|
+
border-top: 1px solid var(--border);
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-wrap: wrap;
|
|
228
|
+
gap: 0.5rem;
|
|
229
|
+
justify-content: flex-end;
|
|
230
|
+
}
|
|
231
|
+
.panel-footer button {
|
|
232
|
+
font-family: inherit;
|
|
233
|
+
font-size: 0.85rem;
|
|
234
|
+
padding: 0.45rem 0.9rem;
|
|
235
|
+
border-radius: 6px;
|
|
236
|
+
border: 1px solid var(--border);
|
|
237
|
+
background: var(--card);
|
|
238
|
+
color: var(--text);
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
}
|
|
241
|
+
.panel-footer button.primary {
|
|
242
|
+
background: var(--accent-dim);
|
|
243
|
+
border-color: var(--accent);
|
|
244
|
+
color: #fff;
|
|
245
|
+
}
|
|
246
|
+
.panel-footer button.primary:hover { filter: brightness(1.08); }
|
|
247
|
+
.panel-footer button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
248
|
+
.view-title {
|
|
249
|
+
font-size: 1.25rem;
|
|
250
|
+
font-weight: 700;
|
|
251
|
+
margin: 0 0 0.75rem;
|
|
252
|
+
line-height: 1.3;
|
|
253
|
+
}
|
|
254
|
+
.view-meta {
|
|
255
|
+
font-family: "JetBrains Mono", monospace;
|
|
256
|
+
font-size: 0.7rem;
|
|
257
|
+
color: var(--muted);
|
|
258
|
+
margin-bottom: 1rem;
|
|
259
|
+
}
|
|
260
|
+
.view-content {
|
|
261
|
+
white-space: pre-wrap;
|
|
262
|
+
word-break: break-word;
|
|
263
|
+
font-size: 0.9rem;
|
|
264
|
+
line-height: 1.5;
|
|
265
|
+
color: var(--text);
|
|
266
|
+
margin: 0 0 1rem;
|
|
267
|
+
padding: 0.75rem;
|
|
268
|
+
background: var(--bg);
|
|
269
|
+
border-radius: 8px;
|
|
270
|
+
border: 1px solid var(--border);
|
|
271
|
+
min-height: 3rem;
|
|
272
|
+
}
|
|
273
|
+
.view-content.empty { color: var(--muted); font-style: italic; }
|
|
274
|
+
dl.field-list { margin: 0; }
|
|
275
|
+
dl.field-list dt {
|
|
276
|
+
font-size: 0.7rem;
|
|
277
|
+
text-transform: uppercase;
|
|
278
|
+
letter-spacing: 0.05em;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
margin-top: 0.65rem;
|
|
281
|
+
}
|
|
282
|
+
dl.field-list dt:first-child { margin-top: 0; }
|
|
283
|
+
dl.field-list dd { margin: 0.2rem 0 0; font-size: 0.9rem; }
|
|
284
|
+
.form-group { margin-bottom: 1rem; }
|
|
285
|
+
.form-group label {
|
|
286
|
+
display: block;
|
|
287
|
+
font-size: 0.75rem;
|
|
288
|
+
font-weight: 600;
|
|
289
|
+
text-transform: uppercase;
|
|
290
|
+
letter-spacing: 0.04em;
|
|
291
|
+
color: var(--muted);
|
|
292
|
+
margin-bottom: 0.35rem;
|
|
293
|
+
}
|
|
294
|
+
.form-group input[type="text"],
|
|
295
|
+
.form-group input[type="url"],
|
|
296
|
+
.form-group input[type="number"],
|
|
297
|
+
.form-group input[type="date"],
|
|
298
|
+
.form-group input[type="datetime-local"],
|
|
299
|
+
.form-group select,
|
|
300
|
+
.form-group textarea {
|
|
301
|
+
width: 100%;
|
|
302
|
+
font-family: inherit;
|
|
303
|
+
font-size: 0.9rem;
|
|
304
|
+
padding: 0.5rem 0.65rem;
|
|
305
|
+
border-radius: 6px;
|
|
306
|
+
border: 1px solid var(--border);
|
|
307
|
+
background: var(--bg);
|
|
308
|
+
color: var(--text);
|
|
309
|
+
}
|
|
310
|
+
.form-group textarea { min-height: 140px; resize: vertical; }
|
|
311
|
+
.form-group .checkbox-row {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
gap: 0.5rem;
|
|
315
|
+
}
|
|
316
|
+
.form-group .checkbox-row input { width: auto; }
|
|
317
|
+
.multi-options {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
gap: 0.35rem;
|
|
321
|
+
}
|
|
322
|
+
.multi-options label {
|
|
323
|
+
text-transform: none;
|
|
324
|
+
font-weight: 400;
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
gap: 0.45rem;
|
|
328
|
+
color: var(--text);
|
|
329
|
+
}
|
|
330
|
+
#panel-error {
|
|
331
|
+
font-size: 0.85rem;
|
|
332
|
+
color: var(--danger);
|
|
333
|
+
margin-bottom: 0.75rem;
|
|
334
|
+
}
|
|
335
|
+
#panel-error[hidden] { display: none; }
|
|
161
336
|
</style>
|
|
162
337
|
</head>
|
|
163
338
|
<body>
|
|
@@ -172,6 +347,27 @@
|
|
|
172
347
|
<div id="board" class="board" hidden></div>
|
|
173
348
|
<div id="empty" hidden>No tasks yet. Use <code>yarlo add</code> to create one.</div>
|
|
174
349
|
|
|
350
|
+
<div id="overlay" class="overlay" hidden>
|
|
351
|
+
<div class="panel" role="dialog" aria-modal="true" aria-labelledby="panel-heading">
|
|
352
|
+
<div class="panel-header">
|
|
353
|
+
<h2 id="panel-heading">Task</h2>
|
|
354
|
+
<button type="button" id="panel-close" aria-label="Close">✕</button>
|
|
355
|
+
</div>
|
|
356
|
+
<div class="panel-body">
|
|
357
|
+
<div id="panel-error" hidden></div>
|
|
358
|
+
<div id="panel-content"></div>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="panel-footer" id="panel-footer-view">
|
|
361
|
+
<button type="button" id="btn-edit">Edit</button>
|
|
362
|
+
<button type="button" id="btn-close-footer">Close</button>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="panel-footer" id="panel-footer-edit" hidden>
|
|
365
|
+
<button type="button" id="btn-cancel">Cancel</button>
|
|
366
|
+
<button type="button" class="primary" id="btn-save">Save</button>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
175
371
|
<script>
|
|
176
372
|
(function () {
|
|
177
373
|
const ORPHAN_KEY = "__orphan__";
|
|
@@ -180,15 +376,69 @@
|
|
|
180
376
|
const emptyEl = document.getElementById("empty");
|
|
181
377
|
const statusEl = document.getElementById("status");
|
|
182
378
|
const refreshBtn = document.getElementById("refresh");
|
|
379
|
+
const overlayEl = document.getElementById("overlay");
|
|
380
|
+
const panelContentEl = document.getElementById("panel-content");
|
|
381
|
+
const panelErrorEl = document.getElementById("panel-error");
|
|
382
|
+
const panelHeadingEl = document.getElementById("panel-heading");
|
|
383
|
+
const panelFooterView = document.getElementById("panel-footer-view");
|
|
384
|
+
const panelFooterEdit = document.getElementById("panel-footer-edit");
|
|
385
|
+
const btnEdit = document.getElementById("btn-edit");
|
|
386
|
+
const btnCloseFooter = document.getElementById("btn-close-footer");
|
|
387
|
+
const btnCloseX = document.getElementById("panel-close");
|
|
388
|
+
const btnCancel = document.getElementById("btn-cancel");
|
|
389
|
+
const btnSave = document.getElementById("btn-save");
|
|
183
390
|
|
|
184
391
|
let columns = [];
|
|
185
392
|
let tasks = [];
|
|
393
|
+
let fieldDefinitions = [];
|
|
394
|
+
let panelMode = "view";
|
|
395
|
+
let panelDetail = null;
|
|
396
|
+
let panelLoading = false;
|
|
186
397
|
|
|
187
398
|
function setStatus(msg, isError) {
|
|
188
399
|
statusEl.textContent = msg || "";
|
|
189
400
|
statusEl.classList.toggle("error", !!isError);
|
|
190
401
|
}
|
|
191
402
|
|
|
403
|
+
function setPanelError(msg) {
|
|
404
|
+
if (!msg) {
|
|
405
|
+
panelErrorEl.hidden = true;
|
|
406
|
+
panelErrorEl.textContent = "";
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
panelErrorEl.hidden = false;
|
|
410
|
+
panelErrorEl.textContent = msg;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function summaryFromDetail(d) {
|
|
414
|
+
const f = d.fields || {};
|
|
415
|
+
return {
|
|
416
|
+
id: d.id,
|
|
417
|
+
title: d.title,
|
|
418
|
+
status: f.status != null ? f.status : null,
|
|
419
|
+
priority: f.priority != null ? f.priority : null,
|
|
420
|
+
labels: f.labels != null ? f.labels : null,
|
|
421
|
+
assignee: f.assignee != null ? f.assignee : null,
|
|
422
|
+
due_date: f.due_date != null ? f.due_date : null,
|
|
423
|
+
estimate: f.estimate != null ? f.estimate : null,
|
|
424
|
+
updated_at: d.updated_at,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function mergeTaskIntoBoard(detail) {
|
|
429
|
+
const s = summaryFromDetail(detail);
|
|
430
|
+
const idx = tasks.findIndex(function (t) { return t.id === s.id; });
|
|
431
|
+
if (idx >= 0) tasks[idx] = s;
|
|
432
|
+
else tasks.push(s);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function syncTaskUrl(id) {
|
|
436
|
+
const u = new URL(window.location.href);
|
|
437
|
+
if (id) u.searchParams.set("task", id);
|
|
438
|
+
else u.searchParams.delete("task");
|
|
439
|
+
window.history.replaceState({}, "", u.pathname + u.search);
|
|
440
|
+
}
|
|
441
|
+
|
|
192
442
|
function priorityClass(p) {
|
|
193
443
|
if (!p || typeof p !== "string") return "";
|
|
194
444
|
return "priority-" + p.replace(/_/g, "-");
|
|
@@ -199,6 +449,40 @@
|
|
|
199
449
|
return String(key).replace(/_/g, " ");
|
|
200
450
|
}
|
|
201
451
|
|
|
452
|
+
function formatFieldValueForView(def, value) {
|
|
453
|
+
if (value === undefined || value === null) return "—";
|
|
454
|
+
if (def.type === "boolean") return value ? "Yes" : "No";
|
|
455
|
+
if (def.type === "multi_select" && Array.isArray(value)) {
|
|
456
|
+
return value.length ? value.join(", ") : "—";
|
|
457
|
+
}
|
|
458
|
+
return String(value);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function isoToDateInput(iso) {
|
|
462
|
+
if (typeof iso !== "string" || !iso) return "";
|
|
463
|
+
const d = new Date(iso);
|
|
464
|
+
if (isNaN(d.getTime())) return "";
|
|
465
|
+
return d.toISOString().slice(0, 10);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isoToDatetimeLocal(iso) {
|
|
469
|
+
if (typeof iso !== "string" || !iso) return "";
|
|
470
|
+
const d = new Date(iso);
|
|
471
|
+
if (isNaN(d.getTime())) return "";
|
|
472
|
+
const pad = function (n) { return String(n).padStart(2, "0"); };
|
|
473
|
+
return (
|
|
474
|
+
d.getFullYear() +
|
|
475
|
+
"-" +
|
|
476
|
+
pad(d.getMonth() + 1) +
|
|
477
|
+
"-" +
|
|
478
|
+
pad(d.getDate()) +
|
|
479
|
+
"T" +
|
|
480
|
+
pad(d.getHours()) +
|
|
481
|
+
":" +
|
|
482
|
+
pad(d.getMinutes())
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
202
486
|
function bucketTasks(cols, taskList) {
|
|
203
487
|
const colSet = new Set(cols);
|
|
204
488
|
const buckets = new Map();
|
|
@@ -222,10 +506,30 @@
|
|
|
222
506
|
card.dataset.taskId = task.id;
|
|
223
507
|
card.dataset.status = typeof task.status === "string" ? task.status : "";
|
|
224
508
|
|
|
509
|
+
const head = document.createElement("div");
|
|
510
|
+
head.className = "card-head";
|
|
511
|
+
|
|
225
512
|
const title = document.createElement("h2");
|
|
226
513
|
title.className = "card-title";
|
|
227
514
|
title.textContent = task.title || "(no title)";
|
|
228
515
|
|
|
516
|
+
const openBtn = document.createElement("button");
|
|
517
|
+
openBtn.type = "button";
|
|
518
|
+
openBtn.className = "card-open";
|
|
519
|
+
openBtn.textContent = "Open";
|
|
520
|
+
openBtn.draggable = false;
|
|
521
|
+
openBtn.addEventListener("click", function (e) {
|
|
522
|
+
e.stopPropagation();
|
|
523
|
+
e.preventDefault();
|
|
524
|
+
openTaskPanel(task.id);
|
|
525
|
+
});
|
|
526
|
+
openBtn.addEventListener("mousedown", function (e) {
|
|
527
|
+
e.stopPropagation();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
head.appendChild(title);
|
|
531
|
+
head.appendChild(openBtn);
|
|
532
|
+
|
|
229
533
|
const badges = document.createElement("div");
|
|
230
534
|
badges.className = "badges";
|
|
231
535
|
if (task.priority) {
|
|
@@ -249,7 +553,7 @@
|
|
|
249
553
|
badges.appendChild(b);
|
|
250
554
|
}
|
|
251
555
|
|
|
252
|
-
card.appendChild(
|
|
556
|
+
card.appendChild(head);
|
|
253
557
|
if (badges.children.length) card.appendChild(badges);
|
|
254
558
|
|
|
255
559
|
card.addEventListener("dragstart", function (e) {
|
|
@@ -345,9 +649,7 @@
|
|
|
345
649
|
}
|
|
346
650
|
const updated = data.task;
|
|
347
651
|
if (updated && updated.id) {
|
|
348
|
-
|
|
349
|
-
return t.id === updated.id ? updated : t;
|
|
350
|
-
});
|
|
652
|
+
mergeTaskIntoBoard(updated);
|
|
351
653
|
}
|
|
352
654
|
setStatus("Saved");
|
|
353
655
|
render();
|
|
@@ -356,6 +658,374 @@
|
|
|
356
658
|
}
|
|
357
659
|
}
|
|
358
660
|
|
|
661
|
+
function renderPanelView() {
|
|
662
|
+
const d = panelDetail;
|
|
663
|
+
if (!d) return;
|
|
664
|
+
panelContentEl.innerHTML = "";
|
|
665
|
+
const titleEl = document.createElement("h3");
|
|
666
|
+
titleEl.className = "view-title";
|
|
667
|
+
titleEl.textContent = d.title || "(no title)";
|
|
668
|
+
|
|
669
|
+
const meta = document.createElement("div");
|
|
670
|
+
meta.className = "view-meta";
|
|
671
|
+
meta.textContent =
|
|
672
|
+
d.id +
|
|
673
|
+
" · updated " +
|
|
674
|
+
(d.updated_at || "—") +
|
|
675
|
+
" · created " +
|
|
676
|
+
(d.created_at || "—");
|
|
677
|
+
|
|
678
|
+
const contentEl = document.createElement("div");
|
|
679
|
+
const body = (d.content || "").trim();
|
|
680
|
+
contentEl.className = "view-content" + (body ? "" : " empty");
|
|
681
|
+
contentEl.textContent = body || "No description.";
|
|
682
|
+
|
|
683
|
+
panelContentEl.appendChild(titleEl);
|
|
684
|
+
panelContentEl.appendChild(meta);
|
|
685
|
+
panelContentEl.appendChild(contentEl);
|
|
686
|
+
|
|
687
|
+
const dl = document.createElement("dl");
|
|
688
|
+
dl.className = "field-list";
|
|
689
|
+
fieldDefinitions.forEach(function (def) {
|
|
690
|
+
const dt = document.createElement("dt");
|
|
691
|
+
dt.textContent = def.name.replace(/_/g, " ");
|
|
692
|
+
const dd = document.createElement("dd");
|
|
693
|
+
dd.textContent = formatFieldValueForView(def, d.fields[def.name]);
|
|
694
|
+
dl.appendChild(dt);
|
|
695
|
+
dl.appendChild(dd);
|
|
696
|
+
});
|
|
697
|
+
panelContentEl.appendChild(dl);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function renderFieldEditor(def, fields) {
|
|
701
|
+
const wrap = document.createElement("div");
|
|
702
|
+
wrap.className = "form-group";
|
|
703
|
+
const label = document.createElement("label");
|
|
704
|
+
label.setAttribute("for", "fld-" + def.name);
|
|
705
|
+
label.textContent = (def.description || def.name).replace(/_/g, " ");
|
|
706
|
+
wrap.appendChild(label);
|
|
707
|
+
|
|
708
|
+
const raw = fields[def.name];
|
|
709
|
+
const id = "fld-" + def.name;
|
|
710
|
+
|
|
711
|
+
if (def.type === "single_select") {
|
|
712
|
+
const sel = document.createElement("select");
|
|
713
|
+
sel.id = id;
|
|
714
|
+
sel.dataset.fieldName = def.name;
|
|
715
|
+
sel.dataset.fieldType = def.type;
|
|
716
|
+
const opts = def.options || [];
|
|
717
|
+
if (!def.required) {
|
|
718
|
+
const o = document.createElement("option");
|
|
719
|
+
o.value = "";
|
|
720
|
+
o.textContent = "—";
|
|
721
|
+
sel.appendChild(o);
|
|
722
|
+
}
|
|
723
|
+
opts.forEach(function (op) {
|
|
724
|
+
const o = document.createElement("option");
|
|
725
|
+
o.value = op;
|
|
726
|
+
o.textContent = op;
|
|
727
|
+
sel.appendChild(o);
|
|
728
|
+
});
|
|
729
|
+
sel.value = typeof raw === "string" ? raw : "";
|
|
730
|
+
wrap.appendChild(sel);
|
|
731
|
+
return wrap;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (def.type === "multi_select") {
|
|
735
|
+
const box = document.createElement("div");
|
|
736
|
+
box.className = "multi-options";
|
|
737
|
+
box.dataset.fieldName = def.name;
|
|
738
|
+
box.dataset.fieldType = def.type;
|
|
739
|
+
const selected = Array.isArray(raw) ? raw.map(String) : [];
|
|
740
|
+
(def.options || []).forEach(function (op) {
|
|
741
|
+
const row = document.createElement("label");
|
|
742
|
+
const cb = document.createElement("input");
|
|
743
|
+
cb.type = "checkbox";
|
|
744
|
+
cb.value = op;
|
|
745
|
+
cb.checked = selected.indexOf(op) >= 0;
|
|
746
|
+
row.appendChild(cb);
|
|
747
|
+
row.appendChild(document.createTextNode(" " + op));
|
|
748
|
+
box.appendChild(row);
|
|
749
|
+
});
|
|
750
|
+
wrap.appendChild(box);
|
|
751
|
+
return wrap;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (def.type === "boolean") {
|
|
755
|
+
const row = document.createElement("div");
|
|
756
|
+
row.className = "checkbox-row";
|
|
757
|
+
const cb = document.createElement("input");
|
|
758
|
+
cb.type = "checkbox";
|
|
759
|
+
cb.id = id;
|
|
760
|
+
cb.dataset.fieldName = def.name;
|
|
761
|
+
cb.dataset.fieldType = def.type;
|
|
762
|
+
cb.checked = !!raw;
|
|
763
|
+
row.appendChild(cb);
|
|
764
|
+
row.appendChild(document.createTextNode(" Enabled"));
|
|
765
|
+
wrap.appendChild(row);
|
|
766
|
+
return wrap;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (def.type === "number") {
|
|
770
|
+
const inp = document.createElement("input");
|
|
771
|
+
inp.type = "number";
|
|
772
|
+
inp.id = id;
|
|
773
|
+
inp.step = "any";
|
|
774
|
+
inp.dataset.fieldName = def.name;
|
|
775
|
+
inp.dataset.fieldType = def.type;
|
|
776
|
+
inp.value = raw != null && raw !== "" ? String(raw) : "";
|
|
777
|
+
wrap.appendChild(inp);
|
|
778
|
+
return wrap;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (def.type === "date") {
|
|
782
|
+
const inp = document.createElement("input");
|
|
783
|
+
inp.type = "datetime-local";
|
|
784
|
+
inp.id = id;
|
|
785
|
+
inp.dataset.fieldName = def.name;
|
|
786
|
+
inp.dataset.fieldType = def.type;
|
|
787
|
+
inp.value = isoToDatetimeLocal(typeof raw === "string" ? raw : "");
|
|
788
|
+
wrap.appendChild(inp);
|
|
789
|
+
return wrap;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const inp = document.createElement("input");
|
|
793
|
+
inp.type = def.type === "url" ? "url" : "text";
|
|
794
|
+
inp.id = id;
|
|
795
|
+
inp.dataset.fieldName = def.name;
|
|
796
|
+
inp.dataset.fieldType = def.type;
|
|
797
|
+
inp.value = raw != null && raw !== undefined ? String(raw) : "";
|
|
798
|
+
wrap.appendChild(inp);
|
|
799
|
+
return wrap;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function renderPanelEdit() {
|
|
803
|
+
const d = panelDetail;
|
|
804
|
+
if (!d) return;
|
|
805
|
+
panelContentEl.innerHTML = "";
|
|
806
|
+
|
|
807
|
+
const titleGroup = document.createElement("div");
|
|
808
|
+
titleGroup.className = "form-group";
|
|
809
|
+
const tl = document.createElement("label");
|
|
810
|
+
tl.setAttribute("for", "edit-title");
|
|
811
|
+
tl.textContent = "Title";
|
|
812
|
+
const titleInp = document.createElement("input");
|
|
813
|
+
titleInp.type = "text";
|
|
814
|
+
titleInp.id = "edit-title";
|
|
815
|
+
titleInp.value = d.title || "";
|
|
816
|
+
titleGroup.appendChild(tl);
|
|
817
|
+
titleGroup.appendChild(titleInp);
|
|
818
|
+
panelContentEl.appendChild(titleGroup);
|
|
819
|
+
|
|
820
|
+
const contentGroup = document.createElement("div");
|
|
821
|
+
contentGroup.className = "form-group";
|
|
822
|
+
const cl = document.createElement("label");
|
|
823
|
+
cl.setAttribute("for", "edit-content");
|
|
824
|
+
cl.textContent = "Content (markdown)";
|
|
825
|
+
const ta = document.createElement("textarea");
|
|
826
|
+
ta.id = "edit-content";
|
|
827
|
+
ta.value = d.content || "";
|
|
828
|
+
contentGroup.appendChild(cl);
|
|
829
|
+
contentGroup.appendChild(ta);
|
|
830
|
+
panelContentEl.appendChild(contentGroup);
|
|
831
|
+
|
|
832
|
+
const f = { ...d.fields };
|
|
833
|
+
fieldDefinitions.forEach(function (def) {
|
|
834
|
+
panelContentEl.appendChild(renderFieldEditor(def, f));
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function collectEditPayload() {
|
|
839
|
+
const titleInp = document.getElementById("edit-title");
|
|
840
|
+
const contentTa = document.getElementById("edit-content");
|
|
841
|
+
const payload = {
|
|
842
|
+
title: titleInp ? titleInp.value : "",
|
|
843
|
+
content: contentTa ? contentTa.value : "",
|
|
844
|
+
fields: {},
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
fieldDefinitions.forEach(function (def) {
|
|
848
|
+
if (def.type === "single_select") {
|
|
849
|
+
const sel = panelContentEl.querySelector(
|
|
850
|
+
'select[data-field-name="' + def.name + '"]',
|
|
851
|
+
);
|
|
852
|
+
if (!sel) return;
|
|
853
|
+
const v = sel.value;
|
|
854
|
+
if (v === "" && !def.required) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
payload.fields[def.name] = v;
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (def.type === "multi_select") {
|
|
861
|
+
const box = panelContentEl.querySelector(
|
|
862
|
+
'div[data-field-name="' + def.name + '"][data-field-type="multi_select"]',
|
|
863
|
+
);
|
|
864
|
+
if (!box) return;
|
|
865
|
+
const checked = [];
|
|
866
|
+
box.querySelectorAll('input[type="checkbox"]').forEach(function (cb) {
|
|
867
|
+
if (cb.checked) checked.push(cb.value);
|
|
868
|
+
});
|
|
869
|
+
payload.fields[def.name] = checked;
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (def.type === "boolean") {
|
|
873
|
+
const cb = panelContentEl.querySelector(
|
|
874
|
+
'input[data-field-name="' + def.name + '"][data-field-type="boolean"]',
|
|
875
|
+
);
|
|
876
|
+
if (cb) payload.fields[def.name] = !!cb.checked;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (def.type === "number") {
|
|
880
|
+
const inp = panelContentEl.querySelector(
|
|
881
|
+
'input[data-field-name="' + def.name + '"][data-field-type="number"]',
|
|
882
|
+
);
|
|
883
|
+
if (!inp || inp.value === "") return;
|
|
884
|
+
const n = Number.parseFloat(inp.value);
|
|
885
|
+
if (!Number.isNaN(n)) payload.fields[def.name] = n;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (def.type === "date") {
|
|
889
|
+
const inp = panelContentEl.querySelector(
|
|
890
|
+
'input[data-field-name="' + def.name + '"][data-field-type="date"]',
|
|
891
|
+
);
|
|
892
|
+
if (!inp || !inp.value) return;
|
|
893
|
+
const iso = new Date(inp.value);
|
|
894
|
+
if (!isNaN(iso.getTime())) {
|
|
895
|
+
payload.fields[def.name] = iso.toISOString();
|
|
896
|
+
}
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const inp = panelContentEl.querySelector(
|
|
900
|
+
'input[data-field-name="' + def.name + '"]',
|
|
901
|
+
);
|
|
902
|
+
if (inp) payload.fields[def.name] = inp.value;
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
return payload;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function setPanelMode(mode) {
|
|
909
|
+
panelMode = mode;
|
|
910
|
+
if (mode === "view") {
|
|
911
|
+
panelFooterView.hidden = false;
|
|
912
|
+
panelFooterEdit.hidden = true;
|
|
913
|
+
renderPanelView();
|
|
914
|
+
} else {
|
|
915
|
+
panelFooterView.hidden = true;
|
|
916
|
+
panelFooterEdit.hidden = false;
|
|
917
|
+
renderPanelEdit();
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function closeTaskPanel() {
|
|
922
|
+
overlayEl.hidden = true;
|
|
923
|
+
panelDetail = null;
|
|
924
|
+
setPanelError("");
|
|
925
|
+
syncTaskUrl(null);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function openTaskPanel(id) {
|
|
929
|
+
setPanelError("");
|
|
930
|
+
overlayEl.hidden = false;
|
|
931
|
+
panelHeadingEl.textContent = "Task";
|
|
932
|
+
panelContentEl.innerHTML =
|
|
933
|
+
'<p class="view-meta">Loading…</p>';
|
|
934
|
+
panelFooterView.hidden = false;
|
|
935
|
+
panelFooterEdit.hidden = true;
|
|
936
|
+
btnEdit.disabled = true;
|
|
937
|
+
panelLoading = true;
|
|
938
|
+
syncTaskUrl(id);
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
const res = await fetch("/api/tasks/" + encodeURIComponent(id));
|
|
942
|
+
const data = await res.json().catch(function () { return {}; });
|
|
943
|
+
if (!res.ok) {
|
|
944
|
+
panelContentEl.innerHTML = "";
|
|
945
|
+
setPanelError(data.error || res.statusText);
|
|
946
|
+
btnEdit.disabled = true;
|
|
947
|
+
panelLoading = false;
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
panelDetail = data.task;
|
|
951
|
+
panelHeadingEl.textContent = panelDetail.title || panelDetail.id;
|
|
952
|
+
btnEdit.disabled = false;
|
|
953
|
+
setPanelMode("view");
|
|
954
|
+
} catch (err) {
|
|
955
|
+
panelContentEl.innerHTML = "";
|
|
956
|
+
setPanelError(err.message || "Request failed");
|
|
957
|
+
btnEdit.disabled = true;
|
|
958
|
+
}
|
|
959
|
+
panelLoading = false;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function saveTask() {
|
|
963
|
+
if (!panelDetail) return;
|
|
964
|
+
setPanelError("");
|
|
965
|
+
const p = collectEditPayload();
|
|
966
|
+
btnSave.disabled = true;
|
|
967
|
+
try {
|
|
968
|
+
const res = await fetch(
|
|
969
|
+
"/api/tasks/" + encodeURIComponent(panelDetail.id),
|
|
970
|
+
{
|
|
971
|
+
method: "PATCH",
|
|
972
|
+
headers: { "Content-Type": "application/json" },
|
|
973
|
+
body: JSON.stringify({
|
|
974
|
+
title: p.title,
|
|
975
|
+
content: p.content,
|
|
976
|
+
fields: p.fields,
|
|
977
|
+
}),
|
|
978
|
+
},
|
|
979
|
+
);
|
|
980
|
+
const data = await res.json().catch(function () { return {}; });
|
|
981
|
+
if (!res.ok) {
|
|
982
|
+
setPanelError(data.error || res.statusText);
|
|
983
|
+
btnSave.disabled = false;
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
panelDetail = data.task;
|
|
987
|
+
panelHeadingEl.textContent = panelDetail.title || panelDetail.id;
|
|
988
|
+
mergeTaskIntoBoard(panelDetail);
|
|
989
|
+
render();
|
|
990
|
+
setPanelMode("view");
|
|
991
|
+
} catch (err) {
|
|
992
|
+
setPanelError(err.message || "Request failed");
|
|
993
|
+
}
|
|
994
|
+
btnSave.disabled = false;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
btnEdit.addEventListener("click", function () {
|
|
998
|
+
if (panelLoading || !panelDetail) return;
|
|
999
|
+
setPanelMode("edit");
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
btnCancel.addEventListener("click", function () {
|
|
1003
|
+
setPanelMode("view");
|
|
1004
|
+
setPanelError("");
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
btnSave.addEventListener("click", saveTask);
|
|
1008
|
+
|
|
1009
|
+
function closePanelClick(e) {
|
|
1010
|
+
if (e.target === overlayEl) closeTaskPanel();
|
|
1011
|
+
}
|
|
1012
|
+
overlayEl.addEventListener("click", closePanelClick);
|
|
1013
|
+
btnCloseX.addEventListener("click", closeTaskPanel);
|
|
1014
|
+
btnCloseFooter.addEventListener("click", closeTaskPanel);
|
|
1015
|
+
|
|
1016
|
+
document.addEventListener("keydown", function (e) {
|
|
1017
|
+
if (e.key === "Escape" && !overlayEl.hidden) {
|
|
1018
|
+
closeTaskPanel();
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
const panelInner = overlayEl.querySelector(".panel");
|
|
1023
|
+
if (panelInner) {
|
|
1024
|
+
panelInner.addEventListener("click", function (e) {
|
|
1025
|
+
e.stopPropagation();
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
359
1029
|
async function load() {
|
|
360
1030
|
setStatus("");
|
|
361
1031
|
loadingEl.hidden = false;
|
|
@@ -367,9 +1037,15 @@
|
|
|
367
1037
|
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
368
1038
|
columns = data.columns || [];
|
|
369
1039
|
tasks = data.tasks || [];
|
|
1040
|
+
fieldDefinitions = data.fieldDefinitions || [];
|
|
370
1041
|
loadingEl.hidden = true;
|
|
371
1042
|
render();
|
|
372
1043
|
setStatus(columns.length + " columns · " + tasks.length + " tasks");
|
|
1044
|
+
|
|
1045
|
+
const qTask = new URLSearchParams(window.location.search).get("task");
|
|
1046
|
+
if (qTask) {
|
|
1047
|
+
openTaskPanel(qTask);
|
|
1048
|
+
}
|
|
373
1049
|
} catch (err) {
|
|
374
1050
|
loadingEl.hidden = true;
|
|
375
1051
|
setStatus(err.message || "Failed to load", true);
|