import { LitElement, html, css } from "lit"; import { customElement, property } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { sanitizeHTML } from "../utils/sanitize.js"; import { marked } from "marked"; import { prepareResponseDefinitions } from "../utils/response-utils.js"; import { exampleLabel } from "~/utils/schema-utils"; import FontStyles from "~/styles/font-styles"; import FlexStyles from "~/styles/flex-styles"; import ParamListStyles from "~/styles/param-list-styles"; import InputStyles from "~/styles/input-styles"; import TabStyles from "~/styles/tab-styles"; import BorderStyles from "~/styles/border-styles"; import CustomStyles from "~/styles/custom-styles"; import "~/components/json-tree"; import "~/components/schema-tree"; import "~/components/schema-table"; @customElement("api-response") export default class ApiResponse extends LitElement { @property({ type: Object }) config: any; @property({ type: String }) callback = ""; @property({ type: String }) webhook = ""; @property({ type: Object }) responses: any; @property({ type: Object }) parser: any; @property({ type: String, attribute: "schema-style" }) schemaStyle = ""; @property({ type: String, attribute: "render-style" }) renderStyle = ""; @property({ type: String, attribute: "selected-status" }) selectedStatus = ""; @property({ type: String, attribute: "selected-mime-type" }) selectedMimeType = ""; @property({ type: String, attribute: "active-schema-tab" }) activeSchemaTab = "schema"; @property({ type: Number, attribute: "schema-expand-level" }) schemaExpandLevel = 99999; @property({ type: String, attribute: "schema-description-expanded" }) schemaDescriptionExpanded = "false"; @property({ type: String, attribute: "allow-schema-description-expand-toggle" }) allowSchemaDescriptionExpandToggle = "false"; @property({ type: String, attribute: "schema-hide-read-only" }) schemaHideReadOnly = "false"; @property({ type: String, attribute: "schema-hide-write-only" }) schemaHideWriteOnly = "true"; defaultStatus = ""; defaultMimeType = ""; headersForEachRespStatus: any = {}; mimeResponsesForEachStatus: any = {}; constructor() { super(); this.selectedStatus = ""; this.defaultStatus = ""; this.defaultMimeType = ""; this.headersForEachRespStatus = {}; this.mimeResponsesForEachStatus = {}; this.activeSchemaTab = "schema"; } static get styles() { return [ FontStyles, FlexStyles, TabStyles, ParamListStyles, InputStyles, BorderStyles, css` .resp-headers-list { margin-bottom: 16px; } :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); } .resp-head { vertical-align: middle; padding: 16px 0 8px; &.divider { border-top: 1px solid var(--border); margin-top: 10px; } } .resp-status { font-weight: bold; font-size: calc(var(--font-size-small) + 1px); } .resp-descr { font-size: var(--font-size-description); color: var(--muted-foreground); text-align: left; } .top-gap { margin-top: 16px; } .example-panel { font-size: var(--font-size-small); margin: 0; } /* Response codes: a segmented control (one choice), the selected code tinted with the primary color */ .status-group { display: inline-flex; flex-wrap: wrap; margin-top: 8px; } .status-btn { margin: 0 0 0 -1px; border-color: var(--input); border-radius: 0; background: color-mix(in srgb, var(--foreground) 5%, transparent); color: var(--muted-foreground); &:first-child { margin-left: 0; border-start-start-radius: var(--radius); border-end-start-radius: var(--radius); } &:last-child { border-start-end-radius: var(--radius); border-end-end-radius: var(--radius); } &:hover { background: var(--muted); color: var(--foreground); } &[aria-pressed='true'] { position: relative; z-index: 1; border-color: var(--primary); background: color-mix(in srgb, var(--primary) 14%, transparent); color: var(--primary); } &:focus-visible { z-index: 2; } } `, CustomStyles, ]; } willUpdate(changedProperties) { super.willUpdate?.(changedProperties); // Sync configuration options passed down from parent 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 responses, readOnly fields are shown by default ('false') while writeOnly fields are hidden ('true'). // For webhooks, this behavior is inverted unless configured to 'never'. if (this.config.schemaHideReadOnly !== undefined) this.schemaHideReadOnly = this.config.schemaHideReadOnly === "never" ? "false" : this.webhook === "true" ? "true" : "false"; if (this.config.schemaHideWriteOnly !== undefined) this.schemaHideWriteOnly = this.config.schemaHideWriteOnly === "never" ? "false" : this.webhook === "true" ? "false" : "true"; if (this.config.defaultSchemaTab) this.activeSchemaTab = this.config.defaultSchemaTab; } // Parse response definitions, headers, examples, and AST schemas when responses change if ( changedProperties.has("responses") || changedProperties.has("callback") || changedProperties.has("webhook") ) { if (!this.responses || typeof this.responses !== "object") { return; } const defs = prepareResponseDefinitions(this.responses, { callback: this.callback, webhook: this.webhook, }); this.headersForEachRespStatus = defs.headersForEachRespStatus; this.mimeResponsesForEachStatus = defs.mimeResponsesForEachStatus; this.defaultStatus = defs.defaultStatus; this.defaultMimeType = defs.defaultMimeType; // Default to primary status code (e.g. '200') if none is currently selected or if the old status doesn't exist on this response if (!this.selectedStatus || !this.responses?.[this.selectedStatus]) { this.selectedStatus = defs.defaultStatus; } if (!this.selectedMimeType) { this.selectedMimeType = defs.defaultMimeType; } } } render() { return html`
${ this.callback === "true" ? html`
Callback response
` : html`

Response

` }
${this.responseTemplate()}
`; } // Called by rapi-doc after navigating in focused mode. // In focused mode, the single element instance is reused across operations. // We reset selections back to the default status code and MIME type of the active endpoint, // preventing user selections from a prior endpoint (e.g., status 400) from carrying over or hiding panels. resetSelection() { this.selectedStatus = this.defaultStatus || (this.responses ? Object.keys(this.responses).find((k) => k !== "$$ref") || "" : ""); if (this.selectedStatus && this.mimeResponsesForEachStatus?.[this.selectedStatus]) { this.selectedMimeType = Object.keys(this.mimeResponsesForEachStatus[this.selectedStatus])[0] || ""; } else { this.selectedMimeType = this.defaultMimeType || ""; } } responseTemplate() { if (!this.responses) { return ""; } // Fallback guard: Ensure selectedStatus and selectedMimeType always point to valid options // for this endpoint, even if resetSelection was called or responses have not re-rendered yet. if (!this.selectedStatus || !this.responses[this.selectedStatus]) { this.selectedStatus = this.defaultStatus || Object.keys(this.responses).find((k) => k !== "$$ref") || Object.keys(this.responses)[0] || ""; } if ( !this.selectedMimeType && this.selectedStatus && this.mimeResponsesForEachStatus?.[this.selectedStatus] ) { this.selectedMimeType = Object.keys(this.mimeResponsesForEachStatus[this.selectedStatus])[0] || this.defaultMimeType || ""; } return html` ${ Object.keys(this.responses).length > 1 ? html`
${Object.keys(this.responses).map( (respStatus) => html` ${ respStatus === "$$ref" // Ignore '$$ref' if present from legacy reference parsing ? "" : html`` }`, )}
` : html`${Object.keys(this.responses)[0]}` } ${Object.keys(this.responses).map( (status) => html`
${unsafeHTML(sanitizeHTML(marked(this.responses[status]?.description || "")))} ${ this.headersForEachRespStatus[status] && this.headersForEachRespStatus[status]?.length > 0 ? html`${this.responseHeaderListTemplate(this.headersForEachRespStatus[status])}` : "" }
${ !this.mimeResponsesForEachStatus[status] || Object.keys(this.mimeResponsesForEachStatus[status]).length === 0 ? "" : html`
${ Object.keys(this.mimeResponsesForEachStatus[status]).length === 1 ? html`${Object.keys(this.mimeResponsesForEachStatus[status])[0]}` : html`${this.mimeTypeDropdownTemplate(Object.keys(this.mimeResponsesForEachStatus[status]))}` }
${ this.activeSchemaTab === "example" ? html`
${this.mimeExampleTemplate(this.mimeResponsesForEachStatus[status][this.selectedMimeType])}
` : html`
${this.mimeSchemaTemplate(this.mimeResponsesForEachStatus[status][this.selectedMimeType])}
` }
` }
`, )} `; } responseHeaderListTemplate(respHeaders) { return html`
Response headers
    ${respHeaders.map( (v) => html`
  • ${v.name || ""}
    ${v.schema?.type ? html`
    ${v.schema.type}
    ` : ""}
    ${ v.schema?.example ? html`
    Example:
    ${v.schema.example}
    ` : "" }
    ${v.description ? html`
    ${unsafeHTML(sanitizeHTML(marked(v.description)))}
    ` : ""}
  • `, )}
`; } mimeTypeDropdownTemplate(mimeTypes) { return html``; } onSelectExample(e) { const exampleContainerEl = e.target.closest(".example-panel"); const exampleEls = [...exampleContainerEl.querySelectorAll(".example")]; exampleEls.forEach((v) => { v.style.display = v.dataset.example === e.target.value ? "block" : "none"; }); } mimeExampleTemplate(mimeRespDetails) { if (!mimeRespDetails || !mimeRespDetails.examples || mimeRespDetails.examples.length === 0) { return html`
 No example provided 
`; } return html` ${ mimeRespDetails.examples.length === 1 ? html` ${ mimeRespDetails.examples[0].exampleFormat === "json" ? html` ${ mimeRespDetails.examples[0].exampleSummary && mimeRespDetails.examples[0].exampleSummary.length > 80 ? html`
${mimeRespDetails.examples[0].exampleSummary}
` : "" } ${ mimeRespDetails.examples[0].exampleDescription ? html`
${unsafeHTML(sanitizeHTML(marked(mimeRespDetails.examples[0].exampleDescription || "")))}
` : "" } ` : html` ${ mimeRespDetails.examples[0].exampleSummary && mimeRespDetails.examples[0].exampleSummary.length > 80 ? html`
${mimeRespDetails.examples[0].exampleSummary}
` : "" } ${ mimeRespDetails.examples[0].exampleDescription ? html`
${unsafeHTML(sanitizeHTML(marked(mimeRespDetails.examples[0].exampleDescription || "")))}
` : "" }
${mimeRespDetails.examples[0].exampleValue}
` }` : html`
${mimeRespDetails.examples.map( (v) => html`
${ v.exampleDescription ? html`
${unsafeHTML(sanitizeHTML(marked(v.exampleDescription || "")))}
` : "" } ${ v.exampleFormat === "json" ? html`` : html`
${v.exampleValue}
` }
`, )}
` } `; } mimeSchemaTemplate(mimeRespDetails) { if (!mimeRespDetails) { return html`
 Schema not found
`; } return html` ${ this.schemaStyle === "table" ? html` ` : html` ` }`; } }