Skip to content

Commit

Permalink
feat: add x-tags (Redocly#2355)
Browse files Browse the repository at this point in the history
* feat: add x-tags

* chore: fix e2e tests and add new for x-tag

* chore: add x-tags to demo definition

* chore: update snapshots
  • Loading branch information
AlexVarchuk authored and ckoegel committed Nov 1, 2023
1 parent 04b60d7 commit 9656b37
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 78 deletions.
1 change: 1 addition & 0 deletions demo/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,7 @@ components:
message:
type: string
Cat:
'x-tags': ['pet']
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
Expand Down
139 changes: 74 additions & 65 deletions e2e/integration/menu.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,89 @@
describe('Menu', () => {
beforeEach(() => {
cy.visit('e2e/standalone.html');
});
describe('3.0 spec', () => {
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', () => {
cy.get('.menu-content').find('li').should('have.length', 34);
});
it('should sync active menu items while scroll', () => {
cy.contains('h1', 'Introduction')
.scrollIntoView()
.get('[role=menuitem] > label.active')
.should('have.text', 'Introduction');

it('should sync active menu items while scroll', () => {
cy.contains('h1', 'Introduction')
.scrollIntoView()
.get('[role=menuitem].active')
.should('have.text', 'Introduction');
cy.contains('h2', 'Add a new pet to the store')
.scrollIntoView()
.wait(100)
.get('[role=menuitem] > label.active')
.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')
.scrollIntoView()
.wait(100)
.get('[role=menuitem].active')
.children()
.last()
.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('h2', 'Add a new pet to the store')
.scrollIntoView()
.wait(100)
.get('[role=menuitem] > label.active')
.children()
.last()
.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('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', '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')
.scrollIntoView()
.wait(100)
.get('[role=menuitem].active')
.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', () => {
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', () => {
cy.contains('[role=menuitem].-depth1', 'pet').click({ force: true });
cy.location('hash').should('equal', '#tag/pet');
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.contains('[role=menuitem]', 'Find pet by ID').click({ force: true });
cy.location('hash').should('equal', '#tag/pet/operation/getPetById');
});
cy.contains('[role=menuitem]', 'Find pet by ID').click({ force: true });
cy.location('hash').should('equal', '#tag/pet/operation/getPetById');
});

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

petItem().click({ force: true }).should('have.class', 'active');
cy.contains('[role=menuitem].-depth1', 'store').click({ force: true });
petItem().should('not.have.class', 'active');
});
petItem().click({ force: true }).should('have.class', 'active');
cy.contains('[role=menuitem] > label.-depth1', 'store').click({ force: true });
petItem().should('not.have.class', 'active');
});

it('should be able to open a response object to see more details', () => {
cy.contains('h2', 'Find pet by ID')
.scrollIntoView()
.wait(100)
.parent()
.find('div h3')
.should('have.text', 'Responses')
.parent()
.find('div:first button')
.click()
.should('have.attr', 'aria-expanded', 'true')
.parent()
.find('div h5')
.then($h5 => $h5[0].firstChild!.nodeValue!.trim())
.should('eq', 'Response Schema:');
it('should be able to open a response object to see more details', () => {
cy.contains('h2', 'Find pet by ID')
.scrollIntoView()
.wait(100)
.parent()
.find('div h3')
.should('have.text', 'Responses')
.parent()
.find('div:first button')
.click()
.should('have.attr', 'aria-expanded', 'true')
.parent()
.find('div h5')
.then($h5 => $h5[0].firstChild!.nodeValue!.trim())
.should('eq', 'Response Schema:');
});
});

it('should be able to open the operation details when the operation IDs have quotes', () => {
Expand All @@ -85,7 +94,7 @@ describe('Menu', () => {
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.get('label span[title="pet"]').click({ multiple: true, force: true });
cy.get('li').contains('OperationId with backslash').click({ multiple: true, force: true });
Expand Down
3 changes: 2 additions & 1 deletion src/components/SideMenu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export class MenuItem extends React.Component<MenuItemProps> {
<OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : (
<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}
{this.props.children}
</MenuItemTitle>
Expand Down
24 changes: 14 additions & 10 deletions src/components/SideMenu/styled.elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,43 +26,47 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
margin-top: 2px;
&.get {
background-color: ${props => props.theme.colors.http.get};
background-color: ${({ theme }) => theme.colors.http.get};
}
&.post {
background-color: ${props => props.theme.colors.http.post};
background-color: ${({ theme }) => theme.colors.http.post};
}
&.put {
background-color: ${props => props.theme.colors.http.put};
background-color: ${({ theme }) => theme.colors.http.put};
}
&.options {
background-color: ${props => props.theme.colors.http.options};
background-color: ${({ theme }) => theme.colors.http.options};
}
&.patch {
background-color: ${props => props.theme.colors.http.patch};
background-color: ${({ theme }) => theme.colors.http.patch};
}
&.delete {
background-color: ${props => props.theme.colors.http.delete};
background-color: ${({ theme }) => theme.colors.http.delete};
}
&.basic {
background-color: ${props => props.theme.colors.http.basic};
background-color: ${({ theme }) => theme.colors.http.basic};
}
&.link {
background-color: ${props => props.theme.colors.http.link};
background-color: ${({ theme }) => theme.colors.http.link};
}
&.head {
background-color: ${props => props.theme.colors.http.head};
background-color: ${({ theme }) => theme.colors.http.head};
}
&.hook {
background-color: ${props => props.theme.colors.primary.main};
background-color: ${({ theme }) => theme.colors.primary.main};
}
&.schema {
background-color: ${({ theme }) => theme.colors.http.basic};
}
`;

Expand Down
38 changes: 37 additions & 1 deletion src/services/MenuBuilder.ts
Original file line number Diff line number Diff line change
@@ -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 { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models';
Expand Down Expand Up @@ -137,7 +137,14 @@ export class MenuBuilder {
continue;
}

const relatedSchemas = this.getTagRelatedSchema({
parser,
tag,
parent: item,
});

item.items = [
...relatedSchemas,
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
];
Expand Down Expand Up @@ -248,4 +255,33 @@ export class MenuBuilder {
}
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[];
}
}
2 changes: 1 addition & 1 deletion src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export interface TagGroup {
tags: string[];
}

export type MenuItemGroupType = 'group' | 'tag' | 'section';
export type MenuItemGroupType = 'group' | 'tag' | 'section' | 'schema';
export type MenuItemType = MenuItemGroupType | 'operation';

export interface IMenuItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ Object {
},
],
"description": "A representation of a cat",
"x-tags": Array [
"pet",
],
},
"Category": Object {
"properties": Object {
Expand Down

0 comments on commit 9656b37

Please sign in to comment.