x-openapi-flow 1.2.3 → 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.
- package/README.md +212 -18
- package/bin/x-openapi-flow.js +690 -18
- package/lib/swagger-ui/x-openapi-flow-plugin.js +472 -71
- package/lib/validator.js +36 -3
- package/package.json +3 -2
- package/schema/flow-schema.json +2 -2
|
@@ -12,6 +12,20 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
12
12
|
return String(value);
|
|
13
13
|
}
|
|
14
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
|
+
|
|
15
29
|
function transitionsList(currentState, transitions) {
|
|
16
30
|
if (!Array.isArray(transitions) || transitions.length === 0) {
|
|
17
31
|
return h("div", { style: { opacity: 0.85, fontStyle: "italic" } }, "No transitions (terminal state)");
|
|
@@ -143,6 +157,7 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
143
157
|
|
|
144
158
|
(function () {
|
|
145
159
|
const styleId = 'x-openapi-flow-ui-style';
|
|
160
|
+
const FLOW_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
146
161
|
|
|
147
162
|
function injectStyles() {
|
|
148
163
|
if (document.getElementById(styleId)) return;
|
|
@@ -150,19 +165,79 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
150
165
|
const style = document.createElement('style');
|
|
151
166
|
style.id = styleId;
|
|
152
167
|
style.textContent = `
|
|
153
|
-
.xof-card { border: 1px solid rgba(
|
|
154
|
-
.xof-
|
|
155
|
-
.xof-
|
|
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; }
|
|
156
177
|
.xof-meta-label { opacity: 0.85; }
|
|
157
178
|
.xof-list { margin: 0; padding-left: 18px; }
|
|
158
|
-
.xof-list li { margin:
|
|
159
|
-
.xof-
|
|
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; }
|
|
160
188
|
.xof-graph-title { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
|
|
161
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; }
|
|
162
190
|
.xof-empty { opacity: 0.85; font-style: italic; }
|
|
163
|
-
.xof-overview { margin:
|
|
164
|
-
.xof-overview
|
|
165
|
-
.xof-overview-
|
|
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
|
+
}
|
|
166
241
|
`;
|
|
167
242
|
|
|
168
243
|
document.head.appendChild(style);
|
|
@@ -174,6 +249,29 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
174
249
|
return String(value);
|
|
175
250
|
}
|
|
176
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, '<')
|
|
270
|
+
.replace(/>/g, '>')
|
|
271
|
+
.replace(/"/g, '"')
|
|
272
|
+
.replace(/'/g, ''');
|
|
273
|
+
}
|
|
274
|
+
|
|
177
275
|
function renderTransitions(currentState, transitions) {
|
|
178
276
|
if (!Array.isArray(transitions) || transitions.length === 0) {
|
|
179
277
|
return '<div class="xof-empty">No transitions (terminal state)</div>';
|
|
@@ -181,40 +279,56 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
181
279
|
|
|
182
280
|
return `<ul class="xof-list">${transitions
|
|
183
281
|
.map((transition) => {
|
|
184
|
-
const condition = transition.condition ? ` — ${
|
|
185
|
-
const nextOperation = transition.next_operation_id
|
|
186
|
-
|
|
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>`;
|
|
187
296
|
})
|
|
188
297
|
.join('')}</ul>`;
|
|
189
298
|
}
|
|
190
299
|
|
|
191
300
|
function renderGraph(currentState, transitions) {
|
|
192
301
|
if (!Array.isArray(transitions) || transitions.length === 0) {
|
|
193
|
-
return `<div class="xof-edge">${
|
|
302
|
+
return `<div class="xof-edge">${escapeHtml(currentState)} [terminal]</div>`;
|
|
194
303
|
}
|
|
195
304
|
|
|
196
305
|
return transitions
|
|
197
|
-
.map((transition) => `<div class="xof-edge">${
|
|
306
|
+
.map((transition) => `<div class="xof-edge">${escapeHtml(currentState)} --> ${escapeHtml(transition.target_state)} [${escapeHtml(transition.trigger_type)}]</div>`)
|
|
198
307
|
.join('');
|
|
199
308
|
}
|
|
200
309
|
|
|
201
310
|
function renderCard(flow) {
|
|
202
311
|
const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
|
|
203
312
|
return `
|
|
204
|
-
<
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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>
|
|
216
330
|
</div>
|
|
217
|
-
</
|
|
331
|
+
</details>
|
|
218
332
|
`;
|
|
219
333
|
}
|
|
220
334
|
|
|
@@ -234,12 +348,11 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
234
348
|
function extractFlowsFromSpec(spec) {
|
|
235
349
|
const result = [];
|
|
236
350
|
const paths = (spec && spec.paths) || {};
|
|
237
|
-
const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
238
351
|
|
|
239
352
|
Object.entries(paths).forEach(([pathKey, pathItem]) => {
|
|
240
353
|
if (!pathItem || typeof pathItem !== 'object') return;
|
|
241
354
|
|
|
242
|
-
|
|
355
|
+
FLOW_METHODS.forEach((method) => {
|
|
243
356
|
const operation = pathItem[method];
|
|
244
357
|
if (!operation || typeof operation !== 'object') return;
|
|
245
358
|
|
|
@@ -248,6 +361,8 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
248
361
|
|
|
249
362
|
result.push({
|
|
250
363
|
operationId: operation.operationId || `${method}_${pathKey}`,
|
|
364
|
+
method,
|
|
365
|
+
pathKey,
|
|
251
366
|
flow,
|
|
252
367
|
});
|
|
253
368
|
});
|
|
@@ -260,46 +375,106 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
260
375
|
return extractFlowsFromSpec(spec).length > 0;
|
|
261
376
|
}
|
|
262
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
|
+
|
|
263
397
|
function buildOverviewMermaid(flows) {
|
|
264
|
-
const lines = ['stateDiagram-v2'];
|
|
265
|
-
const
|
|
398
|
+
const lines = ['stateDiagram-v2', ' direction LR'];
|
|
399
|
+
const statesByName = new Map();
|
|
266
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
|
+
}
|
|
267
427
|
|
|
268
428
|
flows.forEach(({ flow }) => {
|
|
269
|
-
const current = flow.current_state;
|
|
429
|
+
const current = text(flow.current_state);
|
|
270
430
|
if (!current) return;
|
|
271
431
|
|
|
272
|
-
|
|
432
|
+
const fromId = getStateId(current);
|
|
273
433
|
const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
|
|
274
434
|
transitions.forEach((transition) => {
|
|
275
|
-
const target = transition.target_state;
|
|
435
|
+
const target = text(transition.target_state);
|
|
276
436
|
if (!target) return;
|
|
277
|
-
|
|
437
|
+
const toId = getStateId(target);
|
|
278
438
|
|
|
279
439
|
const labelParts = [];
|
|
280
440
|
if (transition.next_operation_id) {
|
|
281
|
-
labelParts.push(`next
|
|
441
|
+
labelParts.push(`next ${text(transition.next_operation_id)}`);
|
|
282
442
|
}
|
|
283
|
-
|
|
284
|
-
|
|
443
|
+
const preOperations = getPrerequisiteOperationIds(transition);
|
|
444
|
+
if (preOperations.length) {
|
|
445
|
+
labelParts.push(`requires ${preOperations.join(',')}`);
|
|
285
446
|
}
|
|
286
|
-
const label = labelParts.join('
|
|
287
|
-
const key = `${
|
|
447
|
+
const label = sanitizeLabel(labelParts.join(' / '));
|
|
448
|
+
const key = `${fromId}::${toId}::${label}`;
|
|
288
449
|
if (seen.has(key)) return;
|
|
289
450
|
seen.add(key);
|
|
290
|
-
|
|
451
|
+
edgeLines.push(` ${fromId} --> ${toId}${label ? `: ${label}` : ''}`);
|
|
291
452
|
});
|
|
292
453
|
});
|
|
293
454
|
|
|
294
|
-
|
|
295
|
-
.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
455
|
+
statesByName.forEach((stateId, stateName) => {
|
|
456
|
+
lines.push(` state "${sanitizeLabel(stateName)}" as ${stateId}`);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
lines.push(...edgeLines);
|
|
299
460
|
|
|
300
461
|
return lines.join('\n');
|
|
301
462
|
}
|
|
302
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
|
+
|
|
303
478
|
let mermaidLoaderPromise = null;
|
|
304
479
|
function ensureMermaid() {
|
|
305
480
|
if (window.mermaid) {
|
|
@@ -316,7 +491,25 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
316
491
|
script.async = true;
|
|
317
492
|
script.onload = () => {
|
|
318
493
|
if (window.mermaid) {
|
|
319
|
-
window.mermaid.initialize({
|
|
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
|
+
});
|
|
320
513
|
resolve(window.mermaid);
|
|
321
514
|
} else {
|
|
322
515
|
reject(new Error('Mermaid library not available after load'));
|
|
@@ -334,31 +527,96 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
334
527
|
return `data:image/svg+xml;base64,${encoded}`;
|
|
335
528
|
}
|
|
336
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
|
+
|
|
337
539
|
let overviewRenderedHash = null;
|
|
338
540
|
let overviewRenderInProgress = false;
|
|
339
541
|
let overviewPendingHash = null;
|
|
340
|
-
|
|
341
|
-
const spec = getSpecFromUi();
|
|
342
|
-
const flows = extractFlowsFromSpec(spec);
|
|
343
|
-
if (!flows.length) return;
|
|
344
|
-
|
|
345
|
-
const mermaid = buildOverviewMermaid(flows);
|
|
346
|
-
const currentHash = `${flows.length}:${mermaid}`;
|
|
347
|
-
if (overviewRenderedHash === currentHash) return;
|
|
348
|
-
if (overviewRenderInProgress && overviewPendingHash === currentHash) return;
|
|
542
|
+
let overviewTimeoutId = null;
|
|
349
543
|
|
|
544
|
+
function getOrCreateOverviewHolder() {
|
|
350
545
|
const infoContainer = document.querySelector('.swagger-ui .information-container');
|
|
351
|
-
if (!infoContainer) return;
|
|
546
|
+
if (!infoContainer) return null;
|
|
352
547
|
|
|
353
548
|
let holder = document.getElementById('xof-overview-holder');
|
|
354
549
|
if (!holder) {
|
|
355
550
|
holder = document.createElement('div');
|
|
356
551
|
holder.id = 'xof-overview-holder';
|
|
357
|
-
holder.className = 'xof-overview
|
|
358
|
-
infoContainer.
|
|
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);
|
|
359
563
|
}
|
|
564
|
+
overviewRenderedHash = null;
|
|
565
|
+
}
|
|
360
566
|
|
|
361
|
-
|
|
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
|
+
`;
|
|
362
620
|
overviewRenderInProgress = true;
|
|
363
621
|
overviewPendingHash = currentHash;
|
|
364
622
|
|
|
@@ -370,18 +628,37 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
370
628
|
const dataUri = svgToDataUri(svg);
|
|
371
629
|
|
|
372
630
|
holder.innerHTML = `
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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, '<').replace(/>/g, '>')}</div>
|
|
644
|
+
</details>
|
|
645
|
+
</div>
|
|
378
646
|
</details>
|
|
379
647
|
`;
|
|
380
|
-
} catch (
|
|
648
|
+
} catch (error) {
|
|
649
|
+
const details = error && error.message ? escapeHtml(error.message) : 'Unknown Mermaid error';
|
|
381
650
|
holder.innerHTML = `
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
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, '<').replace(/>/g, '>')}</div>
|
|
660
|
+
</div>
|
|
661
|
+
</details>
|
|
385
662
|
`;
|
|
386
663
|
} finally {
|
|
387
664
|
overviewRenderInProgress = false;
|
|
@@ -390,6 +667,79 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
390
667
|
overviewRenderedHash = currentHash;
|
|
391
668
|
}
|
|
392
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
|
+
|
|
393
743
|
function findXOpenApiFlowValueCell(opblock) {
|
|
394
744
|
const rows = opblock.querySelectorAll('tr');
|
|
395
745
|
for (const row of rows) {
|
|
@@ -420,18 +770,43 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
420
770
|
valueCell.dataset.xofEnhanced = '1';
|
|
421
771
|
}
|
|
422
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
|
+
|
|
423
799
|
function enhanceAll() {
|
|
424
800
|
const spec = getSpecFromUi();
|
|
425
801
|
if (!hasFlowData(spec)) {
|
|
802
|
+
clearOverviewHolder();
|
|
426
803
|
return;
|
|
427
804
|
}
|
|
428
805
|
|
|
429
806
|
injectStyles();
|
|
430
807
|
const opblocks = document.querySelectorAll('.opblock');
|
|
431
808
|
opblocks.forEach((opblock) => enhanceOperation(opblock));
|
|
432
|
-
|
|
433
|
-
// keep plugin resilient in environments where async rendering fails
|
|
434
|
-
});
|
|
809
|
+
scheduleOverviewRender();
|
|
435
810
|
}
|
|
436
811
|
|
|
437
812
|
let enhanceScheduled = false;
|
|
@@ -448,8 +823,34 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
448
823
|
scheduleEnhance();
|
|
449
824
|
});
|
|
450
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
|
+
|
|
451
842
|
window.addEventListener('load', () => {
|
|
452
843
|
scheduleEnhance();
|
|
453
844
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
454
845
|
});
|
|
846
|
+
|
|
847
|
+
window.XOpenApiFlowUiInternals = {
|
|
848
|
+
extractFlowsFromSpec,
|
|
849
|
+
hasFlowData,
|
|
850
|
+
hasOverviewTransitionData,
|
|
851
|
+
buildOverviewMermaid,
|
|
852
|
+
createOverviewHash,
|
|
853
|
+
getOverviewTitleFromSpec,
|
|
854
|
+
getMermaidFallbackMessage,
|
|
855
|
+
};
|
|
455
856
|
})();
|