Co-authored-by: Tim Lai <timothy.lai@gmail.com>bubble
@@ -1,5 +1,6 @@ | |||
import React, { Component } from "react" | |||
import PropTypes from "prop-types" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
import Im from "immutable" | |||
export default class ModelCollapse extends Component { | |||
@@ -13,7 +14,8 @@ export default class ModelCollapse extends Component { | |||
onToggle: PropTypes.func, | |||
hideSelfOnExpand: PropTypes.bool, | |||
layoutActions: PropTypes.object, | |||
layoutSelectors: PropTypes.object.isRequired | |||
layoutSelectors: PropTypes.object.isRequired, | |||
specPath: ImPropTypes.list.isRequired, | |||
} | |||
static defaultProps = { | |||
@@ -21,7 +23,8 @@ export default class ModelCollapse extends Component { | |||
expanded: false, | |||
title: null, | |||
onToggle: () => {}, | |||
hideSelfOnExpand: false | |||
hideSelfOnExpand: false, | |||
specPath: Im.List([]), | |||
} | |||
constructor(props, context) { | |||
@@ -63,11 +66,10 @@ export default class ModelCollapse extends Component { | |||
onLoad = (ref) => { | |||
if(ref) { | |||
const name = this.props.modelName | |||
const scrollToKey = this.props.layoutSelectors.getScrollToKey() | |||
if( Im.is(scrollToKey, Im.fromJS(["models", name])) ) this.toggleCollapsed() | |||
this.props.layoutActions.readyToScroll(["models", name], ref.parentElement) | |||
if( Im.is(scrollToKey, this.props.specPath) ) this.toggleCollapsed() | |||
this.props.layoutActions.readyToScroll(this.props.specPath, ref.parentElement) | |||
} | |||
} | |||
@@ -83,7 +85,7 @@ export default class ModelCollapse extends Component { | |||
} | |||
return ( | |||
<span className={classes || ""}> | |||
<span className={classes || ""} ref={this.onLoad}> | |||
{ title && <span onClick={this.toggleCollapsed} className="pointer">{title}</span> } | |||
<span onClick={ this.toggleCollapsed } className="pointer"> | |||
<span className={ "model-toggle" + ( this.state.expanded ? "" : " collapsed" ) }></span> | |||
@@ -1,15 +1,15 @@ | |||
import React, { Component, } from "react" | |||
import PropTypes from "prop-types" | |||
//import layoutActions from "actions/layout" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
export default class ModelWrapper extends Component { | |||
static propTypes = { | |||
schema: PropTypes.object.isRequired, | |||
name: PropTypes.string, | |||
displayName: PropTypes.string, | |||
fullPath: PropTypes.array.isRequired, | |||
specPath: ImPropTypes.list.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
getConfigs: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
@@ -20,15 +20,10 @@ export default class ModelWrapper extends Component { | |||
includeWriteOnly: PropTypes.bool, | |||
} | |||
getSchemaBasePath = () => { | |||
const isOAS3 = this.props.specSelectors.isOAS3() | |||
return isOAS3 ? ["components", "schemas"] : ["definitions"] | |||
} | |||
onToggle = (name,isShown) => { | |||
// If this prop is present, we'll have deepLinking for it | |||
if(this.props.layoutActions) { | |||
this.props.layoutActions.show([...this.getSchemaBasePath(), name],isShown) | |||
this.props.layoutActions.show(this.props.fullPath, isShown) | |||
} | |||
} | |||
@@ -39,7 +34,7 @@ export default class ModelWrapper extends Component { | |||
let expanded | |||
if(this.props.layoutSelectors) { | |||
// If this is prop is present, we'll have deepLinking for it | |||
expanded = this.props.layoutSelectors.isShown(["models",this.props.name]) | |||
expanded = this.props.layoutSelectors.isShown(this.props.fullPath) | |||
} | |||
return <div className="model-box"> | |||
@@ -23,16 +23,22 @@ export default class Models extends Component { | |||
handleToggle = (name, isExpanded) => { | |||
const { layoutActions } = this.props | |||
layoutActions.show(["models", name], isExpanded) | |||
layoutActions.show([...this.getSchemaBasePath(), name], isExpanded) | |||
if(isExpanded) { | |||
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name]) | |||
} | |||
} | |||
onLoad = (ref) => { | |||
onLoadModels = (ref) => { | |||
if (ref) { | |||
this.props.layoutActions.readyToScroll(this.getSchemaBasePath(), ref) | |||
} | |||
} | |||
onLoadModel = (ref) => { | |||
if (ref) { | |||
const name = ref.getAttribute("data-name") | |||
this.props.layoutActions.readyToScroll(["models", name], ref) | |||
this.props.layoutActions.readyToScroll([...this.getSchemaBasePath(), name], ref) | |||
} | |||
} | |||
@@ -42,8 +48,8 @@ export default class Models extends Component { | |||
let { docExpansion, defaultModelsExpandDepth } = getConfigs() | |||
if (!definitions.size || defaultModelsExpandDepth < 0) return null | |||
let showModels = layoutSelectors.isShown("models", defaultModelsExpandDepth > 0 && docExpansion !== "none") | |||
const specPathBase = this.getSchemaBasePath() | |||
let showModels = layoutSelectors.isShown(specPathBase, defaultModelsExpandDepth > 0 && docExpansion !== "none") | |||
const isOAS3 = specSelectors.isOAS3() | |||
const ModelWrapper = getComponent("ModelWrapper") | |||
@@ -51,8 +57,8 @@ export default class Models extends Component { | |||
const ModelCollapse = getComponent("ModelCollapse") | |||
const JumpToPath = getComponent("JumpToPath") | |||
return <section className={ showModels ? "models is-open" : "models"}> | |||
<h4 onClick={() => layoutActions.show("models", !showModels)}> | |||
return <section className={ showModels ? "models is-open" : "models"} ref={this.onLoadModels}> | |||
<h4 onClick={() => layoutActions.show(specPathBase, !showModels)}> | |||
<span>{isOAS3 ? "Schemas" : "Models" }</span> | |||
<svg width="20" height="20"> | |||
<use xlinkHref={showModels ? "#large-arrow-down" : "#large-arrow"} /> | |||
@@ -63,6 +69,7 @@ export default class Models extends Component { | |||
definitions.entrySeq().map(([name])=>{ | |||
const fullPath = [...specPathBase, name] | |||
const specPath = Im.List(fullPath) | |||
const schemaValue = specSelectors.specResolvedSubtree(fullPath) | |||
const rawSchemaValue = specSelectors.specJson().getIn(fullPath) | |||
@@ -71,20 +78,19 @@ export default class Models extends Component { | |||
const rawSchema = Map.isMap(rawSchemaValue) ? rawSchemaValue : Im.Map() | |||
const displayName = schema.get("title") || rawSchema.get("title") || name | |||
const isShown = layoutSelectors.isShown( ["models", name], false ) | |||
const isShown = layoutSelectors.isShown(fullPath, false) | |||
if( isShown && (schema.size === 0 && rawSchema.size > 0) ) { | |||
// Firing an action in a container render is not great, | |||
// but it works for now. | |||
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name]) | |||
this.props.specActions.requestResolvedSubtree(fullPath) | |||
} | |||
const specPath = Im.List([...specPathBase, name]) | |||
const content = <ModelWrapper name={ name } | |||
expandDepth={ defaultModelsExpandDepth } | |||
schema={ schema || Im.Map() } | |||
displayName={displayName} | |||
fullPath={fullPath} | |||
specPath={specPath} | |||
getComponent={ getComponent } | |||
specSelectors={ specSelectors } | |||
@@ -101,7 +107,7 @@ export default class Models extends Component { | |||
</span> | |||
return <div id={ `model-${name}` } className="model-container" key={ `models-section-${name}` } | |||
data-name={name} ref={this.onLoad} > | |||
data-name={name} ref={this.onLoadModel} > | |||
<span className="models-jump-to-path"><JumpToPath specPath={specPath} /></span> | |||
<ModelCollapse | |||
classes="model-box" | |||
@@ -110,6 +116,9 @@ export default class Models extends Component { | |||
title={title} | |||
displayName={displayName} | |||
modelName={name} | |||
specPath={specPath} | |||
layoutSelectors={layoutSelectors} | |||
layoutActions={layoutActions} | |||
hideSelfOnExpand={true} | |||
expanded={ defaultModelsExpandDepth > 0 && isShown } | |||
>{content}</ModelCollapse> | |||
@@ -0,0 +1,93 @@ | |||
openapi: "3.0.0" | |||
components: | |||
schemas: | |||
Order: | |||
type: object | |||
properties: | |||
id: | |||
type: integer | |||
format: int64 | |||
petId: | |||
type: integer | |||
format: int64 | |||
quantity: | |||
type: integer | |||
format: int32 | |||
shipDate: | |||
type: string | |||
format: date-time | |||
status: | |||
type: string | |||
description: Order Status | |||
enum: | |||
- placed | |||
- approved | |||
- delivered | |||
complete: | |||
type: boolean | |||
default: false | |||
xml: | |||
name: Order | |||
User: | |||
type: object | |||
properties: | |||
id: | |||
type: integer | |||
format: int64 | |||
username: | |||
type: string | |||
firstName: | |||
type: string | |||
lastName: | |||
type: string | |||
email: | |||
type: string | |||
password: | |||
type: string | |||
phone: | |||
type: string | |||
userStatus: | |||
type: integer | |||
format: int32 | |||
description: User Status | |||
xml: | |||
name: User | |||
Pet: | |||
type: object | |||
required: | |||
- name | |||
- photoUrls | |||
properties: | |||
id: | |||
type: integer | |||
format: int64 | |||
category: | |||
$ref: '#/components/schemas/Category' | |||
name: | |||
type: string | |||
example: doggie | |||
photoUrls: | |||
type: array | |||
xml: | |||
name: photoUrl | |||
wrapped: true | |||
items: | |||
type: string | |||
tags: | |||
type: array | |||
xml: | |||
name: tag | |||
wrapped: true | |||
items: | |||
$ref: '#/components/schemas/Tag' | |||
status: | |||
type: string | |||
description: pet status in the store | |||
enum: | |||
- available | |||
- pending | |||
- sold | |||
xml: | |||
name: Pet | |||
@@ -0,0 +1,92 @@ | |||
swagger: "2.0" | |||
definitions: | |||
Order: | |||
type: object | |||
properties: | |||
id: | |||
type: integer | |||
format: int64 | |||
petId: | |||
type: integer | |||
format: int64 | |||
quantity: | |||
type: integer | |||
format: int32 | |||
shipDate: | |||
type: string | |||
format: date-time | |||
status: | |||
type: string | |||
description: Order Status | |||
enum: | |||
- placed | |||
- approved | |||
- delivered | |||
complete: | |||
type: boolean | |||
default: false | |||
xml: | |||
name: Order | |||
User: | |||
type: object | |||
properties: | |||
id: | |||
type: integer | |||
format: int64 | |||
username: | |||
type: string | |||
firstName: | |||
type: string | |||
lastName: | |||
type: string | |||
email: | |||
type: string | |||
password: | |||
type: string | |||
phone: | |||
type: string | |||
userStatus: | |||
type: integer | |||
format: int32 | |||
description: User Status | |||
xml: | |||
name: User | |||
Pet: | |||
type: object | |||
required: | |||
- name | |||
- photoUrls | |||
properties: | |||
id: | |||
type: integer | |||
format: int64 | |||
category: | |||
$ref: '#/definitions/Category' | |||
name: | |||
type: string | |||
example: doggie | |||
photoUrls: | |||
type: array | |||
xml: | |||
name: photoUrl | |||
wrapped: true | |||
items: | |||
type: string | |||
tags: | |||
type: array | |||
xml: | |||
name: tag | |||
wrapped: true | |||
items: | |||
$ref: '#/definitions/Tag' | |||
status: | |||
type: string | |||
description: pet status in the store | |||
enum: | |||
- available | |||
- pending | |||
- sold | |||
xml: | |||
name: Pet | |||
@@ -0,0 +1,62 @@ | |||
describe("Model collapse/expand feature", () => { | |||
describe("in Swagger 2", () => { | |||
const swagger2BaseUrl = "/?deepLinking=true&url=/documents/features/models.swagger.yaml" | |||
const urlFragment = "#/definitions/Pet" | |||
ModelCollapseTest(swagger2BaseUrl, urlFragment) | |||
}) | |||
describe("in OpenAPI 3", () => { | |||
const openAPI3BaseUrl = "/?deepLinking=true&url=/documents/features/models.openapi.yaml" | |||
ModelCollapseTest(openAPI3BaseUrl) | |||
}) | |||
}) | |||
function ModelCollapseTest(baseUrl, urlFragment) { | |||
it("Models section should be expanded on load", () => { | |||
cy.visit(baseUrl) | |||
.get(".models") | |||
.should("have.class", "is-open") | |||
.get("#model-Pet") | |||
.should("exist") | |||
}) | |||
it("Models section should collapse and expand when toggled", () => { | |||
cy.visit(baseUrl) | |||
.get(".models h4") | |||
.click() | |||
.get(".models") | |||
.should("not.have.class", "is-open") | |||
.get("#model-Order") | |||
.should("not.exist") | |||
.get(".models h4") | |||
.click() | |||
.get(".models") | |||
.should("have.class", "is-open") | |||
.get("#model-Order") | |||
.should("exist") | |||
}) | |||
it("Model should collapse and expand when toggled clicking title", () => { | |||
cy.visit(baseUrl) | |||
.get("#model-User .model-box .pointer:nth-child(1)") | |||
.click() | |||
.get("#model-User .model-box .model .inner-object") | |||
.should("exist") | |||
.get("#model-User .model-box .pointer:nth-child(1)") | |||
.click() | |||
.get("#model-User .model-box .model .inner-object") | |||
.should("not.exist") | |||
}) | |||
it("Model should collapse and expand when toggled clicking arrow", () => { | |||
cy.visit(baseUrl) | |||
.get("#model-User .model-box .pointer:nth-child(2)") | |||
.click() | |||
.get("#model-User .model-box .model .inner-object") | |||
.should("exist") | |||
.get("#model-User .model-box .pointer:nth-child(2)") | |||
.click() | |||
.get("#model-User .model-box .model .inner-object") | |||
.should("not.exist") | |||
}) | |||
} | |||