refactor: consolidate all JSON Schema 5 rendering code into json-schema-5 plugin (#9798)

This commit is contained in:
Vladimír Gorej
2024-04-10 12:11:51 +02:00
committed by GitHub
parent 46c849b0b3
commit 3b72ee18bc
27 changed files with 56 additions and 52 deletions

View File

@@ -0,0 +1,80 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
import { sanitizeUrl } from "core/utils"
const propClass = "property"
export default class ArrayModel extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
name: PropTypes.string,
displayName: PropTypes.string,
required: PropTypes.bool,
expandDepth: PropTypes.number,
specPath: ImPropTypes.list.isRequired,
depth: PropTypes.number,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
}
render(){
let { getComponent, getConfigs, schema, depth, expandDepth, name, displayName, specPath } = this.props
let description = schema.get("description")
let items = schema.get("items")
let title = schema.get("title") || displayName || name
let properties = schema.filter( ( v, key) => ["type", "items", "description", "$$ref", "externalDocs"].indexOf(key) === -1 )
let externalDocsUrl = schema.getIn(["externalDocs", "url"])
let externalDocsDescription = schema.getIn(["externalDocs", "description"])
const Markdown = getComponent("Markdown", true)
const ModelCollapse = getComponent("ModelCollapse")
const Model = getComponent("Model")
const Property = getComponent("Property")
const Link = getComponent("Link")
const titleEl = title &&
<span className="model-title">
<span className="model-title__text">{ title }</span>
</span>
/*
Note: we set `name={null}` in <Model> below because we don't want
the name of the current Model passed (and displayed) as the name of the array element Model
*/
return <span className="model">
<ModelCollapse title={titleEl} expanded={ depth <= expandDepth } collapsedContent="[...]">
[
{
properties.size ? properties.entrySeq().map( ( [ key, v ] ) => <Property key={`${key}-${v}`} propKey={ key } propVal={ v } propClass={ propClass } />) : null
}
{
!description ? (properties.size ? <div className="markdown"></div> : null) :
<Markdown source={ description } />
}
{ externalDocsUrl &&
<div className="external-docs">
<Link target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
</div>
}
<span>
<Model
{ ...this.props }
getConfigs={ getConfigs }
specPath={specPath.push("items")}
name={null}
schema={ items }
required={ false }
depth={ depth + 1 }
/>
</span>
]
</ModelCollapse>
</span>
}
}

View File

@@ -0,0 +1,19 @@
import React from "react"
import ImPropTypes from "react-immutable-proptypes"
const EnumModel = ({ value, getComponent }) => {
let ModelCollapse = getComponent("ModelCollapse")
let collapsedContent = <span>Array [ { value.count() } ]</span>
return <span className="prop-enum">
Enum:<br />
<ModelCollapse collapsedContent={ collapsedContent }>
[ { value.join(", ") } ]
</ModelCollapse>
</span>
}
EnumModel.propTypes = {
value: ImPropTypes.iterable,
getComponent: ImPropTypes.func
}
export default EnumModel

View File

