mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 08:25:55 +08:00
feat: refactor JSON schema visual editor with new context and store management
This commit is contained in:
parent
7a647cf18e
commit
4a93aba8ba
@ -1,49 +0,0 @@
|
|||||||
import {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useRef,
|
|
||||||
} from 'react'
|
|
||||||
import { createJsonSchemaConfigStore } from './store'
|
|
||||||
import { useMitt } from '@/hooks/use-mitt'
|
|
||||||
|
|
||||||
type JsonSchemaConfigStore = ReturnType<typeof createJsonSchemaConfigStore>
|
|
||||||
|
|
||||||
type JsonSchemaConfigContextType = JsonSchemaConfigStore | null
|
|
||||||
|
|
||||||
type JsonSchemaConfigProviderProps = {
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const JsonSchemaConfigContext = createContext<JsonSchemaConfigContextType>(null)
|
|
||||||
|
|
||||||
export const JsonSchemaConfigContextProvider = ({ children }: JsonSchemaConfigProviderProps) => {
|
|
||||||
const storeRef = useRef<JsonSchemaConfigStore>()
|
|
||||||
|
|
||||||
if (!storeRef.current)
|
|
||||||
storeRef.current = createJsonSchemaConfigStore()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<JsonSchemaConfigContext.Provider value={storeRef.current}>
|
|
||||||
{children}
|
|
||||||
</JsonSchemaConfigContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MittContext = createContext<ReturnType<typeof useMitt>>({
|
|
||||||
emit: () => {},
|
|
||||||
useSubscribe: () => {},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const MittProvider = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const mitt = useMitt()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MittContext.Provider value={mitt}>
|
|
||||||
{children}
|
|
||||||
</MittContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMittContext = () => {
|
|
||||||
return useContext(MittContext)
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ import React, { type FC } from 'react'
|
|||||||
import Modal from '../../../../../base/modal'
|
import Modal from '../../../../../base/modal'
|
||||||
import type { SchemaRoot } from '../../types'
|
import type { SchemaRoot } from '../../types'
|
||||||
import JsonSchemaConfig from './json-schema-config'
|
import JsonSchemaConfig from './json-schema-config'
|
||||||
import { JsonSchemaConfigContextProvider, MittProvider } from './context'
|
|
||||||
|
|
||||||
type JsonSchemaConfigModalProps = {
|
type JsonSchemaConfigModalProps = {
|
||||||
isShow: boolean
|
isShow: boolean
|
||||||
@ -23,15 +22,11 @@ const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
className='max-w-[960px] h-[800px] p-0'
|
className='max-w-[960px] h-[800px] p-0'
|
||||||
>
|
>
|
||||||
<MittProvider>
|
|
||||||
<JsonSchemaConfigContextProvider>
|
|
||||||
<JsonSchemaConfig
|
<JsonSchemaConfig
|
||||||
defaultSchema={defaultSchema}
|
defaultSchema={defaultSchema}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</JsonSchemaConfigContextProvider>
|
|
||||||
</MittProvider >
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { type FC, useCallback, useState } from 'react'
|
import React, { type FC, useCallback, useState } from 'react'
|
||||||
import { ArrayType, type Field, type SchemaRoot, Type } from '../../types'
|
import { type SchemaRoot, Type } from '../../types'
|
||||||
import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react'
|
import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react'
|
||||||
import { SegmentedControl } from '../../../../../base/segmented-control'
|
import { SegmentedControl } from '../../../../../base/segmented-control'
|
||||||
import JsonSchemaGenerator from './json-schema-generator'
|
import JsonSchemaGenerator from './json-schema-generator'
|
||||||
@ -9,12 +9,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import VisualEditor from './visual-editor'
|
import VisualEditor from './visual-editor'
|
||||||
import SchemaEditor from './schema-editor'
|
import SchemaEditor from './schema-editor'
|
||||||
import { useJsonSchemaConfigStore } from './store'
|
|
||||||
import { useMittContext } from './context'
|
|
||||||
import type { EditData } from './visual-editor/edit-card'
|
|
||||||
import produce from 'immer'
|
|
||||||
import Toast from '@/app/components/base/toast'
|
|
||||||
import { jsonToSchema } from '../../utils'
|
import { jsonToSchema } from '../../utils'
|
||||||
|
import { MittProvider, VisualEditorContextProvider } from './visual-editor/context'
|
||||||
|
|
||||||
type JsonSchemaConfigProps = {
|
type JsonSchemaConfigProps = {
|
||||||
defaultSchema?: SchemaRoot
|
defaultSchema?: SchemaRoot
|
||||||
@ -39,435 +35,17 @@ const DEFAULT_SCHEMA: SchemaRoot = {
|
|||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangeEventParams = {
|
|
||||||
path: string[],
|
|
||||||
parentPath: string[],
|
|
||||||
oldFields: EditData,
|
|
||||||
fields: EditData,
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddEventParams = {
|
|
||||||
path: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const findPropertyWithPath = (target: any, path: string[]) => {
|
|
||||||
let current = target
|
|
||||||
for (const key of path)
|
|
||||||
current = current[key]
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||||
defaultSchema,
|
defaultSchema,
|
||||||
onSave,
|
onSave,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const backupSchema = useJsonSchemaConfigStore(state => state.backupSchema)
|
|
||||||
const setBackupSchema = useJsonSchemaConfigStore(state => state.setBackupSchema)
|
|
||||||
const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField)
|
|
||||||
const setHoveringProperty = useJsonSchemaConfigStore(state => state.setHoveringProperty)
|
|
||||||
const { emit, useSubscribe } = useMittContext()
|
|
||||||
const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor)
|
const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor)
|
||||||
const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA)
|
const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA)
|
||||||
const [json, setJson] = useState(JSON.stringify(jsonSchema, null, 2))
|
const [json, setJson] = useState(JSON.stringify(jsonSchema, null, 2))
|
||||||
const [btnWidth, setBtnWidth] = useState(0)
|
const [btnWidth, setBtnWidth] = useState(0)
|
||||||
|
|
||||||
useSubscribe('restoreSchema', () => {
|
|
||||||
if (backupSchema) {
|
|
||||||
setJsonSchema(backupSchema)
|
|
||||||
setBackupSchema(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useSubscribe('propertyNameChange', (params) => {
|
|
||||||
const { parentPath, oldFields, fields } = params as ChangeEventParams
|
|
||||||
const { name: oldName } = oldFields
|
|
||||||
const { name: newName } = fields
|
|
||||||
const newSchema = produce(jsonSchema, (draft) => {
|
|
||||||
if (oldName === newName) return
|
|
||||||
const schema = findPropertyWithPath(draft, parentPath) as Field
|
|
||||||
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
const properties = schema.properties || {}
|
|
||||||
if (properties[newName]) {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Property name already exists',
|
|
||||||
})
|
|
||||||
emit('restorePropertyName')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
|
||||||
acc[key === oldName ? newName : key] = value
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, Field>)
|
|
||||||
|
|
||||||
const required = schema.required || []
|
|
||||||
const newRequired = produce(required, (draft) => {
|
|
||||||
const index = draft.indexOf(oldName)
|
|
||||||
if (index !== -1)
|
|
||||||
draft.splice(index, 1, newName)
|
|
||||||
})
|
|
||||||
|
|
||||||
schema.properties = newProperties
|
|
||||||
schema.required = newRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
|
||||||
const properties = schema.items.properties || {}
|
|
||||||
if (properties[newName]) {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Property name already exists',
|
|
||||||
})
|
|
||||||
emit('restorePropertyName')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
|
||||||
acc[key === oldName ? newName : key] = value
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, Field>)
|
|
||||||
const required = schema.items.required || []
|
|
||||||
const newRequired = produce(required, (draft) => {
|
|
||||||
const index = draft.indexOf(oldName)
|
|
||||||
if (index !== -1)
|
|
||||||
draft.splice(index, 1, newName)
|
|
||||||
})
|
|
||||||
|
|
||||||
schema.items.properties = newProperties
|
|
||||||
schema.items.required = newRequired
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setJsonSchema(newSchema)
|
|
||||||
})
|
|
||||||
|
|
||||||
useSubscribe('propertyTypeChange', (params) => {
|
|
||||||
const { path, oldFields, fields } = params as ChangeEventParams
|
|
||||||
const { type: oldType } = oldFields
|
|
||||||
const { type: newType } = fields
|
|
||||||
if (oldType === newType) return
|
|
||||||
const newSchema = produce(jsonSchema, (draft) => {
|
|
||||||
const schema = findPropertyWithPath(draft, path) as Field
|
|
||||||
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
delete schema.properties
|
|
||||||
delete schema.required
|
|
||||||
}
|
|
||||||
if (schema.type === Type.array)
|
|
||||||
delete schema.items
|
|
||||||
switch (newType) {
|
|
||||||
case Type.object:
|
|
||||||
schema.type = Type.object
|
|
||||||
schema.properties = {}
|
|
||||||
schema.required = []
|
|
||||||
break
|
|
||||||
case ArrayType.string:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.string,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.number:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.number,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.boolean:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.boolean,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.object:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.object,
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
schema.type = newType as Type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setJsonSchema(newSchema)
|
|
||||||
})
|
|
||||||
|
|
||||||
useSubscribe('propertyRequiredToggle', (params) => {
|
|
||||||
const { parentPath, fields } = params as ChangeEventParams
|
|
||||||
const { name } = fields
|
|
||||||
const newSchema = produce(jsonSchema, (draft) => {
|
|
||||||
const schema = findPropertyWithPath(draft, parentPath) as Field
|
|
||||||
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
const required = schema.required || []
|
|
||||||
const newRequired = required.includes(name)
|
|
||||||
? required.filter(item => item !== name)
|
|
||||||
: [...required, name]
|
|
||||||
schema.required = newRequired
|
|
||||||
}
|
|
||||||
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
|
||||||
const required = schema.items.required || []
|
|
||||||
const newRequired = required.includes(name)
|
|
||||||
? required.filter(item => item !== name)
|
|
||||||
: [...required, name]
|
|
||||||
schema.items.required = newRequired
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setJsonSchema(newSchema)
|
|
||||||
})
|
|
||||||
|
|
||||||
useSubscribe('propertyOptionsChange', (params) => {
|
|
||||||
const { path, fields } = params as ChangeEventParams
|
|
||||||
const newSchema = produce(jsonSchema, (draft) => {
|
|
||||||
const schema = findPropertyWithPath(draft, path) as Field
|
|
||||||
schema.description = fields.description
|
|
||||||
schema.enum = fields.enum
|
|
||||||
})
|
|
||||||
setJsonSchema(newSchema)
|
|
||||||
})
|
|
||||||
|
|
||||||
useSubscribe('propertyDelete', (params) => {
|
|
||||||
const { parentPath, fields } = params as ChangeEventParams
|
|
||||||
const { name } = fields
|
|
||||||
const newSchema = produce(jsonSchema, (draft) => {
|
|
||||||
const schema = findPropertyWithPath(draft, parentPath) as Field
|
|
||||||
if (schema.type === Type.object && schema.properties) {
|
|
||||||
delete schema.properties[name]
|
|
||||||
schema.required = schema.required?.filter(item => item !== name)
|
|
||||||
}
|
|
||||||
if (schema.type === Type.array && schema.items?.properties && schema.items?.type === Type.object) {
|
|
||||||
delete schema.items.properties[name]
|
|
||||||
schema.items.required = schema.items.required?.filter(item => item !== name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setJsonSchema(newSchema)
|
|
||||||
})
|
|
||||||
|
|
||||||
useSubscribe('addField', (params) => {
|
|
||||||
setBackupSchema(jsonSchema)
|
|
||||||
const { path } = params as AddEventParams
|
|
||||||
setIsAddingNewField(true)
|
|
||||||
const newSchema = produce(jsonSchema, (draft) => {
|
|
||||||
const schema = findPropertyWithPath(draft, path) as Field
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
schema.properties = {
|
|
||||||
...(schema.properties || {}),
|
|
||||||
'': {
|
|
||||||
type: Type.string,
|
|
||||||
description: '',
|
|
||||||
enum: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
setHoveringProperty([...path, 'properties', ''].join('.'))
|
|
||||||
}
|
|
||||||
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
|
||||||
schema.items.properties = {
|
|
||||||
...(schema.items.properties || {}),
|
|
||||||
'': {
|
|
||||||
type: Type.string,
|
|
||||||
description: '',
|
|
||||||
enum: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
setHoveringProperty([...path, 'items', 'properties', ''].join('.'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setJsonSchema(newSchema)
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
const { name: oldName, type: oldType, required: oldRequired } = oldFields
|
|
||||||
const { name: newName, type: newType, required: newRequired } = fields
|
|
||||||
if (parentSchema.type === Type.object && parentSchema.properties) {
|
|
||||||
// name change
|
|
||||||
if (oldName !== newName) {
|
|
||||||
const properties = parentSchema.properties
|
|
||||||
if (properties[newName]) {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Property name already exists',
|
|
||||||
})
|
|
||||||
samePropertyNameError = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
|
||||||
acc[key === oldName ? newName : key] = value
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, Field>)
|
|
||||||
|
|
||||||
const requiredProperties = parentSchema.required || []
|
|
||||||
const newRequiredProperties = produce(requiredProperties, (draft) => {
|
|
||||||
const index = draft.indexOf(oldName)
|
|
||||||
if (index !== -1)
|
|
||||||
draft.splice(index, 1, newName)
|
|
||||||
})
|
|
||||||
|
|
||||||
parentSchema.properties = newProperties
|
|
||||||
parentSchema.required = newRequiredProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
// required change
|
|
||||||
if (oldRequired !== newRequired) {
|
|
||||||
const required = parentSchema.required || []
|
|
||||||
const newRequired = required.includes(newName)
|
|
||||||
? required.filter(item => item !== newName)
|
|
||||||
: [...required, newName]
|
|
||||||
parentSchema.required = newRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = parentSchema.properties[newName]
|
|
||||||
|
|
||||||
// type change
|
|
||||||
if (oldType !== newType) {
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
delete schema.properties
|
|
||||||
delete schema.required
|
|
||||||
}
|
|
||||||
if (schema.type === Type.array)
|
|
||||||
delete schema.items
|
|
||||||
switch (newType) {
|
|
||||||
case Type.object:
|
|
||||||
schema.type = Type.object
|
|
||||||
schema.properties = {}
|
|
||||||
schema.required = []
|
|
||||||
break
|
|
||||||
case ArrayType.string:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.string,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.number:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.number,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.boolean:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.boolean,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.object:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.object,
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
schema.type = newType as Type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// other options change
|
|
||||||
schema.description = fields.description
|
|
||||||
schema.enum = fields.enum
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentSchema.type === Type.array && parentSchema.items && parentSchema.items.type === Type.object && parentSchema.items.properties) {
|
|
||||||
// name change
|
|
||||||
if (oldName !== newName) {
|
|
||||||
const properties = parentSchema.items.properties || {}
|
|
||||||
if (properties[newName]) {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Property name already exists',
|
|
||||||
})
|
|
||||||
samePropertyNameError = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
|
||||||
acc[key === oldName ? newName : key] = value
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, Field>)
|
|
||||||
const required = parentSchema.items.required || []
|
|
||||||
const newRequired = produce(required, (draft) => {
|
|
||||||
const index = draft.indexOf(oldName)
|
|
||||||
if (index !== -1)
|
|
||||||
draft.splice(index, 1, newName)
|
|
||||||
})
|
|
||||||
|
|
||||||
parentSchema.items.properties = newProperties
|
|
||||||
parentSchema.items.required = newRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
// required change
|
|
||||||
if (oldRequired !== newRequired) {
|
|
||||||
const required = parentSchema.items.required || []
|
|
||||||
const newRequired = required.includes(newName)
|
|
||||||
? required.filter(item => item !== newName)
|
|
||||||
: [...required, newName]
|
|
||||||
parentSchema.items.required = newRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = parentSchema.items.properties[newName]
|
|
||||||
// type change
|
|
||||||
if (oldType !== newType) {
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
delete schema.properties
|
|
||||||
delete schema.required
|
|
||||||
}
|
|
||||||
if (schema.type === Type.array)
|
|
||||||
delete schema.items
|
|
||||||
switch (newType) {
|
|
||||||
case Type.object:
|
|
||||||
schema.type = Type.object
|
|
||||||
schema.properties = {}
|
|
||||||
schema.required = []
|
|
||||||
break
|
|
||||||
case ArrayType.string:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.string,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.number:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.number,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.boolean:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.boolean,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.object:
|
|
||||||
schema.type = Type.array
|
|
||||||
schema.items = {
|
|
||||||
type: Type.object,
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
schema.type = newType as Type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// other options change
|
|
||||||
schema.description = fields.description
|
|
||||||
schema.enum = fields.enum
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (samePropertyNameError) return
|
|
||||||
setJsonSchema(newSchema)
|
|
||||||
emit('fieldChangeSuccess')
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateBtnWidth = useCallback((width: number) => {
|
const updateBtnWidth = useCallback((width: number) => {
|
||||||
setBtnWidth(width + 32)
|
setBtnWidth(width + 32)
|
||||||
}, [])
|
}, [])
|
||||||
@ -481,6 +59,10 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||||||
setJsonSchema(jsonSchema)
|
setJsonSchema(jsonSchema)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleVisualEditorUpdate = useCallback((schema: SchemaRoot) => {
|
||||||
|
setJsonSchema(schema)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSchemaEditorUpdate = useCallback((schema: string) => {
|
const handleSchemaEditorUpdate = useCallback((schema: string) => {
|
||||||
setJson(schema)
|
setJson(schema)
|
||||||
}, [])
|
}, [])
|
||||||
@ -535,7 +117,14 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='px-6 grow overflow-hidden'>
|
<div className='px-6 grow overflow-hidden'>
|
||||||
{currentTab === SchemaView.VisualEditor && (
|
{currentTab === SchemaView.VisualEditor && (
|
||||||
<VisualEditor schema={jsonSchema} />
|
<MittProvider>
|
||||||
|
<VisualEditorContextProvider>
|
||||||
|
<VisualEditor
|
||||||
|
schema={jsonSchema}
|
||||||
|
onChange={handleVisualEditorUpdate}
|
||||||
|
/>
|
||||||
|
</VisualEditorContextProvider>
|
||||||
|
</MittProvider>
|
||||||
)}
|
)}
|
||||||
{currentTab === SchemaView.JsonSchema && (
|
{currentTab === SchemaView.JsonSchema && (
|
||||||
<SchemaEditor
|
<SchemaEditor
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { RiAddCircleFill } from '@remixicon/react'
|
import { RiAddCircleFill } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useJsonSchemaConfigStore } from '../store'
|
import { useVisualEditorStore } from './store'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useMittContext } from '../context'
|
import { useMittContext } from './context'
|
||||||
|
|
||||||
const AddField = () => {
|
const AddField = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField)
|
const setIsAddingNewField = useVisualEditorStore(state => state.setIsAddingNewField)
|
||||||
const { emit } = useMittContext()
|
const { emit } = useMittContext()
|
||||||
|
|
||||||
const handleAddField = useCallback(() => {
|
const handleAddField = useCallback(() => {
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
} from 'react'
|
||||||
|
import { createVisualEditorStore } from './store'
|
||||||
|
import { useMitt } from '@/hooks/use-mitt'
|
||||||
|
|
||||||
|
type VisualEditorStore = ReturnType<typeof createVisualEditorStore>
|
||||||
|
|
||||||
|
type VisualEditorContextType = VisualEditorStore | null
|
||||||
|
|
||||||
|
type VisualEditorProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VisualEditorContext = createContext<VisualEditorContextType>(null)
|
||||||
|
|
||||||
|
export const VisualEditorContextProvider = ({ children }: VisualEditorProviderProps) => {
|
||||||
|
const storeRef = useRef<VisualEditorStore>()
|
||||||
|
|
||||||
|
if (!storeRef.current)
|
||||||
|
storeRef.current = createVisualEditorStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VisualEditorContext.Provider value={storeRef.current}>
|
||||||
|
{children}
|
||||||
|
</VisualEditorContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MittContext = createContext<ReturnType<typeof useMitt>>({
|
||||||
|
emit: () => {},
|
||||||
|
useSubscribe: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MittProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const mitt = useMitt()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MittContext.Provider value={mitt}>
|
||||||
|
{children}
|
||||||
|
</MittContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMittContext = () => {
|
||||||
|
return useContext(MittContext)
|
||||||
|
}
|
@ -10,8 +10,8 @@ import AdvancedActions from './advanced-actions'
|
|||||||
import AdvancedOptions, { type AdvancedOptionsType } from './advanced-options'
|
import AdvancedOptions, { type AdvancedOptionsType } from './advanced-options'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
import { useJsonSchemaConfigStore } from '../../store'
|
import { useVisualEditorStore } from '../store'
|
||||||
import { useMittContext } from '../../context'
|
import { useMittContext } from '../context'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import { useUnmount } from 'ahooks'
|
import { useUnmount } from 'ahooks'
|
||||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||||
@ -56,10 +56,10 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [currentFields, setCurrentFields] = useState(fields)
|
const [currentFields, setCurrentFields] = useState(fields)
|
||||||
const [backupFields, setBackupFields] = useState<EditData | null>(null)
|
const [backupFields, setBackupFields] = useState<EditData | null>(null)
|
||||||
const isAddingNewField = useJsonSchemaConfigStore(state => state.isAddingNewField)
|
const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField)
|
||||||
const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField)
|
const setIsAddingNewField = useVisualEditorStore(state => state.setIsAddingNewField)
|
||||||
const advancedEditing = useJsonSchemaConfigStore(state => state.advancedEditing)
|
const advancedEditing = useVisualEditorStore(state => state.advancedEditing)
|
||||||
const setAdvancedEditing = useJsonSchemaConfigStore(state => state.setAdvancedEditing)
|
const setAdvancedEditing = useVisualEditorStore(state => state.setAdvancedEditing)
|
||||||
const { emit, useSubscribe } = useMittContext()
|
const { emit, useSubscribe } = useMittContext()
|
||||||
const blurWithActions = useRef(false)
|
const blurWithActions = useRef(false)
|
||||||
|
|
||||||
|
@ -0,0 +1,423 @@
|
|||||||
|
import produce from 'immer'
|
||||||
|
import type { VisualEditorProps } from '.'
|
||||||
|
import { useMittContext } from './context'
|
||||||
|
import { useVisualEditorStore } from './store'
|
||||||
|
import type { EditData } from './edit-card'
|
||||||
|
import { ArrayType, type Field, Type } from '../../../types'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { findPropertyWithPath } from '../../../utils'
|
||||||
|
|
||||||
|
type ChangeEventParams = {
|
||||||
|
path: string[],
|
||||||
|
parentPath: string[],
|
||||||
|
oldFields: EditData,
|
||||||
|
fields: EditData,
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddEventParams = {
|
||||||
|
path: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSchemaNodeOperations = (props: VisualEditorProps) => {
|
||||||
|
const { schema: jsonSchema, onChange } = props
|
||||||
|
const backupSchema = useVisualEditorStore(state => state.backupSchema)
|
||||||
|
const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema)
|
||||||
|
const setIsAddingNewField = useVisualEditorStore(state => state.setIsAddingNewField)
|
||||||
|
const setHoveringProperty = useVisualEditorStore(state => state.setHoveringProperty)
|
||||||
|
const { emit, useSubscribe } = useMittContext()
|
||||||
|
|
||||||
|
useSubscribe('restoreSchema', () => {
|
||||||
|
if (backupSchema) {
|
||||||
|
onChange(backupSchema)
|
||||||
|
setBackupSchema(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useSubscribe('propertyNameChange', (params) => {
|
||||||
|
const { parentPath, oldFields, fields } = params as ChangeEventParams
|
||||||
|
const { name: oldName } = oldFields
|
||||||
|
const { name: newName } = fields
|
||||||
|
const newSchema = produce(jsonSchema, (draft) => {
|
||||||
|
if (oldName === newName) return
|
||||||
|
const schema = findPropertyWithPath(draft, parentPath) as Field
|
||||||
|
|
||||||
|
if (schema.type === Type.object) {
|
||||||
|
const properties = schema.properties || {}
|
||||||
|
if (properties[newName]) {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Property name already exists',
|
||||||
|
})
|
||||||
|
emit('restorePropertyName')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
||||||
|
acc[key === oldName ? newName : key] = value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, Field>)
|
||||||
|
|
||||||
|
const required = schema.required || []
|
||||||
|
const newRequired = produce(required, (draft) => {
|
||||||
|
const index = draft.indexOf(oldName)
|
||||||
|
if (index !== -1)
|
||||||
|
draft.splice(index, 1, newName)
|
||||||
|
})
|
||||||
|
|
||||||
|
schema.properties = newProperties
|
||||||
|
schema.required = newRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
||||||
|
const properties = schema.items.properties || {}
|
||||||
|
if (properties[newName]) {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Property name already exists',
|
||||||
|
})
|
||||||
|
emit('restorePropertyName')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
||||||
|
acc[key === oldName ? newName : key] = value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, Field>)
|
||||||
|
const required = schema.items.required || []
|
||||||
|
const newRequired = produce(required, (draft) => {
|
||||||
|
const index = draft.indexOf(oldName)
|
||||||
|
if (index !== -1)
|
||||||
|
draft.splice(index, 1, newName)
|
||||||
|
})
|
||||||
|
|
||||||
|
schema.items.properties = newProperties
|
||||||
|
schema.items.required = newRequired
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onChange(newSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
useSubscribe('propertyTypeChange', (params) => {
|
||||||
|
const { path, oldFields, fields } = params as ChangeEventParams
|
||||||
|
const { type: oldType } = oldFields
|
||||||
|
const { type: newType } = fields
|
||||||
|
if (oldType === newType) return
|
||||||
|
const newSchema = produce(jsonSchema, (draft) => {
|
||||||
|
const schema = findPropertyWithPath(draft, path) as Field
|
||||||
|
|
||||||
|
if (schema.type === Type.object) {
|
||||||
|
delete schema.properties
|
||||||
|
delete schema.required
|
||||||
|
}
|
||||||
|
if (schema.type === Type.array)
|
||||||
|
delete schema.items
|
||||||
|
switch (newType) {
|
||||||
|
case Type.object:
|
||||||
|
schema.type = Type.object
|
||||||
|
schema.properties = {}
|
||||||
|
schema.required = []
|
||||||
|
break
|
||||||
|
case ArrayType.string:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.string,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.number:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.number,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.boolean:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.boolean,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.object:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.object,
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
schema.type = newType as Type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onChange(newSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
useSubscribe('propertyRequiredToggle', (params) => {
|
||||||
|
const { parentPath, fields } = params as ChangeEventParams
|
||||||
|
const { name } = fields
|
||||||
|
const newSchema = produce(jsonSchema, (draft) => {
|
||||||
|
const schema = findPropertyWithPath(draft, parentPath) as Field
|
||||||
|
|
||||||
|
if (schema.type === Type.object) {
|
||||||
|
const required = schema.required || []
|
||||||
|
const newRequired = required.includes(name)
|
||||||
|
? required.filter(item => item !== name)
|
||||||
|
: [...required, name]
|
||||||
|
schema.required = newRequired
|
||||||
|
}
|
||||||
|
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
||||||
|
const required = schema.items.required || []
|
||||||
|
const newRequired = required.includes(name)
|
||||||
|
? required.filter(item => item !== name)
|
||||||
|
: [...required, name]
|
||||||
|
schema.items.required = newRequired
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onChange(newSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
useSubscribe('propertyOptionsChange', (params) => {
|
||||||
|
const { path, fields } = params as ChangeEventParams
|
||||||
|
const newSchema = produce(jsonSchema, (draft) => {
|
||||||
|
const schema = findPropertyWithPath(draft, path) as Field
|
||||||
|
schema.description = fields.description
|
||||||
|
schema.enum = fields.enum
|
||||||
|
})
|
||||||
|
onChange(newSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
useSubscribe('propertyDelete', (params) => {
|
||||||
|
const { parentPath, fields } = params as ChangeEventParams
|
||||||
|
const { name } = fields
|
||||||
|
const newSchema = produce(jsonSchema, (draft) => {
|
||||||
|
const schema = findPropertyWithPath(draft, parentPath) as Field
|
||||||
|
if (schema.type === Type.object && schema.properties) {
|
||||||
|
delete schema.properties[name]
|
||||||
|
schema.required = schema.required?.filter(item => item !== name)
|
||||||
|
}
|
||||||
|
if (schema.type === Type.array && schema.items?.properties && schema.items?.type === Type.object) {
|
||||||
|
delete schema.items.properties[name]
|
||||||
|
schema.items.required = schema.items.required?.filter(item => item !== name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onChange(newSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
useSubscribe('addField', (params) => {
|
||||||
|
setBackupSchema(jsonSchema)
|
||||||
|
const { path } = params as AddEventParams
|
||||||
|
setIsAddingNewField(true)
|
||||||
|
const newSchema = produce(jsonSchema, (draft) => {
|
||||||
|
const schema = findPropertyWithPath(draft, path) as Field
|
||||||
|
if (schema.type === Type.object) {
|
||||||
|
schema.properties = {
|
||||||
|
...(schema.properties || {}),
|
||||||
|
'': {
|
||||||
|
type: Type.string,
|
||||||
|
description: '',
|
||||||
|
enum: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setHoveringProperty([...path, 'properties', ''].join('.'))
|
||||||
|
}
|
||||||
|
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
||||||
|
schema.items.properties = {
|
||||||
|
...(schema.items.properties || {}),
|
||||||
|
'': {
|
||||||
|
type: Type.string,
|
||||||
|
description: '',
|
||||||
|
enum: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setHoveringProperty([...path, 'items', 'properties', ''].join('.'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onChange(newSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
const { name: oldName, type: oldType, required: oldRequired } = oldFields
|
||||||
|
const { name: newName, type: newType, required: newRequired } = fields
|
||||||
|
if (parentSchema.type === Type.object && parentSchema.properties) {
|
||||||
|
// name change
|
||||||
|
if (oldName !== newName) {
|
||||||
|
const properties = parentSchema.properties
|
||||||
|
if (properties[newName]) {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Property name already exists',
|
||||||
|
})
|
||||||
|
samePropertyNameError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
||||||
|
acc[key === oldName ? newName : key] = value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, Field>)
|
||||||
|
|
||||||
|
const requiredProperties = parentSchema.required || []
|
||||||
|
const newRequiredProperties = produce(requiredProperties, (draft) => {
|
||||||
|
const index = draft.indexOf(oldName)
|
||||||
|
if (index !== -1)
|
||||||
|
draft.splice(index, 1, newName)
|
||||||
|
})
|
||||||
|
|
||||||
|
parentSchema.properties = newProperties
|
||||||
|
parentSchema.required = newRequiredProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
// required change
|
||||||
|
if (oldRequired !== newRequired) {
|
||||||
|
const required = parentSchema.required || []
|
||||||
|
const newRequired = required.includes(newName)
|
||||||
|
? required.filter(item => item !== newName)
|
||||||
|
: [...required, newName]
|
||||||
|
parentSchema.required = newRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = parentSchema.properties[newName]
|
||||||
|
|
||||||
|
// type change
|
||||||
|
if (oldType !== newType) {
|
||||||
|
if (schema.type === Type.object) {
|
||||||
|
delete schema.properties
|
||||||
|
delete schema.required
|
||||||
|
}
|
||||||
|
if (schema.type === Type.array)
|
||||||
|
delete schema.items
|
||||||
|
switch (newType) {
|
||||||
|
case Type.object:
|
||||||
|
schema.type = Type.object
|
||||||
|
schema.properties = {}
|
||||||
|
schema.required = []
|
||||||
|
break
|
||||||
|
case ArrayType.string:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.string,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.number:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.number,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.boolean:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.boolean,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.object:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.object,
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
schema.type = newType as Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// other options change
|
||||||
|
schema.description = fields.description
|
||||||
|
schema.enum = fields.enum
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentSchema.type === Type.array && parentSchema.items && parentSchema.items.type === Type.object && parentSchema.items.properties) {
|
||||||
|
// name change
|
||||||
|
if (oldName !== newName) {
|
||||||
|
const properties = parentSchema.items.properties || {}
|
||||||
|
if (properties[newName]) {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Property name already exists',
|
||||||
|
})
|
||||||
|
samePropertyNameError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
||||||
|
acc[key === oldName ? newName : key] = value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, Field>)
|
||||||
|
const required = parentSchema.items.required || []
|
||||||
|
const newRequired = produce(required, (draft) => {
|
||||||
|
const index = draft.indexOf(oldName)
|
||||||
|
if (index !== -1)
|
||||||
|
draft.splice(index, 1, newName)
|
||||||
|
})
|
||||||
|
|
||||||
|
parentSchema.items.properties = newProperties
|
||||||
|
parentSchema.items.required = newRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// required change
|
||||||
|
if (oldRequired !== newRequired) {
|
||||||
|
const required = parentSchema.items.required || []
|
||||||
|
const newRequired = required.includes(newName)
|
||||||
|
? required.filter(item => item !== newName)
|
||||||
|
: [...required, newName]
|
||||||
|
parentSchema.items.required = newRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = parentSchema.items.properties[newName]
|
||||||
|
// type change
|
||||||
|
if (oldType !== newType) {
|
||||||
|
if (schema.type === Type.object) {
|
||||||
|
delete schema.properties
|
||||||
|
delete schema.required
|
||||||
|
}
|
||||||
|
if (schema.type === Type.array)
|
||||||
|
delete schema.items
|
||||||
|
switch (newType) {
|
||||||
|
case Type.object:
|
||||||
|
schema.type = Type.object
|
||||||
|
schema.properties = {}
|
||||||
|
schema.required = []
|
||||||
|
break
|
||||||
|
case ArrayType.string:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.string,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.number:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.number,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.boolean:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.boolean,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ArrayType.object:
|
||||||
|
schema.type = Type.array
|
||||||
|
schema.items = {
|
||||||
|
type: Type.object,
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
schema.type = newType as Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// other options change
|
||||||
|
schema.description = fields.description
|
||||||
|
schema.enum = fields.enum
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (samePropertyNameError) return
|
||||||
|
onChange(newSchema)
|
||||||
|
emit('fieldChangeSuccess')
|
||||||
|
})
|
||||||
|
}
|
@ -1,14 +1,17 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { SchemaRoot } from '../../../types'
|
import type { SchemaRoot } from '../../../types'
|
||||||
import SchemaNode from './schema-node'
|
import SchemaNode from './schema-node'
|
||||||
|
import { useSchemaNodeOperations } from './hooks'
|
||||||
|
|
||||||
type VisualEditorProps = {
|
export type VisualEditorProps = {
|
||||||
schema: SchemaRoot
|
schema: SchemaRoot
|
||||||
|
onChange: (schema: SchemaRoot) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const VisualEditor: FC<VisualEditorProps> = ({
|
const VisualEditor: FC<VisualEditorProps> = (props) => {
|
||||||
schema,
|
const { schema } = props
|
||||||
}) => {
|
useSchemaNodeOperations(props)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full rounded-xl p-1 pl-2 bg-background-section-burn overflow-auto'>
|
<div className='h-full rounded-xl p-1 pl-2 bg-background-section-burn overflow-auto'>
|
||||||
<SchemaNode
|
<SchemaNode
|
||||||
|
@ -7,7 +7,7 @@ import { getFieldType, getHasChildren } from '../../../utils'
|
|||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import EditCard from './edit-card'
|
import EditCard from './edit-card'
|
||||||
import Card from './card'
|
import Card from './card'
|
||||||
import { useJsonSchemaConfigStore } from '../store'
|
import { useVisualEditorStore } from './store'
|
||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
import AddField from './add-field'
|
import AddField from './add-field'
|
||||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||||
@ -56,10 +56,10 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
depth,
|
depth,
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(true)
|
const [isExpanded, setIsExpanded] = useState(true)
|
||||||
const hoveringProperty = useJsonSchemaConfigStore(state => state.hoveringProperty)
|
const hoveringProperty = useVisualEditorStore(state => state.hoveringProperty)
|
||||||
const setHoveringProperty = useJsonSchemaConfigStore(state => state.setHoveringProperty)
|
const setHoveringProperty = useVisualEditorStore(state => state.setHoveringProperty)
|
||||||
const isAddingNewField = useJsonSchemaConfigStore(state => state.isAddingNewField)
|
const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField)
|
||||||
const advancedEditing = useJsonSchemaConfigStore(state => state.advancedEditing)
|
const advancedEditing = useVisualEditorStore(state => state.advancedEditing)
|
||||||
|
|
||||||
const { run: setHoveringPropertyDebounced } = useDebounceFn((path: string) => {
|
const { run: setHoveringPropertyDebounced } = useDebounceFn((path: string) => {
|
||||||
setHoveringProperty(path)
|
setHoveringProperty(path)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
import { createStore, useStore } from 'zustand'
|
import { createStore, useStore } from 'zustand'
|
||||||
import type { SchemaRoot } from '../../types'
|
import type { SchemaRoot } from '../../../types'
|
||||||
import { JsonSchemaConfigContext } from './context'
|
import { VisualEditorContext } from './context'
|
||||||
|
|
||||||
type JsonSchemaConfigStore = {
|
type VisualEditorStore = {
|
||||||
hoveringProperty: string | ''
|
hoveringProperty: string | ''
|
||||||
setHoveringProperty: (propertyPath: string) => void
|
setHoveringProperty: (propertyPath: string) => void
|
||||||
isAddingNewField: boolean
|
isAddingNewField: boolean
|
||||||
@ -14,7 +14,7 @@ type JsonSchemaConfigStore = {
|
|||||||
setBackupSchema: (schema: SchemaRoot | null) => void
|
setBackupSchema: (schema: SchemaRoot | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createJsonSchemaConfigStore = () => createStore<JsonSchemaConfigStore>(set => ({
|
export const createVisualEditorStore = () => createStore<VisualEditorStore>(set => ({
|
||||||
hoveringProperty: '',
|
hoveringProperty: '',
|
||||||
setHoveringProperty: (propertyPath: string) => set({ hoveringProperty: propertyPath }),
|
setHoveringProperty: (propertyPath: string) => set({ hoveringProperty: propertyPath }),
|
||||||
isAddingNewField: false,
|
isAddingNewField: false,
|
||||||
@ -25,10 +25,10 @@ export const createJsonSchemaConfigStore = () => createStore<JsonSchemaConfigSto
|
|||||||
setBackupSchema: (schema: SchemaRoot | null) => set({ backupSchema: schema }),
|
setBackupSchema: (schema: SchemaRoot | null) => set({ backupSchema: schema }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const useJsonSchemaConfigStore = <T>(selector: (state: JsonSchemaConfigStore) => T): T => {
|
export const useVisualEditorStore = <T>(selector: (state: VisualEditorStore) => T): T => {
|
||||||
const store = useContext(JsonSchemaConfigContext)
|
const store = useContext(VisualEditorContext)
|
||||||
if (!store)
|
if (!store)
|
||||||
throw new Error('Missing JsonSchemaConfigContext.Provider in the tree')
|
throw new Error('Missing VisualEditorContext.Provider in the tree')
|
||||||
|
|
||||||
return useStore(store, selector)
|
return useStore(store, selector)
|
||||||
}
|
}
|
@ -72,3 +72,10 @@ export const checkDepth = (json: any, currentDepth = 1) => {
|
|||||||
}
|
}
|
||||||
return maxDepth
|
return maxDepth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const findPropertyWithPath = (target: any, path: string[]) => {
|
||||||
|
let current = target
|
||||||
|
for (const key of path)
|
||||||
|
current = current[key]
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user