Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Examples for OpenAPI 3 Parameters, Request Bodies, and Responses #5427

Merged
merged 78 commits into from
Jun 29, 2019
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
8e030a0
add opt-in Prettier config
shockey Jun 17, 2019
b0725d1
remove legacy `examples` implementation
shockey Jun 17, 2019
3e52c40
create ExamplesSelect
shockey Jun 18, 2019
3cacbeb
support `Response.examples` in OpenAPI 3
shockey Jun 18, 2019
540e0de
create response controls group
shockey Jun 18, 2019
258cc86
prettier reformat
shockey Jun 18, 2019
de535ca
prepare to break up Parameters
shockey Jun 18, 2019
7875473
reunify Parameters and OAS3 Parameters
shockey Jun 18, 2019
6153a65
Parameter Examples
shockey Jun 18, 2019
696afca
Example component
shockey Jun 18, 2019
1eba4aa
handle parameter value stringification correctly
shockey Jun 18, 2019
80848c9
FOR REVIEW: add prop for controlling Select
shockey Jun 18, 2019
7fa5832
use regular header for param examples in Try-It-Out
shockey Jun 18, 2019
4f7d7ec
manage active examples member via Redux
shockey Jun 19, 2019
6c0e528
Request Body Try-It-Out examples
shockey Jun 19, 2019
fa1cb20
remove special Response description styling
shockey Jun 19, 2019
be1b798
omit Example value display in Try-It-Out
shockey Jun 19, 2019
bd6c61d
support disabled text inputs in JsonSchemaForm
shockey Jun 25, 2019
799270f
Example.omitValue => Example.showValue
shockey Jun 25, 2019
23930be
ExamplesSelectValueRetainer
shockey Jun 25, 2019
daf7b0f
styling for disabled inputs
shockey Jun 25, 2019
7798b23
remove console.log
shockey Jun 25, 2019
b176291
support "Modified Values" in ExamplesSelect
shockey Jun 25, 2019
9f4a396
remove Examples component
shockey Jun 25, 2019
704f736
use ParameterRow.getParamKey for active examples member keying
shockey Jun 25, 2019
b09d2bd
split-rendering of examples in ParameterRow
shockey Jun 25, 2019
4c531c5
send disabled prop to JsonSchemaForm
shockey Jun 25, 2019
ff01b2a
use content type to key request body active examples members
shockey Jun 25, 2019
da45305
remove debugger
shockey Jun 25, 2019
f6bd312
rewire RequestBodyEditor to be a controlled component
shockey Jun 25, 2019
403215c
trigger synthetic onSelect events in ExamplesSelect
shockey Jun 25, 2019
ee1d9f4
prettier updates
shockey Jun 25, 2019
ca238d1
remove outdated Examples usage in RequestBody
shockey Jun 25, 2019
c913da1
don't handle examples changes in ESVR
shockey Jun 25, 2019
6eec50b
make RequestBodyEditor semi-controlled
shockey Jun 25, 2019
ab18c56
don't default to an empty Map for request bodies
shockey Jun 25, 2019
81cbab5
add namespaceKey to ESVR for state mgmt
shockey Jun 25, 2019
4102e79
don't key RequestBody activeExampleKeys on media type
shockey Jun 25, 2019
bf0805f
tweak ESVR isModifiedValueSelected calculation
shockey Jun 25, 2019
445ddc3
add trace class to ExamplesSelect
shockey Jun 25, 2019
9f12e30
remove usage of ESVR.currentNamespace
shockey Jun 25, 2019
8397376
reset to first example if currentExampleKey is invalid
shockey Jun 25, 2019
9229ad3
add default values to RequestBody rendering
shockey Jun 25, 2019
d3a412b
stringify things in ESVR
shockey Jun 26, 2019
47ae7e6
avoid null select value (silences React warning)
shockey Jun 26, 2019
27ec68f
detect user inputs that match any examples member's value
shockey Jun 26, 2019
67e22e4
add trace class for json-schema-array
shockey Jun 26, 2019
536dedd
shallowly convert namespace state, to preserve Immutable stucts in state
shockey Jun 26, 2019
390758b
stringify RBE values; don't trim JSON in editor
shockey Jun 26, 2019
411b6b0
match user input to an example when non-primitives are expressed in s…
shockey Jun 26, 2019
3a44f32
update Cypress
shockey Jun 27, 2019
5fc9fdb
don't apply sample values in JsonSchema_Object
shockey Jun 27, 2019
75fb5dc
support disabling all JsonSchemaForm subcomponents
shockey Jun 27, 2019
b4af897
Core tests
shockey Jun 27, 2019
0c9f947
style changes to accomodate Examples
shockey Jun 27, 2019
0af602f
fix version-checking error in Response
shockey Jun 27, 2019
e921e40
disable SCU for Responses
shockey Jun 27, 2019
28c1c7e
don't stringify Select values
shockey Jun 27, 2019
3e88c33
ModelExample: default to Model tab if no example is available; provid…
shockey Jun 27, 2019
b54e6f2
don't trim JSON ParamBody inputs
shockey Jun 28, 2019
0c3a90a
read directly from 2.0 Response.schema instead of inferring a value
shockey Jun 28, 2019
5a88cc0
show current Example information in RequestBody
shockey Jun 28, 2019
62d906e
show label for Examples dropdown by default
shockey Jun 28, 2019
6245271
rework Response content ordering
shockey Jun 29, 2019
6a0e3a8
style disabled textareas like other read-only blocks
shockey Jun 29, 2019
357a11a
meta: fix sourcemaps
shockey Jun 29, 2019
cc74df2
refactor ESVR setNameForNamespace
shockey Jun 29, 2019
bc3ed6b
protect second half of ternary expession
shockey Jun 29, 2019
0181ce0
cypress: `select.examples-select` => `.examples-select > select`
shockey Jun 29, 2019
671f2aa
clarify ModelExample.componentWillReceiveProps
shockey Jun 29, 2019
7d8a1e1
add gates/defaults to prevent issues in very bare-boned documents
shockey Jun 29, 2019
d2b1766
fix test block organization problem
shockey Jun 29, 2019
a83ca18
simplify RequestBodyEditor interface
shockey Jun 29, 2019
42f03c5
linter fixes
shockey Jun 29, 2019
3862ff7
prettier updates
shockey Jun 29, 2019
72757aa
Merge branch 'master' of github.com:swagger-api/swagger-ui into feat/…
shockey Jun 29, 2019
7b904be
use plugin system for new components
shockey Jun 29, 2019
1432e6a
move ME Cypress helpers to other file
shockey Jun 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .prettierrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
semi: false
trailingComma: es5
endOfLine: lf
requirePragma: true
insertPragma: true
shockey marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 34 additions & 0 deletions src/core/components/example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @prettier
*/

