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": [
|
"LanguageTexts": [
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Product.ListComponent",
|
||||||
|
"en": "Product List Component",
|
||||||
|
"tr": "Ürün Liste Bileşeni"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform",
|
"key": "App.Platform",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
{
|
{
|
||||||
"Routes": [
|
"Routes": [
|
||||||
|
{
|
||||||
|
"key": "dynamic.ProductListComponent",
|
||||||
|
"path": "/admin/ProductListComponent",
|
||||||
|
"componentPath": "dynamic:ProductListComponent",
|
||||||
|
"routeType": "protected",
|
||||||
|
"authority": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "home",
|
"key": "home",
|
||||||
"path": "/home",
|
"path": "/home",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,15 @@
|
||||||
"MultiTenancySide": 3,
|
"MultiTenancySide": 3,
|
||||||
"MenuGroup": "Erp|Kurs"
|
"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",
|
"GroupName": "App.Saas",
|
||||||
"Name": "AbpTenantManagement.Tenants",
|
"Name": "AbpTenantManagement.Tenants",
|
||||||
|
|
@ -2239,7 +2248,7 @@
|
||||||
{
|
{
|
||||||
"GroupName": "App.Administration",
|
"GroupName": "App.Administration",
|
||||||
"Name": "App.DeveloperKit.CustomEndpoints",
|
"Name": "App.DeveloperKit.CustomEndpoints",
|
||||||
"ParentName": null,
|
"ParentName": "App.DeveloperKit",
|
||||||
"DisplayName": "App.DeveloperKit.CustomEndpoints",
|
"DisplayName": "App.DeveloperKit.CustomEndpoints",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 3,
|
"MultiTenancySide": 3,
|
||||||
|
|
|
||||||
|
|
@ -87,20 +87,20 @@
|
||||||
],
|
],
|
||||||
"CustomComponents": [
|
"CustomComponents": [
|
||||||
{
|
{
|
||||||
"name": "AxiosListComponent",
|
"name": "DynamicEntityComponent",
|
||||||
"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;",
|
"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,
|
"props": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "EntityListComponent",
|
"name": "ProductListComponent",
|
||||||
"code": "const EntityListComponent = ({\n title = \"Product\"\n}) => {\n return (\n <AxiosListComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default EntityListComponent;",
|
"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,
|
"props": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"dependencies": ["AxiosListComponent"]
|
"dependencies": ["DynamicEntityComponent"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ReportCategories": [
|
"ReportCategories": [
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { BrowserRouter } from 'react-router-dom'
|
||||||
import { store } from './store'
|
import { store } from './store'
|
||||||
import { DynamicRoutesProvider } from './routes/dynamicRoutesContext'
|
import { DynamicRoutesProvider } from './routes/dynamicRoutesContext'
|
||||||
import { ComponentProvider } from './contexts/ComponentContext'
|
import { ComponentProvider } from './contexts/ComponentContext'
|
||||||
import ComponentRegistryProvider from './contexts/ComponentRegistryContext'
|
|
||||||
import { registerServiceWorker } from './views/version/swRegistration'
|
import { registerServiceWorker } from './views/version/swRegistration'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
|
@ -22,13 +21,11 @@ function App() {
|
||||||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
<DynamicRoutesProvider>
|
<DynamicRoutesProvider>
|
||||||
<ComponentProvider>
|
<ComponentProvider>
|
||||||
<ComponentRegistryProvider>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Theme>
|
<Theme>
|
||||||
<Layout />
|
<Layout />
|
||||||
</Theme>
|
</Theme>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ComponentRegistryProvider>
|
|
||||||
</ComponentProvider>
|
</ComponentProvider>
|
||||||
</DynamicRoutesProvider>
|
</DynamicRoutesProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ const EntityManager: React.FC = () => {
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="bg-slate-50 rounded-lg p-3">
|
<div className="bg-slate-50 rounded-lg p-3">
|
||||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||||
{translate('::App.DeveloperKit.Entity.FieldsLabel')}:
|
{translate('::App.DeveloperKit.Entity.FieldLabel')}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
{entity.fields.slice(0, 4).map((field) => (
|
{entity.fields.slice(0, 4).map((field) => (
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,53 @@ import {
|
||||||
} from '@/proxy/developerKit/models'
|
} from '@/proxy/developerKit/models'
|
||||||
import { developerKitService } from '@/services/developerKit.service'
|
import { developerKitService } from '@/services/developerKit.service'
|
||||||
import { useStoreState } from '@/store/store'
|
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 {
|
interface ComponentContextType {
|
||||||
components: CustomComponent[]
|
components: CustomComponent[]
|
||||||
|
|
@ -17,8 +63,15 @@ interface ComponentContextType {
|
||||||
getComponent: (id: string) => CustomComponent | undefined
|
getComponent: (id: string) => CustomComponent | undefined
|
||||||
getComponentByName: (name: string) => CustomComponent | undefined
|
getComponentByName: (name: string) => CustomComponent | undefined
|
||||||
refreshComponents: () => Promise<void>
|
refreshComponents: () => Promise<void>
|
||||||
|
// Manual registered components
|
||||||
registeredComponents: Record<string, React.ComponentType<unknown>>
|
registeredComponents: Record<string, React.ComponentType<unknown>>
|
||||||
registerComponent: (name: string, component: React.ComponentType<unknown>) => void
|
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)
|
const ComponentContext = createContext<ComponentContextType | undefined>(undefined)
|
||||||
|
|
@ -40,6 +93,9 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const [registeredComponents, setRegisteredComponents] = useState<
|
const [registeredComponents, setRegisteredComponents] = useState<
|
||||||
Record<string, React.ComponentType<unknown>>
|
Record<string, React.ComponentType<unknown>>
|
||||||
>({})
|
>({})
|
||||||
|
const [compiledComponents, setCompiledComponents] = useState<
|
||||||
|
Record<string, React.ComponentType<ComponentProps>>
|
||||||
|
>({})
|
||||||
|
|
||||||
const refreshComponents = async () => {
|
const refreshComponents = async () => {
|
||||||
try {
|
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 = {
|
const value = {
|
||||||
components,
|
components,
|
||||||
loading,
|
loading,
|
||||||
|
|
@ -131,6 +394,11 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
refreshComponents,
|
refreshComponents,
|
||||||
registeredComponents,
|
registeredComponents,
|
||||||
registerComponent,
|
registerComponent,
|
||||||
|
renderComponent,
|
||||||
|
compileAndRender,
|
||||||
|
isComponentRegistered,
|
||||||
|
getRegisteredComponents,
|
||||||
|
getComponentCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ComponentContext.Provider value={value}>{children}</ComponentContext.Provider>
|
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 { RouteDto } from '@/proxy/routes/models'
|
||||||
import { lazy } from 'react'
|
import { lazy } from 'react'
|
||||||
|
import { useComponents } from '@/contexts/ComponentContext'
|
||||||
|
|
||||||
// Tüm view bileşenlerini import et (vite özel)
|
// Tüm view bileşenlerini import et (vite özel)
|
||||||
// shared klasörü hariç, çünkü bu bileşenler genellikle başka yerlerde statik import ediliyor
|
// 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>>>()
|
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 cleanedPath = componentPath.replace(/^@\//, '')
|
||||||
const fullPath = `../${cleanedPath}.tsx`
|
const fullPath = `../${cleanedPath}.tsx`
|
||||||
|
|
||||||
if (lazyComponentCache.has(fullPath)) {
|
if (lazyComponentCache.has(fullPath)) {
|
||||||
|
// console.log(`Physical component loaded from cache: ${fullPath}`)
|
||||||
return lazyComponentCache.get(fullPath)!
|
return lazyComponentCache.get(fullPath)!
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = modules[fullPath]
|
const loader = modules[fullPath]
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
console.error(`Component not found for path: ${fullPath}`)
|
console.error(`Physical component not found for path: ${fullPath}`)
|
||||||
throw new Error(`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> }>)
|
const LazyComponent = lazy(loader as () => Promise<{ default: React.ComponentType<any> }>)
|
||||||
|
|
@ -26,13 +41,79 @@ export function loadComponent(componentPath: string) {
|
||||||
return LazyComponent
|
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
|
// React Router için uygun bir route tipi
|
||||||
export interface DynamicReactRoute {
|
export interface DynamicReactRoute {
|
||||||
key: string
|
key: string
|
||||||
path: 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
|
routeType: string
|
||||||
authority?: string[]
|
authority?: string[]
|
||||||
|
componentPath: string
|
||||||
|
isPhysical: boolean
|
||||||
|
isDynamic: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// API'den gelen route objesini, React Router için uygun hale getirir
|
// 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) => ({
|
return routes.map((route) => ({
|
||||||
key: route.path,
|
key: route.path,
|
||||||
path: route.path,
|
path: route.path,
|
||||||
getComponent: () => loadComponent(route.componentPath),
|
getComponent: (registeredComponents, renderComponent, isComponentRegistered) =>
|
||||||
|
loadComponent(route.componentPath, registeredComponents, renderComponent, isComponentRegistered),
|
||||||
routeType: route.routeType,
|
routeType: route.routeType,
|
||||||
authority: route.authority,
|
authority: route.authority,
|
||||||
|
componentPath: route.componentPath,
|
||||||
|
isPhysical: isPhysicalComponent(route.componentPath),
|
||||||
|
isDynamic: isDynamicComponent(route.componentPath),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// DynamicRouter.tsx
|
// DynamicRouter.tsx
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { mapDynamicRoutes, loadComponent } from './dynamicRouteLoader'
|
import { mapDynamicRoutes } from './dynamicRouteLoader'
|
||||||
import { useDynamicRoutes } from './dynamicRoutesContext'
|
import { useDynamicRoutes } from './dynamicRoutesContext'
|
||||||
|
import { useComponents } from '@/contexts/ComponentContext'
|
||||||
import ProtectedRoute from '@/components/route/ProtectedRoute'
|
import ProtectedRoute from '@/components/route/ProtectedRoute'
|
||||||
import PermissionGuard from '@/components/route/PermissionGuard'
|
import PermissionGuard from '@/components/route/PermissionGuard'
|
||||||
import PageContainer from '@/components/template/PageContainer'
|
import PageContainer from '@/components/template/PageContainer'
|
||||||
|
|
@ -15,6 +16,8 @@ const NotFound = React.lazy(() => import('@/views/NotFound'))
|
||||||
|
|
||||||
export const DynamicRouter: React.FC = () => {
|
export const DynamicRouter: React.FC = () => {
|
||||||
const { routes, loading, error } = useDynamicRoutes()
|
const { routes, loading, error } = useDynamicRoutes()
|
||||||
|
const { registeredComponents, renderComponent, isComponentRegistered } = useComponents()
|
||||||
|
|
||||||
const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes])
|
const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes])
|
||||||
|
|
||||||
if (loading) return <div>Loading...</div>
|
if (loading) return <div>Loading...</div>
|
||||||
|
|
@ -27,7 +30,7 @@ export const DynamicRouter: React.FC = () => {
|
||||||
{dynamicRoutes
|
{dynamicRoutes
|
||||||
.filter((r) => r.routeType === 'protected')
|
.filter((r) => r.routeType === 'protected')
|
||||||
.map((route) => {
|
.map((route) => {
|
||||||
const Component = route.getComponent()
|
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered)
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
key={route.key}
|
key={route.key}
|
||||||
|
|
@ -79,7 +82,7 @@ export const DynamicRouter: React.FC = () => {
|
||||||
hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected',
|
hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected',
|
||||||
)
|
)
|
||||||
.map((route) => {
|
.map((route) => {
|
||||||
const Component = route.getComponent()
|
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered)
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
key={route.key}
|
key={route.key}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue