From 7870ef8ebfda33967cc7732bcfa0468c3a9052d7 Mon Sep 17 00:00:00 2001 From: Yousef Haggy Date: Tue, 26 Nov 2024 18:31:44 -0500 Subject: [PATCH 1/2] feat: support discriminator dropdown for allof --- src/core/components/parameter-row.jsx | 3 +- src/core/components/parameters/parameters.jsx | 1 + src/core/components/response.jsx | 3 + src/core/components/responses.jsx | 2 + .../components/model-example.jsx | 3 + .../components/model-wrapper.jsx | 132 ++++++++++++++++-- .../json-schema-5/components/models.jsx | 3 +- .../plugins/oas3/components/request-body.jsx | 3 + .../plugins/oas31/components/model/model.jsx | 8 +- src/core/plugins/spec/selectors.js | 30 ++++ src/style/_models.scss | 3 +- 11 files changed, 176 insertions(+), 15 deletions(-) diff --git a/src/core/components/parameter-row.jsx b/src/core/components/parameter-row.jsx index cb3b7780143..7adb3fddba6 100644 --- a/src/core/components/parameter-row.jsx +++ b/src/core/components/parameter-row.jsx @@ -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() @@ -357,6 +357,7 @@ export default class ParameterRow extends Component { getConfigs={ getConfigs } isExecute={ isExecute } specSelectors={ specSelectors } + specActions={ specActions } schema={ schema } example={ bodyParam } includeWriteOnly={ true }/> diff --git a/src/core/components/parameters/parameters.jsx b/src/core/components/parameters/parameters.jsx index ee3e46a85fb..de7621f1992 100644 --- a/src/core/components/parameters/parameters.jsx +++ b/src/core/components/parameters/parameters.jsx @@ -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)} diff --git a/src/core/components/response.jsx b/src/core/components/response.jsx index f703a9194d5..09793dc4e0e 100644 --- a/src/core/components/response.jsx +++ b/src/core/components/response.jsx @@ -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, @@ -84,6 +85,7 @@ export default class Response extends React.Component { getComponent, getConfigs, specSelectors, + specActions, contentType, controlsAcceptHeader, oas3Actions, @@ -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 }/> diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index d6bc8bbe78b..37126b66c8b 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -65,6 +65,7 @@ export default class Responses extends React.Component { getComponent, getConfigs, specSelectors, + specActions, fn, producesValue, displayRequestDuration, @@ -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 } diff --git a/src/core/plugins/json-schema-5/components/model-example.jsx b/src/core/plugins/json-schema-5/components/model-example.jsx index abbdfcc97b1..73cf9a14c7b 100644 --- a/src/core/plugins/json-schema-5/components/model-example.jsx +++ b/src/core/plugins/json-schema-5/components/model-example.jsx @@ -47,6 +47,7 @@ const ModelExample = ({ getComponent, getConfigs, specSelectors, + specActions, }) => { const { defaultModelRendering, defaultModelExpandDepth } = getConfigs() const ModelWrapper = getComponent("ModelWrapper") @@ -132,6 +133,7 @@ const ModelExample = ({ getComponent={getComponent} getConfigs={getConfigs} specSelectors={specSelectors} + specActions={specActions} expandDepth={defaultModelExpandDepth} specPath={specPath} includeReadOnly={includeReadOnly} @@ -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, diff --git a/src/core/plugins/json-schema-5/components/model-wrapper.jsx b/src/core/plugins/json-schema-5/components/model-wrapper.jsx index 5ed380952f0..9a109328f05 100644 --- a/src/core/plugins/json-schema-5/components/model-wrapper.jsx +++ b/src/core/plugins/json-schema-5/components/model-wrapper.jsx @@ -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, @@ -13,6 +12,7 @@ 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, @@ -20,25 +20,139 @@ export default class ModelWrapper extends Component { 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} 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
- -
+ 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 ( +
+ {showDropdown && ( +
+ +
+ )} + +
+ ) } } diff --git a/src/core/plugins/json-schema-5/components/models.jsx b/src/core/plugins/json-schema-5/components/models.jsx index 62c87259206..c0be654fa7c 100644 --- a/src/core/plugins/json-schema-5/components/models.jsx +++ b/src/core/plugins/json-schema-5/components/models.jsx @@ -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 @@ -100,6 +100,7 @@ export default class Models extends Component { specPath={specPath} getComponent={ getComponent } specSelectors={ specSelectors } + specActions={ specActions } getConfigs = {getConfigs} layoutSelectors = {layoutSelectors} layoutActions = {layoutActions} diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 64318fa9ec9..21b99b363e1 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -41,6 +41,7 @@ const RequestBody = ({ getComponent, getConfigs, specSelectors, + specActions, fn, contentType, isExecute, @@ -284,6 +285,7 @@ const RequestBody = ({ getComponent={ getComponent } getConfigs={ getConfigs } specSelectors={ specSelectors } + specActions={ specActions } expandDepth={1} isExecute={isExecute} schema={mediaTypeValue.get("schema")} @@ -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, diff --git a/src/core/plugins/oas31/components/model/model.jsx b/src/core/plugins/oas31/components/model/model.jsx index ceb484b8cd7..cee331e4df0 100644 --- a/src/core/plugins/oas31/components/model/model.jsx +++ b/src/core/plugins/oas31/components/model/model.jsx @@ -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) => { @@ -36,6 +35,7 @@ const Model = forwardRef( @@ -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, } diff --git a/src/core/plugins/spec/selectors.js b/src/core/plugins/spec/selectors.js index 62eddd38120..e06940ebbd3 100644 --- a/src/core/plugins/spec/selectors.js +++ b/src/core/plugins/spec/selectors.js @@ -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) } diff --git a/src/style/_models.scss b/src/style/_models.scss index 6683c567a62..9660be9ff61 100644 --- a/src/style/_models.scss +++ b/src/style/_models.scss @@ -2,7 +2,8 @@ { font-size: 12px; font-weight: 300; - + display: block; + @include text_code(); .deprecated From 0db7da8a283bc07b76db3d26158848635f916a36 Mon Sep 17 00:00:00 2001 From: Yousef Haggy Date: Tue, 26 Nov 2024 19:37:02 -0500 Subject: [PATCH 2/2] fix linter error --- src/core/plugins/spec/selectors.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/plugins/spec/selectors.js b/src/core/plugins/spec/selectors.js index e06940ebbd3..a98e8aad719 100644 --- a/src/core/plugins/spec/selectors.js +++ b/src/core/plugins/spec/selectors.js @@ -55,8 +55,8 @@ export const getParentToChildMap = createSelector( specJS, spec => { const schemaMap = {} - const schemas = spec?.components?.schemas; - if (!!schemas) { + const schemas = spec?.components?.schemas + if (schemas) { Object.entries(schemas).forEach(([schemaName, schema]) => { if (schema.allOf) { schema.allOf.forEach(item => {