@vuu-ui/vuu-shell 3.3.10 → 4.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/package.json +18 -16
  2. package/src/app-header/AppHeader.js +36 -26
  3. package/src/application-provider/ApplicationContext.js +2 -5
  4. package/src/application-provider/ApplicationProvider.js +8 -59
  5. package/src/feature/Feature.js +3 -34
  6. package/src/feature/index.js +1 -0
  7. package/src/feature-list/FeatureList.css.js +27 -5
  8. package/src/feature-list/FeatureList.js +117 -96
  9. package/src/index.js +3 -3
  10. package/src/left-nav/LeftNav.css.js +2 -4
  11. package/src/left-nav/LeftNav.js +139 -100
  12. package/src/module-registry/getRegisteredModules.js +12 -0
  13. package/src/persistence-manager/LocalWorkspacePersistenceService.js +129 -0
  14. package/src/persistence-manager/PersistenceProvider.js +10 -3
  15. package/src/persistence-manager/RemoteWorkspacePersistenceService.js +112 -0
  16. package/src/persistence-manager/WorkspacePersistenceService.js +9 -0
  17. package/src/persistence-manager/index.js +3 -0
  18. package/src/shell-layout-templates/context-panel/ApplicationSettingsContextPanel.js +24 -0
  19. package/src/shell-layout-templates/context-panel/ContextPanel.css.js +8 -8
  20. package/src/shell-layout-templates/context-panel/ContextPanel.js +17 -35
  21. package/src/shell-layout-templates/context-panel/index.js +1 -0
  22. package/src/shell-layout-templates/full-height-left-panel/useFullHeightLeftPanel.js +54 -40
  23. package/src/shell-layout-templates/inlay-left-panel/useInlayLeftPanel.js +54 -64
  24. package/src/shell-layout-templates/left-main-tabs/useLeftMainTabs.js +49 -28
  25. package/src/shell-layout-templates/side-panel/SidePanel.css.js +3 -1
  26. package/src/shell-layout-templates/simple-content-pane/useSimpleContentPane.js +43 -35
  27. package/src/shell.css.js +85 -2
  28. package/src/shell.js +148 -70
  29. package/src/workspace-management/LayoutList.js +59 -99
  30. package/src/workspace-management/SaveLayoutPanel.js +3 -3
  31. package/src/workspace-management/WorkspaceController.js +73 -0
  32. package/src/workspace-management/WorkspaceHost.js +194 -0
  33. package/src/workspace-management/WorkspacePersistentState.js +67 -0
  34. package/src/workspace-management/WorkspaceProvider.js +699 -178
  35. package/src/workspace-management/WorkspaceRepository.js +127 -0
  36. package/src/workspace-management/WorkspaceStartPanel.css.js +44 -0
  37. package/src/workspace-management/WorkspaceStartPanel.js +34 -0
  38. package/src/workspace-management/index.js +8 -0
  39. package/src/workspace-management/shell-workspace-components.js +59 -0
  40. package/src/workspace-management/useWorkspaceContextMenuItems.js +12 -6
  41. package/src/workspace-management/workspace-component-registry.js +30 -0
  42. package/src/workspace-management/workspace-schemas.js +257 -0
  43. package/types/app-header/AppHeader.d.ts +2 -2
  44. package/types/application-provider/ApplicationContext.d.ts +1 -6
  45. package/types/application-provider/ApplicationProvider.d.ts +5 -14
  46. package/types/feature/Feature.d.ts +2 -10
  47. package/types/feature/Loader.d.ts +1 -1
  48. package/types/feature/index.d.ts +2 -1
  49. package/types/feature-list/FeatureList.d.ts +4 -4
  50. package/types/index.d.ts +3 -3
  51. package/types/left-nav/LeftNav.d.ts +2 -1
  52. package/types/login/LoginPanel.d.ts +1 -1
  53. package/types/login/VuuLogo.d.ts +1 -1
  54. package/types/persistence-manager/PersistenceProvider.d.ts +7 -3
  55. package/types/persistence-manager/index.d.ts +3 -0
  56. package/types/shell-layout-templates/context-panel/ContextPanel.d.ts +9 -11
  57. package/types/shell-layout-templates/context-panel/index.d.ts +1 -0
  58. package/types/shell-layout-templates/full-height-left-panel/useFullHeightLeftPanel.d.ts +1 -1
  59. package/types/shell-layout-templates/inlay-left-panel/useInlayLeftPanel.d.ts +1 -1
  60. package/types/shell-layout-templates/left-main-tabs/useLeftMainTabs.d.ts +1 -1
  61. package/types/shell-layout-templates/side-panel/SidePanel.d.ts +1 -1
  62. package/types/shell-layout-templates/simple-content-pane/useSimpleContentPane.d.ts +1 -1
  63. package/types/shell-layout-templates/useShellLayout.d.ts +4 -2
  64. package/types/shell.d.ts +19 -15
  65. package/types/theme-switch/ThemeSwitch.d.ts +1 -1
  66. package/types/workspace-management/LayoutList.d.ts +2 -2
  67. package/types/workspace-management/LayoutTile.d.ts +1 -1
  68. package/types/workspace-management/SaveLayoutPanel.d.ts +2 -2
  69. package/types/workspace-management/WorkspaceProvider.d.ts +61 -50
  70. package/types/workspace-management/defaultWorkspaceJSON.d.ts +7 -6
  71. package/types/workspace-management/index.d.ts +8 -0
  72. package/types/workspace-management/useWorkspaceContextMenuItems.d.ts +1 -1
  73. package/src/feature/FeatureErrorBoundary.js +0 -33
  74. package/src/user-settings/SettingsForm.css.js +0 -10
  75. package/src/user-settings/SettingsForm.js +0 -141
  76. package/src/user-settings/UserSettingsPanel.css.js +0 -9
  77. package/src/user-settings/UserSettingsPanel.js +0 -29
  78. package/src/user-settings/index.js +0 -2