@@ -0,0 +1,420 @@
import React, { PureComponent, Component } from "react"
import PropTypes from "prop-types"
import { List, fromJS } from "immutable"
import cx from "classnames"
import ImPropTypes from "react-immutable-proptypes"
import DebounceInput from "react-debounce-input"
import { stringify } from "core/utils"
const noop = ()=> {}
const JsonSchemaPropShape = {
getComponent: PropTypes.func.isRequired,
value: PropTypes.any,
onChange: PropTypes.func,
keyName: PropTypes.any,
fn: PropTypes.object.isRequired,
schema: PropTypes.object,
errors: ImPropTypes.list,
required: PropTypes.bool,
dispatchInitialValue: PropTypes.bool,
description: PropTypes.any,
disabled: PropTypes.bool,
}
const JsonSchemaDefaultProps = {
value: "",
onChange: noop,
schema: {},
keyName: "",
required: false,
errors: List()
}
export class JsonSchemaForm extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
componentDidMount() {
const { dispatchInitialValue, value, onChange } = this.props
if(dispatchInitialValue) {
onChange(value)
} else if(dispatchInitialValue === false) {
onChange("")
}
}
render() {
let { schema, errors, value, onChange, getComponent, fn, disabled } = this.props
const format = schema && schema.get ? schema.get("format") : null
const type = schema && schema.get ? schema.get("type") : null
let getComponentSilently = (name) => getComponent(name, false, { failSilently: true })
let Comp = type ? format ?
getComponentSilently(`JsonSchema_${type}_${format}`) :
getComponentSilently(`JsonSchema_${type}`) :
getComponent("JsonSchema_string")
if (!Comp) {
Comp = getComponent("JsonSchema_string")
}
return <Comp { ...this.props } errors={errors} fn={fn} getComponent={getComponent} value={value} onChange={onChange} schema={schema} disabled={disabled}/>
}
}
export class JsonSchema_string extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
onChange = (e) => {
const value = this.props.schema && this.props.schema.get("type") === "file" ? e.target.files[0] : e.target.value
this.props.onChange(value, this.props.keyName)
}
onEnumChange = (val) => this.props.onChange(val)
render() {
let { getComponent, value, schema, errors, required, description, disabled } = this.props
const enumValue = schema && schema.get ? schema.get("enum") : null
const format = schema && schema.get ? schema.get("format") : null
const type = schema && schema.get ? schema.get("type") : null
const schemaIn = schema && schema.get ? schema.get("in") : null
if (!value) {
value = "" // value should not be null; this fixes a Debounce error
}
errors = errors.toJS ? errors.toJS() : []
if ( enumValue ) {
const Select = getComponent("Select")
return (<Select className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
allowedValues={ [...enumValue] }
value={ value }
allowEmptyValue={ !required }
disabled={disabled}
onChange={ this.onEnumChange }/>)
}
const isDisabled = disabled || (schemaIn && schemaIn === "formData" && !("FormData" in window))
const Input = getComponent("Input")
if (type && type === "file") {
return (
<Input type="file"
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
onChange={this.onChange}
disabled={isDisabled} />
)
}
else {
return (
<DebounceInput
type={format && format === "password" ? "password" : "text"}
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
value={value}
minLength={0}
debounceTimeout={350}
placeholder={description}
onChange={this.onChange}
disabled={isDisabled} />
)
}
}
}
export class JsonSchema_array extends PureComponent {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
constructor(props, context) {
super(props, context)
this.state = { value: valueOrEmptyList(props.value), schema: props.schema}
}
UNSAFE_componentWillReceiveProps(props) {
const value = valueOrEmptyList(props.value)
if(value !== this.state.value)
this.setState({ value })
if(props.schema !== this.state.schema)
this.setState({ schema: props.schema })
}
onChange = () => {
this.props.onChange(this.state.value)
}
onItemChange = (itemVal, i) => {
this.setState(({ value }) => ({
value: value.set(i, itemVal)
}), this.onChange)
}
removeItem = (i) => {
this.setState(({ value }) => ({
value: value.delete(i)
}), this.onChange)
}
addItem = () => {
const { fn } = this.props
let newValue = valueOrEmptyList(this.state.value)
this.setState(() => ({
value: newValue.push(fn.getSampleSchema(this.state.schema.get("items"), false, {
includeWriteOnly: true
}))
}), this.onChange)
}
onEnumChange = (value) => {
this.setState(() => ({
value: value
}), this.onChange)
}
render() {
let { getComponent, required, schema, errors, fn, disabled } = this.props
errors = errors.toJS ? errors.toJS() : Array.isArray(errors) ? errors : []
const arrayErrors = errors.filter(e => typeof e === "string")
const needsRemoveError = errors.filter(e => e.needRemove !== undefined)
.map(e => e.error)
const value = this.state.value // expect Im List
const shouldRenderValue =
value && value.count && value.count() > 0 ? true : false
const schemaItemsEnum = schema.getIn(["items", "enum"])
const schemaItemsType = schema.getIn(["items", "type"])
const schemaItemsFormat = schema.getIn(["items", "format"])
const schemaItemsSchema = schema.get("items")
let ArrayItemsComponent
let isArrayItemText = false
let isArrayItemFile = (schemaItemsType === "file" || (schemaItemsType === "string" && schemaItemsFormat === "binary")) ? true : false
if (schemaItemsType && schemaItemsFormat) {
ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}_${schemaItemsFormat}`)
} else if (schemaItemsType === "boolean" || schemaItemsType === "array" || schemaItemsType === "object") {
ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}`)
}
// if ArrayItemsComponent not assigned or does not exist,
// use default schemaItemsType === "string" & JsonSchemaArrayItemText component
if (!ArrayItemsComponent && !isArrayItemFile) {
isArrayItemText = true
}
if ( schemaItemsEnum ) {
const Select = getComponent("Select")
return (<Select className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
multiple={ true }
value={ value }
disabled={disabled}
allowedValues={ schemaItemsEnum }
allowEmptyValue={ !required }
onChange={ this.onEnumChange }/>)
}
const Button = getComponent("Button")
return (
<div className="json-schema-array">
{shouldRenderValue ?
(value.map((item, i) => {
const itemErrors = fromJS([
...errors.filter((err) => err.index === i)
.map(e => e.error)
])
return (
<div key={i} className="json-schema-form-item">
{
isArrayItemFile ?
<JsonSchemaArrayItemFile
value={item}
onChange={(val)=> this.onItemChange(val, i)}
disabled={disabled}
errors={itemErrors}
getComponent={getComponent}
/>
: isArrayItemText ?
<JsonSchemaArrayItemText
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={itemErrors}
/>
: <ArrayItemsComponent {...this.props}
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={itemErrors}
schema={schemaItemsSchema}
getComponent={getComponent}
fn={fn}
/>
}
{!disabled ? (
<Button
className={`btn btn-sm json-schema-form-item-remove ${needsRemoveError.length ? "invalid" : null}`}
title={needsRemoveError.length ? needsRemoveError : ""}
onClick={() => this.removeItem(i)}
> - </Button>
) : null}
</div>
)
})
) : null
}
{!disabled ? (
<Button
className={`btn btn-sm json-schema-form-item-add ${arrayErrors.length ? "invalid" : null}`}
title={arrayErrors.length ? arrayErrors : ""}
onClick={this.addItem}
>
Add {schemaItemsType ? `${schemaItemsType} ` : ""}item
</Button>
) : null}
</div>
)
}
}
export class JsonSchemaArrayItemText extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
onChange = (e) => {
const value = e.target.value
this.props.onChange(value, this.props.keyName)
}
render() {
let { value, errors, description, disabled } = this.props
if (!value) {
value = "" // value should not be null
}
errors = errors.toJS ? errors.toJS() : []
return (<DebounceInput
type={"text"}
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
value={value}
minLength={0}
debounceTimeout={350}
placeholder={description}
onChange={this.onChange}
disabled={disabled} />)
}
}
export class JsonSchemaArrayItemFile extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
onFileChange = (e) => {
const value = e.target.files[0]
this.props.onChange(value, this.props.keyName)
}
render() {
let { getComponent, errors, disabled } = this.props
const Input = getComponent("Input")
const isDisabled = disabled || !("FormData" in window)
return (<Input type="file"
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
onChange={this.onFileChange}
disabled={isDisabled} />)
}
}
export class JsonSchema_boolean extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
onEnumChange = (val) => this.props.onChange(val)
render() {
let { getComponent, value, errors, schema, required, disabled } = this.props
errors = errors.toJS ? errors.toJS() : []
let enumValue = schema && schema.get ? schema.get("enum") : null
let allowEmptyValue = !enumValue || !required
let booleanValue = !enumValue && ["true", "false"]
const Select = getComponent("Select")
return (<Select className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
value={ String(value) }
disabled={ disabled }
allowedValues={ enumValue ? [...enumValue] : booleanValue }
allowEmptyValue={ allowEmptyValue }
onChange={ this.onEnumChange }/>)
}
}
const stringifyObjectErrors = (errors) => {
return errors.map(err => {
const meta = err.propKey !== undefined ? err.propKey : err.index
let stringError = typeof err === "string" ? err : typeof err.error === "string" ? err.error : null
if(!meta && stringError) {
return stringError
}
let currentError = err.error
let path = `/${err.propKey}`
while(typeof currentError === "object") {
const part = currentError.propKey !== undefined ? currentError.propKey : currentError.index
if(part === undefined) {
break
}
path += `/${part}`
if (!currentError.error) {
break
}
currentError = currentError.error
}
return `${path}: ${currentError}`
})
}
export class JsonSchema_object extends PureComponent {
constructor() {
super()
}
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
onChange = (value) => {
this.props.onChange(value)
}
handleOnChange = e => {
const inputValue = e.target.value
this.onChange(inputValue)
}
render() {
let {
getComponent,
value,
errors,
disabled
} = this.props
const TextArea = getComponent("TextArea")
errors = errors.toJS ? errors.toJS() : Array.isArray(errors) ? errors : []
return (
<div>
<TextArea
className={cx({ invalid: errors.length })}
title={ errors.length ? stringifyObjectErrors(errors).join(", ") : ""}
value={stringify(value)}
disabled={disabled}
onChange={ this.handleOnChange }/>
</div>
)
}
}
function valueOrEmptyList(value) {
return List.isList(value) ? value : Array.isArray(value) ? fromJS(value) : List()
}

