diff --git a/src/core/components/parameter-include-empty.jsx b/src/core/components/parameter-include-empty.jsx index ef56663f..6656574c 100644 --- a/src/core/components/parameter-include-empty.jsx +++ b/src/core/components/parameter-include-empty.jsx @@ -1,22 +1,53 @@ -import React from "react" +import React, { Component } from "react" import cx from "classnames" import PropTypes from "prop-types" -export const ParameterIncludeEmpty = ({ isIncluded, onChange, isDisabled }) => { - const onCheckboxChange = e => { - onChange(e.target.checked) - } - return -} -ParameterIncludeEmpty.propTypes = { + +const noop = () => { } + +const ParameterIncludeEmptyPropTypes = { isIncluded: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired, + isIncludedOptions: PropTypes.object, onChange: PropTypes.func.isRequired, } -export default ParameterIncludeEmpty +const ParameterIncludeEmptyDefaultProps = { + onChange: noop, + isIncludedOptions: {}, +} +export default class ParameterIncludeEmpty extends Component { + static propTypes = ParameterIncludeEmptyPropTypes + static defaultProps = ParameterIncludeEmptyDefaultProps + + componentDidMount() { + const { isIncludedOptions, onChange } = this.props + const { shouldDispatchInit, defaultValue } = isIncludedOptions + if (shouldDispatchInit) { + onChange(defaultValue) + } + } + + onCheckboxChange = e => { + const { onChange } = this.props + onChange(e.target.checked) + } + + render() { + let { isIncluded, isDisabled } = this.props + + return ( +
+ +
+ ) + } +} diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index b21acc4c..b33dd5ef 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -53,6 +53,19 @@ const RequestBody = ({ const handleFile = (e) => { onChange(e.target.files[0]) } + const setIsIncludedOptions = (key) => { + let options = { + key, + shouldDispatchInit: false, + defaultValue: true + } + let currentInclusion = requestBodyInclusionSetting.get(key, "no value") + if (currentInclusion === "no value") { + options.shouldDispatchInit = true + // future: can get/set defaultValue from a config setting + } + return options + } const Markdown = getComponent("Markdown", true) const ModelExample = getComponent("modelExample") @@ -173,7 +186,8 @@ const RequestBody = ({ {required ? null : ( onChangeIncludeEmpty(key, value)} - isIncluded={requestBodyInclusionSetting.get(key)} + isIncluded={requestBodyInclusionSetting.get(key) || false} + isIncludedOptions={setIsIncludedOptions(key)} isDisabled={!isEmptyValue(currentValue)} /> )} diff --git a/test/e2e-cypress/static/documents/features/petstore-only-pet.openapi.yaml b/test/e2e-cypress/static/documents/features/petstore-only-pet.openapi.yaml new file mode 100644 index 00000000..2d5d0060 --- /dev/null +++ b/test/e2e-cypress/static/documents/features/petstore-only-pet.openapi.yaml @@ -0,0 +1,418 @@ +openapi: 3.0.2 +servers: + - url: /v3 +info: + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + version: 1.0.5-SNAPSHOT + title: Swagger Petstore - OpenAPI 3.0 + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + description: Create a new pet in the store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + description: Update an existent pet in the store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + explode: true + schema: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: false + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + required: true + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + description: '' + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + schemas: + Category: + x-swagger-router-model: io.swagger.petstore.model.Category + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + type: object + Tag: + x-swagger-router-model: io.swagger.petstore.model.Tag + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + type: object + Pet: + x-swagger-router-model: io.swagger.petstore.model.Pet + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + xml: + name: tag + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + type: object + ApiResponse: + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + type: object + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'https://petstore.swagger.io/oauth/authorize' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header \ No newline at end of file diff --git a/test/e2e-cypress/tests/features/oas3-request-body-allow-empty-values.js b/test/e2e-cypress/tests/features/oas3-request-body-allow-empty-values.js new file mode 100644 index 00000000..65c7383c --- /dev/null +++ b/test/e2e-cypress/tests/features/oas3-request-body-allow-empty-values.js @@ -0,0 +1,132 @@ +/** + * @prettier + */ + +describe("OpenAPI 3.0 Allow Empty Values in Request Body", () => { + it("should not apply or render to required fields", () => { + cy.visit( + "/?url=/documents/features/petstore-only-pet.openapi.yaml" + ) + .get("#operations-pet-addPet") + .click() + .get(".opblock-section .opblock-section-request-body .body-param-content-type > select") + .select("application/x-www-form-urlencoded") + // Expand Try It Out + .get(".try-out__btn") + .click() + // Request Body + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(2) > .parameters-col_description .parameter__empty_value_toggle input") + .should("not.exist") + }) + + it("by default, should be checked for all non-required fields", () => { + cy.visit( + "/?url=/documents/features/petstore-only-pet.openapi.yaml" + ) + .get("#operations-pet-addPet") + .click() + .get(".opblock-section .opblock-section-request-body .body-param-content-type > select") + .select("application/x-www-form-urlencoded") + // Expand Try It Out + .get(".try-out__btn") + .click() + // Request Body + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(5) > .parameters-col_description .parameter__empty_value_toggle input") + .should("be.checked") + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(6) > .parameters-col_description .parameter__empty_value_toggle input") + .should("be.checked") + }) + + it("checkbox should be toggle-able", () => { + cy.visit( + "/?url=/documents/features/petstore-only-pet.openapi.yaml" + ) + .get("#operations-pet-addPet") + .click() + .get(".opblock-section .opblock-section-request-body .body-param-content-type > select") + .select("application/x-www-form-urlencoded") + // Expand Try It Out + .get(".try-out__btn") + .click() + // Request Body + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(5) > .parameters-col_description .parameter__empty_value_toggle input") + .should("be.checked") + .uncheck() + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(5) > .parameters-col_description .parameter__empty_value_toggle input") + .should("not.be.checked") + }) + + it("on execute, should allow send with all empty values", () => { + cy.visit( + "/?url=/documents/features/petstore-only-pet.openapi.yaml" + ) + .get("#operations-pet-addPet") + .click() + .get(".opblock-section .opblock-section-request-body .body-param-content-type > select") + .select("application/x-www-form-urlencoded") + // Expand Try It Out + .get(".try-out__btn") + .click() + // Execute + .get(".execute.opblock-control__btn") + .click() + // cURL component + .get(".responses-wrapper .copy-paste") + .should("exist") + .get(".responses-wrapper .copy-paste textarea") + .should("contains.text", "tags=&status=") + }) + + it("on execute, should allow send with some empty values", () => { + cy.visit( + "/?url=/documents/features/petstore-only-pet.openapi.yaml" + ) + .get("#operations-pet-addPet") + .click() + .get(".opblock-section .opblock-section-request-body .body-param-content-type > select") + .select("application/x-www-form-urlencoded") + // Expand Try It Out + .get(".try-out__btn") + .click() + // Request Body + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(5) > .parameters-col_description .parameter__empty_value_toggle input") + .uncheck() + // Execute + .get(".execute.opblock-control__btn") + .click() + // cURL component + .get(".responses-wrapper .copy-paste") + .should("exist") + .get(".responses-wrapper .copy-paste textarea") + .should("contains.text", "&status=") + .should("not.contains.text", "tags=") + }) + + it("on execute, should allow send with skip all empty values", () => { + cy.visit( + "/?url=/documents/features/petstore-only-pet.openapi.yaml" + ) + .get("#operations-pet-addPet") + .click() + .get(".opblock-section .opblock-section-request-body .body-param-content-type > select") + .select("application/x-www-form-urlencoded") + // Expand Try It Out + .get(".try-out__btn") + .click() + // Request Body + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(5) > .parameters-col_description .parameter__empty_value_toggle input") + .uncheck() + .get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(6) > .parameters-col_description .parameter__empty_value_toggle input") + .uncheck() + // Execute + .get(".execute.opblock-control__btn") + .click() + // cURL component + .get(".responses-wrapper .copy-paste") + .should("exist") + .get(".responses-wrapper .copy-paste textarea") + .should("not.contains.text", "tags=") + .should("not.contains.text", "status=") + }) + +})