@lavalogic/scoria 0.40.13 → 0.40.15
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/README.md +58 -58
- package/dist/Components/AccordionGroup.svelte +35 -46
- package/dist/Components/AccordionGroupButton.svelte +68 -76
- package/dist/Components/Action.svelte +103 -135
- package/dist/Components/AlertModal.svelte +171 -190
- package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +614 -697
- package/dist/Components/Base/BaseModal.svelte +90 -116
- package/dist/Components/Base/ContextWrapper.svelte +67 -72
- package/dist/Components/Base/Pagination.svelte +173 -196
- package/dist/Components/Base/Snippets.svelte +34 -36
- package/dist/Components/BigRadioSet.svelte +36 -38
- package/dist/Components/Bubble.svelte +78 -101
- package/dist/Components/Button.svelte +175 -208
- package/dist/Components/Checkbox.svelte +75 -86
- package/dist/Components/CollapsibleCard.svelte +72 -87
- package/dist/Components/CollapsibleMenuGroup.svelte +53 -57
- package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +39 -64
- package/dist/Components/Contexts/ContextMenu.svelte +141 -156
- package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
- package/dist/Components/Contexts/ContextMenuOption.svelte +53 -60
- package/dist/Components/Contexts/Toast.svelte +81 -86
- package/dist/Components/Contexts/ToastContainer.svelte +58 -62
- package/dist/Components/Contexts/Tooltip.svelte +36 -37
- package/dist/Components/DataMatrixIcon.svelte +95 -108
- package/dist/Components/DateInput.svelte +118 -146
- package/dist/Components/DatePicker.svelte +255 -294
- package/dist/Components/DesktopModal.svelte +70 -90
- package/dist/Components/Dial.svelte +167 -197
- package/dist/Components/DimensionInput.svelte +124 -169
- package/dist/Components/DragMenu/DragMenuHolder.svelte +98 -114
- package/dist/Components/DragMenu/DraggableCard.svelte +64 -76
- package/dist/Components/FileCard.svelte +21 -24
- package/dist/Components/FileUpload.svelte +190 -225
- package/dist/Components/GridInput.svelte +159 -196
- package/dist/Components/GridInputSet.svelte +24 -29
- package/dist/Components/HorizontalTabGroup.svelte +73 -89
- package/dist/Components/HorizontalTabGroupButton.svelte +40 -44
- package/dist/Components/Icon.svelte +99 -128
- package/dist/Components/Icons.js +357 -357
- package/dist/Components/InfoAlert.svelte +35 -38
- package/dist/Components/LoadingAnimation.svelte +691 -755
- package/dist/Components/LoadingModal.svelte +36 -44
- package/dist/Components/LoadingOverlay.svelte +22 -26
- package/dist/Components/Modal.svelte +204 -254
- package/dist/Components/MultiSelect.svelte +186 -216
- package/dist/Components/Nav/Nav.svelte +94 -110
- package/dist/Components/Nav/NavServiceMenuList.svelte +57 -61
- package/dist/Components/Nav/NavTab.svelte +58 -62
- package/dist/Components/Nav/ServiceMenuItem.svelte +103 -111
- package/dist/Components/Nav/ServicesNav.svelte +72 -79
- package/dist/Components/NumberInput.svelte +121 -155
- package/dist/Components/OptionCards.svelte +62 -73
- package/dist/Components/PageHeading.svelte +23 -25
- package/dist/Components/PasswordInput.svelte +69 -88
- package/dist/Components/PopoverButton.svelte +151 -0
- package/dist/Components/PopoverButton.svelte.d.ts +9 -0
- package/dist/Components/PopoverButtonProps.d.ts +21 -0
- package/dist/Components/PopoverButtonProps.js +1 -0
- package/dist/Components/Portal.svelte +29 -35
- package/dist/Components/ProgressBubble.svelte +91 -106
- package/dist/Components/QuerySelect.svelte +260 -346
- package/dist/Components/SidebarPanel.svelte +34 -36
- package/dist/Components/SingleSelect.svelte +196 -244
- package/dist/Components/StepButton.svelte +65 -70
- package/dist/Components/StepForm.svelte +115 -135
- package/dist/Components/Switch.svelte +57 -69
- package/dist/Components/Table/ARCHITECTURE.md +298 -298
- package/dist/Components/Table/Body/QuickSearchCell.svelte +578 -655
- package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +42 -51
- package/dist/Components/Table/Body/Rows/ColumnList.svelte +42 -58
- package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +403 -442
- package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +51 -78
- package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +165 -184
- package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +110 -123
- package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +134 -160
- package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +190 -231
- package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +82 -104
- package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +52 -87
- package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +122 -150
- package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +132 -159
- package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +190 -236
- package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +173 -211
- package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +204 -244
- package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +140 -177
- package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +188 -217
- package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +173 -193
- package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +166 -187
- package/dist/Components/Table/Body/Rows/TableRow.svelte +245 -288
- package/dist/Components/Table/Body/TableBody.svelte +141 -154
- package/dist/Components/Table/Headers/HighlightAllHeader.svelte +35 -41
- package/dist/Components/Table/Headers/SelectAllHeader.svelte +24 -30
- package/dist/Components/Table/Headers/TableHead.svelte +348 -402
- package/dist/Components/Table/Misc/ColumnPanel.svelte +37 -39
- package/dist/Components/Table/Misc/ColumnPanelModal.svelte +35 -39
- package/dist/Components/Table/Misc/FilterInput.svelte +556 -619
- package/dist/Components/Table/Misc/FilterPanel.svelte +170 -183
- package/dist/Components/Table/Misc/FilterPanelModal.svelte +25 -31
- package/dist/Components/Table/Misc/ShareViewModal.svelte +252 -281
- package/dist/Components/Table/Misc/SidePanel.svelte +32 -35
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte +574 -629
- package/dist/Components/Table/Misc/TableFooter.svelte +326 -332
- package/dist/Components/Table/Misc/TableHorizontalBar.svelte +134 -140
- package/dist/Components/Table/Misc/TableSidebar.svelte +183 -198
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +83 -103
- package/dist/Components/Table/Misc/ToolboxPanel.svelte +83 -100
- package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +88 -107
- package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +163 -190
- package/dist/Components/Table/NestedTable.svelte +102 -128
- package/dist/Components/Table/Table.svelte +531 -663
- package/dist/Components/Test/TestContextConsumer.svelte +11 -13
- package/dist/Components/TextArea.svelte +95 -130
- package/dist/Components/TextInput.svelte +125 -176
- package/dist/Components/ThreeStateRadio.svelte +111 -130
- package/dist/Components/Touch/BoolCard.svelte +33 -45
- package/dist/Components/Touch/DateModal.svelte +164 -204
- package/dist/Components/Touch/EditCard.svelte +48 -59
- package/dist/Components/Touch/InfoCard.svelte +28 -31
- package/dist/Components/Touch/InfoTextCard.svelte +26 -28
- package/dist/Components/Touch/ModalExplanation.svelte +15 -17
- package/dist/Components/Touch/NumberKeyboard.svelte +181 -218
- package/dist/Components/Touch/NumberModal.svelte +59 -79
- package/dist/Components/Touch/PlusMinusCard.svelte +43 -46
- package/dist/Components/Touch/SelectModal.svelte +111 -126
- package/dist/Components/Touch/SummaryCard.svelte +80 -93
- package/dist/Components/Touch/TextAreaModal.svelte +72 -94
- package/dist/Components/Touch/TextModal.svelte +77 -105
- package/dist/Components/Touch/TouchCard.svelte +31 -33
- package/dist/Components/Touch/TouchModal.svelte +214 -270
- package/dist/Components/Touch/UtilityButton.svelte +88 -109
- package/dist/Components/VerticalTabGroup.svelte +60 -78
- package/dist/Components/VerticalTabGroupButton.svelte +54 -58
- package/dist/Helpers/Datamatrix.d.ts +19 -19
- package/dist/Helpers/Datamatrix.js +291 -291
- package/dist/Types/Examples/ExampleModal.svelte +31 -35
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/scss/_basics.scss +12 -12
- package/dist/scss/_colours.scss +134 -134
- package/dist/scss/_mixins.scss +3392 -3392
- package/dist/scss/_motion.scss +57 -57
- package/dist/scss/_sizing.scss +85 -85
- package/dist/scss/_transitions.scss +8 -8
- package/package.json +1 -1
|
@@ -1,646 +1,591 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<script
|
|
4
|
-
lang="ts"
|
|
5
|
-
generics="T extends object, IdType extends Primitive"
|
|
6
|
-
>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script
|
|
4
|
+
lang="ts"
|
|
5
|
+
generics="T extends object, IdType extends Primitive"
|
|
6
|
+
>import Button from "../../Button.svelte";
|
|
7
|
+
import DesktopModal from "../../DesktopModal.svelte";
|
|
8
|
+
import HorizontalTabGroup from "../../HorizontalTabGroup.svelte";
|
|
9
|
+
import Icon from "../../Icon.svelte";
|
|
10
|
+
import InfoAlert from "../../InfoAlert.svelte";
|
|
11
|
+
import TextInput from "../../TextInput.svelte";
|
|
12
|
+
import VerticalTabGroup from "../../VerticalTabGroup.svelte";
|
|
13
|
+
import { cssLength } from "../../../Helpers/Helpers.svelte.js";
|
|
14
|
+
import { Size } from "../../../Types/Internal/Size.js";
|
|
15
|
+
import { Variant } from "../../../Types/Internal/Variant.js";
|
|
16
|
+
import { getContext, onMount } from "svelte";
|
|
17
|
+
import { TableContext } from "../Types/Context/TableContext.svelte.js";
|
|
18
|
+
import ShareViewModal from "./ShareViewModal.svelte";
|
|
19
|
+
import UpdateViewOptionsModal from "./UpdateViewOptionsModal.svelte";
|
|
20
|
+
const tableContext = getContext(TableContext.identifier);
|
|
21
|
+
const { onclose } = $props();
|
|
22
|
+
// ── Left-nav section + right-pane tab selection ──────────────────────
|
|
23
|
+
// `section` picks the kind (layout vs filter); `tab` picks which of the
|
|
24
|
+
// three horizontal panes is visible for that section. Both are plain
|
|
25
|
+
// `$state` since they are pure local UI selection. They drive the
|
|
26
|
+
// `selected` prop of the two scoria tab groups below.
|
|
27
|
+
let section = $state("layout");
|
|
28
|
+
let tab = $state("mine");
|
|
29
|
+
// Name field for the "Save a new ..." tab.
|
|
30
|
+
let newViewName = $state("");
|
|
31
|
+
// Public-views discovery state for the "Subscribe to Additional" flow.
|
|
32
|
+
// `null` = not yet loaded; an array = the last successful fetch.
|
|
33
|
+
let publicViews = $state(null);
|
|
34
|
+
let loadingPublicViews = $state(false);
|
|
35
|
+
// User-visible error banner. Every adapter call is wrapped so a
|
|
36
|
+
// rejection lands here instead of throwing into the UI. scoria cannot
|
|
37
|
+
// assume a host-provided toast context exists, so the modal surfaces
|
|
38
|
+
// errors inline in its own body.
|
|
39
|
+
let errorMessage = $state(null);
|
|
40
|
+
// True while the open-time fetch of saved views is in flight, so the
|
|
41
|
+
// My-tab shows "Loading…" rather than the "nothing saved yet" message.
|
|
42
|
+
let loadingViews = $state(false);
|
|
43
|
+
// The view whose Share modal is currently open. `null` when no share
|
|
44
|
+
// modal is rendered. Owner cards (rendered on the My-tab) wire their
|
|
45
|
+
// Share button to open `ShareViewModal` against the view they belong
|
|
46
|
+
// to; the modal closes itself on Cancel / Save.
|
|
47
|
+
let sharingView = $state(null);
|
|
48
|
+
// The view whose Update Options modal is currently open. `null` when
|
|
49
|
+
// no options modal is rendered. Same pattern as `sharingView`.
|
|
50
|
+
let updatingOptionsView = $state(null);
|
|
51
|
+
/**
|
|
52
|
+
* Overwrite a saved view's stored body with the table's current
|
|
53
|
+
* live state. Triggered by the "Save Layout" / "Save Filter"
|
|
54
|
+
* button on an owner card - the typical flow is "apply view A,
|
|
55
|
+
* tweak the columns / filters, then click Save on A". Refreshes
|
|
56
|
+
* the saved-views list afterwards so the card label and dropdown
|
|
57
|
+
* pick up the new body, and surfaces any failure in the inline
|
|
58
|
+
* error banner.
|
|
59
|
+
*/
|
|
60
|
+
function saveLiveStateInto(view) {
|
|
61
|
+
void guard(async () => {
|
|
62
|
+
await tableContext.saveLiveStateIntoView(view);
|
|
63
|
+
await tableContext.refreshSavedViews();
|
|
64
|
+
}, `Could not save changes to "${view.name}". Please try again.`);
|
|
65
|
+
}
|
|
66
|
+
// Whether remote saved views are available at all. A table created
|
|
67
|
+
// without `datatableUuid` / `remoteLayouts` opts out of remote views;
|
|
68
|
+
// the modal then renders a graceful "unavailable" empty state and
|
|
69
|
+
// makes zero adapter calls.
|
|
70
|
+
const remoteAvailable = $derived(tableContext.datatableUuid !== undefined && tableContext.remoteLayouts !== undefined);
|
|
71
|
+
// The opaque current-user id, used to scope "mine" vs "shared" and to
|
|
72
|
+
// drive subscribe / unsubscribe. `undefined` when the host supplied
|
|
73
|
+
// `() => undefined` - sharing controls then degrade gracefully.
|
|
74
|
+
const currentUserId = $derived(tableContext.getUserId());
|
|
75
|
+
// All saved views of the active section, split by ownership. A view the
|
|
76
|
+
// current user owns is "mine"; anything else they can see is "shared"
|
|
77
|
+
// (a subscription). When no user id is available every view falls into
|
|
78
|
+
// "mine" so the user can still manage what they have.
|
|
79
|
+
const sectionViews = $derived(tableContext._viewState.savedViews(section));
|
|
80
|
+
const myViews = $derived(sectionViews.filter((view) => currentUserId === undefined || view.ownerUserId === currentUserId));
|
|
81
|
+
const sharedViews = $derived(sectionViews.filter((view) => currentUserId !== undefined && view.ownerUserId !== currentUserId));
|
|
82
|
+
// Human labels for the active section, so the three tabs and headings
|
|
83
|
+
// read "My Layouts" / "My Filters" etc. without branching everywhere.
|
|
84
|
+
const sectionNoun = $derived(section === "filter" ? "Filter" : "Layout");
|
|
85
|
+
const sectionNounPlural = $derived(section === "filter" ? "Filters" : "Layouts");
|
|
86
|
+
/**
|
|
87
|
+
* Run an adapter-backed action, clearing any prior error first and
|
|
88
|
+
* routing a rejection to the inline error banner. Keeps every async
|
|
89
|
+
* branch below a one-liner and guarantees no rejection escapes to the
|
|
90
|
+
* UI (the modal never throws).
|
|
91
|
+
*/
|
|
92
|
+
async function guard(action, failureMessage) {
|
|
93
|
+
errorMessage = null;
|
|
94
|
+
try {
|
|
95
|
+
await action();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// Surface the underlying error rather than swallowing it: the
|
|
98
|
+
// generic message alone has made failures hard to diagnose. The
|
|
99
|
+
// full error (with stack) goes to the console; its message is
|
|
100
|
+
// appended to the banner so it is visible without devtools.
|
|
101
|
+
console.error("[TableConfigurationModal] action failed:", e);
|
|
102
|
+
errorMessage = e instanceof Error ? `${failureMessage} (${e.message})` : failureMessage;
|
|
82
103
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
// The opaque current-user id, used to scope "mine" vs "shared" and to
|
|
93
|
-
// drive subscribe / unsubscribe. `undefined` when the host supplied
|
|
94
|
-
// `() => undefined` - sharing controls then degrade gracefully.
|
|
95
|
-
const currentUserId = $derived(tableContext.getUserId());
|
|
96
|
-
|
|
97
|
-
// All saved views of the active section, split by ownership. A view the
|
|
98
|
-
// current user owns is "mine"; anything else they can see is "shared"
|
|
99
|
-
// (a subscription). When no user id is available every view falls into
|
|
100
|
-
// "mine" so the user can still manage what they have.
|
|
101
|
-
const sectionViews = $derived(tableContext._viewState.savedViews(section));
|
|
102
|
-
const myViews = $derived(
|
|
103
|
-
sectionViews.filter((view) => currentUserId === undefined || view.ownerUserId === currentUserId)
|
|
104
|
-
);
|
|
105
|
-
const sharedViews = $derived(
|
|
106
|
-
sectionViews.filter((view) => currentUserId !== undefined && view.ownerUserId !== currentUserId)
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Human labels for the active section, so the three tabs and headings
|
|
110
|
-
// read "My Layouts" / "My Filters" etc. without branching everywhere.
|
|
111
|
-
const sectionNoun = $derived(section === 'filter' ? 'Filter' : 'Layout');
|
|
112
|
-
const sectionNounPlural = $derived(section === 'filter' ? 'Filters' : 'Layouts');
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Run an adapter-backed action, clearing any prior error first and
|
|
116
|
-
* routing a rejection to the inline error banner. Keeps every async
|
|
117
|
-
* branch below a one-liner and guarantees no rejection escapes to the
|
|
118
|
-
* UI (the modal never throws).
|
|
119
|
-
*/
|
|
120
|
-
async function guard(action: () => Promise<void>, failureMessage: string): Promise<void> {
|
|
121
|
-
errorMessage = null;
|
|
122
|
-
try {
|
|
123
|
-
await action();
|
|
124
|
-
} catch (e) {
|
|
125
|
-
// Surface the underlying error rather than swallowing it: the
|
|
126
|
-
// generic message alone has made failures hard to diagnose. The
|
|
127
|
-
// full error (with stack) goes to the console; its message is
|
|
128
|
-
// appended to the banner so it is visible without devtools.
|
|
129
|
-
console.error('[TableConfigurationModal] action failed:', e);
|
|
130
|
-
errorMessage = e instanceof Error ? `${failureMessage} (${e.message})` : failureMessage;
|
|
131
|
-
}
|
|
104
|
+
}
|
|
105
|
+
// Fetch the saved views fresh every time the modal opens. The boot-time
|
|
106
|
+
// fetch can miss (it races table construction / auth readiness), so the
|
|
107
|
+
// modal cannot rely on `_viewState.savedViews` already being populated -
|
|
108
|
+
// it pulls `GET /datatable-state` itself on open. A failure surfaces in
|
|
109
|
+
// the inline error banner via `guard` rather than being swallowed.
|
|
110
|
+
onMount(() => {
|
|
111
|
+
if (!remoteAvailable) {
|
|
112
|
+
return;
|
|
132
113
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// modal cannot rely on `_viewState.savedViews` already being populated -
|
|
137
|
-
// it pulls `GET /datatable-state` itself on open. A failure surfaces in
|
|
138
|
-
// the inline error banner via `guard` rather than being swallowed.
|
|
139
|
-
onMount(() => {
|
|
140
|
-
if (!remoteAvailable) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
loadingViews = true;
|
|
144
|
-
void guard(
|
|
145
|
-
() => tableContext.refreshSavedViews(),
|
|
146
|
-
'Could not load saved layouts and filters. Please try again.'
|
|
147
|
-
).finally(() => {
|
|
148
|
-
loadingViews = false;
|
|
149
|
-
});
|
|
114
|
+
loadingViews = true;
|
|
115
|
+
void guard(() => tableContext.refreshSavedViews(), "Could not load saved layouts and filters. Please try again.").finally(() => {
|
|
116
|
+
loadingViews = false;
|
|
150
117
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
118
|
+
});
|
|
119
|
+
/** Apply a saved view to the live table. Pure local call - no await. */
|
|
120
|
+
function applyView(view) {
|
|
121
|
+
errorMessage = null;
|
|
122
|
+
tableContext.applyDatatableView(view);
|
|
123
|
+
}
|
|
124
|
+
/** Delete a view the current user owns, then refresh the lists. */
|
|
125
|
+
function deleteView(view) {
|
|
126
|
+
const adapter = tableContext.remoteLayouts;
|
|
127
|
+
if (!adapter) {
|
|
128
|
+
return;
|
|
156
129
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
130
|
+
void guard(async () => {
|
|
131
|
+
await adapter.deleteView(view.id);
|
|
132
|
+
await tableContext.refreshSavedViews();
|
|
133
|
+
}, `Could not delete "${view.name}". Please try again.`);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Save the live state of the active section as a brand-new view.
|
|
137
|
+
* `saveCurrentAsNewView` re-fetches the saved views and makes the new
|
|
138
|
+
* one active; the `My …` list and the panel dropdowns are all driven
|
|
139
|
+
* off the reactive `_viewState.savedViews(section)` so they update
|
|
140
|
+
* without a reopen. On success we also switch to the `My …` tab so
|
|
141
|
+
* the user immediately sees the view they just saved.
|
|
142
|
+
*/
|
|
143
|
+
function saveNewView() {
|
|
144
|
+
const trimmed = newViewName.trim();
|
|
145
|
+
if (!trimmed) {
|
|
146
|
+
return;
|
|
168
147
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!trimmed) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
void guard(async () => {
|
|
184
|
-
await tableContext.saveCurrentAsNewView(trimmed, section);
|
|
185
|
-
newViewName = '';
|
|
186
|
-
selectTab('mine');
|
|
187
|
-
}, `Could not save the new ${sectionNoun.toLowerCase()}. Please try again.`);
|
|
148
|
+
void guard(async () => {
|
|
149
|
+
await tableContext.saveCurrentAsNewView(trimmed, section);
|
|
150
|
+
newViewName = "";
|
|
151
|
+
selectTab("mine");
|
|
152
|
+
}, `Could not save the new ${sectionNoun.toLowerCase()}. Please try again.`);
|
|
153
|
+
}
|
|
154
|
+
/** Remove the current user as a subscriber of a shared view. */
|
|
155
|
+
function unsubscribe(view) {
|
|
156
|
+
const adapter = tableContext.remoteLayouts;
|
|
157
|
+
if (!adapter || currentUserId === undefined) {
|
|
158
|
+
return;
|
|
188
159
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}, `Could not unsubscribe from "${view.name}". Please try again.`);
|
|
160
|
+
void guard(async () => {
|
|
161
|
+
await adapter.removeSubscriber(view.id, currentUserId);
|
|
162
|
+
await tableContext.refreshSavedViews();
|
|
163
|
+
}, `Could not unsubscribe from "${view.name}". Please try again.`);
|
|
164
|
+
}
|
|
165
|
+
/** Remove a named subscriber from a view the current user owns. */
|
|
166
|
+
function removeSubscriber(view, userId) {
|
|
167
|
+
const adapter = tableContext.remoteLayouts;
|
|
168
|
+
if (!adapter) {
|
|
169
|
+
return;
|
|
200
170
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
171
|
+
void guard(async () => {
|
|
172
|
+
await adapter.removeSubscriber(view.id, userId);
|
|
173
|
+
await tableContext.refreshSavedViews();
|
|
174
|
+
}, "Could not remove the subscriber. Please try again.");
|
|
175
|
+
}
|
|
176
|
+
/** Fetch the public views the user could subscribe to. */
|
|
177
|
+
function loadPublicViews() {
|
|
178
|
+
const adapter = tableContext.remoteLayouts;
|
|
179
|
+
const datatableUuid = tableContext.datatableUuid;
|
|
180
|
+
if (!adapter || datatableUuid === undefined) {
|
|
181
|
+
return;
|
|
212
182
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
loadingPublicViews = false;
|
|
227
|
-
});
|
|
183
|
+
loadingPublicViews = true;
|
|
184
|
+
void guard(async () => {
|
|
185
|
+
const all = await adapter.listPublicViews(datatableUuid);
|
|
186
|
+
publicViews = all.filter((view) => view.kind === section);
|
|
187
|
+
}, "Could not load views available to subscribe to. Please try again.").finally(() => {
|
|
188
|
+
loadingPublicViews = false;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** Subscribe the current user to a public view, then refresh. */
|
|
192
|
+
function subscribe(view) {
|
|
193
|
+
const adapter = tableContext.remoteLayouts;
|
|
194
|
+
if (!adapter || currentUserId === undefined) {
|
|
195
|
+
return;
|
|
228
196
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
197
|
+
void guard(async () => {
|
|
198
|
+
await adapter.addSubscriber(view.id, currentUserId);
|
|
199
|
+
await tableContext.refreshSavedViews();
|
|
200
|
+
loadPublicViews();
|
|
201
|
+
}, `Could not subscribe to "${view.name}". Please try again.`);
|
|
202
|
+
}
|
|
203
|
+
/** Switch the active left-nav section, resetting the tab + scratch UI. */
|
|
204
|
+
function selectSection(next) {
|
|
205
|
+
section = next;
|
|
206
|
+
tab = "mine";
|
|
207
|
+
newViewName = "";
|
|
208
|
+
publicViews = null;
|
|
209
|
+
errorMessage = null;
|
|
210
|
+
}
|
|
211
|
+
/** Switch the active right-pane tab, resetting tab-local scratch UI. */
|
|
212
|
+
function selectTab(next) {
|
|
213
|
+
tab = next;
|
|
214
|
+
errorMessage = null;
|
|
215
|
+
if (next !== "save") {
|
|
216
|
+
newViewName = "";
|
|
241
217
|
}
|
|
242
|
-
|
|
243
|
-
/** Switch the active left-nav section, resetting the tab + scratch UI. */
|
|
244
|
-
function selectSection(next: DatatableViewKind): void {
|
|
245
|
-
section = next;
|
|
246
|
-
tab = 'mine';
|
|
247
|
-
newViewName = '';
|
|
218
|
+
if (next !== "shared") {
|
|
248
219
|
publicViews = null;
|
|
249
|
-
errorMessage = null;
|
|
250
220
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
221
|
+
}
|
|
222
|
+
// ── scoria tab-group wiring ──────────────────────────────────────────
|
|
223
|
+
// The left-hand vertical nav is a `HorizontalTabGroup` (tabs strip on
|
|
224
|
+
// the left, content on the right); the top horizontal tabs are a
|
|
225
|
+
// `VerticalTabGroup` (tabs strip on top, content below). Same two
|
|
226
|
+
// scoria primitives the flowms order-allocation modal uses, so the
|
|
227
|
+
// look is consistent across the app.
|
|
228
|
+
/** The three top tabs for the active section. Rebuilt when the section
|
|
229
|
+
* flips so the labels track "My Layouts" / "My Filters" etc. */
|
|
230
|
+
const innerTabs = $derived([
|
|
231
|
+
{
|
|
232
|
+
label: `My ${sectionNounPlural}`,
|
|
233
|
+
content: tabBody,
|
|
234
|
+
args: { tab: "mine" },
|
|
235
|
+
onclick: () => {
|
|
236
|
+
selectTab("mine");
|
|
258
237
|
}
|
|
259
|
-
|
|
260
|
-
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
label: `Shared ${sectionNounPlural}`,
|
|
241
|
+
content: tabBody,
|
|
242
|
+
args: { tab: "shared" },
|
|
243
|
+
onclick: () => {
|
|
244
|
+
selectTab("shared");
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
label: `Save a new ${sectionNoun}`,
|
|
249
|
+
content: tabBody,
|
|
250
|
+
args: { tab: "save" },
|
|
251
|
+
onclick: () => {
|
|
252
|
+
selectTab("save");
|
|
261
253
|
}
|
|
262
254
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
{
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}}>
|
|
381
|
-
>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
{:else}
|
|
598
|
-
<div class="config">
|
|
599
|
-
<HorizontalTabGroup
|
|
600
|
-
tabs={sectionTabs}
|
|
601
|
-
selected={selectedSectionTab}
|
|
602
|
-
grow
|
|
603
|
-
/>
|
|
604
|
-
</div>
|
|
605
|
-
{/if}
|
|
606
|
-
</DesktopModal>
|
|
607
|
-
|
|
608
|
-
<!--
|
|
609
|
-
Share dialog stacked on top of the Configuration modal. Rendered as a
|
|
610
|
-
sibling (not nested in the modal body) so the second modal's overlay
|
|
611
|
-
covers the first - matching how every other layered modal in scoria
|
|
612
|
-
stacks. After a successful Save we refresh the saved-views list so the
|
|
613
|
-
owner's card's subscriber strip updates immediately, then close.
|
|
614
|
-
-->
|
|
615
|
-
{#if sharingView}
|
|
616
|
-
<ShareViewModal
|
|
617
|
-
view={sharingView}
|
|
618
|
-
onclose={() => {
|
|
619
|
-
sharingView = null;
|
|
620
|
-
}}
|
|
621
|
-
onsaved={async () => {
|
|
622
|
-
await tableContext.refreshSavedViews();
|
|
623
|
-
}}
|
|
624
|
-
/>
|
|
625
|
-
{/if}
|
|
626
|
-
|
|
627
|
-
<!--
|
|
628
|
-
Update-Options dialog stacked the same way as the Share dialog.
|
|
629
|
-
After a successful Save we refresh the saved views so the My-* card
|
|
630
|
-
picks up the new name / visibility immediately.
|
|
631
|
-
-->
|
|
632
|
-
{#if updatingOptionsView}
|
|
633
|
-
<UpdateViewOptionsModal
|
|
634
|
-
view={updatingOptionsView}
|
|
635
|
-
onclose={() => {
|
|
636
|
-
updatingOptionsView = null;
|
|
637
|
-
}}
|
|
638
|
-
onsaved={async () => {
|
|
639
|
-
await tableContext.refreshSavedViews();
|
|
640
|
-
}}
|
|
641
|
-
/>
|
|
642
|
-
{/if}
|
|
643
|
-
|
|
255
|
+
]);
|
|
256
|
+
const selectedInnerTab = $derived(innerTabs.find((t) => t.args.tab === tab) ?? innerTabs[0]);
|
|
257
|
+
/** The two left-nav sections. Each renders the inner `VerticalTabGroup`
|
|
258
|
+
* via the shared `sectionPane` snippet. */
|
|
259
|
+
const sectionTabs = [{
|
|
260
|
+
label: "Table Layouts",
|
|
261
|
+
content: sectionPane,
|
|
262
|
+
args: { kind: "layout" },
|
|
263
|
+
onclick: () => {
|
|
264
|
+
selectSection("layout");
|
|
265
|
+
}
|
|
266
|
+
}, {
|
|
267
|
+
label: "Table Filters",
|
|
268
|
+
content: sectionPane,
|
|
269
|
+
args: { kind: "filter" },
|
|
270
|
+
onclick: () => {
|
|
271
|
+
selectSection("filter");
|
|
272
|
+
}
|
|
273
|
+
}];
|
|
274
|
+
const selectedSectionTab = $derived(sectionTabs.find((t) => t.args.kind === section) ?? sectionTabs[0]);
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<!-- Left-nav section pane: the top horizontal tabs for the active kind. -->
|
|
278
|
+
{#snippet sectionPane(_args: { kind: DatatableViewKind })}
|
|
279
|
+
<div class="pane">
|
|
280
|
+
<VerticalTabGroup
|
|
281
|
+
tabs={innerTabs}
|
|
282
|
+
selected={selectedInnerTab}
|
|
283
|
+
nohistory
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
{/snippet}
|
|
287
|
+
|
|
288
|
+
<!-- Body of a single top tab (My / Shared / Save). -->
|
|
289
|
+
{#snippet tabBody(args: { tab: ConfigTab })}
|
|
290
|
+
<div class="tab-body">
|
|
291
|
+
{#if errorMessage}
|
|
292
|
+
<div
|
|
293
|
+
class="error-banner"
|
|
294
|
+
role="alert"
|
|
295
|
+
>
|
|
296
|
+
<Icon
|
|
297
|
+
name="circle-exclamation"
|
|
298
|
+
size={Size.Small}
|
|
299
|
+
/>
|
|
300
|
+
<span>{errorMessage}</span>
|
|
301
|
+
</div>
|
|
302
|
+
{/if}
|
|
303
|
+
|
|
304
|
+
{#if args.tab === 'mine'}
|
|
305
|
+
{#if loadingViews}
|
|
306
|
+
<InfoAlert>Loading saved {sectionNounPlural.toLowerCase()}…</InfoAlert>
|
|
307
|
+
{:else if myViews.length === 0}
|
|
308
|
+
<InfoAlert>
|
|
309
|
+
You have not saved any {sectionNounPlural.toLowerCase()} yet. Use the "Save a new {sectionNoun}"
|
|
310
|
+
tab to create one.
|
|
311
|
+
</InfoAlert>
|
|
312
|
+
{:else}
|
|
313
|
+
<ul class="card-list">
|
|
314
|
+
{#each myViews as view (view.id)}
|
|
315
|
+
<li class="view-card">
|
|
316
|
+
<div class="view-card-head">
|
|
317
|
+
<span class="view-name">{view.name}</span>
|
|
318
|
+
<div class="view-actions">
|
|
319
|
+
<Button
|
|
320
|
+
variant={Variant.Secondary}
|
|
321
|
+
size={Size.MediumSmall}
|
|
322
|
+
nowrap
|
|
323
|
+
onclick={() => {
|
|
324
|
+
applyView(view);
|
|
325
|
+
}}>Apply</Button
|
|
326
|
+
>
|
|
327
|
+
<!--
|
|
328
|
+
"Save Layout" / "Save Filter" overwrites THIS saved
|
|
329
|
+
view's stored body with the table's current live state.
|
|
330
|
+
Typical flow: user applies the view, tweaks the
|
|
331
|
+
columns or filter inputs, then clicks Save to make
|
|
332
|
+
the tweaks part of the saved view. Distinct from
|
|
333
|
+
"Save a new …" (which always creates a fresh record).
|
|
334
|
+
-->
|
|
335
|
+
<Button
|
|
336
|
+
variant={Variant.Secondary}
|
|
337
|
+
size={Size.MediumSmall}
|
|
338
|
+
nowrap
|
|
339
|
+
iconLeft={{ name: 'circle-tick', size: Size.Small }}
|
|
340
|
+
onclick={() => {
|
|
341
|
+
saveLiveStateInto(view);
|
|
342
|
+
}}>Save {sectionNoun}</Button
|
|
343
|
+
>
|
|
344
|
+
<!--
|
|
345
|
+
"Update Options" opens a dedicated modal that edits
|
|
346
|
+
the view's name and sharing visibility (private /
|
|
347
|
+
public). Kept separate from the inline body-save
|
|
348
|
+
above so a quick name fix never accidentally
|
|
349
|
+
overwrites the saved layout / filter.
|
|
350
|
+
-->
|
|
351
|
+
<Button
|
|
352
|
+
variant={Variant.Secondary}
|
|
353
|
+
size={Size.MediumSmall}
|
|
354
|
+
nowrap
|
|
355
|
+
iconLeft={{ name: 'edit', size: Size.Small }}
|
|
356
|
+
onclick={() => {
|
|
357
|
+
updatingOptionsView = view;
|
|
358
|
+
}}>Update Options</Button
|
|
359
|
+
>
|
|
360
|
+
<Button
|
|
361
|
+
variant={Variant.Secondary}
|
|
362
|
+
size={Size.MediumSmall}
|
|
363
|
+
nowrap
|
|
364
|
+
iconLeft={{ name: 'external-link', size: Size.Small }}
|
|
365
|
+
onclick={() => {
|
|
366
|
+
// Open the share dialog rather than switching to
|
|
367
|
+
// the "Shared" tab - subscribing OTHER users to
|
|
368
|
+
// this view is a distinct flow from "Subscribe
|
|
369
|
+
// to Additional" (which is the current user
|
|
370
|
+
// joining a public view).
|
|
371
|
+
sharingView = view;
|
|
372
|
+
}}>Share</Button
|
|
373
|
+
>
|
|
374
|
+
<Button
|
|
375
|
+
variant={Variant.Secondary}
|
|
376
|
+
size={Size.MediumSmall}
|
|
377
|
+
nowrap
|
|
378
|
+
iconLeft={{ name: 'circle-cross', size: Size.Small }}
|
|
379
|
+
onclick={() => {
|
|
380
|
+
deleteView(view);
|
|
381
|
+
}}>Delete</Button
|
|
382
|
+
>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
{#if view.subscribers && view.subscribers.length > 0}
|
|
386
|
+
<ul class="subscriber-list">
|
|
387
|
+
{#each view.subscribers as subscriber (subscriber.userId)}
|
|
388
|
+
<li class="subscriber">
|
|
389
|
+
<Icon
|
|
390
|
+
name="user-3"
|
|
391
|
+
size={Size.Small}
|
|
392
|
+
/>
|
|
393
|
+
<span>{subscriber.displayName ?? subscriber.userId}</span>
|
|
394
|
+
<Button
|
|
395
|
+
variant={Variant.Secondary}
|
|
396
|
+
size={Size.Tiny}
|
|
397
|
+
nowrap
|
|
398
|
+
onclick={() => {
|
|
399
|
+
removeSubscriber(view, subscriber.userId);
|
|
400
|
+
}}>Remove</Button
|
|
401
|
+
>
|
|
402
|
+
</li>
|
|
403
|
+
{/each}
|
|
404
|
+
</ul>
|
|
405
|
+
{/if}
|
|
406
|
+
</li>
|
|
407
|
+
{/each}
|
|
408
|
+
</ul>
|
|
409
|
+
{/if}
|
|
410
|
+
{:else if args.tab === 'shared'}
|
|
411
|
+
<div class="shared-head">
|
|
412
|
+
<Button
|
|
413
|
+
variant={Variant.Secondary}
|
|
414
|
+
size={Size.MediumSmall}
|
|
415
|
+
nowrap
|
|
416
|
+
disabled={loadingPublicViews}
|
|
417
|
+
iconLeft={{ name: 'circle-plus', size: Size.Small }}
|
|
418
|
+
onclick={loadPublicViews}>Subscribe to Additional...</Button
|
|
419
|
+
>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
{#if publicViews !== null}
|
|
423
|
+
{#if publicViews.length === 0}
|
|
424
|
+
<InfoAlert>
|
|
425
|
+
There are no further {sectionNounPlural.toLowerCase()} available to subscribe to.
|
|
426
|
+
</InfoAlert>
|
|
427
|
+
{:else}
|
|
428
|
+
<ul class="card-list">
|
|
429
|
+
{#each publicViews as view (view.id)}
|
|
430
|
+
<li class="view-card">
|
|
431
|
+
<div class="view-card-head">
|
|
432
|
+
<span class="view-name">{view.name}</span>
|
|
433
|
+
<div class="view-actions">
|
|
434
|
+
<Button
|
|
435
|
+
variant={Variant.Secondary}
|
|
436
|
+
size={Size.MediumSmall}
|
|
437
|
+
nowrap
|
|
438
|
+
disabled={currentUserId === undefined}
|
|
439
|
+
onclick={() => {
|
|
440
|
+
subscribe(view);
|
|
441
|
+
}}>Subscribe</Button
|
|
442
|
+
>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</li>
|
|
446
|
+
{/each}
|
|
447
|
+
</ul>
|
|
448
|
+
{/if}
|
|
449
|
+
{/if}
|
|
450
|
+
|
|
451
|
+
{#if sharedViews.length === 0}
|
|
452
|
+
<InfoAlert>
|
|
453
|
+
No {sectionNounPlural.toLowerCase()} are currently shared with you.
|
|
454
|
+
</InfoAlert>
|
|
455
|
+
{:else}
|
|
456
|
+
<ul class="card-list">
|
|
457
|
+
{#each sharedViews as view (view.id)}
|
|
458
|
+
<li class="view-card">
|
|
459
|
+
<div class="view-card-head">
|
|
460
|
+
<span class="view-name">{view.name}</span>
|
|
461
|
+
<div class="view-actions">
|
|
462
|
+
<Button
|
|
463
|
+
variant={Variant.Secondary}
|
|
464
|
+
size={Size.MediumSmall}
|
|
465
|
+
nowrap
|
|
466
|
+
onclick={() => {
|
|
467
|
+
applyView(view);
|
|
468
|
+
}}>Apply</Button
|
|
469
|
+
>
|
|
470
|
+
<Button
|
|
471
|
+
variant={Variant.Secondary}
|
|
472
|
+
size={Size.MediumSmall}
|
|
473
|
+
nowrap
|
|
474
|
+
disabled={currentUserId === undefined}
|
|
475
|
+
onclick={() => {
|
|
476
|
+
unsubscribe(view);
|
|
477
|
+
}}>Unsubscribe</Button
|
|
478
|
+
>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</li>
|
|
482
|
+
{/each}
|
|
483
|
+
</ul>
|
|
484
|
+
{/if}
|
|
485
|
+
{:else}
|
|
486
|
+
<div class="save-tab">
|
|
487
|
+
<InfoAlert>
|
|
488
|
+
Save the table's current {sectionNoun.toLowerCase()} as a reusable named
|
|
489
|
+
{sectionNoun.toLowerCase()}.
|
|
490
|
+
</InfoAlert>
|
|
491
|
+
<!-- The InfoAlert above spans the full tab body so the
|
|
492
|
+
empty-state visuals are consistent across all three tabs.
|
|
493
|
+
The name input + Save button below stay grouped in a
|
|
494
|
+
narrower form column (`.save-tab-form`) so they keep
|
|
495
|
+
comfortable form proportions instead of stretching across
|
|
496
|
+
the whole 50vw modal. -->
|
|
497
|
+
<div class="save-tab-form">
|
|
498
|
+
<TextInput
|
|
499
|
+
placeholder={`${sectionNoun} name`}
|
|
500
|
+
bind:value={newViewName}
|
|
501
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
502
|
+
if (e.key === 'Enter') {
|
|
503
|
+
e.preventDefault();
|
|
504
|
+
saveNewView();
|
|
505
|
+
}
|
|
506
|
+
}}
|
|
507
|
+
/>
|
|
508
|
+
<!-- Wrap the button in a row so the flex column's default
|
|
509
|
+
`align-items: stretch` does not blow it up to the input's
|
|
510
|
+
width. The row itself can stretch; the button inside sits
|
|
511
|
+
at its natural content width like every other Button. -->
|
|
512
|
+
<div class="save-tab-actions">
|
|
513
|
+
<Button
|
|
514
|
+
disabled={!newViewName.trim()}
|
|
515
|
+
onclick={saveNewView}>Save Current</Button
|
|
516
|
+
>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
{/if}
|
|
521
|
+
</div>
|
|
522
|
+
{/snippet}
|
|
523
|
+
|
|
524
|
+
<DesktopModal
|
|
525
|
+
headerLabel="Table Configuration"
|
|
526
|
+
headerSubtitle={tableContext.datatableUuid
|
|
527
|
+
? `Table Identifier: ${tableContext.datatableUuid}`
|
|
528
|
+
: undefined}
|
|
529
|
+
onclose={() => onclose?.()}
|
|
530
|
+
buttons={[{ label: 'Close', callback: () => onclose?.() }]}
|
|
531
|
+
width={cssLength('50vw')}
|
|
532
|
+
minWidth={cssLength('40rem')}
|
|
533
|
+
>
|
|
534
|
+
{#if !remoteAvailable}
|
|
535
|
+
<div class="empty-state">
|
|
536
|
+
<Icon
|
|
537
|
+
name="circle-information"
|
|
538
|
+
size={Size.Large}
|
|
539
|
+
/>
|
|
540
|
+
<p>Saved layouts and filters are unavailable for this table.</p>
|
|
541
|
+
</div>
|
|
542
|
+
{:else}
|
|
543
|
+
<div class="config">
|
|
544
|
+
<HorizontalTabGroup
|
|
545
|
+
tabs={sectionTabs}
|
|
546
|
+
selected={selectedSectionTab}
|
|
547
|
+
grow
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
{/if}
|
|
551
|
+
</DesktopModal>
|
|
552
|
+
|
|
553
|
+
<!--
|
|
554
|
+
Share dialog stacked on top of the Configuration modal. Rendered as a
|
|
555
|
+
sibling (not nested in the modal body) so the second modal's overlay
|
|
556
|
+
covers the first - matching how every other layered modal in scoria
|
|
557
|
+
stacks. After a successful Save we refresh the saved-views list so the
|
|
558
|
+
owner's card's subscriber strip updates immediately, then close.
|
|
559
|
+
-->
|
|
560
|
+
{#if sharingView}
|
|
561
|
+
<ShareViewModal
|
|
562
|
+
view={sharingView}
|
|
563
|
+
onclose={() => {
|
|
564
|
+
sharingView = null;
|
|
565
|
+
}}
|
|
566
|
+
onsaved={async () => {
|
|
567
|
+
await tableContext.refreshSavedViews();
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
570
|
+
{/if}
|
|
571
|
+
|
|
572
|
+
<!--
|
|
573
|
+
Update-Options dialog stacked the same way as the Share dialog.
|
|
574
|
+
After a successful Save we refresh the saved views so the My-* card
|
|
575
|
+
picks up the new name / visibility immediately.
|
|
576
|
+
-->
|
|
577
|
+
{#if updatingOptionsView}
|
|
578
|
+
<UpdateViewOptionsModal
|
|
579
|
+
view={updatingOptionsView}
|
|
580
|
+
onclose={() => {
|
|
581
|
+
updatingOptionsView = null;
|
|
582
|
+
}}
|
|
583
|
+
onsaved={async () => {
|
|
584
|
+
await tableContext.refreshSavedViews();
|
|
585
|
+
}}
|
|
586
|
+
/>
|
|
587
|
+
{/if}
|
|
588
|
+
|
|
644
589
|
<style>.empty-state {
|
|
645
590
|
display: flex;
|
|
646
591
|
flex-flow: column nowrap;
|
|
@@ -799,4 +744,4 @@
|
|
|
799
744
|
display: flex;
|
|
800
745
|
flex-flow: row nowrap;
|
|
801
746
|
justify-content: flex-start;
|
|
802
|
-
}</style>
|
|
747
|
+
}</style>
|