View File

@@ -0,0 +1,99 @@
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 {
static propTypes = {
collapsedContent: PropTypes.any,
expanded: PropTypes.bool,
children: PropTypes.any,
title: PropTypes.element,
modelName: PropTypes.string,
classes: PropTypes.string,
onToggle: PropTypes.func,
hideSelfOnExpand: PropTypes.bool,
layoutActions: PropTypes.object,
layoutSelectors: PropTypes.object.isRequired,
specPath: ImPropTypes.list.isRequired,
}
static defaultProps = {
collapsedContent: "{...}",
expanded: false,
title: null,
onToggle: () => {},
hideSelfOnExpand: false,
specPath: Im.List([]),
}
constructor(props, context) {
super(props, context)
let { expanded, collapsedContent } = this.props
this.state = {
expanded : expanded,
collapsedContent: collapsedContent || ModelCollapse.defaultProps.collapsedContent
}
}
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)
}
}
UNSAFE_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)
}
this.setState({
expanded: !this.state.expanded
})
}
onLoad = (ref) => {
if (ref && this.props.layoutSelectors) {
const scrollToKey = this.props.layoutSelectors.getScrollToKey()
if( Im.is(scrollToKey, this.props.specPath) ) this.toggleCollapsed()
this.props.layoutActions.readyToScroll(this.props.specPath, ref.parentElement)
}
}
render () {
const { title, classes } = this.props
if(this.state.expanded ) {
if(this.props.hideSelfOnExpand) {
return <span className={classes || ""}>
{this.props.children}
</span>
}
}
return (
<span className={classes || ""} ref={this.onLoad}>
<button aria-expanded={this.state.expanded} className="model-box-control" onClick={this.toggleCollapsed}>
{ title && <span className="pointer">{title}</span> }
<span className={ "model-toggle" + ( this.state.expanded ? "" : " collapsed" ) }></span>
{ !this.state.expanded && <span>{this.state.collapsedContent}</span> }
</button>
{ this.state.expanded && this.props.children }
</span>
)
}
}

