mdboard 1.0.0 → 1.2.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/api.js +752 -0
- package/bin.js +56 -0
- package/config.js +73 -0
- package/defaults.json +43 -0
- package/index.html +865 -137
- package/init.js +45 -4
- package/package.json +9 -2
- package/scanner.js +491 -0
- package/server.js +269 -542
- package/watcher.js +131 -0
- package/workspace.js +220 -0
- package/yaml.js +129 -0
package/index.html
CHANGED
|
@@ -25,9 +25,23 @@ a{color:var(--accent);text-decoration:none}
|
|
|
25
25
|
|
|
26
26
|
/* ── Layout ──────────────────────────────────────────────── */
|
|
27
27
|
.app{display:flex;height:100vh;overflow:hidden}
|
|
28
|
+
.source-rail{width:56px;background:var(--bg);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:12px 0;gap:8px;flex-shrink:0;overflow-y:auto}
|
|
29
|
+
.source-rail:empty{display:none}
|
|
30
|
+
.rail-icon{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer;transition:all .15s;position:relative;color:var(--text2);background:var(--surface2);border:2px solid transparent;flex-shrink:0}
|
|
31
|
+
.rail-icon:hover{border-radius:10px;background:var(--surface);color:var(--text)}
|
|
32
|
+
.rail-icon.active{border-color:var(--accent);border-radius:10px;color:var(--text)}
|
|
33
|
+
.rail-icon.active::before{content:'';position:absolute;left:-14px;top:50%;transform:translateY(-50%);width:4px;height:24px;border-radius:0 4px 4px 0;background:var(--accent)}
|
|
34
|
+
.rail-icon img{width:24px;height:24px;border-radius:4px;object-fit:contain}
|
|
35
|
+
.rail-icon svg{width:20px;height:20px}
|
|
36
|
+
.rail-divider{width:24px;height:2px;background:var(--border);border-radius:1px;flex-shrink:0}
|
|
37
|
+
.rail-icon[data-tooltip]{position:relative}
|
|
38
|
+
.rail-icon[data-tooltip]:hover::after{content:attr(data-tooltip);position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%);background:var(--surface);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;white-space:nowrap;z-index:50;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.3)}
|
|
28
39
|
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
|
29
|
-
.sidebar-logo{padding:20px 16px 16px;font-
|
|
30
|
-
.sidebar-logo
|
|
40
|
+
.sidebar-logo{padding:20px 16px 16px;font-size:14px;font-weight:700;color:var(--text);cursor:pointer;display:flex;align-items:center;gap:10px;transition:opacity .15s}
|
|
41
|
+
.sidebar-logo:hover{opacity:.8}
|
|
42
|
+
.sidebar-logo-icon{width:28px;height:28px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);background:var(--accent-dim);flex-shrink:0}
|
|
43
|
+
.sidebar-logo img{width:28px;height:28px;border-radius:var(--radius-sm);object-fit:contain}
|
|
44
|
+
.sidebar-logo-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
31
45
|
#sidebar-nav{flex:1;padding:8px}
|
|
32
46
|
#sidebar-nav a{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:var(--radius-sm);color:var(--text2);font-size:13px;font-weight:500;transition:all .15s;border-left:2px solid transparent;margin-bottom:2px}
|
|
33
47
|
#sidebar-nav a:hover{color:var(--text);background:var(--surface2)}
|
|
@@ -39,9 +53,6 @@ a{color:var(--accent);text-decoration:none}
|
|
|
39
53
|
|
|
40
54
|
/* ── Header ──────────────────────────────────────────────── */
|
|
41
55
|
.header{padding:16px 24px;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;gap:24px;flex-wrap:wrap}
|
|
42
|
-
.header-title{flex-shrink:0}
|
|
43
|
-
.header-title h1{font-size:18px;font-weight:700;line-height:1.3}
|
|
44
|
-
.header-title p{font-size:12px;color:var(--text2);margin-top:2px}
|
|
45
56
|
.header-section{display:flex;flex-direction:column;gap:4px;min-width:120px}
|
|
46
57
|
.header-section-label{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);font-weight:600}
|
|
47
58
|
.header-section-value{font-size:13px;font-weight:600}
|
|
@@ -89,12 +100,6 @@ a{color:var(--accent);text-decoration:none}
|
|
|
89
100
|
.card:hover{border-color:var(--border2);background:var(--surface2)}
|
|
90
101
|
.card:active{cursor:grabbing}
|
|
91
102
|
.card.dragging{opacity:.4;transform:scale(.97)}
|
|
92
|
-
.card[data-status="backlog"]{border-left-color:var(--text3)}
|
|
93
|
-
.card[data-status="todo"]{border-left-color:var(--accent)}
|
|
94
|
-
.card[data-status="in-progress"]{border-left-color:var(--warning)}
|
|
95
|
-
.card[data-status="in-review"]{border-left-color:var(--purple)}
|
|
96
|
-
.card[data-status="done"]{border-left-color:var(--success)}
|
|
97
|
-
.card[data-status="blocked"]{border-left-color:var(--danger)}
|
|
98
103
|
.card-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}
|
|
99
104
|
.card-id{font-family:var(--mono);font-size:11px;color:var(--text3)}
|
|
100
105
|
.card-priority{display:flex;align-items:center}
|
|
@@ -107,6 +112,7 @@ a{color:var(--accent);text-decoration:none}
|
|
|
107
112
|
.pill-points{background:var(--surface2);color:var(--text2);font-family:var(--mono)}
|
|
108
113
|
.pill-epic{color:#fff;font-size:10px;font-weight:600}
|
|
109
114
|
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--radius-sm);font-size:11px;font-weight:600;text-transform:capitalize}
|
|
115
|
+
/* Fallback badge styles — overridden by dynamic styles once config loads */
|
|
110
116
|
.badge-backlog{background:var(--surface2);color:var(--text2)}
|
|
111
117
|
.badge-todo{background:var(--accent-dim);color:var(--accent)}
|
|
112
118
|
.badge-in-progress{background:var(--warning-dim);color:var(--warning)}
|
|
@@ -212,11 +218,39 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
212
218
|
.loading-container{padding:20px}
|
|
213
219
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
214
220
|
|
|
221
|
+
/* ── Workspace: Source Icon (reused in header chips) ──────── */
|
|
222
|
+
.source-icon{width:20px;height:20px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;font-weight:700;line-height:1}
|
|
223
|
+
|
|
224
|
+
/* ── Link Chips ──────────────────────────────────────────── */
|
|
225
|
+
.link-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface2);color:var(--text2);transition:all .15s;white-space:nowrap}
|
|
226
|
+
.link-chip:hover{background:var(--border);color:var(--text)}
|
|
227
|
+
.link-chip-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
228
|
+
.link-chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
|
|
229
|
+
|
|
230
|
+
/* ── CRUD Buttons ────────────────────────────────────────── */
|
|
231
|
+
.btn-sm{padding:5px 12px;font-size:12px;border-radius:var(--radius-sm)}
|
|
232
|
+
.btn-create{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
|
|
233
|
+
.btn-create:hover{opacity:.9}
|
|
234
|
+
.card-readonly{opacity:.75;cursor:default}
|
|
235
|
+
.card-readonly:hover{border-color:var(--border);background:var(--bg)}
|
|
236
|
+
|
|
237
|
+
/* ── Overview: Tracked Milestones ────────────────────────── */
|
|
238
|
+
.tracked-ms{margin-top:12px;padding:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm)}
|
|
239
|
+
.tracked-ms-header{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text3);font-weight:600;margin-bottom:8px}
|
|
240
|
+
.tracked-ms-item{display:flex;align-items:center;gap:8px;padding:4px 0}
|
|
241
|
+
.tracked-ms-item .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
242
|
+
.tracked-ms-item .progress{flex:1;max-width:120px}
|
|
243
|
+
.tracked-ms-item span{font-size:12px}
|
|
244
|
+
.tracked-ms-pct{font-family:var(--mono);font-weight:600;font-size:12px;min-width:36px;text-align:right}
|
|
245
|
+
|
|
215
246
|
/* ── Responsive ──────────────────────────────────────────── */
|
|
216
247
|
@media(max-width:1024px){
|
|
248
|
+
.source-rail{width:48px;padding:8px 0;gap:6px}
|
|
249
|
+
.rail-icon{width:34px;height:34px;font-size:15px;border-radius:10px}
|
|
250
|
+
.rail-icon.active::before{left:-12px;height:18px}
|
|
217
251
|
.sidebar{width:56px}
|
|
218
|
-
.sidebar-logo
|
|
219
|
-
.sidebar-logo{padding:16px 12px;
|
|
252
|
+
.sidebar-logo-text,.sidebar-footer,#sidebar-nav a span{display:none}
|
|
253
|
+
.sidebar-logo{padding:16px 12px;justify-content:center}
|
|
220
254
|
#sidebar-nav a{justify-content:center;padding:10px}
|
|
221
255
|
.header{padding:12px 16px}
|
|
222
256
|
.content{padding:16px}
|
|
@@ -229,12 +263,20 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
229
263
|
.panel-props{grid-template-columns:1fr}
|
|
230
264
|
}
|
|
231
265
|
</style>
|
|
266
|
+
<style id="dynamic-styles"></style>
|
|
267
|
+
<link id="custom-theme" rel="stylesheet" href="/mdboard.css">
|
|
232
268
|
</head>
|
|
233
269
|
<body>
|
|
234
270
|
<div class="app">
|
|
271
|
+
<!-- Source Rail (Discord/Slack style) -->
|
|
272
|
+
<nav class="source-rail" id="source-rail"></nav>
|
|
273
|
+
|
|
235
274
|
<!-- Sidebar -->
|
|
236
275
|
<aside class="sidebar">
|
|
237
|
-
<div class="sidebar-logo"
|
|
276
|
+
<div class="sidebar-logo" id="sidebar-logo">
|
|
277
|
+
<span class="sidebar-logo-icon" id="sidebar-logo-icon">■</span>
|
|
278
|
+
<span class="sidebar-logo-text" id="sidebar-logo-text">mdboard</span>
|
|
279
|
+
</div>
|
|
238
280
|
<nav id="sidebar-nav">
|
|
239
281
|
<a href="#board" data-view="board" class="active">
|
|
240
282
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
|
@@ -259,10 +301,6 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
259
301
|
<div class="main">
|
|
260
302
|
<!-- Header -->
|
|
261
303
|
<header class="header">
|
|
262
|
-
<div class="header-title">
|
|
263
|
-
<h1 id="h-project-name">Loading...</h1>
|
|
264
|
-
<p id="h-project-desc"></p>
|
|
265
|
-
</div>
|
|
266
304
|
<div class="header-section" id="h-milestone-wrap" style="display:none">
|
|
267
305
|
<span class="header-section-label">Milestone</span>
|
|
268
306
|
<span class="header-section-value" id="h-milestone-name"></span>
|
|
@@ -294,6 +332,9 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
294
332
|
<div id="view-metrics" class="view">
|
|
295
333
|
<div id="metrics-container"></div>
|
|
296
334
|
</div>
|
|
335
|
+
<div id="view-overview" class="view">
|
|
336
|
+
<div id="overview-container"></div>
|
|
337
|
+
</div>
|
|
297
338
|
</div>
|
|
298
339
|
</div>
|
|
299
340
|
</div>
|
|
@@ -304,37 +345,45 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
304
345
|
|
|
305
346
|
<script>
|
|
306
347
|
/* ══════════════════════════════════════════════════════════════
|
|
307
|
-
|
|
348
|
+
ICON SHAPES — SVG generators by icon name
|
|
308
349
|
══════════════════════════════════════════════════════════════ */
|
|
350
|
+
var ICON_SHAPES = {
|
|
351
|
+
'dashed-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5" stroke-dasharray="3 2"/></svg>'; },
|
|
352
|
+
'circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/></svg>'; },
|
|
353
|
+
'half-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 0 1 8 14Z" fill="' + c + '"/></svg>'; },
|
|
354
|
+
'three-quarter-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 1 1 2 8L8 8Z" fill="' + c + '"/></svg>'; },
|
|
355
|
+
'check-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="' + c + '"/><path d="M5 8.5L7 10.5L11 5.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>'; },
|
|
356
|
+
'x-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; },
|
|
357
|
+
'slash-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; }
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/* Config-driven maps — populated by initFromConfig() */
|
|
361
|
+
var STATUS_ICON_MAP = {};
|
|
362
|
+
var STATUS_COLOR_MAP = {};
|
|
363
|
+
var ENTITY_NAMES = {};
|
|
364
|
+
|
|
309
365
|
function statusIcon(status) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#8B8B93" stroke-width="1.5"/></svg>';
|
|
315
|
-
case 'in-progress':
|
|
316
|
-
return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#D4A72C" stroke-width="1.5"/><path d="M8 2A6 6 0 0 1 8 14Z" fill="#D4A72C"/></svg>';
|
|
317
|
-
case 'in-review':
|
|
318
|
-
return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#8B5CF6" stroke-width="1.5"/><path d="M8 2A6 6 0 1 1 2 8L8 8Z" fill="#8B5CF6"/></svg>';
|
|
319
|
-
case 'done':
|
|
320
|
-
return '<svg class="icon" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="#2EA043"/><path d="M5 8.5L7 10.5L11 5.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
321
|
-
case 'blocked':
|
|
322
|
-
return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#DA3633" stroke-width="1.5"/><path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="#DA3633" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
|
323
|
-
case 'cancelled':
|
|
324
|
-
return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#5A5A63" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="#5A5A63" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
|
325
|
-
default:
|
|
326
|
-
return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="#5A5A63" stroke-width="1.5" stroke-dasharray="3 2"/></svg>';
|
|
366
|
+
var entry = STATUS_ICON_MAP[status];
|
|
367
|
+
if (entry) {
|
|
368
|
+
var fn = ICON_SHAPES[entry.icon];
|
|
369
|
+
if (fn) return fn(entry.color);
|
|
327
370
|
}
|
|
371
|
+
// Fallback: dashed circle
|
|
372
|
+
return ICON_SHAPES['dashed-circle']('#5A5A63');
|
|
328
373
|
}
|
|
329
374
|
|
|
330
375
|
function priorityIcon(priority) {
|
|
331
|
-
var c, n;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
376
|
+
var c = '#3A3A40', n = 0;
|
|
377
|
+
if (D.config && D.config.priorities) {
|
|
378
|
+
var entry = D.config.priorities.find(function(p) { return p.key === priority; });
|
|
379
|
+
if (entry) { c = entry.color; n = entry.bars; }
|
|
380
|
+
} else {
|
|
381
|
+
switch (priority) {
|
|
382
|
+
case 'urgent': c = '#F97316'; n = 4; break;
|
|
383
|
+
case 'high': c = '#F97316'; n = 3; break;
|
|
384
|
+
case 'medium': c = '#D4A72C'; n = 2; break;
|
|
385
|
+
case 'low': c = '#5B6EF5'; n = 1; break;
|
|
386
|
+
}
|
|
338
387
|
}
|
|
339
388
|
var bars = '';
|
|
340
389
|
for (var i = 0; i < 4; i++) {
|
|
@@ -347,13 +396,17 @@ function priorityIcon(priority) {
|
|
|
347
396
|
}
|
|
348
397
|
|
|
349
398
|
function milestoneIcon(status) {
|
|
399
|
+
var msStatuses = D.config && D.config.statuses ? D.config.statuses.milestone : null;
|
|
400
|
+
var entry = msStatuses ? msStatuses.find(function(s) { return s.key === status; }) : null;
|
|
401
|
+
var c = entry ? entry.color : '#8B8B93';
|
|
402
|
+
|
|
350
403
|
switch (status) {
|
|
351
404
|
case 'active':
|
|
352
|
-
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="
|
|
405
|
+
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/><path d="M8 5L11 8L8 11L5 8Z" fill="' + c + '"/></svg>';
|
|
353
406
|
case 'completed':
|
|
354
|
-
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="
|
|
407
|
+
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="' + c + '"/><path d="M5.5 8L7 9.5L10.5 6" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
355
408
|
default:
|
|
356
|
-
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="
|
|
409
|
+
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/></svg>';
|
|
357
410
|
}
|
|
358
411
|
}
|
|
359
412
|
|
|
@@ -361,8 +414,9 @@ function milestoneIcon(status) {
|
|
|
361
414
|
DATA STORE
|
|
362
415
|
══════════════════════════════════════════════════════════════ */
|
|
363
416
|
var D = {
|
|
364
|
-
project: null, milestones: [], epics: [],
|
|
365
|
-
sprints: [], allSprints: [], metrics: null, health: null, loaded: false
|
|
417
|
+
config: null, project: null, milestones: [], epics: [], tasks: [],
|
|
418
|
+
sprints: [], allSprints: [], metrics: null, health: null, loaded: false,
|
|
419
|
+
sources: [], activeSource: null, overviewLinks: null
|
|
366
420
|
};
|
|
367
421
|
|
|
368
422
|
/* ── Helpers ─────────────────────────────────────────────── */
|
|
@@ -392,6 +446,8 @@ var STATUSES = ['backlog','todo','in-progress','in-review','done','blocked','can
|
|
|
392
446
|
var PRIORITIES = ['urgent','high','medium','low'];
|
|
393
447
|
var STATUS_LABELS = {backlog:'Backlog',todo:'Todo','in-progress':'In Progress','in-review':'In Review',done:'Done',blocked:'Blocked',cancelled:'Cancelled'};
|
|
394
448
|
var PRIORITY_LABELS = {urgent:'Urgent',high:'High',medium:'Medium',low:'Low'};
|
|
449
|
+
var BOARD_COLS = ['backlog','todo','in-progress','in-review','done'];
|
|
450
|
+
var COMPLETED_STATUS = 'done';
|
|
395
451
|
|
|
396
452
|
/* ── Toast ───────────────────────────────────────────────── */
|
|
397
453
|
function showToast(msg, type) {
|
|
@@ -410,7 +466,8 @@ async function fetchJson(url) {
|
|
|
410
466
|
|
|
411
467
|
async function patchItem(type, id, updates) {
|
|
412
468
|
try {
|
|
413
|
-
var
|
|
469
|
+
var base = apiBase();
|
|
470
|
+
var r = await fetch(base + '/' + type + '/' + encodeURIComponent(id), {
|
|
414
471
|
method: 'PATCH',
|
|
415
472
|
headers: { 'Content-Type': 'application/json' },
|
|
416
473
|
body: JSON.stringify(updates),
|
|
@@ -429,27 +486,283 @@ async function patchItem(type, id, updates) {
|
|
|
429
486
|
}
|
|
430
487
|
}
|
|
431
488
|
|
|
489
|
+
/* ── CRUD API helpers ─────────────────────────────────────── */
|
|
490
|
+
function apiBase() {
|
|
491
|
+
if (D.activeSource) return '/api/sources/' + encodeURIComponent(D.activeSource);
|
|
492
|
+
return '/api';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function createItem(collection, data) {
|
|
496
|
+
try {
|
|
497
|
+
var url = apiBase() + '/' + collection;
|
|
498
|
+
var r = await fetch(url, {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { 'Content-Type': 'application/json' },
|
|
501
|
+
body: JSON.stringify(data),
|
|
502
|
+
});
|
|
503
|
+
var result = await r.json();
|
|
504
|
+
if (result && result.ok) {
|
|
505
|
+
showToast('Created ' + (result.id || ''), 'success');
|
|
506
|
+
return result;
|
|
507
|
+
} else {
|
|
508
|
+
showToast('Error: ' + (result.error || 'Unknown'), 'error');
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
} catch (e) {
|
|
512
|
+
showToast('Error: ' + e.message, 'error');
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function deleteItem(collection, id) {
|
|
518
|
+
try {
|
|
519
|
+
var url = apiBase() + '/' + collection + '/' + encodeURIComponent(id);
|
|
520
|
+
var r = await fetch(url, { method: 'DELETE' });
|
|
521
|
+
var result = await r.json();
|
|
522
|
+
if (result && result.ok) {
|
|
523
|
+
showToast('Archived successfully', 'success');
|
|
524
|
+
return true;
|
|
525
|
+
} else {
|
|
526
|
+
showToast('Error: ' + (result.error || 'Unknown'), 'error');
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
showToast('Error: ' + e.message, 'error');
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* ── Data Loading ────────────────────────────────────────── */
|
|
432
536
|
async function loadAll() {
|
|
537
|
+
var base = apiBase();
|
|
433
538
|
var results = await Promise.all([
|
|
434
|
-
fetchJson('/
|
|
435
|
-
fetchJson('/
|
|
436
|
-
fetchJson('/
|
|
539
|
+
fetchJson(base + '/project'), fetchJson(base + '/milestones'), fetchJson(base + '/epics'),
|
|
540
|
+
fetchJson(base + '/tasks'), fetchJson(base + '/sprint'), fetchJson(base + '/metrics'),
|
|
541
|
+
fetchJson(base + '/health'), fetchJson(base + '/sprints'), fetchJson('/api/config'),
|
|
542
|
+
fetchJson('/api/sources')
|
|
437
543
|
]);
|
|
438
544
|
D.project = results[0]; D.milestones = results[1] || []; D.epics = results[2] || [];
|
|
439
|
-
D.
|
|
440
|
-
if (results[4]) D.sprints = [results[4]];
|
|
545
|
+
D.tasks = results[3] || [];
|
|
546
|
+
if (results[4]) D.sprints = [results[4]]; else D.sprints = [];
|
|
441
547
|
D.metrics = results[5]; D.health = results[6];
|
|
442
548
|
D.allSprints = results[7] || [];
|
|
549
|
+
D.config = results[8];
|
|
550
|
+
D.sources = results[9] || [];
|
|
443
551
|
D.loaded = true;
|
|
444
552
|
}
|
|
445
553
|
|
|
446
|
-
function refreshData() {
|
|
554
|
+
function refreshData() {
|
|
555
|
+
loadAll().then(function() {
|
|
556
|
+
initFromConfig();
|
|
557
|
+
renderAll();
|
|
558
|
+
if (hasWorkspace()) buildSourceRail();
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function hasWorkspace() {
|
|
563
|
+
return D.config && D.config.workspace && D.config.workspace.sources && D.config.workspace.sources.length > 0;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* ── Config-driven initialization ────────────────────────── */
|
|
567
|
+
function initFromConfig() {
|
|
568
|
+
if (!D.config) return;
|
|
569
|
+
|
|
570
|
+
var cfg = D.config;
|
|
571
|
+
|
|
572
|
+
// Derive entity names
|
|
573
|
+
ENTITY_NAMES = cfg.entities || {};
|
|
574
|
+
|
|
575
|
+
// Derive statuses, labels, board columns
|
|
576
|
+
if (cfg.statuses && cfg.statuses.task) {
|
|
577
|
+
STATUSES = cfg.statuses.task.map(function(s) { return s.key; });
|
|
578
|
+
STATUS_LABELS = {};
|
|
579
|
+
cfg.statuses.task.forEach(function(s) { STATUS_LABELS[s.key] = s.label; });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Build icon/color maps for all status types
|
|
583
|
+
STATUS_ICON_MAP = {};
|
|
584
|
+
STATUS_COLOR_MAP = {};
|
|
585
|
+
if (cfg.statuses) {
|
|
586
|
+
Object.keys(cfg.statuses).forEach(function(entityType) {
|
|
587
|
+
cfg.statuses[entityType].forEach(function(s) {
|
|
588
|
+
if (!STATUS_ICON_MAP[s.key]) {
|
|
589
|
+
STATUS_ICON_MAP[s.key] = { icon: s.icon || 'dashed-circle', color: s.color || '#5A5A63' };
|
|
590
|
+
STATUS_COLOR_MAP[s.key] = s.color || '#5A5A63';
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Derive priorities
|
|
597
|
+
if (cfg.priorities) {
|
|
598
|
+
PRIORITIES = cfg.priorities.map(function(p) { return p.key; });
|
|
599
|
+
PRIORITY_LABELS = {};
|
|
600
|
+
cfg.priorities.forEach(function(p) { PRIORITY_LABELS[p.key] = p.label; });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Board columns
|
|
604
|
+
if (cfg.boardColumns) BOARD_COLS = cfg.boardColumns;
|
|
605
|
+
|
|
606
|
+
// Completed status
|
|
607
|
+
if (cfg.completedStatus) COMPLETED_STATUS = cfg.completedStatus;
|
|
608
|
+
|
|
609
|
+
generateDynamicStyles();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function generateDynamicStyles() {
|
|
613
|
+
var css = '';
|
|
614
|
+
if (D.config && D.config.statuses && D.config.statuses.task) {
|
|
615
|
+
D.config.statuses.task.forEach(function(s) {
|
|
616
|
+
var key = s.key;
|
|
617
|
+
var color = s.color;
|
|
618
|
+
css += '.badge-' + key + '{background:' + color + '20;color:' + color + '}\n';
|
|
619
|
+
css += '.card[data-status="' + key + '"]{border-left-color:' + color + '}\n';
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
// Milestone/epic/sprint statuses
|
|
623
|
+
['milestone', 'epic', 'sprint'].forEach(function(entityType) {
|
|
624
|
+
if (D.config && D.config.statuses && D.config.statuses[entityType]) {
|
|
625
|
+
D.config.statuses[entityType].forEach(function(s) {
|
|
626
|
+
css += '.badge-' + s.key + '{background:' + s.color + '20;color:' + s.color + '}\n';
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
// Priority badges
|
|
631
|
+
if (D.config && D.config.priorities) {
|
|
632
|
+
D.config.priorities.forEach(function(p) {
|
|
633
|
+
css += '.badge-' + p.key + '{background:' + p.color + '20;color:' + p.color + '}\n';
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
var styleEl = document.getElementById('dynamic-styles');
|
|
637
|
+
if (styleEl) styleEl.textContent = css;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* ══════════════════════════════════════════════════════════════
|
|
641
|
+
WORKSPACE — Source switching & sidebar
|
|
642
|
+
══════════════════════════════════════════════════════════════ */
|
|
643
|
+
function renderSidebarLogo() {
|
|
644
|
+
var p = D.project || {};
|
|
645
|
+
var logoEl = document.getElementById('sidebar-logo');
|
|
646
|
+
var iconEl = document.getElementById('sidebar-logo-icon');
|
|
647
|
+
var textEl = document.getElementById('sidebar-logo-text');
|
|
648
|
+
|
|
649
|
+
textEl.textContent = p.name || 'Project';
|
|
650
|
+
|
|
651
|
+
if (D.config && D.config.logo) {
|
|
652
|
+
var logoSrc = D.config.logo.startsWith('http') ? D.config.logo : '/logo';
|
|
653
|
+
iconEl.innerHTML = '<img src="' + escHtml(logoSrc) + '" alt="">';
|
|
654
|
+
} else {
|
|
655
|
+
var initial = (p.name || 'P').charAt(0).toUpperCase();
|
|
656
|
+
iconEl.textContent = initial;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
logoEl.onclick = function() {
|
|
660
|
+
if (hasWorkspace()) {
|
|
661
|
+
switchSource('overview');
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function buildSourceRail() {
|
|
667
|
+
var rail = document.getElementById('source-rail');
|
|
668
|
+
if (!rail) return;
|
|
669
|
+
rail.innerHTML = '';
|
|
670
|
+
|
|
671
|
+
if (!hasWorkspace()) return;
|
|
672
|
+
|
|
673
|
+
// Home icon (overview)
|
|
674
|
+
var home = document.createElement('div');
|
|
675
|
+
home.className = 'rail-icon' + (D.activeSource === 'overview' ? ' active' : '');
|
|
676
|
+
home.dataset.source = 'overview';
|
|
677
|
+
home.setAttribute('data-tooltip', 'Overview');
|
|
678
|
+
home.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
|
|
679
|
+
home.onclick = function() { switchSource('overview'); };
|
|
680
|
+
rail.appendChild(home);
|
|
681
|
+
|
|
682
|
+
// Divider
|
|
683
|
+
var divider = document.createElement('div');
|
|
684
|
+
divider.className = 'rail-divider';
|
|
685
|
+
rail.appendChild(divider);
|
|
686
|
+
|
|
687
|
+
// Source icons
|
|
688
|
+
var wsSources = D.config.workspace.sources;
|
|
689
|
+
for (var i = 0; i < wsSources.length; i++) {
|
|
690
|
+
var s = wsSources[i];
|
|
691
|
+
if (s.name === 'overview') continue;
|
|
692
|
+
|
|
693
|
+
var color = s.color || 'var(--accent)';
|
|
694
|
+
var icon = document.createElement('div');
|
|
695
|
+
icon.className = 'rail-icon' + (D.activeSource === s.name ? ' active' : '');
|
|
696
|
+
icon.dataset.source = s.name;
|
|
697
|
+
icon.setAttribute('data-tooltip', s.label || s.name);
|
|
698
|
+
icon.style.background = color + '20';
|
|
699
|
+
icon.style.color = color;
|
|
700
|
+
icon.innerHTML = escHtml(s.icon || s.name.charAt(0).toUpperCase());
|
|
701
|
+
icon.onclick = (function(name) {
|
|
702
|
+
return function() { switchSource(name); };
|
|
703
|
+
})(s.name);
|
|
704
|
+
rail.appendChild(icon);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function switchSource(sourceName) {
|
|
709
|
+
if (sourceName === D.activeSource) return;
|
|
710
|
+
|
|
711
|
+
// Update active source marker
|
|
712
|
+
D.activeSource = sourceName;
|
|
713
|
+
D.loaded = false;
|
|
714
|
+
|
|
715
|
+
// Persist selection
|
|
716
|
+
try { localStorage.setItem('mdboard-source', sourceName); } catch(e) {}
|
|
717
|
+
|
|
718
|
+
// Update source rail active state
|
|
719
|
+
document.querySelectorAll('.rail-icon').forEach(function(el) {
|
|
720
|
+
el.classList.toggle('active', el.dataset.source === sourceName);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// If switching to overview, show overview tabs; otherwise show normal tabs
|
|
724
|
+
var isOverview = sourceName === 'overview';
|
|
725
|
+
var viewTabs = document.querySelectorAll('#sidebar-nav a[data-view]');
|
|
726
|
+
viewTabs.forEach(function(tab) {
|
|
727
|
+
var view = tab.dataset.view;
|
|
728
|
+
if (isOverview) {
|
|
729
|
+
tab.style.display = (view === 'overview' || view === 'metrics') ? '' : 'none';
|
|
730
|
+
} else {
|
|
731
|
+
tab.style.display = (view === 'overview') ? 'none' : '';
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Switch to appropriate view
|
|
736
|
+
if (isOverview) {
|
|
737
|
+
switchView('overview');
|
|
738
|
+
} else {
|
|
739
|
+
var currentView = document.querySelector('.view.active');
|
|
740
|
+
if (currentView && currentView.id === 'view-overview') {
|
|
741
|
+
switchView('board');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Reload data for this source
|
|
746
|
+
loadAll().then(function() {
|
|
747
|
+
initFromConfig();
|
|
748
|
+
renderAll();
|
|
749
|
+
buildSourceRail();
|
|
750
|
+
});
|
|
751
|
+
}
|
|
447
752
|
|
|
448
753
|
/* ── SSE — Hot Reload ────────────────────────────────────── */
|
|
449
754
|
function connectSSE() {
|
|
450
755
|
try {
|
|
451
756
|
var src = new EventSource('/api/events');
|
|
452
|
-
src.onmessage = function() {
|
|
757
|
+
src.onmessage = function(e) {
|
|
758
|
+
var data;
|
|
759
|
+
try { data = JSON.parse(e.data); } catch { data = {}; }
|
|
760
|
+
if (data.cssReload) {
|
|
761
|
+
var link = document.getElementById('custom-theme');
|
|
762
|
+
if (link) link.href = '/mdboard.css?' + Date.now();
|
|
763
|
+
}
|
|
764
|
+
refreshData();
|
|
765
|
+
};
|
|
453
766
|
src.onerror = function() { setTimeout(connectSSE, 5000); src.close(); };
|
|
454
767
|
} catch { /* no SSE support */ }
|
|
455
768
|
}
|
|
@@ -457,12 +770,17 @@ function connectSSE() {
|
|
|
457
770
|
/* ══════════════════════════════════════════════════════════════
|
|
458
771
|
RENDER
|
|
459
772
|
══════════════════════════════════════════════════════════════ */
|
|
460
|
-
function renderAll() {
|
|
773
|
+
function renderAll() {
|
|
774
|
+
renderHeader(); renderBoardFilters(); renderBoard(); renderTableControls(); renderTableBody(); renderMsFilters(); renderMilestones(); renderMetrics();
|
|
775
|
+
// Render overview if active
|
|
776
|
+
var overviewView = document.getElementById('view-overview');
|
|
777
|
+
if (overviewView && overviewView.classList.contains('active')) {
|
|
778
|
+
renderOverview();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
461
781
|
|
|
462
782
|
function renderHeader() {
|
|
463
|
-
|
|
464
|
-
document.getElementById('h-project-name').textContent = p.name || 'Project';
|
|
465
|
-
document.getElementById('h-project-desc').textContent = p.description || '';
|
|
783
|
+
renderSidebarLogo();
|
|
466
784
|
|
|
467
785
|
var mw = document.getElementById('h-milestone-wrap');
|
|
468
786
|
var am = D.milestones.find(function(m) { return m.status === 'active'; });
|
|
@@ -481,12 +799,15 @@ function renderHeader() {
|
|
|
481
799
|
document.getElementById('h-sprint-days').textContent = dr !== null ? (dr >= 0 ? dr + ' days left' : Math.abs(dr) + ' days over') : '';
|
|
482
800
|
} else { sw.style.display = 'none'; }
|
|
483
801
|
|
|
484
|
-
var
|
|
485
|
-
var
|
|
486
|
-
var
|
|
802
|
+
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
|
|
803
|
+
var total = D.tasks.length;
|
|
804
|
+
var done = D.tasks.filter(function(f) { return f.status === COMPLETED_STATUS; }).length;
|
|
805
|
+
var inProgStatus = (D.config && D.config.statuses && D.config.statuses.task) ?
|
|
806
|
+
((D.config.statuses.task.find(function(s) { return s.icon === 'half-circle'; }) || {}).key || 'in-progress') : 'in-progress';
|
|
807
|
+
var inProg = D.tasks.filter(function(f) { return f.status === inProgStatus; }).length;
|
|
487
808
|
var vel = D.health && D.health.velocity != null ? D.health.velocity : (total ? Math.round(done / total * 100) : 0);
|
|
488
809
|
document.getElementById('h-stats').innerHTML =
|
|
489
|
-
'<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">
|
|
810
|
+
'<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">' + escHtml(taskPlural) + '</span></div>' +
|
|
490
811
|
'<div class="stat"><span class="stat-val" style="color:var(--success)">' + done + '</span><span class="stat-label">Done</span></div>' +
|
|
491
812
|
'<div class="stat"><span class="stat-val" style="color:var(--warning)">' + inProg + '</span><span class="stat-label">In Progress</span></div>' +
|
|
492
813
|
'<div class="stat"><span class="stat-val" style="color:var(--accent)">' + vel + '%</span><span class="stat-label">Velocity</span></div>';
|
|
@@ -495,26 +816,32 @@ function renderHeader() {
|
|
|
495
816
|
/* ══════════════════════════════════════════════════════════════
|
|
496
817
|
BOARD VIEW
|
|
497
818
|
══════════════════════════════════════════════════════════════ */
|
|
498
|
-
var BOARD_COLS = ['backlog','todo','in-progress','in-review','done'];
|
|
499
819
|
var boardFilters = { search: '', priority: '', epic: '', milestone: '' };
|
|
500
820
|
|
|
501
821
|
function renderBoardFilters() {
|
|
502
822
|
var c = document.getElementById('board-filters');
|
|
503
823
|
var ep = [], ms = [];
|
|
504
|
-
D.
|
|
824
|
+
D.tasks.forEach(function(f) {
|
|
505
825
|
if (f.epic && ep.indexOf(f.epic) === -1) ep.push(f.epic);
|
|
506
826
|
if (f.milestone && ms.indexOf(f.milestone) === -1) ms.push(f.milestone);
|
|
507
827
|
});
|
|
508
828
|
|
|
829
|
+
var epicPlural = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.plural : 'Epics';
|
|
830
|
+
var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural : 'Milestones';
|
|
831
|
+
|
|
509
832
|
function opts(arr, key, label) {
|
|
510
833
|
return '<select data-filter="' + key + '"><option value="">All ' + label + '</option>' +
|
|
511
834
|
arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (boardFilters[key] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
|
|
512
835
|
}
|
|
513
836
|
|
|
514
|
-
|
|
837
|
+
var createBtn = isSourceWritable() ?
|
|
838
|
+
'<button class="btn btn-sm btn-create" id="board-create-btn">+ New ' + escHtml(ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task') + '</button>' : '';
|
|
839
|
+
|
|
840
|
+
c.innerHTML = createBtn +
|
|
841
|
+
'<input type="text" data-filter="search" placeholder="Search cards..." value="' + escHtml(boardFilters.search) + '">' +
|
|
515
842
|
'<select data-filter="priority"><option value="">All Priorities</option>' +
|
|
516
843
|
PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (boardFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>' +
|
|
517
|
-
opts(ep, 'epic',
|
|
844
|
+
opts(ep, 'epic', epicPlural) + opts(ms, 'milestone', msPlural);
|
|
518
845
|
|
|
519
846
|
c.querySelectorAll('input[data-filter]').forEach(function(el) {
|
|
520
847
|
el.addEventListener('input', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
|
|
@@ -522,10 +849,13 @@ function renderBoardFilters() {
|
|
|
522
849
|
c.querySelectorAll('select[data-filter]').forEach(function(el) {
|
|
523
850
|
el.addEventListener('change', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
|
|
524
851
|
});
|
|
852
|
+
|
|
853
|
+
var btn = document.getElementById('board-create-btn');
|
|
854
|
+
if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
|
|
525
855
|
}
|
|
526
856
|
|
|
527
|
-
function
|
|
528
|
-
var list = D.
|
|
857
|
+
function getFilteredBoardTasks() {
|
|
858
|
+
var list = D.tasks.slice();
|
|
529
859
|
var f = boardFilters;
|
|
530
860
|
if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function(x) { return (x.id || '').toLowerCase().indexOf(q) !== -1 || (x.title || '').toLowerCase().indexOf(q) !== -1; }); }
|
|
531
861
|
if (f.priority) list = list.filter(function(x) { return x.priority === f.priority; });
|
|
@@ -558,15 +888,16 @@ function renderBoard() {
|
|
|
558
888
|
return;
|
|
559
889
|
}
|
|
560
890
|
|
|
561
|
-
var
|
|
891
|
+
var filteredTasks = getFilteredBoardTasks();
|
|
892
|
+
var taskSingular = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular.toLowerCase() : 'task';
|
|
562
893
|
|
|
563
|
-
if (!D.
|
|
564
|
-
board.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12h6M12 9v6"/></svg><p>No
|
|
894
|
+
if (!D.tasks.length) {
|
|
895
|
+
board.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12h6M12 9v6"/></svg><p>No ' + escHtml(taskSingular) + 's yet. Create ' + escHtml(taskSingular) + ' files in your project/ directory to get started.</p></div>';
|
|
565
896
|
return;
|
|
566
897
|
}
|
|
567
898
|
|
|
568
899
|
board.innerHTML = BOARD_COLS.map(function(status) {
|
|
569
|
-
var cards =
|
|
900
|
+
var cards = filteredTasks.filter(function(f) { return f.status === status; });
|
|
570
901
|
return '<div class="column" data-status="' + status + '">' +
|
|
571
902
|
'<div class="column-header"><span class="col-title">' + statusIcon(status) + ' ' + (STATUS_LABELS[status] || status) + '</span><span class="col-count">' + cards.length + '</span></div>' +
|
|
572
903
|
'<div class="column-body">' + cards.map(renderCard).join('') + '</div></div>';
|
|
@@ -605,8 +936,8 @@ function setupDragDrop() {
|
|
|
605
936
|
});
|
|
606
937
|
card.addEventListener('click', function(e) {
|
|
607
938
|
if (e.defaultPrevented) return;
|
|
608
|
-
var
|
|
609
|
-
if (
|
|
939
|
+
var task = D.tasks.find(function(f) { return f.id === card.dataset.id; });
|
|
940
|
+
if (task) openPanel('tasks', task);
|
|
610
941
|
});
|
|
611
942
|
});
|
|
612
943
|
|
|
@@ -624,16 +955,16 @@ function setupDragDrop() {
|
|
|
624
955
|
col.addEventListener('drop', function(e) {
|
|
625
956
|
e.preventDefault();
|
|
626
957
|
col.classList.remove('drag-over');
|
|
627
|
-
var
|
|
958
|
+
var taskId = e.dataTransfer.getData('text/plain');
|
|
628
959
|
var newStatus = col.dataset.status;
|
|
629
|
-
if (!
|
|
960
|
+
if (!taskId || !newStatus) return;
|
|
630
961
|
|
|
631
962
|
// Optimistic update
|
|
632
|
-
var
|
|
633
|
-
if (
|
|
634
|
-
|
|
963
|
+
var task = D.tasks.find(function(f) { return f.id === taskId; });
|
|
964
|
+
if (task && task.status !== newStatus) {
|
|
965
|
+
task.status = newStatus;
|
|
635
966
|
renderBoard();
|
|
636
|
-
patchItem('
|
|
967
|
+
patchItem('tasks', taskId, { status: newStatus });
|
|
637
968
|
}
|
|
638
969
|
});
|
|
639
970
|
});
|
|
@@ -654,18 +985,27 @@ var TABLE_COLS = [
|
|
|
654
985
|
function renderTableControls() {
|
|
655
986
|
var c = document.getElementById('table-controls');
|
|
656
987
|
var ms = [], ep = [], st = [], sp = [];
|
|
657
|
-
D.
|
|
988
|
+
D.tasks.forEach(function(f) {
|
|
658
989
|
if (f.milestone && ms.indexOf(f.milestone) === -1) ms.push(f.milestone);
|
|
659
990
|
if (f.epic && ep.indexOf(f.epic) === -1) ep.push(f.epic);
|
|
660
991
|
if (f.status && st.indexOf(f.status) === -1) st.push(f.status);
|
|
661
992
|
if (f.sprint && sp.indexOf(f.sprint) === -1) sp.push(f.sprint);
|
|
662
993
|
});
|
|
994
|
+
|
|
995
|
+
var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural : 'Milestones';
|
|
996
|
+
var epicPlural = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.plural : 'Epics';
|
|
997
|
+
var sprintPlural = ENTITY_NAMES.sprint ? ENTITY_NAMES.sprint.plural : 'Sprints';
|
|
998
|
+
|
|
663
999
|
function opts(arr, filter, label) {
|
|
664
1000
|
return '<select data-tf="' + filter + '"><option value="">All ' + label + '</option>' +
|
|
665
1001
|
arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (tableFilters[filter] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
|
|
666
1002
|
}
|
|
667
|
-
|
|
668
|
-
|
|
1003
|
+
var createBtn = isSourceWritable() ?
|
|
1004
|
+
'<button class="btn btn-sm btn-create" id="table-create-btn">+ New ' + escHtml(ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task') + '</button>' : '';
|
|
1005
|
+
|
|
1006
|
+
c.innerHTML = createBtn +
|
|
1007
|
+
'<input type="text" data-tf="search" placeholder="Search ID or title..." value="' + escHtml(tableFilters.search) + '">' +
|
|
1008
|
+
opts(ms, 'milestone', msPlural) + opts(ep, 'epic', epicPlural) + opts(st, 'status', 'Statuses') + opts(sp, 'sprint', sprintPlural) +
|
|
669
1009
|
'<select data-tf="priority"><option value="">All Priorities</option>' +
|
|
670
1010
|
PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (tableFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>';
|
|
671
1011
|
|
|
@@ -673,10 +1013,13 @@ function renderTableControls() {
|
|
|
673
1013
|
c.querySelectorAll('select[data-tf]').forEach(function(sel) {
|
|
674
1014
|
sel.addEventListener('change', function() { tableFilters[sel.dataset.tf] = sel.value; renderTableBody(); });
|
|
675
1015
|
});
|
|
1016
|
+
|
|
1017
|
+
var btn = document.getElementById('table-create-btn');
|
|
1018
|
+
if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
|
|
676
1019
|
}
|
|
677
1020
|
|
|
678
|
-
function
|
|
679
|
-
var list = D.
|
|
1021
|
+
function getFilteredTasks() {
|
|
1022
|
+
var list = D.tasks.slice();
|
|
680
1023
|
var f = tableFilters;
|
|
681
1024
|
if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function(x) { return (x.id || '').toLowerCase().indexOf(q) !== -1 || (x.title || '').toLowerCase().indexOf(q) !== -1; }); }
|
|
682
1025
|
if (f.milestone) list = list.filter(function(x) { return x.milestone === f.milestone; });
|
|
@@ -695,10 +1038,11 @@ function getFilteredFeatures() {
|
|
|
695
1038
|
|
|
696
1039
|
function renderTableBody() {
|
|
697
1040
|
var w = document.getElementById('table-wrapper');
|
|
1041
|
+
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
|
|
698
1042
|
if (!D.loaded) { w.innerHTML = '<div class="loading-container">' + '<div class="skeleton skeleton-line" style="width:100%;height:32px"></div>'.repeat(6) + '</div>'; return; }
|
|
699
|
-
var
|
|
700
|
-
if (!
|
|
701
|
-
w.innerHTML = '<div class="empty"><p>' + (D.
|
|
1043
|
+
var tasks = getFilteredTasks();
|
|
1044
|
+
if (!tasks.length) {
|
|
1045
|
+
w.innerHTML = '<div class="empty"><p>' + (D.tasks.length ? 'No ' + escHtml(taskPlural) + ' match filters.' : 'No ' + escHtml(taskPlural) + ' yet.') + '</p></div>';
|
|
702
1046
|
return;
|
|
703
1047
|
}
|
|
704
1048
|
var html = '<table class="ftable"><thead><tr>';
|
|
@@ -707,7 +1051,7 @@ function renderTableBody() {
|
|
|
707
1051
|
html += '<th class="' + (sorted ? 'sorted' : '') + '" data-col="' + c.key + '">' + c.label + '<span class="sort-arrow">' + (sorted ? (tableSort.asc ? '\u25B2' : '\u25BC') : '') + '</span></th>';
|
|
708
1052
|
});
|
|
709
1053
|
html += '</tr></thead><tbody>';
|
|
710
|
-
|
|
1054
|
+
tasks.forEach(function(f) {
|
|
711
1055
|
var assigned = Array.isArray(f.assigned) ? f.assigned.join(', ') : (f.assigned || '');
|
|
712
1056
|
html += '<tr class="clickable-row" data-fid="' + escHtml(f.id || '') + '">' +
|
|
713
1057
|
'<td class="td-id">' + escHtml(f.id || '') + '</td>' +
|
|
@@ -732,8 +1076,8 @@ function renderTableBody() {
|
|
|
732
1076
|
});
|
|
733
1077
|
w.querySelectorAll('.clickable-row').forEach(function(tr) {
|
|
734
1078
|
tr.addEventListener('click', function() {
|
|
735
|
-
var
|
|
736
|
-
if (
|
|
1079
|
+
var task = D.tasks.find(function(f) { return f.id === tr.dataset.fid; });
|
|
1080
|
+
if (task) openPanel('tasks', task);
|
|
737
1081
|
});
|
|
738
1082
|
});
|
|
739
1083
|
}
|
|
@@ -745,23 +1089,30 @@ var msFilter = { status: '' };
|
|
|
745
1089
|
|
|
746
1090
|
function renderMsFilters() {
|
|
747
1091
|
var c = document.getElementById('ms-filters');
|
|
1092
|
+
var msStatuses = (D.config && D.config.statuses && D.config.statuses.milestone) ?
|
|
1093
|
+
D.config.statuses.milestone.map(function(s) { return s; }) :
|
|
1094
|
+
[{key:'planned',label:'Planned'},{key:'active',label:'Active'},{key:'completed',label:'Completed'}];
|
|
1095
|
+
|
|
748
1096
|
c.innerHTML = '<label>Filter:</label>' +
|
|
749
1097
|
'<select data-msf="status"><option value="">All Statuses</option>' +
|
|
750
|
-
|
|
751
|
-
return '<option value="' + s + '"' + (msFilter.status === s ? ' selected' : '') + '>' +
|
|
1098
|
+
msStatuses.map(function(s) {
|
|
1099
|
+
return '<option value="' + s.key + '"' + (msFilter.status === s.key ? ' selected' : '') + '>' + escHtml(s.label) + '</option>';
|
|
752
1100
|
}).join('') + '</select>';
|
|
753
1101
|
c.querySelector('select[data-msf]').addEventListener('change', function(e) { msFilter.status = e.target.value; renderMilestones(); });
|
|
754
1102
|
}
|
|
755
1103
|
|
|
756
1104
|
function renderMilestones() {
|
|
757
1105
|
var c = document.getElementById('milestones-container');
|
|
1106
|
+
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
|
|
1107
|
+
var msDir = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.dir : 'milestones';
|
|
1108
|
+
var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural.toLowerCase() : 'milestones';
|
|
758
1109
|
if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
|
|
759
1110
|
|
|
760
1111
|
var milestones = D.milestones;
|
|
761
1112
|
if (msFilter.status) milestones = milestones.filter(function(m) { return m.status === msFilter.status; });
|
|
762
1113
|
|
|
763
1114
|
if (!milestones.length) {
|
|
764
|
-
c.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg><p>' + (D.milestones.length ? 'No
|
|
1115
|
+
c.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg><p>' + (D.milestones.length ? 'No ' + escHtml(msPlural) + ' match filter.' : 'No ' + escHtml(msPlural) + ' yet. Create milestone directories under project/' + escHtml(msDir) + '/ to get started.') + '</p></div>';
|
|
765
1116
|
return;
|
|
766
1117
|
}
|
|
767
1118
|
|
|
@@ -773,7 +1124,7 @@ function renderMilestones() {
|
|
|
773
1124
|
var ep = e.progress || 0;
|
|
774
1125
|
var ec = epicColor(e.id || e.title || '');
|
|
775
1126
|
return '<div class="ms-epic" data-epic-id="' + escHtml(e.id || '') + '"><div class="epic-name"><span class="dot" style="background:' + ec + '"></span>' + escHtml(e.title || e.id || '') + '</div>' +
|
|
776
|
-
'<div class="epic-counts">' + (e.completedCount || 0) + ' / ' + (e.featureCount || 0) + '
|
|
1127
|
+
'<div class="epic-counts">' + (e.completedCount || 0) + ' / ' + (e.featureCount || 0) + ' ' + escHtml(taskPlural) + ' · ' + (e.totalPoints || 0) + ' pts</div>' +
|
|
777
1128
|
'<div class="progress progress-accent"><div class="progress-fill" style="width:' + ep + '%;background:' + ec + '"></div></div>' +
|
|
778
1129
|
'<div class="epic-meta">' +
|
|
779
1130
|
(e.status ? '<span class="badge ' + badgeClass('badge', e.status) + '">' + statusIcon(e.status) + ' ' + escHtml(e.status) + '</span>' : '') +
|
|
@@ -785,7 +1136,7 @@ function renderMilestones() {
|
|
|
785
1136
|
'<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
|
|
786
1137
|
'<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
|
|
787
1138
|
(ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
|
|
788
|
-
'<div class="ms-progress"><div class="progress-label"><span>' + cc + ' / ' + fc + '
|
|
1139
|
+
'<div class="ms-progress"><div class="progress-label"><span>' + cc + ' / ' + fc + ' ' + escHtml(taskPlural) + '</span><span>' + pct + '%</span></div>' +
|
|
789
1140
|
'<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
|
|
790
1141
|
'<div class="ms-epics">' + epicCards + '</div></div>';
|
|
791
1142
|
}).join('');
|
|
@@ -814,6 +1165,7 @@ function renderMilestones() {
|
|
|
814
1165
|
══════════════════════════════════════════════════════════════ */
|
|
815
1166
|
function renderMetrics() {
|
|
816
1167
|
var c = document.getElementById('metrics-container');
|
|
1168
|
+
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
|
|
817
1169
|
if (!D.loaded) { c.innerHTML = '<div class="metrics-grid">' + '<div class="skeleton skeleton-card" style="height:200px"></div>'.repeat(4) + '</div>'; return; }
|
|
818
1170
|
|
|
819
1171
|
var msProg = D.milestones.length ? D.milestones.map(function(ms) {
|
|
@@ -823,19 +1175,19 @@ function renderMetrics() {
|
|
|
823
1175
|
}).join('') : '<div style="color:var(--text3);padding:8px 0">No milestones.</div>';
|
|
824
1176
|
|
|
825
1177
|
var statuses = {};
|
|
826
|
-
D.
|
|
1178
|
+
D.tasks.forEach(function(f) { var s = f.status || 'unknown'; statuses[s] = (statuses[s] || 0) + 1; });
|
|
827
1179
|
var statusHtml = Object.keys(statuses).map(function(s) {
|
|
828
|
-
return '<div class="health-row">' + statusIcon(s) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(s) + '</div><div class="health-val">' + statuses[s] + '</div></div>';
|
|
1180
|
+
return '<div class="health-row">' + statusIcon(s) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(STATUS_LABELS[s] || s) + '</div><div class="health-val">' + statuses[s] + '</div></div>';
|
|
829
1181
|
}).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
|
|
830
1182
|
|
|
831
1183
|
var priorities = {};
|
|
832
|
-
D.
|
|
1184
|
+
D.tasks.forEach(function(f) { var p = f.priority || 'none'; priorities[p] = (priorities[p] || 0) + 1; });
|
|
833
1185
|
var priorityHtml = Object.keys(priorities).map(function(p) {
|
|
834
|
-
return '<div class="health-row">' + priorityIcon(p) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(p) + '</div><div class="health-val">' + priorities[p] + '</div></div>';
|
|
1186
|
+
return '<div class="health-row">' + priorityIcon(p) + '<div class="health-label" style="text-transform:capitalize">' + escHtml(PRIORITY_LABELS[p] || p) + '</div><div class="health-val">' + priorities[p] + '</div></div>';
|
|
835
1187
|
}).join('') || '<div style="color:var(--text3);padding:8px 0">No data.</div>';
|
|
836
1188
|
|
|
837
1189
|
var h = D.health || {};
|
|
838
|
-
var qualHtml = '<div class="health-row"><div class="health-dot" style="background:var(--text2)"></div><div class="health-label">Total
|
|
1190
|
+
var qualHtml = '<div class="health-row"><div class="health-dot" style="background:var(--text2)"></div><div class="health-label">Total ' + escHtml(taskPlural) + '</div><div class="health-val">' + (h.totalFeatures || D.tasks.length) + '</div></div>' +
|
|
839
1191
|
'<div class="health-row"><div class="health-dot" style="background:var(--success)"></div><div class="health-label">Completed</div><div class="health-val">' + (h.completedFeatures || 0) + '</div></div>' +
|
|
840
1192
|
'<div class="health-row"><div class="health-dot" style="background:var(--warning)"></div><div class="health-label">In Progress</div><div class="health-val">' + (h.inProgressFeatures || 0) + '</div></div>' +
|
|
841
1193
|
'<div class="health-row"><div class="health-dot" style="background:var(--accent)"></div><div class="health-label">Avg Velocity</div><div class="health-val">' + (h.velocity != null ? h.velocity + '%' : 'N/A') + '</div></div>';
|
|
@@ -848,12 +1200,162 @@ function renderMetrics() {
|
|
|
848
1200
|
}
|
|
849
1201
|
|
|
850
1202
|
/* ══════════════════════════════════════════════════════════════
|
|
851
|
-
|
|
1203
|
+
CRUD HELPERS
|
|
852
1204
|
══════════════════════════════════════════════════════════════ */
|
|
853
|
-
|
|
1205
|
+
function isSourceWritable() {
|
|
1206
|
+
if (!hasWorkspace() || !D.activeSource) return true; // legacy mode = writable
|
|
1207
|
+
var src = D.config.workspace.sources.find(function(s) { return s.name === D.activeSource; });
|
|
1208
|
+
return src ? !src.readonly : true;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function openCreateDialog(collection) {
|
|
1212
|
+
var item = { _isNew: true };
|
|
1213
|
+
|
|
1214
|
+
if (collection === 'tasks') {
|
|
1215
|
+
// Pre-fill milestone and epic from available ones
|
|
1216
|
+
var ms = D.milestones.length > 0 ? (D.milestones[0].id || D.milestones[0]._dir || '') : '';
|
|
1217
|
+
var ep = D.epics.length > 0 ? (D.epics[0]._dir || D.epics[0].id || '') : '';
|
|
1218
|
+
item.title = '';
|
|
1219
|
+
item.status = 'backlog';
|
|
1220
|
+
item.priority = '';
|
|
1221
|
+
item.points = null;
|
|
1222
|
+
item.assigned = '';
|
|
1223
|
+
item.sprint = '';
|
|
1224
|
+
item.milestone = ms;
|
|
1225
|
+
item.epic = ep;
|
|
1226
|
+
item.content = '';
|
|
1227
|
+
} else if (collection === 'milestones') {
|
|
1228
|
+
item.title = '';
|
|
1229
|
+
item.status = 'planned';
|
|
1230
|
+
item.deadline = '';
|
|
1231
|
+
item.content = '';
|
|
1232
|
+
} else if (collection === 'epics') {
|
|
1233
|
+
var ms = D.milestones.length > 0 ? (D.milestones[0]._dir || D.milestones[0].id || '') : '';
|
|
1234
|
+
item.title = '';
|
|
1235
|
+
item.status = 'active';
|
|
1236
|
+
item.priority = '';
|
|
1237
|
+
item.milestone = ms;
|
|
1238
|
+
item.content = '';
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
panelState = { open: true, type: collection, item: item, isCreate: true };
|
|
1242
|
+
renderPanel();
|
|
1243
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
1244
|
+
document.getElementById('panel-overlay').classList.add('open');
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/* ══════════════════════════════════════════════════════════════
|
|
1248
|
+
OVERVIEW VIEW
|
|
1249
|
+
══════════════════════════════════════════════════════════════ */
|
|
1250
|
+
async function renderOverview() {
|
|
1251
|
+
var c = document.getElementById('overview-container');
|
|
1252
|
+
if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
|
|
1253
|
+
|
|
1254
|
+
// Fetch overview data
|
|
1255
|
+
var overviewMs = await fetchJson('/api/overview/milestones') || [];
|
|
1256
|
+
var overviewLinks = await fetchJson('/api/overview/links');
|
|
1257
|
+
var overviewMetrics = await fetchJson('/api/overview/metrics');
|
|
1258
|
+
|
|
1259
|
+
D.overviewLinks = overviewLinks;
|
|
1260
|
+
|
|
1261
|
+
var html = '<h2 style="margin-bottom:16px;font-size:16px;font-weight:700">Workspace Overview</h2>';
|
|
1262
|
+
|
|
1263
|
+
// Global Milestones
|
|
1264
|
+
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:16px 0 8px">Global Milestones</h3>';
|
|
1265
|
+
if (overviewMs.length > 0) {
|
|
1266
|
+
html += overviewMs.map(function(ms) {
|
|
1267
|
+
var pct = ms.combinedProgress != null ? ms.combinedProgress : (ms.progress || 0);
|
|
1268
|
+
var tracked = ms.tracked || [];
|
|
1269
|
+
var trackedHtml = '';
|
|
1270
|
+
if (tracked.length > 0) {
|
|
1271
|
+
trackedHtml = '<div class="tracked-ms"><div class="tracked-ms-header">Tracked Sub-Milestones</div>' +
|
|
1272
|
+
tracked.map(function(t) {
|
|
1273
|
+
return '<div class="tracked-ms-item"><span class="dot" style="background:' + (t.sourceColor || 'var(--accent)') + '"></span>' +
|
|
1274
|
+
'<span style="font-size:12px;flex:1">' + escHtml(t.title || t.id || '') + '</span>' +
|
|
1275
|
+
'<div class="progress progress-accent" style="width:80px"><div class="progress-fill" style="width:' + t.progress + '%;background:' + (t.sourceColor || 'var(--accent)') + '"></div></div>' +
|
|
1276
|
+
'<span class="tracked-ms-pct">' + t.progress + '%</span></div>';
|
|
1277
|
+
}).join('') + '</div>';
|
|
1278
|
+
}
|
|
1279
|
+
return '<div class="ms-card">' +
|
|
1280
|
+
'<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
|
|
1281
|
+
'<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
|
|
1282
|
+
(ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
|
|
1283
|
+
'<div class="ms-progress"><div class="progress-label"><span>' + (ms.completedCount || 0) + ' / ' + (ms.featureCount || 0) + ' tasks</span><span>' + pct + '%</span></div>' +
|
|
1284
|
+
'<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
|
|
1285
|
+
trackedHtml + '</div>';
|
|
1286
|
+
}).join('');
|
|
1287
|
+
} else {
|
|
1288
|
+
html += '<div style="color:var(--text3);padding:8px 0">No milestones found across sources.</div>';
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Cross-project links
|
|
1292
|
+
if (overviewLinks && overviewLinks.links && overviewLinks.links.length > 0) {
|
|
1293
|
+
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Cross-Project Links</h3>';
|
|
1294
|
+
html += '<div class="metrics-grid">';
|
|
1295
|
+
var linkGroups = {};
|
|
1296
|
+
overviewLinks.links.forEach(function(l) {
|
|
1297
|
+
var key = (l.fromSource || 'unknown');
|
|
1298
|
+
if (!linkGroups[key]) linkGroups[key] = [];
|
|
1299
|
+
linkGroups[key].push(l);
|
|
1300
|
+
});
|
|
1301
|
+
Object.keys(linkGroups).forEach(function(src) {
|
|
1302
|
+
var links = linkGroups[src];
|
|
1303
|
+
html += '<div class="metric-card"><h3>' + escHtml(src) + ' Links</h3>';
|
|
1304
|
+
html += links.map(function(l) {
|
|
1305
|
+
return '<div class="health-row"><span style="font-size:12px;font-family:var(--mono)">' + escHtml(l.from || '') + '</span>' +
|
|
1306
|
+
'<span style="color:var(--text3);margin:0 4px">→</span>' +
|
|
1307
|
+
'<span class="link-chip" data-link="' + escHtml(l.to || '') + '">' +
|
|
1308
|
+
'<span class="link-chip-dot" style="background:var(--accent)"></span>' + escHtml(l.to || '') + '</span></div>';
|
|
1309
|
+
}).join('');
|
|
1310
|
+
html += '</div>';
|
|
1311
|
+
});
|
|
1312
|
+
html += '</div>';
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Source metrics
|
|
1316
|
+
if (overviewMetrics && overviewMetrics.sources) {
|
|
1317
|
+
html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Source Metrics</h3>';
|
|
1318
|
+
html += '<div class="metrics-grid">';
|
|
1319
|
+
Object.keys(overviewMetrics.sources).forEach(function(key) {
|
|
1320
|
+
var m = overviewMetrics.sources[key];
|
|
1321
|
+
var pct = m.totalTasks > 0 ? Math.round((m.completedTasks / m.totalTasks) * 100) : 0;
|
|
1322
|
+
html += '<div class="metric-card" style="border-left:3px solid ' + (m.color || 'var(--accent)') + '">' +
|
|
1323
|
+
'<h3>' + escHtml(m.label || key) + '</h3>' +
|
|
1324
|
+
'<div class="health-row"><div class="health-label">Tasks</div><div class="health-val">' + m.totalTasks + '</div></div>' +
|
|
1325
|
+
'<div class="health-row"><div class="health-label">Completed</div><div class="health-val">' + m.completedTasks + '</div></div>' +
|
|
1326
|
+
'<div class="health-row"><div class="health-label">Points</div><div class="health-val">' + m.totalPoints + '</div></div>' +
|
|
1327
|
+
'<div class="progress progress-lg progress-success" style="margin-top:8px"><div class="progress-fill" style="width:' + pct + '%;background:' + (m.color || 'var(--success)') + '"></div></div>' +
|
|
1328
|
+
'</div>';
|
|
1329
|
+
});
|
|
1330
|
+
html += '</div>';
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
c.innerHTML = html;
|
|
1334
|
+
|
|
1335
|
+
// Click handlers for link chips
|
|
1336
|
+
c.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
|
|
1337
|
+
chip.addEventListener('click', function() {
|
|
1338
|
+
var ref = chip.dataset.link;
|
|
1339
|
+
var parts = ref.split(':');
|
|
1340
|
+
if (parts.length === 2) {
|
|
1341
|
+
switchSource(parts[0]);
|
|
1342
|
+
// After switch, try to find and open the item
|
|
1343
|
+
setTimeout(function() {
|
|
1344
|
+
var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
|
|
1345
|
+
if (task) openPanel('tasks', task);
|
|
1346
|
+
}, 500);
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/* ══════════════════════════════════════════════════════════════
|
|
1353
|
+
DETAIL PANEL — Edit/Create tasks, epics, milestones
|
|
1354
|
+
══════════════════════════════════════════════════════════════ */
|
|
1355
|
+
var panelState = { open: false, type: null, item: null, isCreate: false };
|
|
854
1356
|
|
|
855
1357
|
function openPanel(type, item) {
|
|
856
|
-
panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)) };
|
|
1358
|
+
panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)), isCreate: false };
|
|
857
1359
|
renderPanel();
|
|
858
1360
|
document.getElementById('detail-panel').classList.add('open');
|
|
859
1361
|
document.getElementById('panel-overlay').classList.add('open');
|
|
@@ -869,36 +1371,66 @@ function renderPanel() {
|
|
|
869
1371
|
var panel = document.getElementById('detail-panel');
|
|
870
1372
|
var t = panelState.type;
|
|
871
1373
|
var item = panelState.item;
|
|
1374
|
+
var isCreate = panelState.isCreate;
|
|
872
1375
|
if (!item) return;
|
|
873
1376
|
|
|
874
|
-
var
|
|
1377
|
+
var isReadonly = !isCreate && item.readonly;
|
|
1378
|
+
|
|
1379
|
+
var typeLabel;
|
|
1380
|
+
if (t === 'tasks') typeLabel = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task';
|
|
1381
|
+
else if (t === 'epics') typeLabel = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.singular : 'Epic';
|
|
1382
|
+
else typeLabel = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.singular : 'Milestone';
|
|
875
1383
|
|
|
876
1384
|
var html = '<div class="panel-header">' +
|
|
877
|
-
'<span class="panel-type">' + typeLabel + '</span>' +
|
|
878
|
-
'<span class="panel-item-id">' + escHtml(item.id || '') + '</span>'
|
|
879
|
-
|
|
880
|
-
|
|
1385
|
+
'<span class="panel-type">' + escHtml(isCreate ? 'New ' + typeLabel : typeLabel) + '</span>' +
|
|
1386
|
+
'<span class="panel-item-id">' + escHtml(isCreate ? '' : (item.id || '')) + '</span>';
|
|
1387
|
+
|
|
1388
|
+
// Source badge
|
|
1389
|
+
if (item.source && item.sourceColor) {
|
|
1390
|
+
html += '<span class="pill" style="background:' + item.sourceColor + '20;color:' + item.sourceColor + ';font-size:10px">' + escHtml(item.sourceLabel || item.source) + '</span>';
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
html += '<button class="panel-close" id="panel-close-btn">×</button></div>';
|
|
881
1394
|
|
|
882
1395
|
html += '<div class="panel-body">';
|
|
883
1396
|
|
|
884
1397
|
// Title
|
|
885
|
-
html += '<div class="panel-field"><label>Title</label><input type="text" id="p-title" value="' + escHtml(item.title || '') + '"></div>';
|
|
1398
|
+
html += '<div class="panel-field"><label>Title</label><input type="text" id="p-title" value="' + escHtml(item.title || '') + '"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
886
1399
|
|
|
887
1400
|
// Status + Priority row
|
|
888
1401
|
html += '<div class="panel-props">';
|
|
889
1402
|
|
|
890
|
-
// Status
|
|
891
|
-
var statusOptions
|
|
892
|
-
|
|
1403
|
+
// Status — derived from config
|
|
1404
|
+
var statusOptions, statusLabels;
|
|
1405
|
+
if (t === 'tasks' && D.config && D.config.statuses && D.config.statuses.task) {
|
|
1406
|
+
statusOptions = D.config.statuses.task.map(function(s) { return s.key; });
|
|
1407
|
+
statusLabels = {};
|
|
1408
|
+
D.config.statuses.task.forEach(function(s) { statusLabels[s.key] = s.label; });
|
|
1409
|
+
} else if (t === 'epics' && D.config && D.config.statuses && D.config.statuses.epic) {
|
|
1410
|
+
statusOptions = D.config.statuses.epic.map(function(s) { return s.key; });
|
|
1411
|
+
statusLabels = {};
|
|
1412
|
+
D.config.statuses.epic.forEach(function(s) { statusLabels[s.key] = s.label; });
|
|
1413
|
+
} else if (t === 'milestones' && D.config && D.config.statuses && D.config.statuses.milestone) {
|
|
1414
|
+
statusOptions = D.config.statuses.milestone.map(function(s) { return s.key; });
|
|
1415
|
+
statusLabels = {};
|
|
1416
|
+
D.config.statuses.milestone.forEach(function(s) { statusLabels[s.key] = s.label; });
|
|
1417
|
+
} else if (t === 'tasks') {
|
|
1418
|
+
statusOptions = STATUSES;
|
|
1419
|
+
statusLabels = STATUS_LABELS;
|
|
1420
|
+
} else {
|
|
1421
|
+
statusOptions = ['planned', 'active', 'completed', 'cancelled'];
|
|
1422
|
+
statusLabels = { planned: 'Planned', active: 'Active', completed: 'Completed', cancelled: 'Cancelled' };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
893
1425
|
html += '<div class="panel-field"><label>Status</label><div class="field-with-icon"><span id="p-status-icon">' + statusIcon(item.status) + '</span>' +
|
|
894
|
-
'<select id="p-status">' + statusOptions.map(function(s) {
|
|
1426
|
+
'<select id="p-status"' + (isReadonly ? ' disabled' : '') + '>' + statusOptions.map(function(s) {
|
|
895
1427
|
return '<option value="' + s + '"' + (item.status === s ? ' selected' : '') + '>' + (statusLabels[s] || s) + '</option>';
|
|
896
1428
|
}).join('') + '</select></div></div>';
|
|
897
1429
|
|
|
898
1430
|
// Priority
|
|
899
|
-
if (t === '
|
|
1431
|
+
if (t === 'tasks' || t === 'epics') {
|
|
900
1432
|
html += '<div class="panel-field"><label>Priority</label><div class="field-with-icon"><span id="p-priority-icon">' + priorityIcon(item.priority) + '</span>' +
|
|
901
|
-
'<select id="p-priority"><option value="">None</option>' + PRIORITIES.map(function(p) {
|
|
1433
|
+
'<select id="p-priority"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' + PRIORITIES.map(function(p) {
|
|
902
1434
|
return '<option value="' + p + '"' + (item.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>';
|
|
903
1435
|
}).join('') + '</select></div></div>';
|
|
904
1436
|
}
|
|
@@ -906,45 +1438,115 @@ function renderPanel() {
|
|
|
906
1438
|
html += '</div>'; // close panel-props
|
|
907
1439
|
|
|
908
1440
|
// Type-specific fields
|
|
909
|
-
if (t === '
|
|
1441
|
+
if (t === 'tasks') {
|
|
910
1442
|
html += '<div class="panel-props">';
|
|
911
|
-
html += '<div class="panel-field"><label>Points</label><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '"></div>';
|
|
1443
|
+
html += '<div class="panel-field"><label>Points</label><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
912
1444
|
|
|
913
1445
|
var assignedVal = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
|
|
914
|
-
html += '<div class="panel-field"><label>Assigned</label><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="agent-name"></div>';
|
|
1446
|
+
html += '<div class="panel-field"><label>Assigned</label><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="agent-name"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
915
1447
|
html += '</div>';
|
|
916
1448
|
|
|
917
1449
|
html += '<div class="panel-props">';
|
|
918
1450
|
// Sprint
|
|
919
|
-
html += '<div class="panel-field"><label>Sprint</label><select id="p-sprint"><option value="">None</option>' +
|
|
1451
|
+
html += '<div class="panel-field"><label>Sprint</label><select id="p-sprint"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' +
|
|
920
1452
|
D.allSprints.map(function(s) { return '<option value="' + escHtml(s.id || '') + '"' + (item.sprint === s.id ? ' selected' : '') + '>' + escHtml(s.id || '') + '</option>'; }).join('') +
|
|
921
1453
|
'</select></div>';
|
|
922
1454
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1455
|
+
if (isCreate) {
|
|
1456
|
+
// Milestone and Epic as editable dropdowns for create mode
|
|
1457
|
+
html += '<div class="panel-field"><label>Milestone</label><select id="p-milestone">' +
|
|
1458
|
+
D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '"' + (item.milestone === (m._dir || m.id) ? ' selected' : '') + '>' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
|
|
1459
|
+
'</select></div>';
|
|
1460
|
+
html += '</div>';
|
|
1461
|
+
html += '<div class="panel-field"><label>Epic</label><select id="p-epic-select">' +
|
|
1462
|
+
D.epics.map(function(e) { return '<option value="' + escHtml(e._dir || e.id || '') + '"' + (item.epic === (e._dir || e.id) ? ' selected' : '') + '>' + escHtml(e.title || e.id || '') + '</option>'; }).join('') +
|
|
1463
|
+
'</select></div>';
|
|
1464
|
+
} else {
|
|
1465
|
+
// Epic (read-only info)
|
|
1466
|
+
html += '<div class="panel-field"><label>Epic</label><input type="text" id="p-epic" value="' + escHtml(item.epic || '') + '" disabled style="opacity:.6" title="Epic is determined by file location"></div>';
|
|
1467
|
+
html += '</div>';
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Links rendering
|
|
1471
|
+
if (!isCreate && item.links && Array.isArray(item.links) && item.links.length > 0) {
|
|
1472
|
+
html += '<div class="panel-field"><label>Links</label><div class="link-chips">';
|
|
1473
|
+
item.links.forEach(function(link) {
|
|
1474
|
+
var parts = String(link).split(':');
|
|
1475
|
+
var srcName = parts.length === 2 ? parts[0] : null;
|
|
1476
|
+
var srcColor = 'var(--accent)';
|
|
1477
|
+
if (srcName && D.config && D.config.workspace && D.config.workspace.sources) {
|
|
1478
|
+
var src = D.config.workspace.sources.find(function(s) { return s.name === srcName; });
|
|
1479
|
+
if (src && src.color) srcColor = src.color;
|
|
1480
|
+
}
|
|
1481
|
+
html += '<span class="link-chip" data-link="' + escHtml(String(link)) + '">' +
|
|
1482
|
+
'<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
|
|
1483
|
+
escHtml(String(link)) + '</span>';
|
|
1484
|
+
});
|
|
1485
|
+
html += '</div></div>';
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Reverse links
|
|
1489
|
+
if (!isCreate && D.overviewLinks && D.overviewLinks.reverseLinks && item.id) {
|
|
1490
|
+
var reverseRefs = D.overviewLinks.reverseLinks[item.id] || [];
|
|
1491
|
+
if (reverseRefs.length > 0) {
|
|
1492
|
+
html += '<div class="panel-field"><label>Referenced By</label><div class="link-chips">';
|
|
1493
|
+
reverseRefs.forEach(function(ref) {
|
|
1494
|
+
var srcColor = ref.sourceColor || 'var(--accent)';
|
|
1495
|
+
html += '<span class="link-chip" data-link="' + escHtml(ref.from || '') + '">' +
|
|
1496
|
+
'<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
|
|
1497
|
+
escHtml(ref.from || '') + '</span>';
|
|
1498
|
+
});
|
|
1499
|
+
html += '</div></div>';
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
926
1502
|
}
|
|
927
1503
|
|
|
928
1504
|
if (t === 'milestones') {
|
|
929
|
-
html += '<div class="panel-field"><label>Deadline</label><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"></div>';
|
|
1505
|
+
html += '<div class="panel-field"><label>Deadline</label><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"' + (isReadonly ? ' disabled' : '') + '></div>';
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (isCreate && (t === 'epics' || t === 'milestones')) {
|
|
1509
|
+
if (t === 'epics') {
|
|
1510
|
+
html += '<div class="panel-field"><label>Milestone</label><select id="p-milestone">' +
|
|
1511
|
+
D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '">' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
|
|
1512
|
+
'</select></div>';
|
|
1513
|
+
}
|
|
930
1514
|
}
|
|
931
1515
|
|
|
932
1516
|
// Description / Content
|
|
933
|
-
html += '<div class="panel-field"><label>Description</label><textarea id="p-content">' + escHtml(item.content || '') + '</textarea></div>';
|
|
1517
|
+
html += '<div class="panel-field"><label>Description</label><textarea id="p-content"' + (isReadonly ? ' disabled' : '') + '>' + escHtml(item.content || '') + '</textarea></div>';
|
|
934
1518
|
|
|
935
1519
|
html += '</div>'; // close panel-body
|
|
936
1520
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
'</
|
|
1521
|
+
// Footer with buttons
|
|
1522
|
+
html += '<div class="panel-footer">';
|
|
1523
|
+
if (!isCreate && !isReadonly && isSourceWritable()) {
|
|
1524
|
+
html += '<button class="btn btn-danger" id="panel-archive-btn">Archive</button>';
|
|
1525
|
+
}
|
|
1526
|
+
html += '<span style="flex:1"></span>';
|
|
1527
|
+
html += '<button class="btn" id="panel-cancel-btn">Cancel</button>';
|
|
1528
|
+
if (!isReadonly) {
|
|
1529
|
+
html += '<button class="btn btn-primary" id="panel-save-btn">' + (isCreate ? 'Create' : 'Save Changes') + '</button>';
|
|
1530
|
+
}
|
|
1531
|
+
html += '</div>';
|
|
941
1532
|
|
|
942
1533
|
panel.innerHTML = html;
|
|
943
1534
|
|
|
944
1535
|
// Event listeners
|
|
945
1536
|
document.getElementById('panel-close-btn').addEventListener('click', closePanel);
|
|
946
1537
|
document.getElementById('panel-cancel-btn').addEventListener('click', closePanel);
|
|
947
|
-
document.getElementById('panel-save-btn')
|
|
1538
|
+
var saveBtn = document.getElementById('panel-save-btn');
|
|
1539
|
+
if (saveBtn) saveBtn.addEventListener('click', isCreate ? saveCreatePanel : savePanel);
|
|
1540
|
+
|
|
1541
|
+
var archiveBtn = document.getElementById('panel-archive-btn');
|
|
1542
|
+
if (archiveBtn) {
|
|
1543
|
+
archiveBtn.addEventListener('click', function() {
|
|
1544
|
+
if (!confirm('Archive this ' + typeLabel.toLowerCase() + '? It will be moved to the archive directory.')) return;
|
|
1545
|
+
deleteItem(t, item.id).then(function(ok) {
|
|
1546
|
+
if (ok) { closePanel(); refreshData(); }
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
948
1550
|
|
|
949
1551
|
// Live icon updates
|
|
950
1552
|
var statusSel = document.getElementById('p-status');
|
|
@@ -961,6 +1563,22 @@ function renderPanel() {
|
|
|
961
1563
|
if (iconEl) iconEl.innerHTML = priorityIcon(prioritySel.value);
|
|
962
1564
|
});
|
|
963
1565
|
}
|
|
1566
|
+
|
|
1567
|
+
// Link chip click handlers
|
|
1568
|
+
panel.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
|
|
1569
|
+
chip.addEventListener('click', function() {
|
|
1570
|
+
var ref = chip.dataset.link;
|
|
1571
|
+
var parts = ref.split(':');
|
|
1572
|
+
if (parts.length === 2 && hasWorkspace()) {
|
|
1573
|
+
closePanel();
|
|
1574
|
+
switchSource(parts[0]);
|
|
1575
|
+
setTimeout(function() {
|
|
1576
|
+
var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
|
|
1577
|
+
if (task) openPanel('tasks', task);
|
|
1578
|
+
}, 500);
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
964
1582
|
}
|
|
965
1583
|
|
|
966
1584
|
async function savePanel() {
|
|
@@ -976,12 +1594,12 @@ async function savePanel() {
|
|
|
976
1594
|
var statusEl = document.getElementById('p-status');
|
|
977
1595
|
if (statusEl && statusEl.value !== (item.status || '')) updates.status = statusEl.value;
|
|
978
1596
|
|
|
979
|
-
if (t === '
|
|
1597
|
+
if (t === 'tasks' || t === 'epics') {
|
|
980
1598
|
var priorityEl = document.getElementById('p-priority');
|
|
981
1599
|
if (priorityEl && priorityEl.value !== (item.priority || '')) updates.priority = priorityEl.value || null;
|
|
982
1600
|
}
|
|
983
1601
|
|
|
984
|
-
if (t === '
|
|
1602
|
+
if (t === 'tasks') {
|
|
985
1603
|
var pointsEl = document.getElementById('p-points');
|
|
986
1604
|
if (pointsEl) {
|
|
987
1605
|
var pv = pointsEl.value ? Number(pointsEl.value) : null;
|
|
@@ -1020,6 +1638,60 @@ async function savePanel() {
|
|
|
1020
1638
|
}
|
|
1021
1639
|
}
|
|
1022
1640
|
|
|
1641
|
+
async function saveCreatePanel() {
|
|
1642
|
+
var t = panelState.type;
|
|
1643
|
+
if (!t) return;
|
|
1644
|
+
|
|
1645
|
+
var data = {};
|
|
1646
|
+
|
|
1647
|
+
var titleEl = document.getElementById('p-title');
|
|
1648
|
+
if (titleEl) data.title = titleEl.value || 'Untitled';
|
|
1649
|
+
|
|
1650
|
+
var statusEl = document.getElementById('p-status');
|
|
1651
|
+
if (statusEl) data.status = statusEl.value;
|
|
1652
|
+
|
|
1653
|
+
if (t === 'tasks' || t === 'epics') {
|
|
1654
|
+
var priorityEl = document.getElementById('p-priority');
|
|
1655
|
+
if (priorityEl && priorityEl.value) data.priority = priorityEl.value;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (t === 'tasks') {
|
|
1659
|
+
var pointsEl = document.getElementById('p-points');
|
|
1660
|
+
if (pointsEl && pointsEl.value) data.points = Number(pointsEl.value);
|
|
1661
|
+
|
|
1662
|
+
var assignedEl = document.getElementById('p-assigned');
|
|
1663
|
+
if (assignedEl && assignedEl.value.trim()) data.assigned = assignedEl.value.trim();
|
|
1664
|
+
|
|
1665
|
+
var sprintEl = document.getElementById('p-sprint');
|
|
1666
|
+
if (sprintEl && sprintEl.value) data.sprint = sprintEl.value;
|
|
1667
|
+
|
|
1668
|
+
var milestoneEl = document.getElementById('p-milestone');
|
|
1669
|
+
if (milestoneEl) data.milestone = milestoneEl.value;
|
|
1670
|
+
|
|
1671
|
+
var epicEl = document.getElementById('p-epic-select');
|
|
1672
|
+
if (epicEl) data.epic = epicEl.value;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (t === 'epics' || t === 'milestones') {
|
|
1676
|
+
var milestoneEl = document.getElementById('p-milestone');
|
|
1677
|
+
if (milestoneEl) data.milestone = milestoneEl.value;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (t === 'milestones') {
|
|
1681
|
+
var deadlineEl = document.getElementById('p-deadline');
|
|
1682
|
+
if (deadlineEl && deadlineEl.value) data.deadline = deadlineEl.value;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
var contentEl = document.getElementById('p-content');
|
|
1686
|
+
if (contentEl && contentEl.value) data.content = contentEl.value;
|
|
1687
|
+
|
|
1688
|
+
var result = await createItem(t, data);
|
|
1689
|
+
if (result) {
|
|
1690
|
+
closePanel();
|
|
1691
|
+
refreshData();
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1023
1695
|
// Close panel on overlay click or Escape
|
|
1024
1696
|
document.getElementById('panel-overlay').addEventListener('click', closePanel);
|
|
1025
1697
|
document.addEventListener('keydown', function(e) {
|
|
@@ -1031,11 +1703,17 @@ document.addEventListener('keydown', function(e) {
|
|
|
1031
1703
|
══════════════════════════════════════════════════════════════ */
|
|
1032
1704
|
function switchView(name) {
|
|
1033
1705
|
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
1034
|
-
document.querySelectorAll('#sidebar-nav a').forEach(function(a) { a.classList.remove('active'); });
|
|
1706
|
+
document.querySelectorAll('#sidebar-nav a[data-view]').forEach(function(a) { a.classList.remove('active'); });
|
|
1035
1707
|
var t = document.getElementById('view-' + name);
|
|
1036
1708
|
if (t) t.classList.add('active');
|
|
1037
1709
|
var l = document.querySelector('#sidebar-nav a[data-view="' + name + '"]');
|
|
1038
1710
|
if (l) l.classList.add('active');
|
|
1711
|
+
// Persist view selection
|
|
1712
|
+
try { localStorage.setItem('mdboard-view', name); } catch(e) {}
|
|
1713
|
+
// Trigger overview rendering when switching to it
|
|
1714
|
+
if (name === 'overview' && D.loaded) {
|
|
1715
|
+
renderOverview();
|
|
1716
|
+
}
|
|
1039
1717
|
}
|
|
1040
1718
|
|
|
1041
1719
|
document.getElementById('sidebar-nav').addEventListener('click', function(e) {
|
|
@@ -1047,8 +1725,12 @@ document.getElementById('sidebar-nav').addEventListener('click', function(e) {
|
|
|
1047
1725
|
});
|
|
1048
1726
|
|
|
1049
1727
|
function handleHash() {
|
|
1050
|
-
var hash = window.location.hash.replace('#', '')
|
|
1051
|
-
|
|
1728
|
+
var hash = window.location.hash.replace('#', '');
|
|
1729
|
+
if (!hash) {
|
|
1730
|
+
try { hash = localStorage.getItem('mdboard-view') || ''; } catch(e) {}
|
|
1731
|
+
}
|
|
1732
|
+
hash = hash || 'board';
|
|
1733
|
+
var valid = ['board','table','milestones','metrics','overview'];
|
|
1052
1734
|
switchView(valid.indexOf(hash) !== -1 ? hash : 'board');
|
|
1053
1735
|
}
|
|
1054
1736
|
window.addEventListener('hashchange', handleHash);
|
|
@@ -1059,8 +1741,54 @@ window.addEventListener('hashchange', handleHash);
|
|
|
1059
1741
|
(async function() {
|
|
1060
1742
|
handleHash();
|
|
1061
1743
|
await loadAll();
|
|
1744
|
+
initFromConfig();
|
|
1745
|
+
|
|
1746
|
+
// Sidebar logo always shows project name
|
|
1747
|
+
renderSidebarLogo();
|
|
1748
|
+
|
|
1749
|
+
// Workspace detection: build source rail + restore last source
|
|
1750
|
+
if (hasWorkspace()) {
|
|
1751
|
+
// Create overview tab (hidden by default)
|
|
1752
|
+
var overviewTab = document.querySelector('#sidebar-nav a[data-view="overview"]');
|
|
1753
|
+
if (!overviewTab) {
|
|
1754
|
+
var nav = document.getElementById('sidebar-nav');
|
|
1755
|
+
var metricsLink = document.querySelector('#sidebar-nav a[data-view="metrics"]');
|
|
1756
|
+
var overviewLink = document.createElement('a');
|
|
1757
|
+
overviewLink.href = '#overview';
|
|
1758
|
+
overviewLink.dataset.view = 'overview';
|
|
1759
|
+
overviewLink.style.display = 'none';
|
|
1760
|
+
overviewLink.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg><span>Overview</span>';
|
|
1761
|
+
if (metricsLink && metricsLink.nextSibling) {
|
|
1762
|
+
nav.insertBefore(overviewLink, metricsLink.nextSibling);
|
|
1763
|
+
} else {
|
|
1764
|
+
nav.appendChild(overviewLink);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Restore last source or default to overview
|
|
1769
|
+
var savedSource = null;
|
|
1770
|
+
try { savedSource = localStorage.getItem('mdboard-source'); } catch(e) {}
|
|
1771
|
+
var validSource = savedSource && (savedSource === 'overview' || D.config.workspace.sources.some(function(s) { return s.name === savedSource; }));
|
|
1772
|
+
var initialSource = validSource ? savedSource : 'overview';
|
|
1773
|
+
|
|
1774
|
+
buildSourceRail();
|
|
1775
|
+
// switchSource skips if same as current, so set activeSource first then trigger
|
|
1776
|
+
D.activeSource = null;
|
|
1777
|
+
switchSource(initialSource);
|
|
1778
|
+
// Wait for switchSource data reload before continuing
|
|
1779
|
+
await loadAll();
|
|
1780
|
+
initFromConfig();
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1062
1783
|
renderAll();
|
|
1063
1784
|
connectSSE();
|
|
1785
|
+
|
|
1786
|
+
// Load overview links in background for reverse link display
|
|
1787
|
+
if (hasWorkspace()) {
|
|
1788
|
+
fetchJson('/api/overview/links').then(function(data) {
|
|
1789
|
+
D.overviewLinks = data;
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1064
1792
|
})();
|
|
1065
1793
|
</script>
|
|
1066
1794
|
</body>
|