diff --git a/src/features/settings/__tests__/settingsWindowOpen.test.ts b/src/features/settings/__tests__/settingsWindowOpen.test.ts index f1ed12a2e..64b5ced29 100644 --- a/src/features/settings/__tests__/settingsWindowOpen.test.ts +++ b/src/features/settings/__tests__/settingsWindowOpen.test.ts @@ -38,7 +38,10 @@ describe("the native Settings window", () => { it("opens even when a page fails to register, and logs why", async () => { vi.spyOn(console, "error").mockImplementation(() => {}); const dispose = activateSettingsWindow("main"); + expect(useSettingsDialog.getState().open).toBe(true); + expect(useSettingsDialog.getState().pagesLoading).toBe(true); await vi.waitFor(() => expect(useSettingsDialog.getState().open).toBe(true)); + await vi.waitFor(() => expect(useSettingsDialog.getState().pagesLoading).toBe(false)); expect(logFrontend).toHaveBeenCalledWith( "error", expect.stringContaining("cannot register settings pages: Error: page failed"), diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index d7bad0428..f457b4a8d 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -63,6 +63,15 @@ flex-direction: column; min-height: 0; } +.loading { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + min-height: 0; + color: var(--fg-subtle); + font-size: var(--fs-small); +} .pageHeader { flex: none; display: flex; diff --git a/src/features/settings/dialog/SettingsDialog.tsx b/src/features/settings/dialog/SettingsDialog.tsx index 9d6bbf74d..0ae1ab284 100644 --- a/src/features/settings/dialog/SettingsDialog.tsx +++ b/src/features/settings/dialog/SettingsDialog.tsx @@ -17,6 +17,7 @@ import { Icon, IconButton, SearchField, + Spinner, Tree, type DialogButton, type TreeHandle, @@ -220,7 +221,7 @@ function SettingsDialogInner() { /** Page tree + current page; reads the shared settings staging store. */ export function SettingsLayout() { const pages = useSettingsPages((s) => s.pages); - const { pageId, query, staged, stagedSecrets, stagedExternal, saving } = useSettingsDialog(); + const { pageId, query, staged, stagedSecrets, stagedExternal, saving, pagesLoading } = useSettingsDialog(); const treeRef = useRef(null); const tree = useMemo(() => buildPageTree(pages.values()), [pages]); @@ -265,6 +266,16 @@ export function SettingsLayout() { } }; + if (pagesLoading) { + return ( +
+
+ Loading… +
+
+ ); + } + return (
diff --git a/src/features/settings/index.ts b/src/features/settings/index.ts index b78ba3780..a42918f70 100644 --- a/src/features/settings/index.ts +++ b/src/features/settings/index.ts @@ -24,6 +24,7 @@ import { LazySettingsDialog, loadBuiltinPages, unloadBuiltinPages } from "./dial import { closeSettings, openSettings, + rebaseSettings, selectPage, setNewProjects, setQuery, @@ -208,20 +209,24 @@ export function activateSettingsWindow(parent: string): () => void { }), ]; void (async () => { - let launch: SettingsLaunch | null = null; + useSettingsDialog.setState({ pagesLoading: true }); + openSettings(); try { - // A page that fails to register is reported; the window still opens with the others. - await loadBuiltinPages().catch((e) => logError("cannot register settings pages", e)); - await restoreRemote(); - const project = await commands.projectCurrent().catch(() => null); - if (project) useProject.setState({ project, settings: project.settings }); - else await loadSettings().catch((e) => console.warn("cannot load settings", e)); - // `listenLaunch` may have taken the launch first and applied it already. - launch = await takeLaunch(); + const launch = await takeLaunch(); + relaunch(launch); + await Promise.all([ + loadBuiltinPages().catch((e) => logError("cannot register settings pages", e)), + (async () => { + await restoreRemote(); + const project = await commands.projectCurrent().catch(() => null); + if (project) useProject.setState({ project, settings: project.settings }); + else await loadSettings().catch((e) => console.warn("cannot load settings", e)); + rebaseSettings(); + })(), + ]); } finally { - // Without this the window would stay an empty title bar. - openSettings(launch?.page, { newProjects: launch?.newProjects ?? useSettingsDialog.getState().newProjects }); + useSettingsDialog.setState({ pagesLoading: false }); } - })(); + })().catch((e) => logError("settings window startup failed", e)); return () => disposers.reverse().forEach((d) => d()); } diff --git a/src/features/settings/model/staging.ts b/src/features/settings/model/staging.ts index 6bc437ad3..b78d67bcb 100644 --- a/src/features/settings/model/staging.ts +++ b/src/features/settings/model/staging.ts @@ -46,6 +46,7 @@ export interface SettingsDialogState { scopes: Record; saving: boolean; error: string | null; + pagesLoading: boolean; } export const DEFAULT_PAGE = "appearance"; @@ -66,6 +67,7 @@ export const useSettingsDialog = create(() => ({ scopes: {}, saving: false, error: null, + pagesLoading: false, })); const stageKey = (scope: Scope, key: string) => `${scope}|${key}`; @@ -109,6 +111,12 @@ async function loadGlobalBase() { } } +export function rebaseSettings() { + const base = validateSettings(useProject.getState().settings); + useSettingsDialog.setState((s) => (s.open ? { base, globalBase: base } : {})); + void loadGlobalBase(); +} + /** IDEA's File › New Projects Setup › Settings for New Projects. */ export const settingsTitle = (newProjects: boolean) => (newProjects ? "Settings for New Projects" : "Settings"); @@ -129,6 +137,7 @@ export function closeSettings() { externalApplied: {}, error: null, saving: false, + pagesLoading: false, }); } diff --git a/src/features/settings/window/SettingsWindow.tsx b/src/features/settings/window/SettingsWindow.tsx index 231a60640..445dc59b4 100644 --- a/src/features/settings/window/SettingsWindow.tsx +++ b/src/features/settings/window/SettingsWindow.tsx @@ -14,12 +14,15 @@ export function SettingsWindow() { const os = usePlatform((s) => s.os); const ownTitleBar = usePlatform((s) => s.overlayTitleBar || s.framelessTitleBar); const open = useSettingsDialog((s) => s.open); + const pagesLoading = useSettingsDialog((s) => s.pagesLoading); const title = useSettingsTitle(); const ref = useRef(null); useEffect(() => { - if (open) ref.current?.querySelector("[data-autofocus]")?.focus({ preventScroll: true }); - }, [open]); + if (open && !pagesLoading) { + ref.current?.querySelector("[data-autofocus]")?.focus({ preventScroll: true }); + } + }, [open, pagesLoading]); return (