Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dropdown for discriminator #10224

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/core/components/parameter-row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default class ParameterRow extends Component {
}

render() {
let {param, rawParam, getComponent, getConfigs, isExecute, fn, onChangeConsumes, specSelectors, pathMethod, specPath, oas3Selectors} = this.props
let {param, rawParam, getComponent, getConfigs, isExecute, fn, onChangeConsumes, specSelectors, specActions, pathMethod, specPath, oas3Selectors} = this.props

let isOAS3 = specSelectors.isOAS3()

Expand Down Expand Up @@ -357,6 +357,7 @@ export default class ParameterRow extends Component {
getConfigs={ getConfigs }
isExecute={ isExecute }
specSelectors={ specSelectors }
specActions={ specActions }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to get specAction down to model-wrapper.jsx than plumbing it down everywhere like this? I couldn't find a context provider with specActions in the hierarchy

schema={ schema }
example={ bodyParam }
includeWriteOnly={ true }/>
Expand Down
1 change: 1 addition & 0 deletions src/core/components/parameters/parameters.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export default class Parameters extends Component {
setRetainRequestBodyValueFlag={retainRequestBodyValueFlagForOperation}
userHasEditedBody={oas3Selectors.hasUserEditedBody(...pathMethod)}
specPath={specPath.slice(0, -1).push("requestBody")}
specActions={specActions}
requestBody={requestBody}
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod)}
requestBodyInclusionSetting={oas3Selectors.requestBodyInclusionSetting(...pathMethod)}
Expand Down
3 changes: 3 additions & 0 deletions src/core/components/response.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default class Response extends React.Component {
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
specPath: ImPropTypes.list.isRequired,
fn: PropTypes.object.isRequired,
Expand Down Expand Up @@ -84,6 +85,7 @@ export default class Response extends React.Component {
getComponent,
getConfigs,
specSelectors,
specActions,
contentType,
controlsAcceptHeader,
oas3Actions,
Expand Down Expand Up @@ -237,6 +239,7 @@ export default class Response extends React.Component {
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
specActions={ specActions }
schema={ fromJSOrdered(schema) }
example={ example }
includeReadOnly={ true }/>
Expand Down
2 changes: 2 additions & 0 deletions src/core/components/responses.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default class Responses extends React.Component {
getComponent,
getConfigs,
specSelectors,
specActions,
fn,
producesValue,
displayRequestDuration,
Expand Down Expand Up @@ -145,6 +146,7 @@ export default class Responses extends React.Component {
code={ code }
response={ response }
specSelectors={ specSelectors }
specActions={ specActions }
controlsAcceptHeader={response === acceptControllingResponse}
onContentTypeChange={this.onResponseContentTypeChange}
contentType={ producesValue }
Expand Down
3 changes: 3 additions & 0 deletions src/core/plugins/json-schema-5/components/model-example.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const ModelExample = ({
getComponent,
getConfigs,
specSelectors,
specActions,
}) => {
const { defaultModelRendering, defaultModelExpandDepth } = getConfigs()
const ModelWrapper = getComponent("ModelWrapper")
Expand Down Expand Up @@ -132,6 +133,7 @@ const ModelExample = ({
getComponent={getComponent}
getConfigs={getConfigs}
specSelectors={specSelectors}
specActions={specActions}
expandDepth={defaultModelExpandDepth}
specPath={specPath}
includeReadOnly={includeReadOnly}
Expand All @@ -147,6 +149,7 @@ ModelExample.propTypes = {
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.shape({ isOAS3: PropTypes.func.isRequired })
.isRequired,
specActions: PropTypes.object.isRequired,
schema: PropTypes.object.isRequired,
example: PropTypes.any.isRequired,
isExecute: PropTypes.bool,
Expand Down
132 changes: 123 additions & 9 deletions src/core/plugins/json-schema-5/components/model-wrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { Component, } from "react"
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,
Expand All @@ -13,32 +12,147 @@ export default class ModelWrapper extends Component {
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
expandDepth: PropTypes.number,
layoutActions: PropTypes.object,
layoutSelectors: PropTypes.object.isRequired,
includeReadOnly: PropTypes.bool,
includeWriteOnly: PropTypes.bool,
}

onToggle = (name,isShown) => {
constructor(props) {
super(props)
this.state = {
selectedSchema: null
}
}

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
onSchemaSelect = (e) => {
const selectedSchema = e.target.value
const schemaPath = ["components", "schemas", selectedSchema]

const isResolved = this.props.specSelectors.specResolvedSubtree(schemaPath) != null
if (!isResolved) {
this.props.specActions.requestResolvedSubtree(schemaPath)
}

this.setState({ selectedSchema })
}

decodeRefName = (uri) => {
const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~")
try {
return decodeURIComponent(unescaped)
} catch {
return unescaped
}
}

getModelName = (uri) => {
if (typeof uri === "string" && uri.includes("#/components/schemas/")) {
return this.decodeRefName(uri.replace(/^.*#\/components\/schemas\//, ""))
}
return null
}

/**
* Builds a Map of schema options combining explicit discriminator mappings and implicit mappings.
*
* @returns {Map<string, string[]>} A Map where:
* - key: the schema name (e.g., "Cat", "Dog")
* - value: array of discriminator values that map to this schema
*
* Examples:
* 1. Explicit mapping only:
* { "Cat": ["kitty", "kitten"], "Dog": ["puppy"] }
*
* 2. Implicit mapping only:
* { "Cat": ["Cat"], "Dog": ["Dog"] }
*
* 3. Mixed mapping:
* { "Cat": ["kitty", "kitten"], "Dog": ["Dog"] }
* where "Cat" has explicit mappings but "Dog" uses implicit
*/
buildSchemaOptions = (name, discriminator, schemaMap) => {
const options = new Map()
const mapping = discriminator && discriminator.get("mapping")

// First add any explicit mappings
if (mapping && mapping.size > 0) {
mapping.forEach((schemaRef, key) => {
const schemaName = this.getModelName(schemaRef)
if (schemaName) {
const existing = options.get(schemaName) || []
options.set(schemaName, [...existing, key])
}
})
}

// Then add implicit mappings for any schemas not already mapped
const childSchemas = schemaMap[name] || []
childSchemas.forEach(childName => {
if (!options.has(childName)) {
// No explicit mapping for this schema, use implicit
options.set(childName, [childName])
}
})

return options
}

render() {
let { getComponent, getConfigs, schema, specSelectors } = 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>
const name = this.getModelName(schema.get("$$ref"))
const schemaMap = specSelectors.getParentToChildMap()
const discriminator = schema.get("discriminator")

const options = this.buildSchemaOptions(name, discriminator, schemaMap)
const showDropdown = !!discriminator && options.size > 0

// Use selected schema or original base schema
const effectiveSchema = this.state.selectedSchema
? specSelectors.findDefinition(this.state.selectedSchema)
: schema

return (
<div className="model-box">
{showDropdown && (
<div className="model-box-control">
<select onChange={this.onSchemaSelect} value={this.state.selectedSchema || ""}>
<option value="">Base: {name}</option>
{Array.from(options.entries()).map(([schemaName, keys]) => (
<option key={schemaName} value={schemaName}>
{keys.length > 1 ? `${keys.join(" | ")} (${schemaName})` : schemaName}
</option>
))}
</select>
</div>
)}
<Model
{ ...this.props }
name={this.state.selectedSchema}
schema={effectiveSchema}
getConfigs={getConfigs}
expanded={expanded}
depth={1}
onToggle={this.onToggle}
expandDepth={this.props.expandDepth || 0}
/>
</div>
)
}
}
3 changes: 2 additions & 1 deletion src/core/plugins/json-schema-5/components/models.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class Models extends Component {
}

render(){
let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
let { specSelectors, specActions, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
let definitions = specSelectors.definitions()
let { docExpansion, defaultModelsExpandDepth } = getConfigs()
if (!definitions.size || defaultModelsExpandDepth < 0) return null
Expand Down Expand Up @@ -100,6 +100,7 @@ export default class Models extends Component {
specPath={specPath}
getComponent={ getComponent }
specSelectors={ specSelectors }
specActions={ specActions }
getConfigs = {getConfigs}
layoutSelectors = {layoutSelectors}
layoutActions = {layoutActions}
Expand Down
3 changes: 3 additions & 0 deletions src/core/plugins/oas3/components/request-body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const RequestBody = ({
getComponent,
getConfigs,
specSelectors,
specActions,
fn,
contentType,
isExecute,
Expand Down Expand Up @@ -284,6 +285,7 @@ const RequestBody = ({
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
specActions={ specActions }
expandDepth={1}
isExecute={isExecute}
schema={mediaTypeValue.get("schema")}
Expand Down Expand Up @@ -319,6 +321,7 @@ RequestBody.propTypes = {
getConfigs: PropTypes.func.isRequired,
fn: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired,
contentType: PropTypes.string,
isExecute: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
Expand Down
8 changes: 5 additions & 3 deletions src/core/plugins/oas31/components/model/model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ const getModelName = (uri) => {
}
return null
}

const Model = forwardRef(
({ schema, getComponent, onToggle = () => {} }, ref) => {
({ schema, name: nameFromProp, getComponent, specSelectors, onToggle = () => {} }, ref) => {
const JSONSchema202012 = getComponent("JSONSchema202012")
const name = getModelName(schema.get("$$ref"))
const name = nameFromProp || getModelName(schema.get("$$ref"))

const handleExpand = useCallback(
(e, expanded) => {
Expand All @@ -36,6 +35,7 @@ const Model = forwardRef(
<JSONSchema202012
name={name}
schema={schema.toJS()}
specSelectors={specSelectors}
ref={ref}
onExpand={handleExpand}
/>
Expand All @@ -45,6 +45,8 @@ const Model = forwardRef(

Model.propTypes = {
schema: ImPropTypes.map.isRequired,
name: PropTypes.string,
specSelectors: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired,
onToggle: PropTypes.func,
}
Expand Down
30 changes: 30 additions & 0 deletions src/core/plugins/spec/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,36 @@ export const specResolved = createSelector(
spec => spec.get("resolved", Map())
)



// Get parent-child schema map
export const getParentToChildMap = createSelector(
specJS,
spec => {
const schemaMap = {}
const schemas = spec?.components?.schemas
if (schemas) {
Object.entries(schemas).forEach(([schemaName, schema]) => {
if (schema.allOf) {
schema.allOf.forEach(item => {
if (item.$ref) {
// Extract parent schema name from $ref
const parentName = item.$ref.split("/").pop()
// Add current schema as child of parent
if (!schemaMap[parentName]) {
schemaMap[parentName] = []
}
schemaMap[parentName].push(schemaName)
}
})
}
})
}
return schemaMap
}
)


export const specResolvedSubtree = (state, path) => {
return state.getIn(["resolvedSubtrees", ...path], undefined)
}
Expand Down
3 changes: 2 additions & 1 deletion src/style/_models.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
{
font-size: 12px;
font-weight: 300;

display: block;
Copy link
Author

@YousefHaggy YousefHaggy Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for OAS 3.0.X, the model is inline with the dropdown if we don't specify this


@include text_code();

.deprecated
Expand Down