Merge tag 'v2.0.0-rc.59' into sections-at-the-end

This commit is contained in:
Roberto Fernández 2021-12-14 14:23:37 +01:00
commit d05cf42ec6
19 changed files with 1543 additions and 5938 deletions

View File

@ -1,3 +1,16 @@
# [2.0.0-rc.59](https://github.com/Redocly/redoc/compare/v2.0.0-rc.58...v2.0.0-rc.59) (2021-12-09)
### Bug Fixes
* fix scroll in example dropdown ([#1803](https://github.com/Redocly/redoc/issues/1803)) ([bc2d9a7](https://github.com/Redocly/redoc/commit/bc2d9a7d9cd530274483fecd136db290a5b46ff7))
* x-examples for request body param does not display [#1743](https://github.com/Redocly/redoc/issues/1743) ([#1826](https://github.com/Redocly/redoc/issues/1826)) ([aaa3b32](https://github.com/Redocly/redoc/commit/aaa3b3280c8422d450e8849ae02135dde199d6d5))
### Features
* add option sideNavStyle ([#1805](https://github.com/Redocly/redoc/pull/1805)) ([2e4663b](https://github.com/Redocly/redoc/commit/2e4663b3b7022f25d3dc808afbcb3b3ad9483c41))
# [2.0.0-rc.58](https://github.com/Redocly/redoc/compare/v2.0.0-rc.57...v2.0.0-rc.58) (2021-11-29) # [2.0.0-rc.58](https://github.com/Redocly/redoc/compare/v2.0.0-rc.57...v2.0.0-rc.58) (2021-11-29)

View File

@ -240,6 +240,9 @@ You can use all of the following options with the standalone version of the <red
* `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0. * `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
* `theme` - ReDoc theme. For details check [theme docs](#redoc-theme-object). * `theme` - ReDoc theme. For details check [theme docs](#redoc-theme-object).
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!** * `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
* `sideNavStyle` - can be specified in various ways:
* **summary-only**: displays a summary in the sidebar navigation item. (**default**)
* **path-only**: displays a path in the sidebar navigation item.
### `<redoc>` theme object ### `<redoc>` theme object
* `spacing` * `spacing`

View File

@ -1,12 +1,12 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.0", "version": "0.13.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.0", "version": "0.13.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@ -17,7 +17,7 @@
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"redoc": "2.0.0-rc.57", "redoc": "2.0.0-rc.58",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"yargs": "^17.0.1" "yargs": "^17.0.1"
}, },
@ -921,9 +921,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": ["darwin"],
"darwin"
],
"engines": { "engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.0", "version": "0.13.1",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -19,7 +19,7 @@
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"redoc": "2.0.0-rc.57", "redoc": "2.0.0-rc.58",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"yargs": "^17.0.1" "yargs": "^17.0.1"
}, },

View File

@ -33,14 +33,14 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
}, },
devServer: { devServer: {
contentBase: __dirname, static: __dirname,
watchContentBase: true,
port: 9090, port: 9090,
disableHostCheck: true,
stats: 'minimal',
hot: true, hot: true,
historyApiFallback: true,
},
stats: {
children: true,
}, },
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'], extensions: ['.ts', '.tsx', '.js', '.json'],
fallback: { fallback: {
@ -72,7 +72,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' }, { test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: [getBabelLoader({useBuiltIns: true, hot: true} )], use: [getBabelLoader({ useBuiltIns: true, hot: true })],
exclude: { exclude: {
and: [/node_modules/], and: [/node_modules/],
not: { not: {

7105
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.58", "version": "2.0.0-rc.59",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -37,7 +37,7 @@
"unit": "jest --coverage", "unit": "jest --coverage",
"e2e": "cypress run", "e2e": "cypress run",
"e2e-ci": "cypress run --record", "e2e-ci": "cypress run --record",
"bundlesize": "bundlesize", "bundlesize": "size-limit",
"ts-check": "tsc --noEmit --skipLibCheck", "ts-check": "tsc --noEmit --skipLibCheck",
"cy:open": "cypress open", "cy:open": "cypress open",
"bundle:clean": "rimraf bundles", "bundle:clean": "rimraf bundles",
@ -75,6 +75,7 @@
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.13.0",
"@cypress/webpack-preprocessor": "^5.9.0", "@cypress/webpack-preprocessor": "^5.9.0",
"@hot-loader/react-dom": "^17.0.1", "@hot-loader/react-dom": "^17.0.1",
"@size-limit/preset-app": "^7.0.4",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/dompurify": "^2.2.2", "@types/dompurify": "^2.2.2",
"@types/enzyme": "^3.10.5", "@types/enzyme": "^3.10.5",
@ -83,9 +84,9 @@
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.170", "@types/lodash": "^4.14.170",
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/node": "^15.6.1",
"@types/mark.js": "^8.11.5", "@types/mark.js": "^8.11.5",
"@types/marked": "^1.1.0", "@types/marked": "^1.1.0",
"@types/node": "^15.6.1",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/prop-types": "^15.7.3", "@types/prop-types": "^15.7.3",
"@types/react": "^17.0.8", "@types/react": "^17.0.8",
@ -102,7 +103,6 @@
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-styled-components": "^1.12.0", "babel-plugin-styled-components": "^1.12.0",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.18.1",
"conventional-changelog-cli": "^2.0.34", "conventional-changelog-cli": "^2.0.34",
"copy-webpack-plugin": "^9.0.0", "copy-webpack-plugin": "^9.0.0",
"core-js": "^3.13.1", "core-js": "^3.13.1",
@ -130,6 +130,7 @@
"react-hot-loader": "^4.13.0", "react-hot-loader": "^4.13.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"shelljs": "^0.8.4", "shelljs": "^0.8.4",
"size-limit": "^7.0.4",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"ts-jest": "^27.0.2", "ts-jest": "^27.0.2",
@ -140,7 +141,7 @@
"url-polyfill": "^1.1.12", "url-polyfill": "^1.1.12",
"webpack": "^5.38.1", "webpack": "^5.38.1",
"webpack-cli": "^4.7.2", "webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2", "webpack-dev-server": "^4.6.0",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
"workerize-loader": "github:redocly/workerize-loader#webpack-5-dist" "workerize-loader": "github:redocly/workerize-loader#webpack-5-dist"
}, },
@ -163,7 +164,6 @@
"lunr": "^2.3.9", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.7.0", "marked": "^0.7.0",
"memoize-one": "^5.2.1",
"mobx-react": "^7.2.0", "mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.1", "openapi-sampler": "^1.0.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
@ -177,10 +177,10 @@
"swagger2openapi": "^7.0.6", "swagger2openapi": "^7.0.6",
"url-template": "^2.0.8" "url-template": "^2.0.8"
}, },
"bundlesize": [ "size-limit": [
{ {
"path": "./bundles/redoc.standalone.js", "path": "./bundles/redoc.standalone.js",
"maxSize": "350 kB" "limit": "350 kB"
} }
], ],
"jest": { "jest": {

View File

@ -18,12 +18,6 @@ import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement'; import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
const OperationRow = styled(Row)`
backface-visibility: hidden;
contain: content;
overflow: hidden;
`;
const Description = styled.div` const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px; margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
`; `;
@ -43,7 +37,7 @@ export class Operation extends React.Component<OperationProps> {
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {options => (
<OperationRow> <Row>
<MiddlePanel> <MiddlePanel>
<H2> <H2>
<ShareLink to={operation.id} /> <ShareLink to={operation.id} />
@ -71,7 +65,7 @@ export class Operation extends React.Component<OperationProps> {
<ResponseSamples operation={operation} /> <ResponseSamples operation={operation} />
<CallbackSamples callbacks={operation.callbacks} /> <CallbackSamples callbacks={operation.callbacks} />
</DarkRightPanel> </DarkRightPanel>
</OperationRow> </Row>
)} )}
</OptionsContext.Consumer> </OptionsContext.Consumer>
); );

View File

@ -7,6 +7,7 @@ import { shortenHTTPVerb } from '../../utils/openapi';
import { MenuItems } from './MenuItems'; import { MenuItems } from './MenuItems';
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements'; import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { scrollIntoViewIfNeeded } from '../../utils';
export interface MenuItemProps { export interface MenuItemProps {
item: IMenuItem; item: IMenuItem;
@ -33,7 +34,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
scrollIntoViewIfActive() { scrollIntoViewIfActive() {
if (this.props.item.active && this.ref.current) { if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded(); scrollIntoViewIfNeeded(this.ref.current);
} }
} }
@ -50,8 +51,8 @@ export class MenuItem extends React.Component<MenuItemProps> {
<OperationMenuItemContent {...this.props} item={item as OperationModel} /> <OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : ( ) : (
<MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}> <MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}>
<MenuItemTitle title={item.name}> <MenuItemTitle title={item.sidebarLabel}>
{item.name} {item.sidebarLabel}
{this.props.children} {this.props.children}
</MenuItemTitle> </MenuItemTitle>
{(item.depth > 0 && item.items.length > 0 && ( {(item.depth > 0 && item.items.length > 0 && (
@ -82,7 +83,7 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
componentDidUpdate() { componentDidUpdate() {
if (this.props.item.active && this.ref.current) { if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded(); scrollIntoViewIfNeeded(this.ref.current);
} }
} }
@ -101,7 +102,7 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge> <OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
)} )}
<MenuItemTitle width="calc(100% - 38px)"> <MenuItemTitle width="calc(100% - 38px)">
{item.name} {item.sidebarLabel}
{this.props.children} {this.props.children}
</MenuItemTitle> </MenuItemTitle>
</MenuItemLabel> </MenuItemLabel>

View File

@ -37,7 +37,6 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
if (item && item.active && this.context.menuToggle) { if (item && item.active && this.context.menuToggle) {
return item.expanded ? item.collapse() : item.expand(); return item.expanded ? item.collapse() : item.expand();
} }
this.props.menu.activateAndScroll(item, true); this.props.menu.activateAndScroll(item, true);
setTimeout(() => { setTimeout(() => {
if (this._updateScroll) { if (this._updateScroll) {

View File

@ -16,6 +16,7 @@ export interface IMenuItem {
id: string; id: string;
absoluteIdx?: number; absoluteIdx?: number;
name: string; name: string;
sidebarLabel: string;
description?: string; description?: string;
depth: number; depth: number;
active: boolean; active: boolean;

View File

@ -5,6 +5,11 @@ import { isNumeric, mergeObjects } from '../utils/helpers';
import { LabelsConfigRaw, setRedocLabels } from './Labels'; import { LabelsConfigRaw, setRedocLabels } from './Labels';
import { MDXComponentMeta } from './MarkdownRenderer'; import { MDXComponentMeta } from './MarkdownRenderer';
export enum SideNavStyleEnum {
SummaryOnly = 'summary-only',
PathOnly = 'path-only',
}
export interface RedocRawOptions { export interface RedocRawOptions {
theme?: ThemeInterface; theme?: ThemeInterface;
scrollYOffset?: number | string | (() => number); scrollYOffset?: number | string | (() => number);
@ -22,6 +27,7 @@ export interface RedocRawOptions {
disableSearch?: boolean | string; disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string; onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
sideNavStyle?: SideNavStyleEnum;
hideSingleRequestSampleTab?: boolean | string; hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string; menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all'; jsonSampleExpandLevel?: number | string | 'all';
@ -143,6 +149,22 @@ export class RedocNormalizedOptions {
} }
} }
static normalizeSideNavStyle(value: RedocRawOptions['sideNavStyle']): SideNavStyleEnum {
const defaultValue = SideNavStyleEnum.SummaryOnly;
if (typeof value !== 'string') {
return defaultValue;
}
switch (value) {
case defaultValue:
return value;
case SideNavStyleEnum.PathOnly:
return SideNavStyleEnum.PathOnly;
default:
return defaultValue;
}
}
static normalizeSectionsAtTheEnd(value: RedocRawOptions['sectionsAtTheEnd']): string[] { static normalizeSectionsAtTheEnd(value: RedocRawOptions['sectionsAtTheEnd']): string[] {
if (typeof value === 'undefined' || typeof value !== 'string') { if (typeof value === 'undefined' || typeof value !== 'string') {
return new Array(0); return new Array(0);
@ -198,6 +220,7 @@ export class RedocNormalizedOptions {
disableSearch: boolean; disableSearch: boolean;
onlyRequiredInSamples: boolean; onlyRequiredInSamples: boolean;
showExtensions: boolean | string[]; showExtensions: boolean | string[];
sideNavStyle: SideNavStyleEnum;
hideSingleRequestSampleTab: boolean; hideSingleRequestSampleTab: boolean;
menuToggle: boolean; menuToggle: boolean;
jsonSampleExpandLevel: number; jsonSampleExpandLevel: number;
@ -257,6 +280,7 @@ export class RedocNormalizedOptions {
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);
this.sideNavStyle = RedocNormalizedOptions.normalizeSideNavStyle(raw.sideNavStyle);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab); this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle, true); this.menuToggle = argValueToBoolean(raw.menuToggle, true);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel( this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(

View File

@ -14,6 +14,7 @@ export class GroupModel implements IMenuItem {
id: string; id: string;
absoluteIdx?: number; absoluteIdx?: number;
name: string; name: string;
sidebarLabel: string;
description?: string; description?: string;
type: MenuItemGroupType; type: MenuItemGroupType;
@ -44,6 +45,8 @@ export class GroupModel implements IMenuItem {
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name; this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
this.level = (tagOrGroup as MarkdownHeading).level || 1; this.level = (tagOrGroup as MarkdownHeading).level || 1;
this.sidebarLabel = this.name;
// remove sections from markdown, same as in ApiInfo // remove sections from markdown, same as in ApiInfo
this.description = tagOrGroup.description || ''; this.description = tagOrGroup.description || '';

View File

@ -25,6 +25,7 @@ import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent'; import { MediaContentModel } from './MediaContent';
import { RequestBodyModel } from './RequestBody'; import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response'; import { ResponseModel } from './Response';
import { SideNavStyleEnum } from '../RedocNormalizedOptions';
export interface XPayloadSample { export interface XPayloadSample {
lang: 'payload'; lang: 'payload';
@ -49,6 +50,7 @@ export class OperationModel implements IMenuItem {
id: string; id: string;
absoluteIdx?: number; absoluteIdx?: number;
name: string; name: string;
sidebarLabel: string;
description?: string; description?: string;
type = 'operation' as const; type = 'operation' as const;
@ -105,6 +107,8 @@ export class OperationModel implements IMenuItem {
this.name = getOperationSummary(operationSpec); this.name = getOperationSummary(operationSpec);
this.sidebarLabel = options.sideNavStyle === SideNavStyleEnum.PathOnly ? this.path : this.name;
if (this.isCallback) { if (this.isCallback) {
// NOTE: Callbacks by default should not inherit the specification's global `security` definition. // NOTE: Callbacks by default should not inherit the specification's global `security` definition.
// Can be defined individually per-callback in the specification. Defaults to none. // Can be defined individually per-callback in the specification. Defaults to none.

View File

@ -3,6 +3,7 @@ import { OpenAPIRequestBody, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { MediaContentModel } from './MediaContent'; import { MediaContentModel } from './MediaContent';
import { getContentWithLegacyExamples } from '../../utils';
type RequestBodyProps = { type RequestBodyProps = {
parser: OpenAPIParser; parser: OpenAPIParser;
@ -18,13 +19,15 @@ export class RequestBodyModel {
constructor(props: RequestBodyProps) { constructor(props: RequestBodyProps) {
const { parser, infoOrRef, options, isEvent } = props; const { parser, infoOrRef, options, isEvent } = props;
const isRequest = isEvent ? false : true; const isRequest = !isEvent;
const info = parser.deref(infoOrRef); const info = parser.deref(infoOrRef);
this.description = info.description || ''; this.description = info.description || '';
this.required = !!info.required; this.required = !!info.required;
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);
if (info.content !== undefined) {
this.content = new MediaContentModel(parser, info.content, isRequest, options); const mediaContent = getContentWithLegacyExamples(info);
if (mediaContent !== undefined) {
this.content = new MediaContentModel(parser, mediaContent, isRequest, options);
} }
} }
} }

View File

@ -186,17 +186,20 @@ export interface OpenAPIRequestBody {
description?: string; description?: string;
required?: boolean; required?: boolean;
content: { [mime: string]: OpenAPIMediaType }; content: { [mime: string]: OpenAPIMediaType };
'x-examples'?: { [mime: string]: { [name: string]: Referenced<OpenAPIExample> } };
'x-example'?: { [mime: string]: any };
} }
export interface OpenAPIResponses { export interface OpenAPIResponses {
[code: string]: OpenAPIResponse; [code: string]: OpenAPIResponse;
} }
export interface OpenAPIResponse { export interface OpenAPIResponse
description?: string; extends Pick<OpenAPIRequestBody, 'description' | 'x-examples' | 'x-example'> {
headers?: { [name: string]: Referenced<OpenAPIHeader> }; headers?: { [name: string]: Referenced<OpenAPIHeader> };
content?: { [mime: string]: OpenAPIMediaType };
links?: { [name: string]: Referenced<OpenAPILink> }; links?: { [name: string]: Referenced<OpenAPILink> };
content?: { [mime: string]: OpenAPIMediaType };
} }
export interface OpenAPILink { export interface OpenAPILink {

View File

@ -11,10 +11,16 @@ import {
serializeParameterValue, serializeParameterValue,
sortByRequired, sortByRequired,
humanizeNumberRange, humanizeNumberRange,
getContentWithLegacyExamples,
} from '../'; } from '../';
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services'; import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types'; import {
OpenAPIMediaType,
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
} from '../../types';
import { expandDefaultServerVariables } from '../openapi'; import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => { describe('Utils', () => {
@ -1161,4 +1167,106 @@ describe('Utils', () => {
]); ]);
}); });
}); });
describe('OpenAPI getContentWithLegacyExamples', () => {
it('should return undefined if no x-examples/x-example and no content', () => {
expect(getContentWithLegacyExamples({})).toBeUndefined();
});
it('should return unmodified object if no x-examples or x-example', () => {
const info = {
content: {
'application/json': {},
},
};
const content = getContentWithLegacyExamples(info);
expect(content).toStrictEqual(info.content);
});
it('should create a new content object if no content and x-examples', () => {
const info = {
'x-examples': {
'application/json': {
name: {
value: 'test',
},
},
},
};
const content = getContentWithLegacyExamples(info);
expect(content).toEqual({
'application/json': {
examples: {
name: {
value: 'test',
},
},
},
});
});
it('should create a new content object if no content and x-example', () => {
const info = {
'x-example': {
'application/json': 'test',
},
};
const content = getContentWithLegacyExamples(info);
expect(content).toEqual({
'application/json': { example: 'test' },
});
});
it('should return copy of content with injected x-example', () => {
const info = {
'x-example': {
'application/json': 'test',
},
content: {
'application/json': {
schema: { type: 'string' },
},
'text/plain': { schema: { type: 'string' } },
},
};
const content = getContentWithLegacyExamples(info) as { [mime: string]: OpenAPIMediaType };
expect(content).toEqual({
'application/json': { schema: { type: 'string' }, example: 'test' },
'text/plain': { schema: { type: 'string' } },
});
expect(content).not.toStrictEqual(info.content);
expect(content['application/json']).not.toStrictEqual(info.content['application/json']);
expect(content['text/plain']).toStrictEqual(info.content['text/plain']);
});
it('should prefer x-examples over x-example', () => {
const info = {
'x-example': {
'application/json': 'test',
},
'x-examples': {
'application/json': { name: { value: 'test' } },
},
content: {
'application/json': {
schema: { type: 'string' },
},
'text/plain': { schema: { type: 'string' } },
},
};
const content = getContentWithLegacyExamples(info) as { [mime: string]: OpenAPIMediaType };
expect(content).toEqual({
'application/json': { schema: { type: 'string' }, examples: { name: { value: 'test' } } },
'text/plain': { schema: { type: 'string' } },
});
expect(content).not.toStrictEqual(info.content);
expect(content['application/json']).not.toStrictEqual(info.content['application/json']);
expect(content['text/plain']).toStrictEqual(info.content['text/plain']);
});
});
}); });

View File

@ -24,52 +24,54 @@ export function html2Str(html: string): string {
.join(' '); .join(' ');
} }
// scrollIntoViewIfNeeded polyfill // Alternate scrollIntoViewIfNeeded implementation.
// Used in all cases, since it seems Chrome's implementation is buggy
// when "Experimental Web Platform Features" is enabled (at least of version 96).
// See #1714, #1742
if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) { export function scrollIntoViewIfNeeded(el: HTMLElement, centerIfNeeded = true) {
(Element as any).prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { const parent = el.parentNode as HTMLElement | null;
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; if (!parent) {
return;
}
const parentComputedStyle = window.getComputedStyle(parent, undefined);
const parentBorderTopWidth = parseInt(
parentComputedStyle.getPropertyValue('border-top-width'),
10,
);
const parentBorderLeftWidth = parseInt(
parentComputedStyle.getPropertyValue('border-left-width'),
10,
);
const overTop = el.offsetTop - parent.offsetTop < parent.scrollTop;
const overBottom =
el.offsetTop - parent.offsetTop + el.clientHeight - parentBorderTopWidth >
parent.scrollTop + parent.clientHeight;
const overLeft = el.offsetLeft - parent.offsetLeft < parent.scrollLeft;
const overRight =
el.offsetLeft - parent.offsetLeft + el.clientWidth - parentBorderLeftWidth >
parent.scrollLeft + parent.clientWidth;
const alignWithTop = overTop && !overBottom;
const parent = this.parentNode; if ((overTop || overBottom) && centerIfNeeded) {
const parentComputedStyle = window.getComputedStyle(parent, undefined); parent.scrollTop =
const parentBorderTopWidth = parseInt( el.offsetTop -
parentComputedStyle.getPropertyValue('border-top-width'), parent.offsetTop -
10, parent.clientHeight / 2 -
); parentBorderTopWidth +
const parentBorderLeftWidth = parseInt( el.clientHeight / 2;
parentComputedStyle.getPropertyValue('border-left-width'), }
10,
);
const overTop = this.offsetTop - parent.offsetTop < parent.scrollTop;
const overBottom =
this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth >
parent.scrollTop + parent.clientHeight;
const overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft;
const overRight =
this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth >
parent.scrollLeft + parent.clientWidth;
const alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) { if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollTop = parent.scrollLeft =
this.offsetTop - el.offsetLeft -
parent.offsetTop - parent.offsetLeft -
parent.clientHeight / 2 - parent.clientWidth / 2 -
parentBorderTopWidth + parentBorderLeftWidth +
this.clientHeight / 2; el.clientWidth / 2;
} }
if ((overLeft || overRight) && centerIfNeeded) { if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
parent.scrollLeft = el.scrollIntoView(alignWithTop);
this.offsetLeft - }
parent.offsetLeft -
parent.clientWidth / 2 -
parentBorderLeftWidth +
this.clientWidth / 2;
}
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop);
}
};
} }

View File

@ -9,6 +9,8 @@ import {
OpenAPIMediaType, OpenAPIMediaType,
OpenAPIParameter, OpenAPIParameter,
OpenAPIParameterStyle, OpenAPIParameterStyle,
OpenAPIRequestBody,
OpenAPIResponse,
OpenAPISchema, OpenAPISchema,
OpenAPIServer, OpenAPIServer,
Referenced, Referenced,
@ -638,3 +640,33 @@ export function pluralizeType(displayType: string): string {
.map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2')) .map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2'))
.join(' or '); .join(' or ');
} }
export function getContentWithLegacyExamples(
info: OpenAPIRequestBody | OpenAPIResponse,
): { [mime: string]: OpenAPIMediaType } | undefined {
let mediaContent = info.content;
const xExamples = info['x-examples']; // converted from OAS2 body param
const xExample = info['x-example']; // converted from OAS2 body param
if (xExamples) {
mediaContent = { ...mediaContent };
for (const mime of Object.keys(xExamples)) {
const examples = xExamples[mime];
mediaContent[mime] = {
...mediaContent[mime],
examples,
};
}
} else if (xExample) {
mediaContent = { ...mediaContent };
for (const mime of Object.keys(xExample)) {
const example = xExample[mime];
mediaContent[mime] = {
...mediaContent[mime],
example,
};
}
}
return mediaContent;
}