diff --git a/CHANGELOG.md b/CHANGELOG.md index 219e12d5..5483f808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ + +# [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) + + + + +# [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)) + # [2.0.0-alpha.32](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.31...v2.0.0-alpha.32) (2018-07-26) diff --git a/package.json b/package.json index 8520a27e..b111e0db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redoc", - "version": "2.0.0-alpha.33", + "version": "2.0.0-alpha.34", "description": "ReDoc", "repository": { "type": "git", diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 9728410d..ef5c43ec 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -82,7 +82,7 @@ export class ApiInfo extends React.Component { Download diff --git a/src/components/ApiInfo/styled.elements.ts b/src/components/ApiInfo/styled.elements.ts index 445eb4b3..c9bc0598 100644 --- a/src/components/ApiInfo/styled.elements.ts +++ b/src/components/ApiInfo/styled.elements.ts @@ -20,6 +20,7 @@ export const DownloadButton = styled.a` padding: 4px 8px 4px; display: inline-block; text-decoration: none; + cursor: pointer; ${extensionsHook('DownloadButton')}; `; diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 028398b5..8258ccf8 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -5,10 +5,10 @@ import { SECTION_ATTR } from '../../services/MenuStore'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; 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 { ContentItemModel } from '../../services/MenuBuilder'; -import { OperationModel } from '../../services/models'; +import { GroupModel, OperationModel } from '../../services/models'; import { Operation } from '../Operation/Operation'; import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; import { StoreConsumer } from '../StoreBuilder'; @@ -80,23 +80,24 @@ export class ContentItem extends React.Component { @observer export class SectionItem extends React.Component { render() { - const { name, description, externalDocs } = this.props.item; + const { name, description, externalDocs, level } = this.props.item as GroupModel; const components = this.props.allowedMdComponents; + const Header = level === 2 ? H2 : H1; return ( -

+
{name} -

