fix: parse for error in slide of template beforehand and show eht error while edit
This commit is contained in:
parent
65bff822eb
commit
3bf5280a25
4 changed files with 234 additions and 77 deletions
|
|
@ -0,0 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface SlideErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface SlideErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export class SlideErrorBoundary extends React.Component<
|
||||
SlideErrorBoundaryProps,
|
||||
SlideErrorBoundaryState
|
||||
> {
|
||||
constructor(props: SlideErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, errorMessage: "" };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: unknown): SlideErrorBoundaryState {
|
||||
return {
|
||||
hasError: true,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: unknown) {
|
||||
// Optionally log to an error reporting service
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Slide render error:", error);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="aspect-video w-full h-full bg-red-50 text-red-700 flex flex-col items-start justify-start p-4 space-y-2 rounded-md border border-red-200">
|
||||
<div className="text-sm font-semibold">
|
||||
{this.props.label ? `${this.props.label} render error` : "Slide render error"}
|
||||
</div>
|
||||
<pre className="text-xs whitespace-pre-wrap break-words max-h-full overflow-auto bg-red-100 rounded-md p-2 border border-red-200">
|
||||
{this.state.errorMessage}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlideErrorBoundary;
|
||||
|
||||
|
||||
|
|
@ -378,80 +378,167 @@ export const LayoutProvider: React.FC<{
|
|||
const groupLayouts: LayoutInfo[] = [];
|
||||
const groupFullData: FullDataInfo[] = [];
|
||||
|
||||
for (const i of allLayout) {
|
||||
/* ---------- 1. compile JSX to plain script ------------------ */
|
||||
const module = compileCustomLayout(i.layout_code, React, z);
|
||||
|
||||
if (!module.default) {
|
||||
toast.error(`Custom Layout has no default export`, {
|
||||
description:
|
||||
"Please ensure the layout file exports a default component",
|
||||
});
|
||||
console.warn(`❌ Custom Layout has no default export`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!module.Schema) {
|
||||
toast.error(`Custom Layout has no Schema export`, {
|
||||
description: "Please ensure the layout file exports a Schema",
|
||||
});
|
||||
console.warn(`❌ Custom Layout has no Schema export`);
|
||||
continue;
|
||||
}
|
||||
const cacheKey = createCacheKey(
|
||||
`custom-${presentationId}`,
|
||||
i.layout_name
|
||||
// Helper to create an inline error component for this specific slide
|
||||
const createErrorComponent = (title: string, message: string): React.ComponentType<{ data: any }> => {
|
||||
const ErrorSlide: React.FC<{ data: any }> = () => (
|
||||
<div className="aspect-video w-full h-full bg-red-50 text-red-700 flex flex-col items-start justify-start p-4 space-y-2">
|
||||
<div className="text-sm font-semibold">{title}</div>
|
||||
<pre className="text-xs whitespace-pre-wrap break-words max-h-full overflow-auto bg-red-100 rounded-md p-2 border border-red-200">{message}</pre>
|
||||
</div>
|
||||
);
|
||||
if (!layoutCache.has(cacheKey)) {
|
||||
layoutCache.set(cacheKey, module.default);
|
||||
ErrorSlide.displayName = "CustomTemplateErrorSlide";
|
||||
return ErrorSlide;
|
||||
};
|
||||
|
||||
for (const i of allLayout) {
|
||||
try {
|
||||
/* ---------- 1. compile JSX to plain script ------------------ */
|
||||
const module = compileCustomLayout(i.layout_code, React, z);
|
||||
|
||||
// Determine identifiers even if subsequent steps fail
|
||||
const originalLayoutId =
|
||||
(module && (module as any).layoutId) ||
|
||||
i.layout_name.toLowerCase().replace(/layout$/, "");
|
||||
const uniqueKey = `${`custom-${presentationId}`}:${originalLayoutId}`;
|
||||
const layoutName =
|
||||
(module && (module as any).layoutName) ||
|
||||
i.layout_name.replace(/([A-Z])/g, " $1").trim();
|
||||
const layoutDescription =
|
||||
(module && (module as any).layoutDescription) ||
|
||||
`${layoutName} layout for presentations`;
|
||||
|
||||
let fullData: FullDataInfo | null = null;
|
||||
let jsonSchema: any = null;
|
||||
let componentToUse: React.ComponentType<{ data: any } | any> | null = null;
|
||||
let sampleData: any = {};
|
||||
|
||||
// Validate exports
|
||||
if (!module || !(module as any).default) {
|
||||
const errorComp = createErrorComponent(
|
||||
`Invalid export in ${i.layout_name}`,
|
||||
"Default export not found. Please export a default React component."
|
||||
);
|
||||
componentToUse = errorComp;
|
||||
jsonSchema = {};
|
||||
} else if (!(module as any).Schema) {
|
||||
const errorComp = createErrorComponent(
|
||||
`Schema missing in ${i.layout_name}`,
|
||||
"Schema export not found. Please export a Zod Schema as 'Schema'."
|
||||
);
|
||||
componentToUse = errorComp;
|
||||
jsonSchema = {};
|
||||
} else {
|
||||
// Cache valid component
|
||||
const cacheKey = createCacheKey(
|
||||
`custom-${presentationId}`,
|
||||
i.layout_name
|
||||
);
|
||||
if (!layoutCache.has(cacheKey)) {
|
||||
layoutCache.set(cacheKey, (module as any).default);
|
||||
}
|
||||
componentToUse = (module as any).default;
|
||||
|
||||
// Build schema and sample data with guards
|
||||
try {
|
||||
jsonSchema = z.toJSONSchema((module as any).Schema, {
|
||||
override: (ctx) => {
|
||||
delete ctx.jsonSchema.default;
|
||||
},
|
||||
});
|
||||
} catch (schemaErr: any) {
|
||||
const errorComp = createErrorComponent(
|
||||
`Schema generation failed for ${i.layout_name}`,
|
||||
schemaErr?.message || String(schemaErr)
|
||||
);
|
||||
componentToUse = errorComp;
|
||||
jsonSchema = {};
|
||||
}
|
||||
|
||||
if (componentToUse !== null && componentToUse !== (module as any).default) {
|
||||
// componentToUse already replaced with error component
|
||||
sampleData = {};
|
||||
} else {
|
||||
try {
|
||||
sampleData = (module as any).Schema.parse({});
|
||||
} catch (parseErr: any) {
|
||||
const errorComp = createErrorComponent(
|
||||
`Schema.parse failed for ${i.layout_name}`,
|
||||
parseErr?.message || String(parseErr)
|
||||
);
|
||||
componentToUse = errorComp;
|
||||
sampleData = {};
|
||||
jsonSchema = jsonSchema || {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customFonts.set(presentationId, i.fonts);
|
||||
|
||||
const layout: LayoutInfo = {
|
||||
id: uniqueKey,
|
||||
name: layoutName,
|
||||
description: layoutDescription,
|
||||
json_schema: jsonSchema,
|
||||
groupName: groupName,
|
||||
};
|
||||
|
||||
fullData = {
|
||||
name: layoutName,
|
||||
component: componentToUse as React.ComponentType<any>,
|
||||
schema: jsonSchema,
|
||||
sampleData: sampleData,
|
||||
fileName: i.layout_name,
|
||||
groupName: groupName,
|
||||
layoutId: uniqueKey,
|
||||
};
|
||||
|
||||
groupFullData.push(fullData);
|
||||
|
||||
layoutsById.set(uniqueKey, layout);
|
||||
layoutsByGroup.get(groupName)!.add(uniqueKey);
|
||||
fileMap.set(uniqueKey, {
|
||||
fileName: i.layout_name,
|
||||
groupName: groupName,
|
||||
});
|
||||
groupLayouts.push(layout);
|
||||
layouts.push(layout);
|
||||
} catch (e: any) {
|
||||
// Handle compilation/runtime errors during transformation
|
||||
const uniqueKey = `${`custom-${presentationId}`}:${i.layout_name.toLowerCase().replace(/layout$/, "")}`;
|
||||
const layoutName = i.layout_name.replace(/([A-Z])/g, " $1").trim();
|
||||
const errorComp = createErrorComponent(
|
||||
`Compilation error in ${i.layout_name}`,
|
||||
e?.message || String(e)
|
||||
);
|
||||
|
||||
const layout: LayoutInfo = {
|
||||
id: uniqueKey,
|
||||
name: layoutName,
|
||||
description: `Failed to compile ${i.layout_name}`,
|
||||
json_schema: {},
|
||||
groupName: groupName,
|
||||
};
|
||||
|
||||
const fullData: FullDataInfo = {
|
||||
name: layoutName,
|
||||
component: errorComp,
|
||||
schema: {},
|
||||
sampleData: {},
|
||||
fileName: i.layout_name,
|
||||
groupName: groupName,
|
||||
layoutId: uniqueKey,
|
||||
};
|
||||
|
||||
groupFullData.push(fullData);
|
||||
layoutsById.set(uniqueKey, layout);
|
||||
layoutsByGroup.get(groupName)!.add(uniqueKey);
|
||||
fileMap.set(uniqueKey, {
|
||||
fileName: i.layout_name,
|
||||
groupName: groupName,
|
||||
});
|
||||
groupLayouts.push(layout);
|
||||
layouts.push(layout);
|
||||
}
|
||||
|
||||
customFonts.set(presentationId, i.fonts);
|
||||
|
||||
const originalLayoutId =
|
||||
module.layoutId ||
|
||||
i.layout_name.toLowerCase().replace(/layout$/, "");
|
||||
const uniqueKey = `${`custom-${presentationId}`}:${originalLayoutId}`;
|
||||
const layoutName =
|
||||
module.layoutName ||
|
||||
i.layout_name.replace(/([A-Z])/g, " $1").trim();
|
||||
const layoutDescription =
|
||||
module.layoutDescription ||
|
||||
`${layoutName} layout for presentations`;
|
||||
|
||||
const jsonSchema = z.toJSONSchema(module.Schema, {
|
||||
override: (ctx) => {
|
||||
delete ctx.jsonSchema.default;
|
||||
},
|
||||
});
|
||||
|
||||
const layout: LayoutInfo = {
|
||||
id: uniqueKey,
|
||||
name: layoutName,
|
||||
description: layoutDescription,
|
||||
json_schema: jsonSchema,
|
||||
groupName: groupName,
|
||||
};
|
||||
const sampleData = module.Schema.parse({});
|
||||
const fullData: FullDataInfo = {
|
||||
name: layoutName,
|
||||
component: module.default,
|
||||
schema: jsonSchema,
|
||||
sampleData: sampleData,
|
||||
fileName: i.layout_name,
|
||||
groupName: groupName,
|
||||
layoutId: uniqueKey,
|
||||
};
|
||||
groupFullData.push(fullData);
|
||||
|
||||
layoutsById.set(uniqueKey, layout);
|
||||
layoutsByGroup.get(groupName)!.add(uniqueKey);
|
||||
fileMap.set(uniqueKey, {
|
||||
fileName: i.layout_name,
|
||||
groupName: groupName,
|
||||
});
|
||||
groupLayouts.push(layout);
|
||||
layouts.push(layout);
|
||||
}
|
||||
setCustomTemplateFonts(customFonts);
|
||||
// Cache grouped layouts
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React, { useMemo } from "react";
|
|||
import { useDispatch } from "react-redux";
|
||||
import { useLayout } from "../context/LayoutContext";
|
||||
import EditableLayoutWrapper from "../components/EditableLayoutWrapper";
|
||||
import SlideErrorBoundary from "../components/SlideErrorBoundary";
|
||||
import TiptapTextReplacer from "../components/TiptapTextReplacer";
|
||||
import { updateSlideContent } from "../../../store/slices/presentationGeneration";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
|
@ -66,7 +67,6 @@ export const useGroupLayouts = () => {
|
|||
dataPath: string,
|
||||
slideIndex?: number
|
||||
) => {
|
||||
// Dispatch Redux action to update slide content
|
||||
if (dataPath && slideIndex !== undefined) {
|
||||
dispatch(
|
||||
updateSlideContent({
|
||||
|
|
@ -78,12 +78,18 @@ export const useGroupLayouts = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Layout data={slide.content} />
|
||||
<SlideErrorBoundary label={`Slide ${slide.index + 1}`}>
|
||||
<Layout data={slide.content} />
|
||||
</SlideErrorBoundary>
|
||||
</TiptapTextReplacer>
|
||||
</EditableLayoutWrapper>
|
||||
);
|
||||
}
|
||||
return <Layout data={slide.content} />;
|
||||
return (
|
||||
<SlideErrorBoundary label={`Slide ${slide.index + 1}`}>
|
||||
<Layout data={slide.content} />
|
||||
</SlideErrorBoundary>
|
||||
);
|
||||
};
|
||||
}, [getGroupLayout, dispatch]);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
getLayoutsByGroup,
|
||||
getGroupSetting,
|
||||
getAllGroups,
|
||||
getFullDataByGroup,
|
||||
loading
|
||||
} = useLayout();
|
||||
|
||||
|
|
@ -47,7 +48,14 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
const groups = getAllGroups();
|
||||
if (groups.length === 0) return [];
|
||||
|
||||
const Groups: LayoutGroup[] = groups.map(groupName => {
|
||||
const Groups: LayoutGroup[] = groups
|
||||
.filter(groupName => {
|
||||
// Filter out groups that contain any errored layouts (from custom templates compile/parse errors)
|
||||
const fullData = getFullDataByGroup(groupName);
|
||||
const hasErroredLayouts = fullData.some(fd => (fd as any)?.component?.displayName === "CustomTemplateErrorSlide");
|
||||
return !hasErroredLayouts;
|
||||
})
|
||||
.map(groupName => {
|
||||
const settings = getGroupSetting(groupName);
|
||||
const customMeta = summaryMap[groupName];
|
||||
const isCustom = groupName.toLowerCase().startsWith("custom-");
|
||||
|
|
@ -66,7 +74,7 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
if (!a.default && b.default) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [getAllGroups, getLayoutsByGroup, getGroupSetting, summaryMap]);
|
||||
}, [getAllGroups, getLayoutsByGroup, getGroupSetting, getFullDataByGroup, summaryMap]);
|
||||
|
||||
const inBuiltGroups = React.useMemo(
|
||||
() => layoutGroups.filter(g => !g.id.toLowerCase().startsWith("custom-")),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue