diff --git a/packages/elements-core/src/components/Docs/HttpOperation/LazySchemaTreePreviewer.tsx b/packages/elements-core/src/components/Docs/HttpOperation/LazySchemaTreePreviewer.tsx index 454d30916..cdc5f49e4 100644 --- a/packages/elements-core/src/components/Docs/HttpOperation/LazySchemaTreePreviewer.tsx +++ b/packages/elements-core/src/components/Docs/HttpOperation/LazySchemaTreePreviewer.tsx @@ -1,5 +1,5 @@ import { Box, Flex, VStack } from '@stoplight/mosaic'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; interface LazySchemaTreePreviewerProps { schema: any; @@ -83,6 +83,7 @@ const trimSlashes = (str: string) => { }; function isPropertiesAllHidden(path: string, hideData: Array<{ path: string; required?: boolean }>) { + if (!hideData.length) return false; const current = trimSlashes(path); const parts = current.split('/'); for (let i = parts.length; i >= 2; i--) { @@ -100,34 +101,46 @@ function isPropertiesAllHidden(path: string, hideData: Array<{ path: string; req } function isRequiredOverride(path: string, hideData: Array<{ path: string; required?: boolean }>) { + if (!hideData.length) return undefined; const entry = hideData.find(h => trimSlashes(h.path) === trimSlashes(path)); return entry && typeof entry.required === 'boolean' ? entry.required : undefined; } -// New utility for array/items-based hiding function isPathHidden(path: string, hideData: Array<{ path: string; required?: boolean }>) { const normalizedPath = trimSlashes(path); - // Direct match (root-level or property-level) const direct = hideData.find(h => trimSlashes(h.path) === normalizedPath); if (direct && direct.required === undefined) return true; - // Check for ancestor "properties" disables (properties/carr/properties, etc) if (isPropertiesAllHidden(path, hideData)) return true; - // Check for array/items disables: e.g. properties/aircraftGroup/items/aircraft/items/aircraftGroup - // Go up the path looking for a hideData.path which is a prefix of this path and ends with '/items/[field]' for (const h of hideData) { const hPath = trimSlashes(h.path); if (h.required !== undefined) continue; - // Must be prefix + const oneOfPattern = hPath.match(/^(.*\/)?oneOf\/\d+\/(.*)$/); + const anyOfPattern = hPath.match(/^(.*\/)?anyOf\/\d+\/(.*)$/); + const allOfPattern = hPath.match(/^(.*\/)?allOf\/\d+\/(.*)$/); + const pattern = oneOfPattern || anyOfPattern || allOfPattern; + if (pattern) { + const prefix = pattern[1] || ''; + const suffix = pattern[2] || ''; + const schemaType = oneOfPattern ? 'oneOf' : anyOfPattern ? 'anyOf' : 'allOf'; + const flexiblePattern = `${prefix}${schemaType}/\\d+/${suffix}`; + const regex = new RegExp(`^${flexiblePattern.replace(/\//g, '\\/')}$`); + if (regex.test(normalizedPath)) { + return true; + } + const flexiblePrefixPattern = `${prefix}${schemaType}/\\d+/${suffix}`; + const prefixRegex = new RegExp(`^${flexiblePrefixPattern.replace(/\//g, '\\/')}`); + if (prefixRegex.test(normalizedPath) && normalizedPath.startsWith(prefix + schemaType)) { + return true; + } + } if ( normalizedPath.length > hPath.length && normalizedPath.startsWith(hPath) && - // hPath is items/field (array hiding) (hPath.endsWith('/items') || (hPath.match(/\/items\/[^\/]+$/) && normalizedPath.startsWith(hPath + '/'))) ) { - // Hide all descendants under this path return true; } } @@ -163,24 +176,36 @@ const LazySchemaTreePreviewer: React.FC = ({ return initialState; }); + const nestedSchemaKey = schema?.allOf + ? JSON.stringify( + schema.allOf.map((item: any) => { + const resolved = dereference(item, root); + return { + oneOfLength: resolved.oneOf?.length, + anyOfLength: resolved.anyOf?.length, + }; + }), + ) + : null; + useEffect(() => { setSelectedSchemaIndex(0); - }, [schema?.anyOf, schema?.oneOf]); - const thisNodeRequiredOverride = isRequiredOverride(path, hideData); + }, [ + schema?.anyOf?.length, + schema?.oneOf?.length, + schema?.allOf?.length, + schema?.items?.anyOf?.length, + schema?.items?.oneOf?.length, + schema?.items?.allOf?.length, + nestedSchemaKey, + ]); + const thisNodeRequiredOverride = isRequiredOverride(path, hideData); const shouldHideAllChildren = (isRoot && hideData.some(h => trimSlashes(h.path) === 'properties' && h.required === undefined)) || (!isRoot && isPropertiesAllHidden(path, hideData)); - const shouldHideNode = useMemo(() => { - if (isRoot) return false; - if (isPathHidden(path, hideData) && thisNodeRequiredOverride === undefined) return true; - return false; - }, [path, hideData, isRoot, thisNodeRequiredOverride]); - - if (!schema || shouldHideNode) { - return null; - } + const shouldHideNode = !isRoot && isPathHidden(path, hideData) && thisNodeRequiredOverride === undefined; const displayTitle = level === 1 && (title === undefined || path === '') ? '' : title ?? schema?.title ?? 'Node'; @@ -198,27 +223,103 @@ const LazySchemaTreePreviewer: React.FC = ({ const children: JSX.Element[] = []; if (schema?.type === 'object' && (schema?.properties || schema?.allOf || schema?.anyOf || schema?.oneOf)) { - let props = schema?.properties; + let props = schema?.properties || {}; + let requiredFields = schema?.required || []; if (schema?.allOf) { - schema?.allOf.forEach((item: any) => { - props = { ...props, ...item.properties }; + schema?.allOf.forEach((item: any, allOfIndex: number) => { + const resolvedItem = dereference(item, root); + if (resolvedItem.properties) { + props = { ...props, ...resolvedItem.properties }; + } + if (resolvedItem.required) { + requiredFields = [...requiredFields, ...resolvedItem.required]; + } + // Handle nested oneOf/anyOf within allOf items + if (resolvedItem.oneOf && resolvedItem.oneOf.length > 0) { + const selectedNestedSchema = resolvedItem.oneOf[selectedSchemaIndex] || resolvedItem.oneOf[0]; + const resolvedNested = dereference(selectedNestedSchema, root); + if (resolvedNested.properties) { + props = { ...props, ...resolvedNested.properties }; + } + if (resolvedNested.required) { + requiredFields = [...requiredFields, ...resolvedNested.required]; + } + } + if (resolvedItem.anyOf && resolvedItem.anyOf.length > 0) { + const selectedNestedSchema = resolvedItem.anyOf[selectedSchemaIndex] || resolvedItem.anyOf[0]; + const resolvedNested = dereference(selectedNestedSchema, root); + if (resolvedNested.properties) { + props = { ...props, ...resolvedNested.properties }; + } + if (resolvedNested.required) { + requiredFields = [...requiredFields, ...resolvedNested.required]; + } + } }); } + if (schema?.anyOf && schema?.anyOf.length > 0) { const selectedSchema = schema?.anyOf[selectedSchemaIndex] || schema?.anyOf[0]; - props = { ...props, ...selectedSchema.properties }; + if (selectedSchema.properties) { + props = { ...props, ...selectedSchema.properties }; + } + if (selectedSchema.required) { + requiredFields = [...requiredFields, ...selectedSchema.required]; + } } + if (schema?.oneOf && schema?.oneOf.length > 0) { const selectedSchema = schema?.oneOf[selectedSchemaIndex] || schema?.oneOf[0]; - props = { ...props, ...selectedSchema.properties }; + if (selectedSchema.properties) { + props = { ...props, ...selectedSchema.properties }; + } + if (selectedSchema.required) { + requiredFields = [...requiredFields, ...selectedSchema.required]; + } } for (const [key, child] of Object.entries(props || {})) { - const childPath = `${path}/properties/${key}`; + let childPath = `${path}/properties/${key}`; + let foundInNestedSchema = false; + // Check if this property comes from a nested oneOf/anyOf within allOf + if (schema?.allOf) { + for (let allOfIndex = 0; allOfIndex < schema.allOf.length; allOfIndex++) { + const allOfItem = dereference(schema.allOf[allOfIndex], root); + // Check if property is in a oneOf within this allOf item + if (allOfItem.oneOf && allOfItem.oneOf.length > 0) { + const selectedNestedSchema = allOfItem.oneOf[selectedSchemaIndex] || allOfItem.oneOf[0]; + const resolvedNested = dereference(selectedNestedSchema, root); + if (resolvedNested.properties && resolvedNested.properties[key]) { + childPath = `${path}/allOf/${allOfIndex}/oneOf/${selectedSchemaIndex}/properties/${key}`; + foundInNestedSchema = true; + break; + } + } + // Check if property is in an anyOf within this allOf item + if (allOfItem.anyOf && allOfItem.anyOf.length > 0) { + const selectedNestedSchema = allOfItem.anyOf[selectedSchemaIndex] || allOfItem.anyOf[0]; + const resolvedNested = dereference(selectedNestedSchema, root); + if (resolvedNested.properties && resolvedNested.properties[key]) { + childPath = `${path}/allOf/${allOfIndex}/anyOf/${selectedSchemaIndex}/properties/${key}`; + foundInNestedSchema = true; + break; + } + } + } + } + // Handle top-level oneOf/anyOf if not found in nested schemas + if (!foundInNestedSchema) { + if (schema?.oneOf && schema?.oneOf.length > 0) { + childPath = `${path}/oneOf/${selectedSchemaIndex}/properties/${key}`; + } else if (schema?.anyOf && schema?.anyOf.length > 0) { + childPath = `${path}/anyOf/${selectedSchemaIndex}/properties/${key}`; + } else if (schema?.allOf && schema?.allOf.length > 0) { + childPath = `${path}/allOf/${selectedSchemaIndex}/properties/${key}`; + } + } const childRequiredOverride = isRequiredOverride(childPath, hideData); const shouldHideChild = isPathHidden(childPath, hideData) && childRequiredOverride === undefined; - const resolved = dereference(child, root); if (!shouldHideChild) { children.push( @@ -230,7 +331,7 @@ const LazySchemaTreePreviewer: React.FC = ({ level={level + 1} path={childPath} hideData={hideData} - parentRequired={schema?.required} + parentRequired={requiredFields} propertyKey={key} _subType={resolved?.items?.type} /> @@ -247,35 +348,83 @@ const LazySchemaTreePreviewer: React.FC = ({ const resolvedItems = dereference(schema?.items, root); const itemsPath = `${path}/items`; - if (resolvedItems && resolvedItems.type === 'object' && resolvedItems.properties) { - for (const [key, child] of Object.entries(resolvedItems.properties)) { - const childPath = `${itemsPath}/properties/${key}`; + if ( + resolvedItems && + (resolvedItems.type === 'object' || resolvedItems.anyOf || resolvedItems.oneOf || resolvedItems.allOf) + ) { + if (resolvedItems.anyOf || resolvedItems.oneOf || resolvedItems.allOf) { + const childPath = `${path}/items`; const childRequiredOverride = isRequiredOverride(childPath, hideData); const shouldHideChild = isPathHidden(childPath, hideData) && childRequiredOverride === undefined; + let schemaToPass = resolvedItems; + if ( + schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf) + ) { + if (schema.items.anyOf && selectedSchemaIndex < schema.items.anyOf.length) { + schemaToPass = schema.items.anyOf[selectedSchemaIndex]; + } else if (schema.items.oneOf && selectedSchemaIndex < schema.items.oneOf.length) { + schemaToPass = schema.items.oneOf[selectedSchemaIndex]; + } else if (schema.items.allOf && selectedSchemaIndex < schema.items.allOf.length) { + schemaToPass = schema.items.allOf[selectedSchemaIndex]; + } + } + if (!shouldHideChild) { children.push( -
  • +
  • , ); } + } else if (resolvedItems.properties) { + for (const [key, child] of Object.entries(resolvedItems.properties)) { + let childPath = `${itemsPath}/properties/${key}`; + if (schema?.items?.oneOf && schema?.items?.oneOf.length > 0) { + childPath = `${path}/items/oneOf/${selectedSchemaIndex}/properties/${key}`; + } else if (schema?.items?.anyOf && schema?.items?.anyOf.length > 0) { + childPath = `${path}/items/anyOf/${selectedSchemaIndex}/properties/${key}`; + } else if (schema?.items?.allOf && schema?.items?.allOf.length > 0) { + childPath = `${path}/items/allOf/${selectedSchemaIndex}/properties/${key}`; + } + const childRequiredOverride = isRequiredOverride(childPath, hideData); + const shouldHideChild = isPathHidden(childPath, hideData) && childRequiredOverride === undefined; + + if (!shouldHideChild) { + children.push( +
  • + +
  • , + ); + } + } } } else if (resolvedItems && resolvedItems.type === 'array' && resolvedItems.items.length > 0) { const childPath = `${path}/items`; const childRequiredOverride = isRequiredOverride(childPath, hideData); const shouldHideChild = isPathHidden(childPath, hideData) && childRequiredOverride === undefined; - if (!shouldHideChild) { children.push(
  • @@ -299,6 +448,32 @@ const LazySchemaTreePreviewer: React.FC = ({ }; const combinedSchemaSelector = () => { + let schemaOptions = []; + // Check for top-level combinations first + if (schema?.anyOf) { + schemaOptions = schema.anyOf; + } else if (schema?.oneOf) { + schemaOptions = schema.oneOf; + } else if (schema?.allOf) { + // For allOf, check if any items have nested oneOf/anyOf + for (const allOfItem of schema.allOf) { + const resolvedItem = dereference(allOfItem, root); + if (resolvedItem.oneOf && resolvedItem.oneOf.length > 0) { + schemaOptions = resolvedItem.oneOf; + break; + } else if (resolvedItem.anyOf && resolvedItem.anyOf.length > 0) { + schemaOptions = resolvedItem.anyOf; + break; + } + } + } else if (schema?.type === 'array' && schema?.items) { + if (schema.items.anyOf) schemaOptions = schema.items.anyOf; + else if (schema.items.oneOf) schemaOptions = schema.items.oneOf; + else if (schema.items.allOf) schemaOptions = schema.items.allOf; + } + + if (!schemaOptions || schemaOptions.length === 0) return null; + return ( <> = ({ fontSize="sm" onClick={(e: React.MouseEvent) => e.stopPropagation()} > - {(schema?.anyOf || schema?.oneOf)?.map((schemaOption: any, index: number) => ( + {schemaOptions.map((schemaOption: any, index: number) => ( = ({ display="flex" alignItems="center" style={{ - borderBottom: - index < (schema?.anyOf || schema?.oneOf).length - 1 ? '1px solid rgba(0, 0, 0, 0.1)' : 'none', + borderBottom: index < schemaOptions.length - 1 ? '1px solid rgba(0, 0, 0, 0.1)' : 'none', gap: '8px', }} onMouseEnter={(e: React.MouseEvent) => { @@ -424,6 +598,11 @@ const LazySchemaTreePreviewer: React.FC = ({ schema = dereference(schema, root); } + // Early return for hidden/invalid schemas + if (!schema || shouldHideNode) { + return null; + } + return (
    @@ -446,13 +625,35 @@ const LazySchemaTreePreviewer: React.FC = ({ {' ' + displayTitle} ) : null} - {!isRoot ? ( + {!isRoot || + (isRoot && + (schema?.anyOf || + schema?.oneOf || + (schema?.allOf && + schema.allOf.some((item: any) => { + const resolved = dereference(item, root); + return resolved.oneOf || resolved.anyOf; + })) || + (schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf)))) ? ( { - if (schema?.anyOf || schema?.oneOf) { + if ( + schema?.anyOf || + schema?.oneOf || + (schema?.allOf && + schema.allOf.some((item: any) => { + const resolved = dereference(item, root); + return resolved.oneOf || resolved.anyOf; + })) || + (schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf)) + ) { setIsHoveringSelector(true); } }} @@ -462,31 +663,83 @@ const LazySchemaTreePreviewer: React.FC = ({ } }} onClick={(e: React.MouseEvent) => { - if (schema?.anyOf || schema?.oneOf) { + if ( + schema?.anyOf || + schema?.oneOf || + (schema?.allOf && + schema.allOf.some((item: any) => { + const resolved = dereference(item, root); + return resolved.oneOf || resolved.anyOf; + })) || + (schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf)) + ) { e.stopPropagation(); setShowSchemaDropdown(prev => !prev); } }} style={{ - cursor: schema?.anyOf || schema?.oneOf ? 'pointer' : 'default', + cursor: + schema?.anyOf || + schema?.oneOf || + (schema?.allOf && + schema.allOf.some((item: any) => { + const resolved = dereference(item, root); + return resolved.oneOf || resolved.anyOf; + })) || + (schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf)) + ? 'pointer' + : 'default', }} > + {isRoot && + (schema?.anyOf || + schema?.oneOf || + (schema?.allOf && + schema.allOf.some((item: any) => { + const resolved = dereference(item, root); + return resolved.oneOf || resolved.anyOf; + })) || + (schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf))) && ( + + {title || schema?.title || 'Schema'} + + )} {(() => { let typeDisplay = schema?.type === 'object' && schema?.title ? schema?.title : schema?.type || root?.title; if (schema?.anyOf && schema?.anyOf.length > 0) { - return `any of ${typeDisplay}`; + return `any of`; } else if (schema?.oneOf && schema?.oneOf.length > 0) { - return `one of ${typeDisplay}`; + return `one of`; } return typeDisplay; })()} {schema?.items && schema?.items?.title !== undefined ? ` [${schema?.items?.title}] ` : null} + {schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf) + ? ` [${schema?.items?.anyOf ? 'any of' : schema?.items?.oneOf ? 'one of' : 'all of'} object]` + : null} - {(schema?.anyOf || schema?.oneOf) && ( + {(schema?.anyOf || + schema?.oneOf || + (schema?.allOf && + schema.allOf.some((item: any) => { + const resolved = dereference(item, root); + return resolved.oneOf || resolved.anyOf; + })) || + (schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf))) && ( = ({ )} {schema?.format !== undefined ? `<${schema?.format}>` : null} - {(schema?.anyOf || schema?.oneOf) && showSchemaDropdown && combinedSchemaSelector()} + {(schema?.anyOf || + schema?.oneOf || + (schema?.allOf && + schema.allOf.some((item: any) => { + const resolved = dereference(item, root); + return resolved.oneOf || resolved.anyOf; + })) || + (schema?.type === 'array' && + schema?.items && + (schema?.items?.anyOf || schema?.items?.oneOf || schema?.items?.allOf))) && + showSchemaDropdown && + combinedSchemaSelector()} ) : null} diff --git a/packages/elements-dev-portal/src/version.ts b/packages/elements-dev-portal/src/version.ts index ecd86f5e7..7d3dd749c 100644 --- a/packages/elements-dev-portal/src/version.ts +++ b/packages/elements-dev-portal/src/version.ts @@ -1,2 +1,2 @@ // auto-updated during build -export const appVersion = '3.0.4'; +export const appVersion = '3.0.6';