@vnphu/nestjs-api-explorer 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"api-explorer.html.d.ts","sourceRoot":"","sources":["../src/api-explorer.html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAEnE,wBAAgB,eAAe,CAAC,OAAO,EAAE,0BAA0B,GAAG,MAAM,CAmhD3E"}
1
+ {"version":3,"file":"api-explorer.html.d.ts","sourceRoot":"","sources":["../src/api-explorer.html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAEnE,wBAAgB,eAAe,CAAC,OAAO,EAAE,0BAA0B,GAAG,MAAM,CAknD3E"}
@@ -254,7 +254,7 @@ function getExplorerHtml(options) {
254
254
 
255
255
  /* ── Summary Panel ── */
256
256
  #summary-panel {
257
- width: 240px; flex-shrink: 0; border-left: 1px solid var(--border);
257
+ width: 400px; flex-shrink: 0; border-left: 1px solid var(--border);
258
258
  background: var(--bg-white); display: flex; flex-direction: column; overflow: hidden;
259
259
  }
260
260
  #summary-panel.hidden { display: none; }
@@ -263,6 +263,13 @@ function getExplorerHtml(options) {
263
263
  display: flex; align-items: center; justify-content: space-between;
264
264
  }
265
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; }
266
273
  #summary-body { flex: 1; overflow-y: auto; padding: 10px 14px 16px; display: flex; flex-direction: column; gap: 14px; }
267
274
  .summary-section { display: flex; flex-direction: column; gap: 6px; }
268
275
  .summary-section-label {
@@ -829,6 +836,10 @@ function getExplorerHtml(options) {
829
836
  <div id="summary-panel" class="hidden">
830
837
  <div class="summary-header">
831
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>
832
843
  </div>
833
844
  <div id="summary-body">
834
845
  <div class="summary-empty" id="summary-empty">
@@ -928,11 +939,9 @@ function groupRoutes(routes) {
928
939
 
929
940
  function renderRouteItem(r) {
930
941
  const active = S.selected?.method === r.method && S.selected?.path === r.path ? ' active' : '';
931
- const desc = r.description ? \`<span class="route-desc">\${esc(r.description.split('\\n')[0])}</span>\` : '';
932
942
  return \`<div class="route-item\${active}" data-method="\${r.method}" data-path="\${esc(r.path)}">
933
943
  <span class="method-badge method-\${r.method}">\${r.method}</span>
934
944
  <span class="route-path" title="\${esc(r.path)}">\${esc(r.path)}</span>
935
- \${desc}
936
945
  </div>\`;
937
946
  }
938
947
 
@@ -1011,8 +1020,28 @@ function selectRoute(route) {
1011
1020
  document.querySelectorAll('#req-panel-inner .tab-panel').forEach(p => p.classList.remove('active'));
1012
1021
  document.getElementById('tab-params').classList.add('active');
1013
1022
 
1014
- // Auto-switch to Body tab for methods that send a body
1023
+ // Auto-fill body editor from body schema if available
1015
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
1016
1045
  if (bodyMethods.includes(route.method) && !route.params.length) {
1017
1046
  el.reqTabBar.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
1018
1047
  el.reqTabBar.querySelector('[data-tab="body"]').classList.add('active');
@@ -1198,16 +1227,21 @@ function renderSummary() {
1198
1227
  const empty = $('summary-empty');
1199
1228
  const content = $('summary-content');
1200
1229
 
1230
+ const copyBtn = $('summary-copy-btn');
1231
+
1201
1232
  if (!S.selected) {
1202
1233
  panel.classList.remove('hidden');
1203
1234
  empty.style.display = 'flex';
1204
- content.style.display = 'none';
1235
+ content.classList.add('hidden');
1236
+ copyBtn.classList.add('hidden');
1205
1237
  return;
1206
1238
  }
1207
1239
 
1208
1240
  panel.classList.remove('hidden');
1209
1241
  empty.style.display = 'none';
1242
+ content.classList.remove('hidden');
1210
1243
  content.style.display = 'flex';
1244
+ copyBtn.classList.remove('hidden');
1211
1245
 
1212
1246
  const method = S.selected.method;
1213
1247
  const fullUrl = buildUrl();
@@ -1320,6 +1354,65 @@ function renderSummary() {
1320
1354
  \`;
1321
1355
  }
1322
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
+
1323
1416
  // Render a list of DocField items (body/query/headers/response) in the summary panel
1324
1417
  function renderDocFields(label, fields) {
1325
1418
  if (!fields || fields.length === 0) return '';
@@ -1487,6 +1580,8 @@ function bindEvents() {
1487
1580
  el.baseUrl.addEventListener('input', () => { renderUrlBar(); renderSummary(); });
1488
1581
  el.sendBtn.addEventListener('click', sendRequest);
1489
1582
 
1583
+ $('summary-copy-btn').addEventListener('click', copySummary);
1584
+
1490
1585
  el.addQueryBtn.addEventListener('click', () => {
1491
1586
  S.queryParams.push({ id: uid(), key: '', value: '', enabled: true });
1492
1587
  renderQueryParams();
@@ -1 +1 @@
1
- {"version":3,"file":"api-explorer.html.js","sourceRoot":"","sources":["../src/api-explorer.html.ts"],"names":[],"mappings":";;AAEA,0CAmhDC;AAnhDD,SAAgB,eAAe,CAAC,OAAmC;IACjE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/C,OAAO,UAAU,CAAC;;;;;WAKT,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAm0BC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAssBf,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"api-explorer.html.js","sourceRoot":"","sources":["../src/api-explorer.html.ts"],"names":[],"mappings":";;AAEA,0CAknDC;AAlnDD,SAAgB,eAAe,CAAC,OAAmC;IACjE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/C,OAAO,UAAU,CAAC;;;;;WAKT,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA80BC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0xBf,CAAC;AACT,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vnphu/nestjs-api-explorer",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "An in-app API explorer for NestJS — like Postman, but built right into your app.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",