Remember the Library page size

This commit is contained in:
Donkie committed 2026-10-18 09:05:23 +02:00
1 parent b7a282d2eb
commit 5fc8e6cf42
4 files changed
+70 -16

No files matched your search

+18
View File
@@ -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();
});
});
+8 -5
View File
@@ -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
});
}
+23 -4
View File
@@ -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);
});
+21 -7
View File
@@ -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<string, unknown>;
const { group, sort, asc, empty, lots, size } = parsed as Record<string, unknown>;
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 {