+ {components ? ( - - ) : ( {store => ( )} + ) : ( + )} {externalDocs && (

diff --git a/src/components/Markdown/styled.elements.ts b/src/components/Markdown/styled.elements.ts index 249f2ecd..512b4531 100644 --- a/src/components/Markdown/styled.elements.ts +++ b/src/components/Markdown/styled.elements.ts @@ -26,16 +26,21 @@ export const StyledMarkdownBlock = withProps<{ dense?: boolean; inline?: boolean line-height: ${props => props.theme.typography.lineHeight}; p { - &:last-of-type { + &:last-child { margin-bottom: 0; } } ${({ dense }) => dense && - ` p { - margin: 0; - }`} + ` + p:first-child { + margin-top: 0; + } + p:last-child { + margin-bottom: 0; + } + `} ${({ inline }) => inline && @@ -108,9 +113,9 @@ export const StyledMarkdownBlock = withProps<{ dense?: boolean; inline?: boolean padding-left: 2em; margin: 0; margin-bottom: 1em; - > li { - margin: 1em 0; - } + // > li { + // margin: 0.5em 0; + // } } table { diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 2df45087..6aaffa99 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -17,6 +17,7 @@ import { ResponsesList } from '../Responses/ResponsesList'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { OperationModel as OperationType } from '../../services/models'; +import styled from '../../styled-components'; const OperationRow = Row.extend` 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 { operation: OperationType; } @@ -55,7 +60,7 @@ export class Operation extends React.Component { {summary} {deprecated && Deprecated } {options.pathInMiddlePanel && } - {description !== undefined && } + {description !== undefined && } {externalDocs && (

diff --git a/src/services/MarkdownRenderer.ts b/src/services/MarkdownRenderer.ts index ab2835c6..95afec75 100644 --- a/src/services/MarkdownRenderer.ts +++ b/src/services/MarkdownRenderer.ts @@ -25,6 +25,7 @@ export interface MDXComponentMeta { export interface MarkdownHeading { id: string; name: string; + level: number; items?: MarkdownHeading[]; description?: string; } @@ -50,12 +51,14 @@ export class MarkdownRenderer { saveHeading( name: string, + level: number, container: MarkdownHeading[] = this.headings, parentId?: string, ): MarkdownHeading { const item = { id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`, name, + level, items: [], }; container.push(item); @@ -105,10 +108,11 @@ export class MarkdownRenderer { headingRule = (text: string, level: number, raw: string) => { if (level === 1) { - this.currentTopHeading = this.saveHeading(text); + this.currentTopHeading = this.saveHeading(text, level); } else if (level === 2) { this.saveHeading( text, + level, this.currentTopHeading && this.currentTopHeading.items, this.currentTopHeading && this.currentTopHeading.id, ); diff --git a/src/services/OpenAPIParser.ts b/src/services/OpenAPIParser.ts index 8037705a..05cdf77d 100644 --- a/src/services/OpenAPIParser.ts +++ b/src/services/OpenAPIParser.ts @@ -76,11 +76,11 @@ export class OpenAPIParser { const description = spec.info.description || ''; const legacySecurityRegexp = new RegExp( COMPONENT_REGEXP.replace('{component}', ''), - 'gmi', + 'mi', ); const securityRegexp = new RegExp( MDX_COMPONENT_REGEXP.replace('{component}', 'security-definitions'), - 'gmi', + 'mi', ); if (!legacySecurityRegexp.test(description) && !securityRegexp.test(description)) { const comment = buildComponentComment('security-definitions'); diff --git a/src/services/__tests__/models/ApiInfo.test.ts b/src/services/__tests__/models/ApiInfo.test.ts new file mode 100644 index 00000000..5adb6036 --- /dev/null +++ b/src/services/__tests__/models/ApiInfo.test.ts @@ -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'); + }); + }); +}); diff --git a/src/services/__tests__/models/Response.test.ts b/src/services/__tests__/models/Response.test.ts index 77b861bb..bfd2f6f9 100644 --- a/src/services/__tests__/models/Response.test.ts +++ b/src/services/__tests__/models/Response.test.ts @@ -23,12 +23,12 @@ describe('Models', () => { }); 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'); }); 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'); }); }); diff --git a/src/services/models/ApiInfo.ts b/src/services/models/ApiInfo.ts index 29455012..ceb38d00 100644 --- a/src/services/models/ApiInfo.ts +++ b/src/services/models/ApiInfo.ts @@ -14,7 +14,10 @@ export class ApiInfoModel implements OpenAPIInfo { constructor(private parser: OpenAPIParser) { Object.assign(this, parser.spec.info); 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 { diff --git a/src/services/models/Group.model.ts b/src/services/models/Group.model.ts index 52d9108b..34f2b73c 100644 --- a/src/services/models/Group.model.ts +++ b/src/services/models/Group.model.ts @@ -25,6 +25,7 @@ export class GroupModel implements IMenuItem { @observable expanded: boolean = false; depth: number; + level: number; //#endregion constructor( @@ -36,6 +37,7 @@ export class GroupModel implements IMenuItem { this.id = (tagOrGroup as MarkdownHeading).id || type + '/' + safeSlugify(tagOrGroup.name); this.type = type; this.name = tagOrGroup['x-displayName'] || tagOrGroup.name; + this.level = (tagOrGroup as MarkdownHeading).level || 1; this.description = tagOrGroup.description || ''; this.parent = parent; this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs; diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts index 0a5a9319..576a7475 100644 --- a/src/services/models/Schema.ts +++ b/src/services/models/Schema.ts @@ -106,7 +106,7 @@ export class SchemaModel { this.constraints = humanizeConstraints(schema); this.displayType = this.type; this.displayFormat = this.format; - this.isPrimitive = isPrimitiveType(schema); + this.isPrimitive = isPrimitiveType(schema, this.type); this.default = schema.default; this.readOnly = !!schema.readOnly; this.writeOnly = !!schema.writeOnly; @@ -246,14 +246,14 @@ function buildFields( sortByRequired(fields, schema.required); } - if (typeof additionalProps === 'object') { + if (typeof additionalProps === 'object' || additionalProps === true) { fields.push( new FieldModel( parser, { name: 'property name *', required: false, - schema: additionalProps, + schema: additionalProps === true ? {} : additionalProps, kind: 'additionalProperties', }, $ref + '/additionalProperties', diff --git a/src/utils/__tests__/openapi.test.ts b/src/utils/__tests__/openapi.test.ts index 9e61c908..34e847d2 100644 --- a/src/utils/__tests__/openapi.test.ts +++ b/src/utils/__tests__/openapi.test.ts @@ -187,6 +187,17 @@ describe('Utils', () => { }; 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', () => { diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 2aecb645..080d010f 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -105,18 +105,18 @@ export function detectType(schema: OpenAPISchema): string { return 'any'; } -export function isPrimitiveType(schema: OpenAPISchema) { +export function isPrimitiveType(schema: OpenAPISchema, type: string | undefined = schema.type) { if (schema.oneOf !== undefined || schema.anyOf !== undefined) { return false; } - if (schema.type === 'object') { + if (type === 'object') { return schema.properties !== undefined ? Object.keys(schema.properties).length === 0 : schema.additionalProperties === undefined; } - if (schema.type === 'array') { + if (type === 'array') { if (schema.items === undefined) { return true; }