@lavalogic/scoria 0.37.55 → 0.38.0

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.
Files changed (49) hide show
  1. package/dist/Components/Table/ARCHITECTURE.md +22 -22
  2. package/dist/Components/Table/Misc/ColumnPanel.svelte +1 -61
  3. package/dist/Components/Table/Misc/ColumnPanel.svelte.d.ts +2 -24
  4. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +3 -92
  5. package/dist/Components/Table/Misc/ColumnPanelModal.svelte.d.ts +2 -24
  6. package/dist/Components/Table/Misc/ColumnPanelModalProps.d.ts +2 -2
  7. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +644 -0
  8. package/dist/Components/Table/Misc/TableConfigurationModal.svelte.d.ts +26 -0
  9. package/dist/Components/Table/Misc/TableConfigurationModalProps.d.ts +13 -0
  10. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +27 -0
  11. package/dist/Components/Table/Misc/TableSidebar.svelte +45 -2
  12. package/dist/Components/Table/Misc/TableViewDropdown.svelte +175 -0
  13. package/dist/Components/Table/Misc/TableViewDropdown.svelte.d.ts +25 -0
  14. package/dist/Components/Table/SubApis.svelte.js +0 -41
  15. package/dist/Components/Table/Types/Columns/JSONTableLayout.d.ts +5 -5
  16. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +5 -5
  17. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +5 -5
  18. package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +106 -147
  19. package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +156 -203
  20. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +323 -44
  21. package/dist/Components/Table/Types/Context/TableContext.svelte.js +687 -187
  22. package/dist/Components/Table/Types/Context/TableInitOptions.d.ts +9 -0
  23. package/dist/Components/Table/Types/Context/ViewState.svelte.d.ts +152 -0
  24. package/dist/Components/Table/Types/Context/ViewState.svelte.js +223 -0
  25. package/dist/Components/Table/Types/ExpandedPanel.d.ts +9 -5
  26. package/dist/Components/Table/Types/ExpandedPanel.js +9 -5
  27. package/dist/Components/Table/Types/Persistence/DatatableView.d.ts +95 -0
  28. package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.d.ts +21 -0
  29. package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.js +1 -0
  30. package/dist/Components/Table/Types/Persistence/DatatableViewKind.d.ts +10 -0
  31. package/dist/Components/Table/Types/Persistence/DatatableViewKind.js +1 -0
  32. package/dist/Components/Table/Types/Persistence/JSONActiveView.d.ts +58 -0
  33. package/dist/Components/Table/Types/Persistence/JSONActiveView.js +41 -0
  34. package/dist/Components/Table/Types/Persistence/JSONTableFilter.d.ts +53 -0
  35. package/dist/Components/Table/Types/Persistence/JSONTableFilter.js +42 -0
  36. package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.d.ts +47 -0
  37. package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.js +1 -0
  38. package/dist/Components/Table/Types/Public/CreateTableOptions.d.ts +10 -0
  39. package/dist/Components/Table/Types/Public/TablePersistence.d.ts +1 -4
  40. package/dist/Components/Table/Types/Public/TableSubApis.d.ts +0 -18
  41. package/dist/Components/Table/Types/Public/index.d.ts +1 -1
  42. package/dist/Components/Table/createTable.svelte.js +2 -0
  43. package/dist/index.d.ts +8 -1
  44. package/dist/index.js +9 -0
  45. package/package.json +1 -1
  46. package/dist/Components/Table/Types/Columns/Definitions/ColumnDefSet.d.ts +0 -20
  47. package/dist/Components/Table/Types/Columns/Definitions/JSONColumnDefSet.d.ts +0 -25
  48. /package/dist/Components/Table/{Types/Columns/Definitions/ColumnDefSet.js → Misc/TableConfigurationModalProps.js} +0 -0
  49. /package/dist/Components/Table/Types/{Columns/Definitions/JSONColumnDefSet.js → Persistence/DatatableView.js} +0 -0
@@ -1,4 +1,5 @@
1
1
  import type { CellCoordinates } from '../Coordinates/CellCoordinates.js';
2
+ import type { RemoteTableLayoutAdapter } from '../Persistence/RemoteTableLayoutAdapter.js';
2
3
  import type { ToolbarPosition } from '../Toolbar/ToolbarPosition.js';
3
4
  /** Allowable primary-key types for the table's `selectionExtractor`. */
4
5
  export type Primitive = string | number;
@@ -29,5 +30,13 @@ export interface TableInitOptions<T extends object, Identifier extends Primitive
29
30
  displayFooter?: boolean;
