rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
913 lines (847 loc) • 31.8 kB
text/typescript
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { guard } from "lit/directives/guard.js";
import { scheduleHighlight } from "~/utils/highlighter";
import ParamListStyles from "~/styles/param-list-styles";
import FlexStyles from "~/styles/flex-styles";
import InputStyles from "~/styles/input-styles";
import FontStyles from "~/styles/font-styles";
import BorderStyles from "~/styles/border-styles";
import TabStyles from "~/styles/tab-styles";
import MicrolighterStyles from "~/styles/microlighter-styles";
import CustomStyles from "~/styles/custom-styles";
import { constructFetchURL } from "~/utils/url-utils";
import { generateCurlCommand } from "~/utils/curl-utils";
import {
constructFetchHeaders,
buildUrlEncodedBody,
buildMultipartFormData,
serializeMultipartBody,
} from "~/utils/request-utils";
import { processFetchResponse } from "../utils/response-utils.js";
import { inputParametersTemplate } from "~/templates/request-params-template";
import requestBodyTemplate from "~/templates/request-body-template";
import apiCallTemplate from "~/templates/api-response-template";
export default class ApiRequest extends LitElement {
config: any;
serverUrl = "";
servers: any = [];
method = "";
path = "";
security: any = [];
parameters: any = [];
request_body: any;
api_keys: any = [];
parser: any;
accept = "";
callback = "";
webhook = "";
fillRequestFieldsWithExample = "";
allowTry = "true";
showCurlBeforeTry = "false";
renderStyle = "view";
schemaStyle = "tree";
activeSchemaTab = "schema";
activeParameterSchemaTabs: any = {};
schemaExpandLevel = 99999;
schemaDescriptionExpanded =
"false";
allowSchemaDescriptionExpandToggle = "false";
schemaHideReadOnly = "true";
schemaHideWriteOnly = "false";
fetchCredentials = "";
// Internal reactive state
responseMessage: any = "";
responseText = "";
responseHeaders: any = "";
responseStatus = "success";
responseUrl = "";
curlSyntax = "";
responseIsBlob = false;
responseBlobType = "";
responseBlobUrl = "";
respContentDisposition = "";
activeResponseTab = "response"; // allowed values: response, headers, curl
selectedRequestBodyType = "";
selectedRequestBodyExample = "";
fileInputKeys: any = {};
loading = false;
// Try-bar buttons follow the fields: Fill Example shows only when a field has an example,
// Clear is enabled only while a field holds a value
hasExampleToFill = false;
hasValueToClear = false;
activeAbortController: any = null;
constructor() {
super();
this.responseMessage = "";
this.responseStatus = "success";
this.responseHeaders = "";
this.responseText = "";
this.responseUrl = "";
this.curlSyntax = "";
this.responseIsBlob = false;
this.responseBlobType = "";
this.responseBlobUrl = "";
this.respContentDisposition = "";
this.activeResponseTab = "response";
this.selectedRequestBodyType = "";
this.selectedRequestBodyExample = "";
this.activeParameterSchemaTabs = {};
this.fileInputKeys = {};
this.loading = false;
}
static get styles() {
return [
ParamListStyles,
InputStyles,
FontStyles,
FlexStyles,
BorderStyles,
TabStyles,
MicrolighterStyles,
css`
*,
*:before,
*:after {
box-sizing: border-box;
}
:where(button, input[type='checkbox'], [tabindex='0']):focus-visible {
box-shadow: var(--focus-shadow);
}
:where(input[type='text'], input[type='password'], select, textarea):focus-visible {
border-color: var(--primary);
}
tag-input:focus-within {
outline: 1px solid;
}
.top-gap {
margin-top: 24px;
}
/* The first label under the Request heading belongs to it; no section gap */
.request-panel > div > .top-gap:first-child,
.request-panel > div > .request-body-container:first-child > .top-gap {
margin-top: 4px;
}
.textarea {
min-height: 220px;
padding: 5px;
resize: vertical;
direction: ltr;
}
.response-message {
font-weight: bold;
text-overflow: ellipsis;
&.error {
color: var(--destructive);
}
&.success {
color: var(--blue);
}
}
.file-input-container {
align-items: flex-start;
width: 100%;
/* Each file input spans the full width; its remove button sits inside the box, at the right edge */
& .input-set {
position: relative;
width: 100%;
display: flex;
align-items: center;
&:first-child .file-input-remove-btn {
display: none;
}
&:not(:first-child) input[type='file'] {
padding-right: 2.25em;
}
}
/* Inset by the file input's border + padding, so it lines up with Choose File */
& .file-input-add-btn {
align-self: flex-start;
margin: 4px 0 0 0.45em;
font-weight: 400;
}
}
.file-input-remove-btn {
position: absolute;
top: 50%;
right: 0.45em;
transform: translateY(-50%);
padding: 0 0.25em;
font-size: 1.25em;
line-height: 1;
color: var(--destructive);
border: none;
border-radius: var(--radius);
background: none;
cursor: pointer;
&:hover {
background: color-mix(in srgb, var(--destructive) 15%, transparent);
}
&:focus-visible {
outline: none;
box-shadow: var(--focus-shadow);
}
}
.v-tab-btn {
font-size: var(--font-size-small);
height: 24px;
border: none;
background: none;
opacity: 0.3;
cursor: pointer;
padding: 4px 8px;
&.active {
font-weight: bold;
background: var(--background);
opacity: 1;
}
}
(min-width: 768px) {
.textarea {
padding: 8px;
}
}
/*
* Try bar: server/auth summary on the left, actions on the right. The summary is a two-row
* label/value list sized like a parameter label (name over type). The bar is its own container,
* so it reflows by the width it has, not the viewport.
*/
.try-bar {
container-type: inline-size;
margin: 16px 0;
font-size: var(--font-size-small);
}
/* Narrow: summary on top, actions below it on the right */
.try-layout {
display: flex;
flex-direction: column;
gap: 0.75em;
}
.try-info {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 0.25em 0.75em;
margin: 0;
line-height: 1.35;
& > dt {
font-weight: 600;
color: var(--foreground);
}
& > dd {
margin: 0;
min-width: 0;
color: var(--muted-foreground);
overflow-wrap: anywhere;
}
}
.try-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.5em;
}
/* Wide: one row; the summary takes the free space and the actions keep their size */
(min-width: 34rem) {
.try-layout {
flex-direction: row;
align-items: center;
gap: 1.5em;
}
.try-info {
flex: 1;
min-width: 0;
}
.try-actions {
flex: none;
}
}
/* Very narrow: each label above its value */
(max-width: 20rem) {
.try-info {
grid-template-columns: minmax(0, 1fr);
gap: 0;
& > dd + dt {
margin-top: 0.5em;
}
}
}
`,
CustomStyles,
];
}
/** Shared schema & display configurations that affect both parameters and request body */
get commonConfigDeps() {
return [
this.schemaStyle,
this.schemaExpandLevel,
this.schemaDescriptionExpanded,
this.allowSchemaDescriptionExpandToggle,
this.schemaHideReadOnly,
this.schemaHideWriteOnly,
this.fillRequestFieldsWithExample,
this.renderStyle,
this.allowTry,
this.callback,
this.webhook,
];
}
/** Dependencies for query/path/header/cookie parameters */
get parameterGuardDeps() {
return [
this.method,
this.path,
this.parameters,
this.activeParameterSchemaTabs,
...this.commonConfigDeps,
];
}
/** Dependencies for the request body template */
get requestBodyGuardDeps() {
return [
this.method,
this.path,
this.request_body,
this.selectedRequestBodyType,
this.selectedRequestBodyExample,
this.activeSchemaTab,
this.fileInputKeys,
...this.commonConfigDeps,
];
}
render() {
return html`<div
="${this.syncTryBarState}"
="${this.syncTryBarState}"
class="col regular-font request-panel ${
"read focused".includes(this.renderStyle) || this.callback === "true"
? "read-mode"
: "view-mode"
}"
>
${
this.callback === "true"
? html`<h6 class="rapidoc-heading label-heading tiny-title" part="section-request-title heading heading-6">Callback request</h6>`
: html`<h4 class="rapidoc-heading label-heading req-res-title" part="section-request-title heading heading-4">Request</h4>`
}
<div>
${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "path"))}
${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "query"))}
${guard(this.requestBodyGuardDeps, () => requestBodyTemplate.call(this))}
${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "header"))}
${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "cookie"))}
${this.allowTry === "false" ? "" : html`${apiCallTemplate.call(this)}`}
</div>
</div>`;
}
async updated(changedProperties) {
if (this.showCurlBeforeTry === "true") {
if (
!changedProperties ||
!changedProperties.has("curlSyntax") ||
changedProperties.size > 1
) {
this.applyCURLSyntax(this.shadowRoot);
}
}
scheduleHighlight(this.getRootNode()?.host?.shadowRoot || this.shadowRoot);
if (this.webhook === "true") {
this.allowTry = "false";
}
// After the fields are in the DOM; deferred so the update is not requested mid-update
queueMicrotask(() => this.syncTryBarState());
}
// This method is called before navigation change in focused mode
async beforeNavigationFocusedMode() {}
// Called by rapi-doc after navigating to a new endpoint in focused renderStyle.
// In focused mode, the single <api-request> element instance is reused across operations.
// We reset selectedRequestBodyType and selectedRequestBodyExample so stale MIME type/example selections
// from a previous endpoint don't carry over to an endpoint that may not support them.
// The first valid MIME type will be automatically selected by request-body-template during render.
async afterNavigationFocusedMode() {
this.selectedRequestBodyType = "";
this.selectedRequestBodyExample = "";
this.clearResponseData();
}
// Request-Body Event Handlers
onSelectExample(e) {
this.selectedRequestBodyExample = e.target.value;
const exampleDropdownEl = e.target;
window.setTimeout(
(selectEl) => {
const readOnlyExampleEl = selectEl
.closest(".example-panel")
.querySelector(".request-body-param");
const userInputExampleTextareaEl = selectEl
.closest(".example-panel")
.querySelector(".request-body-param-user-input");
userInputExampleTextareaEl.value = readOnlyExampleEl.innerText;
const requestPanelEl = this.getRequestPanel({ target: selectEl });
this.liveCURLSyntaxUpdate(requestPanelEl);
},
0,
exampleDropdownEl,
);
}
onMimeTypeChange(e) {
this.selectedRequestBodyType = e.target.value;
const mimeDropdownEl = e.target;
this.selectedRequestBodyExample = "";
window.setTimeout(
(selectEl) => {
const readOnlyExampleEl = selectEl
.closest(".request-body-container")
.querySelector(".request-body-param");
if (readOnlyExampleEl) {
const userInputExampleTextareaEl = selectEl
.closest(".request-body-container")
.querySelector(".request-body-param-user-input");
userInputExampleTextareaEl.value = readOnlyExampleEl.innerText;
}
},
0,
mimeDropdownEl,
);
}
async onFillRequestData(e) {
const requestPanelEl = e.target.closest(".request-panel");
const requestPanelInputEls = [
...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)"),
];
requestPanelInputEls.forEach((el) => {
if (el.dataset.example) {
if (el.tagName.toUpperCase() === "TAG-INPUT") {
el.value = el.dataset.example.split("~|~");
} else {
el.value = el.dataset.example;
}
}
});
this.syncTryBarState();
}
async onClearRequestData(e) {
const requestPanelEl = e.target.closest(".request-panel");
const requestPanelInputEls = [
...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)"),
];
requestPanelInputEls.forEach((el) => {
el.value = "";
});
this.syncTryBarState();
}
// Reads the request fields (the same ones Fill Example and Clear act on) and updates the
// try-bar flags; setting a flag to its current value schedules no render
syncTryBarState() {
const requestPanelEl = this.shadowRoot?.querySelector(".request-panel");
const inputEls: any[] = requestPanelEl
? [...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)")]
: [];
this.hasExampleToFill = inputEls.some((el) => !!el.dataset.example);
this.hasValueToClear = inputEls.some((el) =>
Array.isArray(el.value) ? el.value.length > 0 : el.value !== "",
);
}
buildFetchURL(requestPanelEl) {
const pathParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='path']")];
const queryParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='query']")];
const queryParamObjTypeEls = [
...requestPanelEl.querySelectorAll("[data-ptype='query-object']"),
];
const pathParams = {};
pathParamEls.forEach((el) => {
pathParams[el.dataset.pname] = el.value;
});
const queryParams = [];
queryParamEls.forEach((el) => {
const isArray = el.dataset.array !== "false";
const style = el.dataset.paramSerializeStyle || "form";
const explode =
el.dataset.paramSerializeExplode !== undefined
? el.dataset.paramSerializeExplode === "true"
: style === "form";
const allowReserved = el.dataset.paramAllowReserved === "true";
queryParams.push({
name: el.dataset.pname,
value: el.value,
style,
explode,
allowReserved,
isArray,
itemType: el.dataset.itemType || "",
});
});
queryParamObjTypeEls.forEach((el) => {
const style = el.dataset.paramSerializeStyle || "form";
const explode =
el.dataset.paramSerializeExplode !== undefined
? el.dataset.paramSerializeExplode === "true"
: style === "form";
const allowReserved = el.dataset.paramAllowReserved === "true";
queryParams.push({
name: el.dataset.pname,
value: el.value,
style,
explode,
allowReserved,
isObject: true,
});
});
return constructFetchURL({
serverUrl: this.serverUrl,
path: this.path,
pathParams,
queryParams,
apiKeys: this.api_keys,
});
}
buildFetchHeaders(requestPanelEl) {
const respEl = this.closest(
".expanded-req-resp-container, .req-resp-container",
)?.getElementsByTagName("api-response")[0];
const headerParamEls = [
...requestPanelEl.querySelectorAll("[data-ptype='header'], [data-ptype='header-object']"),
];
const requestBodyContainerEl = requestPanelEl.querySelector(".request-body-container");
const accept = respEl?.selectedMimeType || this.accept || "";
const contentType = requestBodyContainerEl?.dataset?.selectedRequestBodyType || "";
const headerParams = headerParamEls.map((el) => ({
name: el.dataset.pname,
value: el.value,
explode: el.dataset.paramSerializeExplode === "true",
isObject: el.dataset.ptype === "header-object",
}));
return constructFetchHeaders({
accept,
apiKeys: this.api_keys,
headerParams,
contentType,
});
}
buildFetchBodyOptions(requestPanelEl) {
const requestBodyContainerEl = requestPanelEl.querySelector(".request-body-container");
const fetchOptions: any = {
method: this.method.toUpperCase(),
};
if (requestBodyContainerEl) {
const requestBodyType = requestBodyContainerEl.dataset.selectedRequestBodyType || "";
if (requestBodyType.includes("form-urlencoded")) {
const formUrlDynamicTextAreaEl = requestPanelEl.querySelector(
"[data-ptype='dynamic-form']",
);
if (formUrlDynamicTextAreaEl) {
fetchOptions.body = buildUrlEncodedBody([], formUrlDynamicTextAreaEl.value);
} else {
const formUrlEls = [...requestPanelEl.querySelectorAll("[data-ptype='form-urlencode']")];
const fields = formUrlEls
.filter((v: any) => v.type !== "file")
.map((el: any) => ({
name: el.dataset.pname,
value: el.value,
isArray: el.dataset.array !== "false",
}));
fetchOptions.body = buildUrlEncodedBody(fields);
}
} else if (requestBodyType.includes("form-data")) {
const formDataEls = [...requestPanelEl.querySelectorAll("[data-ptype='form-data']")];
const fields = formDataEls.map((el: any) => ({
name: el.dataset.pname,
value: el.value,
file: el.type === "file" ? el.files?.[0] : undefined,
isArray: el.dataset.array !== "false",
}));
fetchOptions.body = buildMultipartFormData(fields);
// Content-Type of the parts that need one (application/json for objects, or the spec's encoding)
const partContentTypes = Object.fromEntries(
formDataEls
.filter((el: any) => el.dataset.contentType)
.map((el: any) => [el.dataset.pname, el.dataset.contentType]),
);
if (Object.keys(partContentTypes).length > 0) {
fetchOptions.partContentTypes = partContentTypes;
}
} else if (
/^audio\/|^image\/|^video\/|^font\/|tar$|zip$|7z$|rtf$|msword$|excel$|\/pdf$|\/octet-stream$/.test(
requestBodyType,
)
) {
const bodyParamFileEl: any = requestPanelEl.querySelector(".request-body-param-file");
if (bodyParamFileEl?.files[0]) {
fetchOptions.body = bodyParamFileEl.files[0];
}
} else if (
requestBodyType.includes("json") ||
requestBodyType.includes("xml") ||
requestBodyType.includes("text")
) {
const exampleTextAreaEl: any = requestPanelEl.querySelector(
".request-body-param-user-input",
);
if (exampleTextAreaEl?.value) {
fetchOptions.body = exampleTextAreaEl.value;
}
}
}
return fetchOptions;
}
async onTryClick(e) {
const tryBtnEl = e?.target;
const requestPanelEl = tryBtnEl
? this.getRequestPanel(e)
: this.shadowRoot.querySelector(".request-panel");
const fetchUrl = this.buildFetchURL(requestPanelEl);
const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
const reqHeaders = this.buildFetchHeaders(requestPanelEl);
this.responseUrl = "";
this.responseHeaders = [];
this.curlSyntax = this.generateCURLSyntax(fetchUrl, reqHeaders, fetchOptions, requestPanelEl);
this.responseStatus = "success";
this.responseIsBlob = false;
this.respContentDisposition = "";
if (this.responseBlobUrl) {
URL.revokeObjectURL(this.responseBlobUrl);
this.responseBlobUrl = "";
}
if (this.fetchCredentials) {
fetchOptions.credentials = this.fetchCredentials;
}
const controller = new AbortController();
this.activeAbortController = controller;
const { signal } = controller;
fetchOptions.headers = reqHeaders;
const tempRequest = { url: fetchUrl, ...fetchOptions };
this.dispatchEvent(
new CustomEvent("before-try", {
bubbles: true,
composed: true,
detail: {
request: tempRequest,
controller,
},
}),
);
const updatedFetchOptions = {
method: tempRequest.method,
headers: tempRequest.headers,
credentials: tempRequest.credentials,
body: tempRequest.body,
};
// FormData can't give a text part a Content-Type, so such bodies are serialized here
if (tempRequest.body instanceof FormData && tempRequest.partContentTypes) {
const multipart = serializeMultipartBody(tempRequest.body, tempRequest.partContentTypes);
const headers = new Headers(tempRequest.headers);
headers.set("Content-Type", multipart.contentType);
updatedFetchOptions.headers = headers;
updatedFetchOptions.body = multipart.body;
}
let fetchRequest;
let fetchResponse;
let responseClone;
try {
this.loading = true;
this.responseText = "⌛";
this.responseMessage = "";
fetchRequest = new Request(tempRequest.url, updatedFetchOptions);
const startTime = performance.now();
fetchResponse = await fetch(fetchRequest, { signal });
const endTime = performance.now();
responseClone = fetchResponse.clone(); // create a response clone to allow reading response body again (response.json, response.text etc)
this.responseMessage = html`${fetchResponse.statusText ? `${fetchResponse.statusText}:${fetchResponse.status}` : fetchResponse.status}
<div style="color:var(--muted-foreground)">Took ${Math.round(endTime - startTime)} milliseconds</div>`;
this.responseUrl = fetchResponse.url;
const respHeadersObj = {};
fetchResponse.headers.forEach((hdrVal, hdr) => {
respHeadersObj[hdr] = hdrVal;
this.responseHeaders = `${this.responseHeaders}${hdr}: ${hdrVal}\n`;
});
const processed = await processFetchResponse(fetchResponse);
this.responseText = processed.responseText;
this.responseIsBlob = processed.responseIsBlob;
this.responseBlobType = processed.responseBlobType;
this.responseBlobUrl = processed.responseBlobUrl;
this.respContentDisposition = processed.respContentDisposition;
this.dispatchEvent(
new CustomEvent("after-try", {
bubbles: true,
composed: true,
detail: {
request: fetchRequest,
response: responseClone,
responseHeaders: respHeadersObj,
responseBody: processed.respJson || processed.respText || processed.respBlob,
responseStatus: responseClone.ok,
},
}),
);
} catch (err) {
if (err.name === "AbortError") {
this.dispatchEvent(
new CustomEvent("request-aborted", {
bubbles: true,
composed: true,
detail: {
err,
request: fetchRequest,
},
}),
);
this.responseMessage = "Request Aborted";
this.responseText = "Request Aborted";
} else {
this.dispatchEvent(
new CustomEvent("after-try", {
bubbles: true,
composed: true,
detail: {
err,
request: fetchRequest,
},
}),
);
if (tempRequest.method?.toUpperCase() === "TRACE") {
this.responseMessage = `${err.message} (Browser security restricts TRACE requests in fetch. Use the cURL snippet below.)`;
} else {
this.responseMessage = `${err.message} (CORS or Network Issue)`;
}
}
} finally {
this.loading = false;
this.activeAbortController = null;
}
}
liveCURLSyntaxUpdate(requestPanelEl) {
this.applyCURLSyntax(requestPanelEl);
}
onGenerateCURLClick(e) {
const requestPanelEl = this.getRequestPanel(e);
this.applyCURLSyntax(requestPanelEl);
}
getRequestPanel(e) {
return e.target.closest(".request-panel");
}
applyCURLSyntax(requestPanelEl) {
const fetchUrl = this.buildFetchURL(requestPanelEl);
const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
const fetchHeaders = this.buildFetchHeaders(requestPanelEl);
const newCurlSyntax = this.generateCURLSyntax(
fetchUrl,
fetchHeaders,
fetchOptions,
requestPanelEl,
);
if (this.curlSyntax !== newCurlSyntax) {
this.curlSyntax = newCurlSyntax;
}
}
generateCURLSyntax(fetchUrl, fetchHeaders, fetchOptions, requestPanelEl) {
const requestBodyContainerEl = requestPanelEl?.querySelector(".request-body-container");
const requestBodyType = requestBodyContainerEl?.dataset?.selectedRequestBodyType || "";
let body = fetchOptions?.body;
if (!body && requestBodyContainerEl) {
const exampleTextAreaEl: any = requestPanelEl.querySelector(".request-body-param-user-input");
if (exampleTextAreaEl?.value) {
body = exampleTextAreaEl.value;
}
}
return generateCurlCommand({
method: this.method,
url: fetchUrl,
headers: fetchHeaders,
body,
contentType: requestBodyType,
partContentTypes: fetchOptions?.partContentTypes,
});
}
onAddFileInput(fieldName) {
const currentKeys = this.fileInputKeys[fieldName] ?? [0];
const nextKey = currentKeys.length > 0 ? Math.max(...currentKeys) + 1 : 0;
this.fileInputKeys = {
...this.fileInputKeys,
[fieldName]: [...currentKeys, nextKey],
};
}
onRemoveFileInput(fieldName, keyToRemove) {
const currentKeys = this.fileInputKeys[fieldName] ?? [0];
this.fileInputKeys = {
...this.fileInputKeys,
[fieldName]: currentKeys.filter((k) => k !== keyToRemove),
};
}
clearResponseData() {
this.responseUrl = "";
this.responseHeaders = "";
this.responseText = "";
this.responseStatus = "success";
this.responseMessage = "";
this.responseIsBlob = false;
this.responseBlobType = "";
this.respContentDisposition = "";
this.fileInputKeys = {};
if (this.responseBlobUrl) {
URL.revokeObjectURL(this.responseBlobUrl);
this.responseBlobUrl = "";
}
}
willUpdate(changedProperties) {
super.willUpdate?.(changedProperties);
// Sync configuration options passed down from parent <rapi-doc>
if (this.config && changedProperties.has("config")) {
if (this.config.renderStyle) this.renderStyle = this.config.renderStyle;
if (this.config.schemaStyle) this.schemaStyle = this.config.schemaStyle;
if (this.config.schemaExpandLevel !== undefined)
this.schemaExpandLevel = this.config.schemaExpandLevel;
if (this.config.schemaDescriptionExpanded !== undefined)
this.schemaDescriptionExpanded = this.config.schemaDescriptionExpanded;
if (this.config.allowSchemaDescriptionExpandToggle !== undefined)
this.allowSchemaDescriptionExpandToggle = this.config.allowSchemaDescriptionExpandToggle;
// In incoming requests, readOnly fields are hidden by default ('true') unless configured to 'never'.
// For incoming webhooks, the direction is inverted (the external caller sends the payload), so readOnly fields are shown ('false').
if (this.config.schemaHideReadOnly !== undefined)
this.schemaHideReadOnly =
this.config.schemaHideReadOnly === "never"
? "false"
: this.webhook === "true"
? "false"
: "true";
// In incoming requests, writeOnly fields are shown by default ('false').
// For webhooks, writeOnly fields are hidden ('true') unless configured to 'never'.
if (this.config.schemaHideWriteOnly !== undefined)
this.schemaHideWriteOnly =
this.config.schemaHideWriteOnly === "never"
? "false"
: this.webhook === "true"
? "true"
: "false";
if (this.config.fillRequestFieldsWithExample !== undefined)
this.fillRequestFieldsWithExample = this.config.fillRequestFieldsWithExample;
if (this.config.allowTry !== undefined) this.allowTry = this.config.allowTry;
if (this.config.showCurlBeforeTry !== undefined)
this.showCurlBeforeTry = this.config.showCurlBeforeTry;
if (this.config.fetchCredentials !== undefined)
this.fetchCredentials = this.config.fetchCredentials;
if (this.config.defaultSchemaTab) this.activeSchemaTab = this.config.defaultSchemaTab;
}
}
disconnectedCallback() {
this.curlSyntax = "";
if (this.activeAbortController) {
this.activeAbortController.abort();
this.activeAbortController = null;
}
// Cleanup ObjectURL for the blob data if this component created one
if (this.responseBlobUrl) {
URL.revokeObjectURL(this.responseBlobUrl);
this.responseBlobUrl = "";
}
super.disconnectedCallback();
}
}