diff --git a/package.json b/package.json index 31432f52a9a..c9a00d01601 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "react-addons-shallow-compare": "0.14.8", "react-addons-test-utils": "^15.6.2", "react-collapse": "^4.0.3", + "react-debounce-input": "^3.2.0", "react-dom": "^15.6.2", "react-height": "^2.0.0", "react-hot-loader": "1.3.1", diff --git a/src/core/components/model-collapse.jsx b/src/core/components/model-collapse.jsx index 528ec09d0e0..41ae29b94bb 100644 --- a/src/core/components/model-collapse.jsx +++ b/src/core/components/model-collapse.jsx @@ -8,14 +8,17 @@ export default class ModelCollapse extends Component { children: PropTypes.any, title: PropTypes.element, modelName: PropTypes.string, - onToggle: PropTypes.func + classes: PropTypes.string, + onToggle: PropTypes.func, + hideSelfOnExpand: PropTypes.bool, } static defaultProps = { collapsedContent: "{...}", expanded: false, title: null, - onToggle: () => {} + onToggle: () => {}, + hideSelfOnExpand: false } constructor(props, context) { @@ -29,17 +32,23 @@ export default class ModelCollapse extends Component { } } - componentWillReceiveProps(nextProps){ + componentDidMount() { + const { hideSelfOnExpand, expanded, modelName } = this.props + if(hideSelfOnExpand && expanded) { + // We just mounted pre-expanded, and we won't be going back.. + // So let's give our parent an `onToggle` call.. + // Since otherwise it will never be called. + this.props.onToggle(modelName, expanded) + } + } - if(this.props.expanded!= nextProps.expanded){ + componentWillReceiveProps(nextProps){ + if(this.props.expanded !== nextProps.expanded){ this.setState({expanded: nextProps.expanded}) } - } toggleCollapsed=()=>{ - - if(this.props.onToggle){ this.props.onToggle(this.props.modelName,!this.state.expanded) } @@ -50,9 +59,18 @@ export default class ModelCollapse extends Component { } render () { - const {title} = this.props + const { title, classes } = this.props + + if(this.state.expanded ) { + if(this.props.hideSelfOnExpand) { + return + {this.props.children} + + } + } + return ( - + { title && {title} } diff --git a/src/core/components/models.jsx b/src/core/components/models.jsx index 1bd14a77145..3bbe69eef5f 100644 --- a/src/core/components/models.jsx +++ b/src/core/components/models.jsx @@ -6,11 +6,29 @@ export default class Models extends Component { static propTypes = { getComponent: PropTypes.func, specSelectors: PropTypes.object, + specActions: PropTypes.object.isRequired, layoutSelectors: PropTypes.object, layoutActions: PropTypes.object, getConfigs: PropTypes.func.isRequired } + getSchemaBasePath = () => { + const isOAS3 = this.props.specSelectors.isOAS3() + return isOAS3 ? ["components", "schemas"] : ["definitions"] + } + + getCollapsedContent = () => { + return " " + } + + handleToggle = (name, isExpanded) => { + const { layoutActions } = this.props + layoutActions.show(["models", name], isExpanded) + if(isExpanded) { + this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name]) + } + } + render(){ let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props let definitions = specSelectors.definitions() @@ -18,10 +36,11 @@ export default class Models extends Component { if (!definitions.size || defaultModelsExpandDepth < 0) return null let showModels = layoutSelectors.isShown("models", defaultModelsExpandDepth > 0 && docExpansion !== "none") - const specPathBase = specSelectors.isOAS3() ? ["components", "schemas"] : ["definitions"] + const specPathBase = this.getSchemaBasePath() const ModelWrapper = getComponent("ModelWrapper") const Collapse = getComponent("Collapse") + const ModelCollapse = getComponent("ModelCollapse") return

layoutActions.show("models", !showModels)}> @@ -32,18 +51,40 @@ export default class Models extends Component {

{ - definitions.entrySeq().map( ( [ name, model ])=>{ + definitions.entrySeq().map( ( [ name ])=>{ + + const schema = specSelectors.specResolvedSubtree([...specPathBase, name]) + + if(layoutSelectors.isShown(["models", name], false) && schema === undefined) { + // Firing an action in a container render is not great, + // but it works for now. + this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name]) + } + + const content = + + const title = + {name} + return
- + 1} + >{content}
}).toArray() } diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index 14f2122758a..358291823fc 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -9,6 +9,7 @@ export default class Operation extends PureComponent { static propTypes = { specPath: ImPropTypes.list.isRequired, operation: PropTypes.instanceOf(Iterable).isRequired, + summary: PropTypes.string, response: PropTypes.instanceOf(Iterable), request: PropTypes.instanceOf(Iterable), @@ -34,7 +35,8 @@ export default class Operation extends PureComponent { operation: null, response: null, request: null, - specPath: List() + specPath: List(), + summary: "" } render() { @@ -59,6 +61,8 @@ export default class Operation extends PureComponent { let operationProps = this.props.operation let { + summary, + deprecated, isShown, isAuthorized, path, @@ -76,14 +80,13 @@ export default class Operation extends PureComponent { } = operationProps.toJS() let { - summary, + summary: resolvedSummary, description, - deprecated, externalDocs, schemes - } = op.operation + } = op - let operation = operationProps.getIn(["op", "operation"]) + let operation = operationProps.getIn(["op"]) let security = operationProps.get("security") let responses = operation.get("responses") let produces = operation.get("produces") @@ -132,7 +135,7 @@ export default class Operation extends PureComponent { { !showSummary ? null :
- { summary } + { resolvedSummary || summary }
} diff --git a/src/core/components/param-body.jsx b/src/core/components/param-body.jsx index 13a4377f8c7..a6097c76ec2 100644 --- a/src/core/components/param-body.jsx +++ b/src/core/components/param-body.jsx @@ -47,7 +47,7 @@ export default class ParamBody extends PureComponent { updateValues = (props) => { let { specSelectors, pathMethod, param, isExecute, consumesValue="" } = props - let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) : fromJS({}) + let parameter = specSelectors ? specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) : fromJS({}) let isXml = /xml/i.test(consumesValue) let isJson = /json/i.test(consumesValue) let paramValue = isXml ? parameter.get("value_xml") : parameter.get("value") @@ -107,7 +107,7 @@ export default class ParamBody extends PureComponent { const HighlightCode = getComponent("highlightCode") const ContentType = getComponent("contentType") // for domains where specSelectors not passed - let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) : param + let parameter = specSelectors ? specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) : param let errors = parameter.get("errors", List()) let consumesValue = specSelectors.contentTypeValues(pathMethod).get("requestContentType") let consumes = this.props.consumes && this.props.consumes.size ? this.props.consumes : ParamBody.defaultProp.consumes diff --git a/src/core/components/parameter-row.jsx b/src/core/components/parameter-row.jsx index 42f45bd9b48..b3844574965 100644 --- a/src/core/components/parameter-row.jsx +++ b/src/core/components/parameter-row.jsx @@ -24,7 +24,7 @@ export default class ParameterRow extends Component { let { specSelectors, pathMethod, param } = props let defaultValue = param.get("default") - let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) + let parameter = specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) let value = parameter ? parameter.get("value") : "" if ( defaultValue !== undefined && value === undefined ) { this.onChangeWrapper(defaultValue) @@ -37,7 +37,7 @@ export default class ParameterRow extends Component { let example = param.get("example") let defaultValue = param.get("default") - let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) + let parameter = specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) let enumValue if(isOAS3()) { @@ -104,8 +104,7 @@ export default class ParameterRow extends Component { let isFormDataSupported = "FormData" in win let required = param.get("required") let itemType = param.getIn(isOAS3 && isOAS3() ? ["schema", "items", "type"] : ["items", "type"]) - let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) - let value = parameter ? parameter.get("value") : "" + let value = param ? param.get("value") : "" let extensions = getExtensions(param) diff --git a/src/core/components/parameters.jsx b/src/core/components/parameters.jsx index c1c6ded3082..c5b1e2293ad 100644 --- a/src/core/components/parameters.jsx +++ b/src/core/components/parameters.jsx @@ -101,7 +101,7 @@ export default class Parameters extends Component { specPath={specPath.push(i.toString())} getComponent={ getComponent } getConfigs={ getConfigs } - param={ parameter } + param={ specSelectors.parameterWithMeta(pathMethod, parameter.get("name"), parameter.get("in")) } key={ `${parameter.get( "in" )}.${parameter.get("name")}` } onChange={ this.onChange } onChangeConsumes={this.onChangeConsumesWrapper} diff --git a/src/core/components/providers/markdown.jsx b/src/core/components/providers/markdown.jsx index ec096d5f8b1..a4cbf3f900e 100644 --- a/src/core/components/providers/markdown.jsx +++ b/src/core/components/providers/markdown.jsx @@ -3,7 +3,17 @@ import PropTypes from "prop-types" import Remarkable from "remarkable" import sanitize from "sanitize-html" +// eslint-disable-next-line no-useless-escape +const isPlainText = (str) => /^[A-Z\s0-9!?\.]+$/gi.test(str) + function Markdown({ source }) { + if(isPlainText(source)) { + // If the source text is not Markdown, + // let's save some time and just render it. + return
+ {source} +
+ } const html = new Remarkable({ html: true, typographer: true, diff --git a/src/core/containers/OperationContainer.jsx b/src/core/containers/OperationContainer.jsx index ea698c9b7f7..ac382553a4f 100644 --- a/src/core/containers/OperationContainer.jsx +++ b/src/core/containers/OperationContainer.jsx @@ -2,7 +2,7 @@ import React, { PureComponent } from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import { helpers } from "swagger-client" -import { Iterable, fromJS } from "immutable" +import { Iterable, fromJS, Map } from "immutable" const { opId } = helpers @@ -82,13 +82,24 @@ export default class OperationContainer extends PureComponent { } componentWillReceiveProps(nextProps) { - if(nextProps.response !== this.props.response) { + const { path, method, specActions, specSelectors, response, isShown } = nextProps + const resolvedSubtree = specSelectors.specResolvedSubtree(["paths", path, method]) + + if(response !== this.props.response) { this.setState({ executeInProgress: false }) } + + if(isShown && resolvedSubtree === undefined) { + specActions.requestResolvedSubtree(["paths", path, method]) + } } toggleShown =() => { - let { layoutActions, tag, operationId, isShown } = this.props + let { layoutActions, specActions, tag, operationId, path, method, isShown } = this.props + if(!isShown) { + // transitioning from collapsed to expanded + specActions.requestResolvedSubtree(["paths", path, method]) + } layoutActions.show(["operations", tag, operationId], !isShown) } @@ -108,7 +119,7 @@ export default class OperationContainer extends PureComponent { render() { let { - op, + op: unresolvedOp, tag, path, method, @@ -140,10 +151,14 @@ export default class OperationContainer extends PureComponent { const Operation = getComponent( "operation" ) + const resolvedSubtree = specSelectors.specResolvedSubtree(["paths", path, method]) || Map() + const operationProps = fromJS({ - op, + op: resolvedSubtree || Map(), tag, path, + summary: unresolvedOp.getIn(["operation", "summary"]) || "", + deprecated: resolvedSubtree.get("deprecated") || unresolvedOp.getIn(["operation", "deprecated"]) || false, method, security, isAuthorized, diff --git a/src/core/json-schema-components.js b/src/core/json-schema-components.js index 087b617ca33..14998febd3d 100644 --- a/src/core/json-schema-components.js +++ b/src/core/json-schema-components.js @@ -2,6 +2,7 @@ import React, { PureComponent, Component } from "react" import PropTypes from "prop-types" import { List, fromJS } from "immutable" import ImPropTypes from "react-immutable-proptypes" +import DebounceInput from "react-debounce-input" //import "less/json-schema-form" const noop = ()=> {} @@ -79,10 +80,13 @@ export class JsonSchema_string extends Component { disabled={isDisabled}/>) } else { - return () diff --git a/src/core/plugins/deep-linking/spec-wrap-actions.js b/src/core/plugins/deep-linking/spec-wrap-actions.js index d69fc46500f..398fbcbc282 100644 --- a/src/core/plugins/deep-linking/spec-wrap-actions.js +++ b/src/core/plugins/deep-linking/spec-wrap-actions.js @@ -4,7 +4,7 @@ import { escapeDeepLinkPath } from "core/utils" let hasHashBeenParsed = false //TODO this forces code to only run once which may prevent scrolling if page not refreshed -export const updateResolved = (ori, { layoutActions, getConfigs }) => (...args) => { +export const updateJsonSpec = (ori, { layoutActions, getConfigs }) => (...args) => { ori(...args) const isDeepLinkingEnabled = getConfigs().deepLinking diff --git a/src/core/plugins/err/actions.js b/src/core/plugins/err/actions.js index 461971cddc2..6b6f474ae30 100644 --- a/src/core/plugins/err/actions.js +++ b/src/core/plugins/err/actions.js @@ -6,6 +6,7 @@ export const NEW_SPEC_ERR = "err_new_spec_err" export const NEW_SPEC_ERR_BATCH = "err_new_spec_err_batch" export const NEW_AUTH_ERR = "err_new_auth_err" export const CLEAR = "err_clear" +export const CLEAR_BY = "err_clear_by" export function newThrownErr(err) { return { @@ -49,3 +50,11 @@ export function clear(filter = {}) { payload: filter } } + +export function clearBy(filter = () => true) { + // filter is a function + return { + type: CLEAR_BY, + payload: filter + } +} diff --git a/src/core/plugins/err/reducers.js b/src/core/plugins/err/reducers.js index 795f5dd59f7..8a9cbbbae22 100644 --- a/src/core/plugins/err/reducers.js +++ b/src/core/plugins/err/reducers.js @@ -4,12 +4,11 @@ import { NEW_SPEC_ERR, NEW_SPEC_ERR_BATCH, NEW_AUTH_ERR, - CLEAR + CLEAR, + CLEAR_BY, } from "./actions" -import reject from "lodash/reject" - -import Im, { fromJS, List } from "immutable" +import { fromJS, List } from "immutable" import transformErrors from "./error-transformers/hook" @@ -65,11 +64,34 @@ export default function(system) { }, [CLEAR]: (state, { payload }) => { - if(!payload) { - return + if(!payload || !state.get("errors")) { + return state + } + + let newErrors = state.get("errors") + .filter(err => { + return err.keySeq().every(k => { + const errValue = err.get(k) + const filterValue = payload[k] + + if(!filterValue) return true + + return errValue !== filterValue + }) + }) + return state.merge({ + errors: newErrors + }) + }, + + [CLEAR_BY]: (state, { payload }) => { + if(!payload || typeof payload !== "function") { + return state } - // TODO: Rework, to use immutable only, no need for lodash - let newErrors = Im.fromJS(reject((state.get("errors") || List()).toJS(), payload)) + let newErrors = state.get("errors") + .filter(err => { + return payload(err) + }) return state.merge({ errors: newErrors }) diff --git a/src/core/plugins/oas3/spec-extensions/wrap-selectors.js b/src/core/plugins/oas3/spec-extensions/wrap-selectors.js index 82c10f2196a..8109e2eff7f 100644 --- a/src/core/plugins/oas3/spec-extensions/wrap-selectors.js +++ b/src/core/plugins/oas3/spec-extensions/wrap-selectors.js @@ -20,7 +20,7 @@ const state = state => { return state || Map() } -const nullSelector = createSelector(() => null) +const nullSelector = createSelector(() => null) const OAS3NullSelector = onlyOAS3(nullSelector) diff --git a/src/core/plugins/spec/actions.js b/src/core/plugins/spec/actions.js index 0d0d853a0e3..b5c719385a5 100644 --- a/src/core/plugins/spec/actions.js +++ b/src/core/plugins/spec/actions.js @@ -1,6 +1,7 @@ import YAML from "js-yaml" import parseUrl from "url-parse" import serializeError from "serialize-error" +import { Map } from "immutable" import isString from "lodash/isString" import { isJSONObject } from "core/utils" @@ -21,6 +22,7 @@ export const CLEAR_REQUEST = "spec_clear_request" export const CLEAR_VALIDATE_PARAMS = "spec_clear_validate_param" export const UPDATE_OPERATION_META_VALUE = "spec_update_operation_meta_value" export const UPDATE_RESOLVED = "spec_update_resolved" +export const UPDATE_RESOLVED_SUBTREE = "spec_update_resolved_subtree" export const SET_SCHEME = "set_scheme" const toStr = (str) => isString(str) ? str : "" @@ -74,7 +76,14 @@ export const parseToJson = (str) => ({specActions, specSelectors, errActions}) = return {} } +let hasWarnedAboutResolveSpecDeprecation = false + export const resolveSpec = (json, url) => ({specActions, specSelectors, errActions, fn: { fetch, resolve, AST }, getConfigs}) => { + if(!hasWarnedAboutResolveSpecDeprecation) { + console.warn(`specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use resolveIn instead!`) + hasWarnedAboutResolveSpecDeprecation = true + } + const { modelPropertyMacro, parameterMacro, @@ -124,6 +133,55 @@ export const resolveSpec = (json, url) => ({specActions, specSelectors, errActio }) } +export const requestResolvedSubtree = path => system => { + const { + errActions, + fn: { + resolveSubtree, + AST: { getLineNumberForPath } + }, + specSelectors, + specActions, + } = system + + const specStr = specSelectors.specStr() + + if(!resolveSubtree) { + console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.") + return + } + + const currentValue = specSelectors.specResolvedSubtree(path) + + if(currentValue) { + return + } + + return resolveSubtree(specSelectors.specJson().toJS(), path) + .then(({ spec, errors }) => { + errActions.clear({ + type: "thrown" + }) + if(Array.isArray(errors) && errors.length > 0) { + let preparedErrors = errors + .map(err => { + console.error(err) + err.line = err.fullPath ? getLineNumberForPath(specStr, err.fullPath) : null + err.path = err.fullPath ? err.fullPath.join(".") : null + err.level = "error" + err.type = "thrown" + err.source = "resolver" + Object.defineProperty(err, "message", { enumerable: true, value: err.message }) + return err + }) + errActions.newThrownErrBatch(preparedErrors) + } + + return specActions.updateResolvedSubtree(path, spec) + }) + .catch(e => console.error(e)) +} + export function changeParam( path, paramName, paramIn, value, isXml ){ return { type: UPDATE_PARAM, @@ -131,6 +189,23 @@ export function changeParam( path, paramName, paramIn, value, isXml ){ } } +export const updateResolvedSubtree = (path, value) => { + return { + type: UPDATE_RESOLVED_SUBTREE, + payload: { path, value } + } +} + +export const invalidateResolvedSubtreeCache = () => { + return { + type: UPDATE_RESOLVED_SUBTREE, + payload: { + path: [], + value: Map() + } + } +} + export const validateParams = ( payload, isOAS3 ) =>{ return { type: VALIDATE_PARAMS, @@ -251,6 +326,7 @@ export const executeRequest = (req) => // track duration of request const startTime = Date.now() + return fn.execute(req) .then( res => { res.duration = Date.now() - startTime @@ -267,13 +343,22 @@ export const executeRequest = (req) => // I'm using extras as a way to inject properties into the final, `execute` method - It's not great. Anyone have a better idea? @ponelat export const execute = ( { path, method, ...extras }={} ) => (system) => { let { fn:{fetch}, specSelectors, specActions } = system - let spec = specSelectors.spec().toJS() + let spec = specSelectors.specJsonWithResolvedSubtrees().toJS() let scheme = specSelectors.operationScheme(path, method) let { requestContentType, responseContentType } = specSelectors.contentTypeValues([path, method]).toJS() let isXml = /xml/i.test(requestContentType) let parameters = specSelectors.parameterValues([path, method], isXml).toJS() - return specActions.executeRequest({fetch, spec, pathName: path, method, parameters, requestContentType, scheme, responseContentType, ...extras }) + return specActions.executeRequest({ + ...extras, + fetch, + spec, + pathName: path, + method, parameters, + requestContentType, + scheme, + responseContentType + }) } export function clearResponse (path, method) { diff --git a/src/core/plugins/spec/reducers.js b/src/core/plugins/spec/reducers.js index f49519e0a68..c4b9d96fd95 100644 --- a/src/core/plugins/spec/reducers.js +++ b/src/core/plugins/spec/reducers.js @@ -1,7 +1,12 @@ -import { fromJS } from "immutable" +import { fromJS, List } from "immutable" import { fromJSOrdered, validateParam } from "core/utils" import win from "../../window" +// selector-in-reducer is suboptimal, but `operationWithMeta` is more of a helper +import { + operationWithMeta +} from "./selectors" + import { UPDATE_SPEC, UPDATE_URL, @@ -12,6 +17,7 @@ import { SET_REQUEST, SET_MUTATED_REQUEST, UPDATE_RESOLVED, + UPDATE_RESOLVED_SUBTREE, UPDATE_OPERATION_META_VALUE, CLEAR_RESPONSE, CLEAR_REQUEST, @@ -39,38 +45,38 @@ export default { return state.setIn(["resolved"], fromJSOrdered(action.payload)) }, + [UPDATE_RESOLVED_SUBTREE]: (state, action) => { + const { value, path } = action.payload + return state.setIn(["resolvedSubtrees", ...path], fromJSOrdered(value)) + }, + [UPDATE_PARAM]: ( state, {payload} ) => { - let { path, paramName, paramIn, value, isXml } = payload - - return state.updateIn( [ "resolved", "paths", ...path, "parameters" ], fromJS([]), parameters => { - const index = parameters.findIndex(p => p.get( "name" ) === paramName && p.get("in") === paramIn ) - if (!(value instanceof win.File)) { - value = fromJSOrdered( value ) - } - return parameters.setIn( [ index, isXml ? "value_xml" : "value" ], value) - }) + let { path: pathMethod, paramName, paramIn, value, isXml } = payload + + const valueKey = isXml ? "value_xml" : "value" + + return state.setIn( + ["meta", "paths", ...pathMethod, "parameters", `${paramName}.${paramIn}`, valueKey], + value + ) }, [VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => { let meta = state.getIn( [ "meta", "paths", ...pathMethod ], fromJS({}) ) let isXml = /xml/i.test(meta.get("consumes_value")) - return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => { - return parameters.withMutations( parameters => { - for ( let i = 0, len = parameters.count(); i < len; i++ ) { - let errors = validateParam(parameters.get(i), isXml, isOAS3) - parameters.setIn([i, "errors"], fromJS(errors)) - } - }) + const op = operationWithMeta(state, ...pathMethod) + + return state.updateIn(["meta", "paths", ...pathMethod, "parameters"], fromJS({}), paramMeta => { + return op.get("parameters", List()).reduce((res, param) => { + const errors = validateParam(param, isXml, isOAS3) + return res.setIn([`${param.get("name")}.${param.get("in")}`, "errors"], fromJS(errors)) + }, paramMeta) }) }, [CLEAR_VALIDATE_PARAMS]: ( state, { payload: { pathMethod } } ) => { - return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => { - return parameters.withMutations( parameters => { - for ( let i = 0, len = parameters.count(); i < len; i++ ) { - parameters.setIn([i, "errors"], fromJS([])) - } - }) + return state.updateIn( [ "meta", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => { + return parameters.map(param => param.set("errors", fromJS([]))) }) }, @@ -109,10 +115,10 @@ export default { [UPDATE_OPERATION_META_VALUE]: (state, { payload: { path, value, key } }) => { // path is a pathMethod tuple... can't change the name now. - let operationPath = ["resolved", "paths", ...path] + let operationPath = ["paths", ...path] let metaPath = ["meta", "paths", ...path] - if(!state.getIn(operationPath)) { + if(!state.getIn(["json", ...operationPath]) && !state.getIn(["resolved", ...operationPath])) { // do nothing if the operation does not exist return state } diff --git a/src/core/plugins/spec/selectors.js b/src/core/plugins/spec/selectors.js index e19c590c31b..f8fa9a38df6 100644 --- a/src/core/plugins/spec/selectors.js +++ b/src/core/plugins/spec/selectors.js @@ -42,9 +42,18 @@ export const specResolved = createSelector( spec => spec.get("resolved", Map()) ) +export const specResolvedSubtree = (state, path) => { + return state.getIn(["resolvedSubtrees", ...path], undefined) +} + +export const specJsonWithResolvedSubtrees = createSelector( + state, + spec => Map().merge(spec.get("json"), spec.get("resolvedSubtrees")) +) + // Default Spec ( as an object ) export const spec = state => { - let res = specResolved(state) + let res = specJson(state) return res } @@ -137,7 +146,9 @@ export const securityDefinitions = createSelector( export const findDefinition = ( state, name ) => { - return specResolved(state).getIn(["definitions", name], null) + const resolvedRes = state.getIn(["resolvedSubtrees", "definitions", name], null) + const unresolvedRes = state.getIn(["json", "definitions", name], null) + return resolvedRes || unresolvedRes || null } export const definitions = createSelector( @@ -261,10 +272,40 @@ export const allowTryItOutFor = () => { return true } +export const operationWithMeta = (state, path, method) => { + const op = specJsonWithResolvedSubtrees(state).getIn(["paths", path, method], Map()) + const meta = state.getIn(["meta", "paths", path, method], Map()) + + const mergedParams = op.get("parameters", List()).map((param) => { + return Map().merge( + param, + meta.getIn(["parameters", `${param.get("name")}.${param.get("in")}`]) + ) + }) + + return Map() + .merge(op, meta) + .set("parameters", mergedParams) +} + +export const parameterWithMeta = (state, pathMethod, paramName, paramIn) => { + const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], Map()) + const metaParams = state.getIn(["meta", "paths", ...pathMethod, "parameters"], Map()) + + const mergedParams = opParams.map((param) => { + return Map().merge( + param, + metaParams.get(`${param.get("name")}.${param.get("in")}`) + ) + }) + + return mergedParams.find(param => param.get("in") === paramIn && param.get("name") === paramName, Map()) +} + // Get the parameter value by parameter name export function getParameter(state, pathMethod, name, inType) { pathMethod = pathMethod || [] - let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([])) + let params = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([])) return params.find( (p) => { return Map.isMap(p) && p.get("name") === name && p.get("in") === inType }) || Map() // Always return a map @@ -281,8 +322,9 @@ export const hasHost = createSelector( // Get the parameter values, that the user filled out export function parameterValues(state, pathMethod, isXml) { pathMethod = pathMethod || [] - let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([])) - return params.reduce( (hash, p) => { + // let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([])) + let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List()) + return paramValues.reduce( (hash, p) => { let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value") return hash.set(`${p.get("in")}.${p.get("name")}`, value) }, fromJS({})) @@ -305,7 +347,7 @@ export function parametersIncludeType(parameters, typeValue="") { // Get the consumes/produces value that the user selected export function contentTypeValues(state, pathMethod) { pathMethod = pathMethod || [] - let op = spec(state).getIn(["paths", ...pathMethod], fromJS({})) + let op = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod], fromJS({})) let meta = state.getIn(["meta", "paths", ...pathMethod], fromJS({})) let producesValue = currentProducesFor(state, pathMethod) @@ -327,14 +369,14 @@ export function contentTypeValues(state, pathMethod) { // Get the consumes/produces by path export function operationConsumes(state, pathMethod) { pathMethod = pathMethod || [] - return spec(state).getIn(["paths", ...pathMethod, "consumes"], fromJS({})) + return state.getIn(["meta", ...pathMethod, "consumes"], fromJS({})) } // Get the currently selected produces value for an operation export function currentProducesFor(state, pathMethod) { pathMethod = pathMethod || [] - const operation = spec(state).getIn(["paths", ...pathMethod], null) + const operation = specJsonWithResolvedSubtrees(state).getIn([ "paths", ...pathMethod], null) if(operation === null) { // return nothing if the operation does not exist @@ -362,10 +404,10 @@ export const canExecuteScheme = ( state, path, method ) => { export const validateBeforeExecute = ( state, pathMethod ) => { pathMethod = pathMethod || [] - let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([])) + let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([])) let isValid = true - params.forEach( (p) => { + paramValues.forEach( (p) => { let errors = p.get("errors") if ( errors && errors.count() ) { isValid = false diff --git a/src/core/plugins/spec/wrap-actions.js b/src/core/plugins/spec/wrap-actions.js index a9c73f22baa..324ddda1d9c 100644 --- a/src/core/plugins/spec/wrap-actions.js +++ b/src/core/plugins/spec/wrap-actions.js @@ -5,7 +5,7 @@ export const updateSpec = (ori, {specActions}) => (...args) => { export const updateJsonSpec = (ori, {specActions}) => (...args) => { ori(...args) - specActions.resolveSpec(...args) + specActions.invalidateResolvedSubtreeCache() } // Log the request ( just for debugging, shouldn't affect prod ) @@ -16,4 +16,4 @@ export const executeRequest = (ori, { specActions }) => (req) => { export const validateParams = (ori, { specSelectors }) => (req) => { return ori(req, specSelectors.isOAS3()) -} \ No newline at end of file +} diff --git a/src/core/plugins/swagger-js/index.js b/src/core/plugins/swagger-js/index.js index d8744c19922..90ceeca3619 100644 --- a/src/core/plugins/swagger-js/index.js +++ b/src/core/plugins/swagger-js/index.js @@ -7,6 +7,7 @@ module.exports = function({ configs }) { buildRequest: Swagger.buildRequest, execute: Swagger.execute, resolve: Swagger.resolve, + resolveSubtree: Swagger.resolveSubtree, serializeRes: Swagger.serializeRes, opId: Swagger.helpers.opId } diff --git a/src/style/_models.scss b/src/style/_models.scss index 5b69b3fd159..2ee50c281ed 100644 --- a/src/style/_models.scss +++ b/src/style/_models.scss @@ -198,6 +198,7 @@ section.models .model-box { padding: 10px; + display: inline-block; border-radius: 4px; background: rgba($section-models-model-box-background-color,.1); diff --git a/test/components/models.js b/test/components/models.js index 0280e0fba49..11a8b7f4434 100644 --- a/test/components/models.js +++ b/test/components/models.js @@ -24,7 +24,8 @@ describe("", function(){ def1: {}, def2: {} }) - } + }, + specResolvedSubtree: () => {} }, layoutSelectors: { isShown: createSpy() diff --git a/test/core/plugins/spec/actions.js b/test/core/plugins/spec/actions.js index 4a5846da924..58aec54eef1 100644 --- a/test/core/plugins/spec/actions.js +++ b/test/core/plugins/spec/actions.js @@ -177,6 +177,10 @@ describe("spec plugin - actions", function(){ }) }) + describe("requestResolvedSubtree", () => { + it("should return a promise ") + }) + it.skip("should call errActions.newErr, if the fn.execute rejects", function(){ }) diff --git a/test/core/plugins/spec/selectors.js b/test/core/plugins/spec/selectors.js index 824464899f9..a11699ef110 100644 --- a/test/core/plugins/spec/selectors.js +++ b/test/core/plugins/spec/selectors.js @@ -24,7 +24,7 @@ describe("spec plugin - selectors", function(){ // Given const spec = fromJS({ - resolved: { + json: { paths: { "/one": { get: { @@ -55,7 +55,7 @@ describe("spec plugin - selectors", function(){ it("should return { requestContentType, responseContentType } from an operation", function(){ // Given let state = fromJS({ - resolved: { + json: { paths: { "/one": { get: {} @@ -86,7 +86,7 @@ describe("spec plugin - selectors", function(){ it("should default to the first `produces` array value if current is not set", function(){ // Given let state = fromJS({ - resolved: { + json: { paths: { "/one": { get: { @@ -121,7 +121,7 @@ describe("spec plugin - selectors", function(){ it("should default to `application/json` if a default produces value is not available", function(){ // Given let state = fromJS({ - resolved: { + json: { paths: { "/one": { get: {} @@ -151,7 +151,7 @@ describe("spec plugin - selectors", function(){ it("should prioritize consumes value first from an operation", function(){ // Given let state = fromJS({ - resolved: { + json: { paths: { "/one": { get: { @@ -182,7 +182,7 @@ describe("spec plugin - selectors", function(){ it("should fallback to multipart/form-data if there is no consumes value but there is a file parameter", function(){ // Given let state = fromJS({ - resolved: { + json: { paths: { "/one": { get: { @@ -204,7 +204,7 @@ describe("spec plugin - selectors", function(){ it("should fallback to application/x-www-form-urlencoded if there is no consumes value, no file parameter, but there is a formData parameter", function(){ // Given let state = fromJS({ - resolved: { + json: { paths: { "/one": { get: { @@ -244,7 +244,7 @@ describe("spec plugin - selectors", function(){ // Given let state = fromJS({ url: "https://generator.swagger.io/api/swagger.json", - resolved: { + json: { paths: { "/one": { get: {