@transitrix/diagrams 1.13.2 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activities/types.d.ts +5 -0
- package/dist/fgca/parse-canonical.js +1 -0
- package/dist/fgca/preview-layout.d.ts +1 -0
- package/dist/fgca/preview-layout.js +1 -1
- package/dist/fgca/types.d.ts +2 -0
- package/dist/webview/completion-percent.d.ts +10 -0
- package/dist/webview/completion-percent.js +12 -0
- package/dist/webview/render-activities.js +2 -0
- package/dist/webview/render-fgca.d.ts +0 -14
- package/dist/webview/render-fgca.js +3 -15
- package/package.json +4 -4
- package/src/activities/types.ts +2 -0
- package/src/fgca/parse-canonical.ts +1 -0
- package/src/fgca/preview-layout.ts +2 -1
- package/src/fgca/types.ts +2 -0
- package/src/webview/completion-percent.ts +18 -0
- package/src/webview/render-activities.ts +2 -0
- package/src/webview/render-fgca.ts +3 -15
|
@@ -24,6 +24,11 @@ export interface Activity {
|
|
|
24
24
|
resources_cost?: number;
|
|
25
25
|
effort?: number;
|
|
26
26
|
link?: string;
|
|
27
|
+
/** Derived display metadata, loaded from the model sidecar. */
|
|
28
|
+
progress?: {
|
|
29
|
+
percent: number;
|
|
30
|
+
computedAt: string;
|
|
31
|
+
};
|
|
27
32
|
description?: string;
|
|
28
33
|
delivers_changes?: string[];
|
|
29
34
|
}
|
|
@@ -280,6 +280,7 @@ export function parseCanonicalFGCA(input, outOfScopeGoalIds, outOfScopeFactorIds
|
|
|
280
280
|
name: String(el['name'] ?? ''),
|
|
281
281
|
goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
|
|
282
282
|
...(typeVal !== undefined ? { type: typeVal } : {}),
|
|
283
|
+
...(typeof el['link'] === 'string' ? { link: el['link'] } : {}),
|
|
283
284
|
...(progressVal !== undefined ? { progress: progressVal } : {}),
|
|
284
285
|
};
|
|
285
286
|
});
|
|
@@ -250,7 +250,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
|
250
250
|
...changes.map(c => ({ id: `change_${c.id}`, label: c.name })),
|
|
251
251
|
...virtualChanges.map(v => ({ id: v.id, label: '–', virtual: true })),
|
|
252
252
|
],
|
|
253
|
-
activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: a.
|
|
253
|
+
activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: Boolean(a.link?.trim()) })),
|
|
254
254
|
};
|
|
255
255
|
// Build predecessor map: for each node, which node IDs in the previous column
|
|
256
256
|
// connect to it from the left. Used for barycenter crossing minimization.
|
package/dist/fgca/types.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface ActivityItem {
|
|
|
24
24
|
activity_type_id?: number;
|
|
25
25
|
/** Canonical values: initiative | programme | project | work_package. Unknown values pass through without error. */
|
|
26
26
|
type?: string;
|
|
27
|
+
/** Tracker link, independent of goal relationships. */
|
|
28
|
+
link?: string;
|
|
27
29
|
progress?: {
|
|
28
30
|
percent: number;
|
|
29
31
|
computedAt: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared completion display contract for Action, DGA and DGCA previews. */
|
|
2
|
+
export interface ActionProgress {
|
|
3
|
+
percent: number;
|
|
4
|
+
computedAt: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function completionPercentState(progress: ActionProgress | undefined, linked: boolean, enabled?: boolean, now?: number): {
|
|
7
|
+
label: string;
|
|
8
|
+
stale: boolean;
|
|
9
|
+
} | undefined;
|
|
10
|
+
export declare function completionPercentSvg(progress: ActionProgress | undefined, linked: boolean, x: number, y: number, enabled?: boolean): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function completionPercentState(progress, linked, enabled = true, now = Date.now()) {
|
|
2
|
+
if (!enabled)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (progress && Number.isFinite(progress.percent) && progress.percent >= 0 && progress.percent <= 100 && Number.isFinite(Date.parse(progress.computedAt))) {
|
|
5
|
+
return { label: `${progress.percent}%`, stale: now - Date.parse(progress.computedAt) > 7 * 24 * 60 * 60 * 1000 };
|
|
6
|
+
}
|
|
7
|
+
return linked ? { label: '–%', stale: false } : undefined;
|
|
8
|
+
}
|
|
9
|
+
export function completionPercentSvg(progress, linked, x, y, enabled = true) {
|
|
10
|
+
const state = completionPercentState(progress, linked, enabled);
|
|
11
|
+
return state ? `<text class="text-id completion-percent" x="${x}" y="${y}" text-anchor="end" dominant-baseline="hanging" font-size="10"${state.stale ? ' opacity="0.5"' : ''}>${state.label}</text>` : '';
|
|
12
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* in `renderActivitiesNetworkBody` and is shared verbatim with the VS Code
|
|
17
17
|
* preview's `networkSvg`.
|
|
18
18
|
*/
|
|
19
|
+
import { completionPercentSvg } from './completion-percent.js';
|
|
19
20
|
import { layoutActivities } from '../activities/layout.js';
|
|
20
21
|
import { computeCpm } from '../activities/cpm.js';
|
|
21
22
|
import { horizontalCubicEdgePath, bowedCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
@@ -127,6 +128,7 @@ function activityNodeSvg(n, x, y, isCritical, isMilestone, durLabel) {
|
|
|
127
128
|
return [
|
|
128
129
|
`<rect class="${cls}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="8"/>`,
|
|
129
130
|
textSvg,
|
|
131
|
+
completionPercentSvg(n.data.progress, Boolean(n.data.link?.trim()), x + n.width - 8, y + 6),
|
|
130
132
|
durLabel ? `<text class="text-secondary" x="${x + n.width - 8}" y="${y + n.height - 8}" text-anchor="end">${durLabel}</text>` : '',
|
|
131
133
|
].filter(Boolean).join('\n');
|
|
132
134
|
}
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-safe SVG renderer for the canonical DGCA and DGA notations.
|
|
3
|
-
*
|
|
4
|
-
* Step 4 of the IntelliJ epic (ADR 0001): the webview bundle must turn a
|
|
5
|
-
* validated DGCA/DGA document into renderable SVG so JCEF can drop it into the
|
|
6
|
-
* preview panel. The VS Code path lives in `extension/src/dgca-preview.ts` and
|
|
7
|
-
* pulls in VS Code-specific concerns (themes, title block, save dialogs,
|
|
8
|
-
* spacing/scope controls); this module is the host-neutral subset — pure
|
|
9
|
-
* column layout → SVG with no VS Code APIs, no `node:fs`, no `node:path`.
|
|
10
|
-
*
|
|
11
|
-
* DGCA and DGA share the same renderer; DGA only differs by hiding the
|
|
12
|
-
* `change` column (`hideChanges` / the layout's Goal → Activity collapse),
|
|
13
|
-
* exactly as the extension preview does.
|
|
14
|
-
*/
|
|
15
1
|
import { layoutFGCAPreview } from '../fgca/preview-layout.js';
|
|
16
2
|
import type { FGCADoc } from '../fgca/validate.js';
|
|
17
3
|
import { type EdgeStyle } from '../edge-path.js';
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `change` column (`hideChanges` / the layout's Goal → Activity collapse),
|
|
13
13
|
* exactly as the extension preview does.
|
|
14
14
|
*/
|
|
15
|
+
import { completionPercentSvg } from './completion-percent.js';
|
|
15
16
|
import { layoutFGCAPreview, FGCA_NODE_W, FGCA_NODE_H, FGCA_HEADER_H as HEADER_H, FGCA_PAD as PAD, } from '../fgca/preview-layout.js';
|
|
16
17
|
import { previewEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
17
18
|
import { parseNodeSizePreset, resolveDgcaNodeSize } from '../node-size-presets.js';
|
|
@@ -39,7 +40,6 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
|
|
|
39
40
|
const edgeStyle = bodyOptions.edgeStyle;
|
|
40
41
|
const showCompletionPercent = bodyOptions.showCompletionPercent ?? true;
|
|
41
42
|
const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
|
|
42
|
-
const STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
43
43
|
const headerSvg = columns
|
|
44
44
|
.map(({ col, x }) => `<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`)
|
|
45
45
|
.join('\n');
|
|
@@ -69,20 +69,8 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
|
|
|
69
69
|
const typeBadge = n.type && n.col === 'activity'
|
|
70
70
|
? `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 10}" text-anchor="end" dominant-baseline="hanging" font-size="10">${escXml(n.type)}</text>`
|
|
71
71
|
: '';
|
|
72
|
-
const progressBadge =
|
|
73
|
-
? (()
|
|
74
|
-
if (n.progress) {
|
|
75
|
-
const computedAt = new Date(n.progress.computedAt);
|
|
76
|
-
const now = new Date();
|
|
77
|
-
const isStale = now.getTime() - computedAt.getTime() > STALENESS_THRESHOLD_MS;
|
|
78
|
-
const opacity = isStale ? ' opacity="0.5"' : '';
|
|
79
|
-
return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10"${opacity}>${escXml(n.progress.percent + '%')}</text>`;
|
|
80
|
-
}
|
|
81
|
-
if (n.linked) {
|
|
82
|
-
return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10">–%</text>`;
|
|
83
|
-
}
|
|
84
|
-
return '';
|
|
85
|
-
})()
|
|
72
|
+
const progressBadge = n.col === 'activity'
|
|
73
|
+
? completionPercentSvg(n.progress, Boolean(n.linked), n.x + nodeWidth - 6, n.y + 22, showCompletionPercent)
|
|
86
74
|
: '';
|
|
87
75
|
return [
|
|
88
76
|
`<g>`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transitrix/diagrams",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"reactflow": "^11.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@testing-library/dom": "^10.4.
|
|
55
|
+
"@testing-library/dom": "^10.4.2",
|
|
56
56
|
"@testing-library/react": "^16.3.3",
|
|
57
57
|
"@types/react": "^19.2.17",
|
|
58
|
-
"@types/react-dom": "^19.
|
|
58
|
+
"@types/react-dom": "^19.3.0",
|
|
59
59
|
"jsdom": "^29.1.1",
|
|
60
60
|
"react": "^19.2.8",
|
|
61
|
-
"react-dom": "^19.
|
|
61
|
+
"react-dom": "^19.3.0",
|
|
62
62
|
"reactflow": "^11.11.4",
|
|
63
63
|
"typescript": "^5.0.0",
|
|
64
64
|
"vitest": "^4.1.11"
|
package/src/activities/types.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface Activity {
|
|
|
33
33
|
resources_cost?: number;
|
|
34
34
|
effort?: number;
|
|
35
35
|
link?: string;
|
|
36
|
+
/** Derived display metadata, loaded from the model sidecar. */
|
|
37
|
+
progress?: { percent: number; computedAt: string };
|
|
36
38
|
description?: string;
|
|
37
39
|
delivers_changes?: string[];
|
|
38
40
|
}
|
|
@@ -332,6 +332,7 @@ export function parseCanonicalFGCA(
|
|
|
332
332
|
name: String(el!['name'] ?? ''),
|
|
333
333
|
goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
|
|
334
334
|
...(typeVal !== undefined ? { type: typeVal } : {}),
|
|
335
|
+
...(typeof el!['link'] === 'string' ? { link: el!['link'] } : {}),
|
|
335
336
|
...(progressVal !== undefined ? { progress: progressVal } : {}),
|
|
336
337
|
};
|
|
337
338
|
});
|
|
@@ -30,6 +30,7 @@ export interface FGCAPreviewChange {
|
|
|
30
30
|
activity_ids: Array<number | string>;
|
|
31
31
|
}
|
|
32
32
|
export interface FGCAPreviewActivity {
|
|
33
|
+
link?: string;
|
|
33
34
|
id: number | string;
|
|
34
35
|
name: string;
|
|
35
36
|
goal_id?: number | string | null;
|
|
@@ -387,7 +388,7 @@ export function layoutFGCAPreview(
|
|
|
387
388
|
...changes.map(c => ({ id: `change_${c.id}`, label: c.name })),
|
|
388
389
|
...virtualChanges.map(v => ({ id: v.id, label: '–', virtual: true })),
|
|
389
390
|
],
|
|
390
|
-
activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: a.
|
|
391
|
+
activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name, progress: a.progress, linked: Boolean(a.link?.trim()) })),
|
|
391
392
|
};
|
|
392
393
|
|
|
393
394
|
// Build predecessor map: for each node, which node IDs in the previous column
|
package/src/fgca/types.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface ActivityItem {
|
|
|
25
25
|
activity_type_id?: number;
|
|
26
26
|
/** Canonical values: initiative | programme | project | work_package. Unknown values pass through without error. */
|
|
27
27
|
type?: string;
|
|
28
|
+
/** Tracker link, independent of goal relationships. */
|
|
29
|
+
link?: string;
|
|
28
30
|
progress?: {
|
|
29
31
|
percent: number;
|
|
30
32
|
computedAt: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Shared completion display contract for Action, DGA and DGCA previews. */
|
|
2
|
+
export interface ActionProgress {
|
|
3
|
+
percent: number;
|
|
4
|
+
computedAt: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function completionPercentState(progress: ActionProgress | undefined, linked: boolean, enabled = true, now = Date.now()): { label: string; stale: boolean } | undefined {
|
|
8
|
+
if (!enabled) return undefined;
|
|
9
|
+
if (progress && Number.isFinite(progress.percent) && progress.percent >= 0 && progress.percent <= 100 && Number.isFinite(Date.parse(progress.computedAt))) {
|
|
10
|
+
return { label: `${progress.percent}%`, stale: now - Date.parse(progress.computedAt) > 7 * 24 * 60 * 60 * 1000 };
|
|
11
|
+
}
|
|
12
|
+
return linked ? { label: '–%', stale: false } : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function completionPercentSvg(progress: ActionProgress | undefined, linked: boolean, x: number, y: number, enabled = true): string {
|
|
16
|
+
const state = completionPercentState(progress, linked, enabled);
|
|
17
|
+
return state ? `<text class="text-id completion-percent" x="${x}" y="${y}" text-anchor="end" dominant-baseline="hanging" font-size="10"${state.stale ? ' opacity="0.5"' : ''}>${state.label}</text>` : '';
|
|
18
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* in `renderActivitiesNetworkBody` and is shared verbatim with the VS Code
|
|
17
17
|
* preview's `networkSvg`.
|
|
18
18
|
*/
|
|
19
|
+
import { completionPercentSvg } from './completion-percent.js';
|
|
19
20
|
import { layoutActivities } from '../activities/layout.js';
|
|
20
21
|
import { computeCpm } from '../activities/cpm.js';
|
|
21
22
|
import type {
|
|
@@ -147,6 +148,7 @@ function activityNodeSvg(
|
|
|
147
148
|
return [
|
|
148
149
|
`<rect class="${cls}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="8"/>`,
|
|
149
150
|
textSvg,
|
|
151
|
+
completionPercentSvg(n.data.progress, Boolean(n.data.link?.trim()), x + n.width - 8, y + 6),
|
|
150
152
|
durLabel ? `<text class="text-secondary" x="${x + n.width - 8}" y="${y + n.height - 8}" text-anchor="end">${durLabel}</text>` : '',
|
|
151
153
|
].filter(Boolean).join('\n');
|
|
152
154
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `change` column (`hideChanges` / the layout's Goal → Activity collapse),
|
|
13
13
|
* exactly as the extension preview does.
|
|
14
14
|
*/
|
|
15
|
+
import { completionPercentSvg } from './completion-percent.js';
|
|
15
16
|
import {
|
|
16
17
|
layoutFGCAPreview,
|
|
17
18
|
FGCA_NODE_W,
|
|
@@ -65,7 +66,6 @@ export function renderFgcaBody(
|
|
|
65
66
|
const edgeStyle = bodyOptions.edgeStyle;
|
|
66
67
|
const showCompletionPercent = bodyOptions.showCompletionPercent ?? true;
|
|
67
68
|
const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
|
|
68
|
-
const STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
69
69
|
|
|
70
70
|
const headerSvg = columns
|
|
71
71
|
.map(({ col, x }) =>
|
|
@@ -103,20 +103,8 @@ export function renderFgcaBody(
|
|
|
103
103
|
const typeBadge = n.type && n.col === 'activity'
|
|
104
104
|
? `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 10}" text-anchor="end" dominant-baseline="hanging" font-size="10">${escXml(n.type)}</text>`
|
|
105
105
|
: '';
|
|
106
|
-
const progressBadge =
|
|
107
|
-
? (()
|
|
108
|
-
if (n.progress) {
|
|
109
|
-
const computedAt = new Date(n.progress.computedAt);
|
|
110
|
-
const now = new Date();
|
|
111
|
-
const isStale = now.getTime() - computedAt.getTime() > STALENESS_THRESHOLD_MS;
|
|
112
|
-
const opacity = isStale ? ' opacity="0.5"' : '';
|
|
113
|
-
return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10"${opacity}>${escXml(n.progress.percent + '%')}</text>`;
|
|
114
|
-
}
|
|
115
|
-
if (n.linked) {
|
|
116
|
-
return `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10">–%</text>`;
|
|
117
|
-
}
|
|
118
|
-
return '';
|
|
119
|
-
})()
|
|
106
|
+
const progressBadge = n.col === 'activity'
|
|
107
|
+
? completionPercentSvg(n.progress, Boolean(n.linked), n.x + nodeWidth - 6, n.y + 22, showCompletionPercent)
|
|
120
108
|
: '';
|
|
121
109
|
return [
|
|
122
110
|
`<g>`,
|