mirror of
https://github.com/Redocly/redoc.git
synced 2025-01-31 01:54:08 +03:00
feat: externalDocumentation rendered for tags, operations and schema fields (#595)
The externalDocs were missing for tags, schemata and operations. Added them with this pull requests. Solves #550. Additionally, fixes that the URL in External Documentation Object was specified to be optional, which is not correct according to OpenAPI spec.
This commit is contained in:
parent
4b3b5ba974
commit
893c83ed07
|
@ -767,6 +767,9 @@ components:
|
||||||
bee: '#/components/schemas/HoneyBee'
|
bee: '#/components/schemas/HoneyBee'
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
externalDocs:
|
||||||
|
description: "Find more info here"
|
||||||
|
url: "https://example.com"
|
||||||
description: Pet ID
|
description: Pet ID
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Id'
|
- $ref: '#/components/schemas/Id'
|
||||||
|
|
|
@ -4,6 +4,7 @@ import * as React from 'react';
|
||||||
import { AppStore } from '../../services/AppStore';
|
import { AppStore } from '../../services/AppStore';
|
||||||
|
|
||||||
import { MiddlePanel, Row, Section } from '../../common-elements/';
|
import { MiddlePanel, Row, Section } from '../../common-elements/';
|
||||||
|
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
|
||||||
import { Markdown } from '../Markdown/Markdown';
|
import { Markdown } from '../Markdown/Markdown';
|
||||||
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
|
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
|
||||||
import {
|
import {
|
||||||
|
@ -98,15 +99,9 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
||||||
</InfoSpanBoxWrap>
|
</InfoSpanBoxWrap>
|
||||||
)) ||
|
)) ||
|
||||||
null}
|
null}
|
||||||
|
|
||||||
{(externalDocs && (
|
|
||||||
<p>
|
|
||||||
<a href={externalDocs.url}>{externalDocs.description || externalDocs.url}</a>
|
|
||||||
</p>
|
|
||||||
)) ||
|
|
||||||
null}
|
|
||||||
</StyledMarkdownBlock>
|
</StyledMarkdownBlock>
|
||||||
<Markdown source={store.spec.info.description} />
|
<Markdown source={store.spec.info.description} />
|
||||||
|
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
|
||||||
</MiddlePanel>
|
</MiddlePanel>
|
||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
|
||||||
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
|
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
|
||||||
|
|
||||||
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
|
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
|
||||||
|
@ -64,7 +65,7 @@ const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
|
||||||
@observer
|
@observer
|
||||||
export class SectionItem extends React.Component<ContentItemProps> {
|
export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
render() {
|
render() {
|
||||||
const { name, description, level } = this.props.item as GroupModel;
|
const { name, description, externalDocs, level } = this.props.item as GroupModel;
|
||||||
|
|
||||||
const Header = level === 2 ? H2 : H1;
|
const Header = level === 2 ? H2 : H1;
|
||||||
return (
|
return (
|
||||||
|
@ -78,6 +79,13 @@ export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
</MiddlePanel>
|
</MiddlePanel>
|
||||||
</Row>
|
</Row>
|
||||||
<AdvancedMarkdown source={description || ''} htmlWrap={middlePanelWrap} />
|
<AdvancedMarkdown source={description || ''} htmlWrap={middlePanelWrap} />
|
||||||
|
{externalDocs && (
|
||||||
|
<Row>
|
||||||
|
<MiddlePanel>
|
||||||
|
<ExternalDocumentation externalDocs={externalDocs} />
|
||||||
|
</MiddlePanel>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import styled, { withProps } from '../../styled-components';
|
||||||
|
import { OpenAPIExternalDocumentation } from '../../types';
|
||||||
|
import { linksCss } from '../Markdown/styled.elements';
|
||||||
|
|
||||||
|
const LinkWrap = withProps<{ compact?: boolean }>(styled.div)`
|
||||||
|
${linksCss};
|
||||||
|
${({ compact }) => (!compact ? 'margin: 1em 0' : '')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ExternalDocumentation extends React.Component<{
|
||||||
|
externalDocs: OpenAPIExternalDocumentation;
|
||||||
|
compact?: boolean;
|
||||||
|
}> {
|
||||||
|
render() {
|
||||||
|
const { externalDocs } = this.props;
|
||||||
|
if (!externalDocs || !externalDocs.url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkWrap compact={this.props.compact}>
|
||||||
|
<a href={externalDocs.url}>{externalDocs.description || externalDocs.url}</a>
|
||||||
|
</LinkWrap>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import {
|
||||||
TypePrefix,
|
TypePrefix,
|
||||||
TypeTitle,
|
TypeTitle,
|
||||||
} from '../../common-elements/fields';
|
} from '../../common-elements/fields';
|
||||||
|
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
|
||||||
import { Markdown } from '../Markdown/Markdown';
|
import { Markdown } from '../Markdown/Markdown';
|
||||||
import { EnumValues } from './EnumValues';
|
import { EnumValues } from './EnumValues';
|
||||||
import { FieldProps } from './Field';
|
import { FieldProps } from './Field';
|
||||||
|
@ -51,8 +52,11 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
|
||||||
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
|
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
|
||||||
{showExamples && <FieldDetail label={'Example:'} value={example} />}
|
{showExamples && <FieldDetail label={'Example:'} value={example} />}
|
||||||
<div>
|
<div>
|
||||||
<Markdown dense={true} source={description} />
|
<Markdown compact={true} source={description} />
|
||||||
</div>
|
</div>
|
||||||
|
{schema.externalDocs && (
|
||||||
|
<ExternalDocumentation externalDocs={schema.externalDocs} compact={true} />
|
||||||
|
)}
|
||||||
{(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null}
|
{(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -38,7 +38,7 @@ export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
|
||||||
return parts.map((part, idx) => {
|
return parts.map((part, idx) => {
|
||||||
if (typeof part === 'string') {
|
if (typeof part === 'string') {
|
||||||
return React.cloneElement(
|
return React.cloneElement(
|
||||||
htmlWrap(<SanitizedMarkdownHTML html={part} inline={false} dense={false} />),
|
htmlWrap(<SanitizedMarkdownHTML html={part} inline={false} compact={false} />),
|
||||||
{ key: idx },
|
{ key: idx },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { MarkdownRenderer } from '../../services';
|
||||||
import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
|
import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
|
||||||
|
|
||||||
export interface StylingMarkdownProps {
|
export interface StylingMarkdownProps {
|
||||||
dense?: boolean;
|
compact?: boolean;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,13 +21,13 @@ export type MarkdownProps = BaseMarkdownProps &
|
||||||
|
|
||||||
export class Markdown extends React.Component<MarkdownProps> {
|
export class Markdown extends React.Component<MarkdownProps> {
|
||||||
render() {
|
render() {
|
||||||
const { source, inline, dense, className } = this.props;
|
const { source, inline, compact, className } = this.props;
|
||||||
const renderer = new MarkdownRenderer();
|
const renderer = new MarkdownRenderer();
|
||||||
return (
|
return (
|
||||||
<SanitizedMarkdownHTML
|
<SanitizedMarkdownHTML
|
||||||
html={renderer.renderMd(source)}
|
html={renderer.renderMd(source)}
|
||||||
inline={inline}
|
inline={inline}
|
||||||
dense={dense}
|
compact={compact}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { OptionsContext } from '../OptionsProvider';
|
||||||
|
|
||||||
import { ShareLink } from '../../common-elements/linkify';
|
import { ShareLink } from '../../common-elements/linkify';
|
||||||
import { Endpoint } from '../Endpoint/Endpoint';
|
import { Endpoint } from '../Endpoint/Endpoint';
|
||||||
|
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
|
||||||
import { Markdown } from '../Markdown/Markdown';
|
import { Markdown } from '../Markdown/Markdown';
|
||||||
import { Parameters } from '../Parameters/Parameters';
|
import { Parameters } from '../Parameters/Parameters';
|
||||||
import { RequestSamples } from '../RequestSamples/RequestSamples';
|
import { RequestSamples } from '../RequestSamples/RequestSamples';
|
||||||
|
@ -25,7 +26,7 @@ const OperationRow = styled(Row)`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Description = styled(Markdown)`
|
const Description = styled.div`
|
||||||
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
|
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -38,7 +39,9 @@ export class Operation extends React.Component<OperationProps> {
|
||||||
render() {
|
render() {
|
||||||
const { operation } = this.props;
|
const { operation } = this.props;
|
||||||
|
|
||||||
const { name: summary, description, deprecated } = operation;
|
const { name: summary, description, deprecated, externalDocs } = operation;
|
||||||
|
const hasDescription = !!(description || externalDocs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsContext.Consumer>
|
<OptionsContext.Consumer>
|
||||||
{options => (
|
{options => (
|
||||||
|
@ -49,7 +52,12 @@ export class Operation extends React.Component<OperationProps> {
|
||||||
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
|
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
|
||||||
</H2>
|
</H2>
|
||||||
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
|
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
|
||||||
{description !== undefined && <Description source={description} />}
|
{hasDescription && (
|
||||||
|
<Description>
|
||||||
|
{description !== undefined && <Markdown source={description} />}
|
||||||
|
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
|
||||||
|
</Description>
|
||||||
|
)}
|
||||||
<SecurityRequirements securities={operation.security} />
|
<SecurityRequirements securities={operation.security} />
|
||||||
<Parameters parameters={operation.parameters} body={operation.requestBody} />
|
<Parameters parameters={operation.parameters} body={operation.requestBody} />
|
||||||
<ResponsesList responses={operation.responses} />
|
<ResponsesList responses={operation.responses} />
|
||||||
|
|
|
@ -27,7 +27,7 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<strong>{code} </strong>
|
<strong>{code} </strong>
|
||||||
<Markdown dense={true} inline={true} source={title} />
|
<Markdown compact={true} inline={true} source={title} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
|
||||||
name: '',
|
name: '',
|
||||||
required: false,
|
required: false,
|
||||||
description: schema.description,
|
description: schema.description,
|
||||||
|
externalDocs: schema.externalDocs,
|
||||||
deprecated: false,
|
deprecated: false,
|
||||||
toggle: () => null,
|
toggle: () => null,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
|
|
|
@ -24,6 +24,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
||||||
"displayType": "number",
|
"displayType": "number",
|
||||||
"enum": Array [],
|
"enum": Array [],
|
||||||
"example": undefined,
|
"example": undefined,
|
||||||
|
"externalDocs": undefined,
|
||||||
"format": undefined,
|
"format": undefined,
|
||||||
"isCircular": undefined,
|
"isCircular": undefined,
|
||||||
"isPrimitive": true,
|
"isPrimitive": true,
|
||||||
|
@ -72,6 +73,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
||||||
"displayType": "string",
|
"displayType": "string",
|
||||||
"enum": Array [],
|
"enum": Array [],
|
||||||
"example": undefined,
|
"example": undefined,
|
||||||
|
"externalDocs": undefined,
|
||||||
"format": undefined,
|
"format": undefined,
|
||||||
"isCircular": undefined,
|
"isCircular": undefined,
|
||||||
"isPrimitive": true,
|
"isPrimitive": true,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { action, observable } from 'mobx';
|
import { action, observable } from 'mobx';
|
||||||
|
|
||||||
import { OpenAPISchema, Referenced } from '../../types';
|
import { OpenAPIExternalDocumentation, OpenAPISchema, Referenced } from '../../types';
|
||||||
|
|
||||||
import { OpenAPIParser } from '../OpenAPIParser';
|
import { OpenAPIParser } from '../OpenAPIParser';
|
||||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||||
|
@ -25,6 +25,7 @@ export class SchemaModel {
|
||||||
typePrefix: string = '';
|
typePrefix: string = '';
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
externalDocs?: OpenAPIExternalDocumentation;
|
||||||
|
|
||||||
isPrimitive: boolean;
|
isPrimitive: boolean;
|
||||||
isCircular: boolean = false;
|
isCircular: boolean = false;
|
||||||
|
@ -101,6 +102,7 @@ export class SchemaModel {
|
||||||
this.example = schema.example;
|
this.example = schema.example;
|
||||||
this.deprecated = !!schema.deprecated;
|
this.deprecated = !!schema.deprecated;
|
||||||
this.pattern = schema.pattern;
|
this.pattern = schema.pattern;
|
||||||
|
this.externalDocs = schema.externalDocs;
|
||||||
|
|
||||||
this.constraints = humanizeConstraints(schema);
|
this.constraints = humanizeConstraints(schema);
|
||||||
this.displayType = this.type;
|
this.displayType = this.type;
|
||||||
|
|
2
src/types/open-api.d.ts
vendored
2
src/types/open-api.d.ts
vendored
|
@ -255,7 +255,7 @@ export interface OpenAPITag {
|
||||||
|
|
||||||
export interface OpenAPIExternalDocumentation {
|
export interface OpenAPIExternalDocumentation {
|
||||||
description?: string;
|
description?: string;
|
||||||
url?: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAPIContact {
|
export interface OpenAPIContact {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user