From 0557d1337a691d0eda94e27f3ead049c556976d5 Mon Sep 17 00:00:00 2001 From: srivastavaayush1611LT Date: Tue, 16 Jun 2026 15:15:12 +0530 Subject: [PATCH 1/5] TE-17800: surface spec body example + flatten single-item array bodies Three layered fixes for Try It body rendering, all driven by the `requestBody.example` block that already exists in the upstream specs: 1. **Carry spec example through the build.** `extractRequestBody` now attaches `example` alongside `properties` so downstream code can use the author's intended payload. ~18 example-only endpoints (most of Test Manager, Analytics, parts of User Management) previously rendered hollow bodies (e.g. `"folders": []`) and now show the populated example. 2. **Apply local overrides for known-wrong upstream examples.** Adds a `REQUEST_BODY_EXAMPLE_OVERRIDES` map keyed by `||`. Currently fixes the TE-17800 case: upstream YAML uses `parent_folder_id` for Test Manager POST/PUT `/api/v1/folder` but the real API expects `parent_id`. Override flips the key and matches the value. Marked with `// TE-17800` for easy removal once the upstream YAML is corrected. 3. **Flatten single-item array bodies into per-field form inputs.** New `detectFlattenedArrayBody` helper recognises bodies shaped like `{ wrapper: [ { ...primitive fields } ] }` (POST `/api/v1/folder`). When matched, the Body section renders one input per inner field prefilled with the example, and buildCurl/handleSend/langUtils wrap the typed inputs back into the array shape. Other bodies retain existing rendering. Multipart endpoints explicitly gated off. Also: language tabs (Python/JS/PHP/Go/Java/Ruby) and the live Send request go through the same assembly so cURL, code snippets, and the real fetch all agree on the body shape. Co-Authored-By: Claude Opus 4.7 --- scripts/build-api-data.js | 45 +++++++- src/component/ApiReference/TryItModal.js | 141 +++++++++++++++++++---- src/component/ApiReference/langUtils.js | 88 +++++++++++--- 3 files changed, 234 insertions(+), 40 deletions(-) diff --git a/scripts/build-api-data.js b/scripts/build-api-data.js index b362d7de1..1747ec086 100644 --- a/scripts/build-api-data.js +++ b/scripts/build-api-data.js @@ -117,6 +117,30 @@ function categorizeParams(parameters) { return { auth, pathParams, queryParams }; } +// Local patches for known-wrong examples in upstream specs. +// Each function takes the raw example and returns the corrected one. +// Remove the matching entry once the upstream YAML is fixed. +const REQUEST_BODY_EXAMPLE_OVERRIDES = { + // TE-17800: upstream uses `parent_folder_id`, real API expects `parent_id`. + // Rename both the key and its self-referential placeholder value. + 'Test Manager|POST|/api/v1/folder': (ex) => { + if (!ex || !Array.isArray(ex.folders)) return ex; + return { + ...ex, + folders: ex.folders.map((f) => { + if (!f || typeof f !== 'object' || !('parent_folder_id' in f)) return f; + const { parent_folder_id, ...rest } = f; + return { ...rest, parent_id: 'parent_id' }; + }), + }; + }, + 'Test Manager|PUT|/api/v1/folder': (ex) => { + if (!ex || typeof ex !== 'object' || !('parent_folder_id' in ex)) return ex; + const { parent_folder_id, ...rest } = ex; + return { ...rest, parent_id: 'parent_id' }; + }, +}; + function resolveRef(ref, spec) { if (!ref || !ref.startsWith('#/')) return null; const parts = ref.slice(2).split('/'); @@ -145,6 +169,15 @@ function extractRequestBody(requestBody, spec, ctx = {}) { } schema = merged; } + // Resolve the example up front (with local override) so derived property names + // from the example fallback below also pick up the correction. + const rawExample = bodyContent.example ?? schema.example; + const overrideKey = (ctx.specName && ctx.method && ctx.path) + ? `${ctx.specName}|${ctx.method}|${ctx.path}` + : null; + const overrideFn = overrideKey ? REQUEST_BODY_EXAMPLE_OVERRIDES[overrideKey] : null; + const exampleValue = overrideFn ? overrideFn(rawExample) : rawExample; + const props = schema.properties || {}; const required = schema.required || []; let properties = Object.entries(props).map(([name, propSchema]) => { @@ -161,9 +194,8 @@ function extractRequestBody(requestBody, spec, ctx = {}) { // editable inputs. Inferred types and required flags are best-effort; // long-term fix is for spec authors to add proper `properties`. if (properties.length === 0) { - const example = bodyContent.example ?? schema.example; - if (example && typeof example === 'object' && !Array.isArray(example)) { - properties = Object.entries(example).map(([name, value]) => { + if (exampleValue && typeof exampleValue === 'object' && !Array.isArray(exampleValue)) { + properties = Object.entries(exampleValue).map(([name, value]) => { let type = 'string'; if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; else if (typeof value === 'boolean') type = 'boolean'; @@ -179,7 +211,12 @@ function extractRequestBody(requestBody, spec, ctx = {}) { } } - return { contentType, description: requestBody.description || '', properties }; + return { + contentType, + description: requestBody.description || '', + properties, + ...(exampleValue !== undefined && { example: exampleValue }), + }; } function schemaToExample(schema, spec, seen = new Set()) { diff --git a/src/component/ApiReference/TryItModal.js b/src/component/ApiReference/TryItModal.js index 933a59ed0..b240f6e6b 100644 --- a/src/component/ApiReference/TryItModal.js +++ b/src/component/ApiReference/TryItModal.js @@ -4,7 +4,7 @@ import { Highlight, themes } from 'prism-react-renderer'; import MethodBadge from './MethodBadge'; import InlineText from './InlineText'; import styles from './TryItModal.module.css'; -import { LANGUAGES, generateCodeExample, LangDropdownPortal, LangSelectorButton, coerceBodyValue } from './langUtils'; +import { LANGUAGES, generateCodeExample, LangDropdownPortal, LangSelectorButton, coerceBodyValue, detectFlattenedArrayBody } from './langUtils'; const githubWithGreenKeys = { ...themes.github, @@ -94,19 +94,56 @@ function buildCurl(endpoint, username, password, params, baseUrl) { const authLine = hasAuth ? ` \\\n --header 'Authorization: ${authHeader}'` : ''; const bodyProps = endpoint.requestBody?.properties || []; + const rawExample = endpoint.requestBody?.example; const contentType = endpoint.requestBody?.contentType || 'application/json'; + const isMultipart = contentType === 'multipart/form-data'; + const flattenedBody = !isMultipart ? detectFlattenedArrayBody(endpoint) : null; + const userFilledBody = flattenedBody + ? flattenedBody.innerFields.some((f) => params[`__body__${f.name}`]) + : bodyProps.some((p) => params[`__body__${p.name}`]); + const useRawExample = !isMultipart && !flattenedBody && !userFilledBody + && rawExample != null && typeof rawExample === 'object'; let bodyLine = ''; - if (bodyProps.length > 0) { - const bodyEntries = bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]); - if (contentType === 'multipart/form-data') { + if (bodyProps.length > 0 || useRawExample || flattenedBody) { + if (isMultipart) { + const bodyEntries = bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]); bodyLine = bodyEntries .filter(([, v]) => v) .map(([k, v]) => ` \\\n --form '${k}=${v}'`) .join(''); + } else if (flattenedBody) { + // Assemble inner object from per-field inputs (with example fallback), + // wrap under the spec's array key. + const inner = {}; + for (const f of flattenedBody.innerFields) { + const raw = params[`__body__${f.name}`]; + const fromEx = flattenedBody.innerExample[f.name]; + const val = (raw !== undefined && raw !== '') ? raw : fromEx; + if (val !== undefined && val !== '') inner[f.name] = val; + } + const bodyJson = { [flattenedBody.wrapperKey]: [inner] }; + bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyJson, null, 2)}'`; } else { - const bodyObj = Object.fromEntries(bodyEntries.filter(([, v]) => v)); - if (Object.keys(bodyObj).length) { - bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyObj)}'`; + let bodyJson; + if (useRawExample) { + bodyJson = rawExample; + } else { + // Untyped fields fall back to the spec example so editing one field + // doesn't blank out the others. + const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined; + const bodyEntries = bodyProps.map((p) => { + const raw = params[`__body__${p.name}`] || ''; + if (!raw) { + const ex = fromExample(p); + return [p.name, ex !== undefined ? ex : '']; + } + return [p.name, coerceBodyValue(raw, p.type)]; + }); + const bodyObj = Object.fromEntries(bodyEntries.filter(([, v]) => v !== '' && v !== undefined)); + if (Object.keys(bodyObj).length) bodyJson = bodyObj; + } + if (bodyJson !== undefined) { + bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyJson, null, 2)}'`; } } } @@ -284,7 +321,22 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const [params, setParams] = useState({}); + // Compute once at render time — both the params initializer and the body + // section need it, and `endpoint` is stable for the modal's lifetime. + const flattenedBody = detectFlattenedArrayBody(endpoint); + // Prefill body fields with the spec's example so the form is usable on first + // open. Flattened-array bodies expose inner primitive fields (each prefilled + // with the inner example value). + const [params, setParams] = useState(() => { + const initial = {}; + if (flattenedBody) { + for (const f of flattenedBody.innerFields) { + const val = flattenedBody.innerExample[f.name]; + initial[`__body__${f.name}`] = val == null ? '' : String(val); + } + } + return initial; + }); const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); const [activeResTab, setActiveResTab] = useState(null); @@ -348,17 +400,50 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa : ''; const bodyProps = endpoint.requestBody?.properties || []; + const rawExample = endpoint.requestBody?.example; const contentType = endpoint.requestBody?.contentType || 'application/json'; + const isMultipart = contentType === 'multipart/form-data'; + const flattenedBodyHS = !isMultipart ? detectFlattenedArrayBody(endpoint) : null; + const userFilledBody = flattenedBodyHS + ? flattenedBodyHS.innerFields.some((f) => params[`__body__${f.name}`]) + : bodyProps.some((p) => params[`__body__${p.name}`]); + const useRawExample = !isMultipart && !flattenedBodyHS && !userFilledBody + && rawExample != null && typeof rawExample === 'object'; let fetchBody; let fetchHeaders = { ...(authHeader && { Authorization: authHeader }) }; - if (bodyProps.length > 0) { - if (contentType === 'multipart/form-data') { + if (bodyProps.length > 0 || useRawExample || flattenedBodyHS) { + if (isMultipart) { const fd = new FormData(); bodyProps.forEach((p) => { if (params[`__body__${p.name}`]) fd.append(p.name, params[`__body__${p.name}`]); }); fetchBody = fd; // Don't set Content-Type for FormData — browser sets it with boundary + } else if (flattenedBodyHS) { + const inner = {}; + for (const f of flattenedBodyHS.innerFields) { + const raw = params[`__body__${f.name}`]; + const fromEx = flattenedBodyHS.innerExample[f.name]; + const val = (raw !== undefined && raw !== '') ? raw : fromEx; + if (val !== undefined && val !== '') inner[f.name] = val; + } + fetchBody = JSON.stringify({ [flattenedBodyHS.wrapperKey]: [inner] }); + fetchHeaders['Content-Type'] = 'application/json'; + } else if (useRawExample) { + fetchBody = JSON.stringify(rawExample); + fetchHeaders['Content-Type'] = 'application/json'; } else { - const bodyObj = Object.fromEntries(bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]).filter(([, v]) => v)); + // Untyped fields fall back to the spec example so partial edits don't + // strip the other example values. + const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined; + const bodyObj = Object.fromEntries( + bodyProps.map((p) => { + const raw = params[`__body__${p.name}`] || ''; + if (!raw) { + const ex = fromExample(p); + return [p.name, ex !== undefined ? ex : '']; + } + return [p.name, coerceBodyValue(raw, p.type)]; + }).filter(([, v]) => v !== '' && v !== undefined) + ); if (Object.keys(bodyObj).length) { fetchBody = JSON.stringify(bodyObj); fetchHeaders['Content-Type'] = 'application/json'; @@ -550,16 +635,30 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa title="Body" description={endpoint.requestBody.description || null} > - {bodyProps.map((p) => ( - updateParam(`__body__${p.name}`, v)} - placeholder={(p.type || '').toLowerCase().includes('array') ? 'e.g. ["val1", "val2"] or val1, val2' : undefined} - /> - ))} + {flattenedBody ? ( + flattenedBody.innerFields.map((f) => ( + updateParam(`__body__${f.name}`, v)} + /> + )) + ) : ( + bodyProps.map((p) => ( + updateParam(`__body__${p.name}`, v)} + placeholder={(p.type || '').toLowerCase().includes('array') ? 'e.g. ["val1", "val2"] or val1, val2' : undefined} + /> + )) + )} )} diff --git a/src/component/ApiReference/langUtils.js b/src/component/ApiReference/langUtils.js index 11255fdb9..093e772dd 100644 --- a/src/component/ApiReference/langUtils.js +++ b/src/component/ApiReference/langUtils.js @@ -38,6 +38,35 @@ export function coerceBodyValue(raw, type) { return raw; } +// Detect bodies shaped like `{ wrapper: [ { ...primitive fields } ] }` (e.g. +// Test Manager Create Folder). When matched, the form can render the inner +// object's primitive fields directly and the body assembly wraps them back +// into the array shape — giving the same per-field UX as a flat-body endpoint. +export function detectFlattenedArrayBody(endpoint) { + if (!endpoint || !endpoint.requestBody) return null; + const props = endpoint.requestBody.properties || []; + if (props.length !== 1) return null; + const only = props[0]; + if (only.type !== 'array') return null; + const example = endpoint.requestBody.example; + if (!example || typeof example !== 'object' || Array.isArray(example)) return null; + const arr = example[only.name]; + if (!Array.isArray(arr) || arr.length !== 1) return null; + const inner = arr[0]; + if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null; + // Only flatten when every inner value is primitive — keeps the form usable + // and avoids nested-textarea complexity. + const innerFields = Object.entries(inner).map(([name, value]) => { + let type = 'string'; + if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; + else if (typeof value === 'boolean') type = 'boolean'; + else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) return null; + return { name, type, required: false, description: '' }; + }); + if (innerFields.some((f) => f === null)) return null; + return { wrapperKey: only.name, innerFields, innerExample: inner }; +} + // prism language mapping — only use languages bundled in prism-react-renderer export const LANGUAGES = [ { label: 'cURL', prism: 'clike' }, @@ -75,21 +104,50 @@ export function generateCodeExample(endpoint, language, { username, password, pa const bodyProps = endpoint.requestBody?.properties || []; const contentType = endpoint.requestBody?.contentType || 'application/json'; const isMultipart = contentType === 'multipart/form-data'; - const bodyExample = bodyProps.length > 0 - ? Object.fromEntries(bodyProps.map((p) => { - const raw = params && params[`__body__${p.name}`]; - let val; - if (raw) { - val = coerceBodyValue(raw, p.type); - } else { - val = p.type.includes('integer') || p.type.includes('number') ? 0 : - p.type.includes('boolean') ? true : - p.type.includes('array') ? [] : - `<${p.name}>`; - } - return [p.name, val]; - })) - : null; + const rawExample = endpoint.requestBody?.example; + const flattenedBody = !isMultipart ? detectFlattenedArrayBody(endpoint) : null; + const userFilledBody = flattenedBody + ? flattenedBody.innerFields.some((f) => params && params[`__body__${f.name}`]) + : bodyProps.some((p) => params && params[`__body__${p.name}`]); + // Use the spec's example block as the body when the user hasn't typed + // anything — keeps nested arrays/objects that property-based synthesis + // would flatten to `[]` (e.g. Test Manager Create Folder's `folders[]`). + const useRawExample = !isMultipart && !flattenedBody && !userFilledBody + && rawExample != null && typeof rawExample === 'object'; + let bodyExample; + if (flattenedBody) { + // Assemble the inner object from per-field inputs (with example fallback), + // wrap under the spec's array key so the snippet matches the spec shape. + const inner = {}; + for (const f of flattenedBody.innerFields) { + const raw = params && params[`__body__${f.name}`]; + const fromEx = flattenedBody.innerExample[f.name]; + const val = (raw !== undefined && raw !== '') ? raw : fromEx; + if (val !== undefined && val !== '') inner[f.name] = val; + } + bodyExample = { [flattenedBody.wrapperKey]: [inner] }; + } else if (useRawExample) { + bodyExample = rawExample; + } else if (bodyProps.length > 0) { + bodyExample = Object.fromEntries(bodyProps.map((p) => { + const raw = params && params[`__body__${p.name}`]; + const fromExample = rawExample && typeof rawExample === 'object' ? rawExample[p.name] : undefined; + let val; + if (raw) { + val = coerceBodyValue(raw, p.type); + } else if (fromExample !== undefined) { + val = fromExample; + } else { + val = p.type.includes('integer') || p.type.includes('number') ? 0 : + p.type.includes('boolean') ? true : + p.type.includes('array') ? [] : + `<${p.name}>`; + } + return [p.name, val]; + })); + } else { + bodyExample = null; + } switch (language) { case 'cURL': { From 0afe94df28e4ae3635a066a0f0016fef79b8006a Mon Sep 17 00:00:00 2001 From: srivastavaayush1611LT Date: Tue, 16 Jun 2026 15:30:23 +0530 Subject: [PATCH 2/5] Address review: coerce flattened inner values, fix two comments, mark PUT override - Apply coerceBodyValue in all three flattened-body assembly sites (langUtils generateCodeExample, TryItModal buildCurl, TryItModal handleSend) so integer/boolean/number inner fields aren't sent as strings. No behavior change for POST /api/v1/folder (all inner fields are string), but unblocks future flattened endpoints with non-string inner types. - Reword detectFlattenedArrayBody comment to match the predicate: rejection is for arrays/non-null objects only; null values pass through as string. - Rename "Untyped fields" -> "Empty inputs" in two TryItModal comments; the predicate is `if (!raw)`, not a type check. - Add `// TE-17800:` marker to the PUT folder override so a future cleanup sweep that greps for the ticket catches both override entries. Co-Authored-By: Claude Opus 4.7 --- scripts/build-api-data.js | 1 + src/component/ApiReference/TryItModal.js | 12 ++++++++---- src/component/ApiReference/langUtils.js | 8 +++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/build-api-data.js b/scripts/build-api-data.js index 1747ec086..ca74c0ad8 100644 --- a/scripts/build-api-data.js +++ b/scripts/build-api-data.js @@ -134,6 +134,7 @@ const REQUEST_BODY_EXAMPLE_OVERRIDES = { }), }; }, + // TE-17800: same key rename for the update endpoint. 'Test Manager|PUT|/api/v1/folder': (ex) => { if (!ex || typeof ex !== 'object' || !('parent_folder_id' in ex)) return ex; const { parent_folder_id, ...rest } = ex; diff --git a/src/component/ApiReference/TryItModal.js b/src/component/ApiReference/TryItModal.js index b240f6e6b..1407d36fd 100644 --- a/src/component/ApiReference/TryItModal.js +++ b/src/component/ApiReference/TryItModal.js @@ -118,7 +118,9 @@ function buildCurl(endpoint, username, password, params, baseUrl) { for (const f of flattenedBody.innerFields) { const raw = params[`__body__${f.name}`]; const fromEx = flattenedBody.innerExample[f.name]; - const val = (raw !== undefined && raw !== '') ? raw : fromEx; + const val = (raw !== undefined && raw !== '') + ? coerceBodyValue(raw, f.type) + : fromEx; if (val !== undefined && val !== '') inner[f.name] = val; } const bodyJson = { [flattenedBody.wrapperKey]: [inner] }; @@ -128,7 +130,7 @@ function buildCurl(endpoint, username, password, params, baseUrl) { if (useRawExample) { bodyJson = rawExample; } else { - // Untyped fields fall back to the spec example so editing one field + // Empty inputs fall back to the spec example so editing one field // doesn't blank out the others. const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined; const bodyEntries = bodyProps.map((p) => { @@ -422,7 +424,9 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa for (const f of flattenedBodyHS.innerFields) { const raw = params[`__body__${f.name}`]; const fromEx = flattenedBodyHS.innerExample[f.name]; - const val = (raw !== undefined && raw !== '') ? raw : fromEx; + const val = (raw !== undefined && raw !== '') + ? coerceBodyValue(raw, f.type) + : fromEx; if (val !== undefined && val !== '') inner[f.name] = val; } fetchBody = JSON.stringify({ [flattenedBodyHS.wrapperKey]: [inner] }); @@ -431,7 +435,7 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa fetchBody = JSON.stringify(rawExample); fetchHeaders['Content-Type'] = 'application/json'; } else { - // Untyped fields fall back to the spec example so partial edits don't + // Empty inputs fall back to the spec example so partial edits don't // strip the other example values. const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined; const bodyObj = Object.fromEntries( diff --git a/src/component/ApiReference/langUtils.js b/src/component/ApiReference/langUtils.js index 093e772dd..fa5ef433f 100644 --- a/src/component/ApiReference/langUtils.js +++ b/src/component/ApiReference/langUtils.js @@ -54,8 +54,8 @@ export function detectFlattenedArrayBody(endpoint) { if (!Array.isArray(arr) || arr.length !== 1) return null; const inner = arr[0]; if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null; - // Only flatten when every inner value is primitive — keeps the form usable - // and avoids nested-textarea complexity. + // Bail when any inner value is an array or non-null object — the flat form + // can't represent nesting. `null` values are accepted (typed as string). const innerFields = Object.entries(inner).map(([name, value]) => { let type = 'string'; if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; @@ -122,7 +122,9 @@ export function generateCodeExample(endpoint, language, { username, password, pa for (const f of flattenedBody.innerFields) { const raw = params && params[`__body__${f.name}`]; const fromEx = flattenedBody.innerExample[f.name]; - const val = (raw !== undefined && raw !== '') ? raw : fromEx; + const val = (raw !== undefined && raw !== '') + ? coerceBodyValue(raw, f.type) + : fromEx; if (val !== undefined && val !== '') inner[f.name] = val; } bodyExample = { [flattenedBody.wrapperKey]: [inner] }; From 86987344991b12185a3eeda56a3b92b2d8a75303 Mon Sep 17 00:00:00 2001 From: srivastavaayush1611LT Date: Tue, 16 Jun 2026 18:48:28 +0530 Subject: [PATCH 3/5] PUT /api/v1/folder: render Rename / Move as Body tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The endpoint serves two operations distinguished by an `action` field: - Rename (action="update"): id, action, entity_id, name - Move (action="move"): id, action, entity_id, entity_type, name, parent_id, serial_no The upstream YAML ships a single example matching neither real payload. Until upstream is fixed, override the example via a new variants registry and render one tab per variant in the Try It modal. Build script: - Add REQUEST_BODY_VARIANTS_OVERRIDES map keyed by spec|method|path. Each entry returns [{name, example}, ...]. Extracted properties are inferred per-variant from the example. - Factor inferFieldsFromExample helper out of the legacy example-fallback path so variants reuse the same inference. - When variants are present, the first variant's example becomes the canonical top-level requestBody.example/properties — keeps the static CodeExamples panel (which doesn't know about variants) showing a sensible default instead of the now-stale spec example. - Drop the prior example override for PUT folder (now superseded by variants). POST folder retains its example override unchanged. Modal: - Add selectedVariantIdx state and an effectiveEndpoint useMemo that swaps in the selected variant's example + properties. Downstream helpers (buildCurl, handleSend, generateCodeExample) stay variant-agnostic — they just see an endpoint with the chosen body. - Render a tab strip above the Body section when variants exist. - Reset and re-prefill body params on tab switch (path/query/auth params are preserved). POST /api/v1/folder behavior unchanged. Co-Authored-By: Claude Opus 4.7 --- scripts/build-api-data.js | 97 ++++++++++++++++----- src/component/ApiReference/TryItModal.js | 103 +++++++++++++++++++---- 2 files changed, 162 insertions(+), 38 deletions(-) diff --git a/scripts/build-api-data.js b/scripts/build-api-data.js index ca74c0ad8..ff55d5dab 100644 --- a/scripts/build-api-data.js +++ b/scripts/build-api-data.js @@ -134,14 +134,58 @@ const REQUEST_BODY_EXAMPLE_OVERRIDES = { }), }; }, - // TE-17800: same key rename for the update endpoint. - 'Test Manager|PUT|/api/v1/folder': (ex) => { - if (!ex || typeof ex !== 'object' || !('parent_folder_id' in ex)) return ex; - const { parent_folder_id, ...rest } = ex; - return { ...rest, parent_id: 'parent_id' }; - }, + // PUT /api/v1/folder is now in REQUEST_BODY_VARIANTS_OVERRIDES below — the + // upstream example omits the `action` discriminator and conflates rename/move. +}; + +// Variant overrides expose multiple body shapes for one endpoint. The modal +// renders a tab per variant; each variant has its own example + derived fields. +// Use this when an endpoint accepts distinct payload shapes selected by a +// discriminator (e.g. PUT /api/v1/folder uses `action: "update"` vs "move"). +const REQUEST_BODY_VARIANTS_OVERRIDES = { + // TE-17800: PUT /api/v1/folder serves two operations: + // - Rename: action="update", no parent_id/entity_type/serial_no + // - Move: action="move", adds parent_id + entity_type + serial_no + // The upstream YAML ships a single example that matches neither real payload. + 'Test Manager|PUT|/api/v1/folder': () => ([ + { + name: 'Rename', + example: { + id: 'folder_id', + action: 'update', + entity_id: 'project_id', + name: 'Test Folder - LambdaTest Demo', + }, + }, + { + name: 'Move', + example: { + id: 'folder_id', + action: 'move', + entity_id: 'project_id', + entity_type: 'project', + name: 'Test Folder - LambdaTest Demo', + parent_id: 'parent_id', + serial_no: 1, + }, + }, + ]), }; +// Infer flat field metadata from a primitive-only example object — same logic +// used by the legacy schema-less-fallback path, factored out for variant reuse. +function inferFieldsFromExample(example) { + if (!example || typeof example !== 'object' || Array.isArray(example)) return []; + return Object.entries(example).map(([name, value]) => { + let type = 'string'; + if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; + else if (typeof value === 'boolean') type = 'boolean'; + else if (Array.isArray(value)) type = 'array'; + else if (typeof value === 'object' && value !== null) type = 'object'; + return { name, type, required: false, description: '' }; + }); +} + function resolveRef(ref, spec) { if (!ref || !ref.startsWith('#/')) return null; const parts = ref.slice(2).split('/'); @@ -195,28 +239,37 @@ function extractRequestBody(requestBody, spec, ctx = {}) { // editable inputs. Inferred types and required flags are best-effort; // long-term fix is for spec authors to add proper `properties`. if (properties.length === 0) { - if (exampleValue && typeof exampleValue === 'object' && !Array.isArray(exampleValue)) { - properties = Object.entries(exampleValue).map(([name, value]) => { - let type = 'string'; - if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; - else if (typeof value === 'boolean') type = 'boolean'; - else if (Array.isArray(value)) type = 'array'; - else if (typeof value === 'object' && value !== null) type = 'object'; - return { name, type, required: false, description: '' }; - }); - if (properties.length > 0) { - const loc = ctx.method && ctx.path ? `${ctx.method} ${ctx.path}` : '(unknown endpoint)'; - const src = ctx.specName ? ` [${ctx.specName}]` : ''; - console.warn(`⚠️ ${loc}${src}: requestBody has no schema.properties; derived ${properties.length} field(s) from example. Spec should declare properties for accurate types/required/descriptions.`); - } + properties = inferFieldsFromExample(exampleValue); + if (properties.length > 0) { + const loc = ctx.method && ctx.path ? `${ctx.method} ${ctx.path}` : '(unknown endpoint)'; + const src = ctx.specName ? ` [${ctx.specName}]` : ''; + console.warn(`⚠️ ${loc}${src}: requestBody has no schema.properties; derived ${properties.length} field(s) from example. Spec should declare properties for accurate types/required/descriptions.`); } } + // Variant overrides: attach per-tab examples + derived fields when an + // endpoint serves multiple payload shapes (e.g. PUT folder rename vs move). + const variantsFn = overrideKey ? REQUEST_BODY_VARIANTS_OVERRIDES[overrideKey] : null; + const variants = variantsFn + ? variantsFn().map((v) => ({ + name: v.name, + example: v.example, + properties: inferFieldsFromExample(v.example), + })) + : null; + + // When variants are present, the first one becomes the canonical top-level + // example + properties so non-variant-aware consumers (e.g. the static + // CodeExamples panel) show a sensible default instead of stale spec data. + const finalExample = variants && variants.length > 0 ? variants[0].example : exampleValue; + const finalProperties = variants && variants.length > 0 ? variants[0].properties : properties; + return { contentType, description: requestBody.description || '', - properties, - ...(exampleValue !== undefined && { example: exampleValue }), + properties: finalProperties, + ...(finalExample !== undefined && { example: finalExample }), + ...(variants && variants.length > 0 && { variants }), }; } diff --git a/src/component/ApiReference/TryItModal.js b/src/component/ApiReference/TryItModal.js index 1407d36fd..60c8a394b 100644 --- a/src/component/ApiReference/TryItModal.js +++ b/src/component/ApiReference/TryItModal.js @@ -323,22 +323,69 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + // Variant-aware view of the request body: when the spec ships multiple + // payload shapes for one endpoint (e.g. PUT folder rename/move), the modal + // picks one tab and presents it as the effective body. Downstream helpers + // (buildCurl, handleSend, generateCodeExample) operate on `effectiveEndpoint` + // and stay variant-agnostic. + const variants = endpoint.requestBody?.variants || null; + const [selectedVariantIdx, setSelectedVariantIdx] = useState(0); + const selectedVariant = variants ? variants[selectedVariantIdx] : null; + const effectiveEndpoint = useMemo(() => { + if (!selectedVariant) return endpoint; + return { + ...endpoint, + requestBody: { + ...endpoint.requestBody, + properties: selectedVariant.properties, + example: selectedVariant.example, + }, + }; + }, [endpoint, selectedVariant]); // Compute once at render time — both the params initializer and the body - // section need it, and `endpoint` is stable for the modal's lifetime. - const flattenedBody = detectFlattenedArrayBody(endpoint); - // Prefill body fields with the spec's example so the form is usable on first - // open. Flattened-array bodies expose inner primitive fields (each prefilled - // with the inner example value). - const [params, setParams] = useState(() => { + // section need it, and `effectiveEndpoint` is stable for the variant's lifetime. + const flattenedBody = detectFlattenedArrayBody(effectiveEndpoint); + + // Build the per-variant prefill — also reused when the user switches tabs. + function computeInitialParams() { const initial = {}; if (flattenedBody) { for (const f of flattenedBody.innerFields) { const val = flattenedBody.innerExample[f.name]; initial[`__body__${f.name}`] = val == null ? '' : String(val); } + return initial; + } + const ex = effectiveEndpoint.requestBody?.example; + const props = effectiveEndpoint.requestBody?.properties || []; + if (ex && typeof ex === 'object' && !Array.isArray(ex)) { + for (const p of props) { + if (ex[p.name] === undefined) continue; + const v = ex[p.name]; + initial[`__body__${p.name}`] = v == null ? '' : String(v); + } } return initial; - }); + } + // Prefill body fields with the spec example so the form is usable on first + // open. Flattened-array bodies expose inner primitive fields; plain bodies + // get one prefilled input per property. + const [params, setParams] = useState(computeInitialParams); + + // Reset & re-prefill body params whenever the active variant changes. + // Other params (path/query/auth) are not affected by variant switches. + useEffect(() => { + if (!variants) return; + setParams((prev) => { + const next = {}; + for (const k of Object.keys(prev)) { + if (!k.startsWith('__body__')) next[k] = prev[k]; + } + const fresh = computeInitialParams(); + return { ...next, ...fresh }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedVariantIdx]); const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); const [activeResTab, setActiveResTab] = useState(null); @@ -401,11 +448,11 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa ? `Basic ${safeBase64(`${username}:${password}`)}` : ''; - const bodyProps = endpoint.requestBody?.properties || []; - const rawExample = endpoint.requestBody?.example; - const contentType = endpoint.requestBody?.contentType || 'application/json'; + const bodyProps = effectiveEndpoint.requestBody?.properties || []; + const rawExample = effectiveEndpoint.requestBody?.example; + const contentType = effectiveEndpoint.requestBody?.contentType || 'application/json'; const isMultipart = contentType === 'multipart/form-data'; - const flattenedBodyHS = !isMultipart ? detectFlattenedArrayBody(endpoint) : null; + const flattenedBodyHS = !isMultipart ? detectFlattenedArrayBody(effectiveEndpoint) : null; const userFilledBody = flattenedBodyHS ? flattenedBodyHS.innerFields.some((f) => params[`__body__${f.name}`]) : bodyProps.some((p) => params[`__body__${p.name}`]); @@ -473,16 +520,16 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa } } - const curlCode = buildCurl(endpoint, username, password, params, selectedServer); + const curlCode = buildCurl(effectiveEndpoint, username, password, params, selectedServer); const langDef = LANGUAGES.find((l) => l.label === selectedLang) || LANGUAGES[0]; const codeToShow = selectedLang === 'cURL' ? curlCode - : generateCodeExample(endpoint, selectedLang, { username, password, params }); + : generateCodeExample(effectiveEndpoint, selectedLang, { username, password, params }); const hasAuth = endpoint.auth && endpoint.auth.length > 0; const hasQuery = endpoint.queryParams && endpoint.queryParams.length > 0; const hasPath = endpoint.pathParams && endpoint.pathParams.length > 0; - const bodyProps = endpoint.requestBody?.properties || []; - const hasBody = bodyProps.length > 0; + const bodyProps = effectiveEndpoint.requestBody?.properties || []; + const hasBody = bodyProps.length > 0 || !!variants; // Static spec responses — always shown const specResponses = endpoint.responses || {}; @@ -637,8 +684,32 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa {hasBody && ( + {variants && ( +
+ {variants.map((v, idx) => { + const active = idx === selectedVariantIdx; + return ( + + ); + })} +
+ )} {flattenedBody ? ( flattenedBody.innerFields.map((f) => ( Date: Tue, 16 Jun 2026 18:54:29 +0530 Subject: [PATCH 4/5] Variant tabs on the static API reference page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hoist selectedVariantIdx to _ApiDocPage so the Body section (EndpointDetail) and the code panel (CodeExamples) stay in sync — clicking a tab in the Body section swaps the cURL/Python/JS snippet on the right. - _ApiDocPage: add selectedVariantIdx state + reset effect on endpoint nav. - EndpointDetail: accept selectedVariantIdx + onVariantChange; render tab strip above the body fields when requestBody.variants exists; key ParamRow on `${variantIdx}-${i}` so React rebuilds rows on tab switch. - CodeExamples: accept selectedVariantIdx; shim an effectiveEndpoint with the active variant's example + properties before calling generateCodeExample. - TryItModal: accept initialVariantIdx so the modal opens on whichever variant the user was reading on the static page. PUT /api/v1/folder now shows the Rename/Move tab strip on the static page, and clicking Try It carries the current tab into the modal. Co-Authored-By: Claude Opus 4.7 --- src/component/ApiReference/CodeExamples.js | 18 +++++- src/component/ApiReference/EndpointDetail.js | 59 ++++++++++++++------ src/component/ApiReference/TryItModal.js | 6 +- src/pages/api-doc/_ApiDocPage.jsx | 16 +++++- 4 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/component/ApiReference/CodeExamples.js b/src/component/ApiReference/CodeExamples.js index d2a82dd19..297efd759 100644 --- a/src/component/ApiReference/CodeExamples.js +++ b/src/component/ApiReference/CodeExamples.js @@ -86,7 +86,7 @@ function formatResponse(value) { return JSON.stringify(value, null, 2); } -export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, onLangChange }) { +export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, onLangChange, selectedVariantIdx = 0 }) { const [localLang, setLocalLang] = useState('cURL'); const selectedLang = selectedLangProp !== undefined ? selectedLangProp : localLang; const setSelectedLang = onLangChange || setLocalLang; @@ -99,8 +99,22 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, if (!endpoint) return null; + // When the endpoint ships requestBody.variants, swap the selected variant's + // example + properties into a shimmed endpoint so the snippet matches the + // tab the user picked over in the Body section. + const variants = endpoint.requestBody?.variants; + const effectiveEndpoint = (variants && variants[selectedVariantIdx]) + ? { + ...endpoint, + requestBody: { + ...endpoint.requestBody, + properties: variants[selectedVariantIdx].properties, + example: variants[selectedVariantIdx].example, + }, + } + : endpoint; const langDef = LANGUAGES.find((l) => l.label === selectedLang) || LANGUAGES[0]; - const code = generateCodeExample(endpoint, selectedLang); + const code = generateCodeExample(effectiveEndpoint, selectedLang); const responses = endpoint.responses || {}; const responseTabs = Object.keys(responses).filter( diff --git a/src/component/ApiReference/EndpointDetail.js b/src/component/ApiReference/EndpointDetail.js index 1ccce20af..0ed955faa 100644 --- a/src/component/ApiReference/EndpointDetail.js +++ b/src/component/ApiReference/EndpointDetail.js @@ -680,7 +680,7 @@ function UrlBar({ endpoint, onTryIt }) { // ─── Main component ────────────────────────────────────────────────────────── -export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeSlot }) { +export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeSlot, selectedVariantIdx = 0, onVariantChange }) { if (!endpoint) { return (
@@ -765,22 +765,49 @@ export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeS )} {/* Request Body */} - {endpoint.requestBody && Array.isArray(endpoint.requestBody.properties) && endpoint.requestBody.properties.length > 0 && ( -
-
-

Body

- {endpoint.requestBody.contentType && ( - {endpoint.requestBody.contentType} + {endpoint.requestBody && (() => { + const variants = endpoint.requestBody.variants; + const activeVariant = variants && variants[selectedVariantIdx]; + const bodyProps = activeVariant ? activeVariant.properties : endpoint.requestBody.properties; + if (!Array.isArray(bodyProps) || bodyProps.length === 0) return null; + return ( +
+
+

Body

+ {endpoint.requestBody.contentType && ( + {endpoint.requestBody.contentType} + )} +
+ {variants && variants.length > 0 && ( +
+ {variants.map((v, idx) => { + const active = idx === selectedVariantIdx; + return ( + + ); + })} +
)} -
- {endpoint.requestBody.description && ( -

- )} - {endpoint.requestBody.properties.map((prop, i) => ( - - ))} -
- )} + {endpoint.requestBody.description && ( +

+ )} + {bodyProps.map((prop, i) => ( + + ))} + + ); + })()} {/* Response section */} diff --git a/src/component/ApiReference/TryItModal.js b/src/component/ApiReference/TryItModal.js index 60c8a394b..1da075f0c 100644 --- a/src/component/ApiReference/TryItModal.js +++ b/src/component/ApiReference/TryItModal.js @@ -301,7 +301,7 @@ function ParamField({ label, sublabel, type, required, description, value, onCha ); } -export default function TryItModal({ endpoint, onClose, selectedLang: selectedLangProp, onLangChange }) { +export default function TryItModal({ endpoint, onClose, selectedLang: selectedLangProp, onLangChange, initialVariantIdx = 0 }) { // Determine if this is a V2 endpoint (path contains /v2/ or group name contains V2) const isV2Endpoint = (endpoint.path && endpoint.path.toLowerCase().includes('/v2/')) || (endpoint.group && endpoint.group.toLowerCase().includes('v2')); @@ -329,7 +329,9 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa // (buildCurl, handleSend, generateCodeExample) operate on `effectiveEndpoint` // and stay variant-agnostic. const variants = endpoint.requestBody?.variants || null; - const [selectedVariantIdx, setSelectedVariantIdx] = useState(0); + const [selectedVariantIdx, setSelectedVariantIdx] = useState( + variants && initialVariantIdx < variants.length ? initialVariantIdx : 0 + ); const selectedVariant = variants ? variants[selectedVariantIdx] : null; const effectiveEndpoint = useMemo(() => { if (!selectedVariant) return endpoint; diff --git a/src/pages/api-doc/_ApiDocPage.jsx b/src/pages/api-doc/_ApiDocPage.jsx index c7b83e2f2..1ef987628 100644 --- a/src/pages/api-doc/_ApiDocPage.jsx +++ b/src/pages/api-doc/_ApiDocPage.jsx @@ -207,6 +207,15 @@ export default function ApiDocPage({ apiSlug, groupSlug, endpointSlug }) { const [tryItEndpoint, setTryItEndpoint] = useState(null); const [selectedLang, setSelectedLang] = useState('cURL'); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + // Active variant index for endpoints that ship requestBody.variants + // (e.g. PUT /api/v1/folder rename/move). Shared between the body section + // and the code panel so they stay in sync. Reset whenever the endpoint + // navigates. + const [selectedVariantIdx, setSelectedVariantIdx] = useState(0); + + // Reset the active variant tab whenever we navigate to a different endpoint. + const endpointKey = endpoint ? `${endpoint.method}:${endpoint.path}` : null; + useEffect(() => { setSelectedVariantIdx(0); }, [endpointKey]); // Remove alternate links (preserved from original) useEffect(() => { @@ -400,13 +409,15 @@ export default function ApiDocPage({ apiSlug, groupSlug, endpointSlug }) { } + selectedVariantIdx={selectedVariantIdx} + onVariantChange={setSelectedVariantIdx} + mobileCodeSlot={} />
- +
@@ -418,6 +429,7 @@ export default function ApiDocPage({ apiSlug, groupSlug, endpointSlug }) { onClose={() => setTryItEndpoint(null)} selectedLang={selectedLang} onLangChange={setSelectedLang} + initialVariantIdx={selectedVariantIdx} /> )} From b18f08faa2471abce73f8b6e6e43a94603d440ed Mon Sep 17 00:00:00 2001 From: srivastavaayush1611LT Date: Tue, 16 Jun 2026 19:05:48 +0530 Subject: [PATCH 5/5] Static page: stack variants vertically instead of tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per feedback, the static API reference page renders one Body section and one code panel per variant, stacked one below the other. The Try It modal keeps its tab UI (interactive surface; tabs are the right call there). - EndpointDetail: when requestBody.variants exists, render N stacked Body sections titled " ()" — e.g. "Update Folder By ID (Rename)" and "Update Folder By ID (Move)". - CodeExamples: render N stacked code panels, one per variant, sharing the language selector (lives in the first panel's header). Each panel has its own copy button. Extracted CodePanel inline subcomponent. - TryItModal: remove the unused initialVariantIdx prop; the modal manages its own variant state. - _ApiDocPage: drop the lifted selectedVariantIdx state, the reset effect, and the variant props passed to children — no longer needed. POST /api/v1/folder and all non-variant endpoints behave unchanged. Co-Authored-By: Claude Opus 4.7 --- src/component/ApiReference/CodeExamples.js | 136 +++++++++++++------ src/component/ApiReference/EndpointDetail.js | 52 +++---- src/component/ApiReference/TryItModal.js | 6 +- src/pages/api-doc/_ApiDocPage.jsx | 16 +-- 4 files changed, 115 insertions(+), 95 deletions(-) diff --git a/src/component/ApiReference/CodeExamples.js b/src/component/ApiReference/CodeExamples.js index 297efd759..7edef4944 100644 --- a/src/component/ApiReference/CodeExamples.js +++ b/src/component/ApiReference/CodeExamples.js @@ -74,6 +74,57 @@ function CodeHighlight({ code, language }) { ); } +// One stacked code panel — title bar with copy button (and optional language +// selector on the first panel of a multi-variant code section), code body below. +function CodePanel({ + title, code, language, + showLangSelector, selectedLang, setSelectedLang, + langDropdownOpen, setLangDropdownOpen, closeLangDropdown, langBtnRef, + onCopy, +}) { + const [copied, setCopied] = useState(false); + function handleCopy() { + onCopy(setCopied); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } + return ( +
+
+ {title} + {showLangSelector && ( + <> + setLangDropdownOpen((o) => !o)} + btnRef={langBtnRef} + /> + + + )} + +
+
+ +
+
+ ); +} + function formatResponse(value) { if (value == null) return ''; if (typeof value === 'string') { @@ -86,7 +137,7 @@ function formatResponse(value) { return JSON.stringify(value, null, 2); } -export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, onLangChange, selectedVariantIdx = 0 }) { +export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, onLangChange }) { const [localLang, setLocalLang] = useState('cURL'); const selectedLang = selectedLangProp !== undefined ? selectedLangProp : localLang; const setSelectedLang = onLangChange || setLocalLang; @@ -99,22 +150,28 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, if (!endpoint) return null; - // When the endpoint ships requestBody.variants, swap the selected variant's - // example + properties into a shimmed endpoint so the snippet matches the - // tab the user picked over in the Body section. + // When the endpoint ships requestBody.variants (e.g. PUT folder rename/move), + // render one code panel per variant stacked vertically. Each panel gets the + // variant's example + properties as a shimmed endpoint so the snippet matches + // that payload shape. Plain endpoints render a single panel as before. const variants = endpoint.requestBody?.variants; - const effectiveEndpoint = (variants && variants[selectedVariantIdx]) - ? { - ...endpoint, - requestBody: { - ...endpoint.requestBody, - properties: variants[selectedVariantIdx].properties, - example: variants[selectedVariantIdx].example, - }, - } - : endpoint; + const summary = endpoint.name || endpoint.summary || ''; + const codePanels = (variants && variants.length > 0) + ? variants.map((v) => ({ + title: `${summary} (${v.name})`, + code: generateCodeExample( + { + ...endpoint, + requestBody: { ...endpoint.requestBody, properties: v.properties, example: v.example }, + }, + selectedLang, + ), + })) + : [{ title: endpoint.name, code: generateCodeExample(endpoint, selectedLang) }]; const langDef = LANGUAGES.find((l) => l.label === selectedLang) || LANGUAGES[0]; - const code = generateCodeExample(effectiveEndpoint, selectedLang); + // Keep `code` available for the existing single-pane copy handler (used by + // the first panel's copy button when there are no variants). + const code = codePanels[0].code; const responses = endpoint.responses || {}; const responseTabs = Object.keys(responses).filter( @@ -143,36 +200,25 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, return (
- {/* Code Examples Panel */} -
-
- {endpoint.name} - setLangDropdownOpen((o) => !o)} - btnRef={langBtnRef} - /> - - -
-
- -
-
+ {/* Code Examples Panel(s). When the endpoint has variants we render one + panel per variant, stacked vertically. The language selector lives in + the first panel's header and applies to all panels. */} + {codePanels.map((panel, idx) => ( + copyText(panel.code, setCopied)} + /> + ))} {/* Response Panel */} {responseTabs.length > 0 && ( diff --git a/src/component/ApiReference/EndpointDetail.js b/src/component/ApiReference/EndpointDetail.js index 0ed955faa..d493dd77d 100644 --- a/src/component/ApiReference/EndpointDetail.js +++ b/src/component/ApiReference/EndpointDetail.js @@ -680,7 +680,7 @@ function UrlBar({ endpoint, onTryIt }) { // ─── Main component ────────────────────────────────────────────────────────── -export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeSlot, selectedVariantIdx = 0, onVariantChange }) { +export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeSlot }) { if (!endpoint) { return (
@@ -764,49 +764,37 @@ export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeS )} - {/* Request Body */} + {/* Request Body — one section per variant when present, otherwise the + plain body. Variants stack vertically so users see all payload shapes + (e.g. PUT folder rename/move) without a tab interaction. */} {endpoint.requestBody && (() => { const variants = endpoint.requestBody.variants; - const activeVariant = variants && variants[selectedVariantIdx]; - const bodyProps = activeVariant ? activeVariant.properties : endpoint.requestBody.properties; - if (!Array.isArray(bodyProps) || bodyProps.length === 0) return null; - return ( -
+ const summary = endpoint.name || endpoint.summary || 'Body'; + const blocks = (variants && variants.length > 0) + ? variants.map((v) => ({ + title: `${summary} (${v.name})`, + props: v.properties, + })) + : (Array.isArray(endpoint.requestBody.properties) && endpoint.requestBody.properties.length > 0 + ? [{ title: 'Body', props: endpoint.requestBody.properties }] + : []); + if (blocks.length === 0) return null; + return blocks.map((block) => ( +
-

Body

+

{block.title}

{endpoint.requestBody.contentType && ( {endpoint.requestBody.contentType} )}
- {variants && variants.length > 0 && ( -
- {variants.map((v, idx) => { - const active = idx === selectedVariantIdx; - return ( - - ); - })} -
- )} {endpoint.requestBody.description && (

)} - {bodyProps.map((prop, i) => ( - + {block.props.map((prop, i) => ( + ))}
- ); + )); })()} {/* Response section */} diff --git a/src/component/ApiReference/TryItModal.js b/src/component/ApiReference/TryItModal.js index 1da075f0c..60c8a394b 100644 --- a/src/component/ApiReference/TryItModal.js +++ b/src/component/ApiReference/TryItModal.js @@ -301,7 +301,7 @@ function ParamField({ label, sublabel, type, required, description, value, onCha ); } -export default function TryItModal({ endpoint, onClose, selectedLang: selectedLangProp, onLangChange, initialVariantIdx = 0 }) { +export default function TryItModal({ endpoint, onClose, selectedLang: selectedLangProp, onLangChange }) { // Determine if this is a V2 endpoint (path contains /v2/ or group name contains V2) const isV2Endpoint = (endpoint.path && endpoint.path.toLowerCase().includes('/v2/')) || (endpoint.group && endpoint.group.toLowerCase().includes('v2')); @@ -329,9 +329,7 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa // (buildCurl, handleSend, generateCodeExample) operate on `effectiveEndpoint` // and stay variant-agnostic. const variants = endpoint.requestBody?.variants || null; - const [selectedVariantIdx, setSelectedVariantIdx] = useState( - variants && initialVariantIdx < variants.length ? initialVariantIdx : 0 - ); + const [selectedVariantIdx, setSelectedVariantIdx] = useState(0); const selectedVariant = variants ? variants[selectedVariantIdx] : null; const effectiveEndpoint = useMemo(() => { if (!selectedVariant) return endpoint; diff --git a/src/pages/api-doc/_ApiDocPage.jsx b/src/pages/api-doc/_ApiDocPage.jsx index 1ef987628..c7b83e2f2 100644 --- a/src/pages/api-doc/_ApiDocPage.jsx +++ b/src/pages/api-doc/_ApiDocPage.jsx @@ -207,15 +207,6 @@ export default function ApiDocPage({ apiSlug, groupSlug, endpointSlug }) { const [tryItEndpoint, setTryItEndpoint] = useState(null); const [selectedLang, setSelectedLang] = useState('cURL'); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); - // Active variant index for endpoints that ship requestBody.variants - // (e.g. PUT /api/v1/folder rename/move). Shared between the body section - // and the code panel so they stay in sync. Reset whenever the endpoint - // navigates. - const [selectedVariantIdx, setSelectedVariantIdx] = useState(0); - - // Reset the active variant tab whenever we navigate to a different endpoint. - const endpointKey = endpoint ? `${endpoint.method}:${endpoint.path}` : null; - useEffect(() => { setSelectedVariantIdx(0); }, [endpointKey]); // Remove alternate links (preserved from original) useEffect(() => { @@ -409,15 +400,13 @@ export default function ApiDocPage({ apiSlug, groupSlug, endpointSlug }) { } + mobileCodeSlot={} />
- +
@@ -429,7 +418,6 @@ export default function ApiDocPage({ apiSlug, groupSlug, endpointSlug }) { onClose={() => setTryItEndpoint(null)} selectedLang={selectedLang} onLangChange={setSelectedLang} - initialVariantIdx={selectedVariantIdx} /> )}