From b35570c91e024bd23f2c77fdeff21d66dc9ad3bd Mon Sep 17 00:00:00 2001 From: DavidBabinec Date: Wed, 1 Jul 2026 01:35:58 +0200 Subject: [PATCH] fix(editor): retain pending site reloads through remount --- .../siteEditorDataDeepLink.test.tsx | 57 +++++++++++++++++++ .../Settings/useSiteSettingsController.ts | 4 +- src/admin/pages/site/hooks/usePersistence.ts | 14 ++++- src/admin/state/adminEvents.ts | 4 ++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/__tests__/editor-hooks/siteEditorDataDeepLink.test.tsx b/src/__tests__/editor-hooks/siteEditorDataDeepLink.test.tsx index 1ea8c75c6..5a56ca7b2 100644 --- a/src/__tests__/editor-hooks/siteEditorDataDeepLink.test.tsx +++ b/src/__tests__/editor-hooks/siteEditorDataDeepLink.test.tsx @@ -8,6 +8,7 @@ import { makeNode, makePage, makeSite } from '../fixtures' import type { SiteDocument } from '@core/page-tree' import type { VisualComponent } from '@core/visualComponents' import type { IPersistenceAdapter } from '@core/persistence/types' +import { buildCoreFrameworkSettings } from '@core/framework' afterEach(cleanup) @@ -69,6 +70,25 @@ function makeAdapter(site: SiteDocument): IPersistenceAdapter & { loadCount: () } } +function makeControlledAdapter( + site: SiteDocument, +): IPersistenceAdapter & { loadCount: () => number; resolveNextLoad: () => void } { + let loads = 0 + const resolvers: Array<() => void> = [] + return { + async loadSite() { + loads += 1 + await new Promise((resolve) => resolvers.push(resolve)) + return site + }, + async saveSite() {}, + loadCount: () => loads, + resolveNextLoad: () => { + resolvers.shift()?.() + }, + } +} + function useDeepLinkedSiteEditor(adapter: IPersistenceAdapter) { const persistence = usePersistence('default', adapter, { enabled: true }) useSiteEditorUrlSync({ @@ -136,4 +156,41 @@ describe('Site editor Data workspace deep links', () => { ).toBe(true) }) }) + + it('retains a pending CMS site reload when a mount is cancelled before the fresh site hydrates', async () => { + const staleSite = makeEditorSite([]) + const freshSite = makeEditorSite([]) + freshSite.settings.framework = buildCoreFrameworkSettings({ includeUtilities: true }) + const adapter = makeControlledAdapter(freshSite) + + useEditorStore.setState({ + site: staleSite, + activePageId: 'page-home', + hasUnsavedChanges: false, + } as Parameters[0]) + requestCmsSiteReload() + + const firstMount = renderHook(() => usePersistence('default', adapter, { enabled: true })) + await waitFor(() => { + expect(adapter.loadCount()).toBe(1) + }) + firstMount.unmount() + adapter.resolveNextLoad() + await new Promise((resolve) => setTimeout(resolve, 0)) + + await waitFor(() => { + expect(useEditorStore.getState().site?.settings.framework).toBeUndefined() + }) + + renderHook(() => usePersistence('default', adapter, { enabled: true })) + + await waitFor(() => { + expect(adapter.loadCount()).toBe(2) + }) + adapter.resolveNextLoad() + + await waitFor(() => { + expect(useEditorStore.getState().site?.settings.framework).toBeDefined() + }) + }) }) diff --git a/src/admin/modals/Settings/useSiteSettingsController.ts b/src/admin/modals/Settings/useSiteSettingsController.ts index 355942d60..9f25adc7c 100644 --- a/src/admin/modals/Settings/useSiteSettingsController.ts +++ b/src/admin/modals/Settings/useSiteSettingsController.ts @@ -44,7 +44,7 @@ import type { SiteDocument, SiteSettings } from '@core/page-tree' import type { FrameworkPreferencesSettings } from '@core/framework-schema' import { useEditorStore } from '@site/store/store' import { useAdminUi } from '@admin/state/adminUi' -import { CMS_SITE_RELOAD_EVENT } from '@admin/state/adminEvents' +import { requestCmsSiteReload } from '@admin/state/adminEvents' import { getErrorMessage } from '@core/utils/errorMessage' const SITE_ID = 'default' @@ -97,7 +97,7 @@ const useSettingsDraftStore = create((set, get) => { name: next.name, faviconUrl: next.settings.faviconUrl ?? null, }) - window.dispatchEvent(new Event(CMS_SITE_RELOAD_EVENT)) + requestCmsSiteReload() }) .catch((err: unknown) => { console.error('[useSiteSettingsController] failed to save site settings:', err) diff --git a/src/admin/pages/site/hooks/usePersistence.ts b/src/admin/pages/site/hooks/usePersistence.ts index ffc1d671b..90c452c2d 100644 --- a/src/admin/pages/site/hooks/usePersistence.ts +++ b/src/admin/pages/site/hooks/usePersistence.ts @@ -57,6 +57,7 @@ import { CMS_SITE_RELOAD_EVENT, EDITOR_SAVE_REQUEST_EVENT, consumePendingCmsSiteReload, + hasPendingCmsSiteReload, } from '@admin/state/adminEvents' export interface PersistenceSaveStatus { @@ -183,8 +184,9 @@ export function usePersistence( setHasUnsavedChanges, } = useEditorStore.getState() + const pendingCmsSiteReload = hasPendingCmsSiteReload() const shouldReloadExistingSite = existingSite - ? consumePendingCmsSiteReload() || siteMissesEditorDataDeepLink(existingSite) + ? pendingCmsSiteReload || siteMissesEditorDataDeepLink(existingSite) : false if (existingSite && !shouldReloadExistingSite) { @@ -205,6 +207,7 @@ export function usePersistence( // Constraint #230 is satisfied at the adapter boundary. const site = await adapterRef.current.loadSite(idToTry) if (site && !cancelled) { + if (pendingCmsSiteReload) consumePendingCmsSiteReload() syncedPageIdsRef.current = site.pages.map((p) => p.id) loadSite(site) applyDefaultBreakpointPreference(site.breakpoints) @@ -226,6 +229,7 @@ export function usePersistence( } if (cancelled) return + if (pendingCmsSiteReload) consumePendingCmsSiteReload() if (existingSite) { loadedRef.current = true @@ -272,10 +276,14 @@ export function usePersistence( async function reload() { const idToTry = requestedSiteId || 'default' + const pendingCmsSiteReload = hasPendingCmsSiteReload() try { // Adapter validates internally (Constraint #230). const site = await adapterRef.current.loadSite(idToTry) - if (!site) return + if (!site) { + if (pendingCmsSiteReload) consumePendingCmsSiteReload() + return + } const { loadSite, setHasUnsavedChanges } = useEditorStore.getState() syncedPageIdsRef.current = site.pages.map((p) => p.id) loadSite(site) @@ -283,6 +291,7 @@ export function usePersistence( // The site doc on disk is now authoritative; clear the unsaved flag so // the auto-save loop doesn't immediately overwrite it back. setHasUnsavedChanges(false) + if (pendingCmsSiteReload) consumePendingCmsSiteReload() setSaveStatus({ state: 'saved', lastSavedAt: Date.now() }) } catch (err) { console.error('[persistence] Reload after pack install failed:', err) @@ -290,7 +299,6 @@ export function usePersistence( } function handleReload() { - consumePendingCmsSiteReload() void reload() } diff --git a/src/admin/state/adminEvents.ts b/src/admin/state/adminEvents.ts index e16a28e14..73d7efaa0 100644 --- a/src/admin/state/adminEvents.ts +++ b/src/admin/state/adminEvents.ts @@ -35,6 +35,10 @@ export function requestCmsSiteReload(): void { } } +export function hasPendingCmsSiteReload(): boolean { + return cmsSiteReloadPending +} + export function consumePendingCmsSiteReload(): boolean { if (!cmsSiteReloadPending) return false cmsSiteReloadPending = false