From 23d7260f924be8f823cc08bc95615483b5211289 Mon Sep 17 00:00:00 2001 From: kyle Date: Sat, 29 Jun 2019 19:52:51 +0100 Subject: [PATCH] feat: Multiple Examples for OpenAPI 3 Parameters, Request Bodies, and Responses (via #5427) * add opt-in Prettier config * remove legacy `examples` implementation * create ExamplesSelect * support `Response.examples` in OpenAPI 3 * create response controls group * prettier reformat * prepare to break up Parameters * reunify Parameters and OAS3 Parameters * Parameter Examples * Example component * handle parameter value stringification correctly * FOR REVIEW: add prop for controlling Select * use regular header for param examples in Try-It-Out * manage active examples member via Redux * Request Body Try-It-Out examples * remove special Response description styling * omit Example value display in Try-It-Out * support disabled text inputs in JsonSchemaForm * Example.omitValue => Example.showValue * ExamplesSelectValueRetainer * styling for disabled inputs * remove console.log * support "Modified Values" in ExamplesSelect * remove Examples component (wasn't used anywhere) * use ParameterRow.getParamKey for active examples member keying * split-rendering of examples in ParameterRow * send disabled prop to JsonSchemaForm * use content type to key request body active examples members * remove debugger * rewire RequestBodyEditor to be a controlled component REVIEW: does this have perf implications? * trigger synthetic onSelect events in ExamplesSelect * prettier updates * remove outdated Examples usage in RequestBody * don't handle examples changes in ESVR * make RequestBodyEditor semi-controlled * don't default to an empty Map for request bodies * add namespaceKey to ESVR for state mgmt * don't key RequestBody activeExampleKeys on media type * tweak ESVR isModifiedValueSelected calculation * add trace class to ExamplesSelect * remove usage of ESVR.currentNamespace * reset to first example if currentExampleKey is invalid * add default values to RequestBody rendering * stringify things in ESVR * avoid null select value (silences React warning) * detect user inputs that match any examples member's value * add trace class for json-schema-array * shallowly convert namespace state, to preserve Immutable stucts in state * stringify RBE values; don't trim JSON in editor * match user input to an example when non-primitives are expressed in state as strings * update Cypress * don't apply sample values in JsonSchema_Object * support disabling all JsonSchemaForm subcomponents * Core tests * style changes to accomodate Examples * fix version-checking error in Response * disable SCU for Responses * don't stringify Select values * ModelExample: default to Model tab if no example is available; provide a default no example message * don't trim JSON ParamBody inputs * read directly from 2.0 Response.schema instead of inferring a value * show current Example information in RequestBody * show label for Examples dropdown by default * rework Response content ordering * style disabled textareas like other read-only blocks * meta: fix sourcemaps * refactor ESVR setNameForNamespace * protect second half of ternary expession * cypress: `select.examples-select` => `.examples-select > select` * clarify ModelExample.componentWillReceiveProps * add gates/defaults to prevent issues in very bare-boned documents * fix test block organization problem * simplify RequestBodyEditor interface * linter fixes * prettier updates * use plugin system for new components * move ME Cypress helpers to other file --- .prettierrc.yaml | 5 + make-webpack-config.js | 6 +- package-lock.json | 537 +++++++++----- package.json | 3 +- src/core/components/example.jsx | 42 ++ .../examples-select-value-retainer.jsx | 222 ++++++ src/core/components/examples-select.jsx | 138 ++++ src/core/components/layout-utils.jsx | 14 +- src/core/components/model-example.jsx | 29 +- src/core/components/operation.jsx | 3 + src/core/components/param-body.jsx | 3 +- src/core/components/parameter-row.jsx | 90 ++- src/core/components/parameters.jsx | 123 ---- src/core/components/parameters/index.js | 1 + .../parameters}/parameters.jsx | 138 ++-- src/core/components/response.jsx | 162 +++-- src/core/components/responses.jsx | 39 +- src/core/json-schema-components.jsx | 61 +- src/core/plugins/oas3/actions.js | 8 + .../oas3/components/request-body-editor.jsx | 136 ++-- .../plugins/oas3/components/request-body.jsx | 122 +++- src/core/plugins/oas3/reducers.js | 5 + src/core/plugins/oas3/selectors.js | 5 + .../plugins/oas3/wrap-components/index.js | 2 - src/core/presets/base.js | 10 +- src/core/utils.js | 6 +- src/style/_form.scss | 21 + src/style/_layout.scss | 126 ++-- src/style/_table.scss | 15 + src/style/_variables.scss | 6 - test/e2e-cypress/helpers/multiple-examples.js | 679 ++++++++++++++++++ .../multiple-examples-core.openapi.yaml | 400 +++++++++++ .../tests/features/multiple-examples-core.js | 642 +++++++++++++++++ webpack-hot-dev-server.config.js | 2 +- 34 files changed, 3148 insertions(+), 653 deletions(-) create mode 100644 .prettierrc.yaml create mode 100644 src/core/components/example.jsx create mode 100644 src/core/components/examples-select-value-retainer.jsx create mode 100644 src/core/components/examples-select.jsx delete mode 100644 src/core/components/parameters.jsx create mode 100644 src/core/components/parameters/index.js rename src/core/{plugins/oas3/wrap-components => components/parameters}/parameters.jsx (59%) create mode 100644 test/e2e-cypress/helpers/multiple-examples.js create mode 100644 test/e2e-cypress/static/documents/features/multiple-examples-core.openapi.yaml create mode 100644 test/e2e-cypress/tests/features/multiple-examples-core.js diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 00000000..2e34d757 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,5 @@ +semi: false +trailingComma: es5 +endOfLine: lf +requirePragma: true +insertPragma: true diff --git a/make-webpack-config.js b/make-webpack-config.js index fab6f666..553c25ce 100644 --- a/make-webpack-config.js +++ b/make-webpack-config.js @@ -150,7 +150,11 @@ module.exports = function(rules, options) { } }, - devtool: specialOptions.sourcemaps ? "nosource-source-map" : false, + devtool: specialOptions.sourcemaps ? ( + // If we're mangling, size is a concern -- so use trace-only sourcemaps + // Otherwise, provide heavy souremaps suitable for development + specialOptions.minimize ? "nosource-source-map" : "cheap-module-source-map" + ): false, plugins, diff --git a/package-lock.json b/package-lock.json index 5b7c5b06..ee118213 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,9 +84,9 @@ } }, "@cypress/xvfb": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.3.tgz", - "integrity": "sha512-yYrK+/bgL3hwoRHMZG4r5fyLniCy1pXex5fimtewAY6vE/jsVs8Q37UsEO03tFlcmiLnQ3rBNMaZBYTi/+C1cw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", "dev": true, "requires": { "debug": "^3.1.0", @@ -103,9 +103,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } @@ -263,74 +263,6 @@ "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", "dev": true }, - "@types/blob-util": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz", - "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==", - "dev": true - }, - "@types/bluebird": { - "version": "3.5.18", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.18.tgz", - "integrity": "sha512-OTPWHmsyW18BhrnG5x8F7PzeZ2nFxmHGb42bZn79P9hl+GI5cMzyPgQTwNjbem0lJhoru/8vtjAFCUOu3+gE2w==", - "dev": true - }, - "@types/chai": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.8.tgz", - "integrity": "sha512-m812CONwdZn/dMzkIJEY0yAs4apyTkTORgfB2UsMOxgkUbC205AHnm4T8I0I5gPg9MHrFc1dJ35iS75c0CJkjg==", - "dev": true - }, - "@types/chai-jquery": { - "version": "1.1.35", - "resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.35.tgz", - "integrity": "sha512-7aIt9QMRdxuagLLI48dPz96YJdhu64p6FCa6n4qkGN5DQLHnrIjZpD9bXCvV2G0NwgZ1FAmfP214dxc5zNCfgQ==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/jquery": "*" - } - }, - "@types/jquery": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.6.tgz", - "integrity": "sha512-403D4wN95Mtzt2EoQHARf5oe/jEPhzBOBNrunk+ydQGW8WmkQ/E8rViRAEB1qEt/vssfGfNVD6ujP4FVeegrLg==", - "dev": true - }, - "@types/lodash": { - "version": "4.14.87", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.87.tgz", - "integrity": "sha512-AqRC+aEF4N0LuNHtcjKtvF9OTfqZI0iaBoe3dA6m/W+/YZJBZjBmW/QIZ8fBeXC6cnytSY9tBoFBqZ9uSCeVsw==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mocha": { - "version": "2.2.44", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz", - "integrity": "sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw==", - "dev": true - }, - "@types/sinon": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.0.tgz", - "integrity": "sha512-kcYoPw0uKioFVC/oOqafk2yizSceIQXCYnkYts9vJIwQklFRsMubTObTDrjQamUyBRd47332s85074cd/hCwxg==", - "dev": true - }, - "@types/sinon-chai": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", - "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, "JSONSelect": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/JSONSelect/-/JSONSelect-0.4.0.tgz", @@ -530,6 +462,12 @@ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true }, + "arch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", + "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==", + "dev": true + }, "are-we-there-yet": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", @@ -2422,6 +2360,12 @@ "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", "dev": true }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "buffer-indexof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", @@ -3266,13 +3210,10 @@ "dev": true }, "common-tags": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.4.0.tgz", - "integrity": "sha1-EYe+Tz1M8MBCfUP3Tu8fc1AWFMA=", - "dev": true, - "requires": { - "babel-runtime": "^6.18.0" - } + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", + "dev": true }, "commondir": { "version": "1.0.1", @@ -4679,65 +4620,97 @@ "dev": true }, "cypress": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-3.1.5.tgz", - "integrity": "sha512-jzYGKJqU1CHoNocPndinf/vbG28SeU+hg+4qhousT/HDBMJxYgjecXOmSgBX/ga9/TakhqSrIrSP2r6gW/OLtg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-3.3.1.tgz", + "integrity": "sha512-JIo47ZD9P3jAw7oaK7YKUoODzszJbNw41JmBrlMMiupHOlhmXvZz75htuo7mfRFPC9/1MDQktO4lX/V2+a6lGQ==", "dev": true, "requires": { "@cypress/listr-verbose-renderer": "0.4.1", - "@cypress/xvfb": "1.2.3", - "@types/blob-util": "1.3.3", - "@types/bluebird": "3.5.18", - "@types/chai": "4.0.8", - "@types/chai-jquery": "1.1.35", - "@types/jquery": "3.3.6", - "@types/lodash": "4.14.87", - "@types/minimatch": "3.0.3", - "@types/mocha": "2.2.44", - "@types/sinon": "7.0.0", - "@types/sinon-chai": "3.2.2", + "@cypress/xvfb": "1.2.4", + "arch": "2.1.1", "bluebird": "3.5.0", "cachedir": "1.3.0", - "chalk": "2.4.1", + "chalk": "2.4.2", "check-more-types": "2.24.0", - "commander": "2.11.0", - "common-tags": "1.4.0", - "debug": "3.1.0", + "commander": "2.15.1", + "common-tags": "1.8.0", + "debug": "3.2.6", "execa": "0.10.0", "executable": "4.1.1", - "extract-zip": "1.6.6", + "extract-zip": "1.6.7", "fs-extra": "4.0.1", - "getos": "3.1.0", - "glob": "7.1.2", - "is-ci": "1.0.10", + "getos": "3.1.1", + "glob": "7.1.3", + "is-ci": "1.2.1", "is-installed-globally": "0.1.0", "lazy-ass": "1.6.0", "listr": "0.12.0", "lodash": "4.17.11", "log-symbols": "2.2.0", "minimist": "1.2.0", - "moment": "2.22.2", + "moment": "2.24.0", "ramda": "0.24.1", - "request": "2.87.0", - "request-progress": "0.3.1", - "supports-color": "5.1.0", - "tmp": "0.0.31", + "request": "2.88.0", + "request-progress": "0.4.0", + "supports-color": "5.5.0", + "tmp": "0.1.0", "url": "0.11.0", - "yauzl": "2.8.0" + "yauzl": "2.10.0" }, "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, "bluebird": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", "dev": true }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -4752,12 +4725,12 @@ } }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "execa": { @@ -4775,6 +4748,56 @@ "strip-eof": "^1.0.0" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extract-zip": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", + "yauzl": "2.4.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -4797,21 +4820,45 @@ "universalify": "^0.1.0" } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "is-ci": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz", - "integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=", + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { - "ci-info": "^1.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", @@ -4830,12 +4877,51 @@ "chalk": "^2.0.1" } }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, "ramda": { "version": "0.24.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz", @@ -4843,59 +4929,112 @@ "dev": true }, "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "requires": { "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", + "aws4": "^1.8.0", "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" + "uuid": "^3.3.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" } }, "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "^2.0.0" + "has-flag": "^3.0.0" } }, "tmp": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", - "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", "dev": true, "requires": { - "os-tmpdir": "~1.0.1" + "rimraf": "^2.6.3" } }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, "yauzl": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.8.0.tgz", - "integrity": "sha1-eUUK/yKyqcWkHvVOAtuQfM+/nuI=", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, "requires": { "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.0.1" + "fd-slicer": "~1.1.0" + }, + "dependencies": { + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + } } } } @@ -7148,7 +7287,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7169,12 +7309,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7189,17 +7331,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7316,7 +7461,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7328,6 +7474,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7342,6 +7489,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7349,12 +7497,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7373,6 +7523,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7453,7 +7604,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7465,6 +7617,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7550,7 +7703,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7586,6 +7740,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7605,6 +7760,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7648,12 +7804,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -7827,21 +7985,21 @@ "dev": true }, "getos": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.0.tgz", - "integrity": "sha512-i9vrxtDu5DlLVFcrbqUqGWYlZN/zZ4pGMICCAcZoYsX3JA54nYp8r5EThw5K+m2q3wszkx4Th746JstspB0H4Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.1.tgz", + "integrity": "sha512-oUP1rnEhAr97rkitiszGP9EgDVYnmchgFzfqRzSkgtfv7ai6tEi7Ko8GgjNXts7VLWEqrTWyhsOKLe5C5b/Zkg==", "dev": true, "requires": { - "async": "2.4.0" + "async": "2.6.1" }, "dependencies": { "async": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz", - "integrity": "sha1-SZAgDxjqW4N8LMT4wDGmmFw4VhE=", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", "dev": true, "requires": { - "lodash": "^4.14.0" + "lodash": "^4.17.10" } } } @@ -11879,9 +12037,9 @@ "dev": true }, "moment": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", - "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=", + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", "dev": true }, "morgan": { @@ -12100,6 +12258,12 @@ "integrity": "sha1-VfuN62mQcHB/tn+RpGDwRIKUx30=", "dev": true }, + "node-eta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/node-eta/-/node-eta-0.1.1.tgz", + "integrity": "sha1-QGYQmzk3HHYccrfr2pqeoKXeEh8=", + "dev": true + }, "node-fetch": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", @@ -12791,6 +12955,7 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -14099,7 +14264,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true + "dev": true, + "optional": true }, "loose-envify": { "version": "1.3.1", @@ -18572,9 +18738,9 @@ "dev": true }, "prettier": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.12.1.tgz", - "integrity": "sha1-wa0g6APndJ+vkFpAnSNn4Gu+cyU=", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", + "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", "dev": true }, "pretty-bytes": { @@ -20099,12 +20265,13 @@ } }, "request-progress": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-0.3.1.tgz", - "integrity": "sha1-ByHBBdipasayzossia4tXs/Pazo=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-0.4.0.tgz", + "integrity": "sha1-wZVOOQhqqFJpxWYLzuAUKmpw1+c=", "dev": true, "requires": { - "throttleit": "~0.0.2" + "node-eta": "^0.1.1", + "throttleit": "^0.0.2" } }, "request-promise-core": { @@ -23435,7 +23602,7 @@ "webpack-addons": "^1.1.5", "yargs": "^9.0.1", "yeoman-environment": "^2.0.0", - "yeoman-generator": "git://github.com/ev1stensberg/generator.git#9e24fa31c85302ca1145ae34fc68b4f133251ca0" + "yeoman-generator": "git://github.com/ev1stensberg/generator.git#Feature-getArgument" }, "dependencies": { "ansi-escapes": { diff --git a/package.json b/package.json index 55da87f4..ea00037f 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "copy-webpack-plugin": "^4.0.1", "cors": "^2.8.4", "css-loader": "^0.28.11", - "cypress": "^3.1.5", + "cypress": "^3.3.1", "dedent": "^0.7.0", "deepmerge": "^2.1.0", "enzyme": "^2.7.1", @@ -131,6 +131,7 @@ "oauth2-server": "^2.4.1", "open": "0.0.5", "postcss-loader": "^2.1.5", + "prettier": "^1.18.2", "raw-loader": "0.5.1", "react-test-renderer": "^15.5.4", "release-it": "^7.6.1", diff --git a/src/core/components/example.jsx b/src/core/components/example.jsx new file mode 100644 index 00000000..3b0e0737 --- /dev/null +++ b/src/core/components/example.jsx @@ -0,0 +1,42 @@ +/** + * @prettier + */ + +import React from "react" +import PropTypes from "prop-types" +import ImPropTypes from "react-immutable-proptypes" +import { stringify } from "core/utils" + +export default function Example(props) { + const { example, showValue, getComponent } = props + + const Markdown = getComponent("Markdown") + const HighlightCode = getComponent("highlightCode") + + if(!example) return null + + return ( +
+ {example.get("description") ? ( +
+
Example Description
+

+ +

+
+ ) : null} + {showValue && example.has("value") ? ( +
+
Example Value
+ +
+ ) : null} +
+ ) +} + +Example.propTypes = { + example: ImPropTypes.map.isRequired, + showValue: PropTypes.bool, + getComponent: PropTypes.func.isRequired, +} diff --git a/src/core/components/examples-select-value-retainer.jsx b/src/core/components/examples-select-value-retainer.jsx new file mode 100644 index 00000000..e959105c --- /dev/null +++ b/src/core/components/examples-select-value-retainer.jsx @@ -0,0 +1,222 @@ +/** + * @prettier + */ +import React from "react" +import { Map, List } from "immutable" +import PropTypes from "prop-types" +import ImPropTypes from "react-immutable-proptypes" + +import { stringify } from "core/utils" + +// This stateful component lets us avoid writing competing values (user +// modifications vs example values) into global state, and the mess that comes +// with that: tracking which of the two values are currently used for +// Try-It-Out, which example a modified value came from, etc... +// +// The solution here is to retain the last user-modified value in +// ExamplesSelectValueRetainer's component state, so that our global state can stay +// clean, always simply being the source of truth for what value should be both +// displayed to the user and used as a value during request execution. +// +// This approach/tradeoff was chosen in order to encapsulate the particular +// logic of Examples within the Examples component tree, and to avoid +// regressions within our current implementation elsewhere (non-Examples +// definitions, OpenAPI 2.0, etc). A future refactor to global state might make +// this component unnecessary. +// +// TL;DR: this is not our usual approach, but the choice was made consciously. + +// Note that `currentNamespace` isn't currently used anywhere! + +const stringifyUnlessList = input => + List.isList(input) ? input : stringify(input) + +export default class ExamplesSelectValueRetainer extends React.PureComponent { + static propTypes = { + examples: ImPropTypes.map, + onSelect: PropTypes.func, + updateValue: PropTypes.func, // mechanism to update upstream value + getComponent: PropTypes.func.isRequired, + currentUserInputValue: PropTypes.any, + currentKey: PropTypes.string, + currentNamespace: PropTypes.string, + // (also proxies props for Examples) + } + + static defaultProps = { + examples: Map({}), + currentNamespace: "__DEFAULT__NAMESPACE__", + onSelect: (...args) => + console.log( // eslint-disable-line no-console + "ExamplesSelectValueRetainer: no `onSelect` function was provided", + ...args + ), + updateValue: (...args) => + console.log( // eslint-disable-line no-console + "ExamplesSelectValueRetainer: no `updateValue` function was provided", + ...args + ), + } + + constructor(props) { + super(props) + + const valueFromExample = this._getCurrentExampleValue() + + this.state = { + // user edited: last value that came from the world around us, and didn't + // match the current example's value + // internal: last value that came from user selecting an Example + [props.currentNamespace]: Map({ + lastUserEditedValue: this.props.currentUserInputValue, + lastDownstreamValue: valueFromExample, + isModifiedValueSelected: + // valueFromExample !== undefined && + this.props.currentUserInputValue !== valueFromExample, + }), + } + } + + _getStateForCurrentNamespace = () => { + const { currentNamespace } = this.props + + return (this.state[currentNamespace] || Map()).toObject() + } + + _setStateForCurrentNamespace = obj => { + const { currentNamespace } = this.props + + return this._setStateForNamespace(currentNamespace, obj) + } + + _setStateForNamespace = (namespace, obj) => { + const oldStateForNamespace = this.state[namespace] || Map() + const newStateForNamespace = oldStateForNamespace.mergeDeep(obj) + return this.setState({ + [namespace]: newStateForNamespace, + }) + } + + _isCurrentUserInputSameAsExampleValue = () => { + const { currentUserInputValue } = this.props + + const valueFromExample = this._getCurrentExampleValue() + + return valueFromExample === currentUserInputValue + } + + _getValueForExample = (exampleKey, props) => { + // props are accepted so that this can be used in componentWillReceiveProps, + // which has access to `nextProps` + const { examples } = props || this.props + return stringifyUnlessList( + (examples || Map({})).getIn([exampleKey, "value"]) + ) + } + + _getCurrentExampleValue = props => { + // props are accepted so that this can be used in componentWillReceiveProps, + // which has access to `nextProps` + const { currentKey } = props || this.props + return this._getValueForExample(currentKey, props || this.props) + } + + _onExamplesSelect = (key, { isSyntheticChange } = {}, ...otherArgs) => { + const { onSelect, updateValue, currentUserInputValue } = this.props + const { lastUserEditedValue } = this._getStateForCurrentNamespace() + + const valueFromExample = this._getValueForExample(key) + + if (key === "__MODIFIED__VALUE__") { + updateValue(stringifyUnlessList(lastUserEditedValue)) + return this._setStateForCurrentNamespace({ + isModifiedValueSelected: true, + }) + } + + if (typeof onSelect === "function") { + onSelect(key, { isSyntheticChange }, ...otherArgs) + } + + this._setStateForCurrentNamespace({ + lastDownstreamValue: valueFromExample, + isModifiedValueSelected: + isSyntheticChange && + !!currentUserInputValue && + currentUserInputValue !== valueFromExample, + }) + + // we never want to send up value updates from synthetic changes + if (isSyntheticChange) return + + if (typeof updateValue === "function") { + updateValue(stringifyUnlessList(valueFromExample)) + } + } + + componentWillReceiveProps(nextProps) { + // update `lastUserEditedValue` as new currentUserInput values come in + + const { currentUserInputValue: newValue, examples, onSelect } = nextProps + + const { + lastUserEditedValue, + lastDownstreamValue, + } = this._getStateForCurrentNamespace() + + const valueFromCurrentExample = this._getValueForExample( + nextProps.currentKey, + nextProps + ) + + const exampleMatchingNewValue = examples.find( + example => + example.get("value") === newValue || + // sometimes data is stored as a string (e.g. in Request Bodies), so + // let's check against a stringified version of our example too + stringify(example.get("value")) === newValue + ) + + if (exampleMatchingNewValue) { + onSelect(examples.keyOf(exampleMatchingNewValue), { + isSyntheticChange: true, + }) + } else if ( + newValue !== this.props.currentUserInputValue && // value has changed + newValue !== lastUserEditedValue && // value isn't already tracked + newValue !== lastDownstreamValue // value isn't what we've seen on the other side + ) { + this._setStateForNamespace(nextProps.currentNamespace, { + lastUserEditedValue: nextProps.currentUserInputValue, + isModifiedValueSelected: newValue !== valueFromCurrentExample, + }) + } + } + + render() { + const { currentUserInputValue, examples, currentKey, getComponent } = this.props + const { + lastDownstreamValue, + lastUserEditedValue, + isModifiedValueSelected, + } = this._getStateForCurrentNamespace() + + const ExamplesSelect = getComponent("ExamplesSelect") + + return ( + + ) + } +} diff --git a/src/core/components/examples-select.jsx b/src/core/components/examples-select.jsx new file mode 100644 index 00000000..661fe617 --- /dev/null +++ b/src/core/components/examples-select.jsx @@ -0,0 +1,138 @@ +/** + * @prettier + */ + +import React from "react" +import Im from "immutable" +import PropTypes from "prop-types" +import ImPropTypes from "react-immutable-proptypes" + +export default class ExamplesSelect extends React.PureComponent { + static propTypes = { + examples: ImPropTypes.map.isRequired, + onSelect: PropTypes.func, + currentExampleKey: PropTypes.string, + isModifiedValueAvailable: PropTypes.bool, + isValueModified: PropTypes.bool, + showLabels: PropTypes.bool, + } + + static defaultProps = { + examples: Im.Map({}), + onSelect: (...args) => + console.log( // eslint-disable-line no-console + // FIXME: remove before merging to master... + `DEBUG: ExamplesSelect was not given an onSelect callback`, + ...args + ), + currentExampleKey: null, + showLabels: true, + } + + _onSelect = (key, { isSyntheticChange = false } = {}) => { + if (typeof this.props.onSelect === "function") { + this.props.onSelect(key, { + isSyntheticChange, + }) + } + } + + _onDomSelect = e => { + if (typeof this.props.onSelect === "function") { + const element = e.target.selectedOptions[0] + const key = element.getAttribute("value") + + this._onSelect(key, { + isSyntheticChange: false, + }) + } + } + + getCurrentExample = () => { + const { examples, currentExampleKey } = this.props + + const currentExamplePerProps = examples.get(currentExampleKey) + + const firstExamplesKey = examples.keySeq().first() + const firstExample = examples.get(firstExamplesKey) + + return currentExamplePerProps || firstExample || Map({}) + } + + componentDidMount() { + // this is the not-so-great part of ExamplesSelect... here we're + // artificially kicking off an onSelect event in order to set a default + // value in state. the consumer has the option to avoid this by checking + // `isSyntheticEvent`, but we should really be doing this in a selector. + // TODO: clean this up + // FIXME: should this only trigger if `currentExamplesKey` is nullish? + const { onSelect, examples } = this.props + + if (typeof onSelect === "function") { + const firstExample = examples.first() + const firstExampleKey = examples.keyOf(firstExample) + + this._onSelect(firstExampleKey, { + isSyntheticChange: true, + }) + } + } + + componentWillReceiveProps(nextProps) { + const { currentExampleKey, examples } = nextProps + if (examples !== this.props.examples && !examples.has(currentExampleKey)) { + // examples have changed from under us, and the currentExampleKey is no longer + // valid. + const firstExample = examples.first() + const firstExampleKey = examples.keyOf(firstExample) + + this._onSelect(firstExampleKey, { + isSyntheticChange: true, + }) + } + } + + render() { + const { + examples, + currentExampleKey, + isValueModified, + isModifiedValueAvailable, + showLabels, + } = this.props + + return ( +
+ { + showLabels ? ( + Examples: + ) : null + } + +
+ ) + } +} diff --git a/src/core/components/layout-utils.jsx b/src/core/components/layout-utils.jsx index 6dfc4316..936b2e87 100644 --- a/src/core/components/layout-utils.jsx +++ b/src/core/components/layout-utils.jsx @@ -132,7 +132,8 @@ export class Select extends React.Component { onChange: PropTypes.func, multiple: PropTypes.bool, allowEmptyValue: PropTypes.bool, - className: PropTypes.string + className: PropTypes.string, + disabled: PropTypes.bool, } static defaultProps = { @@ -176,12 +177,19 @@ export class Select extends React.Component { onChange && onChange(value) } + componentWillReceiveProps(nextProps) { + // TODO: this puts us in a weird area btwn un/controlled selection... review + if(nextProps.value !== this.props.value) { + this.setState({ value: nextProps.value }) + } + } + render(){ - let { allowedValues, multiple, allowEmptyValue } = this.props + let { allowedValues, multiple, allowEmptyValue, disabled } = this.props let value = this.state.value.toJS ? this.state.value.toJS() : this.state.value return ( - { allowEmptyValue ? : null } { allowedValues.map(function (item, key) { diff --git a/src/core/components/model-example.jsx b/src/core/components/model-example.jsx index 2a7973b2..379b01b8 100644 --- a/src/core/components/model-example.jsx +++ b/src/core/components/model-example.jsx @@ -17,11 +17,19 @@ export default class ModelExample extends React.Component { super(props, context) let { getConfigs, isExecute } = this.props let { defaultModelRendering } = getConfigs() + + let activeTab = defaultModelRendering + if (defaultModelRendering !== "example" && defaultModelRendering !== "model") { - defaultModelRendering = "example" + activeTab = "example" } + + if(isExecute) { + activeTab = "example" + } + this.state = { - activeTab: isExecute ? "example" : defaultModelRendering + activeTab: activeTab } } @@ -33,8 +41,12 @@ export default class ModelExample extends React.Component { }) } - componentWillReceiveProps(props) { - if (props.isExecute && props.isExecute !== this.props.isExecute) { + componentWillReceiveProps(nextProps) { + if ( + nextProps.isExecute && + !this.props.isExecute && + this.props.example + ) { this.setState({ activeTab: "example" }) } } @@ -43,10 +55,11 @@ export default class ModelExample extends React.Component { let { getComponent, specSelectors, schema, example, isExecute, getConfigs, specPath } = this.props let { defaultModelExpandDepth } = getConfigs() const ModelWrapper = getComponent("ModelWrapper") + const HighlightCode = getComponent("highlightCode") let isOAS3 = specSelectors.isOAS3() - return
+ return
{ - this.state.activeTab === "example" && example + this.state.activeTab === "example" ? ( + example ? example : ( + + ) + ) : null } { this.state.activeTab === "model" && } @@ -214,6 +216,7 @@ export default class Operation extends PureComponent { getConfigs={ getConfigs } specSelectors={ specSelectors } oas3Actions={oas3Actions} + oas3Selectors={oas3Selectors} specActions={ specActions } produces={specSelectors.producesOptionsFor([path, method]) } producesValue={ specSelectors.currentProducesFor([path, method]) } diff --git a/src/core/components/param-body.jsx b/src/core/components/param-body.jsx index 9a0e6267..95360315 100644 --- a/src/core/components/param-body.jsx +++ b/src/core/components/param-body.jsx @@ -82,9 +82,8 @@ export default class ParamBody extends PureComponent { handleOnChange = e => { const {consumesValue} = this.props - const isJson = /json/i.test(consumesValue) const isXml = /xml/i.test(consumesValue) - const inputValue = isJson ? e.target.value.trim() : e.target.value + const inputValue = e.target.value this.onChange(inputValue, {isXml}) } diff --git a/src/core/components/parameter-row.jsx b/src/core/components/parameter-row.jsx index 713df170..2e36352b 100644 --- a/src/core/components/parameter-row.jsx +++ b/src/core/components/parameter-row.jsx @@ -1,9 +1,9 @@ import React, { Component } from "react" -import { Map } from "immutable" +import { Map, List } from "immutable" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import win from "core/window" -import { getExtensions, getCommonExtensions, numberToString } from "core/utils" +import { getExtensions, getCommonExtensions, numberToString, stringify } from "core/utils" export default class ParameterRow extends Component { static propTypes = { @@ -18,7 +18,9 @@ export default class ParameterRow extends Component { specActions: PropTypes.object.isRequired, pathMethod: PropTypes.array.isRequired, getConfigs: PropTypes.func.isRequired, - specPath: ImPropTypes.list.isRequired + specPath: ImPropTypes.list.isRequired, + oas3Actions: PropTypes.object.isRequired, + oas3Selectors: PropTypes.object.isRequired, } constructor(props, context) { @@ -29,7 +31,7 @@ export default class ParameterRow extends Component { componentWillReceiveProps(props) { let { specSelectors, pathMethod, rawParam } = props - let { isOAS3 } = specSelectors + let isOAS3 = specSelectors.isOAS3() let parameterWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || new Map() // fallback, if the meta lookup fails @@ -37,7 +39,7 @@ export default class ParameterRow extends Component { let enumValue - if(isOAS3()) { + if(isOAS3) { let schema = parameterWithMeta.get("schema") || Map() enumValue = schema.get("enum") } else { @@ -74,6 +76,15 @@ export default class ParameterRow extends Component { return onChange(rawParam, valueForUpstream, isXml) } + _onExampleSelect = (key, /* { isSyntheticChange } = {} */) => { + this.props.oas3Actions.setActiveExamplesMember({ + name: key, + pathMethod: this.props.pathMethod, + contextType: "parameters", + contextName: this.getParamKey() + }) + } + onChangeIncludeEmpty = (newValue) => { let { specActions, param, pathMethod } = this.props const paramName = param.get("name") @@ -82,10 +93,9 @@ export default class ParameterRow extends Component { } setDefaultValue = () => { - let { specSelectors, pathMethod, rawParam } = this.props - - let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) + let { specSelectors, pathMethod, rawParam, oas3Selectors } = this.props + let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map() if (!paramWithMeta || paramWithMeta.get("value") !== undefined) { return @@ -100,20 +110,32 @@ export default class ParameterRow extends Component { || paramWithMeta.getIn(["schema", "example"]) || paramWithMeta.getIn(["schema", "default"]) } else if (specSelectors.isOAS3()) { - newValue = paramWithMeta.get("example") + const currentExampleKey = oas3Selectors.activeExamplesMember(...pathMethod, "parameters", this.getParamKey()) + newValue = paramWithMeta.getIn(["examples", currentExampleKey, "value"]) + || paramWithMeta.get("example") || paramWithMeta.getIn(["schema", "example"]) || paramWithMeta.getIn(["schema", "default"]) } if(newValue !== undefined) { - this.onChangeWrapper(numberToString(newValue)) + this.onChangeWrapper( + List.isList(newValue) ? newValue : stringify(newValue) + ) } } } - render() { - let {param, rawParam, getComponent, getConfigs, isExecute, fn, onChangeConsumes, specSelectors, pathMethod, specPath} = this.props + getParamKey() { + const { param } = this.props + + if(!param) return null - let { isOAS3 } = specSelectors + return `${param.get("name")}-${param.get("in")}` + } + + render() { + let {param, rawParam, getComponent, getConfigs, isExecute, fn, onChangeConsumes, specSelectors, pathMethod, specPath, oas3Selectors} = this.props + + let isOAS3 = specSelectors.isOAS3() const { showExtensions, showCommonExtensions } = getConfigs() @@ -121,6 +143,8 @@ export default class ParameterRow extends Component { param = rawParam } + if(!rawParam) return null + // const onChangeWrapper = (value) => onChange(param, value) const JsonSchemaForm = getComponent("JsonSchemaForm") const ParamBody = getComponent("ParamBody") @@ -142,10 +166,12 @@ export default class ParameterRow extends Component { const Markdown = getComponent("Markdown") const ParameterExt = getComponent("ParameterExt") const ParameterIncludeEmpty = getComponent("ParameterIncludeEmpty") + const ExamplesSelectValueRetainer = getComponent("ExamplesSelectValueRetainer") + const Example = getComponent("Example") - let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) + let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map() let format = param.get("format") - let schema = isOAS3 && isOAS3() ? param.get("schema") : param + let schema = isOAS3 ? param.get("schema") : param let type = schema.get("type") let isFormData = inType === "formData" let isFormDataSupported = "FormData" in win @@ -199,7 +225,7 @@ export default class ParameterRow extends Component { { format && (${format})}
- { isOAS3 && isOAS3() && param.get("deprecated") ? "deprecated": null } + { isOAS3 && param.get("deprecated") ? "deprecated": null }
({ param.get("in") })
{ !showCommonExtensions || !commonExt.size ? null : commonExt.map((v, key) => )} @@ -224,11 +250,28 @@ export default class ParameterRow extends Component { {(isFormData && !isFormDataSupported) &&
Error: your browser does not support FormData
} - { bodyParam || !isExecute ? null + { + isOAS3 && param.get("examples") ? ( +
+ +
+ ) : null + } + + { bodyParam ? null : + ) : null + } + @@ -265,3 +320,4 @@ export default class ParameterRow extends Component { } } + diff --git a/src/core/components/parameters.jsx b/src/core/components/parameters.jsx deleted file mode 100644 index 5245cc12..00000000 --- a/src/core/components/parameters.jsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { Component } from "react" -import PropTypes from "prop-types" -import ImPropTypes from "react-immutable-proptypes" -import Im from "immutable" - -// More readable, just iterate over maps, only -const eachMap = (iterable, fn) => iterable.valueSeq().filter(Im.Map.isMap).map(fn) - -export default class Parameters extends Component { - - static propTypes = { - parameters: ImPropTypes.list.isRequired, - specActions: PropTypes.object.isRequired, - getComponent: PropTypes.func.isRequired, - specSelectors: PropTypes.object.isRequired, - fn: PropTypes.object.isRequired, - tryItOutEnabled: PropTypes.bool, - allowTryItOut: PropTypes.bool, - onTryoutClick: PropTypes.func, - onCancelClick: PropTypes.func, - onChangeKey: PropTypes.array, - pathMethod: PropTypes.array.isRequired, - getConfigs: PropTypes.func.isRequired, - specPath: ImPropTypes.list.isRequired, - } - - - static defaultProps = { - onTryoutClick: Function.prototype, - onCancelClick: Function.prototype, - tryItOutEnabled: false, - allowTryItOut: true, - onChangeKey: [], - specPath: [], - } - - onChange = ( param, value, isXml ) => { - let { - specActions: { changeParamByIdentity }, - onChangeKey, - } = this.props - - changeParamByIdentity(onChangeKey, param, value, isXml) - } - - onChangeConsumesWrapper = ( val ) => { - let { - specActions: { changeConsumesValue }, - onChangeKey - } = this.props - - changeConsumesValue(onChangeKey, val) - } - - render(){ - - let { - onTryoutClick, - onCancelClick, - parameters, - allowTryItOut, - tryItOutEnabled, - specPath, - - fn, - getComponent, - getConfigs, - specSelectors, - specActions, - pathMethod - } = this.props - - const ParameterRow = getComponent("parameterRow") - const TryItOutButton = getComponent("TryItOutButton") - - const isExecute = tryItOutEnabled && allowTryItOut - - return ( -
-
-
-

Parameters

-
- { allowTryItOut ? ( - - ) : null } -
- { !parameters.count() ?

No parameters

: -
- - - - - - - - - { - eachMap(parameters, (parameter, i) => ( - - )).toArray() - } - -
NameDescription
-
- } -
- ) - } -} diff --git a/src/core/components/parameters/index.js b/src/core/components/parameters/index.js new file mode 100644 index 00000000..5809afbe --- /dev/null +++ b/src/core/components/parameters/index.js @@ -0,0 +1 @@ +export { default as Parameters } from "./parameters" diff --git a/src/core/plugins/oas3/wrap-components/parameters.jsx b/src/core/components/parameters/parameters.jsx similarity index 59% rename from src/core/plugins/oas3/wrap-components/parameters.jsx rename to src/core/components/parameters/parameters.jsx index ba2f2b0f..91d82395 100644 --- a/src/core/plugins/oas3/wrap-components/parameters.jsx +++ b/src/core/components/parameters/parameters.jsx @@ -2,12 +2,11 @@ import React, { Component } from "react" import PropTypes from "prop-types" import Im, { Map, List } from "immutable" import ImPropTypes from "react-immutable-proptypes" -import { OAS3ComponentWrapFactory } from "../helpers" // More readable, just iterate over maps, only const eachMap = (iterable, fn) => iterable.valueSeq().filter(Im.Map.isMap).map(fn) -class Parameters extends Component { +export default class Parameters extends Component { constructor(props) { super(props) @@ -19,21 +18,21 @@ class Parameters extends Component { static propTypes = { parameters: ImPropTypes.list.isRequired, - specActions: PropTypes.object.isRequired, operation: PropTypes.object.isRequired, + specActions: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, - getConfigs: PropTypes.func.isRequired, specSelectors: PropTypes.object.isRequired, oas3Actions: PropTypes.object.isRequired, oas3Selectors: PropTypes.object.isRequired, fn: PropTypes.object.isRequired, tryItOutEnabled: PropTypes.bool, allowTryItOut: PropTypes.bool, - specPath: ImPropTypes.list.isRequired, onTryoutClick: PropTypes.func, onCancelClick: PropTypes.func, onChangeKey: PropTypes.array, - pathMethod: PropTypes.array.isRequired + pathMethod: PropTypes.array.isRequired, + getConfigs: PropTypes.func.isRequired, + specPath: ImPropTypes.list.isRequired, } @@ -43,6 +42,7 @@ class Parameters extends Component { tryItOutEnabled: false, allowTryItOut: true, onChangeKey: [], + specPath: [], } onChange = ( param, value, isXml ) => { @@ -51,7 +51,7 @@ class Parameters extends Component { onChangeKey, } = this.props - changeParamByIdentity( onChangeKey, param, value, isXml) + changeParamByIdentity(onChangeKey, param, value, isXml) } onChangeConsumesWrapper = ( val ) => { @@ -85,16 +85,16 @@ class Parameters extends Component { parameters, allowTryItOut, tryItOutEnabled, + specPath, fn, getComponent, getConfigs, specSelectors, specActions, + pathMethod, oas3Actions, oas3Selectors, - pathMethod, - specPath, operation } = this.props @@ -105,72 +105,79 @@ class Parameters extends Component { const RequestBody = getComponent("RequestBody", true) const isExecute = tryItOutEnabled && allowTryItOut - const { isOAS3 } = specSelectors + const isOAS3 = specSelectors.isOAS3() const requestBody = operation.get("requestBody") - const requestBodySpecPath = specPath.slice(0, -1).push("requestBody") // remove the "parameters" part - return (
+ { isOAS3 ? (
-
this.toggleTab("parameters")} className={`tab-item ${this.state.parametersVisible && "active"}`}> -

Parameters

+
this.toggleTab("parameters")} className={`tab-item ${this.state.parametersVisible && "active"}`}> +

Parameters

+
+ { operation.get("callbacks") ? + ( +
this.toggleTab("callbacks")} className={`tab-item ${this.state.callbackVisible && "active"}`}> +

Callbacks

+
+ ) : null + }
- { operation.get("callbacks") ? - ( -
this.toggleTab("callbacks")} className={`tab-item ${this.state.callbackVisible && "active"}`}> -

Callbacks

-
- ) : null - } + ) : ( +
+

Parameters

+ )} { allowTryItOut ? ( ) : null }
{this.state.parametersVisible ?
- { !parameters.count() ?

No parameters

: -
- - - - - - - - - { - eachMap(parameters, (parameter, i) => ( - - )).toArray() - } - -
NameDescription
-
- } -
: "" } + { !parameters.count() ?

No parameters

: +
+ + + + + + + + + { + eachMap(parameters, (parameter, i) => ( + + )).toArray() + } + +
NameDescription
+
+ } +
: null } {this.state.callbackVisible ?
-
: "" } +
: null } { - isOAS3() && requestBody && this.state.parametersVisible && + isOAS3 && requestBody && this.state.parametersVisible &&

Request body

@@ -186,10 +193,24 @@ class Parameters extends Component {
{ + this.props.oas3Actions.setActiveExamplesMember({ + name: key, + pathMethod: this.props.pathMethod, + contextType: "requestBody", + contextName: "requestBody" // RBs are currently not stored per-mediaType + }) + } + } onChange={(value, path) => { if(path) { const lastValue = oas3Selectors.requestBodyValue(...pathMethod) @@ -209,6 +230,3 @@ class Parameters extends Component { ) } } - - -export default OAS3ComponentWrapFactory(Parameters) diff --git a/src/core/components/response.jsx b/src/core/components/response.jsx index 58af705e..563b803f 100644 --- a/src/core/components/response.jsx +++ b/src/core/components/response.jsx @@ -5,19 +5,11 @@ import cx from "classnames" import { fromJS, Seq, Iterable, List, Map } from "immutable" import { getSampleSchema, fromJSOrdered, stringify } from "core/utils" -const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => { - if ( examples && examples.size ) { - return examples.entrySeq().map( ([ key, example ]) => { - let exampleValue = stringify(example) - - return (
-
{ key }
- -
) - }).toArray() - } - - if ( sampleResponse ) { return
+const getExampleComponent = ( sampleResponse, HighlightCode ) => { + if ( + sampleResponse !== undefined && + sampleResponse !== null + ) { return
} @@ -29,20 +21,24 @@ export default class Response extends React.Component { super(props, context) this.state = { - responseContentType: "" + responseContentType: "", } } static propTypes = { + path: PropTypes.string.isRequired, + method: PropTypes.string.isRequired, code: PropTypes.string.isRequired, response: PropTypes.instanceOf(Iterable), className: PropTypes.string, getComponent: PropTypes.func.isRequired, getConfigs: PropTypes.func.isRequired, specSelectors: PropTypes.object.isRequired, + oas3Actions: PropTypes.object.isRequired, specPath: ImPropTypes.list.isRequired, fn: PropTypes.object.isRequired, contentType: PropTypes.string, + activeExamplesKey: PropTypes.string, controlsAcceptHeader: PropTypes.bool, onContentTypeChange: PropTypes.func } @@ -61,8 +57,21 @@ export default class Response extends React.Component { }) } + getTargetExamplesKey = () => { + const { response, contentType, activeExamplesKey } = this.props + + const activeContentType = this.state.responseContentType || contentType + const activeMediaType = response.getIn(["content", activeContentType], Map({})) + const examplesForMediaType = activeMediaType.get("examples", null) + + const firstExamplesKey = examplesForMediaType.keySeq().first() + return activeExamplesKey || firstExamplesKey + } + render() { let { + path, + method, code, response, className, @@ -72,14 +81,14 @@ export default class Response extends React.Component { getConfigs, specSelectors, contentType, - controlsAcceptHeader + controlsAcceptHeader, + oas3Actions, } = this.props let { inferSchema } = fn - let { isOAS3 } = specSelectors + let isOAS3 = specSelectors.isOAS3() let headers = response.get("headers") - let examples = response.get("examples") let links = response.get("links") const Headers = getComponent("headers") const HighlightCode = getComponent("highlightCode") @@ -87,44 +96,53 @@ export default class Response extends React.Component { const Markdown = getComponent( "Markdown" ) const OperationLink = getComponent("operationLink") const ContentType = getComponent("contentType") + const ExamplesSelect = getComponent("ExamplesSelect") + const Example = getComponent("Example") + var sampleResponse - var sampleSchema var schema, specPathWithPossibleSchema const activeContentType = this.state.responseContentType || contentType + const activeMediaType = response.getIn(["content", activeContentType], Map({})) + const examplesForMediaType = activeMediaType.get("examples", null) - if(isOAS3()) { - const mediaType = response.getIn(["content", activeContentType], Map({})) - const oas3SchemaForContentType = mediaType.get("schema", Map({})) + // Goal: find a schema value for `schema` + if(isOAS3) { + const oas3SchemaForContentType = activeMediaType.get("schema", Map({})) - if(mediaType.get("example") !== undefined) { - sampleSchema = stringify(mediaType.get("example")) - } else { - sampleSchema = getSampleSchema(oas3SchemaForContentType.toJS(), this.state.responseContentType, { - includeReadOnly: true - }) - } - sampleResponse = oas3SchemaForContentType ? sampleSchema : null schema = oas3SchemaForContentType ? inferSchema(oas3SchemaForContentType.toJS()) : null specPathWithPossibleSchema = oas3SchemaForContentType ? List(["content", this.state.responseContentType, "schema"]) : specPath } else { - schema = inferSchema(response.toJS()) // TODO: don't convert back and forth. Lets just stick with immutable for inferSchema + schema = response.get("schema") specPathWithPossibleSchema = response.has("schema") ? specPath.push("schema") : specPath - sampleResponse = schema ? getSampleSchema(schema, activeContentType, { + } + + // Goal: find an example value for `sampleResponse` + if(isOAS3) { + const oas3SchemaForContentType = activeMediaType.get("schema", Map({})) + + if(examplesForMediaType) { + const targetExamplesKey = this.getTargetExamplesKey() + const targetExample = examplesForMediaType.get(targetExamplesKey, Map({})) + sampleResponse = stringify(targetExample.get("value")) + } else if(activeMediaType.get("example") !== undefined) { + // use the example key's value + sampleResponse = stringify(activeMediaType.get("example")) + } else { + // use an example value generated based on the schema + sampleResponse = getSampleSchema(oas3SchemaForContentType.toJS(), this.state.responseContentType, { + includeReadOnly: true + }) + } + } else { + sampleResponse = schema ? getSampleSchema(schema.toJS(), activeContentType, { includeReadOnly: true, includeWriteOnly: true // writeOnly has no filtering effect in swagger 2.0 }) : null } - if(examples) { - examples = examples.map(example => { - // Remove unwanted properties from examples - return example.set ? example.set("$$ref", undefined) : example - }) - } - - let example = getExampleComponent( sampleResponse, examples, HighlightCode ) + let example = getExampleComponent( sampleResponse, HighlightCode ) return ( @@ -137,20 +155,55 @@ export default class Response extends React.Component {
- { isOAS3 ? -
- +
+ + Media type + + + {controlsAcceptHeader ? ( + + Controls Accept header. + + ) : null} +
+ {examplesForMediaType ? ( +
+ + Examples + + + oas3Actions.setActiveExamplesMember({ + name: key, + pathMethod: [path, method], + contextType: "responses", + contextName: code + }) + } + showLabels={false} /> - { controlsAcceptHeader ? Controls Accept header. : null } -
- : null } +
+ ) : null} + + ) : null} - { example ? ( + { example || schema ? ( + ) : null } + + { isOAS3 && examplesForMediaType ? ( + ) : null} { headers ? ( @@ -167,9 +228,8 @@ export default class Response extends React.Component { /> ) : null} - - {specSelectors.isOAS3() ? + {isOAS3 ? { links ? links.toSeq().map((link, key) => { return diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index 397a2c75..474bfd92 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -18,6 +18,7 @@ export default class Responses extends React.Component { specSelectors: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired, oas3Actions: PropTypes.object.isRequired, + oas3Selectors: PropTypes.object.isRequired, specPath: ImPropTypes.list.isRequired, fn: PropTypes.object.isRequired } @@ -28,17 +29,20 @@ export default class Responses extends React.Component { displayRequestDuration: false } - shouldComponentUpdate(nextProps) { - // BUG: props.tryItOutResponse is always coming back as a new Immutable instance - let render = this.props.tryItOutResponse !== nextProps.tryItOutResponse - || this.props.responses !== nextProps.responses - || this.props.produces !== nextProps.produces - || this.props.producesValue !== nextProps.producesValue - || this.props.displayRequestDuration !== nextProps.displayRequestDuration - || this.props.path !== nextProps.path - || this.props.method !== nextProps.method - return render - } + // These performance-enhancing checks were disabled as part of Multiple Examples + // because they were causing data-consistency issues + // + // shouldComponentUpdate(nextProps) { + // // BUG: props.tryItOutResponse is always coming back as a new Immutable instance + // let render = this.props.tryItOutResponse !== nextProps.tryItOutResponse + // || this.props.responses !== nextProps.responses + // || this.props.produces !== nextProps.produces + // || this.props.producesValue !== nextProps.producesValue + // || this.props.displayRequestDuration !== nextProps.displayRequestDuration + // || this.props.path !== nextProps.path + // || this.props.method !== nextProps.method + // return render + // } onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue([this.props.path, this.props.method], val) @@ -64,6 +68,10 @@ export default class Responses extends React.Component { producesValue, displayRequestDuration, specPath, + path, + method, + oas3Selectors, + oas3Actions, } = this.props let defaultCode = defaultStatusCode( responses ) @@ -121,6 +129,8 @@ export default class Responses extends React.Component { let className = tryItOutResponse && tryItOutResponse.get("status") == code ? "response_current" : "" return ( ) }).toArray() diff --git a/src/core/json-schema-components.jsx b/src/core/json-schema-components.jsx index 63a4dbfc..e349ee72 100644 --- a/src/core/json-schema-components.jsx +++ b/src/core/json-schema-components.jsx @@ -4,7 +4,6 @@ import { List, fromJS } from "immutable" import cx from "classnames" import ImPropTypes from "react-immutable-proptypes" import DebounceInput from "react-debounce-input" -import { getSampleSchema } from "core/utils" //import "less/json-schema-form" const noop = ()=> {} @@ -18,7 +17,8 @@ const JsonSchemaPropShape = { errors: ImPropTypes.list, required: PropTypes.bool, dispatchInitialValue: PropTypes.bool, - description: PropTypes.any + description: PropTypes.any, + disabled: PropTypes.bool, } const JsonSchemaDefaultProps = { @@ -43,7 +43,7 @@ export class JsonSchemaForm extends Component { } render() { - let { schema, errors, value, onChange, getComponent, fn } = this.props + let { schema, errors, value, onChange, getComponent, fn, disabled } = this.props if(schema.toJS) schema = schema.toJS() @@ -51,7 +51,7 @@ export class JsonSchemaForm extends Component { let { type, format="" } = schema let Comp = (format ? getComponent(`JsonSchema_${type}_${format}`) : getComponent(`JsonSchema_${type}`)) || getComponent("JsonSchema_string") - return + return } } @@ -65,7 +65,7 @@ export class JsonSchema_string extends Component { } onEnumChange = (val) => this.props.onChange(val) render() { - let { getComponent, value, schema, errors, required, description } = this.props + let { getComponent, value, schema, errors, required, description, disabled } = this.props let enumValue = schema["enum"] errors = errors.toJS ? errors.toJS() : [] @@ -80,7 +80,7 @@ export class JsonSchema_string extends Component { onChange={ this.onEnumChange }/>) } - const isDisabled = schema["in"] === "formData" && !("FormData" in window) + const isDisabled = disabled || (schema["in"] === "formData" && !("FormData" in window)) const Input = getComponent("Input") if (schema["type"] === "file") { return () } return ( -
+
{ !value || !value.count || value.count() < 1 ? null : value.map( (item,i) => { let schema = Object.assign({}, itemSchema) @@ -183,13 +184,32 @@ export class JsonSchema_array extends PureComponent { } return (
- this.onItemChange(val, i)} schema={schema} /> - + this.onItemChange(val, i)} + schema={schema} + disabled={disabled} + /> + { !disabled ? ( + + ) : null }
) }).toArray() } - + { !disabled ? ( + + ) : null }
) } @@ -201,7 +221,7 @@ export class JsonSchema_boolean extends Component { onEnumChange = (val) => this.props.onChange(val) render() { - let { getComponent, value, errors, schema, required } = this.props + let { getComponent, value, errors, schema, required, disabled } = this.props errors = errors.toJS ? errors.toJS() : [] const Select = getComponent("Select") @@ -209,6 +229,7 @@ export class JsonSchema_boolean extends Component { return (