diff --git a/README.md b/README.md index 68e2b3e662..94d1452d16 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Redoc is provided as a CLI tool (also distributed as a Docker image), HTML tag, If you have Node installed, quickly generate documentation using `npx`: ```bash -npx @redocly/cli build-docs openapi.yaml +npx @redocly/cli build-docs openapi.yaml ``` The tool outputs by default to a file named `redoc-static.html` that you can open in your browser. @@ -116,6 +116,7 @@ Redoc uses the following [specification extensions](https://redocly.com/docs/api * [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo * [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for tags that refer to non-navigation properties like Pagination, Rate-Limits, etc * [`x-codeSamples`](docs/redoc-vendor-extensions.md#x-codeSamples) - specify operation code samples +* [`x-badges`](docs/redoc-vendor-extensions.md#x-badges) - specify operation badges * [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests * [`x-nullable`](docs/redoc-vendor-extensions.md#x-nullable) - mark schema param as a nullable * [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories diff --git a/demo/museum.yaml b/demo/museum.yaml index 4d6f55e015..8473be695a 100644 --- a/demo/museum.yaml +++ b/demo/museum.yaml @@ -22,6 +22,10 @@ paths: operationId: getMuseumHours tags: - Operations + x-badges: + - name: 'Beta' + position: before + color: purple parameters: - $ref: '#/components/parameters/StartDate' - $ref: '#/components/parameters/PaginationPage' @@ -64,6 +68,9 @@ paths: summary: Create special event tags: - Events + x-badges: + - name: 'Alpha' + color: purple requestBody: required: true content: @@ -92,6 +99,8 @@ paths: description: Return a list of upcoming special events at the museum. security: [] operationId: listSpecialEvents + x-badges: + - name: 'Gamma' tags: - Events parameters: diff --git a/demo/openapi.yaml b/demo/openapi.yaml index be7cb578b6..d715155e63 100644 --- a/demo/openapi.yaml +++ b/demo/openapi.yaml @@ -106,6 +106,10 @@ paths: post: tags: - pet + x-badges: + - name: 'Beta' + position: before + color: purple summary: Add a new pet to the store description: Add new pet to the store inventory. operationId: addPet @@ -150,6 +154,9 @@ paths: put: tags: - pet + x-badges: + - name: 'Alpha' + color: purple summary: Update an existing pet description: '' operationId: updatePet @@ -183,6 +190,8 @@ paths: get: tags: - pet + x-badges: + - name: 'Gamma' summary: Find pet by ID description: Returns a single pet operationId: getPetById diff --git a/docs/redoc-vendor-extensions.md b/docs/redoc-vendor-extensions.md index 550964b0fc..c5a5a0dfb6 100644 --- a/docs/redoc-vendor-extensions.md +++ b/docs/redoc-vendor-extensions.md @@ -252,6 +252,11 @@ lang: JavaScript source: console.log('Hello World'); ``` +### x-badges +| Field Name | Type | Description | +| :------------- | :------: | :---------- | +| x-badges | [[Badge Object](https://redocly.com/docs/realm/author/reference/openapi-extensions/x-badges#badge-object)] | A list of badges associated with operation | + ## Parameter Object Extends the OpenAPI [Parameter Object](https://redocly.com/docs/openapi-visual-reference/parameter/) diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index 38160fdb70..51788a5246 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -52,6 +52,30 @@ describe('Menu', () => { cy.location('hash').should('equal', '#schema/Cat'); }); + it('should contains badge schema from x-badges', () => { + cy.contains('h2 > span', 'Beta') + .scrollIntoView() + .wait(100) + .get('[role=menuitem] > label.active') + .children('span[type="badge"]') + .should('have.text', 'Beta') + .should('be.visible'); + cy.contains('h2 > span', 'Alpha') + .scrollIntoView() + .wait(100) + .get('[role=menuitem] > label.active') + .children('span[type="badge"]') + .should('have.text', 'Alpha') + .should('be.visible'); + cy.contains('h2 > span', 'Gamma') + .scrollIntoView() + .wait(100) + .get('[role=menuitem] > label.active') + .children('span[type="badge"]') + .should('have.text', 'Gamma') + .should('be.visible'); + }); + 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'); diff --git a/src/common-elements/shelfs.tsx b/src/common-elements/shelfs.tsx index 8eb3d4a729..e0c354ed18 100644 --- a/src/common-elements/shelfs.tsx +++ b/src/common-elements/shelfs.tsx @@ -47,11 +47,11 @@ export const ShelfIcon = styled(IntShelfIcon)` } `; -export const Badge = styled.span<{ type: string }>` +export const Badge = styled.span<{ type: string; color?: string }>` display: inline-block; padding: 2px 8px; margin: 0; - background-color: ${props => props.theme.colors[props.type].main}; + background-color: ${props => props.color || props.theme.colors[props.type].main}; color: ${props => props.theme.colors[props.type].contrastText}; font-size: ${props => props.theme.typography.code.fontSize}; vertical-align: middle; diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index b6904f62cc..38602467ac 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -28,9 +28,20 @@ export interface OperationProps { } export const Operation = observer(({ operation }: OperationProps): JSX.Element => { - const { name: summary, description, deprecated, externalDocs, isWebhook, httpVerb } = operation; + const { + name: summary, + description, + deprecated, + externalDocs, + isWebhook, + httpVerb, + badges, + } = operation; const hasDescription = !!(description || externalDocs); const { showWebhookVerb } = React.useContext(OptionsContext); + const badgesBefore = badges.filter(({ position }) => position === 'before'); + const badgesAfter = badges.filter(({ position }) => position === 'after'); + return ( {options => ( @@ -38,6 +49,11 @@ export const Operation = observer(({ operation }: OperationProps): JSX.Element =

+ {badgesBefore.map(({ name, color }) => ( + + {name} + + ))} {summary} {deprecated && Deprecated } {isWebhook && ( @@ -45,6 +61,11 @@ export const Operation = observer(({ operation }: OperationProps): JSX.Element = Webhook {showWebhookVerb && httpVerb && '| ' + httpVerb.toUpperCase()} )} + {badgesAfter.map(({ name, color }) => ( + + {name} + + ))}

{options.pathInMiddlePanel && !isWebhook && ( diff --git a/src/components/SideMenu/MenuItem.tsx b/src/components/SideMenu/MenuItem.tsx index e704b9d490..19f8971803 100644 --- a/src/components/SideMenu/MenuItem.tsx +++ b/src/components/SideMenu/MenuItem.tsx @@ -101,6 +101,12 @@ export const OperationMenuItemContent = observer((props: OperationMenuItemConten $deprecated={item.deprecated} ref={ref} > + {item.badges && + item.badges?.map(({ name, color }) => ( + + {name} + + ))} {item.isWebhook ? ( {showWebhookVerb ? item.httpVerb : l('webhook')} diff --git a/src/components/SideMenu/styled.elements.ts b/src/components/SideMenu/styled.elements.ts index 2cd1c4eb10..33de76ab3b 100644 --- a/src/components/SideMenu/styled.elements.ts +++ b/src/components/SideMenu/styled.elements.ts @@ -4,14 +4,14 @@ import { darken } from 'polished'; import { deprecatedCss, ShelfIcon } from '../../common-elements'; import styled, { css, media, ResolvedThemeInterface } from '../../styled-components'; -export const OperationBadge = styled.span.attrs((props: { type: string }) => ({ +export const OperationBadge = styled.span.attrs((props: { type: string; color?: string }) => ({ className: `operation-type ${props.type}`, -}))<{ type: string }>` +}))<{ type: string; color?: string }>` width: 9ex; display: inline-block; height: ${props => props.theme.typography.code.fontSize}; line-height: ${props => props.theme.typography.code.fontSize}; - background-color: #333; + background-color: ${props => props.color || '#333'}; border-radius: 3px; background-repeat: no-repeat; background-position: 6px 4px; diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index f0a3a7852e..cb8bd73ddc 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -20,7 +20,12 @@ import { RequestBodyModel } from './RequestBody'; import { ResponseModel } from './Response'; import { SideNavStyleEnum } from '../types'; -import type { OpenAPIExternalDocumentation, OpenAPIServer, OpenAPIXCodeSample } from '../../types'; +import type { + OpenAPIExternalDocumentation, + OpenAPIServer, + OpenAPIXBadges, + OpenAPIXCodeSample, +} from '../../types'; import type { OpenAPIParser } from '../OpenAPIParser'; import type { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import type { MediaContentModel } from './MediaContent'; @@ -71,6 +76,7 @@ export class OperationModel implements IMenuItem { operationId?: string; operationHash?: string; httpVerb: string; + badges: OpenAPIXBadges[]; deprecated: boolean; path: string; servers: OpenAPIServer[]; @@ -112,6 +118,12 @@ export class OperationModel implements IMenuItem { : options.sideNavStyle === SideNavStyleEnum.PathOnly ? this.path : this.name; + this.badges = + operationSpec['x-badges']?.map(({ name, color, position }) => ({ + name, + color: color, + position: position || 'after', + })) || []; if (this.isCallback) { // NOTE: Callbacks by default should not inherit the specification's global `security` definition. diff --git a/src/types/open-api.ts b/src/types/open-api.ts index fd80bf8d6e..648d98e424 100644 --- a/src/types/open-api.ts +++ b/src/types/open-api.ts @@ -70,6 +70,12 @@ export interface OpenAPIXCodeSample { source: string; } +export interface OpenAPIXBadges { + name: string; + color?: string; + position?: 'before' | 'after'; +} + export interface OpenAPIOperation { tags?: string[]; summary?: string; @@ -85,6 +91,7 @@ export interface OpenAPIOperation { servers?: OpenAPIServer[]; 'x-codeSamples'?: OpenAPIXCodeSample[]; 'x-code-samples'?: OpenAPIXCodeSample[]; // deprecated + 'x-badges'?: OpenAPIXBadges[]; } export interface OpenAPIParameter { diff --git a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap index b490182b32..cefc7d3736 100644 --- a/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap +++ b/src/utils/__tests__/__snapshots__/loadAndBundleSpec.test.ts.snap @@ -581,6 +581,13 @@ and standard method from web, mobile and desktop applications. "tags": [ "pet", ], + "x-badges": [ + { + "color": "purple", + "name": "Beta", + "position": "before", + }, + ], "x-codeSamples": [ { "lang": "C#", @@ -645,6 +652,12 @@ try { "tags": [ "pet", ], + "x-badges": [ + { + "color": "purple", + "name": "Alpha", + }, + ], "x-codeSamples": [ { "lang": "PHP", @@ -883,6 +896,11 @@ try { "tags": [ "pet", ], + "x-badges": [ + { + "name": "Gamma", + }, + ], }, "post": { "description": "", diff --git a/src/utils/openapi.ts b/src/utils/openapi.ts index 17b5d619fd..33e0193a29 100644 --- a/src/utils/openapi.ts +++ b/src/utils/openapi.ts @@ -660,6 +660,7 @@ export function isRedocExtension(key: string): boolean { 'x-servers': true, 'x-tagGroups': true, 'x-traitTag': true, + 'x-badges': true, 'x-additionalPropertiesName': true, 'x-explicitMappingOnly': true, };