mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-17 02:10:39 +03:00
feat: menu items from tags + md extension for Schema Definition (#681)
* add section menus for tags and object description * bundle and test * add depth calculation * add object descriptions to test * enable operations spacing for operations as well * bring back section rule, as this could be solved better * update read/writeonly filter rule to be able to filter both * add showReadOnly and showWriteOnly options to object-description * update demo to show use cases * remove forgotten console.log * adjust demo test with newly added items * do the right match with the menu items :/ * chore: refactor + jsxify md tags * chore: simplify demo spec * fix: dropdown fixes related to object description
This commit is contained in:
commit
ac41f0bde5
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import styled from '../../styled-components';
|
||||||
|
|
||||||
import { DropdownProps } from '../../common-elements';
|
import { DropdownProps } from '../../common-elements';
|
||||||
import { MediaTypeModel } from '../../services/models';
|
import { MediaTypeModel } from '../../services/models';
|
||||||
import { Markdown } from '../Markdown/Markdown';
|
import { Markdown } from '../Markdown/Markdown';
|
||||||
|
@ -48,7 +50,7 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
|
||||||
const description = example.description;
|
const description = example.description;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SamplesWrapper>
|
||||||
<DropdownWrapper>
|
<DropdownWrapper>
|
||||||
<DropdownLabel>Example</DropdownLabel>
|
<DropdownLabel>Example</DropdownLabel>
|
||||||
{this.props.renderDropdown({
|
{this.props.renderDropdown({
|
||||||
|
@ -61,16 +63,20 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
|
||||||
{description && <Markdown source={description} />}
|
{description && <Markdown source={description} />}
|
||||||
<Example example={example} mimeType={mimeType} />
|
<Example example={example} mimeType={mimeType} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</SamplesWrapper>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const example = examples[examplesNames[0]];
|
const example = examples[examplesNames[0]];
|
||||||
return (
|
return (
|
||||||
<div>
|
<SamplesWrapper>
|
||||||
{example.description && <Markdown source={example.description} />}
|
{example.description && <Markdown source={example.description} />}
|
||||||
<Example example={example} mimeType={mimeType} />
|
<Example example={example} mimeType={mimeType} />
|
||||||
</div>
|
</SamplesWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SamplesWrapper = styled.div`
|
||||||
|
margin-top: 15px;
|
||||||
|
`;
|
||||||
|
|
|
@ -2,11 +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 styled from '../../../src/styled-components';
|
|
||||||
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 {
|
||||||
|
@ -24,13 +22,11 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
|
||||||
return (
|
return (
|
||||||
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
|
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
|
||||||
{mediaType => (
|
{mediaType => (
|
||||||
<SamplesWrapper>
|
<MediaTypeSamples
|
||||||
<MediaTypeSamples
|
key="samples"
|
||||||
key="samples"
|
mediaType={mediaType}
|
||||||
mediaType={mediaType}
|
renderDropdown={this.renderDropdown}
|
||||||
renderDropdown={this.renderDropdown}
|
/>
|
||||||
/>
|
|
||||||
</SamplesWrapper>
|
|
||||||
)}
|
)}
|
||||||
</MediaTypesSwitch>
|
</MediaTypesSwitch>
|
||||||
);
|
);
|
||||||
|
@ -40,7 +36,3 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
|
||||||
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
|
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SamplesWrapper = styled.div`
|
|
||||||
margin-top: 15px;
|
|
||||||
`;
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
|
||||||
}
|
}
|
||||||
.Dropdown-menu {
|
.Dropdown-menu {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 10px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
93
src/components/SchemaDefinition/SchemaDefinition.tsx
Normal file
93
src/components/SchemaDefinition/SchemaDefinition.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
|
@ -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,
|
||||||
|
}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -496,6 +496,9 @@ export function normalizeServers(
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user