@lavalogic/scoria 0.38.5 → 0.38.7
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/Table/Misc/TableFooter.svelte +25 -10
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +12 -22
- package/dist/Components/Table/Types/Context/TableContext.svelte.js +38 -12
- package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.d.ts +6 -0
- package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.js +34 -6
- package/package.json +1 -1
|
@@ -111,17 +111,27 @@
|
|
|
111
111
|
$effect(() => {
|
|
112
112
|
if (tableContext.paginationRepo?.paginationState.pageIndex == null) {
|
|
113
113
|
pageButtons = [1];
|
|
114
|
-
|
|
115
|
-
const startingPage = Math.min(
|
|
116
|
-
tableContext.lastPage - 2,
|
|
117
|
-
tableContext.paginationRepo.paginationState.pageIndex - 1
|
|
118
|
-
);
|
|
119
|
-
const pageToStartWith = Math.max(1, Math.min(startingPage, tableContext.lastPage - 4));
|
|
120
|
-
|
|
121
|
-
pageButtons = new Array<number>(Math.min(tableContext.lastPage, 5))
|
|
122
|
-
.fill(pageToStartWith)
|
|
123
|
-
.map((item, index) => item + index);
|
|
114
|
+
return;
|
|
124
115
|
}
|
|
116
|
+
const current = tableContext.paginationRepo.paginationState.pageIndex + 1;
|
|
117
|
+
const last = tableContext.lastPage;
|
|
118
|
+
// `pages` accumulates the dedup'd, sorted button set. The minimum
|
|
119
|
+
// the user always sees between the Previous and Next arrows is the
|
|
120
|
+
// current page, the next page (when one exists), and the last
|
|
121
|
+
// page. On top of that we still surface a small window around the
|
|
122
|
+
// current page for visual context.
|
|
123
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
124
|
+
const pages = new Set<number>();
|
|
125
|
+
const windowStart = Math.max(1, Math.min(current - 2, last - 4));
|
|
126
|
+
for (let i = windowStart; i < windowStart + 5 && i <= last; i++) {
|
|
127
|
+
pages.add(i);
|
|
128
|
+
}
|
|
129
|
+
pages.add(current);
|
|
130
|
+
if (current + 1 <= last) {
|
|
131
|
+
pages.add(current + 1);
|
|
132
|
+
}
|
|
133
|
+
pages.add(last);
|
|
134
|
+
pageButtons = [...pages].sort((a, b) => a - b);
|
|
125
135
|
});
|
|
126
136
|
|
|
127
137
|
let hasNoSelectedItems = $derived(tableContext.selectedItems.size == 0);
|
|
@@ -250,6 +260,11 @@
|
|
|
250
260
|
id="{tableContext.tableName}-jump-to-page"
|
|
251
261
|
value={pageValue}
|
|
252
262
|
oninput={onNumberInput}
|
|
263
|
+
onfocus={(e) => {
|
|
264
|
+
// Select the existing value so typing replaces it, instead of
|
|
265
|
+
// appending to it (typing "5" on a "4" otherwise yields "45").
|
|
266
|
+
(e.target as HTMLInputElement).select();
|
|
267
|
+
}}
|
|
253
268
|
disabled={tableContext.showSelectedOnly}
|
|
254
269
|
/>
|
|
255
270
|
</div>
|
|
@@ -32,18 +32,9 @@
|
|
|
32
32
|
// strings) and from `UNSAVED_OPTION`.
|
|
33
33
|
const DEFAULT_OPTION = ' default';
|
|
34
34
|
|
|
35
|
-
// Sentinel value for the *transient* "…(unsaved)" option shown while
|
|
36
|
-
// the table state is dirty. It is deliberately distinct from
|
|
37
|
-
// `DEFAULT_OPTION` and from every real saved-view id so the transient
|
|
38
|
-
// option never coincides with a selectable option: selecting any real
|
|
39
|
-
// option (Default included) from a dirty state is then always seen as
|
|
40
|
-
// a genuine change by the underlying select, so the handler fires.
|
|
41
|
-
const UNSAVED_OPTION = ' unsaved';
|
|
42
|
-
|
|
43
35
|
// The view-state slice for `kind`, read live from the context.
|
|
44
36
|
const activeView = $derived(tableContext._viewState.activeView(kind));
|
|
45
37
|
const savedViews = $derived(tableContext._viewState.savedViews(kind));
|
|
46
|
-
const isDirty = $derived(tableContext._viewState.isDirty(kind));
|
|
47
38
|
|
|
48
39
|
// Dropdown label: 'Default' / 'Custom (unsaved)' / '<name>' / '<name>
|
|
49
40
|
// (unsaved)'. Used as the displayed value of the select so it reflects
|
|
@@ -68,19 +59,18 @@
|
|
|
68
59
|
...savedViews.map((view) => ({ label: view.name, value: view.id })),
|
|
69
60
|
]);
|
|
70
61
|
|
|
71
|
-
// The value handed to `SingleSelect`. `
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// `
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
const selectedOption: SelectOption<string> = $derived(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
62
|
+
// The value handed to `SingleSelect`. The `value` is always the active
|
|
63
|
+
// option's real value (the Default sentinel or a saved-view id), so it
|
|
64
|
+
// matches an entry in `options` and `SingleSelect` keeps the selection
|
|
65
|
+
// rendered. The `label` is `dropdownLabel`, which already encodes the
|
|
66
|
+
// dirty state ("…(unsaved)"); `SingleSelect` shows it verbatim via its
|
|
67
|
+
// `selection` snippet. An earlier approach fed a transient value that
|
|
68
|
+
// was absent from `options`, which made svelecte drop the selection
|
|
69
|
+
// and fall back to its placeholder.
|
|
70
|
+
const selectedOption: SelectOption<string> = $derived({
|
|
71
|
+
label: dropdownLabel,
|
|
72
|
+
value: activeValue,
|
|
73
|
+
});
|
|
84
74
|
|
|
85
75
|
/** Apply the option the user picked from the dropdown. */
|
|
86
76
|
function selectOption(option: SelectOption<string> | null): void {
|
|
@@ -37,6 +37,31 @@ const LAYOUT_PERSIST_DEBOUNCE_MS = 250;
|
|
|
37
37
|
* auto-persist effects behave consistently.
|
|
38
38
|
*/
|
|
39
39
|
const FILTER_PERSIST_DEBOUNCE_MS = 250;
|
|
40
|
+
/**
|
|
41
|
+
* `JSON.stringify` with object keys sorted recursively, so two
|
|
42
|
+
* structurally-equal values produce an identical string regardless of
|
|
43
|
+
* key insertion order. Array element order is preserved (it is
|
|
44
|
+
* significant for `columnOrder` and pinning entries).
|
|
45
|
+
*
|
|
46
|
+
* Used for the layout / filter dirty comparison: a baseline parsed from a
|
|
47
|
+
* backend JSON column has arbitrary key order, while `_captureLayout` /
|
|
48
|
+
* `_captureFilter` build their objects in a fixed order - a plain
|
|
49
|
+
* `JSON.stringify` compare would then report a false "dirty" the moment a
|
|
50
|
+
* saved view is applied.
|
|
51
|
+
*/
|
|
52
|
+
function stableStringify(value) {
|
|
53
|
+
if (value === null || typeof value !== 'object') {
|
|
54
|
+
return JSON.stringify(value) ?? 'null';
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
58
|
+
}
|
|
59
|
+
const record = value;
|
|
60
|
+
return `{${Object.keys(record)
|
|
61
|
+
.sort()
|
|
62
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
63
|
+
.join(',')}}`;
|
|
64
|
+
}
|
|
40
65
|
/**
|
|
41
66
|
* The complete set of filter-mode operator strings scoria recognises -
|
|
42
67
|
* the union of every `StringFilterMode` and `NumberDateFilterMode`
|
|
@@ -412,13 +437,14 @@ export class TableContext {
|
|
|
412
437
|
this._layoutPersistTimer = undefined;
|
|
413
438
|
this._preferences.persistLayout(snapshot);
|
|
414
439
|
// Dirty tracking: the layout is dirty when it diverges
|
|
415
|
-
// from the active view's baseline.
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
440
|
+
// from the active view's baseline. The baseline can be a
|
|
441
|
+
// layout parsed from a backend JSON column (arbitrary key
|
|
442
|
+
// order), so the comparison must be key-order-insensitive
|
|
443
|
+
// - a plain `JSON.stringify` compare flagged every applied
|
|
444
|
+
// saved view as dirty. When no baseline has been captured
|
|
445
|
+
// yet (should not happen past boot) treat it as clean.
|
|
420
446
|
const differs = this._viewBaseline !== undefined &&
|
|
421
|
-
|
|
447
|
+
stableStringify(snapshot) !== stableStringify(this._viewBaseline);
|
|
422
448
|
this._viewState.markDirty('layout', differs);
|
|
423
449
|
this._persistActiveView();
|
|
424
450
|
}, LAYOUT_PERSIST_DEBOUNCE_MS);
|
|
@@ -449,13 +475,13 @@ export class TableContext {
|
|
|
449
475
|
this._filterPersistTimer = undefined;
|
|
450
476
|
this._preferences.persistFilter(snapshot);
|
|
451
477
|
// Dirty tracking: the filter is dirty when it diverges from
|
|
452
|
-
// the active filter view's baseline.
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
// not happen past boot) treat the filter as clean.
|
|
478
|
+
// the active filter view's baseline. The baseline can be a
|
|
479
|
+
// filter parsed from a backend JSON column (arbitrary key
|
|
480
|
+
// order), so the comparison is key-order-insensitive via
|
|
481
|
+
// `stableStringify`. When no baseline has been captured yet
|
|
482
|
+
// (should not happen past boot) treat the filter as clean.
|
|
457
483
|
const differs = this._filterBaseline !== undefined &&
|
|
458
|
-
|
|
484
|
+
stableStringify(snapshot) !== stableStringify(this._filterBaseline);
|
|
459
485
|
this._viewState.markDirty('filter', differs);
|
|
460
486
|
this._persistActiveFilterView();
|
|
461
487
|
}, FILTER_PERSIST_DEBOUNCE_MS);
|
|
@@ -68,8 +68,11 @@ export declare class BuiltInRemoteRepository<TRow extends object> extends TableD
|
|
|
68
68
|
readonly setFilterMode: (id: string, mode: FilterMode) => void;
|
|
69
69
|
readonly filterBy: (id: string, value: unknown) => void;
|
|
70
70
|
readonly resetFilters: () => void;
|
|
71
|
+
/** Jumps to the given 1-based page number, clamped at >= 1. */
|
|
71
72
|
readonly setPage: (page: number) => void;
|
|
73
|
+
/** Advances to the next page. */
|
|
72
74
|
readonly nextPage: () => void;
|
|
75
|
+
/** Returns to the previous page, clamped at >= 1. */
|
|
73
76
|
readonly prevPage: () => void;
|
|
74
77
|
readonly setPageSize: (size: number | string) => void;
|
|
75
78
|
readonly updateSorting: (next: SortingState | ((prev: SortingState) => SortingState)) => void;
|
|
@@ -121,8 +124,11 @@ export declare class CustomRemoteRepository<TRow extends object> extends TableDa
|
|
|
121
124
|
readonly setFilterMode: (id: string, mode: FilterMode) => void;
|
|
122
125
|
readonly filterBy: (id: string, value: unknown) => void;
|
|
123
126
|
readonly resetFilters: () => void;
|
|
127
|
+
/** Jumps to the given 1-based page number, clamped at >= 1. */
|
|
124
128
|
readonly setPage: (page: number) => void;
|
|
129
|
+
/** Advances to the next page. */
|
|
125
130
|
readonly nextPage: () => void;
|
|
131
|
+
/** Returns to the previous page, clamped at >= 1. */
|
|
126
132
|
readonly prevPage: () => void;
|
|
127
133
|
readonly setPageSize: (size: number | string) => void;
|
|
128
134
|
readonly updateSorting: (next: SortingState | ((prev: SortingState) => SortingState)) => void;
|
|
@@ -118,15 +118,29 @@ export class BuiltInRemoteRepository extends TableDataRepository {
|
|
|
118
118
|
this._filtering = [];
|
|
119
119
|
this.scheduleReload();
|
|
120
120
|
};
|
|
121
|
+
/** Jumps to the given 1-based page number, clamped at >= 1. */
|
|
121
122
|
setPage = (page) => {
|
|
122
|
-
this.paginationState = {
|
|
123
|
+
this.paginationState = {
|
|
124
|
+
...this.paginationState,
|
|
125
|
+
pageIndex: Math.max(0, page - 1),
|
|
126
|
+
};
|
|
123
127
|
this.scheduleReload({ immediate: true });
|
|
124
128
|
};
|
|
129
|
+
/** Advances to the next page. */
|
|
125
130
|
nextPage = () => {
|
|
126
|
-
this.
|
|
131
|
+
this.paginationState = {
|
|
132
|
+
...this.paginationState,
|
|
133
|
+
pageIndex: this.paginationState.pageIndex + 1,
|
|
134
|
+
};
|
|
135
|
+
this.scheduleReload({ immediate: true });
|
|
127
136
|
};
|
|
137
|
+
/** Returns to the previous page, clamped at >= 1. */
|
|
128
138
|
prevPage = () => {
|
|
129
|
-
this.
|
|
139
|
+
this.paginationState = {
|
|
140
|
+
...this.paginationState,
|
|
141
|
+
pageIndex: Math.max(0, this.paginationState.pageIndex - 1),
|
|
142
|
+
};
|
|
143
|
+
this.scheduleReload({ immediate: true });
|
|
130
144
|
};
|
|
131
145
|
setPageSize = (size) => {
|
|
132
146
|
const parsed = typeof size === 'number' ? size : Number.parseInt(size, 10);
|
|
@@ -379,15 +393,29 @@ export class CustomRemoteRepository extends TableDataRepository {
|
|
|
379
393
|
this._filtering = [];
|
|
380
394
|
void this.reload();
|
|
381
395
|
};
|
|
396
|
+
/** Jumps to the given 1-based page number, clamped at >= 1. */
|
|
382
397
|
setPage = (page) => {
|
|
383
|
-
this.paginationState = {
|
|
398
|
+
this.paginationState = {
|
|
399
|
+
...this.paginationState,
|
|
400
|
+
pageIndex: Math.max(0, page - 1),
|
|
401
|
+
};
|
|
384
402
|
void this.reload();
|
|
385
403
|
};
|
|
404
|
+
/** Advances to the next page. */
|
|
386
405
|
nextPage = () => {
|
|
387
|
-
this.
|
|
406
|
+
this.paginationState = {
|
|
407
|
+
...this.paginationState,
|
|
408
|
+
pageIndex: this.paginationState.pageIndex + 1,
|
|
409
|
+
};
|
|
410
|
+
void this.reload();
|
|
388
411
|
};
|
|
412
|
+
/** Returns to the previous page, clamped at >= 1. */
|
|
389
413
|
prevPage = () => {
|
|
390
|
-
this.
|
|
414
|
+
this.paginationState = {
|
|
415
|
+
...this.paginationState,
|
|
416
|
+
pageIndex: Math.max(0, this.paginationState.pageIndex - 1),
|
|
417
|
+
};
|
|
418
|
+
void this.reload();
|
|
391
419
|
};
|
|
392
420
|
setPageSize = (size) => {
|
|
393
421
|
const parsed = typeof size === 'number' ? size : Number.parseInt(size, 10);
|