From 64ccc150df898e7c5a56cc16f40e5915155ca60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Mon, 11 Aug 2025 13:48:36 +0300 Subject: [PATCH] =?UTF-8?q?CustomComponents=20hatalar=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/deployment/docker-compose-app.yml | 4 +- configs/deployment/docker-compose-data.yml | 5 +- .../componentEditor/ComponentPreview.tsx | 9 +- ui/src/components/developerKit/CodeLayout.tsx | 703 ++++++-------- .../developerKit/ComponentEditor.tsx | 29 +- .../developerKit/ComponentManager.tsx | 157 ++-- ui/src/contexts/ComponentContext.tsx | 151 +-- ui/src/contexts/ComponentRegistryContext.tsx | 863 +++++++++++------- 8 files changed, 1000 insertions(+), 921 deletions(-) diff --git a/configs/deployment/docker-compose-app.yml b/configs/deployment/docker-compose-app.yml index 45d756f9..f1a8b1b2 100644 --- a/configs/deployment/docker-compose-app.yml +++ b/configs/deployment/docker-compose-app.yml @@ -18,7 +18,7 @@ services: - ASPNETCORE_ENVIRONMENT=Dev - SEED=${SEED} networks: - - kurs-platform-data_db + - db # Backend API api: @@ -33,7 +33,7 @@ services: - cdn:/etc/api/cdn - api-keys:/root/.aspnet/DataProtection-Keys networks: - - kurs-platform-data_db + - db - default # Frontend (UI) diff --git a/configs/deployment/docker-compose-data.yml b/configs/deployment/docker-compose-data.yml index f2362a86..70a61323 100644 --- a/configs/deployment/docker-compose-data.yml +++ b/configs/deployment/docker-compose-data.yml @@ -3,7 +3,8 @@ name: kurs-platform-data networks: db: - external: false + external: true + name: kurs-platform-data_db volumes: pg: @@ -46,4 +47,4 @@ services: volumes: - mssql:/var/opt/mssql networks: - - db \ No newline at end of file + - db diff --git a/ui/src/components/componentEditor/ComponentPreview.tsx b/ui/src/components/componentEditor/ComponentPreview.tsx index 93960e3a..e3f2ada2 100644 --- a/ui/src/components/componentEditor/ComponentPreview.tsx +++ b/ui/src/components/componentEditor/ComponentPreview.tsx @@ -8,16 +8,21 @@ export interface ComponentPreviewProps { } const ComponentPreview: React.FC = ({ componentName, className = '' }) => { - const { components } = useComponents() + const { components, loading } = useComponents() if (!componentName) { return
Bileşen ismi yok.
} + // components dizisinin varlığını kontrol et + if (loading || !components || !Array.isArray(components)) { + return
Bileşenler yükleniyor...
+ } + // Belirtilen bileşeni bul const component = components.find((c) => c.name === componentName && c.isActive) - let dependencies: string[] = []; + let dependencies: string[] = [] if (component?.dependencies) { try { diff --git a/ui/src/components/developerKit/CodeLayout.tsx b/ui/src/components/developerKit/CodeLayout.tsx index 9f06587b..c2ba35a0 100644 --- a/ui/src/components/developerKit/CodeLayout.tsx +++ b/ui/src/components/developerKit/CodeLayout.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useCallback } from "react"; -import { PanelTop as Panels } from "lucide-react"; +import { useState, useEffect, useCallback } from 'react' +import { PanelTop as Panels } from 'lucide-react' import { parseReactCode, updateComponentProp, @@ -11,23 +11,19 @@ import { 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 "../../@types/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"; +} 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 '../../@types/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 ( @@ -37,29 +33,29 @@ const INITIAL_CODE = `const Component = () => { }; export default Component -`; +` function CodeLayout() { - const { id } = useParams(); - const { getComponent, updateComponent } = useComponents(); - const [showPanelManager, setShowPanelManager] = useState(false); + const { id } = useParams() + const { getComponent, updateComponent } = useComponents() + const [showPanelManager, setShowPanelManager] = useState(false) const [panelState, setPanelState] = useState({ toolbox: true, properties: true, - }); + }) const [editorState, setEditorState] = useState({ code: INITIAL_CODE, components: [], selectedComponentId: null, - }); + }) - const isEditing = !!id; - const [code, setCode] = useState(INITIAL_CODE); - const [hasCodeChanges, setHasCodeChanges] = useState(false); - const [isLoaded, setIsLoaded] = useState(false); - const [name, setName] = useState(""); - const [dependencies, setDependencies] = useState([]); - const [isActive, setIsActive] = useState(true); + const isEditing = !!id + const [code, setCode] = useState(INITIAL_CODE) + const [hasCodeChanges, setHasCodeChanges] = useState(false) + const [isLoaded, setIsLoaded] = useState(false) + const [name, setName] = useState('') + const [dependencies, setDependencies] = useState([]) + const [isActive, setIsActive] = useState(true) const handleSave = async () => { try { @@ -68,77 +64,75 @@ function CodeLayout() { dependencies: JSON.stringify(dependencies), // Serialize dependencies to JSON string code: code.trim(), isActive, - }; + } if (isEditing && id) { - updateComponent(id, componentData); - parseAndUpdateComponents(componentData.code); + updateComponent(id, componentData) + parseAndUpdateComponents(componentData.code) } } catch (error) { - console.error("Error saving component:", error); - alert("Failed to save component. Please try again."); + console.error('Error saving component:', error) + alert('Failed to save component. Please try again.') } finally { - setHasCodeChanges(false); - setIsLoaded(true); + setHasCodeChanges(false) + setIsLoaded(true) toast.push( "Bileşen başarıyla kaydedildi." , { - placement: "top-center", - } - ); + placement: 'top-center', + }, + ) } - }; + } // Load existing component data - sadece edit modunda useEffect(() => { if (isEditing && id && !isLoaded) { - const component = getComponent(id); + const component = getComponent(id) if (component) { - setName(component.name); + 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 : []); + const deps = component.dependencies ? JSON.parse(component.dependencies) : [] + setDependencies(Array.isArray(deps) ? deps : []) } catch { - setDependencies([]); + setDependencies([]) } - setCode(component.code); // Mevcut kodu yükle + setCode(component.code) // Mevcut kodu yükle // Parse components from the loaded code - parseAndUpdateComponents(component.code); - setIsActive(component.isActive); - setIsLoaded(true); + parseAndUpdateComponents(component.code) + setIsActive(component.isActive) + setIsLoaded(true) } } else if (!isEditing && !isLoaded) { // Yeni komponent için boş başla - TEMPLATE YOK - setIsLoaded(true); + setIsLoaded(true) } - }, [id, isEditing, getComponent, isLoaded]); + }, [id, isEditing, getComponent, isLoaded]) // NEW: Handle drop to Code Editor instead of Canvas const handleDropToCodeEditor = ( componentDef: ComponentDefinition, - position: { line: number; column: number } + position: { line: number; column: number }, ): void => { - if (!componentDef || typeof componentDef !== "object") { - console.error("Component definition is null or undefined"); - return; + 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; + console.error('Invalid component definition in handleDropToCodeEditor') + return } - const propertiesObj: Record = {}; + const propertiesObj: Record = {} if (Array.isArray(componentDef.properties)) { componentDef.properties.forEach((prop) => { - propertiesObj[prop.name] = prop; - }); + propertiesObj[prop.name] = prop + }) } const newComponent: ComponentInfo = { id: generateUniqueId(), @@ -149,141 +143,123 @@ function CodeLayout() { endLine: 0, startColumn: 0, endColumn: 0, - }; + } // Generate JSX string for this component - const componentJSX = generateComponentJSX(newComponent); + const componentJSX = generateComponentJSX(newComponent) // Insert the JSX at the cursor position in the code editor - setCode(insertJSXAtPosition(code, componentJSX, position)); - }; + setCode(insertJSXAtPosition(code, componentJSX, position)) + } const handleAppDragOver = (e: React.DragEvent) => { - e.preventDefault(); - }; + e.preventDefault() + } const parseAndUpdateComponents = useCallback((code: string) => { try { - const parsed = parseReactCode(code); + 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); + 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); + setCode(value) + setHasCodeChanges(value !== editorState.code) } }, - [editorState.code] - ); + [editorState.code], + ) // Apply code changes const handleApplyCodeChanges = useCallback(() => { - parseAndUpdateComponents(code); - setHasCodeChanges(false); - }, [parseAndUpdateComponents, code]); + parseAndUpdateComponents(code) + setHasCodeChanges(false) + }, [parseAndUpdateComponents, code]) // Reset code changes const handleResetCodeChanges = useCallback(() => { - setCode(editorState.code); - setHasCodeChanges(false); - }, [editorState.code]); + 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:", { + console.log('🪝 App: handleHookToggle called:', { componentId, hookType, enabled, - }); - const selectedComponent = editorState.components.find( - (c) => c.id === componentId - ); - if (!selectedComponent) return; + }) + 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" - ); + 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); + 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*\\(` - ); + `const\\s+(?:\\[.*?${varName}.*?\\]|${varName})\\s*=\\s*${hookType}\\s*\\(`, + ) if (hookDeclarationRegex.test(updatedCode)) { - console.log("⚠️ App: Hook already exists, skipping"); - return; // Hook already exists + 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); + 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(",") + .split(',') .map((h) => h.trim()) - .filter((h) => h); + .filter((h) => h) // Eğer hookType yoksa ekle if (!existingHooks.includes(hookType)) { - existingHooks.push(hookType); + existingHooks.push(hookType) } // Tekrarları kaldır (güvenlik için) - existingHooks = [...new Set(existingHooks)]; + existingHooks = [...new Set(existingHooks)] - const newImport = `import { ${existingHooks.join( - ", " - )} } from 'react';\n`; - updatedCode = updatedCode.replace(reactImportRegex, newImport); + const newImport = `import { ${existingHooks.join(', ')} } from 'react';\n` + updatedCode = updatedCode.replace(reactImportRegex, newImport) - console.log( - "🔄 App: Updated existing React import:", - newImport.trim() - ); + 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()); + 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); + const hookCode = generateHookCode(hookType, componentId, selectedComponent.type) + console.log('🔍 App: Generated hook code:', hookCode) // Try multiple patterns for function declaration const functionPatterns = [ @@ -291,445 +267,323 @@ function CodeLayout() { /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; + let match2 = null for (const pattern of functionPatterns) { - match2 = updatedCode.match(pattern); + match2 = updatedCode.match(pattern) if (match2) { - console.log("🔍 App: Found function with pattern:", pattern); - break; + 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) - ); + 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; + if (match2 && typeof match2.index === 'number') { + const insertPosition = match2.index + match2[0].length updatedCode = updatedCode.slice(0, insertPosition) + - "\n " + + '\n ' + hookCode + - "\n" + - updatedCode.slice(insertPosition); - console.log("✅ App: Hook code inserted successfully"); + '\n' + + updatedCode.slice(insertPosition) + console.log('✅ App: Hook code inserted successfully') } else { - console.log("⚠️ App: Could not find function body to insert hook"); + console.log('⚠️ App: Could not find function body to insert hook') // Fallback: insert after the first opening brace - const firstBrace = updatedCode.indexOf("{"); + const firstBrace = updatedCode.indexOf('{') if (firstBrace !== -1) { updatedCode = updatedCode.slice(0, firstBrace + 1) + - "\n " + + '\n ' + hookCode + - "\n" + - updatedCode.slice(firstBrace + 1); - console.log("✅ App: Hook code inserted using fallback method"); + '\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) - }`; + if (hookType === 'useState') { + const setterName = `set${varName.charAt(0).toUpperCase() + varName.slice(1)}` // Update component props based on type - if (selectedComponent.type === "input") { + if (selectedComponent.type === 'input') { + updatedCode = updateComponentProp(updatedCode, componentId, 'value', `{${varName}}`) updatedCode = updateComponentProp( updatedCode, componentId, - "value", - `{${varName}}` - ); + 'onChange', + `{(e) => ${setterName}(e.target.value)}`, + ) + } else if (selectedComponent.type === 'button') { updatedCode = updateComponentProp( updatedCode, componentId, - "onChange", - `{(e) => ${setterName}(e.target.value)}` - ); - } else if (selectedComponent.type === "button") { + 'onClick', + `{() => ${setterName}(!${varName})}`, + ) + } else if (selectedComponent.type === 'checkbox') { + updatedCode = updateComponentProp(updatedCode, componentId, 'checked', `{${varName}}`) 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)}` - ); + 'onChange', + `{(val) => ${setterName}(val)}`, + ) } - } else if (hookType === "useRef") { - updatedCode = updateComponentProp( - updatedCode, - componentId, - "ref", - `{${varName}}` - ); + } else if (hookType === 'useRef') { + updatedCode = updateComponentProp(updatedCode, componentId, 'ref', `{${varName}}`) } } else { // Remove hook - updatedCode = removeHookFromCode(updatedCode, hookType, componentId); + 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 - ); + 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 - ); + } 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); + 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; - }); + } + console.log('🔄 App: New editor state code preview:', newState.code.substring(0, 300)) + return newState + }) // Also update pending code to match - setCode(updatedCode); + setCode(updatedCode) }, - [editorState.code, editorState.components, code] - ); + [editorState.code, editorState.components, code], + ) // Zincirleme hook güncelleme fonksiyonu const applyMultipleHookToggles = ( - toggles: { componentId: string; hookType: string; enabled: boolean }[] + toggles: { componentId: string; hookType: string; enabled: boolean }[], ) => { - let updatedCode = code || editorState.code; + 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); + 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 - ); + 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 - ); + } 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); + 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*\\(` - ); + `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(); + /import\\s+React(?:\\s*,\\s*\\{([^}]*)\\})?\\s+from\\s+['\"]react['\"];?/ + const importMatch = updatedCode.match(reactImportRegex) + const allHooks = new Set() if (importMatch && importMatch[1]) { importMatch[1] - .split(",") + .split(',') .map((h) => h.trim()) .filter((h) => h) - .forEach((h) => allHooks.add(h)); + .forEach((h) => allHooks.add(h)) } - allHooks.add(hookType); - let newImport = ""; + allHooks.add(hookType) + let newImport = '' if (allHooks.size > 0) { - newImport = `import { ${Array.from(allHooks).join( - ", " - )} } from 'react';\n`; + newImport = `import { ${Array.from(allHooks).join(', ')} } from 'react';\n` } if (importMatch) { - updatedCode = updatedCode.replace(reactImportRegex, newImport); + updatedCode = updatedCode.replace(reactImportRegex, newImport) } else { - updatedCode = newImport + updatedCode; + updatedCode = newImport + updatedCode } // Hook kodunu fonksiyon gövdesine ekle - const hookCode = generateHookCode( - hookType, - componentId, - selectedComponent.type - ); + 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; + ] + let match2 = null for (const pattern of functionPatterns) { - match2 = updatedCode.match(pattern); - if (match2) break; + match2 = updatedCode.match(pattern) + if (match2) break } - if (match2 && typeof match2.index === "number") { - const insertPosition = match2.index + match2[0].length; + if (match2 && typeof match2.index === 'number') { + const insertPosition = match2.index + match2[0].length updatedCode = updatedCode.slice(0, insertPosition) + - "\n " + + '\n ' + hookCode + - "\n" + - updatedCode.slice(insertPosition); + '\n' + + updatedCode.slice(insertPosition) } else { - const firstBrace = updatedCode.indexOf("{"); + const firstBrace = updatedCode.indexOf('{') if (firstBrace !== -1) { updatedCode = updatedCode.slice(0, firstBrace + 1) + - "\n " + + '\n ' + hookCode + - "\n" + - updatedCode.slice(firstBrace + 1); + '\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") { + 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, - "value", - `{${varName}}` - ); + 'onChange', + `{(e) => ${setterName}(e.target.value)}`, + ) + } else if (selectedComponent.type === 'button') { updatedCode = updateComponentProp( updatedCode, componentId, - "onChange", - `{(e) => ${setterName}(e.target.value)}` - ); - } else if (selectedComponent.type === "button") { + 'onClick', + `{() => ${setterName}(!${varName})}`, + ) + } else if (selectedComponent.type === 'checkbox') { + updatedCode = updateComponentProp(updatedCode, componentId, 'checked', `{${varName}}`) 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)}` - ); + 'onChange', + `{(e) => ${setterName}(e.target.checked)}`, + ) } - } else if (hookType === "useRef") { - updatedCode = updateComponentProp( - updatedCode, - componentId, - "ref", - `{${varName}}` - ); + } else if (hookType === 'useRef') { + updatedCode = updateComponentProp(updatedCode, componentId, 'ref', `{${varName}}`) } - }); + }) setEditorState((prev) => { - const parsed = parseReactCode(updatedCode); + const parsed = parseReactCode(updatedCode) return { code: updatedCode, components: parsed.components, selectedComponentId: prev.selectedComponentId, - }; - }); - setCode(updatedCode); - }; + } + }) + setCode(updatedCode) + } // Handle multiple property changes at once const handlePropertiesChange = useCallback( (componentId: string, updates: Record) => { - console.log("🔄 App: handlePropertiesChange called:", { + console.log('🔄 App: handlePropertiesChange called:', { componentId, updates, - }); + }) - const updatedCode = updateComponentProps( - editorState.code, - componentId, - updates - ); - console.log( - "📝 App: Properties updated, code changed:", - editorState.code !== updatedCode - ); + const updatedCode = updateComponentProps(editorState.code, componentId, updates) + console.log('📝 App: Properties updated, code changed:', editorState.code !== updatedCode) setEditorState((prev) => { - const parsed = parseReactCode(updatedCode); + const parsed = parseReactCode(updatedCode) return { code: updatedCode, components: parsed.components, selectedComponentId: prev.selectedComponentId, // Preserve selection - }; - }); + } + }) // Update pending code to reflect changes - setCode(updatedCode); + setCode(updatedCode) }, - [editorState.code] - ); + [editorState.code], + ) // Handle component list refresh const handleRefreshComponents = useCallback(() => { - parseAndUpdateComponents(editorState.code); - }, [parseAndUpdateComponents, editorState.code]); + 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]); + parseAndUpdateComponents(INITIAL_CODE) + }, [parseAndUpdateComponents]) - const handleDragStart = ( - _componentDef: ComponentDefinition, - e: React.DragEvent - ) => { - e.stopPropagation(); - }; + const handleDragStart = (_componentDef: ComponentDefinition, e: React.DragEvent) => { + e.stopPropagation() + } const selectedComponent = - editorState.components.find( - (c) => c.id === editorState.selectedComponentId - ) || null; + editorState.components?.find((c) => c.id === editorState.selectedComponentId) || null const renderLeftPanel = () => { - if (!panelState.toolbox) return null; - return ; - }; + if (!panelState.toolbox) return null + return + } // Komponent ve ilgili hook'ları koddan silen fonksiyon const handleDeleteComponent = (componentId: string) => { // Koddan JSX ve hook'ları sil - const updatedCode = removeComponentAndHooksFromCode(code, componentId); + const updatedCode = removeComponentAndHooksFromCode(code, componentId) // Koddan parse edip state'i güncelle - parseAndUpdateComponents(updatedCode); - setCode(updatedCode); + parseAndUpdateComponents(updatedCode) + setCode(updatedCode) // Seçili komponenti kaldır setEditorState((prev) => ({ ...prev, selectedComponentId: null, - })); - }; + })) + } const renderRightPanel = () => { - if (!panelState.properties) return null; + if (!panelState.properties) return null return (
@@ -749,8 +603,8 @@ function CodeLayout() { onDeleteComponent={handleDeleteComponent} />
- ); - }; + ) + } const mainContent = (
@@ -759,7 +613,7 @@ function CodeLayout() {

{name}

-

{dependencies.join(", ")}

+

{dependencies.join(', ')}

@@ -788,15 +642,15 @@ function CodeLayout() {
- ); + ) return (
{ - e.preventDefault(); - e.stopPropagation(); + e.preventDefault() + e.stopPropagation() }} > {/* Panel Yöneticisi Modal */} @@ -805,19 +659,12 @@ function CodeLayout() { isOpen={showPanelManager} onClose={() => setShowPanelManager(false)} panelState={panelState} - onPanelToggle={(panel) => - setPanelState((prev) => ({ ...prev, [panel]: !prev[panel] })) - } + onPanelToggle={(panel) => setPanelState((prev) => ({ ...prev, [panel]: !prev[panel] }))} /> )} {/* Sol Sidebar ve ana içerik */} {panelState.toolbox ? ( - + {renderLeftPanel()}
{panelState.properties ? ( @@ -855,7 +702,7 @@ function CodeLayout() {
)}
- ); + ) } -export default CodeLayout; +export default CodeLayout diff --git a/ui/src/components/developerKit/ComponentEditor.tsx b/ui/src/components/developerKit/ComponentEditor.tsx index f14bc2c6..c9ac563d 100644 --- a/ui/src/components/developerKit/ComponentEditor.tsx +++ b/ui/src/components/developerKit/ComponentEditor.tsx @@ -19,7 +19,7 @@ const ComponentEditor: React.FC = () => { const { id } = useParams() const navigate = useNavigate() const { translate } = useLocalization() - + const { getComponent, addComponent, updateComponent } = useComponents() const [name, setName] = useState('') @@ -59,10 +59,10 @@ const ComponentEditor: React.FC = () => { }, []) useEffect(() => { - if (code && editorState.components.length === 0) { + if (code && editorState.components?.length === 0) { parseAndUpdateComponents(code) } - }, [code, editorState.components.length]) + }, [code, editorState.components?.length]) // Load existing component data - sadece edit modunda useEffect(() => { @@ -144,7 +144,9 @@ const ComponentEditor: React.FC = () => {
-

