Dinamik Route ve Dinamik Component düzenlemesi

This commit is contained in:
Sedat Öztürk 2025-10-30 22:23:59 +03:00
parent fe0a4b1c7c
commit a290556ba1
10 changed files with 399 additions and 902 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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,

View file

@ -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": [

View file

@ -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>

View file

@ -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) => (

View file

@ -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>

View file

@ -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 &lt;${tagName}&gt; 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

View file

@ -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),
})) }))
} }

View file

@ -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}