mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-10 02:33:04 +02:00
Persist collapae state
This commit is contained in:
@@ -35,6 +35,31 @@ type CollapseState = {
|
|||||||
repeaters: boolean;
|
repeaters: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SIDEBAR_COLLAPSE_STATE_KEY = 'remoteterm-sidebar-collapse-state';
|
||||||
|
|
||||||
|
const DEFAULT_COLLAPSE_STATE: CollapseState = {
|
||||||
|
favorites: false,
|
||||||
|
channels: false,
|
||||||
|
contacts: false,
|
||||||
|
repeaters: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadCollapsedState(): CollapseState {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(SIDEBAR_COLLAPSE_STATE_KEY);
|
||||||
|
if (!raw) return DEFAULT_COLLAPSE_STATE;
|
||||||
|
const parsed = JSON.parse(raw) as Partial<CollapseState>;
|
||||||
|
return {
|
||||||
|
favorites: parsed.favorites ?? DEFAULT_COLLAPSE_STATE.favorites,
|
||||||
|
channels: parsed.channels ?? DEFAULT_COLLAPSE_STATE.channels,
|
||||||
|
contacts: parsed.contacts ?? DEFAULT_COLLAPSE_STATE.contacts,
|
||||||
|
repeaters: parsed.repeaters ?? DEFAULT_COLLAPSE_STATE.repeaters,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_COLLAPSE_STATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
contacts: Contact[];
|
contacts: Contact[];
|
||||||
channels: Channel[];
|
channels: Channel[];
|
||||||
@@ -75,10 +100,11 @@ export function Sidebar({
|
|||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const sortOrder = sortOrderProp;
|
const sortOrder = sortOrderProp;
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [favoritesCollapsed, setFavoritesCollapsed] = useState(false);
|
const initialCollapsedState = useMemo(loadCollapsedState, []);
|
||||||
const [channelsCollapsed, setChannelsCollapsed] = useState(false);
|
const [favoritesCollapsed, setFavoritesCollapsed] = useState(initialCollapsedState.favorites);
|
||||||
const [contactsCollapsed, setContactsCollapsed] = useState(false);
|
const [channelsCollapsed, setChannelsCollapsed] = useState(initialCollapsedState.channels);
|
||||||
const [repeatersCollapsed, setRepeatersCollapsed] = useState(false);
|
const [contactsCollapsed, setContactsCollapsed] = useState(initialCollapsedState.contacts);
|
||||||
|
const [repeatersCollapsed, setRepeatersCollapsed] = useState(initialCollapsedState.repeaters);
|
||||||
const collapseSnapshotRef = useRef<CollapseState | null>(null);
|
const collapseSnapshotRef = useRef<CollapseState | null>(null);
|
||||||
|
|
||||||
const handleSortToggle = () => {
|
const handleSortToggle = () => {
|
||||||
@@ -260,6 +286,23 @@ export function Sidebar({
|
|||||||
}
|
}
|
||||||
}, [isSearching, favoritesCollapsed, channelsCollapsed, contactsCollapsed, repeatersCollapsed]);
|
}, [isSearching, favoritesCollapsed, channelsCollapsed, contactsCollapsed, repeatersCollapsed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSearching) return;
|
||||||
|
|
||||||
|
const state: CollapseState = {
|
||||||
|
favorites: favoritesCollapsed,
|
||||||
|
channels: channelsCollapsed,
|
||||||
|
contacts: contactsCollapsed,
|
||||||
|
repeaters: repeatersCollapsed,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SIDEBAR_COLLAPSE_STATE_KEY, JSON.stringify(state));
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage write failures (e.g., disabled storage)
|
||||||
|
}
|
||||||
|
}, [isSearching, favoritesCollapsed, channelsCollapsed, contactsCollapsed, repeatersCollapsed]);
|
||||||
|
|
||||||
// Separate favorites from regular items, and build combined favorites list
|
// Separate favorites from regular items, and build combined favorites list
|
||||||
const { favoriteItems, nonFavoriteChannels, nonFavoriteContacts, nonFavoriteRepeaters } =
|
const { favoriteItems, nonFavoriteChannels, nonFavoriteContacts, nonFavoriteRepeaters } =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { Sidebar } from '../components/Sidebar';
|
import { Sidebar } from '../components/Sidebar';
|
||||||
import { CONTACT_TYPE_REPEATER, type Channel, type Contact, type Favorite } from '../types';
|
import { CONTACT_TYPE_REPEATER, type Channel, type Contact, type Favorite } from '../types';
|
||||||
@@ -54,7 +54,7 @@ function renderSidebar(overrides?: {
|
|||||||
|
|
||||||
const favorites = overrides?.favorites ?? [{ type: 'channel', id: flightChannel.key }];
|
const favorites = overrides?.favorites ?? [{ type: 'channel', id: flightChannel.key }];
|
||||||
|
|
||||||
render(
|
const view = render(
|
||||||
<Sidebar
|
<Sidebar
|
||||||
contacts={[alice, relay]}
|
contacts={[alice, relay]}
|
||||||
channels={[publicChannel, flightChannel, opsChannel]}
|
channels={[publicChannel, flightChannel, opsChannel]}
|
||||||
@@ -74,7 +74,7 @@ function renderSidebar(overrides?: {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return { flightChannel, opsChannel, aliceName };
|
return { ...view, flightChannel, opsChannel, aliceName };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSectionHeaderContainer(title: string): HTMLElement {
|
function getSectionHeaderContainer(title: string): HTMLElement {
|
||||||
@@ -85,6 +85,10 @@ function getSectionHeaderContainer(title: string): HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Sidebar section summaries', () => {
|
describe('Sidebar section summaries', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
it('shows muted section unread totals in each visible section header', () => {
|
it('shows muted section unread totals in each visible section header', () => {
|
||||||
renderSidebar();
|
renderSidebar();
|
||||||
|
|
||||||
@@ -117,4 +121,20 @@ describe('Sidebar section summaries', () => {
|
|||||||
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('persists collapsed section state across unmount and remount', () => {
|
||||||
|
const { opsChannel, aliceName, unmount } = renderSidebar();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /Channels/i }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /Contacts/i }));
|
||||||
|
|
||||||
|
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
renderSidebar();
|
||||||
|
|
||||||
|
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user