Persist sidebar collapsed preference to localStorage
This commit is contained in:
@@ -415,7 +415,9 @@ const source = timeRange === "24h" ? "fail2ban" : "archive";
|
||||
|
||||
---
|
||||
|
||||
### TASK-021 — Persist sidebar collapse preference to `localStorage`
|
||||
### TASK-021 — Persist sidebar collapse preference to `localStorage` (done)
|
||||
|
||||
**Where fixed:** `frontend/src/layouts/MainLayout.tsx`
|
||||
|
||||
**Where found:** `frontend/src/layouts/MainLayout.tsx` — `useState(() => window.innerWidth < 640)` initialises collapsed state but never saves the user's toggle preference.
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import { useBlocklistStatus } from "../hooks/useBlocklist";
|
||||
|
||||
const SIDEBAR_FULL = "240px";
|
||||
const SIDEBAR_COLLAPSED = "48px";
|
||||
const SIDEBAR_COLLAPSED_STORAGE_KEY = "bangui_sidebar_collapsed";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
@@ -220,23 +221,52 @@ export function MainLayout(): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
const { logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
// Initialise collapsed based on screen width so narrow viewports start
|
||||
// with the icon-only sidebar rather than the full-width one.
|
||||
const [collapsed, setCollapsed] = useState(() => window.innerWidth < 640);
|
||||
|
||||
const readSavedCollapsed = (): boolean => {
|
||||
try {
|
||||
const savedValue = localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
|
||||
if (savedValue === "true") {
|
||||
return true;
|
||||
}
|
||||
if (savedValue === "false") {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors and fall back to viewport heuristics.
|
||||
}
|
||||
return window.innerWidth < 640;
|
||||
};
|
||||
|
||||
const [collapsed, setCollapsed] = useState<boolean>(readSavedCollapsed);
|
||||
const { status } = useServerStatus();
|
||||
const { hasErrors: blocklistHasErrors } = useBlocklistStatus();
|
||||
|
||||
/** True only after the first successful poll and fail2ban is unreachable. */
|
||||
const serverOffline = status !== null && !status.online;
|
||||
|
||||
// Auto-collapse / auto-expand when the viewport crosses the 640 px breakpoint.
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, String(collapsed));
|
||||
} catch {
|
||||
// Local storage may be unavailable in some environments.
|
||||
}
|
||||
}, [collapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedValue = localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
|
||||
if (savedValue !== null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mq = window.matchMedia("(max-width: 639px)");
|
||||
const handler = (e: MediaQueryListEvent): void => {
|
||||
setCollapsed(e.matches);
|
||||
};
|
||||
|
||||
mq.addEventListener("change", handler);
|
||||
return (): void => { mq.removeEventListener("change", handler); };
|
||||
return (): void => {
|
||||
mq.removeEventListener("change", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleCollapse = useCallback(() => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { MainLayout } from "../../layouts/MainLayout";
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock("../../providers/AuthProvider", () => ({
|
||||
vi.mock("../../hooks/useAuth", () => ({
|
||||
useAuth: () => ({ logout: vi.fn() }),
|
||||
}));
|
||||
|
||||
@@ -56,6 +56,7 @@ function renderLayout(): void {
|
||||
describe("MainLayout", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("renders the navigation sidebar", () => {
|
||||
@@ -75,4 +76,17 @@ describe("MainLayout", () => {
|
||||
await userEvent.click(toggleButton);
|
||||
expect(screen.queryByText("BanGUI")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("initialises sidebar collapsed state from localStorage", () => {
|
||||
localStorage.setItem("bangui_sidebar_collapsed", "true");
|
||||
renderLayout();
|
||||
expect(screen.queryByText("BanGUI")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("persists sidebar collapsed state to localStorage when toggled", async () => {
|
||||
renderLayout();
|
||||
const toggleButton = screen.getByRole("button", { name: /collapse sidebar/i });
|
||||
await userEvent.click(toggleButton);
|
||||
expect(localStorage.getItem("bangui_sidebar_collapsed")).toBe("true");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user