From 30db4dfe76e9ea8f22c0146fe99ff40fe9c2b8d3 Mon Sep 17 00:00:00 2001 From: Alex Varchuk Date: Wed, 15 Jan 2025 16:17:40 +0100 Subject: [PATCH] fix: unify redoc config --- demo/index.tsx | 2 +- src/components/ApiInfo/ApiInfo.tsx | 37 ++++++------- src/components/JsonViewer/JsonViewer.tsx | 2 +- src/components/Markdown/SanitizedMdBlock.tsx | 4 +- src/components/Schema/ObjectSchema.tsx | 4 +- src/services/RedocNormalizedOptions.ts | 55 ++++++++++++------- src/services/__tests__/models/ApiInfo.test.ts | 9 ++- src/services/models/ApiInfo.ts | 29 +++++----- src/services/models/Operation.ts | 2 +- src/services/models/Schema.ts | 2 +- 10 files changed, 83 insertions(+), 63 deletions(-) diff --git a/demo/index.tsx b/demo/index.tsx index f134eaa7..1e621a45 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -122,7 +122,7 @@ class DemoApp extends React.Component< ); diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 4e3ce62e..7d1f7586 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 3e31e74a..8ae9d9b9 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 d542c8c6..b05e5b50 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 0cdd7f9e..1a62934e 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 867d50e9..e446074e 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 fee23150..738b98e6 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 cb8bd73d..0d0e96c6 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 44b04279..2a9def5d 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); }