Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/__tests__/editor-hooks/siteEditorDataDeepLink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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<void>((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({
Expand Down Expand Up @@ -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<typeof useEditorStore.setState>[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()
})
})
})
4 changes: 2 additions & 2 deletions src/admin/modals/Settings/useSiteSettingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -97,7 +97,7 @@ const useSettingsDraftStore = create<SettingsDraftState>((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)
Expand Down
14 changes: 11 additions & 3 deletions src/admin/pages/site/hooks/usePersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
CMS_SITE_RELOAD_EVENT,
EDITOR_SAVE_REQUEST_EVENT,
consumePendingCmsSiteReload,
hasPendingCmsSiteReload,
} from '@admin/state/adminEvents'

export interface PersistenceSaveStatus {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -226,6 +229,7 @@ export function usePersistence(
}

if (cancelled) return
if (pendingCmsSiteReload) consumePendingCmsSiteReload()

if (existingSite) {
loadedRef.current = true
Expand Down Expand Up @@ -272,25 +276,29 @@ 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)
applyDefaultBreakpointPreference(site.breakpoints)
// 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)
}
}

function handleReload() {
consumePendingCmsSiteReload()
void reload()
}

Expand Down
4 changes: 4 additions & 0 deletions src/admin/state/adminEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export function requestCmsSiteReload(): void {
}
}

export function hasPendingCmsSiteReload(): boolean {
return cmsSiteReloadPending
}

export function consumePendingCmsSiteReload(): boolean {
if (!cmsSiteReloadPending) return false
cmsSiteReloadPending = false
Expand Down