* 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
223 lines
7.3 KiB
JavaScript
223 lines
7.3 KiB
JavaScript
/**
|
|
* @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()
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
}
|