View File

@@ -0,0 +1,159 @@
/**
* @prettier
*/
import React, { useMemo, useState, useEffect, useCallback, useRef } from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
import cx from "classnames"
import randomBytes from "randombytes"
const usePrevious = (value) => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
const useTabs = ({ initialTab, isExecute, schema, example }) => {
const tabs = useMemo(() => ({ example: "example", model: "model" }), [])
const allowedTabs = useMemo(() => Object.keys(tabs), [tabs])
const tab =
!allowedTabs.includes(initialTab) || !schema || isExecute
? tabs.example
: initialTab
const prevIsExecute = usePrevious(isExecute)
const [activeTab, setActiveTab] = useState(tab)
const handleTabChange = useCallback((e) => {
setActiveTab(e.target.dataset.name)
}, [])
useEffect(() => {
if (prevIsExecute && !isExecute && example) {
setActiveTab(tabs.example)
}
}, [prevIsExecute, isExecute, example])
return { activeTab, onTabChange: handleTabChange, tabs }
}
const ModelExample = ({
schema,
example,
isExecute = false,
specPath,
includeWriteOnly = false,
includeReadOnly = false,
getComponent,
getConfigs,
specSelectors,
}) => {
const { defaultModelRendering, defaultModelExpandDepth } = getConfigs()
const ModelWrapper = getComponent("ModelWrapper")
const HighlightCode = getComponent("HighlightCode", true)
const exampleTabId = randomBytes(5).toString("base64")
const examplePanelId = randomBytes(5).toString("base64")
const modelTabId = randomBytes(5).toString("base64")
const modelPanelId = randomBytes(5).toString("base64")
const isOAS3 = specSelectors.isOAS3()
const { activeTab, tabs, onTabChange } = useTabs({
initialTab: defaultModelRendering,
isExecute,
schema,
example,
})
return (
<div className="model-example">
<ul className="tab" role="tablist">
<li
className={cx("tabitem", { active: activeTab === tabs.example })}
role="presentation"
>
<button
aria-controls={examplePanelId}
aria-selected={activeTab === tabs.example}
className="tablinks"
data-name="example"
id={exampleTabId}
onClick={onTabChange}
role="tab"
>
{isExecute ? "Edit Value" : "Example Value"}
</button>
</li>
{schema && (
<li
className={cx("tabitem", { active: activeTab === tabs.model })}
role="presentation"
>
<button
aria-controls={modelPanelId}
aria-selected={activeTab === tabs.model}
className={cx("tablinks", { inactive: isExecute })}
data-name="model"
id={modelTabId}
onClick={onTabChange}
role="tab"
>
{isOAS3 ? "Schema" : "Model"}
</button>
</li>
)}
</ul>
{activeTab === tabs.example && (
<div
aria-hidden={activeTab !== tabs.example}
aria-labelledby={exampleTabId}
data-name="examplePanel"
id={examplePanelId}
role="tabpanel"
tabIndex="0"
>
{example ? (
example
) : (
<HighlightCode>(no example available</HighlightCode>
)}
</div>
)}
{activeTab === tabs.model && (
<div
aria-hidden={activeTab === tabs.example}
aria-labelledby={modelTabId}
data-name="modelPanel"
id={modelPanelId}
role="tabpanel"
tabIndex="0"
>
<ModelWrapper
schema={schema}
getComponent={getComponent}
getConfigs={getConfigs}
specSelectors={specSelectors}
expandDepth={defaultModelExpandDepth}
specPath={specPath}
includeReadOnly={includeReadOnly}
includeWriteOnly={includeWriteOnly}
/>
</div>
)}
</div>
)
}
ModelExample.propTypes = {
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.shape({ isOAS3: PropTypes.func.isRequired })
.isRequired,
schema: PropTypes.object.isRequired,
example: PropTypes.any.isRequired,
isExecute: PropTypes.bool,
getConfigs: PropTypes.func.isRequired,
specPath: ImPropTypes.list.isRequired,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
}
export default ModelExample

View File

@@ -0,0 +1,44 @@
import React, { Component, } from "react"
import PropTypes from "prop-types"
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,
expandDepth: PropTypes.number,
layoutActions: PropTypes.object,
layoutSelectors: PropTypes.object.isRequired,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
}
onToggle = (name,isShown) => {
// If this prop is present, we'll have deepLinking for it
if(this.props.layoutActions) {
this.props.layoutActions.show(this.props.fullPath, isShown)
}
}
render(){
let { getComponent, getConfigs } = this.props
const Model = getComponent("Model")
let expanded
if(this.props.layoutSelectors) {
// If this is prop is present, we'll have deepLinking for it
expanded = this.props.layoutSelectors.isShown(this.props.fullPath)
}
return <div className="model-box">
<Model { ...this.props } getConfigs={ getConfigs } expanded={expanded} depth={ 1 } onToggle={ this.onToggle } expandDepth={ this.props.expandDepth || 0 }/>
</div>
}
}

View File

