mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-04-23 14:19:42 +08:00
feat: add format and copy functionality to JSON schema code editor with tooltip support
This commit is contained in:
parent
86b1295efa
commit
a32bc341fb
@ -5,6 +5,8 @@ import classNames from '@/utils/classnames'
|
|||||||
import { Editor } from '@monaco-editor/react'
|
import { Editor } from '@monaco-editor/react'
|
||||||
import { RiClipboardLine, RiIndentIncrease } from '@remixicon/react'
|
import { RiClipboardLine, RiIndentIncrease } from '@remixicon/react'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type CodeEditorProps = {
|
type CodeEditorProps = {
|
||||||
value: string
|
value: string
|
||||||
@ -22,6 +24,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
|
|||||||
readOnly = false,
|
readOnly = false,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const monacoRef = useRef<any>(null)
|
const monacoRef = useRef<any>(null)
|
||||||
const editorRef = useRef<any>(null)
|
const editorRef = useRef<any>(null)
|
||||||
@ -79,6 +82,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-x-0.5'>
|
<div className='flex items-center gap-x-0.5'>
|
||||||
{showFormatButton && (
|
{showFormatButton && (
|
||||||
|
<Tooltip popupContent={t('common.operation.format')}>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='flex items-center justify-center h-6 w-6'
|
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' />
|
<RiIndentIncrease className='w-4 h-4 text-text-tertiary' />
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip popupContent={t('common.operation.copy')}>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='flex items-center justify-center h-6 w-6'
|
className='flex items-center justify-center h-6 w-6'
|
||||||
onClick={() => copy(value)}>
|
onClick={() => copy(value)}>
|
||||||
<RiClipboardLine className='w-4 h-4 text-text-tertiary' />
|
<RiClipboardLine className='w-4 h-4 text-text-tertiary' />
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames('relative', editorWrapperClassName)}>
|
<div className={classNames('relative', editorWrapperClassName)}>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
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 { useVisualEditorStore } from './store'
|
import { useVisualEditorStore } from './store'
|
||||||
import { useCallback } from 'react'
|
|
||||||
import { useMittContext } from './context'
|
import { useMittContext } from './context'
|
||||||
|
|
||||||
const AddField = () => {
|
const AddField = () => {
|
||||||
@ -30,4 +30,4 @@ const AddField = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddField
|
export default React.memo(AddField)
|
||||||
|
@ -43,4 +43,4 @@ const Card: FC<CardProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Card
|
export default React.memo(Card)
|
||||||
|
@ -75,4 +75,4 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdvancedOptions
|
export default React.memo(AdvancedOptions)
|
||||||
|
@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
import { useVisualEditorStore } from '../store'
|
import { useVisualEditorStore } from '../store'
|
||||||
import { useMittContext } from '../context'
|
import { useMittContext } from '../context'
|
||||||
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'
|
||||||
|
|
||||||
@ -63,13 +62,16 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
const { emit, useSubscribe } = useMittContext()
|
const { emit, useSubscribe } = useMittContext()
|
||||||
const blurWithActions = useRef(false)
|
const blurWithActions = useRef(false)
|
||||||
|
|
||||||
const disableAddBtn = depth >= JSON_SCHEMA_MAX_DEPTH || (fields.type !== Type.object && fields.type !== ArrayType.object)
|
const disableAddBtn = depth >= JSON_SCHEMA_MAX_DEPTH || (currentFields.type !== Type.object && currentFields.type !== ArrayType.object)
|
||||||
const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number
|
const hasAdvancedOptions = currentFields.type === Type.string || currentFields.type === Type.number
|
||||||
const isAdvancedEditing = advancedEditing || isAddingNewField
|
const isAdvancedEditing = advancedEditing || isAddingNewField
|
||||||
|
|
||||||
const advancedOptions = useMemo(() => {
|
const advancedOptions = useMemo(() => {
|
||||||
return { enum: (currentFields.enum || []).join(', ') }
|
let enumValue = ''
|
||||||
}, [currentFields.enum])
|
if (currentFields.type === Type.string || currentFields.type === Type.number)
|
||||||
|
enumValue = (currentFields.enum || []).join(', ')
|
||||||
|
return { enum: enumValue }
|
||||||
|
}, [currentFields.type, currentFields.enum])
|
||||||
|
|
||||||
useSubscribe('restorePropertyName', () => {
|
useSubscribe('restorePropertyName', () => {
|
||||||
setCurrentFields(prev => ({ ...prev, name: fields.name }))
|
setCurrentFields(prev => ({ ...prev, name: fields.name }))
|
||||||
@ -83,19 +85,13 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
setAdvancedEditing(false)
|
setAdvancedEditing(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
const emitPropertyNameChange = useCallback((name: string) => {
|
const emitPropertyNameChange = useCallback(() => {
|
||||||
const newFields = produce(fields, (draft) => {
|
emit('propertyNameChange', { path, parentPath, oldFields: fields, fields: currentFields })
|
||||||
draft.name = name
|
}, [fields, currentFields, path, parentPath, emit])
|
||||||
})
|
|
||||||
emit('propertyNameChange', { path, parentPath, oldFields: fields, fields: newFields })
|
|
||||||
}, [fields, path, parentPath, emit])
|
|
||||||
|
|
||||||
const emitPropertyTypeChange = useCallback((type: Type | ArrayType) => {
|
const emitPropertyTypeChange = useCallback(() => {
|
||||||
const newFields = produce(fields, (draft) => {
|
emit('propertyTypeChange', { path, parentPath, oldFields: fields, fields: currentFields })
|
||||||
draft.type = type
|
}, [fields, currentFields, path, parentPath, emit])
|
||||||
})
|
|
||||||
emit('propertyTypeChange', { path, parentPath, oldFields: fields, fields: newFields })
|
|
||||||
}, [fields, path, parentPath, emit])
|
|
||||||
|
|
||||||
const emitPropertyRequiredToggle = useCallback(() => {
|
const emitPropertyRequiredToggle = useCallback(() => {
|
||||||
emit('propertyRequiredToggle', { path, parentPath, oldFields: fields, fields: currentFields })
|
emit('propertyRequiredToggle', { path, parentPath, oldFields: fields, fields: currentFields })
|
||||||
@ -121,15 +117,15 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
setCurrentFields(prev => ({ ...prev, name: e.target.value }))
|
setCurrentFields(prev => ({ ...prev, name: e.target.value }))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handlePropertyNameBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
const handlePropertyNameBlur = useCallback(() => {
|
||||||
if (isAdvancedEditing) return
|
if (isAdvancedEditing) return
|
||||||
emitPropertyNameChange(e.target.value)
|
emitPropertyNameChange()
|
||||||
}, [isAdvancedEditing, emitPropertyNameChange])
|
}, [isAdvancedEditing, emitPropertyNameChange])
|
||||||
|
|
||||||
const handleTypeChange = useCallback((item: TypeItem) => {
|
const handleTypeChange = useCallback((item: TypeItem) => {
|
||||||
setCurrentFields(prev => ({ ...prev, type: item.value }))
|
setCurrentFields(prev => ({ ...prev, type: item.value }))
|
||||||
if (isAdvancedEditing) return
|
if (isAdvancedEditing) return
|
||||||
emitPropertyTypeChange(item.value)
|
emitPropertyTypeChange()
|
||||||
}, [isAdvancedEditing, emitPropertyTypeChange])
|
}, [isAdvancedEditing, emitPropertyTypeChange])
|
||||||
|
|
||||||
const toggleRequired = useCallback(() => {
|
const toggleRequired = useCallback(() => {
|
||||||
@ -142,16 +138,17 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
setCurrentFields(prev => ({ ...prev, description: e.target.value }))
|
setCurrentFields(prev => ({ ...prev, description: e.target.value }))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDescriptionBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
const handleDescriptionBlur = useCallback(() => {
|
||||||
if (isAdvancedEditing) return
|
if (isAdvancedEditing) return
|
||||||
emitPropertyOptionsChange({ description: e.target.value, enum: fields.enum })
|
emitPropertyOptionsChange({ description: currentFields.description, enum: currentFields.enum })
|
||||||
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
|
}, [isAdvancedEditing, emitPropertyOptionsChange, currentFields])
|
||||||
|
|
||||||
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
|
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
|
||||||
|
const enumValue = options.enum.replace(/\s/g, '').split(',')
|
||||||
|
setCurrentFields(prev => ({ ...prev, enum: enumValue }))
|
||||||
if (isAdvancedEditing) return
|
if (isAdvancedEditing) return
|
||||||
const enumValue = options.enum.replace(' ', '').split(',')
|
emitPropertyOptionsChange({ description: currentFields.description, enum: enumValue })
|
||||||
emitPropertyOptionsChange({ description: fields.description, enum: enumValue })
|
}, [isAdvancedEditing, emitPropertyOptionsChange, currentFields])
|
||||||
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
blurWithActions.current = true
|
blurWithActions.current = true
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { type Field, Type } from '../../../types'
|
import { type Field, Type } from '../../../types'
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
import { RiArrowDropDownLine, RiArrowDropRightLine } from '@remixicon/react'
|
import { RiArrowDropDownLine, RiArrowDropRightLine } from '@remixicon/react'
|
||||||
@ -65,8 +65,8 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
setHoveringProperty(path)
|
setHoveringProperty(path)
|
||||||
}, { wait: 50 })
|
}, { wait: 50 })
|
||||||
|
|
||||||
const hasChildren = getHasChildren(schema)
|
const hasChildren = useMemo(() => getHasChildren(schema), [schema])
|
||||||
const type = getFieldType(schema)
|
const type = useMemo(() => getFieldType(schema), [schema])
|
||||||
const isHovering = hoveringProperty === path.join('.') && depth > 1
|
const isHovering = hoveringProperty === path.join('.') && depth > 1
|
||||||
|
|
||||||
const handleExpand = () => {
|
const handleExpand = () => {
|
||||||
@ -137,7 +137,10 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
schema.description ? 'h-[calc(100%-3rem)]' : 'h-[calc(100%-1.75rem)]',
|
schema.description ? 'h-[calc(100%-3rem)]' : 'h-[calc(100%-1.75rem)]',
|
||||||
indentLeft[depth + 1],
|
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>
|
</div>
|
||||||
|
|
||||||
{isExpanded && hasChildren && depth < JSON_SCHEMA_MAX_DEPTH && (
|
{isExpanded && hasChildren && depth < JSON_SCHEMA_MAX_DEPTH && (
|
||||||
|
@ -54,6 +54,7 @@ const translation = {
|
|||||||
regenerate: 'Regenerate',
|
regenerate: 'Regenerate',
|
||||||
submit: 'Submit',
|
submit: 'Submit',
|
||||||
skip: 'Skip',
|
skip: 'Skip',
|
||||||
|
format: 'Format',
|
||||||
},
|
},
|
||||||
errorMsg: {
|
errorMsg: {
|
||||||
fieldRequired: '{{field}} is required',
|
fieldRequired: '{{field}} is required',
|
||||||
|
@ -54,6 +54,7 @@ const translation = {
|
|||||||
regenerate: '重新生成',
|
regenerate: '重新生成',
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
skip: '跳过',
|
skip: '跳过',
|
||||||
|
format: '格式化',
|
||||||
},
|
},
|
||||||
errorMsg: {
|
errorMsg: {
|
||||||
fieldRequired: '{{field}} 为必填项',
|
fieldRequired: '{{field}} 为必填项',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user