import React from "react"
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 (
<div className="example">
{example.get("description") ? (
<section className="example__section">
<div className="example__section-header">Example Description</div>
<p>
<Markdown source={example.get("description")} />
</p>
</section>
) : null}
{showValue && example.has("value") ? (
<section className="example__section">
<div className="example__section-header">Example Value</div>
<HighlightCode value={stringify(example.get("value"))} />
</section>
) : null}
</div>
)
}
176 changes: 176 additions & 0 deletions src/core/components/examples-select-value-retainer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* @prettier
*/
import React from "react"
import { Map } from "immutable"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"

import ExamplesSelect from "./examples-select"

// 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.

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,
// (also proxies props for Examples)
}

static defaultProps = {
examples: Map({}),
onSelect: (...args) =>
console.log(
"ExamplesSelectValueRetainer: no `onSelect` function was provided",
...args
),
updateValue: (...args) =>
console.log(
"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
lastUserEditedValue: this.props.currentUserInputValue,
lastDownstreamValue: valueFromExample,
isModifiedValueSelected:
valueFromExample !== undefined &&
this.props.currentUserInputValue !== valueFromExample,
}
}

_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 (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.state

const valueFromExample = this._getValueForExample(key)

if (key === "__MODIFIED__VALUE__") {
updateValue(lastUserEditedValue)
return this.setState({
isModifiedValueSelected: true,
})
}

if (typeof onSelect === "function") {
onSelect(key, { isSyntheticChange }, ...otherArgs)
}

this.setState({
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(valueFromExample)
}
}

componentWillReceiveProps(nextProps) {
// update `lastUserEditedValue` as new currentUserInput values come in

const { currentUserInputValue: newValue } = nextProps

const lastUserEditedValue = this.state.lastUserEditedValue
const lastDownstreamValue = this.state.lastDownstreamValue

const valueFromExample = this._getValueForExample(
nextProps.currentKey,
nextProps)

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
newValue !== valueFromExample // value isn't the example's value
) {
this.setState({
lastUserEditedValue: nextProps.currentUserInputValue,
isModifiedValueSelected: newValue !== valueFromExample,
})
}
}

