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,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 (
<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>
)
}
Example.propTypes = {
example: ImPropTypes.map.isRequired,
showValue: PropTypes.bool,
getComponent: PropTypes.func.isRequired,
}

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()
}
/>
)
}
}

View File

@@ -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 (
<div className="examples-select">
{
showLabels ? (
<span className="examples-select__section-label">Examples: </span>
) : null
}
<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>
</div>
)
}
}

View File

@@ -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 (
<select className={this.props.className} multiple={ multiple } value={ value } onChange={ this.onChange } >
<select className={this.props.className} multiple={ multiple } value={value} onChange={ this.onChange } disabled={disabled} >
{ allowEmptyValue ? <option value="">--</option> : null }
{
allowedValues.map(function (item, key) {

View File

@@ -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 <div>
return <div className="model-example">
<ul className="tab">
<li className={ "tabitem" + ( this.state.activeTab === "example" ? " active" : "") }>
<a className="tablinks" data-name="example" onClick={ this.activeTab }>{isExecute ? "Edit Value" : "Example Value"}</a>
@@ -59,7 +72,11 @@ export default class ModelExample extends React.Component {
</ul>
<div>
{
this.state.activeTab === "example" && example
this.state.activeTab === "example" ? (
example ? example : (
<HighlightCode value="(no example available)" />
)
) : null
}
{
this.state.activeTab === "model" && <ModelWrapper schema={ schema }

View File

@@ -156,6 +156,8 @@ export default class Operation extends PureComponent {
specSelectors={ specSelectors }
pathMethod={ [path, method] }
getConfigs={ getConfigs }
oas3Actions={ oas3Actions }
oas3Selectors={ oas3Selectors }
/>
}
@@ -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]) }

View File

@@ -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})
}

View File

@@ -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 && <span className="prop-format">(${format})</span>}
</div>
<div className="parameter__deprecated">
{ isOAS3 && isOAS3() && param.get("deprecated") ? "deprecated": null }
{ isOAS3 && param.get("deprecated") ? "deprecated": null }
</div>
<div className="parameter__in">({ param.get("in") })</div>
{ !showCommonExtensions || !commonExt.size ? null : commonExt.map((v, key) => <ParameterExt key={`${key}-${v}`} xKey={key} xVal={v} /> )}
@@ -224,11 +250,28 @@ export default class ParameterRow extends Component {
{(isFormData && !isFormDataSupported) && <div>Error: your browser does not support FormData</div>}
{ bodyParam || !isExecute ? null
{
isOAS3 && param.get("examples") ? (
<section className="parameter-controls">
<ExamplesSelectValueRetainer
examples={param.get("examples")}
onSelect={this._onExampleSelect}
updateValue={this.onChangeWrapper}
getComponent={getComponent}
defaultToFirstExample={true}
currentKey={oas3Selectors.activeExamplesMember(...pathMethod, "parameters", this.getParamKey())}
currentUserInputValue={value}
/>
</section>
) : null
}
{ bodyParam ? null
: <JsonSchemaForm fn={fn}
getComponent={getComponent}
value={ value }
required={ required }
disabled={!isExecute}
description={param.get("description") ? `${param.get("name")} - ${param.get("description")}` : `${param.get("name")}`}
onChange={ this.onChangeWrapper }
errors={ paramWithMeta.get("errors") }
@@ -257,6 +300,18 @@ export default class ParameterRow extends Component {
: null
}
{
isOAS3 && param.get("examples") ? (
<Example
example={param.getIn([
"examples",
oas3Selectors.activeExamplesMember(...pathMethod, "parameters", this.getParamKey())
])}
getComponent={getComponent}
/>
) : null
}
</td>
</tr>
@@ -265,3 +320,4 @@ export default class ParameterRow extends Component {
}
}

View File

@@ -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 (
<div className="opblock-section">
<div className="opblock-section-header">
<div className="tab-header">
<h4 className="opblock-title">Parameters</h4>
</div>
{ allowTryItOut ? (
<TryItOutButton enabled={ tryItOutEnabled } onCancelClick={ onCancelClick } onTryoutClick={ onTryoutClick } />
) : null }
</div>
{ !parameters.count() ? <div className="opblock-description-wrapper"><p>No parameters</p></div> :
<div className="table-container">
<table className="parameters">
<thead>
<tr>
<th className="col col_header parameters-col_name">Name</th>
<th className="col col_header parameters-col_description">Description</th>
</tr>
</thead>
<tbody>
{
eachMap(parameters, (parameter, i) => (
<ParameterRow
fn={ fn }
specPath={specPath.push(i.toString())}
getComponent={ getComponent }
getConfigs={ getConfigs }
rawParam={ parameter }
param={ specSelectors.parameterWithMetaByIdentity(pathMethod, parameter) }
key={ `${parameter.get( "in" )}.${parameter.get("name")}` }
onChange={ this.onChange }
onChangeConsumes={this.onChangeConsumesWrapper}
specSelectors={ specSelectors }
specActions={specActions}
pathMethod={ pathMethod }
isExecute={ isExecute }/>
)).toArray()
}
</tbody>
</table>
</div>
}
</div>
)
}
}

