95 lines
3.0 KiB
TypeScript
95 lines
3.0 KiB
TypeScript
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
|
import type { PagesData } from '../common/types/component.types';
|
|
import { importFromJSON } from '../common/utils/serialization';
|
|
import { formatScopedBindingViolations, validatePagesDataScopedBindings } from '../common/state/validateScopedBindings';
|
|
import { RendererRuntimeProvider } from './runtime';
|
|
import { Preview } from './components/Preview';
|
|
|
|
// Ensure component definitions are registered once per bundle load.
|
|
import '../common/registry/definitions';
|
|
|
|
export interface RendererRoutesProps {
|
|
json: string;
|
|
workflow?: { executeUrl: string };
|
|
initialRoute?: string;
|
|
}
|
|
|
|
export const RendererRoutes: React.FC<RendererRoutesProps> = ({ json, workflow, initialRoute }) => {
|
|
const parseResult = useMemo<{ pagesData: PagesData | null; error: string | null }>(() => {
|
|
const parsed = importFromJSON(json);
|
|
if (!parsed || !Array.isArray(parsed.pages)) {
|
|
return { pagesData: null, error: 'Invalid renderer JSON.' };
|
|
}
|
|
|
|
const violations = validatePagesDataScopedBindings(parsed);
|
|
if (violations.length > 0) {
|
|
return {
|
|
pagesData: null,
|
|
error: `Invalid scoped bindings detected:\n${formatScopedBindingViolations(violations)}`,
|
|
};
|
|
}
|
|
|
|
return { pagesData: parsed, error: null };
|
|
}, [json]);
|
|
|
|
const defaultRoute = useMemo(() => {
|
|
if (!parseResult.pagesData) return initialRoute || '/';
|
|
const defaultPage = parseResult.pagesData.pages.find((p) => p.isDefault) || parseResult.pagesData.pages[0];
|
|
return initialRoute || defaultPage?.route || '/';
|
|
}, [parseResult.pagesData, initialRoute]);
|
|
|
|
const getHashRoute = (): string | null => {
|
|
const hash = window.location.hash;
|
|
return hash ? hash.slice(1) : null;
|
|
};
|
|
|
|
const [currentRoute, setCurrentRoute] = useState<string>(
|
|
getHashRoute() || defaultRoute
|
|
);
|
|
|
|
// Set initial hash on mount
|
|
useEffect(() => {
|
|
if (!window.location.hash) {
|
|
window.location.hash = defaultRoute;
|
|
}
|
|
}, []);
|
|
|
|
const handleNavigate = useCallback((route: string) => {
|
|
window.location.hash = route;
|
|
setCurrentRoute(route);
|
|
}, []);
|
|
|
|
// Sync state with browser back/forward
|
|
useEffect(() => {
|
|
const onHashChange = () => {
|
|
const route = getHashRoute();
|
|
if (route) setCurrentRoute(route);
|
|
};
|
|
window.addEventListener('hashchange', onHashChange);
|
|
return () => window.removeEventListener('hashchange', onHashChange);
|
|
}, []);
|
|
|
|
if (!parseResult.pagesData || !Array.isArray(parseResult.pagesData.pages)) {
|
|
return (
|
|
<div style={{ padding: 16, fontFamily: 'system-ui, sans-serif', color: '#333', whiteSpace: 'pre-wrap' }}>
|
|
{parseResult.error || 'Invalid renderer JSON.'}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<RendererRuntimeProvider
|
|
pagesData={parseResult.pagesData}
|
|
workflow={workflow}
|
|
currentRoute={currentRoute}
|
|
onNavigate={handleNavigate}
|
|
>
|
|
<Preview
|
|
pages={parseResult.pagesData.pages}
|
|
currentRoute={currentRoute}
|
|
onNavigate={handleNavigate}
|
|
/>
|
|
</RendererRuntimeProvider>
|
|
);
|
|
};
|