feat(builder): send + restore site head code in save/load
Extend the save payload with head_code + design so the backend can inject SiteDesign.headCode into published pages, and restore design tokens on load() so the editor reflects the last-saved state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import { buildSavePayload } from './useWhpApi';
|
import { buildSavePayload } from './useWhpApi';
|
||||||
import { PageData } from '../types';
|
import { PageData } from '../types';
|
||||||
|
import { DEFAULT_SITE_DESIGN } from '../state/SiteDesignContext';
|
||||||
|
|
||||||
const pageA: PageData = { id: 'home', name: 'Home', slug: 'index', craftState: 'STORED_HOME' };
|
const pageA: PageData = { id: 'home', name: 'Home', slug: 'index', craftState: 'STORED_HOME' };
|
||||||
const pageB: PageData = { id: 'page_2', name: 'About', slug: 'about', craftState: 'STORED_ABOUT' };
|
const pageB: PageData = { id: 'page_2', name: 'About', slug: 'about', craftState: 'STORED_ABOUT' };
|
||||||
@@ -19,6 +20,8 @@ describe('buildSavePayload', () => {
|
|||||||
activePageId: '__header__',
|
activePageId: '__header__',
|
||||||
isEditingHeader: true,
|
isEditingHeader: true,
|
||||||
isEditingFooter: false,
|
isEditingFooter: false,
|
||||||
|
headCode: DEFAULT_SITE_DESIGN.headCode,
|
||||||
|
design: DEFAULT_SITE_DESIGN,
|
||||||
});
|
});
|
||||||
|
|
||||||
// The fresh live canvas must be reflected in header_craft_state, not the stale stored one.
|
// The fresh live canvas must be reflected in header_craft_state, not the stale stored one.
|
||||||
@@ -52,6 +55,8 @@ describe('buildSavePayload', () => {
|
|||||||
activePageId: '__footer__',
|
activePageId: '__footer__',
|
||||||
isEditingHeader: false,
|
isEditingHeader: false,
|
||||||
isEditingFooter: true,
|
isEditingFooter: true,
|
||||||
|
headCode: DEFAULT_SITE_DESIGN.headCode,
|
||||||
|
design: DEFAULT_SITE_DESIGN,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(payload.footer_craft_state).toBe('LIVE_FOOTER');
|
expect(payload.footer_craft_state).toBe('LIVE_FOOTER');
|
||||||
@@ -75,6 +80,8 @@ describe('buildSavePayload', () => {
|
|||||||
activePageId: 'home',
|
activePageId: 'home',
|
||||||
isEditingHeader: false,
|
isEditingHeader: false,
|
||||||
isEditingFooter: false,
|
isEditingFooter: false,
|
||||||
|
headCode: DEFAULT_SITE_DESIGN.headCode,
|
||||||
|
design: DEFAULT_SITE_DESIGN,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Live canvas goes to the active page and top-level slots.
|
// Live canvas goes to the active page and top-level slots.
|
||||||
@@ -105,6 +112,8 @@ describe('buildSavePayload', () => {
|
|||||||
activePageId: 'home',
|
activePageId: 'home',
|
||||||
isEditingHeader: false,
|
isEditingHeader: false,
|
||||||
isEditingFooter: false,
|
isEditingFooter: false,
|
||||||
|
headCode: DEFAULT_SITE_DESIGN.headCode,
|
||||||
|
design: DEFAULT_SITE_DESIGN,
|
||||||
});
|
});
|
||||||
|
|
||||||
// The live edit must land in pages_craft_state[0] (index.html slot),
|
// The live edit must land in pages_craft_state[0] (index.html slot),
|
||||||
@@ -116,4 +125,26 @@ describe('buildSavePayload', () => {
|
|||||||
// The other page is untouched.
|
// The other page is untouched.
|
||||||
expect(payload.pages_craft_state.find((p) => p.id === 'p2')?.craftState).toBe('STORED_ABOUT_2');
|
expect(payload.pages_craft_state.find((p) => p.id === 'p2')?.craftState).toBe('STORED_ABOUT_2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('head code + design tokens are included in the save payload', () => {
|
||||||
|
const design = { ...DEFAULT_SITE_DESIGN, headCode: '<meta name="x">' };
|
||||||
|
|
||||||
|
const payload = buildSavePayload({
|
||||||
|
siteId: 1,
|
||||||
|
siteName: 'Test Site',
|
||||||
|
liveCraftState: 'LIVE_PAGE_HOME',
|
||||||
|
pages: [pageA, pageB],
|
||||||
|
headerPage,
|
||||||
|
footerPage,
|
||||||
|
activePageId: 'home',
|
||||||
|
isEditingHeader: false,
|
||||||
|
isEditingFooter: false,
|
||||||
|
headCode: '<meta name="x">',
|
||||||
|
design,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload.head_code).toBe('<meta name="x">');
|
||||||
|
expect(payload.design).toEqual(design);
|
||||||
|
expect(payload.design.headCode).toBe('<meta name="x">');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { useEditor } from '@craftjs/core';
|
import { useEditor } from '@craftjs/core';
|
||||||
import { useEditorConfig } from '../state/EditorConfigContext';
|
import { useEditorConfig } from '../state/EditorConfigContext';
|
||||||
import { usePages } from '../state/PageContext';
|
import { usePages } from '../state/PageContext';
|
||||||
|
import { useSiteDesign, SiteDesign } from '../state/SiteDesignContext';
|
||||||
import { exportBodyHtml } from '../utils/html-export';
|
import { exportBodyHtml } from '../utils/html-export';
|
||||||
import { PageData } from '../types';
|
import { PageData } from '../types';
|
||||||
|
|
||||||
@@ -18,6 +19,10 @@ export interface BuildSavePayloadInput {
|
|||||||
isEditingHeader: boolean;
|
isEditingHeader: boolean;
|
||||||
/** True when the live canvas is showing the footer zone (activePageId === '__footer__'). */
|
/** True when the live canvas is showing the footer zone (activePageId === '__footer__'). */
|
||||||
isEditingFooter: boolean;
|
isEditingFooter: boolean;
|
||||||
|
/** Site-wide custom `<head>` code (also present on `design.headCode`). */
|
||||||
|
headCode: string;
|
||||||
|
/** Full site design tokens object -- lets load() restore colors/fonts/headCode. */
|
||||||
|
design: SiteDesign;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +50,8 @@ export function buildSavePayload(input: BuildSavePayloadInput) {
|
|||||||
activePageId,
|
activePageId,
|
||||||
isEditingHeader,
|
isEditingHeader,
|
||||||
isEditingFooter,
|
isEditingFooter,
|
||||||
|
headCode,
|
||||||
|
design,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
const isPageActive = !isEditingHeader && !isEditingFooter;
|
const isPageActive = !isEditingHeader && !isEditingFooter;
|
||||||
@@ -173,6 +180,8 @@ export function buildSavePayload(input: BuildSavePayloadInput) {
|
|||||||
header_craft_state: headerCraftState,
|
header_craft_state: headerCraftState,
|
||||||
footer_craft_state: footerCraftState,
|
footer_craft_state: footerCraftState,
|
||||||
pages_craft_state: pagesGrapesjs,
|
pages_craft_state: pagesGrapesjs,
|
||||||
|
head_code: headCode,
|
||||||
|
design,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +200,7 @@ export function useWhpApi() {
|
|||||||
setPagesCraftState,
|
setPagesCraftState,
|
||||||
setActivePageIdDirect,
|
setActivePageIdDirect,
|
||||||
} = usePages();
|
} = usePages();
|
||||||
|
const { design, updateDesign } = useSiteDesign();
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = useCallback(async () => {
|
||||||
if (!isWHP || !whpConfig) return null;
|
if (!isWHP || !whpConfig) return null;
|
||||||
@@ -210,6 +220,8 @@ export function useWhpApi() {
|
|||||||
activePageId,
|
activePageId,
|
||||||
isEditingHeader,
|
isEditingHeader,
|
||||||
isEditingFooter,
|
isEditingFooter,
|
||||||
|
headCode: design.headCode,
|
||||||
|
design,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resp = await fetch(`${whpConfig.apiUrl}?action=save`, {
|
const resp = await fetch(`${whpConfig.apiUrl}?action=save`, {
|
||||||
@@ -221,7 +233,7 @@ export function useWhpApi() {
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}, [isWHP, whpConfig, query, pages, activePageId, headerPage, footerPage, isEditingHeader, isEditingFooter]);
|
}, [isWHP, whpConfig, query, pages, activePageId, headerPage, footerPage, isEditingHeader, isEditingFooter, design]);
|
||||||
|
|
||||||
const publish = useCallback(async () => {
|
const publish = useCallback(async () => {
|
||||||
if (!isWHP || !whpConfig) return null;
|
if (!isWHP || !whpConfig) return null;
|
||||||
@@ -255,6 +267,17 @@ export function useWhpApi() {
|
|||||||
if (data.success && data.project) {
|
if (data.success && data.project) {
|
||||||
const proj = data.project;
|
const proj = data.project;
|
||||||
|
|
||||||
|
// Restore site design tokens (colors/fonts/headCode) so the editor
|
||||||
|
// reflects what was last saved. Prefer the full `design` object when
|
||||||
|
// present; fall back to just `head_code` for older project.json files
|
||||||
|
// saved before this field existed (backward-compatible: defaults for
|
||||||
|
// everything else).
|
||||||
|
if (proj.design && typeof proj.design === 'object') {
|
||||||
|
updateDesign(proj.design);
|
||||||
|
} else if (typeof proj.head_code === 'string') {
|
||||||
|
updateDesign({ headCode: proj.head_code });
|
||||||
|
}
|
||||||
|
|
||||||
// Restore header craft state
|
// Restore header craft state
|
||||||
if (proj.header_craft_state) {
|
if (proj.header_craft_state) {
|
||||||
setHeaderCraftState(typeof proj.header_craft_state === 'string'
|
setHeaderCraftState(typeof proj.header_craft_state === 'string'
|
||||||
@@ -300,7 +323,7 @@ export function useWhpApi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState, setActivePageIdDirect, isEditingHeader, isEditingFooter]);
|
}, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState, setActivePageIdDirect, isEditingHeader, isEditingFooter, updateDesign]);
|
||||||
|
|
||||||
const uploadAsset = useCallback(
|
const uploadAsset = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user