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",
|
"key": "admin.developerkit.components.edit",
|
||||||
"path": "/admin/developerkit/components/edit/:id",
|
"path": "/admin/developerkit/components/edit/:id",
|
||||||
"componentPath": "@/views/developerKit/CodeLayout",
|
"componentPath": "@/views/developerKit/ComponentCodeLayout",
|
||||||
"routeType": "protected",
|
"routeType": "protected",
|
||||||
"authority": [
|
"authority": [
|
||||||
"App.DeveloperKit.Components"
|
"App.DeveloperKit.Components"
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ import Editor from '@monaco-editor/react'
|
||||||
import { ComponentDefinition } from '../../proxy/developerKit/componentInfo'
|
import { ComponentDefinition } from '../../proxy/developerKit/componentInfo'
|
||||||
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
|
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
|
||||||
import { FaCheck, FaCode, FaSpinner, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa'
|
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
|
code: string
|
||||||
onChange: (code: string) => void
|
onChange: (code: string) => void
|
||||||
onApplyCodeChanges: (code: string) => void
|
onApplyCodeChanges: (code: string) => void
|
||||||
|
|
@ -18,7 +20,7 @@ interface CodeEditorProps {
|
||||||
onComponentSave: () => void
|
onComponentSave: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeEditor: React.FC<CodeEditorProps> = ({
|
export const ComponentCodeEditor: React.FC<ComponentCodeEditorProps> = ({
|
||||||
code,
|
code,
|
||||||
onChange,
|
onChange,
|
||||||
onApplyCodeChanges,
|
onApplyCodeChanges,
|
||||||
|
|
@ -47,12 +49,23 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
const editorRef = useRef<any>(null)
|
const editorRef = useRef<any>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const cursorChangeTimeout = useRef<number | null>(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(() => {
|
useEffect(() => {
|
||||||
setLocalCode(code)
|
setLocalCode(code)
|
||||||
setHasChanges(false)
|
setHasChanges(false)
|
||||||
}, [code])
|
}, [code])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localCodeRef.current = localCode
|
||||||
|
}, [localCode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dropCallbacksRef.current = { onChange, onComponentAdded, onApplyCodeChanges }
|
||||||
|
}, [onChange, onComponentAdded, onApplyCodeChanges])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (cursorChangeTimeout.current) {
|
if (cursorChangeTimeout.current) {
|
||||||
|
|
@ -135,6 +148,44 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
return null
|
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) => {
|
const handleEditorCursorChange = (_event: any) => {
|
||||||
if (!editorRef.current) return
|
if (!editorRef.current) return
|
||||||
const position = editorRef.current.getPosition()
|
const position = editorRef.current.getPosition()
|
||||||
|
|
@ -173,6 +224,94 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.onDidChangeCursorPosition(handleEditorCursorChange)
|
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) => {
|
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 { FaThLarge } from 'react-icons/fa'
|
||||||
import {
|
import {
|
||||||
parseReactCode,
|
parseReactCode,
|
||||||
|
|
@ -15,7 +15,6 @@ import {
|
||||||
import { ComponentLibrary } from '../../components/codeLayout/ComponentLibrary'
|
import { ComponentLibrary } from '../../components/codeLayout/ComponentLibrary'
|
||||||
import { Splitter } from '../../components/codeLayout/Splitter'
|
import { Splitter } from '../../components/codeLayout/Splitter'
|
||||||
import { PanelManager } from '../../components/codeLayout/PanelManager'
|
import { PanelManager } from '../../components/codeLayout/PanelManager'
|
||||||
import { CodeEditor } from '../../components/codeLayout/CodeEditor'
|
|
||||||
import { ComponentDefinition, ComponentInfo, EditorState } from '../../proxy/developerKit/componentInfo'
|
import { ComponentDefinition, ComponentInfo, EditorState } from '../../proxy/developerKit/componentInfo'
|
||||||
import PropertyPanel from '../../components/codeLayout/PropertyPanel'
|
import PropertyPanel from '../../components/codeLayout/PropertyPanel'
|
||||||
import ComponentSelector from '../../components/codeLayout/ComponentSelector'
|
import ComponentSelector from '../../components/codeLayout/ComponentSelector'
|
||||||
|
|
@ -24,6 +23,7 @@ import { useComponents } from '../../contexts/ComponentContext'
|
||||||
import { toast } from '../../components/ui'
|
import { toast } from '../../components/ui'
|
||||||
import Notification from '../../components/ui/Notification/Notification'
|
import Notification from '../../components/ui/Notification/Notification'
|
||||||
import { PanelState } from '../../components/codeLayout/data/componentDefinitions'
|
import { PanelState } from '../../components/codeLayout/data/componentDefinitions'
|
||||||
|
import { ComponentCodeEditor } from '@/components/codeLayout/ComponentCodeEditor'
|
||||||
|
|
||||||
const INITIAL_CODE = `const Component = () => {
|
const INITIAL_CODE = `const Component = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -35,7 +35,7 @@ const INITIAL_CODE = `const Component = () => {
|
||||||
export default Component
|
export default Component
|
||||||
`
|
`
|
||||||
|
|
||||||
function CodeLayout() {
|
function ComponentCodeLayout() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { getComponent, updateComponent } = useComponents()
|
const { getComponent, updateComponent } = useComponents()
|
||||||
const [showPanelManager, setShowPanelManager] = useState(false)
|
const [showPanelManager, setShowPanelManager] = useState(false)
|
||||||
|
|
@ -53,6 +53,7 @@ function CodeLayout() {
|
||||||
const [code, setCode] = useState<string>(INITIAL_CODE)
|
const [code, setCode] = useState<string>(INITIAL_CODE)
|
||||||
const [hasCodeChanges, setHasCodeChanges] = useState(false)
|
const [hasCodeChanges, setHasCodeChanges] = useState(false)
|
||||||
const [isLoaded, setIsLoaded] = useState(false)
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
const parseDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [dependencies, setDependencies] = useState<string[]>([])
|
const [dependencies, setDependencies] = useState<string[]>([])
|
||||||
const [isActive, setIsActive] = useState(true)
|
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(() => {
|
useEffect(() => {
|
||||||
parseAndUpdateComponents(INITIAL_CODE)
|
if (parseDebounceRef.current) {
|
||||||
}, [parseAndUpdateComponents])
|
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) => {
|
const handleDragStart = (_componentDef: ComponentDefinition, e: React.DragEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
@ -631,7 +642,7 @@ function CodeLayout() {
|
||||||
|
|
||||||
<div className="flex flex-col flex-1 min-h-0 h-full">
|
<div className="flex flex-col flex-1 min-h-0 h-full">
|
||||||
<div className="flex-1 h-full min-h-0">
|
<div className="flex-1 h-full min-h-0">
|
||||||
<CodeEditor
|
<ComponentCodeEditor
|
||||||
code={code}
|
code={code}
|
||||||
onChange={handleCodeChange}
|
onChange={handleCodeChange}
|
||||||
onApplyCodeChanges={handleApplyCodeChanges}
|
onApplyCodeChanges={handleApplyCodeChanges}
|
||||||
|
|
@ -705,4 +716,4 @@ function CodeLayout() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CodeLayout
|
export default ComponentCodeLayout
|
||||||
Loading…
Reference in a new issue