@burdenoff/fluidgrids-fe-sdk 2026.1101.4 → 2026.1102.2
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/{ExecutionDetailPage-JQCJR5XD.js → ExecutionDetailPage-XHII23QR.js} +11 -11
- package/dist/{ExecutionsListPage-UDDASCDL.js → ExecutionsListPage-GTCSYSC6.js} +7 -7
- package/dist/{WorkflowBuilder-P52OTD2Y.js → WorkflowBuilder-ZIXEU7DQ.js} +12 -11
- package/dist/{WorkflowsList-QMN25IG4.js → WorkflowsList-NYSQEZWU.js} +7 -7
- package/dist/{agentTools-F2USMIDU.js → agentTools-PA62A53S.js} +4 -4
- package/dist/{chunk-V4TI5JMD.js → chunk-23LYGZ27.js} +1 -1
- package/dist/{chunk-QBYLI3HK.js → chunk-37BKVPO7.js} +12 -40
- package/dist/{chunk-D56WJCK6.js → chunk-3Y4266JL.js} +3 -3
- package/dist/{chunk-7MGYG6DR.js → chunk-4FDMLKLL.js} +2 -2
- package/dist/{chunk-6JJKKWVD.js → chunk-6VODFT2Q.js} +5 -5
- package/dist/{chunk-EZRX2PZT.js → chunk-7JLLH4YE.js} +4 -4
- package/dist/{chunk-LIVFWFT4.js → chunk-7SY2SL5Y.js} +6 -6
- package/dist/{chunk-IQZ3DD7U.js → chunk-A4XNCGWT.js} +2 -2
- package/dist/{chunk-FV77SZWB.js → chunk-A7QPQ6MO.js} +1 -1
- package/dist/{chunk-UN6NUIBV.js → chunk-B2DNUVKW.js} +4 -4
- package/dist/{chunk-ITLNDVAU.js → chunk-BDBSRN2O.js} +4 -4
- package/dist/{chunk-FLYFZJUC.js → chunk-CXNJKM4D.js} +1 -1
- package/dist/{chunk-XGMGCOTR.js → chunk-DLKGWGRN.js} +1 -1
- package/dist/{chunk-BUWFUPN2.js → chunk-EFOJEQDS.js} +2 -2
- package/dist/{chunk-6EE4PRD6.js → chunk-JCHE4V55.js} +1 -1
- package/dist/{chunk-DO554TUK.js → chunk-JTQFBZTV.js} +3 -1
- package/dist/{chunk-KEPQLNP6.js → chunk-MY752OLT.js} +67 -195
- package/dist/{chunk-6XVGLGJ4.js → chunk-OR7KZZ2D.js} +1 -1
- package/dist/{chunk-AVZ4SXUG.js → chunk-OUYEMZT6.js} +1 -1
- package/dist/{chunk-O4RZZXVP.js → chunk-OZCJHPMH.js} +3 -3
- package/dist/{chunk-7FYXSA76.js → chunk-P6IRD5SA.js} +10 -10
- package/dist/chunk-PBCBDSVA.js +1480 -0
- package/dist/{chunk-UWCGAXYV.js → chunk-PC3CICOW.js} +1 -1
- package/dist/{chunk-LFCF5KTH.js → chunk-QAXP5HON.js} +2 -2
- package/dist/{chunk-WKNYQ4M6.js → chunk-TJ2PFY7I.js} +2 -2
- package/dist/{chunk-QUIZ5IP4.js → chunk-UETHT3KG.js} +2 -2
- package/dist/{chunk-K5TN3BKD.js → chunk-VFLHURFM.js} +1 -1
- package/dist/{chunk-LGMSYVMT.js → chunk-W74MXVFX.js} +1 -1
- package/dist/{chunk-PC66C2AA.js → chunk-XNVYPEBO.js} +129 -21
- package/dist/{chunk-QRECENSK.js → chunk-YVOHCC7J.js} +4 -4
- package/dist/{chunk-WN4FBSFD.js → chunk-ZTJX5ONT.js} +4 -4
- package/dist/executions.js +12 -12
- package/dist/history.js +5 -4
- package/dist/index.d.ts +29 -1
- package/dist/index.js +43 -42
- package/dist/{node-detail-view-NTWGYZMB.js → node-detail-view-KET5DNEH.js} +427 -9
- package/dist/nodes.js +2 -2
- package/dist/observability.js +5 -5
- package/dist/{quick-config-panel-ZMCB6VXB.js → quick-config-panel-L6CTZQUG.js} +5 -5
- package/dist/settings.js +6 -6
- package/dist/templates.js +11 -10
- package/dist/workflow-builder.js +13 -12
- package/dist/workflows-list.js +7 -7
- package/package.json +1 -1
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
import { mergeStickyNotes, describeOperationError, throwIfResultError } from './chunk-23LYGZ27.js';
|
|
2
|
+
import { WorkflowDefinitionDocument, WorkflowVersionDiffDocument } from './chunk-JTQFBZTV.js';
|
|
3
|
+
import { useTr } from './chunk-UULVIT25.js';
|
|
4
|
+
import { getFluidGridsClient } from './chunk-NTWG3CJX.js';
|
|
5
|
+
import { memo, useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
|
6
|
+
import { ArrowLeftRight, Eye, RotateCcw, Loader2 } from 'lucide-react';
|
|
7
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Button, Badge, AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, Tabs, TabsList, TabsTrigger, TabsContent } from '@burdenoff/fe-libs/ui';
|
|
8
|
+
import { cn } from '@burdenoff/fe-libs/utils';
|
|
9
|
+
import { Handle, Position, ReactFlowProvider, ReactFlow, Background, Controls, MiniMap } from '@xyflow/react';
|
|
10
|
+
import '@xyflow/react/dist/style.css';
|
|
11
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
12
|
+
|
|
13
|
+
// src/modules/history/utils/version-lifecycle.ts
|
|
14
|
+
var LIFECYCLE_STAGES = [
|
|
15
|
+
"DRAFT",
|
|
16
|
+
"ACTIVE",
|
|
17
|
+
"DEPRECATED",
|
|
18
|
+
"ARCHIVED"
|
|
19
|
+
];
|
|
20
|
+
function lifecycleStageOf(status) {
|
|
21
|
+
const upper = (status ?? "").toUpperCase();
|
|
22
|
+
return LIFECYCLE_STAGES.includes(upper) ? upper : "DRAFT";
|
|
23
|
+
}
|
|
24
|
+
function lifecycleIndex(status) {
|
|
25
|
+
return LIFECYCLE_STAGES.indexOf(lifecycleStageOf(status));
|
|
26
|
+
}
|
|
27
|
+
function countLifecycle(versions) {
|
|
28
|
+
const counts = {
|
|
29
|
+
DRAFT: 0,
|
|
30
|
+
ACTIVE: 0,
|
|
31
|
+
DEPRECATED: 0,
|
|
32
|
+
ARCHIVED: 0
|
|
33
|
+
};
|
|
34
|
+
for (const version of versions) counts[lifecycleStageOf(version.status)] += 1;
|
|
35
|
+
return counts;
|
|
36
|
+
}
|
|
37
|
+
function lifecycleSteps(status) {
|
|
38
|
+
const current = lifecycleIndex(status);
|
|
39
|
+
return LIFECYCLE_STAGES.map((stage, index) => ({
|
|
40
|
+
stage,
|
|
41
|
+
state: index < current ? "done" : index === current ? "current" : "upcoming"
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
function versionsOfWorkflow(rows, workflowKey) {
|
|
45
|
+
const time = (value) => {
|
|
46
|
+
const parsed = value ? new Date(value).getTime() : Number.NaN;
|
|
47
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48
|
+
};
|
|
49
|
+
return rows.filter((row) => row.workflowKey === workflowKey).sort((a, b) => time(b.createdAt) - time(a.createdAt));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/builder/live/definition-canvas.ts
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
function hasId(item) {
|
|
57
|
+
return isRecord(item) && typeof item.id === "string" && item.id.length > 0;
|
|
58
|
+
}
|
|
59
|
+
function canvasFromDefinition(definitionJson) {
|
|
60
|
+
let definition = definitionJson;
|
|
61
|
+
if (typeof definition === "string") {
|
|
62
|
+
try {
|
|
63
|
+
definition = JSON.parse(definition);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!isRecord(definition)) return null;
|
|
69
|
+
const data = isRecord(definition.data) ? definition.data : definition;
|
|
70
|
+
if (!Array.isArray(data.nodes)) return null;
|
|
71
|
+
const nodes = mergeStickyNotes({
|
|
72
|
+
nodes: data.nodes,
|
|
73
|
+
stickyNotes: Array.isArray(data.stickyNotes) ? data.stickyNotes : []
|
|
74
|
+
}).filter(hasId);
|
|
75
|
+
const edges = (Array.isArray(data.edges) ? data.edges : []).filter(
|
|
76
|
+
hasId
|
|
77
|
+
);
|
|
78
|
+
return { nodes, edges };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/modules/history/utils/version-graph-diff.ts
|
|
82
|
+
var LAYOUT_NODE_KEYS = /* @__PURE__ */ new Set([
|
|
83
|
+
"position",
|
|
84
|
+
"positionAbsolute",
|
|
85
|
+
"selected",
|
|
86
|
+
"dragging",
|
|
87
|
+
"measured",
|
|
88
|
+
"width",
|
|
89
|
+
"height",
|
|
90
|
+
"zIndex",
|
|
91
|
+
"resizing",
|
|
92
|
+
"style",
|
|
93
|
+
"className"
|
|
94
|
+
]);
|
|
95
|
+
var LAYOUT_EDGE_KEYS = /* @__PURE__ */ new Set([
|
|
96
|
+
"id",
|
|
97
|
+
"selected",
|
|
98
|
+
"animated",
|
|
99
|
+
"style",
|
|
100
|
+
"className",
|
|
101
|
+
"zIndex",
|
|
102
|
+
"interactionWidth",
|
|
103
|
+
"markerEnd",
|
|
104
|
+
"markerStart"
|
|
105
|
+
]);
|
|
106
|
+
var DIFF_CARD_FOOTPRINT = { width: 220, height: 90 };
|
|
107
|
+
var MAX_CHANGED_FIELDS = 20;
|
|
108
|
+
function isRecord2(value) {
|
|
109
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
function equalJson(left, right) {
|
|
112
|
+
if (Object.is(left, right)) return true;
|
|
113
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
114
|
+
return left.length === right.length && left.every((value, index) => equalJson(value, right[index]));
|
|
115
|
+
}
|
|
116
|
+
if (isRecord2(left) && isRecord2(right)) {
|
|
117
|
+
const leftKeys = Object.keys(left).filter((k) => left[k] !== void 0);
|
|
118
|
+
const rightKeys = Object.keys(right).filter((k) => right[k] !== void 0);
|
|
119
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
120
|
+
return leftKeys.every(
|
|
121
|
+
(key) => key in right && equalJson(left[key], right[key])
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
function changedPaths(before, after, prefix = "", limit = MAX_CHANGED_FIELDS) {
|
|
127
|
+
const paths = [];
|
|
128
|
+
const visit = (left, right, path) => {
|
|
129
|
+
if (paths.length >= limit || equalJson(left, right)) return;
|
|
130
|
+
if (isRecord2(left) && isRecord2(right)) {
|
|
131
|
+
const keys = [
|
|
132
|
+
.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])
|
|
133
|
+
].sort((a, b) => a.localeCompare(b));
|
|
134
|
+
for (const key of keys) {
|
|
135
|
+
visit(left[key], right[key], path ? `${path}.${key}` : key);
|
|
136
|
+
if (paths.length >= limit) return;
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
paths.push(path || "(value)");
|
|
141
|
+
};
|
|
142
|
+
visit(before, after, prefix);
|
|
143
|
+
return paths;
|
|
144
|
+
}
|
|
145
|
+
function withoutKeys(value, keys) {
|
|
146
|
+
const out = {};
|
|
147
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
148
|
+
if (!keys.has(key)) out[key] = entry;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
function text(value) {
|
|
153
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
154
|
+
}
|
|
155
|
+
function nodeLabel(node) {
|
|
156
|
+
const data = isRecord2(node.data) ? node.data : {};
|
|
157
|
+
return text(data.label) ?? text(data.name) ?? text(data.title) ?? String(node.id);
|
|
158
|
+
}
|
|
159
|
+
function nodeTypeOf(node) {
|
|
160
|
+
const data = isRecord2(node.data) ? node.data : {};
|
|
161
|
+
return text(data.nodeType) ?? text(data.type) ?? text(node.type) ?? "node";
|
|
162
|
+
}
|
|
163
|
+
function positionOf(node) {
|
|
164
|
+
const position = node?.position;
|
|
165
|
+
if (position && typeof position.x === "number" && Number.isFinite(position.x) && typeof position.y === "number" && Number.isFinite(position.y)) {
|
|
166
|
+
return { x: position.x, y: position.y };
|
|
167
|
+
}
|
|
168
|
+
return { x: 0, y: 0 };
|
|
169
|
+
}
|
|
170
|
+
function overlaps(a, b) {
|
|
171
|
+
return Math.abs(a.x - b.x) < DIFF_CARD_FOOTPRINT.width && Math.abs(a.y - b.y) < DIFF_CARD_FOOTPRINT.height;
|
|
172
|
+
}
|
|
173
|
+
function clearOf(position, taken) {
|
|
174
|
+
let candidate = position;
|
|
175
|
+
for (let attempt = 0; attempt <= taken.length; attempt += 1) {
|
|
176
|
+
const blocker = taken.find((spot) => overlaps(candidate, spot));
|
|
177
|
+
if (!blocker) return candidate;
|
|
178
|
+
candidate = {
|
|
179
|
+
x: candidate.x,
|
|
180
|
+
y: blocker.y + DIFF_CARD_FOOTPRINT.height
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return candidate;
|
|
184
|
+
}
|
|
185
|
+
function handle(value) {
|
|
186
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
187
|
+
}
|
|
188
|
+
function edgeSignature(edge) {
|
|
189
|
+
return [
|
|
190
|
+
edge.source,
|
|
191
|
+
handle(edge.sourceHandle) ?? "",
|
|
192
|
+
edge.target,
|
|
193
|
+
handle(edge.targetHandle) ?? ""
|
|
194
|
+
].join("|");
|
|
195
|
+
}
|
|
196
|
+
function edgeLabel(edge) {
|
|
197
|
+
if (typeof edge.label === "string" && edge.label.trim())
|
|
198
|
+
return edge.label.trim();
|
|
199
|
+
const data = isRecord2(edge.data) ? edge.data : {};
|
|
200
|
+
return text(data.label);
|
|
201
|
+
}
|
|
202
|
+
function emptyCounts() {
|
|
203
|
+
return { added: 0, removed: 0, modified: 0, unchanged: 0 };
|
|
204
|
+
}
|
|
205
|
+
function diffWorkflowGraphs(from, to) {
|
|
206
|
+
const fromNodes = new Map((from?.nodes ?? []).map((n) => [String(n.id), n]));
|
|
207
|
+
const toNodes = new Map((to?.nodes ?? []).map((n) => [String(n.id), n]));
|
|
208
|
+
const nodes = [];
|
|
209
|
+
const nodeCounts = emptyCounts();
|
|
210
|
+
for (const [id, after] of toNodes) {
|
|
211
|
+
const before = fromNodes.get(id);
|
|
212
|
+
const afterPosition = positionOf(after);
|
|
213
|
+
if (!before) {
|
|
214
|
+
nodes.push({
|
|
215
|
+
id,
|
|
216
|
+
status: "added",
|
|
217
|
+
label: nodeLabel(after),
|
|
218
|
+
nodeType: nodeTypeOf(after),
|
|
219
|
+
position: afterPosition,
|
|
220
|
+
changedFields: [],
|
|
221
|
+
moved: false
|
|
222
|
+
});
|
|
223
|
+
nodeCounts.added += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const fields = changedPaths(
|
|
227
|
+
withoutKeys(
|
|
228
|
+
before,
|
|
229
|
+
LAYOUT_NODE_KEYS
|
|
230
|
+
),
|
|
231
|
+
withoutKeys(
|
|
232
|
+
after,
|
|
233
|
+
LAYOUT_NODE_KEYS
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
const beforePosition = positionOf(before);
|
|
237
|
+
const status = fields.length ? "modified" : "unchanged";
|
|
238
|
+
nodes.push({
|
|
239
|
+
id,
|
|
240
|
+
status,
|
|
241
|
+
label: nodeLabel(after),
|
|
242
|
+
nodeType: nodeTypeOf(after),
|
|
243
|
+
position: afterPosition,
|
|
244
|
+
changedFields: fields,
|
|
245
|
+
moved: beforePosition.x !== afterPosition.x || beforePosition.y !== afterPosition.y
|
|
246
|
+
});
|
|
247
|
+
nodeCounts[status] += 1;
|
|
248
|
+
}
|
|
249
|
+
const placed = nodes.map((node) => node.position);
|
|
250
|
+
for (const [id, before] of fromNodes) {
|
|
251
|
+
if (toNodes.has(id)) continue;
|
|
252
|
+
const position = clearOf(positionOf(before), placed);
|
|
253
|
+
placed.push(position);
|
|
254
|
+
nodes.push({
|
|
255
|
+
id,
|
|
256
|
+
status: "removed",
|
|
257
|
+
label: nodeLabel(before),
|
|
258
|
+
nodeType: nodeTypeOf(before),
|
|
259
|
+
position,
|
|
260
|
+
changedFields: [],
|
|
261
|
+
moved: false
|
|
262
|
+
});
|
|
263
|
+
nodeCounts.removed += 1;
|
|
264
|
+
}
|
|
265
|
+
const firstBySignature = (list) => {
|
|
266
|
+
const bySignature = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const edge of list) {
|
|
268
|
+
const signature = edgeSignature(edge);
|
|
269
|
+
if (!bySignature.has(signature)) bySignature.set(signature, edge);
|
|
270
|
+
}
|
|
271
|
+
return bySignature;
|
|
272
|
+
};
|
|
273
|
+
const fromEdges = firstBySignature(from?.edges ?? []);
|
|
274
|
+
const toEdges = firstBySignature(to?.edges ?? []);
|
|
275
|
+
const edges = [];
|
|
276
|
+
const edgeCounts = emptyCounts();
|
|
277
|
+
const pushEdge = (edge, status) => {
|
|
278
|
+
edges.push({
|
|
279
|
+
id: edgeSignature(edge),
|
|
280
|
+
status,
|
|
281
|
+
source: String(edge.source),
|
|
282
|
+
target: String(edge.target),
|
|
283
|
+
sourceHandle: handle(edge.sourceHandle),
|
|
284
|
+
targetHandle: handle(edge.targetHandle),
|
|
285
|
+
label: edgeLabel(edge)
|
|
286
|
+
});
|
|
287
|
+
edgeCounts[status] += 1;
|
|
288
|
+
};
|
|
289
|
+
for (const [signature, after] of toEdges) {
|
|
290
|
+
const before = fromEdges.get(signature);
|
|
291
|
+
if (!before) {
|
|
292
|
+
pushEdge(after, "added");
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const same = equalJson(
|
|
296
|
+
withoutKeys(
|
|
297
|
+
before,
|
|
298
|
+
LAYOUT_EDGE_KEYS
|
|
299
|
+
),
|
|
300
|
+
withoutKeys(
|
|
301
|
+
after,
|
|
302
|
+
LAYOUT_EDGE_KEYS
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
pushEdge(after, same ? "unchanged" : "modified");
|
|
306
|
+
}
|
|
307
|
+
for (const [signature, before] of fromEdges) {
|
|
308
|
+
if (!toEdges.has(signature)) pushEdge(before, "removed");
|
|
309
|
+
}
|
|
310
|
+
const identical = nodeCounts.added + nodeCounts.removed + nodeCounts.modified + edgeCounts.added + edgeCounts.removed + edgeCounts.modified === 0;
|
|
311
|
+
return { nodes, edges, nodeCounts, edgeCounts, identical };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/modules/history/hooks/useVersionComparison.ts
|
|
315
|
+
async function fetchDefinition(client, workflowKey, version) {
|
|
316
|
+
const result = throwIfResultError(
|
|
317
|
+
await client.query({
|
|
318
|
+
query: WorkflowDefinitionDocument,
|
|
319
|
+
variables: { workflowKey, version },
|
|
320
|
+
fetchPolicy: "network-only"
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
const definition = result.data?.workflowDefinition;
|
|
324
|
+
if (!definition) throw new Error(`Version ${version} was not found`);
|
|
325
|
+
return definition;
|
|
326
|
+
}
|
|
327
|
+
async function fetchServerDiff(client, workflowKey, fromVersion, toVersion) {
|
|
328
|
+
const result = throwIfResultError(
|
|
329
|
+
await client.query({
|
|
330
|
+
query: WorkflowVersionDiffDocument,
|
|
331
|
+
variables: { workflowKey, fromVersion, toVersion },
|
|
332
|
+
fetchPolicy: "network-only"
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
const diff = result.data?.workflowVersionDiff;
|
|
336
|
+
if (!diff) throw new Error("The service returned no diff");
|
|
337
|
+
return diff;
|
|
338
|
+
}
|
|
339
|
+
async function loadVersionComparison(client, workflowKey, fromVersion, toVersion) {
|
|
340
|
+
const [from, to, diff] = await Promise.all([
|
|
341
|
+
fetchDefinition(client, workflowKey, fromVersion),
|
|
342
|
+
fetchDefinition(client, workflowKey, toVersion),
|
|
343
|
+
fetchServerDiff(client, workflowKey, fromVersion, toVersion).then(
|
|
344
|
+
(value) => ({ value, error: null }),
|
|
345
|
+
(error) => ({
|
|
346
|
+
value: null,
|
|
347
|
+
error: describeOperationError(error, "Failed to load the change list")
|
|
348
|
+
})
|
|
349
|
+
)
|
|
350
|
+
]);
|
|
351
|
+
const fromCanvas = canvasFromDefinition(from.definitionJson);
|
|
352
|
+
const toCanvas = canvasFromDefinition(to.definitionJson);
|
|
353
|
+
return {
|
|
354
|
+
workflowKey,
|
|
355
|
+
fromVersion,
|
|
356
|
+
toVersion,
|
|
357
|
+
from,
|
|
358
|
+
to,
|
|
359
|
+
fromCanvas,
|
|
360
|
+
toCanvas,
|
|
361
|
+
graph: diffWorkflowGraphs(fromCanvas, toCanvas),
|
|
362
|
+
serverDiff: diff.value,
|
|
363
|
+
serverDiffError: diff.error
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function useVersionComparison(workflowKey, fromVersion, toVersion) {
|
|
367
|
+
const [comparison, setComparison] = useState(null);
|
|
368
|
+
const [loading, setLoading] = useState(false);
|
|
369
|
+
const [error, setError] = useState(null);
|
|
370
|
+
const [nonce, setNonce] = useState(0);
|
|
371
|
+
const requestId = useRef(0);
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (!workflowKey || !fromVersion || !toVersion || fromVersion === toVersion) {
|
|
374
|
+
setComparison(null);
|
|
375
|
+
setError(null);
|
|
376
|
+
setLoading(false);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const id = ++requestId.current;
|
|
380
|
+
setLoading(true);
|
|
381
|
+
setError(null);
|
|
382
|
+
loadVersionComparison(
|
|
383
|
+
getFluidGridsClient(),
|
|
384
|
+
workflowKey,
|
|
385
|
+
fromVersion,
|
|
386
|
+
toVersion
|
|
387
|
+
).then((result) => {
|
|
388
|
+
if (id === requestId.current) setComparison(result);
|
|
389
|
+
}).catch((err) => {
|
|
390
|
+
if (id !== requestId.current) return;
|
|
391
|
+
setComparison(null);
|
|
392
|
+
setError(describeOperationError(err, "Failed to compare versions"));
|
|
393
|
+
}).finally(() => {
|
|
394
|
+
if (id === requestId.current) setLoading(false);
|
|
395
|
+
});
|
|
396
|
+
}, [workflowKey, fromVersion, toVersion, nonce]);
|
|
397
|
+
const reload = useCallback(() => setNonce((n) => n + 1), []);
|
|
398
|
+
const current = comparison && comparison.workflowKey === workflowKey && comparison.fromVersion === fromVersion && comparison.toVersion === toVersion ? comparison : null;
|
|
399
|
+
return { comparison: current, loading, error, reload };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/modules/history/components/diff-styles.ts
|
|
403
|
+
var DIFF_STATUS_CLASSES = {
|
|
404
|
+
added: {
|
|
405
|
+
card: "border-[var(--color-status-success-border)] bg-[var(--color-status-success-bg)]",
|
|
406
|
+
chip: "border-[var(--color-status-success-border)] bg-[var(--color-status-success-bg)] text-[var(--color-status-success-text)]",
|
|
407
|
+
text: "text-[var(--color-status-success-text)]"
|
|
408
|
+
},
|
|
409
|
+
removed: {
|
|
410
|
+
card: "border-dashed border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg)] opacity-80",
|
|
411
|
+
chip: "border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg)] text-[var(--color-status-error-text)]",
|
|
412
|
+
text: "text-[var(--color-status-error-text)]"
|
|
413
|
+
},
|
|
414
|
+
modified: {
|
|
415
|
+
card: "border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg)]",
|
|
416
|
+
chip: "border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg)] text-[var(--color-status-warning-text)]",
|
|
417
|
+
text: "text-[var(--color-status-warning-text)]"
|
|
418
|
+
},
|
|
419
|
+
unchanged: {
|
|
420
|
+
card: "border-border bg-card",
|
|
421
|
+
chip: "border-border bg-muted text-muted-foreground",
|
|
422
|
+
text: "text-muted-foreground"
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var DIFF_EDGE_STROKE = {
|
|
426
|
+
added: "var(--color-status-success-text)",
|
|
427
|
+
removed: "var(--color-status-error-text)",
|
|
428
|
+
modified: "var(--color-status-warning-text)",
|
|
429
|
+
unchanged: "var(--color-border-strong)"
|
|
430
|
+
};
|
|
431
|
+
function diffStatusLabel(tr, status) {
|
|
432
|
+
switch (status) {
|
|
433
|
+
case "added":
|
|
434
|
+
return tr("fluidgrids.history.versionDiff.statusAdded", "Added");
|
|
435
|
+
case "removed":
|
|
436
|
+
return tr("fluidgrids.history.versionDiff.statusRemoved", "Removed");
|
|
437
|
+
case "modified":
|
|
438
|
+
return tr("fluidgrids.history.versionDiff.statusModified", "Modified");
|
|
439
|
+
default:
|
|
440
|
+
return tr("fluidgrids.history.versionDiff.statusUnchanged", "Unchanged");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function lifecycleStageLabel(tr, stage) {
|
|
444
|
+
switch (stage) {
|
|
445
|
+
case "ACTIVE":
|
|
446
|
+
return tr("fluidgrids.history.lifecycle.active", "Active");
|
|
447
|
+
case "DEPRECATED":
|
|
448
|
+
return tr("fluidgrids.history.lifecycle.deprecated", "Deprecated");
|
|
449
|
+
case "ARCHIVED":
|
|
450
|
+
return tr("fluidgrids.history.lifecycle.archived", "Archived");
|
|
451
|
+
default:
|
|
452
|
+
return tr("fluidgrids.history.lifecycle.draft", "Draft");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/modules/history/components/diff-flow.ts
|
|
457
|
+
var DEFAULT_HANDLES = /* @__PURE__ */ new Set(["source", "target"]);
|
|
458
|
+
function namedHandle(value) {
|
|
459
|
+
return value && !DEFAULT_HANDLES.has(value) ? value : null;
|
|
460
|
+
}
|
|
461
|
+
function handleCaption(edge) {
|
|
462
|
+
const source = namedHandle(edge.sourceHandle);
|
|
463
|
+
const target = namedHandle(edge.targetHandle);
|
|
464
|
+
if (source && target) return `${source} \u2192 ${target}`;
|
|
465
|
+
return source ?? (target ? `\u2192 ${target}` : null);
|
|
466
|
+
}
|
|
467
|
+
function edgeCaption(edge) {
|
|
468
|
+
const handles = handleCaption(edge);
|
|
469
|
+
if (edge.label && handles) return `${edge.label} (${handles})`;
|
|
470
|
+
return edge.label ?? handles ?? void 0;
|
|
471
|
+
}
|
|
472
|
+
function toFlowElements(graph) {
|
|
473
|
+
const known = new Set(graph.nodes.map((node) => node.id));
|
|
474
|
+
return {
|
|
475
|
+
nodes: graph.nodes.map((node) => ({
|
|
476
|
+
id: node.id,
|
|
477
|
+
type: "diff",
|
|
478
|
+
position: node.position,
|
|
479
|
+
data: { diff: node },
|
|
480
|
+
draggable: false,
|
|
481
|
+
connectable: false
|
|
482
|
+
})),
|
|
483
|
+
// Handle ids are not passed to React Flow: the diff cards have one handle
|
|
484
|
+
// per side, and an edge naming a handle the card does not have would not
|
|
485
|
+
// be drawn. The handles still identify the connection (a branch rerouted
|
|
486
|
+
// from one output of an If node to another is a removed edge plus an added
|
|
487
|
+
// edge between the same two nodes), so they are named in the label, and
|
|
488
|
+
// removed edges take a different path so the pair does not draw as one.
|
|
489
|
+
edges: graph.edges.filter((edge) => known.has(edge.source) && known.has(edge.target)).map((edge) => ({
|
|
490
|
+
id: edge.id,
|
|
491
|
+
source: edge.source,
|
|
492
|
+
target: edge.target,
|
|
493
|
+
type: edge.status === "removed" ? "smoothstep" : void 0,
|
|
494
|
+
label: edgeCaption(edge),
|
|
495
|
+
animated: edge.status === "added",
|
|
496
|
+
focusable: false,
|
|
497
|
+
data: { status: edge.status },
|
|
498
|
+
style: {
|
|
499
|
+
stroke: DIFF_EDGE_STROKE[edge.status],
|
|
500
|
+
strokeWidth: edge.status === "unchanged" ? 1.5 : 2.5,
|
|
501
|
+
strokeDasharray: edge.status === "removed" ? "6 4" : void 0
|
|
502
|
+
}
|
|
503
|
+
}))
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
var DiffNodeCard = memo(function DiffNodeCard2({
|
|
507
|
+
data,
|
|
508
|
+
selected
|
|
509
|
+
}) {
|
|
510
|
+
const tr = useTr();
|
|
511
|
+
const { diff } = data;
|
|
512
|
+
const styles = DIFF_STATUS_CLASSES[diff.status];
|
|
513
|
+
return /* @__PURE__ */ jsxs(
|
|
514
|
+
"div",
|
|
515
|
+
{
|
|
516
|
+
className: cn(
|
|
517
|
+
"w-[200px] rounded-lg border-2 px-3 py-2 text-left shadow-sm",
|
|
518
|
+
styles.card,
|
|
519
|
+
selected && "ring-2 ring-primary"
|
|
520
|
+
),
|
|
521
|
+
"data-testid": "fluidgrids-version-diff-node",
|
|
522
|
+
"data-diff-status": diff.status,
|
|
523
|
+
"data-node-id": diff.id,
|
|
524
|
+
children: [
|
|
525
|
+
/* @__PURE__ */ jsx(
|
|
526
|
+
Handle,
|
|
527
|
+
{
|
|
528
|
+
type: "target",
|
|
529
|
+
position: Position.Left,
|
|
530
|
+
isConnectable: false,
|
|
531
|
+
className: "!size-2 !border-0 !bg-muted-foreground"
|
|
532
|
+
}
|
|
533
|
+
),
|
|
534
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
535
|
+
/* @__PURE__ */ jsx(
|
|
536
|
+
"span",
|
|
537
|
+
{
|
|
538
|
+
className: cn(
|
|
539
|
+
"truncate text-sm font-medium text-foreground",
|
|
540
|
+
diff.status === "removed" && "line-through"
|
|
541
|
+
),
|
|
542
|
+
title: diff.label,
|
|
543
|
+
children: diff.label
|
|
544
|
+
}
|
|
545
|
+
),
|
|
546
|
+
/* @__PURE__ */ jsx(
|
|
547
|
+
"span",
|
|
548
|
+
{
|
|
549
|
+
className: cn(
|
|
550
|
+
"shrink-0 text-[10px] font-semibold uppercase",
|
|
551
|
+
styles.text
|
|
552
|
+
),
|
|
553
|
+
children: diffStatusLabel(tr, diff.status)
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
] }),
|
|
557
|
+
/* @__PURE__ */ jsx(
|
|
558
|
+
"p",
|
|
559
|
+
{
|
|
560
|
+
className: "mt-0.5 truncate font-mono text-[10px] text-muted-foreground",
|
|
561
|
+
title: diff.nodeType,
|
|
562
|
+
children: diff.nodeType
|
|
563
|
+
}
|
|
564
|
+
),
|
|
565
|
+
diff.status === "modified" && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-muted-foreground", children: tr(
|
|
566
|
+
"fluidgrids.history.versionDiff.changedFieldCount",
|
|
567
|
+
"{{count}} field(s) changed",
|
|
568
|
+
{ count: diff.changedFields.length }
|
|
569
|
+
) }),
|
|
570
|
+
/* @__PURE__ */ jsx(
|
|
571
|
+
Handle,
|
|
572
|
+
{
|
|
573
|
+
type: "source",
|
|
574
|
+
position: Position.Right,
|
|
575
|
+
isConnectable: false,
|
|
576
|
+
className: "!size-2 !border-0 !bg-muted-foreground"
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
var NODE_TYPES = { diff: DiffNodeCard };
|
|
584
|
+
var LEGEND_ORDER = [
|
|
585
|
+
"added",
|
|
586
|
+
"modified",
|
|
587
|
+
"removed",
|
|
588
|
+
"unchanged"
|
|
589
|
+
];
|
|
590
|
+
function VersionDiffLegend({
|
|
591
|
+
nodeCounts,
|
|
592
|
+
edgeCounts
|
|
593
|
+
}) {
|
|
594
|
+
const tr = useTr();
|
|
595
|
+
return /* @__PURE__ */ jsx(
|
|
596
|
+
"ul",
|
|
597
|
+
{
|
|
598
|
+
className: "flex flex-wrap items-center gap-2 text-xs",
|
|
599
|
+
"aria-label": tr("fluidgrids.history.versionDiff.legend", "Legend"),
|
|
600
|
+
"data-testid": "fluidgrids-version-diff-legend",
|
|
601
|
+
children: LEGEND_ORDER.map((status) => /* @__PURE__ */ jsxs(
|
|
602
|
+
"li",
|
|
603
|
+
{
|
|
604
|
+
"data-diff-status": status,
|
|
605
|
+
className: cn(
|
|
606
|
+
"flex items-center gap-1.5 rounded-full border px-2 py-0.5",
|
|
607
|
+
DIFF_STATUS_CLASSES[status].chip
|
|
608
|
+
),
|
|
609
|
+
children: [
|
|
610
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: diffStatusLabel(tr, status) }),
|
|
611
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: tr(
|
|
612
|
+
"fluidgrids.history.versionDiff.legendCounts",
|
|
613
|
+
"{{nodes}} nodes \xB7 {{edges}} connections",
|
|
614
|
+
{ nodes: nodeCounts[status], edges: edgeCounts[status] }
|
|
615
|
+
) })
|
|
616
|
+
]
|
|
617
|
+
},
|
|
618
|
+
status
|
|
619
|
+
))
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
function VersionDiffGraphInner({ graph, className }) {
|
|
624
|
+
const tr = useTr();
|
|
625
|
+
const { nodes, edges } = useMemo(() => toFlowElements(graph), [graph]);
|
|
626
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
627
|
+
const selected = graph.nodes.find((node) => node.id === selectedId) ?? null;
|
|
628
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3", className), children: [
|
|
629
|
+
/* @__PURE__ */ jsx(
|
|
630
|
+
VersionDiffLegend,
|
|
631
|
+
{
|
|
632
|
+
nodeCounts: graph.nodeCounts,
|
|
633
|
+
edgeCounts: graph.edgeCounts
|
|
634
|
+
}
|
|
635
|
+
),
|
|
636
|
+
graph.nodes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "rounded-md border border-dashed border-border p-6 text-center text-sm text-muted-foreground", children: tr(
|
|
637
|
+
"fluidgrids.history.versionDiff.noCanvas",
|
|
638
|
+
"Neither version has a canvas to draw. Use the JSON view to compare them."
|
|
639
|
+
) }) : /* @__PURE__ */ jsx(
|
|
640
|
+
"div",
|
|
641
|
+
{
|
|
642
|
+
className: "h-[420px] w-full overflow-hidden rounded-lg border border-border bg-background",
|
|
643
|
+
"data-testid": "fluidgrids-version-diff-canvas",
|
|
644
|
+
children: /* @__PURE__ */ jsxs(
|
|
645
|
+
ReactFlow,
|
|
646
|
+
{
|
|
647
|
+
nodes,
|
|
648
|
+
edges,
|
|
649
|
+
nodeTypes: NODE_TYPES,
|
|
650
|
+
nodesDraggable: false,
|
|
651
|
+
nodesConnectable: false,
|
|
652
|
+
edgesFocusable: false,
|
|
653
|
+
elementsSelectable: true,
|
|
654
|
+
onNodeClick: (_, node) => setSelectedId(node.id),
|
|
655
|
+
onPaneClick: () => setSelectedId(null),
|
|
656
|
+
fitView: true,
|
|
657
|
+
fitViewOptions: { padding: 0.2 },
|
|
658
|
+
minZoom: 0.2,
|
|
659
|
+
maxZoom: 1.5,
|
|
660
|
+
proOptions: { hideAttribution: true },
|
|
661
|
+
children: [
|
|
662
|
+
/* @__PURE__ */ jsx(Background, { gap: 16, size: 1 }),
|
|
663
|
+
/* @__PURE__ */ jsx(Controls, { showInteractive: false }),
|
|
664
|
+
/* @__PURE__ */ jsx(MiniMap, { pannable: true, zoomable: true })
|
|
665
|
+
]
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
selected && /* @__PURE__ */ jsxs(
|
|
671
|
+
"section",
|
|
672
|
+
{
|
|
673
|
+
className: "rounded-md border border-border bg-card p-3 text-xs",
|
|
674
|
+
"aria-label": tr(
|
|
675
|
+
"fluidgrids.history.versionDiff.nodeDetails",
|
|
676
|
+
"Node details"
|
|
677
|
+
),
|
|
678
|
+
"data-testid": "fluidgrids-version-diff-node-details",
|
|
679
|
+
children: [
|
|
680
|
+
/* @__PURE__ */ jsxs("p", { className: "font-medium text-foreground", children: [
|
|
681
|
+
selected.label,
|
|
682
|
+
" ",
|
|
683
|
+
/* @__PURE__ */ jsxs("span", { className: DIFF_STATUS_CLASSES[selected.status].text, children: [
|
|
684
|
+
"\xB7 ",
|
|
685
|
+
diffStatusLabel(tr, selected.status)
|
|
686
|
+
] })
|
|
687
|
+
] }),
|
|
688
|
+
/* @__PURE__ */ jsx("p", { className: "font-mono text-muted-foreground", children: selected.id }),
|
|
689
|
+
selected.moved && /* @__PURE__ */ jsx("p", { className: "mt-1 text-muted-foreground", children: tr(
|
|
690
|
+
"fluidgrids.history.versionDiff.moved",
|
|
691
|
+
"Moved on the canvas (layout only)."
|
|
692
|
+
) }),
|
|
693
|
+
selected.changedFields.length > 0 && /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-0.5", children: selected.changedFields.map((field) => /* @__PURE__ */ jsx("li", { className: "font-mono text-foreground", children: field }, field)) })
|
|
694
|
+
]
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
] });
|
|
698
|
+
}
|
|
699
|
+
function VersionDiffGraph(props) {
|
|
700
|
+
return /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(VersionDiffGraphInner, { ...props }) });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/modules/history/utils/json-line-diff.ts
|
|
704
|
+
var DEFAULT_MAX_CELLS = 4e6;
|
|
705
|
+
function sortKeys(value) {
|
|
706
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
707
|
+
if (typeof value === "object" && value !== null) {
|
|
708
|
+
const out = {};
|
|
709
|
+
for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b))) {
|
|
710
|
+
out[key] = sortKeys(value[key]);
|
|
711
|
+
}
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
716
|
+
function canonicalJson(value) {
|
|
717
|
+
let parsed = value;
|
|
718
|
+
if (typeof parsed === "string") {
|
|
719
|
+
try {
|
|
720
|
+
parsed = JSON.parse(parsed);
|
|
721
|
+
} catch {
|
|
722
|
+
return parsed;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (parsed === void 0) return "";
|
|
726
|
+
return JSON.stringify(sortKeys(parsed), null, 2) ?? "";
|
|
727
|
+
}
|
|
728
|
+
function splitLines(text2) {
|
|
729
|
+
return text2 === "" ? [] : text2.split("\n");
|
|
730
|
+
}
|
|
731
|
+
function diffJsonLines(before, after, maxCells = DEFAULT_MAX_CELLS) {
|
|
732
|
+
const a = splitLines(canonicalJson(before));
|
|
733
|
+
const b = splitLines(canonicalJson(after));
|
|
734
|
+
let head = 0;
|
|
735
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head += 1;
|
|
736
|
+
let tail = 0;
|
|
737
|
+
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail])
|
|
738
|
+
tail += 1;
|
|
739
|
+
const lines = [];
|
|
740
|
+
for (let i = 0; i < head; i += 1) {
|
|
741
|
+
lines.push({ kind: "same", text: a[i], fromLine: i + 1, toLine: i + 1 });
|
|
742
|
+
}
|
|
743
|
+
const midA = a.slice(head, a.length - tail);
|
|
744
|
+
const midB = b.slice(head, b.length - tail);
|
|
745
|
+
let approximate = false;
|
|
746
|
+
let added = 0;
|
|
747
|
+
let removed = 0;
|
|
748
|
+
const pushRemoved = (index) => {
|
|
749
|
+
lines.push({
|
|
750
|
+
kind: "removed",
|
|
751
|
+
text: midA[index],
|
|
752
|
+
fromLine: head + index + 1,
|
|
753
|
+
toLine: null
|
|
754
|
+
});
|
|
755
|
+
removed += 1;
|
|
756
|
+
};
|
|
757
|
+
const pushAdded = (index) => {
|
|
758
|
+
lines.push({
|
|
759
|
+
kind: "added",
|
|
760
|
+
text: midB[index],
|
|
761
|
+
fromLine: null,
|
|
762
|
+
toLine: head + index + 1
|
|
763
|
+
});
|
|
764
|
+
added += 1;
|
|
765
|
+
};
|
|
766
|
+
if ((midA.length + 1) * (midB.length + 1) > maxCells) {
|
|
767
|
+
approximate = true;
|
|
768
|
+
for (let i = 0; i < midA.length; i += 1) pushRemoved(i);
|
|
769
|
+
for (let j = 0; j < midB.length; j += 1) pushAdded(j);
|
|
770
|
+
} else {
|
|
771
|
+
const width = midB.length + 1;
|
|
772
|
+
const lcs = new Uint32Array((midA.length + 1) * width);
|
|
773
|
+
for (let i2 = midA.length - 1; i2 >= 0; i2 -= 1) {
|
|
774
|
+
for (let j2 = midB.length - 1; j2 >= 0; j2 -= 1) {
|
|
775
|
+
lcs[i2 * width + j2] = midA[i2] === midB[j2] ? lcs[(i2 + 1) * width + j2 + 1] + 1 : Math.max(lcs[(i2 + 1) * width + j2], lcs[i2 * width + j2 + 1]);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
let i = 0;
|
|
779
|
+
let j = 0;
|
|
780
|
+
while (i < midA.length && j < midB.length) {
|
|
781
|
+
if (midA[i] === midB[j]) {
|
|
782
|
+
lines.push({
|
|
783
|
+
kind: "same",
|
|
784
|
+
text: midA[i],
|
|
785
|
+
fromLine: head + i + 1,
|
|
786
|
+
toLine: head + j + 1
|
|
787
|
+
});
|
|
788
|
+
i += 1;
|
|
789
|
+
j += 1;
|
|
790
|
+
} else if (lcs[(i + 1) * width + j] >= lcs[i * width + j + 1]) {
|
|
791
|
+
pushRemoved(i);
|
|
792
|
+
i += 1;
|
|
793
|
+
} else {
|
|
794
|
+
pushAdded(j);
|
|
795
|
+
j += 1;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
while (i < midA.length) pushRemoved(i++);
|
|
799
|
+
while (j < midB.length) pushAdded(j++);
|
|
800
|
+
}
|
|
801
|
+
for (let k = 0; k < tail; k += 1) {
|
|
802
|
+
const fromIndex = a.length - tail + k;
|
|
803
|
+
const toIndex = b.length - tail + k;
|
|
804
|
+
lines.push({
|
|
805
|
+
kind: "same",
|
|
806
|
+
text: a[fromIndex],
|
|
807
|
+
fromLine: fromIndex + 1,
|
|
808
|
+
toLine: toIndex + 1
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return { lines, added, removed, approximate };
|
|
812
|
+
}
|
|
813
|
+
function toHunks(lines, context = 3) {
|
|
814
|
+
const keep = new Array(lines.length).fill(false);
|
|
815
|
+
lines.forEach((line, index) => {
|
|
816
|
+
if (line.kind === "same") return;
|
|
817
|
+
const start = Math.max(0, index - context);
|
|
818
|
+
const end = Math.min(lines.length - 1, index + context);
|
|
819
|
+
for (let k = start; k <= end; k += 1) keep[k] = true;
|
|
820
|
+
});
|
|
821
|
+
const hunks = [];
|
|
822
|
+
let skipped = 0;
|
|
823
|
+
let current = null;
|
|
824
|
+
lines.forEach((line, index) => {
|
|
825
|
+
if (!keep[index]) {
|
|
826
|
+
skipped += 1;
|
|
827
|
+
current = null;
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (!current) {
|
|
831
|
+
current = { skippedBefore: skipped, lines: [] };
|
|
832
|
+
hunks.push(current);
|
|
833
|
+
skipped = 0;
|
|
834
|
+
}
|
|
835
|
+
current.lines.push(line);
|
|
836
|
+
});
|
|
837
|
+
return { hunks, skippedAfter: skipped };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/modules/history/components/diff-format.ts
|
|
841
|
+
var VERSION_TIME_FORMAT = new Intl.DateTimeFormat(void 0, {
|
|
842
|
+
year: "numeric",
|
|
843
|
+
month: "short",
|
|
844
|
+
day: "numeric",
|
|
845
|
+
hour: "2-digit",
|
|
846
|
+
minute: "2-digit"
|
|
847
|
+
});
|
|
848
|
+
function formatVersionTime(value) {
|
|
849
|
+
if (!value) return "";
|
|
850
|
+
const date = new Date(value);
|
|
851
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
852
|
+
return VERSION_TIME_FORMAT.format(date);
|
|
853
|
+
}
|
|
854
|
+
function formatDiffValue(value) {
|
|
855
|
+
if (value === void 0 || value === null) return "\u2014";
|
|
856
|
+
try {
|
|
857
|
+
return JSON.stringify(value);
|
|
858
|
+
} catch {
|
|
859
|
+
return String(value);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function lineKey(line) {
|
|
863
|
+
return `${line.fromLine ?? "-"}:${line.toLine ?? "-"}`;
|
|
864
|
+
}
|
|
865
|
+
function hunkKey(hunk) {
|
|
866
|
+
const first = hunk.lines[0];
|
|
867
|
+
return first ? lineKey(first) : `skip-${hunk.skippedBefore}`;
|
|
868
|
+
}
|
|
869
|
+
function kindStatus(kind) {
|
|
870
|
+
if (kind === "ADDED") return "added";
|
|
871
|
+
if (kind === "REMOVED") return "removed";
|
|
872
|
+
return "modified";
|
|
873
|
+
}
|
|
874
|
+
function VersionChangeList({
|
|
875
|
+
diff,
|
|
876
|
+
error
|
|
877
|
+
}) {
|
|
878
|
+
const tr = useTr();
|
|
879
|
+
if (!diff) {
|
|
880
|
+
return /* @__PURE__ */ jsx(
|
|
881
|
+
"p",
|
|
882
|
+
{
|
|
883
|
+
role: "alert",
|
|
884
|
+
className: "rounded-md border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg)] p-3 text-xs text-[var(--color-status-error-text)]",
|
|
885
|
+
children: tr(
|
|
886
|
+
"fluidgrids.history.versionDiff.changeListError",
|
|
887
|
+
"The change list could not be loaded: {{error}}",
|
|
888
|
+
{ error: error ?? "" }
|
|
889
|
+
)
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
if (diff.entries.length === 0) {
|
|
894
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: tr(
|
|
895
|
+
"fluidgrids.history.versionDiff.identical",
|
|
896
|
+
"The definitions are identical."
|
|
897
|
+
) });
|
|
898
|
+
}
|
|
899
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
900
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
901
|
+
tr(
|
|
902
|
+
"fluidgrids.history.versionDiff.changeCount",
|
|
903
|
+
"{{count}} change(s)",
|
|
904
|
+
{ count: diff.entries.length }
|
|
905
|
+
),
|
|
906
|
+
diff.truncated && ` \xB7 ${tr(
|
|
907
|
+
"fluidgrids.history.versionDiff.truncated",
|
|
908
|
+
"the list is truncated; the raw JSON view shows everything"
|
|
909
|
+
)}`
|
|
910
|
+
] }),
|
|
911
|
+
/* @__PURE__ */ jsx(
|
|
912
|
+
"ul",
|
|
913
|
+
{
|
|
914
|
+
className: "max-h-[420px] space-y-1 overflow-y-auto",
|
|
915
|
+
"data-testid": "fluidgrids-version-diff-changes",
|
|
916
|
+
children: diff.entries.map((entry) => {
|
|
917
|
+
const status = kindStatus(entry.kind);
|
|
918
|
+
return /* @__PURE__ */ jsxs(
|
|
919
|
+
"li",
|
|
920
|
+
{
|
|
921
|
+
className: "rounded border border-border p-2 text-xs",
|
|
922
|
+
"data-diff-kind": entry.kind,
|
|
923
|
+
children: [
|
|
924
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
|
|
925
|
+
/* @__PURE__ */ jsx(
|
|
926
|
+
Badge,
|
|
927
|
+
{
|
|
928
|
+
variant: "outline",
|
|
929
|
+
className: cn(
|
|
930
|
+
"shrink-0 text-[10px]",
|
|
931
|
+
DIFF_STATUS_CLASSES[status].chip
|
|
932
|
+
),
|
|
933
|
+
children: entry.kind
|
|
934
|
+
}
|
|
935
|
+
),
|
|
936
|
+
/* @__PURE__ */ jsx("code", { className: "min-w-0 break-all font-mono", children: entry.path })
|
|
937
|
+
] }),
|
|
938
|
+
entry.kind !== "ADDED" && /* @__PURE__ */ jsxs("p", { className: "mt-1 break-all font-mono text-[11px] text-[var(--color-status-error-text)]", children: [
|
|
939
|
+
"\u2212 ",
|
|
940
|
+
formatDiffValue(entry.before)
|
|
941
|
+
] }),
|
|
942
|
+
entry.kind !== "REMOVED" && /* @__PURE__ */ jsxs("p", { className: "mt-1 break-all font-mono text-[11px] text-[var(--color-status-success-text)]", children: [
|
|
943
|
+
"+ ",
|
|
944
|
+
formatDiffValue(entry.after)
|
|
945
|
+
] })
|
|
946
|
+
]
|
|
947
|
+
},
|
|
948
|
+
`${entry.kind}-${entry.path}`
|
|
949
|
+
);
|
|
950
|
+
})
|
|
951
|
+
}
|
|
952
|
+
)
|
|
953
|
+
] });
|
|
954
|
+
}
|
|
955
|
+
function lineClass(line) {
|
|
956
|
+
if (line.kind === "added")
|
|
957
|
+
return "bg-[var(--color-status-success-bg)] text-[var(--color-status-success-text)]";
|
|
958
|
+
if (line.kind === "removed")
|
|
959
|
+
return "bg-[var(--color-status-error-bg)] text-[var(--color-status-error-text)]";
|
|
960
|
+
return "text-foreground";
|
|
961
|
+
}
|
|
962
|
+
function JsonLineDiffView({
|
|
963
|
+
before,
|
|
964
|
+
after,
|
|
965
|
+
fromVersion,
|
|
966
|
+
toVersion
|
|
967
|
+
}) {
|
|
968
|
+
const tr = useTr();
|
|
969
|
+
const diff = useMemo(() => diffJsonLines(before, after), [before, after]);
|
|
970
|
+
const [full, setFull] = useState(false);
|
|
971
|
+
const { hunks, skippedAfter } = useMemo(
|
|
972
|
+
() => full ? { hunks: [{ skippedBefore: 0, lines: diff.lines }], skippedAfter: 0 } : toHunks(diff.lines),
|
|
973
|
+
[diff, full]
|
|
974
|
+
);
|
|
975
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
976
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground", children: [
|
|
977
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
978
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[var(--color-status-error-text)]", children: [
|
|
979
|
+
"\u2212 ",
|
|
980
|
+
fromVersion
|
|
981
|
+
] }),
|
|
982
|
+
" ",
|
|
983
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[var(--color-status-success-text)]", children: [
|
|
984
|
+
"+ ",
|
|
985
|
+
toVersion
|
|
986
|
+
] }),
|
|
987
|
+
" ",
|
|
988
|
+
"\xB7",
|
|
989
|
+
" ",
|
|
990
|
+
tr(
|
|
991
|
+
"fluidgrids.history.versionDiff.lineCounts",
|
|
992
|
+
"{{added}} line(s) added, {{removed}} removed",
|
|
993
|
+
{ added: diff.added, removed: diff.removed }
|
|
994
|
+
),
|
|
995
|
+
diff.approximate && ` \xB7 ${tr(
|
|
996
|
+
"fluidgrids.history.versionDiff.approximate",
|
|
997
|
+
"too large for a line-by-line match; the changed block is shown whole"
|
|
998
|
+
)}`
|
|
999
|
+
] }),
|
|
1000
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setFull((v) => !v), children: full ? tr(
|
|
1001
|
+
"fluidgrids.history.versionDiff.showChangesOnly",
|
|
1002
|
+
"Changes only"
|
|
1003
|
+
) : tr("fluidgrids.history.versionDiff.showFullJson", "Full JSON") })
|
|
1004
|
+
] }),
|
|
1005
|
+
diff.added + diff.removed === 0 && !full ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: tr(
|
|
1006
|
+
"fluidgrids.history.versionDiff.identical",
|
|
1007
|
+
"The definitions are identical."
|
|
1008
|
+
) }) : /* @__PURE__ */ jsxs(
|
|
1009
|
+
"div",
|
|
1010
|
+
{
|
|
1011
|
+
className: "max-h-[480px] overflow-auto rounded-md border border-border bg-background font-mono text-[11px] leading-5",
|
|
1012
|
+
"data-testid": "fluidgrids-version-diff-json",
|
|
1013
|
+
children: [
|
|
1014
|
+
hunks.map((hunk) => /* @__PURE__ */ jsxs("div", { children: [
|
|
1015
|
+
hunk.skippedBefore > 0 && /* @__PURE__ */ jsx("div", { className: "border-y border-border bg-muted px-2 text-muted-foreground", children: tr(
|
|
1016
|
+
"fluidgrids.history.versionDiff.unchangedLines",
|
|
1017
|
+
"\u2026 {{count}} unchanged line(s)",
|
|
1018
|
+
{ count: hunk.skippedBefore }
|
|
1019
|
+
) }),
|
|
1020
|
+
hunk.lines.map((line) => /* @__PURE__ */ jsxs(
|
|
1021
|
+
"div",
|
|
1022
|
+
{
|
|
1023
|
+
className: cn("flex whitespace-pre", lineClass(line)),
|
|
1024
|
+
"data-line-kind": line.kind,
|
|
1025
|
+
children: [
|
|
1026
|
+
/* @__PURE__ */ jsx("span", { className: "w-10 shrink-0 select-none pr-1 text-right text-muted-foreground", children: line.fromLine ?? "" }),
|
|
1027
|
+
/* @__PURE__ */ jsx("span", { className: "w-10 shrink-0 select-none pr-1 text-right text-muted-foreground", children: line.toLine ?? "" }),
|
|
1028
|
+
/* @__PURE__ */ jsx("span", { className: "w-4 shrink-0 select-none text-center", children: line.kind === "added" ? "+" : line.kind === "removed" ? "\u2212" : " " }),
|
|
1029
|
+
/* @__PURE__ */ jsx("span", { className: "pr-2", children: line.text })
|
|
1030
|
+
]
|
|
1031
|
+
},
|
|
1032
|
+
lineKey(line)
|
|
1033
|
+
))
|
|
1034
|
+
] }, hunkKey(hunk))),
|
|
1035
|
+
skippedAfter > 0 && /* @__PURE__ */ jsx("div", { className: "border-t border-border bg-muted px-2 text-muted-foreground", children: tr(
|
|
1036
|
+
"fluidgrids.history.versionDiff.unchangedLines",
|
|
1037
|
+
"\u2026 {{count}} unchanged line(s)",
|
|
1038
|
+
{ count: skippedAfter }
|
|
1039
|
+
) })
|
|
1040
|
+
]
|
|
1041
|
+
}
|
|
1042
|
+
)
|
|
1043
|
+
] });
|
|
1044
|
+
}
|
|
1045
|
+
function VersionLifecycleBar({
|
|
1046
|
+
status,
|
|
1047
|
+
counts,
|
|
1048
|
+
className
|
|
1049
|
+
}) {
|
|
1050
|
+
const tr = useTr();
|
|
1051
|
+
const steps = lifecycleSteps(status);
|
|
1052
|
+
return /* @__PURE__ */ jsx(
|
|
1053
|
+
"ol",
|
|
1054
|
+
{
|
|
1055
|
+
className: cn("flex w-full items-stretch gap-1", className),
|
|
1056
|
+
"aria-label": tr(
|
|
1057
|
+
"fluidgrids.history.lifecycle.ariaLabel",
|
|
1058
|
+
"Version lifecycle"
|
|
1059
|
+
),
|
|
1060
|
+
"data-testid": "fluidgrids-version-lifecycle",
|
|
1061
|
+
children: steps.map(({ stage, state }) => /* @__PURE__ */ jsxs(
|
|
1062
|
+
"li",
|
|
1063
|
+
{
|
|
1064
|
+
"aria-current": state === "current" ? "step" : void 0,
|
|
1065
|
+
"data-stage": stage,
|
|
1066
|
+
"data-state": state,
|
|
1067
|
+
className: cn(
|
|
1068
|
+
"flex min-w-0 flex-1 flex-col items-center rounded-md border px-1.5 py-1 text-center text-[11px] leading-tight",
|
|
1069
|
+
state === "current" && "border-primary bg-primary/10 font-semibold text-primary",
|
|
1070
|
+
state === "done" && "border-border bg-muted text-muted-foreground",
|
|
1071
|
+
state === "upcoming" && "border-dashed border-border text-muted-foreground"
|
|
1072
|
+
),
|
|
1073
|
+
children: [
|
|
1074
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: lifecycleStageLabel(tr, stage) }),
|
|
1075
|
+
counts && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-normal tabular-nums", children: counts[stage] })
|
|
1076
|
+
]
|
|
1077
|
+
},
|
|
1078
|
+
stage
|
|
1079
|
+
))
|
|
1080
|
+
}
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
function statusVariant(status) {
|
|
1084
|
+
switch (lifecycleStageOf(status)) {
|
|
1085
|
+
case "ACTIVE":
|
|
1086
|
+
return "default";
|
|
1087
|
+
case "DEPRECATED":
|
|
1088
|
+
return "destructive";
|
|
1089
|
+
case "ARCHIVED":
|
|
1090
|
+
return "outline";
|
|
1091
|
+
default:
|
|
1092
|
+
return "secondary";
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function VersionSide({
|
|
1096
|
+
tr,
|
|
1097
|
+
side,
|
|
1098
|
+
version,
|
|
1099
|
+
counts,
|
|
1100
|
+
authorName,
|
|
1101
|
+
currentVersion,
|
|
1102
|
+
busy,
|
|
1103
|
+
onRestore,
|
|
1104
|
+
onOpenVersion
|
|
1105
|
+
}) {
|
|
1106
|
+
if (!version) return null;
|
|
1107
|
+
const author = authorName?.(version.createdById) ?? null;
|
|
1108
|
+
const when = formatVersionTime(version.updatedAt ?? version.createdAt);
|
|
1109
|
+
const isCurrent = currentVersion === version.version;
|
|
1110
|
+
return /* @__PURE__ */ jsxs(
|
|
1111
|
+
"section",
|
|
1112
|
+
{
|
|
1113
|
+
className: "flex min-w-0 flex-col gap-2 rounded-lg border border-border bg-card p-3",
|
|
1114
|
+
"data-testid": `fluidgrids-version-diff-${side}`,
|
|
1115
|
+
"aria-label": (
|
|
1116
|
+
// The pair can be swapped, so "from" is not always the older one:
|
|
1117
|
+
// name the side by its role in the comparison, not by its age.
|
|
1118
|
+
side === "from" ? tr("fluidgrids.history.versionDiff.fromSideAria", "Compared from") : tr("fluidgrids.history.versionDiff.toSideAria", "Compared to")
|
|
1119
|
+
),
|
|
1120
|
+
children: [
|
|
1121
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1122
|
+
/* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 text-xs text-foreground", children: version.version }),
|
|
1123
|
+
/* @__PURE__ */ jsx(Badge, { variant: statusVariant(version.status), className: "text-[10px]", children: lifecycleStageLabel(tr, lifecycleStageOf(version.status)) }),
|
|
1124
|
+
isCurrent && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-[10px]", children: tr(
|
|
1125
|
+
"fluidgrids.history.versionDiff.currentlyOpen",
|
|
1126
|
+
"Currently open"
|
|
1127
|
+
) })
|
|
1128
|
+
] }),
|
|
1129
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
1130
|
+
author ? `${author} \xB7 ` : "",
|
|
1131
|
+
when
|
|
1132
|
+
] }),
|
|
1133
|
+
/* @__PURE__ */ jsx(VersionLifecycleBar, { status: version.status, counts }),
|
|
1134
|
+
(onRestore || onOpenVersion && !isCurrent) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
1135
|
+
onOpenVersion && !isCurrent && /* @__PURE__ */ jsxs(
|
|
1136
|
+
Button,
|
|
1137
|
+
{
|
|
1138
|
+
size: "sm",
|
|
1139
|
+
variant: "outline",
|
|
1140
|
+
disabled: busy,
|
|
1141
|
+
onClick: () => onOpenVersion(version.version),
|
|
1142
|
+
children: [
|
|
1143
|
+
/* @__PURE__ */ jsx(Eye, { className: "mr-1.5 size-3.5" }),
|
|
1144
|
+
tr("fluidgrids.history.versionDiff.view", "View in builder")
|
|
1145
|
+
]
|
|
1146
|
+
}
|
|
1147
|
+
),
|
|
1148
|
+
onRestore && !isCurrent && /* @__PURE__ */ jsxs(
|
|
1149
|
+
Button,
|
|
1150
|
+
{
|
|
1151
|
+
size: "sm",
|
|
1152
|
+
variant: "secondary",
|
|
1153
|
+
disabled: busy,
|
|
1154
|
+
onClick: () => onRestore(version.version),
|
|
1155
|
+
"data-testid": `fluidgrids-version-diff-restore-${side}`,
|
|
1156
|
+
children: [
|
|
1157
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: "mr-1.5 size-3.5" }),
|
|
1158
|
+
tr(
|
|
1159
|
+
"fluidgrids.history.versionDiff.restore",
|
|
1160
|
+
"Restore as new draft"
|
|
1161
|
+
)
|
|
1162
|
+
]
|
|
1163
|
+
}
|
|
1164
|
+
)
|
|
1165
|
+
] })
|
|
1166
|
+
]
|
|
1167
|
+
}
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
function VersionSelect({
|
|
1171
|
+
versions,
|
|
1172
|
+
value,
|
|
1173
|
+
onChange,
|
|
1174
|
+
label
|
|
1175
|
+
}) {
|
|
1176
|
+
const tr = useTr();
|
|
1177
|
+
return /* @__PURE__ */ jsx(
|
|
1178
|
+
"select",
|
|
1179
|
+
{
|
|
1180
|
+
value,
|
|
1181
|
+
onChange: (event) => onChange(event.target.value),
|
|
1182
|
+
"aria-label": label,
|
|
1183
|
+
className: "h-8 min-w-0 flex-1 rounded-md border border-border bg-background px-2 text-xs text-foreground",
|
|
1184
|
+
children: versions.map((v) => /* @__PURE__ */ jsxs("option", { value: v.version, children: [
|
|
1185
|
+
v.version,
|
|
1186
|
+
" \xB7 ",
|
|
1187
|
+
lifecycleStageLabel(tr, lifecycleStageOf(v.status))
|
|
1188
|
+
] }, v.id))
|
|
1189
|
+
}
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
function ComparisonTabs({
|
|
1193
|
+
comparison,
|
|
1194
|
+
tab,
|
|
1195
|
+
onTabChange
|
|
1196
|
+
}) {
|
|
1197
|
+
const tr = useTr();
|
|
1198
|
+
return /* @__PURE__ */ jsxs(
|
|
1199
|
+
Tabs,
|
|
1200
|
+
{
|
|
1201
|
+
value: tab,
|
|
1202
|
+
onValueChange: (value) => onTabChange(value === "json" ? "json" : "graph"),
|
|
1203
|
+
children: [
|
|
1204
|
+
/* @__PURE__ */ jsxs(TabsList, { children: [
|
|
1205
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "graph", children: tr("fluidgrids.history.versionDiff.graphTab", "Graph") }),
|
|
1206
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "json", children: tr("fluidgrids.history.versionDiff.jsonTab", "JSON") })
|
|
1207
|
+
] }),
|
|
1208
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "graph", className: "mt-3", children: [
|
|
1209
|
+
comparison.graph.identical && /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs text-muted-foreground", children: tr(
|
|
1210
|
+
"fluidgrids.history.versionDiff.graphIdentical",
|
|
1211
|
+
"No node or connection changed between these versions."
|
|
1212
|
+
) }),
|
|
1213
|
+
/* @__PURE__ */ jsx(VersionDiffGraph, { graph: comparison.graph })
|
|
1214
|
+
] }),
|
|
1215
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "json", className: "mt-3 space-y-4", children: [
|
|
1216
|
+
/* @__PURE__ */ jsxs("section", { children: [
|
|
1217
|
+
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-semibold text-foreground", children: tr("fluidgrids.history.versionDiff.changesHeading", "Changes") }),
|
|
1218
|
+
/* @__PURE__ */ jsx(
|
|
1219
|
+
VersionChangeList,
|
|
1220
|
+
{
|
|
1221
|
+
diff: comparison.serverDiff,
|
|
1222
|
+
error: comparison.serverDiffError
|
|
1223
|
+
}
|
|
1224
|
+
)
|
|
1225
|
+
] }),
|
|
1226
|
+
/* @__PURE__ */ jsxs("section", { children: [
|
|
1227
|
+
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-semibold text-foreground", children: tr("fluidgrids.history.versionDiff.rawHeading", "Definition JSON") }),
|
|
1228
|
+
/* @__PURE__ */ jsx(
|
|
1229
|
+
JsonLineDiffView,
|
|
1230
|
+
{
|
|
1231
|
+
before: comparison.from.definitionJson,
|
|
1232
|
+
after: comparison.to.definitionJson,
|
|
1233
|
+
fromVersion: comparison.fromVersion,
|
|
1234
|
+
toVersion: comparison.toVersion
|
|
1235
|
+
}
|
|
1236
|
+
)
|
|
1237
|
+
] })
|
|
1238
|
+
] })
|
|
1239
|
+
]
|
|
1240
|
+
}
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
function ComparisonBody({
|
|
1244
|
+
samePair,
|
|
1245
|
+
loading,
|
|
1246
|
+
error,
|
|
1247
|
+
comparison,
|
|
1248
|
+
onRetry,
|
|
1249
|
+
tab,
|
|
1250
|
+
onTabChange
|
|
1251
|
+
}) {
|
|
1252
|
+
const tr = useTr();
|
|
1253
|
+
if (samePair) {
|
|
1254
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: tr(
|
|
1255
|
+
"fluidgrids.history.versionDiff.sameVersion",
|
|
1256
|
+
"Pick two different versions to compare."
|
|
1257
|
+
) });
|
|
1258
|
+
}
|
|
1259
|
+
if (loading && !comparison) {
|
|
1260
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground", children: [
|
|
1261
|
+
/* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }),
|
|
1262
|
+
tr("fluidgrids.history.versionDiff.loading", "Comparing versions\u2026")
|
|
1263
|
+
] });
|
|
1264
|
+
}
|
|
1265
|
+
if (error) {
|
|
1266
|
+
return /* @__PURE__ */ jsxs(
|
|
1267
|
+
"div",
|
|
1268
|
+
{
|
|
1269
|
+
role: "alert",
|
|
1270
|
+
className: "flex flex-wrap items-center justify-between gap-2 rounded-md border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg)] p-3 text-sm text-[var(--color-status-error-text)]",
|
|
1271
|
+
children: [
|
|
1272
|
+
/* @__PURE__ */ jsx("span", { children: error }),
|
|
1273
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: onRetry, children: tr("fluidgrids.history.versionDiff.retry", "Retry") })
|
|
1274
|
+
]
|
|
1275
|
+
}
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
return comparison ? /* @__PURE__ */ jsx(
|
|
1279
|
+
ComparisonTabs,
|
|
1280
|
+
{
|
|
1281
|
+
comparison,
|
|
1282
|
+
tab,
|
|
1283
|
+
onTabChange
|
|
1284
|
+
}
|
|
1285
|
+
) : null;
|
|
1286
|
+
}
|
|
1287
|
+
function RestoreConfirmDialog({
|
|
1288
|
+
target,
|
|
1289
|
+
restoring,
|
|
1290
|
+
className,
|
|
1291
|
+
onCancel,
|
|
1292
|
+
onConfirm
|
|
1293
|
+
}) {
|
|
1294
|
+
const tr = useTr();
|
|
1295
|
+
return /* @__PURE__ */ jsx(
|
|
1296
|
+
AlertDialog,
|
|
1297
|
+
{
|
|
1298
|
+
open: target !== null,
|
|
1299
|
+
onOpenChange: (next) => !next && !restoring && onCancel(),
|
|
1300
|
+
children: /* @__PURE__ */ jsxs(AlertDialogContent, { className, children: [
|
|
1301
|
+
/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
|
|
1302
|
+
/* @__PURE__ */ jsx(AlertDialogTitle, { children: tr(
|
|
1303
|
+
"fluidgrids.history.versionDiff.restoreTitle",
|
|
1304
|
+
"Restore version {{version}}?",
|
|
1305
|
+
{ version: target ?? "" }
|
|
1306
|
+
) }),
|
|
1307
|
+
/* @__PURE__ */ jsx(AlertDialogDescription, { children: tr(
|
|
1308
|
+
"fluidgrids.history.versionDiff.restoreBody",
|
|
1309
|
+
"This creates a new draft version from it. Existing versions, including the active one, are not changed."
|
|
1310
|
+
) })
|
|
1311
|
+
] }),
|
|
1312
|
+
/* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
|
|
1313
|
+
/* @__PURE__ */ jsx(AlertDialogCancel, { disabled: restoring, children: tr("fluidgrids.history.versionDiff.cancel", "Cancel") }),
|
|
1314
|
+
/* @__PURE__ */ jsxs(
|
|
1315
|
+
AlertDialogAction,
|
|
1316
|
+
{
|
|
1317
|
+
disabled: restoring,
|
|
1318
|
+
onClick: (event) => {
|
|
1319
|
+
event.preventDefault();
|
|
1320
|
+
onConfirm();
|
|
1321
|
+
},
|
|
1322
|
+
children: [
|
|
1323
|
+
restoring && /* @__PURE__ */ jsx(Loader2, { className: "mr-2 size-3.5 animate-spin" }),
|
|
1324
|
+
tr("fluidgrids.history.versionDiff.restoreConfirm", "Restore")
|
|
1325
|
+
]
|
|
1326
|
+
}
|
|
1327
|
+
)
|
|
1328
|
+
] })
|
|
1329
|
+
] })
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
function VersionDiffDialog({
|
|
1334
|
+
open,
|
|
1335
|
+
onOpenChange,
|
|
1336
|
+
workflowKey,
|
|
1337
|
+
workflowName,
|
|
1338
|
+
versions,
|
|
1339
|
+
initialFrom,
|
|
1340
|
+
initialTo,
|
|
1341
|
+
currentVersion,
|
|
1342
|
+
authorName,
|
|
1343
|
+
onRestore,
|
|
1344
|
+
onOpenVersion,
|
|
1345
|
+
className
|
|
1346
|
+
}) {
|
|
1347
|
+
const tr = useTr();
|
|
1348
|
+
const [from, setFrom] = useState(initialFrom);
|
|
1349
|
+
const [to, setTo] = useState(initialTo);
|
|
1350
|
+
const [tab, setTab] = useState("graph");
|
|
1351
|
+
const [restoreTarget, setRestoreTarget] = useState(null);
|
|
1352
|
+
const [restoring, setRestoring] = useState(false);
|
|
1353
|
+
useEffect(() => {
|
|
1354
|
+
if (!open) return;
|
|
1355
|
+
setFrom(initialFrom);
|
|
1356
|
+
setTo(initialTo);
|
|
1357
|
+
}, [open, initialFrom, initialTo]);
|
|
1358
|
+
const { comparison, loading, error, reload } = useVersionComparison(
|
|
1359
|
+
workflowKey,
|
|
1360
|
+
open ? from : null,
|
|
1361
|
+
open ? to : null
|
|
1362
|
+
);
|
|
1363
|
+
const counts = useMemo(() => countLifecycle(versions), [versions]);
|
|
1364
|
+
const byVersion = useMemo(
|
|
1365
|
+
() => new Map(versions.map((v) => [v.version, v])),
|
|
1366
|
+
[versions]
|
|
1367
|
+
);
|
|
1368
|
+
const confirmRestore = async () => {
|
|
1369
|
+
if (!restoreTarget || !onRestore) return;
|
|
1370
|
+
setRestoring(true);
|
|
1371
|
+
try {
|
|
1372
|
+
await onRestore(restoreTarget);
|
|
1373
|
+
setRestoreTarget(null);
|
|
1374
|
+
} finally {
|
|
1375
|
+
setRestoring(false);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1379
|
+
/* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
1380
|
+
DialogContent,
|
|
1381
|
+
{
|
|
1382
|
+
className: cn(
|
|
1383
|
+
"max-h-[92vh] w-[calc(100vw-2rem)] overflow-y-auto sm:max-w-5xl",
|
|
1384
|
+
className
|
|
1385
|
+
),
|
|
1386
|
+
"data-testid": "fluidgrids-version-diff-dialog",
|
|
1387
|
+
children: [
|
|
1388
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
1389
|
+
/* @__PURE__ */ jsxs(DialogTitle, { children: [
|
|
1390
|
+
tr("fluidgrids.history.versionDiff.title", "Version diff"),
|
|
1391
|
+
workflowName ? ` \xB7 ${workflowName}` : ""
|
|
1392
|
+
] }),
|
|
1393
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: tr(
|
|
1394
|
+
"fluidgrids.history.versionDiff.description",
|
|
1395
|
+
"Compare two versions on the canvas or as JSON. The graph ignores layout-only moves; the JSON view lists every difference."
|
|
1396
|
+
) })
|
|
1397
|
+
] }),
|
|
1398
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1399
|
+
/* @__PURE__ */ jsx(
|
|
1400
|
+
VersionSelect,
|
|
1401
|
+
{
|
|
1402
|
+
versions,
|
|
1403
|
+
value: from,
|
|
1404
|
+
onChange: setFrom,
|
|
1405
|
+
label: tr(
|
|
1406
|
+
"fluidgrids.history.versionDiff.fromLabel",
|
|
1407
|
+
"From version"
|
|
1408
|
+
)
|
|
1409
|
+
}
|
|
1410
|
+
),
|
|
1411
|
+
/* @__PURE__ */ jsx(
|
|
1412
|
+
Button,
|
|
1413
|
+
{
|
|
1414
|
+
size: "icon",
|
|
1415
|
+
variant: "ghost",
|
|
1416
|
+
"aria-label": tr(
|
|
1417
|
+
"fluidgrids.history.versionDiff.swap",
|
|
1418
|
+
"Swap versions"
|
|
1419
|
+
),
|
|
1420
|
+
onClick: () => {
|
|
1421
|
+
setFrom(to);
|
|
1422
|
+
setTo(from);
|
|
1423
|
+
},
|
|
1424
|
+
children: /* @__PURE__ */ jsx(ArrowLeftRight, { className: "size-4" })
|
|
1425
|
+
}
|
|
1426
|
+
),
|
|
1427
|
+
/* @__PURE__ */ jsx(
|
|
1428
|
+
VersionSelect,
|
|
1429
|
+
{
|
|
1430
|
+
versions,
|
|
1431
|
+
value: to,
|
|
1432
|
+
onChange: setTo,
|
|
1433
|
+
label: tr("fluidgrids.history.versionDiff.toLabel", "To version")
|
|
1434
|
+
}
|
|
1435
|
+
)
|
|
1436
|
+
] }),
|
|
1437
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: ["from", "to"].map((side) => /* @__PURE__ */ jsx(
|
|
1438
|
+
VersionSide,
|
|
1439
|
+
{
|
|
1440
|
+
tr,
|
|
1441
|
+
side,
|
|
1442
|
+
version: byVersion.get(side === "from" ? from : to),
|
|
1443
|
+
counts,
|
|
1444
|
+
authorName,
|
|
1445
|
+
currentVersion,
|
|
1446
|
+
busy: restoring,
|
|
1447
|
+
onRestore: onRestore ? setRestoreTarget : void 0,
|
|
1448
|
+
onOpenVersion
|
|
1449
|
+
},
|
|
1450
|
+
side
|
|
1451
|
+
)) }),
|
|
1452
|
+
/* @__PURE__ */ jsx(
|
|
1453
|
+
ComparisonBody,
|
|
1454
|
+
{
|
|
1455
|
+
samePair: from === to,
|
|
1456
|
+
loading,
|
|
1457
|
+
error,
|
|
1458
|
+
comparison,
|
|
1459
|
+
onRetry: reload,
|
|
1460
|
+
tab,
|
|
1461
|
+
onTabChange: setTab
|
|
1462
|
+
}
|
|
1463
|
+
)
|
|
1464
|
+
]
|
|
1465
|
+
}
|
|
1466
|
+
) }),
|
|
1467
|
+
/* @__PURE__ */ jsx(
|
|
1468
|
+
RestoreConfirmDialog,
|
|
1469
|
+
{
|
|
1470
|
+
target: restoreTarget,
|
|
1471
|
+
restoring,
|
|
1472
|
+
className,
|
|
1473
|
+
onCancel: () => setRestoreTarget(null),
|
|
1474
|
+
onConfirm: () => void confirmRestore()
|
|
1475
|
+
}
|
|
1476
|
+
)
|
|
1477
|
+
] });
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
export { VersionDiffDialog, VersionLifecycleBar, canvasFromDefinition, countLifecycle, versionsOfWorkflow };
|