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
This commit is contained in:
kyle
2019-06-29 19:52:51 +01:00
committed by GitHub
parent 332ddaedcd
commit 23d7260f92
34 changed files with 3148 additions and 653 deletions

View File

@@ -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 (
<ExamplesSelect
examples={examples}
currentExampleKey={currentKey}
onSelect={this._onExamplesSelect}
isModifiedValueAvailable={
!!lastUserEditedValue && lastUserEditedValue !== lastDownstreamValue
}
isValueModified={
currentUserInputValue !== undefined &&
isModifiedValueSelected &&
currentUserInputValue !== this._getCurrentExampleValue()
}
/>
)
}
}