@vnphu/nestjs-api-explorer 0.1.0 → 0.2.1
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 +409 -77
- package/dist/api-docs-parser.d.ts +24 -0
- package/dist/api-docs-parser.d.ts.map +1 -0
- package/dist/api-docs-parser.js +234 -0
- package/dist/api-docs-parser.js.map +1 -0
- package/dist/api-explorer.html.d.ts.map +1 -1
- package/dist/api-explorer.html.js +610 -22
- package/dist/api-explorer.html.js.map +1 -1
- package/dist/api-explorer.module.d.ts +27 -4
- package/dist/api-explorer.module.d.ts.map +1 -1
- package/dist/api-explorer.module.js +10 -1
- package/dist/api-explorer.module.js.map +1 -1
- package/dist/api-explorer.service.d.ts +12 -0
- package/dist/api-explorer.service.d.ts.map +1 -1
- package/dist/api-explorer.service.js +44 -26
- package/dist/api-explorer.service.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -106,9 +106,53 @@ function getExplorerHtml(options) {
|
|
|
106
106
|
width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
|
|
107
107
|
border-radius: var(--radius); border: 1.5px solid var(--border);
|
|
108
108
|
background: var(--bg-white); color: var(--text-muted);
|
|
109
|
-
cursor: pointer; transition: all .15s; flex-shrink: 0;
|
|
109
|
+
cursor: pointer; transition: all .15s; flex-shrink: 0; position: relative;
|
|
110
110
|
}
|
|
111
111
|
.icon-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
|
|
112
|
+
.icon-btn.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
|
|
113
|
+
.icon-btn .dot {
|
|
114
|
+
position: absolute; top: -3px; right: -3px;
|
|
115
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
116
|
+
background: var(--get-fg); border: 2px solid var(--bg-white);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── Global Auth Dropdown ── */
|
|
120
|
+
#global-auth-dropdown {
|
|
121
|
+
position: absolute; top: 56px; right: 16px; z-index: 100;
|
|
122
|
+
width: 340px; background: var(--bg-white);
|
|
123
|
+
border: 1.5px solid var(--border); border-radius: var(--radius);
|
|
124
|
+
box-shadow: var(--shadow); padding: 16px;
|
|
125
|
+
}
|
|
126
|
+
#global-auth-dropdown.hidden { display: none; }
|
|
127
|
+
.gauth-title {
|
|
128
|
+
font-size: 12px; font-weight: 700; color: var(--text);
|
|
129
|
+
margin-bottom: 12px; display: flex; align-items: center; gap: 8px;
|
|
130
|
+
}
|
|
131
|
+
.gauth-title svg { color: var(--accent); }
|
|
132
|
+
.gauth-active-badge {
|
|
133
|
+
font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 10px;
|
|
134
|
+
background: var(--get-bg); border: 1px solid var(--get-bdr); color: var(--get-fg);
|
|
135
|
+
}
|
|
136
|
+
.gauth-type-row { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
137
|
+
.gauth-type-btn {
|
|
138
|
+
padding: 4px 11px; border-radius: 20px; border: 1.5px solid var(--border);
|
|
139
|
+
background: var(--bg-input); color: var(--text-muted);
|
|
140
|
+
font-size: 11px; font-weight: 500; cursor: pointer; transition: all .12s;
|
|
141
|
+
}
|
|
142
|
+
.gauth-type-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
143
|
+
.gauth-type-btn.active { border-color: var(--accent); background: var(--accent-bg); color: var(--accent); font-weight: 600; }
|
|
144
|
+
.gauth-footer { margin-top: 14px; display: flex; gap: 8px; }
|
|
145
|
+
.gauth-save-btn {
|
|
146
|
+
flex: 1; height: 32px; background: var(--accent); color: #fff; border: none;
|
|
147
|
+
border-radius: var(--radius-sm); font-size: 12px; font-weight: 600; cursor: pointer; transition: background .15s;
|
|
148
|
+
}
|
|
149
|
+
.gauth-save-btn:hover { background: #2563eb; }
|
|
150
|
+
.gauth-clear-btn {
|
|
151
|
+
height: 32px; padding: 0 12px; background: var(--bg-input); color: var(--text-muted);
|
|
152
|
+
border: 1.5px solid var(--border); border-radius: var(--radius-sm);
|
|
153
|
+
font-size: 12px; cursor: pointer; transition: all .15s;
|
|
154
|
+
}
|
|
155
|
+
.gauth-clear-btn:hover { border-color: var(--del-fg); color: var(--del-fg); }
|
|
112
156
|
|
|
113
157
|
/* ── Main ── */
|
|
114
158
|
#main { display: flex; flex: 1; overflow: hidden; }
|
|
@@ -137,9 +181,33 @@ function getExplorerHtml(options) {
|
|
|
137
181
|
#search::placeholder { color: var(--text-dim); }
|
|
138
182
|
.search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-dim); pointer-events: none; }
|
|
139
183
|
|
|
140
|
-
#route-list { flex: 1; overflow-y: auto; padding:
|
|
184
|
+
#route-list { flex: 1; overflow-y: auto; padding: 4px 8px 16px; min-height: 0; }
|
|
141
185
|
.route-count { font-size: 10px; color: var(--text-dim); padding: 8px 6px 4px; font-weight: 500; }
|
|
142
186
|
|
|
187
|
+
.route-group { margin-bottom: 4px; }
|
|
188
|
+
.route-group-header {
|
|
189
|
+
display: flex; align-items: center; gap: 6px;
|
|
190
|
+
padding: 6px 8px 4px; cursor: pointer; user-select: none;
|
|
191
|
+
border-radius: var(--radius-sm);
|
|
192
|
+
transition: background .12s;
|
|
193
|
+
}
|
|
194
|
+
.route-group-header:hover { background: var(--bg-hover); }
|
|
195
|
+
.route-group-name {
|
|
196
|
+
font-size: 11px; font-weight: 700; color: var(--text);
|
|
197
|
+
text-transform: uppercase; letter-spacing: 0.5px; flex: 1;
|
|
198
|
+
}
|
|
199
|
+
.route-group-count {
|
|
200
|
+
font-size: 10px; font-weight: 600; color: var(--text-dim);
|
|
201
|
+
background: var(--bg-hover); border: 1px solid var(--border);
|
|
202
|
+
padding: 1px 6px; border-radius: 10px;
|
|
203
|
+
}
|
|
204
|
+
.route-group-chevron {
|
|
205
|
+
color: var(--text-dim); transition: transform .2s; flex-shrink: 0;
|
|
206
|
+
}
|
|
207
|
+
.route-group-chevron.collapsed { transform: rotate(-90deg); }
|
|
208
|
+
.route-group-body { padding-left: 4px; }
|
|
209
|
+
.route-group-body.collapsed { display: none; }
|
|
210
|
+
|
|
143
211
|
.route-item {
|
|
144
212
|
display: flex; align-items: center; gap: 8px;
|
|
145
213
|
padding: 7px 9px; border-radius: var(--radius-sm);
|
|
@@ -166,6 +234,10 @@ function getExplorerHtml(options) {
|
|
|
166
234
|
.method-HEAD { background: var(--head-bg); border-color: var(--head-bdr); color: var(--head-fg); }
|
|
167
235
|
.method-OPTIONS{ background: var(--opt-bg); border-color: var(--opt-bdr); color: var(--opt-fg); }
|
|
168
236
|
|
|
237
|
+
.route-desc {
|
|
238
|
+
display: block; font-size: 10px; color: var(--text-dim); margin-top: 1px;
|
|
239
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;
|
|
240
|
+
}
|
|
169
241
|
.route-path {
|
|
170
242
|
font-family: var(--font-mono); font-size: 11.5px; color: var(--text-muted);
|
|
171
243
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
|
|
@@ -177,7 +249,81 @@ function getExplorerHtml(options) {
|
|
|
177
249
|
#sidebar-resize:hover, #sidebar-resize.dragging { background: var(--accent); opacity: .4; }
|
|
178
250
|
|
|
179
251
|
/* ── Content ── */
|
|
180
|
-
#content { flex: 1; display: flex; flex-direction:
|
|
252
|
+
#content { flex: 1; display: flex; flex-direction: row; overflow: hidden; min-width: 0; }
|
|
253
|
+
#content-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
|
254
|
+
|
|
255
|
+
/* ── Summary Panel ── */
|
|
256
|
+
#summary-panel {
|
|
257
|
+
width: 400px; flex-shrink: 0; border-left: 1px solid var(--border);
|
|
258
|
+
background: var(--bg-white); display: flex; flex-direction: column; overflow: hidden;
|
|
259
|
+
}
|
|
260
|
+
#summary-panel.hidden { display: none; }
|
|
261
|
+
.summary-header {
|
|
262
|
+
padding: 10px 14px 8px; border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
263
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
264
|
+
}
|
|
265
|
+
.summary-header-title { font-size: 11px; font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
266
|
+
.summary-copy-btn {
|
|
267
|
+
display: flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 500;
|
|
268
|
+
color: var(--text-muted); background: none; border: 1px solid var(--border);
|
|
269
|
+
border-radius: var(--radius-sm); padding: 3px 8px; cursor: pointer; transition: all 0.15s;
|
|
270
|
+
}
|
|
271
|
+
.summary-copy-btn:hover { background: var(--bg-input); color: var(--accent); border-color: var(--accent); }
|
|
272
|
+
.summary-copy-btn.copied { color: #16a34a; border-color: #86efac; background: #f0fdf4; }
|
|
273
|
+
#summary-body { flex: 1; overflow-y: auto; padding: 10px 14px 16px; display: flex; flex-direction: column; gap: 14px; }
|
|
274
|
+
.summary-section { display: flex; flex-direction: column; gap: 6px; }
|
|
275
|
+
.summary-section-label {
|
|
276
|
+
font-size: 10px; font-weight: 700; color: var(--text-dim);
|
|
277
|
+
text-transform: uppercase; letter-spacing: 0.7px;
|
|
278
|
+
}
|
|
279
|
+
.summary-url {
|
|
280
|
+
font-family: var(--font-mono); font-size: 11px; color: var(--text);
|
|
281
|
+
word-break: break-all; line-height: 1.6; background: var(--bg-input);
|
|
282
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 8px;
|
|
283
|
+
}
|
|
284
|
+
.summary-url .url-path-param { color: var(--put-fg); font-weight: 600; }
|
|
285
|
+
.summary-row {
|
|
286
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
|
|
287
|
+
font-size: 11px; padding: 3px 0; border-bottom: 1px dashed var(--border);
|
|
288
|
+
}
|
|
289
|
+
.summary-row:last-child { border-bottom: none; }
|
|
290
|
+
.summary-key { font-family: var(--font-mono); color: var(--accent); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
291
|
+
.summary-val { font-family: var(--font-mono); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
292
|
+
.summary-none { font-size: 11px; color: var(--text-dim); font-style: italic; }
|
|
293
|
+
.summary-method-pill {
|
|
294
|
+
display: inline-block; font-family: var(--font-mono); font-size: 10px; font-weight: 700;
|
|
295
|
+
padding: 2px 7px; border-radius: 4px; margin-right: 6px; vertical-align: middle;
|
|
296
|
+
border-width: 1px; border-style: solid;
|
|
297
|
+
}
|
|
298
|
+
.summary-auth-row {
|
|
299
|
+
display: flex; align-items: center; gap: 6px; font-size: 11px;
|
|
300
|
+
padding: 4px 8px; background: var(--bg-input); border-radius: var(--radius-sm);
|
|
301
|
+
border: 1px solid var(--border);
|
|
302
|
+
}
|
|
303
|
+
.summary-auth-icon { color: var(--accent); flex-shrink: 0; }
|
|
304
|
+
.summary-auth-label { color: var(--text-muted); }
|
|
305
|
+
.summary-auth-val { font-family: var(--font-mono); color: var(--text); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
306
|
+
.summary-body-preview {
|
|
307
|
+
font-family: var(--font-mono); font-size: 11px; color: var(--text-muted);
|
|
308
|
+
background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
309
|
+
padding: 6px 8px; max-height: 100px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; line-height: 1.5;
|
|
310
|
+
}
|
|
311
|
+
.summary-empty {
|
|
312
|
+
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
313
|
+
gap: 8px; padding: 20px; color: var(--text-dim); text-align: center;
|
|
314
|
+
}
|
|
315
|
+
.summary-empty p { font-size: 11px; line-height: 1.6; }
|
|
316
|
+
/* ── DocField rows (body/query/headers/response from docs file) ── */
|
|
317
|
+
.df-row { padding: 5px 0; border-bottom: 1px dashed var(--border); }
|
|
318
|
+
.df-row:last-child { border-bottom: none; }
|
|
319
|
+
.df-top { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
|
|
320
|
+
.df-bottom { margin-top: 2px; }
|
|
321
|
+
.df-name { font-family: var(--font-mono); font-size: 11px; font-weight: 600; color: var(--accent); }
|
|
322
|
+
.df-type { font-size: 9px; font-weight: 600; padding: 1px 5px; border-radius: 3px; background: var(--bg-input); border: 1px solid var(--border); color: var(--text-muted); }
|
|
323
|
+
.df-req { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; background: #fef2f2; border: 1px solid #fecaca; color: var(--del-fg); }
|
|
324
|
+
.df-opt { font-size: 9px; font-weight: 500; padding: 1px 5px; border-radius: 3px; background: var(--bg-input); border: 1px solid var(--border); color: var(--text-dim); }
|
|
325
|
+
.df-rules { font-size: 10px; color: var(--text-dim); font-family: var(--font-mono); }
|
|
326
|
+
.df-desc { font-size: 10px; color: var(--text-muted); font-style: italic; }
|
|
181
327
|
|
|
182
328
|
/* ── URL bar ── */
|
|
183
329
|
#url-bar {
|
|
@@ -462,7 +608,13 @@ function getExplorerHtml(options) {
|
|
|
462
608
|
<span class="base-url-label">Base URL</span>
|
|
463
609
|
<input id="base-url" type="url" spellcheck="false" />
|
|
464
610
|
</div>
|
|
465
|
-
<button class="icon-btn" id="
|
|
611
|
+
<button class="icon-btn" id="global-auth-btn" title="Global Authentication">
|
|
612
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
613
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
614
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
615
|
+
</svg>
|
|
616
|
+
</button>
|
|
617
|
+
<button class="icon-btn" id="reload-btn" title="Reload routes">
|
|
466
618
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
467
619
|
<polyline points="23 4 23 10 17 10"/>
|
|
468
620
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
@@ -470,6 +622,48 @@ function getExplorerHtml(options) {
|
|
|
470
622
|
</button>
|
|
471
623
|
</header>
|
|
472
624
|
|
|
625
|
+
<!-- Global Auth Dropdown -->
|
|
626
|
+
<div id="global-auth-dropdown" class="hidden">
|
|
627
|
+
<div class="gauth-title">
|
|
628
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
629
|
+
Global Authentication
|
|
630
|
+
<span class="gauth-active-badge hidden" id="gauth-active-badge">Active</span>
|
|
631
|
+
</div>
|
|
632
|
+
<div class="gauth-type-row">
|
|
633
|
+
<button class="gauth-type-btn active" data-gauth="none">None</button>
|
|
634
|
+
<button class="gauth-type-btn" data-gauth="bearer">Bearer Token</button>
|
|
635
|
+
<button class="gauth-type-btn" data-gauth="apikey">API Key</button>
|
|
636
|
+
</div>
|
|
637
|
+
<div id="gauth-none-panel" class="text-muted" style="font-size:12px">No global auth. Per-request auth (Auth tab) still applies.</div>
|
|
638
|
+
<div id="gauth-bearer-panel" class="hidden flex-col gap-6">
|
|
639
|
+
<div class="field-group">
|
|
640
|
+
<label class="field-label">Bearer Token</label>
|
|
641
|
+
<input class="kv-input" id="gauth-bearer-token" type="text" style="width:100%" placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" spellcheck="false" autocomplete="off" />
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
<div id="gauth-apikey-panel" class="hidden flex-col gap-6">
|
|
645
|
+
<div class="field-group">
|
|
646
|
+
<label class="field-label">Key Name</label>
|
|
647
|
+
<input class="kv-input" id="gauth-apikey-name" type="text" style="width:100%" placeholder="X-API-Key" value="X-API-Key" spellcheck="false" />
|
|
648
|
+
</div>
|
|
649
|
+
<div class="field-group">
|
|
650
|
+
<label class="field-label">Key Value</label>
|
|
651
|
+
<input class="kv-input" id="gauth-apikey-value" type="text" style="width:100%" placeholder="your-secret-key" autocomplete="off" spellcheck="false" />
|
|
652
|
+
</div>
|
|
653
|
+
<div class="field-group">
|
|
654
|
+
<label class="field-label">Add To</label>
|
|
655
|
+
<select class="styled-select" id="gauth-apikey-in" style="width:160px">
|
|
656
|
+
<option value="header">Header</option>
|
|
657
|
+
<option value="query">Query String</option>
|
|
658
|
+
</select>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
<div class="gauth-footer">
|
|
662
|
+
<button class="gauth-save-btn" id="gauth-save-btn">Apply Globally</button>
|
|
663
|
+
<button class="gauth-clear-btn" id="gauth-clear-btn">Clear</button>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
473
667
|
<!-- Main -->
|
|
474
668
|
<div id="main">
|
|
475
669
|
|
|
@@ -491,6 +685,7 @@ function getExplorerHtml(options) {
|
|
|
491
685
|
|
|
492
686
|
<!-- Content -->
|
|
493
687
|
<div id="content">
|
|
688
|
+
<div id="content-main">
|
|
494
689
|
|
|
495
690
|
<!-- URL bar -->
|
|
496
691
|
<div id="url-bar">
|
|
@@ -635,9 +830,29 @@ function getExplorerHtml(options) {
|
|
|
635
830
|
</div>
|
|
636
831
|
|
|
637
832
|
</div>
|
|
833
|
+
</div><!-- content-main -->
|
|
834
|
+
|
|
835
|
+
<!-- Summary Panel -->
|
|
836
|
+
<div id="summary-panel" class="hidden">
|
|
837
|
+
<div class="summary-header">
|
|
838
|
+
<span class="summary-header-title">Request Summary</span>
|
|
839
|
+
<button id="summary-copy-btn" class="summary-copy-btn hidden" title="Copy summary">
|
|
840
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
841
|
+
<span id="summary-copy-label">Copy</span>
|
|
842
|
+
</button>
|
|
843
|
+
</div>
|
|
844
|
+
<div id="summary-body">
|
|
845
|
+
<div class="summary-empty" id="summary-empty">
|
|
846
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|
847
|
+
<p>Select a route to see the request summary</p>
|
|
848
|
+
</div>
|
|
849
|
+
<div id="summary-content" class="hidden" style="display:flex;flex-direction:column;gap:14px"></div>
|
|
850
|
+
</div>
|
|
638
851
|
</div>
|
|
639
|
-
|
|
640
|
-
</div
|
|
852
|
+
|
|
853
|
+
</div><!-- content -->
|
|
854
|
+
</div><!-- main -->
|
|
855
|
+
</div><!-- app -->
|
|
641
856
|
|
|
642
857
|
<script>
|
|
643
858
|
const CONFIG = ${config};
|
|
@@ -648,7 +863,8 @@ const S = {
|
|
|
648
863
|
pathParams: {}, queryParams: [], reqHeaders: [],
|
|
649
864
|
auth: { type: 'none', bearerToken: '', basicUsername: '', basicPassword: '', apiKeyName: 'X-API-Key', apiKeyValue: '', apiKeyIn: 'header' },
|
|
650
865
|
body: { type: 'none', content: '' },
|
|
651
|
-
response: null, reqTab: 'params', resTab: 'body', loading: false, _uid: 0,
|
|
866
|
+
response: null, reqTab: 'params', resTab: 'body', loading: false, _uid: 0, collapsedGroups: new Set(),
|
|
867
|
+
globalAuth: { type: 'none', bearerToken: '', apiKeyName: 'X-API-Key', apiKeyValue: '', apiKeyIn: 'header' },
|
|
652
868
|
};
|
|
653
869
|
function uid() { return ++S._uid; }
|
|
654
870
|
|
|
@@ -685,6 +901,7 @@ function init() {
|
|
|
685
901
|
if (isLocal) el.envTag.classList.add('dev');
|
|
686
902
|
bindEvents();
|
|
687
903
|
loadRoutes();
|
|
904
|
+
renderSummary();
|
|
688
905
|
}
|
|
689
906
|
|
|
690
907
|
// ── Routes ─────────────────────────────────────────────────────────
|
|
@@ -709,25 +926,83 @@ function applyFilter() {
|
|
|
709
926
|
S.filtered = q ? base.filter(r => r.path.toLowerCase().includes(q) || r.method.toLowerCase().includes(q)) : base;
|
|
710
927
|
}
|
|
711
928
|
|
|
929
|
+
function groupRoutes(routes) {
|
|
930
|
+
const groups = {};
|
|
931
|
+
routes.forEach(r => {
|
|
932
|
+
// Use group from docs file if available, otherwise fall back to first path segment
|
|
933
|
+
const key = r.group || (r.path.split('/').filter(Boolean)[0] || 'general');
|
|
934
|
+
if (!groups[key]) groups[key] = [];
|
|
935
|
+
groups[key].push(r);
|
|
936
|
+
});
|
|
937
|
+
return groups;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function renderRouteItem(r) {
|
|
941
|
+
const active = S.selected?.method === r.method && S.selected?.path === r.path ? ' active' : '';
|
|
942
|
+
return \`<div class="route-item\${active}" data-method="\${r.method}" data-path="\${esc(r.path)}">
|
|
943
|
+
<span class="method-badge method-\${r.method}">\${r.method}</span>
|
|
944
|
+
<span class="route-path" title="\${esc(r.path)}">\${esc(r.path)}</span>
|
|
945
|
+
</div>\`;
|
|
946
|
+
}
|
|
947
|
+
|
|
712
948
|
function renderRouteList() {
|
|
713
949
|
if (!S.filtered.length) {
|
|
714
950
|
el.routeList.innerHTML = \`<div class="empty-routes">\${S.routes.length ? 'No routes match your search.' : 'No routes found.'}</div>\`;
|
|
715
951
|
return;
|
|
716
952
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
953
|
+
|
|
954
|
+
const searching = el.search.value.trim().length > 0;
|
|
955
|
+
const total = S.filtered.length;
|
|
956
|
+
|
|
957
|
+
// When searching, show flat list; otherwise group by prefix
|
|
958
|
+
if (searching) {
|
|
959
|
+
el.routeList.innerHTML =
|
|
960
|
+
\`<div class="route-count">\${total} result\${total !== 1 ? 's' : ''}</div>\` +
|
|
961
|
+
S.filtered.map(renderRouteItem).join('');
|
|
962
|
+
} else {
|
|
963
|
+
const groups = groupRoutes(S.filtered);
|
|
964
|
+
el.routeList.innerHTML =
|
|
965
|
+
\`<div class="route-count">\${total} route\${total !== 1 ? 's' : ''}</div>\` +
|
|
966
|
+
Object.entries(groups).map(([name, routes]) => {
|
|
967
|
+
const isCollapsed = S.collapsedGroups && S.collapsedGroups.has(name) ? 'collapsed' : '';
|
|
968
|
+
return \`<div class="route-group" data-group="\${esc(name)}">
|
|
969
|
+
<div class="route-group-header" data-toggle="\${esc(name)}">
|
|
970
|
+
<svg class="route-group-chevron \${isCollapsed}" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
|
971
|
+
<span class="route-group-name">/\${esc(name)}</span>
|
|
972
|
+
<span class="route-group-count">\${routes.length}</span>
|
|
973
|
+
</div>
|
|
974
|
+
<div class="route-group-body \${isCollapsed}">\${routes.map(renderRouteItem).join('')}</div>
|
|
975
|
+
</div>\`;
|
|
976
|
+
}).join('');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Click handlers for route items
|
|
725
980
|
el.routeList.querySelectorAll('.route-item').forEach(item => {
|
|
726
981
|
item.addEventListener('click', () => {
|
|
727
982
|
const route = S.routes.find(r => r.method === item.dataset.method && r.path === item.dataset.path);
|
|
728
983
|
if (route) selectRoute(route);
|
|
729
984
|
});
|
|
730
985
|
});
|
|
986
|
+
|
|
987
|
+
// Collapse/expand group headers
|
|
988
|
+
el.routeList.querySelectorAll('[data-toggle]').forEach(header => {
|
|
989
|
+
header.addEventListener('click', () => {
|
|
990
|
+
const name = header.dataset.toggle;
|
|
991
|
+
if (!S.collapsedGroups) S.collapsedGroups = new Set();
|
|
992
|
+
const group = header.closest('.route-group');
|
|
993
|
+
const body = group.querySelector('.route-group-body');
|
|
994
|
+
const chevron = group.querySelector('.route-group-chevron');
|
|
995
|
+
if (S.collapsedGroups.has(name)) {
|
|
996
|
+
S.collapsedGroups.delete(name);
|
|
997
|
+
body.classList.remove('collapsed');
|
|
998
|
+
chevron.classList.remove('collapsed');
|
|
999
|
+
} else {
|
|
1000
|
+
S.collapsedGroups.add(name);
|
|
1001
|
+
body.classList.add('collapsed');
|
|
1002
|
+
chevron.classList.add('collapsed');
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
731
1006
|
}
|
|
732
1007
|
|
|
733
1008
|
function selectRoute(route) {
|
|
@@ -735,7 +1010,7 @@ function selectRoute(route) {
|
|
|
735
1010
|
S.pathParams = {};
|
|
736
1011
|
(route.params || []).forEach(p => { S.pathParams[p] = ''; });
|
|
737
1012
|
S.response = null;
|
|
738
|
-
renderResponse(); renderUrlBar(); renderPathParams(); renderRouteList();
|
|
1013
|
+
renderResponse(); renderUrlBar(); renderPathParams(); renderRouteList(); renderSummary();
|
|
739
1014
|
el.sendBtn.disabled = false;
|
|
740
1015
|
el.methodPill.style.visibility = 'visible';
|
|
741
1016
|
|
|
@@ -745,8 +1020,28 @@ function selectRoute(route) {
|
|
|
745
1020
|
document.querySelectorAll('#req-panel-inner .tab-panel').forEach(p => p.classList.remove('active'));
|
|
746
1021
|
document.getElementById('tab-params').classList.add('active');
|
|
747
1022
|
|
|
748
|
-
// Auto-
|
|
1023
|
+
// Auto-fill body editor from body schema if available
|
|
749
1024
|
const bodyMethods = ['POST', 'PUT', 'PATCH'];
|
|
1025
|
+
if (bodyMethods.includes(route.method) && route.body?.length) {
|
|
1026
|
+
const template = {};
|
|
1027
|
+
route.body.forEach(f => {
|
|
1028
|
+
if (f.type === 'number') template[f.name] = 0;
|
|
1029
|
+
else if (f.type === 'boolean') template[f.name] = false;
|
|
1030
|
+
else if (f.type === 'array') template[f.name] = [];
|
|
1031
|
+
else if (f.type === 'object') template[f.name] = {};
|
|
1032
|
+
else template[f.name] = '';
|
|
1033
|
+
});
|
|
1034
|
+
S.body.content = JSON.stringify(template, null, 2);
|
|
1035
|
+
S.body.type = 'json';
|
|
1036
|
+
el.bodyEditor.value = S.body.content;
|
|
1037
|
+
// Sync body type radio
|
|
1038
|
+
document.querySelectorAll('[name="body-type"]').forEach(r => {
|
|
1039
|
+
r.checked = r.value === 'json';
|
|
1040
|
+
});
|
|
1041
|
+
el.formatBtn.style.display = '';
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Auto-switch to Body tab for methods that send a body
|
|
750
1045
|
if (bodyMethods.includes(route.method) && !route.params.length) {
|
|
751
1046
|
el.reqTabBar.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
752
1047
|
el.reqTabBar.querySelector('[data-tab="body"]').classList.add('active');
|
|
@@ -764,8 +1059,13 @@ function buildUrl() {
|
|
|
764
1059
|
Object.entries(S.pathParams).forEach(([k, v]) => {
|
|
765
1060
|
path = path.replace(':' + k, encodeURIComponent(v || (':' + k)));
|
|
766
1061
|
});
|
|
767
|
-
|
|
768
|
-
|
|
1062
|
+
// Per-route apikey in query (overrides global if set)
|
|
1063
|
+
const perRouteQpAuth = S.auth.type === 'apikey' && S.auth.apiKeyIn === 'query' && S.auth.apiKeyValue
|
|
1064
|
+
? [[S.auth.apiKeyName, S.auth.apiKeyValue]] : [];
|
|
1065
|
+
// Global apikey in query (only if per-route auth is 'none')
|
|
1066
|
+
const globalQpAuth = S.auth.type === 'none' && S.globalAuth.type === 'apikey' && S.globalAuth.apiKeyIn === 'query' && S.globalAuth.apiKeyValue
|
|
1067
|
+
? [[S.globalAuth.apiKeyName, S.globalAuth.apiKeyValue]] : [];
|
|
1068
|
+
const qp = [...S.queryParams.filter(p => p.enabled && p.key).map(p => [p.key, p.value]), ...perRouteQpAuth, ...globalQpAuth];
|
|
769
1069
|
const qs = qp.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&');
|
|
770
1070
|
return base + path + (qs ? '?' + qs : '');
|
|
771
1071
|
}
|
|
@@ -788,7 +1088,7 @@ function renderPathParams() {
|
|
|
788
1088
|
<input class="kv-input" data-path-param="\${esc(p)}" placeholder="value" value="\${esc(S.pathParams[p] || '')}" spellcheck="false" />
|
|
789
1089
|
</div>\`).join('');
|
|
790
1090
|
el.pathParamsRows.querySelectorAll('[data-path-param]').forEach(input => {
|
|
791
|
-
input.addEventListener('input', () => { S.pathParams[input.dataset.pathParam] = input.value; renderUrlBar(); });
|
|
1091
|
+
input.addEventListener('input', () => { S.pathParams[input.dataset.pathParam] = input.value; renderUrlBar(); renderSummary(); });
|
|
792
1092
|
});
|
|
793
1093
|
}
|
|
794
1094
|
|
|
@@ -840,6 +1140,14 @@ async function sendRequest() {
|
|
|
840
1140
|
|
|
841
1141
|
const headers = {};
|
|
842
1142
|
S.reqHeaders.filter(h => h.enabled && h.key).forEach(h => { headers[h.key] = h.value; });
|
|
1143
|
+
|
|
1144
|
+
// Apply global auth first (lower priority)
|
|
1145
|
+
if (S.globalAuth.type === 'bearer' && S.globalAuth.bearerToken)
|
|
1146
|
+
headers['Authorization'] = 'Bearer ' + S.globalAuth.bearerToken;
|
|
1147
|
+
else if (S.globalAuth.type === 'apikey' && S.globalAuth.apiKeyIn === 'header' && S.globalAuth.apiKeyValue)
|
|
1148
|
+
headers[S.globalAuth.apiKeyName || 'X-API-Key'] = S.globalAuth.apiKeyValue;
|
|
1149
|
+
|
|
1150
|
+
// Per-route auth overrides global (higher priority)
|
|
843
1151
|
if (S.auth.type === 'bearer' && S.auth.bearerToken) headers['Authorization'] = 'Bearer ' + S.auth.bearerToken;
|
|
844
1152
|
else if (S.auth.type === 'basic') headers['Authorization'] = 'Basic ' + btoa(S.auth.basicUsername + ':' + S.auth.basicPassword);
|
|
845
1153
|
else if (S.auth.type === 'apikey' && S.auth.apiKeyIn === 'header' && S.auth.apiKeyValue) headers[S.auth.apiKeyName || 'X-API-Key'] = S.auth.apiKeyValue;
|
|
@@ -913,6 +1221,219 @@ function renderResponse() {
|
|
|
913
1221
|
).join('');
|
|
914
1222
|
}
|
|
915
1223
|
|
|
1224
|
+
// ── Summary Panel ──────────────────────────────────────────────────
|
|
1225
|
+
function renderSummary() {
|
|
1226
|
+
const panel = $('summary-panel');
|
|
1227
|
+
const empty = $('summary-empty');
|
|
1228
|
+
const content = $('summary-content');
|
|
1229
|
+
|
|
1230
|
+
const copyBtn = $('summary-copy-btn');
|
|
1231
|
+
|
|
1232
|
+
if (!S.selected) {
|
|
1233
|
+
panel.classList.remove('hidden');
|
|
1234
|
+
empty.style.display = 'flex';
|
|
1235
|
+
content.classList.add('hidden');
|
|
1236
|
+
copyBtn.classList.add('hidden');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
panel.classList.remove('hidden');
|
|
1241
|
+
empty.style.display = 'none';
|
|
1242
|
+
content.classList.remove('hidden');
|
|
1243
|
+
content.style.display = 'flex';
|
|
1244
|
+
copyBtn.classList.remove('hidden');
|
|
1245
|
+
|
|
1246
|
+
const method = S.selected.method;
|
|
1247
|
+
const fullUrl = buildUrl();
|
|
1248
|
+
|
|
1249
|
+
// Highlight path params in URL
|
|
1250
|
+
let urlHtml = esc(fullUrl);
|
|
1251
|
+
(S.selected.params || []).forEach(p => {
|
|
1252
|
+
const val = S.pathParams[p];
|
|
1253
|
+
if (val) urlHtml = urlHtml.replace(esc(encodeURIComponent(val)), \`<span class="url-path-param">\${esc(val)}</span>\`);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// Active query params
|
|
1257
|
+
const activeQuery = S.queryParams.filter(p => p.enabled && p.key);
|
|
1258
|
+
|
|
1259
|
+
// Active headers (custom + auth)
|
|
1260
|
+
const activeHeaders = S.reqHeaders.filter(h => h.enabled && h.key);
|
|
1261
|
+
|
|
1262
|
+
// Auth being used
|
|
1263
|
+
const effectiveAuth = S.auth.type !== 'none' ? S.auth : S.globalAuth;
|
|
1264
|
+
let authHtml = '';
|
|
1265
|
+
if (effectiveAuth.type === 'bearer') {
|
|
1266
|
+
const tok = effectiveAuth.bearerToken;
|
|
1267
|
+
authHtml = \`<div class="summary-auth-row">
|
|
1268
|
+
<svg class="summary-auth-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
1269
|
+
<span class="summary-auth-label">Bearer</span>
|
|
1270
|
+
<span class="summary-auth-val">\${tok ? tok.slice(0,24) + (tok.length > 24 ? '…' : '') : '<em>no token</em>'}</span>
|
|
1271
|
+
\${S.auth.type === 'none' ? '<span style="font-size:9px;color:var(--text-dim);margin-left:auto">global</span>' : ''}
|
|
1272
|
+
</div>\`;
|
|
1273
|
+
} else if (effectiveAuth.type === 'basic') {
|
|
1274
|
+
authHtml = \`<div class="summary-auth-row">
|
|
1275
|
+
<svg class="summary-auth-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
1276
|
+
<span class="summary-auth-label">Basic</span>
|
|
1277
|
+
<span class="summary-auth-val">\${esc(effectiveAuth.basicUsername || '—')}</span>
|
|
1278
|
+
</div>\`;
|
|
1279
|
+
} else if (effectiveAuth.type === 'apikey') {
|
|
1280
|
+
authHtml = \`<div class="summary-auth-row">
|
|
1281
|
+
<svg class="summary-auth-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
|
|
1282
|
+
<span class="summary-auth-label">\${esc(effectiveAuth.apiKeyName)}</span>
|
|
1283
|
+
<span class="summary-auth-val">\${effectiveAuth.apiKeyIn === 'query' ? '(query)' : '(header)'}</span>
|
|
1284
|
+
\${S.auth.type === 'none' && S.globalAuth.type !== 'none' ? '<span style="font-size:9px;color:var(--text-dim);margin-left:auto">global</span>' : ''}
|
|
1285
|
+
</div>\`;
|
|
1286
|
+
} else {
|
|
1287
|
+
authHtml = '<span class="summary-none">No auth</span>';
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Body
|
|
1291
|
+
let bodyHtml = '';
|
|
1292
|
+
if (['POST','PUT','PATCH'].includes(method) && S.body.type !== 'none' && S.body.content) {
|
|
1293
|
+
const preview = S.body.content.slice(0, 200) + (S.body.content.length > 200 ? '…' : '');
|
|
1294
|
+
bodyHtml = \`<div class="summary-section">
|
|
1295
|
+
<div class="summary-section-label">Body <span style="font-weight:400;text-transform:none;color:var(--text-dim)">\${S.body.type}</span></div>
|
|
1296
|
+
<div class="summary-body-preview">\${esc(preview)}</div>
|
|
1297
|
+
</div>\`;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
content.innerHTML = \`
|
|
1301
|
+
<!-- URL -->
|
|
1302
|
+
<div class="summary-section">
|
|
1303
|
+
<div class="summary-section-label">
|
|
1304
|
+
<span class="summary-method-pill method-\${method}" style="border-color:inherit">\${method}</span>
|
|
1305
|
+
Endpoint
|
|
1306
|
+
</div>
|
|
1307
|
+
<div class="summary-url">\${urlHtml}</div>
|
|
1308
|
+
\${S.selected.description ? \`<div style="font-size:11px;color:var(--text-muted);margin-top:5px;line-height:1.5">\${esc(S.selected.description)}</div>\` : ''}
|
|
1309
|
+
</div>
|
|
1310
|
+
|
|
1311
|
+
<!-- Path params -->
|
|
1312
|
+
\${(S.selected.params || []).length ? \`<div class="summary-section">
|
|
1313
|
+
<div class="summary-section-label">Path Params</div>
|
|
1314
|
+
\${S.selected.params.map(p => \`<div class="summary-row">
|
|
1315
|
+
<span class="summary-key">:\${esc(p)}</span>
|
|
1316
|
+
<span class="summary-val">\${S.pathParams[p] ? esc(S.pathParams[p]) : '<em style="color:var(--del-fg)">empty</em>'}</span>
|
|
1317
|
+
</div>\`).join('')}
|
|
1318
|
+
</div>\` : ''}
|
|
1319
|
+
|
|
1320
|
+
<!-- Query params -->
|
|
1321
|
+
<div class="summary-section">
|
|
1322
|
+
<div class="summary-section-label">Query Params</div>
|
|
1323
|
+
\${activeQuery.length
|
|
1324
|
+
? activeQuery.map(p => \`<div class="summary-row">
|
|
1325
|
+
<span class="summary-key">\${esc(p.key)}</span>
|
|
1326
|
+
<span class="summary-val">\${esc(p.value)}</span>
|
|
1327
|
+
</div>\`).join('')
|
|
1328
|
+
: '<span class="summary-none">None</span>'}
|
|
1329
|
+
</div>
|
|
1330
|
+
|
|
1331
|
+
<!-- Headers -->
|
|
1332
|
+
<div class="summary-section">
|
|
1333
|
+
<div class="summary-section-label">Headers</div>
|
|
1334
|
+
\${activeHeaders.length
|
|
1335
|
+
? activeHeaders.map(h => \`<div class="summary-row">
|
|
1336
|
+
<span class="summary-key">\${esc(h.key)}</span>
|
|
1337
|
+
<span class="summary-val">\${esc(h.value)}</span>
|
|
1338
|
+
</div>\`).join('')
|
|
1339
|
+
: '<span class="summary-none">None</span>'}
|
|
1340
|
+
</div>
|
|
1341
|
+
|
|
1342
|
+
<!-- Auth -->
|
|
1343
|
+
<div class="summary-section">
|
|
1344
|
+
<div class="summary-section-label">Auth</div>
|
|
1345
|
+
\${authHtml}
|
|
1346
|
+
</div>
|
|
1347
|
+
|
|
1348
|
+
\${bodyHtml}
|
|
1349
|
+
|
|
1350
|
+
\${renderDocFields('Body Schema', S.selected.body)}
|
|
1351
|
+
\${renderDocFields('Query Params', S.selected.query)}
|
|
1352
|
+
\${renderDocFields('Required Headers', S.selected.headers)}
|
|
1353
|
+
\${renderDocFields('Response', S.selected.response)}
|
|
1354
|
+
\`;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// ── Copy Summary ───────────────────────────────────────────────────
|
|
1358
|
+
function copySummary() {
|
|
1359
|
+
if (!S.selected) return;
|
|
1360
|
+
const r = S.selected;
|
|
1361
|
+
const lines = [];
|
|
1362
|
+
|
|
1363
|
+
lines.push(\`\${r.method} \${buildUrl()}\`);
|
|
1364
|
+
if (r.description) lines.push(\`\`, ...r.description.split('\\n').map(l => \`# \${l}\`));
|
|
1365
|
+
|
|
1366
|
+
if (r.params?.length) {
|
|
1367
|
+
lines.push(\`\`, \`[Path Params]\`);
|
|
1368
|
+
r.params.forEach(p => lines.push(\` \${p}: \${S.pathParams[p] || ''}\`));
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const activeQuery = S.queryParams.filter(p => p.enabled && p.key);
|
|
1372
|
+
if (activeQuery.length) {
|
|
1373
|
+
lines.push(\`\`, \`[Query Params]\`);
|
|
1374
|
+
activeQuery.forEach(p => lines.push(\` \${p.key}: \${p.value}\`));
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (r.query?.length) {
|
|
1378
|
+
lines.push(\`\`, \`[Query Schema]\`);
|
|
1379
|
+
r.query.forEach(f => lines.push(\` \${f.name}: \${f.type}\${f.required ? ' (required)' : ''}\${f.rules?.length ? ' | ' + f.rules.join(', ') : ''}\${f.description ? ' — ' + f.description : ''}\`));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const activeHeaders = S.reqHeaders.filter(h => h.enabled && h.key);
|
|
1383
|
+
if (activeHeaders.length) {
|
|
1384
|
+
lines.push(\`\`, \`[Headers]\`);
|
|
1385
|
+
activeHeaders.forEach(h => lines.push(\` \${h.key}: \${h.value}\`));
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (r.headers?.length) {
|
|
1389
|
+
lines.push(\`\`, \`[Required Headers]\`);
|
|
1390
|
+
r.headers.forEach(f => lines.push(\` \${f.name}\${f.required ? ' (required)' : ''}\${f.description ? ' — ' + f.description : ''}\`));
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (r.body?.length) {
|
|
1394
|
+
lines.push(\`\`, \`[Body Schema]\`);
|
|
1395
|
+
r.body.forEach(f => lines.push(\` \${f.name}: \${f.type}\${f.required ? ' (required)' : ' (optional)'}\${f.rules?.length ? ' | ' + f.rules.join(', ') : ''}\${f.description ? ' — ' + f.description : ''}\`));
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (['POST','PUT','PATCH'].includes(r.method) && S.body.type !== 'none' && S.body.content) {
|
|
1399
|
+
lines.push(\`\`, \`[Body Content (\${S.body.type})]\`, S.body.content);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (r.response?.length) {
|
|
1403
|
+
lines.push(\`\`, \`[Response Schema]\`);
|
|
1404
|
+
r.response.forEach(f => lines.push(\` \${f.name}: \${f.type}\${f.description ? ' — ' + f.description : ''}\`));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
navigator.clipboard.writeText(lines.join('\\n')).then(() => {
|
|
1408
|
+
const btn = $('summary-copy-btn');
|
|
1409
|
+
const label = $('summary-copy-label');
|
|
1410
|
+
btn.classList.add('copied');
|
|
1411
|
+
label.textContent = 'Copied!';
|
|
1412
|
+
setTimeout(() => { btn.classList.remove('copied'); label.textContent = 'Copy'; }, 1800);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Render a list of DocField items (body/query/headers/response) in the summary panel
|
|
1417
|
+
function renderDocFields(label, fields) {
|
|
1418
|
+
if (!fields || fields.length === 0) return '';
|
|
1419
|
+
return \`<div class="summary-section">
|
|
1420
|
+
<div class="summary-section-label">\${label}</div>
|
|
1421
|
+
\${fields.map(f => {
|
|
1422
|
+
const typeTag = f.type ? \`<span class="df-type">\${esc(f.type)}</span>\` : '';
|
|
1423
|
+
const reqTag = f.required ? '<span class="df-req">required</span>' : '<span class="df-opt">optional</span>';
|
|
1424
|
+
const rulesTxt = f.rules?.length ? \`<span class="df-rules">\${esc(f.rules.join(' · '))}</span>\` : '';
|
|
1425
|
+
const descTxt = f.description ? \`<span class="df-desc">\${esc(f.description)}</span>\` : '';
|
|
1426
|
+
return \`<div class="df-row">
|
|
1427
|
+
<div class="df-top">
|
|
1428
|
+
<span class="df-name">\${esc(f.name)}</span>
|
|
1429
|
+
\${typeTag}\${reqTag}\${rulesTxt}
|
|
1430
|
+
</div>
|
|
1431
|
+
\${descTxt ? \`<div class="df-bottom">\${descTxt}</div>\` : ''}
|
|
1432
|
+
</div>\`;
|
|
1433
|
+
}).join('')}
|
|
1434
|
+
</div>\`;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
916
1437
|
function syntaxHighlight(json) {
|
|
917
1438
|
return json.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
918
1439
|
.replace(/("(?:\\\\u[\\da-fA-F]{4}|\\\\[^u]|[^\\\\\\"])*"(?:\\s*:)?|\\b(?:true|false|null)\\b|-?\\d+(?:\\.\\d+)?(?:[eE][+\\-]?\\d+)?)/g, m => {
|
|
@@ -991,9 +1512,76 @@ function initSidebarResize() {
|
|
|
991
1512
|
function bindEvents() {
|
|
992
1513
|
el.reloadBtn.addEventListener('click', loadRoutes);
|
|
993
1514
|
el.search.addEventListener('input', () => { applyFilter(); renderRouteList(); });
|
|
994
|
-
|
|
1515
|
+
|
|
1516
|
+
// ── Global Auth ──
|
|
1517
|
+
const gauthBtn = $('global-auth-btn');
|
|
1518
|
+
const gauthDrop = $('global-auth-dropdown');
|
|
1519
|
+
const gauthBadge = $('gauth-active-badge');
|
|
1520
|
+
const gauthBearer = $('gauth-bearer-token');
|
|
1521
|
+
const gauthKeyName= $('gauth-apikey-name');
|
|
1522
|
+
const gauthKeyVal = $('gauth-apikey-value');
|
|
1523
|
+
const gauthKeyIn = $('gauth-apikey-in');
|
|
1524
|
+
|
|
1525
|
+
gauthBtn.addEventListener('click', e => {
|
|
1526
|
+
e.stopPropagation();
|
|
1527
|
+
gauthDrop.classList.toggle('hidden');
|
|
1528
|
+
gauthBtn.classList.toggle('active', !gauthDrop.classList.contains('hidden'));
|
|
1529
|
+
});
|
|
1530
|
+
document.addEventListener('click', e => {
|
|
1531
|
+
if (!gauthDrop.contains(e.target) && e.target !== gauthBtn) {
|
|
1532
|
+
gauthDrop.classList.add('hidden');
|
|
1533
|
+
gauthBtn.classList.remove('active');
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
document.querySelectorAll('.gauth-type-btn').forEach(btn => {
|
|
1538
|
+
btn.addEventListener('click', () => {
|
|
1539
|
+
document.querySelectorAll('.gauth-type-btn').forEach(b => b.classList.remove('active'));
|
|
1540
|
+
btn.classList.add('active');
|
|
1541
|
+
const t = btn.dataset.gauth;
|
|
1542
|
+
['none','bearer','apikey'].forEach(x => $('gauth-'+x+'-panel').classList.toggle('hidden', x !== t));
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
$('gauth-save-btn').addEventListener('click', () => {
|
|
1547
|
+
const activeBtn = document.querySelector('.gauth-type-btn.active');
|
|
1548
|
+
const type = activeBtn ? activeBtn.dataset.gauth : 'none';
|
|
1549
|
+
S.globalAuth = {
|
|
1550
|
+
type,
|
|
1551
|
+
bearerToken: gauthBearer.value,
|
|
1552
|
+
apiKeyName: gauthKeyName.value || 'X-API-Key',
|
|
1553
|
+
apiKeyValue: gauthKeyVal.value,
|
|
1554
|
+
apiKeyIn: gauthKeyIn.value,
|
|
1555
|
+
};
|
|
1556
|
+
const isActive = type !== 'none' && (S.globalAuth.bearerToken || S.globalAuth.apiKeyValue);
|
|
1557
|
+
gauthBadge.classList.toggle('hidden', !isActive);
|
|
1558
|
+
gauthBtn.classList.toggle('active', isActive);
|
|
1559
|
+
// Show dot indicator
|
|
1560
|
+
const dot = gauthBtn.querySelector('.dot');
|
|
1561
|
+
if (isActive && !dot) {
|
|
1562
|
+
const d = document.createElement('span'); d.className = 'dot';
|
|
1563
|
+
gauthBtn.appendChild(d);
|
|
1564
|
+
} else if (!isActive && dot) dot.remove();
|
|
1565
|
+
gauthDrop.classList.add('hidden');
|
|
1566
|
+
renderUrlBar(); renderSummary();
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
$('gauth-clear-btn').addEventListener('click', () => {
|
|
1570
|
+
S.globalAuth = { type: 'none', bearerToken: '', apiKeyName: 'X-API-Key', apiKeyValue: '', apiKeyIn: 'header' };
|
|
1571
|
+
document.querySelectorAll('.gauth-type-btn').forEach(b => b.classList.remove('active'));
|
|
1572
|
+
document.querySelector('[data-gauth="none"]').classList.add('active');
|
|
1573
|
+
['none','bearer','apikey'].forEach(x => $('gauth-'+x+'-panel').classList.toggle('hidden', x !== 'none'));
|
|
1574
|
+
gauthBearer.value = ''; gauthKeyVal.value = '';
|
|
1575
|
+
gauthBadge.classList.add('hidden');
|
|
1576
|
+
gauthBtn.classList.remove('active');
|
|
1577
|
+
gauthBtn.querySelector('.dot')?.remove();
|
|
1578
|
+
renderUrlBar();
|
|
1579
|
+
});
|
|
1580
|
+
el.baseUrl.addEventListener('input', () => { renderUrlBar(); renderSummary(); });
|
|
995
1581
|
el.sendBtn.addEventListener('click', sendRequest);
|
|
996
1582
|
|
|
1583
|
+
$('summary-copy-btn').addEventListener('click', copySummary);
|
|
1584
|
+
|
|
997
1585
|
el.addQueryBtn.addEventListener('click', () => {
|
|
998
1586
|
S.queryParams.push({ id: uid(), key: '', value: '', enabled: true });
|
|
999
1587
|
renderQueryParams();
|
|
@@ -1029,7 +1617,7 @@ function bindEvents() {
|
|
|
1029
1617
|
el.formatBtn.style.display = S.body.type === 'json' ? '' : 'none';
|
|
1030
1618
|
});
|
|
1031
1619
|
});
|
|
1032
|
-
el.bodyEditor.addEventListener('input', () => { S.body.content = el.bodyEditor.value; });
|
|
1620
|
+
el.bodyEditor.addEventListener('input', () => { S.body.content = el.bodyEditor.value; renderSummary(); });
|
|
1033
1621
|
el.formatBtn.addEventListener('click', () => {
|
|
1034
1622
|
try {
|
|
1035
1623
|
el.bodyEditor.value = JSON.stringify(JSON.parse(el.bodyEditor.value), null, 2);
|