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
spec={this.state.spec}
specUrl={proxiedUrl}
options={{ scrollYOffset: 'nav', untrustedSpec: true }}
options={{ scrollYOffset: 'nav', sanitize: true }}
/>
</>
);

View File

@ -22,20 +22,14 @@ export interface ApiInfoProps {
@observer
export class ApiInfo extends React.Component<ApiInfoProps> {
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 && (
<InfoSpan>
@ -83,17 +77,22 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
<ApiHeader>
{info.title} {version}
</ApiHeader>
{!hideDownloadButton && (
{!hideDownloadButtons && (
<p>
{l('downloadSpecification')}:
<DownloadButton
download={downloadFilename || true}
target="_blank"
href={downloadLink}
onClick={this.handleDownloadClick}
>
{l('download')}
</DownloadButton>
{downloadUrls?.map(({ title, url }) => {
return (
<DownloadButton
download={title}
target="_blank"
href={url}
rel="noreferrer"
key={title}
>
{downloadUrls.length > 1 ? title : l('download')}
</DownloadButton>
);
})}
</p>
)}
<StyledMarkdownBlock>

View File

@ -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),
}}
/>
)}

View File

@ -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({
<Wrap
className={'redoc-markdown ' + (rest.className || '')}
dangerouslySetInnerHTML={{
__html: sanitize(options.untrustedSpec, rest.html),
__html: sanitize(options.sanitize, rest.html),
}}
data-role={rest['data-role']}
{...rest}

View File

@ -27,7 +27,7 @@ export const ObjectSchema = observer(
skipWriteOnly,
level,
}: ObjectSchemaProps) => {
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 (
<PropertiesTable>

View File

@ -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);

View File

@ -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');
});
});
});

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}