View File

@@ -0,0 +1 @@
export { default as Parameters } from "./parameters"

View File

@@ -0,0 +1,232 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import Im, { Map, List } from "immutable"
import ImPropTypes from "react-immutable-proptypes"
// 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 {
constructor(props) {
super(props)
this.state = {
callbackVisible: false,
parametersVisible: true
}
}
static propTypes = {
parameters: ImPropTypes.list.isRequired,
operation: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
getComponent: 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,
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)
}
toggleTab = (tab) => {
if(tab === "parameters"){
return this.setState({
parametersVisible: true,
callbackVisible: false
})
}else if(tab === "callbacks"){
return this.setState({
callbackVisible: true,
parametersVisible: false
})
}
}
render(){
let {
onTryoutClick,
onCancelClick,
parameters,
allowTryItOut,
tryItOutEnabled,
specPath,
fn,
getComponent,
getConfigs,
specSelectors,
specActions,
pathMethod,
oas3Actions,
oas3Selectors,
operation
} = this.props
const ParameterRow = getComponent("parameterRow")
const TryItOutButton = getComponent("TryItOutButton")
const ContentType = getComponent("contentType")
const Callbacks = getComponent("Callbacks", true)
const RequestBody = getComponent("RequestBody", true)
const isExecute = tryItOutEnabled && allowTryItOut
const isOAS3 = specSelectors.isOAS3()
const requestBody = operation.get("requestBody")
return (
<div className="opblock-section">
<div className="opblock-section-header">
{ isOAS3 ? (
<div className="tab-header">
<div onClick={() => this.toggleTab("parameters")} className={`tab-item ${this.state.parametersVisible && "active"}`}>
<h4 className="opblock-title"><span>Parameters</span></h4>
</div>
{ operation.get("callbacks") ?
(
<div onClick={() => this.toggleTab("callbacks")} className={`tab-item ${this.state.callbackVisible && "active"}`}>
<h4 className="opblock-title"><span>Callbacks</span></h4>
</div>
) : null
}
</div>
) : (
<div className="tab-header">
<h4 className="opblock-title">Parameters</h4>
</div>
)}
{ allowTryItOut ? (
<TryItOutButton enabled={ tryItOutEnabled } onCancelClick={ onCancelClick } onTryoutClick={ onTryoutClick } />
) : null }
</div>
{this.state.parametersVisible ? <div className="parameters-container">
{ !parameters.count() ? <div className="opblock-description-wrapper"><p>No parameters</p></div> :
<div className="table-container">
<table className="parameters">
<thead>
<tr>
<th className="col col_header parameters-col_name">Name</th>
<th className="col col_header parameters-col_description">Description</th>
</tr>
</thead>
<tbody>
{
eachMap(parameters, (parameter, i) => (
<ParameterRow
fn={ fn }
specPath={specPath.push(i.toString())}
getComponent={ getComponent }
getConfigs={ getConfigs }
rawParam={ parameter }
param={ specSelectors.parameterWithMetaByIdentity(pathMethod, parameter) }
key={ `${parameter.get( "in" )}.${parameter.get("name")}` }
onChange={ this.onChange }
onChangeConsumes={this.onChangeConsumesWrapper}
specSelectors={ specSelectors }
specActions={specActions}
oas3Actions={oas3Actions}
oas3Selectors={oas3Selectors}
pathMethod={ pathMethod }
isExecute={ isExecute }/>
)).toArray()
}
</tbody>
</table>
</div>
}
</div> : null }
{this.state.callbackVisible ? <div className="callbacks-container opblock-description-wrapper">
<Callbacks
callbacks={Map(operation.get("callbacks"))}
specPath={specPath.slice(0, -1).push("callbacks")}
/>
</div> : null }
{
isOAS3 && requestBody && this.state.parametersVisible &&
<div className="opblock-section opblock-section-request-body">
<div className="opblock-section-header">
<h4 className={`opblock-title parameter__name ${requestBody.get("required") && "required"}`}>Request body</h4>
<label>
<ContentType
value={oas3Selectors.requestContentType(...pathMethod)}
contentTypes={ requestBody.get("content", List()).keySeq() }
onChange={(value) => {
oas3Actions.setRequestContentType({ value, pathMethod })
}}
className="body-param-content-type" />
</label>
</div>
<div className="opblock-description-wrapper">
<RequestBody
specPath={specPath.slice(0, -1).push("requestBody")}
requestBody={requestBody}
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod)}
isExecute={isExecute}
activeExamplesKey={oas3Selectors.activeExamplesMember(
...pathMethod,
"requestBody",
"requestBody" // RBs are currently not stored per-mediaType
)}
updateActiveExamplesKey={key => {
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)
const usableValue = Map.isMap(lastValue) ? lastValue : Map()
return oas3Actions.setRequestBodyValue({
pathMethod,
value: usableValue.setIn(path, value)
})
}
oas3Actions.setRequestBodyValue({ value, pathMethod })
}}
contentType={oas3Selectors.requestContentType(...pathMethod)}/>
</div>
</div>
}
</div>
)
}
}

