diff --git a/src/core/components/execute.jsx b/src/core/components/execute.jsx index 8d114b14..23546ad6 100644 --- a/src/core/components/execute.jsx +++ b/src/core/components/execute.jsx @@ -8,7 +8,6 @@ export default class Execute extends Component { specActions: PropTypes.object.isRequired, operation: PropTypes.object.isRequired, path: PropTypes.string.isRequired, - getComponent: PropTypes.func.isRequired, method: PropTypes.string.isRequired, onExecute: PropTypes.func } diff --git a/src/core/components/live-response.jsx b/src/core/components/live-response.jsx index cf9cf63b..67dd19c7 100644 --- a/src/core/components/live-response.jsx +++ b/src/core/components/live-response.jsx @@ -1,6 +1,7 @@ import React from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" +import { Iterable } from "immutable" const Headers = ( { headers } )=>{ return ( @@ -28,19 +29,29 @@ Duration.propTypes = { export default class LiveResponse extends React.Component { static propTypes = { - response: PropTypes.object.isRequired, - specSelectors: PropTypes.object.isRequired, - pathMethod: PropTypes.object.isRequired, - getComponent: PropTypes.func.isRequired, + response: PropTypes.instanceOf(Iterable).isRequired, + path: PropTypes.string.isRequired, + method: PropTypes.string.isRequired, displayRequestDuration: PropTypes.bool.isRequired, + specSelectors: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, getConfigs: PropTypes.func.isRequired } + shouldComponentUpdate(nextProps) { + // BUG: props.response is always coming back as a new Immutable instance + // same issue as responses.jsx (tryItOutResponse) + return this.props.response !== nextProps.response + || this.props.path !== nextProps.path + || this.props.method !== nextProps.method + || this.props.displayRequestDuration !== nextProps.displayRequestDuration + } + render() { - const { response, getComponent, getConfigs, displayRequestDuration, specSelectors, pathMethod } = this.props + const { response, getComponent, getConfigs, displayRequestDuration, specSelectors, path, method } = this.props const { showMutatedRequest } = getConfigs() - const curlRequest = showMutatedRequest ? specSelectors.mutatedRequestFor(pathMethod[0], pathMethod[1]) : specSelectors.requestFor(pathMethod[0], pathMethod[1]) + const curlRequest = showMutatedRequest ? specSelectors.mutatedRequestFor(path, method) : specSelectors.requestFor(path, method) const status = response.get("status") const url = response.get("url") const headers = response.get("headers").toJS() @@ -118,7 +129,6 @@ export default class LiveResponse extends React.Component { static propTypes = { getComponent: PropTypes.func.isRequired, - request: ImPropTypes.map, response: ImPropTypes.map } } diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index 457ee863..9b71d465 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -1,32 +1,22 @@ import React, { PureComponent } from "react" import PropTypes from "prop-types" import { getList } from "core/utils" -import * as CustomPropTypes from "core/proptypes" import { sanitizeUrl } from "core/utils" - -//import "less/opblock" +import { Iterable } from "immutable" export default class Operation extends PureComponent { static propTypes = { - path: PropTypes.string.isRequired, - method: PropTypes.string.isRequired, - operation: PropTypes.object.isRequired, - showSummary: PropTypes.bool, - isShown: PropTypes.bool.isRequired, + operation: PropTypes.instanceOf(Iterable).isRequired, + response: PropTypes.instanceOf(Iterable), + request: PropTypes.instanceOf(Iterable), - tagKey: PropTypes.string, - operationKey: PropTypes.string, - jumpToKey: CustomPropTypes.arrayOrString.isRequired, - - allowTryItOut: PropTypes.bool, - - displayOperationId: PropTypes.bool, - displayRequestDuration: PropTypes.bool, - - response: PropTypes.object, - request: PropTypes.object, + toggleShown: PropTypes.func.isRequired, + onTryoutClick: PropTypes.func.isRequired, + onCancelClick: PropTypes.func.isRequired, + onExecute: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired, + getConfigs: PropTypes.func.isRequired, authActions: PropTypes.object, authSelectors: PropTypes.object, specActions: PropTypes.object.isRequired, @@ -34,87 +24,64 @@ export default class Operation extends PureComponent { oas3Actions: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired, layoutSelectors: PropTypes.object.isRequired, - fn: PropTypes.object.isRequired, - getConfigs: PropTypes.func.isRequired + fn: PropTypes.object.isRequired } static defaultProps = { - showSummary: true, + operation: null, response: null, - allowTryItOut: true, - displayOperationId: false, - displayRequestDuration: false - } - - constructor(props, context) { - super(props, context) - this.state = { - tryItOutEnabled: false - } - } - - componentWillReceiveProps(nextProps) { - if(nextProps.response !== this.props.response) { - this.setState({ executeInProgress: false }) - } - } - - toggleShown =() => { - let { layoutActions, tagKey, operationKey, isShown } = this.props - const isShownKey = ["operations", tagKey, operationKey] - - layoutActions.show(isShownKey, !isShown) - } - - onTryoutClick =() => { - this.setState({tryItOutEnabled: !this.state.tryItOutEnabled}) - } - - onCancelClick =() => { - let { specActions, path, method } = this.props - this.setState({tryItOutEnabled: !this.state.tryItOutEnabled}) - specActions.clearValidateParams([path, method]) - } - - onExecute = () => { - this.setState({ executeInProgress: true }) + request: null } render() { let { - operationKey, - tagKey, - isShown, - jumpToKey, - path, - method, - operation, - showSummary, response, request, - allowTryItOut, - displayOperationId, - displayRequestDuration, + toggleShown, + onTryoutClick, + onCancelClick, + onExecute, fn, getComponent, + getConfigs, specActions, specSelectors, authActions, authSelectors, - getConfigs, oas3Actions } = this.props + let operationProps = this.props.operation - let summary = operation.get("summary") - let description = operation.get("description") - let deprecated = operation.get("deprecated") - let externalDocs = operation.get("externalDocs") + let { + isShown, + isShownKey, + jumpToKey, + path, + method, + op, + showSummary, + operationId, + allowTryItOut, + displayOperationId, + displayRequestDuration, + isDeepLinkingEnabled, + tryItOutEnabled, + executeInProgress + } = operationProps.toJS() + + let { + summary, + description, + deprecated, + externalDocs, + schemes + } = op.operation + + let operation = operationProps.getIn(["op", "operation"]) let responses = operation.get("responses") - let security = operation.get("security") || specSelectors.security() let produces = operation.get("produces") - let schemes = operation.get("schemes") + let security = operation.get("security") || specSelectors.security() let parameters = getList(operation, ["parameters"]) - let operationId = operation.get("__originalOperationId") let operationScheme = specSelectors.operationScheme(path, method) const Responses = getComponent("responses") @@ -127,22 +94,17 @@ export default class Operation extends PureComponent { const Markdown = getComponent( "Markdown" ) const Schemes = getComponent( "schemes" ) - const { deepLinking } = getConfigs() - - const isDeepLinkingEnabled = deepLinking && deepLinking !== "false" - // Merge in Live Response if(responses && response && response.size > 0) { let notDocumented = !responses.get(String(response.get("status"))) response = response.set("notDocumented", notDocumented) } - let { tryItOutEnabled } = this.state let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method ) return ( -
-
+
+
{method.toUpperCase()} + onExecute={ onExecute } /> } { (!tryItOutEnabled || !response || !allowTryItOut) ? null : }
- {this.state.executeInProgress ?
: null} + {executeInProgress ?
: null} { !responses ? null : } diff --git a/src/core/components/operations.jsx b/src/core/components/operations.jsx index 1784fd8e..ea76fa84 100644 --- a/src/core/components/operations.jsx +++ b/src/core/components/operations.jsx @@ -34,15 +34,12 @@ export default class Operations extends React.Component { let taggedOps = specSelectors.taggedOperations() - const Operation = getComponent("operation") + const OperationContainer = getComponent("OperationContainer", true) const Collapse = getComponent("Collapse") const Markdown = getComponent("Markdown") - let showSummary = layoutSelectors.showSummary() let { docExpansion, - displayOperationId, - displayRequestDuration, maxDisplayedTags, deepLinking } = getConfigs() @@ -120,46 +117,23 @@ export default class Operations extends React.Component { { operations.map( op => { + const path = op.get("path") + const method = op.get("method") - const path = op.get("path", "") - const method = op.get("method", "") - const jumpToKey = `paths.${path}.${method}` - - const operationId = - op.getIn(["operation", "operationId"]) || op.getIn(["operation", "__originalOperationId"]) || opId(op.get("operation"), path, method) || op.get("id") - const tagKey = createDeepLinkPath(tag) - const operationKey = createDeepLinkPath(operationId) - - const allowTryItOut = specSelectors.allowTryItOutFor(op.get("path"), op.get("method")) - const response = specSelectors.responseFor(op.get("path"), op.get("method")) - const request = specSelectors.requestFor(op.get("path"), op.get("method")) - - return { @@ -42,7 +42,7 @@ export default class Response extends React.Component { static propTypes = { code: PropTypes.string.isRequired, - response: PropTypes.object, + response: PropTypes.instanceOf(Iterable), className: PropTypes.string, getComponent: PropTypes.func.isRequired, getConfigs: PropTypes.func.isRequired, @@ -58,6 +58,12 @@ export default class Response extends React.Component { onContentTypeChange: () => {} }; + shouldComponentUpdate(nextProps) { + return this.props.code !== nextProps.code + || this.props.response !== nextProps.response + || this.props.className !== nextProps.className + } + _onContentTypeChange = (value) => { const { onContentTypeChange, controlsAcceptHeader } = this.props this.setState({ responseContentType: value }) diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index ebde3425..408d4621 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -1,41 +1,52 @@ import React from "react" import PropTypes from "prop-types" -import { fromJS } from "immutable" +import { fromJS, Iterable } from "immutable" import { defaultStatusCode, getAcceptControllingResponse } from "core/utils" export default class Responses extends React.Component { - static propTypes = { - request: PropTypes.object, - tryItOutResponse: PropTypes.object, - responses: PropTypes.object.isRequired, - produces: PropTypes.object, + tryItOutResponse: PropTypes.instanceOf(Iterable), + responses: PropTypes.instanceOf(Iterable).isRequired, + produces: PropTypes.instanceOf(Iterable), producesValue: PropTypes.any, + displayRequestDuration: PropTypes.bool.isRequired, + path: PropTypes.string.isRequired, + method: PropTypes.string.isRequired, getComponent: PropTypes.func.isRequired, getConfigs: PropTypes.func.isRequired, specSelectors: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired, oas3Actions: PropTypes.object.isRequired, - pathMethod: PropTypes.array.isRequired, - displayRequestDuration: PropTypes.bool.isRequired, fn: PropTypes.object.isRequired } static defaultProps = { - request: null, tryItOutResponse: null, produces: fromJS(["application/json"]), displayRequestDuration: false } - onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val) + 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) onResponseContentTypeChange = ({ controlsAcceptHeader, value }) => { - const { oas3Actions, pathMethod } = this.props + const { oas3Actions, path, method } = this.props if(controlsAcceptHeader) { oas3Actions.setResponseContentType({ value, - pathMethod + path, + method }) } } @@ -43,7 +54,6 @@ export default class Responses extends React.Component { render() { let { responses, - request, tryItOutResponse, getComponent, getConfigs, @@ -81,12 +91,12 @@ export default class Responses extends React.Component { { !tryItOutResponse ? null :
-

Responses

diff --git a/src/core/containers/OperationContainer.jsx b/src/core/containers/OperationContainer.jsx new file mode 100644 index 00000000..6b06c6cd --- /dev/null +++ b/src/core/containers/OperationContainer.jsx @@ -0,0 +1,212 @@ +import React, { PureComponent } from "react" +import PropTypes from "prop-types" +import { helpers } from "swagger-client" +import { Iterable, fromJS } from "immutable" + +const { opId } = helpers + +export default class OperationContainer extends PureComponent { + constructor(props, context) { + super(props, context) + this.state = { + tryItOutEnabled: false, + executeInProgress: false + } + } + + static propTypes = { + op: PropTypes.instanceOf(Iterable).isRequired, + tag: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + method: PropTypes.string.isRequired, + operationId: PropTypes.string.isRequired, + showSummary: PropTypes.bool.isRequired, + isShown: PropTypes.bool.isRequired, + isShownKey: PropTypes.instanceOf(Iterable).isRequired, + jumpToKey: PropTypes.string.isRequired, + allowTryItOut: PropTypes.bool, + displayOperationId: PropTypes.bool, + displayRequestDuration: PropTypes.bool, + response: PropTypes.instanceOf(Iterable), + request: PropTypes.instanceOf(Iterable), + isDeepLinkingEnabled: PropTypes.bool.isRequired, + + getComponent: PropTypes.func.isRequired, + authActions: PropTypes.object, + authSelectors: PropTypes.object, + specActions: PropTypes.object.isRequired, + specSelectors: PropTypes.object.isRequired, + layoutActions: PropTypes.object.isRequired, + layoutSelectors: PropTypes.object.isRequired, + fn: PropTypes.object.isRequired, + getConfigs: PropTypes.func.isRequired + } + + static defaultProps = { + showSummary: true, + response: null, + allowTryItOut: true, + displayOperationId: false, + displayRequestDuration: false + } + + mapStateToProps(nextState, props) { + const { op, layoutSelectors, getConfigs } = props + const { docExpansion, deepLinking, displayOperationId, displayRequestDuration } = getConfigs() + const showSummary = layoutSelectors.showSummary() + const operationId = op.getIn(["operation", "operationId"]) || op.getIn(["operation", "__originalOperationId"]) || opId(op.get("operation"), props.path, props.method) || op.get("id") + const isShownKey = fromJS(["operations", props.tag, operationId]) + const isDeepLinkingEnabled = deepLinking && deepLinking !== "false" + + return { + operationId, + isDeepLinkingEnabled, + isShownKey, + showSummary, + displayOperationId, + displayRequestDuration, + isShown: layoutSelectors.isShown(isShownKey, docExpansion === "full" ), + jumpToKey: `paths.${props.path}.${props.method}`, + allowTryItOut: props.specSelectors.allowTryItOutFor(props.path, props.method), + response: props.specSelectors.responseFor(props.path, props.method), + request: props.specSelectors.requestFor(props.path, props.method) + } + } + + componentWillReceiveProps(nextProps) { + const defaultContentType = "application/json" + let { specActions, path, method, op } = nextProps + let operation = op.get("operation") + let producesValue = operation.get("produces_value") + let produces = operation.get("produces") + let consumes = operation.get("consumes") + let consumesValue = operation.get("consumes_value") + + if(nextProps.response !== this.props.response) { + this.setState({ executeInProgress: false }) + } + + if (producesValue === undefined) { + producesValue = produces && produces.size ? produces.first() : defaultContentType + specActions.changeProducesValue([path, method], producesValue) + } + + if (consumesValue === undefined) { + consumesValue = consumes && consumes.size ? consumes.first() : defaultContentType + specActions.changeConsumesValue([path, method], consumesValue) + } + } + + shouldComponentUpdate(nextProps, nextState) { + const render = this.state.tryItOutEnabled !== nextState.tryItOutEnabled + || this.state.executeInProgress !== nextState.executeInProgress + || this.props.op !== nextProps.op + || this.props.tag !== nextProps.tag + || this.props.path !== nextProps.path + || this.props.method !== nextProps.method + || this.props.operationId !== nextProps.operationId + || this.props.showSummary !== nextProps.showSummary + || this.props.isShown !== nextProps.isShown + || this.props.jumpToKey !== nextProps.jumpToKey + || this.props.allowTryItOut !== nextProps.allowTryItOut + || this.props.displayOperationId !== nextProps.displayOperationId + || this.props.displayRequestDuration !== nextProps.displayRequestDuration + || this.props.response !== nextProps.response + || this.props.request !== nextProps.request + || this.props.isDeepLinkingEnabled !== nextProps.isDeepLinkingEnabled + return render + } + + toggleShown =() => { + let { layoutActions, isShownKey, isShown } = this.props + layoutActions.show(isShownKey, !isShown) + } + + onTryoutClick =() => { + this.setState({tryItOutEnabled: !this.state.tryItOutEnabled}) + } + + onCancelClick =() => { + let { specActions, path, method } = this.props + this.setState({tryItOutEnabled: !this.state.tryItOutEnabled}) + specActions.clearValidateParams([path, method]) + } + + onExecute = () => { + this.setState({ executeInProgress: true }) + } + + render() { + let { + op, + tag, + path, + method, + operationId, + showSummary, + isShown, + isShownKey, + jumpToKey, + allowTryItOut, + response, + request, + displayOperationId, + displayRequestDuration, + isDeepLinkingEnabled, + specSelectors, + specActions, + getComponent, + getConfigs, + layoutSelectors, + layoutActions, + authActions, + authSelectors, + fn + } = this.props + + const Operation = getComponent( "operation" ) + + const operationProps = fromJS({ + op, + tag, + path, + method, + operationId, + showSummary, + isShown, + isShownKey, + jumpToKey, + allowTryItOut, + request, + displayOperationId, + displayRequestDuration, + isDeepLinkingEnabled, + executeInProgress: this.state.executeInProgress, + tryItOutEnabled: this.state.tryItOutEnabled + }) + + return ( + + ) + } + +} diff --git a/src/core/index.js b/src/core/index.js index 76a0d705..7298043a 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -7,8 +7,7 @@ import * as AllPlugins from "core/plugins/all" import { parseSearch } from "core/utils" if (process.env.NODE_ENV !== "production") { - const Perf = require("react-addons-perf") - window.Perf = Perf + window.Perf = require("react-addons-perf") } // eslint-disable-next-line no-undef diff --git a/src/core/plugins/oas3/actions.js b/src/core/plugins/oas3/actions.js index ad81f3e7..c9abc855 100644 --- a/src/core/plugins/oas3/actions.js +++ b/src/core/plugins/oas3/actions.js @@ -28,10 +28,10 @@ export function setRequestContentType ({ value, pathMethod }) { } } -export function setResponseContentType ({ value, pathMethod }) { +export function setResponseContentType ({ value, path, method }) { return { type: UPDATE_RESPONSE_CONTENT_TYPE, - payload: { value, pathMethod } + payload: { value, path, method } } } diff --git a/src/core/plugins/oas3/reducers.js b/src/core/plugins/oas3/reducers.js index 149f55e3..8590284d 100644 --- a/src/core/plugins/oas3/reducers.js +++ b/src/core/plugins/oas3/reducers.js @@ -18,8 +18,7 @@ export default { let [path, method] = pathMethod return state.setIn( [ "requestData", path, method, "requestContentType" ], value) }, - [UPDATE_RESPONSE_CONTENT_TYPE]: (state, { payload: { value, pathMethod } } ) =>{ - let [path, method] = pathMethod + [UPDATE_RESPONSE_CONTENT_TYPE]: (state, { payload: { value, path, method } } ) =>{ return state.setIn( [ "requestData", path, method, "responseContentType" ], value) }, [UPDATE_SERVER_VARIABLE_VALUE]: (state, { payload: { server, key, val } } ) =>{ diff --git a/src/core/plugins/oas3/wrap-components/parameters.jsx b/src/core/plugins/oas3/wrap-components/parameters.jsx index ac61e90f..33e31d81 100644 --- a/src/core/plugins/oas3/wrap-components/parameters.jsx +++ b/src/core/plugins/oas3/wrap-components/parameters.jsx @@ -95,6 +95,8 @@ class Parameters extends Component { operation } = this.props + console.log('rendering Parameters') + const ParameterRow = getComponent("parameterRow") const TryItOutButton = getComponent("TryItOutButton") const ContentType = getComponent("contentType") diff --git a/src/core/plugins/view/root-injects.js b/src/core/plugins/view/root-injects.js index 1d996102..568ac6ac 100644 --- a/src/core/plugins/view/root-injects.js +++ b/src/core/plugins/view/root-injects.js @@ -20,8 +20,11 @@ const RootWrapper = (reduxStore, ComponentToWrap) => class extends Component { } const makeContainer = (getSystem, component, reduxStore) => { + const mapStateToProps = component.prototype.mapStateToProps || function(state) { + return {state} + } let wrappedWithSystem = SystemWrapper(getSystem, component, reduxStore) - let connected = connect(state => ({state}))(wrappedWithSystem) + let connected = connect( mapStateToProps )(wrappedWithSystem) if(reduxStore) return RootWrapper(reduxStore, connected) return connected diff --git a/src/core/presets/base.js b/src/core/presets/base.js index d173336e..d608b544 100644 --- a/src/core/presets/base.js +++ b/src/core/presets/base.js @@ -13,6 +13,8 @@ import downloadUrlPlugin from "core/plugins/download-url" import configsPlugin from "plugins/configs" import deepLinkingPlugin from "core/plugins/deep-linking" +import OperationContainer from "core/containers/OperationContainer" + import App from "core/components/app" import AuthorizationPopup from "core/components/auth/authorization-popup" import AuthorizeBtn from "core/components/auth/authorize-btn" @@ -112,7 +114,8 @@ export default function() { TryItOutButton, Markdown, BaseLayout, - VersionStamp + VersionStamp, + OperationContainer } } diff --git a/src/core/system.js b/src/core/system.js index e4eee5b8..06071bc9 100644 --- a/src/core/system.js +++ b/src/core/system.js @@ -289,8 +289,7 @@ export default class Store { getMapStateToProps() { return () => { - let obj = Object.assign({}, this.getSystem()) - return obj + return Object.assign({}, this.getSystem()) } } diff --git a/test/core/system/system.js b/test/core/system/system.js index ada3a93b..2b4990df 100644 --- a/test/core/system/system.js +++ b/test/core/system/system.js @@ -1,7 +1,11 @@ /* eslint-env mocha */ +import React, { PureComponent } from "react" import expect from "expect" import System from "core/system" import { fromJS } from "immutable" +import { render } from "enzyme" +import ViewPlugin from "core/plugins/view/index.js" +import { connect, Provider } from "react-redux" describe("bound system", function(){ @@ -444,4 +448,66 @@ describe("bound system", function(){ }) + describe("getComponent", function() { + it("returns a component from the system", function() { + const system = new System({ + plugins: [ + ViewPlugin, + { + components: { + test: ({ name }) =>
{name} component
+ } + } + ] + }) + + // When + var Component = system.getSystem().getComponent("test") + const renderedComponent = render() + expect(renderedComponent.text()).toEqual("Test component") + }) + + it("allows container components to provide their own `mapStateToProps` function", function() { + // Given + class ContainerComponent extends PureComponent { + mapStateToProps(nextState, props) { + return { + "abc": "This came from mapStateToProps" + } + } + + static defaultProps = { + "abc" : "" + } + + render() { + return ( +
{ this.props.abc }
+ ) + } + } + const system = new System({ + plugins: [ + ViewPlugin, + { + components: { + ContainerComponent + } + } + ] + }) + + // When + var Component = system.getSystem().getComponent("ContainerComponent", true) + const renderedComponent = render( + + + + ) + + // Then + expect(renderedComponent.text()).toEqual("This came from mapStateToProps") + }) + }) + })