diff --git a/demo/index.tsx b/demo/index.tsx index f134eaa797..1e621a45e1 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -122,7 +122,7 @@ class DemoApp extends React.Component< ); diff --git a/docs/config.md b/docs/config.md index 28e24f7eda..b18c6c37fd 100644 --- a/docs/config.md +++ b/docs/config.md @@ -26,19 +26,7 @@ Sets the minimum amount of characters that need to be typed into the search dial _Default: 3_ -### expandDefaultServerVariables - -Enables or disables expanding default server variables. - -### expandResponses - -Controls which responses to expand by default. Specify one or more responses by providing their response codes as a comma-separated list without spaces, for example `expandResponses='200,201'`. Special value 'all' expands all responses by default. Be careful: this option can slow down documentation rendering time. - -### expandSingleSchemaField - -Automatically expands the single field in a schema. - -### hideDownloadButton +### hideDownloadButtons Hides the 'Download' button for saving the API definition source file. **This setting does not make the API definition private**; it just hides the button. @@ -78,7 +66,7 @@ Hides the request sample tab for requests with only one sample. Sets the path to the optional HTML file used to modify the layout of the reference docs page. -### jsonSampleExpandLevel +### jsonSamplesExpandLevel Sets the default expand level for JSON payload samples (response and request body). The default value is 2, and the maximum supported value is '+Infinity'. It can also be configured as a string with the special value `all` that expands all levels. @@ -102,21 +90,13 @@ If set to `true`, the sidebar uses the native scrollbar instead of perfect-scrol Shows only required fields in request samples. -### pathInMiddlePanel - -Shows the path link and HTTP verb in the middle panel instead of the right panel. - -### payloadSampleIdx - -If set, the payload sample is inserted at the specified index. If there are `N` payload samples and the value configured here is bigger than `N`, the payload sample is inserted last. Indexes start from 0. - -### requiredPropsFirst +### sortRequiredPropsFirst Shows required properties in schemas first, ordered in the same order as in the required array. -### schemaExpansionLevel +### schemasExpansionLevel -Specifies whether to automatically expand schemas in Reference docs. Set it to `all` to expand all schemas regardless of their level, or set it to a number to expand schemas up to the specified level. For example, `schemaExpansionLevel: 3` expands schemas up to three levels deep. The default value is `0`, meaning no schemas are expanded automatically. +Specifies whether to automatically expand schemas in Reference docs. Set it to `all` to expand all schemas regardless of their level, or set it to a number to expand schemas up to the specified level. For example, `schemasExpansionLevel: 3` expands schemas up to three levels deep. The default value is `0`, meaning no schemas are expanded automatically. ### scrollYOffset diff --git a/docs/index.md b/docs/index.md index a98776c210..6e2716e8e1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,7 +67,7 @@ theme: openapi: disableSearch: true expandResponses: 200,202 - jsonSampleExpandLevel: 1 + jsonSamplesExpandLevel: 1 theme: sidebar: diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 4e3ce62e2d..7d1f75864e 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -22,20 +22,14 @@ export interface ApiInfoProps { @observer export class ApiInfo extends React.Component { - handleDownloadClick = e => { - if (!e.target.href) { - e.target.href = this.props.store.spec.info.downloadLink; - } - }; - render() { const { store } = this.props; const { info, externalDocs } = store.spec; - const hideDownloadButton = store.options.hideDownloadButton; - - const downloadFilename = info.downloadFileName; - const downloadLink = info.downloadLink; + const hideDownloadButtons = store.options.hideDownloadButtons; + // FIXME: use downloadUrls + const downloadUrls = info.downloadUrls; + console.log(downloadUrls); const license = (info.license && ( @@ -83,17 +77,22 @@ export class ApiInfo extends React.Component { {info.title} {version} - {!hideDownloadButton && ( + {!hideDownloadButtons && (

