fix: unify redoc config

This commit is contained in:
Alex Varchuk 2025-01-15 16:17:40 +01:00
parent 59ee73fefa
commit 30db4dfe76
No known key found for this signature in database
GPG Key ID: 8A9260AE529FF454
10 changed files with 83 additions and 63 deletions

View File

@ -122,7 +122,7 @@ class DemoApp extends React.Component<
<RedocStandalone <RedocStandalone
spec={this.state.spec} spec={this.state.spec}
specUrl={proxiedUrl} specUrl={proxiedUrl}
options={{ scrollYOffset: 'nav', untrustedSpec: true }} options={{ scrollYOffset: 'nav', sanitize: true }}
/> />
</> </>
); );

View File

@ -22,20 +22,14 @@ export interface ApiInfoProps {
@observer @observer
export class ApiInfo extends React.Component<ApiInfoProps> { export class ApiInfo extends React.Component<ApiInfoProps> {
handleDownloadClick = e => {
if (!e.target.href) {
e.target.href = this.props.store.spec.info.downloadLink;
}
};
render() { render() {
const { store } = this.props; const { store } = this.props;
const { info, externalDocs } = store.spec; const { info, externalDocs } = store.spec;
const hideDownloadButton = store.options.hideDownloadButton; const hideDownloadButtons = store.options.hideDownloadButtons;
const downloadFilename = info.downloadFileName;
const downloadLink = info.downloadLink;
// FIXME: use downloadUrls
const downloadUrls = info.downloadUrls;
console.log(downloadUrls);
const license = const license =
(info.license && ( (info.license && (
<InfoSpan> <InfoSpan>
@ -83,17 +77,22 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
<ApiHeader> <ApiHeader>
{info.title} {version} {info.title} {version}
</ApiHeader> </ApiHeader>
{!hideDownloadButton && ( {!hideDownloadButtons && (
<p> <p>
{l('downloadSpecification')}: {l('downloadSpecification')}:
{downloadUrls?.map(({ title, url }) => {
return (
<DownloadButton <DownloadButton
download={downloadFilename || true} download={title}
target="_blank" target="_blank"
href={downloadLink} href={url}
onClick={this.handleDownloadClick} rel="noreferrer"
key={title}
> >
{l('download')} {downloadUrls.length > 1 ? title : l('download')}
</DownloadButton> </DownloadButton>
);
})}
</p> </p>
)} )}
<StyledMarkdownBlock> <StyledMarkdownBlock>

View File

@ -45,7 +45,7 @@ const Json = (props: JsonProps) => {
// tslint:disable-next-line // tslint:disable-next-line
ref={node => setNode(node!)} ref={node => setNode(node!)}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: jsonToHTML(props.data, options.jsonSampleExpandLevel), __html: jsonToHTML(props.data, options.jsonSamplesExpandLevel),
}} }}
/> />
)} )}

View File

@ -10,7 +10,7 @@ const StyledMarkdownSpan = styled(StyledMarkdownBlock)`
display: inline; display: inline;
`; `;
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html); const sanitize = (sanitize, html) => (sanitize ? DOMPurify.sanitize(html) : html);
export function SanitizedMarkdownHTML({ export function SanitizedMarkdownHTML({
inline, inline,
@ -25,7 +25,7 @@ export function SanitizedMarkdownHTML({
<Wrap <Wrap
className={'redoc-markdown ' + (rest.className || '')} className={'redoc-markdown ' + (rest.className || '')}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: sanitize(options.untrustedSpec, rest.html), __html: sanitize(options.sanitize, rest.html),
}} }}
data-role={rest['data-role']} data-role={rest['data-role']}
{...rest} {...rest}

View File

@ -27,7 +27,7 @@ export const ObjectSchema = observer(
skipWriteOnly, skipWriteOnly,
level, level,
}: ObjectSchemaProps) => { }: ObjectSchemaProps) => {
const { expandSingleSchemaField, showObjectSchemaExamples, schemaExpansionLevel } = const { expandSingleSchemaField, showObjectSchemaExamples, schemasExpansionLevel } =
React.useContext(OptionsContext); React.useContext(OptionsContext);
const filteredFields = React.useMemo( const filteredFields = React.useMemo(
@ -45,7 +45,7 @@ export const ObjectSchema = observer(
); );
const expandByDefault = const expandByDefault =
(expandSingleSchemaField && filteredFields.length === 1) || schemaExpansionLevel >= level!; (expandSingleSchemaField && filteredFields.length === 1) || schemasExpansionLevel >= level!;
return ( return (
<PropertiesTable> <PropertiesTable>

View File

@ -6,23 +6,32 @@ import { setRedocLabels } from './Labels';
import { SideNavStyleEnum } from './types'; import { SideNavStyleEnum } from './types';
import type { LabelsConfigRaw, MDXComponentMeta } from './types'; import type { LabelsConfigRaw, MDXComponentMeta } from './types';
export type DownloadUrlsConfig = {
title?: string;
url: string;
}[];
export interface RedocRawOptions { export interface RedocRawOptions {
theme?: ThemeInterface; theme?: ThemeInterface;
scrollYOffset?: number | string | (() => number); scrollYOffset?: number | string | (() => number);
hideHostname?: boolean | string; hideHostname?: boolean | string;
expandResponses?: string | 'all'; expandResponses?: string | 'all';
requiredPropsFirst?: boolean | string; requiredPropsFirst?: boolean | string; // remove in next major release
sortRequiredPropsFirst?: boolean | string;
sortPropsAlphabetically?: boolean | string; sortPropsAlphabetically?: boolean | string;
sortEnumValuesAlphabetically?: boolean | string; sortEnumValuesAlphabetically?: boolean | string;
sortOperationsAlphabetically?: boolean | string; sortOperationsAlphabetically?: boolean | string;
sortTagsAlphabetically?: boolean | string; sortTagsAlphabetically?: boolean | string;
nativeScrollbars?: boolean | string; nativeScrollbars?: boolean | string;
pathInMiddlePanel?: boolean | string; pathInMiddlePanel?: boolean | string;
untrustedSpec?: boolean | string; untrustedSpec?: boolean | string; // remove in next major release
sanitize?: boolean | string;
hideLoading?: boolean | string; hideLoading?: boolean | string;
hideDownloadButton?: boolean | string; hideDownloadButton?: boolean | string; // remove in next major release
hideDownloadButtons?: boolean | string;
downloadFileName?: string; downloadFileName?: string;
downloadDefinitionUrl?: string; downloadDefinitionUrl?: string;
downloadUrls?: DownloadUrlsConfig;
disableSearch?: boolean | string; disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string; onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
@ -30,12 +39,14 @@ export interface RedocRawOptions {
hideSingleRequestSampleTab?: boolean | string; hideSingleRequestSampleTab?: boolean | string;
hideRequestPayloadSample?: boolean; hideRequestPayloadSample?: boolean;
menuToggle?: boolean | string; menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all'; jsonSampleExpandLevel?: number | string | 'all'; // remove in next major release
jsonSamplesExpandLevel?: number | string | 'all';
hideSchemaTitles?: boolean | string; hideSchemaTitles?: boolean | string;
simpleOneOfTypeLabel?: boolean | string; simpleOneOfTypeLabel?: boolean | string;
payloadSampleIdx?: number; payloadSampleIdx?: number;
expandSingleSchemaField?: boolean | string; expandSingleSchemaField?: boolean | string;
schemaExpansionLevel?: number | string | 'all'; schemaExpansionLevel?: number | string | 'all'; // remove in next major release
schemasExpansionLevel?: number | string | 'all';
showObjectSchemaExamples?: boolean | string; showObjectSchemaExamples?: boolean | string;
showSecuritySchemeType?: boolean; showSecuritySchemeType?: boolean;
hideSecuritySection?: boolean; hideSecuritySection?: boolean;
@ -216,17 +227,16 @@ export class RedocNormalizedOptions {
scrollYOffset: () => number; scrollYOffset: () => number;
hideHostname: boolean; hideHostname: boolean;
expandResponses: { [code: string]: boolean } | 'all'; expandResponses: { [code: string]: boolean } | 'all';
requiredPropsFirst: boolean; sortRequiredPropsFirst: boolean;
sortPropsAlphabetically: boolean; sortPropsAlphabetically: boolean;
sortEnumValuesAlphabetically: boolean; sortEnumValuesAlphabetically: boolean;
sortOperationsAlphabetically: boolean; sortOperationsAlphabetically: boolean;
sortTagsAlphabetically: boolean; sortTagsAlphabetically: boolean;
nativeScrollbars: boolean; nativeScrollbars: boolean;
pathInMiddlePanel: boolean; pathInMiddlePanel: boolean;
untrustedSpec: boolean; sanitize: boolean;
hideDownloadButton: boolean; hideDownloadButtons: boolean;
downloadFileName?: string; downloadUrls?: DownloadUrlsConfig;
downloadDefinitionUrl?: string;
disableSearch: boolean; disableSearch: boolean;
onlyRequiredInSamples: boolean; onlyRequiredInSamples: boolean;
showExtensions: boolean | string[]; showExtensions: boolean | string[];
@ -234,13 +244,13 @@ export class RedocNormalizedOptions {
hideSingleRequestSampleTab: boolean; hideSingleRequestSampleTab: boolean;
hideRequestPayloadSample: boolean; hideRequestPayloadSample: boolean;
menuToggle: boolean; menuToggle: boolean;
jsonSampleExpandLevel: number; jsonSamplesExpandLevel: number;
enumSkipQuotes: boolean; enumSkipQuotes: boolean;
hideSchemaTitles: boolean; hideSchemaTitles: boolean;
simpleOneOfTypeLabel: boolean; simpleOneOfTypeLabel: boolean;
payloadSampleIdx: number; payloadSampleIdx: number;
expandSingleSchemaField: boolean; expandSingleSchemaField: boolean;
schemaExpansionLevel: number; schemasExpansionLevel: number;
showObjectSchemaExamples: boolean; showObjectSchemaExamples: boolean;
showSecuritySchemeType?: boolean; showSecuritySchemeType?: boolean;
hideSecuritySection?: boolean; hideSecuritySection?: boolean;
@ -288,17 +298,20 @@ export class RedocNormalizedOptions {
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset); this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);
this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst); this.sortRequiredPropsFirst = argValueToBoolean(
raw.sortRequiredPropsFirst || raw.requiredPropsFirst,
);
this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically); this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically);
this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically); this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically);
this.sortOperationsAlphabetically = argValueToBoolean(raw.sortOperationsAlphabetically); this.sortOperationsAlphabetically = argValueToBoolean(raw.sortOperationsAlphabetically);
this.sortTagsAlphabetically = argValueToBoolean(raw.sortTagsAlphabetically); this.sortTagsAlphabetically = argValueToBoolean(raw.sortTagsAlphabetically);
this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars);
this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel);
this.untrustedSpec = argValueToBoolean(raw.untrustedSpec); this.sanitize = argValueToBoolean(raw.sanitize || raw.untrustedSpec);
this.hideDownloadButton = argValueToBoolean(raw.hideDownloadButton); this.hideDownloadButtons = argValueToBoolean(raw.hideDownloadButtons || raw.hideDownloadButton);
this.downloadFileName = raw.downloadFileName; this.downloadUrls =
this.downloadDefinitionUrl = raw.downloadDefinitionUrl; raw.downloadUrls ||
([{ title: raw.downloadFileName, url: raw.downloadDefinitionUrl }] as DownloadUrlsConfig);
this.disableSearch = argValueToBoolean(raw.disableSearch); this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
@ -306,15 +319,17 @@ export class RedocNormalizedOptions {
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab); this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.hideRequestPayloadSample = argValueToBoolean(raw.hideRequestPayloadSample); this.hideRequestPayloadSample = argValueToBoolean(raw.hideRequestPayloadSample);
this.menuToggle = argValueToBoolean(raw.menuToggle, true); this.menuToggle = argValueToBoolean(raw.menuToggle, true);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel( this.jsonSamplesExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
raw.jsonSampleExpandLevel, raw.jsonSamplesExpandLevel || raw.jsonSampleExpandLevel,
); );
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes); this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles); this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles);
this.simpleOneOfTypeLabel = argValueToBoolean(raw.simpleOneOfTypeLabel); this.simpleOneOfTypeLabel = argValueToBoolean(raw.simpleOneOfTypeLabel);
this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx); this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx);
this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField); this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField);
this.schemaExpansionLevel = argValueToExpandLevel(raw.schemaExpansionLevel); this.schemasExpansionLevel = argValueToExpandLevel(
raw.schemasExpansionLevel || raw.schemaExpansionLevel,
);
this.showObjectSchemaExamples = argValueToBoolean(raw.showObjectSchemaExamples); this.showObjectSchemaExamples = argValueToBoolean(raw.showObjectSchemaExamples);
this.showSecuritySchemeType = argValueToBoolean(raw.showSecuritySchemeType); this.showSecuritySchemeType = argValueToBoolean(raw.showSecuritySchemeType);
this.hideSecuritySection = argValueToBoolean(raw.hideSecuritySection); this.hideSecuritySection = argValueToBoolean(raw.hideSecuritySection);

View File

@ -139,7 +139,7 @@ describe('Models', () => {
} as any; } as any;
const opts = new RedocNormalizedOptions({ 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); const info = new ApiInfoModel(parser, opts);
expect(info.downloadLink).toEqual('https:test.com/filename.yaml'); expect(info.downloadLink).toEqual('https:test.com/filename.yaml');
@ -160,6 +160,13 @@ describe('Models', () => {
const info = new ApiInfoModel(parser, opts); const info = new ApiInfoModel(parser, opts);
expect(info.downloadLink).toEqual('https:test.com/filename.yaml'); expect(info.downloadLink).toEqual('https:test.com/filename.yaml');
expect(info.downloadFileName).toEqual('test.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');
}); });
}); });
}); });

View File

@ -1,7 +1,7 @@
import type { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types'; import type { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types';
import { IS_BROWSER } from '../../utils/'; import { IS_BROWSER } from '../../utils/';
import type { OpenAPIParser } from '../OpenAPIParser'; import type { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { DownloadUrlsConfig, RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class ApiInfoModel implements OpenAPIInfo { export class ApiInfoModel implements OpenAPIInfo {
title: string; title: string;
@ -13,8 +13,7 @@ export class ApiInfoModel implements OpenAPIInfo {
contact?: OpenAPIContact; contact?: OpenAPIContact;
license?: OpenAPILicense; license?: OpenAPILicense;
downloadLink?: string; downloadUrls?: DownloadUrlsConfig;
downloadFileName?: string;
constructor( constructor(
private parser: OpenAPIParser, private parser: OpenAPIParser,
@ -29,13 +28,20 @@ export class ApiInfoModel implements OpenAPIInfo {
this.description = this.description.substring(0, firstHeadingLinePos); this.description = this.description.substring(0, firstHeadingLinePos);
} }
this.downloadLink = this.getDownloadLink(); this.downloadUrls = this.getDownloadUrls();
this.downloadFileName = this.getDownloadFileName(); }
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 { private getDownloadLink(url?: string): string | undefined {
if (this.options.downloadDefinitionUrl) { if (url) {
return this.options.downloadDefinitionUrl; return url;
} }
if (this.parser.specUrl) { if (this.parser.specUrl) {
@ -49,11 +55,4 @@ export class ApiInfoModel implements OpenAPIInfo {
return window.URL.createObjectURL(blob); 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;
}
} }

View File

@ -247,7 +247,7 @@ export class OperationModel implements IMenuItem {
if (this.options.sortPropsAlphabetically) { if (this.options.sortPropsAlphabetically) {
return sortByField(_parameters, 'name'); return sortByField(_parameters, 'name');
} }
if (this.options.requiredPropsFirst) { if (this.options.sortRequiredPropsFirst) {
return sortByRequired(_parameters); return sortByRequired(_parameters);
} }

View File

@ -463,7 +463,7 @@ function buildFields(
if (options.sortPropsAlphabetically) { if (options.sortPropsAlphabetically) {
fields = sortByField(fields, 'name'); fields = sortByField(fields, 'name');
} }
if (options.requiredPropsFirst) { if (options.sortRequiredPropsFirst) {
// if not sort alphabetically sort in the order from required keyword // if not sort alphabetically sort in the order from required keyword
fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined); fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
} }