From 5fc8e6cf42644aacd6c341331f40d62598fd1a1b Mon Sep 17 00:00:00 2001 From: Donkie Date: Sun, 18 Oct 2026 09:05:23 +0200 Subject: [PATCH] Remember the Library page size --- client_v2/src/lib/library/params.test.ts | 18 +++++++++++++ client_v2/src/lib/library/params.ts | 13 ++++++---- client_v2/src/lib/library/viewPrefs.test.ts | 27 +++++++++++++++++--- client_v2/src/lib/library/viewPrefs.ts | 28 +++++++++++++++------ 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/client_v2/src/lib/library/params.test.ts b/client_v2/src/lib/library/params.test.ts index fbfa498a..8e337d78 100644 --- a/client_v2/src/lib/library/params.test.ts +++ b/client_v2/src/lib/library/params.test.ts @@ -236,3 +236,21 @@ describe('lot number visibility', () => { expect(await hrefFor(stored, '?lots=0')).toBeNull(); }); }); + +describe('page size', () => { + const DEFAULT_VIEW = '{"group":"filament","sort":"last_used","asc":false'; + + it('restores a stored page size on a bare URL', async () => { + expect(await hrefFor(`${DEFAULT_VIEW},"size":100}`, '')).toBe('/?size=100'); + }); + + it('lets an explicit URL page size win', async () => { + expect(await hrefFor(`${DEFAULT_VIEW},"size":100}`, '?size=50')).toBeNull(); + }); + + it('falls back to the default for an old stored view or an invalid size', async () => { + expect(await hrefFor(`${DEFAULT_VIEW}}`, '')).toBeNull(); + expect(await hrefFor(`${DEFAULT_VIEW},"size":"100"}`, '')).toBeNull(); + expect(await hrefFor(`${DEFAULT_VIEW},"size":-5}`, '')).toBeNull(); + }); +}); diff --git a/client_v2/src/lib/library/params.ts b/client_v2/src/lib/library/params.ts index 2544c673..0252f3b9 100644 --- a/client_v2/src/lib/library/params.ts +++ b/client_v2/src/lib/library/params.ts @@ -64,7 +64,7 @@ const VIEWS: LibraryView[] = ['spools', 'filaments', 'manufacturers']; * holds. A URL naming none of them is the one that defers to the remembered * view; naming any of them describes a view of its own, which is taken whole so * a stored grouping never gets spliced onto a link's sort. */ -const VIEW_PARAMS = ['view', 'group', 'sort', 'dir', 'empty', 'lots']; +const VIEW_PARAMS = ['view', 'group', 'sort', 'dir', 'empty', 'lots', 'size']; const DEFAULTS = { group: 'filament' as GroupMode, @@ -229,7 +229,7 @@ function currentState(): LibraryState { /** * Where entering the Library on `url` should actually land: the same view with - * the remembered grouping and sort spelled out in the query string, or null when + * the remembered grouping, sort and page size spelled out in the query string, or null when * the URL already describes a view (or there's nothing worth restoring). The * Library page navigates there on entry, replacing the history entry — see * routes/+page.svelte. @@ -258,7 +258,8 @@ export function rememberedViewHref(url: URL): string | null { stored.sortKey === DEFAULTS.sortKey && stored.sortAsc === DEFAULTS.sortAsc && stored.showEmpty === DEFAULTS.showEmpty && - stored.showLotNumbers === DEFAULTS.showLotNumbers + stored.showLotNumbers === DEFAULTS.showLotNumbers && + stored.pageSize === DEFAULTS.pageSize ) { return null; } @@ -272,7 +273,8 @@ export function rememberedViewHref(url: URL): string | null { sortKey: stored.sortKey, sortAsc: stored.sortAsc, showEmpty: stored.showEmpty, - showLotNumbers: stored.showLotNumbers + showLotNumbers: stored.showLotNumbers, + pageSize: stored.pageSize }); return `${url.pathname}?${qs}`; } @@ -297,7 +299,8 @@ function navigate(next: LibraryState, replace = false): void { sortKey: next.sortKey, sortAsc: next.sortAsc, showEmpty: next.showEmpty, - showLotNumbers: next.showLotNumbers + showLotNumbers: next.showLotNumbers, + pageSize: next.pageSize }); } diff --git a/client_v2/src/lib/library/viewPrefs.test.ts b/client_v2/src/lib/library/viewPrefs.test.ts index 846c7f37..ea0d8a1a 100644 --- a/client_v2/src/lib/library/viewPrefs.test.ts +++ b/client_v2/src/lib/library/viewPrefs.test.ts @@ -14,7 +14,8 @@ describe('parseStoredView', () => { sortKey: 'name', sortAsc: true, showEmpty: false, - showLotNumbers: false + showLotNumbers: false, + pageSize: 20 }); }); @@ -24,7 +25,8 @@ describe('parseStoredView', () => { sortKey: 'name', sortAsc: true, showEmpty: true, - showLotNumbers: false + showLotNumbers: false, + pageSize: 20 }); }); @@ -57,7 +59,8 @@ describe('parseStoredView', () => { sortKey: 'name', sortAsc: false, showEmpty: false, - showLotNumbers: false + showLotNumbers: false, + pageSize: 20 }); }); }); @@ -72,7 +75,14 @@ it('round-trips the lot number preference through storage', () => { stored = value; } }); - const view = { group: 'filament', sortKey: 'name', sortAsc: true, showEmpty: false, showLotNumbers: true }; + const view = { + group: 'filament', + sortKey: 'name', + sortAsc: true, + showEmpty: false, + showLotNumbers: true, + pageSize: 20 + }; rememberView(view); expect(parseStoredView(stored)).toEqual(view); rememberView({ ...view, showLotNumbers: false }); @@ -84,3 +94,12 @@ it('defaults lot numbers off for an old stored view with empty enabled', () => { false ); }); + +it('round-trips the page size and defaults it for old or invalid entries', () => { + const old = '{"group":"filament","sort":"name","asc":true'; + expect(parseStoredView(`${old},"size":100}`)?.pageSize).toBe(100); + expect(parseStoredView(`${old}}`)?.pageSize).toBe(20); + expect(parseStoredView(`${old},"size":"100"}`)?.pageSize).toBe(20); + expect(parseStoredView(`${old},"size":0}`)?.pageSize).toBe(20); + expect(parseStoredView(`${old},"size":2.5}`)?.pageSize).toBe(20); +}); diff --git a/client_v2/src/lib/library/viewPrefs.ts b/client_v2/src/lib/library/viewPrefs.ts index c885aa3b..a07a3a57 100644 --- a/client_v2/src/lib/library/viewPrefs.ts +++ b/client_v2/src/lib/library/viewPrefs.ts @@ -16,13 +16,16 @@ // cache, one URL per distinct view — depends on the address bar being the whole // truth about what's on screen. // -// Only grouping and sort are remembered, plus toggles that ADD information -// (showEmpty adds rows, showLotNumbers adds text). Filters, search and archived- +// Only grouping and sort are remembered, the page size (a layout choice that +// hides nothing, it just sets how many rows load at once), plus toggles that ADD +// information (showEmpty adds rows, showLotNumbers adds text). Filters, search and archived- // visibility hide spools, and a hidden filter silently restored days later reads // as missing data rather than as a preference. Showing empty filaments or lot // numbers cannot produce that confusion -- restoring them can only put more on // screen, never less -- so they are remembered while the hiding toggles are not. +const DEFAULT_PAGE_SIZE = 20; + const KEY = 'spoolman-v2-library-view'; /** @@ -36,6 +39,7 @@ export interface StoredView { sortAsc: boolean; showEmpty: boolean; showLotNumbers: boolean; + pageSize: number; } /** Rebuild a stored view from its JSON, or null if there isn't a usable one. */ @@ -44,15 +48,23 @@ export function parseStoredView(raw: string | null): StoredView | null { try { const parsed: unknown = JSON.parse(raw); if (typeof parsed !== 'object' || parsed === null) return null; - const { group, sort, asc, empty, lots } = parsed as Record; + const { group, sort, asc, empty, lots, size } = parsed as Record; if (typeof group !== 'string' || typeof sort !== 'string' || typeof asc !== 'boolean') { return null; } - // `empty` and `lots` arrived after this key shipped, so an entry written + // `empty`, `lots` and `size` arrived after this key shipped, so an entry written // by an older build simply has no such fields. Treating that as "off" keeps those // entries valid; rejecting them would throw away a grouping the user // picked long ago the first time they loaded a new version. - return { group, sortKey: sort, sortAsc: asc, showEmpty: empty === true, showLotNumbers: lots === true }; + return { + group, + sortKey: sort, + sortAsc: asc, + showEmpty: empty === true, + showLotNumbers: lots === true, + // An absent or unusable size is the default, not a reason to drop the entry. + pageSize: typeof size === 'number' && Number.isInteger(size) && size > 0 ? size : DEFAULT_PAGE_SIZE + }; } catch { // Corrupt entry: the shipped view is no worse than what a first-time // visitor gets. @@ -82,7 +94,8 @@ export function rememberView(view: StoredView): void { prev.sortKey === view.sortKey && prev.sortAsc === view.sortAsc && prev.showEmpty === view.showEmpty && - prev.showLotNumbers === view.showLotNumbers + prev.showLotNumbers === view.showLotNumbers && + prev.pageSize === view.pageSize ) { // Selecting a spool or turning a page navigates too; only the changes that // actually move the view are worth a synchronous localStorage write. @@ -112,7 +125,8 @@ function write(view: StoredView): void { sort: view.sortKey, asc: view.sortAsc, empty: view.showEmpty, - lots: view.showLotNumbers + lots: view.showLotNumbers, + size: view.pageSize }) ); } catch {