Merge branch 'master' into update/toggleMenuItem

This commit is contained in:
Roman Hotsiy 2019-07-29 17:30:26 +03:00 committed by GitHub
commit bfd703d2da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 812 additions and 1081 deletions

View File

@ -1,7 +1,11 @@
language: node_js language: node_js
node_js: node_js:
- '8' - '10'
cache: yarn cache:
yarn: true
directories:
# we also need to cache folder with Cypress binary
- ~/.cache
env: env:
global: global:
- GH_REF: github.com/Redocly/redoc.git - GH_REF: github.com/Redocly/redoc.git
@ -14,6 +18,9 @@ env:
- secure: SEqTg6WoGPPpcWzJ03ZfcSBb3nZ2Mdhug0ec2PszuzYO3libCb9usiqi+jils9z6qyXsL6ecz8HYazDGOUepnubhIpI5otLgfn9XiapjMT06Bj//AjbKpH7eu3TJSpJMzoRHZrKIE1y9ZKIBqKwl9Xs7ko+1oa+MLhrLuxXkoi0JqRB5UzkQtJRDoxVNjysnLQn+hsfnm+yuqPHZd2+Loy++q//WHuf9bwJrlkXn2ICYQIX5oQGlxNO6ui+OZklb0YknvyO5GdQeoKaHYru3MMKKCIS6I7AG9wLmPs5Ou3T0Ia0Xx4/7xazs0rH4NCVpIceSYc3v6evR37pp8MsFTC3BzjL1V3slTnmitC1KSNM8ndGRUg1nsCBkJysnR3HpX6SHuCH+UzOuMxEjwiPdSRnzJPEbTHa1HqMfTkTJMbm4zhp7W4/ozX4TtjUB0ql6NoQE2n0Z3aYgR2C78TmzaPQun8EgredWnCID1FedyexaNcw4HyZ2rXlcvG3rBzSwLHH5PePT9skyqy6KtIaL0MlAP556ilgUeyCZfCNdTmzCvPDZuqaeLRezWDdsKnRfTkxIW80QWlmZ6sW0hynJV5JN2Oghk9Tr+QzgV4ZF68FHwoU9YXCTyX4w5iTYq/GjvfTBqB3VSGPOz3PwU7r47tmaYzPj+I44zqktgxyuxDo= - secure: SEqTg6WoGPPpcWzJ03ZfcSBb3nZ2Mdhug0ec2PszuzYO3libCb9usiqi+jils9z6qyXsL6ecz8HYazDGOUepnubhIpI5otLgfn9XiapjMT06Bj//AjbKpH7eu3TJSpJMzoRHZrKIE1y9ZKIBqKwl9Xs7ko+1oa+MLhrLuxXkoi0JqRB5UzkQtJRDoxVNjysnLQn+hsfnm+yuqPHZd2+Loy++q//WHuf9bwJrlkXn2ICYQIX5oQGlxNO6ui+OZklb0YknvyO5GdQeoKaHYru3MMKKCIS6I7AG9wLmPs5Ou3T0Ia0Xx4/7xazs0rH4NCVpIceSYc3v6evR37pp8MsFTC3BzjL1V3slTnmitC1KSNM8ndGRUg1nsCBkJysnR3HpX6SHuCH+UzOuMxEjwiPdSRnzJPEbTHa1HqMfTkTJMbm4zhp7W4/ozX4TtjUB0ql6NoQE2n0Z3aYgR2C78TmzaPQun8EgredWnCID1FedyexaNcw4HyZ2rXlcvG3rBzSwLHH5PePT9skyqy6KtIaL0MlAP556ilgUeyCZfCNdTmzCvPDZuqaeLRezWDdsKnRfTkxIW80QWlmZ6sW0hynJV5JN2Oghk9Tr+QzgV4ZF68FHwoU9YXCTyX4w5iTYq/GjvfTBqB3VSGPOz3PwU7r47tmaYzPj+I44zqktgxyuxDo=
addons: addons:
chrome: stable chrome: stable
apt:
packages:
- libgconf-2-4 # for cypress
before_script: npm run bundle before_script: npm run bundle
script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm
run e2e) run e2e)

View File

