@lavalogic/scoria 0.38.0 → 0.38.1
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/ColumnPanel.svelte +16 -0
- package/dist/Components/Table/Misc/FilterPanel.svelte +25 -113
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte +293 -311
- package/dist/Components/Table/Misc/TableHorizontalBar.svelte +0 -12
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +34 -112
- package/dist/Components/Table/Misc/TableViewDropdown.svelte.d.ts +12 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import ColumnList from '../Body/Rows/ColumnList.svelte';
|
|
5
5
|
import TextInput from '../../TextInput.svelte';
|
|
6
6
|
import { Size } from '../../../Types/Internal/Size.js';
|
|
7
|
+
import TableViewDropdown from './TableViewDropdown.svelte';
|
|
7
8
|
|
|
8
9
|
let searchValue = $state('');
|
|
9
10
|
</script>
|
|
@@ -14,6 +15,10 @@
|
|
|
14
15
|
</header>
|
|
15
16
|
|
|
16
17
|
<div class="toolbar">
|
|
18
|
+
<!-- Quick-switch dropdown for saved column layouts. Sits above the
|
|
19
|
+
search box; saving a layout is done from the Table
|
|
20
|
+
Configuration modal's "Save a new Layout" tab. -->
|
|
21
|
+
<TableViewDropdown kind="layout" />
|
|
17
22
|
<TextInput
|
|
18
23
|
bind:value={searchValue}
|
|
19
24
|
leftIcon={{ name: 'search', size: Size.Medium }}
|
|
@@ -83,6 +88,10 @@
|
|
|
83
88
|
box-sizing: border-box;
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
/* Top toolbar (saved-layout dropdown + Search input) sits below the
|
|
92
|
+
header but above the scrollable list. It is intentionally not
|
|
93
|
+
inside `.side-panel-body` because the list takes the remaining
|
|
94
|
+
vertical space and scrolls on its own. */
|
|
86
95
|
.toolbar {
|
|
87
96
|
flex-shrink: 0;
|
|
88
97
|
padding: 1rem 1.25rem;
|
|
@@ -100,6 +109,13 @@
|
|
|
100
109
|
display: flex;
|
|
101
110
|
flex-flow: column nowrap;
|
|
102
111
|
background-color: #ffffff;
|
|
112
|
+
/* Horizontal padding aligned with FilterPanel's `.side-panel-body`
|
|
113
|
+
so the column-list cards leave the same gutter before the panel's
|
|
114
|
+
right border as the filter chips do, and the SidePanel's
|
|
115
|
+
`border-right` stays visible between the content and the toolbar
|
|
116
|
+
icons. Vertical padding is 0 because `.column-list` provides
|
|
117
|
+
its own 0.5rem padding (which doubles up to a 1.5rem gap from
|
|
118
|
+
the toolbar above and the footer below). */
|
|
103
119
|
padding: 0 1.25rem;
|
|
104
120
|
box-sizing: border-box;
|
|
105
121
|
}</style>
|
|
@@ -5,17 +5,16 @@
|
|
|
5
5
|
generics="T extends object, IdType extends Primitive"
|
|
6
6
|
>
|
|
7
7
|
import Action from '../../Action.svelte';
|
|
8
|
-
import Button from '../../Button.svelte';
|
|
9
8
|
import TextInput from '../../TextInput.svelte';
|
|
10
9
|
import { Colour, ColourSet } from '../../../scss/colours.js';
|
|
11
10
|
import { Size } from '../../../Types/Internal/Size.js';
|
|
12
|
-
import { Variant } from '../../../Types/Internal/Variant.js';
|
|
13
11
|
import { SvelteSet } from 'svelte/reactivity';
|
|
14
12
|
import { getContext } from 'svelte';
|
|
15
13
|
import type { Primitive } from '../Types/Context/TableInitOptions.js';
|
|
16
14
|
import type { ColumnDef } from '../Types/Columns/Definitions/ColumnDef.svelte.js';
|
|
17
15
|
import { TableContext } from '../Types/Context/TableContext.svelte.js';
|
|
18
16
|
import FilterInput from './FilterInput.svelte';
|
|
17
|
+
import TableViewDropdown from './TableViewDropdown.svelte';
|
|
19
18
|
|
|
20
19
|
const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
|
|
21
20
|
|
|
@@ -31,11 +30,6 @@
|
|
|
31
30
|
* string is the "no filter" state. */
|
|
32
31
|
let quickFindValue: string = $state('');
|
|
33
32
|
|
|
34
|
-
/** Whether the Save Filter overflow menu is open. The chevron
|
|
35
|
-
* toggles it; clicking anywhere else (via the window-level
|
|
36
|
-
* listener below) closes it. */
|
|
37
|
-
let saveMenuOpen: boolean = $state(false);
|
|
38
|
-
|
|
39
33
|
/** Names of currently-collapsed groups. A name absent from this
|
|
40
34
|
* set is rendered expanded. Reactive via `SvelteSet` so the panel
|
|
41
35
|
* re-renders on toggle without manually re-assigning. */
|
|
@@ -106,39 +100,6 @@
|
|
|
106
100
|
}
|
|
107
101
|
}
|
|
108
102
|
|
|
109
|
-
function resetFilters(): void {
|
|
110
|
-
tableContext.paginationRepo?.resetFilters();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Placeholder for the future Save-Filter / preset workflow. For
|
|
115
|
-
* now this just closes the overflow menu; consumers wire the real
|
|
116
|
-
* persistence layer in a follow-up workorder. The chevron menu is
|
|
117
|
-
* still useful today as a home for the Reset action and as the
|
|
118
|
-
* insertion point for "Load Filter" / "Manage Filters" entries
|
|
119
|
-
* once those land.
|
|
120
|
-
*/
|
|
121
|
-
function saveFilter(): void {
|
|
122
|
-
saveMenuOpen = false;
|
|
123
|
-
if (typeof window !== 'undefined') {
|
|
124
|
-
window.dispatchEvent(
|
|
125
|
-
new CustomEvent('scoria:save-filter', {
|
|
126
|
-
detail: { tableName: tableContext.tableName },
|
|
127
|
-
})
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function handleWindowClick(e: MouseEvent): void {
|
|
133
|
-
if (!saveMenuOpen) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const target = e.target as HTMLElement | null;
|
|
137
|
-
if (target?.closest('.save-filter-cluster') == null) {
|
|
138
|
-
saveMenuOpen = false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
103
|
/**
|
|
143
104
|
* No-op keydown handler used to override `TextInput`'s default
|
|
144
105
|
* `passthrough` keydown (defined in `Helpers.svelte.ts`), which
|
|
@@ -155,14 +116,16 @@
|
|
|
155
116
|
}
|
|
156
117
|
</script>
|
|
157
118
|
|
|
158
|
-
<svelte:window onclick={handleWindowClick} />
|
|
159
|
-
|
|
160
119
|
<section class="side-panel-section">
|
|
161
120
|
<header class="side-panel-header">
|
|
162
121
|
<h2>Advanced Filter</h2>
|
|
163
122
|
</header>
|
|
164
123
|
|
|
165
124
|
<div class="toolbar">
|
|
125
|
+
<!-- Quick-switch dropdown for saved filter sets. Sits above the
|
|
126
|
+
search box; saving a filter is done from the Table
|
|
127
|
+
Configuration modal's "Save a new Filter" tab. -->
|
|
128
|
+
<TableViewDropdown kind="filter" />
|
|
166
129
|
<div class="quick-find">
|
|
167
130
|
<TextInput
|
|
168
131
|
labelTop="Quick Find a Filter"
|
|
@@ -172,36 +135,6 @@
|
|
|
172
135
|
onkeydown={noopKeydown}
|
|
173
136
|
/>
|
|
174
137
|
</div>
|
|
175
|
-
|
|
176
|
-
<div class="save-filter-cluster">
|
|
177
|
-
<Button
|
|
178
|
-
variant={Variant.Secondary}
|
|
179
|
-
nowrap
|
|
180
|
-
disabled
|
|
181
|
-
size={Size.MediumSmall}
|
|
182
|
-
onclick={saveFilter}>Save Filter</Button
|
|
183
|
-
>
|
|
184
|
-
<Action
|
|
185
|
-
label="More save options"
|
|
186
|
-
colourSet={ColourSet.Auxiliary}
|
|
187
|
-
icon={{ name: 'chevron-down', size: Size.Medium }}
|
|
188
|
-
onclick={() => {
|
|
189
|
-
saveMenuOpen = !saveMenuOpen;
|
|
190
|
-
}}
|
|
191
|
-
/>
|
|
192
|
-
{#if saveMenuOpen}
|
|
193
|
-
<div class="save-menu">
|
|
194
|
-
<button
|
|
195
|
-
type="button"
|
|
196
|
-
class="save-menu-item"
|
|
197
|
-
onclick={() => {
|
|
198
|
-
saveMenuOpen = false;
|
|
199
|
-
resetFilters();
|
|
200
|
-
}}>Reset all filters</button
|
|
201
|
-
>
|
|
202
|
-
</div>
|
|
203
|
-
{/if}
|
|
204
|
-
</div>
|
|
205
138
|
</div>
|
|
206
139
|
|
|
207
140
|
<div class="side-panel-body">
|
|
@@ -306,59 +239,24 @@
|
|
|
306
239
|
box-sizing: border-box;
|
|
307
240
|
}
|
|
308
241
|
|
|
242
|
+
/* Top toolbar (saved-filter dropdown + Quick Find) sits below the
|
|
243
|
+
header but above the scrollable body. Kept outside
|
|
244
|
+
`.side-panel-body` so it stays pinned while the groups list
|
|
245
|
+
scrolls underneath. The two rows stack vertically: the
|
|
246
|
+
quick-switch dropdown on top, the search field below it. */
|
|
309
247
|
.toolbar {
|
|
310
248
|
flex-shrink: 0;
|
|
311
249
|
display: flex;
|
|
312
|
-
flex-flow:
|
|
313
|
-
align-items: center;
|
|
250
|
+
flex-flow: column nowrap;
|
|
314
251
|
gap: 0.75rem;
|
|
315
252
|
padding: 1rem 1.25rem;
|
|
316
253
|
background-color: #ffffff;
|
|
317
254
|
border-bottom: solid 1px #cbd4da;
|
|
318
255
|
}
|
|
319
256
|
.toolbar .quick-find {
|
|
320
|
-
flex: 1;
|
|
321
257
|
min-width: 0;
|
|
322
258
|
--input-height: 3rem;
|
|
323
259
|
}
|
|
324
|
-
.toolbar .save-filter-cluster {
|
|
325
|
-
position: relative;
|
|
326
|
-
display: flex;
|
|
327
|
-
flex-flow: row nowrap;
|
|
328
|
-
align-items: center;
|
|
329
|
-
gap: 0.25rem;
|
|
330
|
-
}
|
|
331
|
-
.toolbar .save-filter-cluster .save-menu {
|
|
332
|
-
position: absolute;
|
|
333
|
-
top: 100%;
|
|
334
|
-
right: 0;
|
|
335
|
-
margin-top: 0.25rem;
|
|
336
|
-
background-color: #ffffff;
|
|
337
|
-
border: solid 1px #cbd4da;
|
|
338
|
-
border-radius: 4px;
|
|
339
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
340
|
-
min-width: 12rem;
|
|
341
|
-
z-index: 5;
|
|
342
|
-
display: flex;
|
|
343
|
-
flex-flow: column nowrap;
|
|
344
|
-
padding: 0.25rem 0;
|
|
345
|
-
}
|
|
346
|
-
.toolbar .save-filter-cluster .save-menu-item {
|
|
347
|
-
background: transparent;
|
|
348
|
-
border: none;
|
|
349
|
-
text-align: left;
|
|
350
|
-
padding: 0.5rem 0.75rem;
|
|
351
|
-
cursor: pointer;
|
|
352
|
-
font: inherit;
|
|
353
|
-
color: #3a4952;
|
|
354
|
-
}
|
|
355
|
-
.toolbar .save-filter-cluster .save-menu-item:hover {
|
|
356
|
-
background-color: #f4f7f9;
|
|
357
|
-
}
|
|
358
|
-
.toolbar .save-filter-cluster .save-menu-item:focus-visible {
|
|
359
|
-
outline: 2px solid #259fc7;
|
|
360
|
-
outline-offset: -2px;
|
|
361
|
-
}
|
|
362
260
|
|
|
363
261
|
p.empty {
|
|
364
262
|
color: #647d8e;
|
|
@@ -372,6 +270,11 @@ p.empty {
|
|
|
372
270
|
gap: 1rem;
|
|
373
271
|
}
|
|
374
272
|
|
|
273
|
+
/* `overflow: visible` (no longer `overflow: hidden`) so a filter
|
|
274
|
+
row's select dropdown can extend below the group's bottom edge
|
|
275
|
+
when there are not enough rows in the group to push the dropdown
|
|
276
|
+
past it. The header / body each round their own outer corners
|
|
277
|
+
instead so the rounded-card look survives without clipping. */
|
|
375
278
|
.group {
|
|
376
279
|
border: solid 1px #cbd4da;
|
|
377
280
|
border-radius: 4px;
|
|
@@ -387,6 +290,11 @@ p.empty {
|
|
|
387
290
|
padding: 0.5rem 0.75rem;
|
|
388
291
|
background-color: #f4f7f9;
|
|
389
292
|
border-bottom: solid 1px #cbd4da;
|
|
293
|
+
/* Round the header's top corners directly so the tinted strip
|
|
294
|
+
follows the parent's rounded outline. Without this rule
|
|
295
|
+
(combined with the `.group` `overflow: visible` change) the
|
|
296
|
+
header's blue band would leak past the parent's rounded
|
|
297
|
+
corners. */
|
|
390
298
|
border-top-left-radius: 4px;
|
|
391
299
|
border-top-right-radius: 4px;
|
|
392
300
|
}
|
|
@@ -399,10 +307,14 @@ p.empty {
|
|
|
399
307
|
|
|
400
308
|
.group.collapsed > .group-header {
|
|
401
309
|
border-bottom: none;
|
|
310
|
+
/* When collapsed the header is the only visible row, so it
|
|
311
|
+
has to round all four corners to follow the parent outline. */
|
|
402
312
|
border-bottom-left-radius: 4px;
|
|
403
313
|
border-bottom-right-radius: 4px;
|
|
404
314
|
}
|
|
405
315
|
|
|
316
|
+
/* Round the bottom of the body so its visual edge follows the
|
|
317
|
+
parent's rounded outline once `overflow: hidden` is gone. */
|
|
406
318
|
.group:not(.collapsed) > .group-body {
|
|
407
319
|
border-bottom-left-radius: 4px;
|
|
408
320
|
border-bottom-right-radius: 4px;
|
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
>
|
|
7
7
|
import Button from '../../Button.svelte';
|
|
8
8
|
import DesktopModal from '../../DesktopModal.svelte';
|
|
9
|
+
import HorizontalTabGroup from '../../HorizontalTabGroup.svelte';
|
|
9
10
|
import Icon from '../../Icon.svelte';
|
|
10
11
|
import TextInput from '../../TextInput.svelte';
|
|
12
|
+
import VerticalTabGroup from '../../VerticalTabGroup.svelte';
|
|
13
|
+
import { cssLength } from '../../../Helpers/Helpers.svelte.js';
|
|
11
14
|
import { Size } from '../../../Types/Internal/Size.js';
|
|
15
|
+
import type { TabOption } from '../../../Types/Internal/TabOption.js';
|
|
12
16
|
import { Variant } from '../../../Types/Internal/Variant.js';
|
|
13
17
|
import { getContext } from 'svelte';
|
|
14
18
|
import { TableContext } from '../Types/Context/TableContext.svelte.js';
|
|
@@ -24,7 +28,8 @@
|
|
|
24
28
|
// ── Left-nav section + right-pane tab selection ──────────────────────
|
|
25
29
|
// `section` picks the kind (layout vs filter); `tab` picks which of the
|
|
26
30
|
// three horizontal panes is visible for that section. Both are plain
|
|
27
|
-
// `$state` since they are pure local UI selection.
|
|
31
|
+
// `$state` since they are pure local UI selection. They drive the
|
|
32
|
+
// `selected` prop of the two scoria tab groups below.
|
|
28
33
|
let section = $state<DatatableViewKind>('layout');
|
|
29
34
|
type ConfigTab = 'mine' | 'shared' | 'save';
|
|
30
35
|
let tab = $state<ConfigTab>('mine');
|
|
@@ -191,8 +196,272 @@
|
|
|
191
196
|
publicViews = null;
|
|
192
197
|
}
|
|
193
198
|
}
|
|
199
|
+
|
|
200
|
+
// ── scoria tab-group wiring ──────────────────────────────────────────
|
|
201
|
+
// The left-hand vertical nav is a `HorizontalTabGroup` (tabs strip on
|
|
202
|
+
// the left, content on the right); the top horizontal tabs are a
|
|
203
|
+
// `VerticalTabGroup` (tabs strip on top, content below). Same two
|
|
204
|
+
// scoria primitives the flowms order-allocation modal uses, so the
|
|
205
|
+
// look is consistent across the app.
|
|
206
|
+
|
|
207
|
+
/** The three top tabs for the active section. Rebuilt when the section
|
|
208
|
+
* flips so the labels track "My Layouts" / "My Filters" etc. */
|
|
209
|
+
const innerTabs: Array<TabOption<{ tab: ConfigTab }>> = $derived([
|
|
210
|
+
{
|
|
211
|
+
label: `My ${sectionNounPlural}`,
|
|
212
|
+
content: tabBody,
|
|
213
|
+
args: { tab: 'mine' },
|
|
214
|
+
onclick: () => {
|
|
215
|
+
selectTab('mine');
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
// DESIGN-REVIEW: the mockups were internally inconsistent
|
|
220
|
+
// ("Subscribed to Filters" on one, "Shared Filters" on
|
|
221
|
+
// another). Resolved to "Shared" consistently for both the
|
|
222
|
+
// layout and filter sections.
|
|
223
|
+
label: `Shared ${sectionNounPlural}`,
|
|
224
|
+
content: tabBody,
|
|
225
|
+
args: { tab: 'shared' },
|
|
226
|
+
onclick: () => {
|
|
227
|
+
selectTab('shared');
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
label: `Save a new ${sectionNoun}`,
|
|
232
|
+
content: tabBody,
|
|
233
|
+
args: { tab: 'save' },
|
|
234
|
+
onclick: () => {
|
|
235
|
+
selectTab('save');
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const selectedInnerTab = $derived(innerTabs.find((t) => t.args.tab === tab) ?? innerTabs[0]);
|
|
241
|
+
|
|
242
|
+
/** The two left-nav sections. Each renders the inner `VerticalTabGroup`
|
|
243
|
+
* via the shared `sectionPane` snippet. */
|
|
244
|
+
const sectionTabs: Array<TabOption<{ kind: DatatableViewKind }>> = [
|
|
245
|
+
{
|
|
246
|
+
label: 'Table Layouts',
|
|
247
|
+
content: sectionPane,
|
|
248
|
+
args: { kind: 'layout' },
|
|
249
|
+
onclick: () => {
|
|
250
|
+
selectSection('layout');
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
label: 'Table Filters',
|
|
255
|
+
content: sectionPane,
|
|
256
|
+
args: { kind: 'filter' },
|
|
257
|
+
onclick: () => {
|
|
258
|
+
selectSection('filter');
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const selectedSectionTab = $derived(
|
|
264
|
+
sectionTabs.find((t) => t.args.kind === section) ?? sectionTabs[0]
|
|
265
|
+
);
|
|
194
266
|
</script>
|
|
195
267
|
|
|
268
|
+
<!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
|
|
269
|
+
{#snippet sectionPane(_args: { kind: DatatableViewKind })}
|
|
270
|
+
<div class="pane">
|
|
271
|
+
<VerticalTabGroup
|
|
272
|
+
tabs={innerTabs}
|
|
273
|
+
selected={selectedInnerTab}
|
|
274
|
+
nohistory
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
{/snippet}
|
|
278
|
+
|
|
279
|
+
<!-- Body of a single top tab (My / Shared / Save). -->
|
|
280
|
+
{#snippet tabBody(args: { tab: ConfigTab })}
|
|
281
|
+
<div class="tab-body">
|
|
282
|
+
{#if errorMessage}
|
|
283
|
+
<div
|
|
284
|
+
class="error-banner"
|
|
285
|
+
role="alert"
|
|
286
|
+
>
|
|
287
|
+
<Icon
|
|
288
|
+
name="circle-exclamation"
|
|
289
|
+
size={Size.Small}
|
|
290
|
+
/>
|
|
291
|
+
<span>{errorMessage}</span>
|
|
292
|
+
</div>
|
|
293
|
+
{/if}
|
|
294
|
+
|
|
295
|
+
{#if args.tab === 'mine'}
|
|
296
|
+
{#if myViews.length === 0}
|
|
297
|
+
<p class="muted">
|
|
298
|
+
You have not saved any {sectionNounPlural.toLowerCase()} yet. Use the "Save a new {sectionNoun}"
|
|
299
|
+
tab to create one.
|
|
300
|
+
</p>
|
|
301
|
+
{:else}
|
|
302
|
+
<ul class="card-list">
|
|
303
|
+
{#each myViews as view (view.id)}
|
|
304
|
+
<li class="view-card">
|
|
305
|
+
<div class="view-card-head">
|
|
306
|
+
<span class="view-name">{view.name}</span>
|
|
307
|
+
<div class="view-actions">
|
|
308
|
+
<Button
|
|
309
|
+
variant={Variant.Secondary}
|
|
310
|
+
size={Size.MediumSmall}
|
|
311
|
+
nowrap
|
|
312
|
+
onclick={() => {
|
|
313
|
+
applyView(view);
|
|
314
|
+
}}>Apply</Button
|
|
315
|
+
>
|
|
316
|
+
<Button
|
|
317
|
+
variant={Variant.Secondary}
|
|
318
|
+
size={Size.MediumSmall}
|
|
319
|
+
nowrap
|
|
320
|
+
iconLeft={{ name: 'external-link', size: Size.Small }}
|
|
321
|
+
onclick={() => {
|
|
322
|
+
selectTab('shared');
|
|
323
|
+
}}>Share</Button
|
|
324
|
+
>
|
|
325
|
+
<Button
|
|
326
|
+
variant={Variant.Secondary}
|
|
327
|
+
size={Size.MediumSmall}
|
|
328
|
+
nowrap
|
|
329
|
+
iconLeft={{ name: 'circle-cross', size: Size.Small }}
|
|
330
|
+
onclick={() => {
|
|
331
|
+
deleteView(view);
|
|
332
|
+
}}>Delete</Button
|
|
333
|
+
>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
{#if view.subscribers && view.subscribers.length > 0}
|
|
337
|
+
<ul class="subscriber-list">
|
|
338
|
+
{#each view.subscribers as subscriber (subscriber.userId)}
|
|
339
|
+
<li class="subscriber">
|
|
340
|
+
<Icon
|
|
341
|
+
name="user-3"
|
|
342
|
+
size={Size.Small}
|
|
343
|
+
/>
|
|
344
|
+
<span>{subscriber.displayName ?? subscriber.userId}</span>
|
|
345
|
+
<Button
|
|
346
|
+
variant={Variant.Secondary}
|
|
347
|
+
size={Size.Tiny}
|
|
348
|
+
nowrap
|
|
349
|
+
onclick={() => {
|
|
350
|
+
removeSubscriber(view, subscriber.userId);
|
|
351
|
+
}}>Remove</Button
|
|
352
|
+
>
|
|
353
|
+
</li>
|
|
354
|
+
{/each}
|
|
355
|
+
</ul>
|
|
356
|
+
{/if}
|
|
357
|
+
</li>
|
|
358
|
+
{/each}
|
|
359
|
+
</ul>
|
|
360
|
+
{/if}
|
|
361
|
+
{:else if args.tab === 'shared'}
|
|
362
|
+
<div class="shared-head">
|
|
363
|
+
<Button
|
|
364
|
+
variant={Variant.Secondary}
|
|
365
|
+
size={Size.MediumSmall}
|
|
366
|
+
nowrap
|
|
367
|
+
disabled={loadingPublicViews}
|
|
368
|
+
iconLeft={{ name: 'circle-plus', size: Size.Small }}
|
|
369
|
+
onclick={loadPublicViews}>Subscribe to Additional...</Button
|
|
370
|
+
>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
{#if publicViews !== null}
|
|
374
|
+
{#if publicViews.length === 0}
|
|
375
|
+
<p class="muted">
|
|
376
|
+
There are no further {sectionNounPlural.toLowerCase()} available to subscribe to.
|
|
377
|
+
</p>
|
|
378
|
+
{:else}
|
|
379
|
+
<ul class="card-list">
|
|
380
|
+
{#each publicViews as view (view.id)}
|
|
381
|
+
<li class="view-card">
|
|
382
|
+
<div class="view-card-head">
|
|
383
|
+
<span class="view-name">{view.name}</span>
|
|
384
|
+
<div class="view-actions">
|
|
385
|
+
<Button
|
|
386
|
+
variant={Variant.Secondary}
|
|
387
|
+
size={Size.MediumSmall}
|
|
388
|
+
nowrap
|
|
389
|
+
disabled={currentUserId === undefined}
|
|
390
|
+
onclick={() => {
|
|
391
|
+
subscribe(view);
|
|
392
|
+
}}>Subscribe</Button
|
|
393
|
+
>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</li>
|
|
397
|
+
{/each}
|
|
398
|
+
</ul>
|
|
399
|
+
{/if}
|
|
400
|
+
{/if}
|
|
401
|
+
|
|
402
|
+
{#if sharedViews.length === 0}
|
|
403
|
+
<p class="muted">
|
|
404
|
+
No {sectionNounPlural.toLowerCase()} are currently shared with you.
|
|
405
|
+
</p>
|
|
406
|
+
{:else}
|
|
407
|
+
<ul class="card-list">
|
|
408
|
+
{#each sharedViews as view (view.id)}
|
|
409
|
+
<li class="view-card">
|
|
410
|
+
<div class="view-card-head">
|
|
411
|
+
<span class="view-name">{view.name}</span>
|
|
412
|
+
<div class="view-actions">
|
|
413
|
+
<Button
|
|
414
|
+
variant={Variant.Secondary}
|
|
415
|
+
size={Size.MediumSmall}
|
|
416
|
+
nowrap
|
|
417
|
+
onclick={() => {
|
|
418
|
+
applyView(view);
|
|
419
|
+
}}>Apply</Button
|
|
420
|
+
>
|
|
421
|
+
<Button
|
|
422
|
+
variant={Variant.Secondary}
|
|
423
|
+
size={Size.MediumSmall}
|
|
424
|
+
nowrap
|
|
425
|
+
disabled={currentUserId === undefined}
|
|
426
|
+
onclick={() => {
|
|
427
|
+
unsubscribe(view);
|
|
428
|
+
}}>Unsubscribe</Button
|
|
429
|
+
>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</li>
|
|
433
|
+
{/each}
|
|
434
|
+
</ul>
|
|
435
|
+
{/if}
|
|
436
|
+
{:else}
|
|
437
|
+
<div class="save-tab">
|
|
438
|
+
<p class="muted">
|
|
439
|
+
Save the table's current {sectionNoun.toLowerCase()} as a reusable named
|
|
440
|
+
{sectionNoun.toLowerCase()}.
|
|
441
|
+
</p>
|
|
442
|
+
<TextInput
|
|
443
|
+
placeholder={`${sectionNoun} name`}
|
|
444
|
+
bind:value={newViewName}
|
|
445
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
446
|
+
if (e.key === 'Enter') {
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
saveNewView();
|
|
449
|
+
}
|
|
450
|
+
}}
|
|
451
|
+
/>
|
|
452
|
+
<Button
|
|
453
|
+
type="submit"
|
|
454
|
+
variant={Variant.Primary}
|
|
455
|
+
size={Size.MediumSmall}
|
|
456
|
+
nowrap
|
|
457
|
+
disabled={!newViewName.trim()}
|
|
458
|
+
onclick={saveNewView}>Save Current</Button
|
|
459
|
+
>
|
|
460
|
+
</div>
|
|
461
|
+
{/if}
|
|
462
|
+
</div>
|
|
463
|
+
{/snippet}
|
|
464
|
+
|
|
196
465
|
<DesktopModal
|
|
197
466
|
headerLabel="Table Configuration"
|
|
198
467
|
headerSubtitle={tableContext.datatableUuid
|
|
@@ -200,6 +469,8 @@
|
|
|
200
469
|
: undefined}
|
|
201
470
|
onclose={() => onclose?.()}
|
|
202
471
|
buttons={[{ label: 'Close', callback: () => onclose?.() }]}
|
|
472
|
+
width={cssLength('50vw')}
|
|
473
|
+
minWidth={cssLength('40rem')}
|
|
203
474
|
>
|
|
204
475
|
{#if !remoteAvailable}
|
|
205
476
|
<div class="empty-state">
|
|
@@ -211,261 +482,11 @@
|
|
|
211
482
|
</div>
|
|
212
483
|
{:else}
|
|
213
484
|
<div class="config">
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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>
|
|
485
|
+
<HorizontalTabGroup
|
|
486
|
+
tabs={sectionTabs}
|
|
487
|
+
selected={selectedSectionTab}
|
|
488
|
+
grow
|
|
489
|
+
/>
|
|
469
490
|
</div>
|
|
470
491
|
{/if}
|
|
471
492
|
</DesktopModal>
|
|
@@ -481,72 +502,27 @@
|
|
|
481
502
|
color: #3a4952;
|
|
482
503
|
}
|
|
483
504
|
|
|
505
|
+
/* Fixed-frame wrapper. The modal is a fixed 50vw (see the `width`
|
|
506
|
+
prop on `DesktopModal`); this `.config` lets the tab group fill
|
|
507
|
+
that frame and the tab body scroll inside it, so switching nav
|
|
508
|
+
section or top tab never reflows the dialog. */
|
|
484
509
|
.config {
|
|
485
510
|
display: flex;
|
|
486
|
-
flex-flow:
|
|
487
|
-
gap: 1rem;
|
|
511
|
+
flex-flow: column nowrap;
|
|
488
512
|
flex: 1;
|
|
489
513
|
min-height: 0;
|
|
490
514
|
width: 100%;
|
|
491
515
|
}
|
|
492
516
|
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
517
|
+
/* The right-hand content pane that each left-nav section renders:
|
|
518
|
+
holds the top horizontal tabs and their scrolling body. */
|
|
520
519
|
.pane {
|
|
521
520
|
display: flex;
|
|
522
521
|
flex-flow: column nowrap;
|
|
523
|
-
gap: 0.75rem;
|
|
524
522
|
flex: 1;
|
|
525
523
|
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;
|
|
524
|
+
min-height: 0;
|
|
525
|
+
width: 100%;
|
|
550
526
|
}
|
|
551
527
|
|
|
552
528
|
.error-banner {
|
|
@@ -561,9 +537,15 @@
|
|
|
561
537
|
color: #aa1414;
|
|
562
538
|
}
|
|
563
539
|
|
|
540
|
+
/* The body of a single top tab. Scrolls within the fixed modal
|
|
541
|
+
frame rather than growing the dialog. */
|
|
564
542
|
.tab-body {
|
|
565
|
-
|
|
543
|
+
display: flex;
|
|
544
|
+
flex-flow: column nowrap;
|
|
545
|
+
gap: 0.75rem;
|
|
546
|
+
padding: 1rem;
|
|
566
547
|
min-height: 0;
|
|
548
|
+
max-height: 60vh;
|
|
567
549
|
overflow-y: auto;
|
|
568
550
|
}
|
|
569
551
|
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
import ColumnPanelModal from './ColumnPanelModal.svelte';
|
|
17
17
|
import FilterPanelModal from './FilterPanelModal.svelte';
|
|
18
18
|
import TableConfigurationModal from './TableConfigurationModal.svelte';
|
|
19
|
-
import TableViewDropdown from './TableViewDropdown.svelte';
|
|
20
19
|
import ToolboxPanelModal from './ToolboxPanelModal.svelte';
|
|
21
20
|
|
|
22
21
|
const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
|
|
@@ -42,9 +41,6 @@
|
|
|
42
41
|
/>
|
|
43
42
|
{/if}
|
|
44
43
|
<div class="sidebar">
|
|
45
|
-
<div class="view-dropdown-slot">
|
|
46
|
-
<TableViewDropdown />
|
|
47
|
-
</div>
|
|
48
44
|
<Button
|
|
49
45
|
side={Side.Bottom}
|
|
50
46
|
onclick={() => {
|
|
@@ -159,12 +155,4 @@
|
|
|
159
155
|
.sidebar .filler {
|
|
160
156
|
flex-grow: 2;
|
|
161
157
|
background-color: #ffffff;
|
|
162
|
-
}
|
|
163
|
-
.sidebar .view-dropdown-slot {
|
|
164
|
-
display: flex;
|
|
165
|
-
flex-flow: row nowrap;
|
|
166
|
-
align-items: center;
|
|
167
|
-
background-color: #ffffff;
|
|
168
|
-
padding: 0 0.5rem;
|
|
169
|
-
flex-shrink: 0;
|
|
170
158
|
}</style>
|
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
lang="ts"
|
|
5
5
|
generics="T extends object, IdType extends Primitive"
|
|
6
6
|
>
|
|
7
|
-
import Button from '../../Button.svelte';
|
|
8
7
|
import SingleSelect from '../../SingleSelect.svelte';
|
|
9
|
-
import TextInput from '../../TextInput.svelte';
|
|
10
|
-
import { devCatch } from '../../../Helpers/Helpers.svelte.js';
|
|
11
8
|
import type { SelectOption } from '../../../Types/Internal/SelectOption.js';
|
|
12
|
-
import { Size } from '../../../Types/Internal/Size.js';
|
|
13
|
-
import { Variant } from '../../../Types/Internal/Variant.js';
|
|
14
9
|
import { getContext } from 'svelte';
|
|
15
10
|
import { TableContext } from '../Types/Context/TableContext.svelte.js';
|
|
16
11
|
import type { Primitive } from '../Types/Context/TableInitOptions.js';
|
|
12
|
+
import type { DatatableViewKind } from '../Types/Persistence/DatatableViewKind.js';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/**
|
|
16
|
+
* Which view-state slice this dropdown switches between. `'layout'`
|
|
17
|
+
* lists saved column layouts; `'filter'` lists saved filter sets.
|
|
18
|
+
* Drives every `_viewState` read (label / active view / saved
|
|
19
|
+
* views) and which kind `resetToDefault` is called with.
|
|
20
|
+
*/
|
|
21
|
+
kind: DatatableViewKind;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { kind }: Props = $props();
|
|
17
25
|
|
|
18
26
|
const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
|
|
19
27
|
|
|
@@ -22,15 +30,14 @@
|
|
|
22
30
|
// collides with one.
|
|
23
31
|
const DEFAULT_OPTION = '';
|
|
24
32
|
|
|
25
|
-
// The
|
|
26
|
-
const activeView = $derived(tableContext._viewState.activeView(
|
|
27
|
-
const
|
|
28
|
-
const savedViews = $derived(tableContext._viewState.savedViews('layout'));
|
|
33
|
+
// The view-state slice for `kind`, read live from the context.
|
|
34
|
+
const activeView = $derived(tableContext._viewState.activeView(kind));
|
|
35
|
+
const savedViews = $derived(tableContext._viewState.savedViews(kind));
|
|
29
36
|
|
|
30
|
-
//
|
|
37
|
+
// Dropdown label: 'Default' / 'Custom (unsaved)' / '<name>' / '<name>
|
|
31
38
|
// (unsaved)'. Used as the displayed label of the currently-selected
|
|
32
39
|
// option so the dropdown reflects the dirty state exactly.
|
|
33
|
-
const dropdownLabel = $derived(tableContext._viewState.dropdownLabel(
|
|
40
|
+
const dropdownLabel = $derived(tableContext._viewState.dropdownLabel(kind));
|
|
34
41
|
|
|
35
42
|
// The value of the currently-active option: the sentinel for Default,
|
|
36
43
|
// otherwise the saved view's id.
|
|
@@ -38,7 +45,8 @@
|
|
|
38
45
|
|
|
39
46
|
// Options for the select. The currently-active option's label is the
|
|
40
47
|
// `dropdownLabel` (so a dirty view reads "<name> (unsaved)"); every
|
|
41
|
-
// other option shows its plain name. "Default" is always first
|
|
48
|
+
// other option shows its plain name. "Default" is always first, even
|
|
49
|
+
// when remote saved views are disabled and `savedViews` is empty.
|
|
42
50
|
const options: Array<SelectOption<string>> = $derived([
|
|
43
51
|
{
|
|
44
52
|
label: activeValue === DEFAULT_OPTION ? dropdownLabel : 'Default',
|
|
@@ -52,17 +60,13 @@
|
|
|
52
60
|
|
|
53
61
|
const selectedOption = $derived(options.find((o) => o.value === activeValue) ?? options[0]);
|
|
54
62
|
|
|
55
|
-
// Inline "name this layout" prompt state for the Save button.
|
|
56
|
-
let promptOpen = $state(false);
|
|
57
|
-
let pendingName = $state('');
|
|
58
|
-
|
|
59
63
|
/** Apply the option the user picked from the dropdown. */
|
|
60
64
|
function selectOption(option: SelectOption<string> | null): void {
|
|
61
65
|
if (!option) {
|
|
62
66
|
return;
|
|
63
67
|
}
|
|
64
68
|
if (option.value === DEFAULT_OPTION) {
|
|
65
|
-
tableContext.resetToDefault(
|
|
69
|
+
tableContext.resetToDefault(kind);
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
const view = savedViews.find((v) => v.id === option.value);
|
|
@@ -70,106 +74,24 @@
|
|
|
70
74
|
tableContext.applyDatatableView(view);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Handle the Save button. When the active layout is a saved view, save
|
|
76
|
-
* over it directly. When it is the Default (an unsaved custom layout),
|
|
77
|
-
* open the inline name prompt so the user can name a brand-new view.
|
|
78
|
-
*/
|
|
79
|
-
function onSaveClicked(): void {
|
|
80
|
-
if (activeView.kind === 'saved') {
|
|
81
|
-
tableContext.saveActiveView('layout').catch(devCatch);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
promptOpen = true;
|
|
85
|
-
pendingName = '';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Commit the inline name prompt as a brand-new saved layout. */
|
|
89
|
-
function commitNewView(): void {
|
|
90
|
-
const trimmed = pendingName.trim();
|
|
91
|
-
if (!trimmed) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
tableContext.saveCurrentAsNewView(trimmed, 'layout').catch(devCatch);
|
|
95
|
-
promptOpen = false;
|
|
96
|
-
pendingName = '';
|
|
97
|
-
}
|
|
98
77
|
</script>
|
|
99
78
|
|
|
100
79
|
<div class="view-dropdown">
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
/>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
{#if promptOpen}
|
|
116
|
-
<div class="save-prompt">
|
|
117
|
-
<TextInput
|
|
118
|
-
placeholder="Layout name"
|
|
119
|
-
bind:value={pendingName}
|
|
120
|
-
onkeydown={(e: KeyboardEvent) => {
|
|
121
|
-
if (e.key === 'Enter') {
|
|
122
|
-
e.preventDefault();
|
|
123
|
-
commitNewView();
|
|
124
|
-
} else if (e.key === 'Escape') {
|
|
125
|
-
promptOpen = false;
|
|
126
|
-
pendingName = '';
|
|
127
|
-
}
|
|
128
|
-
}}
|
|
129
|
-
/>
|
|
130
|
-
<Button
|
|
131
|
-
type="submit"
|
|
132
|
-
variant={Variant.Primary}
|
|
133
|
-
size={Size.MediumSmall}
|
|
134
|
-
nowrap
|
|
135
|
-
disabled={!pendingName.trim()}
|
|
136
|
-
onclick={commitNewView}>Save</Button
|
|
137
|
-
>
|
|
138
|
-
<Button
|
|
139
|
-
variant={Variant.Secondary}
|
|
140
|
-
size={Size.MediumSmall}
|
|
141
|
-
nowrap
|
|
142
|
-
onclick={() => {
|
|
143
|
-
promptOpen = false;
|
|
144
|
-
pendingName = '';
|
|
145
|
-
}}>Cancel</Button
|
|
146
|
-
>
|
|
147
|
-
</div>
|
|
148
|
-
{:else}
|
|
149
|
-
<Button
|
|
150
|
-
variant={Variant.Secondary}
|
|
151
|
-
size={Size.MediumSmall}
|
|
152
|
-
nowrap
|
|
153
|
-
disabled={!isDirty}
|
|
154
|
-
onclick={onSaveClicked}>Save</Button
|
|
155
|
-
>
|
|
156
|
-
{/if}
|
|
80
|
+
<SingleSelect
|
|
81
|
+
label={kind === 'filter' ? 'Filter' : 'Layout'}
|
|
82
|
+
name={kind === 'filter' ? 'Filter' : 'Layout'}
|
|
83
|
+
{options}
|
|
84
|
+
labelProp="label"
|
|
85
|
+
valueProp="value"
|
|
86
|
+
value={selectedOption}
|
|
87
|
+
valueAsObject
|
|
88
|
+
searchable={false}
|
|
89
|
+
onchange={selectOption}
|
|
90
|
+
/>
|
|
157
91
|
</div>
|
|
158
92
|
|
|
159
93
|
<style>.view-dropdown {
|
|
160
94
|
display: flex;
|
|
161
|
-
flex-flow:
|
|
162
|
-
|
|
163
|
-
gap: 0.5rem;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.select {
|
|
167
|
-
min-width: 12rem;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.save-prompt {
|
|
171
|
-
display: flex;
|
|
172
|
-
flex-flow: row nowrap;
|
|
173
|
-
align-items: end;
|
|
174
|
-
gap: 0.375rem;
|
|
95
|
+
flex-flow: column nowrap;
|
|
96
|
+
width: 100%;
|
|
175
97
|
}</style>
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { Primitive } from '../Types/Context/TableInitOptions.js';
|
|
2
|
+
import type { DatatableViewKind } from '../Types/Persistence/DatatableViewKind.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/**
|
|
5
|
+
* Which view-state slice this dropdown switches between. `'layout'`
|
|
6
|
+
* lists saved column layouts; `'filter'` lists saved filter sets.
|
|
7
|
+
* Drives every `_viewState` read (label / active view / saved
|
|
8
|
+
* views) and which kind `resetToDefault` is called with.
|
|
9
|
+
*/
|
|
10
|
+
kind: DatatableViewKind;
|
|
11
|
+
}
|
|
2
12
|
declare function $$render<T extends object, IdType extends Primitive>(): {
|
|
3
|
-
props:
|
|
13
|
+
props: Props;
|
|
4
14
|
exports: {};
|
|
5
15
|
bindings: "";
|
|
6
16
|
slots: {};
|
|
@@ -17,7 +27,7 @@ interface $$IsomorphicComponent {
|
|
|
17
27
|
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']>> & {
|
|
18
28
|
$$bindings?: ReturnType<__sveltets_Render<T, IdType>['bindings']>;
|
|
19
29
|
} & ReturnType<__sveltets_Render<T, IdType>['exports']>;
|
|
20
|
-
<T extends object, IdType extends Primitive>(internal: unknown, props: {}): ReturnType<__sveltets_Render<T, IdType>['exports']>;
|
|
30
|
+
<T extends object, IdType extends Primitive>(internal: unknown, props: ReturnType<__sveltets_Render<T, IdType>['props']> & {}): ReturnType<__sveltets_Render<T, IdType>['exports']>;
|
|
21
31
|
z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
|
|
22
32
|
}
|
|
23
33
|
declare const TableViewDropdown: $$IsomorphicComponent;
|