Merge pull request #236 from presenton/fix/custom_ai_layout
Fix/custom ai layout
This commit is contained in:
commit
afb32c4c1a
13 changed files with 306 additions and 113 deletions
|
|
@ -8,7 +8,7 @@ events {
|
|||
}
|
||||
|
||||
http {
|
||||
client_max_body_size 20M;
|
||||
client_max_body_size 100M;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ async def upload_files(files: Optional[List[UploadFile]]):
|
|||
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(get_random_uuid())
|
||||
|
||||
validate_files(files, True, True, 50, UPLOAD_ACCEPTED_FILE_TYPES)
|
||||
validate_files(files, True, True, 100, UPLOAD_ACCEPTED_FILE_TYPES)
|
||||
|
||||
temp_files: List[str] = []
|
||||
if files:
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ async def process_pdf_slides(
|
|||
status_code=400,
|
||||
detail=f"Invalid file type. Expected PDF file, got {pdf_file.content_type}"
|
||||
)
|
||||
# Enforce 100MB size limit
|
||||
if hasattr(pdf_file, "size") and pdf_file.size and pdf_file.size > (100 * 1024 * 1024):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="PDF file exceeded max upload size of 100 MB",
|
||||
)
|
||||
|
||||
# Create temporary directory for processing
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
|
|||
|
|
@ -275,6 +275,12 @@ async def process_pptx_slides(
|
|||
status_code=400,
|
||||
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}"
|
||||
)
|
||||
# Enforce 100MB size limit
|
||||
if hasattr(pptx_file, "size") and pptx_file.size and pptx_file.size > (100 * 1024 * 1024):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="PPTX file exceeded max upload size of 100 MB",
|
||||
)
|
||||
|
||||
# Create temporary directory for processing
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
|
|||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="w-5 h-5" />
|
||||
Upload PPTX File
|
||||
Upload PDF or PPTX File
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Select a PowerPoint file (.pptx) to process. Maximum file size: 50MB
|
||||
Select a PDF or PowerPoint file (.pdf or .pptx) to process. Maximum file size: 100MB
|
||||
</CardDescription>
|
||||
{slides.length > 0 && (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
|
|
@ -56,12 +56,12 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
|
|||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<Label htmlFor="file-upload" className="cursor-pointer">
|
||||
<span className="text-lg font-medium text-gray-700">
|
||||
Click to upload a PPTX file
|
||||
Click to upload a PDF or PPTX file
|
||||
</span>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept=".pptx"
|
||||
accept=".pdf,.pptx"
|
||||
onChange={handleFileSelect}
|
||||
className="opacity-0 w-full h-full cursor-pointer absolute top-0 left-0 z-10"
|
||||
/>
|
||||
|
|
@ -106,7 +106,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
|
|||
{isProcessingPptx
|
||||
? "Extracting Slides..."
|
||||
: !selectedFile
|
||||
? "Select a PPTX file"
|
||||
? "Select a PDF or PPTX file"
|
||||
: "Process File"}
|
||||
</Button>
|
||||
{isProcessingPptx && <Timer duration={90} />}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,18 @@ export const useFileUpload = () => {
|
|||
if (!file) return;
|
||||
|
||||
// Validate file type
|
||||
if (!file.name.toLowerCase().endsWith(".pptx")) {
|
||||
toast.error("Please select a valid PPTX file");
|
||||
const lowerName = file.name.toLowerCase();
|
||||
const isPptx = lowerName.endsWith(".pptx");
|
||||
const isPdf = lowerName.endsWith(".pdf");
|
||||
if (!isPptx && !isPdf) {
|
||||
toast.error("Please select a valid PDF or PPTX file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (50MB limit)
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB
|
||||
// Validate file size (100MB limit)
|
||||
const maxSize = 100 * 1024 * 1024; // 100MB
|
||||
if (file.size > maxSize) {
|
||||
toast.error("File size must be less than 50MB");
|
||||
toast.error("File size must be less than 100MB");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,10 +115,10 @@ export const useSlideProcessing = (
|
|||
[]
|
||||
);
|
||||
|
||||
// Process PPTX file to extract slides
|
||||
// Process PDF or PPTX file to extract slides
|
||||
const processFile = useCallback(async () => {
|
||||
if (!selectedFile) {
|
||||
toast.error("Please select a PPTX file first");
|
||||
toast.error("Please select a PDF or PPTX file first");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -126,30 +126,51 @@ export const useSlideProcessing = (
|
|||
setIsProcessingPptx(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("pptx_file", selectedFile);
|
||||
const fileName = selectedFile.name.toLowerCase();
|
||||
const isPdf = fileName.endsWith(".pdf");
|
||||
const isPptx = fileName.endsWith(".pptx");
|
||||
|
||||
const pptxResponse = await fetch("/api/v1/ppt/pptx-slides/process", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const pptxData = await ApiResponseHandler.handleResponse(
|
||||
pptxResponse,
|
||||
"Failed to process PPTX file"
|
||||
);
|
||||
|
||||
if (!pptxData.success || !pptxData.slides?.length) {
|
||||
throw new Error("No slides found in the PPTX file");
|
||||
let slidesResponseData: any = null;
|
||||
if (isPdf) {
|
||||
formData.append("pdf_file", selectedFile);
|
||||
const pdfResponse = await fetch("/api/v1/ppt/pdf-slides/process", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
slidesResponseData = await ApiResponseHandler.handleResponse(
|
||||
pdfResponse,
|
||||
"Failed to process PDF file"
|
||||
);
|
||||
} else if (isPptx) {
|
||||
formData.append("pptx_file", selectedFile);
|
||||
const pptxResponse = await fetch("/api/v1/ppt/pptx-slides/process", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
slidesResponseData = await ApiResponseHandler.handleResponse(
|
||||
pptxResponse,
|
||||
"Failed to process PPTX file"
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported file type. Please upload a PDF or PPTX file.");
|
||||
}
|
||||
|
||||
// Extract fonts data from the response
|
||||
if (pptxData.fonts) {
|
||||
setFontsData(pptxData.fonts);
|
||||
if (!slidesResponseData.success || !slidesResponseData.slides?.length) {
|
||||
throw new Error("No slides found in the uploaded file");
|
||||
}
|
||||
|
||||
// Initialize slides with skeleton state
|
||||
const initialSlides: ProcessedSlide[] = pptxData.slides.map(
|
||||
// Extract fonts data only for PPTX where available
|
||||
if (slidesResponseData.fonts) {
|
||||
setFontsData(slidesResponseData.fonts);
|
||||
}
|
||||
|
||||
// Initialize slides with skeleton state; for PDF, xml/fonts won't exist
|
||||
const initialSlides: ProcessedSlide[] = slidesResponseData.slides.map(
|
||||
(slide: any) => ({
|
||||
...slide,
|
||||
slide_number: slide.slide_number,
|
||||
screenshot_url: slide.screenshot_url,
|
||||
xml_content: slide.xml_content ?? "",
|
||||
normalized_fonts: slide.normalized_fonts ?? [],
|
||||
processing: false,
|
||||
processed: false,
|
||||
})
|
||||
|
|
@ -157,7 +178,7 @@ export const useSlideProcessing = (
|
|||
|
||||
setSlides(initialSlides);
|
||||
|
||||
const hasUnsupported = Array.isArray(pptxData.fonts?.not_supported_fonts) && pptxData.fonts.not_supported_fonts.length > 0;
|
||||
const hasUnsupported = Array.isArray(slidesResponseData.fonts?.not_supported_fonts) && slidesResponseData.fonts.not_supported_fonts.length > 0;
|
||||
|
||||
toast.success(
|
||||
`Template Processing Finished`,
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ const CustomTemplatePage = () => {
|
|||
Custom Template Processor
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
Upload your PPTX file to extract slides and convert them to
|
||||
template which then can be used to generate AI presentations.
|
||||
Upload your PDF or PPTX file to extract slides and convert them to
|
||||
a template which you can use to generate AI presentations.
|
||||
</p>
|
||||
<div className="max-w-2xl mx-auto mt-2">
|
||||
<div className="inline-block rounded border border-orange-200 bg-orange-50 px-3 py-2 text-sm text-orange-700">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type React from "react";
|
|||
export interface SlideData {
|
||||
slide_number: number;
|
||||
screenshot_url: string;
|
||||
xml_content: string;
|
||||
xml_content?: string;
|
||||
normalized_fonts?: string[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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