Component Code Editor düzenlemesi
This commit is contained in:
parent
3eba44072c
commit
ffea9710e4
3 changed files with 161 additions and 11 deletions
|
|
@ -389,7 +389,7 @@
|
|||
{
|
||||
"key": "admin.developerkit.components.edit",
|
||||
"path": "/admin/developerkit/components/edit/:id",
|
||||
"componentPath": "@/views/developerKit/CodeLayout",
|
||||
"componentPath": "@/views/developerKit/ComponentCodeLayout",
|
||||
"routeType": "protected",
|
||||
"authority": [
|
||||
"App.DeveloperKit.Components"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import Editor from '@monaco-editor/react'
|
|||
import { ComponentDefinition } from '../../proxy/developerKit/componentInfo'
|
||||
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
|
||||
import { FaCheck, FaCode, FaSpinner, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa'
|
||||
import { toast } from '../ui'
|
||||
import Notification from '../ui/Notification/Notification'
|
||||
|
||||
interface CodeEditorProps {
|
||||
interface ComponentCodeEditorProps {
|
||||
code: string
|
||||
onChange: (code: string) => void
|
||||
onApplyCodeChanges: (code: string) => void
|
||||
|
|
@ -18,7 +20,7 @@ interface CodeEditorProps {
|
|||
onComponentSave: () => void
|
||||
}
|
||||
|
||||
export const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
export const ComponentCodeEditor: React.FC<ComponentCodeEditorProps> = ({
|
||||
code,
|
||||
onChange,
|
||||
onApplyCodeChanges,
|
||||
|
|
@ -47,12 +49,23 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||
const editorRef = useRef<any>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const cursorChangeTimeout = useRef<number | null>(null)
|
||||
// Refs to keep latest values accessible inside native DOM listeners
|
||||
const localCodeRef = useRef(localCode)
|
||||
const dropCallbacksRef = useRef({ onChange, onComponentAdded, onApplyCodeChanges })
|
||||
|
||||
useEffect(() => {
|
||||
setLocalCode(code)
|
||||
setHasChanges(false)
|
||||
}, [code])
|
||||
|
||||
useEffect(() => {
|
||||
localCodeRef.current = localCode
|
||||
}, [localCode])
|
||||
|
||||
useEffect(() => {
|
||||
dropCallbacksRef.current = { onChange, onComponentAdded, onApplyCodeChanges }
|
||||
}, [onChange, onComponentAdded, onApplyCodeChanges])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (cursorChangeTimeout.current) {
|
||||
|
|
@ -135,6 +148,44 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given 1-based lineNumber is inside a `return (...)` block.
|
||||
* Uses parenthesis depth counting to track entry and exit of the return expression.
|
||||
*/
|
||||
const isPositionInsideJSXReturn = (codeStr: string, lineNumber: number): boolean => {
|
||||
const lines = codeStr.split('\n')
|
||||
let insideReturn = false
|
||||
let parenDepth = 0
|
||||
|
||||
for (let i = 0; i < lineNumber && i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (!insideReturn) {
|
||||
if (/\breturn\s*\(/.test(line)) {
|
||||
insideReturn = true
|
||||
parenDepth = 0
|
||||
const returnIdx = line.search(/\breturn/)
|
||||
const fromReturn = line.slice(returnIdx)
|
||||
for (const ch of fromReturn) {
|
||||
if (ch === '(') parenDepth++
|
||||
else if (ch === ')') {
|
||||
parenDepth--
|
||||
if (parenDepth <= 0) { insideReturn = false; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const ch of line) {
|
||||
if (ch === '(') parenDepth++
|
||||
else if (ch === ')') {
|
||||
parenDepth--
|
||||
if (parenDepth <= 0) { insideReturn = false; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return insideReturn
|
||||
}
|
||||
|
||||
const handleEditorCursorChange = (_event: any) => {
|
||||
if (!editorRef.current) return
|
||||
const position = editorRef.current.getPosition()
|
||||
|
|
@ -173,6 +224,94 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||
})
|
||||
|
||||
editor.onDidChangeCursorPosition(handleEditorCursorChange)
|
||||
|
||||
// Intercept Monaco's native drop so it never inserts raw JSON text.
|
||||
// We handle the drop ourselves: insert JSX when inside return(), skip otherwise.
|
||||
const domNode = editor.getDomNode()
|
||||
if (domNode) {
|
||||
domNode.addEventListener(
|
||||
'drop',
|
||||
(nativeEvent: DragEvent) => {
|
||||
nativeEvent.preventDefault()
|
||||
nativeEvent.stopPropagation()
|
||||
nativeEvent.stopImmediatePropagation()
|
||||
|
||||
if (!nativeEvent.dataTransfer) return
|
||||
|
||||
// Read component definition from drag data
|
||||
let componentDefData: string | undefined
|
||||
for (const fmt of ['application/json', 'text/plain']) {
|
||||
const d = nativeEvent.dataTransfer.getData(fmt)
|
||||
if (d) { componentDefData = d; break }
|
||||
}
|
||||
if (!componentDefData) return
|
||||
|
||||
let componentDef: any
|
||||
try { componentDef = JSON.parse(componentDefData) } catch { return }
|
||||
if (!componentDef?.name) return
|
||||
|
||||
// Resolve the drop position inside Monaco
|
||||
const dropPos = editor.getTargetAtClientPoint(nativeEvent.clientX, nativeEvent.clientY)
|
||||
const position = dropPos?.position ?? editor.getPosition()
|
||||
if (!position) return
|
||||
|
||||
// Only allow drop inside JSX return(...) block
|
||||
if (!isPositionInsideJSXReturn(localCodeRef.current, position.lineNumber)) {
|
||||
toast.push(
|
||||
<Notification type="warning" duration={3000}>
|
||||
Bileşen yalnızca <strong>return(...)</strong> bloğu içine eklenebilir.
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const componentId = generateUniqueId()
|
||||
const adaptedProps = {
|
||||
...(componentDef.properties || []).reduce(
|
||||
(acc: any, prop: any) => {
|
||||
acc[prop.name] = {
|
||||
type: prop.type,
|
||||
value: prop.value,
|
||||
...(prop.options ? { options: prop.options } : {}),
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
),
|
||||
id: { type: 'string', value: componentId },
|
||||
}
|
||||
|
||||
const componentJSX = generateSingleComponentJSX(componentDef.name, adaptedProps)
|
||||
const componentDefWithId = {
|
||||
...componentDef,
|
||||
id: componentId,
|
||||
properties: [
|
||||
...(componentDef.properties || []).filter((p: any) => p.name !== 'id'),
|
||||
{ name: 'id', type: 'string', value: componentId, category: 'properties' },
|
||||
],
|
||||
}
|
||||
|
||||
const lines = localCodeRef.current.split('\n')
|
||||
const indent = (lines[position.lineNumber - 1] || '').match(/^(\s*)/)?.[1] ?? ' '
|
||||
const formattedJSX = `${indent}${componentJSX}`
|
||||
const newLines = [...lines]
|
||||
newLines.splice(position.lineNumber, 0, formattedJSX)
|
||||
const newCode = newLines.join('\n')
|
||||
|
||||
setLocalCode(newCode)
|
||||
const { onChange: onChg, onComponentAdded: onAdded, onApplyCodeChanges: onApply } =
|
||||
dropCallbacksRef.current
|
||||
onChg(newCode)
|
||||
if (onAdded) onAdded(componentDefWithId)
|
||||
setTimeout(() => onApply(newCode), 100)
|
||||
|
||||
setIsDragOver(false)
|
||||
setDropIndicator({ show: false, line: 0, column: 0 })
|
||||
},
|
||||
true, // capture phase — fires before Monaco's own listeners
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCodeChange = (value: string | undefined) => {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { FaThLarge } from 'react-icons/fa'
|
||||
import {
|
||||
parseReactCode,
|
||||
|
|
@ -15,7 +15,6 @@ import {
|
|||
import { ComponentLibrary } from '../../components/codeLayout/ComponentLibrary'
|
||||
import { Splitter } from '../../components/codeLayout/Splitter'
|
||||
import { PanelManager } from '../../components/codeLayout/PanelManager'
|
||||
import { CodeEditor } from '../../components/codeLayout/CodeEditor'
|
||||
import { ComponentDefinition, ComponentInfo, EditorState } from '../../proxy/developerKit/componentInfo'
|
||||
import PropertyPanel from '../../components/codeLayout/PropertyPanel'
|
||||
import ComponentSelector from '../../components/codeLayout/ComponentSelector'
|
||||
|
|
@ -24,6 +23,7 @@ import { useComponents } from '../../contexts/ComponentContext'
|
|||
import { toast } from '../../components/ui'
|
||||
import Notification from '../../components/ui/Notification/Notification'
|
||||
import { PanelState } from '../../components/codeLayout/data/componentDefinitions'
|
||||
import { ComponentCodeEditor } from '@/components/codeLayout/ComponentCodeEditor'
|
||||
|
||||
const INITIAL_CODE = `const Component = () => {
|
||||
return (
|
||||
|
|
@ -35,7 +35,7 @@ const INITIAL_CODE = `const Component = () => {
|
|||
export default Component
|
||||
`
|
||||
|
||||
function CodeLayout() {
|
||||
function ComponentCodeLayout() {
|
||||
const { id } = useParams()
|
||||
const { getComponent, updateComponent } = useComponents()
|
||||
const [showPanelManager, setShowPanelManager] = useState(false)
|
||||
|
|
@ -53,6 +53,7 @@ function CodeLayout() {
|
|||
const [code, setCode] = useState<string>(INITIAL_CODE)
|
||||
const [hasCodeChanges, setHasCodeChanges] = useState(false)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const parseDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [name, setName] = useState('')
|
||||
const [dependencies, setDependencies] = useState<string[]>([])
|
||||
const [isActive, setIsActive] = useState(true)
|
||||
|
|
@ -551,10 +552,20 @@ function CodeLayout() {
|
|||
}))
|
||||
}, [])
|
||||
|
||||
// Initialize parsing on mount
|
||||
// Auto-parse ComponentSelector whenever code changes (debounced for live refresh)
|
||||
useEffect(() => {
|
||||
parseAndUpdateComponents(INITIAL_CODE)
|
||||
}, [parseAndUpdateComponents])
|
||||
if (parseDebounceRef.current) {
|
||||
clearTimeout(parseDebounceRef.current)
|
||||
}
|
||||
parseDebounceRef.current = setTimeout(() => {
|
||||
parseAndUpdateComponents(code)
|
||||
}, 500)
|
||||
return () => {
|
||||
if (parseDebounceRef.current) {
|
||||
clearTimeout(parseDebounceRef.current)
|
||||
}
|
||||
}
|
||||
}, [code, parseAndUpdateComponents])
|
||||
|
||||
const handleDragStart = (_componentDef: ComponentDefinition, e: React.DragEvent) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -631,7 +642,7 @@ function CodeLayout() {
|
|||
|
||||
<div className="flex flex-col flex-1 min-h-0 h-full">
|
||||
<div className="flex-1 h-full min-h-0">
|
||||
<CodeEditor
|
||||
<ComponentCodeEditor
|
||||
code={code}
|
||||
onChange={handleCodeChange}
|
||||
onApplyCodeChanges={handleApplyCodeChanges}
|
||||
|
|
@ -705,4 +716,4 @@ function CodeLayout() {
|
|||
)
|
||||
}
|
||||
|
||||
export default CodeLayout
|
||||
export default ComponentCodeLayout
|
||||
Loading…
Reference in a new issue