@ -1,3 +1,16 @@
# [2.0.0-rc.10](https://github.com/Redocly/redoc/compare/v2.0.0-rc.9...v2.0.0-rc.10) (2019-07-08)
### Bug Fixes
* broken headings with single quote ([51d3b9b](https://github.com/Redocly/redoc/commit/51d3b9b)), closes [#955](https://github.com/Redocly/redoc/issues/955)
* fix fields table overflow if deeply nested with long title ([12b7057](https://github.com/Redocly/redoc/commit/12b7057))
* hide empty example when it is not defined ([4bd499f](https://github.com/Redocly/redoc/commit/4bd499f))
* markdown in examples descriptions + minor ui tweaks ([f52d9e8](https://github.com/Redocly/redoc/commit/f52d9e8))
* organize response examples in dropdown and display description ([995e557](https://github.com/Redocly/redoc/commit/995e557))
# [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27) # [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27)

View File

@ -243,6 +243,7 @@ You can use all of the following options with standalone version on <redoc> tag
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button. * `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `disableSearch` - disable search indexing and search box * `disableSearch` - disable search indexing and search box
* `onlyRequiredInSamples` - shows only required fields in request samples. * `onlyRequiredInSamples` - shows only required fields in request samples.
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts) * `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts)
## Advanced usage of standalone version ## Advanced usage of standalone version

View File

@ -156,7 +156,9 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
}, },
); );
} else if (request.url === '/') { } else if (request.url === '/') {
respondWithGzip(pageHTML, request, response); respondWithGzip(pageHTML, request, response, {
'Content-Type': 'text/html',
});
} else if (request.url === '/spec.json') { } else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2); const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, { respondWithGzip(specStr, request, response, {

View File

@ -1,25 +1,28 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.8.4", "version": "0.8.5",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
"repository": "https://github.com/Redocly/redoc", "repository": "https://github.com/Redocly/redoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>", "author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">= 8"
},
"dependencies": { "dependencies": {
"chokidar": "^2.0.4", "chokidar": "^3.0.2",
"handlebars": "^4.0.11", "handlebars": "^4.1.2",
"isarray": "^2.0.4", "isarray": "^2.0.4",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mobx": "^4.2.0", "mobx": "^4.2.0",
"node-libs-browser": "^2.2.0", "node-libs-browser": "^2.2.1",
"react": "^16.8.4", "react": "^16.8.6",
"react-dom": "^16.8.4", "react-dom": "^16.8.6",
"redoc": "^2.0.0-rc.8-1", "redoc": "^2.0.0-rc.10",
"styled-components": "^4.1.3", "styled-components": "^4.3.2",
"tslib": "^1.9.3", "tslib": "^1.10.0",
"yargs": "^12.0.5" "yargs": "^13.2.4"
}, },
"scripts": { "scripts": {
"ci-publish": "ci-publish" "ci-publish": "ci-publish"
@ -28,7 +31,7 @@
"access": "public" "access": "public"
}, },
"devDependencies": { "devDependencies": {
"@types/chokidar": "^1.7.5", "@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39", "@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2", "@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1" "ci-publish": "^1.3.1"

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ info:
OAuth2 - an open protocol to allow secure authorization in a simple OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications. and standard method from web, mobile and desktop applications.
<security-definitions /> <SecurityDefinitions />
version: 1.0.0 version: 1.0.0
title: Swagger Petstore title: Swagger Petstore
@ -63,6 +63,14 @@ tags:
description: Access to Petstore orders description: Access to Petstore orders
- name: user - name: user
description: Operations about user description: Operations about user
- name: pet_model
x-displayName: The Pet Model
description: |
<ObjectDescription schemaRef="#/components/schemas/Pet" />
- name: store_model
x-displayName: The Order Model
description: |
<ObjectDescription schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} />
x-tagGroups: x-tagGroups:
- name: General - name: General
tags: tags:
@ -71,6 +79,10 @@ x-tagGroups:
- name: User Management - name: User Management
tags: tags:
- user - user
- name: Models
tags:
- pet_model
- store_model
paths: paths:
/pet: /pet:
parameters: parameters:
@ -754,6 +766,11 @@ components:
description: Indicates whenever order was completed or not description: Indicates whenever order was completed or not
type: boolean type: boolean
default: false default: false
readOnly: true
rqeuestId:
description: Unique Request Id
type: string
writeOnly: true
xml: xml:
name: Order name: Order
Pet: Pet:
@ -926,3 +943,10 @@ components:
type: apiKey type: apiKey
name: api_key name: api_key
in: header in: header
examples:
Order:
value:
quantity: 1,
shipDate: 2018-10-19T16:46:45Z,
status: placed,
complete: false

View File

@ -6,7 +6,7 @@ describe('Menu', () => {
it('should have valid items count', () => { it('should have valid items count', () => {
cy.get('.menu-content') cy.get('.menu-content')
.find('li') .find('li')
.should('have.length', 6 + (2 + 8 + 4) + (1 + 8)); .should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8));
}); });
it('should sync active menu items while scroll', () => { it('should sync active menu items while scroll', () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.9", "version": "2.0.0-rc.10",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -28,7 +28,7 @@
"start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts", "start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts",
"start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts", "start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts",
"start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts", "start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts",
"test": "npm run lint && npm run unit && npm run bundlesize && npm run license-check", "test": "npm run lint && npm run unit && npm run license-check",
"unit": "jest --coverage", "unit": "jest --coverage",
"e2e": "cypress run", "e2e": "cypress run",
"e2e-ci": "cypress run --record", "e2e-ci": "cypress run --record",

View File

@ -5,11 +5,7 @@ import { ClipboardService } from '../services/ClipboardService';
export interface CopyButtonWrapperProps { export interface CopyButtonWrapperProps {
data: any; data: any;
children: ( children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
props: {
renderCopyButton: (() => React.ReactNode);
},
) => React.ReactNode;
} }
export class CopyButtonWrapper extends React.PureComponent< export class CopyButtonWrapper extends React.PureComponent<

View File

@ -55,7 +55,7 @@ export const StyledDropdown = styled(Dropdown)`
display: block; display: block;
height: 0; height: 0;
position: absolute; position: absolute;
right: 0.35em; right: 0.6em;
top: 50%; top: 50%;
margin-top: -0.125em; margin-top: -0.125em;
width: 0; width: 0;

View File

@ -32,6 +32,7 @@ export const TypeName = styled(FieldLabel)`
export const TypeTitle = styled(FieldLabel)` export const TypeTitle = styled(FieldLabel)`
color: ${props => props.theme.schema.typeTitleColor}; color: ${props => props.theme.schema.typeTitleColor};
word-break: break-word;
`; `;
export const TypeFormat = TypeName; export const TypeFormat = TypeName;

View File

@ -44,8 +44,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null; null;
const website = const website =
(info.contact && (info.contact && info.contact.url && (
info.contact.url && (
<InfoSpan> <InfoSpan>
URL: <a href={info.contact.url}>{info.contact.url}</a> URL: <a href={info.contact.url}>{info.contact.url}</a>
</InfoSpan> </InfoSpan>
@ -53,8 +52,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null; null;
const email = const email =
(info.contact && (info.contact && info.contact.email && (
info.contact.email && (
<InfoSpan> <InfoSpan>
{info.contact.name || 'E-mail'}:{' '} {info.contact.name || 'E-mail'}:{' '}
<a href={'mailto:' + info.contact.email}>{info.contact.email}</a> <a href={'mailto:' + info.contact.email}>{info.contact.email}</a>
@ -70,11 +68,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) || )) ||
null; null;
const version = const version = (info.version && <span>({info.version})</span>) || null;
(info.version && (
<span>({info.version})</span>
)) ||
null;
return ( return (
<Section> <Section>

View File

@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick'; import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils'; import { expandDefaultServerVariables, getBasePath } from '../../utils';
import { import {
EndpointInfo, EndpointInfo,
HttpVerb, HttpVerb,
@ -60,21 +60,24 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
/> />
</EndpointInfo> </EndpointInfo>
<ServersOverlay expanded={expanded}> <ServersOverlay expanded={expanded}>
{operation.servers.map(server => ( {operation.servers.map(server => {
<ServerItem key={server.url}> const normalizedUrl = expandDefaultServerVariables(server.url, server.variables);
return (
<ServerItem key={normalizedUrl}>
<Markdown source={server.description || ''} compact={true} /> <Markdown source={server.description || ''} compact={true} />
<SelectOnClick> <SelectOnClick>
<ServerUrl> <ServerUrl>
<span> <span>
{hideHostname || options.hideHostname {hideHostname || options.hideHostname
? getBasePath(server.url) ? getBasePath(normalizedUrl)
: server.url} : normalizedUrl}
</span> </span>
{operation.path} {operation.path}
</ServerUrl> </ServerUrl>
</SelectOnClick> </SelectOnClick>
</ServerItem> </ServerItem>
))} );
})}
</ServersOverlay> </ServersOverlay>
</OperationEndpointWrap> </OperationEndpointWrap>
)} )}

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { ExampleValue, FieldLabel } from '../../common-elements/fields'; import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export interface EnumValuesProps { export interface EnumValuesProps {
values: string[]; values: string[];
@ -9,8 +10,10 @@ export interface EnumValuesProps {
} }
export class EnumValues extends React.PureComponent<EnumValuesProps> { export class EnumValues extends React.PureComponent<EnumValuesProps> {
static contextType = OptionsContext;
render() { render() {
const { values, type } = this.props; const { values, type } = this.props;
const { enumSkipQuotes } = this.context;
if (!values.length) { if (!values.length) {
return null; return null;
} }
@ -21,11 +24,14 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
{type === 'array' ? l('enumArray') : ''}{' '} {type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}: {values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel> </FieldLabel>
{values.map((value, idx) => ( {values.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return (
<React.Fragment key={idx}> <React.Fragment key={idx}>
<ExampleValue>{JSON.stringify(value)}</ExampleValue>{' '} <ExampleValue>{exampleValue}</ExampleValue>
</React.Fragment> </React.Fragment>
))} );
})}
</div> </div>
); );
} }

View File

@ -65,8 +65,7 @@ export class Field extends React.Component<FieldProps> {
<FieldDetails {...this.props} /> <FieldDetails {...this.props} />
</PropertyDetailsCell> </PropertyDetailsCell>
</tr> </tr>
{field.expanded && {field.expanded && withSubSchema && (
withSubSchema && (
<tr key={field.name + 'inner'}> <tr key={field.name + 'inner'}>
<PropertyCellWithInner colSpan={2}> <PropertyCellWithInner colSpan={2}>
<InnerPropertiesWrap> <InnerPropertiesWrap>

View File

@ -17,10 +17,7 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
return ( return (
<div> <div>
<FieldLabel> {this.props.label} </FieldLabel>{' '} <FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue>
<ExampleValue>
{value}
</ExampleValue>
</div> </div>
); );
} }

View File

@ -21,10 +21,13 @@ import { FieldDetail } from './FieldDetail';
import { Badge } from '../../common-elements/'; import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export class FieldDetails extends React.PureComponent<FieldProps> { export class FieldDetails extends React.PureComponent<FieldProps> {
static contextType = OptionsContext;
render() { render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props; const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { enumSkipQuotes } = this.context;
const { schema, description, example, deprecated } = field; const { schema, description, example, deprecated } = field;
@ -33,7 +36,8 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
if (showExamples) { if (showExamples) {
const label = l('example') + ':'; const label = l('example') + ':';
if (field.in && field.style) { if (field.in && field.style) {
const serializedValue = serializeParameterValue(field, example); const serializedValue =
example !== undefined ? serializeParameterValue(field, example) : undefined;
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />; exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
} else { } else {
exampleField = <FieldDetail label={label} value={example} />; exampleField = <FieldDetail label={label} value={example} />;
@ -64,7 +68,7 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
<Badge type="warning"> {l('deprecated')} </Badge> <Badge type="warning"> {l('deprecated')} </Badge>
</div> </div>
)} )}
<FieldDetail label={l('default') + ':'} value={schema.default} /> <FieldDetail raw={enumSkipQuotes} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '} {!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{exampleField} {exampleField}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />} {<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}

View File

@ -5,6 +5,7 @@ import { SampleControls } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper'; import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv'; import { PrismDiv } from '../../common-elements/PrismDiv';
import { jsonToHTML } from '../../utils/jsonToHtml'; import { jsonToHTML } from '../../utils/jsonToHtml';
import { OptionsContext } from '../OptionsProvider';
import { jsonStyles } from './style'; import { jsonStyles } from './style';
export interface JsonProps { export interface JsonProps {
@ -32,12 +33,18 @@ class Json extends React.PureComponent<JsonProps> {
<span onClick={this.expandAll}> Expand all </span> <span onClick={this.expandAll}> Expand all </span>
<span onClick={this.collapseAll}> Collapse all </span> <span onClick={this.collapseAll}> Collapse all </span>
</SampleControls> </SampleControls>
<OptionsContext.Consumer>
{options => (
<PrismDiv <PrismDiv
className={this.props.className} className={this.props.className}
// tslint:disable-next-line // tslint:disable-next-line
ref={node => (this.node = node!)} ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }} dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}
/> />
)}
</OptionsContext.Consumer>
</JsonViewerWrap> </JsonViewerWrap>
); );

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { DropdownProps } from '../../common-elements/dropdown'; import { DropdownProps } from '../../common-elements/dropdown';
import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models'; import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface MediaTypeChildProps { export interface MediaTypeChildProps {
schema: SchemaModel; schema: SchemaModel;
@ -11,6 +12,8 @@ export interface MediaTypeChildProps {
export interface MediaTypesSwitchProps { export interface MediaTypesSwitchProps {
content?: MediaContentModel; content?: MediaContentModel;
withLabel?: boolean;
renderDropdown: (props: DropdownProps) => JSX.Element; renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeMime: MediaTypeModel) => JSX.Element; children: (activeMime: MediaTypeModel) => JSX.Element;
} }
@ -37,13 +40,25 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
}; };
}); });
const Wrapper = ({ children }) =>
this.props.withLabel ? (
<DropdownWrapper>
<DropdownLabel>Content type</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return ( return (
<> <>
<Wrapper>
{this.props.renderDropdown({ {this.props.renderDropdown({
value: options[activeMimeIdx], value: options[activeMimeIdx],
options, options,
onChange: this.switchMedia, onChange: this.switchMedia,
})} })}
</Wrapper>
{this.props.children(content.active)} {this.props.children(content.active)}
</> </>
); );

View File

@ -1,17 +1,33 @@
import * as React from 'react'; import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements'; import styled from '../../styled-components';
import { MediaTypeModel } from '../../services/models';
import { DropdownProps } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { Markdown } from '../Markdown/Markdown';
import { Example } from './Example'; import { Example } from './Example';
import { NoSampleLabel } from './styled.elements'; import { DropdownLabel, DropdownWrapper, NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps { export interface PayloadSamplesProps {
mediaType: MediaTypeModel; mediaType: MediaTypeModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
} }
export class MediaTypeSamples extends React.Component<PayloadSamplesProps> { interface MediaTypeSamplesState {
activeIdx: number;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps, MediaTypeSamplesState> {
state = {
activeIdx: 0,
};
switchMedia = ({ value }) => {
this.setState({
activeIdx: parseInt(value, 10),
});
};
render() { render() {
const { activeIdx } = this.state;
const examples = this.props.mediaType.examples || {}; const examples = this.props.mediaType.examples || {};
const mimeType = this.props.mediaType.name; const mimeType = this.props.mediaType.name;
@ -21,28 +37,46 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
if (examplesNames.length === 0) { if (examplesNames.length === 0) {
return noSample; return noSample;
} }
if (examplesNames.length > 1) { if (examplesNames.length > 1) {
const options = examplesNames.map((name, idx) => {
return {
label: examples[name].summary || name,
value: idx.toString(),
};
});
const example = examples[examplesNames[activeIdx]];
const description = example.description;
return ( return (
<SmallTabs defaultIndex={0}> <SamplesWrapper>
<TabList> <DropdownWrapper>
{examplesNames.map(name => ( <DropdownLabel>Example</DropdownLabel>
<Tab key={name}> {examples[name].summary || name} </Tab> {this.props.renderDropdown({
))} value: options[activeIdx],
</TabList> options,
{examplesNames.map(name => ( onChange: this.switchMedia,
<TabPanel key={name}> })}
<Example example={examples[name]} mimeType={mimeType} /> </DropdownWrapper>
</TabPanel> <div>
))} {description && <Markdown source={description} />}
</SmallTabs> <Example example={example} mimeType={mimeType} />
</div>
</SamplesWrapper>
); );
} else { } else {
const name = examplesNames[0]; const example = examples[examplesNames[0]];
return ( return (
<div> <SamplesWrapper>
<Example example={examples[name]} mimeType={mimeType} /> {example.description && <Markdown source={example.description} />}
</div> <Example example={example} mimeType={mimeType} />
</SamplesWrapper>
); );
} }
} }
} }
const SamplesWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -2,10 +2,9 @@ import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { MediaTypeSamples } from './MediaTypeSamples'; import { MediaTypeSamples } from './MediaTypeSamples';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { MediaContentModel } from '../../services/models'; import { MediaContentModel } from '../../services/models';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel'; import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements'; import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
export interface PayloadSamplesProps { export interface PayloadSamplesProps {
@ -21,8 +20,14 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
} }
return ( return (
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown}> <MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
{mediaType => <MediaTypeSamples key="samples" mediaType={mediaType} />} {mediaType => (
<MediaTypeSamples
key="samples"
mediaType={mediaType}
renderDropdown={this.renderDropdown}
/>
)}
</MediaTypesSwitch> </MediaTypesSwitch>
); );
} }

View File

@ -13,8 +13,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
prevRef.current = example; prevRef.current = example;
useEffect( useEffect(() => {
() => {
const load = async () => { const load = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@ -26,9 +25,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
}; };
load(); load();
}, }, [example, mimeType]);
[example, mimeType],
);
return value.current; return value.current;
} }

View File

@ -1,29 +1,48 @@
// @ts-ignore // @ts-ignore
import Dropdown from 'react-dropdown'; import Dropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements'; import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div` export const MimeLabel = styled.div`
border-bottom: 1px solid rgba(255, 255, 255, 0.9); padding: 12px;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
color: rgba(255, 255, 255, 0.8); `;
export const DropdownLabel = styled.span`
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 12px;
position: absolute;
z-index: 1;
top: -11px;
left: 12px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
`;
export const DropdownWrapper = styled.div`
position: relative;
`; `;
export const InvertedSimpleDropdown = styled(StyledDropdown)` export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.929em;
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control, .Dropdown-control,
.Dropdown-control:hover { .Dropdown-control:hover {
font-size: 1em; font-size: 1em;
border: none; border: none;
padding: 0 1.2em 0 0; padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent; background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor}; color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none; box-shadow: none;
@ -34,6 +53,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
} }
.Dropdown-menu { .Dropdown-menu {
margin: 0; margin: 0;
margin-top: 2px;
} }
`; `;

View File

@ -28,8 +28,7 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> {
code={code} code={code}
opened={expanded} opened={expanded}
/> />
{expanded && {expanded && !empty && (
!empty && (
<ResponseDetailsWrap> <ResponseDetailsWrap>
<ResponseDetails response={this.props.response} /> <ResponseDetails response={this.props.response} />
</ResponseDetailsWrap> </ResponseDetailsWrap>

View File

@ -34,9 +34,9 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
const filteredFields = needFilter const filteredFields = needFilter
? fields.filter(item => { ? fields.filter(item => {
return ( return !(
(this.props.skipReadOnly && !item.schema.readOnly) || (this.props.skipReadOnly && item.schema.readOnly) ||
(this.props.skipWriteOnly && !item.schema.writeOnly) (this.props.skipWriteOnly && item.schema.writeOnly)
); );
}) })
: fields; : fields;

View File

@ -0,0 +1,93 @@
import * as React from 'react';
import { DarkRightPanel, MiddlePanel, MimeLabel, Row, Section } from '../../common-elements';
import { MediaTypeModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import styled from '../../styled-components';
import { OpenAPIMediaType } from '../../types';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples';
import { InvertedSimpleDropdown } from '../PayloadSamples/styled.elements';
import { Schema } from '../Schema';
export interface ObjectDescriptionProps {
schemaRef: string;
exampleRef?: string;
showReadOnly?: boolean;
showWriteOnly?: boolean;
parser: OpenAPIParser;
options: RedocNormalizedOptions;
}
export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps> {
private static getMediaType(schemaRef: string, exampleRef?: string): OpenAPIMediaType {
if (!schemaRef) {
return {};
}
const info: OpenAPIMediaType = {
schema: { $ref: schemaRef },
};
if (exampleRef) {
info.examples = { example: { $ref: exampleRef } };
}
return info;
}
private _mediaModel: MediaTypeModel;
private get mediaModel() {
const { parser, schemaRef, exampleRef, options } = this.props;
if (!this._mediaModel) {
this._mediaModel = new MediaTypeModel(
parser,
'json',
false,
SchemaDefinition.getMediaType(schemaRef, exampleRef),
options,
);
}
return this._mediaModel;
}
render() {
const { showReadOnly = true, showWriteOnly = false } = this.props;
return (
<Section>
<Row>
<MiddlePanel>
<Schema
skipWriteOnly={!showWriteOnly}
skipReadOnly={!showReadOnly}
schema={this.mediaModel.schema}
/>
</MiddlePanel>
<DarkRightPanel>
<MediaSamplesWrap>
<MediaTypeSamples renderDropdown={this.renderDropdown} mediaType={this.mediaModel} />
</MediaSamplesWrap>
</DarkRightPanel>
</Row>
</Section>
);
}
private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
};
}
const MediaSamplesWrap = styled.div`
background: ${({ theme }) => theme.codeSample.backgroundColor};
& > div,
& > pre {
padding: ${props => props.theme.spacing.unit * 4}px;
margin: 0;
}
& > div > pre {
padding: 0;
}
`;

View File

@ -57,16 +57,13 @@ export class MenuItem extends React.Component<MenuItemProps> {
{item.name} {item.name}
{this.props.children} {this.props.children}
</MenuItemTitle> </MenuItemTitle>
{(item.depth > 0 && {(item.depth > 0 && item.items.length > 0 && (
item.items.length > 0 && (
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} /> <ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
)) || )) ||
null} null}
</MenuItemLabel> </MenuItemLabel>
)} )}
{!withoutChildren && {!withoutChildren && item.items && item.items.length > 0 && (
item.items &&
item.items.length > 0 && (
<MenuItems <MenuItems
expanded={item.expanded} expanded={item.expanded}
items={item.items} items={item.items}
@ -83,7 +80,7 @@ export interface OperationMenuItemContentProps {
} }
@observer @observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> { export class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
render() { render() {
const { item } = this.props; const { item } = this.props;
return ( return (

View File

@ -10,8 +10,13 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore'; import { SearchStore } from './SearchStore';
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes'; import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi'; import {
SCHEMA_DEFINITION_JSX_NAME,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
export interface StoreState { export interface StoreState {
menu: { menu: {
@ -151,5 +156,18 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
securitySchemes: store.spec.securitySchemes, securitySchemes: store.spec.securitySchemes,
}), }),
}, },
[SECURITY_DEFINITIONS_JSX_NAME]: {
component: SecurityDefs,
propsSelector: (store: AppStore) => ({
securitySchemes: store.spec.securitySchemes,
}),
},
[SCHEMA_DEFINITION_JSX_NAME]: {
component: SchemaDefinition,
propsSelector: (store: AppStore) => ({
parser: store.spec.parser,
options: store.options,
}),
},
}, },
}; };

View File

@ -1,6 +1,6 @@
import * as marked from 'marked'; import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils'; import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
import { AppStore } from './AppStore'; import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -65,6 +65,7 @@ export class MarkdownRenderer {
container: MarkdownHeading[] = this.headings, container: MarkdownHeading[] = this.headings,
parentId?: string, parentId?: string,
): MarkdownHeading { ): MarkdownHeading {
name = unescapeHTMLChars(name);
const item = { const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`, id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
name, name,
@ -88,7 +89,7 @@ export class MarkdownRenderer {
} }
attachHeadingsDescriptions(rawText: string) { attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => { const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`); return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
}; };

View File

@ -42,7 +42,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = []; const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec); const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options)); items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) { if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push( items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options), ...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
@ -59,14 +59,16 @@ export class MenuBuilder {
*/ */
static addMarkdownItems( static addMarkdownItems(
description: string, description: string,
parent: GroupModel | undefined,
initialDepth: number,
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
): ContentItemModel[] { ): ContentItemModel[] {
const renderer = new MarkdownRenderer(options); const renderer = new MarkdownRenderer(options);
const headings = renderer.extractHeadings(description || ''); const headings = renderer.extractHeadings(description || '');
const mapHeadingsDeep = (parent, items, depth = 1) => const mapHeadingsDeep = (_parent, items, depth = 1) =>
items.map(heading => { items.map(heading => {
const group = new GroupModel('section', heading, parent); const group = new GroupModel('section', heading, _parent);
group.depth = depth; group.depth = depth;
if (heading.items) { if (heading.items) {
group.items = mapHeadingsDeep(group, heading.items, depth + 1); group.items = mapHeadingsDeep(group, heading.items, depth + 1);
@ -82,7 +84,7 @@ export class MenuBuilder {
return group; return group;
}); });
return mapHeadingsDeep(undefined, headings); return mapHeadingsDeep(parent, headings, initialDepth);
} }
/** /**
@ -144,15 +146,22 @@ export class MenuBuilder {
} }
const item = new GroupModel('tag', tag, parent); const item = new GroupModel('tag', tag, parent);
item.depth = GROUP_DEPTH + 1; item.depth = GROUP_DEPTH + 1;
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
// don't put empty tag into content, instead put its operations // don't put empty tag into content, instead put its operations
if (tag.name === '') { if (tag.name === '') {
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options); const items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, undefined, tag, item.depth + 1, options),
];
res.push(...items); res.push(...items);
continue; continue;
} }
item.items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
];
res.push(item); res.push(item);
} }
return res; return res;

View File

@ -23,12 +23,14 @@ export interface RedocRawOptions {
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
hideSingleRequestSampleTab?: boolean | string; hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string; menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all';
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>; allowedMdComponents?: Dict<MDXComponentMeta>;
labels?: LabelsConfigRaw; labels?: LabelsConfigRaw;
enumSkipQuotes?: boolean | string;
} }
function argValueToBoolean(val?: string | boolean): boolean { function argValueToBoolean(val?: string | boolean): boolean {
@ -111,6 +113,16 @@ export class RedocNormalizedOptions {
return value; return value;
} }
private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number {
if (level === 'all') {
return +Infinity;
}
if (!isNaN(Number(level))) {
return Math.ceil(Number(level));
}
return 2;
}
theme: ResolvedThemeInterface; theme: ResolvedThemeInterface;
scrollYOffset: () => number; scrollYOffset: () => number;
hideHostname: boolean; hideHostname: boolean;
@ -127,6 +139,8 @@ export class RedocNormalizedOptions {
showExtensions: boolean | string[]; showExtensions: boolean | string[];
hideSingleRequestSampleTab: boolean; hideSingleRequestSampleTab: boolean;
menuToggle: boolean; menuToggle: boolean;
jsonSampleExpandLevel: number;
enumSkipQuotes: boolean;
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
@ -159,6 +173,10 @@ export class RedocNormalizedOptions {
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab); this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle); this.menuToggle = argValueToBoolean(raw.menuToggle);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
raw.jsonSampleExpandLevel,
);
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);

View File

@ -40,7 +40,14 @@ export class GroupModel implements IMenuItem {
this.type = type; this.type = type;
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;
// remove sections from markdown, same as in ApiInfo
this.description = tagOrGroup.description || ''; this.description = tagOrGroup.description || '';
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos);
}
this.parent = parent; this.parent = parent;
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs; this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;

View File

@ -13,6 +13,7 @@ import {
import { OpenAPIParser } from '../../services'; import { OpenAPIParser } from '../../services';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types'; import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => { describe('Utils', () => {
describe('openapi getStatusCode', () => { describe('openapi getStatusCode', () => {
@ -297,11 +298,8 @@ describe('Utils', () => {
it('should expand variables', () => { it('should expand variables', () => {
const servers = normalizeServers('', [ const servers = normalizeServers('', [
{ {
url: '{protocol}{host}{basePath}', url: 'http://{host}{basePath}',
variables: { variables: {
protocol: {
default: 'http://',
},
host: { host: {
default: '127.0.0.1', default: '127.0.0.1',
}, },
@ -319,9 +317,15 @@ describe('Utils', () => {
}, },
]); ]);
expect(servers[0].url).toEqual('http://127.0.0.1/path/to/endpoint'); expect(expandDefaultServerVariables(servers[0].url, servers[0].variables)).toEqual(
expect(servers[1].url).toEqual('http://127.0.0.2:{port}'); 'http://127.0.0.1/path/to/endpoint',
expect(servers[2].url).toEqual('http://127.0.0.3'); );
expect(expandDefaultServerVariables(servers[1].url, servers[1].variables)).toEqual(
'http://127.0.0.2:{port}',
);
expect(expandDefaultServerVariables(servers[2].url, servers[2].variables)).toEqual(
'http://127.0.0.3',
);
}); });
}); });

View File

@ -194,3 +194,7 @@ function parseURL(url: string) {
return new URL(url); return new URL(url);
} }
} }
export function unescapeHTMLChars(str: string): string {
return str.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)));
}

View File

@ -1,11 +1,10 @@
let level = 1; let level = 1;
const COLLAPSE_LEVEL = 2;
export function jsonToHTML(json) { export function jsonToHTML(json, maxExpandLevel) {
level = 1; level = 1;
let output = ''; let output = '';
output += '<div class="redoc-json">'; output += '<div class="redoc-json">';
output += valueToHTML(json); output += valueToHTML(json, maxExpandLevel);
output += '</div>'; output += '</div>';
return output; return output;
} }
@ -33,20 +32,20 @@ function punctuation(val) {
return '<span class="token punctuation">' + val + '</span>'; return '<span class="token punctuation">' + val + '</span>';
} }
function valueToHTML(value) { function valueToHTML(value, maxExpandLevel: number) {
const valueType = typeof value; const valueType = typeof value;
let output = ''; let output = '';
if (value === undefined || value === null) { if (value === undefined || value === null) {
output += decorateWithSpan('null', 'token keyword'); output += decorateWithSpan('null', 'token keyword');
} else if (value && value.constructor === Array) { } else if (value && value.constructor === Array) {
level++; level++;
output += arrayToHTML(value); output += arrayToHTML(value, maxExpandLevel);
level--; level--;
} else if (value && value.constructor === Date) { } else if (value && value.constructor === Date) {
output += decorateWithSpan('"' + value.toISOString() + '"', 'token string'); output += decorateWithSpan('"' + value.toISOString() + '"', 'token string');
} else if (valueType === 'object') { } else if (valueType === 'object') {
level++; level++;
output += objectToHTML(value); output += objectToHTML(value, maxExpandLevel);
level--; level--;
} else if (valueType === 'number') { } else if (valueType === 'number') {
output += decorateWithSpan(value, 'token number'); output += decorateWithSpan(value, 'token number');
@ -70,8 +69,8 @@ function valueToHTML(value) {
return output; return output;
} }
function arrayToHTML(json) { function arrayToHTML(json, maxExpandLevel: number) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : ''; const collapsed = level > maxExpandLevel ? 'collapsed' : '';
let output = `<div class="collapser"></div>${punctuation( let output = `<div class="collapser"></div>${punctuation(
'[', '[',
)}<span class="ellipsis"></span><ul class="array collapsible">`; )}<span class="ellipsis"></span><ul class="array collapsible">`;
@ -80,7 +79,7 @@ function arrayToHTML(json) {
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
hasContents = true; hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">'; output += '<li><div class="hoverable ' + collapsed + '">';
output += valueToHTML(json[i]); output += valueToHTML(json[i], maxExpandLevel);
if (i < length - 1) { if (i < length - 1) {
output += ','; output += ',';
} }
@ -93,8 +92,8 @@ function arrayToHTML(json) {
return output; return output;
} }
function objectToHTML(json) { function objectToHTML(json, maxExpandLevel: number) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : ''; const collapsed = level > maxExpandLevel ? 'collapsed' : '';
const keys = Object.keys(json); const keys = Object.keys(json);
const length = keys.length; const length = keys.length;
let output = `<div class="collapser"></div>${punctuation( let output = `<div class="collapser"></div>${punctuation(
@ -106,7 +105,7 @@ function objectToHTML(json) {
hasContents = true; hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">'; output += '<li><div class="hoverable ' + collapsed + '">';
output += '<span class="property token string">"' + htmlEncode(key) + '"</span>: '; output += '<span class="property token string">"' + htmlEncode(key) + '"</span>: ';
output += valueToHTML(json[key]); output += valueToHTML(json[key], maxExpandLevel);
if (i < length - 1) { if (i < length - 1) {
output += punctuation(','); output += punctuation(',');
} }

View File

@ -453,7 +453,7 @@ export function mergeSimilarMediaTypes(types: Dict<OpenAPIMediaType>): Dict<Open
return mergedTypes; return mergedTypes;
} }
function expandVariables(url: string, variables: object = {}) { export function expandDefaultServerVariables(url: string, variables: object = {}) {
return url.replace( return url.replace(
/(?:{)(\w+)(?:})/g, /(?:{)(\w+)(?:})/g,
(match, name) => (variables[name] && variables[name].default) || match, (match, name) => (variables[name] && variables[name].default) || match,
@ -482,21 +482,23 @@ export function normalizeServers(
]; ];
} }
function normalizeUrl(url: string, variables: object | undefined): string { function normalizeUrl(url: string): string {
url = expandVariables(url, variables);
return resolveUrl(baseUrl, url); return resolveUrl(baseUrl, url);
} }
return servers.map(server => { return servers.map(server => {
return { return {
...server, ...server,
url: normalizeUrl(server.url, server.variables), url: normalizeUrl(server.url),
description: server.description || '', description: server.description || '',
}; };
}); });
} }
export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions'; export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions';
export const SECURITY_DEFINITIONS_JSX_NAME = 'SecurityDefinitions';
export const SCHEMA_DEFINITION_JSX_NAME = 'ObjectDescription';
export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/'; export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/';
export function setSecuritySchemePrefix(prefix: string) { export function setSecuritySchemePrefix(prefix: string) {
SECURITY_SCHEMES_SECTION_PREFIX = prefix; SECURITY_SCHEMES_SECTION_PREFIX = prefix;