@lavalogic/scoria 0.38.0 → 0.38.2
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 +306 -313
- package/dist/Components/Table/Misc/TableHorizontalBar.svelte +0 -12
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +0 -0
- package/dist/Components/Table/Misc/TableViewDropdown.svelte.d.ts +12 -2
- package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +12 -4
- package/dist/Components/Table/Types/Context/TableContext.svelte.js +21 -6
- 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');
|
|
@@ -106,7 +111,14 @@
|
|
|
106
111
|
}, `Could not delete "${view.name}". Please try again.`);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Save the live state of the active section as a brand-new view.
|
|
116
|
+
* `saveCurrentAsNewView` re-fetches the saved views and makes the new
|
|
117
|
+
* one active; the `My …` list and the panel dropdowns are all driven
|
|
118
|
+
* off the reactive `_viewState.savedViews(section)` so they update
|
|
119
|
+
* without a reopen. On success we also switch to the `My …` tab so
|
|
120
|
+
* the user immediately sees the view they just saved.
|
|
121
|
+
*/
|
|
110
122
|
function saveNewView(): void {
|
|
111
123
|
const trimmed = newViewName.trim();
|
|
112
124
|
if (!trimmed) {
|
|
@@ -115,6 +127,7 @@
|
|
|
115
127
|
void guard(async () => {
|
|
116
128
|
await tableContext.saveCurrentAsNewView(trimmed, section);
|
|
117
129
|
newViewName = '';
|
|
130
|
+
selectTab('mine');
|
|
118
131
|
}, `Could not save the new ${sectionNoun.toLowerCase()}. Please try again.`);
|
|
119
132
|
}
|
|
120
133
|
|
|
@@ -191,8 +204,272 @@
|
|
|
191
204
|
publicViews = null;
|
|
192
205
|
}
|
|
193
206
|
}
|
|
207
|
+
|
|
208
|
+
// ── scoria tab-group wiring ──────────────────────────────────────────
|
|
209
|
+
// The left-hand vertical nav is a `HorizontalTabGroup` (tabs strip on
|
|
210
|
+
// the left, content on the right); the top horizontal tabs are a
|
|
211
|
+
// `VerticalTabGroup` (tabs strip on top, content below). Same two
|
|
212
|
+
// scoria primitives the flowms order-allocation modal uses, so the
|
|
213
|
+
// look is consistent across the app.
|
|
214
|
+
|
|
215
|
+
/** The three top tabs for the active section. Rebuilt when the section
|
|
216
|
+
* flips so the labels track "My Layouts" / "My Filters" etc. */
|
|
217
|
+
const innerTabs: Array<TabOption<{ tab: ConfigTab }>> = $derived([
|
|
218
|
+
{
|
|
219
|
+
label: `My ${sectionNounPlural}`,
|
|
220
|
+
content: tabBody,
|
|
221
|
+
args: { tab: 'mine' },
|
|
222
|
+
onclick: () => {
|
|
223
|
+
selectTab('mine');
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
// DESIGN-REVIEW: the mockups were internally inconsistent
|
|
228
|
+
// ("Subscribed to Filters" on one, "Shared Filters" on
|
|
229
|
+
// another). Resolved to "Shared" consistently for both the
|
|
230
|
+
// layout and filter sections.
|
|
231
|
+
label: `Shared ${sectionNounPlural}`,
|
|
232
|
+
content: tabBody,
|
|
233
|
+
args: { tab: 'shared' },
|
|
234
|
+
onclick: () => {
|
|
235
|
+
selectTab('shared');
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
label: `Save a new ${sectionNoun}`,
|
|
240
|
+
content: tabBody,
|
|
241
|
+
args: { tab: 'save' },
|
|
242
|
+
onclick: () => {
|
|
243
|
+
selectTab('save');
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
const selectedInnerTab = $derived(innerTabs.find((t) => t.args.tab === tab) ?? innerTabs[0]);
|
|
249
|
+
|
|
250
|
+
/** The two left-nav sections. Each renders the inner `VerticalTabGroup`
|
|
251
|
+
* via the shared `sectionPane` snippet. */
|
|
252
|
+
const sectionTabs: Array<TabOption<{ kind: DatatableViewKind }>> = [
|
|
253
|
+
{
|
|
254
|
+
label: 'Table Layouts',
|
|
255
|
+
content: sectionPane,
|
|
256
|
+
args: { kind: 'layout' },
|
|
257
|
+
onclick: () => {
|
|
258
|
+
selectSection('layout');
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
label: 'Table Filters',
|
|
263
|
+
content: sectionPane,
|
|
264
|
+
args: { kind: 'filter' },
|
|
265
|
+
onclick: () => {
|
|
266
|
+
selectSection('filter');
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const selectedSectionTab = $derived(
|
|
272
|
+
sectionTabs.find((t) => t.args.kind === section) ?? sectionTabs[0]
|
|
273
|
+
);
|
|
194
274
|
</script>
|
|
195
275
|
|
|
276
|
+
<!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
|
|
277
|
+
{#snippet sectionPane(_args: { kind: DatatableViewKind })}
|
|
278
|
+
<div class="pane">
|
|
279
|
+
<VerticalTabGroup
|
|
280
|
+
tabs={innerTabs}
|
|
281
|
+
selected={selectedInnerTab}
|
|
282
|
+
nohistory
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
{/snippet}
|
|
286
|
+
|
|
287
|
+
<!-- Body of a single top tab (My / Shared / Save). -->
|
|
288
|
+
{#snippet tabBody(args: { tab: ConfigTab })}
|
|
289
|
+
<div class="tab-body">
|
|
290
|
+
{#if errorMessage}
|
|
291
|
+
<div
|
|
292
|
+
class="error-banner"
|
|
293
|
+
role="alert"
|
|
294
|
+
>
|
|
295
|
+
<Icon
|
|
296
|
+
name="circle-exclamation"
|
|
297
|
+
size={Size.Small}
|
|
298
|
+
/>
|
|
299
|
+
<span>{errorMessage}</span>
|
|
300
|
+
</div>
|
|
301
|
+
{/if}
|
|
302
|
+
|
|
303
|
+
{#if args.tab === 'mine'}
|
|
304
|
+
{#if myViews.length === 0}
|
|
305
|
+
<p class="muted">
|
|
306
|
+
You have not saved any {sectionNounPlural.toLowerCase()} yet. Use the "Save a new {sectionNoun}"
|
|
307
|
+
tab to create one.
|
|
308
|
+
</p>
|
|
309
|
+
{:else}
|
|
310
|
+
<ul class="card-list">
|
|
311
|
+
{#each myViews as view (view.id)}
|
|
312
|
+
<li class="view-card">
|
|
313
|
+
<div class="view-card-head">
|
|
314
|
+
<span class="view-name">{view.name}</span>
|
|
315
|
+
<div class="view-actions">
|
|
316
|
+
<Button
|
|
317
|
+
variant={Variant.Secondary}
|
|
318
|
+
size={Size.MediumSmall}
|
|
319
|
+
nowrap
|
|
320
|
+
onclick={() => {
|
|
321
|
+
applyView(view);
|
|
322
|
+
}}>Apply</Button
|
|
323
|
+
>
|
|
324
|
+
<Button
|
|
325
|
+
variant={Variant.Secondary}
|
|
326
|
+
size={Size.MediumSmall}
|
|
327
|
+
nowrap
|
|
328
|
+
iconLeft={{ name: 'external-link', size: Size.Small }}
|
|
329
|
+
onclick={() => {
|
|
330
|
+
selectTab('shared');
|
|
331
|
+
}}>Share</Button
|
|
332
|
+
>
|
|
333
|
+
<Button
|
|
334
|
+
variant={Variant.Secondary}
|
|
335
|
+
size={Size.MediumSmall}
|
|
336
|
+
nowrap
|
|
337
|
+
iconLeft={{ name: 'circle-cross', size: Size.Small }}
|
|
338
|
+
onclick={() => {
|
|
339
|
+
deleteView(view);
|
|
340
|
+
}}>Delete</Button
|
|
341
|
+
>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
{#if view.subscribers && view.subscribers.length > 0}
|
|
345
|
+
<ul class="subscriber-list">
|
|
346
|
+
{#each view.subscribers as subscriber (subscriber.userId)}
|
|
347
|
+
<li class="subscriber">
|
|
348
|
+
<Icon
|
|
349
|
+
name="user-3"
|
|
350
|
+
size={Size.Small}
|
|
351
|
+
/>
|
|
352
|
+
<span>{subscriber.displayName ?? subscriber.userId}</span>
|
|
353
|
+
<Button
|
|
354
|
+
variant={Variant.Secondary}
|
|
355
|
+
size={Size.Tiny}
|
|
356
|
+
nowrap
|
|
357
|
+
onclick={() => {
|
|
358
|
+
removeSubscriber(view, subscriber.userId);
|
|
359
|
+
}}>Remove</Button
|
|
360
|
+
>
|
|
361
|
+
</li>
|
|
362
|
+
{/each}
|
|
363
|
+
</ul>
|
|
364
|
+
{/if}
|
|
365
|
+
</li>
|
|
366
|
+
{/each}
|
|
367
|
+
</ul>
|
|
368
|
+
{/if}
|
|
369
|
+
{:else if args.tab === 'shared'}
|
|
370
|
+
<div class="shared-head">
|
|
371
|
+
<Button
|
|
372
|
+
variant={Variant.Secondary}
|
|
373
|
+
size={Size.MediumSmall}
|
|
374
|
+
nowrap
|
|
375
|
+
disabled={loadingPublicViews}
|
|
376
|
+
iconLeft={{ name: 'circle-plus', size: Size.Small }}
|
|
377
|
+
onclick={loadPublicViews}>Subscribe to Additional...</Button
|
|
378
|
+
>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{#if publicViews !== null}
|
|
382
|
+
{#if publicViews.length === 0}
|
|
383
|
+
<p class="muted">
|
|
384
|
+
There are no further {sectionNounPlural.toLowerCase()} available to subscribe to.
|
|
385
|
+
</p>
|
|
386
|
+
{:else}
|
|
387
|
+
<ul class="card-list">
|
|
388
|
+
{#each publicViews as view (view.id)}
|
|
389
|
+
<li class="view-card">
|
|
390
|
+
<div class="view-card-head">
|
|
391
|
+
<span class="view-name">{view.name}</span>
|
|
392
|
+
<div class="view-actions">
|
|
393
|
+
<Button
|
|
394
|
+
variant={Variant.Secondary}
|
|
395
|
+
size={Size.MediumSmall}
|
|
396
|
+
nowrap
|
|
397
|
+
disabled={currentUserId === undefined}
|
|
398
|
+
onclick={() => {
|
|
399
|
+
subscribe(view);
|
|
400
|
+
}}>Subscribe</Button
|
|
401
|
+
>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</li>
|
|
405
|
+
{/each}
|
|
406
|
+
</ul>
|
|
407
|
+
{/if}
|
|
408
|
+
{/if}
|
|
409
|
+
|
|
410
|
+
{#if sharedViews.length === 0}
|
|
411
|
+
<p class="muted">
|
|
412
|
+
No {sectionNounPlural.toLowerCase()} are currently shared with you.
|
|
413
|
+
</p>
|
|
414
|
+
{:else}
|
|
415
|
+
<ul class="card-list">
|
|
416
|
+
{#each sharedViews as view (view.id)}
|
|
417
|
+
<li class="view-card">
|
|
418
|
+
<div class="view-card-head">
|
|
419
|
+
<span class="view-name">{view.name}</span>
|
|
420
|
+
<div class="view-actions">
|
|
421
|
+
<Button
|
|
422
|
+
variant={Variant.Secondary}
|
|
423
|
+
size={Size.MediumSmall}
|
|
424
|
+
nowrap
|
|
425
|
+
onclick={() => {
|
|
426
|
+
applyView(view);
|
|
427
|
+
}}>Apply</Button
|
|
428
|
+
>
|
|
429
|
+
<Button
|
|
430
|
+
variant={Variant.Secondary}
|
|
431
|
+
size={Size.MediumSmall}
|
|
432
|
+
nowrap
|
|
433
|
+
disabled={currentUserId === undefined}
|
|
434
|
+
onclick={() => {
|
|
435
|
+
unsubscribe(view);
|
|
436
|
+
}}>Unsubscribe</Button
|
|
437
|
+
>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</li>
|
|
441
|
+
{/each}
|
|
442
|
+
</ul>
|
|
443
|
+
{/if}
|
|
444
|
+
{:else}
|
|
445
|
+
<div class="save-tab">
|
|
446
|
+
<p class="muted">
|
|
447
|
+
Save the table's current {sectionNoun.toLowerCase()} as a reusable named
|
|
448
|
+
{sectionNoun.toLowerCase()}.
|
|
449
|
+
</p>
|
|
450
|
+
<TextInput
|
|
451
|
+
placeholder={`${sectionNoun} name`}
|
|
452
|
+
bind:value={newViewName}
|
|
453
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
454
|
+
if (e.key === 'Enter') {
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
saveNewView();
|
|
457
|
+
}
|
|
458
|
+
}}
|
|
459
|
+
/>
|
|
460
|
+
<Button
|
|
461
|
+
type="submit"
|
|
462
|
+
variant={Variant.Secondary}
|
|
463
|
+
size={Size.MediumSmall}
|
|
464
|
+
nowrap
|
|
465
|
+
disabled={!newViewName.trim()}
|
|
466
|
+
onclick={saveNewView}>Save Current</Button
|
|
467
|
+
>
|
|
468
|
+
</div>
|
|
469
|
+
{/if}
|
|
470
|
+
</div>
|
|
471
|
+
{/snippet}
|
|
472
|
+
|
|
196
473
|
<DesktopModal
|
|
197
474
|
headerLabel="Table Configuration"
|
|
198
475
|
headerSubtitle={tableContext.datatableUuid
|
|
@@ -200,6 +477,8 @@
|
|
|
200
477
|
: undefined}
|
|
201
478
|
onclose={() => onclose?.()}
|
|
202
479
|
buttons={[{ label: 'Close', callback: () => onclose?.() }]}
|
|
480
|
+
width={cssLength('50vw')}
|
|
481
|
+
minWidth={cssLength('40rem')}
|
|
203
482
|
>
|
|
204
483
|
{#if !remoteAvailable}
|
|
205
484
|
<div class="empty-state">
|
|
@@ -211,261 +490,11 @@
|
|
|
211
490
|
</div>
|
|
212
491
|
{:else}
|
|
213
492
|
<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>
|
|
493
|
+
<HorizontalTabGroup
|
|
494
|
+
tabs={sectionTabs}
|
|
495
|
+
selected={selectedSectionTab}
|
|
496
|
+
grow
|
|
497
|
+
/>
|
|
469
498
|
</div>
|
|
470
499
|
{/if}
|
|
471
500
|
</DesktopModal>
|
|
@@ -481,72 +510,27 @@
|
|
|
481
510
|
color: #3a4952;
|
|
482
511
|
}
|
|
483
512
|
|
|
513
|
+
/* Fixed-frame wrapper. The modal is a fixed 50vw (see the `width`
|
|
514
|
+
prop on `DesktopModal`); this `.config` lets the tab group fill
|
|
515
|
+
that frame and the tab body scroll inside it, so switching nav
|
|
516
|
+
section or top tab never reflows the dialog. */
|
|
484
517
|
.config {
|
|
485
518
|
display: flex;
|
|
486
|
-
flex-flow:
|
|
487
|
-
gap: 1rem;
|
|
519
|
+
flex-flow: column nowrap;
|
|
488
520
|
flex: 1;
|
|
489
521
|
min-height: 0;
|
|
490
522
|
width: 100%;
|
|
491
523
|
}
|
|
492
524
|
|
|
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
|
-
|
|
525
|
+
/* The right-hand content pane that each left-nav section renders:
|
|
526
|
+
holds the top horizontal tabs and their scrolling body. */
|
|
520
527
|
.pane {
|
|
521
528
|
display: flex;
|
|
522
529
|
flex-flow: column nowrap;
|
|
523
|
-
gap: 0.75rem;
|
|
524
530
|
flex: 1;
|
|
525
531
|
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;
|
|
532
|
+
min-height: 0;
|
|
533
|
+
width: 100%;
|
|
550
534
|
}
|
|
551
535
|
|
|
552
536
|
.error-banner {
|
|
@@ -561,9 +545,18 @@
|
|
|
561
545
|
color: #aa1414;
|
|
562
546
|
}
|
|
563
547
|
|
|
548
|
+
/* The body of a single top tab. A `min-height` floor keeps the modal
|
|
549
|
+
frame visually stable across every section/tab combination (short
|
|
550
|
+
tabs no longer shrink the dialog); the `max-height` + `overflow-y`
|
|
551
|
+
keep taller content scrolling within that fixed frame rather than
|
|
552
|
+
growing the dialog. */
|
|
564
553
|
.tab-body {
|
|
565
|
-
|
|
566
|
-
|
|
554
|
+
display: flex;
|
|
555
|
+
flex-flow: column nowrap;
|
|
556
|
+
gap: 0.75rem;
|
|
557
|
+
padding: 1rem;
|
|
558
|
+
min-height: 30vh;
|
|
559
|
+
max-height: 60vh;
|
|
567
560
|
overflow-y: auto;
|
|
568
561
|
}
|
|
569
562
|
|
|
@@ -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>
|
|
Binary file
|
|
@@ -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;
|
|
@@ -892,10 +892,18 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
|
|
|
892
892
|
* `{ kind:'filter', filter: _captureFilter() }`, plus the
|
|
893
893
|
* host-supplied `datatableUuid` and the owning user;
|
|
894
894
|
* 2. `await remoteLayouts.createView(...)`;
|
|
895
|
-
* 3.
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
895
|
+
* 3. `await refreshSavedViews()` so the matching kind's saved-views
|
|
896
|
+
* list is re-fetched from the backend - the new view then appears
|
|
897
|
+
* in the Table Configuration modal's My-Layouts / My-Filters list
|
|
898
|
+
* (and the panel quick-switch dropdowns) immediately, without a
|
|
899
|
+
* reopen, because both read reactively off `_viewState`;
|
|
900
|
+
* 4. set the newly-saved view as that kind's active view via
|
|
901
|
+
* `applyDatatableView`, which re-points that kind's baseline and
|
|
902
|
+
* clears that kind's dirty flag - so the quick-switch dropdown
|
|
903
|
+
* shows the new view's name straight away rather than
|
|
904
|
+
* `'Custom (unsaved)'`. The authoritative refreshed copy (matched
|
|
905
|
+
* by id) is preferred over the raw `createView` return so the
|
|
906
|
+
* baseline reflects exactly what the backend persisted.
|
|
899
907
|
*
|
|
900
908
|
* Only the named kind's state is touched. No-op (dev-logged via
|
|
901
909
|
* `devCatch`) when remote saved views are disabled (`datatableUuid` /
|
|
@@ -2309,10 +2309,18 @@ export class TableContext {
|
|
|
2309
2309
|
* `{ kind:'filter', filter: _captureFilter() }`, plus the
|
|
2310
2310
|
* host-supplied `datatableUuid` and the owning user;
|
|
2311
2311
|
* 2. `await remoteLayouts.createView(...)`;
|
|
2312
|
-
* 3.
|
|
2313
|
-
*
|
|
2314
|
-
*
|
|
2315
|
-
*
|
|
2312
|
+
* 3. `await refreshSavedViews()` so the matching kind's saved-views
|
|
2313
|
+
* list is re-fetched from the backend - the new view then appears
|
|
2314
|
+
* in the Table Configuration modal's My-Layouts / My-Filters list
|
|
2315
|
+
* (and the panel quick-switch dropdowns) immediately, without a
|
|
2316
|
+
* reopen, because both read reactively off `_viewState`;
|
|
2317
|
+
* 4. set the newly-saved view as that kind's active view via
|
|
2318
|
+
* `applyDatatableView`, which re-points that kind's baseline and
|
|
2319
|
+
* clears that kind's dirty flag - so the quick-switch dropdown
|
|
2320
|
+
* shows the new view's name straight away rather than
|
|
2321
|
+
* `'Custom (unsaved)'`. The authoritative refreshed copy (matched
|
|
2322
|
+
* by id) is preferred over the raw `createView` return so the
|
|
2323
|
+
* baseline reflects exactly what the backend persisted.
|
|
2316
2324
|
*
|
|
2317
2325
|
* Only the named kind's state is touched. No-op (dev-logged via
|
|
2318
2326
|
* `devCatch`) when remote saved views are disabled (`datatableUuid` /
|
|
@@ -2342,8 +2350,15 @@ export class TableContext {
|
|
|
2342
2350
|
ownerUserId,
|
|
2343
2351
|
};
|
|
2344
2352
|
const created = await adapter.createView(request);
|
|
2345
|
-
|
|
2346
|
-
|
|
2353
|
+
// Re-fetch the authoritative saved-views lists so the new view
|
|
2354
|
+
// shows up in the modal and the panel dropdowns immediately.
|
|
2355
|
+
await this.refreshSavedViews();
|
|
2356
|
+
// Prefer the refreshed copy (matched by id); fall back to the
|
|
2357
|
+
// raw create return if the refresh did not surface it.
|
|
2358
|
+
const authoritative = this._viewState.savedViews(kind).find((view) => view.id === created.id) ?? created;
|
|
2359
|
+
// Make the just-saved view active so the quick-switch dropdown
|
|
2360
|
+
// shows its name (not "Custom (unsaved)") and dirty is cleared.
|
|
2361
|
+
this.applyDatatableView(authoritative);
|
|
2347
2362
|
}
|
|
2348
2363
|
catch (e) {
|
|
2349
2364
|
devCatch(e);
|