30
31
  onEditCell?: (item: T, coords: CellCoordinates, value: unknown) => void | Promise<void>;
31
32
  getUserId: () => string | undefined;
33
+ /** Stable per-table identifier supplied by the host app. Identifies
34
+ * which datatable saved views belong to. Omit to disable remote views
35
+ * (the table then behaves exactly as before). */
36
+ datatableUuid?: string;
37
+ /** Injected backend adapter for saved layouts/filters. Omit to disable
38
+ * remote views. scoria stays backend-agnostic; the host app supplies
39
+ * this. */
40
+ remoteLayouts?: RemoteTableLayoutAdapter;
32
41
  debug?: boolean;
33
42
  }
@@ -0,0 +1,152 @@
1
+ import type { DatatableView } from '../Persistence/DatatableView.js';
2
+ import type { DatatableViewKind } from '../Persistence/DatatableViewKind.js';
3
+ import type { ActiveViewRef } from '../Persistence/JSONActiveView.js';
4
+ /**
5
+ * Sub-context owning the datatable *view selection* state machine for
6
+ * **both** view kinds - column `'layout'` and `'filter'` - as two fully
7
+ * independent slices (see `ViewKindSlice`). For each kind it tracks:
8
+ *
9
+ * - `activeView` - which view the live table belongs to (the code-defined
10
+ * Default, or a named saved `DatatableView`);
11
+ * - `isDirty` - whether the live state has diverged from the active
12
+ * view's saved baseline;
13
+ * - the saved views of that kind fetched through the injected
14
+ * `RemoteTableLayoutAdapter`;
15
+ * - `dropdownLabel` - the `$derived` label the toolbar view-picker
16
+ * renders (`'Default'` / `'Custom (unsaved)'` / `'<name>'` / `'<name>
17
+ * (unsaved)'`).
18
+ *
19
+ * Layouts and filters are *fully independent*: applying or dirtying one
20
+ * kind never touches the other. The two slices share only the
21
+ * `deriveDropdownLabel` formatting logic.
22
+ *
23
+ * It is a deliberately *pure state holder*: it never applies a layout or
24
+ * filter to the live table and never calls the remote adapter. Those side
25
+ * effects live on `TableContext` (which owns `_applyLayout` /
26
+ * `_captureLayout` / `_applyFilter` / `_captureFilter`, the baseline
27
+ * tracking and the adapter wiring), mirroring how `SelectionState` and
28
+ * `PreferencesState` keep their effects on the parent. `TableContext`
29
+ * drives this sub-context through the writer methods below.
30
+ *
31
+ * The sub-context holds no parent reference; it has no dependency thunks
32
+ * because it reads nothing from the parent - the parent pushes state in.
33
+ *
34
+ * ## Public surface
35
+ *
36
+ * The kind-aware methods (`markDirty(kind, ...)`,
37
+ * `selectDefault(kind)`, `selectSavedView(kind, ...)`,
38
+ * `setActiveView(kind, ...)`, `setSavedViews(kind, ...)`) and the
39
+ * per-kind getters (`activeView(kind)`, `isDirty(kind)`,
40
+ * `savedViews(kind)`, `dropdownLabel(kind)`) are the canonical surface.
41
+ * The unsuffixed `*Layout` aliases (`savedLayouts`, plus the
42
+ * single-arg behaviour of the kind methods defaulting to `'layout'`)
43
+ * are retained for the A2 layout call sites.
44
+ */
45
+ export declare class ViewState {
46
+ /** Layout-kind slice (column order / visibility / pinning / width). */
47
+ private readonly _layout;
48
+ /** Filter-kind slice (per-column filter values and modes). */
49
+ private readonly _filter;
50
+ /**
51
+ * Toolbar label `$derived` for the **layout** kind. Kept as a field
52
+ * (rather than a method) so the existing A2 layout call sites that
53
+ * read `dropdownLabelFor('layout')` and the legacy `layoutDropdownLabel`
54
+ * stay reactive without re-deriving on every read.
55
+ *
56
+ * - `'Default'` - Default active, not dirty;
57
+ * - `'Custom (unsaved)'` - Default active, dirty;
58
+ * - `'<name>'` - a saved view is active and unmodified;
59
+ * - `'<name> (unsaved)'` - a saved view is active but has been edited.
60
+ */
61
+ readonly layoutDropdownLabel: string;
62
+ /**
63
+ * Toolbar label `$derived` for the **filter** kind. Same four-way
64
+ * matrix as `layoutDropdownLabel`, computed independently from the
65
+ * filter slice.
66
+ */
67
+ readonly filterDropdownLabel: string;
68
+ /**
69
+ * Shared label-formatting logic for both kinds. The four-way branch is
70
+ * driven purely by the active-view ref and the dirty flag, so it is
71
+ * identical for layouts and filters - hence factored out as a static
72
+ * pure function consumed by both `$derived` labels above.
73
+ *
74
+ * - Default active: `'Default'` (clean) / `'Custom (unsaved)'` (dirty);
75
+ * - saved active: `'<name>'` (clean) / `'<name> (unsaved)'` (dirty).
76
+ */
77
+ private static deriveDropdownLabel;
78
+ /** Resolve the mutable slice for a `DatatableViewKind`. */
79
+ private slice;
80
+ /**
81
+ * The view the live table currently belongs to for `kind`. Read-only
82
+ * externally; the writers are `selectDefault` / `selectSavedView` /
83
+ * `setActiveView`.
84
+ */
85
+ activeView(kind: DatatableViewKind): ActiveViewRef;
86
+ /**
87
+ * Whether the live state of `kind` has diverged from the active
88
+ * view's saved baseline. Read-only externally; the writer is
89
+ * `markDirty`.
90
+ */
91
+ isDirty(kind: DatatableViewKind): boolean;
92
+ /**
93
+ * The saved views of `kind` available in the view-picker. Read-only
94
+ * externally; the writer is `setSavedViews`.
95
+ */
96
+ savedViews(kind: DatatableViewKind): ReadonlyArray<DatatableView>;
97
+ /**
98
+ * The toolbar label for `kind` (see `layoutDropdownLabel` /
99
+ * `filterDropdownLabel`). A thin kind-keyed accessor over the two
100
+ * `$derived` label fields.
101
+ */
102
+ dropdownLabel(kind: DatatableViewKind): string;
103
+ /**
104
+ * Set the dirty flag for `kind`. Called by the parent's debounced
105
+ * auto-persist `$effect` after comparing the freshly captured layout /
106
+ * filter against the current baseline. Idempotent - reassigning the
107
+ * same value short-circuits to avoid a redundant `$state` write.
108
+ */
109
+ markDirty(kind: DatatableViewKind, dirty: boolean): void;
110
+ /**
111
+ * Mark the Default view of `kind` as active and clear that kind's
112
+ * dirty flag. The parent calls this after applying the Default
113
+ * baseline to the live table (`resetToDefault`) or when boot
114
+ * hydration finds no persisted saved-view ref for the kind. Only the
115
+ * named kind's slice is touched.
116
+ */
117
+ selectDefault(kind: DatatableViewKind): void;
118
+ /**
119
+ * Mark a saved view of `kind` as active and clear that kind's dirty
120
+ * flag. The `ref` carries the server-assigned id and the cached
121
+ * display name. The parent calls this after applying the view to the
122
+ * live table (`applyDatatableView` / `saveCurrentAsNewView`) or when
123
+ * boot hydration restores a persisted saved-view ref. Only the named
124
+ * kind's slice is touched.
125
+ */
126
+ selectSavedView(kind: DatatableViewKind, ref: {
127
+ id: string;
128
+ name: string;
129
+ }): void;
130
+ /**
131
+ * Set the active view of `kind` directly from a persisted
132
+ * `ActiveViewRef`. Used by boot hydration, which reads the
133
+ * `JSONActiveView` envelope and must restore whatever ref it recorded
134
+ * *without* clearing the dirty flag (the persisted `dirty` value is
135
+ * restored separately via `markDirty`). Prefer `selectDefault` /
136
+ * `selectSavedView` for the normal apply flows, which also reset
137
+ * dirty.
138
+ */
139
+ setActiveView(kind: DatatableViewKind, ref: ActiveViewRef): void;
140
+ /**
141
+ * Replace the saved-views list for `kind` with a defensive copy.
142
+ * Called by the parent when the boot-time `refreshSavedViews` fetch
143
+ * resolves and after each save flow mutates the list. Only the named
144
+ * kind's slice is touched.
145
+ */
146
+ setSavedViews(kind: DatatableViewKind, views: ReadonlyArray<DatatableView>): void;
147
+ /**
148
+ * The `kind: 'layout'` saved views. A2 compatibility alias for
149
+ * `savedViews('layout')`.
150
+ */
151
+ get savedLayouts(): ReadonlyArray<DatatableView>;
152
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Per-kind slice of the view-selection state machine - the mutable state
3
+ * for *one* `DatatableViewKind` (`'layout'` or `'filter'`):
4
+ *
5
+ * - `activeView` - which view of this kind the live table belongs to (the
6
+ * code-defined Default, or a named saved `DatatableView`);
7
+ * - `isDirty` - whether the live state of this kind has diverged from the
8
+ * active view's saved baseline;
9
+ * - `savedViews` - the saved views of this kind fetched through the
10
+ * injected `RemoteTableLayoutAdapter`.
11
+ *
12
+ * `ViewState` holds one `ViewKindSlice` per kind so the two are *fully
13
+ * independent*: dirtying or applying a layout view never touches the
14
+ * filter slice and vice versa. The `$state` fields live here (rather than
15
+ * on `ViewState`) so each slice is an independent reactive unit.
16
+ */
17
+ class ViewKindSlice {
18
+ /**
19
+ * The view of this kind the live table currently belongs to. Defaults
20
+ * to the code-defined Default. Written by the parent through
21
+ * `selectDefault` / `selectSavedView` when a view is applied (or
22
+ * restored at boot).
23
+ */
24
+ activeView = $state({ kind: 'default' });
25
+ /**
26
+ * `true` when the live state of this kind has diverged from the active
27
+ * view's saved baseline. Written by the parent's debounced
28
+ * auto-persist `$effect` through `ViewState.markDirty`.
29
+ */
30
+ isDirty = $state(false);
31
+ /**
32
+ * The saved views of this kind fetched through the injected
33
+ * `RemoteTableLayoutAdapter`. Empty when remote saved views are
34
+ * disabled or the fetch has not completed.
35
+ */
36
+ savedViews = $state([]);
37
+ }
38
+ /**
39
+ * Sub-context owning the datatable *view selection* state machine for
40
+ * **both** view kinds - column `'layout'` and `'filter'` - as two fully
41
+ * independent slices (see `ViewKindSlice`). For each kind it tracks:
42
+ *
43
+ * - `activeView` - which view the live table belongs to (the code-defined
44
+ * Default, or a named saved `DatatableView`);
45
+ * - `isDirty` - whether the live state has diverged from the active
46
+ * view's saved baseline;
47
+ * - the saved views of that kind fetched through the injected
48
+ * `RemoteTableLayoutAdapter`;
49
+ * - `dropdownLabel` - the `$derived` label the toolbar view-picker
50
+ * renders (`'Default'` / `'Custom (unsaved)'` / `'<name>'` / `'<name>
51
+ * (unsaved)'`).
52
+ *
53
+ * Layouts and filters are *fully independent*: applying or dirtying one
54
+ * kind never touches the other. The two slices share only the
55
+ * `deriveDropdownLabel` formatting logic.
56
+ *
57
+ * It is a deliberately *pure state holder*: it never applies a layout or
58
+ * filter to the live table and never calls the remote adapter. Those side
59
+ * effects live on `TableContext` (which owns `_applyLayout` /
60
+ * `_captureLayout` / `_applyFilter` / `_captureFilter`, the baseline
61
+ * tracking and the adapter wiring), mirroring how `SelectionState` and
62
+ * `PreferencesState` keep their effects on the parent. `TableContext`
63
+ * drives this sub-context through the writer methods below.
64
+ *
65
+ * The sub-context holds no parent reference; it has no dependency thunks
66
+ * because it reads nothing from the parent - the parent pushes state in.
67
+ *
68
+ * ## Public surface
69
+ *
70
+ * The kind-aware methods (`markDirty(kind, ...)`,
71
+ * `selectDefault(kind)`, `selectSavedView(kind, ...)`,
72
+ * `setActiveView(kind, ...)`, `setSavedViews(kind, ...)`) and the
73
+ * per-kind getters (`activeView(kind)`, `isDirty(kind)`,
74
+ * `savedViews(kind)`, `dropdownLabel(kind)`) are the canonical surface.
75
+ * The unsuffixed `*Layout` aliases (`savedLayouts`, plus the
76
+ * single-arg behaviour of the kind methods defaulting to `'layout'`)
77
+ * are retained for the A2 layout call sites.
78
+ */
79
+ export class ViewState {
80
+ /** Layout-kind slice (column order / visibility / pinning / width). */
81
+ _layout = new ViewKindSlice();
82
+ /** Filter-kind slice (per-column filter values and modes). */
83
+ _filter = new ViewKindSlice();
84
+ /**
85
+ * Toolbar label `$derived` for the **layout** kind. Kept as a field
86
+ * (rather than a method) so the existing A2 layout call sites that
87
+ * read `dropdownLabelFor('layout')` and the legacy `layoutDropdownLabel`
88
+ * stay reactive without re-deriving on every read.
89
+ *
90
+ * - `'Default'` - Default active, not dirty;
91
+ * - `'Custom (unsaved)'` - Default active, dirty;
92
+ * - `'<name>'` - a saved view is active and unmodified;
93
+ * - `'<name> (unsaved)'` - a saved view is active but has been edited.
94
+ */
95
+ layoutDropdownLabel = $derived.by(() => ViewState.deriveDropdownLabel(this._layout.activeView, this._layout.isDirty));
96
+ /**
97
+ * Toolbar label `$derived` for the **filter** kind. Same four-way
98
+ * matrix as `layoutDropdownLabel`, computed independently from the
99
+ * filter slice.
100
+ */
101
+ filterDropdownLabel = $derived.by(() => ViewState.deriveDropdownLabel(this._filter.activeView, this._filter.isDirty));
102
+ /**
103
+ * Shared label-formatting logic for both kinds. The four-way branch is
104
+ * driven purely by the active-view ref and the dirty flag, so it is
105
+ * identical for layouts and filters - hence factored out as a static
106
+ * pure function consumed by both `$derived` labels above.
107
+ *
108
+ * - Default active: `'Default'` (clean) / `'Custom (unsaved)'` (dirty);
109
+ * - saved active: `'<name>'` (clean) / `'<name> (unsaved)'` (dirty).
110
+ */
111
+ static deriveDropdownLabel(active, dirty) {
112
+ if (active.kind === 'default') {
113
+ return dirty ? 'Custom (unsaved)' : 'Default';
114
+ }
115
+ return dirty ? `${active.name} (unsaved)` : active.name;
116
+ }
117
+ /** Resolve the mutable slice for a `DatatableViewKind`. */
118
+ slice(kind) {
119
+ return kind === 'filter' ? this._filter : this._layout;
120
+ }
121
+ // #region kind-aware getters
122
+ /**
123
+ * The view the live table currently belongs to for `kind`. Read-only
124
+ * externally; the writers are `selectDefault` / `selectSavedView` /
125
+ * `setActiveView`.
126
+ */
127
+ activeView(kind) {
128
+ return this.slice(kind).activeView;
129
+ }
130
+ /**
131
+ * Whether the live state of `kind` has diverged from the active
132
+ * view's saved baseline. Read-only externally; the writer is
133
+ * `markDirty`.
134
+ */
135
+ isDirty(kind) {
136
+ return this.slice(kind).isDirty;
137
+ }
138
+ /**
139
+ * The saved views of `kind` available in the view-picker. Read-only
140
+ * externally; the writer is `setSavedViews`.
141
+ */
142
+ savedViews(kind) {
143
+ return this.slice(kind).savedViews;
144
+ }
145
+ /**
146
+ * The toolbar label for `kind` (see `layoutDropdownLabel` /
147
+ * `filterDropdownLabel`). A thin kind-keyed accessor over the two
148
+ * `$derived` label fields.
149
+ */
150
+ dropdownLabel(kind) {
151
+ return kind === 'filter' ? this.filterDropdownLabel : this.layoutDropdownLabel;
152
+ }
153
+ // #endregion
154
+ // #region kind-aware writers
155
+ /**
156
+ * Set the dirty flag for `kind`. Called by the parent's debounced
157
+ * auto-persist `$effect` after comparing the freshly captured layout /
158
+ * filter against the current baseline. Idempotent - reassigning the
159
+ * same value short-circuits to avoid a redundant `$state` write.
160
+ */
161
+ markDirty(kind, dirty) {
162
+ const slice = this.slice(kind);
163
+ if (dirty === slice.isDirty) {
164
+ return;
165
+ }
166
+ slice.isDirty = dirty;
167
+ }
168
+ /**
169
+ * Mark the Default view of `kind` as active and clear that kind's
170
+ * dirty flag. The parent calls this after applying the Default
171
+ * baseline to the live table (`resetToDefault`) or when boot
172
+ * hydration finds no persisted saved-view ref for the kind. Only the
173
+ * named kind's slice is touched.
174
+ */
175
+ selectDefault(kind) {
176
+ const slice = this.slice(kind);
177
+ slice.activeView = { kind: 'default' };
178
+ slice.isDirty = false;
179
+ }
180
+ /**
181
+ * Mark a saved view of `kind` as active and clear that kind's dirty
182
+ * flag. The `ref` carries the server-assigned id and the cached
183
+ * display name. The parent calls this after applying the view to the
184
+ * live table (`applyDatatableView` / `saveCurrentAsNewView`) or when
185
+ * boot hydration restores a persisted saved-view ref. Only the named
186
+ * kind's slice is touched.
187
+ */
188
+ selectSavedView(kind, ref) {
189
+ const slice = this.slice(kind);
190
+ slice.activeView = { kind: 'saved', id: ref.id, name: ref.name };
191
+ slice.isDirty = false;
192
+ }
193
+ /**
194
+ * Set the active view of `kind` directly from a persisted
195
+ * `ActiveViewRef`. Used by boot hydration, which reads the
196
+ * `JSONActiveView` envelope and must restore whatever ref it recorded
197
+ * *without* clearing the dirty flag (the persisted `dirty` value is
198
+ * restored separately via `markDirty`). Prefer `selectDefault` /
199
+ * `selectSavedView` for the normal apply flows, which also reset
200
+ * dirty.
201
+ */
202
+ setActiveView(kind, ref) {
203
+ this.slice(kind).activeView = ref;
204
+ }
205
+ /**
206
+ * Replace the saved-views list for `kind` with a defensive copy.
207
+ * Called by the parent when the boot-time `refreshSavedViews` fetch
208
+ * resolves and after each save flow mutates the list. Only the named
209
+ * kind's slice is touched.
210
+ */
211
+ setSavedViews(kind, views) {
212
+ this.slice(kind).savedViews = [...views];
213
+ }
214
+ // #endregion
215
+ // #region layout-kind aliases (A2 call-site compatibility)
216
+ /**
217
+ * The `kind: 'layout'` saved views. A2 compatibility alias for
218
+ * `savedViews('layout')`.
219
+ */
220
+ get savedLayouts() {
221
+ return this._layout.savedViews;
222
+ }
223
+ }
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Discriminator on the currently-visible toolbar sidebar panel:
3
3
  *
4
- * - `None` - no panel open (currently the `null` state is preferred in
5
- * `TableSidebar.svelte`; `None` is kept for symmetry but unused).
6
- * - `Column` - column-picker panel.
7
- * - `Filter` - advanced filter panel.
8
- * - `Toolbox` - settings / toolbox panel.
4
+ * - `None` - no panel open (currently the `null` state is
5
+ * preferred in `TableSidebar.svelte`; `None` is kept for symmetry but
6
+ * unused).
7
+ * - `Column` - column-picker panel.
8
+ * - `Filter` - advanced filter panel.
9
+ * - `Toolbox` - settings / toolbox panel.
10
+ * - `Configuration` - FPM 403 "Table Configuration" modal: saved
11
+ * datatable layouts and filters (the new view system).
9
12
  */
10
13
  export declare const ExpandedPanel: {
11
14
  readonly None: "None";
12
15
  readonly Column: "Column";
13
16
  readonly Filter: "Filter";
14
17
  readonly Toolbox: "Toolbox";
18
+ readonly Configuration: "Configuration";
15
19
  };
16
20
  export type ExpandedPanel = (typeof ExpandedPanel)[keyof typeof ExpandedPanel];
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Discriminator on the currently-visible toolbar sidebar panel:
3
3
  *
4
- * - `None` - no panel open (currently the `null` state is preferred in
5
- * `TableSidebar.svelte`; `None` is kept for symmetry but unused).
6
- * - `Column` - column-picker panel.
7
- * - `Filter` - advanced filter panel.
8
- * - `Toolbox` - settings / toolbox panel.
4
+ * - `None` - no panel open (currently the `null` state is
5
+ * preferred in `TableSidebar.svelte`; `None` is kept for symmetry but
6
+ * unused).
7
+ * - `Column` - column-picker panel.
8
+ * - `Filter` - advanced filter panel.
9
+ * - `Toolbox` - settings / toolbox panel.
10
+ * - `Configuration` - FPM 403 "Table Configuration" modal: saved
11
+ * datatable layouts and filters (the new view system).
9
12
  */
10
13
  export const ExpandedPanel = {
11
14
  None: 'None',
12
15
  Column: 'Column',
13
16
  Filter: 'Filter',
14
17
  Toolbox: 'Toolbox',
18
+ Configuration: 'Configuration',
15
19
  };
16
20
  Object.freeze(ExpandedPanel);
@@ -0,0 +1,95 @@
1
+ import type { DatatableViewBody } from './DatatableViewEnvelope.js';
2
+ import type { DatatableViewKind } from './DatatableViewKind.js';
3
+ /**
4
+ * A saved datatable view - a named, persisted column layout or filter
5
+ * that round-trips through a `RemoteTableLayoutAdapter`. This is the
6
+ * frozen DTO contract the consuming host app codes against; scoria
7
+ * itself stays backend-agnostic and never constructs the persistence.
8
+ *
9
+ * Ownership and sharing in v1:
10
+ * - The UI always creates exactly one owner: the user who creates the
11
+ * view (`ownerUserId`). scoria never interprets that identifier - it
12
+ * is an opaque token supplied and understood only by the host app.
13
+ * - The backend's `editor` permission and `systemOwned` flag are
14
+ * reserved no-ops in v1: scoria never sets them and never relies on
15
+ * them. Sharing is subscriber-only (see `DatatableViewSubscription`).
16
+ */
17
+ export interface DatatableView {
18
+ /** Server-assigned, opaque record id. Set by the backend on create;
19
+ * scoria treats it as a meaningless handle for update/delete. */
20
+ id: string;
21
+ /** Stable identifier of the datatable this view belongs to (the
22
+ * `datatableUuid` constant supplied by the host app). */
23
+ datatableUuid: string;
24
+ /** Whether this view holds a column layout or a filter. Matches the
25
+ * `kind` discriminant of `body`. */
26
+ kind: DatatableViewKind;
27
+ /** Human-readable name shown in the saved-views picker. */
28
+ name: string;
29
+ /** The typed payload: a `JSONTableLayout` or a `JSONTableFilter`,
30
+ * discriminated by `kind`. */
31
+ body: DatatableViewBody;
32
+ /** Opaque identifier of the user who owns this view. scoria never
33
+ * interprets it; the UI always sets it to the creating user. */
34
+ ownerUserId: string;
35
+ /**
36
+ * The users this view is currently shared with (v1 sharing is
37
+ * subscriber-only - see `DatatableViewSubscription`). Optional: the
38
+ * backend populates it on a view the *current* user owns so the
39
+ * Table Configuration modal can list and manage subscribers. It is
40
+ * omitted (or empty) on views the current user merely subscribes to
41
+ * - a non-owner has no business seeing the full subscriber roster.
42
+ *
43
+ * Added in A4a (the modal needs to render an owner's subscriber list
44
+ * and an Unsubscribe control). scoria never interprets the entries;
45
+ * `userId` is the same opaque token as `ownerUserId`. `displayName`
46
+ * is an optional human-readable label the host may attach for the
47
+ * UI - scoria falls back to `userId` when it is absent.
48
+ */
49
+ subscribers?: ReadonlyArray<DatatableViewSubscriber>;
50
+ }
51
+ /**
52
+ * One entry in a `DatatableView.subscribers` roster: an opaque user
53
+ * identifier plus an optional human-readable label. scoria never
54
+ * interprets `userId`; `displayName`, when supplied by the host, is
55
+ * shown in the Table Configuration modal in preference to the raw id.
56
+ */
57
+ export interface DatatableViewSubscriber {
58
+ /** Opaque identifier of the subscribed user (same token space as
59
+ * `DatatableView.ownerUserId`). */
60
+ userId: string;
61
+ /** Optional human-readable label for the subscriber. The modal falls
62
+ * back to `userId` when this is absent. */
63
+ displayName?: string;
64
+ }
65
+ /**
66
+ * Request payload for creating a brand-new `DatatableView`. Identical to
67
+ * `DatatableView` except the server-assigned `id` is omitted - the
68
+ * backend assigns it and returns the complete record.
69
+ */
70
+ export type CreateDatatableViewRequest = Omit<DatatableView, 'id'>;
71
+ /**
72
+ * Request payload for updating an existing `DatatableView`. `id`
73
+ * identifies the record; `name` and `body` are each optional so a caller
74
+ * can patch either independently (rename without re-sending the body, or
75
+ * replace the body without renaming).
76
+ */
77
+ export interface UpdateDatatableViewRequest {
78
+ /** Server-assigned id of the view to update. */
79
+ id: string;
80
+ /** New name, if the view is being renamed. */
81
+ name?: string;
82
+ /** New payload, if the layout/filter is being replaced. */
83
+ body?: DatatableViewBody;
84
+ }
85
+ /**
86
+ * A subscriber relationship for a shared `DatatableView`. v1 sharing is
87
+ * subscriber-only: a non-owner is granted read access by being added as
88
+ * a subscriber (there is no `editor` role in v1).
89
+ */
90
+ export interface DatatableViewSubscription {
91
+ /** Id of the `DatatableView` being shared. */
92
+ viewId: string;
93
+ /** Opaque identifier of the subscribed user. */
94
+ userId: string;
95
+ }
@@ -0,0 +1,21 @@
1
+ import type { JSONTableLayout } from '../Columns/JSONTableLayout.js';
2
+ import type { JSONTableFilter } from './JSONTableFilter.js';
3
+ /**
4
+ * The typed payload body stored inside a saved `DatatableView`, as a
5
+ * discriminated union keyed on `kind`:
6
+ *
7
+ * - `{ kind: 'layout' }` carries a `JSONTableLayout` (column order,
8
+ * visibility, pinning, width).
9
+ * - `{ kind: 'filter' }` carries a `JSONTableFilter` (per-column filter
10
+ * values and modes).
11
+ *
12
+ * The `kind` tag matches the `DatatableView.kind` field, so a caller can
13
+ * narrow the body by switching on a single discriminant.
14
+ */
15
+ export type DatatableViewBody = {
16
+ kind: 'layout';
17
+ layout: JSONTableLayout;
18
+ } | {
19
+ kind: 'filter';
20
+ filter: JSONTableFilter;
21
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Discriminant for a saved `DatatableView`: whether the record holds a
3
+ * column layout (`'layout'` - order / visibility / pinning / width) or a
4
+ * filter (`'filter'` - per-column filter values and modes).
5
+ *
6
+ * The same value appears both on the `DatatableView` DTO and as the
7
+ * `kind` tag of the `DatatableViewBody` discriminated union, so a caller
8
+ * can switch on a single field to know which payload shape it holds.
9
+ */
10
+ export type DatatableViewKind = 'layout' | 'filter';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Reference to the datatable view currently driving the table - either the
3
+ * code-defined Default layout, or a named saved view round-tripped through
4
+ * a `RemoteTableLayoutAdapter`.
5
+ *
6
+ * Discriminated on `kind`:
7
+ * - `{ kind: 'default' }` - the live table is tracking the factory column
8
+ * defs (no saved view selected);
9
+ * - `{ kind: 'saved', id, name }` - a saved `DatatableView` is selected.
10
+ * `id` is the server-assigned record id; `name` is cached alongside it
11
+ * so the dropdown label can render before the saved-view list has been
12
+ * fetched (boot-time hydration reads this before the network resolves).
13
+ */
14
+ export type ActiveViewRef = {
15
+ kind: 'default';
16
+ } | {
17
+ kind: 'saved';
18
+ id: string;
19
+ name: string;
20
+ };
21
+ /**
22
+ * Persisted-to-localStorage record of which datatable view is active and
23
+ * whether the live table has diverged from that view's saved baseline.
24
+ *
25
+ * This is the view-selection counterpart of `JSONTableLayout` (which
26
+ * persists the *contents* of the live layout): `JSONActiveView` only
27
+ * records *which* view the working copy belongs to and the dirty flag, so
28
+ * after a reload the toolbar dropdown can show e.g. `"Sales (unsaved)"`
29
+ * rather than resetting to `"Default"`.
30
+ *
31
+ * `TableContext` writes this (debounced, alongside the working-copy
32
+ * layout) on every layout change and reads it back during boot hydration.
33
+ */
34
+ export interface JSONActiveView {
35
+ /** Which view the live table belongs to (Default or a saved view). */
36
+ active: ActiveViewRef;
37
+ /** Whether the live layout has diverged from `active`'s saved
38
+ * baseline. Drives the `"(unsaved)"` suffix in the dropdown label. */
39
+ dirty: boolean;
40
+ }
41
+ /**
42
+ * Schema version for the persisted `JSONActiveView` envelope. Bump when
43
+ * the shape or field semantics change in a backwards-incompatible way;
44
+ * `readVersionedJSON` silently drops envelopes written under a different
45
+ * version.
46
+ */
47
+ export declare const TABLE_ACTIVE_VIEW_SCHEMA_VERSION = 1;
48
+ /**
49
+ * Conservative typeguard for a persisted `JSONActiveView`. localStorage is
50
+ * same-origin-writable so the decoded value is untrusted; this checks the
51
+ * envelope shape, the `active` discriminated union, and every field's
52
+ * primitive type.
53
+ *
54
+ * The `id` referenced by a `kind: 'saved'` slot is *not* verified against
55
+ * the live saved-view list here - `TableContext` reconciles the persisted
56
+ * ref against the fetched views once they load.
57
+ */
58
+ export declare function isJSONActiveView(raw: unknown): raw is JSONActiveView;