@lavalogic/scoria 0.38.13 → 0.38.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +3 -13
- package/dist/Components/Table/Misc/TableFooter.svelte +18 -4
- package/dist/Components/Table/Types/Columns/JSONTableLayout.d.ts +7 -0
- package/dist/Components/Table/Types/Columns/JSONTableLayout.js +6 -0
- package/dist/Components/Table/Types/Context/SelectionState.svelte.js +38 -29
- package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +5 -22
- package/dist/Components/Table/Types/Context/TableContext.svelte.js +52 -59
- package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.d.ts +7 -0
- package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.js +31 -13
- package/dist/Components/Table/Types/DataRepository/LocalTableDataRepository.svelte.d.ts +15 -3
- package/dist/Components/Table/Types/DataRepository/LocalTableDataRepository.svelte.js +33 -5
- package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.js +16 -0
- package/package.json +1 -1
|
@@ -229,12 +229,7 @@
|
|
|
229
229
|
let dragStartSlotIndex: number | null = $state(null);
|
|
230
230
|
let dragEndSlotIndex: number | null = $state(null);
|
|
231
231
|
|
|
232
|
-
function onSlotMouseDown(doorId: number, slotIndex: number
|
|
233
|
-
// Only start a drag-create on the primary (left) button. Otherwise a
|
|
234
|
-
// middle-click (autoscroll) or right-click would open the create modal.
|
|
235
|
-
if (e.button !== 0) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
232
|
+
function onSlotMouseDown(doorId: number, slotIndex: number): void {
|
|
238
233
|
if (!ondragcreate || appointmentDragStarted) {
|
|
239
234
|
return;
|
|
240
235
|
}
|
|
@@ -376,11 +371,6 @@
|
|
|
376
371
|
let moveTargetSlotIndex: number | null = $state(null);
|
|
377
372
|
|
|
378
373
|
function onAppointmentMouseDown(apt: CalendarAppointment, e: MouseEvent): void {
|
|
379
|
-
// Only react to the primary (left) button so middle/right clicks don't
|
|
380
|
-
// start a move or register as an appointment click.
|
|
381
|
-
if (e.button !== 0) {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
374
|
if (!onappointmentmove) {
|
|
385
375
|
return;
|
|
386
376
|
}
|
|
@@ -674,8 +664,8 @@
|
|
|
674
664
|
class:drag-selected={isSlotInDragRange(door.id, slotIdx)}
|
|
675
665
|
class:move-target={isSlotInMoveTarget(door.id, slotIdx)}
|
|
676
666
|
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
677
|
-
onmousedown={(
|
|
678
|
-
onSlotMouseDown(door.id, slotIdx
|
|
667
|
+
onmousedown={() => {
|
|
668
|
+
onSlotMouseDown(door.id, slotIdx);
|
|
679
669
|
}}
|
|
680
670
|
onmouseenter={() => {
|
|
681
671
|
hoveredSlotIndex = slotIdx;
|
|
@@ -115,20 +115,34 @@
|
|
|
115
115
|
}
|
|
116
116
|
const current = tableContext.paginationRepo.paginationState.pageIndex + 1;
|
|
117
117
|
const last = tableContext.lastPage;
|
|
118
|
+
// Empty / no-results case: render a single "1" button rather than
|
|
119
|
+
// a "0" button. `lastPage` is a 1-indexed count, never 0; a
|
|
120
|
+
// misbehaving data repo that returns 0 would otherwise leak
|
|
121
|
+
// through to a "Previous 0 1 Next" strip (see the page-0
|
|
122
|
+
// regression captured in image 2 of the bug report).
|
|
123
|
+
if (last < 1) {
|
|
124
|
+
pageButtons = [1];
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
118
127
|
// `pages` accumulates the dedup'd, sorted button set. The minimum
|
|
119
128
|
// the user always sees between the Previous and Next arrows is the
|
|
120
129
|
// current page, the next page (when one exists), and the last
|
|
121
130
|
// page. On top of that we still surface a small window around the
|
|
122
|
-
// current page for visual context.
|
|
131
|
+
// current page for visual context. Every value pushed is clamped
|
|
132
|
+
// to `[1, last]` defensively so an out-of-range current page (e.g.
|
|
133
|
+
// a stale `pageIndex` waiting for the next remote reload to
|
|
134
|
+
// settle) cannot resurface a page-0 button.
|
|
123
135
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
124
136
|
const pages = new Set<number>();
|
|
125
137
|
const windowStart = Math.max(1, Math.min(current - 2, last - 4));
|
|
126
138
|
for (let i = windowStart; i < windowStart + 5 && i <= last; i++) {
|
|
127
139
|
pages.add(i);
|
|
128
140
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
if (current >= 1 && current <= last) {
|
|
142
|
+
pages.add(current);
|
|
143
|
+
if (current + 1 <= last) {
|
|
144
|
+
pages.add(current + 1);
|
|
145
|
+
}
|
|
132
146
|
}
|
|
133
147
|
pages.add(last);
|
|
134
148
|
pageButtons = [...pages].sort((a, b) => a - b);
|
|
@@ -30,6 +30,13 @@ export interface JSONTableLayout {
|
|
|
30
30
|
* `userResized` so the grid renders the fixed track.
|
|
31
31
|
*/
|
|
32
32
|
sizing: ColumnSizingState;
|
|
33
|
+
/**
|
|
34
|
+
* Records-per-page selection that was active when the layout was
|
|
35
|
+
* captured. Optional for backwards compatibility - older snapshots
|
|
36
|
+
* predate this field and `_applyLayout` simply leaves the live
|
|
37
|
+
* pagination unchanged when it is absent.
|
|
38
|
+
*/
|
|
39
|
+
pageSize?: number;
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* Schema version for the persisted `JSONTableLayout` envelope. Bump when
|
|
@@ -30,6 +30,12 @@ export function isJSONTableLayout(raw) {
|
|
|
30
30
|
if (!isPrimitiveRecord(candidate.sizing, (v) => typeof v === 'number')) {
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
|
+
if (candidate.pageSize !== undefined &&
|
|
34
|
+
(typeof candidate.pageSize !== 'number' ||
|
|
35
|
+
!Number.isFinite(candidate.pageSize) ||
|
|
36
|
+
candidate.pageSize <= 0)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
33
39
|
return isJSONPinningState(candidate.pinning);
|
|
34
40
|
}
|
|
35
41
|
/** Validates a plain (non-array) object whose values all satisfy `valueGuard`. */
|
|
@@ -313,14 +313,12 @@ export class SelectionState {
|
|
|
313
313
|
: Math.min(pageEndIndexRaw, sortedDataLength);
|
|
314
314
|
return { pageStartIndex, pageEndIndexExclusive };
|
|
315
315
|
}
|
|
316
|
-
allSelectedBySelectableMethod(
|
|
317
|
-
const sortedData = this._deps.getSortedData();
|
|
316
|
+
allSelectedBySelectableMethod(pageItems, selectableMethod) {
|
|
318
317
|
const emptyPromise = new Promise(() => undefined);
|
|
319
318
|
if (selectableMethod) {
|
|
320
319
|
const pending = [];
|
|
321
|
-
for (let
|
|
322
|
-
const item =
|
|
323
|
-
const relIdx = i - pageStartIndex;
|
|
320
|
+
for (let relIdx = 0; relIdx < pageItems.length; relIdx++) {
|
|
321
|
+
const item = pageItems[relIdx];
|
|
324
322
|
const raw = this.cachedKey(item);
|
|
325
323
|
const checkUnselected = (key) => {
|
|
326
324
|
const selectable = selectableMethod(item, relIdx);
|
|
@@ -349,8 +347,8 @@ export class SelectionState {
|
|
|
349
347
|
}
|
|
350
348
|
else {
|
|
351
349
|
const pending = [];
|
|
352
|
-
for (let i =
|
|
353
|
-
const raw = this.cachedKey(
|
|
350
|
+
for (let i = 0; i < pageItems.length; i++) {
|
|
351
|
+
const raw = this.cachedKey(pageItems[i]);
|
|
354
352
|
if (raw instanceof Promise) {
|
|
355
353
|
pending.push(raw.then((key) => !this._selectedItems.has(key)));
|
|
356
354
|
}
|
|
@@ -378,17 +376,22 @@ export class SelectionState {
|
|
|
378
376
|
if (this._selectedItems.size === 0) {
|
|
379
377
|
return false;
|
|
380
378
|
}
|
|
381
|
-
// Subscribe to
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
379
|
+
// Subscribe to currentPageData so the derived recomputes when the
|
|
380
|
+
// page shifts. `currentPageData` already accounts for the active
|
|
381
|
+
// pagination mode: for Local pagination it's the sortedData slice
|
|
382
|
+
// for the current page; for Remote pagination it's the
|
|
383
|
+
// repository's data array (which IS only the current page).
|
|
384
|
+
// Iterating it directly avoids the page-2+ remote-pagination bug
|
|
385
|
+
// where absolute `pageStartIndex` indices read undefined entries
|
|
386
|
+
// from the page-sized `sortedData` array.
|
|
387
|
+
const pageItems = this._deps.getCurrentPageData();
|
|
388
|
+
if (pageItems.length === 0) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
388
391
|
const selectableMethod = this._deps
|
|
389
392
|
.getColumnDefs()
|
|
390
393
|
.find((it) => it instanceof RowSelectionDef)?.isSelectable;
|
|
391
|
-
const result = this.allSelectedBySelectableMethod(
|
|
394
|
+
const result = this.allSelectedBySelectableMethod(pageItems, selectableMethod);
|
|
392
395
|
if (result instanceof Promise) {
|
|
393
396
|
result
|
|
394
397
|
.then((flag) => {
|
|
@@ -406,9 +409,14 @@ export class SelectionState {
|
|
|
406
409
|
* `isSelectable` guard is added to the selection map.
|
|
407
410
|
*/
|
|
408
411
|
selectAllInCurrentPage = async () => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
+
// Use `currentPageData` rather than absolute indices into
|
|
413
|
+
// `sortedData`: for Remote pagination `sortedData` only contains
|
|
414
|
+
// the current page's rows but `pageStartIndex` is still the
|
|
415
|
+
// absolute index (e.g. 20 for page 2), so iterating from
|
|
416
|
+
// `pageStartIndex` would read undefined entries and silently no-op
|
|
417
|
+
// for page 2 onwards. `currentPageData` is always the visible
|
|
418
|
+
// page, regardless of pagination mode.
|
|
419
|
+
const pageItems = this._deps.getCurrentPageData();
|
|
412
420
|
// Mutate in place to preserve SvelteMap per-key reactivity AND
|
|
413
421
|
// to honour `setSelectionMap()` - if the consumer replaced the
|
|
414
422
|
// slot wholesale, we mutate THAT map rather than allocating a
|
|
@@ -416,8 +424,8 @@ export class SelectionState {
|
|
|
416
424
|
const sel = this._selectedItems;
|
|
417
425
|
const pending = [];
|
|
418
426
|
if (this.allSelectedInCurrentPage) {
|
|
419
|
-
for (let i =
|
|
420
|
-
const raw = this.cachedKey(
|
|
427
|
+
for (let i = 0; i < pageItems.length; i++) {
|
|
428
|
+
const raw = this.cachedKey(pageItems[i]);
|
|
421
429
|
if (raw instanceof Promise) {
|
|
422
430
|
pending.push(raw.then((key) => {
|
|
423
431
|
sel.delete(key);
|
|
@@ -433,9 +441,8 @@ export class SelectionState {
|
|
|
433
441
|
.getColumnDefs()
|
|
434
442
|
.find((it) => it instanceof RowSelectionDef)?.isSelectable;
|
|
435
443
|
if (selectableMethod) {
|
|
436
|
-
for (let
|
|
437
|
-
const item =
|
|
438
|
-
const relIdx = i - pageStartIndex;
|
|
444
|
+
for (let relIdx = 0; relIdx < pageItems.length; relIdx++) {
|
|
445
|
+
const item = pageItems[relIdx];
|
|
439
446
|
const s = selectableMethod(item, relIdx);
|
|
440
447
|
const doSet = (ok) => {
|
|
441
448
|
if (!ok) {
|
|
@@ -456,8 +463,8 @@ export class SelectionState {
|
|
|
456
463
|
}
|
|
457
464
|
}
|
|
458
465
|
else {
|
|
459
|
-
for (let i =
|
|
460
|
-
const item =
|
|
466
|
+
for (let i = 0; i < pageItems.length; i++) {
|
|
467
|
+
const item = pageItems[i];
|
|
461
468
|
const raw = this.cachedKey(item);
|
|
462
469
|
if (raw instanceof Promise) {
|
|
463
470
|
pending.push(raw.then((key) => {
|
|
@@ -484,14 +491,16 @@ export class SelectionState {
|
|
|
484
491
|
if (this._selectedItems.size === 0) {
|
|
485
492
|
return;
|
|
486
493
|
}
|
|
487
|
-
|
|
488
|
-
|
|
494
|
+
// Use `currentPageData` for the same reason as
|
|
495
|
+
// `selectAllInCurrentPage`: it is always the visible page
|
|
496
|
+
// regardless of pagination mode.
|
|
497
|
+
const pageItems = this._deps.getCurrentPageData();
|
|
489
498
|
// Build key -> item map for the current page (O(page_size) extractions)
|
|
490
499
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
491
500
|
const pageKeyMap = new Map();
|
|
492
501
|
const pending = [];
|
|
493
|
-
for (let i =
|
|
494
|
-
const item =
|
|
502
|
+
for (let i = 0; i < pageItems.length; i++) {
|
|
503
|
+
const item = pageItems[i];
|
|
495
504
|
const raw = this.cachedKey(item);
|
|
496
505
|
if (raw instanceof Promise) {
|
|
497
506
|
pending.push(raw.then((key) => {
|
|
@@ -116,10 +116,11 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
|
|
|
116
116
|
*/
|
|
117
117
|
private _layoutPersistTimer;
|
|
118
118
|
/**
|
|
119
|
-
* Pending debounce timer for the filter
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* write
|
|
119
|
+
* Pending debounce timer for the filter dirty-tracking pass. Filters
|
|
120
|
+
* do NOT persist across page refresh (see the boot chain) so the
|
|
121
|
+
* trailing callback only updates the in-memory `viewState` dirty
|
|
122
|
+
* flag; no localStorage write happens here. Name retained for parity
|
|
123
|
+
* with `_layoutPersistTimer`.
|
|
123
124
|
*/
|
|
124
125
|
private _filterPersistTimer;
|
|
125
126
|
private _paginationRepo;
|
|
@@ -812,24 +813,6 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
|
|
|
812
813
|
* corresponding step rather than crashing.
|
|
813
814
|
*/
|
|
814
815
|
private _applyFilter;
|
|
815
|
-
/**
|
|
816
|
-
* Boot-time hydration. Reads the auto-saved live-filter snapshot and
|
|
817
|
-
* applies it, restoring the user's last per-column filter values /
|
|
818
|
-
* modes. No-op when no snapshot is stored. The filter counterpart of
|
|
819
|
-
* `_hydrateLayout`.
|
|
820
|
-
*/
|
|
821
|
-
private _hydrateFilter;
|
|
822
|
-
/**
|
|
823
|
-
* Boot-time hydration for the FPM 403 filter view-state machine.
|
|
824
|
-
* Reads the persisted *filter* `JSONActiveView` envelope and restores
|
|
825
|
-
* the filter active-view selection on top of whatever `_hydrateFilter`
|
|
826
|
-
* produced. The filter counterpart of `_hydrateActiveView`; runs
|
|
827
|
-
* independently and never touches the layout slice.
|
|
828
|
-
*
|
|
829
|
-
* Runs before the `_layoutHydrated` gate opens so the filter
|
|
830
|
-
* auto-persist effect does not clobber the restored selection.
|
|
831
|
-
*/
|
|
832
|
-
private _hydrateActiveFilterView;
|
|
833
816
|
/**
|
|
834
817
|
* Re-point `_filterBaseline` at the active filter view's saved
|
|
835
818
|
* filter. The filter counterpart of `_reconcileBaselineForActiveView`:
|
|
@@ -449,20 +449,17 @@ export class TableContext {
|
|
|
449
449
|
this._persistActiveView();
|
|
450
450
|
}, LAYOUT_PERSIST_DEBOUNCE_MS);
|
|
451
451
|
});
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
// filter
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
// typing collapses to one write.
|
|
452
|
+
// Track FILTER dirtiness against the active-filter-view baseline
|
|
453
|
+
// without persisting anything. Filters are intentionally
|
|
454
|
+
// session-scoped: they reset on page navigation / refresh so the
|
|
455
|
+
// user always starts from the Default filter view and an empty
|
|
456
|
+
// quick-filter row, matching the user's mental model of "filters
|
|
457
|
+
// are an in-page action". Only the dirty-flag is updated so the
|
|
458
|
+
// view-picker chip still surfaces an unsaved active filter.
|
|
460
459
|
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
// envelope. Editing a layout never re-runs this effect and vice
|
|
465
|
-
// versa.
|
|
460
|
+
// (We deliberately do NOT call `persistFilter` /
|
|
461
|
+
// `persistActiveFilterView` here; layouts persist, filters do
|
|
462
|
+
// not.)
|
|
466
463
|
$effect(() => {
|
|
467
464
|
const snapshot = this._captureFilter();
|
|
468
465
|
if (!this._layoutHydrated) {
|
|
@@ -473,7 +470,6 @@ export class TableContext {
|
|
|
473
470
|
}
|
|
474
471
|
this._filterPersistTimer = setTimeout(() => {
|
|
475
472
|
this._filterPersistTimer = undefined;
|
|
476
|
-
this._preferences.persistFilter(snapshot);
|
|
477
473
|
// Dirty tracking: the filter is dirty when it diverges from
|
|
478
474
|
// the active filter view's baseline. The baseline can be a
|
|
479
475
|
// filter parsed from a backend JSON column (arbitrary key
|
|
@@ -483,7 +479,6 @@ export class TableContext {
|
|
|
483
479
|
const differs = this._filterBaseline !== undefined &&
|
|
484
480
|
stableStringify(snapshot) !== stableStringify(this._filterBaseline);
|
|
485
481
|
this._viewState.markDirty('filter', differs);
|
|
486
|
-
this._persistActiveFilterView();
|
|
487
482
|
}, FILTER_PERSIST_DEBOUNCE_MS);
|
|
488
483
|
});
|
|
489
484
|
if (browser) {
|
|
@@ -520,10 +515,15 @@ export class TableContext {
|
|
|
520
515
|
// Restore the auto-saved live layout on top of the
|
|
521
516
|
// factory Default.
|
|
522
517
|
this._hydrateLayout();
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
|
|
518
|
+
// Filters intentionally do NOT survive page refresh /
|
|
519
|
+
// navigation - the live working copy is reset to the
|
|
520
|
+
// Default filter view on every mount. Drop any
|
|
521
|
+
// stale persisted slot left by older builds so an
|
|
522
|
+
// older deploy never resurfaces filters the user
|
|
523
|
+
// thought were cleared. Layout persistence is
|
|
524
|
+
// untouched.
|
|
525
|
+
this._preferences.clearFilter();
|
|
526
|
+
this._preferences.clearActiveFilterView();
|
|
527
527
|
// Restore the persisted active-view selection on top of
|
|
528
528
|
// the working copy. The working copy that `_hydrateLayout`
|
|
529
529
|
// applied stays the live state; this only re-points
|
|
@@ -531,9 +531,6 @@ export class TableContext {
|
|
|
531
531
|
// flag. Done before the `_layoutHydrated` gate opens so
|
|
532
532
|
// the auto-persist effect does not clobber it.
|
|
533
533
|
this._hydrateActiveView();
|
|
534
|
-
// Restore the persisted filter active-view selection,
|
|
535
|
-
// independently of the layout one.
|
|
536
|
-
this._hydrateActiveFilterView();
|
|
537
534
|
this._layoutHydrated = true;
|
|
538
535
|
// Fire-and-forget: pull the saved views from the backend.
|
|
539
536
|
// Deliberately not awaited so a slow / failing network
|
|
@@ -566,10 +563,11 @@ export class TableContext {
|
|
|
566
563
|
*/
|
|
567
564
|
_layoutPersistTimer = undefined;
|
|
568
565
|
/**
|
|
569
|
-
* Pending debounce timer for the filter
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
* write
|
|
566
|
+
* Pending debounce timer for the filter dirty-tracking pass. Filters
|
|
567
|
+
* do NOT persist across page refresh (see the boot chain) so the
|
|
568
|
+
* trailing callback only updates the in-memory `viewState` dirty
|
|
569
|
+
* flag; no localStorage write happens here. Name retained for parity
|
|
570
|
+
* with `_layoutPersistTimer`.
|
|
573
571
|
*/
|
|
574
572
|
_filterPersistTimer = undefined;
|
|
575
573
|
_paginationRepo = $state();
|
|
@@ -1964,11 +1962,20 @@ export class TableContext {
|
|
|
1964
1962
|
sizing[def.id] = def.width;
|
|
1965
1963
|
}
|
|
1966
1964
|
}
|
|
1965
|
+
// Capture the current records-per-page selection so applying a
|
|
1966
|
+
// saved layout also restores its page size, and so a drag-resize
|
|
1967
|
+
// or visibility change auto-persists a layout that still reflects
|
|
1968
|
+
// the user's current page size. Reading `paginationState.pageSize`
|
|
1969
|
+
// also subscribes the auto-persist effect to page-size changes -
|
|
1970
|
+
// without it the new size would only land in localStorage on the
|
|
1971
|
+
// next layout mutation.
|
|
1972
|
+
const pageSize = this.paginationRepo?.paginationState.pageSize;
|
|
1967
1973
|
return {
|
|
1968
1974
|
columnOrder,
|
|
1969
1975
|
visibility: { ...this.columnVisibility },
|
|
1970
1976
|
pinning,
|
|
1971
1977
|
sizing,
|
|
1978
|
+
...(pageSize !== undefined ? { pageSize } : {}),
|
|
1972
1979
|
};
|
|
1973
1980
|
}
|
|
1974
1981
|
/**
|
|
@@ -2038,6 +2045,18 @@ export class TableContext {
|
|
|
2038
2045
|
}
|
|
2039
2046
|
}
|
|
2040
2047
|
this.columnPinning = adaptJSONPinningState(layout.pinning);
|
|
2048
|
+
// Restore the saved records-per-page selection. Optional in the
|
|
2049
|
+
// snapshot for backwards compatibility - older saves predate the
|
|
2050
|
+
// field; leave the live pagination untouched in that case.
|
|
2051
|
+
// Guarded against a no-op set (the setter is idempotent but
|
|
2052
|
+
// the localStorage write-through inside `paginationState =` would
|
|
2053
|
+
// still fire) so a hydration that matches the live value stays
|
|
2054
|
+
// silent.
|
|
2055
|
+
if (layout.pageSize !== undefined &&
|
|
2056
|
+
this.paginationRepo &&
|
|
2057
|
+
this.paginationRepo.paginationState.pageSize !== layout.pageSize) {
|
|
2058
|
+
this.paginationRepo.setPageSize(layout.pageSize);
|
|
2059
|
+
}
|
|
2041
2060
|
}
|
|
2042
2061
|
/**
|
|
2043
2062
|
* Boot-time hydration. Reads the auto-saved live-layout snapshot and
|
|
@@ -2235,39 +2254,13 @@ export class TableContext {
|
|
|
2235
2254
|
}
|
|
2236
2255
|
}
|
|
2237
2256
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
const filter = this._preferences.loadFilter();
|
|
2246
|
-
if (filter) {
|
|
2247
|
-
this._applyFilter(filter);
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
/**
|
|
2251
|
-
* Boot-time hydration for the FPM 403 filter view-state machine.
|
|
2252
|
-
* Reads the persisted *filter* `JSONActiveView` envelope and restores
|
|
2253
|
-
* the filter active-view selection on top of whatever `_hydrateFilter`
|
|
2254
|
-
* produced. The filter counterpart of `_hydrateActiveView`; runs
|
|
2255
|
-
* independently and never touches the layout slice.
|
|
2256
|
-
*
|
|
2257
|
-
* Runs before the `_layoutHydrated` gate opens so the filter
|
|
2258
|
-
* auto-persist effect does not clobber the restored selection.
|
|
2259
|
-
*/
|
|
2260
|
-
_hydrateActiveFilterView() {
|
|
2261
|
-
const persisted = this._preferences.loadActiveFilterView();
|
|
2262
|
-
if (persisted === null) {
|
|
2263
|
-
return;
|
|
2264
|
-
}
|
|
2265
|
-
if (persisted.active.kind === 'saved') {
|
|
2266
|
-
this._viewState.setActiveView('filter', persisted.active);
|
|
2267
|
-
this._reconcileBaselineForActiveFilterView();
|
|
2268
|
-
}
|
|
2269
|
-
this._viewState.markDirty('filter', persisted.dirty);
|
|
2270
|
-
}
|
|
2257
|
+
// Boot-time filter hydration helpers used to live here as
|
|
2258
|
+
// `_hydrateFilter` / `_hydrateActiveFilterView`. Filters now reset to
|
|
2259
|
+
// the Default view on every mount (see the boot chain in the
|
|
2260
|
+
// constructor, which calls `clearFilter` / `clearActiveFilterView`),
|
|
2261
|
+
// so neither helper is called anymore. The reconcile/apply machinery
|
|
2262
|
+
// below is still used by `applyDatatableView` and `resetToDefault`,
|
|
2263
|
+
// which run during a live session - they have not been removed.
|
|
2271
2264
|
/**
|
|
2272
2265
|
* Re-point `_filterBaseline` at the active filter view's saved
|
|
2273
2266
|
* filter. The filter counterpart of `_reconcileBaselineForActiveView`:
|
|
@@ -68,6 +68,13 @@ export declare class BuiltInRemoteRepository<TRow extends object> extends TableD
|
|
|
68
68
|
readonly setFilterMode: (id: string, mode: FilterMode | undefined) => void;
|
|
69
69
|
readonly filterBy: (id: string, value: unknown) => void;
|
|
70
70
|
readonly resetFilters: () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Reset the current page to page 1 without touching `pageSize`. Used
|
|
73
|
+
* by every filter-mutation entrypoint so a fresh filter never leaves
|
|
74
|
+
* the user stranded on a page index that no longer exists once the
|
|
75
|
+
* remote re-query returns a smaller result set.
|
|
76
|
+
*/
|
|
77
|
+
private resetToFirstPage;
|
|
71
78
|
/** Jumps to the given 1-based page number, clamped at >= 1. */
|
|
72
79
|
readonly setPage: (page: number) => void;
|
|
73
80
|
/** Advances to the next page. */
|
|
@@ -74,17 +74,14 @@ export class BuiltInRemoteRepository extends TableDataRepository {
|
|
|
74
74
|
constructor(_options) {
|
|
75
75
|
super(_options.tableName, _options.getUserId);
|
|
76
76
|
this._options = _options;
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else if (_options.pageSize) {
|
|
77
|
+
// The base-class constructor already restores the persisted
|
|
78
|
+
// records-per-page selection. Apply the caller-supplied default
|
|
79
|
+
// ONLY when nothing is persisted; otherwise an explicit
|
|
80
|
+
// `pageSize` option would silently overwrite the user's saved
|
|
81
|
+
// choice on every mount.
|
|
82
|
+
if (browser && _options.pageSize) {
|
|
83
|
+
const saved = localStorage.getItem(`scoria-table:${this.getUserId() ?? 'anon'}:${_options.tableName}:page-size`);
|
|
84
|
+
if (saved === null) {
|
|
88
85
|
this.paginationState = { pageIndex: 0, pageSize: _options.pageSize };
|
|
89
86
|
}
|
|
90
87
|
}
|
|
@@ -101,6 +98,7 @@ export class BuiltInRemoteRepository extends TableDataRepository {
|
|
|
101
98
|
else {
|
|
102
99
|
this._filterModes.set(id, mode);
|
|
103
100
|
}
|
|
101
|
+
this.resetToFirstPage();
|
|
104
102
|
this.scheduleReload();
|
|
105
103
|
};
|
|
106
104
|
filterBy = (id, value) => {
|
|
@@ -121,12 +119,32 @@ export class BuiltInRemoteRepository extends TableDataRepository {
|
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
this._filtering = next;
|
|
122
|
+
// Any filter change should drop the user back on page 1; otherwise
|
|
123
|
+
// when typing in a filter while on page 2+ the new (smaller) result
|
|
124
|
+
// set leaves the pagination on an out-of-range page index and the
|
|
125
|
+
// table renders empty until the user manually navigates back.
|
|
126
|
+
this.resetToFirstPage();
|
|
124
127
|
this.scheduleReload();
|
|
125
128
|
};
|
|
126
129
|
resetFilters = () => {
|
|
127
130
|
this._filtering = [];
|
|
131
|
+
this.resetToFirstPage();
|
|
128
132
|
this.scheduleReload();
|
|
129
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* Reset the current page to page 1 without touching `pageSize`. Used
|
|
136
|
+
* by every filter-mutation entrypoint so a fresh filter never leaves
|
|
137
|
+
* the user stranded on a page index that no longer exists once the
|
|
138
|
+
* remote re-query returns a smaller result set.
|
|
139
|
+
*/
|
|
140
|
+
resetToFirstPage() {
|
|
141
|
+
if (this.paginationState.pageIndex !== 0) {
|
|
142
|
+
this.paginationState = {
|
|
143
|
+
...this.paginationState,
|
|
144
|
+
pageIndex: 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
130
148
|
/** Jumps to the given 1-based page number, clamped at >= 1. */
|
|
131
149
|
setPage = (page) => {
|
|
132
150
|
this.paginationState = {
|
|
@@ -217,7 +235,7 @@ export class BuiltInRemoteRepository extends TableDataRepository {
|
|
|
217
235
|
const transformed = this._applyResponseTransform(raw);
|
|
218
236
|
this._data = [...transformed.rows];
|
|
219
237
|
this._totalRows = transformed.totalRows;
|
|
220
|
-
this._lastPage = Math.max(
|
|
238
|
+
this._lastPage = Math.max(1, Math.ceil(transformed.totalRows / this.paginationState.pageSize));
|
|
221
239
|
}
|
|
222
240
|
catch (e) {
|
|
223
241
|
if (e.name === 'AbortError') {
|
|
@@ -472,7 +490,7 @@ export class CustomRemoteRepository extends TableDataRepository {
|
|
|
472
490
|
}
|
|
473
491
|
this._data = [...response.rows];
|
|
474
492
|
this._totalRows = response.totalRows;
|
|
475
|
-
this._lastPage = Math.max(
|
|
493
|
+
this._lastPage = Math.max(1, Math.ceil(response.totalRows / this.paginationState.pageSize));
|
|
476
494
|
}
|
|
477
495
|
catch (e) {
|
|
478
496
|
if (e.name === 'AbortError') {
|
|
@@ -76,6 +76,12 @@ export declare class LocalTableDataRepository<T extends object> extends TableDat
|
|
|
76
76
|
* `filterByDebounced` helper here.
|
|
77
77
|
*/
|
|
78
78
|
readonly filterBy: (id: string, value: unknown) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Reset the current page to page 1 without touching `pageSize`. Used
|
|
81
|
+
* by every filter-mutation entrypoint so a fresh filter never leaves
|
|
82
|
+
* the user stranded on a page that no longer exists.
|
|
83
|
+
*/
|
|
84
|
+
private resetToFirstPage;
|
|
79
85
|
private _pageStartIndex;
|
|
80
86
|
/** Index of the first row on the current page. */
|
|
81
87
|
get pageStartIndex(): number;
|
|
@@ -85,9 +91,15 @@ export declare class LocalTableDataRepository<T extends object> extends TableDat
|
|
|
85
91
|
/** Clears every active filter value (keeps filter modes intact). */
|
|
86
92
|
readonly resetFilters: () => void;
|
|
87
93
|
/**
|
|
88
|
-
* Sets the page size and
|
|
89
|
-
* page
|
|
90
|
-
*
|
|
94
|
+
* Sets the page size and resets the page index to 0 (page 1). A
|
|
95
|
+
* page-size change is a "show me the start of the list at this
|
|
96
|
+
* density" action; clamping the previous page index forward instead
|
|
97
|
+
* (e.g. page 5 of size 20 -> page 5 of size 100) leaves the user on
|
|
98
|
+
* an arbitrary window of the new page-sized result, which does not
|
|
99
|
+
* match the user's mental model.
|
|
100
|
+
*
|
|
101
|
+
* Accepts a string so a raw `<select>` value can be passed through
|
|
102
|
+
* without parsing first.
|
|
91
103
|
*/
|
|
92
104
|
setPageSize: (size: number | string) => void;
|
|
93
105
|
/** Jumps to the given 1-based page number, clamped to the last page. */
|
|
@@ -133,6 +133,10 @@ export class LocalTableDataRepository extends TableDataRepository {
|
|
|
133
133
|
}
|
|
134
134
|
this._filterModes.delete(id);
|
|
135
135
|
}
|
|
136
|
+
// Changing the active filter mode can change which rows are
|
|
137
|
+
// included; reset to page 1 so the user is not stranded on a
|
|
138
|
+
// now-empty page.
|
|
139
|
+
this.resetToFirstPage();
|
|
136
140
|
};
|
|
137
141
|
/**
|
|
138
142
|
* Sets or updates the filter value for a column. Empty values are
|
|
@@ -180,7 +184,25 @@ export class LocalTableDataRepository extends TableDataRepository {
|
|
|
180
184
|
}
|
|
181
185
|
}
|
|
182
186
|
this.filtering = newFiltering;
|
|
187
|
+
// Any filter change should drop the user back on page 1; otherwise
|
|
188
|
+
// when typing in a filter while on page 2+ the new (smaller) result
|
|
189
|
+
// set leaves the pagination on an out-of-range page index and the
|
|
190
|
+
// table renders empty until the user manually navigates back.
|
|
191
|
+
this.resetToFirstPage();
|
|
183
192
|
};
|
|
193
|
+
/**
|
|
194
|
+
* Reset the current page to page 1 without touching `pageSize`. Used
|
|
195
|
+
* by every filter-mutation entrypoint so a fresh filter never leaves
|
|
196
|
+
* the user stranded on a page that no longer exists.
|
|
197
|
+
*/
|
|
198
|
+
resetToFirstPage() {
|
|
199
|
+
if (this.paginationState.pageIndex !== 0) {
|
|
200
|
+
this.paginationState = {
|
|
201
|
+
...this.paginationState,
|
|
202
|
+
pageIndex: 0,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
184
206
|
_pageStartIndex = $derived(this.paginationState.pageIndex * this.paginationState.pageSize);
|
|
185
207
|
/** Index of the first row on the current page. */
|
|
186
208
|
get pageStartIndex() {
|
|
@@ -194,11 +216,18 @@ export class LocalTableDataRepository extends TableDataRepository {
|
|
|
194
216
|
/** Clears every active filter value (keeps filter modes intact). */
|
|
195
217
|
resetFilters = () => {
|
|
196
218
|
this.filtering = [];
|
|
219
|
+
this.resetToFirstPage();
|
|
197
220
|
};
|
|
198
221
|
/**
|
|
199
|
-
* Sets the page size and
|
|
200
|
-
* page
|
|
201
|
-
*
|
|
222
|
+
* Sets the page size and resets the page index to 0 (page 1). A
|
|
223
|
+
* page-size change is a "show me the start of the list at this
|
|
224
|
+
* density" action; clamping the previous page index forward instead
|
|
225
|
+
* (e.g. page 5 of size 20 -> page 5 of size 100) leaves the user on
|
|
226
|
+
* an arbitrary window of the new page-sized result, which does not
|
|
227
|
+
* match the user's mental model.
|
|
228
|
+
*
|
|
229
|
+
* Accepts a string so a raw `<select>` value can be passed through
|
|
230
|
+
* without parsing first.
|
|
202
231
|
*/
|
|
203
232
|
setPageSize = (size) => {
|
|
204
233
|
if (typeof size === 'string') {
|
|
@@ -208,10 +237,9 @@ export class LocalTableDataRepository extends TableDataRepository {
|
|
|
208
237
|
console.warn('page size is nan');
|
|
209
238
|
return;
|
|
210
239
|
}
|
|
211
|
-
const lastIndex = Math.ceil(this._dataRef.value.length / size) - 1;
|
|
212
240
|
this.paginationState = {
|
|
213
241
|
pageSize: size,
|
|
214
|
-
pageIndex:
|
|
242
|
+
pageIndex: 0,
|
|
215
243
|
};
|
|
216
244
|
};
|
|
217
245
|
/** Jumps to the given 1-based page number, clamped to the last page. */
|
|
@@ -13,6 +13,22 @@ export class TableDataRepository {
|
|
|
13
13
|
constructor(tableName, getUserId) {
|
|
14
14
|
this.tableName = tableName;
|
|
15
15
|
this.getUserId = getUserId;
|
|
16
|
+
// Restore the persisted records-per-page selection. The setter
|
|
17
|
+
// writes through to localStorage on every change, but without this
|
|
18
|
+
// load the next mount would always snap back to the hardcoded 20
|
|
19
|
+
// default. Done in the base class so every concrete repository
|
|
20
|
+
// (local + every remote variant) gets the same behaviour for free.
|
|
21
|
+
// Browser-only: SSR has no localStorage and the setter's
|
|
22
|
+
// write-through is similarly guarded.
|
|
23
|
+
if (browser) {
|
|
24
|
+
const saved = localStorage.getItem(`scoria-table:${this.getUserId() ?? 'anon'}:${this.tableName}:page-size`);
|
|
25
|
+
if (saved !== null) {
|
|
26
|
+
const parsed = Number.parseInt(saved, 10);
|
|
27
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
28
|
+
this._paginationState = { pageIndex: 0, pageSize: parsed };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
16
32
|
}
|
|
17
33
|
_paginationState = $state({
|
|
18
34
|
pageIndex: 0,
|