From f86303cc0510e4ecb3f0fbbbfa716e01c2bf8cf9 Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Mon, 3 Jun 2024 15:49:45 +0100 Subject: [PATCH] feat: add support for vendor extension renderers (#2545) * feat: add support for vendor extension renderers * fix: update the code based on PR feedback * style: resolve linting issues * fix: add the ability to render vendor extensions in the body Changed the handling of vendor extensions to allow to render vendor extensions in the body of a `Model` or `HttpOperation` between the title and the request block * clean up usage --------- Co-authored-by: Daniel A. White --- package.json | 3 +- packages/elements-core/package.json | 8 +- .../src/__fixtures__/articles/kitchen-sink.md | 8 + .../__fixtures__/articles/schema-with-refs.ts | 13 + .../src/__fixtures__/operations/put-todos.ts | 25 + .../operations/vendor-extensions.ts | 337 +++++++++++ .../src/__fixtures__/schemas/contact.json | 541 ++++++++++-------- .../src/components/Docs/Docs.stories.tsx | 6 +- .../src/components/Docs/Docs.tsx | 31 +- .../components/Docs/HttpOperation/Body.tsx | 4 +- .../Docs/HttpOperation/Callbacks.tsx | 2 + .../Docs/HttpOperation/HttpOperation.spec.tsx | 91 +++ .../HttpOperation/HttpOperation.stories.ts | 9 +- .../Docs/HttpOperation/HttpOperation.tsx | 3 + .../Docs/HttpOperation/Parameters.tsx | 3 +- .../Docs/HttpOperation/Responses.tsx | 3 +- .../Docs/HttpService/HttpService.stories.ts | 7 +- .../src/components/Docs/Model/Model.spec.tsx | 57 ++ .../components/Docs/Model/Model.stories.tsx | 7 +- .../src/components/Docs/Model/Model.tsx | 6 +- .../components/Docs/NodeVendorExtensions.tsx | 63 ++ .../src/components/Docs/story-helper.ts | 10 +- .../components/Docs/story-renderer-helper.tsx | 56 ++ .../CustomComponents/CodeComponent.tsx | 10 +- .../elements-core/src/context/Options.tsx | 8 +- packages/elements-core/src/index.ts | 3 +- packages/elements/package.json | 2 +- .../API/APIWithResponsiveSidebarLayout.tsx | 35 +- .../components/API/APIWithSidebarLayout.tsx | 29 +- .../components/API/APIWithStackedLayout.tsx | 4 + .../elements/src/containers/API.stories.tsx | 8 + packages/elements/src/containers/API.tsx | 10 + .../elements/src/containers/story-helper.tsx | 53 ++ .../elements/src/web-components/components.ts | 1 + yarn.lock | 123 ++-- 35 files changed, 1207 insertions(+), 372 deletions(-) create mode 100644 packages/elements-core/src/__fixtures__/operations/vendor-extensions.ts create mode 100644 packages/elements-core/src/components/Docs/NodeVendorExtensions.tsx create mode 100644 packages/elements-core/src/components/Docs/story-renderer-helper.tsx create mode 100644 packages/elements/src/containers/story-helper.tsx diff --git a/package.json b/package.json index 294fcdff6..334cc7280 100644 --- a/package.json +++ b/package.json @@ -122,5 +122,6 @@ "extends": [ "@commitlint/config-conventional" ] - } + }, + "dependencies": {} } diff --git a/packages/elements-core/package.json b/packages/elements-core/package.json index 0cc80efda..7f60d6093 100644 --- a/packages/elements-core/package.json +++ b/packages/elements-core/package.json @@ -58,9 +58,9 @@ }, "dependencies": { "@stoplight/http-spec": "^7.0.3", - "@stoplight/json": "^3.18.1", - "@stoplight/json-schema-ref-parser": "^9.0.5", - "@stoplight/json-schema-sampler": "0.2.3", + "@stoplight/json": "^3.21.0", + "@stoplight/json-schema-ref-parser": "^9.2.7", + "@stoplight/json-schema-sampler": "0.3.0", "@stoplight/json-schema-tree": "^4.0.0", "@stoplight/json-schema-viewer": "4.16.1", "@stoplight/markdown-viewer": "^5.7.0", @@ -70,7 +70,7 @@ "@stoplight/path": "^1.3.2", "@stoplight/react-error-boundary": "^3.0.0", "@stoplight/types": "^14.1.1", - "@stoplight/yaml": "^4.2.3", + "@stoplight/yaml": "^4.3.0", "classnames": "^2.2.6", "httpsnippet-lite": "^3.0.5", "jotai": "1.3.9", diff --git a/packages/elements-core/src/__fixtures__/articles/kitchen-sink.md b/packages/elements-core/src/__fixtures__/articles/kitchen-sink.md index 0eed689ed..dc5ca67d9 100644 --- a/packages/elements-core/src/__fixtures__/articles/kitchen-sink.md +++ b/packages/elements-core/src/__fixtures__/articles/kitchen-sink.md @@ -211,6 +211,14 @@ be the json schema object to be rendered. "type": "number", "minimum": 0, "maximum": 150 + }, + "type": { + "type": "string", + "enum": ["STANDARD", "ADMIN"], + "x-enum-descriptions": { + "STANDARD": "A standard user", + "ADMIN": "A user with administrative powers" + } } }, "required": [ diff --git a/packages/elements-core/src/__fixtures__/articles/schema-with-refs.ts b/packages/elements-core/src/__fixtures__/articles/schema-with-refs.ts index d8636b986..ccc2b3e5c 100644 --- a/packages/elements-core/src/__fixtures__/articles/schema-with-refs.ts +++ b/packages/elements-core/src/__fixtures__/articles/schema-with-refs.ts @@ -56,6 +56,19 @@ This is bundled schema with refs "property3": { "type": "boolean", "description": "Property 3" + }, + "property4": { + "type": "string", + "enum": [ + "BUSINESS", + "PERSONAL", + "OTHER" + ], + "x-enum-descriptions": { + "BUSINESS": "Enum description for BUSINESS", + "PERSONAL": "Enum description for PERSONAL", + "OTHER": "Enum description for OTHER" + } } } } diff --git a/packages/elements-core/src/__fixtures__/operations/put-todos.ts b/packages/elements-core/src/__fixtures__/operations/put-todos.ts index 4ea0cd7bc..176ce51ff 100644 --- a/packages/elements-core/src/__fixtures__/operations/put-todos.ts +++ b/packages/elements-core/src/__fixtures__/operations/put-todos.ts @@ -6,6 +6,12 @@ export const httpOperation: IHttpOperation = { method: 'put', path: '/todos/{todoId}', summary: 'Update Todo', + extensions: { + 'x-stoplight-info': { + id: 'http-operation-id', + version: '1.0.0', + }, + }, responses: [ { id: '?http-response-200?', @@ -79,10 +85,29 @@ export const httpOperation: IHttpOperation = { minimum: 0, maximum: 150, }, + plan: { + enum: ['FREE', 'BASIC', 'DELUXE'], + // @ts-ignore + 'x-enum-descriptions': { + FREE: 'A happy customer', + BASIC: 'Just what is needed', + DELUXE: 'Big bucks', + }, + }, }, required: ['name', 'age'], description: 'Here lies the user model', }, + type: { + description: 'The type of todo', + type: 'string', + enum: ['REMINDER', 'TASK'], + // @ts-ignore + 'x-enum-descriptions': { + REMINDER: 'A reminder', + TASK: 'A task', + }, + }, }, required: ['id', 'user'], }, diff --git a/packages/elements-core/src/__fixtures__/operations/vendor-extensions.ts b/packages/elements-core/src/__fixtures__/operations/vendor-extensions.ts new file mode 100644 index 000000000..614abf49a --- /dev/null +++ b/packages/elements-core/src/__fixtures__/operations/vendor-extensions.ts @@ -0,0 +1,337 @@ +import { HttpParamStyles, IHttpOperation } from '@stoplight/types'; + +export const vendorExtensions: IHttpOperation = { + id: '?vendor-extensions-id?', + iid: 'PUT_vendors', + method: 'put', + path: '/todos/{todoId}', + summary: 'Update Todo with vendor extensions', + responses: [ + { + id: '?http-response-200?', + code: '200', + description: '', + headers: [ + { + schema: { + type: 'string', + description: 'Resolver errors.', + }, + name: 'X-Stoplight-Resolver', + style: HttpParamStyles.Simple, + required: true, + }, + ], + contents: [ + { + mediaType: 'application/json', + schema: { + $schema: 'http://json-schema.org/draft-04/schema#', + title: 'Todo Full', + allOf: [ + { + $schema: 'http://json-schema.org/draft-04/schema#', + title: 'Todo Partial', + type: 'object', + properties: { + name: { + type: 'string', + }, + completed: { + type: ['boolean', 'null'], + }, + }, + required: ['name', 'completed'], + }, + { + type: 'object', + properties: { + id: { + type: 'integer', + minimum: 0, + maximum: 1000000, + }, + completed_at: { + type: ['string', 'null'], + format: 'date-time', + }, + created_at: { + type: 'string', + format: 'date-time', + }, + updated_at: { + type: 'string', + format: 'date-time', + }, + user: { + $schema: 'http://json-schema.org/draft-04/schema#', + title: 'User', + type: 'object', + properties: { + name: { + type: 'string', + description: "The user's full name.", + }, + age: { + type: 'number', + minimum: 0, + maximum: 150, + }, + type: { + type: 'string', + enum: ['STANDARD', 'ADMIN'], + 'x-enum-descriptions': { + STANDARD: 'A standard user', + ADMIN: 'A user with administrative powers', + }, + }, + }, + required: ['name', 'age'], + description: 'Here lies the user model', + }, + }, + required: ['id', 'user'], + }, + ], + }, + }, + ], + }, + ], + servers: [ + { + id: '?http-server-todos.stoplight.io?', + url: 'https://todos.stoplight.io', + }, + ], + request: { + query: [ + { + schema: { + type: 'number', + default: 1, + enum: [0, 1, 3], + exclusiveMinimum: 0, + exclusiveMaximum: 10, + minimum: 5, + maximum: 10, + }, + deprecated: true, + description: 'How many todos to limit?', + name: 'limit', + style: HttpParamStyles.Form, + required: true, + }, + { + schema: { + type: 'string', + default: '1', + enum: ['0', '1', '3'], + minLength: 0, + maxLength: 10, + }, + deprecated: true, + description: 'How many string todos to limit?', + name: 'value', + style: HttpParamStyles.Form, + }, + { + schema: { + type: 'array', + minItems: 5, + maxItems: 10, + }, + name: 'items', + style: HttpParamStyles.Form, + }, + { + schema: { + type: 'array', + minItems: 1, + maxItems: 3, + }, + name: 'items_not_exploded', + style: HttpParamStyles.Form, + explode: false, + }, + { + schema: { + type: 'array', + minItems: 1, + maxItems: 3, + }, + name: 'items_spaces', + style: HttpParamStyles.SpaceDelimited, + }, + { + schema: { + type: 'array', + minItems: 1, + maxItems: 3, + }, + name: 'items_spaces_not_exploded', + style: HttpParamStyles.SpaceDelimited, + explode: false, + }, + { + schema: { + type: 'array', + minItems: 1, + maxItems: 3, + }, + name: 'items_pipes', + style: HttpParamStyles.PipeDelimited, + }, + { + schema: { + type: 'array', + minItems: 1, + maxItems: 3, + }, + name: 'items_pipes_not_exploded', + style: HttpParamStyles.PipeDelimited, + explode: false, + }, + { + schema: { + type: 'array', + minItems: 1, + maxItems: 3, + }, + name: 'default_style_items', + }, + { + schema: { + type: 'object', + }, + name: 'nested', + style: HttpParamStyles.Form, + }, + { + schema: { + type: 'object', + }, + name: 'nested_not_exploded', + style: HttpParamStyles.Form, + explode: false, + }, + { + schema: { + type: 'boolean', + description: 'Only return completed', + }, + name: 'completed', + required: false, + style: HttpParamStyles.Form, + }, + { + schema: { + type: 'string', + enum: ['something', 'another'], + }, + name: 'type', + style: HttpParamStyles.SpaceDelimited, + }, + { + name: 'super_duper_long_parameter_name_with_unnecessary_text', + schema: { + type: 'string', + default: 'some interesting string with interesting content, but still pretty long', + }, + style: HttpParamStyles.Form, + required: true, + }, + { + name: 'optional_value_with_default', + schema: { + type: 'string', + default: 'some default value', + }, + style: HttpParamStyles.Form, + }, + ], + headers: [ + { + schema: { + type: 'string', + description: 'Your Stoplight account id', + default: 'account-id-default', + }, + name: 'b-account-id', + style: HttpParamStyles.Simple, + examples: [ + { + value: 'example id', + key: 'example', + }, + ], + }, + { + schema: { + type: 'string', + description: 'Your Stoplight account id', + default: 'account-id-default', + }, + name: 'account-id', + style: HttpParamStyles.Simple, + required: false, + examples: [ + { + value: 'example id', + key: 'example', + }, + ], + }, + { + schema: { + type: 'string', + description: 'Your Stoplight account id', + }, + name: 'message-id', + style: HttpParamStyles.Simple, + required: true, + examples: [ + { + value: 'example value', + key: 'example 1', + }, + { + value: 'another example', + key: 'example 2', + }, + { + value: 'something else', + key: 'example 3', + }, + ], + }, + ], + path: [ + { + schema: { + type: 'string', + }, + name: 'todoId', + style: HttpParamStyles.Simple, + required: true, + }, + { + schema: { + type: 'string', + }, + name: 'bAnotherId', + style: HttpParamStyles.Simple, + }, + { + schema: { + type: 'string', + }, + name: 'anotherId', + style: HttpParamStyles.Simple, + required: false, + }, + ], + }, +} as any; + +export default vendorExtensions; diff --git a/packages/elements-core/src/__fixtures__/schemas/contact.json b/packages/elements-core/src/__fixtures__/schemas/contact.json index e04189a6f..609411a4f 100644 --- a/packages/elements-core/src/__fixtures__/schemas/contact.json +++ b/packages/elements-core/src/__fixtures__/schemas/contact.json @@ -1,250 +1,293 @@ { - "type": "object", - "description": "represents a contact", - "title": "Contact", - "properties": { - "addresseeName": { - "type": "object", - "description": "Description of the name of a physical person", - "title": "Name", - "properties": { - "firstName": { - "description": "First name.", - "type": "string" - }, - "lastName": { - "description": "Last name.", - "type": "string" - }, - "title": { - "description": "Contains all the suffixes and prefixes that can be appended to a name - Mr, Miss, Pr. - E.g. \" Mr\".", - "type": "string" - }, - "maidenName": { - "description": "The name given at birth time and that may have changed after a marriage.", - "type": "string" - }, - "middleName": { - "description": "Middle name(s), for example \"Lee\" in \"John Lee Smith\".", - "type": "string" - }, - "prefix": { - "type": "string", - "description": "Name prefix (e.g. Doctor)" - }, - "suffix": { - "description": "Name suffix (e.g. Junior, III, etc).", - "type": "string" - }, - "referenceName": { - "description": "Indicator to advise if the name is a reference name", - "type": "boolean" - }, - "transliterationMethod": { - "type": "string", - "description": "The method (if applicated) that was used to transform the name from universal character (e.g. korean characters) to latin characters/phonetic transcription/..." - }, - "nameType": { - "type": "string", - "description": "the type of the reference name - When several name entities exist for a given Name element(e.g. Universal name, both Native names, Romanized name), the notion of reference name (i.e. active or main name) exists. It can be either the Universal name or the Native name/Phonetic name.", - "enum": [ - "universal", - "native", - "romanization" - ] - }, - "displayName": { - "type": "boolean", - "description": "Signifies if the name is displayed on PNR face" - }, - "fullName": { - "type": "string", - "description": "free flow , Concatenation of first/mid/last. No order, no restriction, no pattern, blank separator ...." - } - } - }, - "phone": { - "type": "object", - "description": "Phone information.", - "properties": { - "category": { - "description": "Category of the contact element", - "type": "string", - "enum": [ - "BUSINESS", - "PERSONAL", - "OTHER" - ] - }, - "addresseeName": { - "type": "string", - "description": "Adressee name (e.g. in case of emergency purpose it corresponds to name of the person to be contacted).", - "pattern": "[a-zA-Z -]" - }, - "deviceType": { - "type": "string", - "description": "Type of the device (Landline, Mobile or Fax)", - "enum": [ - "MOBILE", - "LANDLINE", - "FAX" - ] - }, - "countryCode": { - "type": "string", - "description": "Country code of the country (ISO3166-1). E.g. \"US\" for the United States", - "pattern": "[A-Z]{2}" - }, - "countryCallingCode": { - "type": "string", - "description": "Country calling code of the phone number, as defined by the International Communication Union. Examples - \"1\" for US, \"371\" for Latvia.", - "pattern": "[0-9+]{2,5}" - }, - "areaCode": { - "type": "string", - "description": "Corresponds to a regional code or a city code. The length of the field varies depending on the area.", - "pattern": "[0-9]{1,4}" - }, - "number": { - "type": "string", - "description": "Phone number. Composed of digits only. The number of digits depends on the country.", - "pattern": "[0-9]{1,15}" - }, - "extension": { - "type": "string", - "description": "Extension of the phone" - }, - "text": { - "type": "string", - "description": "String containing the full phone number - applicable only when a structured phone (i.e. countryCallingCode + number) is not provided" - } - }, - "title": "Phone" - }, - "address": { - "type": "object", - "description": "Address information", - "properties": { - "category": { - "description": "Category of the contact element", - "type": "string", - "enum": [ - "BUSINESS", - "PERSONAL", - "OTHER" - ] - }, - "lines": { - "type": "array", - "description": "Line 1 = Street address, Line 2 = Apartment, suite, unit, building, floor, etc", - "items": { - "type": "string" - } - }, - "postalCode": { - "type": "string", - "description": "Example: 74130" - }, - "countryCode": { - "type": "string", - "description": "ISO 3166-1 country code", - "pattern": "[a-zA-Z]{2}" - }, - "cityName": { - "type": "string", - "description": "Full city name. Example: Dublin", - "pattern": "[a-zA-Z -]{1,35}" - }, - "stateCode": { - "type": "string", - "description": "State code (two character standard IATA state code)", - "pattern": "[a-zA-Z0-9]{1,2}" - }, - "postalBox": { - "type": "string", - "description": "E.g. BP 220" - }, - "text": { - "type": "string", - "description": "Field containing a full unformatted address. Only applicable when the fields lines, postalCode, countryCode, cityName are not filled." - } - }, - "title": "Address" - }, - "email": { - "type": "object", - "description": "Email information.", - "properties": { - "category": { - "description": "Category of the contact element", - "type": "string", - "enum": [ - "BUSINESS", - "PERSONAL", - "OTHER" - ] - }, - "address": { - "type": "string", - "description": "Email address (e.g. john@smith.com)" - } - }, - "title": "Email" - }, - "notificationType": { - "description": "the preferred method of notifying this Contact of events", - "type": "string", - "enum": [ - "CALL", - "TEXT" - ] - }, - "language": { - "description": "the preferred language of communication with this Contact", - "type": "string" - }, - "purpose": { - "description": "the purpose for which this contact is to be used", - "type": "array", - "items": { - "type": "string", - "enum": [ - "STANDARD", - "NOTIFICATION", - "EMERGENCY" - ] - } - }, - "isDeclined": { - "type": "boolean", - "description": "Justification why the \"contact\" structure is empty - the subject was asked, if they wish to provide contact details, but they decided to decline to provide it for the purpose listed above" - }, - "comment": { - "description": "a general comment about this Contact", - "type": "string" - }, - "source": { - "description": "The source system which added the Contact information", - "type": "string", - "enum": [ - "RESERVATION", - "DCS" - ], - "example": "DCS" - }, - "priority": { - "description": "The priority for this Contact information", - "type": "string", - "enum": [ - "HIGH", - "MEDIUM", - "LOW" - ], - "example": "HIGH" - }, - "isThirdParty": { - "type": "boolean", - "description": "If set, this flag indicates that the contact belongs to an other person than the passenger (e.g. friend or family member not part of the trip). This option is only available for mobile phone and email and for a notification purpose." - } - } -} \ No newline at end of file + "type": "object", + "description": "represents a contact", + "title": "Contact", + "properties": { + "addresseeName": { + "type": "object", + "description": "Description of the name of a physical person", + "title": "Name", + "properties": { + "firstName": { + "description": "First name.", + "type": "string" + }, + "lastName": { + "description": "Last name.", + "type": "string" + }, + "title": { + "description": "Contains all the suffixes and prefixes that can be appended to a name - Mr, Miss, Pr. - E.g. \" Mr\".", + "type": "string" + }, + "maidenName": { + "description": "The name given at birth time and that may have changed after a marriage.", + "type": "string" + }, + "middleName": { + "description": "Middle name(s), for example \"Lee\" in \"John Lee Smith\".", + "type": "string" + }, + "prefix": { + "type": "string", + "description": "Name prefix (e.g. Doctor)" + }, + "suffix": { + "description": "Name suffix (e.g. Junior, III, etc).", + "type": "string" + }, + "referenceName": { + "description": "Indicator to advise if the name is a reference name", + "type": "boolean" + }, + "transliterationMethod": { + "type": "string", + "description": "The method (if applicated) that was used to transform the name from universal character (e.g. korean characters) to latin characters/phonetic transcription/..." + }, + "nameType": { + "type": "string", + "description": "the type of the reference name - When several name entities exist for a given Name element(e.g. Universal name, both Native names, Romanized name), the notion of reference name (i.e. active or main name) exists. It can be either the Universal name or the Native name/Phonetic name.", + "x-enum-descriptions": { + "universal": "Enum description for universal", + "native": "Enum description for native", + "romanization": "Enum description for romanization" + }, + "enum": [ + "universal", + "native", + "romanization" + ] + }, + "displayName": { + "type": "boolean", + "description": "Signifies if the name is displayed on PNR face" + }, + "fullName": { + "type": "string", + "description": "free flow , Concatenation of first/mid/last. No order, no restriction, no pattern, blank separator ...." + } + } + }, + "phone": { + "type": "object", + "description": "Phone information.", + "properties": { + "category": { + "description": "Category of the contact element", + "type": "string", + "enum": [ + "BUSINESS", + "PERSONAL", + "OTHER" + ], + "x-enum-descriptions": { + "BUSINESS": "Enum description for BUSINESS", + "PERSONAL": "Enum description for PERSONAL", + "OTHER": "Enum description for OTHER" + } + }, + "addresseeName": { + "type": "string", + "description": "Adressee name (e.g. in case of emergency purpose it corresponds to name of the person to be contacted).", + "pattern": "[a-zA-Z -]" + }, + "deviceType": { + "type": "string", + "description": "Type of the device (Landline, Mobile or Fax)", + "enum": [ + "MOBILE", + "LANDLINE", + "FAX" + ], + "x-enum-descriptions": { + "MOBILE": "Enum description for MOBILE", + "LANDLINE": "Enum description for LANDLINE", + "FAX": "Enum description for FAX" + } + }, + "countryCode": { + "type": "string", + "description": "Country code of the country (ISO3166-1). E.g. \"US\" for the United States", + "pattern": "[A-Z]{2}" + }, + "countryCallingCode": { + "type": "string", + "description": "Country calling code of the phone number, as defined by the International Communication Union. Examples - \"1\" for US, \"371\" for Latvia.", + "pattern": "[0-9+]{2,5}" + }, + "areaCode": { + "type": "string", + "description": "Corresponds to a regional code or a city code. The length of the field varies depending on the area.", + "pattern": "[0-9]{1,4}" + }, + "number": { + "type": "string", + "description": "Phone number. Composed of digits only. The number of digits depends on the country.", + "pattern": "[0-9]{1,15}" + }, + "extension": { + "type": "string", + "description": "Extension of the phone" + }, + "text": { + "type": "string", + "description": "String containing the full phone number - applicable only when a structured phone (i.e. countryCallingCode + number) is not provided" + } + }, + "title": "Phone" + }, + "address": { + "type": "object", + "description": "Address information", + "properties": { + "category": { + "description": "Category of the contact element", + "type": "string", + "enum": [ + "BUSINESS", + "PERSONAL", + "OTHER" + ], + "x-enum-descriptions": { + "BUSINESS": "Enum description for BUSINESS", + "PERSONAL": "Enum description for PERSONAL", + "OTHER": "Enum description for OTHER" + } + }, + "lines": { + "type": "array", + "description": "Line 1 = Street address, Line 2 = Apartment, suite, unit, building, floor, etc", + "items": { + "type": "string" + } + }, + "postalCode": { + "type": "string", + "description": "Example: 74130" + }, + "countryCode": { + "type": "string", + "description": "ISO 3166-1 country code", + "pattern": "[a-zA-Z]{2}" + }, + "cityName": { + "type": "string", + "description": "Full city name. Example: Dublin", + "pattern": "[a-zA-Z -]{1,35}" + }, + "stateCode": { + "type": "string", + "description": "State code (two character standard IATA state code)", + "pattern": "[a-zA-Z0-9]{1,2}" + }, + "postalBox": { + "type": "string", + "description": "E.g. BP 220" + }, + "text": { + "type": "string", + "description": "Field containing a full unformatted address. Only applicable when the fields lines, postalCode, countryCode, cityName are not filled." + } + }, + "title": "Address" + }, + "email": { + "type": "object", + "description": "Email information.", + "properties": { + "category": { + "description": "Category of the contact element", + "type": "string", + "enum": [ + "BUSINESS", + "PERSONAL", + "OTHER" + ], + "x-enum-descriptions": { + "BUSINESS": "Enum description for BUSINESS", + "PERSONAL": "Enum description for PERSONAL", + "OTHER": "Enum description for OTHER" + } + }, + "address": { + "type": "string", + "description": "Email address (e.g. john@smith.com)" + } + }, + "title": "Email" + }, + "notificationType": { + "description": "the preferred method of notifying this Contact of events", + "type": "string", + "enum": [ + "CALL", + "TEXT" + ], + "x-enum-descriptions": { + "CALL": "Enum description for CALL", + "TEXT": "Enum description for TEXT" + } + }, + "language": { + "description": "the preferred language of communication with this Contact", + "type": "string" + }, + "purpose": { + "description": "the purpose for which this contact is to be used", + "type": "array", + "items": { + "type": "string", + "enum": [ + "STANDARD", + "NOTIFICATION", + "EMERGENCY" + ], + "x-enum-descriptions": { + "STANDARD": "Enum description for STANDARD", + "NOTIFICATION": "Enum description for NOTIFICATION", + "EMERGENCY": "Enum description for EMERGENCY" + } + } + }, + "isDeclined": { + "type": "boolean", + "description": "Justification why the \"contact\" structure is empty - the subject was asked, if they wish to provide contact details, but they decided to decline to provide it for the purpose listed above" + }, + "comment": { + "description": "a general comment about this Contact", + "type": "string" + }, + "source": { + "description": "The source system which added the Contact information", + "type": "string", + "enum": [ + "RESERVATION", + "DCS" + ], + "x-enum-descriptions": { + "RESERVATION": "Enum description for RESERVATION", + "DCS": "Enum description for DCS" + }, + "example": "DCS" + }, + "priority": { + "description": "The priority for this Contact information", + "type": "string", + "enum": [ + "HIGH", + "MEDIUM", + "LOW" + ], + "x-enum-descriptions": { + "HIGH": "Enum description for HIGH", + "MEDIUM": "Enum description for MEDIUM", + "LOW": "Enum description for LOW" + }, + "example": "HIGH" + }, + "isThirdParty": { + "type": "boolean", + "description": "If set, this flag indicates that the contact belongs to an other person than the passenger (e.g. friend or family member not part of the trip). This option is only available for mobile phone and email and for a notification purpose." + } + } +} diff --git a/packages/elements-core/src/components/Docs/Docs.stories.tsx b/packages/elements-core/src/components/Docs/Docs.stories.tsx index 62a2dcdf6..2068948bd 100644 --- a/packages/elements-core/src/components/Docs/Docs.stories.tsx +++ b/packages/elements-core/src/components/Docs/Docs.stories.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import model from '../../__fixtures__/schemas/contact.json'; import { Docs, DocsProps } from './Docs'; +import { renderExtensionRenderer } from './story-renderer-helper'; export default { title: 'Internal/Docs', @@ -21,9 +22,12 @@ export default { }, } as Meta; -export const DocsStory: Story = args => ; +export const DocsStory: Story = args => { + return ; +}; DocsStory.args = { nodeType: NodeType.Model, nodeData: model, + renderExtensionAddon: renderExtensionRenderer, }; DocsStory.storyName = 'Docs Playground'; diff --git a/packages/elements-core/src/components/Docs/Docs.tsx b/packages/elements-core/src/components/Docs/Docs.tsx index 2fe59f9a2..1fc77aab3 100644 --- a/packages/elements-core/src/components/Docs/Docs.tsx +++ b/packages/elements-core/src/components/Docs/Docs.tsx @@ -1,3 +1,4 @@ +import { SchemaNode } from '@stoplight/json-schema-tree'; import type { NodeHasChangedFn, NodeType } from '@stoplight/types'; import { Location } from 'history'; import * as React from 'react'; @@ -15,6 +16,22 @@ import { Model } from './Model'; type NodeUnsupportedFn = (err: 'dataEmpty' | 'invalidType' | Error) => void; +export type VendorExtensionsData = Record; + +/** + * A set of props that are passed to the extension renderer + */ +export type ExtensionRowProps = { + schemaNode: SchemaNode; + nestingLevel: number; + vendorExtensions: VendorExtensionsData; +}; + +/** + * Renderer function for rendering an vendor extension + */ +export type ExtensionAddonRenderer = (props: ExtensionRowProps) => React.ReactNode; + interface BaseDocsProps { /** * CSS class to add to the root container. @@ -126,6 +143,13 @@ interface BaseDocsProps { * @default undefined */ nodeUnsupported?: NodeUnsupportedFn; + + /** + * Allows to define renderers for vendor extensions + * @type {ExtensionAddonRenderer} + * @default undefined + */ + renderExtensionAddon?: ExtensionAddonRenderer; } export interface DocsProps extends BaseDocsProps { @@ -151,6 +175,7 @@ export const Docs = React.memo( refResolver, maxRefDepth, nodeHasChanged, + renderExtensionAddon, ...commonProps }) => { const parsedNode = useParsedData(nodeType, nodeData); @@ -170,7 +195,11 @@ export const Docs = React.memo( ); } - return {elem}; + return ( + + {elem} + + ); }, ); diff --git a/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx b/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx index 89f12cd46..f90d2792e 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx +++ b/packages/elements-core/src/components/Docs/HttpOperation/Body.tsx @@ -26,7 +26,7 @@ export const isBodyEmpty = (body?: BodyProps['body']) => { export const Body = ({ body, onChange }: BodyProps) => { const [refResolver, maxRefDepth] = useSchemaInlineRefResolver(); const [chosenContent, setChosenContent] = React.useState(0); - const { nodeHasChanged } = useOptionsCtx(); + const { nodeHasChanged, renderExtensionAddon } = useOptionsCtx(); React.useEffect(() => { onChange?.(chosenContent); @@ -55,7 +55,6 @@ export const Body = ({ body, onChange }: BodyProps) => { )} - {description && ( @@ -71,6 +70,7 @@ export const Body = ({ body, onChange }: BodyProps) => { viewMode="write" renderRootTreeLines nodeHasChanged={nodeHasChanged} + renderExtensionAddon={renderExtensionAddon} /> )} diff --git a/packages/elements-core/src/components/Docs/HttpOperation/Callbacks.tsx b/packages/elements-core/src/components/Docs/HttpOperation/Callbacks.tsx index e074d1728..205828975 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/Callbacks.tsx +++ b/packages/elements-core/src/components/Docs/HttpOperation/Callbacks.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useOptionsCtx } from '../../../context/Options'; import { MarkdownViewer } from '../../MarkdownViewer'; +import { ExtensionAddonRenderer } from '../Docs'; import { SectionSubtitle, SectionTitle } from '../Sections'; import { OperationHeader } from './HttpOperation'; import { Request } from './Request'; @@ -17,6 +18,7 @@ export interface CallbacksProps { export interface CallbackProps { data: IHttpCallbackOperation; isCompact?: boolean; + renderExtensionAddon?: ExtensionAddonRenderer; } export const Callbacks = ({ callbacks, isCompact }: CallbacksProps) => { diff --git a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.spec.tsx b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.spec.tsx index 17f8a4385..1de7daab8 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.spec.tsx +++ b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.spec.tsx @@ -7,9 +7,11 @@ import { MemoryRouter } from 'react-router-dom'; import httpOperation from '../../../__fixtures__/operations/put-todos'; import requestBody from '../../../__fixtures__/operations/request-body'; +import { ElementsOptionsProvider } from '../../../context/Options'; import { withPersistenceBoundary } from '../../../context/Persistence'; import { withMosaicProvider } from '../../../hoc/withMosaicProvider'; import { chooseOption } from '../../../utils/tests/chooseOption'; +import { renderExtensionRenderer } from '../story-renderer-helper'; import { HttpOperation as HttpOperationWithoutPersistence } from './index'; const _HttpOperation = withMosaicProvider(withPersistenceBoundary(HttpOperationWithoutPersistence)); @@ -680,6 +682,95 @@ describe('HttpOperation', () => { unmount(); }); }); + + describe('Vendor Extensions', () => { + it('should call rendorExtensionAddon', async () => { + const vendorExtensionRenderer = jest.fn(); + const { unmount } = render( + + + , + ); + + expect(vendorExtensionRenderer).toHaveBeenLastCalledWith( + expect.objectContaining({ + nestingLevel: 1, + vendorExtensions: { + 'x-enum-descriptions': expect.objectContaining({ REMINDER: 'A reminder', TASK: 'A task' }), + }, + }), + ); + + unmount(); + }); + + it('should display vendor extensions in body', async () => { + const vendorExtensionRenderer = jest.fn().mockImplementation(props => { + if ('x-stoplight-info' in props.vendorExtensions) { + return
Stoplight Information Extension
; + } + + return null; + }); + + const { unmount } = render( + + + , + ); + + expect(screen.queryByText('Stoplight Information Extension')).toBeInTheDocument(); + + unmount(); + }); + + it('should display vendor extensions', async () => { + const vendorExtensionRenderer = jest.fn().mockImplementation(props => { + return renderExtensionRenderer(props); + }); + + const { unmount } = render( + + + , + ); + + expect(screen.queryAllByRole('columnheader', { name: /Enum value/i })).toHaveLength(2); + expect(screen.queryAllByRole('columnheader', { name: /Description/i })).toHaveLength(2); + + expect(screen.queryByText('A reminder')).toBeInTheDocument(); + expect(screen.queryByText('A task')).toBeInTheDocument(); + + unmount(); + }); + }); }); function getDeprecatedBadge() { diff --git a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.stories.ts b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.stories.ts index f1bdbe974..5eaf56e40 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.stories.ts +++ b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.stories.ts @@ -1,15 +1,14 @@ import { httpOperation } from '../../../__fixtures__/operations/put-todos'; -import { xcodeSamples } from '../../../__fixtures__/operations/x-code-samples'; import { createStoriesForDocsComponent } from '../story-helper'; +import { renderExtensionRenderer } from '../story-renderer-helper'; import { HttpOperation } from './HttpOperation'; const { meta, createHoistedStory } = createStoriesForDocsComponent(HttpOperation, 'HttpOperation'); export default meta; -export const Story = createHoistedStory({ data: httpOperation, layoutOptions: { compact: 600 } }); - -export const StoryWithCustomCodeSamples = createHoistedStory({ - data: xcodeSamples, +export const Story = createHoistedStory({ + data: httpOperation, + renderExtensionAddon: renderExtensionRenderer, layoutOptions: { compact: 600 }, }); diff --git a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx index d2b11c697..468e42947 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx +++ b/packages/elements-core/src/components/Docs/HttpOperation/HttpOperation.tsx @@ -14,6 +14,7 @@ import { isHttpOperation, isHttpWebhookOperation } from '../../../utils/guards'; import { MarkdownViewer } from '../../MarkdownViewer'; import { chosenServerAtom, TryItWithRequestSamples } from '../../TryIt'; import { DocsComponentProps } from '..'; +import { NodeVendorExtensions } from '../NodeVendorExtensions'; import { TwoColumnLayout } from '../TwoColumnLayout'; import { DeprecatedBadge, InternalBadge } from './Badges'; import { Callbacks } from './Callbacks'; @@ -85,6 +86,8 @@ const HttpOperationComponent = React.memo(
)} + + {data.responses && ( diff --git a/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx b/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx index 71ee6a42c..ef6d8813c 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx +++ b/packages/elements-core/src/components/Docs/HttpOperation/Parameters.tsx @@ -34,7 +34,7 @@ const defaultStyle = { } as const; export const Parameters: React.FunctionComponent = ({ parameters, parameterType }) => { - const { nodeHasChanged } = useOptionsCtx(); + const { nodeHasChanged, renderExtensionAddon } = useOptionsCtx(); const [refResolver, maxRefDepth] = useSchemaInlineRefResolver(); const schema = React.useMemo( @@ -51,6 +51,7 @@ export const Parameters: React.FunctionComponent = ({ parameter schema={schema} disableCrumbs nodeHasChanged={nodeHasChanged} + renderExtensionAddon={renderExtensionAddon} /> ); }; diff --git a/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx b/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx index 0ab3906c2..b31eb5096 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx +++ b/packages/elements-core/src/components/Docs/HttpOperation/Responses.tsx @@ -166,7 +166,7 @@ const Response = ({ response, onMediaTypeChange }: ResponseProps) => { const { contents = [], headers = [], description } = response; const [chosenContent, setChosenContent] = React.useState(0); const [refResolver, maxRefDepth] = useSchemaInlineRefResolver(); - const { nodeHasChanged } = useOptionsCtx(); + const { nodeHasChanged, renderExtensionAddon } = useOptionsCtx(); const responseContent = contents[chosenContent]; const schema = responseContent?.schema; @@ -217,6 +217,7 @@ const Response = ({ response, onMediaTypeChange }: ResponseProps) => { parentCrumbs={['responses', response.code]} renderRootTreeLines nodeHasChanged={nodeHasChanged} + renderExtensionAddon={renderExtensionAddon} /> )} diff --git a/packages/elements-core/src/components/Docs/HttpService/HttpService.stories.ts b/packages/elements-core/src/components/Docs/HttpService/HttpService.stories.ts index 259694852..dbc6ed0e3 100644 --- a/packages/elements-core/src/components/Docs/HttpService/HttpService.stories.ts +++ b/packages/elements-core/src/components/Docs/HttpService/HttpService.stories.ts @@ -1,9 +1,14 @@ import { httpService } from '../../../__fixtures__/services/petstore'; import { createStoriesForDocsComponent } from '../story-helper'; +import { renderExtensionRenderer } from '../story-renderer-helper'; import { HttpService } from './HttpService'; const { meta, createHoistedStory } = createStoriesForDocsComponent(HttpService, 'HttpService'); export default meta; -export const Story = createHoistedStory({ data: httpService, layoutOptions: { compact: 600 } }); +export const Story = createHoistedStory({ + data: httpService, + layoutOptions: { compact: 600 }, + renderExtensionAddon: renderExtensionRenderer, +}); diff --git a/packages/elements-core/src/components/Docs/Model/Model.spec.tsx b/packages/elements-core/src/components/Docs/Model/Model.spec.tsx index 4a07ad0e1..6c3a39c5e 100644 --- a/packages/elements-core/src/components/Docs/Model/Model.spec.tsx +++ b/packages/elements-core/src/components/Docs/Model/Model.spec.tsx @@ -4,8 +4,10 @@ import userEvent from '@testing-library/user-event'; import { JSONSchema7 } from 'json-schema'; import * as React from 'react'; +import { ElementsOptionsProvider } from '../../../context/Options'; import * as exampleGenerationUtils from '../../../utils/exampleGeneration/exampleGeneration'; import { chooseOption } from '../../../utils/tests/chooseOption'; +import { renderExtensionRenderer } from '../story-renderer-helper'; import { Model } from './Model'; const generatedExample = '{\n"iamtoobig": "string",\n"name": "string",\n"id": "number",\n"email": "string", \n}'; @@ -16,6 +18,10 @@ const exampleSchema: JSONSchema7 = { propA: { type: 'string', enum: ['valueA'], + // @ts-ignore + 'x-enum-descriptions': { + valueA: 'description of valueA', + }, }, }, }; @@ -32,6 +38,7 @@ describe('Model', () => { expect(screen.getByRole('heading', { name: /example/i })).toBeInTheDocument(); expect(container).toHaveTextContent('"propA": "valueA"'); }); + it('does not show examples with more lines than supported, by default', async () => { jest.spyOn(exampleGenerationUtils, 'exceedsSize').mockImplementation((example: string, size: number = 2) => { return example.split(/\r\n|\r|\n/).length > size; @@ -181,4 +188,54 @@ describe('Model', () => { expect(exportButton).not.toBeInTheDocument(); }); }); + + describe('Vendor Extensions', () => { + it('should call rendorExtensionAddon', async () => { + const vendorExtensionRenderer = jest.fn(); + const { unmount } = render( + + + , + ); + + expect(vendorExtensionRenderer).toHaveBeenLastCalledWith( + expect.objectContaining({ + nestingLevel: 1, + vendorExtensions: { + 'x-enum-descriptions': expect.objectContaining({ valueA: 'description of valueA' }), + }, + }), + ); + + unmount(); + }); + + it('should display vendor extensions', async () => { + const vendorExtensionRenderer = jest.fn().mockImplementation(props => { + return renderExtensionRenderer(props); + }); + + const { unmount } = render( + + + , + ); + + expect(screen.queryByRole('columnheader', { name: /Enum value/i })).toBeInTheDocument(); + expect(screen.queryByRole('columnheader', { name: /Description/i })).toBeInTheDocument(); + + expect(screen.queryByText('description of valueA')).toBeInTheDocument(); + + unmount(); + }); + }); }); diff --git a/packages/elements-core/src/components/Docs/Model/Model.stories.tsx b/packages/elements-core/src/components/Docs/Model/Model.stories.tsx index 5775fef6d..30715733a 100644 --- a/packages/elements-core/src/components/Docs/Model/Model.stories.tsx +++ b/packages/elements-core/src/components/Docs/Model/Model.stories.tsx @@ -2,10 +2,15 @@ import { JSONSchema7 } from 'json-schema'; import model from '../../../__fixtures__/schemas/contact.json'; import { createStoriesForDocsComponent } from '../story-helper'; +import { renderExtensionRenderer } from '../story-renderer-helper'; import { Model } from './Model'; const { meta, createHoistedStory } = createStoriesForDocsComponent(Model); export default meta; -export const Story = createHoistedStory({ data: model as JSONSchema7, layoutOptions: { compact: 600 } }); +export const Story = createHoistedStory({ + data: model as JSONSchema7, + renderExtensionAddon: renderExtensionRenderer, + layoutOptions: { compact: 600 }, +}); diff --git a/packages/elements-core/src/components/Docs/Model/Model.tsx b/packages/elements-core/src/components/Docs/Model/Model.tsx index 0e8f997a7..a9747d172 100644 --- a/packages/elements-core/src/components/Docs/Model/Model.tsx +++ b/packages/elements-core/src/components/Docs/Model/Model.tsx @@ -16,6 +16,7 @@ import { MarkdownViewer } from '../../MarkdownViewer'; import { DocsComponentProps } from '..'; import { DeprecatedBadge, InternalBadge } from '../HttpOperation/Badges'; import { ExportButton } from '../HttpService/ExportButton'; +import { NodeVendorExtensions } from '../NodeVendorExtensions'; import { TwoColumnLayout } from '../TwoColumnLayout'; export type ModelProps = DocsComponentProps; @@ -29,7 +30,7 @@ const ModelComponent: React.FC = ({ }) => { const [resolveRef, maxRefDepth] = useSchemaInlineRefResolver(); const data = useResolvedObject(unresolvedData) as JSONSchema7; - const { nodeHasChanged } = useOptionsCtx(); + const { nodeHasChanged, renderExtensionAddon } = useOptionsCtx(); const { ref: layoutRef, isCompact } = useIsCompact(layoutOptions); @@ -77,6 +78,8 @@ const ModelComponent: React.FC = ({ )} + + {isCompact && modelExamples} = ({ maxRefDepth={maxRefDepth} schema={getOriginalObject(data)} nodeHasChanged={nodeHasChanged} + renderExtensionAddon={renderExtensionAddon} skipTopLevelDescription /> diff --git a/packages/elements-core/src/components/Docs/NodeVendorExtensions.tsx b/packages/elements-core/src/components/Docs/NodeVendorExtensions.tsx new file mode 100644 index 000000000..f4fce4eb6 --- /dev/null +++ b/packages/elements-core/src/components/Docs/NodeVendorExtensions.tsx @@ -0,0 +1,63 @@ +import { INode } from '@stoplight/types'; +import type { JSONSchema7 } from 'json-schema'; +import { memoize } from 'lodash'; +import * as React from 'react'; + +import { useOptionsCtx } from '../../context/Options'; +import { getOriginalObject } from '../../utils/ref-resolving/resolvedObject'; + +export type NodeVendorExtensionsProps = { + /** + * The input data for the component to display. + */ + data: INode | JSONSchema7; +}; + +/** + * @private + * Resolves the vendor extensions from the given object, + * covers the case where the given data is not a INode which has already parsed + * the vendor extensions into the `extensions` property. + * + * @param data The object to extract the vendor extensions from. + */ +const getVendorExtensions = memoize((data: object) => { + const vendorExtensionNames = Object.keys(data).filter(item => item.startsWith('x-')); + const vendorExtensions = vendorExtensionNames.reduce((previousValue, currentValue, currentIndex: number) => { + return { + ...previousValue, + [currentValue]: data[currentValue], + }; + }, {}); + return vendorExtensions; +}); + +/** + * @private + * Renders the vendor extensions for a content node + */ +export const NodeVendorExtensions = React.memo(({ data }) => { + const { renderExtensionAddon } = useOptionsCtx(); + + if (!renderExtensionAddon) { + return null; + } + + const originalObject = getOriginalObject(data) as INode; + const vendorExtensions = originalObject.extensions ? originalObject.extensions : getVendorExtensions(originalObject); + const vendorExtensionKeys = Object.keys(vendorExtensions); + if (vendorExtensionKeys.length === 0) { + return null; + } + return ( + <> + {renderExtensionAddon({ + // Use nestingLevel -1 to represent the root node of the document + nestingLevel: -1, + schemaNode: originalObject as any, + vendorExtensions, + })} + + ); +}); +NodeVendorExtensions.displayName = 'NodeVendorExtensions'; diff --git a/packages/elements-core/src/components/Docs/story-helper.ts b/packages/elements-core/src/components/Docs/story-helper.ts index a87b979b0..fd06d8207 100644 --- a/packages/elements-core/src/components/Docs/story-helper.ts +++ b/packages/elements-core/src/components/Docs/story-helper.ts @@ -2,7 +2,10 @@ import type { ErrorBoundaryProps } from '@stoplight/react-error-boundary'; import type { Meta, StoryFn } from '@storybook/react'; import * as React from 'react'; -type DocsProps = { data: any } & ErrorBoundaryProps; +import { ExtensionAddonRenderer } from './Docs'; +import { wrapOptionsContext } from './story-renderer-helper'; + +type DocsProps = { data: any; renderExtensionAddon?: ExtensionAddonRenderer } & ErrorBoundaryProps; type storyOptions = DocsProps & { layoutOptions?: object }; @@ -17,7 +20,10 @@ export const createStoriesForDocsComponent = ( title?: string, ): HelperReturn => { const createStory = (name: string, input: storyOptions) => { - const story: StoryFn = (args: any) => React.createElement(Component, args); + const story: StoryFn = (args: any) => { + const component = React.createElement(Component, args); + return wrapOptionsContext(component); + }; story.args = input; story.storyName = name; return story; diff --git a/packages/elements-core/src/components/Docs/story-renderer-helper.tsx b/packages/elements-core/src/components/Docs/story-renderer-helper.tsx new file mode 100644 index 000000000..b1d2e25bc --- /dev/null +++ b/packages/elements-core/src/components/Docs/story-renderer-helper.tsx @@ -0,0 +1,56 @@ +import { isPlainObject } from '@stoplight/json'; +import { isRegularNode } from '@stoplight/json-schema-tree'; +import { MarkdownViewer } from '@stoplight/markdown-viewer'; +import { Box } from '@stoplight/mosaic'; +import * as React from 'react'; + +import { ElementsOptionsProvider } from '../../context/Options'; +import { ExtensionAddonRenderer, ExtensionRowProps } from './Docs'; + +/** + * Renders the known x-enum-Description vendor extension + * @returns React.ReactElement + */ +// eslint-disable-next-line storybook/prefer-pascal-case +export const renderExtensionRenderer: ExtensionAddonRenderer = (props: ExtensionRowProps) => { + const { nestingLevel, schemaNode, vendorExtensions } = props; + const { 'x-enum-descriptions': enumDescriptions = {} } = vendorExtensions; + + // If the nesting level is 0, we are at the root of the schema and should not render anything + if (nestingLevel === 0) { + return null; + } + + // This implementation of the extension renderer only supports the `x-enum-descriptions`-extension + if ('x-enum-descriptions' in vendorExtensions && isRegularNode(schemaNode) && isPlainObject(enumDescriptions)) { + let value = `| Enum value | Description |\n|---|---|\n`; + + for (const enumValue of schemaNode.enum ?? []) { + const description = enumDescriptions[String(enumValue)]; + value += `| ${enumValue} | ${description} |\n`; + } + + return ( + + + + ); + } + + return null; +}; + +/** + * @private + * Helper function to wrap the options context for the Docs subcomponents + */ +export const wrapOptionsContext = (story: any) => { + return {story}; +}; diff --git a/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx b/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx index 128b2d5da..505db4d67 100644 --- a/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx +++ b/packages/elements-core/src/components/MarkdownViewer/CustomComponents/CodeComponent.tsx @@ -13,6 +13,7 @@ import { useInlineRefResolver, useSchemaInlineRefResolver, } from '../../../context/InlineRefResolver'; +import { useOptionsCtx } from '../../../context/Options'; import { PersistenceContextProvider } from '../../../context/Persistence'; import { useParsedValue } from '../../../hooks/useParsedValue'; import { JSONSchema } from '../../../types'; @@ -40,6 +41,8 @@ interface ISchemaAndDescriptionProps { const SchemaAndDescription = ({ title: titleProp, schema }: ISchemaAndDescriptionProps) => { const [resolveRef, maxRefDepth] = useSchemaInlineRefResolver(); + const { renderExtensionAddon } = useOptionsCtx(); + const title = titleProp ?? schema.title; return ( @@ -52,7 +55,12 @@ const SchemaAndDescription = ({ title: titleProp, schema }: ISchemaAndDescriptio )} - + ); }; diff --git a/packages/elements-core/src/context/Options.tsx b/packages/elements-core/src/context/Options.tsx index ba2d5ea93..4f9c9f6d0 100644 --- a/packages/elements-core/src/context/Options.tsx +++ b/packages/elements-core/src/context/Options.tsx @@ -4,7 +4,7 @@ import type { DocsProps } from '../components/Docs'; const DEFAULT_CONTEXT: ElementsOptionsContextProps = {}; -export type ElementsOptionsContextProps = Pick; +export type ElementsOptionsContextProps = Pick; export const ElementsOptionsContext = React.createContext(DEFAULT_CONTEXT); @@ -16,9 +16,11 @@ export type ProviderProps = Partial & { children: React.ReactNode; }; -export function ElementsOptionsProvider({ children, nodeHasChanged }: ProviderProps) { +export function ElementsOptionsProvider({ children, nodeHasChanged, renderExtensionAddon }: ProviderProps) { return ( - + {children} ); diff --git a/packages/elements-core/src/index.ts b/packages/elements-core/src/index.ts index 6ef93f5ee..fd1df3790 100644 --- a/packages/elements-core/src/index.ts +++ b/packages/elements-core/src/index.ts @@ -1,4 +1,4 @@ -export { Docs, DocsProps, ParsedDocs } from './components/Docs'; +export { Docs, DocsProps, ExtensionAddonRenderer, ExtensionRowProps, ParsedDocs } from './components/Docs'; export { DeprecatedBadge } from './components/Docs/HttpOperation/Badges'; export { ExportButton, ExportButtonProps } from './components/Docs/HttpService/ExportButton'; export { ResponsiveSidebarLayout } from './components/Layout/ResponsiveSidebarLayout'; @@ -26,6 +26,7 @@ export { TryIt, TryItProps, TryItWithRequestSamples, TryItWithRequestSamplesProp export { HttpMethodColors, NodeTypeColors, NodeTypeIconDefs, NodeTypePrettyName } from './constants'; export { MockingProvider } from './containers/MockingProvider'; export { InlineRefResolverProvider } from './context/InlineRefResolver'; +export { ElementsOptionsProvider } from './context/Options'; export { PersistenceContextProvider, withPersistenceBoundary } from './context/Persistence'; export { RouterTypeContext } from './context/RouterType'; export { withMosaicProvider } from './hoc/withMosaicProvider'; diff --git a/packages/elements/package.json b/packages/elements/package.json index d62763473..019068360 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -68,7 +68,7 @@ "@stoplight/json": "^3.18.1", "@stoplight/mosaic": "^1.53.1", "@stoplight/types": "^14.1.1", - "@stoplight/yaml": "^4.2.3", + "@stoplight/yaml": "^4.3.0", "classnames": "^2.2.6", "file-saver": "^2.0.5", "lodash": "^4.17.21", diff --git a/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx b/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx index 3eaf96ede..640493f20 100644 --- a/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx +++ b/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx @@ -1,4 +1,10 @@ -import { ExportButtonProps, ParsedDocs, ResponsiveSidebarLayout } from '@stoplight/elements-core'; +import { + ElementsOptionsProvider, + ExportButtonProps, + ParsedDocs, + ResponsiveSidebarLayout, +} from '@stoplight/elements-core'; +import { ExtensionAddonRenderer } from '@stoplight/elements-core/components/Docs'; import { NodeType } from '@stoplight/types'; import * as React from 'react'; import { Redirect, useLocation } from 'react-router-dom'; @@ -17,6 +23,7 @@ type SidebarLayoutProps = { tryItCredentialsPolicy?: 'omit' | 'include' | 'same-origin'; tryItCorsProxy?: string; compact?: number | boolean; + renderExtensionAddon?: ExtensionAddonRenderer; }; export const APIWithResponsiveSidebarLayout: React.FC = ({ @@ -30,6 +37,7 @@ export const APIWithResponsiveSidebarLayout: React.FC = ({ exportProps, tryItCredentialsPolicy, tryItCorsProxy, + renderExtensionAddon, }) => { const container = React.useRef(null); const tree = React.useMemo( @@ -75,17 +83,20 @@ export const APIWithResponsiveSidebarLayout: React.FC = ({ name={serviceNode.name} > {node && ( - + + + )} ); diff --git a/packages/elements/src/components/API/APIWithSidebarLayout.tsx b/packages/elements/src/components/API/APIWithSidebarLayout.tsx index fceebe811..0d47d8d10 100644 --- a/packages/elements/src/components/API/APIWithSidebarLayout.tsx +++ b/packages/elements/src/components/API/APIWithSidebarLayout.tsx @@ -1,4 +1,5 @@ import { + ElementsOptionsProvider, ExportButtonProps, Logo, ParsedDocs, @@ -7,6 +8,7 @@ import { TableOfContents, TableOfContentsItem, } from '@stoplight/elements-core'; +import { ExtensionAddonRenderer } from '@stoplight/elements-core/components/Docs'; import { Flex, Heading } from '@stoplight/mosaic'; import { NodeType } from '@stoplight/types'; import * as React from 'react'; @@ -25,6 +27,7 @@ type SidebarLayoutProps = { exportProps?: ExportButtonProps; tryItCredentialsPolicy?: 'omit' | 'include' | 'same-origin'; tryItCorsProxy?: string; + renderExtensionAddon?: ExtensionAddonRenderer; }; export const APIWithSidebarLayout: React.FC = ({ @@ -37,6 +40,7 @@ export const APIWithSidebarLayout: React.FC = ({ exportProps, tryItCredentialsPolicy, tryItCorsProxy, + renderExtensionAddon, }) => { const container = React.useRef(null); const tree = React.useMemo( @@ -73,17 +77,20 @@ export const APIWithSidebarLayout: React.FC = ({ return ( {node && ( - + + + )} ); diff --git a/packages/elements/src/components/API/APIWithStackedLayout.tsx b/packages/elements/src/components/API/APIWithStackedLayout.tsx index bd058c1f3..e1fbbf425 100644 --- a/packages/elements/src/components/API/APIWithStackedLayout.tsx +++ b/packages/elements/src/components/API/APIWithStackedLayout.tsx @@ -6,6 +6,7 @@ import { ParsedDocs, TryItWithRequestSamples, } from '@stoplight/elements-core'; +import { ExtensionAddonRenderer } from '@stoplight/elements-core/components/Docs'; import { Box, Flex, Heading, Icon, Tab, TabList, TabPanel, TabPanels, Tabs } from '@stoplight/mosaic'; import { NodeType } from '@stoplight/types'; import cn from 'classnames'; @@ -33,6 +34,7 @@ type StackedLayoutProps = { tryItCorsProxy?: string; showPoweredByLink?: boolean; location: Location; + renderExtensionAddon?: ExtensionAddonRenderer; }; const itemMatchesHash = (hash: string, item: OperationNode | WebhookNode) => { @@ -73,6 +75,7 @@ export const APIWithStackedLayout: React.FC = ({ exportProps, tryItCredentialsPolicy, tryItCorsProxy, + renderExtensionAddon, showPoweredByLink = true, location, }) => { @@ -93,6 +96,7 @@ export const APIWithStackedLayout: React.FC = ({ layoutOptions={{ showPoweredByLink, hideExport }} exportProps={exportProps} tryItCredentialsPolicy={tryItCredentialsPolicy} + renderExtensionAddon={renderExtensionAddon} /> {operationGroups.length > 0 && webhookGroups.length > 0 ? Endpoints : null} diff --git a/packages/elements/src/containers/API.stories.tsx b/packages/elements/src/containers/API.stories.tsx index 7873662ba..2874bc585 100644 --- a/packages/elements/src/containers/API.stories.tsx +++ b/packages/elements/src/containers/API.stories.tsx @@ -8,6 +8,7 @@ import { simpleApiWithoutDescription } from '../__fixtures__/api-descriptions/si import { todosApiBundled } from '../__fixtures__/api-descriptions/todosApiBundled'; import { zoomApiYaml } from '../__fixtures__/api-descriptions/zoomApiYaml'; import { API, APIProps } from './API'; +import { renderExtensionRenderer } from './story-helper'; export default { title: 'Public/API', @@ -107,3 +108,10 @@ Instagram.args = { apiDescriptionUrl: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', }; Instagram.storyName = 'Instagram'; + +export const WithExtensionRenderer = Template.bind({}); +WithExtensionRenderer.args = { + renderExtensionAddon: renderExtensionRenderer, + apiDescriptionDocument: zoomApiYaml, +}; +WithExtensionRenderer.storyName = 'With Extension Renderer'; diff --git a/packages/elements/src/containers/API.tsx b/packages/elements/src/containers/API.tsx index 12c501df1..9712ef0df 100644 --- a/packages/elements/src/containers/API.tsx +++ b/packages/elements/src/containers/API.tsx @@ -11,6 +11,7 @@ import { withRouter, withStyles, } from '@stoplight/elements-core'; +import { ExtensionAddonRenderer } from '@stoplight/elements-core/components/Docs'; import { Box, Flex, Icon } from '@stoplight/mosaic'; import { flow } from 'lodash'; import * as React from 'react'; @@ -97,6 +98,11 @@ export interface CommonAPIProps extends RoutingProps { * @default undefined */ maxRefDepth?: number; + + /** + * Allows to define renderers for vendor extensions + */ + renderExtensionAddon?: ExtensionAddonRenderer; } const propsAreWithDocument = (props: APIProps): props is APIPropsWithDocument => { @@ -115,6 +121,7 @@ export const APIImpl: React.FC = props => { tryItCredentialsPolicy, tryItCorsProxy, maxRefDepth, + renderExtensionAddon, } = props; const location = useLocation(); const apiDescriptionDocument = propsAreWithDocument(props) ? props.apiDescriptionDocument : undefined; @@ -181,6 +188,7 @@ export const APIImpl: React.FC = props => { exportProps={exportProps} tryItCredentialsPolicy={tryItCredentialsPolicy} tryItCorsProxy={tryItCorsProxy} + renderExtensionAddon={renderExtensionAddon} location={location} /> )} @@ -195,6 +203,7 @@ export const APIImpl: React.FC = props => { exportProps={exportProps} tryItCredentialsPolicy={tryItCredentialsPolicy} tryItCorsProxy={tryItCorsProxy} + renderExtensionAddon={renderExtensionAddon} /> )} {layout === 'responsive' && ( @@ -208,6 +217,7 @@ export const APIImpl: React.FC = props => { exportProps={exportProps} tryItCredentialsPolicy={tryItCredentialsPolicy} tryItCorsProxy={tryItCorsProxy} + renderExtensionAddon={renderExtensionAddon} compact={isResponsiveLayoutEnabled} /> )} diff --git a/packages/elements/src/containers/story-helper.tsx b/packages/elements/src/containers/story-helper.tsx new file mode 100644 index 000000000..def6edd5b --- /dev/null +++ b/packages/elements/src/containers/story-helper.tsx @@ -0,0 +1,53 @@ +import { ElementsOptionsProvider } from '@stoplight/elements-core/context/Options'; +import { MarkdownViewer } from '@stoplight/markdown-viewer'; +import { Box } from '@stoplight/mosaic'; +import * as React from 'react'; + +/** + * Renders the known x-enum-Description vendor extension + * @returns React.ReactElement + */ +// eslint-disable-next-line storybook/prefer-pascal-case +export const renderExtensionRenderer = (props: any) => { + const { nestingLevel, schemaNode: node, vendorExtensions } = props; + + // If the nesting level is 0, we are at the root of the schema and should not render anything + if (nestingLevel === 0) { + return null; + } + + // This implementation of the extension renderer only supports the `x-enum-descriptions`-extension + if ('x-enum-descriptions' in vendorExtensions) { + const { 'x-enum-descriptions': enumDescriptions = {} } = vendorExtensions; + + let value = `| Enum value | Description |\n|---|---|\n`; + const enums = node.enum ?? []; + enums.forEach((name: string) => { + const description = enumDescriptions[name as string]; + value += `| ${name} | ${description} |\n`; + }); + + return ( + + + + ); + } + + return null; +}; + +/** + * @private + * Helper function to wrap the options context for the Docs subcomponents + */ +export const wrapOptionsContext = (story: any) => { + return {story}; +}; diff --git a/packages/elements/src/web-components/components.ts b/packages/elements/src/web-components/components.ts index 4aee9f2e9..863a63826 100644 --- a/packages/elements/src/web-components/components.ts +++ b/packages/elements/src/web-components/components.ts @@ -17,4 +17,5 @@ export const ApiElement = createElementClass(API, { tryItCredentialsPolicy: { type: 'string' }, tryItCorsProxy: { type: 'string' }, maxRefDepth: { type: 'number' }, + renderExtensionAddon: { type: 'function' }, }); diff --git a/yarn.lock b/yarn.lock index 649296f1f..3cab5fcd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3944,23 +3944,23 @@ json-schema-compare "^0.2.2" lodash "^4.17.4" -"@stoplight/json-schema-ref-parser@^9.0.5": - version "9.2.1" - resolved "https://registry.yarnpkg.com/@stoplight/json-schema-ref-parser/-/json-schema-ref-parser-9.2.1.tgz#48a55c61e7c518e9a7332c424cd53a41b405e9af" - integrity sha512-iKWeomA0HHDcbG0G8yVzQFs9y5vtF/GtjEDSuhofdHcPxXMhnTUh8d9NdbhXPCVhwIRmdvzP3jMv2A3747hyWg== +"@stoplight/json-schema-ref-parser@^9.2.7": + version "9.2.7" + resolved "https://registry.yarnpkg.com/@stoplight/json-schema-ref-parser/-/json-schema-ref-parser-9.2.7.tgz#b1320b3a8ac9783663af870667fae9a616c4bfd3" + integrity sha512-1vNzJ7iSrFTAFNbZHPyhI6GiJJw74+WaV61bARUQEDR4Jm80f9s0Tq9uCvGoMYwIFmWDJAoTiyegnUs6SvVxDw== dependencies: "@jsdevtools/ono" "^7.1.3" "@stoplight/path" "^1.3.2" "@stoplight/yaml" "^4.0.2" - abort-controller "^3.0.0" call-me-maybe "^1.0.1" fastestsmallesttextencoderdecoder "^1.0.22" isomorphic-fetch "^3.0.0" + node-abort-controller "^3.0.1" -"@stoplight/json-schema-sampler@0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@stoplight/json-schema-sampler/-/json-schema-sampler-0.2.3.tgz#025dde617ede939321db7277258e6919195deb7a" - integrity sha512-57PqNll9y/Rkfp4/t1AkVfz5C0PIrDd8i2AW/N0XU5wVJ50kIrmJg3BD+PzmVcrF3lXFH7/LojoOUkzLZXMJpg== +"@stoplight/json-schema-sampler@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@stoplight/json-schema-sampler/-/json-schema-sampler-0.3.0.tgz#0d20ccc37e886cdcb4ec1955efe7fad60938f635" + integrity sha512-G7QImi2xr9+8iPEg0D9YUi1BWhIiiEm19aMb91oWBSdxuhezOAqqRP3XNY6wczHV9jLWW18f+KkghTy9AG0BQA== dependencies: "@types/json-schema" "^7.0.7" json-pointer "^0.6.1" @@ -4002,6 +4002,18 @@ lodash "^4.17.21" safe-stable-stringify "^1.1" +"@stoplight/json@^3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.21.0.tgz#c0dff9c478f3365d7946cb6e34c17cc2fa84250b" + integrity sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g== + dependencies: + "@stoplight/ordered-object-literal" "^1.0.3" + "@stoplight/path" "^1.3.2" + "@stoplight/types" "^13.6.0" + jsonc-parser "~2.2.1" + lodash "^4.17.21" + safe-stable-stringify "^1.1" + "@stoplight/lifecycle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@stoplight/lifecycle/-/lifecycle-2.3.2.tgz#d61dff9ba20648241432e2daaef547214dc8976e" @@ -4143,6 +4155,11 @@ resolved "https://registry.yarnpkg.com/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.4.tgz#c8bb2698ab229f31e31a16dd1852c867c1f2f2ed" integrity sha512-OF8uib1jjDs5/cCU+iOVy+GJjU3X7vk/qJIkIJFqwmlJKrrtijFmqwbu8XToXrwTYLQTP+Hebws5gtZEmk9jag== +"@stoplight/ordered-object-literal@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz#06689095a4f1a53e9d9a5f0055f707c387af966a" + integrity sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg== + "@stoplight/path@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@stoplight/path/-/path-1.3.2.tgz#96e591496b72fde0f0cdae01a61d64f065bd9ede" @@ -4221,15 +4238,7 @@ "@types/json-schema" "^7.0.4" utility-types "^3.10.0" -"@stoplight/types@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-14.0.0.tgz#f444490664c2c16d5f06265fcbac8d94a33481e8" - integrity sha512-w7Ejau6TaB7RqR0vWzGJSdmgLEYD2frjgbHPZoxgGQwAq/R8Qh/D9p9Bl9JFdii+YTL5xoDjyX0c1WDRlbMV8g== - dependencies: - "@types/json-schema" "^7.0.4" - utility-types "^3.10.0" - -"@stoplight/types@^14.1.1": +"@stoplight/types@^14.0.0", "@stoplight/types@^14.1.1": version "14.1.1" resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-14.1.1.tgz#0dd5761aac25673a951955e984c724c138368b7a" integrity sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g== @@ -4242,7 +4251,12 @@ resolved "https://registry.yarnpkg.com/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz#442b21f419427acaa8a3106ebc5d73351c407002" integrity sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg== -"@stoplight/yaml@^4.0.2", "@stoplight/yaml@^4.2.2", "@stoplight/yaml@^4.2.3": +"@stoplight/yaml-ast-parser@0.0.50": + version "0.0.50" + resolved "https://registry.yarnpkg.com/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz#ed625a1d9ae63eb61980446e058fa745386ab61e" + integrity sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ== + +"@stoplight/yaml@^4.0.2", "@stoplight/yaml@^4.2.2": version "4.2.3" resolved "https://registry.yarnpkg.com/@stoplight/yaml/-/yaml-4.2.3.tgz#d177664fecd6b2fd0d4f264f1078550c30cfd8d1" integrity sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw== @@ -4252,6 +4266,16 @@ "@stoplight/yaml-ast-parser" "0.0.48" tslib "^2.2.0" +"@stoplight/yaml@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@stoplight/yaml/-/yaml-4.3.0.tgz#ca403157472509812ccec6f277185e7e65d7bd7d" + integrity sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w== + dependencies: + "@stoplight/ordered-object-literal" "^1.0.5" + "@stoplight/types" "^14.1.1" + "@stoplight/yaml-ast-parser" "0.0.50" + tslib "^2.2.0" + "@storybook/addon-controls@7.5.3": version "7.5.3" resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-7.5.3.tgz#03ce5a31603b360fe906cefb3fe4945ef7188e62" @@ -6501,13 +6525,6 @@ abbrev@^1.0.0, abbrev@~1.1.1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -7659,20 +7676,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001317: - version "1.0.30001327" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001327.tgz#c1546d7d7bb66506f0ccdad6a7d07fc6d668c858" - integrity sha512-1/Cg4jlD9qjZzhbzkzEaAC2JHsP0WrOc8Rd/3a3LuajGzGWR/hD7TVyvq99VqmTy99eVh8Zkmdq213OgvgXx7w== - -caniuse-lite@^1.0.30001541: - version "1.0.30001561" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz#752f21f56f96f1b1a52e97aae98c57c562d5d9da" - integrity sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw== - -caniuse-lite@^1.0.30001565: - version "1.0.30001568" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz#53fa9297273c9a977a560663f48cbea1767518b7" - integrity sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001565: + version "1.0.30001597" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz" + integrity sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w== capture-exit@^2.0.0: version "2.0.0" @@ -10081,11 +10088,6 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter2@6.4.7: version "6.4.7" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" @@ -18839,7 +18841,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -18857,15 +18859,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -18954,7 +18947,7 @@ stringify-object@3.3.0, stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18982,13 +18975,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -20714,7 +20700,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -20749,15 +20735,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"