CustomComponents hataları

This commit is contained in:
Sedat ÖZTÜRK 2025-08-11 13:48:36 +03:00
parent 8cc8ed07f9
commit 64ccc150df
8 changed files with 1000 additions and 921 deletions

View file

@ -18,7 +18,7 @@ services:
- ASPNETCORE_ENVIRONMENT=Dev - ASPNETCORE_ENVIRONMENT=Dev
- SEED=${SEED} - SEED=${SEED}
networks: networks:
- kurs-platform-data_db - db
# Backend API # Backend API
api: api:
@ -33,7 +33,7 @@ services:
- cdn:/etc/api/cdn - cdn:/etc/api/cdn
- api-keys:/root/.aspnet/DataProtection-Keys - api-keys:/root/.aspnet/DataProtection-Keys
networks: networks:
- kurs-platform-data_db - db
- default - default
# Frontend (UI) # Frontend (UI)

View file

@ -3,7 +3,8 @@ name: kurs-platform-data
networks: networks:
db: db:
external: false external: true
name: kurs-platform-data_db
volumes: volumes:
pg: pg:

View file

@ -8,16 +8,21 @@ export interface ComponentPreviewProps {
} }
const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => { const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => {
const { components } = useComponents() const { components, loading } = useComponents()
if (!componentName) { if (!componentName) {
return <div className="text-sm text-gray-500">Bileşen ismi yok.</div> return <div className="text-sm text-gray-500">Bileşen ismi yok.</div>
} }
// components dizisinin varlığını kontrol et
if (loading || !components || !Array.isArray(components)) {
return <div className="text-sm text-gray-500">Bileşenler yükleniyor...</div>
}
// Belirtilen bileşeni bul // Belirtilen bileşeni bul
const component = components.find((c) => c.name === componentName && c.isActive) const component = components.find((c) => c.name === componentName && c.isActive)
let dependencies: string[] = []; let dependencies: string[] = []
if (component?.dependencies) { if (component?.dependencies) {
try { try {

File diff suppressed because it is too large Load diff

View file

@ -59,10 +59,10 @@ const ComponentEditor: React.FC = () => {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (code && editorState.components.length === 0) { if (code && editorState.components?.length === 0) {
parseAndUpdateComponents(code) parseAndUpdateComponents(code)
} }
}, [code, editorState.components.length]) }, [code, editorState.components?.length])
// Load existing component data - sadece edit modunda // Load existing component data - sadece edit modunda
useEffect(() => { useEffect(() => {
@ -144,7 +144,9 @@ const ComponentEditor: React.FC = () => {
<div className="h-screen flex items-center justify-center"> <div className="h-screen flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<RefreshCw className="w-8 h-8 text-blue-500 animate-spin mx-auto mb-3" /> <RefreshCw className="w-8 h-8 text-blue-500 animate-spin mx-auto mb-3" />
<p className="text-slate-600">{translate('::App.DeveloperKit.ComponentEditor.Loading')}</p> <p className="text-slate-600">
{translate('::App.DeveloperKit.ComponentEditor.Loading')}
</p>
</div> </div>
</div> </div>
) )
@ -220,7 +222,9 @@ const ComponentEditor: React.FC = () => {
className="flex items-center gap-2 bg-yellow-600 text-white px-4 py-2 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" className="flex items-center gap-2 bg-yellow-600 text-white px-4 py-2 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
> >
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
{isSaving ? translate('::App.DeveloperKit.ComponentEditor.Saving') : translate('::App.DeveloperKit.ComponentEditor.Save')} {isSaving
? translate('::App.DeveloperKit.ComponentEditor.Saving')
: translate('::App.DeveloperKit.ComponentEditor.Save')}
</button> </button>
</div> </div>
</div> </div>
@ -237,19 +241,26 @@ const ComponentEditor: React.FC = () => {
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-red-800 font-medium mb-2"> <p className="text-sm text-red-800 font-medium mb-2">
{validationErrors.length} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')} {validationErrors.length}{' '}
{validationErrors.length !== 1 ? 's' : ''} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')}
{validationErrors.length !== 1 ? 's' : ''}{' '}
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')}
</p> </p>
<div className="space-y-1"> <div className="space-y-1">
{validationErrors.slice(0, 5).map((error, index) => ( {validationErrors.slice(0, 5).map((error, index) => (
<div key={index} className="text-sm text-red-700"> <div key={index} className="text-sm text-red-700">
<span className="font-medium">{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Line')} {error.startLineNumber}:</span>{' '} <span className="font-medium">
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Line')}{' '}
{error.startLineNumber}:
</span>{' '}
{error.message} {error.message}
</div> </div>
))} ))}
{validationErrors.length > 5 && ( {validationErrors.length > 5 && (
<div className="text-sm text-red-600 italic"> <div className="text-sm text-red-600 italic">
... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')} {validationErrors.length - 5} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')} ... {translate('::App.DeveloperKit.ComponentEditor.ValidationError.And')}{' '}
{validationErrors.length - 5}{' '}
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.More')}
{validationErrors.length - 5 !== 1 ? 's' : ''} {validationErrors.length - 5 !== 1 ? 's' : ''}
</div> </div>
)} )}

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from 'react'
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom'
import { useComponents } from "../../contexts/ComponentContext"; import { useComponents } from '../../contexts/ComponentContext'
import { import {
Plus, Plus,
Search, Search,
@ -14,21 +14,19 @@ import {
CheckCircle, CheckCircle,
XCircle, XCircle,
View, View,
} from "lucide-react"; } from 'lucide-react'
import { ROUTES_ENUM } from "@/routes/route.constant"; import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from "@/utils/hooks/useLocalization"; import { useLocalization } from '@/utils/hooks/useLocalization'
const ComponentManager: React.FC = () => { const ComponentManager: React.FC = () => {
const { components, updateComponent, deleteComponent } = useComponents(); const { components, loading, updateComponent, deleteComponent } = useComponents()
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState('')
const [filterActive, setFilterActive] = useState< const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all')
"all" | "active" | "inactive"
>("all");
// Calculate statistics // Calculate statistics
const totalComponents = components?.length; const totalComponents = components?.length || 0
const activeComponents = components?.filter((c) => c.isActive).length; const activeComponents = components?.filter((c) => c.isActive).length || 0
const inactiveComponents = totalComponents - activeComponents; const inactiveComponents = totalComponents - activeComponents
const { translate } = useLocalization() const { translate } = useLocalization()
const stats = [ const stats = [
@ -36,62 +34,58 @@ const ComponentManager: React.FC = () => {
name: translate('::App.DeveloperKit.Component.Total'), name: translate('::App.DeveloperKit.Component.Total'),
value: totalComponents, value: totalComponents,
icon: Puzzle, icon: Puzzle,
color: "text-purple-600", color: 'text-purple-600',
bgColor: "bg-purple-100", bgColor: 'bg-purple-100',
}, },
{ {
name: translate('::App.DeveloperKit.Component.Active'), name: translate('::App.DeveloperKit.Component.Active'),
value: activeComponents, value: activeComponents,
icon: CheckCircle, icon: CheckCircle,
color: "text-emerald-600", color: 'text-emerald-600',
bgColor: "bg-emerald-100", bgColor: 'bg-emerald-100',
}, },
{ {
name: translate('::App.DeveloperKit.Component.Inactive'), name: translate('::App.DeveloperKit.Component.Inactive'),
value: inactiveComponents, value: inactiveComponents,
icon: XCircle, icon: XCircle,
color: "text-slate-600", color: 'text-slate-600',
bgColor: "bg-slate-100", bgColor: 'bg-slate-100',
}, },
]; ]
const filteredComponents = components?.filter((component) => { const filteredComponents = components?.filter((component) => {
const matchesSearch = const matchesSearch =
component.name.toLowerCase().includes(searchTerm.toLowerCase()) || component.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(component.description || "") (component.description || '').toLowerCase().includes(searchTerm.toLowerCase())
.toLowerCase()
.includes(searchTerm.toLowerCase());
const matchesFilter = const matchesFilter =
filterActive === "all" || filterActive === 'all' ||
(filterActive === "active" && component.isActive) || (filterActive === 'active' && component.isActive) ||
(filterActive === "inactive" && !component.isActive); (filterActive === 'inactive' && !component.isActive)
return matchesSearch && matchesFilter; return matchesSearch && matchesFilter
}); })
const handleToggleActive = async (id: string, isActive: boolean) => { const handleToggleActive = async (id: string, isActive: boolean) => {
try { try {
const component = components.find((c) => c.id === id); const component = components?.find((c) => c.id === id)
if (component) { if (component) {
await updateComponent(id, { ...component, isActive }); await updateComponent(id, { ...component, isActive })
} }
} catch (err) { } catch (err) {
console.error("Failed to toggle component status:", err); console.error('Failed to toggle component status:', err)
}
} }
};
const handleDelete = async (id: string, name: string) => { const handleDelete = async (id: string, name: string) => {
if ( if (window.confirm(translate('::App.DeveloperKit.Component.ConfirmDelete'))) {
window.confirm(translate('::App.DeveloperKit.Component.ConfirmDelete'))
) {
try { try {
await deleteComponent(id); await deleteComponent(id)
} catch (err) { } catch (err) {
console.error("Failed to delete component:", err); console.error('Failed to delete component:', err)
}
} }
} }
};
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@ -114,18 +108,11 @@ const ComponentManager: React.FC = () => {
{/* Statistics Cards */} {/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
{stats.map((stat, index) => ( {stats.map((stat, index) => (
<div <div key={index} className="bg-white rounded-lg border border-slate-200 p-6">
key={index}
className="bg-white rounded-lg border border-slate-200 p-6"
>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-slate-600 mb-1"> <p className="text-sm font-medium text-slate-600 mb-1">{stat.name}</p>
{stat.name} <p className="text-3xl font-bold text-slate-900">{stat.value}</p>
</p>
<p className="text-3xl font-bold text-slate-900">
{stat.value}
</p>
</div> </div>
<div className={`p-3 rounded-lg ${stat.bgColor}`}> <div className={`p-3 rounded-lg ${stat.bgColor}`}>
<stat.icon className={`w-6 h-6 ${stat.color}`} /> <stat.icon className={`w-6 h-6 ${stat.color}`} />
@ -152,21 +139,27 @@ const ComponentManager: React.FC = () => {
<Filter className="w-5 h-5 text-slate-500" /> <Filter className="w-5 h-5 text-slate-500" />
<select <select
value={filterActive} value={filterActive}
onChange={(e) => onChange={(e) => setFilterActive(e.target.value as 'all' | 'active' | 'inactive')}
setFilterActive(e.target.value as "all" | "active" | "inactive")
}
className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="all">{translate('::App.DeveloperKit.Component.Filter.All')}</option> <option value="all">{translate('::App.DeveloperKit.Component.Filter.All')}</option>
<option value="active">{translate('::App.DeveloperKit.Component.Filter.Active')}</option> <option value="active">
<option value="inactive">{translate('::App.DeveloperKit.Component.Filter.Inactive')}</option> {translate('::App.DeveloperKit.Component.Filter.Active')}
</option>
<option value="inactive">
{translate('::App.DeveloperKit.Component.Filter.Inactive')}
</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
{/* Components List */} {/* Components List */}
{filteredComponents?.length > 0 ? ( {loading ? (
<div className="flex items-center justify-center py-12">
<div className="text-slate-600">Bileşenler yükleniyor...</div>
</div>
) : filteredComponents?.length > 0 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredComponents.map((component) => ( {filteredComponents.map((component) => (
<div <div
@ -177,43 +170,35 @@ const ComponentManager: React.FC = () => {
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-slate-900"> <h3 className="text-lg font-semibold text-slate-900">{component.name}</h3>
{component.name}
</h3>
<div <div
className={`w-2 h-2 rounded-full ${ className={`w-2 h-2 rounded-full ${
component.isActive ? "bg-green-500" : "bg-slate-300" component.isActive ? 'bg-green-500' : 'bg-slate-300'
}`} }`}
/> />
</div> </div>
<p className="text-slate-600 text-sm mb-3"> <p className="text-slate-600 text-sm mb-3">
{(() => { {(() => {
try { try {
const parsed = JSON.parse( const parsed = JSON.parse(component.dependencies ?? '[]')
component.dependencies ?? "[]"
);
return Array.isArray(parsed) && parsed.length > 0 return Array.isArray(parsed) && parsed.length > 0
? `${parsed.join(", ")}` ? `${parsed.join(', ')}`
: translate('::App.DeveloperKit.Component.NoDependencies'); : translate('::App.DeveloperKit.Component.NoDependencies')
} catch { } catch {
return translate('::App.DeveloperKit.Component.NoDependencies'); return translate('::App.DeveloperKit.Component.NoDependencies')
} }
})()} })()}
</p> </p>
{component.description && ( {component.description && (
<p className="text-slate-600 text-sm mb-3"> <p className="text-slate-600 text-sm mb-3">{component.description}</p>
{component.description}
</p>
)} )}
<div className="flex items-center gap-4 text-xs text-slate-500"> <div className="flex items-center gap-4 text-xs text-slate-500">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="w-3 h-3" /> <Calendar className="w-3 h-3" />
<span> <span>
{component.lastModificationTime {component.lastModificationTime
? new Date( ? new Date(component.lastModificationTime).toLocaleDateString()
component.lastModificationTime
).toLocaleDateString()
: translate('::App.DeveloperKit.Component.DateNotAvailable')} : translate('::App.DeveloperKit.Component.DateNotAvailable')}
</span> </span>
</div> </div>
@ -237,13 +222,11 @@ const ComponentManager: React.FC = () => {
<div className="flex items-center justify-between pt-4 border-t border-slate-100"> <div className="flex items-center justify-between pt-4 border-t border-slate-100">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => onClick={() => handleToggleActive(component.id, !component.isActive)}
handleToggleActive(component.id, !component.isActive)
}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors ${ className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors ${
component.isActive component.isActive
? "bg-green-100 text-green-700 hover:bg-green-200" ? 'bg-green-100 text-green-700 hover:bg-green-200'
: "bg-slate-100 text-slate-600 hover:bg-slate-200" : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`} }`}
> >
{component.isActive ? ( {component.isActive ? (
@ -261,7 +244,10 @@ const ComponentManager: React.FC = () => {
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Link <Link
to={ROUTES_ENUM.protected.saas.developerKitComponentsEdit.replace(':id', component.id)} to={ROUTES_ENUM.protected.saas.developerKitComponentsEdit.replace(
':id',
component.id,
)}
target="_blank" target="_blank"
className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors" className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title={translate('::App.DeveloperKit.Component.Edit')} title={translate('::App.DeveloperKit.Component.Edit')}
@ -269,7 +255,10 @@ const ComponentManager: React.FC = () => {
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Link> </Link>
<Link <Link
to={ROUTES_ENUM.protected.saas.developerKitComponentsView.replace(':id', component.id)} to={ROUTES_ENUM.protected.saas.developerKitComponentsView.replace(
':id',
component.id,
)}
className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors" className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title={translate('::App.DeveloperKit.Component.View')} title={translate('::App.DeveloperKit.Component.View')}
> >
@ -295,16 +284,16 @@ const ComponentManager: React.FC = () => {
<Plus className="w-8 h-8 text-slate-500" /> <Plus className="w-8 h-8 text-slate-500" />
</div> </div>
<h3 className="text-lg font-medium text-slate-900 mb-2"> <h3 className="text-lg font-medium text-slate-900 mb-2">
{searchTerm || filterActive !== "all" {searchTerm || filterActive !== 'all'
? translate('::App.DeveloperKit.Component.Empty.Filtered.Title') ? translate('::App.DeveloperKit.Component.Empty.Filtered.Title')
: translate('::App.DeveloperKit.Component.Empty.Initial.Title')} : translate('::App.DeveloperKit.Component.Empty.Initial.Title')}
</h3> </h3>
<p className="text-slate-600 mb-6"> <p className="text-slate-600 mb-6">
{searchTerm || filterActive !== "all" {searchTerm || filterActive !== 'all'
? translate('::App.DeveloperKit.Component.Empty.Filtered.Description') ? translate('::App.DeveloperKit.Component.Empty.Filtered.Description')
: translate('::App.DeveloperKit.Component.Empty.Initial.Description')} : translate('::App.DeveloperKit.Component.Empty.Initial.Description')}
</p> </p>
{!searchTerm && filterActive === "all" && ( {!searchTerm && filterActive === 'all' && (
<Link <Link
to={ROUTES_ENUM.protected.saas.developerKitComponentsNew} to={ROUTES_ENUM.protected.saas.developerKitComponentsNew}
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors" className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
@ -317,7 +306,7 @@ const ComponentManager: React.FC = () => {
</div> </div>
)} )}
</div> </div>
); )
}; }
export default ComponentManager; export default ComponentManager

View file

@ -1,116 +1,123 @@
import { CreateUpdateCustomComponentDto, CustomComponent, CustomComponentDto } from '@/proxy/developerKit/models'; import {
import { developerKitService } from '@/services/developerKit.service'; CreateUpdateCustomComponentDto,
import { useStoreState } from '@/store/store'; CustomComponent,
import React, { createContext, useContext, useState, useEffect } from 'react'; CustomComponentDto,
} from '@/proxy/developerKit/models'
import { developerKitService } from '@/services/developerKit.service'
import { useStoreState } from '@/store/store'
import React, { createContext, useContext, useState, useEffect } from 'react'
interface ComponentContextType { interface ComponentContextType {
components: CustomComponent[]; components: CustomComponent[]
loading: boolean; loading: boolean
error: string | null; error: string | null
addComponent: (component: CreateUpdateCustomComponentDto) => Promise<void>; addComponent: (component: CreateUpdateCustomComponentDto) => Promise<void>
updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise<void>; updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise<void>
deleteComponent: (id: string) => Promise<void>; deleteComponent: (id: string) => Promise<void>
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>
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
} }
const ComponentContext = createContext<ComponentContextType | undefined>(undefined); const ComponentContext = createContext<ComponentContextType | undefined>(undefined)
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export const useComponents = () => { export const useComponents = () => {
const context = useContext(ComponentContext); const context = useContext(ComponentContext)
if (context === undefined) { if (context === undefined) {
throw new Error('useComponents must be used within a ComponentProvider'); throw new Error('useComponents must be used within a ComponentProvider')
}
return context
} }
return context;
};
export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const extraProperties = useStoreState((state) => state.abpConfig?.config?.extraProperties) const extraProperties = useStoreState((state) => state.abpConfig?.config?.extraProperties)
const [components, setComponents] = useState<CustomComponent[]>([]); const [components, setComponents] = useState<CustomComponent[]>([])
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null)
const [registeredComponents, setRegisteredComponents] = useState<Record<string, React.ComponentType<unknown>>>({}); const [registeredComponents, setRegisteredComponents] = useState<
Record<string, React.ComponentType<unknown>>
>({})
const refreshComponents = async () => { const refreshComponents = async () => {
try { try {
setLoading(true); setLoading(true)
setError(null); setError(null)
const customComponents = extraProperties?.customComponents as CustomComponentDto[] const customComponents = extraProperties?.customComponents as CustomComponentDto[]
setComponents(customComponents); setComponents(customComponents || [])
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch components'); setError(err instanceof Error ? err.message : 'Failed to fetch components')
console.error('Failed to fetch components:', err); console.error('Failed to fetch components:', err)
setComponents([])
} finally { } finally {
setLoading(false); setLoading(false)
}
} }
};
useEffect(() => { useEffect(() => {
refreshComponents(); refreshComponents()
}, []); }, [extraProperties])
const addComponent = async (componentData: CreateUpdateCustomComponentDto) => { const addComponent = async (componentData: CreateUpdateCustomComponentDto) => {
try { try {
setLoading(true); setLoading(true)
setError(null); setError(null)
const newComponent = await developerKitService.createCustomComponent(componentData); const newComponent = await developerKitService.createCustomComponent(componentData)
setComponents(prev => [...prev, newComponent]); setComponents((prev) => [...prev, newComponent])
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create component'); setError(err instanceof Error ? err.message : 'Failed to create component')
throw err; throw err
} finally { } finally {
setLoading(false); setLoading(false)
}
} }
};
const updateComponent = async (id: string, componentData: CreateUpdateCustomComponentDto) => { const updateComponent = async (id: string, componentData: CreateUpdateCustomComponentDto) => {
try { try {
setLoading(true); setLoading(true)
setError(null); setError(null)
const updatedComponent = await developerKitService.updateCustomComponent(id, componentData); const updatedComponent = await developerKitService.updateCustomComponent(id, componentData)
setComponents(prev => prev.map(component => setComponents((prev) =>
component.id === id ? updatedComponent : component prev.map((component) => (component.id === id ? updatedComponent : component)),
)); )
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update component'); setError(err instanceof Error ? err.message : 'Failed to update component')
throw err; throw err
} finally { } finally {
setLoading(false); setLoading(false)
}
} }
};
const deleteComponent = async (id: string) => { const deleteComponent = async (id: string) => {
try { try {
setLoading(true); setLoading(true)
setError(null); setError(null)
await developerKitService.deleteCustomComponent(id); await developerKitService.deleteCustomComponent(id)
setComponents(prev => prev.filter(component => component.id !== id)); setComponents((prev) => prev.filter((component) => component.id !== id))
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete component'); setError(err instanceof Error ? err.message : 'Failed to delete component')
throw err; throw err
} finally { } finally {
setLoading(false); setLoading(false)
}
} }
};
const getComponent = (id: string) => { const getComponent = (id: string) => {
return components.find(comp => comp.id === id); return components?.find((comp) => comp.id === id)
}; }
const getComponentByName = (name: string) => { const getComponentByName = (name: string) => {
return components.find(comp => comp.name === name); return components?.find((comp) => comp.name === name)
}; }
const registerComponent = (name: string, component: React.ComponentType<unknown>) => { const registerComponent = (name: string, component: React.ComponentType<unknown>) => {
setRegisteredComponents(prev => ({ setRegisteredComponents((prev) => ({
...prev, ...prev,
[name]: component [name]: component,
})); }))
}; }
const value = { const value = {
components, components,
@ -123,8 +130,8 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
getComponentByName, getComponentByName,
refreshComponents, refreshComponents,
registeredComponents, registeredComponents,
registerComponent registerComponent,
}; }
return <ComponentContext.Provider value={value}>{children}</ComponentContext.Provider>; return <ComponentContext.Provider value={value}>{children}</ComponentContext.Provider>
}; }

View file

@ -1,92 +1,140 @@
import React, { createContext, useCallback, useState, useEffect } from 'react'; import React, { createContext, useCallback, useState, useEffect } from 'react'
import * as Babel from '@babel/standalone'; import * as Babel from '@babel/standalone'
import { useComponents } from './ComponentContext'; import { useComponents } from './ComponentContext'
import {
Alert,
Avatar,
Badge,
Button,
Calendar,
Card,
Checkbox,
ConfigProvider,
DatePicker,
Dialog,
Drawer,
Dropdown,
FormItem,
FormContainer,
Input,
InputGroup,
Menu,
MenuItem,
Notification,
Pagination,
Progress,
Radio,
RangeCalendar,
ScrollBar,
Segment,
Select,
Skeleton,
Spinner,
Steps,
Switcher,
Table,
Tabs,
Tag,
TimeInput,
Timeline,
toast,
Tooltip,
Upload,
} from '../components/ui'
interface ComponentProps { interface ComponentProps {
[key: string]: unknown; [key: string]: unknown
} }
interface ComponentRegistryContextType { interface ComponentRegistryContextType {
renderComponent: (name: string, props?: ComponentProps) => React.ReactNode; renderComponent: (name: string, props?: ComponentProps) => React.ReactNode
compileAndRender: (code: string, props?: ComponentProps) => React.ReactNode; compileAndRender: (code: string, props?: ComponentProps) => React.ReactNode
isComponentRegistered: (name: string) => boolean; isComponentRegistered: (name: string) => boolean
getRegisteredComponents: () => string[]; getRegisteredComponents: () => string[]
getComponentCode: (name: string) => string | null; getComponentCode: (name: string) => string | null
} }
export const ComponentRegistryContext = createContext<ComponentRegistryContextType | undefined>(undefined); export const ComponentRegistryContext = createContext<ComponentRegistryContextType | undefined>(
undefined,
)
const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { components } = useComponents(); const { components } = useComponents()
const [compiledComponents, setCompiledComponents] = useState<Record<string, React.ComponentType<ComponentProps>>>({}); const [compiledComponents, setCompiledComponents] = useState<
Record<string, React.ComponentType<ComponentProps>>
>({})
const extractComponentInfo = useCallback((code: string, defaultName = '') => { const extractComponentInfo = useCallback((code: string, defaultName = '') => {
try { try {
// FC Type declaration with explicit component name // FC Type declaration with explicit component name
const fcTypeMatch = code.match(/const\s+([A-Za-z]\w*)\s*:\s*React\.FC/); const fcTypeMatch = code.match(/const\s+([A-Za-z]\w*)\s*:\s*React\.FC/)
if (fcTypeMatch) return fcTypeMatch[1]; if (fcTypeMatch) return fcTypeMatch[1]
// Function declaration: function MyComponent() {} // Function declaration: function MyComponent() {}
const functionMatch = code.match(/function\s+([A-Za-z]\w*)/); const functionMatch = code.match(/function\s+([A-Za-z]\w*)/)
if (functionMatch) return functionMatch[1]; if (functionMatch) return functionMatch[1]
// Arrow function with explicit name: const MyComponent = () => {} // Arrow function with explicit name: const MyComponent = () => {}
const arrowMatch = code.match(/const\s+([A-Za-z]\w*)\s*=/); const arrowMatch = code.match(/const\s+([A-Za-z]\w*)\s*=/)
if (arrowMatch) return arrowMatch[1]; if (arrowMatch) return arrowMatch[1]
// Class declaration: class MyComponent extends React.Component {} // Class declaration: class MyComponent extends React.Component {}
const classMatch = code.match(/class\s+([A-Za-z]\w*)/); const classMatch = code.match(/class\s+([A-Za-z]\w*)/)
if (classMatch) return classMatch[1]; if (classMatch) return classMatch[1]
// Default export name // Default export name
const exportMatch = code.match(/export\s+default\s+([A-Za-z]\w*)/); const exportMatch = code.match(/export\s+default\s+([A-Za-z]\w*)/)
if (exportMatch) return exportMatch[1]; if (exportMatch) return exportMatch[1]
// Interface name which might indicate component name // Interface name which might indicate component name
const interfaceMatch = code.match(/interface\s+([A-Za-z]\w*)Props/); const interfaceMatch = code.match(/interface\s+([A-Za-z]\w*)Props/)
if (interfaceMatch) return interfaceMatch[1]; if (interfaceMatch) return interfaceMatch[1]
// Look for TypeScript type definitions that might indicate a component name // Look for TypeScript type definitions that might indicate a component name
const tsTypeMatch = code.match(/type\s+([A-Za-z]\w*)Props/); const tsTypeMatch = code.match(/type\s+([A-Za-z]\w*)Props/)
if (tsTypeMatch) return tsTypeMatch[1]; if (tsTypeMatch) return tsTypeMatch[1]
// Try to find any capitalized identifier that might be a component // Try to find any capitalized identifier that might be a component
const capitalNameMatch = code.match(/\b([A-Z][A-Za-z0-9]*)\b/); const capitalNameMatch = code.match(/\b([A-Z][A-Za-z0-9]*)\b/)
if (capitalNameMatch && capitalNameMatch[1] !== 'React' && capitalNameMatch[1] !== 'Component') { if (
return capitalNameMatch[1]; capitalNameMatch &&
capitalNameMatch[1] !== 'React' &&
capitalNameMatch[1] !== 'Component'
) {
return capitalNameMatch[1]
} }
// Use the default name provided (usually the component name from DB) // Use the default name provided (usually the component name from DB)
if (defaultName) { if (defaultName) {
return defaultName; return defaultName
} }
// Last resort - use "DynamicComponent" as it's descriptive and unlikely to conflict // Last resort - use "DynamicComponent" as it's descriptive and unlikely to conflict
return "DynamicComponent"; return 'DynamicComponent'
} catch (err) { } catch (err) {
console.error("Error extracting component name:", err); console.error('Error extracting component name:', err)
return defaultName || "DynamicComponent"; return defaultName || 'DynamicComponent'
} }
}, []); }, [])
// Compile all components when the component list changes // Compile all components when the component list changes
useEffect(() => { useEffect(() => {
if (!components || !components?.length) return; if (!components || !components?.length) return
try { try {
// Create a bundle of all active components // Create a bundle of all active components
const activeComponents = components?.filter(c => c.isActive); const activeComponents = components?.filter((c) => c.isActive)
if (!activeComponents.length) { if (!activeComponents.length) {
setCompiledComponents({}); setCompiledComponents({})
return; return
} }
// First, extract all component names and create both lowercase and normal versions // First, extract all component names and create both lowercase and normal versions
const componentInfos = activeComponents.map(comp => { const componentInfos = activeComponents.map((comp) => {
const name = comp.name; const name = comp.name
// Create both original and capitalized versions for more reliable lookups // Create both original and capitalized versions for more reliable lookups
const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1); const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1)
return { return {
name: name, name: name,
@ -95,78 +143,194 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
code: comp.code code: comp.code
.replace(/import\s+.*?;/g, '') .replace(/import\s+.*?;/g, '')
.replace(/export\s+default\s+/, '') .replace(/export\s+default\s+/, '')
.trim() .trim(),
}; }
}); })
// Prepare the combined code in a way that avoids naming conflicts // Prepare the combined code in a way that avoids naming conflicts
const componentBundle = componentInfos.map(info => const componentBundle = componentInfos
`// Component: ${info.name}\nconst ${info.name}_Component = (function() {\n${info.code}\nreturn ${info.internalName};\n})();` .map(
).join('\n\n'); (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 // Create a function that returns an object with all components
const bundledCode = ` const bundledCode = `
(function(React) { (function(React, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload) {
// Global components and hooks available to all custom components
const { useState, useEffect, useCallback, useMemo, useRef, createContext, useContext } = React;
// Basic HTML elements for fallback
const Div = (props) => React.createElement('div', props);
const Span = (props) => React.createElement('span', props);
const Textarea = (props) => React.createElement('textarea', props);
const Option = (props) => React.createElement('option', props);
const Form = (props) => React.createElement('form', props);
const Label = (props) => React.createElement('label', props);
const H1 = (props) => React.createElement('h1', props);
const H2 = (props) => React.createElement('h2', props);
const H3 = (props) => React.createElement('h3', props);
const H4 = (props) => React.createElement('h4', props);
const H5 = (props) => React.createElement('h5', props);
const H6 = (props) => React.createElement('h6', props);
const P = (props) => React.createElement('p', props);
const A = (props) => React.createElement('a', props);
const Img = (props) => React.createElement('img', props);
const Ul = (props) => React.createElement('ul', props);
const Li = (props) => React.createElement('li', props);
const Tr = (props) => React.createElement('tr', props);
const Td = (props) => React.createElement('td', props);
const Th = (props) => React.createElement('th', props);
const Thead = (props) => React.createElement('thead', props);
const Tbody = (props) => React.createElement('tbody', props);
const componentRegistry = {}; const componentRegistry = {};
${componentBundle} ${componentBundle}
// Add all components to the registry with both original and capitalized names // Add all components to the registry with both original and capitalized names
${componentInfos.map(info => ` ${componentInfos
.map(
(info) => `
// Register with original name // Register with original name
componentRegistry["${info.name}"] = ${info.name}_Component; componentRegistry["${info.name}"] = ${info.name}_Component;
// Register with capitalized name for proper React convention // Register with capitalized name for proper React convention
componentRegistry["${info.nameCapitalized}"] = ${info.name}_Component; componentRegistry["${info.nameCapitalized}"] = ${info.name}_Component;
`).join('\n')} `,
)
.join('\n')}
return componentRegistry; return componentRegistry;
})(React) })(React, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload)
`; `
// Compile the bundle // Compile the bundle
const compiledBundle = Babel.transform(bundledCode, { const compiledBundle = Babel.transform(bundledCode, {
presets: ['react', 'typescript'], presets: ['react', 'typescript'],
filename: 'components-bundle.tsx' filename: 'components-bundle.tsx',
}).code; }).code
if (!compiledBundle) { if (!compiledBundle) {
throw new Error('Failed to compile components bundle'); throw new Error('Failed to compile components bundle')
} }
// Evaluate the bundle to get all components // Evaluate the bundle to get all components
const componentsFactory = new Function('React', `return ${compiledBundle}`); const componentsFactory = new Function(
const compiledComponentsRegistry = componentsFactory(React); 'React',
'Alert',
'Avatar',
'Badge',
'Button',
'Calendar',
'Card',
'Checkbox',
'ConfigProvider',
'DatePicker',
'Dialog',
'Drawer',
'Dropdown',
'FormItem',
'FormContainer',
'Input',
'InputGroup',
'Menu',
'MenuItem',
'Notification',
'Pagination',
'Progress',
'Radio',
'RangeCalendar',
'ScrollBar',
'Segment',
'Select',
'Skeleton',
'Spinner',
'Steps',
'Switcher',
'Table',
'Tabs',
'Tag',
'TimeInput',
'Timeline',
'toast',
'Tooltip',
'Upload',
`return ${compiledBundle}`,
)
const compiledComponentsRegistry = componentsFactory(
React,
Alert,
Avatar,
Badge,
Button,
Calendar,
Card,
Checkbox,
ConfigProvider,
DatePicker,
Dialog,
Drawer,
Dropdown,
FormItem,
FormContainer,
Input,
InputGroup,
Menu,
MenuItem,
Notification,
Pagination,
Progress,
Radio,
RangeCalendar,
ScrollBar,
Segment,
Select,
Skeleton,
Spinner,
Steps,
Switcher,
Table,
Tabs,
Tag,
TimeInput,
Timeline,
toast,
Tooltip,
Upload,
)
setCompiledComponents(compiledComponentsRegistry); setCompiledComponents(compiledComponentsRegistry)
} catch (error) { } catch (error) {
console.error('Error compiling components bundle:', error); console.error('Error compiling components bundle:', error)
setCompiledComponents({}); setCompiledComponents({})
} }
}, [components, extractComponentInfo]); }, [components, extractComponentInfo])
const compileCode = useCallback((code: string) => { const compileCode = useCallback(
(code: string) => {
try { try {
// Clean the code and extract component name // Clean the code and extract component name
const cleanCode = code const cleanCode = code
.replace(/import\s+.*?;/g, '') .replace(/import\s+.*?;/g, '')
.replace(/export\s+default\s+/, '') .replace(/export\s+default\s+/, '')
.trim(); .trim()
// Try to extract a meaningful name from the component, // Try to extract a meaningful name from the component,
// but don't generate random names that could cause reference issues // but don't generate random names that could cause reference issues
const componentName = extractComponentInfo(code, 'AnonComponent'); const componentName = extractComponentInfo(code, 'AnonComponent')
// Extract all potential component references from JSX // Extract all potential component references from JSX
// Look for JSX tags like <ComponentName ...> or <ComponentName/> // Look for JSX tags like <ComponentName ...> or <ComponentName/>
const jsxComponentRegex = /<([A-Z][A-Za-z0-9_]*)/g; const jsxComponentRegex = /<([A-Z][A-Za-z0-9_]*)/g
const jsxMatches = [...cleanCode.matchAll(jsxComponentRegex)].map(match => match[1]); const jsxMatches = [...cleanCode.matchAll(jsxComponentRegex)].map((match) => match[1])
// Get unique component names from JSX // Get unique component names from JSX
const jsxComponentNames = [...new Set(jsxMatches)]; const jsxComponentNames = [...new Set(jsxMatches)]
// Generate a warning for JSX tags that might be components // Generate a warning for JSX tags that might be components
if (jsxComponentNames.length > 0) { if (jsxComponentNames.length > 0) {
console.log("JSX tags that might be components:", jsxComponentNames.join(", ")); 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 // Transform code to a component factory that wraps the component to provide dynamic component access
@ -300,20 +464,23 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
throw error; throw error;
} }
})(React, compiledComponentsObj) })(React, compiledComponentsObj)
`; `
// Compile the code // Compile the code
const compiledCode = Babel.transform(transformedCode, { const compiledCode = Babel.transform(transformedCode, {
presets: ['react', 'typescript'], presets: ['react', 'typescript'],
filename: 'component.tsx' filename: 'component.tsx',
}).code; }).code
if (!compiledCode) { if (!compiledCode) {
throw new Error('Failed to compile component'); throw new Error('Failed to compile component')
} }
// Create and return the component with better error handling // Create and return the component with better error handling
const ComponentFactory = new Function('React', 'compiledComponentsObj', ` const ComponentFactory = new Function(
'React',
'compiledComponentsObj',
`
try { try {
// Create a local variable to ensure it exists // Create a local variable to ensure it exists
var AnonComponent; var AnonComponent;
@ -350,62 +517,72 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
]); ]);
}; };
} }
`); `,
)
// Create the component with our registry of all other components // Create the component with our registry of all other components
const Component = ComponentFactory(React, compiledComponents); const Component = ComponentFactory(React, compiledComponents)
if (!Component || typeof Component !== 'function') { if (!Component || typeof Component !== 'function') {
throw new Error('Invalid component definition'); throw new Error('Invalid component definition')
} }
return Component; return Component
} catch (error) { } catch (error) {
console.error('Component compilation error:', error); console.error('Component compilation error:', error)
return () => ( return () => (
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700"> <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> <p className="font-semibold mb-2">Compilation Error</p>
<div className="text-sm whitespace-pre-wrap">{String(error)}</div> <div className="text-sm whitespace-pre-wrap">{String(error)}</div>
</div> </div>
); )
} }
}, [extractComponentInfo, compiledComponents]); },
[extractComponentInfo, compiledComponents],
)
const compileAndRender = useCallback((code: string, props: ComponentProps = {}) => { const compileAndRender = useCallback(
(code: string, props: ComponentProps = {}) => {
if (!code?.trim()) { if (!code?.trim()) {
return null; return null
} }
try { try {
// Create a component without adding it to registry yet // Create a component without adding it to registry yet
const Component = compileCode(code); const Component = compileCode(code)
return <Component {...props} />; return <Component {...props} />
} catch (error) { } catch (error) {
console.error('Render error:', error); console.error('Render error:', error)
return ( return (
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700"> <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> <p className="font-semibold mb-2">Render Error</p>
<div className="text-sm whitespace-pre-wrap">{String(error)}</div> <div className="text-sm whitespace-pre-wrap">{String(error)}</div>
</div> </div>
); )
} }
}, [compileCode]); },
[compileCode],
)
const renderComponent = useCallback((name: string, props: ComponentProps = {}) => { const renderComponent = useCallback(
(name: string, props: ComponentProps = {}) => {
// Check if the component is already compiled // Check if the component is already compiled
if (compiledComponents[name]) { if (compiledComponents[name]) {
const Component = compiledComponents[name]; const Component = compiledComponents[name]
return <Component {...props} />; return <Component {...props} />
} }
// Otherwise, try to compile it from the source code // Otherwise, try to compile it from the source code
const component = components.find(c => c.name === name && c.isActive); const component = components.find((c) => c.name === name && c.isActive)
if (!component) { if (!component) {
console.error(`Component not found: ${name}. Available components: ${Object.keys(compiledComponents).join(', ')}`); console.error(
`Component not found: ${name}. Available components: ${Object.keys(compiledComponents).join(', ')}`,
)
return ( return (
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700"> <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-sm">Component not found: {name}</div>
<div className="text-xs mt-2">This could be because: <div className="text-xs mt-2">
This could be because:
<ul className="list-disc ml-5 mt-1"> <ul className="list-disc ml-5 mt-1">
<li>The component has not been saved to the database</li> <li>The component has not been saved to the database</li>
<li>The component name is misspelled</li> <li>The component name is misspelled</li>
@ -413,7 +590,7 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
</ul> </ul>
</div> </div>
</div> </div>
); )
} }
try { try {
@ -421,235 +598,277 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
// This ensures all components are available for cross-referencing // This ensures all components are available for cross-referencing
if (Object.keys(compiledComponents).length === 0) { if (Object.keys(compiledComponents).length === 0) {
// If no components are compiled yet, this is the first render // If no components are compiled yet, this is the first render
console.warn("Component registry is empty. Components might not be available for references."); console.warn(
'Component registry is empty. Components might not be available for references.',
)
} }
return compileAndRender(component.code, props); return compileAndRender(component.code, props)
} catch (error) { } catch (error) {
console.error(`Error rendering component ${name}:`, error); console.error(`Error rendering component ${name}:`, error)
return ( return (
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700"> <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-sm font-semibold">Error rendering {name}</div>
<div className="text-xs mt-2 whitespace-pre-wrap">{String(error)}</div> <div className="text-xs mt-2 whitespace-pre-wrap">{String(error)}</div>
</div> </div>
); )
} }
}, [components, compileAndRender, compiledComponents]); },
[components, compileAndRender, compiledComponents],
)
const isComponentRegistered = useCallback((name: string) => { const isComponentRegistered = useCallback(
return components.some(c => c.name === name && c.isActive); (name: string) => {
}, [components]); return components.some((c) => c.name === name && c.isActive)
},
[components],
)
const getRegisteredComponents = useCallback(() => { const getRegisteredComponents = useCallback(() => {
return components?.filter(c => c.isActive).map(c => c.name); return components?.filter((c) => c.isActive).map((c) => c.name)
}, [components]); }, [components])
const getComponentCode = useCallback((name: string) => { const getComponentCode = useCallback(
const component = components.find(c => c.name === name); (name: string) => {
return component ? component.code : null; const component = components.find((c) => c.name === name)
}, [components]); return component ? component.code : null
},
[components],
)
// Create a helper to process DOM nodes and replace custom lowercase elements with actual components // Create a helper to process DOM nodes and replace custom lowercase elements with actual components
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || !document) return; if (typeof window === 'undefined' || !document) return
// Create a type definition for ReactDOM // Create a type definition for ReactDOM
// This approach avoids TypeScript errors // This approach avoids TypeScript errors
type ReactDOMType = { type ReactDOMType = {
render: (element: React.ReactNode, container: Element) => void; render: (element: React.ReactNode, container: Element) => void
createRoot?: (container: Element) => { render: (element: React.ReactNode) => void }; createRoot?: (container: Element) => { render: (element: React.ReactNode) => void }
}; }
// Check if we have any components to work with // Check if we have any components to work with
if (!Object.keys(compiledComponents).length) return; if (!Object.keys(compiledComponents).length) return
// Get all lowercase component names from registry and add them to a Set for faster lookup // Get all lowercase component names from registry and add them to a Set for faster lookup
const lowercaseComponentNames = new Set<string>(); const lowercaseComponentNames = new Set<string>()
Object.keys(compiledComponents).forEach(name => { Object.keys(compiledComponents).forEach((name) => {
// Only include lowercase component names, but not standard HTML elements // Only include lowercase component names, but not standard HTML elements
if (name.charAt(0) === name.charAt(0).toLowerCase() && if (
name.charAt(0) === name.charAt(0).toLowerCase() &&
name.charAt(0) !== name.charAt(0).toUpperCase() && 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)) { 'div',
lowercaseComponentNames.add(name); 'span',
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'button',
'a',
'img',
'input',
'form',
'label',
'select',
'option',
'textarea',
'ul',
'ol',
'li',
'table',
'tr',
'td',
'th',
].includes(name)
) {
lowercaseComponentNames.add(name)
} }
}); })
if (lowercaseComponentNames.size === 0) return; if (lowercaseComponentNames.size === 0) return
// Create a function to process a DOM node and its children // Create a function to process a DOM node and its children
const processNode = (rootNode: Element) => { const processNode = (rootNode: Element) => {
// Convert to array for better filtering // Convert to array for better filtering
const componentsToFind = [...lowercaseComponentNames]; const componentsToFind = [...lowercaseComponentNames]
// Create CSS selector for all lowercase component tags // Create CSS selector for all lowercase component tags
const selector = componentsToFind.join(','); const selector = componentsToFind.join(',')
if (!selector) return false; if (!selector) return false
// Find all matching elements // Find all matching elements
const elements = rootNode.tagName && lowercaseComponentNames.has(rootNode.tagName.toLowerCase()) const elements =
rootNode.tagName && lowercaseComponentNames.has(rootNode.tagName.toLowerCase())
? [rootNode] ? [rootNode]
: Array.from(rootNode.querySelectorAll(selector)); : Array.from(rootNode.querySelectorAll(selector))
if (elements.length === 0) return false; if (elements.length === 0) return false
// Process each element // Process each element
elements.forEach(element => { elements.forEach((element) => {
// Skip if already processed // Skip if already processed
if (element.hasAttribute('data-component-processed')) return; if (element.hasAttribute('data-component-processed')) return
try { try {
// Mark as processed to avoid infinite loops // Mark as processed to avoid infinite loops
element.setAttribute('data-component-processed', 'true'); element.setAttribute('data-component-processed', 'true')
// Get the tag name in lowercase // Get the tag name in lowercase
const tagName = element.tagName.toLowerCase(); const tagName = element.tagName.toLowerCase()
// Get the component from registry // Get the component from registry
const Component = compiledComponents[tagName]; const Component = compiledComponents[tagName]
if (!Component) { if (!Component) {
console.warn(`Component ${tagName} exists in registry but couldn't be loaded`); console.warn(`Component ${tagName} exists in registry but couldn't be loaded`)
return; return
} }
// Get props from element attributes // Get props from element attributes
const props: Record<string, unknown> = {}; const props: Record<string, unknown> = {}
Array.from(element.attributes).forEach(attr => { Array.from(element.attributes).forEach((attr) => {
// Skip data-component-processed attribute // Skip data-component-processed attribute
if (attr.name === 'data-component-processed') return; if (attr.name === 'data-component-processed') return
// Convert kebab-case to camelCase for props (React convention) // Convert kebab-case to camelCase for props (React convention)
const propName = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); const propName = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
let propValue: string | boolean | number = attr.value; let propValue: string | boolean | number = attr.value
// Handle boolean attributes (present without value) // Handle boolean attributes (present without value)
if (propValue === '' || propValue === propName) { if (propValue === '' || propValue === propName) {
propValue = true; propValue = true
} }
// Try to parse JSON values // Try to parse JSON values
if (propValue && typeof propValue === 'string') { if (propValue && typeof propValue === 'string') {
if ((propValue.startsWith('{') && propValue.endsWith('}')) || if (
(propValue.startsWith('{') && propValue.endsWith('}')) ||
(propValue.startsWith('[') && propValue.endsWith(']')) || (propValue.startsWith('[') && propValue.endsWith(']')) ||
propValue === 'true' || propValue === 'false' || !isNaN(Number(propValue))) { try { propValue === 'true' ||
propValue = JSON.parse(propValue); propValue === 'false' ||
!isNaN(Number(propValue))
) {
try {
propValue = JSON.parse(propValue)
} catch { } catch {
// Keep as string if parsing fails // Keep as string if parsing fails
} }
} }
} }
props[propName] = propValue; props[propName] = propValue
}); })
// Process children // Process children
const children = Array.from(element.childNodes).map(child => { const children = Array.from(element.childNodes).map((child) => {
if (child.nodeType === Node.TEXT_NODE) { if (child.nodeType === Node.TEXT_NODE) {
return child.textContent; return child.textContent
} }
return child; return child
}); })
if (children.length) { if (children.length) {
props.children = children.length === 1 ? children[0] : children; props.children = children.length === 1 ? children[0] : children
} }
// Create a wrapper that preserves the original element's position // Create a wrapper that preserves the original element's position
const wrapper = document.createElement('div'); const wrapper = document.createElement('div')
wrapper.style.display = 'contents'; // Don't add extra layout structure wrapper.style.display = 'contents' // Don't add extra layout structure
wrapper.dataset.customComponent = tagName; wrapper.dataset.customComponent = tagName
// Insert wrapper and remove original // Insert wrapper and remove original
element.parentNode?.insertBefore(wrapper, element); element.parentNode?.insertBefore(wrapper, element)
element.parentNode?.removeChild(element); element.parentNode?.removeChild(element)
// Render React component into wrapper // Render React component into wrapper
try { try {
const reactElement = React.createElement(Component, props); const reactElement = React.createElement(Component, props)
const ReactDOM = (window as Window & typeof globalThis & { ReactDOM?: ReactDOMType }).ReactDOM; const ReactDOM = (window as Window & typeof globalThis & { ReactDOM?: ReactDOMType })
.ReactDOM
if (ReactDOM) { if (ReactDOM) {
// Use modern createRoot API if available (React 18+) // Use modern createRoot API if available (React 18+)
if (ReactDOM.createRoot) { if (ReactDOM.createRoot) {
const root = ReactDOM.createRoot(wrapper); const root = ReactDOM.createRoot(wrapper)
root.render(reactElement); root.render(reactElement)
} }
// Fallback to legacy render API // Fallback to legacy render API
else if (ReactDOM.render) { else if (ReactDOM.render) {
ReactDOM.render(reactElement, wrapper); ReactDOM.render(reactElement, wrapper)
} }
} else { } else {
console.error("ReactDOM not found in window - cannot render custom components"); console.error('ReactDOM not found in window - cannot render custom components')
} }
} catch (err) { } catch (err) {
console.error(`Error rendering ${tagName} React component:`, err); console.error(`Error rendering ${tagName} React component:`, err)
// Show error UI in place of the component // Show error UI in place of the component
wrapper.innerHTML = `<div style="border: 1px solid red; color: red; padding: 8px;"> wrapper.innerHTML = `<div style="border: 1px solid red; color: red; padding: 8px;">
Error rendering &lt;${tagName}&gt; component Error rendering &lt;${tagName}&gt; component
</div>`; </div>`
} }
} catch (error) { } catch (error) {
console.error(`Error processing ${element.tagName} element:`, error); console.error(`Error processing ${element.tagName} element:`, error)
} }
}); })
return elements.length > 0; return elements.length > 0
}; }
// Create a mutation observer to watch for our custom elements // Create a mutation observer to watch for our custom elements
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
let hasProcessed = false; let hasProcessed = false
mutations.forEach(mutation => { mutations.forEach((mutation) => {
if (mutation.type === 'childList') { if (mutation.type === 'childList') {
// Process added nodes // Process added nodes
mutation.addedNodes.forEach(node => { mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
// Process this node and its children // Process this node and its children
hasProcessed = processNode(node as Element) || hasProcessed; hasProcessed = processNode(node as Element) || hasProcessed
} }
}); })
} }
}); })
// Also check the entire document occasionally to catch any missed elements // Also check the entire document occasionally to catch any missed elements
// This handles cases where elements are added before our observer is active // This handles cases where elements are added before our observer is active
if (!hasProcessed && document.body) { if (!hasProcessed && document.body) {
processNode(document.body); processNode(document.body)
} }
}); })
// Start observing the entire document // Start observing the entire document
observer.observe(document.body, { observer.observe(document.body, {
childList: true, childList: true,
subtree: true, subtree: true,
characterData: true characterData: true,
}); })
// Initial scan of the whole document // Initial scan of the whole document
if (document.body) { if (document.body) {
processNode(document.body); processNode(document.body)
} }
return () => { return () => {
observer.disconnect(); observer.disconnect()
}; }
}, [compiledComponents]); }, [compiledComponents])
const value = { const value = {
renderComponent, renderComponent,
compileAndRender, compileAndRender,
isComponentRegistered, isComponentRegistered,
getRegisteredComponents, getRegisteredComponents,
getComponentCode getComponentCode,
}; }
return ( return (
<ComponentRegistryContext.Provider value={value}> <ComponentRegistryContext.Provider value={value}>{children}</ComponentRegistryContext.Provider>
{children} )
</ComponentRegistryContext.Provider> }
);
};
export default ComponentRegistryProvider; export default ComponentRegistryProvider