@lavalogic/scoria 0.38.10 → 0.38.11

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.
@@ -22,6 +22,7 @@
22
22
  import type { DatatableViewKind } from '../Types/Persistence/DatatableViewKind.js';
23
23
  import ShareViewModal from './ShareViewModal.svelte';
24
24
  import type { TableConfigurationModalProps } from './TableConfigurationModalProps.js';
25
+ import UpdateViewOptionsModal from './UpdateViewOptionsModal.svelte';
25
26
 
26
27
  const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
27
28
 
@@ -60,6 +61,26 @@
60
61
  // to; the modal closes itself on Cancel / Save.
61
62
  let sharingView = $state<DatatableView | null>(null);
62
63
 
64
+ // The view whose Update Options modal is currently open. `null` when
65
+ // no options modal is rendered. Same pattern as `sharingView`.
66
+ let updatingOptionsView = $state<DatatableView | null>(null);
67
+
68
+ /**
69
+ * Overwrite a saved view's stored body with the table's current
70
+ * live state. Triggered by the "Save Layout" / "Save Filter"
71
+ * button on an owner card - the typical flow is "apply view A,
72
+ * tweak the columns / filters, then click Save on A". Refreshes
73
+ * the saved-views list afterwards so the card label and dropdown
74
+ * pick up the new body, and surfaces any failure in the inline
75
+ * error banner.
76
+ */
77
+ function saveLiveStateInto(view: DatatableView): void {
78
+ void guard(async () => {
79
+ await tableContext.saveLiveStateIntoView(view);
80
+ await tableContext.refreshSavedViews();
81
+ }, `Could not save changes to "${view.name}". Please try again.`);
82
+ }
83
+
63
84
  // Whether remote saved views are available at all. A table created
64
85
  // without `datatableUuid` / `remoteLayouts` opts out of remote views;
65
86
  // the modal then renders a graceful "unavailable" empty state and
@@ -358,6 +379,39 @@
358
379
  applyView(view);
359
380
  }}>Apply</Button
360
381
  >
382
+ <!--
383
+ "Save Layout" / "Save Filter" overwrites THIS saved
384
+ view's stored body with the table's current live state.
385
+ Typical flow: user applies the view, tweaks the
386
+ columns or filter inputs, then clicks Save to make
387
+ the tweaks part of the saved view. Distinct from
388
+ "Save a new …" (which always creates a fresh record).
389
+ -->
390
+ <Button
391
+ variant={Variant.Secondary}
392
+ size={Size.MediumSmall}
393
+ nowrap
394
+ iconLeft={{ name: 'circle-tick', size: Size.Small }}
395
+ onclick={() => {
396
+ saveLiveStateInto(view);
397
+ }}>Save {sectionNoun}</Button
398
+ >
399
+ <!--
400
+ "Update Options" opens a dedicated modal that edits
401
+ the view's name and sharing visibility (private /
402
+ public). Kept separate from the inline body-save
403
+ above so a quick name fix never accidentally
404
+ overwrites the saved layout / filter.
405
+ -->
406
+ <Button
407
+ variant={Variant.Secondary}
408
+ size={Size.MediumSmall}
409
+ nowrap
410
+ iconLeft={{ name: 'edit', size: Size.Small }}
411
+ onclick={() => {
412
+ updatingOptionsView = view;
413
+ }}>Update Options</Button
414
+ >
361
415
  <Button
362
416
  variant={Variant.Secondary}
363
417
  size={Size.MediumSmall}
@@ -570,6 +624,23 @@
570
624
  />
571
625
  {/if}
572
626
 
