diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json index 7baa8c6..8407c1a 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json @@ -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" diff --git a/ui/src/components/codeLayout/CodeEditor.tsx b/ui/src/components/codeLayout/ComponentCodeEditor.tsx similarity index 80% rename from ui/src/components/codeLayout/CodeEditor.tsx rename to ui/src/components/codeLayout/ComponentCodeEditor.tsx index 9d5d599..a4449a1 100644 --- a/ui/src/components/codeLayout/CodeEditor.tsx +++ b/ui/src/components/codeLayout/ComponentCodeEditor.tsx @@ -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 = ({ +export const ComponentCodeEditor: React.FC = ({ code, onChange, onApplyCodeChanges, @@ -47,12 +49,23 @@ export const CodeEditor: React.FC = ({ const editorRef = useRef(null) const containerRef = useRef(null) const cursorChangeTimeout = useRef(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 = ({ 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 = ({ }) 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( + + Bileşen yalnızca return(...) bloğu içine eklenebilir. + , + { 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, + ), + 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) => { diff --git a/ui/src/views/developerKit/CodeLayout.tsx b/ui/src/views/developerKit/ComponentCodeLayout.tsx similarity index 97% rename from ui/src/views/developerKit/CodeLayout.tsx rename to ui/src/views/developerKit/ComponentCodeLayout.tsx index 3008401..d64e3f5 100644 --- a/ui/src/views/developerKit/CodeLayout.tsx +++ b/ui/src/views/developerKit/ComponentCodeLayout.tsx @@ -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(INITIAL_CODE) const [hasCodeChanges, setHasCodeChanges] = useState(false) const [isLoaded, setIsLoaded] = useState(false) + const parseDebounceRef = useRef | null>(null) const [name, setName] = useState('') const [dependencies, setDependencies] = useState([]) 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() {
-