@vuu-ui/vuu-shell 3.3.17 → 4.0.0-alpha.10
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/package.json +18 -16
- package/src/app-header/AppHeader.js +36 -26
- package/src/application-provider/ApplicationContext.js +2 -5
- package/src/application-provider/ApplicationProvider.js +8 -59
- package/src/feature/Feature.js +3 -34
- package/src/feature/index.js +1 -0
- package/src/feature-list/FeatureList.css.js +27 -5
- package/src/feature-list/FeatureList.js +117 -96
- package/src/index.js +3 -3
- package/src/left-nav/LeftNav.css.js +2 -4
- package/src/left-nav/LeftNav.js +139 -100
- package/src/module-registry/getRegisteredModules.js +12 -0
- package/src/persistence-manager/LocalWorkspacePersistenceService.js +129 -0
- package/src/persistence-manager/PersistenceProvider.js +10 -3
- package/src/persistence-manager/RemoteWorkspacePersistenceService.js +112 -0
- package/src/persistence-manager/WorkspacePersistenceService.js +9 -0
- package/src/persistence-manager/index.js +3 -0
- package/src/shell-layout-templates/context-panel/ApplicationSettingsContextPanel.js +24 -0
- package/src/shell-layout-templates/context-panel/ContextPanel.css.js +8 -8
- package/src/shell-layout-templates/context-panel/ContextPanel.js +17 -35
- package/src/shell-layout-templates/context-panel/index.js +1 -0
- package/src/shell-layout-templates/full-height-left-panel/useFullHeightLeftPanel.js +54 -40
- package/src/shell-layout-templates/inlay-left-panel/useInlayLeftPanel.js +54 -64
- package/src/shell-layout-templates/left-main-tabs/useLeftMainTabs.js +49 -28
- package/src/shell-layout-templates/side-panel/SidePanel.css.js +3 -1
- package/src/shell-layout-templates/simple-content-pane/useSimpleContentPane.js +43 -35
- package/src/shell.css.js +85 -2
- package/src/shell.js +148 -70
- package/src/workspace-management/LayoutList.js +59 -99
- package/src/workspace-management/SaveLayoutPanel.js +3 -3
- package/src/workspace-management/WorkspaceController.js +73 -0
- package/src/workspace-management/WorkspaceHost.js +194 -0
- package/src/workspace-management/WorkspacePersistentState.js +67 -0
- package/src/workspace-management/WorkspaceProvider.js +699 -178
- package/src/workspace-management/WorkspaceRepository.js +127 -0
- package/src/workspace-management/WorkspaceStartPanel.css.js +44 -0
- package/src/workspace-management/WorkspaceStartPanel.js +34 -0
- package/src/workspace-management/index.js +8 -0
- package/src/workspace-management/shell-workspace-components.js +59 -0
- package/src/workspace-management/useWorkspaceContextMenuItems.js +13 -7
- package/src/workspace-management/workspace-component-registry.js +30 -0
- package/src/workspace-management/workspace-schemas.js +257 -0
- package/types/app-header/AppHeader.d.ts +2 -2
- package/types/application-provider/ApplicationContext.d.ts +1 -6
- package/types/application-provider/ApplicationProvider.d.ts +5 -14
- package/types/feature/Feature.d.ts +2 -10
- package/types/feature/Loader.d.ts +1 -1
- package/types/feature/index.d.ts +2 -1
- package/types/feature-list/FeatureList.d.ts +4 -4
- package/types/index.d.ts +3 -3
- package/types/left-nav/LeftNav.d.ts +2 -1
- package/types/login/LoginPanel.d.ts +1 -1
- package/types/login/VuuLogo.d.ts +1 -1
- package/types/persistence-manager/PersistenceProvider.d.ts +7 -3
- package/types/persistence-manager/index.d.ts +3 -0
- package/types/shell-layout-templates/context-panel/ContextPanel.d.ts +9 -11
- package/types/shell-layout-templates/context-panel/index.d.ts +1 -0
- package/types/shell-layout-templates/full-height-left-panel/useFullHeightLeftPanel.d.ts +1 -1
- package/types/shell-layout-templates/inlay-left-panel/useInlayLeftPanel.d.ts +1 -1
- package/types/shell-layout-templates/left-main-tabs/useLeftMainTabs.d.ts +1 -1
- package/types/shell-layout-templates/side-panel/SidePanel.d.ts +1 -1
- package/types/shell-layout-templates/simple-content-pane/useSimpleContentPane.d.ts +1 -1
- package/types/shell-layout-templates/useShellLayout.d.ts +4 -2
- package/types/shell.d.ts +19 -15
- package/types/theme-switch/ThemeSwitch.d.ts +1 -1
- package/types/workspace-management/LayoutList.d.ts +2 -2
- package/types/workspace-management/LayoutTile.d.ts +1 -1
- package/types/workspace-management/SaveLayoutPanel.d.ts +2 -2
- package/types/workspace-management/WorkspaceProvider.d.ts +61 -50
- package/types/workspace-management/defaultWorkspaceJSON.d.ts +7 -6
- package/types/workspace-management/index.d.ts +8 -0
- package/types/workspace-management/useWorkspaceContextMenuItems.d.ts +1 -1
- package/src/feature/FeatureErrorBoundary.js +0 -33
- package/src/user-settings/SettingsForm.css.js +0 -10
- package/src/user-settings/SettingsForm.js +0 -141
- package/src/user-settings/UserSettingsPanel.css.js +0 -9
- package/src/user-settings/UserSettingsPanel.js +0 -29
- package/src/user-settings/index.js +0 -2
package/src/left-nav/LeftNav.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { VuuLogo } from "@vuu-ui/vuu-icons";
|
|
3
|
-
import {
|
|
4
|
-
import { Tab, Tabstrip } from "@vuu-ui/vuu-ui-controls";
|
|
3
|
+
import { GridLayout, GridLayoutItem } from "@vuu-ui/grid-layout";
|
|
4
|
+
import { IconButton, Tab, Tabstrip } from "@vuu-ui/vuu-ui-controls";
|
|
5
5
|
import { hasFilterTableFeatureProps } from "@vuu-ui/vuu-utils";
|
|
6
6
|
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
7
7
|
import { useWindow } from "@salt-ds/window";
|
|
8
8
|
import clsx from "clsx";
|
|
9
|
-
import { useCallback, useMemo, useState } from "react";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
10
10
|
import { FeatureList } from "../feature-list/index.js";
|
|
11
|
-
import { LayoutList } from "../workspace-management/index.js";
|
|
11
|
+
import { LayoutList, useWorkspace } from "../workspace-management/index.js";
|
|
12
12
|
import LeftNav from "./LeftNav.css";
|
|
13
13
|
import { useFeatures } from "../feature-and-layout-provider/index.js";
|
|
14
|
-
import { Button } from "@salt-ds/core";
|
|
15
14
|
const classBase = "vuuLeftNav";
|
|
16
15
|
const getDisplayStatus = (activeTabIndex, expanded)=>{
|
|
17
16
|
if (0 === activeTabIndex) return expanded ? "menu-full" : "menu-icons";
|
|
@@ -32,8 +31,7 @@ const byModule = (f1, f2)=>{
|
|
|
32
31
|
throw Error("Invalid tableFeature");
|
|
33
32
|
};
|
|
34
33
|
const LeftNav_LeftNav = (props)=>{
|
|
35
|
-
const
|
|
36
|
-
const { "data-path": path, defaultExpanded = true, defaultActiveTabIndex = 0, onActiveChange, onTogglePrimaryMenu, sizeCollapsed = 80, sizeContent = 300, sizeExpanded = 240, style: styleProp, ...htmlAttributes } = props;
|
|
34
|
+
const { "data-path": path, defaultExpanded = true, defaultActiveTabIndex = 0, onActiveChange, onTogglePrimaryMenu, onWidthChange, sizeCollapsed = 80, sizeContent = 300, sizeExpanded = 240, style: styleProp, ...htmlAttributes } = props;
|
|
37
35
|
const targetWindow = useWindow();
|
|
38
36
|
useComponentCssInjection({
|
|
39
37
|
testId: "vuu-left-nav",
|
|
@@ -41,20 +39,45 @@ const LeftNav_LeftNav = (props)=>{
|
|
|
41
39
|
window: targetWindow
|
|
42
40
|
});
|
|
43
41
|
const { dynamicFeatures = [], tableFeatures = [] } = useFeatures();
|
|
42
|
+
const { getApplicationSetting, scopeIdentity, setApplicationSetting, status } = useWorkspace();
|
|
43
|
+
const storedActiveTabIndex = getApplicationSetting("leftNav.activeTabIndex");
|
|
44
|
+
const storedExpanded = getApplicationSetting("leftNav.expanded");
|
|
44
45
|
const [navState, setNavState] = useState({
|
|
45
|
-
activeTabIndex: defaultActiveTabIndex,
|
|
46
|
-
expanded: defaultExpanded
|
|
46
|
+
activeTabIndex: "number" == typeof storedActiveTabIndex ? storedActiveTabIndex : defaultActiveTabIndex,
|
|
47
|
+
expanded: "boolean" == typeof storedExpanded ? storedExpanded : defaultExpanded,
|
|
48
|
+
scopeIdentity: "loading" === status ? "" : scopeIdentity
|
|
47
49
|
});
|
|
50
|
+
const visibleNavState = "loading" !== status && navState.scopeIdentity === scopeIdentity ? navState : {
|
|
51
|
+
activeTabIndex: defaultActiveTabIndex,
|
|
52
|
+
expanded: defaultExpanded,
|
|
53
|
+
scopeIdentity: ""
|
|
54
|
+
};
|
|
55
|
+
useEffect(()=>{
|
|
56
|
+
"loading" === status ? setNavState({
|
|
57
|
+
activeTabIndex: defaultActiveTabIndex,
|
|
58
|
+
expanded: defaultExpanded,
|
|
59
|
+
scopeIdentity: ""
|
|
60
|
+
}) : setNavState({
|
|
61
|
+
activeTabIndex: "number" == typeof storedActiveTabIndex ? storedActiveTabIndex : defaultActiveTabIndex,
|
|
62
|
+
expanded: "boolean" == typeof storedExpanded ? storedExpanded : defaultExpanded,
|
|
63
|
+
scopeIdentity
|
|
64
|
+
});
|
|
65
|
+
}, [
|
|
66
|
+
defaultActiveTabIndex,
|
|
67
|
+
defaultExpanded,
|
|
68
|
+
scopeIdentity,
|
|
69
|
+
status,
|
|
70
|
+
storedActiveTabIndex,
|
|
71
|
+
storedExpanded
|
|
72
|
+
]);
|
|
48
73
|
const tableFeaturesByGroup = useMemo(()=>tableFeatures?.sort(byModule).reduce((acc, filterTableFeature)=>{
|
|
49
74
|
if (!hasFilterTableFeatureProps(filterTableFeature)) return acc;
|
|
50
75
|
{
|
|
51
76
|
const { table } = filterTableFeature.ComponentProps.tableSchema;
|
|
52
77
|
const key = `${table.module} Tables`;
|
|
53
78
|
if (!acc[key]) acc[key] = [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
[key]: acc[key].concat(filterTableFeature)
|
|
57
|
-
};
|
|
79
|
+
acc[key].push(filterTableFeature);
|
|
80
|
+
return acc;
|
|
58
81
|
}
|
|
59
82
|
}, {}), [
|
|
60
83
|
tableFeatures
|
|
@@ -67,48 +90,50 @@ const LeftNav_LeftNav = (props)=>{
|
|
|
67
90
|
sizeContent,
|
|
68
91
|
sizeExpanded
|
|
69
92
|
]);
|
|
93
|
+
useEffect(()=>{
|
|
94
|
+
onWidthChange?.(getFullWidth(visibleNavState.activeTabIndex, visibleNavState.expanded));
|
|
95
|
+
}, [
|
|
96
|
+
getFullWidth,
|
|
97
|
+
onWidthChange,
|
|
98
|
+
visibleNavState
|
|
99
|
+
]);
|
|
70
100
|
const handleTabSelection = useCallback((activeTabIndex)=>{
|
|
71
|
-
const { activeTabIndex: currentIndex, expanded } =
|
|
101
|
+
const { activeTabIndex: currentIndex, expanded } = visibleNavState;
|
|
72
102
|
const newState = {
|
|
73
103
|
activeTabIndex,
|
|
74
|
-
expanded
|
|
104
|
+
expanded,
|
|
105
|
+
scopeIdentity
|
|
75
106
|
};
|
|
76
107
|
setNavState(newState);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
dispatch({
|
|
80
|
-
type: "layout-resize",
|
|
81
|
-
path: "#vuu-side-panel",
|
|
82
|
-
size: width
|
|
83
|
-
});
|
|
84
|
-
}
|
|
108
|
+
getFullWidth(activeTabIndex, expanded);
|
|
109
|
+
setApplicationSetting("leftNav.activeTabIndex", activeTabIndex);
|
|
85
110
|
onActiveChange?.(activeTabIndex);
|
|
86
111
|
}, [
|
|
87
|
-
dispatch,
|
|
88
112
|
getFullWidth,
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
onActiveChange,
|
|
114
|
+
scopeIdentity,
|
|
115
|
+
setApplicationSetting,
|
|
116
|
+
visibleNavState
|
|
91
117
|
]);
|
|
92
|
-
const displayStatus = getDisplayStatus(
|
|
118
|
+
const displayStatus = getDisplayStatus(visibleNavState.activeTabIndex, visibleNavState.expanded);
|
|
93
119
|
const toggleExpanded = useCallback(()=>{
|
|
94
|
-
const { activeTabIndex, expanded } =
|
|
120
|
+
const { activeTabIndex, expanded } = visibleNavState;
|
|
95
121
|
const primaryMenuExpanded = !expanded;
|
|
96
122
|
const newState = {
|
|
97
123
|
activeTabIndex,
|
|
98
|
-
expanded: primaryMenuExpanded
|
|
124
|
+
expanded: primaryMenuExpanded,
|
|
125
|
+
scopeIdentity
|
|
99
126
|
};
|
|
100
127
|
setNavState(newState);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
path: "#vuu-side-panel",
|
|
104
|
-
size: getFullWidth(activeTabIndex, primaryMenuExpanded)
|
|
105
|
-
});
|
|
128
|
+
getFullWidth(activeTabIndex, primaryMenuExpanded);
|
|
129
|
+
setApplicationSetting("leftNav.expanded", primaryMenuExpanded);
|
|
106
130
|
onTogglePrimaryMenu?.(primaryMenuExpanded);
|
|
107
131
|
}, [
|
|
108
|
-
dispatch,
|
|
109
132
|
getFullWidth,
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
onTogglePrimaryMenu,
|
|
134
|
+
scopeIdentity,
|
|
135
|
+
setApplicationSetting,
|
|
136
|
+
visibleNavState
|
|
112
137
|
]);
|
|
113
138
|
const style = {
|
|
114
139
|
...styleProp,
|
|
@@ -116,79 +141,93 @@ const LeftNav_LeftNav = (props)=>{
|
|
|
116
141
|
"--nav-menu-expanded-width": `${sizeExpanded}px`,
|
|
117
142
|
"--nav-menu-content-width": `${sizeContent}px`
|
|
118
143
|
};
|
|
119
|
-
return /*#__PURE__*/
|
|
144
|
+
return /*#__PURE__*/ jsx("div", {
|
|
120
145
|
...htmlAttributes,
|
|
121
146
|
className: clsx(classBase, `${classBase}-${displayStatus}`),
|
|
122
147
|
style: style,
|
|
123
|
-
children:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
children: /*#__PURE__*/ jsxs(GridLayout, {
|
|
149
|
+
colsAndRows: {
|
|
150
|
+
cols: [
|
|
151
|
+
`${visibleNavState.expanded ? sizeExpanded : sizeCollapsed}px`,
|
|
152
|
+
`${0 === visibleNavState.activeTabIndex ? 0 : sizeContent}px`
|
|
153
|
+
],
|
|
154
|
+
rows: [
|
|
155
|
+
"1fr"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
id: "vuu-left-nav-grid",
|
|
159
|
+
children: [
|
|
160
|
+
/*#__PURE__*/ jsxs(GridLayoutItem, {
|
|
161
|
+
className: clsx(`${classBase}-menu-primary`),
|
|
162
|
+
"data-mode": "dark",
|
|
163
|
+
id: "vuu-left-nav-primary",
|
|
164
|
+
style: {
|
|
165
|
+
gridArea: "1/1/2/2"
|
|
166
|
+
},
|
|
167
|
+
children: [
|
|
168
|
+
/*#__PURE__*/ jsx("div", {
|
|
169
|
+
className: "vuuLeftNav-logo",
|
|
170
|
+
children: /*#__PURE__*/ jsx(VuuLogo, {})
|
|
171
|
+
}),
|
|
172
|
+
/*#__PURE__*/ jsx("div", {
|
|
173
|
+
className: `${classBase}-main`,
|
|
174
|
+
children: /*#__PURE__*/ jsxs(Tabstrip, {
|
|
175
|
+
activeTabIndex: visibleNavState.activeTabIndex,
|
|
176
|
+
animateSelectionThumb: false,
|
|
177
|
+
className: `${classBase}-Tabstrip`,
|
|
178
|
+
onActiveChange: handleTabSelection,
|
|
179
|
+
orientation: "vertical",
|
|
180
|
+
children: [
|
|
181
|
+
/*#__PURE__*/ jsx(Tab, {
|
|
182
|
+
"data-icon": "demo",
|
|
183
|
+
label: "DEMO"
|
|
184
|
+
}),
|
|
185
|
+
/*#__PURE__*/ jsx(Tab, {
|
|
186
|
+
"data-icon": "features",
|
|
187
|
+
label: "VUU FEATURES"
|
|
188
|
+
}),
|
|
189
|
+
/*#__PURE__*/ jsx(Tab, {
|
|
190
|
+
"data-icon": "tables",
|
|
191
|
+
label: "VUU TABLES"
|
|
192
|
+
}),
|
|
193
|
+
/*#__PURE__*/ jsx(Tab, {
|
|
194
|
+
"data-icon": "layouts",
|
|
195
|
+
label: "MY LAYOUTS"
|
|
196
|
+
})
|
|
197
|
+
]
|
|
198
|
+
})
|
|
199
|
+
}),
|
|
200
|
+
/*#__PURE__*/ jsx("div", {
|
|
201
|
+
className: "vuuLeftNav-buttonBar",
|
|
202
|
+
children: /*#__PURE__*/ jsx(IconButton, {
|
|
203
|
+
className: clsx("vuuLeftNav-toggleButton", {
|
|
204
|
+
"vuuLeftNav-toggleButton-open": displayStatus.startsWith("menu-full"),
|
|
205
|
+
"vuuLeftNav-toggleButton-closed": displayStatus.startsWith("menu-icons")
|
|
152
206
|
}),
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
})
|
|
157
|
-
]
|
|
158
|
-
})
|
|
159
|
-
}),
|
|
160
|
-
/*#__PURE__*/ jsx("div", {
|
|
161
|
-
className: "vuuLeftNav-buttonBar",
|
|
162
|
-
children: /*#__PURE__*/ jsx(Button, {
|
|
163
|
-
className: clsx("vuuLeftNav-toggleButton", {
|
|
164
|
-
"vuuLeftNav-toggleButton-open": displayStatus.startsWith("menu-full"),
|
|
165
|
-
"vuuLeftNav-toggleButton-closed": displayStatus.startsWith("menu-icons")
|
|
166
|
-
}),
|
|
167
|
-
"data-icon": displayStatus.startsWith("menu-full") ? "chevron-left" : "chevron-right",
|
|
168
|
-
onClick: toggleExpanded
|
|
207
|
+
icon: displayStatus.startsWith("menu-full") ? "chevron-left" : "chevron-right",
|
|
208
|
+
onClick: toggleExpanded
|
|
209
|
+
})
|
|
169
210
|
})
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
/*#__PURE__*/ jsx(FeatureList, {
|
|
211
|
+
]
|
|
212
|
+
}),
|
|
213
|
+
/*#__PURE__*/ jsx(GridLayoutItem, {
|
|
214
|
+
className: `${classBase}-menu-secondary`,
|
|
215
|
+
id: "vuu-left-nav-secondary",
|
|
216
|
+
style: {
|
|
217
|
+
gridArea: "1/2/2/3"
|
|
218
|
+
},
|
|
219
|
+
children: 1 === visibleNavState.activeTabIndex ? /*#__PURE__*/ jsx(FeatureList, {
|
|
179
220
|
features: dynamicFeatures,
|
|
180
221
|
title: "VUU FEATURES"
|
|
181
|
-
}),
|
|
182
|
-
/*#__PURE__*/ jsx(FeatureList, {
|
|
222
|
+
}) : 2 === visibleNavState.activeTabIndex ? /*#__PURE__*/ jsx(FeatureList, {
|
|
183
223
|
features: tableFeaturesByGroup,
|
|
184
224
|
title: "VUU TABLES"
|
|
185
|
-
}),
|
|
186
|
-
/*#__PURE__*/ jsx(LayoutList, {
|
|
225
|
+
}) : 3 === visibleNavState.activeTabIndex ? /*#__PURE__*/ jsx(LayoutList, {
|
|
187
226
|
title: "MY LAYOUTS"
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
227
|
+
}) : null
|
|
228
|
+
})
|
|
229
|
+
]
|
|
230
|
+
}, `${visibleNavState.expanded}-${visibleNavState.activeTabIndex}`)
|
|
192
231
|
});
|
|
193
232
|
};
|
|
194
233
|
export { LeftNav_LeftNav as LeftNav };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const getRegisteredModules = async (registryUrl, bearerToken)=>{
|
|
2
|
+
console.log(`getRegisteredModules ${registryUrl}`);
|
|
3
|
+
const response = await fetch(registryUrl, {
|
|
4
|
+
cache: "no-store",
|
|
5
|
+
headers: {
|
|
6
|
+
Authorization: `Bearer ${bearerToken}`
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
if (!response.ok) throw Error("bad return from module registry");
|
|
10
|
+
return await response.json();
|
|
11
|
+
};
|
|
12
|
+
export { getRegisteredModules };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { WorkspaceValidationError, cloneWorkspaceValue, validateApplicationSessionV1, validateNamedWorkspaceDefinitionV1, validateNamedWorkspaceMetadataV1, validateWorkspaceSnapshotRecordV1, workspaceMetadata } from "../workspace-management/workspace-schemas.js";
|
|
2
|
+
import { WorkspacePersistenceError } from "./WorkspacePersistenceService.js";
|
|
3
|
+
const validateScope = (scope)=>{
|
|
4
|
+
for (const [name, value] of Object.entries(scope))if (!value) throw new WorkspacePersistenceError(`Persistence scope ${name} must not be empty`, "construct");
|
|
5
|
+
};
|
|
6
|
+
const requireId = (id, operation)=>{
|
|
7
|
+
if (!id) throw new WorkspacePersistenceError("Entity id must not be empty", operation);
|
|
8
|
+
return id;
|
|
9
|
+
};
|
|
10
|
+
class LocalWorkspacePersistenceService {
|
|
11
|
+
scope;
|
|
12
|
+
#storage;
|
|
13
|
+
#prefix;
|
|
14
|
+
constructor(scope, storage = localStorage){
|
|
15
|
+
validateScope(scope);
|
|
16
|
+
this.scope = Object.freeze({
|
|
17
|
+
...scope
|
|
18
|
+
});
|
|
19
|
+
this.#storage = storage;
|
|
20
|
+
this.#prefix = [
|
|
21
|
+
"vuu-workspace",
|
|
22
|
+
scope.applicationNamespace,
|
|
23
|
+
scope.applicationId,
|
|
24
|
+
scope.userId
|
|
25
|
+
].map(encodeURIComponent).join(":");
|
|
26
|
+
}
|
|
27
|
+
async loadApplicationSession() {
|
|
28
|
+
return this.#read("application-session", void 0, validateApplicationSessionV1);
|
|
29
|
+
}
|
|
30
|
+
async saveApplicationSession(session) {
|
|
31
|
+
this.#write("application-session", void 0, validateApplicationSessionV1(session));
|
|
32
|
+
}
|
|
33
|
+
async deleteApplicationSession() {
|
|
34
|
+
this.#remove("application-session");
|
|
35
|
+
}
|
|
36
|
+
async listWorkspaceSnapshots() {
|
|
37
|
+
return this.#ids("snapshot").map((id)=>{
|
|
38
|
+
const snapshot = this.#read("snapshot", id, validateWorkspaceSnapshotRecordV1);
|
|
39
|
+
if (!snapshot) throw new WorkspacePersistenceError(`Snapshot index references missing workspace "${id}"`, "listWorkspaceSnapshots");
|
|
40
|
+
return snapshot;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async loadWorkspaceSnapshot(workspaceInstanceId) {
|
|
44
|
+
return this.#read("snapshot", requireId(workspaceInstanceId, "loadWorkspaceSnapshot"), validateWorkspaceSnapshotRecordV1);
|
|
45
|
+
}
|
|
46
|
+
async saveWorkspaceSnapshot(snapshot) {
|
|
47
|
+
const validated = validateWorkspaceSnapshotRecordV1(snapshot);
|
|
48
|
+
this.#write("snapshot", validated.workspaceInstanceId, validated);
|
|
49
|
+
this.#addId("snapshot", validated.workspaceInstanceId);
|
|
50
|
+
}
|
|
51
|
+
async deleteWorkspaceSnapshot(workspaceInstanceId) {
|
|
52
|
+
const id = requireId(workspaceInstanceId, "deleteWorkspaceSnapshot");
|
|
53
|
+
this.#remove("snapshot", id);
|
|
54
|
+
this.#deleteId("snapshot", id);
|
|
55
|
+
}
|
|
56
|
+
async listNamedWorkspaceMetadata() {
|
|
57
|
+
return this.#ids("definition").map((id)=>{
|
|
58
|
+
const metadata = this.#read("definition-metadata", id, validateNamedWorkspaceMetadataV1);
|
|
59
|
+
if (!metadata) throw new WorkspacePersistenceError(`Definition index references missing metadata "${id}"`, "listNamedWorkspaceMetadata");
|
|
60
|
+
return metadata;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async loadNamedWorkspaceDefinition(id) {
|
|
64
|
+
return this.#read("definition", requireId(id, "loadNamedWorkspaceDefinition"), validateNamedWorkspaceDefinitionV1);
|
|
65
|
+
}
|
|
66
|
+
async saveNamedWorkspaceDefinition(definition) {
|
|
67
|
+
const validated = validateNamedWorkspaceDefinitionV1(definition);
|
|
68
|
+
this.#write("definition", validated.id, validated);
|
|
69
|
+
this.#write("definition-metadata", validated.id, workspaceMetadata(validated));
|
|
70
|
+
this.#addId("definition", validated.id);
|
|
71
|
+
}
|
|
72
|
+
async deleteNamedWorkspaceDefinition(id) {
|
|
73
|
+
const validatedId = requireId(id, "deleteNamedWorkspaceDefinition");
|
|
74
|
+
this.#remove("definition", validatedId);
|
|
75
|
+
this.#remove("definition-metadata", validatedId);
|
|
76
|
+
this.#deleteId("definition", validatedId);
|
|
77
|
+
}
|
|
78
|
+
#key(entity, id) {
|
|
79
|
+
return `${this.#prefix}:${encodeURIComponent(entity)}${void 0 === id ? "" : `:${encodeURIComponent(id)}`}`;
|
|
80
|
+
}
|
|
81
|
+
#read(entity, id, validator) {
|
|
82
|
+
const stored = this.#storage.getItem(this.#key(entity, id));
|
|
83
|
+
if (null === stored) return;
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(stored);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
89
|
+
throw new WorkspacePersistenceError(`Stored ${entity} is not valid JSON: ${error.message}`, `load:${entity}`);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return cloneWorkspaceValue(validator(parsed));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!(error instanceof WorkspaceValidationError)) throw error;
|
|
95
|
+
throw new WorkspacePersistenceError(`Stored ${entity} is invalid: ${error.message}`, `load:${entity}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
#write(entity, id, value) {
|
|
99
|
+
this.#storage.setItem(this.#key(entity, id), JSON.stringify(cloneWorkspaceValue(value)));
|
|
100
|
+
}
|
|
101
|
+
#remove(entity, id) {
|
|
102
|
+
this.#storage.removeItem(this.#key(entity, id));
|
|
103
|
+
}
|
|
104
|
+
#ids(entity) {
|
|
105
|
+
const stored = this.#storage.getItem(this.#key(`${entity}-index`));
|
|
106
|
+
if (null === stored) return [];
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(stored);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
112
|
+
throw new WorkspacePersistenceError(`Stored ${entity} index is not valid JSON: ${error.message}`, `list:${entity}`);
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(parsed) || parsed.some((id)=>"string" != typeof id || !id) || new Set(parsed).size !== parsed.length) throw new WorkspacePersistenceError(`Stored ${entity} index is invalid`, `list:${entity}`);
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
#addId(entity, id) {
|
|
118
|
+
const ids = this.#ids(entity);
|
|
119
|
+
if (!ids.includes(id)) this.#storage.setItem(this.#key(`${entity}-index`), JSON.stringify([
|
|
120
|
+
...ids,
|
|
121
|
+
id
|
|
122
|
+
]));
|
|
123
|
+
}
|
|
124
|
+
#deleteId(entity, id) {
|
|
125
|
+
const ids = this.#ids(entity).filter((existing)=>existing !== id);
|
|
126
|
+
this.#storage.setItem(this.#key(`${entity}-index`), JSON.stringify(ids));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export { LocalWorkspacePersistenceService };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
const PersistenceContext = /*#__PURE__*/ createContext({});
|
|
4
|
-
const PersistenceProvider = ({ children, persistenceManager })=>/*#__PURE__*/ jsx(PersistenceContext.Provider, {
|
|
4
|
+
const PersistenceProvider = ({ children, persistenceManager, workspacePersistenceService })=>/*#__PURE__*/ jsx(PersistenceContext.Provider, {
|
|
5
5
|
value: {
|
|
6
|
-
persistenceManager
|
|
6
|
+
persistenceManager,
|
|
7
|
+
workspacePersistenceService
|
|
7
8
|
},
|
|
8
9
|
children: children
|
|
9
10
|
});
|
|
@@ -11,4 +12,10 @@ const usePersistenceManager = ()=>{
|
|
|
11
12
|
const { persistenceManager } = useContext(PersistenceContext);
|
|
12
13
|
return persistenceManager;
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
+
const useWorkspacePersistenceService = ()=>{
|
|
16
|
+
const { workspacePersistenceService } = useContext(PersistenceContext);
|
|
17
|
+
if (!workspacePersistenceService) throw new Error("useWorkspacePersistenceService requires a configured PersistenceProvider");
|
|
18
|
+
return workspacePersistenceService;
|
|
19
|
+
};
|
|
20
|
+
const useOptionalWorkspacePersistenceService = ()=>useContext(PersistenceContext).workspacePersistenceService;
|
|
21
|
+
export { PersistenceContext, PersistenceProvider, useOptionalWorkspacePersistenceService, usePersistenceManager, useWorkspacePersistenceService };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { cloneWorkspaceValue, validateApplicationSessionV1, validateNamedWorkspaceDefinitionV1, validateNamedWorkspaceMetadataV1, validateWorkspaceSnapshotRecordV1 } from "../workspace-management/workspace-schemas.js";
|
|
2
|
+
import { WorkspacePersistenceError } from "./WorkspacePersistenceService.js";
|
|
3
|
+
const trimSlash = (value)=>value.replace(/\/+$/, "");
|
|
4
|
+
const requireId = (id, operation)=>{
|
|
5
|
+
if (!id) throw new WorkspacePersistenceError("Entity id must not be empty", operation);
|
|
6
|
+
return encodeURIComponent(id);
|
|
7
|
+
};
|
|
8
|
+
class RemoteWorkspacePersistenceService {
|
|
9
|
+
scope;
|
|
10
|
+
#baseUrl;
|
|
11
|
+
#requestInit;
|
|
12
|
+
#requestInitProvider;
|
|
13
|
+
#fetch;
|
|
14
|
+
constructor(scope, { baseUrl, requestInit, requestInitProvider, fetch: fetchImplementation = globalThis.fetch }){
|
|
15
|
+
if (!scope.applicationNamespace || !scope.applicationId || !scope.userId || !baseUrl) throw new WorkspacePersistenceError("Remote persistence scope and base URL must not be empty", "construct");
|
|
16
|
+
this.scope = Object.freeze({
|
|
17
|
+
...scope
|
|
18
|
+
});
|
|
19
|
+
this.#baseUrl = trimSlash(baseUrl);
|
|
20
|
+
this.#requestInit = requestInit;
|
|
21
|
+
this.#requestInitProvider = requestInitProvider;
|
|
22
|
+
this.#fetch = fetchImplementation;
|
|
23
|
+
}
|
|
24
|
+
async loadApplicationSession() {
|
|
25
|
+
return this.#optional("loadApplicationSession", "application-session", validateApplicationSessionV1);
|
|
26
|
+
}
|
|
27
|
+
async saveApplicationSession(session) {
|
|
28
|
+
await this.#void("saveApplicationSession", "application-session", "PUT", validateApplicationSessionV1(session));
|
|
29
|
+
}
|
|
30
|
+
async deleteApplicationSession() {
|
|
31
|
+
await this.#void("deleteApplicationSession", "application-session", "DELETE");
|
|
32
|
+
}
|
|
33
|
+
async listWorkspaceSnapshots() {
|
|
34
|
+
const value = await this.#json("listWorkspaceSnapshots", "snapshots");
|
|
35
|
+
if (!Array.isArray(value)) throw new WorkspacePersistenceError("Remote snapshot list response must be an array", "listWorkspaceSnapshots");
|
|
36
|
+
return value.map(validateWorkspaceSnapshotRecordV1);
|
|
37
|
+
}
|
|
38
|
+
async loadWorkspaceSnapshot(workspaceInstanceId) {
|
|
39
|
+
return this.#optional("loadWorkspaceSnapshot", `snapshots/${requireId(workspaceInstanceId, "loadWorkspaceSnapshot")}`, validateWorkspaceSnapshotRecordV1);
|
|
40
|
+
}
|
|
41
|
+
async saveWorkspaceSnapshot(snapshot) {
|
|
42
|
+
const validated = validateWorkspaceSnapshotRecordV1(snapshot);
|
|
43
|
+
await this.#void("saveWorkspaceSnapshot", `snapshots/${encodeURIComponent(validated.workspaceInstanceId)}`, "PUT", validated);
|
|
44
|
+
}
|
|
45
|
+
async deleteWorkspaceSnapshot(workspaceInstanceId) {
|
|
46
|
+
await this.#void("deleteWorkspaceSnapshot", `snapshots/${requireId(workspaceInstanceId, "deleteWorkspaceSnapshot")}`, "DELETE");
|
|
47
|
+
}
|
|
48
|
+
async listNamedWorkspaceMetadata() {
|
|
49
|
+
const value = await this.#json("listNamedWorkspaceMetadata", "named-workspaces");
|
|
50
|
+
if (!Array.isArray(value)) throw new WorkspacePersistenceError("Remote workspace metadata response must be an array", "listNamedWorkspaceMetadata");
|
|
51
|
+
return value.map(validateNamedWorkspaceMetadataV1);
|
|
52
|
+
}
|
|
53
|
+
async loadNamedWorkspaceDefinition(id) {
|
|
54
|
+
return this.#optional("loadNamedWorkspaceDefinition", `named-workspaces/${requireId(id, "loadNamedWorkspaceDefinition")}`, validateNamedWorkspaceDefinitionV1);
|
|
55
|
+
}
|
|
56
|
+
async saveNamedWorkspaceDefinition(definition) {
|
|
57
|
+
const validated = validateNamedWorkspaceDefinitionV1(definition);
|
|
58
|
+
await this.#void("saveNamedWorkspaceDefinition", `named-workspaces/${encodeURIComponent(validated.id)}`, "PUT", validated);
|
|
59
|
+
}
|
|
60
|
+
async deleteNamedWorkspaceDefinition(id) {
|
|
61
|
+
await this.#void("deleteNamedWorkspaceDefinition", `named-workspaces/${requireId(id, "deleteNamedWorkspaceDefinition")}`, "DELETE");
|
|
62
|
+
}
|
|
63
|
+
async #optional(operation, path, validator) {
|
|
64
|
+
const response = await this.#request(operation, path, "GET");
|
|
65
|
+
if (404 === response.status) return;
|
|
66
|
+
await this.#assertOk(response, operation);
|
|
67
|
+
return cloneWorkspaceValue(validator(await response.json()));
|
|
68
|
+
}
|
|
69
|
+
async #json(operation, path) {
|
|
70
|
+
const response = await this.#request(operation, path, "GET");
|
|
71
|
+
await this.#assertOk(response, operation);
|
|
72
|
+
return response.json();
|
|
73
|
+
}
|
|
74
|
+
async #void(operation, path, method, body) {
|
|
75
|
+
const response = await this.#request(operation, path, method, body);
|
|
76
|
+
await this.#assertOk(response, operation);
|
|
77
|
+
}
|
|
78
|
+
async #request(operation, path, method, body) {
|
|
79
|
+
const provided = this.#requestInitProvider ? await this.#requestInitProvider() : {};
|
|
80
|
+
const configured = this.#requestInit ?? {};
|
|
81
|
+
const headers = new Headers(configured.headers);
|
|
82
|
+
new Headers(provided.headers).forEach((value, key)=>{
|
|
83
|
+
headers.set(key, value);
|
|
84
|
+
});
|
|
85
|
+
headers.set("Accept", "application/json");
|
|
86
|
+
headers.set("X-Vuu-Application-Namespace", this.scope.applicationNamespace);
|
|
87
|
+
headers.set("X-Vuu-Application-Id", this.scope.applicationId);
|
|
88
|
+
headers.set("X-Vuu-User-Id", this.scope.userId);
|
|
89
|
+
if (void 0 !== body) headers.set("Content-Type", "application/json");
|
|
90
|
+
const scopePath = [
|
|
91
|
+
this.scope.applicationNamespace,
|
|
92
|
+
this.scope.applicationId,
|
|
93
|
+
this.scope.userId
|
|
94
|
+
].map(encodeURIComponent).join("/");
|
|
95
|
+
try {
|
|
96
|
+
return await this.#fetch(`${this.#baseUrl}/${scopePath}/${path}`, {
|
|
97
|
+
...configured,
|
|
98
|
+
...provided,
|
|
99
|
+
headers,
|
|
100
|
+
method,
|
|
101
|
+
body: void 0 === body ? void 0 : JSON.stringify(cloneWorkspaceValue(body))
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!(error instanceof Error)) throw error;
|
|
105
|
+
throw new WorkspacePersistenceError(error.message, operation);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async #assertOk(response, operation) {
|
|
109
|
+
if (!response.ok) throw new WorkspacePersistenceError(response.statusText || `Remote persistence request failed (${response.status})`, operation, response.status);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export { RemoteWorkspacePersistenceService };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class WorkspacePersistenceError extends Error {
|
|
2
|
+
operation;
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, operation, status){
|
|
5
|
+
super(message), this.operation = operation, this.status = status;
|
|
6
|
+
this.name = "WorkspacePersistenceError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export { WorkspacePersistenceError };
|
|
@@ -3,3 +3,6 @@ export * from "./PersistenceProvider.js";
|
|
|
3
3
|
export * from "./LocalPersistenceManager.js";
|
|
4
4
|
export * from "./RemotePersistenceManager.js";
|
|
5
5
|
export * from "./StaticPersistenceManager.js";
|
|
6
|
+
export * from "./WorkspacePersistenceService.js";
|
|
7
|
+
export * from "./LocalWorkspacePersistenceService.js";
|
|
8
|
+
export * from "./RemoteWorkspacePersistenceService.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { useWorkspace } from "../../workspace-management/index.js";
|
|
4
|
+
import { ContextPanel } from "./ContextPanel.js";
|
|
5
|
+
const ApplicationSettingsContextPanel = ()=>{
|
|
6
|
+
const { getApplicationSetting, setApplicationSetting } = useWorkspace();
|
|
7
|
+
const expanded = true === getApplicationSetting("applicationSettings.panelOpen");
|
|
8
|
+
const handleClose = useCallback(()=>{
|
|
9
|
+
setApplicationSetting("applicationSettings.panelOpen", false);
|
|
10
|
+
}, [
|
|
11
|
+
setApplicationSetting
|
|
12
|
+
]);
|
|
13
|
+
return /*#__PURE__*/ jsx(ContextPanel, {
|
|
14
|
+
content: /*#__PURE__*/ jsx("div", {
|
|
15
|
+
children: "Application settings"
|
|
16
|
+
}),
|
|
17
|
+
expanded: expanded,
|
|
18
|
+
id: "vuu-context-panel",
|
|
19
|
+
onClose: handleClose,
|
|
20
|
+
overlay: true,
|
|
21
|
+
title: "Settings"
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export { ApplicationSettingsContextPanel };
|