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:
222
src/core/components/examples-select-value-retainer.jsx
Normal file
222
src/core/components/examples-select-value-retainer.jsx
Normal 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()
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user