my-test-app-builder-2024/src/renderer/RendererRoutes.tsx

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>
);
};