{l('downloadSpecification')}: - - {l('download')} - + {downloadUrls?.map(({ title, url }) => { + return ( + + {downloadUrls.length > 1 ? title : l('download')} + + ); + })}

)} diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index 3e31e74a9d..8ae9d9b9e6 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -45,7 +45,7 @@ const Json = (props: JsonProps) => { // tslint:disable-next-line ref={node => setNode(node!)} dangerouslySetInnerHTML={{ - __html: jsonToHTML(props.data, options.jsonSampleExpandLevel), + __html: jsonToHTML(props.data, options.jsonSamplesExpandLevel), }} /> )} diff --git a/src/components/Markdown/SanitizedMdBlock.tsx b/src/components/Markdown/SanitizedMdBlock.tsx index d542c8c667..b05e5b50d3 100644 --- a/src/components/Markdown/SanitizedMdBlock.tsx +++ b/src/components/Markdown/SanitizedMdBlock.tsx @@ -10,7 +10,7 @@ const StyledMarkdownSpan = styled(StyledMarkdownBlock)` display: inline; `; -const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html); +const sanitize = (sanitize, html) => (sanitize ? DOMPurify.sanitize(html) : html); export function SanitizedMarkdownHTML({ inline, @@ -25,7 +25,7 @@ export function SanitizedMarkdownHTML({ { - const { expandSingleSchemaField, showObjectSchemaExamples, schemaExpansionLevel } = + const { expandSingleSchemaField, showObjectSchemaExamples, schemasExpansionLevel } = React.useContext(OptionsContext); const filteredFields = React.useMemo( @@ -45,7 +45,7 @@ export const ObjectSchema = observer( ); const expandByDefault = - (expandSingleSchemaField && filteredFields.length === 1) || schemaExpansionLevel >= level!; + (expandSingleSchemaField && filteredFields.length === 1) || schemasExpansionLevel >= level!; return ( diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 0cdd7f9e2a..1a62934e68 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -6,23 +6,32 @@ import { setRedocLabels } from './Labels'; import { SideNavStyleEnum } from './types'; import type { LabelsConfigRaw, MDXComponentMeta } from './types'; +export type DownloadUrlsConfig = { + title?: string; + url: string; +}[]; + export interface RedocRawOptions { theme?: ThemeInterface; scrollYOffset?: number | string | (() => number); hideHostname?: boolean | string; expandResponses?: string | 'all'; - requiredPropsFirst?: boolean | string; + requiredPropsFirst?: boolean | string; // remove in next major release + sortRequiredPropsFirst?: boolean | string; sortPropsAlphabetically?: boolean | string; sortEnumValuesAlphabetically?: boolean | string; sortOperationsAlphabetically?: boolean | string; sortTagsAlphabetically?: boolean | string; nativeScrollbars?: boolean | string; pathInMiddlePanel?: boolean | string; - untrustedSpec?: boolean | string; + untrustedSpec?: boolean | string; // remove in next major release + sanitize?: boolean | string; hideLoading?: boolean | string; - hideDownloadButton?: boolean | string; + hideDownloadButton?: boolean | string; // remove in next major release + hideDownloadButtons?: boolean | string; downloadFileName?: string; downloadDefinitionUrl?: string; + downloadUrls?: DownloadUrlsConfig; disableSearch?: boolean | string; onlyRequiredInSamples?: boolean | string; showExtensions?: boolean | string | string[]; @@ -30,12 +39,14 @@ export interface RedocRawOptions { hideSingleRequestSampleTab?: boolean | string; hideRequestPayloadSample?: boolean; menuToggle?: boolean | string; - jsonSampleExpandLevel?: number | string | 'all'; + jsonSampleExpandLevel?: number | string | 'all'; // remove in next major release + jsonSamplesExpandLevel?: number | string | 'all'; hideSchemaTitles?: boolean | string; simpleOneOfTypeLabel?: boolean | string; payloadSampleIdx?: number; expandSingleSchemaField?: boolean | string; - schemaExpansionLevel?: number | string | 'all'; + schemaExpansionLevel?: number | string | 'all'; // remove in next major release + schemasExpansionLevel?: number | string | 'all'; showObjectSchemaExamples?: boolean | string; showSecuritySchemeType?: boolean; hideSecuritySection?: boolean; @@ -216,17 +227,16 @@ export class RedocNormalizedOptions { scrollYOffset: () => number; hideHostname: boolean; expandResponses: { [code: string]: boolean } | 'all'; - requiredPropsFirst: boolean; + sortRequiredPropsFirst: boolean; sortPropsAlphabetically: boolean; sortEnumValuesAlphabetically: boolean; sortOperationsAlphabetically: boolean; sortTagsAlphabetically: boolean; nativeScrollbars: boolean; pathInMiddlePanel: boolean; - untrustedSpec: boolean; - hideDownloadButton: boolean; - downloadFileName?: string; - downloadDefinitionUrl?: string; + sanitize: boolean; + hideDownloadButtons: boolean; + downloadUrls?: DownloadUrlsConfig; disableSearch: boolean; onlyRequiredInSamples: boolean; showExtensions: boolean | string[]; @@ -234,13 +244,13 @@ export class RedocNormalizedOptions { hideSingleRequestSampleTab: boolean; hideRequestPayloadSample: boolean; menuToggle: boolean; - jsonSampleExpandLevel: number; + jsonSamplesExpandLevel: number; enumSkipQuotes: boolean; hideSchemaTitles: boolean; simpleOneOfTypeLabel: boolean; payloadSampleIdx: number; expandSingleSchemaField: boolean; - schemaExpansionLevel: number; + schemasExpansionLevel: number; showObjectSchemaExamples: boolean; showSecuritySchemeType?: boolean; hideSecuritySection?: boolean; @@ -288,17 +298,20 @@ export class RedocNormalizedOptions { this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset); this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); - this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst); + this.sortRequiredPropsFirst = argValueToBoolean( + raw.sortRequiredPropsFirst || raw.requiredPropsFirst, + ); this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically); this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically); this.sortOperationsAlphabetically = argValueToBoolean(raw.sortOperationsAlphabetically); this.sortTagsAlphabetically = argValueToBoolean(raw.sortTagsAlphabetically); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); - this.untrustedSpec = argValueToBoolean(raw.untrustedSpec); - this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton); - this.downloadFileName = raw.downloadFileName; - this.downloadDefinitionUrl = raw.downloadDefinitionUrl; + this.sanitize = argValueToBoolean(raw.sanitize || raw.untrustedSpec); + this.hideDownloadButtons = argValueToBoolean(raw.hideDownloadButtons || raw.hideDownloadButton); + this.downloadUrls = + raw.downloadUrls || + ([{ title: raw.downloadFileName, url: raw.downloadDefinitionUrl }] as DownloadUrlsConfig); this.disableSearch = argValueToBoolean(raw.disableSearch); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); @@ -306,15 +319,17 @@ export class RedocNormalizedOptions { this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab); this.hideRequestPayloadSample = argValueToBoolean(raw.hideRequestPayloadSample); this.menuToggle = argValueToBoolean(raw.menuToggle, true); - this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel( - raw.jsonSampleExpandLevel, + this.jsonSamplesExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel( + raw.jsonSamplesExpandLevel || raw.jsonSampleExpandLevel, ); this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes); this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles); this.simpleOneOfTypeLabel = argValueToBoolean(raw.simpleOneOfTypeLabel); this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx); this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField); - this.schemaExpansionLevel = argValueToExpandLevel(raw.schemaExpansionLevel); + this.schemasExpansionLevel = argValueToExpandLevel( + raw.schemasExpansionLevel || raw.schemaExpansionLevel, + ); this.showObjectSchemaExamples = argValueToBoolean(raw.showObjectSchemaExamples); this.showSecuritySchemeType = argValueToBoolean(raw.showSecuritySchemeType); this.hideSecuritySection = argValueToBoolean(raw.hideSecuritySection); diff --git a/src/services/__tests__/models/ApiInfo.test.ts b/src/services/__tests__/models/ApiInfo.test.ts index 867d50e981..e446074e9e 100644 --- a/src/services/__tests__/models/ApiInfo.test.ts +++ b/src/services/__tests__/models/ApiInfo.test.ts @@ -139,7 +139,7 @@ describe('Models', () => { } as any; const opts = new RedocNormalizedOptions({ - downloadDefinitionUrl: 'https:test.com/filename.yaml', + downloadUrls: [{ title: 'Openapi description', url: 'https:test.com/filename.yaml' }], }); const info = new ApiInfoModel(parser, opts); expect(info.downloadLink).toEqual('https:test.com/filename.yaml'); @@ -160,6 +160,13 @@ describe('Models', () => { const info = new ApiInfoModel(parser, opts); expect(info.downloadLink).toEqual('https:test.com/filename.yaml'); expect(info.downloadFileName).toEqual('test.yaml'); + + const opts2 = new RedocNormalizedOptions({ + downloadUrls: [{ title: 'test.yaml', url: 'https:test.com/filename.yaml' }], + }); + const info2 = new ApiInfoModel(parser, opts2); + expect(info2.downloadLink).toEqual('https:test.com/filename.yaml'); + expect(info2.downloadFileName).toEqual('test.yaml'); }); }); }); diff --git a/src/services/models/ApiInfo.ts b/src/services/models/ApiInfo.ts index fee2315021..738b98e632 100644 --- a/src/services/models/ApiInfo.ts +++ b/src/services/models/ApiInfo.ts @@ -1,7 +1,7 @@ import type { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types'; import { IS_BROWSER } from '../../utils/'; import type { OpenAPIParser } from '../OpenAPIParser'; -import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; +import { DownloadUrlsConfig, RedocNormalizedOptions } from '../RedocNormalizedOptions'; export class ApiInfoModel implements OpenAPIInfo { title: string; @@ -13,8 +13,7 @@ export class ApiInfoModel implements OpenAPIInfo { contact?: OpenAPIContact; license?: OpenAPILicense; - downloadLink?: string; - downloadFileName?: string; + downloadUrls?: DownloadUrlsConfig; constructor( private parser: OpenAPIParser, @@ -29,13 +28,20 @@ export class ApiInfoModel implements OpenAPIInfo { this.description = this.description.substring(0, firstHeadingLinePos); } - this.downloadLink = this.getDownloadLink(); - this.downloadFileName = this.getDownloadFileName(); + this.downloadUrls = this.getDownloadUrls(); + } + private getDownloadUrls(): DownloadUrlsConfig | undefined { + return this.options.downloadUrls + ?.map(({ title, url }) => ({ + title: title || 'openapi.json', + url: this.getDownloadLink(url) || '', + })) + .filter(({ title, url }) => title && url); } - private getDownloadLink(): string | undefined { - if (this.options.downloadDefinitionUrl) { - return this.options.downloadDefinitionUrl; + private getDownloadLink(url?: string): string | undefined { + if (url) { + return url; } if (this.parser.specUrl) { @@ -49,11 +55,4 @@ export class ApiInfoModel implements OpenAPIInfo { return window.URL.createObjectURL(blob); } } - - private getDownloadFileName(): string | undefined { - if (!this.parser.specUrl && !this.options.downloadDefinitionUrl) { - return this.options.downloadFileName || 'openapi.json'; - } - return this.options.downloadFileName; - } } diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index cb8bd73ddc..0d0e96c6e4 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -247,7 +247,7 @@ export class OperationModel implements IMenuItem { if (this.options.sortPropsAlphabetically) { return sortByField(_parameters, 'name'); } - if (this.options.requiredPropsFirst) { + if (this.options.sortRequiredPropsFirst) { return sortByRequired(_parameters); } diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 44b04279e0..2a9def5d44 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -463,7 +463,7 @@ function buildFields( if (options.sortPropsAlphabetically) { fields = sortByField(fields, 'name'); } - if (options.requiredPropsFirst) { + if (options.sortRequiredPropsFirst) { // if not sort alphabetically sort in the order from required keyword fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined); }