{translate('::App.DeveloperKit.ComponentEditor.Loading')}

+

+ {translate('::App.DeveloperKit.ComponentEditor.Loading')} +

) @@ -220,7 +222,9 @@ const ComponentEditor: React.FC = () => { className="flex items-center gap-2 bg-yellow-600 text-white px-4 py-2 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" > - {isSaving ? translate('::App.DeveloperKit.ComponentEditor.Saving') : translate('::App.DeveloperKit.ComponentEditor.Save')} + {isSaving + ? translate('::App.DeveloperKit.ComponentEditor.Saving') + : translate('::App.DeveloperKit.ComponentEditor.Save')} @@ -237,19 +241,26 @@ const ComponentEditor: React.FC = () => {

- {validationErrors.length} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')} - {validationErrors.length !== 1 ? 's' : ''} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')} + {validationErrors.length}{' '} + {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')} + {validationErrors.length !== 1 ? 's' : ''}{' '} + {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')}

{validationErrors.slice(0, 5).map((error, index) => (
- {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Line')} {error.startLineNumber}:{' '} + + {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Line')}{' '} + {error.startLineNumber}: + {' '} {error.message}
))} {validationErrors.length > 5 && (
- ... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')} {validationErrors.length - 5} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')} + ... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')}{' '} + {validationErrors.length - 5}{' '} + {translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')} {validationErrors.length - 5 !== 1 ? 's' : ''}
)} diff --git a/ui/src/components/developerKit/ComponentManager.tsx b/ui/src/components/developerKit/ComponentManager.tsx index cbd12878..103ecae1 100644 --- a/ui/src/components/developerKit/ComponentManager.tsx +++ b/ui/src/components/developerKit/ComponentManager.tsx @@ -1,6 +1,6 @@ -import React, { useState } from "react"; -import { Link } from "react-router-dom"; -import { useComponents } from "../../contexts/ComponentContext"; +import React, { useState } from 'react' +import { Link } from 'react-router-dom' +import { useComponents } from '../../contexts/ComponentContext' import { Plus, Search, @@ -14,21 +14,19 @@ import { CheckCircle, XCircle, View, -} from "lucide-react"; -import { ROUTES_ENUM } from "@/routes/route.constant"; -import { useLocalization } from "@/utils/hooks/useLocalization"; +} from 'lucide-react' +import { ROUTES_ENUM } from '@/routes/route.constant' +import { useLocalization } from '@/utils/hooks/useLocalization' const ComponentManager: React.FC = () => { - const { components, updateComponent, deleteComponent } = useComponents(); - const [searchTerm, setSearchTerm] = useState(""); - const [filterActive, setFilterActive] = useState< - "all" | "active" | "inactive" - >("all"); + const { components, loading, updateComponent, deleteComponent } = useComponents() + const [searchTerm, setSearchTerm] = useState('') + const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all') // Calculate statistics - const totalComponents = components?.length; - const activeComponents = components?.filter((c) => c.isActive).length; - const inactiveComponents = totalComponents - activeComponents; + const totalComponents = components?.length || 0 + const activeComponents = components?.filter((c) => c.isActive).length || 0 + const inactiveComponents = totalComponents - activeComponents const { translate } = useLocalization() const stats = [ @@ -36,62 +34,58 @@ const ComponentManager: React.FC = () => { name: translate('::App.DeveloperKit.Component.Total'), value: totalComponents, icon: Puzzle, - color: "text-purple-600", - bgColor: "bg-purple-100", + color: 'text-purple-600', + bgColor: 'bg-purple-100', }, { name: translate('::App.DeveloperKit.Component.Active'), value: activeComponents, icon: CheckCircle, - color: "text-emerald-600", - bgColor: "bg-emerald-100", + color: 'text-emerald-600', + bgColor: 'bg-emerald-100', }, { name: translate('::App.DeveloperKit.Component.Inactive'), value: inactiveComponents, icon: XCircle, - color: "text-slate-600", - bgColor: "bg-slate-100", + color: 'text-slate-600', + bgColor: 'bg-slate-100', }, - ]; + ] const filteredComponents = components?.filter((component) => { const matchesSearch = component.name.toLowerCase().includes(searchTerm.toLowerCase()) || - (component.description || "") - .toLowerCase() - .includes(searchTerm.toLowerCase()); + (component.description || '').toLowerCase().includes(searchTerm.toLowerCase()) const matchesFilter = - filterActive === "all" || - (filterActive === "active" && component.isActive) || - (filterActive === "inactive" && !component.isActive); + filterActive === 'all' || + (filterActive === 'active' && component.isActive) || + (filterActive === 'inactive' && !component.isActive) - return matchesSearch && matchesFilter; - }); + return matchesSearch && matchesFilter + }) const handleToggleActive = async (id: string, isActive: boolean) => { try { - const component = components.find((c) => c.id === id); + const component = components?.find((c) => c.id === id) if (component) { - await updateComponent(id, { ...component, isActive }); + await updateComponent(id, { ...component, isActive }) } } catch (err) { - console.error("Failed to toggle component status:", err); + console.error('Failed to toggle component status:', err) } - }; + } const handleDelete = async (id: string, name: string) => { - if ( - window.confirm(translate('::App.DeveloperKit.Component.ConfirmDelete')) - ) { + if (window.confirm(translate('::App.DeveloperKit.Component.ConfirmDelete'))) { try { - await deleteComponent(id); + await deleteComponent(id) } catch (err) { - console.error("Failed to delete component:", err); + console.error('Failed to delete component:', err) } } - }; + } return (
@@ -114,18 +108,11 @@ const ComponentManager: React.FC = () => { {/* Statistics Cards */}
{stats.map((stat, index) => ( -
+
-

- {stat.name} -

-

- {stat.value} -

+

{stat.name}

+

{stat.value}

@@ -152,21 +139,27 @@ const ComponentManager: React.FC = () => {
{/* Components List */} - {filteredComponents?.length > 0 ? ( + {loading ? ( +
+
Bileşenler yükleniyor...
+
+ ) : filteredComponents?.length > 0 ? (
{filteredComponents.map((component) => (
{
-

- {component.name} -

+

{component.name}

{(() => { try { - const parsed = JSON.parse( - component.dependencies ?? "[]" - ); + const parsed = JSON.parse(component.dependencies ?? '[]') return Array.isArray(parsed) && parsed.length > 0 - ? `${parsed.join(", ")}` - : translate('::App.DeveloperKit.Component.NoDependencies'); + ? `${parsed.join(', ')}` + : translate('::App.DeveloperKit.Component.NoDependencies') } catch { - return translate('::App.DeveloperKit.Component.NoDependencies'); + return translate('::App.DeveloperKit.Component.NoDependencies') } })()}

{component.description && ( -

- {component.description} -

+

{component.description}

)}
{component.lastModificationTime - ? new Date( - component.lastModificationTime - ).toLocaleDateString() + ? new Date(component.lastModificationTime).toLocaleDateString() : translate('::App.DeveloperKit.Component.DateNotAvailable')}
@@ -237,13 +222,11 @@ const ComponentManager: React.FC = () => {
{ @@ -295,16 +284,16 @@ const ComponentManager: React.FC = () => {

- {searchTerm || filterActive !== "all" + {searchTerm || filterActive !== 'all' ? translate('::App.DeveloperKit.Component.Empty.Filtered.Title') : translate('::App.DeveloperKit.Component.Empty.Initial.Title')}

- {searchTerm || filterActive !== "all" + {searchTerm || filterActive !== 'all' ? translate('::App.DeveloperKit.Component.Empty.Filtered.Description') : translate('::App.DeveloperKit.Component.Empty.Initial.Description')}

- {!searchTerm && filterActive === "all" && ( + {!searchTerm && filterActive === 'all' && ( {
)}
- ); -}; + ) +} -export default ComponentManager; +export default ComponentManager diff --git a/ui/src/contexts/ComponentContext.tsx b/ui/src/contexts/ComponentContext.tsx index 91c1010a..fa7adea7 100644 --- a/ui/src/contexts/ComponentContext.tsx +++ b/ui/src/contexts/ComponentContext.tsx @@ -1,116 +1,123 @@ -import { CreateUpdateCustomComponentDto, CustomComponent, CustomComponentDto } from '@/proxy/developerKit/models'; -import { developerKitService } from '@/services/developerKit.service'; -import { useStoreState } from '@/store/store'; -import React, { createContext, useContext, useState, useEffect } from 'react'; +import { + CreateUpdateCustomComponentDto, + CustomComponent, + CustomComponentDto, +} from '@/proxy/developerKit/models' +import { developerKitService } from '@/services/developerKit.service' +import { useStoreState } from '@/store/store' +import React, { createContext, useContext, useState, useEffect } from 'react' interface ComponentContextType { - components: CustomComponent[]; - loading: boolean; - error: string | null; - addComponent: (component: CreateUpdateCustomComponentDto) => Promise; - updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise; - deleteComponent: (id: string) => Promise; - getComponent: (id: string) => CustomComponent | undefined; - getComponentByName: (name: string) => CustomComponent | undefined; - refreshComponents: () => Promise; - registeredComponents: Record>; - registerComponent: (name: string, component: React.ComponentType) => void; + components: CustomComponent[] + loading: boolean + error: string | null + addComponent: (component: CreateUpdateCustomComponentDto) => Promise + updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise + deleteComponent: (id: string) => Promise + getComponent: (id: string) => CustomComponent | undefined + getComponentByName: (name: string) => CustomComponent | undefined + refreshComponents: () => Promise + registeredComponents: Record> + registerComponent: (name: string, component: React.ComponentType) => void } -const ComponentContext = createContext(undefined); +const ComponentContext = createContext(undefined) // eslint-disable-next-line react-refresh/only-export-components export const useComponents = () => { - const context = useContext(ComponentContext); + const context = useContext(ComponentContext) if (context === undefined) { - throw new Error('useComponents must be used within a ComponentProvider'); + throw new Error('useComponents must be used within a ComponentProvider') } - return context; -}; + return context +} export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const extraProperties = useStoreState((state) => state.abpConfig?.config?.extraProperties) - const [components, setComponents] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [registeredComponents, setRegisteredComponents] = useState>>({}); + const [components, setComponents] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [registeredComponents, setRegisteredComponents] = useState< + Record> + >({}) const refreshComponents = async () => { try { - setLoading(true); - setError(null); + setLoading(true) + setError(null) const customComponents = extraProperties?.customComponents as CustomComponentDto[] - setComponents(customComponents); + setComponents(customComponents || []) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch components'); - console.error('Failed to fetch components:', err); - } finally { - setLoading(false); + setError(err instanceof Error ? err.message : 'Failed to fetch components') + console.error('Failed to fetch components:', err) + setComponents([]) + } finally { + setLoading(false) } - }; + } useEffect(() => { - refreshComponents(); - }, []); + refreshComponents() + }, [extraProperties]) const addComponent = async (componentData: CreateUpdateCustomComponentDto) => { try { - setLoading(true); - setError(null); - const newComponent = await developerKitService.createCustomComponent(componentData); - setComponents(prev => [...prev, newComponent]); + setLoading(true) + setError(null) + const newComponent = await developerKitService.createCustomComponent(componentData) + setComponents((prev) => [...prev, newComponent]) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to create component'); - throw err; + setError(err instanceof Error ? err.message : 'Failed to create component') + throw err } finally { - setLoading(false); + setLoading(false) } - }; + } const updateComponent = async (id: string, componentData: CreateUpdateCustomComponentDto) => { try { - setLoading(true); - setError(null); - const updatedComponent = await developerKitService.updateCustomComponent(id, componentData); - setComponents(prev => prev.map(component => - component.id === id ? updatedComponent : component - )); + setLoading(true) + setError(null) + const updatedComponent = await developerKitService.updateCustomComponent(id, componentData) + setComponents((prev) => + prev.map((component) => (component.id === id ? updatedComponent : component)), + ) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to update component'); - throw err; + setError(err instanceof Error ? err.message : 'Failed to update component') + throw err } finally { - setLoading(false); + setLoading(false) } - }; + } const deleteComponent = async (id: string) => { try { - setLoading(true); - setError(null); - await developerKitService.deleteCustomComponent(id); - setComponents(prev => prev.filter(component => component.id !== id)); + setLoading(true) + setError(null) + await developerKitService.deleteCustomComponent(id) + setComponents((prev) => prev.filter((component) => component.id !== id)) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to delete component'); - throw err; + setError(err instanceof Error ? err.message : 'Failed to delete component') + throw err } finally { - setLoading(false); + setLoading(false) } - }; + } const getComponent = (id: string) => { - return components.find(comp => comp.id === id); - }; + return components?.find((comp) => comp.id === id) + } const getComponentByName = (name: string) => { - return components.find(comp => comp.name === name); - }; + return components?.find((comp) => comp.name === name) + } const registerComponent = (name: string, component: React.ComponentType) => { - setRegisteredComponents(prev => ({ + setRegisteredComponents((prev) => ({ ...prev, - [name]: component - })); - }; + [name]: component, + })) + } const value = { components, @@ -123,8 +130,8 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi getComponentByName, refreshComponents, registeredComponents, - registerComponent - }; + registerComponent, + } - return {children}; -}; \ No newline at end of file + return {children} +} diff --git a/ui/src/contexts/ComponentRegistryContext.tsx b/ui/src/contexts/ComponentRegistryContext.tsx index 4527c87e..99c39912 100644 --- a/ui/src/contexts/ComponentRegistryContext.tsx +++ b/ui/src/contexts/ComponentRegistryContext.tsx @@ -1,93 +1,141 @@ -import React, { createContext, useCallback, useState, useEffect } from 'react'; -import * as Babel from '@babel/standalone'; -import { useComponents } from './ComponentContext'; +import React, { createContext, useCallback, useState, useEffect } from 'react' +import * as Babel from '@babel/standalone' +import { useComponents } from './ComponentContext' +import { + Alert, + Avatar, + Badge, + Button, + Calendar, + Card, + Checkbox, + ConfigProvider, + DatePicker, + Dialog, + Drawer, + Dropdown, + FormItem, + FormContainer, + Input, + InputGroup, + Menu, + MenuItem, + Notification, + Pagination, + Progress, + Radio, + RangeCalendar, + ScrollBar, + Segment, + Select, + Skeleton, + Spinner, + Steps, + Switcher, + Table, + Tabs, + Tag, + TimeInput, + Timeline, + toast, + Tooltip, + Upload, +} from '../components/ui' interface ComponentProps { - [key: string]: unknown; + [key: string]: unknown } interface ComponentRegistryContextType { - renderComponent: (name: string, props?: ComponentProps) => React.ReactNode; - compileAndRender: (code: string, props?: ComponentProps) => React.ReactNode; - isComponentRegistered: (name: string) => boolean; - getRegisteredComponents: () => string[]; - getComponentCode: (name: string) => string | null; + renderComponent: (name: string, props?: ComponentProps) => React.ReactNode + compileAndRender: (code: string, props?: ComponentProps) => React.ReactNode + isComponentRegistered: (name: string) => boolean + getRegisteredComponents: () => string[] + getComponentCode: (name: string) => string | null } -export const ComponentRegistryContext = createContext(undefined); +export const ComponentRegistryContext = createContext( + undefined, +) const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { components } = useComponents(); - const [compiledComponents, setCompiledComponents] = useState>>({}); - + const { components } = useComponents() + const [compiledComponents, setCompiledComponents] = useState< + Record> + >({}) + const extractComponentInfo = useCallback((code: string, defaultName = '') => { try { // FC Type declaration with explicit component name - const fcTypeMatch = code.match(/const\s+([A-Za-z]\w*)\s*:\s*React\.FC/); - if (fcTypeMatch) return fcTypeMatch[1]; - + const fcTypeMatch = code.match(/const\s+([A-Za-z]\w*)\s*:\s*React\.FC/) + if (fcTypeMatch) return fcTypeMatch[1] + // Function declaration: function MyComponent() {} - const functionMatch = code.match(/function\s+([A-Za-z]\w*)/); - if (functionMatch) return functionMatch[1]; + const functionMatch = code.match(/function\s+([A-Za-z]\w*)/) + if (functionMatch) return functionMatch[1] // Arrow function with explicit name: const MyComponent = () => {} - const arrowMatch = code.match(/const\s+([A-Za-z]\w*)\s*=/); - if (arrowMatch) return arrowMatch[1]; + const arrowMatch = code.match(/const\s+([A-Za-z]\w*)\s*=/) + if (arrowMatch) return arrowMatch[1] // Class declaration: class MyComponent extends React.Component {} - const classMatch = code.match(/class\s+([A-Za-z]\w*)/); - if (classMatch) return classMatch[1]; + const classMatch = code.match(/class\s+([A-Za-z]\w*)/) + if (classMatch) return classMatch[1] // Default export name - const exportMatch = code.match(/export\s+default\s+([A-Za-z]\w*)/); - if (exportMatch) return exportMatch[1]; + const exportMatch = code.match(/export\s+default\s+([A-Za-z]\w*)/) + if (exportMatch) return exportMatch[1] // Interface name which might indicate component name - const interfaceMatch = code.match(/interface\s+([A-Za-z]\w*)Props/); - if (interfaceMatch) return interfaceMatch[1]; - + const interfaceMatch = code.match(/interface\s+([A-Za-z]\w*)Props/) + if (interfaceMatch) return interfaceMatch[1] + // Look for TypeScript type definitions that might indicate a component name - const tsTypeMatch = code.match(/type\s+([A-Za-z]\w*)Props/); - if (tsTypeMatch) return tsTypeMatch[1]; - + const tsTypeMatch = code.match(/type\s+([A-Za-z]\w*)Props/) + if (tsTypeMatch) return tsTypeMatch[1] + // Try to find any capitalized identifier that might be a component - const capitalNameMatch = code.match(/\b([A-Z][A-Za-z0-9]*)\b/); - if (capitalNameMatch && capitalNameMatch[1] !== 'React' && capitalNameMatch[1] !== 'Component') { - return capitalNameMatch[1]; + const capitalNameMatch = code.match(/\b([A-Z][A-Za-z0-9]*)\b/) + if ( + capitalNameMatch && + capitalNameMatch[1] !== 'React' && + capitalNameMatch[1] !== 'Component' + ) { + return capitalNameMatch[1] } - + // Use the default name provided (usually the component name from DB) if (defaultName) { - return defaultName; + return defaultName } - + // Last resort - use "DynamicComponent" as it's descriptive and unlikely to conflict - return "DynamicComponent"; + return 'DynamicComponent' } catch (err) { - console.error("Error extracting component name:", err); - return defaultName || "DynamicComponent"; + console.error('Error extracting component name:', err) + return defaultName || 'DynamicComponent' } - }, []); + }, []) // Compile all components when the component list changes useEffect(() => { - if (!components || !components?.length) return; - + if (!components || !components?.length) return + try { // Create a bundle of all active components - const activeComponents = components?.filter(c => c.isActive); - + const activeComponents = components?.filter((c) => c.isActive) + if (!activeComponents.length) { - setCompiledComponents({}); - return; + setCompiledComponents({}) + return } // First, extract all component names and create both lowercase and normal versions - const componentInfos = activeComponents.map(comp => { - const name = comp.name; + const componentInfos = activeComponents.map((comp) => { + const name = comp.name // Create both original and capitalized versions for more reliable lookups - const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1); - + const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1) + return { name: name, nameCapitalized: nameCapitalized, @@ -95,82 +143,198 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch code: comp.code .replace(/import\s+.*?;/g, '') .replace(/export\s+default\s+/, '') - .trim() - }; - }); - + .trim(), + } + }) + // Prepare the combined code in a way that avoids naming conflicts - const componentBundle = componentInfos.map(info => - `// Component: ${info.name}\nconst ${info.name}_Component = (function() {\n${info.code}\nreturn ${info.internalName};\n})();` - ).join('\n\n'); - + const componentBundle = componentInfos + .map( + (info) => + `// Component: ${info.name}\nconst ${info.name}_Component = (function() {\n${info.code}\nreturn ${info.internalName};\n})();`, + ) + .join('\n\n') + // Create a function that returns an object with all components const bundledCode = ` - (function(React) { + (function(React, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload) { + // Global components and hooks available to all custom components + const { useState, useEffect, useCallback, useMemo, useRef, createContext, useContext } = React; + + // Basic HTML elements for fallback + const Div = (props) => React.createElement('div', props); + const Span = (props) => React.createElement('span', props); + const Textarea = (props) => React.createElement('textarea', props); + const Option = (props) => React.createElement('option', props); + const Form = (props) => React.createElement('form', props); + const Label = (props) => React.createElement('label', props); + const H1 = (props) => React.createElement('h1', props); + const H2 = (props) => React.createElement('h2', props); + const H3 = (props) => React.createElement('h3', props); + const H4 = (props) => React.createElement('h4', props); + const H5 = (props) => React.createElement('h5', props); + const H6 = (props) => React.createElement('h6', props); + const P = (props) => React.createElement('p', props); + const A = (props) => React.createElement('a', props); + const Img = (props) => React.createElement('img', props); + const Ul = (props) => React.createElement('ul', props); + const Li = (props) => React.createElement('li', props); + const Tr = (props) => React.createElement('tr', props); + const Td = (props) => React.createElement('td', props); + const Th = (props) => React.createElement('th', props); + const Thead = (props) => React.createElement('thead', props); + const Tbody = (props) => React.createElement('tbody', props); + const componentRegistry = {}; ${componentBundle} // Add all components to the registry with both original and capitalized names - ${componentInfos.map(info => ` + ${componentInfos + .map( + (info) => ` // Register with original name componentRegistry["${info.name}"] = ${info.name}_Component; // Register with capitalized name for proper React convention componentRegistry["${info.nameCapitalized}"] = ${info.name}_Component; - `).join('\n')} + `, + ) + .join('\n')} return componentRegistry; - })(React) - `; - + })(React, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload) + ` + // Compile the bundle const compiledBundle = Babel.transform(bundledCode, { presets: ['react', 'typescript'], - filename: 'components-bundle.tsx' - }).code; - + filename: 'components-bundle.tsx', + }).code + if (!compiledBundle) { - throw new Error('Failed to compile components bundle'); + throw new Error('Failed to compile components bundle') } - + // Evaluate the bundle to get all components - const componentsFactory = new Function('React', `return ${compiledBundle}`); - const compiledComponentsRegistry = componentsFactory(React); - - setCompiledComponents(compiledComponentsRegistry); + const componentsFactory = new Function( + 'React', + 'Alert', + 'Avatar', + 'Badge', + 'Button', + 'Calendar', + 'Card', + 'Checkbox', + 'ConfigProvider', + 'DatePicker', + 'Dialog', + 'Drawer', + 'Dropdown', + 'FormItem', + 'FormContainer', + 'Input', + 'InputGroup', + 'Menu', + 'MenuItem', + 'Notification', + 'Pagination', + 'Progress', + 'Radio', + 'RangeCalendar', + 'ScrollBar', + 'Segment', + 'Select', + 'Skeleton', + 'Spinner', + 'Steps', + 'Switcher', + 'Table', + 'Tabs', + 'Tag', + 'TimeInput', + 'Timeline', + 'toast', + 'Tooltip', + 'Upload', + `return ${compiledBundle}`, + ) + const compiledComponentsRegistry = componentsFactory( + React, + Alert, + Avatar, + Badge, + Button, + Calendar, + Card, + Checkbox, + ConfigProvider, + DatePicker, + Dialog, + Drawer, + Dropdown, + FormItem, + FormContainer, + Input, + InputGroup, + Menu, + MenuItem, + Notification, + Pagination, + Progress, + Radio, + RangeCalendar, + ScrollBar, + Segment, + Select, + Skeleton, + Spinner, + Steps, + Switcher, + Table, + Tabs, + Tag, + TimeInput, + Timeline, + toast, + Tooltip, + Upload, + ) + + setCompiledComponents(compiledComponentsRegistry) } catch (error) { - console.error('Error compiling components bundle:', error); - setCompiledComponents({}); + console.error('Error compiling components bundle:', error) + setCompiledComponents({}) } - }, [components, extractComponentInfo]); + }, [components, extractComponentInfo]) - const compileCode = useCallback((code: string) => { - try { - // Clean the code and extract component name - const cleanCode = code - .replace(/import\s+.*?;/g, '') - .replace(/export\s+default\s+/, '') - .trim(); + const compileCode = useCallback( + (code: string) => { + try { + // Clean the code and extract component name + const cleanCode = code + .replace(/import\s+.*?;/g, '') + .replace(/export\s+default\s+/, '') + .trim() - // Try to extract a meaningful name from the component, - // but don't generate random names that could cause reference issues - const componentName = extractComponentInfo(code, 'AnonComponent'); + // Try to extract a meaningful name from the component, + // but don't generate random names that could cause reference issues + const componentName = extractComponentInfo(code, 'AnonComponent') - // Extract all potential component references from JSX - // Look for JSX tags like or - const jsxComponentRegex = /<([A-Z][A-Za-z0-9_]*)/g; - const jsxMatches = [...cleanCode.matchAll(jsxComponentRegex)].map(match => match[1]); - - // Get unique component names from JSX - const jsxComponentNames = [...new Set(jsxMatches)]; - - // Generate a warning for JSX tags that might be components - if (jsxComponentNames.length > 0) { - console.log("JSX tags that might be components:", jsxComponentNames.join(", ")); - } + // Extract all potential component references from JSX + // Look for JSX tags like or + const jsxComponentRegex = /<([A-Z][A-Za-z0-9_]*)/g + const jsxMatches = [...cleanCode.matchAll(jsxComponentRegex)].map((match) => match[1]) - // Transform code to a component factory that wraps the component to provide dynamic component access - const transformedCode = ` + // Get unique component names from JSX + const jsxComponentNames = [...new Set(jsxMatches)] + + // Generate a warning for JSX tags that might be components + if (jsxComponentNames.length > 0) { + console.log('JSX tags that might be components:', jsxComponentNames.join(', ')) + } + + // Transform code to a component factory that wraps the component to provide dynamic component access + const transformedCode = ` (function createComponent(React, componentsRegistry) { // Define a component wrapper function that will handle component references function DynamicComponentRenderer(name, props) { @@ -300,20 +464,23 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch throw error; } })(React, compiledComponentsObj) - `; + ` - // Compile the code - const compiledCode = Babel.transform(transformedCode, { - presets: ['react', 'typescript'], - filename: 'component.tsx' - }).code; + // Compile the code + const compiledCode = Babel.transform(transformedCode, { + presets: ['react', 'typescript'], + filename: 'component.tsx', + }).code - if (!compiledCode) { - throw new Error('Failed to compile component'); - } + if (!compiledCode) { + throw new Error('Failed to compile component') + } - // Create and return the component with better error handling - const ComponentFactory = new Function('React', 'compiledComponentsObj', ` + // Create and return the component with better error handling + const ComponentFactory = new Function( + 'React', + 'compiledComponentsObj', + ` try { // Create a local variable to ensure it exists var AnonComponent; @@ -350,306 +517,358 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch ]); }; } - `); - - // Create the component with our registry of all other components - const Component = ComponentFactory(React, compiledComponents); + `, + ) - if (!Component || typeof Component !== 'function') { - throw new Error('Invalid component definition'); - } + // Create the component with our registry of all other components + const Component = ComponentFactory(React, compiledComponents) - return Component; - } catch (error) { - console.error('Component compilation error:', error); - return () => ( -
-

Compilation Error

-
{String(error)}
-
- ); - } - }, [extractComponentInfo, compiledComponents]); + if (!Component || typeof Component !== 'function') { + throw new Error('Invalid component definition') + } - const compileAndRender = useCallback((code: string, props: ComponentProps = {}) => { - if (!code?.trim()) { - return null; - } - - try { - // Create a component without adding it to registry yet - const Component = compileCode(code); - return ; - } catch (error) { - console.error('Render error:', error); - return ( -
-

Render Error

-
{String(error)}
-
- ); - } - }, [compileCode]); - - const renderComponent = useCallback((name: string, props: ComponentProps = {}) => { - // Check if the component is already compiled - if (compiledComponents[name]) { - const Component = compiledComponents[name]; - return ; - } - - // Otherwise, try to compile it from the source code - const component = components.find(c => c.name === name && c.isActive); - if (!component) { - console.error(`Component not found: ${name}. Available components: ${Object.keys(compiledComponents).join(', ')}`); - return ( -
-
Component not found: {name}
-
This could be because: -
    -
  • The component has not been saved to the database
  • -
  • The component name is misspelled
  • -
  • There was an error compiling the component
  • -
+ return Component + } catch (error) { + console.error('Component compilation error:', error) + return () => ( +
+

Compilation Error

+
{String(error)}
-
- ); - } - - try { - // Force refresh component registry when rendering a component directly - // This ensures all components are available for cross-referencing - if (Object.keys(compiledComponents).length === 0) { - // If no components are compiled yet, this is the first render - console.warn("Component registry is empty. Components might not be available for references."); + ) } - - return compileAndRender(component.code, props); - } catch (error) { - console.error(`Error rendering component ${name}:`, error); - return ( -
-
Error rendering {name}
-
{String(error)}
-
- ); - } - }, [components, compileAndRender, compiledComponents]); + }, + [extractComponentInfo, compiledComponents], + ) - const isComponentRegistered = useCallback((name: string) => { - return components.some(c => c.name === name && c.isActive); - }, [components]); + const compileAndRender = useCallback( + (code: string, props: ComponentProps = {}) => { + if (!code?.trim()) { + return null + } + + try { + // Create a component without adding it to registry yet + const Component = compileCode(code) + return + } catch (error) { + console.error('Render error:', error) + return ( +
+

Render Error

+
{String(error)}
+
+ ) + } + }, + [compileCode], + ) + + const renderComponent = useCallback( + (name: string, props: ComponentProps = {}) => { + // Check if the component is already compiled + if (compiledComponents[name]) { + const Component = compiledComponents[name] + return + } + + // Otherwise, try to compile it from the source code + const component = components.find((c) => c.name === name && c.isActive) + if (!component) { + console.error( + `Component not found: ${name}. Available components: ${Object.keys(compiledComponents).join(', ')}`, + ) + return ( +
+
Component not found: {name}
+
+ This could be because: +
    +
  • The component has not been saved to the database
  • +
  • The component name is misspelled
  • +
  • There was an error compiling the component
  • +
+
+
+ ) + } + + try { + // Force refresh component registry when rendering a component directly + // This ensures all components are available for cross-referencing + if (Object.keys(compiledComponents).length === 0) { + // If no components are compiled yet, this is the first render + console.warn( + 'Component registry is empty. Components might not be available for references.', + ) + } + + return compileAndRender(component.code, props) + } catch (error) { + console.error(`Error rendering component ${name}:`, error) + return ( +
+
Error rendering {name}
+
{String(error)}
+
+ ) + } + }, + [components, compileAndRender, compiledComponents], + ) + + const isComponentRegistered = useCallback( + (name: string) => { + return components.some((c) => c.name === name && c.isActive) + }, + [components], + ) const getRegisteredComponents = useCallback(() => { - return components?.filter(c => c.isActive).map(c => c.name); - }, [components]); + return components?.filter((c) => c.isActive).map((c) => c.name) + }, [components]) - const getComponentCode = useCallback((name: string) => { - const component = components.find(c => c.name === name); - return component ? component.code : null; - }, [components]); + const getComponentCode = useCallback( + (name: string) => { + const component = components.find((c) => c.name === name) + return component ? component.code : null + }, + [components], + ) // Create a helper to process DOM nodes and replace custom lowercase elements with actual components useEffect(() => { - if (typeof window === 'undefined' || !document) return; - - // Create a type definition for ReactDOM + if (typeof window === 'undefined' || !document) return + + // Create a type definition for ReactDOM // This approach avoids TypeScript errors type ReactDOMType = { - render: (element: React.ReactNode, container: Element) => void; - createRoot?: (container: Element) => { render: (element: React.ReactNode) => void }; - }; - + render: (element: React.ReactNode, container: Element) => void + createRoot?: (container: Element) => { render: (element: React.ReactNode) => void } + } + // Check if we have any components to work with - if (!Object.keys(compiledComponents).length) return; - + if (!Object.keys(compiledComponents).length) return + // Get all lowercase component names from registry and add them to a Set for faster lookup - const lowercaseComponentNames = new Set(); - Object.keys(compiledComponents).forEach(name => { + const lowercaseComponentNames = new Set() + Object.keys(compiledComponents).forEach((name) => { // Only include lowercase component names, but not standard HTML elements - if (name.charAt(0) === name.charAt(0).toLowerCase() && - name.charAt(0) !== name.charAt(0).toUpperCase() && - !['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'button', 'a', 'img', 'input', 'form', - 'label', 'select', 'option', 'textarea', 'ul', 'ol', 'li', 'table', 'tr', 'td', 'th'].includes(name)) { - lowercaseComponentNames.add(name); + if ( + name.charAt(0) === name.charAt(0).toLowerCase() && + name.charAt(0) !== name.charAt(0).toUpperCase() && + ![ + 'div', + 'span', + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'button', + 'a', + 'img', + 'input', + 'form', + 'label', + 'select', + 'option', + 'textarea', + 'ul', + 'ol', + 'li', + 'table', + 'tr', + 'td', + 'th', + ].includes(name) + ) { + lowercaseComponentNames.add(name) } - }); - - if (lowercaseComponentNames.size === 0) return; - + }) + + if (lowercaseComponentNames.size === 0) return + // Create a function to process a DOM node and its children const processNode = (rootNode: Element) => { // Convert to array for better filtering - const componentsToFind = [...lowercaseComponentNames]; - + const componentsToFind = [...lowercaseComponentNames] + // Create CSS selector for all lowercase component tags - const selector = componentsToFind.join(','); - if (!selector) return false; - + const selector = componentsToFind.join(',') + if (!selector) return false + // Find all matching elements - const elements = rootNode.tagName && lowercaseComponentNames.has(rootNode.tagName.toLowerCase()) - ? [rootNode] - : Array.from(rootNode.querySelectorAll(selector)); - - if (elements.length === 0) return false; - + const elements = + rootNode.tagName && lowercaseComponentNames.has(rootNode.tagName.toLowerCase()) + ? [rootNode] + : Array.from(rootNode.querySelectorAll(selector)) + + if (elements.length === 0) return false + // Process each element - elements.forEach(element => { + elements.forEach((element) => { // Skip if already processed - if (element.hasAttribute('data-component-processed')) return; - + if (element.hasAttribute('data-component-processed')) return + try { // Mark as processed to avoid infinite loops - element.setAttribute('data-component-processed', 'true'); - + element.setAttribute('data-component-processed', 'true') + // Get the tag name in lowercase - const tagName = element.tagName.toLowerCase(); - + const tagName = element.tagName.toLowerCase() + // Get the component from registry - const Component = compiledComponents[tagName]; + const Component = compiledComponents[tagName] if (!Component) { - console.warn(`Component ${tagName} exists in registry but couldn't be loaded`); - return; + console.warn(`Component ${tagName} exists in registry but couldn't be loaded`) + return } - + // Get props from element attributes - const props: Record = {}; - Array.from(element.attributes).forEach(attr => { + const props: Record = {} + Array.from(element.attributes).forEach((attr) => { // Skip data-component-processed attribute - if (attr.name === 'data-component-processed') return; - + if (attr.name === 'data-component-processed') return + // Convert kebab-case to camelCase for props (React convention) - const propName = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); - let propValue: string | boolean | number = attr.value; - + const propName = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) + let propValue: string | boolean | number = attr.value + // Handle boolean attributes (present without value) if (propValue === '' || propValue === propName) { - propValue = true; + propValue = true } - + // Try to parse JSON values if (propValue && typeof propValue === 'string') { - if ((propValue.startsWith('{') && propValue.endsWith('}')) || - (propValue.startsWith('[') && propValue.endsWith(']')) || - propValue === 'true' || propValue === 'false' || !isNaN(Number(propValue))) { try { - propValue = JSON.parse(propValue); - } catch { - // Keep as string if parsing fails - } + if ( + (propValue.startsWith('{') && propValue.endsWith('}')) || + (propValue.startsWith('[') && propValue.endsWith(']')) || + propValue === 'true' || + propValue === 'false' || + !isNaN(Number(propValue)) + ) { + try { + propValue = JSON.parse(propValue) + } catch { + // Keep as string if parsing fails + } } } - - props[propName] = propValue; - }); - + + props[propName] = propValue + }) + // Process children - const children = Array.from(element.childNodes).map(child => { + const children = Array.from(element.childNodes).map((child) => { if (child.nodeType === Node.TEXT_NODE) { - return child.textContent; + return child.textContent } - return child; - }); - + return child + }) + if (children.length) { - props.children = children.length === 1 ? children[0] : children; + props.children = children.length === 1 ? children[0] : children } - + // Create a wrapper that preserves the original element's position - const wrapper = document.createElement('div'); - wrapper.style.display = 'contents'; // Don't add extra layout structure - wrapper.dataset.customComponent = tagName; - + const wrapper = document.createElement('div') + wrapper.style.display = 'contents' // Don't add extra layout structure + wrapper.dataset.customComponent = tagName + // Insert wrapper and remove original - element.parentNode?.insertBefore(wrapper, element); - element.parentNode?.removeChild(element); - + element.parentNode?.insertBefore(wrapper, element) + element.parentNode?.removeChild(element) + // Render React component into wrapper try { - const reactElement = React.createElement(Component, props); - const ReactDOM = (window as Window & typeof globalThis & { ReactDOM?: ReactDOMType }).ReactDOM; - + const reactElement = React.createElement(Component, props) + const ReactDOM = (window as Window & typeof globalThis & { ReactDOM?: ReactDOMType }) + .ReactDOM + if (ReactDOM) { // Use modern createRoot API if available (React 18+) if (ReactDOM.createRoot) { - const root = ReactDOM.createRoot(wrapper); - root.render(reactElement); - } + const root = ReactDOM.createRoot(wrapper) + root.render(reactElement) + } // Fallback to legacy render API else if (ReactDOM.render) { - ReactDOM.render(reactElement, wrapper); + ReactDOM.render(reactElement, wrapper) } } else { - console.error("ReactDOM not found in window - cannot render custom components"); + console.error('ReactDOM not found in window - cannot render custom components') } } catch (err) { - console.error(`Error rendering ${tagName} React component:`, err); - + console.error(`Error rendering ${tagName} React component:`, err) + // Show error UI in place of the component wrapper.innerHTML = `
Error rendering <${tagName}> component -
`; +
` } } catch (error) { - console.error(`Error processing ${element.tagName} element:`, error); + console.error(`Error processing ${element.tagName} element:`, error) } - }); - - return elements.length > 0; - }; - + }) + + return elements.length > 0 + } + // Create a mutation observer to watch for our custom elements const observer = new MutationObserver((mutations) => { - let hasProcessed = false; - - mutations.forEach(mutation => { + let hasProcessed = false + + mutations.forEach((mutation) => { if (mutation.type === 'childList') { // Process added nodes - mutation.addedNodes.forEach(node => { + mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // Process this node and its children - hasProcessed = processNode(node as Element) || hasProcessed; + hasProcessed = processNode(node as Element) || hasProcessed } - }); + }) } - }); - + }) + // Also check the entire document occasionally to catch any missed elements // This handles cases where elements are added before our observer is active if (!hasProcessed && document.body) { - processNode(document.body); + processNode(document.body) } - }); - + }) + // Start observing the entire document - observer.observe(document.body, { + observer.observe(document.body, { childList: true, subtree: true, - characterData: true - }); - + characterData: true, + }) + // Initial scan of the whole document if (document.body) { - processNode(document.body); + processNode(document.body) } - + return () => { - observer.disconnect(); - }; - }, [compiledComponents]); - + observer.disconnect() + } + }, [compiledComponents]) + const value = { renderComponent, compileAndRender, isComponentRegistered, getRegisteredComponents, - getComponentCode - }; + getComponentCode, + } return ( - - {children} - - ); -}; + {children} + ) +} -export default ComponentRegistryProvider; \ No newline at end of file +export default ComponentRegistryProvider