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

@@ -2,12 +2,11 @@ import React, { Component } from "react"
import PropTypes from "prop-types"
import Im, { Map, List } from "immutable"
import ImPropTypes from "react-immutable-proptypes"
import { OAS3ComponentWrapFactory } from "../helpers"
// More readable, just iterate over maps, only
const eachMap = (iterable, fn) => iterable.valueSeq().filter(Im.Map.isMap).map(fn)
class Parameters extends Component {
export default class Parameters extends Component {
constructor(props) {
super(props)
@@ -19,21 +18,21 @@ class Parameters extends Component {
static propTypes = {
parameters: ImPropTypes.list.isRequired,
specActions: PropTypes.object.isRequired,
operation: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
getConfigs: 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,
specPath: ImPropTypes.list.isRequired,
onTryoutClick: PropTypes.func,
onCancelClick: PropTypes.func,
onChangeKey: PropTypes.array,
pathMethod: PropTypes.array.isRequired
pathMethod: PropTypes.array.isRequired,
getConfigs: PropTypes.func.isRequired,
specPath: ImPropTypes.list.isRequired,
}
@@ -43,6 +42,7 @@ class Parameters extends Component {
tryItOutEnabled: false,
allowTryItOut: true,
onChangeKey: [],
specPath: [],
}
onChange = ( param, value, isXml ) => {
@@ -51,7 +51,7 @@ class Parameters extends Component {
onChangeKey,
} = this.props
changeParamByIdentity( onChangeKey, param, value, isXml)
changeParamByIdentity(onChangeKey, param, value, isXml)
}
onChangeConsumesWrapper = ( val ) => {
@@ -85,16 +85,16 @@ class Parameters extends Component {
parameters,
allowTryItOut,
tryItOutEnabled,
specPath,
fn,
getComponent,
getConfigs,
specSelectors,
specActions,
pathMethod,
oas3Actions,
oas3Selectors,
pathMethod,
specPath,
operation
} = this.props
@@ -105,72 +105,79 @@ class Parameters extends Component {
const RequestBody = getComponent("RequestBody", true)
const isExecute = tryItOutEnabled && allowTryItOut
const { isOAS3 } = specSelectors
const isOAS3 = specSelectors.isOAS3()
const requestBody = operation.get("requestBody")
const requestBodySpecPath = specPath.slice(0, -1).push("requestBody") // remove the "parameters" part
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 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>
{ 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 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 }
getComponent={ getComponent }
specPath={specPath.push(i)}
getConfigs={ getConfigs }
rawParam={ parameter }
param={ specSelectors.parameterWithMetaByIdentity(pathMethod, parameter) }
key={ parameter.get( "name" ) }
onChange={ this.onChange }
onChangeConsumes={this.onChangeConsumesWrapper}
specSelectors={ specSelectors }
specActions={ specActions }
pathMethod={ pathMethod }
isExecute={ isExecute }/>
)).toArray()
}
</tbody>
</table>
</div>
}
</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}
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> : "" }
</div> : null }
{
isOAS3() && requestBody && this.state.parametersVisible &&
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>
@@ -186,10 +193,24 @@ class Parameters extends Component {
</div>
<div className="opblock-description-wrapper">
<RequestBody
specPath={requestBodySpecPath}
specPath={specPath.slice(0, -1).push("requestBody")}
requestBody={requestBody}
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod) || Map()}
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)
@@ -209,6 +230,3 @@ class Parameters extends Component {
)
}
}
export default OAS3ComponentWrapFactory(Parameters)

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

View File

