Site builder: security & data-loss hardening + unified asset picker + audit backlog #3
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||||
import { useEditor } from '@craftjs/core';
|
import { useEditor } from '@craftjs/core';
|
||||||
import { usePages } from '../../state/PageContext';
|
import { usePages } from '../../state/PageContext';
|
||||||
import { useSiteDesign } from '../../state/SiteDesignContext';
|
import { useSiteDesign } from '../../state/SiteDesignContext';
|
||||||
@@ -50,6 +50,15 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({ open, onClose }) =
|
|||||||
const [applyDesign, setApplyDesign] = useState(true);
|
const [applyDesign, setApplyDesign] = useState(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Guards state updates after the component has unmounted mid-sequence.
|
||||||
|
const mountedRef = useRef(true);
|
||||||
|
useEffect(() => {
|
||||||
|
mountedRef.current = true;
|
||||||
|
return () => {
|
||||||
|
mountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Close on Escape key
|
// Close on Escape key
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -112,73 +121,63 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({ open, onClose }) =
|
|||||||
}
|
}
|
||||||
}, [query, actions]);
|
}, [query, actions]);
|
||||||
|
|
||||||
|
/** Promise-based `setTimeout` — lets the staged apply steps below be awaited
|
||||||
|
* in sequence instead of chained via nested callbacks. */
|
||||||
|
const wait = useCallback((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)), []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After all pages are loaded, apply header and footer template content.
|
* Switch to a zone (header/footer), clear it, and add the template's
|
||||||
* Switches to header/footer editing mode, clears, adds components, then returns to firstPageId.
|
* components, giving Craft.js a tick to settle between each step.
|
||||||
*/
|
*/
|
||||||
const applyHeaderFooter = useCallback(
|
const applyZone = useCallback(
|
||||||
(tpl: TemplateDefinition, firstPageId: string) => {
|
async (switchFn: () => void, components: TemplateComponent[]) => {
|
||||||
const hasHeader = tpl.header?.components?.length > 0;
|
|
||||||
const hasFooter = tpl.footer?.components?.length > 0;
|
|
||||||
|
|
||||||
if (!hasHeader && !hasFooter) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyZone = (
|
|
||||||
switchFn: () => void,
|
|
||||||
components: TemplateComponent[],
|
|
||||||
next: () => void,
|
|
||||||
) => {
|
|
||||||
switchFn();
|
switchFn();
|
||||||
setTimeout(() => {
|
await wait(30);
|
||||||
try {
|
try {
|
||||||
clearCanvas();
|
clearCanvas();
|
||||||
setTimeout(() => {
|
await wait(20);
|
||||||
try {
|
try {
|
||||||
addTemplateComponents(components);
|
addTemplateComponents(components);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to add zone components:', e);
|
console.warn('Failed to add zone components:', e);
|
||||||
}
|
}
|
||||||
setTimeout(next, 30);
|
|
||||||
}, 20);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to clear zone:', e);
|
console.warn('Failed to clear zone:', e);
|
||||||
setTimeout(next, 30);
|
|
||||||
}
|
|
||||||
}, 30);
|
|
||||||
};
|
|
||||||
|
|
||||||
const finish = () => {
|
|
||||||
// Switch back to the first page
|
|
||||||
switchPage(firstPageId);
|
|
||||||
setTimeout(() => setLoading(false), 30);
|
|
||||||
};
|
|
||||||
|
|
||||||
const doFooter = () => {
|
|
||||||
if (hasFooter) {
|
|
||||||
applyZone(editFooter, tpl.footer.components, finish);
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (hasHeader) {
|
|
||||||
applyZone(editHeader, tpl.header.components, doFooter);
|
|
||||||
} else {
|
|
||||||
doFooter();
|
|
||||||
}
|
}
|
||||||
|
await wait(30);
|
||||||
},
|
},
|
||||||
[clearCanvas, addTemplateComponents, switchPage, editHeader, editFooter],
|
[wait, clearCanvas, addTemplateComponents],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load the selected template
|
/**
|
||||||
const handleLoad = useCallback(() => {
|
* After all pages are loaded, apply header and footer template content.
|
||||||
|
* Switches to header/footer editing mode, clears, adds components, then returns to firstPageId.
|
||||||
|
*/
|
||||||
|
const applyHeaderFooter = useCallback(
|
||||||
|
async (tpl: TemplateDefinition, firstPageId: string) => {
|
||||||
|
const hasHeader = tpl.header?.components?.length > 0;
|
||||||
|
const hasFooter = tpl.footer?.components?.length > 0;
|
||||||
|
|
||||||
|
if (hasHeader) await applyZone(editHeader, tpl.header.components);
|
||||||
|
if (hasFooter) await applyZone(editFooter, tpl.footer.components);
|
||||||
|
|
||||||
|
// Switch back to the first page
|
||||||
|
switchPage(firstPageId);
|
||||||
|
await wait(30);
|
||||||
|
},
|
||||||
|
[applyZone, switchPage, editHeader, editFooter, wait],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load the selected template. Awaits the full staged sequence (clear, add
|
||||||
|
// components, add pages, apply header/footer) before closing the modal, so
|
||||||
|
// "Loading…" stays visible for the duration and onClose() never fires
|
||||||
|
// while the sequence is still mutating a canvas the dialog just tore down.
|
||||||
|
const handleLoad = useCallback(async () => {
|
||||||
if (!confirmTemplate) return;
|
if (!confirmTemplate) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const tpl = confirmTemplate;
|
const tpl = confirmTemplate;
|
||||||
|
setConfirmTemplate(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Optionally apply design tokens
|
// 1. Optionally apply design tokens
|
||||||
@@ -195,66 +194,46 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({ open, onClose }) =
|
|||||||
|
|
||||||
// 3. Clear the current canvas and add the first template page's components
|
// 3. Clear the current canvas and add the first template page's components
|
||||||
clearCanvas();
|
clearCanvas();
|
||||||
|
// Short delay to let the clear settle before adding new nodes
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
// Use a short delay to let the clear settle before adding new nodes
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
const firstPage = tpl.pages[0];
|
const firstPage = tpl.pages[0];
|
||||||
addTemplateComponents(firstPage.content.components);
|
addTemplateComponents(firstPage.content.components);
|
||||||
|
|
||||||
// 4. Add remaining pages (if multi-page template)
|
// 4. Add remaining pages (if multi-page template)
|
||||||
const finishPages = () => {
|
|
||||||
// 5. Apply header and footer template content
|
|
||||||
if (keepId) {
|
|
||||||
applyHeaderFooter(tpl, keepId);
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tpl.pages.length > 1) {
|
if (tpl.pages.length > 1) {
|
||||||
let pageIndex = 1;
|
await wait(30);
|
||||||
const addNextPage = () => {
|
for (let pageIndex = 1; pageIndex < tpl.pages.length; pageIndex++) {
|
||||||
if (pageIndex >= tpl.pages.length) {
|
|
||||||
// Switch back to first page, then apply header/footer
|
|
||||||
if (keepId) switchPage(keepId);
|
|
||||||
setTimeout(finishPages, 30);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageDef = tpl.pages[pageIndex];
|
const pageDef = tpl.pages[pageIndex];
|
||||||
addPage(pageDef.name, pageDef.slug);
|
addPage(pageDef.name, pageDef.slug);
|
||||||
|
|
||||||
// After addPage, the new page is active with an empty canvas.
|
// After addPage, the new page is active with an empty canvas.
|
||||||
// Give a tick for Craft.js to settle, then add components.
|
// Give a tick for Craft.js to settle, then add components.
|
||||||
setTimeout(() => {
|
await wait(30);
|
||||||
try {
|
try {
|
||||||
addTemplateComponents(pageDef.content.components);
|
addTemplateComponents(pageDef.content.components);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to add components for page', pageDef.name, e);
|
console.warn('Failed to add components for page', pageDef.name, e);
|
||||||
}
|
}
|
||||||
pageIndex++;
|
await wait(30);
|
||||||
setTimeout(addNextPage, 30);
|
}
|
||||||
}, 30);
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(addNextPage, 30);
|
// Switch back to the first page before applying header/footer
|
||||||
} else {
|
if (keepId) switchPage(keepId);
|
||||||
finishPages();
|
await wait(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Apply header and footer template content
|
||||||
|
if (keepId) {
|
||||||
|
await applyHeaderFooter(tpl, keepId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load template:', e);
|
console.error('Failed to load template:', e);
|
||||||
setLoading(false);
|
} finally {
|
||||||
}
|
if (mountedRef.current) setLoading(false);
|
||||||
}, 20);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load template:', e);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfirmTemplate(null);
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [confirmTemplate, applyDesign, query, actions, pages, addPage, switchPage, deletePage, updateDesign, addTemplateComponents, clearCanvas, applyHeaderFooter, onClose]);
|
}
|
||||||
|
}, [confirmTemplate, applyDesign, pages, addPage, switchPage, deletePage, updateDesign, addTemplateComponents, clearCanvas, applyHeaderFooter, wait, onClose]);
|
||||||
|
|
||||||
// Close on backdrop click
|
// Close on backdrop click
|
||||||
const handleBackdropClick = useCallback(
|
const handleBackdropClick = useCallback(
|
||||||
|
|||||||
Reference in New Issue
Block a user