mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-19 00:29:46 -05:00
Merge pull request #1259 from Donkie/fix/silent-list-caps
Page through filament spools and dashboard groups
This commit is contained in:
4 files changed
+92
-25
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildCatalogQuery, buildGroupQuery, buildVendorTotalsQuery } from './query';
|
||||
import { allPages, buildCatalogQuery, buildGroupQuery, buildVendorTotalsQuery } from './query';
|
||||
import type { LibraryState } from '$lib/library/params';
|
||||
|
||||
// A filament with no spools is the one you need to re-order, and grouping by
|
||||
@@ -171,3 +171,35 @@ describe('buildCatalogQuery', () => {
|
||||
expect(q.filters).toEqual({ material: ['PLA'], vendorId: ['1', '4'] });
|
||||
});
|
||||
});
|
||||
|
||||
// The dashboard and the filament inspector used to stop at their first page
|
||||
// (1000 groups, 100 spools) and show the rest as if it didn't exist.
|
||||
describe('allPages', () => {
|
||||
const rows = Array.from({ length: 25 }, (_, i) => i);
|
||||
const pager = (size: number) => {
|
||||
const offsets: number[] = [];
|
||||
const fetchPage = async (offset: number) => {
|
||||
offsets.push(offset);
|
||||
return { items: rows.slice(offset, offset + size), total: rows.length };
|
||||
};
|
||||
return { offsets, fetchPage };
|
||||
};
|
||||
|
||||
it('keeps fetching until it has every row', async () => {
|
||||
const { offsets, fetchPage } = pager(10);
|
||||
expect(await allPages(10, fetchPage)).toEqual(rows);
|
||||
expect(offsets).toEqual([0, 10, 20]);
|
||||
});
|
||||
|
||||
it('stops after one request when the first page holds everything', async () => {
|
||||
const { offsets, fetchPage } = pager(100);
|
||||
expect(await allPages(100, fetchPage)).toEqual(rows);
|
||||
expect(offsets).toEqual([0]);
|
||||
});
|
||||
|
||||
it('stops on an exact multiple without asking for an empty page', async () => {
|
||||
const { offsets, fetchPage } = pager(5);
|
||||
expect(await allPages(5, fetchPage)).toHaveLength(25);
|
||||
expect(offsets).toEqual([0, 5, 10, 15, 20]);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GroupField, GroupQuery, GroupSummary, SortField, SpoolQuery } from './types';
|
||||
import type { GroupField, GroupQuery, GroupSummary, Page, SortField, SpoolQuery } from './types';
|
||||
import { isSpoolScopedFilter, type CatalogView, type LibraryState } from '$lib/library/params';
|
||||
import { resolveSortField, resolveGroupSortField, catalogSortField } from '$lib/utils/library';
|
||||
import { settings } from '$lib/stores/settings.svelte';
|
||||
@@ -170,3 +170,20 @@ export function buildFlatSpoolQuery(state: LibraryState, signal?: AbortSignal):
|
||||
signal
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Every row of a paged list, fetched `size` at a time: for views that need the
|
||||
* whole set (every dashboard card, every spool of one filament) without asking
|
||||
* the server for all of it in one response.
|
||||
*/
|
||||
export async function allPages<T>(
|
||||
size: number,
|
||||
fetchPage: (offset: number) => Promise<Page<T>>
|
||||
): Promise<T[]> {
|
||||
const all: T[] = [];
|
||||
for (;;) {
|
||||
const page = await fetchPage(all.length);
|
||||
all.push(...page.items);
|
||||
if (page.items.length < size || all.length >= page.total) return all;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
import { pct, weightAuto } from '$lib/utils/format';
|
||||
import { usageLabel } from '$lib/utils/library';
|
||||
import { spoolSource } from '$lib/api/spoolSource';
|
||||
import { allPages } from '$lib/api/query';
|
||||
import { isAbortError } from '$lib/api/http';
|
||||
import { classifyDeleteFailure, planFilamentDelete } from '$lib/library/deletion';
|
||||
import { toasts } from '$lib/stores/toasts.svelte';
|
||||
@@ -80,6 +81,7 @@
|
||||
// fetch's own cache upsert (which replaces the `filament` prop object) doesn't
|
||||
// re-trigger this effect in an infinite loop.
|
||||
let filamentId = $derived(filament.id);
|
||||
const SPOOL_PAGE = 100;
|
||||
let spools = $state<Spool[]>([]);
|
||||
// Bumped by live spool events so adding/removing/editing a spool refetches
|
||||
// this filament's list (the fetch below is server-ordered, not read from the
|
||||
@@ -96,24 +98,33 @@
|
||||
const id = filamentId;
|
||||
const archived = showArchived;
|
||||
void revision; // refetch on live spool events
|
||||
let cancelled = false;
|
||||
spoolSource
|
||||
.listSpools({
|
||||
const ctrl = new AbortController();
|
||||
// Every spool, not the first page: the count and total above are read off
|
||||
// this list. That renders all of them; a filament with thousands of spools
|
||||
// would want a "show more" here and the group endpoint's totals for the header.
|
||||
allPages(SPOOL_PAGE, (offset) =>
|
||||
spoolSource.listSpools({
|
||||
filters: {},
|
||||
sort: [{ field: 'last_used', dir: 'desc' }],
|
||||
sort: [
|
||||
{ field: 'last_used', dir: 'desc' },
|
||||
{ field: 'id', dir: 'asc' }
|
||||
],
|
||||
groupScope: { field: 'filament', key: id },
|
||||
allowArchived: archived,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
lowThreshold: settings.lowThreshold
|
||||
limit: SPOOL_PAGE,
|
||||
offset,
|
||||
lowThreshold: settings.lowThreshold,
|
||||
signal: ctrl.signal
|
||||
})
|
||||
.then((page) => {
|
||||
if (!cancelled) spools = page.items;
|
||||
)
|
||||
.then((items) => {
|
||||
// A spool used mid-load moves up the sort and can turn up on two pages.
|
||||
if (!ctrl.signal.aborted) spools = [...new Map(items.map((s) => [s.id, s])).values()];
|
||||
})
|
||||
.catch((e) => console.error('Failed to load filament spools', e));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
.catch((e) => {
|
||||
if (!isAbortError(e, ctrl.signal)) console.error('Failed to load filament spools', e);
|
||||
});
|
||||
return () => ctrl.abort();
|
||||
});
|
||||
|
||||
const saver = makeSaver<string, Partial<Filament>>((id, patch) =>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { settings } from '$lib/stores/settings.svelte';
|
||||
import { fields } from '$lib/stores/fields.svelte';
|
||||
import { spoolSource } from '$lib/api/spoolSource';
|
||||
import { allPages } from '$lib/api/query';
|
||||
import { live, type LiveEvent } from '$lib/api/live';
|
||||
import { isAbortError } from '$lib/api/http';
|
||||
import { mapSpool } from '$lib/api/map';
|
||||
@@ -36,6 +37,8 @@
|
||||
const FLIP = 160;
|
||||
/** Spools fetched per request when filling (or extending) a card. */
|
||||
const PAGE = 30;
|
||||
/** Groups fetched per request when listing the cards. */
|
||||
const GROUP_PAGE = 1000;
|
||||
/** Distance from the bottom of a card's list at which the next page is fetched. */
|
||||
const SCROLL_MARGIN = 120;
|
||||
/** The unassigned card's key: no value for the grouped-by field. */
|
||||
@@ -170,20 +173,24 @@
|
||||
// controller for a later view.
|
||||
const signal = pageAbort.signal;
|
||||
try {
|
||||
const page = await spoolSource.listGroups({
|
||||
field: fieldKey,
|
||||
filters: {},
|
||||
sort: [{ field: 'group.title', dir: 'asc' }],
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
lowThreshold: settings.lowThreshold,
|
||||
signal
|
||||
});
|
||||
// Every group gets a card, so page through all of them rather than stopping at
|
||||
// the first page; a group past it would never show up at all.
|
||||
const groups = await allPages(GROUP_PAGE, (offset) =>
|
||||
spoolSource.listGroups({
|
||||
field: fieldKey,
|
||||
filters: {},
|
||||
sort: [{ field: 'group.title', dir: 'asc' }],
|
||||
limit: GROUP_PAGE,
|
||||
offset,
|
||||
lowThreshold: settings.lowThreshold,
|
||||
signal
|
||||
})
|
||||
);
|
||||
// A NULL value and an empty-string one are distinct rows to the database but the
|
||||
// same "unassigned" card here, so counts are summed by key.
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local, not reactive state
|
||||
const totals = new Map<string, number>();
|
||||
for (const g of page.items) totals.set(g.key, (totals.get(g.key) ?? 0) + g.spoolCount);
|
||||
for (const g of groups) totals.set(g.key, (totals.get(g.key) ?? 0) + g.spoolCount);
|
||||
|
||||
for (const [key, total] of totals) {
|
||||
const b = bucket(key);
|
||||
|
||||
Reference in new issue
Block a user