@@ -0,0 +1,136 @@
import React from "react"
import ImmutablePureComponent from "react-immutable-pure-component"
import ImPropTypes from "react-immutable-proptypes"
import PropTypes from "prop-types"
import { Map } from "immutable"
import RollingLoadSVG from "core/assets/rolling-load.svg"
const decodeRefName = uri => {
const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~")
try {
return decodeURIComponent(unescaped)
} catch {
return unescaped
}
}
export default class Model extends ImmutablePureComponent {
static propTypes = {
schema: ImPropTypes.map.isRequired,
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
name: PropTypes.string,
displayName: PropTypes.string,
isRef: PropTypes.bool,
required: PropTypes.bool,
expandDepth: PropTypes.number,
depth: PropTypes.number,
specPath: ImPropTypes.list.isRequired,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
}
getModelName =( ref )=> {
if ( ref.indexOf("#/definitions/") !== -1 ) {
return decodeRefName(ref.replace(/^.*#\/definitions\//, ""))
}
if ( ref.indexOf("#/components/schemas/") !== -1 ) {
return decodeRefName(ref.replace(/^.*#\/components\/schemas\//, ""))
}
}
getRefSchema =( model )=> {
let { specSelectors } = this.props
return specSelectors.findDefinition(model)
}
render () {
let { getComponent, getConfigs, specSelectors, schema, required, name, isRef, specPath, displayName,
includeReadOnly, includeWriteOnly} = this.props
const ObjectModel = getComponent("ObjectModel")
const ArrayModel = getComponent("ArrayModel")
const PrimitiveModel = getComponent("PrimitiveModel")
let type = "object"
let $$ref = schema && schema.get("$$ref")
let $ref = schema && schema.get("$ref")
// If we weren't passed a `name` but have a resolved ref, grab the name from the ref
if (!name && $$ref) {
name = this.getModelName($$ref)
}
/*
* If we have an unresolved ref, get the schema and name from the ref.
* If the ref is external, we can't resolve it, so we just display the ref location.
* This is for situations where:
* - the ref was not resolved by Swagger Client because we reached the traversal depth limit
* - we had a circular ref inside the allOf keyword
*/
if ($ref) {
const refName = this.getModelName($ref)
const refSchema = this.getRefSchema(refName)
if (Map.isMap(refSchema)) {
schema = refSchema.mergeDeep(schema)
if (!$$ref) {
schema = schema.set("$$ref", $ref)
$$ref = $ref
}
} else if (Map.isMap(schema) && schema.size === 1) {
schema = null
name = $ref
}
}
if(!schema) {
return <span className="model model-title">
<span className="model-title__text">{ displayName || name }</span>
{!$ref && <RollingLoadSVG height="20px" width="20px" />}
</span>
}
const deprecated = specSelectors.isOAS3() && schema.get("deprecated")
isRef = isRef !== undefined ? isRef : !!$$ref
type = schema && schema.get("type") || type
switch(type) {
case "object":
return <ObjectModel
className="object" { ...this.props }
specPath={specPath}
getConfigs={ getConfigs }
schema={ schema }
name={ name }
deprecated={deprecated}
isRef={ isRef }
includeReadOnly = {includeReadOnly}
includeWriteOnly = {includeWriteOnly}/>
case "array":
return <ArrayModel
className="array" { ...this.props }
getConfigs={ getConfigs }
schema={ schema }
name={ name }
deprecated={deprecated}
required={ required }
includeReadOnly = {includeReadOnly}
includeWriteOnly = {includeWriteOnly}/>
case "string":
case "number":
case "integer":
case "boolean":
default:
return <PrimitiveModel
{ ...this.props }
getComponent={ getComponent }
getConfigs={ getConfigs }
schema={ schema }
name={ name }
deprecated={deprecated}
required={ required }/>
}
}
}

View File

@@ -0,0 +1,137 @@
import React, { Component } from "react"
import Im, { Map } from "immutable"
import PropTypes from "prop-types"
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([...this.getSchemaBasePath(), name], isExpanded)
if(isExpanded) {
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name])
}
}
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([...this.getSchemaBasePath(), name], ref)
}
}
render(){
let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
let definitions = specSelectors.definitions()
let { docExpansion, defaultModelsExpandDepth } = getConfigs()
if (!definitions.size || defaultModelsExpandDepth < 0) return null
const specPathBase = this.getSchemaBasePath()
let showModels = layoutSelectors.isShown(specPathBase, defaultModelsExpandDepth > 0 && docExpansion !== "none")
const isOAS3 = specSelectors.isOAS3()
const ModelWrapper = getComponent("ModelWrapper")
const Collapse = getComponent("Collapse")
const ModelCollapse = getComponent("ModelCollapse")
const JumpToPath = getComponent("JumpToPath", true)
const ArrowUpIcon = getComponent("ArrowUpIcon")
const ArrowDownIcon = getComponent("ArrowDownIcon")
return <section className={ showModels ? "models is-open" : "models"} ref={this.onLoadModels}>
<h4>
<button
aria-expanded={showModels}
className="models-control"
onClick={() => layoutActions.show(specPathBase, !showModels)}
>
<span>{isOAS3 ? "Schemas" : "Models"}</span>
{showModels ? <ArrowUpIcon /> : <ArrowDownIcon />}
</button>
</h4>
<Collapse isOpened={showModels}>
{
definitions.entrySeq().map(([name])=>{
const fullPath = [...specPathBase, name]
const specPath = Im.List(fullPath)
const schemaValue = specSelectors.specResolvedSubtree(fullPath)
const rawSchemaValue = specSelectors.specJson().getIn(fullPath)
const schema = Map.isMap(schemaValue) ? schemaValue : Im.Map()
const rawSchema = Map.isMap(rawSchemaValue) ? rawSchemaValue : Im.Map()
const displayName = schema.get("title") || rawSchema.get("title") || name
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(fullPath)
}
const content = <ModelWrapper name={ name }
expandDepth={ defaultModelsExpandDepth }
schema={ schema || Im.Map() }
displayName={displayName}
fullPath={fullPath}
specPath={specPath}
getComponent={ getComponent }
specSelectors={ specSelectors }
getConfigs = {getConfigs}
layoutSelectors = {layoutSelectors}
layoutActions = {layoutActions}
includeReadOnly = {true}
includeWriteOnly = {true}/>
const title = <span className="model-box">
<span className="model model-title">
{displayName}
</span>
</span>
return <div id={ `model-${name}` } className="model-container" key={ `models-section-${name}` }
data-name={name} ref={this.onLoadModel} >
<span className="models-jump-to-path"><JumpToPath specPath={specPath} /></span>
<ModelCollapse
classes="model-box"
collapsedContent={this.getCollapsedContent(name)}
onToggle={this.handleToggle}
title={title}
displayName={displayName}
modelName={name}
specPath={specPath}
layoutSelectors={layoutSelectors}
layoutActions={layoutActions}
hideSelfOnExpand={true}
expanded={ defaultModelsExpandDepth > 0 && isShown }
>{content}</ModelCollapse>
</div>
}).toArray()
}
</Collapse>
</section>
}
}

