@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.
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte +71 -0
- package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +225 -0
- package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte.d.ts +26 -0
- package/dist/Components/Table/Misc/UpdateViewOptionsModalProps.d.ts +21 -0
- package/dist/Components/Table/Misc/UpdateViewOptionsModalProps.js +1 -0
- package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +49 -1
- package/dist/Components/Table/Types/Context/TableContext.svelte.js +142 -1
- package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.d.ts +2 -2
- package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.js +20 -2
- package/dist/Components/Table/Types/DataRepository/IDataRepository.d.ts +9 -1
- package/dist/Components/Table/Types/DataRepository/ILocalDataRepository.d.ts +1 -1
- package/dist/Components/Table/Types/DataRepository/IRemoteDataRepository.d.ts +1 -1
- package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.d.ts +1 -1
- package/dist/Components/Table/Types/Persistence/DatatableView.d.ts +25 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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';
|