feat: add format and copy functionality to JSON schema code editor with tooltip support

This commit is contained in:
twwu 2025-03-19 12:55:15 +08:00
parent 86b1295efa
commit a32bc341fb
8 changed files with 53 additions and 44 deletions

View File

@ -5,6 +5,8 @@ import classNames from '@/utils/classnames'
import { Editor } from '@monaco-editor/react'
import { RiClipboardLine, RiIndentIncrease } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import Tooltip from '@/app/components/base/tooltip'
import { useTranslation } from 'react-i18next'
type CodeEditorProps = {
value: string
@ -22,6 +24,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
readOnly = false,
className,
}) => {
const { t } = useTranslation()
const { theme } = useTheme()
const monacoRef = useRef<any>(null)
const editorRef = useRef<any>(null)
@ -79,6 +82,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
</div>
<div className='flex items-center gap-x-0.5'>
{showFormatButton && (
<Tooltip popupContent={t('common.operation.format')}>
<button
type='button'
className='flex items-center justify-center h-6 w-6'
@ -86,13 +90,16 @@ const CodeEditor: FC<CodeEditorProps> = ({
>
<RiIndentIncrease className='w-4 h-4 text-text-tertiary' />
</button>
</Tooltip>
)}
<Tooltip popupContent={t('common.operation.copy')}>
<button
type='button'
className='flex items-center justify-center h-6 w-6'
onClick={() => copy(value)}>
<RiClipboardLine className='w-4 h-4 text-text-tertiary' />
</button>
</Tooltip>
</div>
</div>
<div className={classNames('relative', editorWrapperClassName)}>

View File

@ -1,8 +1,8 @@
import React, { useCallback } from 'react'
import Button from '@/app/components/base/button'
import { RiAddCircleFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useVisualEditorStore } from './store'
import { useCallback } from 'react'
import { useMittContext } from './context'
const AddField = () => {
@ -30,4 +30,4 @@ const AddField = () => {
)
}
export default AddField
export default React.memo(AddField)

View File

@ -43,4 +43,4 @@ const Card: FC<CardProps> = ({
)
}
export default Card
export default React.memo(Card)

View File

@ -75,4 +75,4 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
)
}
export default AdvancedOptions
export default React.memo(AdvancedOptions)

View File

@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next'
import classNames from '@/utils/classnames'
import { useVisualEditorStore } from '../store'
import { useMittContext } from '../context'
import produce from 'immer'
import { useUnmount } from 'ahooks'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
@ -63,13 +62,16 @@ const EditCard: FC<EditCardProps> = ({
const { emit, useSubscribe } = useMittContext()
const blurWithActions = useRef(false)
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 disableAddBtn = depth >= JSON_SCHEMA_MAX_DEPTH || (currentFields.type !== Type.object && currentFields.type !== ArrayType.object)
const hasAdvancedOptions = currentFields.type === Type.string || currentFields.type === Type.number
const isAdvancedEditing = advancedEditing || isAddingNewField
const advancedOptions = useMemo(() => {
return { enum: (currentFields.enum || []).join(', ') }
}, [currentFields.enum])
let enumValue = ''
if (currentFields.type === Type.string || currentFields.type === Type.number)
enumValue = (currentFields.enum || []).join(', ')
return { enum: enumValue }
}, [currentFields.type, currentFields.enum])
useSubscribe('restorePropertyName', () => {
setCurrentFields(prev => ({ ...prev, name: fields.name }))
@ -83,19 +85,13 @@ const EditCard: FC<EditCardProps> = ({
setAdvancedEditing(false)
})
const emitPropertyNameChange = useCallback((name: string) => {
const newFields = produce(fields, (draft) => {
draft.name = name
})
emit('propertyNameChange', { path, parentPath, oldFields: fields, fields: newFields })
}, [fields, path, parentPath, emit])
const emitPropertyNameChange = useCallback(() => {
emit('propertyNameChange', { path, parentPath, oldFields: fields, fields: currentFields })
}, [fields, currentFields, path, parentPath, emit])
const emitPropertyTypeChange = useCallback((type: Type | ArrayType) => {
const newFields = produce(fields, (draft) => {
draft.type = type
})
emit('propertyTypeChange', { path, parentPath, oldFields: fields, fields: newFields })
}, [fields, path, parentPath, emit])
const emitPropertyTypeChange = useCallback(() => {
emit('propertyTypeChange', { path, parentPath, oldFields: fields, fields: currentFields })
}, [fields, currentFields, path, parentPath, emit])
const emitPropertyRequiredToggle = useCallback(() => {
emit('propertyRequiredToggle', { path, parentPath, oldFields: fields, fields: currentFields })
@ -121,15 +117,15 @@ const EditCard: FC<EditCardProps> = ({
setCurrentFields(prev => ({ ...prev, name: e.target.value }))
}, [])
const handlePropertyNameBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
const handlePropertyNameBlur = useCallback(() => {
if (isAdvancedEditing) return
emitPropertyNameChange(e.target.value)
emitPropertyNameChange()
}, [isAdvancedEditing, emitPropertyNameChange])
const handleTypeChange = useCallback((item: TypeItem) => {
setCurrentFields(prev => ({ ...prev, type: item.value }))
if (isAdvancedEditing) return
emitPropertyTypeChange(item.value)
emitPropertyTypeChange()
}, [isAdvancedEditing, emitPropertyTypeChange])
const toggleRequired = useCallback(() => {
@ -142,16 +138,17 @@ const EditCard: FC<EditCardProps> = ({
setCurrentFields(prev => ({ ...prev, description: e.target.value }))
}, [])
const handleDescriptionBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
const handleDescriptionBlur = useCallback(() => {
if (isAdvancedEditing) return
emitPropertyOptionsChange({ description: e.target.value, enum: fields.enum })
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
emitPropertyOptionsChange({ description: currentFields.description, enum: currentFields.enum })
}, [isAdvancedEditing, emitPropertyOptionsChange, currentFields])
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
const enumValue = options.enum.replace(/\s/g, '').split(',')
setCurrentFields(prev => ({ ...prev, enum: enumValue }))
if (isAdvancedEditing) return
const enumValue = options.enum.replace(' ', '').split(',')
emitPropertyOptionsChange({ description: fields.description, enum: enumValue })
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
emitPropertyOptionsChange({ description: currentFields.description, enum: enumValue })
}, [isAdvancedEditing, emitPropertyOptionsChange, currentFields])
const handleDelete = useCallback(() => {
blurWithActions.current = true

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import { type Field, Type } from '../../../types'
import classNames from '@/utils/classnames'
import { RiArrowDropDownLine, RiArrowDropRightLine } from '@remixicon/react'
@ -65,8 +65,8 @@ const SchemaNode: FC<SchemaNodeProps> = ({
setHoveringProperty(path)
}, { wait: 50 })
const hasChildren = getHasChildren(schema)
const type = getFieldType(schema)
const hasChildren = useMemo(() => getHasChildren(schema), [schema])
const type = useMemo(() => getFieldType(schema), [schema])
const isHovering = hoveringProperty === path.join('.') && depth > 1
const handleExpand = () => {
@ -137,7 +137,10 @@ const SchemaNode: FC<SchemaNodeProps> = ({
schema.description ? 'h-[calc(100%-3rem)]' : 'h-[calc(100%-1.75rem)]',
indentLeft[depth + 1],
)}>
<Divider type='vertical' className='bg-divider-subtle mx-0' />
<Divider
type='vertical'
className={classNames('mx-0', isHovering ? 'bg-divider-deep' : 'bg-divider-subtle')}
/>
</div>
{isExpanded && hasChildren && depth < JSON_SCHEMA_MAX_DEPTH && (

View File

@ -54,6 +54,7 @@ const translation = {
regenerate: 'Regenerate',
submit: 'Submit',
skip: 'Skip',
format: 'Format',
},
errorMsg: {
fieldRequired: '{{field}} is required',

View File

@ -54,6 +54,7 @@ const translation = {
regenerate: '重新生成',
submit: '提交',
skip: '跳过',
format: '格式化',
},
errorMsg: {
fieldRequired: '{{field}} 为必填项',