sozsoft-platform/ui/src/views/developerKit/CodeLayout.tsx
2026-03-01 23:43:25 +03:00

708 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react'
import { FaThLarge } from 'react-icons/fa'
import {
parseReactCode,
updateComponentProp,
updateComponentProps,
generateHookCode,
generateHookVariableName,
removeHookFromCode,
generateUniqueId,
removeComponentAndHooksFromCode,
generateComponentJSX,
insertJSXAtPosition,
} from '../../utils/codeParser'
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'
import { useParams } from 'react-router-dom'
import { useComponents } from '../../contexts/ComponentContext'
import { toast } from '../../components/ui'
import Notification from '../../components/ui/Notification/Notification'
import { PanelState } from '../../components/codeLayout/data/componentDefinitions'
const INITIAL_CODE = `const Component = () => {
return (
<>
</>
);
};
export default Component
`
function CodeLayout() {
const { id } = useParams()
const { getComponent, updateComponent } = useComponents()
const [showPanelManager, setShowPanelManager] = useState(false)
const [panelState, setPanelState] = useState<PanelState>({
toolbox: true,
properties: true,
})
const [editorState, setEditorState] = useState<EditorState>({
code: INITIAL_CODE,
components: [],
selectedComponentId: null,
})
const isEditing = !!id
const [code, setCode] = useState<string>(INITIAL_CODE)
const [hasCodeChanges, setHasCodeChanges] = useState(false)
const [isLoaded, setIsLoaded] = useState(false)
const [name, setName] = useState('')
const [dependencies, setDependencies] = useState<string[]>([])
const [isActive, setIsActive] = useState(true)
const handleSave = async () => {
try {
const componentData = {
name: name.trim(),
dependencies: JSON.stringify(dependencies), // Serialize dependencies to JSON string
code: code.trim(),
isActive,
}
if (isEditing && id) {
updateComponent(id, componentData)
parseAndUpdateComponents(componentData.code)
}
} catch (error) {
console.error('Error saving component:', error)
alert('Failed to save component. Please try again.')
} finally {
setHasCodeChanges(false)
setIsLoaded(true)
toast.push(
<Notification type="success" duration={2000}>
"Bileşen başarıyla kaydedildi."
</Notification>,
{
placement: 'top-end',
},
)
}
}
// Load existing component data - sadece edit modunda
useEffect(() => {
if (isEditing && id && !isLoaded) {
const component = getComponent(id)
if (component) {
setName(component.name)
// setDescription(component.description || "");
// Parse dependencies from JSON string
try {
const deps = component.dependencies ? JSON.parse(component.dependencies) : []
setDependencies(Array.isArray(deps) ? deps : [])
} catch {
setDependencies([])
}
setCode(component.code) // Mevcut kodu yükle
// Parse components from the loaded code
parseAndUpdateComponents(component.code)
setIsActive(component.isActive)
setIsLoaded(true)
}
} else if (!isEditing && !isLoaded) {
// Yeni komponent için boş başla - TEMPLATE YOK
setIsLoaded(true)
}
}, [id, isEditing, getComponent, isLoaded])
// NEW: Handle drop to Code Editor instead of Canvas
const handleDropToCodeEditor = (
componentDef: ComponentDefinition,
position: { line: number; column: number },
): void => {
if (!componentDef || typeof componentDef !== 'object') {
console.error('Component definition is null or undefined')
return
}
if (!componentDef.name) {
console.error('Invalid component definition in handleDropToCodeEditor')
return
}
const propertiesObj: Record<string, any> = {}
if (Array.isArray(componentDef.properties)) {
componentDef.properties.forEach((prop) => {
propertiesObj[prop.name] = prop
})
}
const newComponent: ComponentInfo = {
id: generateUniqueId(),
type: componentDef.name, // type alanı componentDef.name olmalı
props: propertiesObj,
name: componentDef.name,
startLine: 0,
endLine: 0,
startColumn: 0,
endColumn: 0,
}
// Generate JSX string for this component
const componentJSX = generateComponentJSX(newComponent)
// Insert the JSX at the cursor position in the code editor
setCode(insertJSXAtPosition(code, componentJSX, position))
}
const handleAppDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
}
const parseAndUpdateComponents = useCallback((code: string) => {
try {
const parsed = parseReactCode(code)
setEditorState((prev) => ({
...prev,
code,
components: parsed.components,
}))
} catch (error) {
const msg = error instanceof Error ? error.message : 'Koddan komponentler parse edilemedi.'
console.log('Parse error:', msg)
setEditorState((prev) => ({
...prev,
code, // Only update the code, keep existing components
}))
}
}, [])
// Handle code changes from Monaco Editor
const handleCodeChange = useCallback(
(value: string | undefined) => {
if (value !== undefined) {
setCode(value)
setHasCodeChanges(value !== editorState.code)
}
},
[editorState.code],
)
// Apply code changes
const handleApplyCodeChanges = useCallback(() => {
parseAndUpdateComponents(code)
setHasCodeChanges(false)
}, [parseAndUpdateComponents, code])
// Reset code changes
const handleResetCodeChanges = useCallback(() => {
setCode(editorState.code)
setHasCodeChanges(false)
}, [editorState.code])
// Handle property changes from Property Panel
// Handle hook toggle
const handleHookToggle = useCallback(
(componentId: string, hookType: string, enabled: boolean) => {
console.log('🪝 App: handleHookToggle called:', {
componentId,
hookType,
enabled,
})
const selectedComponent = editorState.components?.find((c) => c.id === componentId)
if (!selectedComponent) return
// Use the most up-to-date code (pendingCode if available, otherwise editorState.code)
let updatedCode = code || editorState.code
console.log('🔍 App: Using code source:', code === editorState.code ? 'same' : 'pendingCode')
if (enabled) {
// Check if hook is already present - more specific check
const varName = generateHookVariableName(hookType, componentId)
console.log('🔍 App: Checking for existing hook variable:', varName)
// Create a more specific regex to check for actual hook declarations
const hookDeclarationRegex = new RegExp(
`const\\s+(?:\\[.*?${varName}.*?\\]|${varName})\\s*=\\s*${hookType}\\s*\\(`,
)
if (hookDeclarationRegex.test(updatedCode)) {
console.log('⚠️ App: Hook already exists, skipping')
return // Hook already exists
}
// Handle React imports - improved approach
const reactImportRegex = /^import\s+\{([^}]*)\}\s+from\s+['"]react['"]\s*;?/m
const reactMatch = reactImportRegex.exec(updatedCode)
if (reactMatch) {
// reactMatch[1] --> süslü parantez içi örn: "useState, useRef"
let existingHooks = reactMatch[1]
.split(',')
.map((h) => h.trim())
.filter((h) => h)
// Eğer hookType yoksa ekle
if (!existingHooks.includes(hookType)) {
existingHooks.push(hookType)
}
// Tekrarları kaldır (güvenlik için)
existingHooks = [...new Set(existingHooks)]
const newImport = `import { ${existingHooks.join(', ')} } from 'react';\n`
updatedCode = updatedCode.replace(reactImportRegex, newImport)
console.log('🔄 App: Updated existing React import:', newImport.trim())
} else {
// React import satırı yoksa ekle
const importLine = `import { ${hookType} } from 'react';\n\n`
updatedCode = importLine + updatedCode
console.log('🔄 App: Added new React import:', importLine.trim())
}
// Add hook declaration
const hookCode = generateHookCode(hookType, componentId, selectedComponent.type)
console.log('🔍 App: Generated hook code:', hookCode)
// Try multiple patterns for function declaration
const functionPatterns = [
/function\s+Component\s*\([^)]*\)\s*\{/, // function Component() {
/function\s+\w+\s*\([^)]*\)\s*\{/, // function AnyName() {
/const\s+\w+\s*=\s*\([^)]*\)\s*=>\s*\{/, // const Component = () => {
/export\s+default\s+function\s*\([^)]*\)\s*\{/, // export default function() {
]
let match2 = null
for (const pattern of functionPatterns) {
match2 = updatedCode.match(pattern)
if (match2) {
console.log('🔍 App: Found function with pattern:', pattern)
break
}
}
console.log('🔍 App: Function match result:', match2)
console.log('🔍 App: Updated code preview:', updatedCode.substring(0, 500))
if (match2 && typeof match2.index === 'number') {
const insertPosition = match2.index + match2[0].length
updatedCode =
updatedCode.slice(0, insertPosition) +
'\n ' +
hookCode +
'\n' +
updatedCode.slice(insertPosition)
console.log('✅ App: Hook code inserted successfully')
} else {
console.log('⚠️ App: Could not find function body to insert hook')
// Fallback: insert after the first opening brace
const firstBrace = updatedCode.indexOf('{')
if (firstBrace !== -1) {
updatedCode =
updatedCode.slice(0, firstBrace + 1) +
'\n ' +
hookCode +
'\n' +
updatedCode.slice(firstBrace + 1)
console.log('✅ App: Hook code inserted using fallback method')
}
}
// Update component properties if needed
if (hookType === 'useState') {
const setterName = `set${varName.charAt(0).toUpperCase() + varName.slice(1)}`
// Update component props based on type
if (selectedComponent.type === 'input') {
updatedCode = updateComponentProp(updatedCode, componentId, 'value', `{${varName}}`)
updatedCode = updateComponentProp(
updatedCode,
componentId,
'onChange',
`{(e) => ${setterName}(e.target.value)}`,
)
} else if (selectedComponent.type === 'button') {
updatedCode = updateComponentProp(
updatedCode,
componentId,
'onClick',
`{() => ${setterName}(!${varName})}`,
)
} else if (selectedComponent.type === 'checkbox') {
updatedCode = updateComponentProp(updatedCode, componentId, 'checked', `{${varName}}`)
updatedCode = updateComponentProp(
updatedCode,
componentId,
'onChange',
`{(val) => ${setterName}(val)}`,
)
}
} else if (hookType === 'useRef') {
updatedCode = updateComponentProp(updatedCode, componentId, 'ref', `{${varName}}`)
}
} else {
// Remove hook
updatedCode = removeHookFromCode(updatedCode, hookType, componentId)
// Remove related props
if (hookType === 'useState') {
if (selectedComponent.type === 'input') {
updatedCode = updateComponentProp(updatedCode, componentId, 'value', '')
updatedCode = updateComponentProp(updatedCode, componentId, 'onChange', null)
} else if (selectedComponent.type === 'button') {
updatedCode = updateComponentProp(updatedCode, componentId, 'onClick', null)
} else if (selectedComponent.type === 'checkbox') {
updatedCode = updateComponentProp(updatedCode, componentId, 'checked', false)
updatedCode = updateComponentProp(updatedCode, componentId, 'onChange', null)
}
} else if (hookType === 'useRef') {
updatedCode = updateComponentProp(updatedCode, componentId, 'ref', null)
}
}
setEditorState((prev) => {
console.log('🔄 App: Final updatedCode before parsing:', updatedCode)
const parsed = parseReactCode(updatedCode)
console.log('🔄 App: Parsed components:', parsed.components?.length)
const newState = {
code: updatedCode,
components: parsed.components,
selectedComponentId: prev.selectedComponentId, // Preserve selection
}
console.log('🔄 App: New editor state code preview:', newState.code.substring(0, 300))
return newState
})
// Also update pending code to match
setCode(updatedCode)
},
[editorState.code, editorState.components, code],
)
// Zincirleme hook güncelleme fonksiyonu
const applyMultipleHookToggles = (
toggles: { componentId: string; hookType: string; enabled: boolean }[],
) => {
let updatedCode = code || editorState.code
// 1. Sadece kaldırılması gereken hook'ları kaldır
toggles
.filter((t) => t.enabled === false)
.forEach(({ hookType, componentId }) => {
const selectedComponent = editorState.components?.find((c) => c.id === componentId)
if (!selectedComponent) return
updatedCode = removeHookFromCode(updatedCode, hookType, componentId)
// Prop temizliği
if (hookType === 'useState') {
if (selectedComponent.type === 'input') {
updatedCode = updateComponentProp(updatedCode, componentId, 'value', '')
updatedCode = updateComponentProp(updatedCode, componentId, 'onChange', null)
} else if (selectedComponent.type === 'button') {
updatedCode = updateComponentProp(updatedCode, componentId, 'onClick', null)
} else if (selectedComponent.type === 'checkbox') {
updatedCode = updateComponentProp(updatedCode, componentId, 'checked', false)
updatedCode = updateComponentProp(updatedCode, componentId, 'onChange', null)
}
} else if (hookType === 'useRef') {
updatedCode = updateComponentProp(updatedCode, componentId, 'ref', null)
}
})
// 2. Eklenmesi gereken hook'ları ekle (veya zaten varsa dokunma)
toggles
.filter((t) => t.enabled === true)
.forEach(({ hookType, componentId }) => {
const selectedComponent = editorState.components?.find((c) => c.id === componentId)
if (!selectedComponent) return
const varName = generateHookVariableName(hookType, componentId)
// Hook kodu fonksiyon gövdesinde yoksa ekle
const hookDeclarationRegex = new RegExp(
`const\\s+(?:\\[.*?${varName}.*?\\]|${varName})\\s*=\\s*${hookType}\\s*\\(`,
)
if (!hookDeclarationRegex.test(updatedCode)) {
const reactImportRegex =
// eslint-disable-next-line no-useless-escape
/import\\s+React(?:\\s*,\\s*\\{([^}]*)\\})?\\s+from\\s+['\"]react['\"];?/
const importMatch = updatedCode.match(reactImportRegex)
const allHooks = new Set<string>()
if (importMatch && importMatch[1]) {
importMatch[1]
.split(',')
.map((h) => h.trim())
.filter((h) => h)
.forEach((h) => allHooks.add(h))
}
allHooks.add(hookType)
let newImport = ''
if (allHooks.size > 0) {
newImport = `import { ${Array.from(allHooks).join(', ')} } from 'react';\n`
}
if (importMatch) {
updatedCode = updatedCode.replace(reactImportRegex, newImport)
} else {
updatedCode = newImport + updatedCode
}
// Hook kodunu fonksiyon gövdesine ekle
const hookCode = generateHookCode(hookType, componentId, selectedComponent.type)
const functionPatterns = [
/function\s+Component\s*\([^)]*\)\s*\{/, // function Component() {
/function\s+\w+\s*\([^)]*\)\s*\{/, // function AnyName() {
/const\s+\w+\s*=\s*\([^)]*\)\s*=>\s*\{/, // const Component = () => {
/export\s+default\s+function\s*\([^)]*\)\s*\{/, // export default function() {
]
let match2 = null
for (const pattern of functionPatterns) {
match2 = updatedCode.match(pattern)
if (match2) break
}
if (match2 && typeof match2.index === 'number') {
const insertPosition = match2.index + match2[0].length
updatedCode =
updatedCode.slice(0, insertPosition) +
'\n ' +
hookCode +
'\n' +
updatedCode.slice(insertPosition)
} else {
const firstBrace = updatedCode.indexOf('{')
if (firstBrace !== -1) {
updatedCode =
updatedCode.slice(0, firstBrace + 1) +
'\n ' +
hookCode +
'\n' +
updatedCode.slice(firstBrace + 1)
}
}
}
// Prop güncellemeleri
if (hookType === 'useState') {
const setterName = `set${varName.charAt(0).toUpperCase() + varName.slice(1)}`
if (selectedComponent.type === 'input') {
updatedCode = updateComponentProp(updatedCode, componentId, 'value', `{${varName}}`)
updatedCode = updateComponentProp(
updatedCode,
componentId,
'onChange',
`{(e) => ${setterName}(e.target.value)}`,
)
} else if (selectedComponent.type === 'button') {
updatedCode = updateComponentProp(
updatedCode,
componentId,
'onClick',
`{() => ${setterName}(!${varName})}`,
)
} else if (selectedComponent.type === 'checkbox') {
updatedCode = updateComponentProp(updatedCode, componentId, 'checked', `{${varName}}`)
updatedCode = updateComponentProp(
updatedCode,
componentId,
'onChange',
`{(e) => ${setterName}(e.target.checked)}`,
)
}
} else if (hookType === 'useRef') {
updatedCode = updateComponentProp(updatedCode, componentId, 'ref', `{${varName}}`)
}
})
setEditorState((prev) => {
const parsed = parseReactCode(updatedCode)
return {
code: updatedCode,
components: parsed.components,
selectedComponentId: prev.selectedComponentId,
}
})
setCode(updatedCode)
}
// Handle multiple property changes at once
const handlePropertiesChange = useCallback(
(componentId: string, updates: Record<string, any>) => {
console.log('🔄 App: handlePropertiesChange called:', {
componentId,
updates,
})
const updatedCode = updateComponentProps(editorState.code, componentId, updates)
console.log('📝 App: Properties updated, code changed:', editorState.code !== updatedCode)
setEditorState((prev) => {
const parsed = parseReactCode(updatedCode)
return {
code: updatedCode,
components: parsed.components,
selectedComponentId: prev.selectedComponentId, // Preserve selection
}
})
// Update pending code to reflect changes
setCode(updatedCode)
},
[editorState.code],
)
// Handle component list refresh
const handleRefreshComponents = useCallback(() => {
parseAndUpdateComponents(editorState.code)
}, [parseAndUpdateComponents, editorState.code])
// Handle component selection
const handleSelectComponent = useCallback((componentId: string | null) => {
setEditorState((prev) => ({
...prev,
selectedComponentId: componentId,
}))
}, [])
// Initialize parsing on mount
useEffect(() => {
parseAndUpdateComponents(INITIAL_CODE)
}, [parseAndUpdateComponents])
const handleDragStart = (_componentDef: ComponentDefinition, e: React.DragEvent) => {
e.stopPropagation()
}
const selectedComponent =
editorState.components?.find((c) => c.id === editorState.selectedComponentId) || null
const renderLeftPanel = () => {
if (!panelState.toolbox) return null
return <ComponentLibrary onDragStart={handleDragStart} />
}
// Komponent ve ilgili hook'ları koddan silen fonksiyon
const handleDeleteComponent = (componentId: string) => {
// Koddan JSX ve hook'ları sil
const updatedCode = removeComponentAndHooksFromCode(code, componentId)
// Koddan parse edip state'i güncelle
parseAndUpdateComponents(updatedCode)
setCode(updatedCode)
// Seçili komponenti kaldır
setEditorState((prev) => ({
...prev,
selectedComponentId: null,
}))
}
const renderRightPanel = () => {
if (!panelState.properties) return null
return (
<div className="flex flex-col flex-1 min-h-0 h-full w-full">
<ComponentSelector
components={editorState.components}
selectedComponentId={editorState.selectedComponentId}
onSelectComponent={handleSelectComponent}
onRefresh={handleRefreshComponents}
/>
<PropertyPanel
selectedComponent={selectedComponent}
currentCode={editorState.code}
onPropertiesChange={handlePropertiesChange}
onHookToggle={handleHookToggle}
onMultipleHookToggle={applyMultipleHookToggles}
onDeleteComponent={handleDeleteComponent}
/>
</div>
)
}
const mainContent = (
<div className="flex-1 flex flex-col min-h-0 h-full">
{/* Top Header */}
<div className="bg-white border-b border-gray-200 px-3 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<h1 className="text-lg font-semibold text-gray-900">{name}</h1>
<p className="text-xs text-gray-500">{dependencies.join(', ')}</p>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => setShowPanelManager(true)}
className="flex items-center space-x-2 px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
title="Panel Manager"
>
<FaThLarge className="w-4 h-4 text-gray-600" />
<span className="text-sm text-gray-600">Panels</span>
</button>
</div>
</div>
</div>
<div className="flex flex-col flex-1 min-h-0 h-full">
<div className="flex-1 h-full min-h-0">
<CodeEditor
code={code}
onChange={handleCodeChange}
onApplyCodeChanges={handleApplyCodeChanges}
onResetCodeChanges={handleResetCodeChanges}
onDrop={handleDropToCodeEditor}
onComponentSave={handleSave}
/>
</div>
</div>
</div>
)
return (
<div
className="flex h-screen bg-gray-50"
onDragOver={handleAppDragOver}
onDrop={(e) => {
e.preventDefault()
e.stopPropagation()
}}
>
{/* Panel Yöneticisi Modal */}
{showPanelManager && (
<PanelManager
isOpen={showPanelManager}
onClose={() => setShowPanelManager(false)}
panelState={panelState}
onPanelToggle={(panel) => setPanelState((prev) => ({ ...prev, [panel]: !prev[panel] }))}
/>
)}
{/* Sol Sidebar ve ana içerik */}
{panelState.toolbox ? (
<Splitter direction="horizontal" initialSize={288} minSize={200} maxSize={400}>
{renderLeftPanel()}
<div className="flex flex-1">
{panelState.properties ? (
<Splitter
direction="horizontal"
initialSize={400}
minSize={300}
maxSize={window.innerWidth - 288 - 100}
reverse={true}
>
{mainContent}
{renderRightPanel()}
</Splitter>
) : (
mainContent
)}
</div>
</Splitter>
) : (
<div className="flex flex-1">
{panelState.properties ? (
<Splitter
direction="horizontal"
initialSize={400}
minSize={300}
maxSize={window.innerWidth - 100}
reverse={true}
>
{mainContent}
{renderRightPanel()}
</Splitter>
) : (
mainContent
)}
</div>
)}
</div>
)
}
export default CodeLayout