Fix I-1: landing-page edits lost when activePageId goes dangling

buildSavePayload matched the active page by page.id === activePageId,
but activePageId defaults to the hardcoded 'home' and load() never
updated it. Deleting the original Home page (its replacement gets id
page_<ts>), reloading, editing, and saving would then match no page --
the live edit only reached the legacy top-level craft_state, while the
authoritative pages[]/pages_craft_state[0] for index.html fell back to
stale state.

Belt-and-suspenders fix:
- buildSavePayload now falls back to pages[0] as the effective active
  page when a real page is active but activePageId matches nothing.
- useWhpApi's load() now points activePageId at the freshly restored
  first page via a new PageContext setActivePageIdDirect setter
  (bookkeeping only -- no re-serialize/deserialize), so activePageId
  stays valid after every load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 17:29:32 -07:00
parent e5f30a4a56
commit 25e674badd
3 changed files with 81 additions and 3 deletions
+23
View File
@@ -21,6 +21,17 @@ interface PageContextValue {
setHeaderCraftState: (craftState: string) => void;
setFooterCraftState: (craftState: string) => void;
setPagesCraftState: (pagesData: { id: string; name: string; slug: string; craftState: string | null }[]) => void;
/**
* Bookkeeping-only: point `activePageId` at an already-loaded page without
* re-serializing/deserializing the canvas (the caller -- e.g. useWhpApi's
* `load()` -- has already put the right state on the canvas itself). Used
* to fix I-1: `activePageId` defaults to the hardcoded `'home'` and
* `load()` never updated it, so after the original Home page was deleted
* (its replacement gets a fresh `page_<ts>` id) and the app reloaded,
* `activePageId` pointed at nothing in `pages`, and a subsequent edit+save
* only reached the legacy top-level fields.
*/
setActivePageIdDirect: (pageId: string) => void;
/** AI helpers — replace entire site or page with a new tree */
replaceAllPages: (pages: { name: string; tree: SerializedTreeNode }[]) => void;
replaceCurrentPage: (page: { name: string; tree: SerializedTreeNode }) => void;
@@ -149,6 +160,7 @@ const PageContext = createContext<PageContextValue>({
setHeaderCraftState: () => {},
setFooterCraftState: () => {},
setPagesCraftState: () => {},
setActivePageIdDirect: () => {},
replaceAllPages: () => {},
replaceCurrentPage: () => {},
setHeader: () => {},
@@ -385,6 +397,16 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
setFooterPage((prev) => ({ ...prev, craftState }));
}, []);
/**
* Bookkeeping-only setter for `activePageId` -- see the doc comment on
* `PageContextValue.setActivePageIdDirect`. Does NOT serialize/deserialize
* the canvas; callers that need that should use `switchPage` instead.
*/
const setActivePageIdDirect = useCallback((pageId: string) => {
setActivePageId(pageId);
activePageIdRef.current = pageId;
}, []);
/** Allow external code (e.g., load from API) to restore pages with craft states */
const setPagesCraftState = useCallback((pagesData: { id: string; name: string; slug: string; craftState: string | null }[]) => {
setPages(pagesData.map((p, i) => ({
@@ -492,6 +514,7 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
setHeaderCraftState,
setFooterCraftState,
setPagesCraftState,
setActivePageIdDirect,
replaceAllPages,
replaceCurrentPage,
setHeader,