@@ -1,17 +1,16 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { VuuLogo } from "@vuu-ui/vuu-icons";
3
- import { Stack, useLayoutProviderDispatch } from "@vuu-ui/vuu-layout";
4
- import { Tab, Tabstrip } from "@vuu-ui/vuu-ui-controls";
3
+ import { GridLayout, GridLayoutItem } from "@heswell/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 dispatch = useLayoutProviderDispatch();
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
- return {
55
- ...acc,
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 } = navState;
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
- if (0 === activeTabIndex || 0 === currentIndex) {
78
- const width = getFullWidth(activeTabIndex, expanded);
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
- navState,
90
- onActiveChange
113
+ onActiveChange,
114
+ scopeIdentity,
115
+ setApplicationSetting,
116
+ visibleNavState
91
117
  ]);
92
- const displayStatus = getDisplayStatus(navState.activeTabIndex, navState.expanded);
118
+ const displayStatus = getDisplayStatus(visibleNavState.activeTabIndex, visibleNavState.expanded);
93
119
  const toggleExpanded = useCallback(()=>{
94
- const { activeTabIndex, expanded } = navState;
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
- dispatch({
102
- type: "layout-resize",
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
- navState,
111
- onTogglePrimaryMenu
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__*/ jsxs("div", {
144
+ return /*#__PURE__*/ jsx("div", {
120
145
  ...htmlAttributes,
121
146
  className: clsx(classBase, `${classBase}-${displayStatus}`),
122
147
  style: style,
123
- children: [
124
- /*#__PURE__*/ jsxs("div", {
125
- className: clsx(`${classBase}-menu-primary`),
126
- "data-mode": "dark",
127
- children: [
128
- /*#__PURE__*/ jsx("div", {
129
- className: "vuuLeftNav-logo",
130
- children: /*#__PURE__*/ jsx(VuuLogo, {})
131
- }),
132
- /*#__PURE__*/ jsx("div", {
133
- className: `${classBase}-main`,
134
- children: /*#__PURE__*/ jsxs(Tabstrip, {
135
- activeTabIndex: navState.activeTabIndex,
136
- animateSelectionThumb: false,
137
- className: `${classBase}-Tabstrip`,
138
- onActiveChange: handleTabSelection,
139
- orientation: "vertical",
140
- children: [
141
- /*#__PURE__*/ jsx(Tab, {
142
- "data-icon": "demo",
143
- label: "DEMO"
144
- }),
145
- /*#__PURE__*/ jsx(Tab, {
146
- "data-icon": "features",
147
- label: "VUU FEATURES"
148
- }),
149
- /*#__PURE__*/ jsx(Tab, {
150
- "data-icon": "tables",
151
- label: "VUU TABLES"
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
- /*#__PURE__*/ jsx(Tab, {
154
- "data-icon": "layouts",
155
- label: "MY LAYOUTS"
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
- /*#__PURE__*/ jsxs(Stack, {
174
- active: navState.activeTabIndex - 1,
175
- className: `${classBase}-menu-secondary`,
176
- showTabs: false,
177
- children: [
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
- export { PersistenceContext, PersistenceProvider, usePersistenceManager };
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 };