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
|
||||
- SEED=${SEED}
|
||||
networks:
|
||||
- kurs-platform-data_db
|
||||
- db
|
||||
|
||||
# Backend API
|
||||
api:
|
||||
|
|
@ -33,7 +33,7 @@ services:
|
|||
- cdn:/etc/api/cdn
|
||||
- api-keys:/root/.aspnet/DataProtection-Keys
|
||||
networks:
|
||||
- kurs-platform-data_db
|
||||
- db
|
||||
- default
|
||||
|
||||
# Frontend (UI)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ name: kurs-platform-data
|
|||
|
||||
networks:
|
||||
db:
|
||||
external: false
|
||||
external: true
|
||||
name: kurs-platform-data_db
|
||||
|
||||
volumes:
|
||||
pg:
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@ export interface ComponentPreviewProps {
|
|||
}
|
||||
|
||||
const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentName, className = '' }) => {
|
||||
const { components } = useComponents()
|
||||
const { components, loading } = useComponents()
|
||||
|
||||
if (!componentName) {
|
||||
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
|
||||
const component = components.find((c) => c.name === componentName && c.isActive)
|
||||
|
||||
let dependencies: string[] = [];
|
||||
let dependencies: string[] = []
|
||||
|
||||
if (component?.dependencies) {
|
||||
try {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -59,10 +59,10 @@ const ComponentEditor: React.FC = () => {
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (code && editorState.components.length === 0) {
|
||||
if (code && editorState.components?.length === 0) {
|
||||
parseAndUpdateComponents(code)
|
||||
}
|
||||
}, [code, editorState.components.length])
|
||||
}, [code, editorState.components?.length])
|
||||
|
||||
// Load existing component data - sadece edit modunda
|
||||
useEffect(() => {
|
||||
|
|
@ -144,7 +144,9 @@ const ComponentEditor: React.FC = () => {
|
|||
<div className="h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<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>
|
||||
)
|
||||
|
|
@ -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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -237,19 +241,26 @@ const ComponentEditor: React.FC = () => {
|
|||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-red-800 font-medium mb-2">
|
||||
{validationErrors.length} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')}
|
||||
{validationErrors.length !== 1 ? 's' : ''} {translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')}
|
||||
{validationErrors.length}{' '}
|
||||
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Title')}
|
||||
{validationErrors.length !== 1 ? 's' : ''}{' '}
|
||||
{translate('::App.DeveloperKit.ComponentEditor.ValidationError.Found')}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{validationErrors.slice(0, 5).map((error, index) => (
|
||||
<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}
|
||||
</div>
|
||||
))}
|
||||
{validationErrors.length > 5 && (
|
||||
<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' : ''}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useComponents } from "../../contexts/ComponentContext";
|
||||
import React, { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useComponents } from '../../contexts/ComponentContext'
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
|
|
@ -14,21 +14,19 @@ import {
|
|||
CheckCircle,
|
||||
XCircle,
|
||||
View,
|
||||
} from "lucide-react";
|
||||
import { ROUTES_ENUM } from "@/routes/route.constant";
|
||||
import { useLocalization } from "@/utils/hooks/useLocalization";
|
||||
} from 'lucide-react'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
||||
const ComponentManager: React.FC = () => {
|
||||
const { components, updateComponent, deleteComponent } = useComponents();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filterActive, setFilterActive] = useState<
|
||||
"all" | "active" | "inactive"
|
||||
>("all");
|
||||
const { components, loading, updateComponent, deleteComponent } = useComponents()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all')
|
||||
|
||||
// Calculate statistics
|
||||
const totalComponents = components?.length;
|
||||
const activeComponents = components?.filter((c) => c.isActive).length;
|
||||
const inactiveComponents = totalComponents - activeComponents;
|
||||
const totalComponents = components?.length || 0
|
||||
const activeComponents = components?.filter((c) => c.isActive).length || 0
|
||||
const inactiveComponents = totalComponents - activeComponents
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const stats = [
|
||||
|
|
@ -36,62 +34,58 @@ const ComponentManager: React.FC = () => {
|
|||
name: translate('::App.DeveloperKit.Component.Total'),
|
||||
value: totalComponents,
|
||||
icon: Puzzle,
|
||||
color: "text-purple-600",
|
||||
bgColor: "bg-purple-100",
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-100',
|
||||
},
|
||||
{
|
||||
name: translate('::App.DeveloperKit.Component.Active'),
|
||||
value: activeComponents,
|
||||
icon: CheckCircle,
|
||||
color: "text-emerald-600",
|
||||
bgColor: "bg-emerald-100",
|
||||
color: 'text-emerald-600',
|
||||
bgColor: 'bg-emerald-100',
|
||||
},
|
||||
{
|
||||
name: translate('::App.DeveloperKit.Component.Inactive'),
|
||||
value: inactiveComponents,
|
||||
icon: XCircle,
|
||||
color: "text-slate-600",
|
||||
bgColor: "bg-slate-100",
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'bg-slate-100',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const filteredComponents = components?.filter((component) => {
|
||||
const matchesSearch =
|
||||
component.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(component.description || "")
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
(component.description || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
||||
const matchesFilter =
|
||||
filterActive === "all" ||
|
||||
(filterActive === "active" && component.isActive) ||
|
||||
(filterActive === "inactive" && !component.isActive);
|
||||
filterActive === 'all' ||
|
||||
(filterActive === 'active' && component.isActive) ||
|
||||
(filterActive === 'inactive' && !component.isActive)
|
||||
|
||||
return matchesSearch && matchesFilter;
|
||||
});
|
||||
return matchesSearch && matchesFilter
|
||||
})
|
||||
|
||||
const handleToggleActive = async (id: string, isActive: boolean) => {
|
||||
try {
|
||||
const component = components.find((c) => c.id === id);
|
||||
const component = components?.find((c) => c.id === id)
|
||||
if (component) {
|
||||
await updateComponent(id, { ...component, isActive });
|
||||
await updateComponent(id, { ...component, isActive })
|
||||
}
|
||||
} 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) => {
|
||||
if (
|
||||
window.confirm(translate('::App.DeveloperKit.Component.ConfirmDelete'))
|
||||
) {
|
||||
if (window.confirm(translate('::App.DeveloperKit.Component.ConfirmDelete'))) {
|
||||
try {
|
||||
await deleteComponent(id);
|
||||
await deleteComponent(id)
|
||||
} catch (err) {
|
||||
console.error("Failed to delete component:", err);
|
||||
console.error('Failed to delete component:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
|
|
@ -114,18 +108,11 @@ const ComponentManager: React.FC = () => {
|
|||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-lg border border-slate-200 p-6"
|
||||
>
|
||||
<div key={index} className="bg-white rounded-lg border border-slate-200 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600 mb-1">
|
||||
{stat.name}
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-slate-900">
|
||||
{stat.value}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-slate-600 mb-1">{stat.name}</p>
|
||||
<p className="text-3xl font-bold text-slate-900">{stat.value}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg ${stat.bgColor}`}>
|
||||
<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" />
|
||||
<select
|
||||
value={filterActive}
|
||||
onChange={(e) =>
|
||||
setFilterActive(e.target.value as "all" | "active" | "inactive")
|
||||
}
|
||||
onChange={(e) => 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"
|
||||
>
|
||||
<option value="all">{translate('::App.DeveloperKit.Component.Filter.All')}</option>
|
||||
<option value="active">{translate('::App.DeveloperKit.Component.Filter.Active')}</option>
|
||||
<option value="inactive">{translate('::App.DeveloperKit.Component.Filter.Inactive')}</option>
|
||||
<option value="active">
|
||||
{translate('::App.DeveloperKit.Component.Filter.Active')}
|
||||
</option>
|
||||
<option value="inactive">
|
||||
{translate('::App.DeveloperKit.Component.Filter.Inactive')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{filteredComponents.map((component) => (
|
||||
<div
|
||||
|
|
@ -177,43 +170,35 @@ const ComponentManager: React.FC = () => {
|
|||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-lg font-semibold text-slate-900">
|
||||
{component.name}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-slate-900">{component.name}</h3>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
component.isActive ? "bg-green-500" : "bg-slate-300"
|
||||
component.isActive ? 'bg-green-500' : 'bg-slate-300'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
component.dependencies ?? "[]"
|
||||
);
|
||||
const parsed = JSON.parse(component.dependencies ?? '[]')
|
||||
return Array.isArray(parsed) && parsed.length > 0
|
||||
? `${parsed.join(", ")}`
|
||||
: translate('::App.DeveloperKit.Component.NoDependencies');
|
||||
? `${parsed.join(', ')}`
|
||||
: translate('::App.DeveloperKit.Component.NoDependencies')
|
||||
} catch {
|
||||
return translate('::App.DeveloperKit.Component.NoDependencies');
|
||||
return translate('::App.DeveloperKit.Component.NoDependencies')
|
||||
}
|
||||
})()}
|
||||
</p>
|
||||
|
||||
{component.description && (
|
||||
<p className="text-slate-600 text-sm mb-3">
|
||||
{component.description}
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm mb-3">{component.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-xs text-slate-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>
|
||||
{component.lastModificationTime
|
||||
? new Date(
|
||||
component.lastModificationTime
|
||||
).toLocaleDateString()
|
||||
? new Date(component.lastModificationTime).toLocaleDateString()
|
||||
: translate('::App.DeveloperKit.Component.DateNotAvailable')}
|
||||
</span>
|
||||
</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 gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleToggleActive(component.id, !component.isActive)
|
||||
}
|
||||
onClick={() => handleToggleActive(component.id, !component.isActive)}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors ${
|
||||
component.isActive
|
||||
? "bg-green-100 text-green-700 hover:bg-green-200"
|
||||
: "bg-slate-100 text-slate-600 hover:bg-slate-200"
|
||||
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{component.isActive ? (
|
||||
|
|
@ -261,7 +244,10 @@ const ComponentManager: React.FC = () => {
|
|||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.saas.developerKitComponentsEdit.replace(':id', component.id)}
|
||||
to={ROUTES_ENUM.protected.saas.developerKitComponentsEdit.replace(
|
||||
':id',
|
||||
component.id,
|
||||
)}
|
||||
target="_blank"
|
||||
className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title={translate('::App.DeveloperKit.Component.Edit')}
|
||||
|
|
@ -269,7 +255,10 @@ const ComponentManager: React.FC = () => {
|
|||
<Edit className="w-4 h-4" />
|
||||
</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"
|
||||
title={translate('::App.DeveloperKit.Component.View')}
|
||||
>
|
||||
|
|
@ -295,16 +284,16 @@ const ComponentManager: React.FC = () => {
|
|||
<Plus className="w-8 h-8 text-slate-500" />
|
||||
</div>
|
||||
<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.Initial.Title')}
|
||||
</h3>
|
||||
<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.Initial.Description')}
|
||||
</p>
|
||||
{!searchTerm && filterActive === "all" && (
|
||||
{!searchTerm && filterActive === 'all' && (
|
||||
<Link
|
||||
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"
|
||||
|
|
@ -317,7 +306,7 @@ const ComponentManager: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ComponentManager;
|
||||
export default ComponentManager
|
||||
|
|
|
|||
|
|
@ -1,116 +1,123 @@
|
|||
import { CreateUpdateCustomComponentDto, CustomComponent, CustomComponentDto } from '@/proxy/developerKit/models';
|
||||
import { developerKitService } from '@/services/developerKit.service';
|
||||
import { useStoreState } from '@/store/store';
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import {
|
||||
CreateUpdateCustomComponentDto,
|
||||
CustomComponent,
|
||||
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 {
|
||||
components: CustomComponent[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
addComponent: (component: CreateUpdateCustomComponentDto) => Promise<void>;
|
||||
updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise<void>;
|
||||
deleteComponent: (id: string) => Promise<void>;
|
||||
getComponent: (id: string) => CustomComponent | undefined;
|
||||
getComponentByName: (name: string) => CustomComponent | undefined;
|
||||
refreshComponents: () => Promise<void>;
|
||||
registeredComponents: Record<string, React.ComponentType<unknown>>;
|
||||
registerComponent: (name: string, component: React.ComponentType<unknown>) => void;
|
||||
components: CustomComponent[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
addComponent: (component: CreateUpdateCustomComponentDto) => Promise<void>
|
||||
updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise<void>
|
||||
deleteComponent: (id: string) => Promise<void>
|
||||
getComponent: (id: string) => CustomComponent | undefined
|
||||
getComponentByName: (name: string) => CustomComponent | undefined
|
||||
refreshComponents: () => Promise<void>
|
||||
registeredComponents: Record<string, React.ComponentType<unknown>>
|
||||
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
|
||||
export const useComponents = () => {
|
||||
const context = useContext(ComponentContext);
|
||||
const context = useContext(ComponentContext)
|
||||
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 }) => {
|
||||
const extraProperties = useStoreState((state) => state.abpConfig?.config?.extraProperties)
|
||||
const [components, setComponents] = useState<CustomComponent[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [registeredComponents, setRegisteredComponents] = useState<Record<string, React.ComponentType<unknown>>>({});
|
||||
const [components, setComponents] = useState<CustomComponent[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [registeredComponents, setRegisteredComponents] = useState<
|
||||
Record<string, React.ComponentType<unknown>>
|
||||
>({})
|
||||
|
||||
const refreshComponents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const customComponents = extraProperties?.customComponents as CustomComponentDto[]
|
||||
setComponents(customComponents);
|
||||
setComponents(customComponents || [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch components');
|
||||
console.error('Failed to fetch components:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch components')
|
||||
console.error('Failed to fetch components:', err)
|
||||
setComponents([])
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshComponents();
|
||||
}, []);
|
||||
refreshComponents()
|
||||
}, [extraProperties])
|
||||
|
||||
const addComponent = async (componentData: CreateUpdateCustomComponentDto) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const newComponent = await developerKitService.createCustomComponent(componentData);
|
||||
setComponents(prev => [...prev, newComponent]);
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const newComponent = await developerKitService.createCustomComponent(componentData)
|
||||
setComponents((prev) => [...prev, newComponent])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create component');
|
||||
throw err;
|
||||
setError(err instanceof Error ? err.message : 'Failed to create component')
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateComponent = async (id: string, componentData: CreateUpdateCustomComponentDto) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const updatedComponent = await developerKitService.updateCustomComponent(id, componentData);
|
||||
setComponents(prev => prev.map(component =>
|
||||
component.id === id ? updatedComponent : component
|
||||
));
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const updatedComponent = await developerKitService.updateCustomComponent(id, componentData)
|
||||
setComponents((prev) =>
|
||||
prev.map((component) => (component.id === id ? updatedComponent : component)),
|
||||
)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update component');
|
||||
throw err;
|
||||
setError(err instanceof Error ? err.message : 'Failed to update component')
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteComponent = async (id: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
await developerKitService.deleteCustomComponent(id);
|
||||
setComponents(prev => prev.filter(component => component.id !== id));
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await developerKitService.deleteCustomComponent(id)
|
||||
setComponents((prev) => prev.filter((component) => component.id !== id))
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete component');
|
||||
throw err;
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete component')
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
const getComponent = (id: string) => {
|
||||
return components.find(comp => comp.id === id);
|
||||
};
|
||||
return components?.find((comp) => comp.id === id)
|
||||
}
|
||||
|
||||
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>) => {
|
||||
setRegisteredComponents(prev => ({
|
||||
setRegisteredComponents((prev) => ({
|
||||
...prev,
|
||||
[name]: component
|
||||
}));
|
||||
};
|
||||
[name]: component,
|
||||
}))
|
||||
}
|
||||
|
||||
const value = {
|
||||
components,
|
||||
|
|
@ -123,8 +130,8 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
getComponentByName,
|
||||
refreshComponents,
|
||||
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 * as Babel from '@babel/standalone';
|
||||
import { useComponents } from './ComponentContext';
|
||||
import React, { createContext, useCallback, useState, useEffect } from 'react'
|
||||
import * as Babel from '@babel/standalone'
|
||||
import { useComponents } from './ComponentContext'
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Calendar,
|
||||
Card,
|
||||
Checkbox,
|
||||
ConfigProvider,
|
||||
DatePicker,
|
||||
Dialog,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
FormItem,
|
||||
FormContainer,
|
||||
Input,
|
||||
InputGroup,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Notification,
|
||||
Pagination,
|
||||
Progress,
|
||||
Radio,
|
||||
RangeCalendar,
|
||||
ScrollBar,
|
||||
Segment,
|
||||
Select,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
Steps,
|
||||
Switcher,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
TimeInput,
|
||||
Timeline,
|
||||
toast,
|
||||
Tooltip,
|
||||
Upload,
|
||||
} from '../components/ui'
|
||||
|
||||
interface ComponentProps {
|
||||
[key: string]: unknown;
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ComponentRegistryContextType {
|
||||
renderComponent: (name: string, props?: ComponentProps) => React.ReactNode;
|
||||
compileAndRender: (code: string, props?: ComponentProps) => React.ReactNode;
|
||||
isComponentRegistered: (name: string) => boolean;
|
||||
getRegisteredComponents: () => string[];
|
||||
getComponentCode: (name: string) => string | null;
|
||||
renderComponent: (name: string, props?: ComponentProps) => React.ReactNode
|
||||
compileAndRender: (code: string, props?: ComponentProps) => React.ReactNode
|
||||
isComponentRegistered: (name: string) => boolean
|
||||
getRegisteredComponents: () => string[]
|
||||
getComponentCode: (name: string) => string | null
|
||||
}
|
||||
|
||||
export const ComponentRegistryContext = createContext<ComponentRegistryContextType | undefined>(undefined);
|
||||
export const ComponentRegistryContext = createContext<ComponentRegistryContextType | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { components } = useComponents();
|
||||
const [compiledComponents, setCompiledComponents] = useState<Record<string, React.ComponentType<ComponentProps>>>({});
|
||||
const { components } = useComponents()
|
||||
const [compiledComponents, setCompiledComponents] = useState<
|
||||
Record<string, React.ComponentType<ComponentProps>>
|
||||
>({})
|
||||
|
||||
const extractComponentInfo = useCallback((code: string, defaultName = '') => {
|
||||
try {
|
||||
// FC Type declaration with explicit component name
|
||||
const fcTypeMatch = code.match(/const\s+([A-Za-z]\w*)\s*:\s*React\.FC/);
|
||||
if (fcTypeMatch) return fcTypeMatch[1];
|
||||
const fcTypeMatch = code.match(/const\s+([A-Za-z]\w*)\s*:\s*React\.FC/)
|
||||
if (fcTypeMatch) return fcTypeMatch[1]
|
||||
|
||||
// Function declaration: function MyComponent() {}
|
||||
const functionMatch = code.match(/function\s+([A-Za-z]\w*)/);
|
||||
if (functionMatch) return functionMatch[1];
|
||||
const functionMatch = code.match(/function\s+([A-Za-z]\w*)/)
|
||||
if (functionMatch) return functionMatch[1]
|
||||
|
||||
// Arrow function with explicit name: const MyComponent = () => {}
|
||||
const arrowMatch = code.match(/const\s+([A-Za-z]\w*)\s*=/);
|
||||
if (arrowMatch) return arrowMatch[1];
|
||||
const arrowMatch = code.match(/const\s+([A-Za-z]\w*)\s*=/)
|
||||
if (arrowMatch) return arrowMatch[1]
|
||||
|
||||
// Class declaration: class MyComponent extends React.Component {}
|
||||
const classMatch = code.match(/class\s+([A-Za-z]\w*)/);
|
||||
if (classMatch) return classMatch[1];
|
||||
const classMatch = code.match(/class\s+([A-Za-z]\w*)/)
|
||||
if (classMatch) return classMatch[1]
|
||||
|
||||
// Default export name
|
||||
const exportMatch = code.match(/export\s+default\s+([A-Za-z]\w*)/);
|
||||
if (exportMatch) return exportMatch[1];
|
||||
const exportMatch = code.match(/export\s+default\s+([A-Za-z]\w*)/)
|
||||
if (exportMatch) return exportMatch[1]
|
||||
|
||||
// Interface name which might indicate component name
|
||||
const interfaceMatch = code.match(/interface\s+([A-Za-z]\w*)Props/);
|
||||
if (interfaceMatch) return interfaceMatch[1];
|
||||
const interfaceMatch = code.match(/interface\s+([A-Za-z]\w*)Props/)
|
||||
if (interfaceMatch) return interfaceMatch[1]
|
||||
|
||||
// Look for TypeScript type definitions that might indicate a component name
|
||||
const tsTypeMatch = code.match(/type\s+([A-Za-z]\w*)Props/);
|
||||
if (tsTypeMatch) return tsTypeMatch[1];
|
||||
const tsTypeMatch = code.match(/type\s+([A-Za-z]\w*)Props/)
|
||||
if (tsTypeMatch) return tsTypeMatch[1]
|
||||
|
||||
// Try to find any capitalized identifier that might be a component
|
||||
const capitalNameMatch = code.match(/\b([A-Z][A-Za-z0-9]*)\b/);
|
||||
if (capitalNameMatch && capitalNameMatch[1] !== 'React' && capitalNameMatch[1] !== 'Component') {
|
||||
return capitalNameMatch[1];
|
||||
const capitalNameMatch = code.match(/\b([A-Z][A-Za-z0-9]*)\b/)
|
||||
if (
|
||||
capitalNameMatch &&
|
||||
capitalNameMatch[1] !== 'React' &&
|
||||
capitalNameMatch[1] !== 'Component'
|
||||
) {
|
||||
return capitalNameMatch[1]
|
||||
}
|
||||
|
||||
// Use the default name provided (usually the component name from DB)
|
||||
if (defaultName) {
|
||||
return defaultName;
|
||||
return defaultName
|
||||
}
|
||||
|
||||
// Last resort - use "DynamicComponent" as it's descriptive and unlikely to conflict
|
||||
return "DynamicComponent";
|
||||
return 'DynamicComponent'
|
||||
} catch (err) {
|
||||
console.error("Error extracting component name:", err);
|
||||
return defaultName || "DynamicComponent";
|
||||
console.error('Error extracting component name:', err)
|
||||
return defaultName || 'DynamicComponent'
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
// Compile all components when the component list changes
|
||||
useEffect(() => {
|
||||
if (!components || !components?.length) return;
|
||||
if (!components || !components?.length) return
|
||||
|
||||
try {
|
||||
// Create a bundle of all active components
|
||||
const activeComponents = components?.filter(c => c.isActive);
|
||||
const activeComponents = components?.filter((c) => c.isActive)
|
||||
|
||||
if (!activeComponents.length) {
|
||||
setCompiledComponents({});
|
||||
return;
|
||||
setCompiledComponents({})
|
||||
return
|
||||
}
|
||||
|
||||
// First, extract all component names and create both lowercase and normal versions
|
||||
const componentInfos = activeComponents.map(comp => {
|
||||
const name = comp.name;
|
||||
const componentInfos = activeComponents.map((comp) => {
|
||||
const name = comp.name
|
||||
// Create both original and capitalized versions for more reliable lookups
|
||||
const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1)
|
||||
|
||||
return {
|
||||
name: name,
|
||||
|
|
@ -95,78 +143,194 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
code: comp.code
|
||||
.replace(/import\s+.*?;/g, '')
|
||||
.replace(/export\s+default\s+/, '')
|
||||
.trim()
|
||||
};
|
||||
});
|
||||
.trim(),
|
||||
}
|
||||
})
|
||||
|
||||
// Prepare the combined code in a way that avoids naming conflicts
|
||||
const componentBundle = componentInfos.map(info =>
|
||||
`// Component: ${info.name}\nconst ${info.name}_Component = (function() {\n${info.code}\nreturn ${info.internalName};\n})();`
|
||||
).join('\n\n');
|
||||
const componentBundle = componentInfos
|
||||
.map(
|
||||
(info) =>
|
||||
`// Component: ${info.name}\nconst ${info.name}_Component = (function() {\n${info.code}\nreturn ${info.internalName};\n})();`,
|
||||
)
|
||||
.join('\n\n')
|
||||
|
||||
// Create a function that returns an object with all components
|
||||
const bundledCode = `
|
||||
(function(React) {
|
||||
(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 = {};
|
||||
|
||||
${componentBundle}
|
||||
|
||||
// Add all components to the registry with both original and capitalized names
|
||||
${componentInfos.map(info => `
|
||||
${componentInfos
|
||||
.map(
|
||||
(info) => `
|
||||
// Register with original name
|
||||
componentRegistry["${info.name}"] = ${info.name}_Component;
|
||||
// Register with capitalized name for proper React convention
|
||||
componentRegistry["${info.nameCapitalized}"] = ${info.name}_Component;
|
||||
`).join('\n')}
|
||||
`,
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
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
|
||||
const compiledBundle = Babel.transform(bundledCode, {
|
||||
presets: ['react', 'typescript'],
|
||||
filename: 'components-bundle.tsx'
|
||||
}).code;
|
||||
filename: 'components-bundle.tsx',
|
||||
}).code
|
||||
|
||||
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
|
||||
const componentsFactory = new Function('React', `return ${compiledBundle}`);
|
||||
const compiledComponentsRegistry = componentsFactory(React);
|
||||
const componentsFactory = new Function(
|
||||
'React',
|
||||
'Alert',
|
||||
'Avatar',
|
||||
'Badge',
|
||||
'Button',
|
||||
'Calendar',
|
||||
'Card',
|
||||
'Checkbox',
|
||||
'ConfigProvider',
|
||||
'DatePicker',
|
||||
'Dialog',
|
||||
'Drawer',
|
||||
'Dropdown',
|
||||
'FormItem',
|
||||
'FormContainer',
|
||||
'Input',
|
||||
'InputGroup',
|
||||
'Menu',
|
||||
'MenuItem',
|
||||
'Notification',
|
||||
'Pagination',
|
||||
'Progress',
|
||||
'Radio',
|
||||
'RangeCalendar',
|
||||
'ScrollBar',
|
||||
'Segment',
|
||||
'Select',
|
||||
'Skeleton',
|
||||
'Spinner',
|
||||
'Steps',
|
||||
'Switcher',
|
||||
'Table',
|
||||
'Tabs',
|
||||
'Tag',
|
||||
'TimeInput',
|
||||
'Timeline',
|
||||
'toast',
|
||||
'Tooltip',
|
||||
'Upload',
|
||||
`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) {
|
||||
console.error('Error compiling components bundle:', error);
|
||||
setCompiledComponents({});
|
||||
console.error('Error compiling components bundle:', error)
|
||||
setCompiledComponents({})
|
||||
}
|
||||
}, [components, extractComponentInfo]);
|
||||
}, [components, extractComponentInfo])
|
||||
|
||||
const compileCode = useCallback((code: string) => {
|
||||
const compileCode = useCallback(
|
||||
(code: string) => {
|
||||
try {
|
||||
// Clean the code and extract component name
|
||||
const cleanCode = code
|
||||
.replace(/import\s+.*?;/g, '')
|
||||
.replace(/export\s+default\s+/, '')
|
||||
.trim();
|
||||
.trim()
|
||||
|
||||
// Try to extract a meaningful name from the component,
|
||||
// but don't generate random names that could cause reference issues
|
||||
const componentName = extractComponentInfo(code, 'AnonComponent');
|
||||
const componentName = extractComponentInfo(code, 'AnonComponent')
|
||||
|
||||
// Extract all potential component references from JSX
|
||||
// Look for JSX tags like <ComponentName ...> or <ComponentName/>
|
||||
const jsxComponentRegex = /<([A-Z][A-Za-z0-9_]*)/g;
|
||||
const jsxMatches = [...cleanCode.matchAll(jsxComponentRegex)].map(match => match[1]);
|
||||
const jsxComponentRegex = /<([A-Z][A-Za-z0-9_]*)/g
|
||||
const jsxMatches = [...cleanCode.matchAll(jsxComponentRegex)].map((match) => match[1])
|
||||
|
||||
// Get unique component names from JSX
|
||||
const jsxComponentNames = [...new Set(jsxMatches)];
|
||||
const jsxComponentNames = [...new Set(jsxMatches)]
|
||||
|
||||
// Generate a warning for JSX tags that might be components
|
||||
if (jsxComponentNames.length > 0) {
|
||||
console.log("JSX tags that might be components:", jsxComponentNames.join(", "));
|
||||
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
|
||||
|
|
@ -300,20 +464,23 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
throw error;
|
||||
}
|
||||
})(React, compiledComponentsObj)
|
||||
`;
|
||||
`
|
||||
|
||||
// Compile the code
|
||||
const compiledCode = Babel.transform(transformedCode, {
|
||||
presets: ['react', 'typescript'],
|
||||
filename: 'component.tsx'
|
||||
}).code;
|
||||
filename: 'component.tsx',
|
||||
}).code
|
||||
|
||||
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
|
||||
const ComponentFactory = new Function('React', 'compiledComponentsObj', `
|
||||
const ComponentFactory = new Function(
|
||||
'React',
|
||||
'compiledComponentsObj',
|
||||
`
|
||||
try {
|
||||
// Create a local variable to ensure it exists
|
||||
var AnonComponent;
|
||||
|
|
@ -350,62 +517,72 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
]);
|
||||
};
|
||||
}
|
||||
`);
|
||||
`,
|
||||
)
|
||||
|
||||
// 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') {
|
||||
throw new Error('Invalid component definition');
|
||||
throw new Error('Invalid component definition')
|
||||
}
|
||||
|
||||
return Component;
|
||||
return Component
|
||||
} catch (error) {
|
||||
console.error('Component compilation error:', error);
|
||||
console.error('Component compilation error:', error)
|
||||
return () => (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<p className="font-semibold mb-2">Compilation Error</p>
|
||||
<div className="text-sm whitespace-pre-wrap">{String(error)}</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
}, [extractComponentInfo, compiledComponents]);
|
||||
},
|
||||
[extractComponentInfo, compiledComponents],
|
||||
)
|
||||
|
||||
const compileAndRender = useCallback((code: string, props: ComponentProps = {}) => {
|
||||
const compileAndRender = useCallback(
|
||||
(code: string, props: ComponentProps = {}) => {
|
||||
if (!code?.trim()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a component without adding it to registry yet
|
||||
const Component = compileCode(code);
|
||||
return <Component {...props} />;
|
||||
const Component = compileCode(code)
|
||||
return <Component {...props} />
|
||||
} catch (error) {
|
||||
console.error('Render error:', error);
|
||||
console.error('Render error:', error)
|
||||
return (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<p className="font-semibold mb-2">Render Error</p>
|
||||
<div className="text-sm whitespace-pre-wrap">{String(error)}</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
}, [compileCode]);
|
||||
},
|
||||
[compileCode],
|
||||
)
|
||||
|
||||
const renderComponent = useCallback((name: string, props: ComponentProps = {}) => {
|
||||
const renderComponent = useCallback(
|
||||
(name: string, props: ComponentProps = {}) => {
|
||||
// Check if the component is already compiled
|
||||
if (compiledComponents[name]) {
|
||||
const Component = compiledComponents[name];
|
||||
return <Component {...props} />;
|
||||
const Component = compiledComponents[name]
|
||||
return <Component {...props} />
|
||||
}
|
||||
|
||||
// Otherwise, try to compile it from the source code
|
||||
const component = components.find(c => c.name === name && c.isActive);
|
||||
const component = components.find((c) => c.name === name && c.isActive)
|
||||
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 (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<div className="text-sm">Component not found: {name}</div>
|
||||
<div className="text-xs mt-2">This could be because:
|
||||
<div className="text-xs mt-2">
|
||||
This could be because:
|
||||
<ul className="list-disc ml-5 mt-1">
|
||||
<li>The component has not been saved to the database</li>
|
||||
<li>The component name is misspelled</li>
|
||||
|
|
@ -413,7 +590,7 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -421,235 +598,277 @@ const ComponentRegistryProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
// This ensures all components are available for cross-referencing
|
||||
if (Object.keys(compiledComponents).length === 0) {
|
||||
// If no components are compiled yet, this is the first render
|
||||
console.warn("Component registry is empty. Components might not be available for references.");
|
||||
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) {
|
||||
console.error(`Error rendering component ${name}:`, error);
|
||||
console.error(`Error rendering component ${name}:`, error)
|
||||
return (
|
||||
<div className="p-4 border-2 border-red-300 rounded-lg bg-red-50 text-red-700">
|
||||
<div className="text-sm font-semibold">Error rendering {name}</div>
|
||||
<div className="text-xs mt-2 whitespace-pre-wrap">{String(error)}</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
}, [components, compileAndRender, compiledComponents]);
|
||||
},
|
||||
[components, compileAndRender, compiledComponents],
|
||||
)
|
||||
|
||||
const isComponentRegistered = useCallback((name: string) => {
|
||||
return components.some(c => c.name === name && c.isActive);
|
||||
}, [components]);
|
||||
const isComponentRegistered = useCallback(
|
||||
(name: string) => {
|
||||
return components.some((c) => c.name === name && c.isActive)
|
||||
},
|
||||
[components],
|
||||
)
|
||||
|
||||
const getRegisteredComponents = useCallback(() => {
|
||||
return components?.filter(c => c.isActive).map(c => c.name);
|
||||
}, [components]);
|
||||
return components?.filter((c) => c.isActive).map((c) => c.name)
|
||||
}, [components])
|
||||
|
||||
const getComponentCode = useCallback((name: string) => {
|
||||
const component = components.find(c => c.name === name);
|
||||
return component ? component.code : null;
|
||||
}, [components]);
|
||||
const getComponentCode = useCallback(
|
||||
(name: string) => {
|
||||
const component = components.find((c) => c.name === name)
|
||||
return component ? component.code : null
|
||||
},
|
||||
[components],
|
||||
)
|
||||
|
||||
// Create a helper to process DOM nodes and replace custom lowercase elements with actual components
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !document) return;
|
||||
if (typeof window === 'undefined' || !document) return
|
||||
|
||||
// Create a type definition for ReactDOM
|
||||
// This approach avoids TypeScript errors
|
||||
type ReactDOMType = {
|
||||
render: (element: React.ReactNode, container: Element) => void;
|
||||
createRoot?: (container: Element) => { render: (element: React.ReactNode) => void };
|
||||
};
|
||||
render: (element: React.ReactNode, container: Element) => void
|
||||
createRoot?: (container: Element) => { render: (element: React.ReactNode) => void }
|
||||
}
|
||||
|
||||
// Check if we have any components to work with
|
||||
if (!Object.keys(compiledComponents).length) return;
|
||||
if (!Object.keys(compiledComponents).length) return
|
||||
|
||||
// Get all lowercase component names from registry and add them to a Set for faster lookup
|
||||
const lowercaseComponentNames = new Set<string>();
|
||||
Object.keys(compiledComponents).forEach(name => {
|
||||
const lowercaseComponentNames = new Set<string>()
|
||||
Object.keys(compiledComponents).forEach((name) => {
|
||||
// Only include lowercase component names, but not standard HTML elements
|
||||
if (name.charAt(0) === name.charAt(0).toLowerCase() &&
|
||||
if (
|
||||
name.charAt(0) === name.charAt(0).toLowerCase() &&
|
||||
name.charAt(0) !== name.charAt(0).toUpperCase() &&
|
||||
!['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'button', 'a', 'img', 'input', 'form',
|
||||
'label', 'select', 'option', 'textarea', 'ul', 'ol', 'li', 'table', 'tr', 'td', 'th'].includes(name)) {
|
||||
lowercaseComponentNames.add(name);
|
||||
![
|
||||
'div',
|
||||
'span',
|
||||
'p',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'button',
|
||||
'a',
|
||||
'img',
|
||||
'input',
|
||||
'form',
|
||||
'label',
|
||||
'select',
|
||||
'option',
|
||||
'textarea',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'table',
|
||||
'tr',
|
||||
'td',
|
||||
'th',
|
||||
].includes(name)
|
||||
) {
|
||||
lowercaseComponentNames.add(name)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (lowercaseComponentNames.size === 0) return;
|
||||
if (lowercaseComponentNames.size === 0) return
|
||||
|
||||
// Create a function to process a DOM node and its children
|
||||
const processNode = (rootNode: Element) => {
|
||||
// Convert to array for better filtering
|
||||
const componentsToFind = [...lowercaseComponentNames];
|
||||
const componentsToFind = [...lowercaseComponentNames]
|
||||
|
||||
// Create CSS selector for all lowercase component tags
|
||||
const selector = componentsToFind.join(',');
|
||||
if (!selector) return false;
|
||||
const selector = componentsToFind.join(',')
|
||||
if (!selector) return false
|
||||
|
||||
// Find all matching elements
|
||||
const elements = rootNode.tagName && lowercaseComponentNames.has(rootNode.tagName.toLowerCase())
|
||||
const elements =
|
||||
rootNode.tagName && lowercaseComponentNames.has(rootNode.tagName.toLowerCase())
|
||||
? [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
|
||||
elements.forEach(element => {
|
||||
elements.forEach((element) => {
|
||||
// Skip if already processed
|
||||
if (element.hasAttribute('data-component-processed')) return;
|
||||
if (element.hasAttribute('data-component-processed')) return
|
||||
|
||||
try {
|
||||
// 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
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
const tagName = element.tagName.toLowerCase()
|
||||
|
||||
// Get the component from registry
|
||||
const Component = compiledComponents[tagName];
|
||||
const Component = compiledComponents[tagName]
|
||||
if (!Component) {
|
||||
console.warn(`Component ${tagName} exists in registry but couldn't be loaded`);
|
||||
return;
|
||||
console.warn(`Component ${tagName} exists in registry but couldn't be loaded`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get props from element attributes
|
||||
const props: Record<string, unknown> = {};
|
||||
Array.from(element.attributes).forEach(attr => {
|
||||
const props: Record<string, unknown> = {}
|
||||
Array.from(element.attributes).forEach((attr) => {
|
||||
// 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)
|
||||
const propName = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
let propValue: string | boolean | number = attr.value;
|
||||
const propName = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
let propValue: string | boolean | number = attr.value
|
||||
|
||||
// Handle boolean attributes (present without value)
|
||||
if (propValue === '' || propValue === propName) {
|
||||
propValue = true;
|
||||
propValue = true
|
||||
}
|
||||
|
||||
// Try to parse JSON values
|
||||
if (propValue && typeof propValue === 'string') {
|
||||
if ((propValue.startsWith('{') && propValue.endsWith('}')) ||
|
||||
if (
|
||||
(propValue.startsWith('{') && propValue.endsWith('}')) ||
|
||||
(propValue.startsWith('[') && propValue.endsWith(']')) ||
|
||||
propValue === 'true' || propValue === 'false' || !isNaN(Number(propValue))) { try {
|
||||
propValue = JSON.parse(propValue);
|
||||
propValue === 'true' ||
|
||||
propValue === 'false' ||
|
||||
!isNaN(Number(propValue))
|
||||
) {
|
||||
try {
|
||||
propValue = JSON.parse(propValue)
|
||||
} catch {
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
props[propName] = propValue;
|
||||
});
|
||||
props[propName] = propValue
|
||||
})
|
||||
|
||||
// Process children
|
||||
const children = Array.from(element.childNodes).map(child => {
|
||||
const children = Array.from(element.childNodes).map((child) => {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
return child.textContent;
|
||||
return child.textContent
|
||||
}
|
||||
return child;
|
||||
});
|
||||
return child
|
||||
})
|
||||
|
||||
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
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.display = 'contents'; // Don't add extra layout structure
|
||||
wrapper.dataset.customComponent = tagName;
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.style.display = 'contents' // Don't add extra layout structure
|
||||
wrapper.dataset.customComponent = tagName
|
||||
|
||||
// Insert wrapper and remove original
|
||||
element.parentNode?.insertBefore(wrapper, element);
|
||||
element.parentNode?.removeChild(element);
|
||||
element.parentNode?.insertBefore(wrapper, element)
|
||||
element.parentNode?.removeChild(element)
|
||||
|
||||
// Render React component into wrapper
|
||||
try {
|
||||
const reactElement = React.createElement(Component, props);
|
||||
const ReactDOM = (window as Window & typeof globalThis & { ReactDOM?: ReactDOMType }).ReactDOM;
|
||||
const reactElement = React.createElement(Component, props)
|
||||
const ReactDOM = (window as Window & typeof globalThis & { ReactDOM?: ReactDOMType })
|
||||
.ReactDOM
|
||||
|
||||
if (ReactDOM) {
|
||||
// Use modern createRoot API if available (React 18+)
|
||||
if (ReactDOM.createRoot) {
|
||||
const root = ReactDOM.createRoot(wrapper);
|
||||
root.render(reactElement);
|
||||
const root = ReactDOM.createRoot(wrapper)
|
||||
root.render(reactElement)
|
||||
}
|
||||
// Fallback to legacy render API
|
||||
else if (ReactDOM.render) {
|
||||
ReactDOM.render(reactElement, wrapper);
|
||||
ReactDOM.render(reactElement, wrapper)
|
||||
}
|
||||
} 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) {
|
||||
console.error(`Error rendering ${tagName} React component:`, err);
|
||||
console.error(`Error rendering ${tagName} React component:`, err)
|
||||
|
||||
// Show error UI in place of the component
|
||||
wrapper.innerHTML = `<div style="border: 1px solid red; color: red; padding: 8px;">
|
||||
Error rendering <${tagName}> component
|
||||
</div>`;
|
||||
</div>`
|
||||
}
|
||||
} 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
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let hasProcessed = false;
|
||||
let hasProcessed = false
|
||||
|
||||
mutations.forEach(mutation => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
// Process added nodes
|
||||
mutation.addedNodes.forEach(node => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// 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
|
||||
// This handles cases where elements are added before our observer is active
|
||||
if (!hasProcessed && document.body) {
|
||||
processNode(document.body);
|
||||
processNode(document.body)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Start observing the entire document
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
characterData: true,
|
||||
})
|
||||
|
||||
// Initial scan of the whole document
|
||||
if (document.body) {
|
||||
processNode(document.body);
|
||||
processNode(document.body)
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [compiledComponents]);
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [compiledComponents])
|
||||
|
||||
const value = {
|
||||
renderComponent,
|
||||
compileAndRender,
|
||||
isComponentRegistered,
|
||||
getRegisteredComponents,
|
||||
getComponentCode
|
||||
};
|
||||
getComponentCode,
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentRegistryContext.Provider value={value}>
|
||||
{children}
|
||||
</ComponentRegistryContext.Provider>
|
||||
);
|
||||
};
|
||||
<ComponentRegistryContext.Provider value={value}>{children}</ComponentRegistryContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComponentRegistryProvider;
|
||||
export default ComponentRegistryProvider
|
||||
|
|
|
|||
Loading…
Reference in a new issue