CustomComponents hataları
This commit is contained in:
parent
8cc8ed07f9
commit
64ccc150df
8 changed files with 1000 additions and 921 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 <${tagName}> component
|
Error rendering <${tagName}> 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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue