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/demo/museum.yaml b/demo/museum.yaml index 8473be695a..5c34236e7f 100644 --- a/demo/museum.yaml +++ b/demo/museum.yaml @@ -309,6 +309,9 @@ components: enum: - event - general + x-enumDescriptions: + event: Special event ticket + general: General museum entry ticket example: event Date: type: string @@ -776,6 +779,9 @@ x-tagGroups: - name: Purchases tags: - Tickets + - name: Entities + tags: + - Schemas security: - MuseumPlaceholderAuth: [] diff --git a/demo/openapi.yaml b/demo/openapi.yaml index d715155e63..b91e3622bb 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -1083,6 +1083,10 @@ components: - available - pending - sold + x-enumDescriptions: + available: Available status + pending: Pending status + sold: Sold status petType: description: Type of a pet type: string diff --git a/demo/playground/hmr-playground.tsx b/demo/playground/hmr-playground.tsx index 16bb507b76..8594482870 100644 --- a/demo/playground/hmr-playground.tsx +++ b/demo/playground/hmr-playground.tsx @@ -11,7 +11,11 @@ const userUrl = window.location.search.match(/url=(.*)$/); const specUrl = (userUrl && userUrl[1]) || (swagger ? 'museum.yaml' : big ? 'big-openapi.json' : 'museum.yaml'); -const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 }; +const options: RedocRawOptions = { + nativeScrollbars: false, + maxDisplayedEnumValues: 3, + schemaDefinitionsTagName: 'schemas', +}; const container = document.getElementById('example'); const root = createRoot(container!); diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 4e3ce62e2d..75e004387a 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -22,20 +22,13 @@ 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; + const downloadUrls = info.downloadUrls; + const downloadFileName = info.downloadFileName; const license = (info.license && ( @@ -83,17 +76,22 @@ export class ApiInfo extends React.Component { {info.title} {version} - {!hideDownloadButton && ( + {!hideDownloadButtons && (

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

)} diff --git a/src/components/Fields/EnumValues.tsx b/src/components/Fields/EnumValues.tsx index 21448ea575..a26673985f 100644 --- a/src/components/Fields/EnumValues.tsx +++ b/src/components/Fields/EnumValues.tsx @@ -5,17 +5,29 @@ import { l } from '../../services/Labels'; import { OptionsContext } from '../OptionsProvider'; import styled from '../../styled-components'; import { RedocRawOptions } from '../../services/RedocNormalizedOptions'; +import { StyledMarkdownBlock } from '../Markdown/styled.elements'; +import { Markdown } from '../Markdown/Markdown'; export interface EnumValuesProps { - values: string[]; - isArrayType: boolean; + values?: string[] | { [name: string]: string }; + type: string | string[]; } export interface EnumValuesState { collapsed: boolean; } +const DescriptionEnumsBlock = styled(StyledMarkdownBlock)` + table { + margin-bottom: 0.2em; + } +`; + export class EnumValues extends React.PureComponent { + constructor(props: EnumValuesProps) { + super(props); + this.toggle = this.toggle.bind(this); + } state: EnumValuesState = { collapsed: true, }; @@ -27,54 +39,94 @@ export class EnumValues extends React.PureComponent ({ + value, + description, + })); // TODO: provide context interface in more elegant way const { enumSkipQuotes, maxDisplayedEnumValues } = this.context as RedocRawOptions; - if (!values.length) { + if (!enums.length) { return null; } const displayedItems = this.state.collapsed && maxDisplayedEnumValues - ? values.slice(0, maxDisplayedEnumValues) - : values; + ? enums.slice(0, maxDisplayedEnumValues) + : enums; - const showToggleButton = maxDisplayedEnumValues - ? values.length > maxDisplayedEnumValues - : false; + const showToggleButton = maxDisplayedEnumValues ? enums.length > maxDisplayedEnumValues : false; const toggleButtonText = maxDisplayedEnumValues ? collapsed - ? `… ${values.length - maxDisplayedEnumValues} more` + ? `… ${enums.length - maxDisplayedEnumValues} more` : 'Hide' : ''; return (
- - {isArrayType ? l('enumArray') : ''}{' '} - {values.length === 1 ? l('enumSingleValue') : l('enum')}: - {' '} - {displayedItems.map((value, idx) => { - const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value); - return ( - - {exampleValue}{' '} - - ); - })} - {showToggleButton ? ( - { - this.toggle(); - }} - > - {toggleButtonText} - - ) : null} + {isDescriptionEnum ? ( + <> + + + + + + + + + + {(displayedItems as { value: string; description: string }[]).map( + ({ description, value }) => { + return ( + + + + + ); + }, + )} + +
+ + {type === 'array' ? l('enumArray') : ''}{' '} + {enums.length === 1 ? l('enumSingleValue') : l('enum')} + {' '} + + Description +
{value} + +
+
+ {showToggleButton ? ( + {toggleButtonText} + ) : null} + + ) : ( + <> + + {type === 'array' ? l('enumArray') : ''}{' '} + {values.length === 1 ? l('enumSingleValue') : l('enum')}: + {' '} + {displayedItems.map((value, idx) => { + const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value); + return ( + + {exampleValue}{' '} + + ); + })} + {showToggleButton ? ( + {toggleButtonText} + ) : null} + + )}
); } diff --git a/src/components/Fields/Field.tsx b/src/components/Fields/Field.tsx index d142c9a5c4..8ac4ef7cf2 100644 --- a/src/components/Fields/Field.tsx +++ b/src/components/Fields/Field.tsx @@ -19,6 +19,8 @@ import { Schema } from '../Schema/Schema'; import type { SchemaOptions } from '../Schema/Schema'; import type { FieldModel } from '../../services/models'; +import { OptionsContext } from '../OptionsProvider'; +import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions'; export interface FieldProps extends SchemaOptions { className?: string; @@ -27,12 +29,15 @@ export interface FieldProps extends SchemaOptions { field: FieldModel; expandByDefault?: boolean; - + fieldParentsName?: string[]; renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element; } @observer export class Field extends React.Component { + static contextType = OptionsContext; + context: RedocNormalizedOptions; + toggle = () => { if (this.props.field.expanded === undefined && this.props.expandByDefault) { this.props.field.collapse(); @@ -49,12 +54,12 @@ export class Field extends React.Component { }; render() { - const { className = '', field, isLast, expandByDefault } = this.props; + const { hidePropertiesPrefix } = this.context; + const { className = '', field, isLast, expandByDefault, fieldParentsName = [] } = this.props; const { name, deprecated, required, kind } = field; const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular; const expanded = field.expanded === undefined ? expandByDefault : field.expanded; - const labels = ( <> {kind === 'additionalProperties' && additional property} @@ -75,6 +80,10 @@ export class Field extends React.Component { onKeyPress={this.handleKeyPress} aria-label={`expand ${name}`} > + {!hidePropertiesPrefix && + fieldParentsName.map( + name => name + '.\u200B', // zero-width space, a special character is used for correct line breaking + )} {name} @@ -83,6 +92,10 @@ export class Field extends React.Component { ) : ( + {!hidePropertiesPrefix && + fieldParentsName.map( + name => name + '.\u200B', // zero-width space, a special character is used for correct line breaking + )} {name} {labels} @@ -102,6 +115,7 @@ export class Field extends React.Component { { )} {!renderDiscriminatorSwitch && ( - + )}{' '} {renderedExamples} 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({ { render() { const schema = this.props.schema; const itemsSchema = schema.items; + const fieldParentsName = this.props.fieldParentsName; const minMaxItems = schema.minItems === undefined && schema.maxItems === undefined ? '' : `(${humanizeConstraints(schema)})`; + const updatedParentsArray = fieldParentsName + ? [...fieldParentsName.slice(0, -1), fieldParentsName[fieldParentsName.length - 1] + '[]'] + : fieldParentsName; if (schema.fields) { - return ; + return ( + + ); } if (schema.displayType && !itemsSchema && !minMaxItems.length) { return ( @@ -37,7 +47,7 @@ export class ArraySchema extends React.PureComponent {
Array {minMaxItems} - +
diff --git a/src/components/Schema/ObjectSchema.tsx b/src/components/Schema/ObjectSchema.tsx index 145ea98d76..b59ea989f5 100644 --- a/src/components/Schema/ObjectSchema.tsx +++ b/src/components/Schema/ObjectSchema.tsx @@ -16,6 +16,7 @@ export interface ObjectSchemaProps extends SchemaProps { fieldName: string; parentSchema: SchemaModel; }; + fieldParentsName?: string[]; } export const ObjectSchema = observer( @@ -26,8 +27,9 @@ export const ObjectSchema = observer( skipReadOnly, skipWriteOnly, level, + fieldParentsName, }: ObjectSchemaProps) => { - const { expandSingleSchemaField, showObjectSchemaExamples, schemaExpansionLevel } = + const { expandSingleSchemaField, showObjectSchemaExamples, schemasExpansionLevel } = React.useContext(OptionsContext); const filteredFields = React.useMemo( @@ -45,7 +47,7 @@ export const ObjectSchema = observer( ); const expandByDefault = - (expandSingleSchemaField && filteredFields.length === 1) || schemaExpansionLevel >= level!; + (expandSingleSchemaField && filteredFields.length === 1) || schemasExpansionLevel >= level!; return ( @@ -58,6 +60,7 @@ export const ObjectSchema = observer( isLast={isLast} field={field} expandByDefault={expandByDefault} + fieldParentsName={Number(level) > 1 ? fieldParentsName : []} renderDiscriminatorSwitch={ discriminator?.fieldName === field.name ? () => ( diff --git a/src/components/Schema/Schema.tsx b/src/components/Schema/Schema.tsx index c0d38b1ea8..b8c08cd512 100644 --- a/src/components/Schema/Schema.tsx +++ b/src/components/Schema/Schema.tsx @@ -21,6 +21,7 @@ export interface SchemaOptions { export interface SchemaProps extends SchemaOptions { schema: SchemaModel; + fieldParentsName?: string[]; } @observer diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index 488199829b..f43b92da0b 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -79,21 +79,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -102,8 +104,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -114,6 +117,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -292,7 +296,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/packSize", @@ -313,6 +316,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "number", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, }, FieldModel { @@ -351,21 +355,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -374,8 +380,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -386,6 +393,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -564,7 +572,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/type", @@ -597,6 +604,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "string", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, }, ], @@ -610,21 +618,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -633,8 +643,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -645,6 +656,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -823,7 +835,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Dog", @@ -878,6 +889,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "object", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, SchemaModel { "activeOneOf": 0, @@ -931,21 +943,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -954,8 +968,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -966,6 +981,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -1144,7 +1160,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Cat/properties/type", @@ -1177,6 +1192,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "string", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, }, FieldModel { @@ -1215,21 +1231,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -1238,8 +1256,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -1250,6 +1269,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -1428,7 +1448,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Cat/properties/packSize", @@ -1457,6 +1476,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "number", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, }, ], @@ -1470,21 +1490,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -1493,8 +1515,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -1505,6 +1528,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -1683,7 +1707,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Cat", @@ -1743,6 +1766,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "object", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, ], "options": RedocNormalizedOptions { @@ -1750,21 +1774,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -1773,8 +1799,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -1785,6 +1812,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -1963,7 +1991,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Pet", @@ -2003,6 +2030,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "object", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, } } @@ -2060,21 +2088,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -2083,8 +2113,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -2095,6 +2126,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -2273,7 +2305,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/packSize", @@ -2294,6 +2325,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "number", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, }, FieldModel { @@ -2332,21 +2364,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -2355,8 +2389,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -2367,6 +2402,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -2545,7 +2581,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Dog/properties/type", @@ -2578,6 +2613,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "string", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, }, ], @@ -2591,21 +2627,23 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, + "downloadUrls": undefined, "enumSkipQuotes": false, "expandDefaultServerVariables": false, "expandResponses": {}, "expandSingleSchemaField": false, - "generatedPayloadSamplesMaxDepth": 10, - "hideDownloadButton": false, + "generatedSamplesMaxDepth": 10, + "hideDownloadButtons": false, "hideFab": false, "hideHostname": false, + "hidePropertiesPrefix": true, "hideRequestPayloadSample": false, "hideSchemaPattern": false, "hideSchemaTitles": false, "hideSecuritySection": false, "hideSingleRequestSampleTab": false, "ignoreNamedSchemas": Set {}, - "jsonSampleExpandLevel": 2, + "jsonSamplesExpandLevel": 2, "maxDisplayedEnumValues": undefined, "menuToggle": true, "minCharacterLengthToInitSearch": 3, @@ -2614,8 +2652,9 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "onlyRequiredInSamples": false, "pathInMiddlePanel": false, "payloadSampleIdx": 0, - "requiredPropsFirst": false, - "schemaExpansionLevel": 0, + "sanitize": false, + "schemaDefinitionsTagName": undefined, + "schemasExpansionLevel": 0, "scrollYOffset": [Function], "showExtensions": false, "showObjectSchemaExamples": false, @@ -2626,6 +2665,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "sortEnumValuesAlphabetically": false, "sortOperationsAlphabetically": false, "sortPropsAlphabetically": false, + "sortRequiredPropsFirst": false, "sortTagsAlphabetically": false, "theme": { "breakpoints": { @@ -2804,7 +2844,6 @@ exports[`Components SchemaView discriminator should correctly render SchemaView }, }, "unstable_ignoreMimeParameters": false, - "untrustedSpec": false, }, "pattern": undefined, "pointer": "#/components/schemas/Dog", @@ -2859,6 +2898,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "type": "object", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, } } /> @@ -2921,9 +2961,11 @@ exports[`Components SchemaView discriminator should correctly render discriminat "type": "number", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, } } + fieldParentsName={[]} isLast={false} key="packSize" showExamples={false} @@ -2994,9 +3036,11 @@ exports[`Components SchemaView discriminator should correctly render discriminat "type": "string", "typePrefix": "", "writeOnly": false, + "x-enumDescriptions": undefined, }, } } + fieldParentsName={[]} isLast={true} key="type" renderDiscriminatorSwitch={[Function]} diff --git a/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap b/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap index 5d6e6811c8..aa1cc03cb1 100644 --- a/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap @@ -159,7 +159,7 @@ exports[`FieldDetailsComponent renders correctly when field items have string ty [ items diff --git a/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap index 67c347821d..cfea66c7fe 100644 --- a/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap @@ -3,21 +3,21 @@ exports[`SecurityRequirement should render SecurityDefs 1`] = ` "

petstore_auth

Get access to data while protecting your account credentials. OAuth2 is also a safer and more secure way to give you access.

-
Security Scheme Type: OAuth2
Flow type: implicit
Scopes:
  • write:pets -

    modify pets in your account

    +
    Security Scheme Type: OAuth2
    Flow type: implicit
    Scopes:
    • write:pets -

      modify pets in your account

    • read:pets -

      read your pets

      -

GitLab_PersonalAccessToken

GitLab Personal Access Token description

-
Security Scheme Type: API Key
Header parameter name: PRIVATE-TOKEN

GitLab_OpenIdConnect

GitLab OpenIdConnect description

-
Security Scheme Type: OpenID Connect

basicAuth

Security Scheme Type: HTTP
HTTP Authorization Scheme: basic
" +

GitLab_PersonalAccessToken

GitLab Personal Access Token description

+
Security Scheme Type: API Key
Header parameter name: PRIVATE-TOKEN

GitLab_OpenIdConnect

GitLab OpenIdConnect description

+
Security Scheme Type: OpenID Connect

basicAuth

Security Scheme Type: HTTP
HTTP Authorization Scheme: basic
" `; -exports[`SecurityRequirement should render authDefinition 1`] = `"
Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth
,"`; +exports[`SecurityRequirement should render authDefinition 1`] = `"
Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth
,"`; exports[`SecurityRequirement should render authDefinition 2`] = ` -"
Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth (write:petsread:pets)
OAuth2: petstore_auth

Get access to data while protecting your account credentials. +"

Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth (write:petsread:pets)
OAuth2: petstore_auth

Get access to data while protecting your account credentials. OAuth2 is also a safer and more secure way to give you access.

-
Flow type: implicit
Required scopes: write:pets read:pets
Scopes:
  • write:pets -

    modify pets in your account

    +
    Flow type: implicit
    Required scopes: write:pets read:pets
    Scopes:
    • write:pets -

      modify pets in your account

    • read:pets -

      read your pets

      -
API Key: GitLab_PersonalAccessToken

GitLab Personal Access Token description

-
Header parameter name: PRIVATE-TOKEN
OpenID Connect: GitLab_OpenIdConnect

GitLab OpenIdConnect description

-
HTTP: basicAuth
HTTP Authorization Scheme: basic
," +
API Key: GitLab_PersonalAccessToken

GitLab Personal Access Token description

+
Header parameter name: PRIVATE-TOKEN
OpenID Connect: GitLab_OpenIdConnect

GitLab OpenIdConnect description

+
HTTP: basicAuth
HTTP Authorization Scheme: basic
," `; diff --git a/src/services/MenuBuilder.ts b/src/services/MenuBuilder.ts index 4ad28f7b77..d4b2450748 100644 --- a/src/services/MenuBuilder.ts +++ b/src/services/MenuBuilder.ts @@ -1,4 +1,4 @@ -import type { OpenAPISpec, OpenAPIPaths, OpenAPITag, OpenAPISchema } from '../types'; +import type { OpenAPIPaths, OpenAPITag, OpenAPISchema } from '../types'; import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils'; import { MarkdownRenderer } from './MarkdownRenderer'; import { GroupModel, OperationModel } from './models'; @@ -17,9 +17,19 @@ export class MenuBuilder { options: RedocNormalizedOptions, ): ContentItemModel[] { const spec = parser.spec; + const { schemaDefinitionsTagName } = options; const items: ContentItemModel[] = []; - const tagsMap = MenuBuilder.getTagsWithOperations(parser, spec); + const tags = [...(spec.tags || [])]; + const hasAutogenerated = tags.find( + tag => tag?.name === schemaDefinitionsTagName, + ); + + if (!hasAutogenerated && schemaDefinitionsTagName) { + tags.push({ name: schemaDefinitionsTagName }); + } + const tagsMap = MenuBuilder.getTagsWithOperations(parser, tags); + items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options)); if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) { items.push( @@ -28,6 +38,7 @@ export class MenuBuilder { } else { items.push(...MenuBuilder.getTagsItems(parser, tagsMap, undefined, undefined, options)); } + return items; } @@ -141,6 +152,7 @@ export class MenuBuilder { parser, tag, parent: item, + schemaDefinitionsTagName: options.schemaDefinitionsTagName, }); item.items = [ @@ -195,10 +207,11 @@ export class MenuBuilder { /** * collects tags and maps each tag to list of operations belonging to this tag */ - static getTagsWithOperations(parser: OpenAPIParser, spec: OpenAPISpec): TagsInfoMap { + static getTagsWithOperations(parser: OpenAPIParser, explicitTags: OpenAPITag[]): TagsInfoMap { + const { spec } = parser; const tags: TagsInfoMap = {}; const webhooks = spec['x-webhooks'] || spec.webhooks; - for (const tag of spec.tags || []) { + for (const tag of explicitTags || []) { tags[tag.name] = { ...tag, operations: [] }; } @@ -260,14 +273,18 @@ export class MenuBuilder { parser, tag, parent, + schemaDefinitionsTagName, }: { parser: OpenAPIParser; tag: TagInfo; parent: GroupModel; + schemaDefinitionsTagName?: string; }): GroupModel[] { + const defaultTags = schemaDefinitionsTagName ? [schemaDefinitionsTagName] : []; + return Object.entries(parser.spec.components?.schemas || {}) .map(([schemaName, schema]) => { - const schemaTags = schema['x-tags']; + const schemaTags = schema['x-tags'] || defaultTags; if (!schemaTags?.includes(tag.name)) return null; const item = new GroupModel( diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 0cdd7f9e2a..a84cb95af9 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,15 @@ 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'; + schemaDefinitionsTagName?: string; showObjectSchemaExamples?: boolean | string; showSecuritySchemeType?: boolean; hideSecuritySection?: boolean; @@ -52,11 +64,13 @@ export interface RedocRawOptions { maxDisplayedEnumValues?: number; ignoreNamedSchemas?: string[] | string; hideSchemaPattern?: boolean; - generatedPayloadSamplesMaxDepth?: number; + generatedPayloadSamplesMaxDepth?: number; // remove in next major release + generatedSamplesMaxDepth?: number; nonce?: string; hideFab?: boolean; minCharacterLengthToInitSearch?: number; showWebhookVerb?: boolean; + hidePropertiesPrefix?: boolean; } export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { @@ -216,17 +230,18 @@ 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; + sanitize: boolean; + hideDownloadButtons: boolean; downloadFileName?: string; downloadDefinitionUrl?: string; + downloadUrls?: DownloadUrlsConfig; disableSearch: boolean; onlyRequiredInSamples: boolean; showExtensions: boolean | string[]; @@ -234,13 +249,14 @@ 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; + schemaDefinitionsTagName?: string; showObjectSchemaExamples: boolean; showSecuritySchemeType?: boolean; hideSecuritySection?: boolean; @@ -254,10 +270,11 @@ export class RedocNormalizedOptions { ignoreNamedSchemas: Set; hideSchemaPattern: boolean; - generatedPayloadSamplesMaxDepth: number; + generatedSamplesMaxDepth: number; hideFab: boolean; minCharacterLengthToInitSearch: number; showWebhookVerb: boolean; + hidePropertiesPrefix?: boolean; nonce?: string; @@ -288,17 +305,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.sanitize = argValueToBoolean(raw.sanitize || raw.untrustedSpec); + this.hideDownloadButtons = argValueToBoolean(raw.hideDownloadButtons || raw.hideDownloadButton); this.downloadFileName = raw.downloadFileName; this.downloadDefinitionUrl = raw.downloadDefinitionUrl; + this.downloadUrls = raw.downloadUrls; this.disableSearch = argValueToBoolean(raw.disableSearch); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); @@ -306,15 +326,18 @@ 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.schemaDefinitionsTagName = raw.schemaDefinitionsTagName; this.showObjectSchemaExamples = argValueToBoolean(raw.showObjectSchemaExamples); this.showSecuritySchemeType = argValueToBoolean(raw.showSecuritySchemeType); this.hideSecuritySection = argValueToBoolean(raw.hideSecuritySection); @@ -330,13 +353,13 @@ export class RedocNormalizedOptions { : raw.ignoreNamedSchemas?.split(',').map(s => s.trim()); this.ignoreNamedSchemas = new Set(ignoreNamedSchemas); this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern); - this.generatedPayloadSamplesMaxDepth = - RedocNormalizedOptions.normalizeGeneratedPayloadSamplesMaxDepth( - raw.generatedPayloadSamplesMaxDepth, - ); + this.generatedSamplesMaxDepth = RedocNormalizedOptions.normalizeGeneratedPayloadSamplesMaxDepth( + raw.generatedSamplesMaxDepth || raw.generatedPayloadSamplesMaxDepth, + ); this.nonce = raw.nonce; this.hideFab = argValueToBoolean(raw.hideFab); this.minCharacterLengthToInitSearch = argValueToNumber(raw.minCharacterLengthToInitSearch) || 3; this.showWebhookVerb = argValueToBoolean(raw.showWebhookVerb); + this.hidePropertiesPrefix = argValueToBoolean(raw.hidePropertiesPrefix, true); } } diff --git a/src/services/__tests__/fixtures/nestedEnumDescroptionSample.json b/src/services/__tests__/fixtures/nestedEnumDescroptionSample.json new file mode 100644 index 0000000000..86108b015f --- /dev/null +++ b/src/services/__tests__/fixtures/nestedEnumDescroptionSample.json @@ -0,0 +1,24 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0", + "title": "Test" + }, + "components": { + "schemas": { + "Test": { + "type": "array", + "description": "test description", + "items": { + "type": "string", + "description": "test description", + "enum": ["authorize", "do-nothing"], + "x-enumDescriptions": { + "authorize-and-void": "Will create an authorize transaction in the amount/currency of the request, followed by a void", + "do-nothing": "Will do nothing, and return an approved `setup` transaction. This is the default behavior." + } + } + } + } + } +} diff --git a/src/services/__tests__/models/ApiInfo.test.ts b/src/services/__tests__/models/ApiInfo.test.ts index 867d50e981..9b9ed51ee2 100644 --- a/src/services/__tests__/models/ApiInfo.test.ts +++ b/src/services/__tests__/models/ApiInfo.test.ts @@ -139,10 +139,18 @@ 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'); + expect(info.downloadUrls).toMatchInlineSnapshot(` + [ + { + "title": "Openapi description", + "url": "https:test.com/filename.yaml", + }, + ] + `); + expect(info.downloadFileName).toMatchInlineSnapshot(`"openapi.json"`); }); test('should correctly populate download link and download file name', () => { @@ -158,8 +166,29 @@ describe('Models', () => { downloadFileName: 'test.yaml', }); const info = new ApiInfoModel(parser, opts); - expect(info.downloadLink).toEqual('https:test.com/filename.yaml'); - expect(info.downloadFileName).toEqual('test.yaml'); + expect(info.downloadUrls).toMatchInlineSnapshot(` + [ + { + "title": "Download", + "url": "https:test.com/filename.yaml", + }, + ] + `); + expect(info.downloadFileName).toMatchInlineSnapshot(`"test.yaml"`); + + const opts2 = new RedocNormalizedOptions({ + downloadUrls: [{ title: 'Download file', url: 'https:test.com/filename.yaml' }], + }); + const info2 = new ApiInfoModel(parser, opts2); + expect(info2.downloadUrls).toMatchInlineSnapshot(` + [ + { + "title": "Download file", + "url": "https:test.com/filename.yaml", + }, + ] + `); + expect(info2.downloadFileName).toMatchInlineSnapshot(`"openapi.json"`); }); }); }); diff --git a/src/services/__tests__/models/Schema.test.ts b/src/services/__tests__/models/Schema.test.ts index 9dab8a1b4a..514599d97f 100644 --- a/src/services/__tests__/models/Schema.test.ts +++ b/src/services/__tests__/models/Schema.test.ts @@ -13,6 +13,17 @@ describe('Models', () => { describe('Schema', () => { let parser; + test('parsing nested x-enumDescription', () => { + const spec = require('../fixtures/nestedEnumDescroptionSample.json'); + parser = new OpenAPIParser(spec, undefined, opts); + const testSchema = spec.components.schemas.Test; + const schemaModel = new SchemaModel(parser, testSchema, '', opts); + + expect(schemaModel['x-enumDescriptions']).toStrictEqual( + testSchema.items['x-enumDescriptions'], + ); + }); + test('discriminator with one field', () => { const spec = require('../fixtures/discriminator.json'); parser = new OpenAPIParser(spec, undefined, opts); diff --git a/src/services/models/ApiInfo.ts b/src/services/models/ApiInfo.ts index fee2315021..db6d3fa534 100644 --- a/src/services/models/ApiInfo.ts +++ b/src/services/models/ApiInfo.ts @@ -1,5 +1,6 @@ import type { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types'; import { IS_BROWSER } from '../../utils/'; +import { l } from '../Labels'; import type { OpenAPIParser } from '../OpenAPIParser'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; @@ -13,7 +14,10 @@ export class ApiInfoModel implements OpenAPIInfo { contact?: OpenAPIContact; license?: OpenAPILicense; - downloadLink?: string; + downloadUrls: { + title?: string; + url?: string; + }[]; downloadFileName?: string; constructor( @@ -29,13 +33,28 @@ export class ApiInfoModel implements OpenAPIInfo { this.description = this.description.substring(0, firstHeadingLinePos); } - this.downloadLink = this.getDownloadLink(); + this.downloadUrls = this.getDownloadUrls(); this.downloadFileName = this.getDownloadFileName(); } + private getDownloadUrls() { + return ( + !this.options.downloadUrls + ? [ + { + title: l('download'), + url: this.getDownloadLink(this.options.downloadDefinitionUrl), + }, + ] + : this.options.downloadUrls.map(({ title, url }) => ({ + title: title || l('download'), + 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) { diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts index 1b7263ae95..de80042a68 100644 --- a/src/services/models/MediaType.ts +++ b/src/services/models/MediaType.ts @@ -14,7 +14,7 @@ export class MediaTypeModel { name: string; isRequestType: boolean; onlyRequiredInSamples: boolean; - generatedPayloadSamplesMaxDepth: number; + generatedSamplesMaxDepth: number; /** * @param isRequestType needed to know if skipe RO/RW fields in objects @@ -30,7 +30,7 @@ export class MediaTypeModel { this.isRequestType = isRequestType; this.schema = info.schema && new SchemaModel(parser, info.schema, '', options); this.onlyRequiredInSamples = options.onlyRequiredInSamples; - this.generatedPayloadSamplesMaxDepth = options.generatedPayloadSamplesMaxDepth; + this.generatedSamplesMaxDepth = options.generatedSamplesMaxDepth; if (info.examples !== undefined) { this.examples = mapValues( info.examples, @@ -55,7 +55,7 @@ export class MediaTypeModel { skipReadOnly: this.isRequestType, skipWriteOnly: !this.isRequestType, skipNonRequired: this.isRequestType && this.onlyRequiredInSamples, - maxSampleDepth: this.generatedPayloadSamplesMaxDepth, + maxSampleDepth: this.generatedSamplesMaxDepth, }; if (this.schema && this.schema.oneOf) { this.examples = {}; 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..a07e071949 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -65,6 +65,7 @@ export class SchemaModel { rawSchema: OpenAPISchema; schema: MergedOpenAPISchema; extensions?: Record; + 'x-enumDescriptions': { [name: string]: string }; const: any; contentEncoding?: string; contentMediaType?: string; @@ -122,6 +123,7 @@ export class SchemaModel { this.type = schema.type || detectType(schema); this.format = schema.format; this.enum = schema.enum || []; + this['x-enumDescriptions'] = schema['x-enumDescriptions']; this.example = schema.example; this.examples = schema.examples; this.deprecated = !!schema.deprecated; @@ -221,6 +223,7 @@ export class SchemaModel { } if (this.items?.isPrimitive) { this.enum = this.items.enum; + this['x-enumDescriptions'] = this.items['x-enumDescriptions']; } if (isArray(this.type)) { const filteredType = this.type.filter(item => item !== 'array'); @@ -463,7 +466,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); } diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index cefc7d3736..21942671cc 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -311,6 +311,11 @@ exports[`#loadAndBundleSpec should load And Bundle Spec demo/openapi.yaml 1`] = "sold", ], "type": "string", + "x-enumDescriptions": { + "available": "Available status", + "pending": "Pending status", + "sold": "Sold status", + }, }, "tags": { "description": "Tags attached to the pet", diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index df2f91edce..15e04a43d6 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -654,6 +654,7 @@ export function isRedocExtension(key: string): boolean { 'x-codeSamples': true, 'x-displayName': true, 'x-examples': true, + 'x-enumDescriptions': true, 'x-logo': true, 'x-nullable': true, 'x-servers': true,