@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.
- package/dist/Components/Table/ARCHITECTURE.md +22 -22
- package/dist/Components/Table/Misc/ColumnPanel.svelte +1 -61
- package/dist/Components/Table/Misc/ColumnPanel.svelte.d.ts +2 -24
- package/dist/Components/Table/Misc/ColumnPanelModal.svelte +3 -92
- package/dist/Components/Table/Misc/ColumnPanelModal.svelte.d.ts +2 -24
- package/dist/Components/Table/Misc/ColumnPanelModalProps.d.ts +2 -2
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte +644 -0
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte.d.ts +26 -0
- package/dist/Components/Table/Misc/TableConfigurationModalProps.d.ts +13 -0
- package/dist/Components/Table/Misc/TableHorizontalBar.svelte +27 -0
- package/dist/Components/Table/Misc/TableSidebar.svelte +45 -2
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +175 -0
- package/dist/Components/Table/Misc/TableViewDropdown.svelte.d.ts +25 -0
- package/dist/Components/Table/SubApis.svelte.js +0 -41
- package/dist/Components/Table/Types/Columns/JSONTableLayout.d.ts +5 -5
- package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +5 -5
- package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +5 -5
- package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +106 -147
- package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +156 -203
- package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +323 -44
- package/dist/Components/Table/Types/Context/TableContext.svelte.js +687 -187
- package/dist/Components/Table/Types/Context/TableInitOptions.d.ts +9 -0
- package/dist/Components/Table/Types/Context/ViewState.svelte.d.ts +152 -0
- package/dist/Components/Table/Types/Context/ViewState.svelte.js +223 -0
- package/dist/Components/Table/Types/ExpandedPanel.d.ts +9 -5
- package/dist/Components/Table/Types/ExpandedPanel.js +9 -5
- package/dist/Components/Table/Types/Persistence/DatatableView.d.ts +95 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.d.ts +21 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.js +1 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewKind.d.ts +10 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewKind.js +1 -0
- package/dist/Components/Table/Types/Persistence/JSONActiveView.d.ts +58 -0
- package/dist/Components/Table/Types/Persistence/JSONActiveView.js +41 -0
- package/dist/Components/Table/Types/Persistence/JSONTableFilter.d.ts +53 -0
- package/dist/Components/Table/Types/Persistence/JSONTableFilter.js +42 -0
- package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.d.ts +47 -0
- package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.js +1 -0
- package/dist/Components/Table/Types/Public/CreateTableOptions.d.ts +10 -0
- package/dist/Components/Table/Types/Public/TablePersistence.d.ts +1 -4
- package/dist/Components/Table/Types/Public/TableSubApis.d.ts +0 -18
- package/dist/Components/Table/Types/Public/index.d.ts +1 -1
- package/dist/Components/Table/createTable.svelte.js +2 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +9 -0
- package/package.json +1 -1
- package/dist/Components/Table/Types/Columns/Definitions/ColumnDefSet.d.ts +0 -20
- package/dist/Components/Table/Types/Columns/Definitions/JSONColumnDefSet.d.ts +0 -25
- /package/dist/Components/Table/{Types/Columns/Definitions/ColumnDefSet.js → Misc/TableConfigurationModalProps.js} +0 -0
- /package/dist/Components/Table/Types/{Columns/Definitions/JSONColumnDefSet.js → Persistence/DatatableView.js} +0 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script
|
|
4
|
+
lang="ts"
|
|
5
|
+
generics="T extends object, IdType extends Primitive"
|
|
6
|
+
>
|
|
7
|
+
import Button from '../../Button.svelte';
|
|
8
|
+
import DesktopModal from '../../DesktopModal.svelte';
|
|
9
|
+
import Icon from '../../Icon.svelte';
|
|
10
|
+
import TextInput from '../../TextInput.svelte';
|
|
11
|
+
import { Size } from '../../../Types/Internal/Size.js';
|
|
12
|
+
import { Variant } from '../../../Types/Internal/Variant.js';
|
|
13
|
+
import { getContext } from 'svelte';
|
|
14
|
+
import { TableContext } from '../Types/Context/TableContext.svelte.js';
|
|
15
|
+
import type { Primitive } from '../Types/Context/TableInitOptions.js';
|
|
16
|
+
import type { DatatableView } from '../Types/Persistence/DatatableView.js';
|
|
17
|
+
import type { DatatableViewKind } from '../Types/Persistence/DatatableViewKind.js';
|
|
18
|
+
import type { TableConfigurationModalProps } from './TableConfigurationModalProps.js';
|
|
19
|
+
|
|
20
|
+
const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
|
|
21
|
+
|
|
22
|
+
const { onclose }: TableConfigurationModalProps = $props();
|
|
23
|
+
|
|
24
|
+
// ── Left-nav section + right-pane tab selection ──────────────────────
|
|
25
|
+
// `section` picks the kind (layout vs filter); `tab` picks which of the
|
|
26
|
+
// three horizontal panes is visible for that section. Both are plain
|
|
27
|
+
// `$state` since they are pure local UI selection.
|
|
28
|
+
let section = $state<DatatableViewKind>('layout');
|
|
29
|
+
type ConfigTab = 'mine' | 'shared' | 'save';
|
|
30
|
+
let tab = $state<ConfigTab>('mine');
|
|
31
|
+
|
|
32
|
+
// Name field for the "Save a new ..." tab.
|
|
33
|
+
let newViewName = $state('');
|
|
34
|
+
|
|
35
|
+
// Public-views discovery state for the "Subscribe to Additional" flow.
|
|
36
|
+
// `null` = not yet loaded; an array = the last successful fetch.
|
|
37
|
+
let publicViews = $state<ReadonlyArray<DatatableView> | null>(null);
|
|
38
|
+
let loadingPublicViews = $state(false);
|
|
39
|
+
|
|
40
|
+
// User-visible error banner. Every adapter call is wrapped so a
|
|
41
|
+
// rejection lands here instead of throwing into the UI. scoria cannot
|
|
42
|
+
// assume a host-provided toast context exists, so the modal surfaces
|
|
43
|
+
// errors inline in its own body.
|
|
44
|
+
let errorMessage = $state<string | null>(null);
|
|
45
|
+
|
|
46
|
+
// Whether remote saved views are available at all. A table created
|
|
47
|
+
// without `datatableUuid` / `remoteLayouts` opts out of remote views;
|
|
48
|
+
// the modal then renders a graceful "unavailable" empty state and
|
|
49
|
+
// makes zero adapter calls.
|
|
50
|
+
const remoteAvailable = $derived(
|
|
51
|
+
tableContext.datatableUuid !== undefined && tableContext.remoteLayouts !== undefined
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// The opaque current-user id, used to scope "mine" vs "shared" and to
|
|
55
|
+
// drive subscribe / unsubscribe. `undefined` when the host supplied
|
|
56
|
+
// `() => undefined` - sharing controls then degrade gracefully.
|
|
57
|
+
const currentUserId = $derived(tableContext.getUserId());
|
|
58
|
+
|
|
59
|
+
// All saved views of the active section, split by ownership. A view the
|
|
60
|
+
// current user owns is "mine"; anything else they can see is "shared"
|
|
61
|
+
// (a subscription). When no user id is available every view falls into
|
|
62
|
+
// "mine" so the user can still manage what they have.
|
|
63
|
+
const sectionViews = $derived(tableContext._viewState.savedViews(section));
|
|
64
|
+
const myViews = $derived(
|
|
65
|
+
sectionViews.filter((view) => currentUserId === undefined || view.ownerUserId === currentUserId)
|
|
66
|
+
);
|
|
67
|
+
const sharedViews = $derived(
|
|
68
|
+
sectionViews.filter((view) => currentUserId !== undefined && view.ownerUserId !== currentUserId)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Human labels for the active section, so the three tabs and headings
|
|
72
|
+
// read "My Layouts" / "My Filters" etc. without branching everywhere.
|
|
73
|
+
const sectionNoun = $derived(section === 'filter' ? 'Filter' : 'Layout');
|
|
74
|
+
const sectionNounPlural = $derived(section === 'filter' ? 'Filters' : 'Layouts');
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run an adapter-backed action, clearing any prior error first and
|
|
78
|
+
* routing a rejection to the inline error banner. Keeps every async
|
|
79
|
+
* branch below a one-liner and guarantees no rejection escapes to the
|
|
80
|
+
* UI (the modal never throws).
|
|
81
|
+
*/
|
|
82
|
+
async function guard(action: () => Promise<void>, failureMessage: string): Promise<void> {
|
|
83
|
+
errorMessage = null;
|
|
84
|
+
try {
|
|
85
|
+
await action();
|
|
86
|
+
} catch {
|
|
87
|
+
errorMessage = failureMessage;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Apply a saved view to the live table. Pure local call - no await. */
|
|
92
|
+
function applyView(view: DatatableView): void {
|
|
93
|
+
errorMessage = null;
|
|
94
|
+
tableContext.applyDatatableView(view);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Delete a view the current user owns, then refresh the lists. */
|
|
98
|
+
function deleteView(view: DatatableView): void {
|
|
99
|
+
const adapter = tableContext.remoteLayouts;
|
|
100
|
+
if (!adapter) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
void guard(async () => {
|
|
104
|
+
await adapter.deleteView(view.id);
|
|
105
|
+
await tableContext.refreshSavedViews();
|
|
106
|
+
}, `Could not delete "${view.name}". Please try again.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Save the live state of the active section as a brand-new view. */
|
|
110
|
+
function saveNewView(): void {
|
|
111
|
+
const trimmed = newViewName.trim();
|
|
112
|
+
if (!trimmed) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
void guard(async () => {
|
|
116
|
+
await tableContext.saveCurrentAsNewView(trimmed, section);
|
|
117
|
+
newViewName = '';
|
|
118
|
+
}, `Could not save the new ${sectionNoun.toLowerCase()}. Please try again.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Remove the current user as a subscriber of a shared view. */
|
|
122
|
+
function unsubscribe(view: DatatableView): void {
|
|
123
|
+
const adapter = tableContext.remoteLayouts;
|
|
124
|
+
if (!adapter || currentUserId === undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
void guard(async () => {
|
|
128
|
+
await adapter.removeSubscriber(view.id, currentUserId);
|
|
129
|
+
await tableContext.refreshSavedViews();
|
|
130
|
+
}, `Could not unsubscribe from "${view.name}". Please try again.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Remove a named subscriber from a view the current user owns. */
|
|
134
|
+
function removeSubscriber(view: DatatableView, userId: string): void {
|
|
135
|
+
const adapter = tableContext.remoteLayouts;
|
|
136
|
+
if (!adapter) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
void guard(async () => {
|
|
140
|
+
await adapter.removeSubscriber(view.id, userId);
|
|
141
|
+
await tableContext.refreshSavedViews();
|
|
142
|
+
}, 'Could not remove the subscriber. Please try again.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Fetch the public views the user could subscribe to. */
|
|
146
|
+
function loadPublicViews(): void {
|
|
147
|
+
const adapter = tableContext.remoteLayouts;
|
|
148
|
+
const datatableUuid = tableContext.datatableUuid;
|
|
149
|
+
if (!adapter || datatableUuid === undefined) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
loadingPublicViews = true;
|
|
153
|
+
void guard(async () => {
|
|
154
|
+
const all = await adapter.listPublicViews(datatableUuid);
|
|
155
|
+
publicViews = all.filter((view) => view.kind === section);
|
|
156
|
+
}, 'Could not load views available to subscribe to. Please try again.').finally(() => {
|
|
157
|
+
loadingPublicViews = false;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Subscribe the current user to a public view, then refresh. */
|
|
162
|
+
function subscribe(view: DatatableView): void {
|
|
163
|
+
const adapter = tableContext.remoteLayouts;
|
|
164
|
+
if (!adapter || currentUserId === undefined) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
void guard(async () => {
|
|
168
|
+
await adapter.addSubscriber(view.id, currentUserId);
|
|
169
|
+
await tableContext.refreshSavedViews();
|
|
170
|
+
loadPublicViews();
|
|
171
|
+
}, `Could not subscribe to "${view.name}". Please try again.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Switch the active left-nav section, resetting the tab + scratch UI. */
|
|
175
|
+
function selectSection(next: DatatableViewKind): void {
|
|
176
|
+
section = next;
|
|
177
|
+
tab = 'mine';
|
|
178
|
+
newViewName = '';
|
|
179
|
+
publicViews = null;
|
|
180
|
+
errorMessage = null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Switch the active right-pane tab, resetting tab-local scratch UI. */
|
|
184
|
+
function selectTab(next: ConfigTab): void {
|
|
185
|
+
tab = next;
|
|
186
|
+
errorMessage = null;
|
|
187
|
+
if (next !== 'save') {
|
|
188
|
+
newViewName = '';
|
|
189
|
+
}
|
|
190
|
+
if (next !== 'shared') {
|
|
191
|
+
publicViews = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<DesktopModal
|
|
197
|
+
headerLabel="Table Configuration"
|
|
198
|
+
headerSubtitle={tableContext.datatableUuid
|
|
199
|
+
? `Table Identifier: ${tableContext.datatableUuid}`
|
|
200
|
+
: undefined}
|
|
201
|
+
onclose={() => onclose?.()}
|
|
202
|
+
buttons={[{ label: 'Close', callback: () => onclose?.() }]}
|
|
203
|
+
>
|
|
204
|
+
{#if !remoteAvailable}
|
|
205
|
+
<div class="empty-state">
|
|
206
|
+
<Icon
|
|
207
|
+
name="circle-information"
|
|
208
|
+
size={Size.Large}
|
|
209
|
+
/>
|
|
210
|
+
<p>Saved layouts and filters are unavailable for this table.</p>
|
|
211
|
+
</div>
|
|
212
|
+
{:else}
|
|
213
|
+
<div class="config">
|
|
214
|
+
<!-- Left vertical nav: pick the kind being configured. -->
|
|
215
|
+
<nav class="side-nav">
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
class="nav-item"
|
|
219
|
+
class:active={section === 'layout'}
|
|
220
|
+
onclick={() => {
|
|
221
|
+
selectSection('layout');
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
Table Layouts
|
|
225
|
+
</button>
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
class="nav-item"
|
|
229
|
+
class:active={section === 'filter'}
|
|
230
|
+
onclick={() => {
|
|
231
|
+
selectSection('filter');
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
Table Filters
|
|
235
|
+
</button>
|
|
236
|
+
</nav>
|
|
237
|
+
|
|
238
|
+
<div class="pane">
|
|
239
|
+
<!-- Three horizontal tabs for the selected section. -->
|
|
240
|
+
<div
|
|
241
|
+
class="tabs"
|
|
242
|
+
role="tablist"
|
|
243
|
+
>
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
role="tab"
|
|
247
|
+
aria-selected={tab === 'mine'}
|
|
248
|
+
class="tab"
|
|
249
|
+
class:active={tab === 'mine'}
|
|
250
|
+
onclick={() => {
|
|
251
|
+
selectTab('mine');
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
My {sectionNounPlural}
|
|
255
|
+
</button>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
role="tab"
|
|
259
|
+
aria-selected={tab === 'shared'}
|
|
260
|
+
class="tab"
|
|
261
|
+
class:active={tab === 'shared'}
|
|
262
|
+
onclick={() => {
|
|
263
|
+
selectTab('shared');
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<!-- DESIGN-REVIEW: the mockups were internally inconsistent
|
|
267
|
+
("Subscribed to Filters" on one, "Shared Filters" on
|
|
268
|
+
another). Resolved to "Shared" consistently for both
|
|
269
|
+
the layout and filter sections. -->
|
|
270
|
+
Shared {sectionNounPlural}
|
|
271
|
+
</button>
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
role="tab"
|
|
275
|
+
aria-selected={tab === 'save'}
|
|
276
|
+
class="tab"
|
|
277
|
+
class:active={tab === 'save'}
|
|
278
|
+
onclick={() => {
|
|
279
|
+
selectTab('save');
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
Save a new {sectionNoun}
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{#if errorMessage}
|
|
287
|
+
<div
|
|
288
|
+
class="error-banner"
|
|
289
|
+
role="alert"
|
|
290
|
+
>
|
|
291
|
+
<Icon
|
|
292
|
+
name="circle-exclamation"
|
|
293
|
+
size={Size.Small}
|
|
294
|
+
/>
|
|
295
|
+
<span>{errorMessage}</span>
|
|
296
|
+
</div>
|
|
297
|
+
{/if}
|
|
298
|
+
|
|
299
|
+
<div class="tab-body">
|
|
300
|
+
{#if tab === 'mine'}
|
|
301
|
+
{#if myViews.length === 0}
|
|
302
|
+
<p class="muted">
|
|
303
|
+
You have not saved any {sectionNounPlural.toLowerCase()} yet. Use the "Save a new {sectionNoun}"
|
|
304
|
+
tab to create one.
|
|
305
|
+
</p>
|
|
306
|
+
{:else}
|
|
307
|
+
<ul class="card-list">
|
|
308
|
+
{#each myViews as view (view.id)}
|
|
309
|
+
<li class="view-card">
|
|
310
|
+
<div class="view-card-head">
|
|
311
|
+
<span class="view-name">{view.name}</span>
|
|
312
|
+
<div class="view-actions">
|
|
313
|
+
<Button
|
|
314
|
+
variant={Variant.Secondary}
|
|
315
|
+
size={Size.MediumSmall}
|
|
316
|
+
nowrap
|
|
317
|
+
onclick={() => {
|
|
318
|
+
applyView(view);
|
|
319
|
+
}}>Apply</Button
|
|
320
|
+
>
|
|
321
|
+
<Button
|
|
322
|
+
variant={Variant.Secondary}
|
|
323
|
+
size={Size.MediumSmall}
|
|
324
|
+
nowrap
|
|
325
|
+
iconLeft={{ name: 'external-link', size: Size.Small }}
|
|
326
|
+
onclick={() => {
|
|
327
|
+
selectTab('shared');
|
|
328
|
+
}}>Share</Button
|
|
329
|
+
>
|
|
330
|
+
<Button
|
|
331
|
+
variant={Variant.Secondary}
|
|
332
|
+
size={Size.MediumSmall}
|
|
333
|
+
nowrap
|
|
334
|
+
iconLeft={{ name: 'circle-cross', size: Size.Small }}
|
|
335
|
+
onclick={() => {
|
|
336
|
+
deleteView(view);
|
|
337
|
+
}}>Delete</Button
|
|
338
|
+
>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
{#if view.subscribers && view.subscribers.length > 0}
|
|
342
|
+
<ul class="subscriber-list">
|
|
343
|
+
{#each view.subscribers as subscriber (subscriber.userId)}
|
|
344
|
+
<li class="subscriber">
|
|
345
|
+
<Icon
|
|
346
|
+
name="user-3"
|
|
347
|
+
size={Size.Small}
|
|
348
|
+
/>
|
|
349
|
+
<span>{subscriber.displayName ?? subscriber.userId}</span>
|
|
350
|
+
<Button
|
|
351
|
+
variant={Variant.Secondary}
|
|
352
|
+
size={Size.Tiny}
|
|
353
|
+
nowrap
|
|
354
|
+
onclick={() => {
|
|
355
|
+
removeSubscriber(view, subscriber.userId);
|
|
356
|
+
}}>Remove</Button
|
|
357
|
+
>
|
|
358
|
+
</li>
|
|
359
|
+
{/each}
|
|
360
|
+
</ul>
|
|
361
|
+
{/if}
|
|
362
|
+
</li>
|
|
363
|
+
{/each}
|
|
364
|
+
</ul>
|
|
365
|
+
{/if}
|
|
366
|
+
{:else if tab === 'shared'}
|
|
367
|
+
<div class="shared-head">
|
|
368
|
+
<Button
|
|
369
|
+
variant={Variant.Secondary}
|
|
370
|
+
size={Size.MediumSmall}
|
|
371
|
+
nowrap
|
|
372
|
+
disabled={loadingPublicViews}
|
|
373
|
+
iconLeft={{ name: 'circle-plus', size: Size.Small }}
|
|
374
|
+
onclick={loadPublicViews}>Subscribe to Additional...</Button
|
|
375
|
+
>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
{#if publicViews !== null}
|
|
379
|
+
{#if publicViews.length === 0}
|
|
380
|
+
<p class="muted">
|
|
381
|
+
There are no further {sectionNounPlural.toLowerCase()} available to subscribe to.
|
|
382
|
+
</p>
|
|
383
|
+
{:else}
|
|
384
|
+
<ul class="card-list">
|
|
385
|
+
{#each publicViews as view (view.id)}
|
|
386
|
+
<li class="view-card">
|
|
387
|
+
<div class="view-card-head">
|
|
388
|
+
<span class="view-name">{view.name}</span>
|
|
389
|
+
<div class="view-actions">
|
|
390
|
+
<Button
|
|
391
|
+
variant={Variant.Secondary}
|
|
392
|
+
size={Size.MediumSmall}
|
|
393
|
+
nowrap
|
|
394
|
+
disabled={currentUserId === undefined}
|
|
395
|
+
onclick={() => {
|
|
396
|
+
subscribe(view);
|
|
397
|
+
}}>Subscribe</Button
|
|
398
|
+
>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</li>
|
|
402
|
+
{/each}
|
|
403
|
+
</ul>
|
|
404
|
+
{/if}
|
|
405
|
+
{/if}
|
|
406
|
+
|
|
407
|
+
{#if sharedViews.length === 0}
|
|
408
|
+
<p class="muted">
|
|
409
|
+
No {sectionNounPlural.toLowerCase()} are currently shared with you.
|
|
410
|
+
</p>
|
|
411
|
+
{:else}
|
|
412
|
+
<ul class="card-list">
|
|
413
|
+
{#each sharedViews as view (view.id)}
|
|
414
|
+
<li class="view-card">
|
|
415
|
+
<div class="view-card-head">
|
|
416
|
+
<span class="view-name">{view.name}</span>
|
|
417
|
+
<div class="view-actions">
|
|
418
|
+
<Button
|
|
419
|
+
variant={Variant.Secondary}
|
|
420
|
+
size={Size.MediumSmall}
|
|
421
|
+
nowrap
|
|
422
|
+
onclick={() => {
|
|
423
|
+
applyView(view);
|
|
424
|
+
}}>Apply</Button
|
|
425
|
+
>
|
|
426
|
+
<Button
|
|
427
|
+
variant={Variant.Secondary}
|
|
428
|
+
size={Size.MediumSmall}
|
|
429
|
+
nowrap
|
|
430
|
+
disabled={currentUserId === undefined}
|
|
431
|
+
onclick={() => {
|
|
432
|
+
unsubscribe(view);
|
|
433
|
+
}}>Unsubscribe</Button
|
|
434
|
+
>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</li>
|
|
438
|
+
{/each}
|
|
439
|
+
</ul>
|
|
440
|
+
{/if}
|
|
441
|
+
{:else}
|
|
442
|
+
<div class="save-tab">
|
|
443
|
+
<p class="muted">
|
|
444
|
+
Save the table's current {sectionNoun.toLowerCase()} as a reusable named
|
|
445
|
+
{sectionNoun.toLowerCase()}.
|
|
446
|
+
</p>
|
|
447
|
+
<TextInput
|
|
448
|
+
placeholder={`${sectionNoun} name`}
|
|
449
|
+
bind:value={newViewName}
|
|
450
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
451
|
+
if (e.key === 'Enter') {
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
saveNewView();
|
|
454
|
+
}
|
|
455
|
+
}}
|
|
456
|
+
/>
|
|
457
|
+
<Button
|
|
458
|
+
type="submit"
|
|
459
|
+
variant={Variant.Primary}
|
|
460
|
+
size={Size.MediumSmall}
|
|
461
|
+
nowrap
|
|
462
|
+
disabled={!newViewName.trim()}
|
|
463
|
+
onclick={saveNewView}>Save Current</Button
|
|
464
|
+
>
|
|
465
|
+
</div>
|
|
466
|
+
{/if}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
{/if}
|
|
471
|
+
</DesktopModal>
|
|
472
|
+
|
|
473
|
+
<style>.empty-state {
|
|
474
|
+
display: flex;
|
|
475
|
+
flex-flow: column nowrap;
|
|
476
|
+
align-items: center;
|
|
477
|
+
justify-content: center;
|
|
478
|
+
gap: 0.75rem;
|
|
479
|
+
padding: 3rem 1rem;
|
|
480
|
+
text-align: center;
|
|
481
|
+
color: #3a4952;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.config {
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-flow: row nowrap;
|
|
487
|
+
gap: 1rem;
|
|
488
|
+
flex: 1;
|
|
489
|
+
min-height: 0;
|
|
490
|
+
width: 100%;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.side-nav {
|
|
494
|
+
display: flex;
|
|
495
|
+
flex-flow: column nowrap;
|
|
496
|
+
gap: 0.25rem;
|
|
497
|
+
flex-shrink: 0;
|
|
498
|
+
border-right: solid 2px #cbd4da;
|
|
499
|
+
padding-right: 1rem;
|
|
500
|
+
}
|
|
501
|
+
.side-nav .nav-item {
|
|
502
|
+
text-align: left;
|
|
503
|
+
white-space: nowrap;
|
|
504
|
+
padding: 0.6rem 0.9rem;
|
|
505
|
+
border: solid 1px #cbd4da;
|
|
506
|
+
border-radius: 4px;
|
|
507
|
+
background-color: #ffffff;
|
|
508
|
+
color: #3a4952;
|
|
509
|
+
font-weight: 600;
|
|
510
|
+
cursor: pointer;
|
|
511
|
+
}
|
|
512
|
+
.side-nav .nav-item:hover {
|
|
513
|
+
background-color: #e9ecf1;
|
|
514
|
+
}
|
|
515
|
+
.side-nav .nav-item.active {
|
|
516
|
+
background-color: #cbd4da;
|
|
517
|
+
color: #455661;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.pane {
|
|
521
|
+
display: flex;
|
|
522
|
+
flex-flow: column nowrap;
|
|
523
|
+
gap: 0.75rem;
|
|
524
|
+
flex: 1;
|
|
525
|
+
min-width: 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.tabs {
|
|
529
|
+
display: flex;
|
|
530
|
+
flex-flow: row nowrap;
|
|
531
|
+
gap: 0.25rem;
|
|
532
|
+
border-bottom: solid 2px #cbd4da;
|
|
533
|
+
}
|
|
534
|
+
.tabs .tab {
|
|
535
|
+
padding: 0.5rem 0.9rem;
|
|
536
|
+
border: none;
|
|
537
|
+
border-bottom: solid 3px transparent;
|
|
538
|
+
background-color: transparent;
|
|
539
|
+
color: #3a4952;
|
|
540
|
+
font-weight: 600;
|
|
541
|
+
white-space: nowrap;
|
|
542
|
+
cursor: pointer;
|
|
543
|
+
}
|
|
544
|
+
.tabs .tab:hover {
|
|
545
|
+
color: #259fc7;
|
|
546
|
+
}
|
|
547
|
+
.tabs .tab.active {
|
|
548
|
+
border-bottom-color: #259fc7;
|
|
549
|
+
color: #259fc7;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.error-banner {
|
|
553
|
+
display: flex;
|
|
554
|
+
flex-flow: row nowrap;
|
|
555
|
+
align-items: center;
|
|
556
|
+
gap: 0.5rem;
|
|
557
|
+
padding: 0.5rem 0.75rem;
|
|
558
|
+
border: solid 1px #aa1414;
|
|
559
|
+
border-radius: 4px;
|
|
560
|
+
background-color: #f9e9e9;
|
|
561
|
+
color: #aa1414;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.tab-body {
|
|
565
|
+
flex: 1;
|
|
566
|
+
min-height: 0;
|
|
567
|
+
overflow-y: auto;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.muted {
|
|
571
|
+
color: #647d8e;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.card-list {
|
|
575
|
+
list-style: none;
|
|
576
|
+
margin: 0;
|
|
577
|
+
padding: 0;
|
|
578
|
+
display: flex;
|
|
579
|
+
flex-flow: column nowrap;
|
|
580
|
+
gap: 0.5rem;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.view-card {
|
|
584
|
+
border: solid 1px #cbd4da;
|
|
585
|
+
border-radius: 4px;
|
|
586
|
+
padding: 0.6rem 0.75rem;
|
|
587
|
+
background-color: #ffffff;
|
|
588
|
+
display: flex;
|
|
589
|
+
flex-flow: column nowrap;
|
|
590
|
+
gap: 0.5rem;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.view-card-head {
|
|
594
|
+
display: flex;
|
|
595
|
+
flex-flow: row nowrap;
|
|
596
|
+
align-items: center;
|
|
597
|
+
justify-content: space-between;
|
|
598
|
+
gap: 0.75rem;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.view-name {
|
|
602
|
+
font-weight: 600;
|
|
603
|
+
overflow: hidden;
|
|
604
|
+
text-overflow: ellipsis;
|
|
605
|
+
white-space: nowrap;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.view-actions {
|
|
609
|
+
display: flex;
|
|
610
|
+
flex-flow: row nowrap;
|
|
611
|
+
gap: 0.375rem;
|
|
612
|
+
flex-shrink: 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.subscriber-list {
|
|
616
|
+
list-style: none;
|
|
617
|
+
margin: 0;
|
|
618
|
+
padding: 0.25rem 0 0;
|
|
619
|
+
display: flex;
|
|
620
|
+
flex-flow: column nowrap;
|
|
621
|
+
gap: 0.25rem;
|
|
622
|
+
border-top: solid 1px #dae0e5;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.subscriber {
|
|
626
|
+
display: flex;
|
|
627
|
+
flex-flow: row nowrap;
|
|
628
|
+
align-items: center;
|
|
629
|
+
gap: 0.5rem;
|
|
630
|
+
font-size: 0.875rem;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.shared-head {
|
|
634
|
+
display: flex;
|
|
635
|
+
flex-flow: row nowrap;
|
|
636
|
+
margin-bottom: 0.5rem;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.save-tab {
|
|
640
|
+
display: flex;
|
|
641
|
+
flex-flow: column nowrap;
|
|
642
|
+
gap: 0.75rem;
|
|
643
|
+
max-width: 24rem;
|
|
644
|
+
}</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Primitive } from '../Types/Context/TableInitOptions.js';
|
|
2
|
+
import type { TableConfigurationModalProps } from './TableConfigurationModalProps.js';
|
|
3
|
+
declare function $$render<T extends object, IdType extends Primitive>(): {
|
|
4
|
+
props: TableConfigurationModalProps;
|
|
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 TableConfigurationModal: $$IsomorphicComponent;
|
|
25
|
+
type TableConfigurationModal<T extends object, IdType extends Primitive> = InstanceType<typeof TableConfigurationModal<T, IdType>>;
|
|
26
|
+
export default TableConfigurationModal;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for `TableConfigurationModal` - the FPM 403 "Table Configuration"
|
|
3
|
+
* modal that manages saved datatable layouts and filters (the new view
|
|
4
|
+
* system that supersedes the old named-preset box).
|
|
5
|
+
*
|
|
6
|
+
* The modal is driven entirely by the `TableContext` it reads from
|
|
7
|
+
* Svelte context; it has no data props of its own. It only needs a way
|
|
8
|
+
* to tell its host the panel should close.
|
|
9
|
+
*/
|
|
10
|
+
export interface TableConfigurationModalProps {
|
|
11
|
+
/** Called when the modal requests close (footer button / backdrop). */
|
|
12
|
+
onclose?: () => void;
|
|
13
|
+
}
|