627
+ <!--
628
+ Update-Options dialog stacked the same way as the Share dialog.
629
+ After a successful Save we refresh the saved views so the My-* card
630
+ picks up the new name / visibility immediately.
631
+ -->
632
+ {#if updatingOptionsView}
633
+ <UpdateViewOptionsModal
634
+ view={updatingOptionsView}
635
+ onclose={() => {
636
+ updatingOptionsView = null;
637
+ }}
638
+ onsaved={async () => {
639
+ await tableContext.refreshSavedViews();
640
+ }}
641
+ />
642
+ {/if}
643
+
573
644
  <style>.empty-state {
574
645
  display: flex;
575
646
  flex-flow: column nowrap;
@@ -0,0 +1,225 @@
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ generics="T extends object, IdType extends Primitive"
6
+ >
7
+ import DesktopModal from '../../DesktopModal.svelte';
8
+ import Icon from '../../Icon.svelte';
9
+ import InfoAlert from '../../InfoAlert.svelte';
10
+ import SingleSelect from '../../SingleSelect.svelte';
11
+ import TextInput from '../../TextInput.svelte';
12
+ import { cssLength } from '../../../Helpers/Helpers.svelte.js';
13
+ import { ColourSet } from '../../../scss/colours.js';
14
+ import type { SelectOption } from '../../../Types/Internal/SelectOption.js';
15
+ import { Size } from '../../../Types/Internal/Size.js';
16
+ import { getContext, onMount } from 'svelte';
17
+ import { TableContext } from '../Types/Context/TableContext.svelte.js';
18
+ import type { Primitive } from '../Types/Context/TableInitOptions.js';
19
+ import type { DatatableViewVisibility } from '../Types/Persistence/DatatableView.js';
20
+ import type { UpdateViewOptionsModalProps } from './UpdateViewOptionsModalProps.js';
21
+
22
+ const { view, onclose, onsaved }: UpdateViewOptionsModalProps = $props();
23
+
24
+ const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
25
+
26
+ // Local form state. Seeded from the view's current values inside
27
+ // `onMount` so the modal opens populated with what's already saved
28
+ // without triggering Svelte's `state_referenced_locally` lint
29
+ // against the prop read; Save diffs against the view's current
30
+ // values to skip a no-op PATCH.
31
+ let name: string = $state('');
32
+ let visibility: DatatableViewVisibility = $state('private');
33
+
34
+ onMount(() => {
35
+ name = view.name;
36
+ visibility = view.visibility ?? 'private';
37
+ });
38
+
39
+ // User-visible error banner. Adapter rejections are routed here so
40
+ // the modal never throws into the host UI.
41
+ let errorMessage = $state<string | null>(null);
42
+
43
+ // `true` while the PATCH is in flight, disables the footer buttons.
44
+ let saving = $state(false);
45
+
46
+ const visibilityOptions: Array<SelectOption<DatatableViewVisibility>> = [
47
+ {
48
+ label: 'Private — only owner and explicit subscribers',
49
+ value: 'private',
50
+ },
51
+ {
52
+ label: 'Public — anyone with access to this table can subscribe',
53
+ value: 'public',
54
+ },
55
+ ];
56
+
57
+ const selectedVisibility: SelectOption<DatatableViewVisibility> = $derived(
58
+ visibilityOptions.find((o) => o.value === visibility) ?? visibilityOptions[0]
59
+ );
60
+
61
+ // Whether anything in the form differs from the initial values.
62
+ // Drives the Save button's disabled state so a no-op Save never
63
+ // hits the network (also enforced inside `updateViewOptions`).
64
+ const dirty = $derived(
65
+ name.trim() !== view.name || visibility !== (view.visibility ?? 'private')
66
+ );
67
+
68
+ /** Whether the name is valid for submission (non-empty, trimmed). */
69
+ const isValidName = $derived(name.trim().length > 0);
70
+
71
+ async function save(): Promise<void> {
72
+ if (!dirty) {
73
+ onclose();
74
+ return;
75
+ }
76
+ if (!isValidName) {
77
+ errorMessage = `Name cannot be empty.`;
78
+ return;
79
+ }
80
+
81
+ errorMessage = null;
82
+ saving = true;
83
+ try {
84
+ await tableContext.updateViewOptions(view, {
85
+ name: name.trim(),
86
+ visibility,
87
+ });
88
+ if (onsaved) {
89
+ await onsaved();
90
+ }
91
+ onclose();
92
+ } catch (e: unknown) {
93
+ console.error('[UpdateViewOptionsModal] save failed:', e);
94
+ errorMessage =
95
+ e instanceof Error
96
+ ? `Could not update options. (${e.message})`
97
+ : 'Could not update options.';
98
+ } finally {
99
+ saving = false;
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <DesktopModal
105
+ headerLabel={`Update Options - "${view.name}"`}
106
+ headerSubtitle={`Rename and change sharing for this ${view.kind === 'filter' ? 'filter' : 'layout'}.`}
107
+ onclose={() => {
108
+ if (!saving) {
109
+ onclose();
110
+ }
111
+ }}
112
+ width={cssLength('40vw')}
113
+ minWidth={cssLength('32rem')}
114
+ buttons={[
115
+ {
116
+ label: 'Cancel',
117
+ callback: () => {
118
+ if (!saving) {
119
+ onclose();
120
+ }
121
+ },
122
+ disabled: saving,
123
+ },
124
+ {
125
+ label: saving ? 'Saving…' : 'Save',
126
+ callback: () => {
127
+ void save();
128
+ },
129
+ colourSet: ColourSet.Primary,
130
+ disabled: saving || !dirty || !isValidName,
131
+ },
132
+ ]}
133
+ >
134
+ <div class="options-body">
135
+ {#if errorMessage}
136
+ <div
137
+ class="error-banner"
138
+ role="alert"
139
+ >
140
+ <Icon
141
+ name="circle-exclamation"
142
+ size={Size.Small}
143
+ />
144
+ <span>{errorMessage}</span>
145
+ </div>
146
+ {/if}
147
+
148
+ <InfoAlert>
149
+ Changes apply to every user who has access to this {view.kind === 'filter'
150
+ ? 'filter'
151
+ : 'layout'}. Making it public allows any user with access to the table to subscribe.
152
+ </InfoAlert>
153
+
154
+ <div class="field">
155
+ <label
156
+ class="field-label"
157
+ for="update-view-name">Name</label
158
+ >
159
+ <TextInput
160
+ id="update-view-name"
161
+ bind:value={name}
162
+ placeholder={`${view.kind === 'filter' ? 'Filter' : 'Layout'} name`}
163
+ required
164
+ invalid={!isValidName}
165
+ />
166
+ </div>
167
+
168
+ <div class="field">
169
+ <label
170
+ class="field-label"
171
+ for="update-view-visibility">Visibility</label
172
+ >
173
+ <SingleSelect
174
+ inputId="update-view-visibility"
175
+ name="Visibility"
176
+ options={visibilityOptions}
177
+ labelProp="label"
178
+ valueProp="value"
179
+ value={selectedVisibility}
180
+ valueAsObject
181
+ searchable={false}
182
+ onchange={(option: SelectOption<DatatableViewVisibility> | null) => {
183
+ if (option) {
184
+ visibility = option.value;
185
+ }
186
+ }}
187
+ />
188
+ </div>
189
+ </div>
190
+ </DesktopModal>
191
+
192
+ <style>.options-body {
193
+ display: flex;
194
+ flex-flow: column nowrap;
195
+ gap: 0.75rem;
196
+ padding: 1rem;
197
+ min-height: 25vh;
198
+ max-height: 60vh;
199
+ overflow-y: auto;
200
+ background-color: #ffffff;
201
+ }
202
+
203
+ .error-banner {
204
+ display: flex;
205
+ flex-flow: row nowrap;
206
+ align-items: center;
207
+ gap: 0.5rem;
208
+ padding: 0.5rem 0.75rem;
209
+ border: solid 1px #aa1414;
210
+ border-radius: 4px;
211
+ background-color: #f9e9e9;
212
+ color: #aa1414;
213
+ }
214
+
215
+ .field {
216
+ display: flex;
217
+ flex-flow: column nowrap;
218
+ gap: 0.25rem;
219
+ }
220
+
221
+ .field-label {
222
+ font-size: 1.125rem;
223
+ font-weight: 600;
224
+ color: #3a4952;
225
+ }</style>
@@ -0,0 +1,26 @@
1
+ import type { Primitive } from '../Types/Context/TableInitOptions.js';
2
+ import type { UpdateViewOptionsModalProps } from './UpdateViewOptionsModalProps.js';
3
+ declare function $$render<T extends object, IdType extends Primitive>(): {
4
+ props: UpdateViewOptionsModalProps;
5
+ exports: {};
6
+ bindings: "";
7
+ slots: {};
8
+ events: {};
9
+ };
10
+ declare class __sveltets_Render<T extends object, IdType extends Primitive> {
11
+ props(): ReturnType<typeof $$render<T, IdType>>['props'];
12
+ events(): ReturnType<typeof $$render<T, IdType>>['events'];
13
+ slots(): ReturnType<typeof $$render<T, IdType>>['slots'];
14
+ bindings(): "";
15
+ exports(): {};
16
+ }
17
+ interface $$IsomorphicComponent {
18
+ new <T extends object, IdType extends Primitive>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, IdType>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, IdType>['props']>, ReturnType<__sveltets_Render<T, IdType>['events']>, ReturnType<__sveltets_Render<T, IdType>['slots']>> & {
19
+ $$bindings?: ReturnType<__sveltets_Render<T, IdType>['bindings']>;
20
+ } & ReturnType<__sveltets_Render<T, IdType>['exports']>;
21
+ <T extends object, IdType extends Primitive>(internal: unknown, props: ReturnType<__sveltets_Render<T, IdType>['props']> & {}): ReturnType<__sveltets_Render<T, IdType>['exports']>;
22
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
23
+ }
24
+ declare const UpdateViewOptionsModal: $$IsomorphicComponent;
25
+ type UpdateViewOptionsModal<T extends object, IdType extends Primitive> = InstanceType<typeof UpdateViewOptionsModal<T, IdType>>;
26
+ export default UpdateViewOptionsModal;
@@ -0,0 +1,21 @@
1
+ import type { DatatableView } from '../Types/Persistence/DatatableView.js';
2
+ /**
3
+ * Props for `UpdateViewOptionsModal` - the FPM 403 round-6 dialog
4
+ * opened from a saved view card's "Update Options" button. The modal
5
+ * lets the user rename the view and toggle its sharing visibility
6
+ * (`'private'` / `'public'`); Save patches both fields in one call
7
+ * through `TableContext.updateViewOptions`.
8
+ */
9
+ export interface UpdateViewOptionsModalProps {
10
+ /** The saved view being edited. Its current `name` / `visibility`
11
+ * pre-populate the form; `id` + `kind` are forwarded to the
12
+ * update call. */
13
+ view: DatatableView;
14
+ /** Called when the modal should close - both on Cancel and after
15
+ * a successful Save. */
16
+ onclose: () => void;
17
+ /** Called after a successful Save, before `onclose`. The host
18
+ * wires this to a saved-views refresh so the My-* card label
19
+ * reflects the new name. Optional - omit to skip the refresh. */
20
+ onsaved?: () => void | Promise<void>;
21
+ }
@@ -13,7 +13,7 @@ import type { VisibilityState } from '../Columns/VisibilityState.js';
13
13
  import type { CellCoordinates } from '../Coordinates/CellCoordinates.js';
14
14
  import type { VisualColumnIndex } from '../Coordinates/ColumnIndices.js';
15
15
  import type { RemoteTableLayoutAdapter } from '../Persistence/RemoteTableLayoutAdapter.js';
16
- import type { DatatableView } from '../Persistence/DatatableView.js';
16
+ import type { DatatableView, DatatableViewVisibility } from '../Persistence/DatatableView.js';
17
17
  import type { DatatableViewKind } from '../Persistence/DatatableViewKind.js';
18
18
  import type { IDataRepository } from '../DataRepository/IDataRepository.js';
19
19
  import { SortingState } from './SortingState.svelte.js';
@@ -943,6 +943,54 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
943
943
  * disabled. An adapter rejection is routed to `devCatch`.
944
944
  */
945
945
  readonly saveActiveView: (kind: DatatableViewKind) => Promise<void>;
946
+ /**
947
+ * Save the live state of the table INTO an arbitrary saved view,
948
+ * overwriting that view's stored body. Unlike `saveActiveView` -
949
+ * which only works when the target IS the kind's active view -
950
+ * this accepts any saved view: a "Save Layout" / "Save Filter"
951
+ * button on a card can overwrite the saved view with the user's
952
+ * current tweaks regardless of what the active view is.
953
+ *
954
+ * Flow:
955
+ *
956
+ * 1. capture the live layout / filter (chosen by `view.kind`);
957
+ * 2. `await remoteLayouts.updateView({ id, body })`;
958
+ * 3. swap the matching entry in that kind's saved-views list with
959
+ * the authoritative server return;
960
+ * 4. if the just-saved view IS the kind's currently active view,
961
+ * repoint that kind's baseline at the new body and clear the
962
+ * dirty flag - the dropdown immediately reads "<name>" instead
963
+ * of "<name> (unsaved)".
964
+ *
965
+ * No-op (dev-logged) when remote saved views are disabled. Adapter
966
+ * rejections are re-thrown so the caller can surface them in the
967
+ * Table Configuration modal's inline error banner.
968
+ */
969
+ readonly saveLiveStateIntoView: (view: DatatableView) => Promise<void>;
970
+ /**
971
+ * Update a saved view's options - currently its `name` and
972
+ * `visibility`. The view's body is left untouched (use
973
+ * `saveLiveStateIntoView` or `saveActiveView` for that). Driven by
974
+ * the "Update Options" modal on each My-* card.
975
+ *
976
+ * Flow:
977
+ *
978
+ * 1. `await remoteLayouts.updateView({ id, name?, visibility? })`;
979
+ * 2. swap the matching entry in that kind's saved-views list with
980
+ * the authoritative server return so the new name / visibility
981
+ * are reflected in every reactive surface;
982
+ * 3. if the renamed view IS the kind's currently active view, push
983
+ * the new name onto the active-view ref so the quick-switch
984
+ * dropdown and "active view" label update immediately.
985
+ *
986
+ * No-op (dev-logged) when remote saved views are disabled. Adapter
987
+ * rejections are re-thrown so the caller can surface them in the
988
+ * modal.
989
+ */
990
+ readonly updateViewOptions: (view: DatatableView, options: {
991
+ name?: string;
992
+ visibility?: DatatableViewVisibility;
993
+ }) => Promise<void>;
946
994
  /**
947
995
  * Fetch the saved views for this datatable through the injected
948
996
  * `remoteLayouts` adapter and publish them into `_viewState`, split by
@@ -2191,10 +2191,33 @@ export class TableContext {
2191
2191
  // plain strings - look up cleanly against the live column set.
2192
2192
  // eslint-disable-next-line svelte/prefer-svelte-reactivity
2193
2193
  const liveColumnIds = new Set(this.columnDefs.map((def) => def.id));
2194
+ // 1. Clear the current filter VALUES. Without this, switching
2195
+ // saved filter views would only ADD entries from the new view
2196
+ // on top of whatever the old view set - any column the old
2197
+ // view filtered but the new one does not would keep its old
2198
+ // value and `FilterInput` would still show it.
2194
2199
  if (typeof repo.resetFilters === 'function') {
2195
2200
  repo.resetFilters();
2196
2201
  }
2197
- // Modes first: a value-bearing mode must be set before its value.
2202
+ // 2. Clear the current filter MODES the same way. `resetFilters`
2203
+ // deliberately keeps modes (so a "Clear filters" toolbar
2204
+ // action retains operator selections); but applying a saved
2205
+ // view is a *replace*, so a stale mode (e.g. the old view's
2206
+ // "Empty" operator) leaking into the new view would leave
2207
+ // `FilterInput`'s mode dropdown out of sync with the value it
2208
+ // renders.
2209
+ if (typeof repo.setFilterMode === 'function') {
2210
+ for (const id of [...repo.filterModes.keys()]) {
2211
+ repo.setFilterMode(id, undefined);
2212
+ }
2213
+ }
2214
+ // 3. Clear the QuerySelect typeahead-echo cache so the user
2215
+ // doesn't see a stale label hanging in a `QuerySelect` whose
2216
+ // underlying value just got reset. `_queryValues` is keyed by
2217
+ // `def.id`, the same key space `FilterInput` reads from.
2218
+ this._queryValues.clear();
2219
+ // 4. Apply the saved modes (before values so a value-bearing
2220
+ // mode like "Empty" sees its mode already set).
2198
2221
  if (typeof repo.setFilterMode === 'function') {
2199
2222
  for (const [id, mode] of Object.entries(filter.modes)) {
2200
2223
  // Drop stale column ids and unrecognised mode strings.
@@ -2203,6 +2226,7 @@ export class TableContext {
2203
2226
  }
2204
2227
  }
2205
2228
  }
2229
+ // 5. Apply the saved values.
2206
2230
  if (typeof repo.filterBy === 'function') {
2207
2231
  for (const entry of filter.filters) {
2208
2232
  if (liveColumnIds.has(entry.id)) {
@@ -2498,6 +2522,123 @@ export class TableContext {
2498
2522
  devCatch(e);
2499
2523
  }
2500
2524
  };
2525
+ /**
2526
+ * Save the live state of the table INTO an arbitrary saved view,
2527
+ * overwriting that view's stored body. Unlike `saveActiveView` -
2528
+ * which only works when the target IS the kind's active view -
2529
+ * this accepts any saved view: a "Save Layout" / "Save Filter"
2530
+ * button on a card can overwrite the saved view with the user's
2531
+ * current tweaks regardless of what the active view is.
2532
+ *
2533
+ * Flow:
2534
+ *
2535
+ * 1. capture the live layout / filter (chosen by `view.kind`);
2536
+ * 2. `await remoteLayouts.updateView({ id, body })`;
2537
+ * 3. swap the matching entry in that kind's saved-views list with
2538
+ * the authoritative server return;
2539
+ * 4. if the just-saved view IS the kind's currently active view,
2540
+ * repoint that kind's baseline at the new body and clear the
2541
+ * dirty flag - the dropdown immediately reads "<name>" instead
2542
+ * of "<name> (unsaved)".
2543
+ *
2544
+ * No-op (dev-logged) when remote saved views are disabled. Adapter
2545
+ * rejections are re-thrown so the caller can surface them in the
2546
+ * Table Configuration modal's inline error banner.
2547
+ */
2548
+ saveLiveStateIntoView = async (view) => {
2549
+ const adapter = this.remoteLayouts;
2550
+ if (!adapter) {
2551
+ if (dev) {
2552
+ console.warn('saveLiveStateIntoView: no adapter - cannot save remotely');
2553
+ }
2554
+ return;
2555
+ }
2556
+ const body = view.kind === 'filter'
2557
+ ? { kind: 'filter', filter: this._captureFilter() }
2558
+ : { kind: 'layout', layout: this._captureLayout() };
2559
+ const request = {
2560
+ id: view.id,
2561
+ body,
2562
+ };
2563
+ const updated = await adapter.updateView(request);
2564
+ // Swap the matching entry in the saved-views list so the picker
2565
+ // + modal reflect the new body without an extra refresh round-trip.
2566
+ this._viewState.setSavedViews(view.kind, this._viewState.savedViews(view.kind).map((v) => (v.id === updated.id ? updated : v)));
2567
+ // If the saved-into view IS the active view for its kind,
2568
+ // repoint the baseline so dirty re-compares against the new
2569
+ // body and clears.
2570
+ const active = this._viewState.activeView(view.kind);
2571
+ if (active.kind === 'saved' && active.id === updated.id) {
2572
+ this._viewState.selectSavedView(view.kind, { id: updated.id, name: updated.name });
2573
+ if (view.kind === 'filter') {
2574
+ this._filterBaseline =
2575
+ updated.body.kind === 'filter' ? updated.body.filter : this._captureFilter();
2576
+ this._persistActiveFilterView();
2577
+ }
2578
+ else {
2579
+ this._viewBaseline =
2580
+ updated.body.kind === 'layout' ? updated.body.layout : this._captureLayout();
2581
+ this._persistActiveView();
2582
+ }
2583
+ }
2584
+ };
2585
+ /**
2586
+ * Update a saved view's options - currently its `name` and
2587
+ * `visibility`. The view's body is left untouched (use
2588
+ * `saveLiveStateIntoView` or `saveActiveView` for that). Driven by
2589
+ * the "Update Options" modal on each My-* card.
2590
+ *
2591
+ * Flow:
2592
+ *
2593
+ * 1. `await remoteLayouts.updateView({ id, name?, visibility? })`;
2594
+ * 2. swap the matching entry in that kind's saved-views list with
2595
+ * the authoritative server return so the new name / visibility
2596
+ * are reflected in every reactive surface;
2597
+ * 3. if the renamed view IS the kind's currently active view, push
2598
+ * the new name onto the active-view ref so the quick-switch
2599
+ * dropdown and "active view" label update immediately.
2600
+ *
2601
+ * No-op (dev-logged) when remote saved views are disabled. Adapter
2602
+ * rejections are re-thrown so the caller can surface them in the
2603
+ * modal.
2604
+ */
2605
+ updateViewOptions = async (view, options) => {
2606
+ const adapter = this.remoteLayouts;
2607
+ if (!adapter) {
2608
+ if (dev) {
2609
+ console.warn('updateViewOptions: no adapter - cannot update remotely');
2610
+ }
2611
+ return;
2612
+ }
2613
+ // Skip the network round-trip entirely when nothing actually
2614
+ // changed. Avoids a no-op PATCH that would still bump the
2615
+ // updated_at timestamp on the backend.
2616
+ const nameChanged = options.name !== undefined && options.name !== view.name;
2617
+ const visibilityChanged = options.visibility !== undefined && options.visibility !== view.visibility;
2618
+ if (!nameChanged && !visibilityChanged) {
2619
+ return;
2620
+ }
2621
+ const request = {
2622
+ id: view.id,
2623
+ ...(nameChanged ? { name: options.name } : {}),
2624
+ ...(visibilityChanged ? { visibility: options.visibility } : {}),
2625
+ };
2626
+ const updated = await adapter.updateView(request);
2627
+ this._viewState.setSavedViews(view.kind, this._viewState.savedViews(view.kind).map((v) => (v.id === updated.id ? updated : v)));
2628
+ // If the just-edited view is the active one for its kind, push
2629
+ // the new name onto the active-view ref so the dropdown label
2630
+ // reflects the rename without waiting for a refresh.
2631
+ const active = this._viewState.activeView(view.kind);
2632
+ if (active.kind === 'saved' && active.id === updated.id) {
2633
+ this._viewState.selectSavedView(view.kind, { id: updated.id, name: updated.name });
2634
+ if (view.kind === 'filter') {
2635
+ this._persistActiveFilterView();
2636
+ }
2637
+ else {
2638
+ this._persistActiveView();
2639
+ }
2640
+ }
2641
+ };
2501
2642
  /**
2502
2643
  * Fetch the saved views for this datatable through the injected
2503
2644
  * `remoteLayouts` adapter and publish them into `_viewState`, split by
@@ -65,7 +65,7 @@ export declare class BuiltInRemoteRepository<TRow extends object> extends TableD
65
65
  private _debounceTimer;
66
66
  private _quickFilterFocusCount;
67
67
  constructor(_options: BuiltInRemoteRepositoryOptions<TRow>);
68
- readonly setFilterMode: (id: string, mode: FilterMode) => void;
68
+ readonly setFilterMode: (id: string, mode: FilterMode | undefined) => void;
69
69
  readonly filterBy: (id: string, value: unknown) => void;
70
70
  readonly resetFilters: () => void;
71
71
  /** Jumps to the given 1-based page number, clamped at >= 1. */
@@ -121,7 +121,7 @@ export declare class CustomRemoteRepository<TRow extends object> extends TableDa
121
121
  get filtering(): ColumnFiltersState;
122
122
  private _abortController;
123
123
  constructor(tableName: string, getUserId: () => string | undefined, _fetch: CustomRowSource<TRow>['fetch']);
124
- readonly setFilterMode: (id: string, mode: FilterMode) => void;
124
+ readonly setFilterMode: (id: string, mode: FilterMode | undefined) => void;
125
125
  readonly filterBy: (id: string, value: unknown) => void;
126
126
  readonly resetFilters: () => void;
127
127
  /** Jumps to the given 1-based page number, clamped at >= 1. */
@@ -91,7 +91,16 @@ export class BuiltInRemoteRepository extends TableDataRepository {
91
91
  }
92
92
  // ── IRemoteDataRepository surface ────────────────────────────────
93
93
  setFilterMode = (id, mode) => {
94
- this._filterModes.set(id, mode);
94
+ if (mode === undefined) {
95
+ // Drop the entry entirely so the column falls back to its
96
+ // kind's default mode on the next render. Used by
97
+ // `TableContext._applyFilter` to clear stale modes when a
98
+ // saved filter view is applied.
99
+ this._filterModes.delete(id);
100
+ }
101
+ else {
102
+ this._filterModes.set(id, mode);
103
+ }
95
104
  this.scheduleReload();
96
105
  };
97
106
  filterBy = (id, value) => {
@@ -366,7 +375,16 @@ export class CustomRemoteRepository extends TableDataRepository {
366
375
  this._fetch = _fetch;
367
376
  }
368
377
  setFilterMode = (id, mode) => {
369
- this._filterModes.set(id, mode);
378
+ if (mode === undefined) {
379
+ // Drop the entry entirely so the column falls back to its
380
+ // kind's default mode on the next render. Used by
381
+ // `TableContext._applyFilter` when restoring a saved filter
382
+ // view to clear modes left over from a prior view.
383
+ this._filterModes.delete(id);
384
+ }
385
+ else {
386
+ this._filterModes.set(id, mode);
387
+ }
370
388
  void this.reload();
371
389
  };
372
390
  filterBy = (id, value) => {
@@ -37,7 +37,15 @@ export interface IDataRepository<T extends object> {
37
37
  sorting: SortingState;
38
38
  filtering: ColumnFiltersState;
39
39
  filterModes: SvelteMap<string, FilterMode>;
40
- setFilterMode: (id: string, mode: FilterMode) => void;
40
+ /**
41
+ * Set the filter mode for column `id`. Pass `undefined` to delete the
42
+ * entry entirely (the column then falls back to its kind's default
43
+ * mode when the next `FilterInput` / `QuickSearchCell` renders).
44
+ * Used by `_applyFilter` when restoring a saved filter view, so any
45
+ * mode from a previously-applied view is dropped before the new
46
+ * one's modes are applied.
47
+ */
48
+ setFilterMode: (id: string, mode: FilterMode | undefined) => void;
41
49
  filterBy: (id: string, value: unknown) => void;
42
50
  dateFilter?: FilterFn<T, DateFilterValueType>;
43
51
  numberFilter?: FilterFn<T, NumberFilterValueType>;
@@ -21,7 +21,7 @@ export interface ILocalDataRepository<T extends object> extends IDataRepository<
21
21
  prevPage: () => void;
22
22
  filtering: ColumnFiltersState;
23
23
  filterModes: SvelteMap<string, FilterMode>;
24
- setFilterMode: (id: string, mode: FilterMode) => void;
24
+ setFilterMode: (id: string, mode: FilterMode | undefined) => void;
25
25
  filterBy: (id: string, value: unknown) => void;
26
26
  dateFilter: FilterFn<T, DateFilterValueType>;
27
27
  numberFilter: FilterFn<T, NumberFilterValueType>;
@@ -26,7 +26,7 @@ export interface IRemoteDataRepository<T extends object> extends IDataRepository
26
26
  prevPage: () => void;
27
27
  filtering: ColumnFiltersState;
28
28
  filterModes: SvelteMap<string, FilterMode>;
29
- setFilterMode: (id: string, mode: FilterMode) => void;
29
+ setFilterMode: (id: string, mode: FilterMode | undefined) => void;
30
30
  filterBy: (id: string, value: unknown) => void;
31
31
  resetFilters: () => void;
32
32
  lastPage: number;
@@ -42,7 +42,7 @@ export declare abstract class TableDataRepository<T extends object, out FilterVa
42
42
  */
43
43
  abstract setPageSize: (size: number | string) => void;
44
44
  abstract filtering: ColumnFiltersState<FilterValueType>;
45
- abstract setFilterMode: (id: string, mode: FilterMode) => void;
45
+ abstract setFilterMode: (id: string, mode: FilterMode | undefined) => void;
46
46
  abstract filterBy: (id: string, value: unknown) => void;
47
47
  abstract resetFilters: () => void;
48
48
  abstract setPage: (page: number) => void;
@@ -1,5 +1,15 @@
1
1
  import type { DatatableViewBody } from './DatatableViewEnvelope.js';
2
2
  import type { DatatableViewKind } from './DatatableViewKind.js';
3
+ /**
4
+ * Sharing visibility for a saved view. `'private'` is the default for
5
+ * v1: only the owner and explicit subscribers can see and apply it.
6
+ * `'public'` exposes the view to every user who has access to the
7
+ * datatable - they see it in the "Subscribe to Additional…" picker and
8
+ * can subscribe to it. scoria never interprets this token beyond
9
+ * passing it back through the adapter; the backend enforces the
10
+ * visibility semantics.
11
+ */
12
+ export type DatatableViewVisibility = 'private' | 'public';
3
13
  /**
4
14
  * A saved datatable view - a named, persisted column layout or filter
5
15
  * that round-trips through a `RemoteTableLayoutAdapter`. This is the
@@ -47,6 +57,15 @@ export interface DatatableView {
47
57
  * UI - scoria falls back to `userId` when it is absent.
48
58
  */
49
59
  subscribers?: ReadonlyArray<DatatableViewSubscriber>;
60
+ /**
61
+ * Sharing visibility. `'private'` (default) is only visible to the
62
+ * owner and explicit subscribers; `'public'` is offered to every
63
+ * user with datatable access through the "Subscribe to Additional…"
64
+ * picker. Optional on read for backwards compatibility - hosts that
65
+ * have not surfaced this column yet can omit it, and scoria falls
66
+ * back to private semantics in the UI.
67
+ */
68
+ visibility?: DatatableViewVisibility;
50
69
  }
51
70
  /**
52
71
  * One entry in a `DatatableView.subscribers` roster: an opaque user
@@ -81,6 +100,12 @@ export interface UpdateDatatableViewRequest {
81
100
  name?: string;
82
101
  /** New payload, if the layout/filter is being replaced. */
83
102
  body?: DatatableViewBody;
103
+ /**
104
+ * New sharing visibility, if it is being changed. `'private'` keeps
105
+ * the view scoped to its owner + subscribers; `'public'` exposes it
106
+ * to every user with datatable access.
107
+ */
108
+ visibility?: DatatableViewVisibility;
84
109
  }
85
110
  /**
86
111
  * A subscriber relationship for a shared `DatatableView`. v1 sharing is
package/dist/index.d.ts CHANGED
@@ -114,7 +114,7 @@ export { type JSONTableLayout, TABLE_LAYOUT_SCHEMA_VERSION, isJSONTableLayout, }
114
114
  export { type JSONTableFilter, TABLE_FILTER_SCHEMA_VERSION, isJSONTableFilter, } from './Components/Table/Types/Persistence/JSONTableFilter.js';
115
115
  export { type DatatableViewKind } from './Components/Table/Types/Persistence/DatatableViewKind.js';
116
116
  export { type DatatableViewBody } from './Components/Table/Types/Persistence/DatatableViewEnvelope.js';
117
- export type { CreateDatatableViewRequest, DatatableView, DatatableViewSubscriber, DatatableViewSubscription, UpdateDatatableViewRequest, } from './Components/Table/Types/Persistence/DatatableView.js';
117
+ export type { CreateDatatableViewRequest, DatatableView, DatatableViewSubscriber, DatatableViewSubscription, DatatableViewVisibility, UpdateDatatableViewRequest, } from './Components/Table/Types/Persistence/DatatableView.js';
118
118
  export { type RemoteTableLayoutAdapter, type DatatableViewUser, } from './Components/Table/Types/Persistence/RemoteTableLayoutAdapter.js';
119
119
  export { type ActiveViewRef, type JSONActiveView, TABLE_ACTIVE_VIEW_SCHEMA_VERSION, isJSONActiveView, } from './Components/Table/Types/Persistence/JSONActiveView.js';
120
120
  export { type ValidationFn } from './Components/Table/Types/Columns/Definitions/ValidationFn.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.38.10",
4
+ "version": "0.38.11",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },