UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

1,037 lines (989 loc) 82.2 kB
import { LitElement, html, css } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; // eslint-disable-line import/extensions import { guard } from 'lit/directives/guard.js'; // eslint-disable-line import/extensions import { live } from 'lit/directives/live.js'; // eslint-disable-line import/extensions import { ifDefined } from 'lit/directives/if-defined.js'; // eslint-disable-line import/extensions import { marked } from 'marked'; import formatXml from 'xml-but-prettier'; import Prism from 'prismjs'; import TableStyles from '~/styles/table-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 PrismStyles from '~/styles/prism-styles'; import CustomStyles from '~/styles/custom-styles'; import { copyToClipboard, downloadResource, viewResource } from '~/utils/common-utils'; import { schemaInObjectNotation, getTypeInfo, generateExample, normalizeExamples, getSchemaFromParam, json2xml, standardizeExample, anyExampleWithSummaryOrDescription } from '~/utils/schema-utils'; import '~/components/json-tree'; import '~/components/schema-tree'; import '~/components/tag-input'; export default class ApiRequest extends LitElement { constructor() { super(); this.responseMessage = ''; this.responseStatus = 'success'; this.responseHeaders = ''; this.responseText = ''; this.responseUrl = ''; this.curlSyntax = ''; this.activeResponseTab = 'response'; // allowed values: response, headers, curl this.selectedRequestBodyType = ''; this.selectedRequestBodyExample = ''; this.activeParameterSchemaTabs = {}; } static get properties() { return { serverUrl: { type: String, attribute: 'server-url' }, servers: { type: Array }, method: { type: String }, path: { type: String }, security: { type: Array }, parameters: { type: Array }, request_body: { type: Object }, api_keys: { type: Array }, parser: { type: Object }, accept: { type: String }, callback: { type: String }, webhook: { type: String }, responseMessage: { type: String, attribute: false }, responseText: { type: String, attribute: false }, responseHeaders: { type: String, attribute: false }, responseStatus: { type: String, attribute: false }, responseUrl: { type: String, attribute: false }, curlSyntax: { type: String, attribute: false }, fillRequestFieldsWithExample: { type: String, attribute: 'fill-request-fields-with-example' }, allowTry: { type: String, attribute: 'allow-try' }, showCurlBeforeTry: { type: String, attribute: 'show-curl-before-try' }, renderStyle: { type: String, attribute: 'render-style' }, schemaStyle: { type: String, attribute: 'schema-style' }, activeSchemaTab: { type: String, attribute: 'active-schema-tab' }, activeParameterSchemaTabs: { type: Object, converter: { fromAttribute: (attr) => JSON.parse(attr), toAttribute: (prop) => JSON.stringify(prop), }, attribute: 'active-parameter-schema-tabs', }, schemaExpandLevel: { type: Number, attribute: 'schema-expand-level' }, schemaDescriptionExpanded: { type: String, attribute: 'schema-description-expanded' }, allowSchemaDescriptionExpandToggle: { type: String, attribute: 'allow-schema-description-expand-toggle' }, schemaHideReadOnly: { type: String, attribute: 'schema-hide-read-only' }, schemaHideWriteOnly: { type: String, attribute: 'schema-hide-write-only' }, fetchCredentials: { type: String, attribute: 'fetch-credentials' }, // properties for internal tracking activeResponseTab: { type: String }, // internal tracking of response-tab not exposed as a attribute selectedRequestBodyType: { type: String, attribute: 'selected-request-body-type' }, // internal tracking of selected request-body type selectedRequestBodyExample: { type: String, attribute: 'selected-request-body-example' }, // internal tracking of selected request-body example }; } static get styles() { return [ TableStyles, InputStyles, FontStyles, FlexStyles, BorderStyles, TabStyles, PrismStyles, css` :host { container-type: inline-size; } *, *: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-color); } tag-input:focus-within { outline: 1px solid;} .read-mode { margin-top: 24px; } .param-name, .param-type { margin: 1px 0; text-align: right; line-height: var(--font-size-small); } .param-name { color: var(--fg); font-family: var(--font-mono); } .param-name.deprecated { color: var(--red); } .param-type{ color: var(--light-fg); font-family: var(--font-regular); } .param-constraint{ min-width:100px; } .param-constraint:empty{ display:none; } .top-gap{margin-top:24px;} .textarea { min-height:220px; padding:5px; resize:vertical; direction: ltr; } .example:first-child { margin-top: -9px; } .response-message{ font-weight:bold; text-overflow: ellipsis; } .response-message.error { color:var(--red); } .response-message.success { color:var(--blue); } .file-input-container { align-items:flex-end; } .file-input-container .input-set:first-child .file-input-remove-btn{ visibility:hidden; } .file-input-remove-btn{ font-size:16px; color:var(--red); outline: none; border: none; background:none; cursor:pointer; } .v-tab-btn { font-size: var(--smal-font-size); height:24px; border:none; background:none; opacity: 0.3; cursor: pointer; padding: 4px 8px; } .v-tab-btn.active { font-weight: bold; background: var(--bg); opacity: 1; } @container (min-width: 768px) { .textarea { padding:8px; } } @container (max-width: 470px) { .hide-in-small-screen { display:none; } } `, CustomStyles, ]; } /* eslint-disable indent */ render() { return html` <div class="col regular-font request-panel ${'read focused'.includes(this.renderStyle) || this.callback === 'true' ? 'read-mode' : 'view-mode'}"> <div class=" ${this.callback === 'true' ? 'tiny-title' : 'req-res-title'} "> ${this.callback === 'true' ? 'CALLBACK REQUEST' : 'REQUEST'} </div> <div> ${guard([this.method, this.path, this.allowTry, this.parameters, this.activeParameterSchemaTabs], () => this.inputParametersTemplate('path'))} ${guard([this.method, this.path, this.allowTry, this.parameters, this.activeParameterSchemaTabs], () => this.inputParametersTemplate('query'))} ${this.requestBodyTemplate()} ${guard([this.method, this.path, this.allowTry, this.parameters, this.activeParameterSchemaTabs], () => this.inputParametersTemplate('header'))} ${guard([this.method, this.path, this.allowTry, this.parameters, this.activeParameterSchemaTabs], () => this.inputParametersTemplate('cookie'))} ${this.allowTry === 'false' ? '' : html`${this.apiCallTemplate()}`} </div> </div> `; } async updated() { if (this.showCurlBeforeTry === 'true') { this.applyCURLSyntax(this.shadowRoot); } // In focused mode after rendering the request component, update the text-areas(which contains examples) using // the original values from hidden textareas // This is done coz, user may update the dom by editing the textarea's and once the DOM is updated externally change detection wont happen, therefore update the values manually // if (this.renderStyle === 'focused') { // if (changedProperties.size === 1 && changedProperties.has('activeSchemaTab')) { // // dont update example as only tabs is switched // } else { // this.requestUpdate(); // } // } if (this.webhook === 'true') { this.allowTry = 'false'; } } async saveExampleState() { if (this.renderStyle === 'focused') { const reqBodyTextAreaEls = [...this.shadowRoot.querySelectorAll('textarea.request-body-param-user-input')]; reqBodyTextAreaEls.forEach((el) => { el.dataset.user_example = el.value; }); const exampleTextAreaEls = [...this.shadowRoot.querySelectorAll('textarea[data-ptype="form-data"]')]; exampleTextAreaEls.forEach((el) => { el.dataset.user_example = el.value; }); this.requestUpdate(); } } async updateExamplesFromDataAttr() { // In focused mode after rendering the request component, update the text-areas(which contains examples) using // the original values from hidden textareas // This is done coz, user may update the dom by editing the textarea's and once the DOM is updated externally change detection wont happen, therefore update the values manually if (this.renderStyle === 'focused') { const reqBodyTextAreaEls = [...this.shadowRoot.querySelectorAll('textarea.request-body-param-user-input')]; reqBodyTextAreaEls.forEach((el) => { el.value = el.dataset.user_example || el.dataset.example; }); const exampleTextAreaEls = [...this.shadowRoot.querySelectorAll('textarea[data-ptype="form-data"]')]; exampleTextAreaEls.forEach((el) => { el.value = el.dataset.user_example || el.dataset.example; }); this.requestUpdate(); } } renderExample(example, paramType, paramName) { return html` ${paramType === 'array' ? '[' : ''} <a part="anchor anchor-param-example" style="display:inline-block; min-width:24px; text-align:center" class="${this.allowTry === 'true' ? '' : 'inactive-link'}" data-example-type="${paramType === 'array' ? paramType : 'string'}" data-example="${example.value && Array.isArray(example.value) ? example.value?.join('~|~') : (typeof example.value === 'object' ? JSON.stringify(example.value, null, 2) : example.value) || ''}" title="${example.value && Array.isArray(example.value) ? example.value?.join('~|~') : (typeof example.value === 'object' ? JSON.stringify(example.value, null, 2) : example.value) || ''}" @click="${(e) => { const inputEl = e.target.closest('table').querySelector(`[data-pname="${paramName}"]`); if (inputEl) { inputEl.value = e.target.dataset.exampleType === 'array' ? e.target.dataset.example.split('~|~') : e.target.dataset.example; } }}" > ${example.printableValue || example.value} </a> ${paramType === 'array' ? '] ' : ''} `; } renderShortFormatExamples(examples, paramType, paramName) { return html`${examples.map((x, i) => html` ${i === 0 ? '' : '┃'} ${this.renderExample(x, paramType, paramName)}`)}`; } renderLongFormatExamples(exampleList, paramType, paramName) { return html` <ul style="list-style-type: disclosure-closed;"> ${exampleList.map((v) => html` <li> ${this.renderExample(v, paramType, paramName)} ${v.summary?.length > 0 ? html`<span>&lpar;${v.summary}&rpar;</span>` : ''} ${v.description?.length > 0 ? html`<p>${unsafeHTML(marked(v.description))}</p>` : ''} </li> `)} </ul>`; } exampleListTemplate(paramName, paramType, exampleList = []) { return html` ${ exampleList.length > 0 ? html`<span style="font-weight:bold">Examples: </span> ${anyExampleWithSummaryOrDescription(exampleList) ? this.renderLongFormatExamples(exampleList, paramType, paramName) : this.renderShortFormatExamples(exampleList, paramType, paramName)}` : '' }`; } inputParametersTemplate(paramType) { const filteredParams = this.parameters ? this.parameters.filter((param) => param.in === paramType) : []; if (filteredParams.length === 0) { return ''; } let title = ''; if (paramType === 'path') { title = 'PATH PARAMETERS'; } else if (paramType === 'query') { title = 'QUERY-STRING PARAMETERS'; } else if (paramType === 'header') { title = 'REQUEST HEADERS'; } else if (paramType === 'cookie') { title = 'COOKIES'; } const tableRows = []; for (const param of filteredParams) { const [declaredParamSchema, serializeStyle, mimeTypeElem] = getSchemaFromParam(param); if (!declaredParamSchema) { continue; } const paramSchema = getTypeInfo(declaredParamSchema); if (!paramSchema) { continue; // eslint-disable-line no-continue } const schemaAsObj = schemaInObjectNotation(declaredParamSchema, {}); // let exampleVal = ''; // let exampleList = []; let paramStyle = 'form'; let paramExplode = true; let paramAllowReserved = false; if (paramType === 'query' || paramType === 'header' || paramType === 'path') { if (param.style && 'form spaceDelimited pipeDelimited'.includes(param.style)) { paramStyle = param.style; } else if (serializeStyle) { paramStyle = serializeStyle; } if (typeof param.explode === 'boolean') { paramExplode = param.explode; } if (typeof param.allowReserved === 'boolean') { paramAllowReserved = param.allowReserved; } } // openapi 3.1.0 spec based examples (which must be Object(string : { value:any, summary?: string, description?: string}) const example = normalizeExamples( (standardizeExample(param.examples) || standardizeExample(param.example) || standardizeExample(mimeTypeElem?.example) || standardizeExample(mimeTypeElem?.examples) || standardizeExample(paramSchema.examples) || standardizeExample(paramSchema.example) ), paramSchema.type, ); if (!example.exampleVal && paramSchema.type === 'object') { example.exampleVal = generateExample( declaredParamSchema, serializeStyle || 'json', {}, {}, this.callback === 'true' || this.webhook === 'true' ? true : false, // eslint-disable-line no-unneeded-ternary this.callback === 'true' || this.webhook === 'true' ? false : true, // eslint-disable-line no-unneeded-ternary true, 'text', false, )[0].exampleValue; } const labelColWidth = 'read focused'.includes(this.renderStyle) ? '200px' : '160px'; tableRows.push(html` <tr title="${param.deprecated ? 'Deprecated' : ''}"> <td rowspan="${this.allowTry === 'true' ? '1' : '2'}" style="width:${labelColWidth}; min-width:100px;"> <div class="param-name ${param.deprecated ? 'deprecated' : ''}" > ${param.deprecated ? html`<svg viewBox="0 0 10 10" width="10" height="10" style="stroke:var(--red); margin-right:-6px"><path d="M2 2L8 8M2 8L8 2"/></svg>` : '' } ${param.required ? html`<span style='color:var(--red)'>*</span>` : ''} ${param.name} </div> <div class="param-type"> ${paramSchema.type === 'array' ? `${paramSchema.arrayType}` : `${paramSchema.format ? paramSchema.format : paramSchema.type}` } </div> </td> ${this.allowTry === 'true' ? html` <td style="min-width:100px;" colspan="${paramSchema.default || paramSchema.constrain || paramSchema.allowedValues || paramSchema.pattern ? '1' : '2'}"> ${paramSchema.type === 'array' ? html` <tag-input class="request-param" id = "tag-input-request-param-${param.name}" style = "width:100%" data-ptype = "${paramType}" data-pname = "${param.name}" data-example = "${Array.isArray(example.exampleVal) ? example.exampleVal.join('~|~') : example.exampleVal}" data-param-serialize-style = "${paramStyle}" data-param-serialize-explode = "${paramExplode}" data-param-allow-reserved = "${paramAllowReserved}" data-x-fill-example = "${param['x-fill-example'] || 'yes'}" data-array = "true" placeholder = "add-multiple &#x21a9;" .value="${param['x-fill-example'] === 'no' ? [] : live(this.fillRequestFieldsWithExample === 'true' ? Array.isArray(example.exampleVal) ? example.exampleVal : [example.exampleVal] : []) }" > </tag-input>` : paramSchema.type === 'object' ? html` <div part="tab-panel" class="tab-panel col" style="border-width:0 0 1px 0;"> <div part="tab-btn-row" class="tab-buttons row" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { const newState = { ...this.activeParameterSchemaTabs }; newState[param.name] = e.target.dataset.tab; this.activeParameterSchemaTabs = newState; } }}"> <button part="tab-btn" class="tab-btn ${this.activeParameterSchemaTabs[param.name] === 'example' ? 'active' : ''}" data-tab = 'example'>EXAMPLE </button> <button part="tab-btn" class="tab-btn ${this.activeParameterSchemaTabs[param.name] !== 'example' ? 'active' : ''}" data-tab = 'schema'>SCHEMA</button> </div> ${html`<div part="tab-content" class="tab-content col" data-tab = 'example' style="display:${this.activeParameterSchemaTabs[param.name] === 'example' ? 'block' : 'none'}; padding-left:5px; width:100%"> <textarea id = "textarea-request-param-${param.name}" class = "textarea request-param" part = "textarea textarea-param" data-ptype = "${paramType}-object" data-pname = "${param.name}" data-example = "${example.exampleVal}" data-param-serialize-style = "${paramStyle}" data-param-serialize-explode = "${paramExplode}" data-param-allow-reserved = "${paramAllowReserved}" data-x-fill-example = "${param['x-fill-example'] || 'yes'}" spellcheck = "false" .textContent="${param['x-fill-example'] === 'no' ? '' : live(this.fillRequestFieldsWithExample === 'true' ? (typeof example.exampleVal === 'object' ? JSON.stringify(example.exampleVal, null, 2) : example.exampleVal) : '')}" style = "resize:vertical; width:100%; height: ${'read focused'.includes(this.renderStyle) ? '180px' : '120px'};" @input=${(e) => { const requestPanelEl = this.getRequestPanel(e); this.liveCURLSyntaxUpdate(requestPanelEl); }} ></textarea> </div>` } ${html`<div part="tab-content" class="tab-content col" data-tab = 'schema' style="display:${this.activeParameterSchemaTabs[param.name] !== 'example' ? 'block' : 'none'}; padding-left:5px; width:100%;"> <schema-tree class = 'json' style = 'display: block' .data = '${schemaAsObj}' schema-expand-level = "${this.schemaExpandLevel}" schema-description-expanded = "${this.schemaDescriptionExpanded}" allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}" schema-hide-read-only = "${this.schemaHideReadOnly.includes(this.method)}" schema-hide-write-only = "${this.schemaHideWriteOnly.includes(this.method)}" exportparts = "wrap-request-btn:wrap-request-btn, btn:btn, btn-fill:btn-fill, btn-outline:btn-outline, btn-try:btn-try, btn-clear:btn-clear, btn-clear-resp:btn-clear-resp, file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param, anchor:anchor, anchor-param-example:anchor-param-example" > </schema-tree> </div>` } </div>` : html` <input type="${paramSchema.format === 'password' ? 'password' : 'text'}" spellcheck="false" style="width:100%" id="input-request-param-${param.name}" class="request-param" part="textbox textbox-param" data-ptype="${paramType}" data-pname="${param.name}" data-example="${Array.isArray(example.exampleVal) ? example.exampleVal.join('~|~') : example.exampleVal}" data-param-allow-reserved = "${paramAllowReserved}" data-x-fill-example = "${param['x-fill-example'] || 'yes'}" data-array="false" .value="${param['x-fill-example'] === 'no' ? '' : live(this.fillRequestFieldsWithExample === 'true' ? example.exampleVal : '')}" @input=${(e) => { const requestPanelEl = this.getRequestPanel(e); this.liveCURLSyntaxUpdate(requestPanelEl); }} />` } </td>` : '' } ${paramSchema.default || paramSchema.constrain || paramSchema.allowedValues || paramSchema.pattern ? html` <td colspan="${(this.allowTry === 'true') ? '1' : '2'}"> <div class="param-constraint"> ${paramSchema.default ? html`<span style="font-weight:bold">Default: </span>${paramSchema.default}<br/>` : ''} ${paramSchema.pattern ? html`<span style="font-weight:bold">Pattern: </span>${paramSchema.pattern}<br/>` : ''} ${paramSchema.constrain ? html`${paramSchema.constrain}<br/>` : ''} ${paramSchema.allowedValues && paramSchema.allowedValues.split('┃').map((v, i) => html` ${i > 0 ? '┃' : html`<span style="font-weight:bold">Allowed: </span>`} ${html` <a part="anchor anchor-param-constraint" class = "${this.allowTry === 'true' ? '' : 'inactive-link'}" data-type="${paramSchema.type === 'array' ? paramSchema.type : 'string'}" data-enum="${v.trim()}" @click="${(e) => { const inputEl = e.target.closest('table').querySelector(`[data-pname="${param.name}"]`); if (inputEl) { if (e.target.dataset.type === 'array') { inputEl.value = [e.target.dataset.enum]; } else { inputEl.value = e.target.dataset.enum; } } }}" >${v}</a>` }`)} </div> </td>` : html`<td></td>` } </tr> <tr> ${this.allowTry === 'true' ? html`<td style="border:none"> </td>` : ''} <td colspan="2" style="border:none"> <span class="m-markdown-small">${unsafeHTML(marked(param.description || ''))}</span> ${this.exampleListTemplate.call(this, param.name, paramSchema.type, example.exampleList)} </td> </tr> `); } return html` <div class="table-title top-gap">${title}</div> <div style="display:block; overflow-x:auto; max-width:100%;"> <table role="presentation" class="m-table" style="width:100%; word-break:break-word;"> ${tableRows} </table> </div>`; } // This method is called before navigation change in focused mode async beforeNavigationFocusedMode() { // this.saveExampleState(); } // This method is called after navigation change in focused mode async afterNavigationFocusedMode() { this.selectedRequestBodyType = ''; this.selectedRequestBodyExample = ''; this.updateExamplesFromDataAttr(); 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); } requestBodyTemplate() { if (!this.request_body) { return ''; } if (Object.keys(this.request_body).length === 0) { return ''; } // Variable to store partial HTMLs let reqBodyTypeSelectorHtml = ''; let reqBodyFileInputHtml = ''; let reqBodyFormHtml = ''; let reqBodySchemaHtml = ''; let reqBodyExampleHtml = ''; const requestBodyTypes = []; const { content } = this.request_body; for (const mimeType in content) { requestBodyTypes.push({ mimeType, schema: content[mimeType].schema, example: content[mimeType].example, examples: content[mimeType].examples, }); if (!this.selectedRequestBodyType) { this.selectedRequestBodyType = mimeType; } } // MIME Type selector reqBodyTypeSelectorHtml = requestBodyTypes.length === 1 ? '' : html` <select style="min-width:100px; max-width:100%; margin-bottom:-1px;" @change = '${(e) => this.onMimeTypeChange(e)}'> ${requestBodyTypes.map((reqBody) => html` <option value = '${reqBody.mimeType}' ?selected = '${reqBody.mimeType === this.selectedRequestBodyType}'> ${reqBody.mimeType} </option> `) } </select> `; // For Loop - Main requestBodyTypes.forEach((reqBody) => { let schemaAsObj; let reqBodyExamples = []; if (this.selectedRequestBodyType.includes('json') || this.selectedRequestBodyType.includes('xml') || this.selectedRequestBodyType.includes('text') || this.selectedRequestBodyType.includes('jose')) { // Generate Example if (reqBody.mimeType === this.selectedRequestBodyType) { reqBodyExamples = generateExample( reqBody.schema, reqBody.mimeType, standardizeExample(reqBody.examples), standardizeExample(reqBody.example), this.callback === 'true' || this.webhook === 'true' ? true : false, // eslint-disable-line no-unneeded-ternary this.callback === 'true' || this.webhook === 'true' ? false : true, // eslint-disable-line no-unneeded-ternary 'text', false, ); if (!this.selectedRequestBodyExample) { this.selectedRequestBodyExample = (reqBodyExamples.length > 0 ? reqBodyExamples[0].exampleId : ''); } reqBodyExampleHtml = html` ${reqBodyExampleHtml} <div class = 'example-panel border-top pad-top-8'> ${reqBodyExamples.length === 1 ? '' : html` <select style="min-width:100px; max-width:100%; margin-bottom:-1px;" @change='${(e) => this.onSelectExample(e)}'> ${reqBodyExamples.map((v) => html`<option value="${v.exampleId}" ?selected=${v.exampleId === this.selectedRequestBodyExample} > ${v.exampleSummary.length > 80 ? v.exampleId : v.exampleSummary ? v.exampleSummary : v.exampleId} </option>`)} </select> ` } ${reqBodyExamples .filter((v) => v.exampleId === this.selectedRequestBodyExample) .map((v) => html` <div class="example ${v.exampleId === this.selectedRequestBodyExample ? 'example-selected' : ''}" data-example = '${v.exampleId}'> ${v.exampleSummary && v.exampleSummary.length > 80 ? html`<div style="padding: 4px 0"> ${v.exampleSummary} </div>` : ''} ${v.exampleDescription ? html`<div class="m-markdown-small" style="padding: 4px 0"> ${unsafeHTML(marked(v.exampleDescription || ''))} </div>` : ''} <!-- This pre(hidden) is to store the original example value, this will remain unchanged when users switches from one example to another, its is used to populate the editable textarea --> <pre class = "textarea is-hidden request-body-param ${reqBody.mimeType.substring(reqBody.mimeType.indexOf('/') + 1)}" spellcheck = "false" data-ptype = "${reqBody.mimeType}" style="width:100%; resize:vertical; display:none" >${(v.exampleFormat === 'text' ? v.exampleValue : JSON.stringify(v.exampleValue, null, 2))}</pre> <!-- this textarea is for user to edit the example --> <textarea class = "textarea request-body-param-user-input" part = "textarea textarea-param" spellcheck = "false" data-ptype = "${reqBody.mimeType}" data-example = "${v.exampleFormat === 'text' ? v.exampleValue : JSON.stringify(v.exampleValue, null, 2)}" data-example-format = "${v.exampleFormat}" style="width:100%; resize:vertical;" .textContent = "${this.fillRequestFieldsWithExample === 'true' ? (v.exampleFormat === 'text' ? v.exampleValue : JSON.stringify(v.exampleValue, null, 2)) : ''}" @input=${(e) => { const requestPanelEl = this.getRequestPanel(e); this.liveCURLSyntaxUpdate(requestPanelEl); }} @keydown=${(e) => { if ((e.keyCode === 10 || e.keyCode === 13) && e.ctrlKey) { return this.onTryClick(e); } }} ></textarea> </div> `)} </div> `; } } else if (this.selectedRequestBodyType.includes('form-urlencoded') || this.selectedRequestBodyType.includes('form-data')) { if (reqBody.mimeType === this.selectedRequestBodyType) { const ex = generateExample( reqBody.schema, reqBody.mimeType, reqBody.examples, reqBody.example, this.callback === 'true' || this.webhook === 'true' ? true : false, // eslint-disable-line no-unneeded-ternary this.callback === 'true' || this.webhook === 'true' ? false : true, // eslint-disable-line no-unneeded-ternary 'text', false, ); if (reqBody.schema) { reqBodyFormHtml = this.formDataTemplate(reqBody.schema, reqBody.mimeType, (ex[0] ? ex[0].exampleValue : '')); } } } else if ((/^audio\/|^image\/|^video\/|^font\/|tar$|zip$|7z$|rtf$|msword$|excel$|\/pdf$|\/octet-stream$/.test(this.selectedRequestBodyType))) { if (reqBody.mimeType === this.selectedRequestBodyType) { reqBodyFileInputHtml = html` <div class = "small-font-size bold-text row"> <input id="input-request-body-param-file" type="file" part="file-input" style="max-width:100%" class="request-body-param-file" data-ptype="${reqBody.mimeType}" spellcheck="false" /> </div> `; } } // Generate Schema if (reqBody.mimeType.includes('json') || reqBody.mimeType.includes('xml') || reqBody.mimeType.includes('text') || this.selectedRequestBodyType.includes('jose')) { schemaAsObj = schemaInObjectNotation(reqBody.schema, {}); if (this.schemaStyle === 'table') { reqBodySchemaHtml = html` ${reqBodySchemaHtml} <schema-table class = '${reqBody.mimeType.substring(reqBody.mimeType.indexOf('/') + 1)}' style = 'display: ${this.selectedRequestBodyType === reqBody.mimeType ? 'block' : 'none'};' .data = '${schemaAsObj}' schema-expand-level = "${this.schemaExpandLevel}" schema-description-expanded = "${this.schemaDescriptionExpanded}" allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}" schema-hide-read-only = "${this.schemaHideReadOnly}" schema-hide-write-only = "${this.schemaHideWriteOnly}" exportparts = "schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle" > </schema-table> `; } else if (this.schemaStyle === 'tree') { reqBodySchemaHtml = html` ${reqBodySchemaHtml} <schema-tree class = "${reqBody.mimeType.substring(reqBody.mimeType.indexOf('/') + 1)}" style = "display: ${this.selectedRequestBodyType === reqBody.mimeType ? 'block' : 'none'};" .data = "${schemaAsObj}" schema-expand-level = "${this.schemaExpandLevel}" schema-description-expanded = "${this.schemaDescriptionExpanded}" allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}" schema-hide-read-only = "${this.schemaHideReadOnly}" schema-hide-write-only = "${this.schemaHideWriteOnly}" exportparts = "schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle" > </schema-tree> `; } } }); return html` <div class='request-body-container' data-selected-request-body-type="${this.selectedRequestBodyType}"> <div class="table-title top-gap row"> REQUEST BODY ${this.request_body.required ? html`<span class="mono-font" style='color:var(--red)'>*</span>` : ''} <span style = "font-weight:normal; margin-left:5px"> ${this.selectedRequestBodyType}</span> <span style="flex:1"></span> ${reqBodyTypeSelectorHtml} </div> ${this.request_body.description ? html`<div class="m-markdown" style="margin-bottom:12px">${unsafeHTML(marked(this.request_body.description))}</div>` : ''} ${(this.selectedRequestBodyType.includes('json') || this.selectedRequestBodyType.includes('xml') || this.selectedRequestBodyType.includes('text') || this.selectedRequestBodyType.includes('jose')) ? html` <div part="tab-panel" class="tab-panel col" style="border-width:0 0 1px 0;"> <div part="tab-btn-row" class="tab-buttons row" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}"> <button part="tab-btn" class="tab-btn ${this.activeSchemaTab === 'example' ? 'active' : ''}" data-tab = 'example'>EXAMPLE</button> <button part="tab-btn" class="tab-btn ${this.activeSchemaTab !== 'example' ? 'active' : ''}" data-tab = 'schema'>SCHEMA</button> </div> ${html`<div part="tab-content" class="tab-content col" style="display:${this.activeSchemaTab === 'example' ? 'block' : 'none'};"> ${reqBodyExampleHtml}</div>`} ${html`<div part="tab-content" class="tab-content col" style="display:${this.activeSchemaTab === 'example' ? 'none' : 'block'};"> ${reqBodySchemaHtml}</div>`} </div>` : html` ${reqBodyFileInputHtml} ${reqBodyFormHtml}` } </div> `; } formDataParamAsObjectTemplate(fieldName, fieldSchema, mimeType) { // This template is used when form-data param should be send as a object (application/json, application/xml) const formdataPartSchema = schemaInObjectNotation(fieldSchema, {}); const formdataPartExample = generateExample( fieldSchema, 'json', standardizeExample(fieldSchema.examples), standardizeExample(fieldSchema.example), this.callback === 'true' || this.webhook === 'true' ? true : false, // eslint-disable-line no-unneeded-ternary this.callback === 'true' || this.webhook === 'true' ? false : true, // eslint-disable-line no-unneeded-ternary 'text', false, ); return html` <div part="tab-panel" class="tab-panel row" style="min-height:220px; border-left: 6px solid var(--light-border-color); align-items: stretch;"> <div style="width:24px; background-color:var(--light-border-color)"> <div class="row" style="flex-direction:row-reverse; width:160px; height:24px; transform:rotate(270deg) translateX(-160px); transform-origin:top left; display:block;" @click="${(e) => { if (e.target.classList.contains('v-tab-btn')) { const { tab } = e.target.dataset; if (tab) { const tabPanelEl = e.target.closest('.tab-panel'); const selectedTabBtnEl = tabPanelEl.querySelector(`.v-tab-btn[data-tab="${tab}"]`); const otherTabBtnEl = [...tabPanelEl.querySelectorAll(`.v-tab-btn:not([data-tab="${tab}"])`)]; const selectedTabContentEl = tabPanelEl.querySelector(`.tab-content[data-tab="${tab}"]`); const otherTabContentEl = [...tabPanelEl.querySelectorAll(`.tab-content:not([data-tab="${tab}"])`)]; selectedTabBtnEl.classList.add('active'); selectedTabContentEl.style.display = 'block'; otherTabBtnEl.forEach((el) => { el.classList.remove('active'); }); otherTabContentEl.forEach((el) => { el.style.display = 'none'; }); } } if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}"> <button class="v-tab-btn ${this.activeSchemaTab === 'example' ? 'active' : ''}" data-tab = 'example'>EXAMPLE</button> <button class="v-tab-btn ${this.activeSchemaTab !== 'example' ? 'active' : ''}" data-tab = 'schema'>SCHEMA</button> </div> </div> ${html` <div class="tab-content col" data-tab = 'example' style="display:${this.activeSchemaTab === 'example' ? 'block' : 'none'}; padding-left:5px; width:100%"> <textarea class = "textarea" part = "textarea textarea-param" style = "width:100%; border:none; resize:vertical;" data-array = "false" data-ptype = "${mimeType.includes('form-urlencode') ? 'form-urlencode' : 'form-data'}" data-pname = "${fieldName}" data-example = "${formdataPartExample[0]?.exampleValue || ''}" .textContent = "${this.fillRequestFieldsWithExample === 'true' ? formdataPartExample[0].exampleValue : ''}" spellcheck = "false" ></textarea> </div>` } ${html` <div class="tab-content col" data-tab = 'schema' style="display:${this.activeSchemaTab !== 'example' ? 'block' : 'none'}; padding-left:5px; width:100%;"> <schema-tree .data = "${formdataPartSchema}" schema-expand-level = "${this.schemaExpandLevel}" schema-description-expanded = "${this.schemaDescriptionExpanded}" allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}", > </schema-tree> </div>` } </div> `; } formDataTemplate(schema, mimeType, exampleValue = '') { const formDataTableRows = []; if (schema.properties) { for (const fieldName in schema.properties) { const fieldSchema = schema.properties[fieldName]; if (fieldSchema.readOnly) { continue; } const fieldExamples = fieldSchema.examples || fieldSchema.example || ''; const fieldType = fieldSchema.type; const paramSchema = getTypeInfo(fieldSchema); const labelColWidth = 'read focused'.includes(this.renderStyle) ? '200px' : '160px'; const example = normalizeExamples((paramSchema.examples || paramSchema.example), paramSchema.type); formDataTableRows.push(html` <tr title="${fieldSchema.deprecated ? 'Deprecated' : ''}"> <td style="width:${labelColWidth}; min-width:100px;"> <div class="param-name ${fieldSchema.deprecated ? 'deprecated' : ''}"> ${fieldName}${(schema.required?.includes(fieldName) || fieldSchema.required) ? html`<span style='color:var(--red);'>*</span>` : ''} </div> <div class="param-type">${paramSchema.type}</div> </td> <td style="${fieldType === 'object' ? 'width:100%; padding:0;' : this.allowTry === 'true' ? '' : 'display:none;'} min-width:100px;" colspan="${fieldType === 'object' ? 2 : 1}"> ${fieldType === 'array' ? fieldSchema.items?.format === 'binary' ? html` <div class="file-input-container col" style='align-items:flex-end;' @click="${(e) => this.onAddRemoveFileInput(e, fieldName, mimeType)}"> <div class='input-set row'> <input type = "file" part = "file-input" style = "width:100%" data-pname = "${fieldName}" data-ptype = "${mimeType.includes('form-urlencode') ? 'form-urlencode' : 'form-data'}" data-array = "false" data-file-array = "true" /> <button class="file-input-remove-btn"> &#x2715; </button> </div> <button class="m-btn primary file-input-add-btn" part="btn btn-fill" style="margin:2px 25px 0 0; padding:2px 6px;">ADD</button> </div> ` : html` <tag-input style = "width:100%" data-ptype = "${mimeType.includes('form-urlencode') ? 'form-urlencode' : 'form-data'}" data-pname = "${fieldName}" data-example = "${Array.isArray(fieldExamples) ? fieldExamples.join('~|~') : fieldExamples}" data-array = "true" placeholder = "add-multiple &#x21a9;" .value = "${Array.isArray(fieldExamples) ? Array.isArray(fieldExamples[0]) ? fieldExamples[0] : fieldExamples : []}" > </tag-input> ` : html` ${fieldType === 'object' ? this.formDataParamAsObjectTemplate.call(this, fieldName, fieldSchema, mimeType) : html` ${this.allowTry === 'true' ? html`<input .value = "${this.fillRequestFieldsWithExample === 'true' ? example.exampleVal : ''}" spellcheck = "false" type = "${fieldSchema.format === 'binary' ? 'file' : fieldSchema.format === 'password' ? 'password' : 'text'}" part = "textbox textbox-param" style = "width:100%" data-ptype = "${mimeType.includes('form-urlencode') ? 'form-urlencode' : 'form-data'}" data-pname = "${fieldName}" data-example = "${Array.isArray(fieldExamples) ? fieldExamples[0] : fieldExamples}" data-array = "false" />` : '' } ` }` } </td> ${fieldType === 'object' ? '' : html` <td> ${paramSchema.default || paramSchema.constrain || paramSchema.allowedValues || paramSchema.pattern ? html` <div class="param-constraint"> ${paramSchema.default ? html`<span style="font-weight:bold">Default: </span>${paramSchema.default}<br/>` : ''} ${paramSchema.pattern ? html`<span style="font-weight:bold">Pattern: </span>${paramSchema.pattern}<br/>` : ''} ${paramSchema.constrain ? html`${paramSchema.constrain}<br/>` : ''} ${paramSchema.allowedValues && paramSchema.allowedValues.split('┃').map((v, i) => html` ${i > 0 ? '┃' : html`<span style="font-weight:bold">Allowed: </span>`} ${html` <a part="anchor anchor-param-constraint" class = "${this.allowTry === 'true' ? '' : 'inactive-link'}" data-type="${paramSchema.type === 'array' ? paramSchema.type : 'string'}" data-enum="${v.trim()}" @click="${(e) => { const inputEl = e.target.closest('table').querySelector(`[data-pname="${fieldName}"]`); if (inputEl) { if (e.target.dataset.type === 'array') { inputEl.value = [e.target.dataset.enum]; } else { inputEl.value = e.target.dataset.enum; } } }}" > ${v} </a>` }`) } </div>` : '' } </td>` } </tr> ${fieldType === 'object' ? '' : html` <tr> <td style="border:none"> </td> <td colspan="2" style="border:none; margin-top:0; padding:0 5px 8px 5px;"> <span class="m-markdown-small">${unsafeHTML(marked(fieldSchema.description || ''))}</span> ${this.exampleListTemplate.call(this, fieldName, paramSchema.type, example.exampleList)} </td> </tr> ` }`); } return html` <table role="presentation" style="width:100%;" class="m-table"> ${formDataTableRows} </table> `; } return html` <textarea class = "textarea dynamic-form-param ${mimeType}" part = "textarea textarea-param" spellcheck = "false" data-pname="dynamic-form" data-ptype="${mimeType}" .textContent = "${exampleValue}" style="width:100%" ></textarea> ${schema.description ? html`<span class="m-markdown-small">${unsafeHTML(marked(schema.description))}</span>` : ''} `; } curlSyntaxTemplate(display = 'flex') { return html` <div class="col m-markdown" style="flex:1; display:${display}; position:relative; max-width: 100%;"> <button class="toolbar-btn" style = "position:absolute; top:12px; right:8px" @click='${(e) => { copyToClipboard(this.curlSyntax.trim().replace(/\\$/, ''), e); }}' part="btn btn-fill"> Copy </button> <pre style="white-space:pre"><code>${unsafeHTML(Prism.highlight(this.curlSyntax.trim().replace(/\\$/, ''), Prism.languages.shell, 'shell'))}</code></pre> </div> `; } apiResponseTabTemplate() { let responseFormat = ''; let responseContent = ''; if (!this.responseIsBlob) { if (this.responseHeaders.includes('application/x-ndj