Skip to content

Commit 9656b37

Browse files
AlexVarchukckoegel
authored andcommitted
feat: add x-tags (Redocly#2355)
* feat: add x-tags * chore: fix e2e tests and add new for x-tag * chore: add x-tags to demo definition * chore: update snapshots
1 parent 04b60d7 commit 9656b37

File tree

7 files changed

+132
-78
lines changed

7 files changed

+132
-78
lines changed

demo/openapi.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,7 @@ components:
15091509
message:
15101510
type: string
15111511
Cat:
1512+
'x-tags': ['pet']
15121513
description: A representation of a cat
15131514
allOf:
15141515
- $ref: '#/components/schemas/Pet'

e2e/integration/menu.e2e.ts

Lines changed: 74 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,89 @@
11
describe('Menu', () => {
2-
beforeEach(() => {
3-
cy.visit('e2e/standalone.html');
4-
});
2+
describe('3.0 spec', () => {
3+
beforeEach(() => {
4+
cy.visit('e2e/standalone.html');
5+
});
6+
it('should have valid items count', () => {
7+
cy.get('.menu-content').find('li').should('have.length', 35);
8+
});
59

6-
it('should have valid items count', () => {
7-
cy.get('.menu-content').find('li').should('have.length', 34);
8-
});
10+
it('should sync active menu items while scroll', () => {
11+
cy.contains('h1', 'Introduction')
12+
.scrollIntoView()
13+
.get('[role=menuitem] > label.active')
14+
.should('have.text', 'Introduction');
915

10-
it('should sync active menu items while scroll', () => {
11-
cy.contains('h1', 'Introduction')
12-
.scrollIntoView()
13-
.get('[role=menuitem].active')
14-
.should('have.text', 'Introduction');
16+
cy.contains('h2', 'Add a new pet to the store')
17+
.scrollIntoView()
18+
.wait(100)
19+
.get('[role=menuitem] > label.active')
20+
.children()
21+
.last()
22+
.should('have.text', 'Add a new pet to the store')
23+
.should('be.visible');
24+
});
1525

16-
cy.contains('h2', 'Add a new pet to the store')
17-
.scrollIntoView()
18-
.wait(100)
19-
.get('[role=menuitem].active')
20-
.children()
21-
.last()
22-
.should('have.text', 'Add a new pet to the store')
23-
.should('be.visible');
24-
});
26+
it('should sync active menu items while scroll back and scroll again', () => {
27+
cy.contains('h2', 'Add a new pet to the store')
28+
.scrollIntoView()
29+
.wait(100)
30+
.get('[role=menuitem] > label.active')
31+
.children()
32+
.last()
33+
.should('have.text', 'Add a new pet to the store')
34+
.should('be.visible');
2535

26-
it('should sync active menu items while scroll back and scroll again', () => {
27-
cy.contains('h2', 'Add a new pet to the store')
28-
.scrollIntoView()
29-
.wait(100)
30-
.get('[role=menuitem].active')
31-
.children()
32-
.last()
33-
.should('have.text', 'Add a new pet to the store')
34-
.should('be.visible');
36+
cy.contains('h1', 'Swagger Petstore').scrollIntoView().wait(100);
3537

36-
cy.contains('h1', 'Swagger Petstore').scrollIntoView().wait(100);
38+
cy.contains('h1', 'Introduction')
39+
.scrollIntoView()
40+
.wait(100)
41+
.get('[role=menuitem] > label.active')
42+
.should('have.text', 'Introduction');
3743

38-
cy.contains('h1', 'Introduction')
39-
.scrollIntoView()
40-
.wait(100)
41-
.get('[role=menuitem].active')
42-
.should('have.text', 'Introduction');
44+
cy.url().should('include', '#section/Introduction');
45+
});
4346

44-
cy.url().should('include', '#section/Introduction');
45-
});
47+
it('should update URL hash when clicking on menu items', () => {
48+
cy.contains('[role=menuitem] > label.-depth1', 'pet').click({ force: true });
49+
cy.get('li[data-item-id="schema/Cat"]')
50+
.should('have.text', 'schemaCat')
51+
.click({ force: true });
52+
cy.location('hash').should('equal', '#schema/Cat');
53+
});
4654

47-
it('should update URL hash when clicking on menu items', () => {
48-
cy.contains('[role=menuitem].-depth1', 'pet').click({ force: true });
49-
cy.location('hash').should('equal', '#tag/pet');
55+
it('should contains Cat schema in Pet using x-tags', () => {
56+
cy.contains('[role=menuitem] > label.-depth1', 'pet').click({ force: true });
57+
cy.location('hash').should('equal', '#tag/pet');
5058

51-
cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true });
52-
cy.location('hash').should('equal', '#tag/pet/operation/getPetById');
53-
});
59+
cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true });
60+
cy.location('hash').should('equal', '#tag/pet/operation/getPetById');
61+
});
5462

55-
it('should deactivate tag when other is activated', () => {
56-
const petItem = () => cy.contains('[role=menuitem].-depth1', 'pet');
63+
it('should deactivate tag when other is activated', () => {
64+
const petItem = () => cy.contains('[role=menuitem] > label.-depth1', 'pet');
5765

58-
petItem().click({ force: true }).should('have.class', 'active');
59-
cy.contains('[role=menuitem].-depth1', 'store').click({ force: true });
60-
petItem().should('not.have.class', 'active');
61-
});
66+
petItem().click({ force: true }).should('have.class', 'active');
67+
cy.contains('[role=menuitem] > label.-depth1', 'store').click({ force: true });
68+
petItem().should('not.have.class', 'active');
69+
});
6270

63-
it('should be able to open a response object to see more details', () => {
64-
cy.contains('h2', 'Find pet by ID')
65-
.scrollIntoView()
66-
.wait(100)
67-
.parent()
68-
.find('div h3')
69-
.should('have.text', 'Responses')
70-
.parent()
71-
.find('div:first button')
72-
.click()
73-
.should('have.attr', 'aria-expanded', 'true')
74-
.parent()
75-
.find('div h5')
76-
.then($h5 => $h5[0].firstChild!.nodeValue!.trim())
77-
.should('eq', 'Response Schema:');
71+
it('should be able to open a response object to see more details', () => {
72+
cy.contains('h2', 'Find pet by ID')
73+
.scrollIntoView()
74+
.wait(100)
75+
.parent()
76+
.find('div h3')
77+
.should('have.text', 'Responses')
78+
.parent()
79+
.find('div:first button')
80+
.click()
81+
.should('have.attr', 'aria-expanded', 'true')
82+
.parent()
83+
.find('div h5')
84+
.then($h5 => $h5[0].firstChild!.nodeValue!.trim())
85+
.should('eq', 'Response Schema:');
86+
});
7887
});
7988

8089
it('should be able to open the operation details when the operation IDs have quotes', () => {
@@ -85,7 +94,7 @@ describe('Menu', () => {
8594
cy.url().should('include', 'deletePetBy%22Id');
8695
});
8796

88-
it.only('should encode URL when the operation IDs have backslashes', () => {
97+
it('should encode URL when the operation IDs have backslashes', () => {
8998
cy.visit('e2e/standalone-3-1.html');
9099
cy.get('label span[title="pet"]').click({ multiple: true, force: true });
91100
cy.get('li').contains('OperationId with backslash').click({ multiple: true, force: true });

src/components/SideMenu/MenuItem.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export class MenuItem extends React.Component<MenuItemProps> {
5555
<OperationMenuItemContent {...this.props} item={item as OperationModel} />
5656
) : (
5757
<MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}>
58-
<MenuItemTitle title={item.sidebarLabel}>
58+
{item.type === 'schema' && <OperationBadge type="schema">schema</OperationBadge>}
59+
<MenuItemTitle width="calc(100% - 38px)" title={item.sidebarLabel}>
5960
{item.sidebarLabel}
6061
{this.props.children}
6162
</MenuItemTitle>

src/components/SideMenu/styled.elements.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,43 +26,47 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
2626
margin-top: 2px;
2727
2828
&.get {
29-
background-color: ${props => props.theme.colors.http.get};
29+
background-color: ${({ theme }) => theme.colors.http.get};
3030
}
3131
3232
&.post {
33-
background-color: ${props => props.theme.colors.http.post};
33+
background-color: ${({ theme }) => theme.colors.http.post};
3434
}
3535
3636
&.put {
37-
background-color: ${props => props.theme.colors.http.put};
37+
background-color: ${({ theme }) => theme.colors.http.put};
3838
}
3939
4040
&.options {
41-
background-color: ${props => props.theme.colors.http.options};
41+
background-color: ${({ theme }) => theme.colors.http.options};
4242
}
4343
4444
&.patch {
45-
background-color: ${props => props.theme.colors.http.patch};
45+
background-color: ${({ theme }) => theme.colors.http.patch};
4646
}
4747
4848
&.delete {
49-
background-color: ${props => props.theme.colors.http.delete};
49+
background-color: ${({ theme }) => theme.colors.http.delete};
5050
}
5151
5252
&.basic {
53-
background-color: ${props => props.theme.colors.http.basic};
53+
background-color: ${({ theme }) => theme.colors.http.basic};
5454
}
5555
5656
&.link {
57-
background-color: ${props => props.theme.colors.http.link};
57+
background-color: ${({ theme }) => theme.colors.http.link};
5858
}
5959
6060
&.head {
61-
background-color: ${props => props.theme.colors.http.head};
61+
background-color: ${({ theme }) => theme.colors.http.head};
6262
}
6363
6464
&.hook {
65-
background-color: ${props => props.theme.colors.primary.main};
65+
background-color: ${({ theme }) => theme.colors.primary.main};
66+
}
67+
68+
&.schema {
69+
background-color: ${({ theme }) => theme.colors.http.basic};
6670
}
6771
`;
6872

src/services/MenuBuilder.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { OpenAPISpec, OpenAPIPaths } from '../types';
1+
import type { OpenAPISpec, OpenAPIPaths, OpenAPITag, OpenAPISchema } from '../types';
22
import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils';
33
import { MarkdownRenderer } from './MarkdownRenderer';
44
import { GroupModel, OperationModel } from './models';
@@ -137,7 +137,14 @@ export class MenuBuilder {
137137
continue;
138138
}
139139

140+
const relatedSchemas = this.getTagRelatedSchema({
141+
parser,
142+
tag,
143+
parent: item,
144+
});
145+
140146
item.items = [
147+
...relatedSchemas,
141148
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
142149
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
143150
];
@@ -248,4 +255,33 @@ export class MenuBuilder {
248255
}
249256
return tags;
250257
}
258+
259+
static getTagRelatedSchema({
260+
parser,
261+
tag,
262+
parent,
263+
}: {
264+
parser: OpenAPIParser;
265+
tag: TagInfo;
266+
parent: GroupModel;
267+
}): GroupModel[] {
268+
return Object.entries(parser.spec.components?.schemas || {})
269+
.map(([schemaName, schema]) => {
270+
const schemaTags = schema['x-tags'];
271+
if (!schemaTags?.includes(tag.name)) return null;
272+
273+
const item = new GroupModel(
274+
'schema',
275+
{
276+
name: schemaName,
277+
'x-displayName': `${(schema as OpenAPISchema).title || schemaName}`,
278+
description: `<SchemaDefinition showWriteOnly={true} schemaRef="#/components/schemas/${schemaName}" />`,
279+
} as OpenAPITag,
280+
parent,
281+
);
282+
item.depth = parent.depth + 1;
283+
return item;
284+
})
285+
.filter(Boolean) as GroupModel[];
286+
}
251287
}

src/services/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export interface TagGroup {
8383
tags: string[];
8484
}
8585

86-
export type MenuItemGroupType = 'group' | 'tag' | 'section';
86+
export type MenuItemGroupType = 'group' | 'tag' | 'section' | 'schema';
8787
export type MenuItemType = MenuItemGroupType | 'operation';
8888

8989
export interface IMenuItem {

src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ Object {
102102
},
103103
],
104104
"description": "A representation of a cat",
105+
"x-tags": Array [
106+
"pet",
107+
],
105108
},
106109
"Category": Object {
107110
"properties": Object {

0 commit comments

Comments
 (0)