sozsoft-platform/ui/src/views/developerKit/CodeLayout.tsx

709 lines
25 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
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