x-openapi-flow 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,856 @@
1
+ window.XOpenApiFlowPlugin = function () {
2
+ const h = React.createElement;
3
+
4
+ function toPlain(value) {
5
+ if (!value) return value;
6
+ return value.toJS ? value.toJS() : value;
7
+ }
8
+
9
+ function text(value) {
10
+ if (value === null || value === undefined || value === "") return "-";
11
+ if (Array.isArray(value)) return value.length ? value.join(", ") : "-";
12
+ return String(value);
13
+ }
14
+
15
+ function getPrerequisiteOperationIds(transition) {
16
+ if (!transition || typeof transition !== 'object') return [];
17
+ if (Array.isArray(transition.prerequisite_operation_ids)) {
18
+ return transition.prerequisite_operation_ids.filter(Boolean);
19
+ }
20
+ if (Array.isArray(transition.pre_operation_ids)) {
21
+ return transition.pre_operation_ids.filter(Boolean);
22
+ }
23
+ if (transition.pre_operation_id) {
24
+ return [transition.pre_operation_id];
25
+ }
26
+ return [];
27
+ }
28
+
29
+ function transitionsList(currentState, transitions) {
30
+ if (!Array.isArray(transitions) || transitions.length === 0) {
31
+ return h("div", { style: { opacity: 0.85, fontStyle: "italic" } }, "No transitions (terminal state)");
32
+ }
33
+
34
+ return h(
35
+ "ul",
36
+ { style: { margin: "6px 0 0 18px", padding: 0 } },
37
+ transitions.map((transition, index) =>
38
+ h(
39
+ "li",
40
+ { key: `${currentState}-${index}`, style: { marginBottom: "4px", lineHeight: 1.45 } },
41
+ h("strong", null, text(transition.trigger_type)),
42
+ " → ",
43
+ h("strong", null, text(transition.target_state)),
44
+ transition.condition ? ` — ${text(transition.condition)}` : "",
45
+ transition.next_operation_id ? ` (next: ${text(transition.next_operation_id)})` : ""
46
+ )
47
+ )
48
+ );
49
+ }
50
+
51
+ function miniGraph(currentState, transitions) {
52
+ if (!Array.isArray(transitions) || transitions.length === 0) {
53
+ return [h("div", { key: "terminal", style: { fontFamily: "monospace" } }, `${text(currentState)} [terminal]`)];
54
+ }
55
+
56
+ return transitions.map((transition, index) =>
57
+ h(
58
+ "div",
59
+ { key: `edge-${index}`, style: { fontFamily: "monospace", lineHeight: 1.45 } },
60
+ `${text(currentState)} --> ${text(transition.target_state)} [${text(transition.trigger_type)}]`
61
+ )
62
+ );
63
+ }
64
+
65
+ return {
66
+ wrapComponents: {
67
+ OperationSummary: (Original) => (props) => {
68
+ const operation = props.operation;
69
+ const flow = operation && operation.get && operation.get("x-openapi-flow");
70
+
71
+ if (!flow) {
72
+ return h(Original, props);
73
+ }
74
+
75
+ const flowObject = toPlain(flow) || {};
76
+ const currentState = flowObject.current_state;
77
+ const transitions = Array.isArray(flowObject.transitions) ? flowObject.transitions : [];
78
+ const graphImageUrl = flowObject.graph_image_url || window.XOpenApiFlowGraphImageUrl;
79
+
80
+ const metadataGrid = h(
81
+ "div",
82
+ {
83
+ style: {
84
+ display: "grid",
85
+ gridTemplateColumns: "140px 1fr",
86
+ gap: "4px 10px",
87
+ fontSize: "12px",
88
+ marginTop: "6px",
89
+ },
90
+ },
91
+ h("div", { style: { opacity: 0.85 } }, "version"),
92
+ h("div", null, text(flowObject.version)),
93
+ h("div", { style: { opacity: 0.85 } }, "id"),
94
+ h("div", null, text(flowObject.id)),
95
+ h("div", { style: { opacity: 0.85 } }, "current_state"),
96
+ h("div", null, text(currentState))
97
+ );
98
+
99
+ const graphImageNode = graphImageUrl
100
+ ? h(
101
+ "div",
102
+ { style: { marginTop: "10px" } },
103
+ h("div", { style: { fontWeight: 700, marginBottom: "6px" } }, "Flow graph image"),
104
+ h("img", {
105
+ src: graphImageUrl,
106
+ alt: "x-openapi-flow graph",
107
+ style: {
108
+ width: "100%",
109
+ maxWidth: "560px",
110
+ border: "1px solid rgba(255,255,255,0.3)",
111
+ borderRadius: "6px",
112
+ },
113
+ })
114
+ )
115
+ : null;
116
+
117
+ return h(
118
+ "div",
119
+ null,
120
+ h(Original, props),
121
+ h(
122
+ "div",
123
+ {
124
+ style: {
125
+ marginTop: "8px",
126
+ padding: "10px",
127
+ border: "1px solid rgba(255,255,255,0.28)",
128
+ borderRadius: "8px",
129
+ background: "rgba(0,0,0,0.12)",
130
+ fontSize: "12px",
131
+ },
132
+ },
133
+ h("div", { style: { fontWeight: 700 } }, "x-openapi-flow"),
134
+ metadataGrid,
135
+ h("div", { style: { marginTop: "10px", fontWeight: 700 } }, "Transitions"),
136
+ transitionsList(currentState, transitions),
137
+ h("div", { style: { marginTop: "10px", fontWeight: 700 } }, "Flow graph (operation-level)"),
138
+ h(
139
+ "div",
140
+ {
141
+ style: {
142
+ marginTop: "6px",
143
+ border: "1px dashed rgba(255,255,255,0.32)",
144
+ borderRadius: "6px",
145
+ padding: "8px",
146
+ },
147
+ },
148
+ ...miniGraph(currentState, transitions)
149
+ ),
150
+ graphImageNode
151
+ )
152
+ );
153
+ },
154
+ },
155
+ };
156
+ };
157
+
158
+ (function () {
159
+ const styleId = 'x-openapi-flow-ui-style';
160
+ const FLOW_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
161
+
162
+ function injectStyles() {
163
+ if (document.getElementById(styleId)) return;
164
+
165
+ const style = document.createElement('style');
166
+ style.id = styleId;
167
+ style.textContent = `
168
+ .xof-card { border: 1px solid rgba(127,127,127,0.38); border-radius: 10px; padding: 10px 12px; background: rgba(127,127,127,0.08); margin-top: 8px; }
169
+ .xof-card > summary { list-style: none; }
170
+ .xof-card > summary::-webkit-details-marker { display: none; }
171
+ .xof-title-row { display: flex; align-items: center; justify-content: space-between; cursor: pointer; gap: 12px; margin: 0; }
172
+ .xof-title { font-weight: 700; font-size: 13px; }
173
+ .xof-toggle-hint { font-size: 11px; opacity: 0.75; }
174
+ .xof-card-body { margin-top: 10px; }
175
+ .xof-section-title { font-size: 12px; font-weight: 700; margin: 10px 0 6px; }
176
+ .xof-meta { display: grid; grid-template-columns: 130px 1fr; gap: 6px 10px; font-size: 12px; margin-bottom: 8px; }
177
+ .xof-meta-label { opacity: 0.85; }
178
+ .xof-list { margin: 0; padding-left: 18px; }
179
+ .xof-list li { margin: 6px 0; line-height: 1.45; }
180
+ .xof-next-link { margin-left: 8px; border: 0; background: none; color: inherit; padding: 0; font-size: 11px; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
181
+ .xof-pre-links { margin-left: 8px; }
182
+ .xof-pre-link { margin-left: 4px; border: 0; background: none; color: inherit; padding: 0; font-size: 11px; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
183
+ .xof-next-link:hover { opacity: 0.95; text-decoration-thickness: 2px; }
184
+ .xof-pre-link:hover { opacity: 0.95; text-decoration-thickness: 2px; }
185
+ .xof-next-link:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 3px; background: rgba(127,127,127,0.14); }
186
+ .xof-pre-link:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 3px; background: rgba(127,127,127,0.14); }
187
+ .xof-graph { margin-top: 10px; padding: 8px; border: 1px dashed rgba(127,127,127,0.42); border-radius: 8px; }
188
+ .xof-graph-title { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
189
+ .xof-edge { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; }
190
+ .xof-empty { opacity: 0.85; font-style: italic; }
191
+ .xof-overview { margin: 12px 0 0; }
192
+ .xof-overview-details > summary { list-style: none; }
193
+ .xof-overview-details > summary::-webkit-details-marker { display: none; }
194
+ .xof-overview-details .xof-overview-toggle::after { content: 'expand'; }
195
+ .xof-overview-details[open] .xof-overview-toggle::after { content: 'collapse'; }
196
+ .xof-overview-sub { font-size: 12px; opacity: 0.82; margin-bottom: 8px; }
197
+ .xof-overview-graph-wrap {
198
+ display: flex;
199
+ justify-content: center;
200
+ align-items: flex-start;
201
+ margin-top: 4px;
202
+ max-height: 320px;
203
+ overflow: auto;
204
+ border: 1px solid rgba(127,127,127,0.3);
205
+ border-radius: 8px;
206
+ background: rgba(255,255,255,0.96);
207
+ padding: 8px;
208
+ }
209
+ .xof-overview img { width: auto; max-width: 100%; height: auto; border-radius: 4px; background: transparent; }
210
+ .xof-overview-code {
211
+ margin-top: 8px;
212
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
213
+ font-size: 10px;
214
+ opacity: 0.9;
215
+ white-space: pre-wrap;
216
+ max-height: 180px;
217
+ overflow: auto;
218
+ }
219
+ @keyframes xof-target-pulse {
220
+ 0% { box-shadow: 0 0 0 0 rgba(127,127,127,0.5); }
221
+ 100% { box-shadow: 0 0 0 10px rgba(127,127,127,0); }
222
+ }
223
+ .xof-jump-target {
224
+ animation: xof-target-pulse 0.9s ease-out 1;
225
+ }
226
+ .xof-jump-feedback {
227
+ position: fixed;
228
+ right: 16px;
229
+ bottom: 16px;
230
+ z-index: 9999;
231
+ max-width: 360px;
232
+ border: 1px solid rgba(127,127,127,0.5);
233
+ border-radius: 8px;
234
+ background: rgba(20,20,20,0.92);
235
+ color: #fff;
236
+ padding: 8px 10px;
237
+ font-size: 12px;
238
+ line-height: 1.35;
239
+ box-shadow: 0 6px 18px rgba(0,0,0,0.25);
240
+ }
241
+ `;
242
+
243
+ document.head.appendChild(style);
244
+ }
245
+
246
+ function text(value) {
247
+ if (value === null || value === undefined || value === '') return '-';
248
+ if (Array.isArray(value)) return value.length ? value.join(', ') : '-';
249
+ return String(value);
250
+ }
251
+
252
+ function getPrerequisiteOperationIds(transition) {
253
+ if (!transition || typeof transition !== 'object') return [];
254
+ if (Array.isArray(transition.prerequisite_operation_ids)) {
255
+ return transition.prerequisite_operation_ids.filter(Boolean);
256
+ }
257
+ if (Array.isArray(transition.pre_operation_ids)) {
258
+ return transition.pre_operation_ids.filter(Boolean);
259
+ }
260
+ if (transition.pre_operation_id) {
261
+ return [transition.pre_operation_id];
262
+ }
263
+ return [];
264
+ }
265
+
266
+ function escapeHtml(value) {
267
+ return text(value)
268
+ .replace(/&/g, '&')
269
+ .replace(/</g, '&lt;')
270
+ .replace(/>/g, '&gt;')
271
+ .replace(/"/g, '&quot;')
272
+ .replace(/'/g, '&#39;');
273
+ }
274
+
275
+ function renderTransitions(currentState, transitions) {
276
+ if (!Array.isArray(transitions) || transitions.length === 0) {
277
+ return '<div class="xof-empty">No transitions (terminal state)</div>';
278
+ }
279
+
280
+ return `<ul class="xof-list">${transitions
281
+ .map((transition) => {
282
+ const condition = transition.condition ? ` — ${escapeHtml(transition.condition)}` : '';
283
+ const nextOperation = transition.next_operation_id
284
+ ? ` <button class="xof-next-link" data-xof-jump="${escapeHtml(transition.next_operation_id)}" type="button" title="Go to operation ${escapeHtml(transition.next_operation_id)}" aria-label="Go to operation ${escapeHtml(transition.next_operation_id)}">next: ${escapeHtml(transition.next_operation_id)}</button>`
285
+ : '';
286
+ const preOperations = getPrerequisiteOperationIds(transition);
287
+ const preOperationLinks = preOperations.length
288
+ ? `<span class="xof-pre-links">requires:${preOperations
289
+ .map(
290
+ (operationId) =>
291
+ ` <button class="xof-pre-link" data-xof-jump="${escapeHtml(operationId)}" type="button" title="Go to operation ${escapeHtml(operationId)}" aria-label="Go to operation ${escapeHtml(operationId)}">${escapeHtml(operationId)}</button>`
292
+ )
293
+ .join('')}</span>`
294
+ : '';
295
+ return `<li><strong>${escapeHtml(transition.trigger_type)}</strong> → <strong>${escapeHtml(transition.target_state)}</strong>${condition}${nextOperation}${preOperationLinks}</li>`;
296
+ })
297
+ .join('')}</ul>`;
298
+ }
299
+
300
+ function renderGraph(currentState, transitions) {
301
+ if (!Array.isArray(transitions) || transitions.length === 0) {
302
+ return `<div class="xof-edge">${escapeHtml(currentState)} [terminal]</div>`;
303
+ }
304
+
305
+ return transitions
306
+ .map((transition) => `<div class="xof-edge">${escapeHtml(currentState)} --> ${escapeHtml(transition.target_state)} [${escapeHtml(transition.trigger_type)}]</div>`)
307
+ .join('');
308
+ }
309
+
310
+ function renderCard(flow) {
311
+ const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
312
+ return `
313
+ <details class="xof-card" open>
314
+ <summary class="xof-title-row">
315
+ <span class="xof-title">x-openapi-flow</span>
316
+ <span class="xof-toggle-hint">toggle</span>
317
+ </summary>
318
+ <div class="xof-card-body">
319
+ <div class="xof-meta">
320
+ <div class="xof-meta-label">version</div><div>${escapeHtml(flow.version)}</div>
321
+ <div class="xof-meta-label">id</div><div>${escapeHtml(flow.id)}</div>
322
+ <div class="xof-meta-label">current_state</div><div>${escapeHtml(flow.current_state)}</div>
323
+ </div>
324
+ <div class="xof-section-title">Transitions</div>
325
+ ${renderTransitions(flow.current_state, transitions)}
326
+ <div class="xof-graph">
327
+ <div class="xof-graph-title">Flow graph (operation-level)</div>
328
+ ${renderGraph(flow.current_state, transitions)}
329
+ </div>
330
+ </div>
331
+ </details>
332
+ `;
333
+ }
334
+
335
+ function getSpecFromUi() {
336
+ try {
337
+ if (!window.ui || !window.ui.specSelectors || !window.ui.specSelectors.specJson) {
338
+ return null;
339
+ }
340
+
341
+ const spec = window.ui.specSelectors.specJson();
342
+ return spec && spec.toJS ? spec.toJS() : spec;
343
+ } catch (_error) {
344
+ return null;
345
+ }
346
+ }
347
+
348
+ function extractFlowsFromSpec(spec) {
349
+ const result = [];
350
+ const paths = (spec && spec.paths) || {};
351
+
352
+ Object.entries(paths).forEach(([pathKey, pathItem]) => {
353
+ if (!pathItem || typeof pathItem !== 'object') return;
354
+
355
+ FLOW_METHODS.forEach((method) => {
356
+ const operation = pathItem[method];
357
+ if (!operation || typeof operation !== 'object') return;
358
+
359
+ const flow = operation['x-openapi-flow'];
360
+ if (!flow || typeof flow !== 'object' || !flow.current_state) return;
361
+
362
+ result.push({
363
+ operationId: operation.operationId || `${method}_${pathKey}`,
364
+ method,
365
+ pathKey,
366
+ flow,
367
+ });
368
+ });
369
+ });
370
+
371
+ return result;
372
+ }
373
+
374
+ function hasFlowData(spec) {
375
+ return extractFlowsFromSpec(spec).length > 0;
376
+ }
377
+
378
+ function createOverviewHash(flows) {
379
+ const normalized = flows
380
+ .map(({ operationId, flow }) => ({
381
+ operationId: text(operationId),
382
+ current: text(flow && flow.current_state),
383
+ transitions: (Array.isArray(flow && flow.transitions) ? flow.transitions : [])
384
+ .map((transition) => ({
385
+ trigger: text(transition.trigger_type),
386
+ target: text(transition.target_state),
387
+ next: text(transition.next_operation_id),
388
+ requires: text(getPrerequisiteOperationIds(transition)),
389
+ }))
390
+ .sort((first, second) => JSON.stringify(first).localeCompare(JSON.stringify(second))),
391
+ }))
392
+ .sort((first, second) => first.operationId.localeCompare(second.operationId));
393
+
394
+ return JSON.stringify(normalized);
395
+ }
396
+
397
+ function buildOverviewMermaid(flows) {
398
+ const lines = ['stateDiagram-v2', ' direction LR'];
399
+ const statesByName = new Map();
400
+ const seen = new Set();
401
+ let stateCounter = 0;
402
+ const edgeLines = [];
403
+
404
+ function getStateId(stateName) {
405
+ const normalized = text(stateName);
406
+ if (statesByName.has(normalized)) {
407
+ return statesByName.get(normalized);
408
+ }
409
+
410
+ const safeBase = normalized
411
+ .toLowerCase()
412
+ .replace(/[^a-z0-9]+/g, '_')
413
+ .replace(/^_+|_+$/g, '');
414
+ stateCounter += 1;
415
+ const candidate = safeBase ? `s_${safeBase}_${stateCounter}` : `s_state_${stateCounter}`;
416
+ statesByName.set(normalized, candidate);
417
+ return candidate;
418
+ }
419
+
420
+ function sanitizeLabel(label) {
421
+ return text(label)
422
+ .replace(/[|]/g, ' / ')
423
+ .replace(/[\n\r]+/g, ' ')
424
+ .replace(/"/g, "'")
425
+ .trim();
426
+ }
427
+
428
+ flows.forEach(({ flow }) => {
429
+ const current = text(flow.current_state);
430
+ if (!current) return;
431
+
432
+ const fromId = getStateId(current);
433
+ const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
434
+ transitions.forEach((transition) => {
435
+ const target = text(transition.target_state);
436
+ if (!target) return;
437
+ const toId = getStateId(target);
438
+
439
+ const labelParts = [];
440
+ if (transition.next_operation_id) {
441
+ labelParts.push(`next ${text(transition.next_operation_id)}`);
442
+ }
443
+ const preOperations = getPrerequisiteOperationIds(transition);
444
+ if (preOperations.length) {
445
+ labelParts.push(`requires ${preOperations.join(',')}`);
446
+ }
447
+ const label = sanitizeLabel(labelParts.join(' / '));
448
+ const key = `${fromId}::${toId}::${label}`;
449
+ if (seen.has(key)) return;
450
+ seen.add(key);
451
+ edgeLines.push(` ${fromId} --> ${toId}${label ? `: ${label}` : ''}`);
452
+ });
453
+ });
454
+
455
+ statesByName.forEach((stateId, stateName) => {
456
+ lines.push(` state "${sanitizeLabel(stateName)}" as ${stateId}`);
457
+ });
458
+
459
+ lines.push(...edgeLines);
460
+
461
+ return lines.join('\n');
462
+ }
463
+
464
+ function hasOverviewTransitionData(flows) {
465
+ return flows.some(({ flow }) => Array.isArray(flow && flow.transitions) && flow.transitions.length > 0);
466
+ }
467
+
468
+ function buildStatesSummary(flows) {
469
+ const states = new Set();
470
+ flows.forEach(({ flow }) => {
471
+ if (flow && flow.current_state) {
472
+ states.add(text(flow.current_state));
473
+ }
474
+ });
475
+ return Array.from(states).sort().join(', ');
476
+ }
477
+
478
+ let mermaidLoaderPromise = null;
479
+ function ensureMermaid() {
480
+ if (window.mermaid) {
481
+ return Promise.resolve(window.mermaid);
482
+ }
483
+
484
+ if (mermaidLoaderPromise) {
485
+ return mermaidLoaderPromise;
486
+ }
487
+
488
+ mermaidLoaderPromise = new Promise((resolve, reject) => {
489
+ const script = document.createElement('script');
490
+ script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
491
+ script.async = true;
492
+ script.onload = () => {
493
+ if (window.mermaid) {
494
+ window.mermaid.initialize({
495
+ startOnLoad: false,
496
+ securityLevel: 'loose',
497
+ theme: 'neutral',
498
+ themeCSS: `
499
+ .edgeLabel {
500
+ background: rgba(255,255,255,0.96) !important;
501
+ padding: 2px 6px !important;
502
+ border-radius: 6px;
503
+ font-size: 12px !important;
504
+ line-height: 1.2;
505
+ }
506
+ .edgeLabel rect {
507
+ fill: rgba(255,255,255,0.96) !important;
508
+ rx: 6;
509
+ ry: 6;
510
+ }
511
+ `,
512
+ });
513
+ resolve(window.mermaid);
514
+ } else {
515
+ reject(new Error('Mermaid library not available after load'));
516
+ }
517
+ };
518
+ script.onerror = () => reject(new Error('Could not load Mermaid library'));
519
+ document.head.appendChild(script);
520
+ });
521
+
522
+ return mermaidLoaderPromise;
523
+ }
524
+
525
+ function svgToDataUri(svg) {
526
+ const encoded = window.btoa(unescape(encodeURIComponent(svg)));
527
+ return `data:image/svg+xml;base64,${encoded}`;
528
+ }
529
+
530
+ function getMermaidFallbackMessage() {
531
+ return 'Could not render Mermaid image. Check CDN/network access or load mermaid manually before Swagger UI.';
532
+ }
533
+
534
+ function getOverviewTitleFromSpec(spec) {
535
+ const apiTitle = spec && spec.info && spec.info.title ? spec.info.title : 'API';
536
+ return `${text(apiTitle)} — Flow Overview (x-openapi-flow)`;
537
+ }
538
+
539
+ let overviewRenderedHash = null;
540
+ let overviewRenderInProgress = false;
541
+ let overviewPendingHash = null;
542
+ let overviewTimeoutId = null;
543
+
544
+ function getOrCreateOverviewHolder() {
545
+ const infoContainer = document.querySelector('.swagger-ui .information-container');
546
+ if (!infoContainer) return null;
547
+
548
+ let holder = document.getElementById('xof-overview-holder');
549
+ if (!holder) {
550
+ holder = document.createElement('div');
551
+ holder.id = 'xof-overview-holder';
552
+ holder.className = 'xof-overview';
553
+ infoContainer.appendChild(holder);
554
+ }
555
+
556
+ return holder;
557
+ }
558
+
559
+ function clearOverviewHolder() {
560
+ const holder = document.getElementById('xof-overview-holder');
561
+ if (holder && holder.parentNode) {
562
+ holder.parentNode.removeChild(holder);
563
+ }
564
+ overviewRenderedHash = null;
565
+ }
566
+
567
+ async function renderOverview() {
568
+ const spec = getSpecFromUi();
569
+ const flows = extractFlowsFromSpec(spec);
570
+ if (!flows.length) {
571
+ clearOverviewHolder();
572
+ return;
573
+ }
574
+
575
+ const currentHash = createOverviewHash(flows);
576
+ const overviewTitle = escapeHtml(getOverviewTitleFromSpec(spec));
577
+ const hasTransitions = hasOverviewTransitionData(flows);
578
+ if (!hasTransitions) {
579
+ const noTransitionsHash = `no-transitions:${currentHash}`;
580
+ if (overviewRenderedHash === noTransitionsHash) return;
581
+ const holderNoTransitions = getOrCreateOverviewHolder();
582
+ if (!holderNoTransitions) return;
583
+ const statesSummary = escapeHtml(buildStatesSummary(flows) || '-');
584
+ holderNoTransitions.innerHTML = `
585
+ <details class="xof-card xof-overview-details">
586
+ <summary class="xof-title-row">
587
+ <span class="xof-title">${overviewTitle}</span>
588
+ <span class="xof-toggle-hint xof-overview-toggle"></span>
589
+ </summary>
590
+ <div class="xof-card-body">
591
+ <div class="xof-overview-sub">All operation transitions in one graph.</div>
592
+ <div class="xof-empty">No transitions found yet. Add transitions in the sidecar and run apply to render the Mermaid overview.</div>
593
+ <div class="xof-overview-code">Current states: ${statesSummary}</div>
594
+ </div>
595
+ </details>
596
+ `;
597
+ overviewRenderedHash = noTransitionsHash;
598
+ return;
599
+ }
600
+
601
+ const mermaid = buildOverviewMermaid(flows);
602
+ if (overviewRenderedHash === currentHash) return;
603
+ if (overviewRenderInProgress && overviewPendingHash === currentHash) return;
604
+
605
+ const holder = getOrCreateOverviewHolder();
606
+ if (!holder) return;
607
+
608
+ holder.innerHTML = `
609
+ <details class="xof-card xof-overview-details">
610
+ <summary class="xof-title-row">
611
+ <span class="xof-title">${overviewTitle}</span>
612
+ <span class="xof-toggle-hint xof-overview-toggle"></span>
613
+ </summary>
614
+ <div class="xof-card-body">
615
+ <div class="xof-overview-sub">All operation transitions in one graph.</div>
616
+ <div class="xof-empty">Rendering Mermaid graph...</div>
617
+ </div>
618
+ </details>
619
+ `;
620
+ overviewRenderInProgress = true;
621
+ overviewPendingHash = currentHash;
622
+
623
+ try {
624
+ const mermaidLib = await ensureMermaid();
625
+ const renderId = `xof-overview-${Date.now()}`;
626
+ const renderResult = await mermaidLib.render(renderId, mermaid);
627
+ const svg = renderResult && renderResult.svg ? renderResult.svg : renderResult;
628
+ const dataUri = svgToDataUri(svg);
629
+
630
+ holder.innerHTML = `
631
+ <details class="xof-card xof-overview-details">
632
+ <summary class="xof-title-row">
633
+ <span class="xof-title">${overviewTitle}</span>
634
+ <span class="xof-toggle-hint xof-overview-toggle"></span>
635
+ </summary>
636
+ <div class="xof-card-body">
637
+ <div class="xof-overview-sub">All operation transitions in one graph.</div>
638
+ <div class="xof-overview-graph-wrap">
639
+ <img src="${dataUri}" alt="x-openapi-flow overview graph" />
640
+ </div>
641
+ <details style="margin-top:8px;">
642
+ <summary style="cursor:pointer;">Mermaid source</summary>
643
+ <div class="xof-overview-code">${mermaid.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
644
+ </details>
645
+ </div>
646
+ </details>
647
+ `;
648
+ } catch (error) {
649
+ const details = error && error.message ? escapeHtml(error.message) : 'Unknown Mermaid error';
650
+ holder.innerHTML = `
651
+ <details class="xof-card xof-overview-details">
652
+ <summary class="xof-title-row">
653
+ <span class="xof-title">${overviewTitle}</span>
654
+ <span class="xof-toggle-hint xof-overview-toggle"></span>
655
+ </summary>
656
+ <div class="xof-card-body">
657
+ <div class="xof-empty">${getMermaidFallbackMessage()}</div>
658
+ <div class="xof-overview-code">Details: ${details}</div>
659
+ <div class="xof-overview-code">${mermaid.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
660
+ </div>
661
+ </details>
662
+ `;
663
+ } finally {
664
+ overviewRenderInProgress = false;
665
+ }
666
+
667
+ overviewRenderedHash = currentHash;
668
+ }
669
+
670
+ function scheduleOverviewRender() {
671
+ if (overviewTimeoutId) {
672
+ window.clearTimeout(overviewTimeoutId);
673
+ }
674
+
675
+ overviewTimeoutId = window.setTimeout(() => {
676
+ overviewTimeoutId = null;
677
+ renderOverview().catch(() => {
678
+ // keep plugin resilient in environments where async rendering fails
679
+ });
680
+ }, 120);
681
+ }
682
+
683
+ function findOperationById(spec, operationId) {
684
+ if (!spec || !spec.paths || !operationId) return null;
685
+
686
+ for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
687
+ if (!pathItem || typeof pathItem !== 'object') continue;
688
+
689
+ for (const method of FLOW_METHODS) {
690
+ const operation = pathItem[method];
691
+ if (!operation || typeof operation !== 'object') continue;
692
+ if (operation.operationId === operationId) {
693
+ return { method, pathKey };
694
+ }
695
+ }
696
+ }
697
+
698
+ return null;
699
+ }
700
+
701
+ function jumpToOperationById(operationId) {
702
+ function highlightTarget(opblock) {
703
+ if (!opblock) return;
704
+ opblock.classList.remove('xof-jump-target');
705
+ window.requestAnimationFrame(() => {
706
+ opblock.classList.add('xof-jump-target');
707
+ window.setTimeout(() => opblock.classList.remove('xof-jump-target'), 950);
708
+ });
709
+ }
710
+
711
+ function getOperationSummaries() {
712
+ return Array.from(document.querySelectorAll('.swagger-ui .opblock-summary')).map((summary) => {
713
+ const opblock = summary.closest('.opblock');
714
+ const pathNode = summary.querySelector('.opblock-summary-path');
715
+ const path = pathNode ? pathNode.textContent.trim() : '';
716
+ return { summary, opblock, path };
717
+ });
718
+ }
719
+
720
+ function tryJump(match) {
721
+ const summaries = getOperationSummaries();
722
+ for (const { summary, opblock, path } of summaries) {
723
+ if (!opblock || !opblock.classList.contains(`opblock-${match.method}`)) continue;
724
+ if (path !== match.pathKey) continue;
725
+
726
+ summary.scrollIntoView({ behavior: 'smooth', block: 'center' });
727
+ if (!opblock.classList.contains('is-open')) {
728
+ summary.click();
729
+ }
730
+ highlightTarget(opblock);
731
+ return true;
732
+ }
733
+ return false;
734
+ }
735
+
736
+ const spec = getSpecFromUi();
737
+ const match = findOperationById(spec, operationId);
738
+ if (!match) return false;
739
+
740
+ return tryJump(match);
741
+ }
742
+
743
+ function findXOpenApiFlowValueCell(opblock) {
744
+ const rows = opblock.querySelectorAll('tr');
745
+ for (const row of rows) {
746
+ const cells = row.querySelectorAll('td');
747
+ if (cells.length < 2) continue;
748
+ if (cells[0].innerText.trim() === 'x-openapi-flow') {
749
+ return cells[1];
750
+ }
751
+ }
752
+ return null;
753
+ }
754
+
755
+ function enhanceOperation(opblock) {
756
+ const valueCell = findXOpenApiFlowValueCell(opblock);
757
+ if (!valueCell || valueCell.dataset.xofEnhanced === '1') return;
758
+
759
+ const raw = valueCell.innerText.trim();
760
+ if (!raw) return;
761
+
762
+ let flow;
763
+ try {
764
+ flow = JSON.parse(raw);
765
+ } catch (_error) {
766
+ return;
767
+ }
768
+
769
+ valueCell.innerHTML = renderCard(flow);
770
+ valueCell.dataset.xofEnhanced = '1';
771
+ }
772
+
773
+ let jumpFeedbackTimeoutId = null;
774
+ function showJumpFeedback(message) {
775
+ injectStyles();
776
+
777
+ let feedback = document.getElementById('xof-jump-feedback');
778
+ if (!feedback) {
779
+ feedback = document.createElement('div');
780
+ feedback.id = 'xof-jump-feedback';
781
+ feedback.className = 'xof-jump-feedback';
782
+ document.body.appendChild(feedback);
783
+ }
784
+
785
+ feedback.textContent = message;
786
+
787
+ if (jumpFeedbackTimeoutId) {
788
+ window.clearTimeout(jumpFeedbackTimeoutId);
789
+ }
790
+
791
+ jumpFeedbackTimeoutId = window.setTimeout(() => {
792
+ if (feedback && feedback.parentNode) {
793
+ feedback.parentNode.removeChild(feedback);
794
+ }
795
+ jumpFeedbackTimeoutId = null;
796
+ }, 2200);
797
+ }
798
+
799
+ function enhanceAll() {
800
+ const spec = getSpecFromUi();
801
+ if (!hasFlowData(spec)) {
802
+ clearOverviewHolder();
803
+ return;
804
+ }
805
+
806
+ injectStyles();
807
+ const opblocks = document.querySelectorAll('.opblock');
808
+ opblocks.forEach((opblock) => enhanceOperation(opblock));
809
+ scheduleOverviewRender();
810
+ }
811
+
812
+ let enhanceScheduled = false;
813
+ function scheduleEnhance() {
814
+ if (enhanceScheduled) return;
815
+ enhanceScheduled = true;
816
+ window.requestAnimationFrame(() => {
817
+ enhanceScheduled = false;
818
+ enhanceAll();
819
+ });
820
+ }
821
+
822
+ const observer = new MutationObserver(() => {
823
+ scheduleEnhance();
824
+ });
825
+
826
+ document.addEventListener('click', (event) => {
827
+ const target = event.target;
828
+ if (!target || !target.closest) return;
829
+
830
+ const jumpButton = target.closest('[data-xof-jump]');
831
+ if (!jumpButton) return;
832
+
833
+ event.preventDefault();
834
+ const operationId = jumpButton.getAttribute('data-xof-jump');
835
+ if (!operationId) return;
836
+ const jumped = jumpToOperationById(operationId);
837
+ if (!jumped) {
838
+ showJumpFeedback(`Could not locate operation '${operationId}' in the rendered Swagger view.`);
839
+ }
840
+ });
841
+
842
+ window.addEventListener('load', () => {
843
+ scheduleEnhance();
844
+ observer.observe(document.body, { childList: true, subtree: true });
845
+ });
846
+
847
+ window.XOpenApiFlowUiInternals = {
848
+ extractFlowsFromSpec,
849
+ hasFlowData,
850
+ hasOverviewTransitionData,
851
+ buildOverviewMermaid,
852
+ createOverviewHash,
853
+ getOverviewTitleFromSpec,
854
+ getMermaidFallbackMessage,
855
+ };
856
+ })();