Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 23 additions & 3 deletions apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,25 @@ export function SettingsDialog({
const showSettingsItem = (id: string) => isMenuItemVisible(desktopSettings.uiProfile, id);
const [open, setOpen] = useState(false);
const [section, setSection] = useState<SettingsSection>("map");
// The Browser is a dockable right panel (open/close via the registry), not a
// persisted layout preference, so its Layout toggle acts on the live registry
// state directly rather than through the draft settings.
// Browser and Comments are dockable right panels, so their checkboxes read the
// live registry state (the user can also close them from their own header)
// while the toggle writes the matching persisted layout setting. The setting
// is what their registration hooks seed from on the next launch, so the
// toggle survives a restart (#1935).
const rightPanelState = useRightPanelState();
const browserPanelOpen = rightPanelState.visibleIds.includes(BROWSER_PANEL_ID);
const commentsPanelOpen = rightPanelState.visibleIds.includes(COMMENTS_PANEL_ID);
// These apply live rather than on Save, so the draft is patched alongside the
// saved settings: the draft was snapshotted when the dialog opened, and Save
// writes it wholesale, which would otherwise revert the toggle the user just
// made in this same dialog.
const applyPanelVisibility = (
key: "browserPanelVisible" | "commentsPanelVisible",
visible: boolean,
) => {
updateSavedLayoutSettings({ [key]: visible });
updateDraftLayoutSettings({ [key]: visible });
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Browser/Comments checkboxes are bound to browserPanelOpen/commentsPanelOpen, which read the live right-panel registry (rightPanelState.visibleIds), not the draft settings. That registry can also change from outside this dialog — e.g. the user closes the panel from its own header, which is explicitly designed to be session-only and not written back to layout.browserPanelVisible/commentsPanelVisible (per the PR description).

Net effect: if a user closes a panel from its header, then opens Settings → Layout, the checkbox shows unchecked (matching the live state) even though the persisted setting is still true. If they then click Save Settings without touching that checkbox, the save writes the untouched draft value (true), so the panel reopens on the next launch — silently contradicting what the checkbox displayed in the dialog they just saved from.

This looks intentional per the PR's stated design, but it's a real display/persistence divergence worth confirming is the desired UX. Confidence: low-medium.

// Show it collapsed on the shared Layers rail, matching its default state, so
// re-enabling from Settings doesn't jump to an expanded panel that buries the
// Layers panel.
Expand All @@ -433,6 +446,7 @@ export function SettingsDialog({
} else {
closeRightPanel(BROWSER_PANEL_ID);
}
applyPanelVisibility("browserPanelVisible", show);
};
// Collapsed for the same reason as Browser above, and to match the state
// Comments registers itself in on mount.
Expand All @@ -443,6 +457,7 @@ export function SettingsDialog({
} else {
closeRightPanel(COMMENTS_PANEL_ID);
}
applyPanelVisibility("commentsPanelVisible", show);
};
// A field a deep-link asked us to focus once its section renders; cleared
// after the focus lands so a later open without a focus request stays put.
Expand Down Expand Up @@ -932,6 +947,11 @@ export function SettingsDialog({

const resetLayoutSettings = () => {
updateDraftLayoutSettings(DEFAULT_DESKTOP_LAYOUT_SETTINGS);
// The Browser/Comments checkboxes render the live registry state, not the
// draft, so reset has to move the panels themselves or those two rows would
// ignore the button.
toggleBrowserPanel(DEFAULT_DESKTOP_LAYOUT_SETTINGS.browserPanelVisible);
toggleCommentsPanel(DEFAULT_DESKTOP_LAYOUT_SETTINGS.commentsPanelVisible);
};
Comment on lines 917 to 919

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetLayoutSettings writes browserPanelVisible/commentsPanelVisible straight to the persisted store via toggleBrowserPanel/toggleCommentsPanel (through applyPanelVisibilityupdateSavedLayoutSettings), while the other four layout fields only land in draftDesktopSettings and are not persisted until saveSettings runs.

That means clicking Reset and then Cancel produces a partial reset: the two panel-visibility settings are permanently changed (survive the dialog close), but layerPanelVisible/stylePanelVisible/toolbarLabels/showProjectInfo silently revert to whatever was last saved, since the draft is discarded and re-seeded from the store the next time the dialog opens (line ~620). A user who resets and then backs out would end up with a half-applied reset with no obvious indication of that split.

Confidence: medium — this follows directly from the code, but it may be an accepted trade-off of the "panels apply live" design described in the PR.


// The accent scheme applies live (instant preview) rather than waiting for
Expand Down
21 changes: 21 additions & 0 deletions apps/geolibre-desktop/src/hooks/useDesktopSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ export interface UpdateSettings {
}

export interface DesktopLayoutSettings {
/**
* Whether the Browser (Data Source Manager) right panel is registered as
* visible. Unlike {@link layerPanelVisible} this does not describe a fixed
* dock slot: the Browser is a dockable right panel, so the flag is the
* persisted seed its registration hook applies on mount (open + collapsed onto
* its rail, or closed). Without it the panel reopened on every launch no
* matter what the Settings toggle said (#1935).
*/
browserPanelVisible: boolean;
/** Same as {@link browserPanelVisible}, for the Comments right panel. */
commentsPanelVisible: boolean;
layerPanelVisible: boolean;
showProjectInfo: boolean;
stylePanelVisible: boolean;
Expand Down Expand Up @@ -157,6 +168,8 @@ interface DesktopSettingsState {
}

export const DEFAULT_DESKTOP_LAYOUT_SETTINGS: DesktopLayoutSettings = {
browserPanelVisible: true,
commentsPanelVisible: true,
layerPanelVisible: true,
showProjectInfo: true,
stylePanelVisible: true,
Expand Down Expand Up @@ -410,6 +423,14 @@ function normalizeDesktopLayoutSettings(layout: unknown): DesktopLayoutSettings
// cannot smuggle non-boolean values into the layout settings.
const candidate = layout as Partial<DesktopLayoutSettings>;
return {
browserPanelVisible:
typeof candidate.browserPanelVisible === "boolean"
? candidate.browserPanelVisible
: DEFAULT_DESKTOP_LAYOUT_SETTINGS.browserPanelVisible,
commentsPanelVisible:
typeof candidate.commentsPanelVisible === "boolean"
? candidate.commentsPanelVisible
: DEFAULT_DESKTOP_LAYOUT_SETTINGS.commentsPanelVisible,
layerPanelVisible:
typeof candidate.layerPanelVisible === "boolean"
? candidate.layerPanelVisible
Expand Down
19 changes: 14 additions & 5 deletions apps/geolibre-desktop/src/hooks/useRegisterBrowserPanel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { collapseRightPanel, openRightPanel, registerRightPanel } from "@geolibre/plugins";
import { useEffect } from "react";
import i18n from "../i18n";
import { useDesktopSettingsStore } from "./useDesktopSettings";

/** Stable id of the Browser (Data Source Manager) right panel. */
export const BROWSER_PANEL_ID = "browser";
Expand All @@ -24,8 +25,10 @@ export const BROWSER_PANEL_ID = "browser";
* The panel is **on by default but collapsed** onto the shared Layers rail: on
* mount it is opened and immediately collapsed, so it shows as a rail entry
* beside Layers rather than covering the map. The user expands it from that
* rail (or toggles it off in Settings → Layout). It reopens collapsed on the
* next load, matching the "on by default" behavior of the Layout toggle.
* rail (or toggles it off in Settings → Layout). "By default" means the default
* of the persisted `layout.browserPanelVisible` setting: turning the panel off
* in Settings → Layout keeps it off across restarts rather than having the
* toggle silently reset (#1935).
*/
export function useRegisterBrowserPanel(): void {
useEffect(() => {
Expand All @@ -38,9 +41,15 @@ export function useRegisterBrowserPanel(): void {
render: () => {},
});
// Default on, but docked collapsed to the Layers rail (open then collapse),
// so it is present without burying the map on first load.
openRightPanel(BROWSER_PANEL_ID);
collapseRightPanel(BROWSER_PANEL_ID);
// so it is present without burying the map on first load. Read the setting
// here rather than subscribing: this seeds the panel's startup state only.
// Once mounted the Settings toggle drives the registry directly, so the
// user can also close the panel from its own header without that being
// written back as a preference.
if (useDesktopSettingsStore.getState().desktopSettings.layout.browserPanelVisible) {
openRightPanel(BROWSER_PANEL_ID);
collapseRightPanel(BROWSER_PANEL_ID);
}
return dispose;
}, []);
}
16 changes: 13 additions & 3 deletions apps/geolibre-desktop/src/hooks/useRegisterCommentsPanel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { collapseRightPanel, openRightPanel, registerRightPanel } from "@geolibre/plugins";
import { useEffect } from "react";
import i18n from "../i18n";
import { useDesktopSettingsStore } from "./useDesktopSettings";

/** Stable id of the Comments right panel. */
export const COMMENTS_PANEL_ID = "comments";
Expand All @@ -10,7 +11,10 @@ export const COMMENTS_PANEL_ID = "comments";
* sidebar's rail (`replace-style`).
*
* Comments is enabled by default but collapsed onto the Style rail, so it is
* discoverable without taking map space.
* discoverable without taking map space. "By default" means the default of the
* persisted `layout.commentsPanelVisible` setting: a user who turned the panel
* off in Settings → Layout gets it back off on the next launch instead of having
* the toggle silently reset (#1935).
*/
export function useRegisterCommentsPanel(): void {
useEffect(() => {
Expand All @@ -22,8 +26,14 @@ export function useRegisterCommentsPanel(): void {
dock: "replace-style",
render: () => {},
});
openRightPanel(COMMENTS_PANEL_ID);
collapseRightPanel(COMMENTS_PANEL_ID);
// Read the setting here rather than subscribing: this seeds the panel's
// startup state only. Once mounted the Settings toggle drives the registry
// directly, so the user can also close the panel from its own header
// without that being written back as a preference.
if (useDesktopSettingsStore.getState().desktopSettings.layout.commentsPanelVisible) {
openRightPanel(COMMENTS_PANEL_ID);
collapseRightPanel(COMMENTS_PANEL_ID);
}
return dispose;
}, []);
}
44 changes: 44 additions & 0 deletions tests/layout-panel-settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
DEFAULT_DESKTOP_LAYOUT_SETTINGS,
normalizeDesktopSettings,
} from "../apps/geolibre-desktop/src/hooks/useDesktopSettings";

// The Browser and Comments right panels used to be session-only: their Settings
// → Layout toggles moved the panel registry but nothing was persisted, so every
// launch reopened them (GeoLibre#1935). They are now layout settings like the
// Layers/Style panels, which means they have to round-trip through
// normalizeDesktopSettings and keep defaulting to on for existing users whose
// stored settings predate the keys.
describe("dockable panel layout settings", () => {
it("defaults both dockable panels to visible", () => {
assert.equal(DEFAULT_DESKTOP_LAYOUT_SETTINGS.browserPanelVisible, true);
assert.equal(DEFAULT_DESKTOP_LAYOUT_SETTINGS.commentsPanelVisible, true);
});

it("keeps a disabled panel disabled across a load", () => {
const layout = normalizeDesktopSettings({
layout: { browserPanelVisible: false, commentsPanelVisible: false },
}).layout;
assert.equal(layout.browserPanelVisible, false);
assert.equal(layout.commentsPanelVisible, false);
});

it("falls back to the defaults for settings saved before the keys existed", () => {
const layout = normalizeDesktopSettings({
layout: { layerPanelVisible: false, stylePanelVisible: true, toolbarLabels: true },
}).layout;
assert.equal(layout.layerPanelVisible, false);
assert.equal(layout.browserPanelVisible, true);
assert.equal(layout.commentsPanelVisible, true);
});

it("rejects non-boolean values from tampered storage", () => {
const layout = normalizeDesktopSettings({
layout: { browserPanelVisible: "no", commentsPanelVisible: 0 },
}).layout;
assert.equal(layout.browserPanelVisible, true);
assert.equal(layout.commentsPanelVisible, true);
});
});
Loading