@unbrained/pm-web 2026.8.10 → 2026.8.14
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/CHANGELOG.md +16 -0
- package/dist/app.d.ts +16 -0
- package/dist/app.js +16 -0
- package/dist/app.js.map +1 -1
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js.map +1 -1
- package/dist/board.d.ts +53 -0
- package/dist/board.js +48 -0
- package/dist/board.js.map +1 -1
- package/dist/crypto.d.ts +23 -0
- package/dist/crypto.js +32 -0
- package/dist/crypto.js.map +1 -1
- package/dist/db.d.ts +19 -0
- package/dist/db.js +19 -0
- package/dist/db.js.map +1 -1
- package/dist/ical.d.ts +72 -0
- package/dist/ical.js +73 -0
- package/dist/ical.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +52 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +8 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/oidc.d.ts +87 -0
- package/dist/oidc.js +113 -0
- package/dist/oidc.js.map +1 -1
- package/dist/routes/admin.js +15 -0
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/github.js +13 -0
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/oidc.d.ts +10 -0
- package/dist/routes/oidc.js +51 -0
- package/dist/routes/oidc.js.map +1 -1
- package/dist/routes/pm.js +132 -1
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/sharing.js +8 -0
- package/dist/routes/sharing.js.map +1 -1
- package/dist/services/mutation-event-watcher.js +13 -0
- package/dist/services/mutation-event-watcher.js.map +1 -1
- package/dist/services/pm-runner.d.ts +82 -0
- package/dist/services/pm-runner.js +121 -0
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/project-watcher.d.ts +22 -0
- package/dist/services/project-watcher.js +51 -0
- package/dist/services/project-watcher.js.map +1 -1
- package/dist/services/realtime-bus.d.ts +46 -0
- package/dist/services/realtime-bus.js +59 -0
- package/dist/services/realtime-bus.js.map +1 -1
- package/dist/services/sse.d.ts +134 -0
- package/dist/services/sse.js +145 -0
- package/dist/services/sse.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +5 -3
- package/public/src/app.js +44 -1
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +44 -1
- package/public/src/components/modals.js +11 -0
- package/public/src/components/modals.js.map +1 -1
- package/public/src/components/modals.ts +11 -0
- package/public/src/components/toast.js +2 -0
- package/public/src/components/toast.js.map +1 -1
- package/public/src/components/toast.ts +2 -0
- package/public/src/constants.js +21 -4
- package/public/src/constants.js.map +1 -1
- package/public/src/constants.ts +21 -4
- package/public/src/filters.js +10 -7
- package/public/src/filters.js.map +1 -1
- package/public/src/filters.ts +10 -7
- package/public/src/i18n.js +11 -1
- package/public/src/i18n.js.map +1 -1
- package/public/src/i18n.ts +13 -1
- package/public/src/state.js +3 -0
- package/public/src/state.js.map +1 -1
- package/public/src/state.ts +3 -0
- package/public/src/sw.ts +71 -7
- package/public/src/theme.js +13 -1
- package/public/src/theme.js.map +1 -1
- package/public/src/theme.ts +15 -1
- package/public/src/types.ts +2 -0
- package/public/src/utils.js +26 -1
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.ts +26 -1
- package/public/src/views/activity.js +1 -0
- package/public/src/views/activity.js.map +1 -1
- package/public/src/views/activity.ts +1 -0
- package/public/src/views/admin.js +134 -0
- package/public/src/views/admin.js.map +1 -1
- package/public/src/views/admin.ts +134 -0
- package/public/src/views/auth.js +31 -0
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +31 -0
- package/public/src/views/calendar.js +3 -0
- package/public/src/views/calendar.js.map +1 -1
- package/public/src/views/calendar.ts +3 -0
- package/public/src/views/comments-audit.js +1 -0
- package/public/src/views/comments-audit.js.map +1 -1
- package/public/src/views/comments-audit.ts +1 -0
- package/public/src/views/config.js +67 -0
- package/public/src/views/config.js.map +1 -1
- package/public/src/views/config.ts +67 -0
- package/public/src/views/context.js +2 -0
- package/public/src/views/context.js.map +1 -1
- package/public/src/views/context.ts +2 -0
- package/public/src/views/create.js +3 -0
- package/public/src/views/create.js.map +1 -1
- package/public/src/views/create.ts +3 -0
- package/public/src/views/dedupe.js +1 -0
- package/public/src/views/dedupe.js.map +1 -1
- package/public/src/views/dedupe.ts +1 -0
- package/public/src/views/export.js +6 -0
- package/public/src/views/export.js.map +1 -1
- package/public/src/views/export.ts +6 -0
- package/public/src/views/github.js +58 -0
- package/public/src/views/github.js.map +1 -1
- package/public/src/views/github.ts +58 -0
- package/public/src/views/graph-canvas.js +18 -0
- package/public/src/views/graph-canvas.js.map +1 -1
- package/public/src/views/graph-canvas.ts +20 -0
- package/public/src/views/graph.js +41 -0
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/graph.ts +42 -1
- package/public/src/views/groups.js +45 -0
- package/public/src/views/groups.js.map +1 -1
- package/public/src/views/groups.ts +45 -0
- package/public/src/views/guide.js +3 -0
- package/public/src/views/guide.js.map +1 -1
- package/public/src/views/guide.ts +3 -0
- package/public/src/views/health.js +3 -0
- package/public/src/views/health.js.map +1 -1
- package/public/src/views/health.ts +3 -0
- package/public/src/views/items.js +196 -6
- package/public/src/views/items.js.map +1 -1
- package/public/src/views/items.ts +196 -6
- package/public/src/views/normalize.js +2 -0
- package/public/src/views/normalize.js.map +1 -1
- package/public/src/views/normalize.ts +2 -0
- package/public/src/views/packages.js +41 -0
- package/public/src/views/packages.js.map +1 -1
- package/public/src/views/packages.ts +41 -0
- package/public/src/views/plan-execution.js +6 -0
- package/public/src/views/plan-execution.js.map +1 -1
- package/public/src/views/plan-execution.ts +8 -0
- package/public/src/views/plan.js +145 -0
- package/public/src/views/plan.js.map +1 -1
- package/public/src/views/plan.ts +146 -1
- package/public/src/views/projects.js +60 -0
- package/public/src/views/projects.js.map +1 -1
- package/public/src/views/projects.ts +60 -0
- package/public/src/views/router.js +43 -0
- package/public/src/views/router.js.map +1 -1
- package/public/src/views/router.ts +43 -0
- package/public/src/views/search.js +5 -0
- package/public/src/views/search.js.map +1 -1
- package/public/src/views/search.ts +5 -0
- package/public/src/views/settings.js +36 -0
- package/public/src/views/settings.js.map +1 -1
- package/public/src/views/settings.ts +36 -0
- package/public/src/views/shared.js +1 -0
- package/public/src/views/shared.js.map +1 -1
- package/public/src/views/shared.ts +1 -0
- package/public/src/views/sharing.js +32 -0
- package/public/src/views/sharing.js.map +1 -1
- package/public/src/views/sharing.ts +32 -0
- package/public/src/views/stats.js +1 -0
- package/public/src/views/stats.js.map +1 -1
- package/public/src/views/stats.ts +1 -0
- package/public/src/views/templates.js +4 -0
- package/public/src/views/templates.js.map +1 -1
- package/public/src/views/templates.ts +4 -0
- package/public/src/views/validate.js +1 -0
- package/public/src/views/validate.js.map +1 -1
- package/public/src/views/validate.ts +1 -0
- package/public/sw.js +60 -5
|
@@ -50,6 +50,7 @@ function removeCtxMenu() {
|
|
|
50
50
|
ctxMenuEl = null;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/** Builds and shows the right-click context menu for a graph node, with actions to open the item, select and focus, show its neighborhood, expand two hops, and copy its id. */
|
|
53
54
|
function showCtxMenu(nodeId, x, y) {
|
|
54
55
|
removeCtxMenu();
|
|
55
56
|
const graph = currentGraph?.graph || {};
|
|
@@ -121,6 +122,7 @@ function showCtxMenu(nodeId, x, y) {
|
|
|
121
122
|
function nodeTitle(node) {
|
|
122
123
|
return String(node.properties?.title || node.id);
|
|
123
124
|
}
|
|
125
|
+
/** Returns a display type for a node, preferring the `kind` property, then `type`, then a non-core label, defaulting to 'Item'. */
|
|
124
126
|
function nodeType(node) {
|
|
125
127
|
return String(node.properties?.kind
|
|
126
128
|
|| node.properties?.type
|
|
@@ -143,6 +145,7 @@ function nodeLane(node) {
|
|
|
143
145
|
return 'external';
|
|
144
146
|
return 'item';
|
|
145
147
|
}
|
|
148
|
+
/** Extracts a short user-facing error message from a raw error string (JSON detail, title, or code, otherwise the trimmed raw text), returning an empty string when the error is just the missing pm-graph extension. */
|
|
146
149
|
function compactError(raw) {
|
|
147
150
|
if (!raw)
|
|
148
151
|
return '';
|
|
@@ -157,6 +160,7 @@ function compactError(raw) {
|
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
162
|
// ── Graph data processing ─────────────────────────────────────
|
|
163
|
+
/** Returns the set of ids directly connected to the given node (plus the node itself) across the provided relationships. */
|
|
160
164
|
function directNeighborIds(nodeId, rels) {
|
|
161
165
|
const ids = new Set([nodeId]);
|
|
162
166
|
for (const r of rels) {
|
|
@@ -167,6 +171,7 @@ function directNeighborIds(nodeId, rels) {
|
|
|
167
171
|
}
|
|
168
172
|
return ids;
|
|
169
173
|
}
|
|
174
|
+
/** Returns the neighbor id(s) on the other end of a relationship relative to a node, filtered by traversal direction (incoming, outgoing, or connected). */
|
|
170
175
|
function walkDirectionMatch(rel, nodeId, dir) {
|
|
171
176
|
if (dir === 'incoming') {
|
|
172
177
|
return rel.to === nodeId ? [rel.from] : [];
|
|
@@ -176,6 +181,7 @@ function walkDirectionMatch(rel, nodeId, dir) {
|
|
|
176
181
|
}
|
|
177
182
|
return rel.to === nodeId ? [rel.from] : rel.from === nodeId ? [rel.to] : [];
|
|
178
183
|
}
|
|
184
|
+
/** Performs a bounded breadth-first traversal out to `depth` hops from a node, returning all reachable ids (including the node itself) while respecting the traversal direction. */
|
|
179
185
|
function expandedNeighborIds(nodeId, rels, depth, direction) {
|
|
180
186
|
const ids = new Set([nodeId]);
|
|
181
187
|
let frontier = new Set([nodeId]);
|
|
@@ -198,6 +204,7 @@ function expandedNeighborIds(nodeId, rels, depth, direction) {
|
|
|
198
204
|
}
|
|
199
205
|
return ids;
|
|
200
206
|
}
|
|
207
|
+
/** Counts both endpoints of every relationship, returning a map of node id to its total edge degree (incrementing the count for both `from` and `to`). */
|
|
201
208
|
function degreeMap(rels) {
|
|
202
209
|
const m = new Map();
|
|
203
210
|
for (const r of rels) {
|
|
@@ -216,6 +223,7 @@ function blockingPair(rel) {
|
|
|
216
223
|
return { blocked: rel.from, blocker: rel.to };
|
|
217
224
|
return null;
|
|
218
225
|
}
|
|
226
|
+
/** Maps a relationship type to a human-readable dependency label (for example 'Depends on'), falling back to the lowercased type with underscores replaced by spaces. */
|
|
219
227
|
function dependencyLabel(rel) {
|
|
220
228
|
const labels = {
|
|
221
229
|
DEPENDS_ON: 'Depends on',
|
|
@@ -230,6 +238,7 @@ function dependencyLabel(rel) {
|
|
|
230
238
|
};
|
|
231
239
|
return labels[rel.type] ?? rel.type.replace(/_/g, ' ').toLowerCase();
|
|
232
240
|
}
|
|
241
|
+
/** Computes the critical (longest) dependency path through the given relationships and returns its node ids as a set, or an empty set when there is no multi-step chain. */
|
|
233
242
|
function computeCriticalPath(rels) {
|
|
234
243
|
const depRels = rels.filter(isDependencyRel);
|
|
235
244
|
if (!depRels.length)
|
|
@@ -247,6 +256,7 @@ function computeCriticalPath(rels) {
|
|
|
247
256
|
allIds.add(pair.blocker);
|
|
248
257
|
}
|
|
249
258
|
const memo = new Map();
|
|
259
|
+
/** Memoized depth-first search returning the longest chain of blockers starting from the given id, guarding against cycles with a visited set. */
|
|
250
260
|
function longestPathFrom(id, seen = new Set()) {
|
|
251
261
|
if (memo.has(id))
|
|
252
262
|
return memo.get(id);
|
|
@@ -271,6 +281,7 @@ function computeCriticalPath(rels) {
|
|
|
271
281
|
.sort((a, b) => b.length - a.length)[0] ?? [];
|
|
272
282
|
return longest.length < 2 ? new Set() : new Set(longest);
|
|
273
283
|
}
|
|
284
|
+
/** Builds per-node dependency stats from blocking relationships, mapping each node id to the sets of items that block it and that it blocks. */
|
|
274
285
|
function blockerStats(rels) {
|
|
275
286
|
const stats = new Map();
|
|
276
287
|
const entry = (id) => {
|
|
@@ -287,6 +298,7 @@ function blockerStats(rels) {
|
|
|
287
298
|
}
|
|
288
299
|
return stats;
|
|
289
300
|
}
|
|
301
|
+
/** Applies the current filters (dep mode, focus neighborhood, node kind, status, text query, relationship type, and direction) to a graph, returning the surviving nodes, relationships, and the set of all connected node ids. */
|
|
290
302
|
function visibleGraph(graph) {
|
|
291
303
|
const nodes = graph.nodes || [];
|
|
292
304
|
let rels = graph.relationships || [];
|
|
@@ -341,6 +353,7 @@ function visibleGraph(graph) {
|
|
|
341
353
|
return { nodes: visNodes, rels: visRels, connected };
|
|
342
354
|
}
|
|
343
355
|
// ── Canvas data conversion ────────────────────────────────────
|
|
356
|
+
/** Converts graph nodes into canvas display nodes, attaching each node's edge degree (computed from the relationships) so the renderer can size them. */
|
|
344
357
|
function toCanvasNodes(nodes, rels) {
|
|
345
358
|
const deg = degreeMap(rels);
|
|
346
359
|
return nodes.map((n) => ({
|
|
@@ -361,6 +374,7 @@ function toCanvasEdges(rels) {
|
|
|
361
374
|
return rels.map((r) => ({ from: r.from, to: r.to, type: r.type }));
|
|
362
375
|
}
|
|
363
376
|
// ── Info panel rendering ──────────────────────────────────────
|
|
377
|
+
/** Renders the graph-coverage HTML: a linked-item percentage bar, the external cross-project reference count, and the top relationship type with its edge count. */
|
|
364
378
|
function renderCoverage(itemNodes, rels, connected, relCounts) {
|
|
365
379
|
const linked = itemNodes.filter((n) => connected.has(n.id)).length;
|
|
366
380
|
const linkedPct = itemNodes.length > 0 ? Math.round((linked / itemNodes.length) * 100) : 0;
|
|
@@ -390,6 +404,7 @@ function renderCoverage(itemNodes, rels, connected, relCounts) {
|
|
|
390
404
|
</div>
|
|
391
405
|
</div>`;
|
|
392
406
|
}
|
|
407
|
+
/** Renders the selected-node HTML panel, including metadata, action buttons, blocker counts, the property grid, tags, an optional description, and its direct relationships. */
|
|
393
408
|
function renderSelectedNode(node, rels, byId, fullItem) {
|
|
394
409
|
if (!node)
|
|
395
410
|
return '<div class="graph-node-empty">Click any node in the graph to inspect it.</div>';
|
|
@@ -454,6 +469,7 @@ function renderSelectedNode(node, rels, byId, fullItem) {
|
|
|
454
469
|
}).join('') + (direct.length > 16 ? `<div class="graph-limit-note">+${direct.length - 16} more — use Focus scope to narrow</div>` : '')}
|
|
455
470
|
</div>`;
|
|
456
471
|
}
|
|
472
|
+
/** Renders the neighborhood HTML for the selected node, showing clickable one-hop and two-hop neighbor chips. */
|
|
457
473
|
function renderPaths(node, rels, byId) {
|
|
458
474
|
if (!node)
|
|
459
475
|
return '<div class="graph-node-empty">Select a node to explore paths.</div>';
|
|
@@ -471,6 +487,7 @@ function renderPaths(node, rels, byId) {
|
|
|
471
487
|
<div class="graph-path-section"><div class="graph-path-label">One hop (${oneHop.size})</div><div class="graph-path-chips">${chips(oneHop) || '<span>None.</span>'}</div></div>
|
|
472
488
|
<div class="graph-path-section"><div class="graph-path-label">Two hops (${twoHop.size})</div><div class="graph-path-chips">${chips(twoHop) || '<span>None.</span>'}</div></div>`;
|
|
473
489
|
}
|
|
490
|
+
/** Renders the item-hubs HTML, listing the top eight most-connected items by degree with their outgoing and incoming edge counts. */
|
|
474
491
|
function renderHubs(nodes, rels) {
|
|
475
492
|
const deg = degreeMap(rels);
|
|
476
493
|
const hubs = nodes.filter(isItemNode)
|
|
@@ -487,6 +504,7 @@ function renderHubs(nodes, rels) {
|
|
|
487
504
|
<small>${e.out}↑ ${e.inn}↓</small>
|
|
488
505
|
</button>`).join('');
|
|
489
506
|
}
|
|
507
|
+
/** Renders the dependency-blocker HTML, listing the top eight items that block or are blocked by the most other items and flagging those on the critical path. */
|
|
490
508
|
function renderBlockingInsights(nodes, rels) {
|
|
491
509
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
492
510
|
const stats = blockerStats(rels);
|
|
@@ -504,6 +522,7 @@ function renderBlockingInsights(nodes, rels) {
|
|
|
504
522
|
<small>${row.blockers.size} blockers · ${row.blocked.size} items blocked${criticalPath.has(row.id) ? ' · critical path' : ''}</small>
|
|
505
523
|
</button>`).join('');
|
|
506
524
|
}
|
|
525
|
+
/** Renders the dependency-chains HTML: the longest dependency chains, potential cycle warnings, and root blockers, with critical-path highlighting. */
|
|
507
526
|
function renderDependencyChains(nodes, rels) {
|
|
508
527
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
509
528
|
const depRels = rels.filter(isDependencyRel);
|
|
@@ -525,6 +544,7 @@ function renderDependencyChains(nodes, rels) {
|
|
|
525
544
|
.map(([id, blocked]) => ({ id, blocked: blocked.length }))
|
|
526
545
|
.sort((a, b) => b.blocked - a.blocked || nodeTitle(byId.get(a.id) || { id: a.id }).localeCompare(nodeTitle(byId.get(b.id) || { id: b.id })))
|
|
527
546
|
.slice(0, 4);
|
|
547
|
+
/** Depth-first trace of a dependency chain from the given id, recording detected cycle starts and returning the longest chain of blockers found. */
|
|
528
548
|
function traceFrom(id, seen = new Set()) {
|
|
529
549
|
if (seen.has(id)) {
|
|
530
550
|
cycleStarts.add(id);
|
|
@@ -570,6 +590,7 @@ function renderDependencyChains(nodes, rels) {
|
|
|
570
590
|
${leafRows}
|
|
571
591
|
</div>`;
|
|
572
592
|
}
|
|
593
|
+
/** Renders the full graph-analysis info panel HTML by composing the coverage, selected node, neighborhood, hubs, blockers, dependency-chain, and node/relationship count sections. */
|
|
573
594
|
function renderInfoPanel(data, fullItem) {
|
|
574
595
|
const graph = data.graph || {};
|
|
575
596
|
const nodes = graph.nodes || [];
|
|
@@ -607,6 +628,7 @@ function renderInfoPanel(data, fullItem) {
|
|
|
607
628
|
</div>`;
|
|
608
629
|
}
|
|
609
630
|
// ── Rel list (inside bottom drawer) ──────────────────────────
|
|
631
|
+
/** Renders the relationship list shown in the bottom drawer: add/remove dependency buttons and up to 100 filtered relationship rows, each linking its two endpoints. */
|
|
610
632
|
function renderRelList(data) {
|
|
611
633
|
const graph = data.graph || {};
|
|
612
634
|
const nodes = graph.nodes || [];
|
|
@@ -640,6 +662,7 @@ function renderRelList(data) {
|
|
|
640
662
|
return editBtns + rows + limitNote;
|
|
641
663
|
}
|
|
642
664
|
// ── Immersive shell ───────────────────────────────────────────
|
|
665
|
+
/** Returns whether the given preset id (knowledge, dependency, unlinked, metadata, critical, or tags) matches the current filter configuration. */
|
|
643
666
|
function graphPresetActive(id) {
|
|
644
667
|
if (id === 'knowledge') {
|
|
645
668
|
return !filter.depMode && (filter.kind === 'items' || filter.kind === 'all') && filter.rel === 'all' && filter.scope === 'all' && filter.colorMode === 'status';
|
|
@@ -656,6 +679,7 @@ function graphPresetActive(id) {
|
|
|
656
679
|
return !filter.depMode && filter.colorMode === 'tag';
|
|
657
680
|
return false;
|
|
658
681
|
}
|
|
682
|
+
/** Renders the preset rail toolbar HTML, with one button per view preset (Knowledge, Dependencies, Unlinked, Tags, Metadata, Critical) showing live counts and active state. */
|
|
659
683
|
function renderGraphPresets(depRels, isolatedCount) {
|
|
660
684
|
const allNodes = currentGraph?.graph?.nodes ?? [];
|
|
661
685
|
const tagSet = new Set();
|
|
@@ -681,6 +705,7 @@ function renderGraphPresets(depRels, isolatedCount) {
|
|
|
681
705
|
</button>`).join('')}
|
|
682
706
|
</div>`;
|
|
683
707
|
}
|
|
708
|
+
/** Renders the full immersive graph shell HTML: the canvas host, the top HUD bar with search and presets, physics controls, filter overlay, legend, info drawer, and relationship drawer. */
|
|
684
709
|
function renderGraphShell(data) {
|
|
685
710
|
const graph = data.graph || {};
|
|
686
711
|
const nodes = graph.nodes || [];
|
|
@@ -878,6 +903,7 @@ function renderGraphShell(data) {
|
|
|
878
903
|
</div>`;
|
|
879
904
|
}
|
|
880
905
|
// ── Canvas init / update ──────────────────────────────────────
|
|
906
|
+
/** Fetches the full item record for the selected item-lane node and re-renders the info panel with its description; clears the cached item for non-item nodes or on failure. */
|
|
881
907
|
async function fetchAndUpdateSelectedItem(nodeId) {
|
|
882
908
|
if (!state.currentProject || !nodeId)
|
|
883
909
|
return;
|
|
@@ -903,6 +929,7 @@ async function fetchAndUpdateSelectedItem(nodeId) {
|
|
|
903
929
|
selectedItemCache = null;
|
|
904
930
|
}
|
|
905
931
|
}
|
|
932
|
+
/** Pushes the current filters and selection onto the canvas and, when searching with no node selected, jumps the camera to the first matching node. */
|
|
906
933
|
function syncCanvas() {
|
|
907
934
|
if (!canvasRef.current || !currentGraph)
|
|
908
935
|
return;
|
|
@@ -926,6 +953,7 @@ function syncCanvas() {
|
|
|
926
953
|
canvasRef.current.jumpToNode(match.id);
|
|
927
954
|
}
|
|
928
955
|
}
|
|
956
|
+
/** Creates the GraphCanvas instance for the current graph, wires its selection, open, and context-menu callbacks, computes the critical path, loads the data, and applies the initial filter. */
|
|
929
957
|
function initCanvas() {
|
|
930
958
|
const host = document.getElementById('graph-canvas-host');
|
|
931
959
|
if (!host || !currentGraph)
|
|
@@ -965,6 +993,7 @@ function initCanvas() {
|
|
|
965
993
|
syncCanvas();
|
|
966
994
|
}
|
|
967
995
|
// ── Panel / drawer updates ────────────────────────────────────
|
|
996
|
+
/** Re-renders the info panel and relationship list into the DOM from the current graph (and cached selected item) and re-binds their event handlers. */
|
|
968
997
|
function updateInfoPanel() {
|
|
969
998
|
if (!currentGraph)
|
|
970
999
|
return;
|
|
@@ -976,6 +1005,7 @@ function updateInfoPanel() {
|
|
|
976
1005
|
relList.innerHTML = renderRelList(currentGraph);
|
|
977
1006
|
bindInfoPanelEvents();
|
|
978
1007
|
}
|
|
1008
|
+
/** Synchronizes the filter toolbar DOM (direction select enabled state, depth slider and label, scope button text, and preset active states) with the current selection and filter. */
|
|
979
1009
|
function updateFilterToolbarState() {
|
|
980
1010
|
const dirSel = document.getElementById('graph-filter-direction');
|
|
981
1011
|
const depthSldr = document.getElementById('graph-filter-depth');
|
|
@@ -1001,6 +1031,7 @@ const TYPE_COLORS_MAP = {
|
|
|
1001
1031
|
milestone: '#fbbf24', story: '#34d399', chore: '#94a3b8', release: '#38bdf8',
|
|
1002
1032
|
};
|
|
1003
1033
|
const TAG_PALETTE_JS = ['#2dd4bf', '#60a5fa', '#a78bfa', '#f87171', '#fbbf24', '#34d399', '#fb923c', '#e879f9'];
|
|
1034
|
+
/** Computes a tag-to-color map by ranking tags by how often they appear across the nodes and assigning each of the most frequent ones a palette color. */
|
|
1004
1035
|
function computeTagColorMap(nodes) {
|
|
1005
1036
|
const freq = new Map();
|
|
1006
1037
|
for (const n of nodes) {
|
|
@@ -1011,6 +1042,7 @@ function computeTagColorMap(nodes) {
|
|
|
1011
1042
|
const top = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, TAG_PALETTE_JS.length).map(([t]) => t);
|
|
1012
1043
|
return new Map(top.map((t, i) => [t, TAG_PALETTE_JS[i]]));
|
|
1013
1044
|
}
|
|
1045
|
+
/** Rebuilds the legend HUD HTML to match the current color mode (status, type, or tag) or dependency mode, including clickable tag chips when coloring by tag. */
|
|
1014
1046
|
function updateLegend() {
|
|
1015
1047
|
const legend = document.getElementById('graph-legend-hud');
|
|
1016
1048
|
if (!legend)
|
|
@@ -1086,6 +1118,7 @@ function updateLegend() {
|
|
|
1086
1118
|
}
|
|
1087
1119
|
}
|
|
1088
1120
|
// ── Event bindings ────────────────────────────────────────────
|
|
1121
|
+
/** Binds click handlers for the info-panel controls (open and clear the selected item, add and remove dependency) and for neighbor, tag, and relationship rows so they update the selection and canvas. */
|
|
1089
1122
|
function bindInfoPanelEvents() {
|
|
1090
1123
|
document.getElementById('graph-open-selected')?.addEventListener('click', () => {
|
|
1091
1124
|
if (selectedNodeId)
|
|
@@ -1138,6 +1171,7 @@ function bindInfoPanelEvents() {
|
|
|
1138
1171
|
});
|
|
1139
1172
|
});
|
|
1140
1173
|
}
|
|
1174
|
+
/** Binds all HUD control handlers for the graph view: back/refresh/sync/fit buttons, presets, physics toggles and sliders, filter selects, search input, drawer toggles, color mode, and global keyboard shortcuts. */
|
|
1141
1175
|
function bindHudEvents() {
|
|
1142
1176
|
// Back button
|
|
1143
1177
|
document.getElementById('graph-back-btn')?.addEventListener('click', () => {
|
|
@@ -1482,6 +1516,7 @@ function bindHudEvents() {
|
|
|
1482
1516
|
window.__graphKeyHandler = graphKeyHandler;
|
|
1483
1517
|
}
|
|
1484
1518
|
// ── URL routing (pushState) ─────────────────────────────────
|
|
1519
|
+
/** Serializes the current selection and filter into URL query parameters and replaces the browser history entry so the view can be restored or shared. */
|
|
1485
1520
|
function pushGraphState() {
|
|
1486
1521
|
if (!state.currentProject)
|
|
1487
1522
|
return;
|
|
@@ -1505,6 +1540,7 @@ function pushGraphState() {
|
|
|
1505
1540
|
history.replaceState(null, '', url);
|
|
1506
1541
|
}
|
|
1507
1542
|
let urlStateRestored = false;
|
|
1543
|
+
/** Reads graph view parameters from the current URL query string and applies them to the selection and filter, marking state as restored. */
|
|
1508
1544
|
function restoreGraphState() {
|
|
1509
1545
|
const params = new URLSearchParams(window.location.search);
|
|
1510
1546
|
if (!params.has('graph'))
|
|
@@ -1524,6 +1560,7 @@ function restoreGraphState() {
|
|
|
1524
1560
|
filter = { ...filter, layout: (params.get('layout') || 'force') };
|
|
1525
1561
|
}
|
|
1526
1562
|
// ── Dependency editing modals ────────────────────────────────
|
|
1563
|
+
/** Shows the add-dependency modal listing item nodes as source and target selects plus a relationship type, and POSTs the new dependency then refreshes the graph on submit. */
|
|
1527
1564
|
function showAddDependencyModal() {
|
|
1528
1565
|
if (!state.currentProject || !currentGraph)
|
|
1529
1566
|
return;
|
|
@@ -1598,6 +1635,7 @@ function showAddDependencyModal() {
|
|
|
1598
1635
|
}
|
|
1599
1636
|
});
|
|
1600
1637
|
}
|
|
1638
|
+
/** Shows the remove-dependency modal listing existing dependency relationships and DELETEs the chosen one on submit, then refreshes the graph. */
|
|
1601
1639
|
function showRemoveDependencyModal() {
|
|
1602
1640
|
if (!state.currentProject || !currentGraph)
|
|
1603
1641
|
return;
|
|
@@ -1663,6 +1701,7 @@ function showRemoveDependencyModal() {
|
|
|
1663
1701
|
});
|
|
1664
1702
|
}
|
|
1665
1703
|
// ── Main entry point ──────────────────────────────────────────
|
|
1704
|
+
/** Renders the full graph view into the `content-graph` element: clears any prior canvas, restores URL state, fetches the project graph, computes the critical path, renders the shell, binds events, and initializes the canvas. */
|
|
1666
1705
|
export async function renderGraphView() {
|
|
1667
1706
|
const el = document.getElementById('content-graph');
|
|
1668
1707
|
if (!el)
|
|
@@ -1735,6 +1774,7 @@ export async function refreshGraphData() {
|
|
|
1735
1774
|
// ── Local graph (embedded mini-graph for item detail) ─────────
|
|
1736
1775
|
// Registry of active local graph canvases so they can be cleaned up
|
|
1737
1776
|
const localGraphRegistry = new Map();
|
|
1777
|
+
/** Destroys and unregisters the local mini-graph canvas previously created for the given container id, if any. */
|
|
1738
1778
|
export function destroyLocalGraph(containerId) {
|
|
1739
1779
|
const existing = localGraphRegistry.get(containerId);
|
|
1740
1780
|
if (existing) {
|
|
@@ -1742,6 +1782,7 @@ export function destroyLocalGraph(containerId) {
|
|
|
1742
1782
|
localGraphRegistry.delete(containerId);
|
|
1743
1783
|
}
|
|
1744
1784
|
}
|
|
1785
|
+
/** Renders an embedded mini-graph for a single item showing its neighborhood out to the given depth, with neighbor-node clicks navigating to that item's detail view. */
|
|
1745
1786
|
export async function renderLocalGraph(containerId, nodeId, depth = 2) {
|
|
1746
1787
|
const container = document.getElementById(containerId);
|
|
1747
1788
|
if (!container || !state.currentProject)
|