render() {
const { currentUserInputValue, examples, currentKey } = this.props
const {
lastDownstreamValue,
lastUserEditedValue,
isModifiedValueSelected,
} = this.state

return (
<div>
<ExamplesSelect
examples={examples}
currentExampleKey={currentKey}
onSelect={this._onExamplesSelect}
isModifiedValueAvailable={
lastUserEditedValue !== undefined &&
lastUserEditedValue !== lastDownstreamValue
}
isValueModified={
currentUserInputValue !== undefined && isModifiedValueSelected
}
/>
</div>
)
}
}
115 changes: 115 additions & 0 deletions src/core/components/examples-select.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @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,
}

static defaultProps = {
examples: Im.Map({}),
onSelect: (...args) =>
console.log(
// FIXME: remove before merging to master...
`DEBUG: ExamplesSelect was not given an onSelect callback`,
...args
),
currentExampleKey: null,
}

_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, currentKey } = this.props

const currentExamplePerProps = examples.get(currentKey)

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 = this.getCurrentExample()
const firstExampleKey = examples.keyOf(firstExample)

this._onSelect(firstExampleKey, {
isSyntheticChange: true,
})
}
}

render() {
const {
examples,
currentExampleKey,
isValueModified,
isModifiedValueAvailable,
} = this.props

return (
<select
onChange={this._onDomSelect}
value={
isModifiedValueAvailable && isValueModified
? "__MODIFIED__VALUE__"
: currentExampleKey
}
>
{isModifiedValueAvailable ? (
<option value="__MODIFIED__VALUE__">[Modified value]</option>
) : null}
{examples
.map((example, exampleName) => {
return (
<option
key={exampleName} // for React
value={exampleName} // for matching to select's `value`
>
{example.get("summary") || exampleName}
</option>
)
})
.valueSeq()}
</select>
)
}
}
9 changes: 8 additions & 1 deletion src/core/components/layout-utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,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 value = this.state.value.toJS ? this.state.value.toJS() : this.state.value

return (
<select className={this.props.className} multiple={ multiple } value={ value } onChange={ this.onChange } >
<select className={this.props.className} multiple={ multiple } value={ String(value) } onChange={ this.onChange } >
{ allowEmptyValue ? <option value="">--</option> : null }
{
allowedValues.map(function (item, key) {
Expand Down
3 changes: 3 additions & 0 deletions src/core/components/operation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export default class Operation extends PureComponent {
specSelectors={ specSelectors }
pathMethod={ [path, method] }
getConfigs={ getConfigs }
oas3Actions={ oas3Actions }
oas3Selectors={ oas3Selectors }
/>
}

Expand Down Expand Up @@ -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]) }
Expand Down
Loading