UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

378 lines (366 loc) • 17.4 kB
/** * Renders individual API operation rows and expanded panels (summary, description, parameters, request, response, try-it console). */ import { html, nothing } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { sanitizeHTML } from "../utils/sanitize.js"; import { marked } from "marked"; import "~/components/api-request"; import "~/components/api-response"; import codeSamplesTemplate from "~/templates/code-samples-template"; import callbackTemplate from "~/templates/callback-template"; import { pathSecurityTemplate } from "~/templates/security-scheme-template"; import { getMatchedPaths, rapidocApiKey } from "~/utils/common-utils"; function toggleExpand(path) { if (path.expanded) { path.expanded = false; // collapse if (this.updateRoute === "true") { this.replaceHistoryState(""); } } else { path.expanded = true; // Expand if (this.updateRoute === "true") { const newHash = `#${path.elementId}`; if (window.location.hash !== newHash) { this.replaceHistoryState(path.elementId); } } } this.requestUpdate(); } // Sets `expanded` on every tag group and tag, so a tag's own toggle starts from what is shown export function expandCollapseAll(action = "expand-all") { const expanded = action === "expand-all"; (this.resolvedSpec.tagGroups || []).forEach((group) => { group.expanded = expanded; (group.tags || []).forEach((tag) => { tag.expanded = expanded; }); }); (this.resolvedSpec.tags || []).forEach((tag) => { tag.expanded = expanded; }); this.requestUpdate(); } function endpointHeadTemplate(path, pathsExpanded = false) { return html` <summary @click="${(e) => { toggleExpand.call(this, path, e); }}" part="section-endpoint-head-${path.expanded ? "expanded" : "collapsed"}" class="endpoint-head ${path.method} ${path.deprecated ? "deprecated" : ""} ${ pathsExpanded || path.expanded ? "expanded" : "collapsed" }" > <div part="section-endpoint-head-method" class="method ${path.method} ${path.deprecated ? "deprecated" : ""}">${path.method}</div> <div part="section-endpoint-head-path" class="path ${path.deprecated ? "deprecated" : ""}"> ${path.path} ${ path.isWebhook ? html`<span style="font-family: var(--font-sans); font-size: var(--font-size-small); color:var(--primary); margin-left: 16px"> Webhook</span >` : "" } </div> ${ path.deprecated ? html` <span style="font-size:var(--font-size-small); text-transform:uppercase; font-weight:bold; color:var(--destructive); margin:2px 0 0 5px;" > deprecated </span>` : "" } ${ this.showSummaryWhenCollapsed ? html`<div part="section-endpoint-head-description" class="descr">${path.summary || path.shortSummary}</div>` : "" } </summary> `; } function endpointBodyTemplate(path) { const acceptContentTypes = new Set(); for (const respStatus in path.responses) { for (const acceptContentType in path.responses[respStatus]?.content) { acceptContentTypes.add(acceptContentType.trim()); } } const accept = [...acceptContentTypes].join(", "); // Filter API Keys that are non-empty and are applicable to the the path const nonEmptyApiKeys = this.resolvedSpec.securitySchemes.filter( (v) => v.finalKeyValue && path.security?.some((ps) => v.securitySchemeId in ps), ) || []; // If a RapiDoc API Key is specified on the element and its value is not hyphen(-) then include it for all paths const rapiDocApiKey = this.resolvedSpec.securitySchemes.find( (v) => v.securitySchemeId === rapidocApiKey && v.value !== "-", ); if (rapiDocApiKey) { nonEmptyApiKeys.push(rapiDocApiKey); } const codeSampleTabPanel = path.xCodeSamples ? codeSamplesTemplate(path.xCodeSamples) : ""; return html` <div part="section-endpoint-body-${path.expanded ? "expanded" : "collapsed"}" class="endpoint-body ${path.method} ${path.deprecated ? "deprecated" : ""}" > <div class="summary"> ${ path.summary ? html`<h3 class="rapidoc-heading title" part="section-endpoint-body-title heading heading-3">${path.summary}</h3>` : path.shortSummary !== path.description ? html`<h3 class="rapidoc-heading title" part="section-endpoint-body-title heading heading-3">${path.shortSummary}</h3>` : html`<h3 class="visually-hidden">${path.method.toUpperCase()} ${path.path}</h3>` } ${ path.xBadges && path.xBadges?.length > 0 ? html` <div style="display:flex; flex-wrap:wrap;font-size: var(--font-size-small);"> ${path.xBadges.map((v) => v.color === "none" ? "" : html`<span part="endpoint-badge" style="margin:1px; margin-right:5px; padding:1px 8px; font-weight:bold; border-radius:var(--radius); background: color-mix(in srgb, var(--${v.color}) 20%, transparent); color:var(--${v.color}); border:1px solid var(--${v.color})" >${v.label}</span >`, )} </div> ` : "" } ${pathSecurityTemplate.call(this, path.security, path.elementId)} ${ path.description ? html`<div part="section-endpoint-body-description" class="m-markdown"> ${unsafeHTML(sanitizeHTML(marked(path.description)))} </div>` : "" } ${ path.externalDocs?.url || path.externalDocs?.description ? html`<div class="external-docs-box"> <div class="m-markdown">${unsafeHTML(sanitizeHTML(marked(path.externalDocs?.description || "")))}</div> ${ path.externalDocs?.url ? html`<a class="external-docs-link" href="${path.externalDocs?.url}" target="_blank"> ${path.externalDocs?.url} <span class="external-docs-arrow">⇲</span> </a>` : "" } </div>` : "" } <slot name="${path.elementId}"></slot> ${codeSampleTabPanel} </div> <div class="req-resp-container ${this.layout}-layout" part="section-req-resp-container"> <div class="request-panel view-mode-request ${this.layout}-layout" part="section-request-panel"> <api-request .config="${this.config}" class="${this.renderStyle}-mode ${this.layout}-layout" style="width:100%;" webhook="${path.isWebhook}" method="${path.method}" path="${path.path}" .security="${path.security}" .parameters="${path.parameters}" .request_body="${path.requestBody}" .api_keys="${nonEmptyApiKeys}" .servers="${path.servers}" server-url="${path.servers?.length > 0 ? path.servers[0].url : this.selectedServer?.computedUrl}" active-schema-tab="${this.defaultSchemaTab}" fill-request-fields-with-example="${this.fillRequestFieldsWithExample}" allow-try="${this.allowTry}" show-curl-before-try="${this.showCurlBeforeTry}" accept="${accept}" render-style="${this.renderStyle}" schema-style="${this.schemaStyle}" schema-expand-level="${this.schemaExpandLevel}" schema-description-expanded="${this.schemaDescriptionExpanded}" allow-schema-description-expand-toggle="${this.allowSchemaDescriptionExpandToggle}" schema-hide-read-only="${this.schemaHideReadOnly === "never" ? "false" : path.isWebhook ? "false" : "true"}" schema-hide-write-only="${this.schemaHideWriteOnly === "never" ? "false" : path.isWebhook ? "true" : "false"}" fetch-credentials="${this.fetchCredentials}" exportparts="section-request-title:section-request-title, heading:heading, heading-4:heading-4, heading-5:heading-5, heading-6:heading-6, 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-fill-example:btn-fill-example, btn-clear-resp:btn-clear-resp, tab-panel:tab-panel, tab-btn:tab-btn, tab-btn-row:tab-btn-row, tab-content:tab-content, file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param, anchor:anchor, anchor-param-example:anchor-param-example, anchor-param-constraint:anchor-param-constraint, param-list:param-list, param:param, param-label:param-label, param-name:param-name, param-type:param-type, param-input:param-input, param-constraint:param-constraint, param-constraint-label:param-constraint-label, param-details:param-details, param-example-list:param-example-list, value-list:value-list, value-list-label:value-list-label, value-list-items:value-list-items, schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle" > </api-request> ${path.callbacks ? callbackTemplate.call(this, path.callbacks) : ""} </div> <api-response .config="${this.config}" class="response-panel ${this.renderStyle}-mode ${this.layout}-layout" part="section-response-panel" style="width:100%;" webhook="${path.isWebhook}" .responses="${path.responses}" active-schema-tab="${this.defaultSchemaTab}" render-style="${this.renderStyle}" schema-style="${this.schemaStyle}" schema-expand-level="${this.schemaExpandLevel}" schema-description-expanded="${this.schemaDescriptionExpanded}" allow-schema-description-expand-toggle="${this.allowSchemaDescriptionExpandToggle}" schema-hide-read-only="${this.schemaHideReadOnly === "never" ? "false" : path.isWebhook ? "true" : "false"}" schema-hide-write-only="${this.schemaHideWriteOnly === "never" ? "false" : path.isWebhook ? "false" : "true"}" selected-status="${Object.keys(path.responses || {})[0] || ""}" exportparts="section-response-title:section-response-title, heading:heading, heading-4:heading-4, heading-5:heading-5, heading-6:heading-6, btn:btn, btn-fill:btn-fill, btn-outline:btn-outline, btn-try:btn-try, file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param, anchor:anchor, anchor-param-example:anchor-param-example, anchor-param-constraint:anchor-param-constraint, param-list:param-list, param:param, param-label:param-label, param-name:param-name, param-type:param-type, param-input:param-input, param-constraint:param-constraint, param-constraint-label:param-constraint-label, param-details:param-details, param-example-list:param-example-list, value-list:value-list, value-list-label:value-list-label, value-list-items:value-list-items, btn-clear-resp:btn-clear-resp, tab-panel:tab-panel, tab-btn:tab-btn, tab-btn-row:tab-btn-row, tab-content:tab-content, schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle" > </api-response> </div> </div>`; } export default function endpointTemplate(isMini = false, pathsExpanded = false) { if (!this.resolvedSpec) { return ""; } const renderTagSection = (tag) => html` ${ isMini ? html` <div class="section-tag-body"> ${ tag.description ? html`<div class="regular-font regular-font-size m-markdown" style="padding-bottom:12px"> ${unsafeHTML(sanitizeHTML(marked(tag.description || "")))} </div>` : "" } ${tag.paths .filter((path) => { if (this.searchVal) { return getMatchedPaths(this.searchVal, path, tag.name); } return true; }) .map( (path, idx) => html` <section id="${path.elementId}" class="m-endpoint regular-font ${path.method} ${pathsExpanded || path.expanded ? "expanded" : "collapsed"}" data-first-operation-in-tag="${idx === 0 ? "true" : nothing}" > ${endpointHeadTemplate.call(this, path, pathsExpanded)} ${pathsExpanded || path.expanded ? endpointBodyTemplate.call(this, path) : ""} </section>`, )} </div> ` : html` <div class="regular-font section-gap section-tag ${tag.expanded ? "expanded" : "collapsed"}"> <div class="section-tag-header" @click="${() => { tag.expanded = !tag.expanded; this.requestUpdate(); }}" > <h2 id="${tag.elementId}" class="rapidoc-heading sub-title tag" part="heading heading-2" style="color:var(--primary)"> ${tag.displayName || tag.name} </h2> </div> <div class="section-tag-body"> <slot name="${tag.elementId}"></slot> ${ tag.description ? html`<div class="regular-font regular-font-size m-markdown" style="padding-bottom:12px"> ${unsafeHTML(sanitizeHTML(marked(tag.description || "")))} </div>` : "" } ${tag.paths .filter((v) => { if (this.searchVal) { return getMatchedPaths(this.searchVal, v, tag.name); } return true; }) .map( (path, idx) => html` <section part="section-endpoint" id="${path.elementId}" class="m-endpoint regular-font ${path.method} ${pathsExpanded || path.expanded ? "expanded" : "collapsed"}" data-first-operation-in-tag="${idx === 0 ? "true" : nothing}" > ${endpointHeadTemplate.call(this, path, pathsExpanded)} ${pathsExpanded || path.expanded ? endpointBodyTemplate.call(this, path) : ""} </section>`, )} </div> </div> ` } `; const hasTagGroups = this.resolvedSpec.tagGroups && this.resolvedSpec.tagGroups.length > 0; const assignedTagNames = new Set(); if (hasTagGroups) { this.resolvedSpec.tagGroups.forEach((group) => { (group.tags || []).forEach((tag) => assignedTagNames.add(tag.name.toLowerCase())); }); } const remainingTags = hasTagGroups ? (this.resolvedSpec.tags || []).filter((tag) => !assignedTagNames.has(tag.name.toLowerCase())) : []; return html`${ isMini ? "" : html`<div class="section-expand-collapse-container"> <span @click="${() => expandCollapseAll.call(this, "expand-all")}" style="color:var(--primary); cursor:pointer;"> Expand all </span> &nbsp;|&nbsp; <span @click="${() => expandCollapseAll.call(this, "collapse-all")}" style="color:var(--primary); cursor:pointer;"> Collapse all </span> &nbsp; sections </div>` } ${ hasTagGroups ? html` ${this.resolvedSpec.tagGroups.map( (group) => html` <div class="regular-font section-gap section-tag-group ${group.expanded ? "expanded" : "collapsed"}" id="${group.elementId}"> <div class="section-tag-group-header" @click="${() => { group.expanded = !group.expanded; this.requestUpdate(); }}" > <h2 class="rapidoc-heading sub-title tag-group" part="heading heading-2" style="color:var(--foreground); font-size:var(--h4-font-size); font-weight:700; text-transform:uppercase; letter-spacing:0.05em;" > ${group.displayName || group.name} </h2> <div class="section-tag-group-icon"></div> </div> <div class="section-tag-group-body"> ${ group.description ? html`<div class="regular-font regular-font-size m-markdown" style="padding-bottom:12px"> ${unsafeHTML(sanitizeHTML(marked(group.description || "")))} </div>` : "" } ${group.tags.map((tag) => renderTagSection(tag))} </div> </div> `, )} ${remainingTags.map((tag) => renderTagSection(tag))} ` : this.resolvedSpec.tags.map((tag) => renderTagSection(tag)) }`; }