View File

@@ -0,0 +1,273 @@
import React, { Component, } from "react"
import PropTypes from "prop-types"
import { List } from "immutable"
import ImPropTypes from "react-immutable-proptypes"
import { sanitizeUrl } from "core/utils"
const braceOpen = "{"
const braceClose = "}"
const propClass = "property"
export default class ObjectModel extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
expanded: PropTypes.bool,
onToggle: PropTypes.func,
specSelectors: PropTypes.object.isRequired,
name: PropTypes.string,
displayName: PropTypes.string,
isRef: PropTypes.bool,
expandDepth: PropTypes.number,
depth: PropTypes.number,
specPath: ImPropTypes.list.isRequired,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
}
render(){
let { schema, name, displayName, isRef, getComponent, getConfigs, depth, onToggle, expanded, specPath, ...otherProps } = this.props
let { specSelectors,expandDepth, includeReadOnly, includeWriteOnly} = otherProps
const { isOAS3 } = specSelectors
if(!schema) {
return null
}
const { showExtensions } = getConfigs()
let description = schema.get("description")
let properties = schema.get("properties")
let additionalProperties = schema.get("additionalProperties")
let title = schema.get("title") || displayName || name
let requiredProperties = schema.get("required")
let infoProperties = schema
.filter( ( v, key) => ["maxProperties", "minProperties", "nullable", "example"].indexOf(key) !== -1 )
let deprecated = schema.get("deprecated")
let externalDocsUrl = schema.getIn(["externalDocs", "url"])
let externalDocsDescription = schema.getIn(["externalDocs", "description"])
const JumpToPath = getComponent("JumpToPath", true)
const Markdown = getComponent("Markdown", true)
const Model = getComponent("Model")
const ModelCollapse = getComponent("ModelCollapse")
const Property = getComponent("Property")
const Link = getComponent("Link")
const JumpToPathSection = () => {
return <span className="model-jump-to-path"><JumpToPath specPath={specPath} /></span>
}
const collapsedContent = (<span>
<span>{ braceOpen }</span>...<span>{ braceClose }</span>
{
isRef ? <JumpToPathSection /> : ""
}
</span>)
const allOf = specSelectors.isOAS3() ? schema.get("allOf") : null
const anyOf = specSelectors.isOAS3() ? schema.get("anyOf") : null
const oneOf = specSelectors.isOAS3() ? schema.get("oneOf") : null
const not = specSelectors.isOAS3() ? schema.get("not") : null
const titleEl = title && <span className="model-title">
{ isRef && schema.get("$$ref") && <span className="model-hint">{ schema.get("$$ref") }</span> }
<span className="model-title__text">{ title }</span>
</span>
return <span className="model">
<ModelCollapse
modelName={name}
title={titleEl}
onToggle = {onToggle}
expanded={ expanded ? true : depth <= expandDepth }
collapsedContent={ collapsedContent }>
<span className="brace-open object">{ braceOpen }</span>
{
!isRef ? null : <JumpToPathSection />
}
<span className="inner-object">
{
<table className="model"><tbody>
{
!description ? null : <tr className="description">
<td>description:</td>
<td>
<Markdown source={ description } />
</td>
</tr>
}
{
externalDocsUrl &&
<tr className={"external-docs"}>
<td>
externalDocs:
</td>
<td>
<Link target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
</td>
</tr>
}
{
!deprecated ? null :
<tr className={"property"}>
<td>
deprecated:
</td>
<td>
true
</td>
</tr>
}
{
!(properties && properties.size) ? null : properties.entrySeq().filter(
([, value]) => {
return (!value.get("readOnly") || includeReadOnly) &&
(!value.get("writeOnly") || includeWriteOnly)
}
).map(
([key, value]) => {
let isDeprecated = isOAS3() && value.get("deprecated")
let isRequired = List.isList(requiredProperties) && requiredProperties.contains(key)
let classNames = ["property-row"]
if (isDeprecated) {
classNames.push("deprecated")
}
if (isRequired) {
classNames.push("required")
}
return (<tr key={key} className={classNames.join(" ")}>
<td>
{ key }{ isRequired && <span className="star">*</span> }
</td>
<td>
<Model key={ `object-${name}-${key}_${value}` } { ...otherProps }
required={ isRequired }
getComponent={ getComponent }
specPath={specPath.push("properties", key)}
getConfigs={ getConfigs }
schema={ value }
depth={ depth + 1 } />
</td>
</tr>)
}).toArray()
}
{
// empty row before extensions...
!showExtensions ? null : <tr><td>&nbsp;</td></tr>
}
{
!showExtensions ? null :
schema.entrySeq().map(
([key, value]) => {
if(key.slice(0,2) !== "x-") {
return
}
const normalizedValue = !value ? null : value.toJS ? value.toJS() : value
return (<tr key={key} className="extension">
<td>
{ key }
</td>
<td>
{ JSON.stringify(normalizedValue) }
</td>
</tr>)
}).toArray()
}
{
!additionalProperties || !additionalProperties.size ? null
: <tr>
<td>{ "< * >:" }</td>
<td>
<Model { ...otherProps } required={ false }
getComponent={ getComponent }
specPath={specPath.push("additionalProperties")}
getConfigs={ getConfigs }
schema={ additionalProperties }
depth={ depth + 1 } />
</td>
</tr>
}
{
!allOf ? null
: <tr>
<td>{ "allOf ->" }</td>
<td>
{allOf.map((schema, k) => {
return <div key={k}><Model { ...otherProps } required={ false }
getComponent={ getComponent }
specPath={specPath.push("allOf", k)}
getConfigs={ getConfigs }
schema={ schema }
depth={ depth + 1 } /></div>
})}
</td>
</tr>
}
{
!anyOf ? null
: <tr>
<td>{ "anyOf ->" }</td>
<td>
{anyOf.map((schema, k) => {
return <div key={k}><Model { ...otherProps } required={ false }
getComponent={ getComponent }
specPath={specPath.push("anyOf", k)}
getConfigs={ getConfigs }
schema={ schema }
depth={ depth + 1 } /></div>
})}
</td>
</tr>
}
{
!oneOf ? null
: <tr>
<td>{ "oneOf ->" }</td>
<td>
{oneOf.map((schema, k) => {
return <div key={k}><Model { ...otherProps } required={ false }
getComponent={ getComponent }
specPath={specPath.push("oneOf", k)}
getConfigs={ getConfigs }
schema={ schema }
depth={ depth + 1 } /></div>
})}
</td>
</tr>
}
{
!not ? null
: <tr>
<td>{ "not ->" }</td>
<td>
<div>
<Model { ...otherProps }
required={ false }
getComponent={ getComponent }
specPath={specPath.push("not")}
getConfigs={ getConfigs }
schema={ not }
depth={ depth + 1 } />
</div>
</td>
</tr>
}
</tbody></table>
}
</span>
<span className="brace-close">{ braceClose }</span>
</ModelCollapse>
{
infoProperties.size ? infoProperties.entrySeq().map( ( [ key, v ] ) => <Property key={`${key}-${v}`} propKey={ key } propVal={ v } propClass={ propClass } />) : null
}
</span>
}
}