@@ -4,7 +4,6 @@ import { List, fromJS } from "immutable"
import cx from "classnames"
import ImPropTypes from "react-immutable-proptypes"
import DebounceInput from "react-debounce-input"
import { getSampleSchema } from "core/utils"
//import "less/json-schema-form"
const noop = ()=> {}
@@ -18,7 +17,8 @@ const JsonSchemaPropShape = {
errors: ImPropTypes.list,
required: PropTypes.bool,
dispatchInitialValue: PropTypes.bool,
description: PropTypes.any
description: PropTypes.any,
disabled: PropTypes.bool,
}
const JsonSchemaDefaultProps = {
@@ -43,7 +43,7 @@ export class JsonSchemaForm extends Component {
}
render() {
let { schema, errors, value, onChange, getComponent, fn } = this.props
let { schema, errors, value, onChange, getComponent, fn, disabled } = this.props
if(schema.toJS)
schema = schema.toJS()
@@ -51,7 +51,7 @@ export class JsonSchemaForm extends Component {
let { type, format="" } = schema
let Comp = (format ? getComponent(`JsonSchema_${type}_${format}`) : getComponent(`JsonSchema_${type}`)) || getComponent("JsonSchema_string")
return <Comp { ...this.props } errors={errors} fn={fn} getComponent={getComponent} value={value} onChange={onChange} schema={schema}/>
return <Comp { ...this.props } errors={errors} fn={fn} getComponent={getComponent} value={value} onChange={onChange} schema={schema} disabled={disabled}/>
}
}
@@ -65,7 +65,7 @@ export class JsonSchema_string extends Component {
}
onEnumChange = (val) => this.props.onChange(val)
render() {
let { getComponent, value, schema, errors, required, description } = this.props
let { getComponent, value, schema, errors, required, description, disabled } = this.props
let enumValue = schema["enum"]
errors = errors.toJS ? errors.toJS() : []
@@ -80,7 +80,7 @@ export class JsonSchema_string extends Component {
onChange={ this.onEnumChange }/>)
}
const isDisabled = schema["in"] === "formData" && !("FormData" in window)
const isDisabled = disabled || (schema["in"] === "formData" && !("FormData" in window))
const Input = getComponent("Input")
if (schema["type"] === "file") {
return (<Input type="file"
@@ -149,7 +149,7 @@ export class JsonSchema_array extends PureComponent {
}
render() {
let { getComponent, required, schema, errors, fn } = this.props
let { getComponent, required, schema, errors, fn, disabled } = this.props
errors = errors.toJS ? errors.toJS() : []
@@ -167,13 +167,14 @@ export class JsonSchema_array extends PureComponent {
title={ errors.length ? errors : ""}
multiple={ true }
value={ value }
disabled={disabled}
allowedValues={ enumValue }
allowEmptyValue={ !required }
onChange={ this.onEnumChange }/>)
}
return (
<div>
<div className="json-schema-array">
{ !value || !value.count || value.count() < 1 ? null :
value.map( (item,i) => {
let schema = Object.assign({}, itemSchema)
@@ -183,13 +184,32 @@ export class JsonSchema_array extends PureComponent {
}
return (
<div key={i} className="json-schema-form-item">
<JsonSchemaForm fn={fn} getComponent={getComponent} value={item} onChange={(val) => this.onItemChange(val, i)} schema={schema} />
<Button className="btn btn-sm json-schema-form-item-remove" onClick={()=> this.removeItem(i)} > - </Button>
<JsonSchemaForm
fn={fn}
getComponent={getComponent}
value={item}
onChange={(val) => this.onItemChange(val, i)}
schema={schema}
disabled={disabled}
/>
{ !disabled ? (
<Button
className="btn btn-sm json-schema-form-item-remove"
onClick={()=> this.removeItem(i)}
> - </Button>
) : null }
</div>
)
}).toArray()
}
<Button className={`btn btn-sm json-schema-form-item-add ${errors.length ? "invalid" : null}`} onClick={this.addItem}> Add item </Button>
{ !disabled ? (
<Button
className={`btn btn-sm json-schema-form-item-add ${errors.length ? "invalid" : null}`}
onClick={this.addItem}
>
Add item
</Button>
) : null }
</div>
)
}
@@ -201,7 +221,7 @@ export class JsonSchema_boolean extends Component {
onEnumChange = (val) => this.props.onChange(val)
render() {
let { getComponent, value, errors, schema, required } = this.props
let { getComponent, value, errors, schema, required, disabled } = this.props
errors = errors.toJS ? errors.toJS() : []
const Select = getComponent("Select")
@@ -209,6 +229,7 @@ export class JsonSchema_boolean extends Component {
return (<Select className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
value={ String(value) }
disabled={disabled}
allowedValues={ fromJS(schema.enum || ["true", "false"]) }
allowEmptyValue={ !schema.enum || !required }
onChange={ this.onEnumChange }/>)
@@ -223,16 +244,6 @@ export class JsonSchema_object extends PureComponent {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
componentDidMount() {
if(!this.props.value && this.props.schema) {
this.resetValueToSample()
}
}
resetValueToSample = () => {
this.onChange(getSampleSchema(this.props.schema) )
}
onChange = (value) => {
this.props.onChange(value)
}
@@ -247,7 +258,8 @@ export class JsonSchema_object extends PureComponent {
let {
getComponent,
value,
errors
errors,
disabled
} = this.props
const TextArea = getComponent("TextArea")
@@ -258,6 +270,7 @@ export class JsonSchema_object extends PureComponent {
className={cx({ invalid: errors.size })}
title={ errors.size ? errors.join(", ") : ""}
value={value}
disabled={disabled}
onChange={ this.handleOnChange }/>
</div>
)
@@ -267,4 +280,4 @@ export class JsonSchema_object extends PureComponent {
function valueOrEmptyList(value) {
return List.isList(value) ? value : List()
}
}

View File

@@ -3,6 +3,7 @@
export const UPDATE_SELECTED_SERVER = "oas3_set_servers"
export const UPDATE_REQUEST_BODY_VALUE = "oas3_set_request_body_value"
export const UPDATE_ACTIVE_EXAMPLES_MEMBER = "oas3_set_active_examples_member"
export const UPDATE_REQUEST_CONTENT_TYPE = "oas3_set_request_content_type"
export const UPDATE_RESPONSE_CONTENT_TYPE = "oas3_set_response_content_type"
export const UPDATE_SERVER_VARIABLE_VALUE = "oas3_set_server_variable_value"
@@ -21,6 +22,13 @@ export function setRequestBodyValue ({ value, pathMethod }) {
}
}
export function setActiveExamplesMember ({ name, pathMethod, contextType, contextName }) {
return {
type: UPDATE_ACTIVE_EXAMPLES_MEMBER,
payload: { name, pathMethod, contextType, contextName }
}
}
export function setRequestContentType ({ value, pathMethod }) {
return {
type: UPDATE_REQUEST_CONTENT_TYPE,

View File

@@ -1,24 +1,19 @@
import React, { PureComponent } from "react"
import PropTypes from "prop-types"
import { fromJS } from "immutable"
import { getSampleSchema, stringify } from "core/utils"
import { stringify } from "core/utils"
const NOOP = Function.prototype
export default class RequestBodyEditor extends PureComponent {
static propTypes = {
requestBody: PropTypes.object.isRequired,
mediaType: PropTypes.string.isRequired,
onChange: PropTypes.func,
getComponent: PropTypes.func.isRequired,
isExecute: PropTypes.bool,
specSelectors: PropTypes.object.isRequired,
value: PropTypes.string,
defaultValue: PropTypes.string,
};
static defaultProps = {
mediaType: "application/json",
requestBody: fromJS({}),
onChange: NOOP,
};
@@ -26,108 +21,75 @@ export default class RequestBodyEditor extends PureComponent {
super(props, context)
this.state = {
isEditBox: false,
userDidModify: false,
value: ""
}
}
componentDidMount() {
this.setValueToSample.call(this)
}
componentWillReceiveProps(nextProps) {
if(this.props.mediaType !== nextProps.mediaType) {
// media type was changed
this.setValueToSample(nextProps.mediaType)
value: stringify(props.value) || props.defaultValue
}
if(!this.props.isExecute && nextProps.isExecute) {
// we just entered execute mode,
// so enable editing for convenience
this.setState({ isEditBox: true })
}
// this is the glue that makes sure our initial value gets set as the
// current request body value
// TODO: achieve this in a selector instead
props.onChange(props.value)
}
componentDidUpdate(prevProps) {
if(this.props.requestBody !== prevProps.requestBody) {
// force recalc of value if the request body definition has changed
this.setValueToSample(this.props.mediaType)
}
}
applyDefaultValue = (nextProps) => {
const { onChange, defaultValue } = (nextProps ? nextProps : this.props)
setValueToSample = (explicitMediaType) => {
this.onChange(this.sample(explicitMediaType))
}
resetValueToSample = (explicitMediaType) => {
this.setState({ userDidModify: false })
this.setValueToSample(explicitMediaType)
}
sample = (explicitMediaType) => {
let { requestBody, mediaType } = this.props
let mediaTypeValue = requestBody.getIn(["content", explicitMediaType || mediaType])
let schema = mediaTypeValue.get("schema").toJS()
let mediaTypeExample = mediaTypeValue.get("example") !== undefined ? stringify(mediaTypeValue.get("example")) : null
return mediaTypeExample || getSampleSchema(schema, explicitMediaType || mediaType, {
includeWriteOnly: true
this.setState({
value: defaultValue
})
return onChange(defaultValue)
}
onChange = (value) => {
this.setState({value})
this.props.onChange(value)
this.props.onChange(stringify(value))
}
handleOnChange = e => {
const { mediaType } = this.props
const isJson = /json/i.test(mediaType)
const inputValue = isJson ? e.target.value.trim() : e.target.value
onDomChange = e => {
const inputValue = e.target.value
this.setState({ userDidModify: true })
this.onChange(inputValue)
this.setState({
value: inputValue,
}, () => this.onChange(inputValue))
}
toggleIsEditBox = () => this.setState( state => ({isEditBox: !state.isEditBox}))
componentWillReceiveProps(nextProps) {
if(
this.props.value !== nextProps.value &&
nextProps.value !== this.state.value
) {
this.setState({
value: stringify(nextProps.value)
})
}
if(!nextProps.value && nextProps.defaultValue && !!this.state.value) {
// if new value is falsy, we have a default, AND the falsy value didn't
// come from us originally
this.applyDefaultValue(nextProps)
}
}
render() {
let {
isExecute,
getComponent,
mediaType,
getComponent
} = this.props
const Button = getComponent("Button")
const TextArea = getComponent("TextArea")
const HighlightCode = getComponent("highlightCode")
let {
value
} = this.state
let { value, isEditBox, userDidModify } = this.state
const TextArea = getComponent("TextArea")
return (
<div className="body-param">
{
isEditBox && isExecute
? <TextArea className={"body-param__text"} value={value} onChange={ this.handleOnChange }/>
: (value && <HighlightCode className="body-param__example"
value={ value }/>)
}
<div className="body-param-options">
<div className="body-param-edit">
{
!isExecute ? null
: <Button className={isEditBox ? "btn cancel body-param__example-edit" : "btn edit body-param__example-edit"}
onClick={this.toggleIsEditBox}>{ isEditBox ? "Cancel" : "Edit"}
</Button>
}
{ userDidModify &&
<Button className="btn ml3" onClick={() => { this.resetValueToSample(mediaType) }}>Reset</Button>
}
</div>
</div>
<TextArea
className={"body-param__text"}
value={value}
onChange={ this.onDomChange }
/>
</div>
)

View File

@@ -4,6 +4,36 @@ import ImPropTypes from "react-immutable-proptypes"
import { Map, OrderedMap, List } from "immutable"
import { getCommonExtensions, getSampleSchema, stringify } from "core/utils"
function getDefaultRequestBodyValue(requestBody, mediaType, activeExamplesKey) {
let mediaTypeValue = requestBody.getIn(["content", mediaType])
let schema = mediaTypeValue.get("schema").toJS()
let example =
mediaTypeValue.get("example") !== undefined
? stringify(mediaTypeValue.get("example"))
: null
let currentExamplesValue = mediaTypeValue.getIn([
"examples",
activeExamplesKey,
"value"
])
if (mediaTypeValue.get("examples")) {
// the media type DOES have examples
return stringify(currentExamplesValue) || ""
} else {
// the media type DOES NOT have examples
return stringify(
example ||
getSampleSchema(schema, mediaType, {
includeWriteOnly: true
}) ||
""
)
}
}
const RequestBody = ({
requestBody,
requestBodyValue,
@@ -14,7 +44,9 @@ const RequestBody = ({
contentType,
isExecute,
specPath,
onChange
onChange,
activeExamplesKey,
updateActiveExamplesKey,
}) => {
const handleFile = (e) => {
onChange(e.target.files[0])
@@ -23,6 +55,9 @@ const RequestBody = ({
const Markdown = getComponent("Markdown")
const ModelExample = getComponent("modelExample")
const RequestBodyEditor = getComponent("RequestBodyEditor")
const HighlightCode = getComponent("highlightCode")
const ExamplesSelectValueRetainer = getComponent("ExamplesSelectValueRetainer")
const Example = getComponent("Example")
const { showCommonExtensions } = getConfigs()
@@ -32,6 +67,11 @@ const RequestBody = ({
const mediaTypeValue = requestBodyContent.get(contentType, OrderedMap())
const schemaForMediaType = mediaTypeValue.get("schema", OrderedMap())
const examplesForMediaType = mediaTypeValue.get("examples", OrderedMap())
const handleExamplesSelect = (key /*, { isSyntheticChange } */) => {
updateActiveExamplesKey(key)
}
if(!mediaTypeValue.size) {
return null
@@ -83,7 +123,7 @@ const RequestBody = ({
const format = prop.get("format")
const description = prop.get("description")
const currentValue = requestBodyValue.get(key)
let initialValue = prop.get("default") || prop.get("example") || ""
if (initialValue === "" && type === "object") {
@@ -139,23 +179,63 @@ const RequestBody = ({
{ requestBodyDescription &&
<Markdown source={requestBodyDescription} />
}
<ModelExample
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
expandDepth={1}
isExecute={isExecute}
schema={mediaTypeValue.get("schema")}
specPath={specPath.push("content", contentType)}
example={<RequestBodyEditor
requestBody={requestBody}
onChange={onChange}
mediaType={contentType}
getComponent={getComponent}
isExecute={isExecute}
specSelectors={specSelectors}
/>}
/>
{
examplesForMediaType ? (
<ExamplesSelectValueRetainer
examples={examplesForMediaType}
currentKey={activeExamplesKey}
currentUserInputValue={requestBodyValue}
onSelect={handleExamplesSelect}
updateValue={onChange}
defaultToFirstExample={true}
getComponent={getComponent}
/>
) : null
}
{
isExecute ? (
<div>
<RequestBodyEditor
value={requestBodyValue}
defaultValue={getDefaultRequestBodyValue(
requestBody,
contentType,
activeExamplesKey,
)}
onChange={onChange}
getComponent={getComponent}
/>
</div>
) : (
<ModelExample
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
expandDepth={1}
isExecute={isExecute}
schema={mediaTypeValue.get("schema")}
specPath={specPath.push("content", contentType)}
example={
<HighlightCode
className="body-param__example"
value={stringify(requestBodyValue) || getDefaultRequestBodyValue(
requestBody,
contentType,
activeExamplesKey,
)}
/>
}
/>
)
}
{
examplesForMediaType ? (
<Example
example={examplesForMediaType.get(activeExamplesKey)}
getComponent={getComponent}
/>
) : null
}
</div>
}
@@ -169,7 +249,9 @@ RequestBody.propTypes = {
contentType: PropTypes.string,
isExecute: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
specPath: PropTypes.array.isRequired
specPath: PropTypes.array.isRequired,
activeExamplesKey: PropTypes.string,
updateActiveExamplesKey: PropTypes.func,
}
export default RequestBody

View File

@@ -1,6 +1,7 @@
import {
UPDATE_SELECTED_SERVER,
UPDATE_REQUEST_BODY_VALUE,
UPDATE_ACTIVE_EXAMPLES_MEMBER,
UPDATE_REQUEST_CONTENT_TYPE,
UPDATE_SERVER_VARIABLE_VALUE,
UPDATE_RESPONSE_CONTENT_TYPE
@@ -15,6 +16,10 @@ export default {
let [path, method] = pathMethod
return state.setIn( [ "requestData", path, method, "bodyValue" ], value)
},
[UPDATE_ACTIVE_EXAMPLES_MEMBER]: (state, { payload: { name, pathMethod, contextType, contextName } } ) =>{
let [path, method] = pathMethod
return state.setIn( [ "examples", path, method, contextType, contextName, "activeExample" ], name)
},
[UPDATE_REQUEST_CONTENT_TYPE]: (state, { payload: { value, pathMethod } } ) =>{
let [path, method] = pathMethod
return state.setIn( [ "requestData", path, method, "requestContentType" ], value)

View File

@@ -26,6 +26,11 @@ export const requestBodyValue = onlyOAS3((state, path, method) => {
}
)
export const activeExamplesMember = onlyOAS3((state, path, method, type, name) => {
return state.getIn(["examples", path, method, type, name, "activeExample"]) || null
}
)
export const requestContentType = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "requestContentType"]) || null
}

View File

@@ -1,6 +1,5 @@
import Markdown from "./markdown"
import AuthItem from "./auth-item"
import parameters from "./parameters"
import VersionStamp from "./version-stamp"
import OnlineValidatorBadge from "./online-validator-badge"
import Model from "./model"
@@ -9,7 +8,6 @@ import JsonSchema_string from "./json-schema-string"
export default {
Markdown,
AuthItem,
parameters,
JsonSchema_string,
VersionStamp,
model: Model,

View File

@@ -25,6 +25,9 @@ import AuthItem from "core/components/auth/auth-item"
import AuthError from "core/components/auth/error"
import ApiKeyAuth from "core/components/auth/api-key-auth"
import BasicAuth from "core/components/auth/basic-auth"
import Example from "core/components/example"
import ExamplesSelect from "core/components/examples-select"
import ExamplesSelectValueRetainer from "core/components/examples-select-value-retainer"
import Oauth2 from "core/components/auth/oauth2"
import Clear from "core/components/clear"
import LiveResponse from "core/components/live-response"
@@ -41,7 +44,7 @@ import HighlightCode from "core/components/highlight-code"
import Responses from "core/components/responses"
import Response from "core/components/response"
import ResponseBody from "core/components/response-body"
import Parameters from "core/components/parameters"
import { Parameters } from "core/components/parameters"
import ParameterExt from "core/components/parameter-extension"
import ParameterIncludeEmpty from "core/components/parameter-include-empty"
import ParameterRow from "core/components/parameter-row"
@@ -152,7 +155,10 @@ export default function() {
DeepLink,
InfoUrl,
InfoBasePath,
SvgAssets
SvgAssets,
Example,
ExamplesSelect,
ExamplesSelectValueRetainer,
}
}

View File

@@ -780,7 +780,7 @@ export function stringify(thing) {
return thing
}
if (thing.toJS) {
if (thing && thing.toJS) {
thing = thing.toJS()
}
@@ -793,6 +793,10 @@ export function stringify(thing) {
}
}
if(thing === null || thing === undefined) {
return ""
}
return thing.toString()
}