Dinamik Route ve Dinamik Component düzenlemesi
This commit is contained in:
parent
fe0a4b1c7c
commit
a290556ba1
10 changed files with 399 additions and 902 deletions
|
|
@ -121,6 +121,12 @@
|
|||
}
|
||||
],
|
||||
"LanguageTexts": [
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Product.ListComponent",
|
||||
"en": "Product List Component",
|
||||
"tr": "Ürün Liste Bileşeni"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
{
|
||||
"Routes": [
|
||||
{
|
||||
"key": "dynamic.ProductListComponent",
|
||||
"path": "/admin/ProductListComponent",
|
||||
"componentPath": "dynamic:ProductListComponent",
|
||||
"routeType": "protected",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "home",
|
||||
"path": "/home",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,15 @@
|
|||
"MultiTenancySide": 3,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Platform",
|
||||
"Name": "App.Product.ListComponent",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Product.ListComponent",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 3,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "AbpTenantManagement.Tenants",
|
||||
|
|
@ -2239,7 +2248,7 @@
|
|||
{
|
||||
"GroupName": "App.Administration",
|
||||
"Name": "App.DeveloperKit.CustomEndpoints",
|
||||
"ParentName": null,
|
||||
"ParentName": "App.DeveloperKit",
|
||||
"DisplayName": "App.DeveloperKit.CustomEndpoints",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 3,
|
||||
|
|
|
|||
|
|
@ -87,20 +87,20 @@
|
|||
],
|
||||
"CustomComponents": [
|
||||
{
|
||||
"name": "AxiosListComponent",
|
||||
"code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface AxiosListComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344\", // defaults'ı her seferinde set etme\n});\n\nconst AxiosListComponent: React.FC<AxiosListComponentProps> = ({ title }) => {\n const [data, setData] = useState<Array<{ id: string; name: string }>>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const fetchData = async () => {\n setLoading(true);\n setError(null);\n\n try {\n const res = await api.get(`/api/app/dynamic/${title}`);\n const raw = Array.isArray(res.data) ? res.data : res.data?.items ?? [];\n\n const filtered = raw.map((item: any) => ({\n id: item.Id ?? item.id,\n name: item.Name ?? item.name,\n }));\n\n setData(filtered);\n } catch (err: any) {\n setError(err.message || \"Failed to fetch data\");\n } finally {\n setLoading(false);\n }\n };\n\n if (title) fetchData();\n }, [title]);\n\n if (loading) return <div>Loading...</div>;\n if (error) return <div className=\"text-red-600\">Error: {error}</div>;\n if (!data.length) return <div>No records found</div>;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n <div className=\"overflow-auto\">\n <table className=\"min-w-full bg-white border border-slate-200 shadow-sm rounded-lg\">\n <thead className=\"bg-slate-100\">\n <tr>\n {headers.map((key) => (\n <th\n key={key}\n className=\"text-left px-4 py-2 border-b border-slate-200 text-sm font-medium text-slate-700\"\n >\n {key === \"actions\" ? \"Actions\" : key}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.map((item, rowIndex) => (\n <tr key={rowIndex} className=\"hover:bg-slate-50\">\n <td className=\"px-4 py-2 border-b border-slate-100 text-sm text-slate-800\">\n {item.id}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 text-sm text-slate-800\">\n {item.name}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100\">\n <button\n onClick={() => alert(item.name)}\n className=\"bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-1 rounded-lg shadow-sm transition\"\n >\n Show Name\n </button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n};\n\nexport default AxiosListComponent;",
|
||||
"name": "DynamicEntityComponent",
|
||||
"code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface DynamicEntityComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344\", // defaults'ı her seferinde set etme\n});\n\nconst DynamicEntityComponent: React.FC<DynamicEntityComponentProps> = ({ title }) => {\n const [data, setData] = useState<Array<{ id: string; name: string }>>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const fetchData = async () => {\n setLoading(true);\n setError(null);\n\n try {\n const res = await api.get(`/api/app/dynamic/${title}`);\n const raw = Array.isArray(res.data) ? res.data : res.data?.items ?? [];\n\n const filtered = raw.map((item: any) => ({\n id: item.Id ?? item.id,\n name: item.Name ?? item.name,\n }));\n\n setData(filtered);\n } catch (err: any) {\n setError(err.message || \"Failed to fetch data\");\n } finally {\n setLoading(false);\n }\n };\n\n if (title) fetchData();\n }, [title]);\n\n if (loading) return <div>Loading...</div>;\n if (error) return <div className=\"text-red-600\">Error: {error}</div>;\n if (!data.length) return <div>No records found</div>;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n <div className=\"overflow-auto\">\n <table className=\"min-w-full bg-white border border-slate-200 shadow-sm rounded-lg\">\n <thead className=\"bg-slate-100\">\n <tr>\n {headers.map((key) => (\n <th\n key={key}\n className=\"text-left px-4 py-2 border-b border-slate-200 text-sm font-medium text-slate-700\"\n >\n {key === \"actions\" ? \"Actions\" : key}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.map((item, rowIndex) => (\n <tr key={rowIndex} className=\"hover:bg-slate-50\">\n <td className=\"px-4 py-2 border-b border-slate-100 text-sm text-slate-800\">\n {item.id}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 text-sm text-slate-800\">\n {item.name}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100\">\n <button\n onClick={() => alert(item.name)}\n className=\"bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-1 rounded-lg shadow-sm transition\"\n >\n Show Name\n </button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n};\n\nexport default DynamicEntityComponent;",
|
||||
"props": null,
|
||||
"description": null,
|
||||
"isActive": true,
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "EntityListComponent",
|
||||
"code": "const EntityListComponent = ({\n title = \"Product\"\n}) => {\n return (\n <AxiosListComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default EntityListComponent;",
|
||||
"name": "ProductListComponent",
|
||||
"code": "const ProductListComponent = ({\n title = \"Product\"\n}) => {\n return (\n <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default ProductListComponent;",
|
||||
"props": null,
|
||||
"description": null,
|
||||
"isActive": true,
|
||||
"dependencies": ["AxiosListComponent"]
|
||||
"dependencies": ["DynamicEntityComponent"]
|
||||
}
|
||||
],
|
||||
"ReportCategories": [
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { BrowserRouter } from 'react-router-dom'
|
|||
import { store } from './store'
|
||||
import { DynamicRoutesProvider } from './routes/dynamicRoutesContext'
|
||||
import { ComponentProvider } from './contexts/ComponentContext'
|
||||
import ComponentRegistryProvider from './contexts/ComponentRegistryContext'
|
||||
import { registerServiceWorker } from './views/version/swRegistration'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
|
|
@ -22,13 +21,11 @@ function App() {
|
|||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<DynamicRoutesProvider>
|
||||
<ComponentProvider>
|
||||
<ComponentRegistryProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Theme>
|
||||
<Layout />
|
||||
</Theme>
|
||||
</QueryClientProvider>
|
||||
</ComponentRegistryProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Theme>
|
||||
<Layout />
|
||||
</Theme>
|
||||
</QueryClientProvider>
|
||||
</ComponentProvider>
|
||||
</DynamicRoutesProvider>
|
||||
</BrowserRouter>
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ const EntityManager: React.FC = () => {
|
|||
<div className="mb-4">
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
{translate('::App.DeveloperKit.Entity.FieldsLabel')}:
|
||||
{translate('::App.DeveloperKit.Entity.FieldLabel')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{entity.fields.slice(0, 4).map((field) => (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,53 @@ import {
|
|||
} from '@/proxy/developerKit/models'
|
||||
import { developerKitService } from '@/services/developerKit.service'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||||
import * as Babel from '@babel/standalone'
|
||||
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'
|
||||
import axios from 'axios'
|
||||
|
||||
interface ComponentProps {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ComponentContextType {
|
||||
components: CustomComponent[]
|
||||
|
|
@ -17,8 +63,15 @@ interface ComponentContextType {
|
|||
getComponent: (id: string) => CustomComponent | undefined
|
||||
getComponentByName: (name: string) => CustomComponent | undefined
|
||||
refreshComponents: () => Promise<void>
|
||||
// Manual registered components
|
||||
registeredComponents: Record<string, React.ComponentType<unknown>>
|
||||
registerComponent: (name: string, component: React.ComponentType<unknown>) => void
|
||||
// Database compiled components
|
||||
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
|
||||
}
|
||||
|
||||
const ComponentContext = createContext<ComponentContextType | undefined>(undefined)
|
||||
|
|
@ -40,6 +93,9 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
const [registeredComponents, setRegisteredComponents] = useState<
|
||||
Record<string, React.ComponentType<unknown>>
|
||||
>({})
|
||||
const [compiledComponents, setCompiledComponents] = useState<
|
||||
Record<string, React.ComponentType<ComponentProps>>
|
||||
>({})
|
||||
|
||||
const refreshComponents = async () => {
|
||||
try {
|
||||
|
|
@ -119,6 +175,213 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
}))
|
||||
}
|
||||
|
||||
// Register some default components on mount
|
||||
useEffect(() => {
|
||||
// Example components for testing
|
||||
const HelloWorldComponent: React.ComponentType<unknown> = () => {
|
||||
return React.createElement('div', {
|
||||
className: 'p-6 bg-blue-50 rounded-lg'
|
||||
}, [
|
||||
React.createElement('h1', {
|
||||
className: 'text-2xl font-bold text-blue-900 mb-4'
|
||||
}, 'Hello World!'),
|
||||
React.createElement('p', {
|
||||
className: 'text-blue-700'
|
||||
}, 'Bu manuel kayıtlı bir komponent!')
|
||||
])
|
||||
}
|
||||
|
||||
const TestListComponent: React.ComponentType<unknown> = () => {
|
||||
return React.createElement('div', {
|
||||
className: 'p-6 bg-green-50 rounded-lg'
|
||||
}, [
|
||||
React.createElement('h1', {
|
||||
className: 'text-2xl font-bold text-green-900 mb-4'
|
||||
}, 'Test List'),
|
||||
React.createElement('p', {
|
||||
className: 'text-green-700'
|
||||
}, 'Bu da test için kayıtlı komponent!')
|
||||
])
|
||||
}
|
||||
|
||||
registerComponent('HelloWorld', HelloWorldComponent)
|
||||
registerComponent('TestList', TestListComponent)
|
||||
}, []) // Empty dependency array - only run once
|
||||
|
||||
// Component compilation functions (moved from ComponentRegistryProvider)
|
||||
const extractComponentInfo = useCallback((code: string, defaultName = '') => {
|
||||
try {
|
||||
const fcTypeMatch = code.match(/const\s+([A-Za-z]\w*)\s*:\s*React\.FC/)
|
||||
if (fcTypeMatch) return fcTypeMatch[1]
|
||||
|
||||
const functionMatch = code.match(/function\s+([A-Za-z]\w*)/)
|
||||
if (functionMatch) return functionMatch[1]
|
||||
|
||||
const arrowMatch = code.match(/const\s+([A-Za-z]\w*)\s*=/)
|
||||
if (arrowMatch) return arrowMatch[1]
|
||||
|
||||
const classMatch = code.match(/class\s+([A-Za-z]\w*)/)
|
||||
if (classMatch) return classMatch[1]
|
||||
|
||||
const exportMatch = code.match(/export\s+default\s+([A-Za-z]\w*)/)
|
||||
if (exportMatch) return exportMatch[1]
|
||||
|
||||
if (defaultName) return defaultName
|
||||
return 'DynamicComponent'
|
||||
} catch (err) {
|
||||
console.error('Error extracting component name:', err)
|
||||
return defaultName || 'DynamicComponent'
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Compile components when they change
|
||||
useEffect(() => {
|
||||
if (!components || !components?.length) return
|
||||
|
||||
try {
|
||||
const activeComponents = components?.filter((c) => c.isActive)
|
||||
|
||||
if (!activeComponents.length) {
|
||||
setCompiledComponents({})
|
||||
return
|
||||
}
|
||||
|
||||
const componentInfos = activeComponents.map((comp) => {
|
||||
const name = comp.name
|
||||
const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1)
|
||||
|
||||
return {
|
||||
name: name,
|
||||
nameCapitalized: nameCapitalized,
|
||||
internalName: extractComponentInfo(comp.code, nameCapitalized),
|
||||
code: comp.code
|
||||
.replace(/import\s+.*?;/g, '')
|
||||
.replace(/export\s+default\s+/, '')
|
||||
.trim(),
|
||||
}
|
||||
})
|
||||
|
||||
// Create cross-referencing bundle
|
||||
const componentDeclarations = componentInfos
|
||||
.map((info) => `let ${info.name}_Component;`)
|
||||
.join('\n')
|
||||
|
||||
const componentDefinitions = componentInfos
|
||||
.map((info) => {
|
||||
const componentVariables = componentInfos
|
||||
.filter(other => other.name !== info.name)
|
||||
.map(other => `const ${other.name} = ${other.name}_Component;`)
|
||||
.join('\n ')
|
||||
|
||||
return `
|
||||
${info.name}_Component = (function() {
|
||||
${componentVariables}
|
||||
${info.code}
|
||||
return ${info.internalName};
|
||||
})();`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const componentBundle = componentDeclarations + '\n' + componentDefinitions
|
||||
|
||||
const bundledCode = `
|
||||
(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, axios) {
|
||||
const { useState, useEffect, useCallback, useMemo, useRef, createContext, useContext } = React;
|
||||
const componentRegistry = {};
|
||||
|
||||
${componentBundle}
|
||||
|
||||
${componentInfos
|
||||
.map(
|
||||
(info) => `
|
||||
componentRegistry["${info.name}"] = ${info.name}_Component;
|
||||
componentRegistry["${info.nameCapitalized}"] = ${info.name}_Component;
|
||||
`,
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
return componentRegistry;
|
||||
})(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, axios)
|
||||
`
|
||||
|
||||
const compiledBundle = Babel.transform(bundledCode, {
|
||||
presets: ['react', 'typescript'],
|
||||
filename: 'components-bundle.tsx',
|
||||
}).code
|
||||
|
||||
if (!compiledBundle) {
|
||||
throw new Error('Failed to compile components bundle')
|
||||
}
|
||||
|
||||
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', 'axios',
|
||||
`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, axios,
|
||||
)
|
||||
|
||||
setCompiledComponents(compiledComponentsRegistry)
|
||||
} catch (error) {
|
||||
console.error('Error compiling components bundle:', error)
|
||||
setCompiledComponents({})
|
||||
}
|
||||
}, [components, extractComponentInfo])
|
||||
|
||||
// Render functions
|
||||
const renderComponent = useCallback(
|
||||
(name: string, props: ComponentProps = {}) => {
|
||||
if (compiledComponents[name]) {
|
||||
const Component = compiledComponents[name]
|
||||
return <Component {...props} />
|
||||
}
|
||||
|
||||
const component = components.find((c) => c.name === name && c.isActive)
|
||||
if (!component) {
|
||||
console.error(`Component not found: ${name}`)
|
||||
return (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<div className="text-sm">Component not found: {name}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
[components, compiledComponents],
|
||||
)
|
||||
|
||||
const compileAndRender = useCallback(
|
||||
(code: string, props: ComponentProps = {}) => {
|
||||
if (!code?.trim()) return null
|
||||
// Simplified version - can be extended later
|
||||
return <div>Code compilation not implemented yet</div>
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const isComponentRegistered = useCallback(
|
||||
(name: string) => {
|
||||
return components.some((c) => c.name === name && c.isActive) || !!compiledComponents[name]
|
||||
},
|
||||
[components, compiledComponents],
|
||||
)
|
||||
|
||||
const getRegisteredComponents = useCallback(() => {
|
||||
const dbComponents = components?.filter((c) => c.isActive).map((c) => c.name) || []
|
||||
const compiledNames = Object.keys(compiledComponents)
|
||||
return [...new Set([...dbComponents, ...compiledNames])]
|
||||
}, [components, compiledComponents])
|
||||
|
||||
const getComponentCode = useCallback(
|
||||
(name: string) => {
|
||||
const component = components.find((c) => c.name === name)
|
||||
return component ? component.code : null
|
||||
},
|
||||
[components],
|
||||
)
|
||||
|
||||
const value = {
|
||||
components,
|
||||
loading,
|
||||
|
|
@ -131,6 +394,11 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
refreshComponents,
|
||||
registeredComponents,
|
||||
registerComponent,
|
||||
renderComponent,
|
||||
compileAndRender,
|
||||
isComponentRegistered,
|
||||
getRegisteredComponents,
|
||||
getComponentCode,
|
||||
}
|
||||
|
||||
return <ComponentContext.Provider value={value}>{children}</ComponentContext.Provider>
|
||||
|
|
|
|||
|
|
@ -1,878 +0,0 @@
|
|||
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'
|
||||
import axios from 'axios'
|
||||
|
||||
interface ComponentProps {
|
||||
[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
|
||||
}
|
||||
|
||||
export const ComponentRegistryContext = createContext<ComponentRegistryContextType | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { components } = useComponents()
|
||||
const [compiledComponents, setCompiledComponents] = useState<
|
||||
Record<string, React.ComponentType<ComponentProps>>
|
||||
>({})
|
||||
|
||||
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]
|
||||
|
||||
// Function declaration: function MyComponent() {}
|
||||
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]
|
||||
|
||||
// Class declaration: class MyComponent extends React.Component {}
|
||||
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]
|
||||
|
||||
// Interface name which might indicate component name
|
||||
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]
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// Use the default name provided (usually the component name from DB)
|
||||
if (defaultName) {
|
||||
return defaultName
|
||||
}
|
||||
|
||||
// Last resort - use "DynamicComponent" as it's descriptive and unlikely to conflict
|
||||
return 'DynamicComponent'
|
||||
} catch (err) {
|
||||
console.error('Error extracting component name:', err)
|
||||
return defaultName || 'DynamicComponent'
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Compile all components when the component list changes
|
||||
useEffect(() => {
|
||||
if (!components || !components?.length) return
|
||||
|
||||
try {
|
||||
// Create a bundle of all active components
|
||||
const activeComponents = components?.filter((c) => c.isActive)
|
||||
|
||||
if (!activeComponents.length) {
|
||||
setCompiledComponents({})
|
||||
return
|
||||
}
|
||||
|
||||
// First, extract all component names and create both lowercase and normal versions
|
||||
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)
|
||||
|
||||
return {
|
||||
name: name,
|
||||
nameCapitalized: nameCapitalized,
|
||||
internalName: extractComponentInfo(comp.code, nameCapitalized), // Pass capitalized name as default
|
||||
code: comp.code
|
||||
.replace(/import\s+.*?;/g, '')
|
||||
.replace(/export\s+default\s+/, '')
|
||||
.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')
|
||||
|
||||
// Create a function that returns an object with all components
|
||||
const bundledCode = `
|
||||
(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, axios) {
|
||||
// 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) => `
|
||||
// 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')}
|
||||
|
||||
return componentRegistry;
|
||||
})(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, axios)
|
||||
`
|
||||
|
||||
// Compile the bundle
|
||||
const compiledBundle = Babel.transform(bundledCode, {
|
||||
presets: ['react', 'typescript'],
|
||||
filename: 'components-bundle.tsx',
|
||||
}).code
|
||||
|
||||
if (!compiledBundle) {
|
||||
throw new Error('Failed to compile components bundle')
|
||||
}
|
||||
|
||||
// Evaluate the bundle to get all components
|
||||
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',
|
||||
'axios',
|
||||
`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,
|
||||
axios,
|
||||
)
|
||||
|
||||
setCompiledComponents(compiledComponentsRegistry)
|
||||
} catch (error) {
|
||||
console.error('Error compiling components bundle:', error)
|
||||
setCompiledComponents({})
|
||||
}
|
||||
}, [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()
|
||||
|
||||
// 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 <ComponentName ...> or <ComponentName/>
|
||||
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(', '))
|
||||
}
|
||||
|
||||
// Transform code to a component factory that wraps the component to provide dynamic component access
|
||||
const transformedCode = `
|
||||
(function createComponent(React, componentsRegistry, axios) {
|
||||
// Define a component wrapper function that will handle component references
|
||||
function DynamicComponentRenderer(name, props) {
|
||||
// Check if the name exists in the registry
|
||||
if (componentsRegistry[name]) {
|
||||
const ComponentToRender = componentsRegistry[name];
|
||||
return React.createElement(ComponentToRender, props);
|
||||
}
|
||||
|
||||
// If not found, show an error UI
|
||||
console.error("Component not found:", name);
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ style: { padding: '8px', border: '1px solid red', color: 'red', margin: '4px' } },
|
||||
"Component not found: " + name
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap the JSX transformer - this is the magic that lets us use <ComponentName /> syntax
|
||||
const originalCreateElement = React.createElement;
|
||||
|
||||
// Standard HTML elements that React natively supports
|
||||
const standardHtmlElements = new Set([
|
||||
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo',
|
||||
'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup',
|
||||
'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed',
|
||||
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend',
|
||||
'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object',
|
||||
'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp',
|
||||
'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong',
|
||||
'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea',
|
||||
'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'
|
||||
]);
|
||||
|
||||
// Override createElement to handle dynamic components
|
||||
React.createElement = function(type, props, ...children) {
|
||||
// If it's already a component function/class, use it as is
|
||||
if (typeof type !== 'string') {
|
||||
return originalCreateElement.apply(React, [type, props, ...children]);
|
||||
}
|
||||
|
||||
// If type is a string starting with uppercase, it's likely a component reference
|
||||
if (/^[A-Z]/.test(type)) {
|
||||
// First check if it's directly in our registry
|
||||
if (componentsRegistry[type]) {
|
||||
return DynamicComponentRenderer(type, Object.assign({}, props, { children }));
|
||||
}
|
||||
|
||||
// Otherwise let React try to handle it
|
||||
// It might be a component defined elsewhere in the code
|
||||
return originalCreateElement.apply(React, [type, props, ...children]);
|
||||
}
|
||||
|
||||
// Special case: check for lowercase component names
|
||||
// If it's not a standard HTML element, it might be a custom component
|
||||
if (!standardHtmlElements.has(type)) {
|
||||
// First check if the lowercase name is registered directly
|
||||
if (componentsRegistry[type]) {
|
||||
console.warn("Using lowercase component name '" + type + "'. React components should use PascalCase.");
|
||||
return DynamicComponentRenderer(type, Object.assign({}, props, { children }));
|
||||
}
|
||||
|
||||
// Then check if we have a PascalCase version of this component
|
||||
const pascalCase = type.charAt(0).toUpperCase() + type.slice(1);
|
||||
if (componentsRegistry[pascalCase]) {
|
||||
console.warn("Using lowercase component name '" + type + "'. Consider using PascalCase: '" + pascalCase + "'");
|
||||
return DynamicComponentRenderer(pascalCase, Object.assign({}, props, { children }));
|
||||
}
|
||||
|
||||
// If neither matched, add a hint in the console to help debugging
|
||||
console.info("Unknown element: <" + type + ">. Treating as HTML element. If this is a custom component, ensure it's registered in PascalCase.");
|
||||
}
|
||||
|
||||
// Otherwise use the original React.createElement for standard HTML elements
|
||||
return originalCreateElement.apply(React, [type, props, ...children]);
|
||||
};
|
||||
|
||||
// Safety net for direct CustomComponent references
|
||||
var CustomComponent = function(props) {
|
||||
console.error("A component is using 'CustomComponent' which was not properly defined");
|
||||
return React.createElement("div", { style: { padding: '12px', border: '2px solid red', color: 'red', margin: '8px' } },
|
||||
"Component definition error: CustomComponent was referenced but not defined properly");
|
||||
};
|
||||
|
||||
try {
|
||||
// Add a safety declaration of AnonComponent to handle default component name
|
||||
var AnonComponent;
|
||||
|
||||
// Execute the component code in this enhanced environment
|
||||
${cleanCode}
|
||||
|
||||
// Restore original createElement to prevent side effects
|
||||
const component = ${componentName};
|
||||
React.createElement = originalCreateElement;
|
||||
|
||||
// If component is still undefined (possible if code doesn't properly assign it)
|
||||
// and we're using AnonComponent as the name, provide a fallback
|
||||
if (${componentName} === undefined && "${componentName}" === "AnonComponent") {
|
||||
console.warn("Component definition didn't properly expose the component. Creating fallback.");
|
||||
// Try to find a React component in the execution context
|
||||
const possibleComponents = Object.keys(this).filter(key =>
|
||||
typeof this[key] === "function" &&
|
||||
/^[A-Z]/.test(key) &&
|
||||
key !== "React" &&
|
||||
key !== "AnonComponent"
|
||||
);
|
||||
|
||||
if (possibleComponents.length > 0) {
|
||||
// Use the first component-like function found
|
||||
return this[possibleComponents[0]];
|
||||
}
|
||||
|
||||
// Last resort: Create a minimal functional component
|
||||
return function DefaultComponent(props) {
|
||||
return React.createElement("div", {
|
||||
style: { padding: "8px", border: "1px solid #ccc" },
|
||||
...props
|
||||
}, props.children || "Unnamed Component");
|
||||
};
|
||||
}
|
||||
|
||||
return component;
|
||||
} catch (error) {
|
||||
// Restore original createElement in case of error too
|
||||
React.createElement = originalCreateElement;
|
||||
throw error;
|
||||
}
|
||||
})(React, compiledComponentsObj, axios)
|
||||
`
|
||||
|
||||
// Compile the code
|
||||
const compiledCode = Babel.transform(transformedCode, {
|
||||
presets: ['react', 'typescript'],
|
||||
filename: 'component.tsx',
|
||||
}).code
|
||||
|
||||
if (!compiledCode) {
|
||||
throw new Error('Failed to compile component')
|
||||
}
|
||||
|
||||
// Create and return the component with better error handling
|
||||
const ComponentFactory = new Function(
|
||||
'React',
|
||||
'compiledComponentsObj',
|
||||
'axios',
|
||||
`
|
||||
try {
|
||||
// Create a local variable to ensure it exists
|
||||
var AnonComponent;
|
||||
|
||||
// Execute the compiled code
|
||||
const component = ${compiledCode};
|
||||
|
||||
// Check if we got a valid component back
|
||||
if (typeof component !== 'function') {
|
||||
console.warn("Component did not return a function, got:", typeof component);
|
||||
|
||||
// Return a fallback component
|
||||
return function FallbackComponent(props) {
|
||||
return React.createElement("div", {
|
||||
style: { padding: '8px', backgroundColor: '#f8f9fa', border: '1px solid #ddd' }
|
||||
}, [
|
||||
React.createElement("p", { style: { fontWeight: 'bold' } }, "Component Preview"),
|
||||
React.createElement("div", {}, props.children || "Component content will appear here")
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
return component;
|
||||
} catch (e) {
|
||||
console.error("Error evaluating component:", e);
|
||||
return function ErrorComponent() {
|
||||
return React.createElement("div", {
|
||||
style: { padding: '12px', border: '2px solid red', color: 'red', margin: '8px' }
|
||||
}, [
|
||||
React.createElement("p", { style: { fontWeight: 'bold' } }, "Error evaluating component"),
|
||||
React.createElement("pre", { style: { whiteSpace: 'pre-wrap', fontSize: '12px' } }, e.message),
|
||||
React.createElement("p", { style: { marginTop: '8px', fontSize: '12px' } },
|
||||
"Check your code for syntax errors or undefined variables.")
|
||||
]);
|
||||
};
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
// Create the component with our registry of all other components
|
||||
const Component = ComponentFactory(React, compiledComponents, axios)
|
||||
|
||||
if (!Component || typeof Component !== 'function') {
|
||||
throw new Error('Invalid component definition')
|
||||
}
|
||||
|
||||
return Component
|
||||
} catch (error) {
|
||||
console.error('Component compilation error:', error)
|
||||
return () => (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<p className="font-semibold mb-2">Compilation Error</p>
|
||||
<div className="text-sm whitespace-pre-wrap">{String(error)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
[extractComponentInfo, compiledComponents],
|
||||
)
|
||||
|
||||
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 <Component {...props} />
|
||||
} catch (error) {
|
||||
console.error('Render error:', error)
|
||||
return (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<p className="font-semibold mb-2">Render Error</p>
|
||||
<div className="text-sm whitespace-pre-wrap">{String(error)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
[compileCode],
|
||||
)
|
||||
|
||||
const renderComponent = useCallback(
|
||||
(name: string, props: ComponentProps = {}) => {
|
||||
// Check if the component is already compiled
|
||||
if (compiledComponents[name]) {
|
||||
const Component = compiledComponents[name]
|
||||
return <Component {...props} />
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<div className="text-sm">Component not found: {name}</div>
|
||||
<div className="text-xs mt-2">
|
||||
This could be because:
|
||||
<ul className="list-disc ml-5 mt-1">
|
||||
<li>The component has not been saved to the database</li>
|
||||
<li>The component name is misspelled</li>
|
||||
<li>There was an error compiling the component</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<div className="text-sm font-semibold">Error rendering {name}</div>
|
||||
<div className="text-xs mt-2 whitespace-pre-wrap">{String(error)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
[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])
|
||||
|
||||
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
|
||||
// This approach avoids TypeScript errors
|
||||
type ReactDOMType = {
|
||||
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
|
||||
|
||||
// Get all lowercase component names from registry and add them to a Set for faster lookup
|
||||
const lowercaseComponentNames = new Set<string>()
|
||||
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 (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]
|
||||
|
||||
// Create CSS selector for all lowercase component tags
|
||||
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
|
||||
|
||||
// Process each element
|
||||
elements.forEach((element) => {
|
||||
// Skip if already processed
|
||||
if (element.hasAttribute('data-component-processed')) return
|
||||
|
||||
try {
|
||||
// Mark as processed to avoid infinite loops
|
||||
element.setAttribute('data-component-processed', 'true')
|
||||
|
||||
// Get the tag name in lowercase
|
||||
const tagName = element.tagName.toLowerCase()
|
||||
|
||||
// Get the component from registry
|
||||
const Component = compiledComponents[tagName]
|
||||
if (!Component) {
|
||||
console.warn(`Component ${tagName} exists in registry but couldn't be loaded`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get props from element attributes
|
||||
const props: Record<string, unknown> = {}
|
||||
Array.from(element.attributes).forEach((attr) => {
|
||||
// Skip data-component-processed attribute
|
||||
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
|
||||
|
||||
// Handle boolean attributes (present without value)
|
||||
if (propValue === '' || propValue === propName) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
props[propName] = propValue
|
||||
})
|
||||
|
||||
// Process children
|
||||
const children = Array.from(element.childNodes).map((child) => {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
return child.textContent
|
||||
}
|
||||
return child
|
||||
})
|
||||
|
||||
if (children.length) {
|
||||
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
|
||||
|
||||
// Insert wrapper and remove original
|
||||
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
|
||||
|
||||
if (ReactDOM) {
|
||||
// Use modern createRoot API if available (React 18+)
|
||||
if (ReactDOM.createRoot) {
|
||||
const root = ReactDOM.createRoot(wrapper)
|
||||
root.render(reactElement)
|
||||
}
|
||||
// Fallback to legacy render API
|
||||
else if (ReactDOM.render) {
|
||||
ReactDOM.render(reactElement, wrapper)
|
||||
}
|
||||
} else {
|
||||
console.error('ReactDOM not found in window - cannot render custom components')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error rendering ${tagName} React component:`, err)
|
||||
|
||||
// Show error UI in place of the component
|
||||
wrapper.innerHTML = `<div style="border: 1px solid red; color: red; padding: 8px;">
|
||||
Error rendering <${tagName}> component
|
||||
</div>`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${element.tagName} element:`, error)
|
||||
}
|
||||
})
|
||||
|
||||
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) => {
|
||||
if (mutation.type === 'childList') {
|
||||
// Process added nodes
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Process this node and its children
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
// Start observing the entire document
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
})
|
||||
|
||||
// Initial scan of the whole document
|
||||
if (document.body) {
|
||||
processNode(document.body)
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [compiledComponents])
|
||||
|
||||
const value = {
|
||||
renderComponent,
|
||||
compileAndRender,
|
||||
isComponentRegistered,
|
||||
getRegisteredComponents,
|
||||
getComponentCode,
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentRegistryContext.Provider value={value}>{children}</ComponentRegistryContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComponentRegistryProvider
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { RouteDto } from '@/proxy/routes/models'
|
||||
import { lazy } from 'react'
|
||||
import { useComponents } from '@/contexts/ComponentContext'
|
||||
|
||||
// Tüm view bileşenlerini import et (vite özel)
|
||||
// shared klasörü hariç, çünkü bu bileşenler genellikle başka yerlerde statik import ediliyor
|
||||
|
|
@ -7,18 +8,32 @@ const modules = import.meta.glob(['../views/**/*.tsx', '!../views/shared/**/*.ts
|
|||
|
||||
const lazyComponentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>()
|
||||
|
||||
export function loadComponent(componentPath: string) {
|
||||
// ComponentPath'in fiziksel mi yoksa dinamik mi olduğunu belirle
|
||||
function isPhysicalComponent(componentPath: string): boolean {
|
||||
// @ ile başlayan path'ler fiziksel dosya yolu
|
||||
// Başka bir kural: dynamic: ile başlayan path'ler dinamik
|
||||
return componentPath.startsWith('@/') || componentPath.startsWith('../')
|
||||
}
|
||||
|
||||
function isDynamicComponent(componentPath: string): boolean {
|
||||
// dynamic: ile başlayan path'ler dinamik komponent
|
||||
return componentPath.startsWith('dynamic:')
|
||||
}
|
||||
|
||||
// Fiziksel komponent yükleme (mevcut mantık)
|
||||
function loadPhysicalComponent(componentPath: string) {
|
||||
const cleanedPath = componentPath.replace(/^@\//, '')
|
||||
const fullPath = `../${cleanedPath}.tsx`
|
||||
|
||||
if (lazyComponentCache.has(fullPath)) {
|
||||
// console.log(`Physical component loaded from cache: ${fullPath}`)
|
||||
return lazyComponentCache.get(fullPath)!
|
||||
}
|
||||
|
||||
const loader = modules[fullPath]
|
||||
if (!loader) {
|
||||
console.error(`Component not found for path: ${fullPath}`)
|
||||
throw new Error(`Component not found for path: ${fullPath}`)
|
||||
console.error(`Physical component not found for path: ${fullPath}`)
|
||||
throw new Error(`Physical component not found for path: ${fullPath}`)
|
||||
}
|
||||
|
||||
const LazyComponent = lazy(loader as () => Promise<{ default: React.ComponentType<any> }>)
|
||||
|
|
@ -26,13 +41,79 @@ export function loadComponent(componentPath: string) {
|
|||
return LazyComponent
|
||||
}
|
||||
|
||||
// Dinamik komponent yükleme (yeni mantık)
|
||||
function loadDynamicComponent(
|
||||
componentPath: string,
|
||||
registeredComponents: Record<string, React.ComponentType<unknown>>,
|
||||
renderComponent?: (name: string, props?: any) => React.ReactNode,
|
||||
isComponentRegistered?: (name: string) => boolean
|
||||
) {
|
||||
const componentName = componentPath.replace('dynamic:', '')
|
||||
|
||||
if (lazyComponentCache.has(componentPath)) {
|
||||
// console.log(`Dynamic component loaded from cache: ${componentName}`)
|
||||
return lazyComponentCache.get(componentPath)!
|
||||
}
|
||||
|
||||
// Önce manuel registered komponentleri kontrol et
|
||||
let DynamicComponent = registeredComponents[componentName]
|
||||
|
||||
// Eğer manuel registered'da yoksa, database compiled komponentleri kontrol et
|
||||
if (!DynamicComponent && isComponentRegistered && renderComponent && isComponentRegistered(componentName)) {
|
||||
// console.log(`Database component found: ${componentName}`)
|
||||
// Database komponentini wrapper ile kullan
|
||||
DynamicComponent = (props: any) => renderComponent(componentName, props) as React.ReactElement
|
||||
}
|
||||
|
||||
if (!DynamicComponent) {
|
||||
console.error(`Dynamic component not found: ${componentName}`)
|
||||
console.log('Available registered components:', Object.keys(registeredComponents))
|
||||
if (isComponentRegistered) {
|
||||
console.log('Database component registry available - checking...')
|
||||
}
|
||||
throw new Error(`Dynamic component not found: ${componentName}`)
|
||||
}
|
||||
|
||||
// console.log(`Dynamic component loaded: ${componentName}`)
|
||||
// Dinamik komponent için lazy wrapper oluştur
|
||||
const LazyComponent = lazy(() => Promise.resolve({ default: DynamicComponent as React.ComponentType<any> }))
|
||||
lazyComponentCache.set(componentPath, LazyComponent)
|
||||
return LazyComponent
|
||||
}
|
||||
|
||||
export function loadComponent(
|
||||
componentPath: string,
|
||||
registeredComponents?: Record<string, React.ComponentType<unknown>>,
|
||||
renderComponent?: (name: string, props?: any) => React.ReactNode,
|
||||
isComponentRegistered?: (name: string) => boolean
|
||||
) {
|
||||
if (isPhysicalComponent(componentPath)) {
|
||||
return loadPhysicalComponent(componentPath)
|
||||
} else if (isDynamicComponent(componentPath)) {
|
||||
if (!registeredComponents) {
|
||||
throw new Error('Registered components required for dynamic component loading')
|
||||
}
|
||||
return loadDynamicComponent(componentPath, registeredComponents, renderComponent, isComponentRegistered)
|
||||
} else {
|
||||
// Backward compatibility: varsayılan olarak fiziksel komponent kabul et
|
||||
return loadPhysicalComponent(componentPath)
|
||||
}
|
||||
}
|
||||
|
||||
// React Router için uygun bir route tipi
|
||||
export interface DynamicReactRoute {
|
||||
key: string
|
||||
path: string
|
||||
getComponent: () => React.LazyExoticComponent<React.ComponentType<any>>
|
||||
getComponent: (
|
||||
registeredComponents?: Record<string, React.ComponentType<unknown>>,
|
||||
renderComponent?: (name: string, props?: any) => React.ReactNode,
|
||||
isComponentRegistered?: (name: string) => boolean
|
||||
) => React.LazyExoticComponent<React.ComponentType<any>>
|
||||
routeType: string
|
||||
authority?: string[]
|
||||
componentPath: string
|
||||
isPhysical: boolean
|
||||
isDynamic: boolean
|
||||
}
|
||||
|
||||
// API'den gelen route objesini, React Router için uygun hale getirir
|
||||
|
|
@ -40,8 +121,12 @@ export function mapDynamicRoutes(routes: RouteDto[]): DynamicReactRoute[] {
|
|||
return routes.map((route) => ({
|
||||
key: route.path,
|
||||
path: route.path,
|
||||
getComponent: () => loadComponent(route.componentPath),
|
||||
getComponent: (registeredComponents, renderComponent, isComponentRegistered) =>
|
||||
loadComponent(route.componentPath, registeredComponents, renderComponent, isComponentRegistered),
|
||||
routeType: route.routeType,
|
||||
authority: route.authority,
|
||||
componentPath: route.componentPath,
|
||||
isPhysical: isPhysicalComponent(route.componentPath),
|
||||
isDynamic: isDynamicComponent(route.componentPath),
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// DynamicRouter.tsx
|
||||
import React from 'react'
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { mapDynamicRoutes, loadComponent } from './dynamicRouteLoader'
|
||||
import { mapDynamicRoutes } from './dynamicRouteLoader'
|
||||
import { useDynamicRoutes } from './dynamicRoutesContext'
|
||||
import { useComponents } from '@/contexts/ComponentContext'
|
||||
import ProtectedRoute from '@/components/route/ProtectedRoute'
|
||||
import PermissionGuard from '@/components/route/PermissionGuard'
|
||||
import PageContainer from '@/components/template/PageContainer'
|
||||
|
|
@ -15,6 +16,8 @@ const NotFound = React.lazy(() => import('@/views/NotFound'))
|
|||
|
||||
export const DynamicRouter: React.FC = () => {
|
||||
const { routes, loading, error } = useDynamicRoutes()
|
||||
const { registeredComponents, renderComponent, isComponentRegistered } = useComponents()
|
||||
|
||||
const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes])
|
||||
|
||||
if (loading) return <div>Loading...</div>
|
||||
|
|
@ -27,7 +30,7 @@ export const DynamicRouter: React.FC = () => {
|
|||
{dynamicRoutes
|
||||
.filter((r) => r.routeType === 'protected')
|
||||
.map((route) => {
|
||||
const Component = route.getComponent()
|
||||
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered)
|
||||
return (
|
||||
<Route
|
||||
key={route.key}
|
||||
|
|
@ -79,7 +82,7 @@ export const DynamicRouter: React.FC = () => {
|
|||
hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected',
|
||||
)
|
||||
.map((route) => {
|
||||
const Component = route.getComponent()
|
||||
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered)
|
||||
return (
|
||||
<Route
|
||||
key={route.key}
|
||||
|
|
|
|||
Loading…
Reference in a new issue