Component Code Editor düzenlemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-05-12 16:42:20 +03:00
parent 3eba44072c
commit ffea9710e4
3 changed files with 161 additions and 11 deletions

View file

@ -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"

View file

@ -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) => {

View file

@ -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