mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-07 05:20:32 +03:00
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:
parent
9b73dae685
commit
0bb21c8128
|
@ -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'
|
||||||
|
|
|
@ -1,80 +1,89 @@
|
||||||
describe('Menu', () => {
|
describe('Menu', () => {
|
||||||
beforeEach(() => {
|
describe('3.0 spec', () => {
|
||||||
cy.visit('e2e/standalone.html');
|
beforeEach(() => {
|
||||||
});
|
cy.visit('e2e/standalone.html');
|
||||||
|
});
|
||||||
|
it('should have valid items count', () => {
|
||||||
|
cy.get('.menu-content').find('li').should('have.length', 35);
|
||||||
|
});
|
||||||
|
|
||||||
it('should have valid items count', () => {
|
it('should sync active menu items while scroll', () => {
|
||||||
cy.get('.menu-content').find('li').should('have.length', 34);
|
cy.contains('h1', 'Introduction')
|
||||||
});
|
.scrollIntoView()
|
||||||
|
.get('[role=menuitem] > label.active')
|
||||||
|
.should('have.text', 'Introduction');
|
||||||
|
|
||||||
it('should sync active menu items while scroll', () => {
|
cy.contains('h2', 'Add a new pet to the store')
|
||||||
cy.contains('h1', 'Introduction')
|
.scrollIntoView()
|
||||||
.scrollIntoView()
|
.wait(100)
|
||||||
.get('[role=menuitem].active')
|
.get('[role=menuitem] > label.active')
|
||||||
.should('have.text', 'Introduction');
|
.children()
|
||||||
|
.last()
|
||||||
|
.should('have.text', 'Add a new pet to the store')
|
||||||
|
.should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
cy.contains('h2', 'Add a new pet to the store')
|
it('should sync active menu items while scroll back and scroll again', () => {
|
||||||
.scrollIntoView()
|
cy.contains('h2', 'Add a new pet to the store')
|
||||||
.wait(100)
|
.scrollIntoView()
|
||||||
.get('[role=menuitem].active')
|
.wait(100)
|
||||||
.children()
|
.get('[role=menuitem] > label.active')
|
||||||
.last()
|
.children()
|
||||||
.should('have.text', 'Add a new pet to the store')
|
.last()
|
||||||
.should('be.visible');
|
.should('have.text', 'Add a new pet to the store')
|
||||||
});
|
.should('be.visible');
|
||||||
|
|
||||||
it('should sync active menu items while scroll back and scroll again', () => {
|
cy.contains('h1', 'Swagger Petstore').scrollIntoView().wait(100);
|
||||||
cy.contains('h2', 'Add a new pet to the store')
|
|
||||||
.scrollIntoView()
|
|
||||||
.wait(100)
|
|
||||||
.get('[role=menuitem].active')
|
|
||||||
.children()
|
|
||||||
.last()
|
|
||||||
.should('have.text', 'Add a new pet to the store')
|
|
||||||
.should('be.visible');
|
|
||||||
|
|
||||||
cy.contains('h1', 'Swagger Petstore').scrollIntoView().wait(100);
|
cy.contains('h1', 'Introduction')
|
||||||
|
.scrollIntoView()
|
||||||
|
.wait(100)
|
||||||
|
.get('[role=menuitem] > label.active')
|
||||||
|
.should('have.text', 'Introduction');
|
||||||
|
|
||||||
cy.contains('h1', 'Introduction')
|
cy.url().should('include', '#section/Introduction');
|
||||||
.scrollIntoView()
|
});
|
||||||
.wait(100)
|
|
||||||
.get('[role=menuitem].active')
|
|
||||||
.should('have.text', 'Introduction');
|
|
||||||
|
|
||||||
cy.url().should('include', '#section/Introduction');
|
it('should update URL hash when clicking on menu items', () => {
|
||||||
});
|
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 update URL hash when clicking on menu items', () => {
|
it('should contains Cat schema in Pet using x-tags', () => {
|
||||||
cy.contains('[role=menuitem].-depth1', 'pet').click({ force: true });
|
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 });
|
||||||
cy.location('hash').should('equal', '#tag/pet/operation/getPetById');
|
cy.location('hash').should('equal', '#tag/pet/operation/getPetById');
|
||||||
});
|
});
|
||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to open a response object to see more details', () => {
|
it('should be able to open a response object to see more details', () => {
|
||||||
cy.contains('h2', 'Find pet by ID')
|
cy.contains('h2', 'Find pet by ID')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.wait(100)
|
.wait(100)
|
||||||
.parent()
|
.parent()
|
||||||
.find('div h3')
|
.find('div h3')
|
||||||
.should('have.text', 'Responses')
|
.should('have.text', 'Responses')
|
||||||
.parent()
|
.parent()
|
||||||
.find('div:first button')
|
.find('div:first button')
|
||||||
.click()
|
.click()
|
||||||
.should('have.attr', 'aria-expanded', 'true')
|
.should('have.attr', 'aria-expanded', 'true')
|
||||||
.parent()
|
.parent()
|
||||||
.find('div h5')
|
.find('div h5')
|
||||||
.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', () => {
|
||||||
|
@ -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 });
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -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[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user