@lavalogic/scoria 0.38.5 → 0.38.6
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.
|
@@ -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);
|