Merge branch 'master' into external-docs

This commit is contained in:
Matthias Mohr 2018-08-08 16:51:34 +02:00 committed by GitHub
commit 964d5237b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 132 additions and 29 deletions

View File

@ -1,3 +1,36 @@
<a name="2.0.0-alpha.34"></a>
# [2.0.0-alpha.34](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.33...v2.0.0-alpha.34) (2018-08-08)
### Bug Fixes
* add some spacing between operation description and parameters ([597688e](https://github.com/Rebilly/ReDoc/commit/597688e))
* description is not rendered if doesn't containt markdown headings ([90ed717](https://github.com/Rebilly/ReDoc/commit/90ed717)), closes [#591](https://github.com/Rebilly/ReDoc/issues/591)
* download button downloads index.html instead of spec with CLI ([334f904](https://github.com/Rebilly/ReDoc/commit/334f904)), closes [#594](https://github.com/Rebilly/ReDoc/issues/594)
* fix Authentication section is not rendered ([2ecc8bc](https://github.com/Rebilly/ReDoc/commit/2ecc8bc)), closes [#590](https://github.com/Rebilly/ReDoc/issues/590)
* fix linebreaks in multiparagraph field descriptions ([8fb9cd6](https://github.com/Rebilly/ReDoc/commit/8fb9cd6))
* preserve md heading level in description ([23559fb](https://github.com/Rebilly/ReDoc/commit/23559fb))
* render additionalProperties set to true ([#597](https://github.com/Rebilly/ReDoc/issues/597)) ([f70ac08](https://github.com/Rebilly/ReDoc/commit/f70ac08)), closes [#596](https://github.com/Rebilly/ReDoc/issues/596)
* schemes without type: object are not expandable ([97e1620](https://github.com/Rebilly/ReDoc/commit/97e1620)), closes [#599](https://github.com/Rebilly/ReDoc/issues/599)
### Features
* Add x-logo alt text support ([#584](https://github.com/Rebilly/ReDoc/issues/584)) ([568ce74](https://github.com/Rebilly/ReDoc/commit/568ce74)), closes [#546](https://github.com/Rebilly/ReDoc/issues/546)
* support label for x-code-samples ([00bd966](https://github.com/Rebilly/ReDoc/commit/00bd966)), closes [#586](https://github.com/Rebilly/ReDoc/issues/586)
<a name="2.0.0-alpha.33"></a>
# [2.0.0-alpha.33](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.32...v2.0.0-alpha.33) (2018-07-31)
### Bug Fixes
* long endpoint url overflow ([d99e918](https://github.com/Rebilly/ReDoc/commit/d99e918))
* allow word-break in code strings in md ([15dfe44](https://github.com/Rebilly/ReDoc/commit/15dfe44))
* show examples for response headers ([ba22b1e](https://github.com/Rebilly/ReDoc/commit/ba22b1e))
<a name="2.0.0-alpha.32"></a> <a name="2.0.0-alpha.32"></a>
# [2.0.0-alpha.32](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.31...v2.0.0-alpha.32) (2018-07-26) # [2.0.0-alpha.32](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.31...v2.0.0-alpha.32) (2018-07-26)

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-alpha.33", "version": "2.0.0-alpha.34",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -82,7 +82,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
<DownloadButton <DownloadButton
download={downloadFilename} download={downloadFilename}
target="_blank" target="_blank"
href={downloadLink || '#'} href={downloadLink}
onClick={this.handleDownloadClick} onClick={this.handleDownloadClick}
> >
Download Download

View File

@ -20,6 +20,7 @@ export const DownloadButton = styled.a`
padding: 4px 8px 4px; padding: 4px 8px 4px;
display: inline-block; display: inline-block;
text-decoration: none; text-decoration: none;
cursor: pointer;
${extensionsHook('DownloadButton')}; ${extensionsHook('DownloadButton')};
`; `;

View File

@ -5,10 +5,10 @@ import { SECTION_ATTR } from '../../services/MenuStore';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { H1, MiddlePanel, Row, ShareLink } from '../../common-elements'; import { H1, H2, MiddlePanel, Row, ShareLink } from '../../common-elements';
import { MDXComponentMeta } from '../../services/MarkdownRenderer'; import { MDXComponentMeta } from '../../services/MarkdownRenderer';
import { ContentItemModel } from '../../services/MenuBuilder'; import { ContentItemModel } from '../../services/MenuBuilder';
import { OperationModel } from '../../services/models'; import { GroupModel, OperationModel } from '../../services/models';
import { Operation } from '../Operation/Operation'; import { Operation } from '../Operation/Operation';
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
import { StoreConsumer } from '../StoreBuilder'; import { StoreConsumer } from '../StoreBuilder';
@ -80,23 +80,24 @@ export class ContentItem extends React.Component<ContentItemProps> {
@observer @observer
export class SectionItem extends React.Component<ContentItemProps> { export class SectionItem extends React.Component<ContentItemProps> {
render() { render() {
const { name, description, externalDocs } = this.props.item; const { name, description, externalDocs, level } = this.props.item as GroupModel;
const components = this.props.allowedMdComponents; const components = this.props.allowedMdComponents;
const Header = level === 2 ? H2 : H1;
return ( return (
<Row> <Row>
<MiddlePanel> <MiddlePanel>
<H1> <Header>
<ShareLink href={'#' + this.props.item.id} /> <ShareLink href={'#' + this.props.item.id} />
{name} {name}
</H1> </Header>
{components ? ( {components ? (
<Markdown source={description || ''} />
) : (
<StoreConsumer> <StoreConsumer>
{store => ( {store => (
<Markdown source={description || ''} allowedComponents={components} store={store} /> <Markdown source={description || ''} allowedComponents={components} store={store} />
)} )}
</StoreConsumer> </StoreConsumer>
) : (
<Markdown source={description || ''} />
)} )}
{externalDocs && ( {externalDocs && (
<p> <p>

View File

@ -26,16 +26,21 @@ export const StyledMarkdownBlock = withProps<{ dense?: boolean; inline?: boolean
line-height: ${props => props.theme.typography.lineHeight}; line-height: ${props => props.theme.typography.lineHeight};
p { p {
&:last-of-type { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
${({ dense }) => ${({ dense }) =>
dense && dense &&
` p { `
margin: 0; p:first-child {
}`} margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
`}
${({ inline }) => ${({ inline }) =>
inline && inline &&
@ -108,9 +113,9 @@ export const StyledMarkdownBlock = withProps<{ dense?: boolean; inline?: boolean
padding-left: 2em; padding-left: 2em;
margin: 0; margin: 0;
margin-bottom: 1em; margin-bottom: 1em;
> li { // > li {
margin: 1em 0; // margin: 0.5em 0;
} // }
} }
table { table {

View File

@ -17,6 +17,7 @@ import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { OperationModel as OperationType } from '../../services/models'; import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components';
const OperationRow = Row.extend` const OperationRow = Row.extend`
backface-visibility: hidden; backface-visibility: hidden;
@ -35,6 +36,10 @@ const OperationRow = Row.extend`
} }
`; `;
const Description = styled(Markdown)`
margin-bottom: ${({ theme }) => theme.spacing.unit * 8};
`;
export interface OperationProps { export interface OperationProps {
operation: OperationType; operation: OperationType;
} }
@ -55,7 +60,7 @@ 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 && <Markdown source={description} />} {description !== undefined && <Description source={description} />}
{externalDocs && ( {externalDocs && (
<p> <p>
<ExternalDocumentation externalDocs={externalDocs} /> <ExternalDocumentation externalDocs={externalDocs} />

View File

@ -25,6 +25,7 @@ export interface MDXComponentMeta {
export interface MarkdownHeading { export interface MarkdownHeading {
id: string; id: string;
name: string; name: string;
level: number;
items?: MarkdownHeading[]; items?: MarkdownHeading[];
description?: string; description?: string;
} }
@ -50,12 +51,14 @@ export class MarkdownRenderer {
saveHeading( saveHeading(
name: string, name: string,
level: number,
container: MarkdownHeading[] = this.headings, container: MarkdownHeading[] = this.headings,
parentId?: string, parentId?: string,
): MarkdownHeading { ): MarkdownHeading {
const item = { const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`, id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
name, name,
level,
items: [], items: [],
}; };
container.push(item); container.push(item);
@ -105,10 +108,11 @@ export class MarkdownRenderer {
headingRule = (text: string, level: number, raw: string) => { headingRule = (text: string, level: number, raw: string) => {
if (level === 1) { if (level === 1) {
this.currentTopHeading = this.saveHeading(text); this.currentTopHeading = this.saveHeading(text, level);
} else if (level === 2) { } else if (level === 2) {
this.saveHeading( this.saveHeading(
text, text,
level,
this.currentTopHeading && this.currentTopHeading.items, this.currentTopHeading && this.currentTopHeading.items,
this.currentTopHeading && this.currentTopHeading.id, this.currentTopHeading && this.currentTopHeading.id,
); );

View File

@ -76,11 +76,11 @@ export class OpenAPIParser {
const description = spec.info.description || ''; const description = spec.info.description || '';
const legacySecurityRegexp = new RegExp( const legacySecurityRegexp = new RegExp(
COMPONENT_REGEXP.replace('{component}', '<security-definitions>'), COMPONENT_REGEXP.replace('{component}', '<security-definitions>'),
'gmi', 'mi',
); );
const securityRegexp = new RegExp( const securityRegexp = new RegExp(
MDX_COMPONENT_REGEXP.replace('{component}', 'security-definitions'), MDX_COMPONENT_REGEXP.replace('{component}', 'security-definitions'),
'gmi', 'mi',
); );
if (!legacySecurityRegexp.test(description) && !securityRegexp.test(description)) { if (!legacySecurityRegexp.test(description) && !securityRegexp.test(description)) {
const comment = buildComponentComment('security-definitions'); const comment = buildComponentComment('security-definitions');

View File

@ -0,0 +1,38 @@
import { ApiInfoModel } from '../../models/ApiInfo';
import { OpenAPIParser } from '../../OpenAPIParser';
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
const opts = new RedocNormalizedOptions({});
describe('Models', () => {
describe('ResponseModel', () => {
let parser: OpenAPIParser;
beforeEach(() => {
parser = new OpenAPIParser({ openapi: '3.0.0' } as any, undefined, opts);
});
test('should correctly populate description field without md headings', () => {
parser.spec = {
openapi: '3.0.0',
info: {
description: 'Test description',
},
} as any;
const info = new ApiInfoModel(parser);
expect(info.description).toEqual('Test description');
});
test('should correctly populate description up to the first md heading', () => {
parser.spec = {
openapi: '3.0.0',
info: {
description: 'Test description\nsome text\n## Heading\n test',
},
} as any;
const info = new ApiInfoModel(parser);
expect(info.description).toEqual('Test description\nsome text\n');
});
});
});

View File

@ -23,12 +23,12 @@ describe('Models', () => {
}); });
test('default should be sucessful by default', () => { test('default should be sucessful by default', () => {
let resp = new ResponseModel(parser, 'default', false, {}, opts); const resp = new ResponseModel(parser, 'default', false, {}, opts);
expect(resp.type).toEqual('success'); expect(resp.type).toEqual('success');
}); });
test('default should be error if defaultAsError is true', () => { test('default should be error if defaultAsError is true', () => {
let resp = new ResponseModel(parser, 'default', true, {}, opts); const resp = new ResponseModel(parser, 'default', true, {}, opts);
expect(resp.type).toEqual('error'); expect(resp.type).toEqual('error');
}); });
}); });

View File

@ -14,7 +14,10 @@ export class ApiInfoModel implements OpenAPIInfo {
constructor(private parser: OpenAPIParser) { constructor(private parser: OpenAPIParser) {
Object.assign(this, parser.spec.info); Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || ''; this.description = parser.spec.info.description || '';
this.description = this.description.substring(0, this.description.search(/^##?\s+/m)); const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos);
}
} }
get downloadLink(): string | undefined { get downloadLink(): string | undefined {

View File

@ -25,6 +25,7 @@ export class GroupModel implements IMenuItem {
@observable expanded: boolean = false; @observable expanded: boolean = false;
depth: number; depth: number;
level: number;
//#endregion //#endregion
constructor( constructor(
@ -36,6 +37,7 @@ export class GroupModel implements IMenuItem {
this.id = (tagOrGroup as MarkdownHeading).id || type + '/' + safeSlugify(tagOrGroup.name); this.id = (tagOrGroup as MarkdownHeading).id || type + '/' + safeSlugify(tagOrGroup.name);
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.description = tagOrGroup.description || ''; this.description = tagOrGroup.description || '';
this.parent = parent; this.parent = parent;
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs; this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;

View File

@ -106,7 +106,7 @@ export class SchemaModel {
this.constraints = humanizeConstraints(schema); this.constraints = humanizeConstraints(schema);
this.displayType = this.type; this.displayType = this.type;
this.displayFormat = this.format; this.displayFormat = this.format;
this.isPrimitive = isPrimitiveType(schema); this.isPrimitive = isPrimitiveType(schema, this.type);
this.default = schema.default; this.default = schema.default;
this.readOnly = !!schema.readOnly; this.readOnly = !!schema.readOnly;
this.writeOnly = !!schema.writeOnly; this.writeOnly = !!schema.writeOnly;
@ -246,14 +246,14 @@ function buildFields(
sortByRequired(fields, schema.required); sortByRequired(fields, schema.required);
} }
if (typeof additionalProps === 'object') { if (typeof additionalProps === 'object' || additionalProps === true) {
fields.push( fields.push(
new FieldModel( new FieldModel(
parser, parser,
{ {
name: 'property name *', name: 'property name *',
required: false, required: false,
schema: additionalProps, schema: additionalProps === true ? {} : additionalProps,
kind: 'additionalProperties', kind: 'additionalProperties',
}, },
$ref + '/additionalProperties', $ref + '/additionalProperties',

View File

@ -187,6 +187,17 @@ describe('Utils', () => {
}; };
expect(isPrimitiveType(schema)).toEqual(false); expect(isPrimitiveType(schema)).toEqual(false);
}); });
it('should work with externally provided type', () => {
const schema = {
properties: {
a: {
type: 'string',
},
},
};
expect(isPrimitiveType(schema, 'object')).toEqual(false);
});
}); });
describe('openapi mergeParams', () => { describe('openapi mergeParams', () => {

View File

@ -105,18 +105,18 @@ export function detectType(schema: OpenAPISchema): string {
return 'any'; return 'any';
} }
export function isPrimitiveType(schema: OpenAPISchema) { export function isPrimitiveType(schema: OpenAPISchema, type: string | undefined = schema.type) {
if (schema.oneOf !== undefined || schema.anyOf !== undefined) { if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
return false; return false;
} }
if (schema.type === 'object') { if (type === 'object') {
return schema.properties !== undefined return schema.properties !== undefined
? Object.keys(schema.properties).length === 0 ? Object.keys(schema.properties).length === 0
: schema.additionalProperties === undefined; : schema.additionalProperties === undefined;
} }
if (schema.type === 'array') { if (type === 'array') {
if (schema.items === undefined) { if (schema.items === undefined) {
return true; return true;
} }