Skip to content

Commit ac41f0b

Browse files
authored
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
2 parents e4c3af6 + 9bf45d8 commit ac41f0b

File tree

11 files changed

+183
-31
lines changed

11 files changed

+183
-31
lines changed

demo/openapi.yaml

+25-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ info:
3838
OAuth2 - an open protocol to allow secure authorization in a simple
3939
and standard method from web, mobile and desktop applications.
4040
41-
<security-definitions />
41+
<SecurityDefinitions />
4242
4343
version: 1.0.0
4444
title: Swagger Petstore
@@ -63,6 +63,14 @@ tags:
6363
description: Access to Petstore orders
6464
- name: user
6565
description: Operations about user
66+
- name: pet_model
67+
x-displayName: The Pet Model
68+
description: |
69+
<ObjectDescription schemaRef="#/components/schemas/Pet" />
70+
- name: store_model
71+
x-displayName: The Order Model
72+
description: |
73+
<ObjectDescription schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} />
6674
x-tagGroups:
6775
- name: General
6876
tags:
@@ -71,6 +79,10 @@ x-tagGroups:
7179
- name: User Management
7280
tags:
7381
- user
82+
- name: Models
83+
tags:
84+
- pet_model
85+
- store_model
7486
paths:
7587
/pet:
7688
parameters:
@@ -754,6 +766,11 @@ components:
754766
description: Indicates whenever order was completed or not
755767
type: boolean
756768
default: false
769+
readOnly: true
770+
rqeuestId:
771+
description: Unique Request Id
772+
type: string
773+
writeOnly: true
757774
xml:
758775
name: Order
759776
Pet:
@@ -926,3 +943,10 @@ components:
926943
type: apiKey
927944
name: api_key
928945
in: header
946+
examples:
947+
Order:
948+
value:
949+
quantity: 1,
950+
shipDate: 2018-10-19T16:46:45Z,
951+
status: placed,
952+
complete: false

e2e/integration/menu.e2e.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('Menu', () => {
66
it('should have valid items count', () => {
77
cy.get('.menu-content')
88
.find('li')
9-
.should('have.length', 6 + (2 + 8 + 4) + (1 + 8));
9+
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8));
1010
});
1111

