feat: implement JSON schema depth validation and update related components

This commit is contained in:
twwu 2025-03-18 15:09:08 +08:00
parent ffe08a35a4
commit 7a647cf18e
7 changed files with 65 additions and 32 deletions

View File

@ -6,6 +6,8 @@ import { RiClipboardLine, RiCloseLine, RiErrorWarningFill, RiIndentIncrease } fr
import copy from 'copy-to-clipboard'
import { Editor } from '@monaco-editor/react'
import Button from '@/app/components/base/button'
import { checkDepth } from '../../utils'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
type JsonImporterProps = {
onSubmit: (schema: string) => void
@ -71,8 +73,17 @@ const JsonImporter: FC<JsonImporterProps> = ({
const handleSubmit = useCallback(() => {
try {
const parsedJSON = JSON.parse(json)
const maxDepth = checkDepth(parsedJSON)
if (maxDepth > JSON_SCHEMA_MAX_DEPTH) {
setParseError({
type: 'error',
message: `Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`,
})
return
}
onSubmit(parsedJSON)
setParseError(null)
setOpen(false)
}
catch (e: any) {
if (e instanceof SyntaxError)

View File

@ -281,6 +281,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
})
useSubscribe('fieldChange', (params) => {
let samePropertyNameError = false
const { parentPath, oldFields, fields } = params as ChangeEventParams
const newSchema = produce(jsonSchema, (draft) => {
const parentSchema = findPropertyWithPath(draft, parentPath) as Field
@ -295,7 +296,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
type: 'error',
message: 'Property name already exists',
})
return
samePropertyNameError = true
}
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
@ -384,8 +385,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
type: 'error',
message: 'Property name already exists',
})
emit('restorePropertyName')
return
samePropertyNameError = true
}
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
@ -463,6 +463,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
schema.enum = fields.enum
}
})
if (samePropertyNameError) return
setJsonSchema(newSchema)
emit('fieldChangeSuccess')
})

View File

@ -14,6 +14,7 @@ import { useJsonSchemaConfigStore } from '../../store'
import { useMittContext } from '../../context'
import produce from 'immer'
import { useUnmount } from 'ahooks'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
export type EditData = {
name: string
@ -46,8 +47,6 @@ const TYPE_OPTIONS = [
{ value: ArrayType.object, text: 'array[object]' },
]
const DEPTH_LIMIT = 10
const EditCard: FC<EditCardProps> = ({
fields,
depth,
@ -64,7 +63,7 @@ const EditCard: FC<EditCardProps> = ({
const { emit, useSubscribe } = useMittContext()
const blurWithActions = useRef(false)
const disableAddBtn = fields.type !== Type.object && fields.type !== ArrayType.object && depth < DEPTH_LIMIT
const disableAddBtn = depth >= JSON_SCHEMA_MAX_DEPTH || (fields.type !== Type.object && fields.type !== ArrayType.object)
const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number
const isAdvancedEditing = advancedEditing || isAddingNewField

View File

@ -16,7 +16,7 @@ const VisualEditor: FC<VisualEditorProps> = ({
schema={schema}
required={false}
path={[]}
depth={0}
depth={1}
/>
</div>
)

View File

@ -10,6 +10,7 @@ import Card from './card'
import { useJsonSchemaConfigStore } from '../store'
import { useDebounceFn } from 'ahooks'
import AddField from './add-field'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
type SchemaNodeProps = {
name: string
@ -22,28 +23,28 @@ type SchemaNodeProps = {
// Support 10 levels of indentation
const indentPadding: Record<number, string> = {
0: 'pl-0',
1: 'pl-[20px]',
2: 'pl-[40px]',
3: 'pl-[60px]',
4: 'pl-[80px]',
5: 'pl-[100px]',
6: 'pl-[120px]',
7: 'pl-[140px]',
8: 'pl-[160px]',
9: 'pl-[180px]',
1: 'pl-0',
2: 'pl-[20px]',
3: 'pl-[40px]',
4: 'pl-[60px]',
5: 'pl-[80px]',
6: 'pl-[100px]',
7: 'pl-[120px]',
8: 'pl-[140px]',
9: 'pl-[160px]',
10: 'pl-[180px]',
}
const indentLeft: Record<number, string> = {
1: 'left-0',
2: 'left-[20px]',
3: 'left-[40px]',
4: 'left-[60px]',
5: 'left-[80px]',
6: 'left-[100px]',
7: 'left-[120px]',
8: 'left-[140px]',
9: 'left-[160px]',
2: 'left-0',
3: 'left-[20px]',
4: 'left-[40px]',
5: 'left-[60px]',
6: 'left-[80px]',
7: 'left-[100px]',
8: 'left-[120px]',
9: 'left-[140px]',
10: 'left-[160px]',
}
const SchemaNode: FC<SchemaNodeProps> = ({
@ -66,7 +67,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
const hasChildren = getHasChildren(schema)
const type = getFieldType(schema)
const isHovering = hoveringProperty === path.join('.') && depth > 0
const isHovering = hoveringProperty === path.join('.') && depth > 1
const handleExpand = () => {
setIsExpanded(!isExpanded)
@ -85,7 +86,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
return (
<div className='relative'>
<div className={classNames('relative z-10', indentPadding[depth])}>
{depth > 0 && hasChildren && (
{depth > 1 && hasChildren && (
<div className={classNames(
'flex items-center absolute top-0 w-5 h-7 px-0.5 z-10 bg-background-section-burn',
indentLeft[depth],
@ -139,7 +140,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
<Divider type='vertical' className='bg-divider-subtle mx-0' />
</div>
{isExpanded && hasChildren && (
{isExpanded && hasChildren && depth < JSON_SCHEMA_MAX_DEPTH && (
<>
{schema.type === Type.object && schema.properties && (
Object.entries(schema.properties).map(([key, childSchema]) => (
@ -176,7 +177,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
)}
{
depth === 0 && !isAddingNewField && (
depth === 1 && !isAddingNewField && (
<AddField />
)
}

View File

@ -32,12 +32,12 @@ export const inferType = (value: any): Type => {
return Type.string
}
export function jsonToSchema(json: any): Field {
export const jsonToSchema = (json: any): Field => {
const schema: Field = {
type: inferType(json),
}
if (schema.type === 'object') {
if (schema.type === Type.object) {
schema.properties = {}
schema.required = []
schema.additionalProperties = false
@ -53,3 +53,22 @@ export function jsonToSchema(json: any): Field {
return schema
}
export const checkDepth = (json: any, currentDepth = 1) => {
const type = inferType(json)
if (type !== Type.object && type !== Type.array)
return currentDepth
let maxDepth = currentDepth
if (type === Type.object) {
Object.keys(json).forEach((key) => {
const depth = checkDepth(json[key], currentDepth + 1)
maxDepth = Math.max(maxDepth, depth)
})
}
else if (type === Type.array && json.length > 0) {
const depth = checkDepth(json[0], currentDepth + 1)
maxDepth = Math.max(maxDepth, depth)
}
return maxDepth
}

View File

@ -276,3 +276,5 @@ export const GITHUB_ACCESS_TOKEN = process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN |
export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl'
export const FULL_DOC_PREVIEW_LENGTH = 50
export const JSON_SCHEMA_MAX_DEPTH = 10