feat: add x-tags (#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
This commit is contained in:
Alex Varchuk 2023-07-26 11:17:56 +03:00 committed by GitHub
parent 9b73dae685
commit 0bb21c8128
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 132 additions and 78 deletions

View File

@ -910,6 +910,7 @@ components:
message: message:
type: string type: string
Cat: Cat:
'x-tags': ['pet']
description: A representation of a cat description: A representation of a cat
allOf: allOf:
- $ref: '#/components/schemas/Pet' - $ref: '#/components/schemas/Pet'

View File

@ -1,22 +1,22 @@
describe('Menu', () => { describe('Menu', () => {
describe('3.0 spec', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('e2e/standalone.html'); cy.visit('e2e/standalone.html');
}); });
it('should have valid items count', () => { it('should have valid items count', () => {
cy.get('.menu-content').find('li').should('have.length', 34); cy.get('.menu-content').find('li').should('have.length', 35);
}); });
it('should sync active menu items while scroll', () => { it('should sync active menu items while scroll', () => {
cy.contains('h1', 'Introduction') cy.contains('h1', 'Introduction')
.scrollIntoView() .scrollIntoView()
.get('[role=menuitem].active') .get('[role=menuitem] > label.active')
.should('have.text', 'Introduction'); .should('have.text', 'Introduction');
cy.contains('h2', 'Add a new pet to the store') cy.contains('h2', 'Add a new pet to the store')
.scrollIntoView() .scrollIntoView()
.wait(100) .wait(100)
.get('[role=menuitem].active') .get('[role=menuitem] > label.active')
.children() .children()
.last() .last()
.should('have.text', 'Add a new pet to the store') .should('have.text', 'Add a new pet to the store')
@ -27,7 +27,7 @@ describe('Menu', () => {
cy.contains('h2', 'Add a new pet to the store') cy.contains('h2', 'Add a new pet to the store')
.scrollIntoView() .scrollIntoView()
.wait(100) .wait(100)
.get('[role=menuitem].active') .get('[role=menuitem] > label.active')
.children() .children()
.last() .last()
.should('have.text', 'Add a new pet to the store') .should('have.text', 'Add a new pet to the store')
@ -38,14 +38,22 @@ describe('Menu', () => {
cy.contains('h1', 'Introduction') cy.contains('h1', 'Introduction')
.scrollIntoView() .scrollIntoView()
.wait(100) .wait(100)
.get('[role=menuitem].active') .get('[role=menuitem] > label.active')
.should('have.text', 'Introduction'); .should('have.text', 'Introduction');
cy.url().should('include', '#section/Introduction'); cy.url().should('include', '#section/Introduction');
}); });
it('should update URL hash when clicking on menu items', () => { it('should update URL hash when clicking on menu items', () => {
cy.contains('[role=menuitem].-depth1', 'pet').click({ force: true }); cy.contains('[role=menuitem] > label.-depth1', 'pet').click({ force: true });
cy.get('li[data-item-id="schema/Cat"]')
.should('have.text', 'schemaCat')
.click({ force: true });
cy.location('hash').should('equal', '#schema/Cat');
});
it('should contains Cat schema in Pet using x-tags', () => {
cy.contains('[role=menuitem] > label.-depth1', 'pet').click({ force: true });
cy.location('hash').should('equal', '#tag/pet'); cy.location('hash').should('equal', '#tag/pet');
cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true }); cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true });
@ -53,10 +61,10 @@ describe('Menu', () => {
}); });
it('should deactivate tag when other is activated', () => { it('should deactivate tag when other is activated', () => {
const petItem = () => cy.contains('[role=menuitem].-depth1', 'pet'); const petItem = () => cy.contains('[role=menuitem] > label.-depth1', 'pet');
petItem().click({ force: true }).should('have.class', 'active'); petItem().click({ force: true }).should('have.class', 'active');
cy.contains('[role=menuitem].-depth1', 'store').click({ force: true }); cy.contains('[role=menuitem] > label.-depth1', 'store').click({ force: true });
petItem().should('not.have.class', 'active'); petItem().should('not.have.class', 'active');
}); });
@ -76,6 +84,7 @@ describe('Menu', () => {
.then($h5 => $h5[0].firstChild!.nodeValue!.trim()) .then($h5 => $h5[0].firstChild!.nodeValue!.trim())
.should('eq', 'Response Schema:'); .should('eq', 'Response Schema:');
}); });
});
it('should be able to open the operation details when the operation IDs have quotes', () => { it('should be able to open the operation details when the operation IDs have quotes', () => {
cy.visit('e2e/standalone-3-1.html'); cy.visit('e2e/standalone-3-1.html');
@ -85,7 +94,7 @@ describe('Menu', () => {
cy.url().should('include', 'deletePetBy%22Id'); cy.url().should('include', 'deletePetBy%22Id');
}); });
it.only('should encode URL when the operation IDs have backslashes', () => { it('should encode URL when the operation IDs have backslashes', () => {
cy.visit('e2e/standalone-3-1.html'); cy.visit('e2e/standalone-3-1.html');
cy.get('label span[title="pet"]').click({ multiple: true, force: true }); cy.get('label span[title="pet"]').click({ multiple: true, force: true });
cy.get('li').contains('OperationId with backslash').click({ multiple: true, force: true }); cy.get('li').contains('OperationId with backslash').click({ multiple: true, force: true });

View File

@ -55,7 +55,8 @@ export class MenuItem extends React.Component<MenuItemProps> {
<OperationMenuItemContent {...this.props} item={item as OperationModel} /> <OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : ( ) : (
<MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}> <MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}>
<MenuItemTitle title={item.sidebarLabel}> {item.type === 'schema' && <OperationBadge type="schema">schema</OperationBadge>}
<MenuItemTitle width="calc(100% - 38px)" title={item.sidebarLabel}>
{item.sidebarLabel} {item.sidebarLabel}
{this.props.children} {this.props.children}
</MenuItemTitle> </MenuItemTitle>

View File

@ -26,43 +26,47 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
margin-top: 2px; margin-top: 2px;
&.get { &.get {
background-color: ${props => props.theme.colors.http.get}; background-color: ${({ theme }) => theme.colors.http.get};
} }
&.post { &.post {
background-color: ${props => props.theme.colors.http.post}; background-color: ${({ theme }) => theme.colors.http.post};
} }
&.put { &.put {
background-color: ${props => props.theme.colors.http.put}; background-color: ${({ theme }) => theme.colors.http.put};
} }
&.options { &.options {
background-color: ${props => props.theme.colors.http.options}; background-color: ${({ theme }) => theme.colors.http.options};
} }
&.patch { &.patch {
background-color: ${props => props.theme.colors.http.patch}; background-color: ${({ theme }) => theme.colors.http.patch};
} }
&.delete { &.delete {
background-color: ${props => props.theme.colors.http.delete}; background-color: ${({ theme }) => theme.colors.http.delete};
} }
&.basic { &.basic {
background-color: ${props => props.theme.colors.http.basic}; background-color: ${({ theme }) => theme.colors.http.basic};
} }
&.link { &.link {
background-color: ${props => props.theme.colors.http.link}; background-color: ${({ theme }) => theme.colors.http.link};
} }
&.head { &.head {
background-color: ${props => props.theme.colors.http.head}; background-color: ${({ theme }) => theme.colors.http.head};
} }
&.hook { &.hook {
background-color: ${props => props.theme.colors.primary.main}; background-color: ${({ theme }) => theme.colors.primary.main};
}
&.schema {
background-color: ${({ theme }) => theme.colors.http.basic};
} }
`; `;

View File

@ -1,4 +1,4 @@
import type { OpenAPISpec, OpenAPIPaths } from '../types'; import type { OpenAPISpec, OpenAPIPaths, OpenAPITag, OpenAPISchema } from '../types';
import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils'; import { isOperationName, JsonPointer, alphabeticallyByProp } from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer'; import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models'; import { GroupModel, OperationModel } from './models';
@ -137,7 +137,14 @@ export class MenuBuilder {
continue; continue;
} }
const relatedSchemas = this.getTagRelatedSchema({
parser,
tag,
parent: item,
});
item.items = [ item.items = [
...relatedSchemas,
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options), ...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, item, tag, item.depth + 1, options), ...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
]; ];
@ -248,4 +255,33 @@ export class MenuBuilder {
} }
return tags; return tags;
} }
static getTagRelatedSchema({
parser,
tag,
parent,
}: {
parser: OpenAPIParser;
tag: TagInfo;
parent: GroupModel;
}): GroupModel[] {
return Object.entries(parser.spec.components?.schemas || {})
.map(([schemaName, schema]) => {
const schemaTags = schema['x-tags'];
if (!schemaTags?.includes(tag.name)) return null;
const item = new GroupModel(
'schema',
{
name: schemaName,
'x-displayName': `${(schema as OpenAPISchema).title || schemaName}`,
description: `<SchemaDefinition showWriteOnly={true} schemaRef="#/components/schemas/${schemaName}" />`,
} as OpenAPITag,
parent,
);
item.depth = parent.depth + 1;
return item;
})
.filter(Boolean) as GroupModel[];
}
} }

View File

@ -83,7 +83,7 @@ export interface TagGroup {
tags: string[]; tags: string[];
} }
export type MenuItemGroupType = 'group' | 'tag' | 'section'; export type MenuItemGroupType = 'group' | 'tag' | 'section' | 'schema';
export type MenuItemType = MenuItemGroupType | 'operation'; export type MenuItemType = MenuItemGroupType | 'operation';
export interface IMenuItem { export interface IMenuItem {

View File

@ -102,6 +102,9 @@ Object {
}, },
], ],
"description": "A representation of a cat", "description": "A representation of a cat",
"x-tags": Array [
"pet",
],
}, },
"Category": Object { "Category": Object {
"properties": Object { "properties": Object {