View File

@@ -0,0 +1,88 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import { getExtensions, sanitizeUrl } from "core/utils"
const propClass = "property primitive"
export default class Primitive extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
name: PropTypes.string,
displayName: PropTypes.string,
depth: PropTypes.number,
expandDepth: PropTypes.number
}
render() {
let { schema, getComponent, getConfigs, name, displayName, depth, expandDepth } = this.props
const { showExtensions } = getConfigs()
if (!schema || !schema.get) {
// don't render if schema isn't correctly formed
return <div></div>
}
let type = schema.get("type")
let format = schema.get("format")
let xml = schema.get("xml")
let enumArray = schema.get("enum")
let title = schema.get("title") || displayName || name
let description = schema.get("description")
let extensions = getExtensions(schema)
let properties = schema
.filter((_, key) => ["enum", "type", "format", "description", "$$ref", "externalDocs"].indexOf(key) === -1)
.filterNot((_, key) => extensions.has(key))
let externalDocsUrl = schema.getIn(["externalDocs", "url"])
let externalDocsDescription = schema.getIn(["externalDocs", "description"])
const Markdown = getComponent("Markdown", true)
const EnumModel = getComponent("EnumModel")
const Property = getComponent("Property")
const ModelCollapse = getComponent("ModelCollapse")
const Link = getComponent("Link")
const titleEl = title &&
<span className="model-title">
<span className="model-title__text">{title}</span>
</span>
return <span className="model">
<ModelCollapse title={titleEl} expanded={depth <= expandDepth} collapsedContent="[...]" hideSelfOnExpand={expandDepth !== depth}>
<span className="prop">
{name && depth > 1 && <span className="prop-name">{title}</span>}
<span className="prop-type">{type}</span>
{format && <span className="prop-format">(${format})</span>}
{
properties.size ? properties.entrySeq().map(([key, v]) => <Property key={`${key}-${v}`} propKey={key} propVal={v} propClass={propClass} />) : null
}
{
showExtensions && extensions.size ? extensions.entrySeq().map(([key, v]) => <Property key={`${key}-${v}`} propKey={key} propVal={v} propClass={propClass} />) : null
}
{
!description ? null :
<Markdown source={description} />
}
{
externalDocsUrl &&
<div className="external-docs">
<Link target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
</div>
}
{
xml && xml.size ? (<span><br /><span className={propClass}>xml:</span>
{
xml.entrySeq().map(([key, v]) => <span key={`${key}-${v}`} className={propClass}><br />&nbsp;&nbsp;&nbsp;{key}: {String(v)}</span>).toArray()
}
</span>) : null
}
{
enumArray && <EnumModel value={enumArray} getComponent={getComponent} />
}
</span>
</ModelCollapse>
</span>
}
}