View File

@@ -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 (<div key={ key }>
<h5>{ key }</h5>
<HighlightCode className="example" value={ exampleValue } />
</div>)
}).toArray()
}
if ( sampleResponse ) { return <div>
const getExampleComponent = ( sampleResponse, HighlightCode ) => {
if (
sampleResponse !== undefined &&
sampleResponse !== null
) { return <div>
<HighlightCode className="example" value={ sampleResponse } />
</div>
}
@@ -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 (
<tr className={ "response " + ( className || "") } data-code={code}>
@@ -137,20 +155,55 @@ export default class Response extends React.Component {
<Markdown source={ response.get( "description" ) } />
</div>
{ isOAS3 ?
<div className={cx("response-content-type", {
"controls-accept-header": controlsAcceptHeader
})}>
<ContentType
{isOAS3 && response.get("content") ? (
<section className="response-controls">
<div
className={cx("response-control-media-type", {
"response-control-media-type--accept-controller": controlsAcceptHeader
})}
>
<small className="response-control-media-type__title">
Media type
</small>
<ContentType
value={this.state.responseContentType}
contentTypes={ response.get("content") ? response.get("content").keySeq() : Seq() }
contentTypes={
response.get("content")
? response.get("content").keySeq()
: Seq()
}
onChange={this._onContentTypeChange}
/>
{controlsAcceptHeader ? (
<small className="response-control-media-type__accept-message">
Controls <code>Accept</code> header.
</small>
) : null}
</div>
{examplesForMediaType ? (
<div className="response-control-examples">
<small className="response-control-examples__title">
Examples
</small>
<ExamplesSelect
examples={examplesForMediaType}
currentExampleKey={this.getTargetExamplesKey()}
onSelect={key =>
oas3Actions.setActiveExamplesMember({
name: key,
pathMethod: [path, method],
contextType: "responses",
contextName: code
})
}
showLabels={false}
/>
{ controlsAcceptHeader ? <small>Controls <code>Accept</code> header.</small> : null }
</div>
: null }
</div>
) : null}
</section>
) : null}
{ example ? (
{ example || schema ? (
<ModelExample
specPath={specPathWithPossibleSchema}
getComponent={ getComponent }
@@ -158,6 +211,14 @@ export default class Response extends React.Component {
specSelectors={ specSelectors }
schema={ fromJSOrdered(schema) }
example={ example }/>
) : null }
{ isOAS3 && examplesForMediaType ? (
<Example
example={examplesForMediaType.get(this.getTargetExamplesKey(), Map({}))}
getComponent={getComponent}
omitValue={true}
/>
) : null}
{ headers ? (
@@ -167,9 +228,8 @@ export default class Response extends React.Component {
/>
) : null}
</td>
{specSelectors.isOAS3() ? <td className="col response-col_links">
{isOAS3 ? <td className="col response-col_links">
{ links ?
links.toSeq().map((link, key) => {
return <OperationLink key={key} name={key} link={ link } getComponent={getComponent}/>

View File

@@ -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 (
<Response key={ code }
path={path}
method={method}
specPath={specPath.push(code)}
isDefault={defaultCode === code}
fn={fn}
@@ -132,6 +142,13 @@ export default class Responses extends React.Component {
onContentTypeChange={this.onResponseContentTypeChange}
contentType={ producesValue }
getConfigs={ getConfigs }
activeExamplesKey={oas3Selectors.activeExamplesMember(
path,
method,
"responses",
code
)}
oas3Actions={oas3Actions}
getComponent={ getComponent }/>
)
}).toArray()