@emeryld/rrroutes-contract 2.7.2 → 2.7.4
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 +2 -1
- package/dist/export/defaultViewerTemplate.d.ts +1 -0
- package/dist/export/index.d.ts +1 -0
- package/dist/index.cjs +87 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +86 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/tools/finalized-leaves-viewer.html +1061 -0
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Finalized Leaves Viewer</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f6f8fc;
|
|
10
|
+
--surface: #ffffff;
|
|
11
|
+
--surface-2: #f1f4f9;
|
|
12
|
+
--border: #d9e0eb;
|
|
13
|
+
--text: #182033;
|
|
14
|
+
--muted: #64708b;
|
|
15
|
+
--accent: #1858c6;
|
|
16
|
+
--ok: #1f8f4e;
|
|
17
|
+
--danger: #d12b2b;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-family: 'Iosevka Web', 'SFMono-Regular', Menlo, Consolas, monospace;
|
|
27
|
+
color: var(--text);
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
mark {
|
|
32
|
+
background: #ffec99;
|
|
33
|
+
color: #402f00;
|
|
34
|
+
border-radius: 2px;
|
|
35
|
+
padding: 0 1px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.wrap {
|
|
39
|
+
max-width: 1200px;
|
|
40
|
+
margin: 0 auto;
|
|
41
|
+
padding: 20px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.card {
|
|
45
|
+
background: var(--surface);
|
|
46
|
+
border: 1px solid #e7ecf4;
|
|
47
|
+
border-radius: 6px;
|
|
48
|
+
padding: 12px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.controls {
|
|
52
|
+
display: grid;
|
|
53
|
+
gap: 10px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.field-row {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-wrap: wrap;
|
|
59
|
+
gap: 8px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.field-actions {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-wrap: wrap;
|
|
65
|
+
gap: 8px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.field-item {
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: 6px;
|
|
72
|
+
padding: 3px 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
input[type='text'],
|
|
76
|
+
select {
|
|
77
|
+
width: 100%;
|
|
78
|
+
border: 1px solid var(--border);
|
|
79
|
+
border-radius: 4px;
|
|
80
|
+
padding: 8px 10px;
|
|
81
|
+
font: inherit;
|
|
82
|
+
background: #fff;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
button {
|
|
86
|
+
border: 1px solid var(--border);
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
padding: 7px 10px;
|
|
89
|
+
font: inherit;
|
|
90
|
+
background: #fff;
|
|
91
|
+
color: var(--text);
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
button:hover {
|
|
96
|
+
background: var(--surface-2);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.meta {
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
font-size: 12px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#results {
|
|
105
|
+
margin-top: 14px;
|
|
106
|
+
display: grid;
|
|
107
|
+
gap: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
details.leaf {
|
|
111
|
+
border-top: 1px solid var(--border);
|
|
112
|
+
padding: 10px 2px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
summary {
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-weight: 700;
|
|
118
|
+
color: var(--accent);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.leaf-content {
|
|
122
|
+
display: grid;
|
|
123
|
+
gap: 4px;
|
|
124
|
+
margin-top: 10px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.section {
|
|
128
|
+
border-top: 1px solid #e8edf5;
|
|
129
|
+
padding: 10px 0 2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.section h3 {
|
|
133
|
+
margin: 0 0 8px;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.grid-2 {
|
|
138
|
+
display: grid;
|
|
139
|
+
gap: 6px;
|
|
140
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.kv {
|
|
144
|
+
border-bottom: 1px dashed #e2e8f2;
|
|
145
|
+
padding: 4px 0 6px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.kv .k {
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
color: var(--muted);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.kv .v {
|
|
154
|
+
margin-top: 2px;
|
|
155
|
+
word-break: break-word;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.chips {
|
|
159
|
+
display: flex;
|
|
160
|
+
gap: 6px;
|
|
161
|
+
flex-wrap: wrap;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.chips.filters {
|
|
165
|
+
margin-top: 2px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.chip {
|
|
169
|
+
border: 1px solid #e0e6f0;
|
|
170
|
+
border-radius: 4px;
|
|
171
|
+
padding: 2px 6px;
|
|
172
|
+
font-size: 12px;
|
|
173
|
+
background: #f6f8fc;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.chip.ok {
|
|
177
|
+
border-color: #b5e4c8;
|
|
178
|
+
color: var(--ok);
|
|
179
|
+
background: #effbf4;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.empty {
|
|
183
|
+
color: var(--muted);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.mono {
|
|
187
|
+
font-family: inherit;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.schema-block {
|
|
191
|
+
border-top: 1px solid #e8edf5;
|
|
192
|
+
padding: 8px 0 2px;
|
|
193
|
+
margin-bottom: 6px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.schema-block:last-child {
|
|
197
|
+
margin-bottom: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.schema-header {
|
|
201
|
+
font-weight: 700;
|
|
202
|
+
margin-bottom: 6px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.schema-tree details {
|
|
206
|
+
margin-left: 12px;
|
|
207
|
+
border-left: 1px solid #e8edf5;
|
|
208
|
+
padding-left: 8px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.tree-row {
|
|
212
|
+
display: grid;
|
|
213
|
+
grid-template-columns: 1fr auto;
|
|
214
|
+
gap: 8px;
|
|
215
|
+
padding: 3px 0;
|
|
216
|
+
align-items: center;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.schema-tree summary .tree-row {
|
|
220
|
+
display: inline-grid;
|
|
221
|
+
vertical-align: middle;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.tree-name {
|
|
225
|
+
display: inline-flex;
|
|
226
|
+
align-items: baseline;
|
|
227
|
+
gap: 2px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.tree-col-muted {
|
|
231
|
+
color: var(--muted);
|
|
232
|
+
font-size: 12px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.required-star {
|
|
236
|
+
color: var(--danger);
|
|
237
|
+
font-weight: 700;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.tree-pill {
|
|
241
|
+
border: 1px solid #e0e6f0;
|
|
242
|
+
border-radius: 4px;
|
|
243
|
+
padding: 1px 6px;
|
|
244
|
+
font-size: 11px;
|
|
245
|
+
background: #f2f5fa;
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
248
|
+
</head>
|
|
249
|
+
<body>
|
|
250
|
+
<div class="wrap">
|
|
251
|
+
<h1>Finalized Leaves Viewer</h1>
|
|
252
|
+
<div class="card controls">
|
|
253
|
+
<label>
|
|
254
|
+
Load export JSON file:
|
|
255
|
+
<input id="fileInput" type="file" accept="application/json,.json" />
|
|
256
|
+
</label>
|
|
257
|
+
|
|
258
|
+
<label>
|
|
259
|
+
Search text:
|
|
260
|
+
<input id="searchInput" type="text" placeholder="Type to search..." />
|
|
261
|
+
</label>
|
|
262
|
+
|
|
263
|
+
<label>
|
|
264
|
+
Type filter:
|
|
265
|
+
<input id="typeFilterInput" type="text" placeholder='Type/kind/enum (e.g. "paid")' />
|
|
266
|
+
</label>
|
|
267
|
+
|
|
268
|
+
<div class="field-row">
|
|
269
|
+
<label class="field-item">
|
|
270
|
+
<input id="caseSensitive" type="checkbox" />
|
|
271
|
+
<span>case sensitive</span>
|
|
272
|
+
</label>
|
|
273
|
+
<label class="field-item">
|
|
274
|
+
<input id="regexSearch" type="checkbox" />
|
|
275
|
+
<span>regex</span>
|
|
276
|
+
</label>
|
|
277
|
+
<label class="field-item">
|
|
278
|
+
<input id="hasEnumOnly" type="checkbox" />
|
|
279
|
+
<span>has enum only</span>
|
|
280
|
+
</label>
|
|
281
|
+
<label class="field-item">
|
|
282
|
+
<span>type match</span>
|
|
283
|
+
<select id="typeMatchMode">
|
|
284
|
+
<option value="contains" selected>contains</option>
|
|
285
|
+
<option value="exact">exact</option>
|
|
286
|
+
</select>
|
|
287
|
+
</label>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div id="fieldCheckboxes" class="field-row"></div>
|
|
291
|
+
<div class="field-actions">
|
|
292
|
+
<button id="selectAllFields" type="button">Select all</button>
|
|
293
|
+
<button id="clearAllFields" type="button">Clear all</button>
|
|
294
|
+
<button id="schemasOnlyFields" type="button">Schemas only</button>
|
|
295
|
+
<button id="metadataOnlyFields" type="button">Metadata only</button>
|
|
296
|
+
<button id="resetFilters" type="button">Reset filters</button>
|
|
297
|
+
</div>
|
|
298
|
+
<div id="activeFilterChips" class="chips filters"></div>
|
|
299
|
+
|
|
300
|
+
<div id="status" class="meta">Load a JSON export to begin.</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div id="results"></div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->
|
|
307
|
+
<script>
|
|
308
|
+
const SEARCH_FIELDS = [
|
|
309
|
+
{ id: 'method', label: 'method', get: (leaf) => [leaf.method] },
|
|
310
|
+
{ id: 'path', label: 'path', get: (leaf) => [leaf.path] },
|
|
311
|
+
{ id: 'key', label: 'key', get: (leaf) => [leaf.key] },
|
|
312
|
+
{ id: 'summary', label: 'summary', get: (leaf) => [leaf.cfg?.summary] },
|
|
313
|
+
{
|
|
314
|
+
id: 'description',
|
|
315
|
+
label: 'description',
|
|
316
|
+
get: (leaf) => [leaf.cfg?.description],
|
|
317
|
+
},
|
|
318
|
+
{ id: 'docsGroup', label: 'docsGroup', get: (leaf) => [leaf.cfg?.docsGroup] },
|
|
319
|
+
{ id: 'tags', label: 'tags', get: (leaf) => leaf.cfg?.tags || [] },
|
|
320
|
+
{ id: 'stability', label: 'stability', get: (leaf) => [leaf.cfg?.stability] },
|
|
321
|
+
{ id: 'docsMeta', label: 'docsMeta', get: (leaf) => [leaf.cfg?.docsMeta] },
|
|
322
|
+
{
|
|
323
|
+
id: 'params',
|
|
324
|
+
label: 'params',
|
|
325
|
+
get: (leaf, schemaFlatByLeaf) =>
|
|
326
|
+
schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'params'),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: 'query',
|
|
330
|
+
label: 'query',
|
|
331
|
+
get: (leaf, schemaFlatByLeaf) =>
|
|
332
|
+
schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'query'),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: 'body',
|
|
336
|
+
label: 'body',
|
|
337
|
+
get: (leaf, schemaFlatByLeaf) =>
|
|
338
|
+
schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'body'),
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: 'output',
|
|
342
|
+
label: 'output',
|
|
343
|
+
get: (leaf, schemaFlatByLeaf) =>
|
|
344
|
+
schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'output'),
|
|
345
|
+
},
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
|
|
349
|
+
const SCHEMA_FIELD_IDS = new Set(SCHEMA_SECTIONS)
|
|
350
|
+
const METADATA_FIELD_IDS = new Set(
|
|
351
|
+
SEARCH_FIELDS.map((field) => field.id).filter((id) => !SCHEMA_FIELD_IDS.has(id)),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
const state = { payload: null, leaves: [] }
|
|
355
|
+
|
|
356
|
+
const fileInput = document.getElementById('fileInput')
|
|
357
|
+
const searchInput = document.getElementById('searchInput')
|
|
358
|
+
const typeFilterInput = document.getElementById('typeFilterInput')
|
|
359
|
+
const caseSensitiveInput = document.getElementById('caseSensitive')
|
|
360
|
+
const regexSearchInput = document.getElementById('regexSearch')
|
|
361
|
+
const hasEnumOnlyInput = document.getElementById('hasEnumOnly')
|
|
362
|
+
const typeMatchModeInput = document.getElementById('typeMatchMode')
|
|
363
|
+
const fieldCheckboxes = document.getElementById('fieldCheckboxes')
|
|
364
|
+
const activeFilterChips = document.getElementById('activeFilterChips')
|
|
365
|
+
const selectAllFieldsBtn = document.getElementById('selectAllFields')
|
|
366
|
+
const clearAllFieldsBtn = document.getElementById('clearAllFields')
|
|
367
|
+
const schemasOnlyFieldsBtn = document.getElementById('schemasOnlyFields')
|
|
368
|
+
const metadataOnlyFieldsBtn = document.getElementById('metadataOnlyFields')
|
|
369
|
+
const resetFiltersBtn = document.getElementById('resetFilters')
|
|
370
|
+
const statusEl = document.getElementById('status')
|
|
371
|
+
const resultsEl = document.getElementById('results')
|
|
372
|
+
|
|
373
|
+
const URL_PARAM_KEY = 'filters'
|
|
374
|
+
let isHydratingFromUrl = false
|
|
375
|
+
|
|
376
|
+
function escapeHtml(value) {
|
|
377
|
+
return String(value)
|
|
378
|
+
.replace(/&/g, '&')
|
|
379
|
+
.replace(/</g, '<')
|
|
380
|
+
.replace(/>/g, '>')
|
|
381
|
+
.replace(/"/g, '"')
|
|
382
|
+
.replace(/'/g, ''')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function escapeRegExp(value) {
|
|
386
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createSearchEngine(queryRaw, options) {
|
|
390
|
+
const query = queryRaw || ''
|
|
391
|
+
const caseSensitive = Boolean(options.caseSensitive)
|
|
392
|
+
const regex = Boolean(options.regex)
|
|
393
|
+
|
|
394
|
+
if (!query) {
|
|
395
|
+
return {
|
|
396
|
+
active: false,
|
|
397
|
+
error: null,
|
|
398
|
+
test: () => true,
|
|
399
|
+
highlight: (text) => escapeHtml(text ?? ''),
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (regex) {
|
|
404
|
+
try {
|
|
405
|
+
const flags = caseSensitive ? 'g' : 'gi'
|
|
406
|
+
const rx = new RegExp(query, flags)
|
|
407
|
+
return {
|
|
408
|
+
active: true,
|
|
409
|
+
error: null,
|
|
410
|
+
test: (text) => {
|
|
411
|
+
const source = String(text ?? '')
|
|
412
|
+
const probe = new RegExp(rx.source, rx.flags)
|
|
413
|
+
return probe.test(source)
|
|
414
|
+
},
|
|
415
|
+
highlight: (text) => {
|
|
416
|
+
const source = String(text ?? '')
|
|
417
|
+
return escapeHtml(source).replace(
|
|
418
|
+
new RegExp(rx.source, rx.flags),
|
|
419
|
+
(m) => `<mark>${m}</mark>`,
|
|
420
|
+
)
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return {
|
|
425
|
+
active: true,
|
|
426
|
+
error: error instanceof Error ? error.message : 'Invalid regex',
|
|
427
|
+
test: () => false,
|
|
428
|
+
highlight: (text) => escapeHtml(text ?? ''),
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const needle = caseSensitive ? query : query.toLowerCase()
|
|
434
|
+
const flags = caseSensitive ? 'g' : 'gi'
|
|
435
|
+
const safe = escapeRegExp(query)
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
active: true,
|
|
439
|
+
error: null,
|
|
440
|
+
test: (text) => {
|
|
441
|
+
const source = String(text ?? '')
|
|
442
|
+
const hay = caseSensitive ? source : source.toLowerCase()
|
|
443
|
+
return hay.includes(needle)
|
|
444
|
+
},
|
|
445
|
+
highlight: (text) =>
|
|
446
|
+
escapeHtml(String(text ?? '')).replace(
|
|
447
|
+
new RegExp(safe, flags),
|
|
448
|
+
(m) => `<mark>${m}</mark>`,
|
|
449
|
+
),
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function toTokens(value) {
|
|
454
|
+
if (value === null || value === undefined) return []
|
|
455
|
+
if (typeof value === 'string') return [value]
|
|
456
|
+
if (typeof value === 'number' || typeof value === 'boolean') return [String(value)]
|
|
457
|
+
if (Array.isArray(value)) return value.flatMap((item) => toTokens(item))
|
|
458
|
+
if (typeof value === 'object') return [JSON.stringify(value)]
|
|
459
|
+
return []
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function flatSchemaToSearchTokens(flatSchema) {
|
|
463
|
+
if (!flatSchema || typeof flatSchema !== 'object') return []
|
|
464
|
+
|
|
465
|
+
const tokens = new Set()
|
|
466
|
+
|
|
467
|
+
Object.entries(flatSchema).forEach(([path, info]) => {
|
|
468
|
+
if (path) {
|
|
469
|
+
tokens.add(path)
|
|
470
|
+
path.split('.').forEach((segment) => {
|
|
471
|
+
if (segment) tokens.add(segment)
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (info && typeof info === 'object') {
|
|
476
|
+
tokens.add(JSON.stringify(info))
|
|
477
|
+
if (info.type) tokens.add(String(info.type))
|
|
478
|
+
if (info.kind) tokens.add(String(info.kind))
|
|
479
|
+
if (info.description) tokens.add(String(info.description))
|
|
480
|
+
if (Array.isArray(info.enumValues)) {
|
|
481
|
+
info.enumValues.forEach((value) => tokens.add(String(value)))
|
|
482
|
+
}
|
|
483
|
+
} else if (info !== null && info !== undefined) {
|
|
484
|
+
tokens.add(String(info))
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
tokens.add(JSON.stringify(flatSchema))
|
|
489
|
+
return Array.from(tokens)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function schemaSectionToSearchTokens(flatSchema, sectionName) {
|
|
493
|
+
if (!flatSchema || typeof flatSchema !== 'object') return []
|
|
494
|
+
const sectionEntries = Object.entries(flatSchema).filter(
|
|
495
|
+
([path]) => path === sectionName || path.startsWith(`${sectionName}.`),
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if (sectionEntries.length === 0) return []
|
|
499
|
+
return flatSchemaToSearchTokens(Object.fromEntries(sectionEntries))
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function typeFilterTokensByLeaf(flatSchema) {
|
|
503
|
+
if (!flatSchema || typeof flatSchema !== 'object') return { tokens: [], hasEnum: false }
|
|
504
|
+
|
|
505
|
+
const tokens = new Set()
|
|
506
|
+
let hasEnum = false
|
|
507
|
+
|
|
508
|
+
Object.values(flatSchema).forEach((info) => {
|
|
509
|
+
if (!info || typeof info !== 'object') return
|
|
510
|
+
if (info.type) tokens.add(String(info.type))
|
|
511
|
+
if (info.kind) tokens.add(String(info.kind))
|
|
512
|
+
if (Array.isArray(info.enumValues) && info.enumValues.length > 0) {
|
|
513
|
+
hasEnum = true
|
|
514
|
+
info.enumValues.forEach((value) => tokens.add(String(value)))
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
return { tokens: Array.from(tokens), hasEnum }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function selectedFieldIds() {
|
|
522
|
+
return SEARCH_FIELDS.filter((field) => {
|
|
523
|
+
const input = document.getElementById(`field-${field.id}`)
|
|
524
|
+
return Boolean(input && input.checked)
|
|
525
|
+
}).map((field) => field.id)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function matchesLeaf(leaf, engine, selectedIds) {
|
|
529
|
+
if (!engine.active) return true
|
|
530
|
+
if (selectedIds.length === 0) return false
|
|
531
|
+
const schemaFlatByLeaf = state.payload?.schemaFlatByLeaf || {}
|
|
532
|
+
|
|
533
|
+
return SEARCH_FIELDS.some((field) => {
|
|
534
|
+
if (!selectedIds.includes(field.id)) return false
|
|
535
|
+
const tokens = toTokens(field.get(leaf, schemaFlatByLeaf))
|
|
536
|
+
return tokens.some((token) => engine.test(token))
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function matchesTypeFilter(leaf, typeEngine, matchMode, hasEnumOnly) {
|
|
541
|
+
const schemaFlatByLeaf = state.payload?.schemaFlatByLeaf || {}
|
|
542
|
+
const { tokens, hasEnum } = typeFilterTokensByLeaf(schemaFlatByLeaf?.[leaf.key])
|
|
543
|
+
|
|
544
|
+
if (hasEnumOnly && !hasEnum) return false
|
|
545
|
+
if (!typeEngine.active) return true
|
|
546
|
+
|
|
547
|
+
if (matchMode === 'exact') {
|
|
548
|
+
const sourceTokens = typeEngine.caseSensitive
|
|
549
|
+
? tokens
|
|
550
|
+
: tokens.map((token) => token.toLowerCase())
|
|
551
|
+
const needle = typeEngine.caseSensitive ? typeEngine.query : typeEngine.query.toLowerCase()
|
|
552
|
+
return sourceTokens.includes(needle)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return tokens.some((token) => typeEngine.test(token))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function createTypeSearchEngine(queryRaw) {
|
|
559
|
+
const query = queryRaw || ''
|
|
560
|
+
const caseSensitive = Boolean(caseSensitiveInput.checked)
|
|
561
|
+
if (!query) return { active: false, query, caseSensitive, test: () => true }
|
|
562
|
+
|
|
563
|
+
const needle = caseSensitive ? query : query.toLowerCase()
|
|
564
|
+
return {
|
|
565
|
+
active: true,
|
|
566
|
+
query,
|
|
567
|
+
caseSensitive,
|
|
568
|
+
test: (text) => {
|
|
569
|
+
const source = String(text ?? '')
|
|
570
|
+
const hay = caseSensitive ? source : source.toLowerCase()
|
|
571
|
+
return hay.includes(needle)
|
|
572
|
+
},
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function el(tag, className, text) {
|
|
577
|
+
const node = document.createElement(tag)
|
|
578
|
+
if (className) node.className = className
|
|
579
|
+
if (text !== undefined) node.textContent = text
|
|
580
|
+
return node
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function setHighlighted(node, text, engine) {
|
|
584
|
+
node.innerHTML = engine.highlight(text ?? '')
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function kv(key, value, engine) {
|
|
588
|
+
const box = el('div', 'kv')
|
|
589
|
+
box.appendChild(el('div', 'k', key))
|
|
590
|
+
const valueNode = el('div', 'v mono')
|
|
591
|
+
setHighlighted(valueNode, value === undefined ? '—' : String(value), engine)
|
|
592
|
+
box.appendChild(valueNode)
|
|
593
|
+
return box
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function renderSchemaSummary(name, schema, engine) {
|
|
597
|
+
const row = el('div', 'kv')
|
|
598
|
+
row.appendChild(el('div', 'k', name))
|
|
599
|
+
|
|
600
|
+
const valueNode = el('div', 'v mono')
|
|
601
|
+
if (!schema) {
|
|
602
|
+
setHighlighted(valueNode, 'not defined', engine)
|
|
603
|
+
row.appendChild(valueNode)
|
|
604
|
+
return row
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const parts = [schema.kind]
|
|
608
|
+
if (schema.optional) parts.push('optional')
|
|
609
|
+
if (schema.nullable) parts.push('nullable')
|
|
610
|
+
if (Array.isArray(schema.enumValues) && schema.enumValues.length > 0) {
|
|
611
|
+
parts.push(`enum: ${schema.enumValues.join('|')}`)
|
|
612
|
+
}
|
|
613
|
+
if (schema.properties) {
|
|
614
|
+
parts.push(`properties: ${Object.keys(schema.properties).length}`)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
setHighlighted(valueNode, parts.join(' | '), engine)
|
|
618
|
+
row.appendChild(valueNode)
|
|
619
|
+
return row
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function splitFlatSchemaBySection(flatSchema) {
|
|
623
|
+
const result = {
|
|
624
|
+
params: {},
|
|
625
|
+
query: {},
|
|
626
|
+
body: {},
|
|
627
|
+
output: {},
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!flatSchema) return result
|
|
631
|
+
|
|
632
|
+
Object.entries(flatSchema).forEach(([path, info]) => {
|
|
633
|
+
const section = SCHEMA_SECTIONS.find(
|
|
634
|
+
(name) => path === name || path.startsWith(`${name}.`),
|
|
635
|
+
)
|
|
636
|
+
if (!section) return
|
|
637
|
+
result[section][path] = info
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
return result
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function createTreeNode(name = '') {
|
|
644
|
+
return {
|
|
645
|
+
name,
|
|
646
|
+
info: null,
|
|
647
|
+
fullPath: null,
|
|
648
|
+
children: {},
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function buildSchemaTree(entries, sectionName) {
|
|
653
|
+
const root = createTreeNode(sectionName)
|
|
654
|
+
Object.entries(entries)
|
|
655
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
656
|
+
.forEach(([fullPath, info]) => {
|
|
657
|
+
const trimmed = fullPath === sectionName ? '' : fullPath.slice(sectionName.length + 1)
|
|
658
|
+
const segments = trimmed ? trimmed.split('.') : []
|
|
659
|
+
|
|
660
|
+
let current = root
|
|
661
|
+
segments.forEach((segment) => {
|
|
662
|
+
if (!current.children[segment]) {
|
|
663
|
+
current.children[segment] = createTreeNode(segment)
|
|
664
|
+
}
|
|
665
|
+
current = current.children[segment]
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
current.info = info
|
|
669
|
+
current.fullPath = fullPath
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
return root
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function renderTreeNode(node, engine, isRoot) {
|
|
676
|
+
const childKeys = Object.keys(node.children)
|
|
677
|
+
const hasChildren = childKeys.length > 0
|
|
678
|
+
const hasInfo = Boolean(node.info)
|
|
679
|
+
const appendNameCell = (row) => {
|
|
680
|
+
const nameWrap = el('span', 'mono tree-name')
|
|
681
|
+
const nameNode = el('span')
|
|
682
|
+
setHighlighted(nameNode, node.name, engine)
|
|
683
|
+
nameWrap.appendChild(nameNode)
|
|
684
|
+
|
|
685
|
+
if (node.info && !node.info.optional) {
|
|
686
|
+
const star = el('span', 'required-star', '*')
|
|
687
|
+
nameWrap.appendChild(star)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (node.info?.nullable) {
|
|
691
|
+
nameWrap.appendChild(el('span', 'tree-col-muted', '-'))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
row.appendChild(nameWrap)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const appendTypeCell = (row, info) => {
|
|
698
|
+
const type = el('span', 'tree-pill')
|
|
699
|
+
setHighlighted(type, info?.type || info?.kind || '—', engine)
|
|
700
|
+
row.appendChild(type)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (isRoot || hasChildren) {
|
|
704
|
+
const details = el('details')
|
|
705
|
+
details.open = true
|
|
706
|
+
const summary = el('summary')
|
|
707
|
+
const row = el('div', 'tree-row')
|
|
708
|
+
|
|
709
|
+
appendNameCell(row)
|
|
710
|
+
appendTypeCell(row, hasInfo ? node.info : null)
|
|
711
|
+
|
|
712
|
+
summary.appendChild(row)
|
|
713
|
+
details.appendChild(summary)
|
|
714
|
+
|
|
715
|
+
const container = el('div', 'schema-tree')
|
|
716
|
+
childKeys
|
|
717
|
+
.sort((a, b) => a.localeCompare(b))
|
|
718
|
+
.forEach((key) => container.appendChild(renderTreeNode(node.children[key], engine, false)))
|
|
719
|
+
details.appendChild(container)
|
|
720
|
+
return details
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const row = el('div', 'tree-row')
|
|
724
|
+
appendNameCell(row)
|
|
725
|
+
appendTypeCell(row, node.info)
|
|
726
|
+
|
|
727
|
+
return row
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function renderSeparatedSchemas(flatSchema, engine) {
|
|
731
|
+
const section = el('div', 'section')
|
|
732
|
+
section.appendChild(el('h3', '', 'Schemas (separated by section)'))
|
|
733
|
+
|
|
734
|
+
const grouped = splitFlatSchemaBySection(flatSchema)
|
|
735
|
+
|
|
736
|
+
SCHEMA_SECTIONS.forEach((sectionName) => {
|
|
737
|
+
const block = el('div', 'schema-block')
|
|
738
|
+
const header = el('div', 'schema-header mono')
|
|
739
|
+
setHighlighted(header, sectionName, engine)
|
|
740
|
+
block.appendChild(header)
|
|
741
|
+
|
|
742
|
+
const entries = grouped[sectionName]
|
|
743
|
+
if (!entries || Object.keys(entries).length === 0) {
|
|
744
|
+
block.appendChild(el('div', 'empty', 'No entries'))
|
|
745
|
+
} else {
|
|
746
|
+
const tree = buildSchemaTree(entries, sectionName)
|
|
747
|
+
block.appendChild(renderTreeNode(tree, engine, true))
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
section.appendChild(block)
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
return section
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function renderLeaf(leaf, engine) {
|
|
757
|
+
const details = el('details', 'leaf')
|
|
758
|
+
const summary = el('summary')
|
|
759
|
+
setHighlighted(summary, `${String(leaf.method || '').toUpperCase()} ${leaf.path || ''}`, engine)
|
|
760
|
+
details.appendChild(summary)
|
|
761
|
+
|
|
762
|
+
const content = el('div', 'leaf-content')
|
|
763
|
+
const cfg = leaf.cfg || {}
|
|
764
|
+
const flatSchema = state.payload?.schemaFlatByLeaf?.[leaf.key]
|
|
765
|
+
|
|
766
|
+
const overview = el('div', 'section')
|
|
767
|
+
overview.appendChild(el('h3', '', 'Overview'))
|
|
768
|
+
const grid = el('div', 'grid-2')
|
|
769
|
+
grid.appendChild(kv('key', leaf.key, engine))
|
|
770
|
+
grid.appendChild(kv('method', leaf.method, engine))
|
|
771
|
+
grid.appendChild(kv('path', leaf.path, engine))
|
|
772
|
+
grid.appendChild(kv('group', cfg.docsGroup, engine))
|
|
773
|
+
grid.appendChild(kv('stability', cfg.stability, engine))
|
|
774
|
+
grid.appendChild(kv('feed', cfg.feed ? 'true' : 'false', engine))
|
|
775
|
+
grid.appendChild(kv('deprecated', cfg.deprecated ? 'true' : 'false', engine))
|
|
776
|
+
grid.appendChild(kv('hidden', cfg.docsHidden ? 'true' : 'false', engine))
|
|
777
|
+
overview.appendChild(grid)
|
|
778
|
+
content.appendChild(overview)
|
|
779
|
+
|
|
780
|
+
const docs = el('div', 'section')
|
|
781
|
+
docs.appendChild(el('h3', '', 'Documentation'))
|
|
782
|
+
const docGrid = el('div', 'grid-2')
|
|
783
|
+
docGrid.appendChild(kv('summary', cfg.summary, engine))
|
|
784
|
+
docGrid.appendChild(kv('description', cfg.description, engine))
|
|
785
|
+
docs.appendChild(docGrid)
|
|
786
|
+
|
|
787
|
+
const tagsRow = el('div', 'chips')
|
|
788
|
+
;(cfg.tags || []).forEach((tag) => {
|
|
789
|
+
const chip = el('span', 'chip')
|
|
790
|
+
setHighlighted(chip, tag, engine)
|
|
791
|
+
tagsRow.appendChild(chip)
|
|
792
|
+
})
|
|
793
|
+
if ((cfg.tags || []).length === 0) {
|
|
794
|
+
tagsRow.appendChild(el('span', 'empty', 'No tags'))
|
|
795
|
+
}
|
|
796
|
+
docs.appendChild(tagsRow)
|
|
797
|
+
|
|
798
|
+
if (cfg.docsMeta && Object.keys(cfg.docsMeta).length > 0) {
|
|
799
|
+
Object.entries(cfg.docsMeta).forEach(([k, v]) => {
|
|
800
|
+
docs.appendChild(kv(`meta.${k}`, typeof v === 'string' ? v : JSON.stringify(v), engine))
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
content.appendChild(docs)
|
|
805
|
+
|
|
806
|
+
const schemas = el('div', 'section')
|
|
807
|
+
schemas.appendChild(el('h3', '', 'Schema Summaries'))
|
|
808
|
+
const schemaGrid = el('div', 'grid-2')
|
|
809
|
+
const schemaObj = cfg.schemas || {}
|
|
810
|
+
schemaGrid.appendChild(renderSchemaSummary('params', schemaObj.params, engine))
|
|
811
|
+
schemaGrid.appendChild(renderSchemaSummary('query', schemaObj.query, engine))
|
|
812
|
+
schemaGrid.appendChild(renderSchemaSummary('body', schemaObj.body, engine))
|
|
813
|
+
schemaGrid.appendChild(renderSchemaSummary('output', schemaObj.output, engine))
|
|
814
|
+
schemaGrid.appendChild(renderSchemaSummary('outputMeta', schemaObj.outputMeta, engine))
|
|
815
|
+
schemaGrid.appendChild(renderSchemaSummary('queryExtension', schemaObj.queryExtension, engine))
|
|
816
|
+
schemas.appendChild(schemaGrid)
|
|
817
|
+
content.appendChild(schemas)
|
|
818
|
+
|
|
819
|
+
const files = el('div', 'section')
|
|
820
|
+
files.appendChild(el('h3', '', 'Body Files'))
|
|
821
|
+
if (Array.isArray(cfg.bodyFiles) && cfg.bodyFiles.length > 0) {
|
|
822
|
+
const chips = el('div', 'chips')
|
|
823
|
+
cfg.bodyFiles.forEach((file) => {
|
|
824
|
+
const chip = el('span', 'chip ok')
|
|
825
|
+
setHighlighted(chip, `${file.name} (max ${file.maxCount})`, engine)
|
|
826
|
+
chips.appendChild(chip)
|
|
827
|
+
})
|
|
828
|
+
files.appendChild(chips)
|
|
829
|
+
} else {
|
|
830
|
+
files.appendChild(el('div', 'empty', 'No file upload fields.'))
|
|
831
|
+
}
|
|
832
|
+
content.appendChild(files)
|
|
833
|
+
|
|
834
|
+
content.appendChild(renderSeparatedSchemas(flatSchema, engine))
|
|
835
|
+
|
|
836
|
+
details.appendChild(content)
|
|
837
|
+
return details
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function renderResults() {
|
|
841
|
+
const engine = createSearchEngine(searchInput.value.trim(), {
|
|
842
|
+
caseSensitive: caseSensitiveInput.checked,
|
|
843
|
+
regex: regexSearchInput.checked,
|
|
844
|
+
})
|
|
845
|
+
const typeEngine = createTypeSearchEngine(typeFilterInput.value.trim())
|
|
846
|
+
const hasEnumOnly = Boolean(hasEnumOnlyInput.checked)
|
|
847
|
+
const typeMatchMode = typeMatchModeInput.value === 'exact' ? 'exact' : 'contains'
|
|
848
|
+
|
|
849
|
+
if (engine.error) {
|
|
850
|
+
statusEl.textContent = `Invalid regex: ${engine.error}`
|
|
851
|
+
resultsEl.innerHTML = ''
|
|
852
|
+
resultsEl.appendChild(el('div', 'empty', 'Fix the regex to continue.'))
|
|
853
|
+
renderActiveFilterChips({
|
|
854
|
+
selectedIds: selectedFieldIds(),
|
|
855
|
+
hasRegexError: true,
|
|
856
|
+
})
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const selectedIds = selectedFieldIds()
|
|
861
|
+
const filtered = state.leaves.filter(
|
|
862
|
+
(leaf) =>
|
|
863
|
+
matchesLeaf(leaf, engine, selectedIds) &&
|
|
864
|
+
matchesTypeFilter(leaf, typeEngine, typeMatchMode, hasEnumOnly),
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
|
|
868
|
+
resultsEl.innerHTML = ''
|
|
869
|
+
renderActiveFilterChips({ selectedIds, hasRegexError: false })
|
|
870
|
+
|
|
871
|
+
if (filtered.length === 0) {
|
|
872
|
+
resultsEl.appendChild(el('div', 'empty', 'No matches.'))
|
|
873
|
+
syncFilterStateToUrl()
|
|
874
|
+
return
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
filtered.forEach((leaf) => {
|
|
878
|
+
resultsEl.appendChild(renderLeaf(leaf, engine))
|
|
879
|
+
})
|
|
880
|
+
syncFilterStateToUrl()
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function renderFieldCheckboxes() {
|
|
884
|
+
fieldCheckboxes.innerHTML = ''
|
|
885
|
+
SEARCH_FIELDS.forEach((field) => {
|
|
886
|
+
const label = el('label', 'field-item')
|
|
887
|
+
const input = document.createElement('input')
|
|
888
|
+
input.type = 'checkbox'
|
|
889
|
+
input.id = `field-${field.id}`
|
|
890
|
+
input.checked = true
|
|
891
|
+
input.addEventListener('change', renderResults)
|
|
892
|
+
label.appendChild(input)
|
|
893
|
+
label.appendChild(el('span', '', field.label))
|
|
894
|
+
fieldCheckboxes.appendChild(label)
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function setFieldSelection(allowedIds) {
|
|
899
|
+
SEARCH_FIELDS.forEach((field) => {
|
|
900
|
+
const input = document.getElementById(`field-${field.id}`)
|
|
901
|
+
if (!input) return
|
|
902
|
+
input.checked = allowedIds.has(field.id)
|
|
903
|
+
})
|
|
904
|
+
renderResults()
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function resetFiltersToDefault() {
|
|
908
|
+
searchInput.value = ''
|
|
909
|
+
typeFilterInput.value = ''
|
|
910
|
+
caseSensitiveInput.checked = false
|
|
911
|
+
regexSearchInput.checked = false
|
|
912
|
+
hasEnumOnlyInput.checked = false
|
|
913
|
+
typeMatchModeInput.value = 'contains'
|
|
914
|
+
setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)))
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function renderActiveFilterChips({ selectedIds, hasRegexError }) {
|
|
918
|
+
activeFilterChips.innerHTML = ''
|
|
919
|
+
const chips = []
|
|
920
|
+
|
|
921
|
+
const searchValue = searchInput.value.trim()
|
|
922
|
+
const typeValue = typeFilterInput.value.trim()
|
|
923
|
+
|
|
924
|
+
if (searchValue) chips.push(`search: ${searchValue}`)
|
|
925
|
+
if (typeValue) chips.push(`type: ${typeValue}`)
|
|
926
|
+
if (typeMatchModeInput.value === 'exact') chips.push('type mode: exact')
|
|
927
|
+
if (caseSensitiveInput.checked) chips.push('case sensitive')
|
|
928
|
+
if (regexSearchInput.checked) chips.push('regex')
|
|
929
|
+
if (hasEnumOnlyInput.checked) chips.push('has enum only')
|
|
930
|
+
if (hasRegexError) chips.push('regex error')
|
|
931
|
+
|
|
932
|
+
const allIds = SEARCH_FIELDS.map((field) => field.id)
|
|
933
|
+
if (selectedIds.length !== allIds.length) {
|
|
934
|
+
chips.push(`fields: ${selectedIds.join(', ') || 'none'}`)
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (chips.length === 0) {
|
|
938
|
+
activeFilterChips.appendChild(el('span', 'empty', 'No active filters'))
|
|
939
|
+
return
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
chips.forEach((chipText) => {
|
|
943
|
+
const chip = el('span', 'chip')
|
|
944
|
+
chip.textContent = chipText
|
|
945
|
+
activeFilterChips.appendChild(chip)
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function collectFilterState() {
|
|
950
|
+
return {
|
|
951
|
+
search: searchInput.value,
|
|
952
|
+
typeFilter: typeFilterInput.value,
|
|
953
|
+
caseSensitive: caseSensitiveInput.checked,
|
|
954
|
+
regex: regexSearchInput.checked,
|
|
955
|
+
hasEnumOnly: hasEnumOnlyInput.checked,
|
|
956
|
+
typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
|
|
957
|
+
selectedFields: selectedFieldIds(),
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function syncFilterStateToUrl() {
|
|
962
|
+
if (isHydratingFromUrl) return
|
|
963
|
+
|
|
964
|
+
const data = collectFilterState()
|
|
965
|
+
const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
|
|
966
|
+
const serialized = encodeURIComponent(JSON.stringify(data))
|
|
967
|
+
params.set(URL_PARAM_KEY, serialized)
|
|
968
|
+
const nextHash = params.toString()
|
|
969
|
+
if (window.location.hash !== `#${nextHash}`) {
|
|
970
|
+
window.location.hash = nextHash
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function hydrateFilterStateFromUrl() {
|
|
975
|
+
const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
|
|
976
|
+
const raw = params.get(URL_PARAM_KEY)
|
|
977
|
+
if (!raw) return
|
|
978
|
+
|
|
979
|
+
try {
|
|
980
|
+
const parsed = JSON.parse(decodeURIComponent(raw))
|
|
981
|
+
isHydratingFromUrl = true
|
|
982
|
+
if (typeof parsed.search === 'string') searchInput.value = parsed.search
|
|
983
|
+
if (typeof parsed.typeFilter === 'string') typeFilterInput.value = parsed.typeFilter
|
|
984
|
+
caseSensitiveInput.checked = Boolean(parsed.caseSensitive)
|
|
985
|
+
regexSearchInput.checked = Boolean(parsed.regex)
|
|
986
|
+
hasEnumOnlyInput.checked = Boolean(parsed.hasEnumOnly)
|
|
987
|
+
if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
|
|
988
|
+
typeMatchModeInput.value = parsed.typeMatchMode
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (Array.isArray(parsed.selectedFields)) {
|
|
992
|
+
const allowed = new Set(parsed.selectedFields)
|
|
993
|
+
SEARCH_FIELDS.forEach((field) => {
|
|
994
|
+
const input = document.getElementById(`field-${field.id}`)
|
|
995
|
+
if (!input) return
|
|
996
|
+
input.checked = allowed.has(field.id)
|
|
997
|
+
})
|
|
998
|
+
}
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
// Ignore malformed hash state.
|
|
1001
|
+
} finally {
|
|
1002
|
+
isHydratingFromUrl = false
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function handleFile(file) {
|
|
1007
|
+
const text = await file.text()
|
|
1008
|
+
const parsed = JSON.parse(text)
|
|
1009
|
+
if (!parsed || !Array.isArray(parsed.leaves)) {
|
|
1010
|
+
throw new Error('Invalid export file: expected top-level "leaves" array.')
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
state.payload = parsed
|
|
1014
|
+
state.leaves = parsed.leaves
|
|
1015
|
+
renderResults()
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function initializeFromBakedPayload() {
|
|
1019
|
+
const baked = window.__FINALIZED_LEAVES_PAYLOAD
|
|
1020
|
+
if (!baked || !Array.isArray(baked.leaves)) return
|
|
1021
|
+
|
|
1022
|
+
state.payload = baked
|
|
1023
|
+
state.leaves = baked.leaves
|
|
1024
|
+
statusEl.textContent = `Loaded baked payload with ${state.leaves.length} routes.`
|
|
1025
|
+
renderResults()
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
fileInput.addEventListener('change', async (event) => {
|
|
1029
|
+
const file = event.target.files?.[0]
|
|
1030
|
+
if (!file) return
|
|
1031
|
+
|
|
1032
|
+
try {
|
|
1033
|
+
await handleFile(file)
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
statusEl.textContent = error instanceof Error ? error.message : String(error)
|
|
1036
|
+
resultsEl.innerHTML = ''
|
|
1037
|
+
}
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
searchInput.addEventListener('input', renderResults)
|
|
1041
|
+
typeFilterInput.addEventListener('input', renderResults)
|
|
1042
|
+
caseSensitiveInput.addEventListener('change', renderResults)
|
|
1043
|
+
regexSearchInput.addEventListener('change', renderResults)
|
|
1044
|
+
hasEnumOnlyInput.addEventListener('change', renderResults)
|
|
1045
|
+
typeMatchModeInput.addEventListener('change', renderResults)
|
|
1046
|
+
|
|
1047
|
+
selectAllFieldsBtn.addEventListener('click', () =>
|
|
1048
|
+
setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id))),
|
|
1049
|
+
)
|
|
1050
|
+
clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
|
|
1051
|
+
schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_FIELD_IDS))
|
|
1052
|
+
metadataOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(METADATA_FIELD_IDS))
|
|
1053
|
+
resetFiltersBtn.addEventListener('click', resetFiltersToDefault)
|
|
1054
|
+
|
|
1055
|
+
renderFieldCheckboxes()
|
|
1056
|
+
hydrateFilterStateFromUrl()
|
|
1057
|
+
initializeFromBakedPayload()
|
|
1058
|
+
renderResults()
|
|
1059
|
+
</script>
|
|
1060
|
+
</body>
|
|
1061
|
+
</html>
|