View File

@@ -0,0 +1,53 @@
import React from "react"
import PropTypes from "prop-types"
export default class Schemes extends React.Component {
static propTypes = {
specActions: PropTypes.object.isRequired,
schemes: PropTypes.object.isRequired,
currentScheme: PropTypes.string.isRequired,
path: PropTypes.string,
method: PropTypes.string,
}
UNSAFE_componentWillMount() {
let { schemes } = this.props
//fire 'change' event to set default 'value' of select
this.setScheme(schemes.first())
}
UNSAFE_componentWillReceiveProps(nextProps) {
if ( !this.props.currentScheme || !nextProps.schemes.includes(this.props.currentScheme) ) {
// if we don't have a selected currentScheme or if our selected scheme is no longer an option,
// then fire 'change' event and select the first scheme in the list of options
this.setScheme(nextProps.schemes.first())
}
}
onChange =( e ) => {
this.setScheme( e.target.value )
}
setScheme = ( value ) => {
let { path, method, specActions } = this.props
specActions.setScheme( value, path, method )
}
render() {
let { schemes, currentScheme } = this.props
return (
<label htmlFor="schemes">
<span className="schemes-title">Schemes</span>
<select onChange={ this.onChange } value={currentScheme} id="schemes">
{ schemes.valueSeq().map(
( scheme ) => <option value={ scheme } key={ scheme }>{ scheme }</option>
).toArray()}
</select>
</label>
)
}
}

View File

@@ -0,0 +1,30 @@
import React from "react"
import PropTypes from "prop-types"
export default class SchemesContainer extends React.Component {
static propTypes = {
specActions: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired
}
render () {
const {specActions, specSelectors, getComponent} = this.props
const currentScheme = specSelectors.operationScheme()
const schemes = specSelectors.schemes()
const Schemes = getComponent("schemes")
const schemesArePresent = schemes && schemes.size
return schemesArePresent ? (
<Schemes
currentScheme={currentScheme}
schemes={schemes}
specActions={specActions}
/>
) : null
}
}

View File

@@ -0,0 +1,34 @@
/**
* @prettier
*/
import ModelCollapse from "./components/model-collapse"
import ModelExample from "./components/model-example"
import ModelWrapper from "./components/model-wrapper"
import Model from "./components/model"
import Models from "./components/models"
import EnumModel from "./components/enum-model"
import ObjectModel from "./components/object-model"
import ArrayModel from "./components/array-model"
import PrimitiveModel from "./components/primitive-model"
import Schemes from "./components/schemes"
import SchemesContainer from "./containers/schemes"
import * as JSONSchemaComponents from "./components/json-schema-components"
const JSONSchema5Plugin = () => ({
components: {
modelExample: ModelExample,
ModelWrapper,
ModelCollapse,
Model,
Models,
EnumModel,
ObjectModel,
ArrayModel,
PrimitiveModel,
schemes: Schemes,
SchemesContainer,
...JSONSchemaComponents,
},
})
export default JSONSchema5Plugin

View File

@@ -1,7 +1,6 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import { OAS3ComponentWrapFactory } from "../helpers"
import Model from "core/components/model"
class ModelComponent extends Component {
static propTypes = {
@@ -13,10 +12,11 @@ class ModelComponent extends Component {
expandDepth: PropTypes.number,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
Ori: PropTypes.func.isRequired,
}
render(){
let { getConfigs, schema } = this.props
let { getConfigs, schema, Ori: Model } = this.props
let classes = ["model-box"]
let isDeprecated = schema.get("deprecated") === true
let message = null