1212
it('should sync active menu items while scroll', () => {

src/components/PayloadSamples/MediaTypeSamples.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as React from 'react';
22

3+
import styled from '../../styled-components';
4+
35
import { DropdownProps } from '../../common-elements';
46
import { MediaTypeModel } from '../../services/models';
57
import { Markdown } from '../Markdown/Markdown';
@@ -48,7 +50,7 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
4850
const description = example.description;
4951

5052
return (
51-
<>
53+
<SamplesWrapper>
5254
<DropdownWrapper>
5355
<DropdownLabel>Example</DropdownLabel>
5456
{this.props.renderDropdown({
@@ -61,16 +63,20 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
6163
{description && <Markdown source={description} />}
6264
<Example example={example} mimeType={mimeType} />
6365
</div>
64-
</>
66+
</SamplesWrapper>
6567
);
6668
} else {
6769
const example = examples[examplesNames[0]];
6870
return (
69-
<div>
71+
<SamplesWrapper>
7072
{example.description && <Markdown source={example.description} />}
7173
<Example example={example} mimeType={mimeType} />
72-
</div>
74+
</SamplesWrapper>
7375
);
7476
}
7577
}
7678
}
79+
80+
const SamplesWrapper = styled.div`
81+
margin-top: 15px;
82+
`;

src/components/PayloadSamples/PayloadSamples.tsx

+6-14
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { observer } from 'mobx-react';
22
import * as React from 'react';
33
import { MediaTypeSamples } from './MediaTypeSamples';
44

5-
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
6-
7-
import styled from '../../../src/styled-components';
85
import { MediaContentModel } from '../../services/models';
96
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
7+
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
108
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
119

1210
export interface PayloadSamplesProps {
@@ -24,13 +22,11 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
2422
return (
2523
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
2624
{mediaType => (
27-
<SamplesWrapper>
28-
<MediaTypeSamples
29-
key="samples"
30-
mediaType={mediaType}
31-
renderDropdown={this.renderDropdown}
32-
/>
33-
</SamplesWrapper>
25+
<MediaTypeSamples
26+
key="samples"
27+
mediaType={mediaType}
28+
renderDropdown={this.renderDropdown}
29+
/>
3430
)}
3531
</MediaTypesSwitch>
3632
);
@@ -40,7 +36,3 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
4036
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
4137
};
4238
}
43-
44-
const SamplesWrapper = styled.div`
45-
margin-top: 15px;
46-
`;

src/components/PayloadSamples/styled.elements.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
5353
}
5454
.Dropdown-menu {
5555
margin: 0;
56-
margin-top: 10px;
56+
margin-top: 2px;
5757
}
5858
`;
5959

src/components/Schema/ObjectSchema.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
3434

3535
const filteredFields = needFilter
3636
? fields.filter(item => {
37-
return (
38-
(this.props.skipReadOnly && !item.schema.readOnly) ||
39-
(this.props.skipWriteOnly && !item.schema.writeOnly)
37+
return !(
38+
(this.props.skipReadOnly && item.schema.readOnly) ||
39+
(this.props.skipWriteOnly && item.schema.writeOnly)
4040
);
4141
})
4242
: fields;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as React from 'react';
2+
3+
import { DarkRightPanel, MiddlePanel, MimeLabel, Row, Section } from '../../common-elements';
4+
import { MediaTypeModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
5+
import styled from '../../styled-components';
6+
import { OpenAPIMediaType } from '../../types';
7+
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
8+
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples';
9+
import { InvertedSimpleDropdown } from '../PayloadSamples/styled.elements';
10+
import { Schema } from '../Schema';
11+
12+
export interface ObjectDescriptionProps {
13+
schemaRef: string;
14+
exampleRef?: string;
15+
showReadOnly?: boolean;
16+
showWriteOnly?: boolean;
17+
parser: OpenAPIParser;
18+
options: RedocNormalizedOptions;
19+
}
20+
21+
export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps> {
22+
private static getMediaType(schemaRef: string, exampleRef?: string): OpenAPIMediaType {
23+
if (!schemaRef) {
24+
return {};
25+
}
26+
27+
const info: OpenAPIMediaType = {
28+
schema: { $ref: schemaRef },
29+
};
30+
31+
if (exampleRef) {
32+
info.examples = { example: { $ref: exampleRef } };
33+
}
34+
35+
return info;
36+
}
37+
38+
private _mediaModel: MediaTypeModel;
39+
40+
private get mediaModel() {
41+
const { parser, schemaRef, exampleRef, options } = this.props;
42+
if (!this._mediaModel) {
43+
this._mediaModel = new MediaTypeModel(
44+
parser,
45+
'json',
46+
false,
47+
SchemaDefinition.getMediaType(schemaRef, exampleRef),
48+
options,
49+
);
50+
}
51+
52+
return this._mediaModel;
53+
}
54+
55+
render() {
56+
const { showReadOnly = true, showWriteOnly = false } = this.props;
57+
return (
58+
<Section>
59+
<Row>
60+
<MiddlePanel>
61+
<Schema
62+
skipWriteOnly={!showWriteOnly}
63+
skipReadOnly={!showReadOnly}
64+
schema={this.mediaModel.schema}
65+
/>
66+
</MiddlePanel>
67+
<DarkRightPanel>
68+
<MediaSamplesWrap>
69+
<MediaTypeSamples renderDropdown={this.renderDropdown} mediaType={this.mediaModel} />
70+
</MediaSamplesWrap>
71+
</DarkRightPanel>
72+
</Row>
73+
</Section>
74+
);
75+
}
76+
77+
private renderDropdown = props => {
78+
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
79+
};
80+
}
81+
82+
const MediaSamplesWrap = styled.div`
83+
background: ${({ theme }) => theme.codeSample.backgroundColor};
84+
& > div,
85+
& > pre {
86+
padding: ${props => props.theme.spacing.unit * 4}px;
87+
margin: 0;
88+
}
89+
90+
& > div > pre {
91+
padding: 0;
92+
}
93+
`;

src/services/AppStore.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
1010
import { ScrollService } from './ScrollService';
1111
import { SearchStore } from './SearchStore';
1212

13+
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
1314
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
14-
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
15+
import {
16+
SCHEMA_DEFINITION_JSX_NAME,
17+
SECURITY_DEFINITIONS_COMPONENT_NAME,
18+
SECURITY_DEFINITIONS_JSX_NAME,
19+
} from '../utils/openapi';
1520

1621
export interface StoreState {
1722
menu: {
@@ -151,5 +156,18 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
151156
securitySchemes: store.spec.securitySchemes,
152157
}),
153158
},
159+
[SECURITY_DEFINITIONS_JSX_NAME]: {
160+
component: SecurityDefs,
161+
propsSelector: (store: AppStore) => ({
162+
securitySchemes: store.spec.securitySchemes,
163+
}),
164+
},
165+
[SCHEMA_DEFINITION_JSX_NAME]: {
166+
component: SchemaDefinition,
167+
propsSelector: (store: AppStore) => ({
168+
parser: store.spec.parser,
169+
options: store.options,
170+
}),
171+
},
154172
},
155173
};

src/services/MenuBuilder.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class MenuBuilder {
4242

4343
const items: ContentItemModel[] = [];
4444
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
45-
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
45+
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
4646
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
4747
items.push(
4848
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
@@ -59,14 +59,16 @@ export class MenuBuilder {
5959
*/
6060
static addMarkdownItems(
6161
description: string,
62+
parent: GroupModel | undefined,
63+
initialDepth: number,
6264
options: RedocNormalizedOptions,
6365
): ContentItemModel[] {
6466
const renderer = new MarkdownRenderer(options);
6567
const headings = renderer.extractHeadings(description || '');
6668

67-
const mapHeadingsDeep = (parent, items, depth = 1) =>
69+
const mapHeadingsDeep = (_parent, items, depth = 1) =>
6870
items.map(heading => {
69-
const group = new GroupModel('section', heading, parent);
71+
const group = new GroupModel('section', heading, _parent);
7072
group.depth = depth;
7173
if (heading.items) {
7274
group.items = mapHeadingsDeep(group, heading.items, depth + 1);
@@ -82,7 +84,7 @@ export class MenuBuilder {
8284
return group;
8385
});
8486

85-
return mapHeadingsDeep(undefined, headings);
87+
return mapHeadingsDeep(parent, headings, initialDepth);
8688
}
8789

8890
/**
@@ -144,15 +146,22 @@ export class MenuBuilder {
144146
}
145147
const item = new GroupModel('tag', tag, parent);
146148
item.depth = GROUP_DEPTH + 1;
147-
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
148149

149150
// don't put empty tag into content, instead put its operations
150151
if (tag.name === '') {
151-
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options);
152+
const items = [
153+
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
154+
...this.getOperationsItems(parser, undefined, tag, item.depth + 1, options),
155+
];
152156
res.push(...items);
153157
continue;
154158
}
155159

160+
item.items = [
161+
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
162+
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
163+
];
164+
156165
res.push(item);
157166
}
158167
return res;

src/services/models/Group.model.ts

+7
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ export class GroupModel implements IMenuItem {
4040
this.type = type;
4141
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
4242
this.level = (tagOrGroup as MarkdownHeading).level || 1;
43+
44+
// remove sections from markdown, same as in ApiInfo
4345
this.description = tagOrGroup.description || '';
46+
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
47+
if (firstHeadingLinePos > -1) {
48+
this.description = this.description.substring(0, firstHeadingLinePos);
49+
}
50+
4451
this.parent = parent;
4552
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;
4653

src/utils/openapi.ts

+3
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,9 @@ export function normalizeServers(
496496
}
497497

498498
export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions';
499+
export const SECURITY_DEFINITIONS_JSX_NAME = 'SecurityDefinitions';
500+
export const SCHEMA_DEFINITION_JSX_NAME = 'ObjectDescription';
501+
499502
export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/';
500503
export function setSecuritySchemePrefix(prefix: string) {
501504
SECURITY_SCHEMES_SECTION_PREFIX = prefix;

0 commit comments

Comments
 (0)