@vuu-ui/vuu-shell 3.3.11 → 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,209 +1,730 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { isLayoutJSON, resolveJSONPath } from "@vuu-ui/vuu-layout";
2
+ import { GRID_LAYOUT_DOCUMENT_KIND, GRID_LAYOUT_DOCUMENT_VERSION, createSequentialGridLayoutIdAllocator, remapGridLayoutDocumentIds } from "@heswell/grid-layout";
3
3
  import { useNotifications } from "@vuu-ui/vuu-notifications";
4
- import { VuuShellLocation, WorkspaceContext, logger } from "@vuu-ui/vuu-utils";
5
- import { useCallback, useContext, useEffect, useRef, useState } from "react";
6
- import { usePersistenceManager } from "../persistence-manager/index.js";
7
- import { getWorkspaceWithLayoutJSON, loadingJSON } from "./defaultWorkspaceJSON.js";
8
- const { info: info } = logger("useLayoutManager");
9
- const ensureLayoutHasTitle = (layout, layoutMetadata)=>{
10
- if (layout.props?.title !== void 0) return layout;
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
+ import { useOptionalWorkspacePersistenceService } from "../persistence-manager/index.js";
6
+ import { WorkspaceController } from "./WorkspaceController.js";
7
+ import { APPLICATION_SESSION_VERSION, NAMED_WORKSPACE_DEFINITION_VERSION, WORKSPACE_SNAPSHOT_VERSION, cloneWorkspaceValue } from "./workspace-schemas.js";
8
+ const emptyLayout = {
9
+ components: [],
10
+ kind: GRID_LAYOUT_DOCUMENT_KIND,
11
+ layout: {
12
+ columns: [
13
+ "1fr"
14
+ ],
15
+ id: "empty-workspace",
16
+ items: [
17
+ {
18
+ column: {
19
+ span: 1,
20
+ start: 1
21
+ },
22
+ id: "empty-workspace-placeholder",
23
+ row: {
24
+ span: 1,
25
+ start: 1
26
+ }
27
+ }
28
+ ],
29
+ placeholderIds: [
30
+ "empty-workspace-placeholder"
31
+ ],
32
+ rows: [
33
+ "1fr"
34
+ ],
35
+ stacks: []
36
+ },
37
+ version: GRID_LAYOUT_DOCUMENT_VERSION
38
+ };
39
+ const emptyWorkspaceSnapshot = {
40
+ componentState: {},
41
+ layout: emptyLayout,
42
+ version: WORKSPACE_SNAPSHOT_VERSION
43
+ };
44
+ const missing = async ()=>{
45
+ throw new Error("WorkspaceProvider is not mounted");
46
+ };
47
+ const WorkspaceContext = /*#__PURE__*/ createContext({
48
+ activeWorkspaceInstanceId: null,
49
+ closeWorkspace: missing,
50
+ createWorkspaceFromSnapshot: async ()=>{
51
+ throw new Error("WorkspaceProvider is not mounted");
52
+ },
53
+ controllers: [],
54
+ deleteNamedWorkspace: missing,
55
+ getApplicationSetting: ()=>void 0,
56
+ getApplicationSettings: ()=>void 0,
57
+ namedWorkspaces: [],
58
+ openNamedWorkspace: missing,
59
+ openSystemWorkspace: missing,
60
+ renameWorkspace: missing,
61
+ saveActiveWorkspaceAs: missing,
62
+ saveApplicationSettings: missing,
63
+ selectWorkspace: missing,
64
+ setApplicationSetting: missing,
65
+ settings: {},
66
+ scopeIdentity: "missing",
67
+ status: "loading",
68
+ systemWorkspaces: [],
69
+ resetApplication: missing
70
+ });
71
+ let nextWorkspaceInstance = 1;
72
+ const createInstanceId = ()=>`workspace-${Date.now().toString(36)}-${nextWorkspaceInstance++}`;
73
+ const errorFrom = (cause, message)=>cause instanceof Error ? cause : new Error(message);
74
+ class StaleWorkspaceScopeError extends Error {
75
+ constructor(){
76
+ super("Workspace operation belongs to an inactive persistence scope");
77
+ this.name = "StaleWorkspaceScopeError";
78
+ }
79
+ }
80
+ const remapSnapshot = (source, instanceId)=>{
81
+ const { document, mappings } = remapGridLayoutDocumentIds(source.layout, createSequentialGridLayoutIdAllocator(instanceId));
82
+ const componentIds = new Map(mappings.filter(({ kind })=>"component" === kind).map(({ newId, oldId })=>[
83
+ oldId,
84
+ newId
85
+ ]));
11
86
  return {
12
- ...layout,
13
- props: {
14
- ...layout.props,
15
- title: layoutMetadata.name
16
- }
87
+ version: WORKSPACE_SNAPSHOT_VERSION,
88
+ layout: document,
89
+ componentState: Object.fromEntries(Object.entries(source.componentState).map(([id, state])=>[
90
+ componentIds.get(id) ?? id,
91
+ cloneWorkspaceValue(state)
92
+ ]))
17
93
  };
18
94
  };
19
- const loadingApplicationJSON = {
20
- workspaceJSON: loadingJSON
21
- };
22
- const WorkspaceProvider = ({ TabstripProps, children, layoutJSON, activeLayoutIndex, layoutPlaceholderJSON, showTabs, workspaceJSON: customWorkspaceJSON })=>{
23
- const [layoutMetadata, setLayoutMetadata] = useState([]);
24
- const [, forceRefresh] = useState({});
95
+ const WorkspaceProvider = ({ activeLayoutIndex, children, componentRenderers, defaultWorkspace, layoutJSON, layoutPlaceholderJSON, persistenceService: serviceProp, settingsCodecs, showTabs, systemWorkspaces = [], TabstripProps, userId, workspaceJSON })=>{
96
+ const inheritedService = useOptionalWorkspacePersistenceService();
97
+ const service = serviceProp ?? inheritedService;
25
98
  const { showNotification } = useNotifications();
26
- const persistenceManager = usePersistenceManager();
27
- const applicationJSONRef = useRef(loadingApplicationJSON);
28
- const setApplicationJSON = useCallback((applicationJSON, rerender = true)=>{
29
- applicationJSONRef.current = applicationJSON;
30
- if (rerender) forceRefresh({});
31
- }, []);
32
- const setWorkspaceJSON = useCallback((workspaceJSON, rerender = true)=>{
33
- setApplicationJSON({
34
- ...applicationJSONRef.current,
35
- workspaceJSON
36
- }, rerender);
37
- }, [
38
- setApplicationJSON
39
- ]);
40
- const setApplicationSettings = useCallback((settings)=>{
41
- setApplicationJSON({
42
- ...applicationJSONRef.current,
43
- settings: {
44
- ...applicationJSONRef.current.settings,
45
- ...settings
46
- }
47
- }, false);
99
+ const [controllers, setControllers] = useState([]);
100
+ const controllersRef = useRef([]);
101
+ const [session, setSession] = useState();
102
+ const sessionRef = useRef(void 0);
103
+ const [namedWorkspaces, setNamedWorkspaces] = useState([]);
104
+ const [loadedScope, setLoadedScope] = useState();
105
+ const [loadError, setLoadError] = useState();
106
+ const generation = useRef(0);
107
+ const operationQueue = useRef(Promise.resolve());
108
+ const resetGeneration = useRef(0);
109
+ const resetPending = useRef(false);
110
+ const scopeKey = service ? `${service.scope.applicationNamespace}/${service.scope.applicationId}/${service.scope.userId}/authenticated:${userId ?? service.scope.userId}` : "missing";
111
+ const scopeIsValid = void 0 !== service && (!userId || service.scope.userId === userId);
112
+ const activeScope = useRef({
113
+ generation: 0,
114
+ scopeKey,
115
+ service
116
+ });
117
+ if (activeScope.current.scopeKey !== scopeKey || activeScope.current.service !== service) activeScope.current = {
118
+ generation: ++generation.current,
119
+ scopeKey,
120
+ service
121
+ };
122
+ const status = loadedScope !== scopeKey ? "loading" : loadError ? "error" : 0 === controllers.length ? "empty" : "ready";
123
+ const notifyError = useCallback((header, cause)=>{
124
+ const error = errorFrom(cause, header);
125
+ showNotification({
126
+ content: error.message,
127
+ header,
128
+ status: "error",
129
+ type: "toast"
130
+ });
131
+ return error;
132
+ }, [
133
+ showNotification
134
+ ]);
135
+ const captureScope = useCallback((expectedService = service, expectedScopeKey = scopeKey)=>{
136
+ const active = activeScope.current;
137
+ if (!expectedService || active.service !== expectedService || active.scopeKey !== expectedScopeKey || active.generation !== generation.current) throw new StaleWorkspaceScopeError();
138
+ return {
139
+ generation: active.generation,
140
+ scopeKey: active.scopeKey,
141
+ service: expectedService
142
+ };
48
143
  }, [
49
- setApplicationJSON
144
+ scopeKey,
145
+ service
50
146
  ]);
147
+ const assertScope = useCallback((guard)=>{
148
+ const active = activeScope.current;
149
+ if (generation.current !== guard.generation || active.generation !== guard.generation || active.scopeKey !== guard.scopeKey || active.service !== guard.service) throw new StaleWorkspaceScopeError();
150
+ }, []);
151
+ const replaceControllers = useCallback((next)=>{
152
+ controllersRef.current = next;
153
+ setControllers(next);
154
+ }, []);
51
155
  useEffect(()=>{
52
- persistenceManager?.loadMetadata().then((metadata)=>{
53
- setLayoutMetadata(metadata);
54
- }).catch((error)=>{
55
- showNotification({
56
- content: "Could not load list of available layouts",
57
- header: "Failed to Load Layouts",
58
- status: "error",
59
- type: "toast"
60
- });
61
- console.error("Error occurred while retrieving metadata", error);
62
- });
63
- persistenceManager?.loadApplicationJSON().then((applicationJSON)=>{
64
- if (applicationJSON) {
65
- info?.("applicationJSON loaded successfully");
66
- setApplicationJSON(applicationJSON);
67
- } else {
68
- const workspaceJSON = getWorkspaceWithLayoutJSON(customWorkspaceJSON, layoutJSON, activeLayoutIndex, {
69
- TabstripProps,
70
- showTabs
71
- });
72
- info?.(`applicationJSON not found, getting defaultWorkspaceJSON,
73
- ${JSON.stringify(workspaceJSON, null, 2)}
74
- `);
75
- setApplicationJSON({
76
- workspaceJSON
156
+ for (const controller of controllersRef.current)controller.dispose();
157
+ controllersRef.current = [];
158
+ setControllers([]);
159
+ setSession(void 0);
160
+ sessionRef.current = void 0;
161
+ setNamedWorkspaces([]);
162
+ setLoadError(void 0);
163
+ setLoadedScope(void 0);
164
+ operationQueue.current = Promise.resolve();
165
+ resetGeneration.current = 0;
166
+ resetPending.current = false;
167
+ if (!service) {
168
+ const error = notifyError("Workspace Persistence Is Not Configured", new Error("Shell requires an injected WorkspacePersistenceService"));
169
+ setLoadError(error);
170
+ setLoadedScope(scopeKey);
171
+ return;
172
+ }
173
+ const legacyProps = [
174
+ void 0 === activeLayoutIndex ? void 0 : "activeLayoutIndex",
175
+ void 0 === layoutJSON ? void 0 : "layoutJSON",
176
+ void 0 === layoutPlaceholderJSON ? void 0 : "layoutPlaceholderJSON",
177
+ void 0 === showTabs ? void 0 : "showTabs",
178
+ void 0 === TabstripProps ? void 0 : "TabstripProps",
179
+ void 0 === workspaceJSON ? void 0 : "workspaceJSON"
180
+ ].filter((name)=>void 0 !== name);
181
+ if (legacyProps.length > 0) {
182
+ const error = notifyError("Legacy Workspace Configuration Is Not Supported", new Error(`WorkspaceProvider no longer accepts ${legacyProps.join(", ")}; inject a WorkspacePersistenceService and provide defaultWorkspace as a versioned snapshot`));
183
+ setLoadError(error);
184
+ setLoadedScope(scopeKey);
185
+ return;
186
+ }
187
+ if (!scopeIsValid) {
188
+ const error = notifyError("Workspace Scope Does Not Match", new Error(`Workspace persistence scope "${service.scope.userId}" does not match authenticated user "${userId}"`));
189
+ setLoadError(error);
190
+ setLoadedScope(scopeKey);
191
+ return;
192
+ }
193
+ const guard = captureScope();
194
+ const load = async ()=>{
195
+ const restored = [];
196
+ try {
197
+ assertScope(guard);
198
+ const [loadedSession, metadata] = await Promise.all([
199
+ guard.service.loadApplicationSession(),
200
+ guard.service.listNamedWorkspaceMetadata()
201
+ ]);
202
+ assertScope(guard);
203
+ if (loadedSession) {
204
+ const descriptors = new Map(loadedSession.openWorkspaces.map((entry)=>[
205
+ entry.instanceId,
206
+ entry
207
+ ]));
208
+ for (const instanceId of loadedSession.workspaceOrder){
209
+ const descriptor = descriptors.get(instanceId);
210
+ if (!descriptor) throw new Error(`Session references missing workspace "${instanceId}"`);
211
+ assertScope(guard);
212
+ const record = await guard.service.loadWorkspaceSnapshot(descriptor.snapshotId);
213
+ assertScope(guard);
214
+ if (!record) throw new Error(`Snapshot for workspace "${instanceId}" was not found`);
215
+ const definitionMetadata = metadata.find(({ id })=>id === descriptor.definitionId);
216
+ restored.push(new WorkspaceController({
217
+ definitionId: descriptor.definitionId,
218
+ instanceId,
219
+ name: descriptor.title ?? definitionMetadata?.name ?? instanceId,
220
+ onError: (error)=>{
221
+ try {
222
+ assertScope(guard);
223
+ notifyError("Failed to Save Workspace", error);
224
+ } catch {}
225
+ },
226
+ renderers: componentRenderers ?? (()=>{
227
+ throw new Error("Workspace component renderers are required");
228
+ })(),
229
+ service: guard.service,
230
+ revision: record.revision,
231
+ settingsCodecs: settingsCodecs ?? (()=>{
232
+ throw new Error("Workspace settings codecs are required");
233
+ })(),
234
+ snapshot: record.snapshot
235
+ }));
236
+ }
237
+ }
238
+ let resolvedSession = loadedSession ?? {
239
+ activeWorkspaceInstanceId: null,
240
+ openWorkspaces: [],
241
+ settings: {},
242
+ version: APPLICATION_SESSION_VERSION,
243
+ workspaceOrder: []
244
+ };
245
+ if (!loadedSession && defaultWorkspace) {
246
+ if (!componentRenderers || !settingsCodecs) throw new Error("Default workspace requires component renderers and settings codecs");
247
+ const instanceId = createInstanceId();
248
+ const initialSnapshot = remapSnapshot(defaultWorkspace, instanceId);
249
+ const controller = new WorkspaceController({
250
+ instanceId,
251
+ name: "Workspace",
252
+ onError: (error)=>{
253
+ try {
254
+ assertScope(guard);
255
+ notifyError("Failed to Save Workspace", error);
256
+ } catch {}
257
+ },
258
+ renderers: componentRenderers,
259
+ service: guard.service,
260
+ settingsCodecs,
261
+ snapshot: initialSnapshot
262
+ });
263
+ assertScope(guard);
264
+ await controller.repository.save(initialSnapshot);
265
+ assertScope(guard);
266
+ restored.push(controller);
267
+ resolvedSession = {
268
+ activeWorkspaceInstanceId: instanceId,
269
+ openWorkspaces: [
270
+ {
271
+ instanceId,
272
+ snapshotId: instanceId,
273
+ title: "Workspace"
274
+ }
275
+ ],
276
+ settings: {},
277
+ version: APPLICATION_SESSION_VERSION,
278
+ workspaceOrder: [
279
+ instanceId
280
+ ]
281
+ };
282
+ await guard.service.saveApplicationSession(resolvedSession);
283
+ assertScope(guard);
284
+ }
285
+ assertScope(guard);
286
+ replaceControllers(restored);
287
+ sessionRef.current = resolvedSession;
288
+ setSession(resolvedSession);
289
+ setNamedWorkspaces(metadata);
290
+ setLoadedScope(scopeKey);
291
+ } catch (cause) {
292
+ restored.forEach((controller)=>{
293
+ controller.dispose();
77
294
  });
295
+ if (!(cause instanceof StaleWorkspaceScopeError)) try {
296
+ assertScope(guard);
297
+ const error = notifyError("Failed to Load Workspaces", cause);
298
+ setLoadError(error);
299
+ setLoadedScope(scopeKey);
300
+ } catch {}
78
301
  }
79
- }).catch((error)=>{
80
- showNotification({
81
- content: "Could not load your latest view",
82
- header: "Failed to Load Layout",
83
- status: "error",
84
- type: "toast"
85
- });
86
- console.error("Error occurred while retrieving application layout", error);
87
- });
302
+ };
303
+ load();
304
+ return ()=>{
305
+ if (activeScope.current.generation === guard.generation) activeScope.current = {
306
+ ...activeScope.current,
307
+ generation: ++generation.current
308
+ };
309
+ for (const controller of controllersRef.current)controller.dispose();
310
+ controllersRef.current = [];
311
+ };
88
312
  }, [
89
- TabstripProps,
313
+ componentRenderers,
90
314
  activeLayoutIndex,
91
- customWorkspaceJSON,
315
+ assertScope,
316
+ captureScope,
317
+ defaultWorkspace,
92
318
  layoutJSON,
93
- showNotification,
94
- persistenceManager,
95
- setApplicationJSON,
96
- showTabs
97
- ]);
98
- const saveApplicationLayout = useCallback((layout)=>{
99
- if (isLayoutJSON(layout)) {
100
- setWorkspaceJSON(layout, false);
101
- persistenceManager?.saveApplicationJSON(applicationJSONRef.current);
102
- } else console.error("Tried to save invalid application layout", layout);
103
- }, [
104
- persistenceManager,
105
- setWorkspaceJSON
106
- ]);
107
- const saveLayout = useCallback((metadata)=>{
108
- let layoutToSave;
109
- try {
110
- const { workspaceJSON } = applicationJSONRef.current;
111
- if (Array.isArray(workspaceJSON)) console.log("how do we identify the right thing to save");
112
- else layoutToSave = resolveJSONPath(workspaceJSON, `#${VuuShellLocation.Workspace}.ACTIVE_CHILD`);
113
- } catch (e) {}
114
- if (layoutToSave && isLayoutJSON(layoutToSave)) persistenceManager?.createLayout(metadata, ensureLayoutHasTitle(layoutToSave, metadata)).then((metadata)=>{
115
- showNotification({
116
- content: `${metadata.name} saved successfully`,
117
- header: "Layout Saved Successfully",
118
- status: "success",
119
- type: "toast"
319
+ layoutPlaceholderJSON,
320
+ notifyError,
321
+ replaceControllers,
322
+ scopeIsValid,
323
+ scopeKey,
324
+ service,
325
+ settingsCodecs,
326
+ showTabs,
327
+ TabstripProps,
328
+ userId,
329
+ workspaceJSON
330
+ ]);
331
+ const writeSession = useCallback(async (next, guard)=>{
332
+ assertScope(guard);
333
+ await guard.service.saveApplicationSession(next);
334
+ assertScope(guard);
335
+ sessionRef.current = next;
336
+ setSession(next);
337
+ }, [
338
+ assertScope
339
+ ]);
340
+ const enqueueOperation = useCallback((guard, operation)=>{
341
+ assertScope(guard);
342
+ if (resetPending.current) return Promise.reject(new Error("Workspace operation was blocked by application reset"));
343
+ const expectedResetGeneration = resetGeneration.current;
344
+ const result = operationQueue.current.catch(()=>void 0).then(async ()=>{
345
+ assertScope(guard);
346
+ if (resetGeneration.current !== expectedResetGeneration) throw new Error("Workspace operation was invalidated by application reset");
347
+ return operation();
348
+ });
349
+ operationQueue.current = result.then(()=>void 0, ()=>void 0);
350
+ return result;
351
+ }, [
352
+ assertScope
353
+ ]);
354
+ const addWorkspace = useCallback(async (sourceSnapshot, name, definitionId, operationGuard)=>{
355
+ if (!service || !componentRenderers || !settingsCodecs) throw new Error("Workspace runtime is not configured");
356
+ const guard = operationGuard ?? captureScope();
357
+ return enqueueOperation(guard, async ()=>{
358
+ assertScope(guard);
359
+ const instanceId = createInstanceId();
360
+ const snapshot = remapSnapshot(sourceSnapshot, instanceId);
361
+ const controller = new WorkspaceController({
362
+ definitionId,
363
+ instanceId,
364
+ name,
365
+ onError: (error)=>void notifyError("Failed to Save Workspace", error),
366
+ renderers: componentRenderers,
367
+ service: guard.service,
368
+ settingsCodecs,
369
+ snapshot
120
370
  });
121
- setLayoutMetadata((prev)=>[
122
- ...prev,
123
- metadata
371
+ await controller.repository.save(snapshot);
372
+ assertScope(guard);
373
+ const current = sessionRef.current ?? {
374
+ activeWorkspaceInstanceId: null,
375
+ openWorkspaces: [],
376
+ settings: {},
377
+ version: APPLICATION_SESSION_VERSION,
378
+ workspaceOrder: []
379
+ };
380
+ try {
381
+ await writeSession({
382
+ ...current,
383
+ activeWorkspaceInstanceId: instanceId,
384
+ openWorkspaces: [
385
+ ...current.openWorkspaces,
386
+ {
387
+ ...void 0 === definitionId ? {} : {
388
+ definitionId
389
+ },
390
+ instanceId,
391
+ snapshotId: instanceId,
392
+ title: name
393
+ }
394
+ ],
395
+ workspaceOrder: [
396
+ ...current.workspaceOrder,
397
+ instanceId
398
+ ]
399
+ }, guard);
400
+ assertScope(guard);
401
+ replaceControllers([
402
+ ...controllersRef.current,
403
+ controller
124
404
  ]);
125
- }).catch((error)=>{
126
- showNotification({
127
- content: `Failed to save layout ${metadata.name}`,
128
- header: "Failed to Save Layout",
129
- status: "error",
130
- type: "toast"
131
- });
132
- console.error("Error occurred while saving layout", error);
405
+ } catch (cause) {
406
+ await controller.dispose();
407
+ assertScope(guard);
408
+ await guard.service.deleteWorkspaceSnapshot(instanceId);
409
+ assertScope(guard);
410
+ throw errorFrom(cause, "Failed to save application session");
411
+ }
412
+ return instanceId;
133
413
  });
134
- else {
135
- console.error("Tried to save invalid layout", layoutToSave);
136
- showNotification({
137
- content: "Cannot save invalid layout",
138
- header: "Failed to Save Layout",
139
- status: "error",
140
- type: "toast"
414
+ }, [
415
+ componentRenderers,
416
+ assertScope,
417
+ captureScope,
418
+ enqueueOperation,
419
+ notifyError,
420
+ replaceControllers,
421
+ service,
422
+ settingsCodecs,
423
+ writeSession
424
+ ]);
425
+ const addDefinition = useCallback((definition, operationGuard)=>addWorkspace(definition.snapshot, definition.name, definition.id, operationGuard), [
426
+ addWorkspace
427
+ ]);
428
+ const openSystemWorkspace = useCallback(async (definition)=>{
429
+ await addDefinition(definition);
430
+ }, [
431
+ addDefinition
432
+ ]);
433
+ const createWorkspaceFromSnapshot = useCallback(async (snapshot, title = "Untitled")=>{
434
+ try {
435
+ return await addWorkspace(snapshot, title);
436
+ } catch (cause) {
437
+ if (!(cause instanceof StaleWorkspaceScopeError)) notifyError("Failed to Create Workspace", cause);
438
+ throw cause;
439
+ }
440
+ }, [
441
+ addWorkspace,
442
+ notifyError
443
+ ]);
444
+ const openNamedWorkspace = useCallback(async (definitionId)=>{
445
+ if (!service) throw new Error("Workspace persistence service is not configured");
446
+ const guard = captureScope();
447
+ const expectedResetGeneration = resetGeneration.current;
448
+ try {
449
+ assertScope(guard);
450
+ const definition = await guard.service.loadNamedWorkspaceDefinition(definitionId);
451
+ assertScope(guard);
452
+ if (resetGeneration.current !== expectedResetGeneration) throw new Error("Workspace operation was invalidated by application reset");
453
+ if (!definition) throw new Error(`Workspace definition "${definitionId}" was not found`);
454
+ await addDefinition(definition, guard);
455
+ } catch (cause) {
456
+ if (!(cause instanceof StaleWorkspaceScopeError)) notifyError("Failed to Open Workspace", cause);
457
+ }
458
+ }, [
459
+ addDefinition,
460
+ assertScope,
461
+ captureScope,
462
+ notifyError,
463
+ service
464
+ ]);
465
+ const selectWorkspace = useCallback(async (instanceId)=>{
466
+ const guard = captureScope();
467
+ await enqueueOperation(guard, async ()=>{
468
+ const current = sessionRef.current;
469
+ if (!current || !current.workspaceOrder.includes(instanceId)) throw new Error(`Workspace "${instanceId}" is not open`);
470
+ await writeSession({
471
+ ...current,
472
+ activeWorkspaceInstanceId: instanceId
473
+ }, guard);
474
+ });
475
+ }, [
476
+ captureScope,
477
+ enqueueOperation,
478
+ writeSession
479
+ ]);
480
+ const renameWorkspace = useCallback(async (instanceId, title)=>{
481
+ const trimmedTitle = title.trim();
482
+ if (!trimmedTitle) throw new Error("Workspace title must not be empty");
483
+ const guard = captureScope();
484
+ try {
485
+ await enqueueOperation(guard, async ()=>{
486
+ const current = sessionRef.current;
487
+ const controller = controllersRef.current.find((candidate)=>candidate.instanceId === instanceId);
488
+ if (!current || !controller) throw new Error(`Workspace "${instanceId}" is not open`);
489
+ await writeSession({
490
+ ...current,
491
+ openWorkspaces: current.openWorkspaces.map((workspace)=>workspace.instanceId === instanceId ? {
492
+ ...workspace,
493
+ title: trimmedTitle
494
+ } : workspace)
495
+ }, guard);
496
+ assertScope(guard);
497
+ controller.rename(trimmedTitle);
498
+ replaceControllers([
499
+ ...controllersRef.current
500
+ ]);
141
501
  });
502
+ } catch (cause) {
503
+ if (!(cause instanceof StaleWorkspaceScopeError)) notifyError("Failed to Rename Workspace", cause);
504
+ throw cause;
142
505
  }
143
506
  }, [
144
- showNotification,
145
- persistenceManager
507
+ assertScope,
508
+ captureScope,
509
+ enqueueOperation,
510
+ notifyError,
511
+ replaceControllers,
512
+ writeSession
513
+ ]);
514
+ const closeWorkspace = useCallback(async (instanceId)=>{
515
+ if (!service) throw new Error("Workspace persistence service is not configured");
516
+ const guard = captureScope();
517
+ await enqueueOperation(guard, async ()=>{
518
+ const current = sessionRef.current;
519
+ const controller = controllersRef.current.find((candidate)=>candidate.instanceId === instanceId);
520
+ if (!current || !controller) return;
521
+ const nextControllers = controllersRef.current.filter((candidate)=>candidate !== controller);
522
+ const closedIndex = current.workspaceOrder.indexOf(instanceId);
523
+ const order = current.workspaceOrder.filter((id)=>id !== instanceId);
524
+ const active = current.activeWorkspaceInstanceId === instanceId ? order[Math.min(closedIndex, order.length - 1)] ?? null : current.activeWorkspaceInstanceId;
525
+ await writeSession({
526
+ ...current,
527
+ activeWorkspaceInstanceId: active,
528
+ openWorkspaces: current.openWorkspaces.filter(({ instanceId: id })=>id !== instanceId),
529
+ workspaceOrder: order
530
+ }, guard);
531
+ assertScope(guard);
532
+ await controller.dispose();
533
+ replaceControllers(nextControllers);
534
+ await guard.service.deleteWorkspaceSnapshot(instanceId);
535
+ assertScope(guard);
536
+ });
537
+ }, [
538
+ assertScope,
539
+ captureScope,
540
+ enqueueOperation,
541
+ replaceControllers,
542
+ service,
543
+ writeSession
146
544
  ]);
147
- const saveApplicationSettings = useCallback((settings, key)=>{
148
- const { settings: applicationSettings } = applicationJSONRef.current;
149
- key ? setApplicationSettings({
150
- ...applicationSettings,
151
- [key]: settings
152
- }) : setApplicationSettings(settings);
153
- persistenceManager?.saveApplicationJSON(applicationJSONRef.current);
545
+ const deleteNamedWorkspace = useCallback(async (definitionId)=>{
546
+ if (!service) throw new Error("Workspace persistence service is not configured");
547
+ const guard = captureScope();
548
+ assertScope(guard);
549
+ await guard.service.deleteNamedWorkspaceDefinition(definitionId);
550
+ assertScope(guard);
551
+ setNamedWorkspaces((current)=>current.filter(({ id })=>id !== definitionId));
154
552
  }, [
155
- persistenceManager,
156
- setApplicationSettings
553
+ assertScope,
554
+ captureScope,
555
+ service
157
556
  ]);
158
- const getApplicationSettings = useCallback((key)=>{
159
- const { settings } = applicationJSONRef.current;
160
- return key ? settings?.[key] : settings;
161
- }, []);
162
- const loadLayoutById = useCallback((id)=>{
163
- persistenceManager?.loadLayout(id).then((layoutJson)=>{
164
- const { workspaceJSON: currentLayout } = applicationJSONRef.current;
165
- if (Array.isArray(currentLayout)) console.log("how do we deal witha amulti layoput");
166
- else setWorkspaceJSON({
167
- ...currentLayout,
168
- children: (currentLayout.children || []).concat(layoutJson),
169
- props: {
170
- ...currentLayout.props,
171
- active: currentLayout.children?.length ?? 0
557
+ const saveActiveWorkspaceAs = useCallback(async (name, description)=>{
558
+ if (!service) throw new Error("Workspace persistence service is not configured");
559
+ const guard = captureScope();
560
+ assertScope(guard);
561
+ const activeId = sessionRef.current?.activeWorkspaceInstanceId;
562
+ const active = controllersRef.current.find(({ instanceId })=>instanceId === activeId);
563
+ if (!active) throw new Error("There is no active workspace to save");
564
+ const now = new Date().toISOString();
565
+ const id = createInstanceId();
566
+ const definition = {
567
+ createdAt: now,
568
+ ...void 0 === description ? {} : {
569
+ description
570
+ },
571
+ id,
572
+ name,
573
+ snapshot: active.snapshot,
574
+ updatedAt: now,
575
+ version: NAMED_WORKSPACE_DEFINITION_VERSION
576
+ };
577
+ await guard.service.saveNamedWorkspaceDefinition(definition);
578
+ assertScope(guard);
579
+ setNamedWorkspaces((current)=>[
580
+ ...current,
581
+ {
582
+ createdAt: now,
583
+ ...void 0 === description ? {} : {
584
+ description
585
+ },
586
+ id,
587
+ name,
588
+ updatedAt: now,
589
+ version: NAMED_WORKSPACE_DEFINITION_VERSION
172
590
  }
173
- });
174
- }).catch((error)=>{
175
- showNotification({
176
- content: "Failed to load the requested layout",
177
- header: "Failed to Load Layout",
178
- status: "error",
179
- type: "toast"
180
- });
181
- console.error("Error occurred while loading layout", error);
591
+ ]);
592
+ }, [
593
+ assertScope,
594
+ captureScope,
595
+ service
596
+ ]);
597
+ const setApplicationSetting = useCallback(async (key, value)=>{
598
+ const guard = captureScope();
599
+ await enqueueOperation(guard, async ()=>{
600
+ const current = sessionRef.current;
601
+ if (!current) throw new Error("Application session has not loaded");
602
+ await writeSession({
603
+ ...current,
604
+ settings: {
605
+ ...current.settings,
606
+ [key]: value
607
+ }
608
+ }, guard);
182
609
  });
183
610
  }, [
184
- showNotification,
185
- persistenceManager,
186
- setWorkspaceJSON
611
+ captureScope,
612
+ enqueueOperation,
613
+ writeSession
187
614
  ]);
188
- return /*#__PURE__*/ jsx(WorkspaceContext.Provider, {
189
- value: {
190
- getApplicationSettings,
191
- layoutMetadata,
192
- layoutPlaceholderJSON,
193
- saveLayout,
194
- workspaceJSON: applicationJSONRef.current.workspaceJSON,
195
- saveApplicationLayout,
615
+ const saveApplicationSettings = useCallback(async (value, key)=>{
616
+ if (void 0 !== key) return void await setApplicationSetting(key, value);
617
+ if ("object" != typeof value || null === value || Array.isArray(value)) throw new Error("Application settings must be a JSON object");
618
+ const guard = captureScope();
619
+ await enqueueOperation(guard, async ()=>{
620
+ const current = sessionRef.current;
621
+ if (!current) throw new Error("Application session has not loaded");
622
+ await writeSession({
623
+ ...current,
624
+ settings: value
625
+ }, guard);
626
+ });
627
+ }, [
628
+ captureScope,
629
+ enqueueOperation,
630
+ setApplicationSetting,
631
+ writeSession
632
+ ]);
633
+ const resetApplication = useCallback(async ()=>{
634
+ const guard = captureScope();
635
+ assertScope(guard);
636
+ if (resetPending.current) throw new Error("Application reset is already in progress");
637
+ resetPending.current = true;
638
+ resetGeneration.current += 1;
639
+ const reset = operationQueue.current.catch(()=>void 0).then(async ()=>{
640
+ assertScope(guard);
641
+ const instanceIds = sessionRef.current?.openWorkspaces.map(({ snapshotId })=>snapshotId) ?? [];
642
+ await guard.service.deleteApplicationSession();
643
+ assertScope(guard);
644
+ await Promise.all(controllersRef.current.map((controller)=>controller.dispose()));
645
+ replaceControllers([]);
646
+ const emptySession = {
647
+ activeWorkspaceInstanceId: null,
648
+ openWorkspaces: [],
649
+ settings: {},
650
+ version: APPLICATION_SESSION_VERSION,
651
+ workspaceOrder: []
652
+ };
653
+ sessionRef.current = emptySession;
654
+ setSession(emptySession);
655
+ for (const snapshotId of instanceIds){
656
+ assertScope(guard);
657
+ await guard.service.deleteWorkspaceSnapshot(snapshotId);
658
+ assertScope(guard);
659
+ }
660
+ });
661
+ operationQueue.current = reset.then(()=>void 0, ()=>void 0);
662
+ try {
663
+ await reset;
664
+ } finally{
665
+ if (activeScope.current.generation === guard.generation && activeScope.current.scopeKey === guard.scopeKey) resetPending.current = false;
666
+ }
667
+ }, [
668
+ assertScope,
669
+ captureScope,
670
+ replaceControllers
671
+ ]);
672
+ const value = useMemo(()=>{
673
+ const settings = session?.settings ?? {};
674
+ const visibleControllers = "loading" === status ? [] : controllers;
675
+ const activeWorkspace = visibleControllers.find(({ instanceId })=>instanceId === session?.activeWorkspaceInstanceId);
676
+ return {
677
+ activeWorkspace,
678
+ activeWorkspaceInstanceId: session?.activeWorkspaceInstanceId ?? null,
679
+ closeWorkspace,
680
+ componentRenderers,
681
+ controllers: visibleControllers,
682
+ createWorkspaceFromSnapshot,
683
+ deleteNamedWorkspace,
684
+ error: loadError,
685
+ getApplicationSetting: (key)=>settings[key],
686
+ getApplicationSettings: (key)=>void 0 === key ? settings : settings[key],
687
+ namedWorkspaces,
688
+ openNamedWorkspace,
689
+ openSystemWorkspace,
690
+ renameWorkspace,
691
+ saveActiveWorkspaceAs,
196
692
  saveApplicationSettings,
197
- loadLayoutById
198
- },
693
+ selectWorkspace,
694
+ setApplicationSetting,
695
+ settings,
696
+ settingsCodecs,
697
+ scopeIdentity: scopeKey,
698
+ status,
699
+ systemWorkspaces,
700
+ resetApplication
701
+ };
702
+ }, [
703
+ closeWorkspace,
704
+ componentRenderers,
705
+ controllers,
706
+ createWorkspaceFromSnapshot,
707
+ deleteNamedWorkspace,
708
+ loadError,
709
+ namedWorkspaces,
710
+ openNamedWorkspace,
711
+ openSystemWorkspace,
712
+ renameWorkspace,
713
+ saveActiveWorkspaceAs,
714
+ saveApplicationSettings,
715
+ selectWorkspace,
716
+ session,
717
+ setApplicationSetting,
718
+ settingsCodecs,
719
+ scopeKey,
720
+ status,
721
+ systemWorkspaces,
722
+ resetApplication
723
+ ]);
724
+ return /*#__PURE__*/ jsx(WorkspaceContext.Provider, {
725
+ value: value,
199
726
  children: children
200
727
  });
201
728
  };
202
- const useWorkspace = ()=>{
203
- const { workspaceJSON = getWorkspaceWithLayoutJSON(), ...contextProps } = useContext(WorkspaceContext);
204
- return {
205
- ...contextProps,
206
- workspaceJSON
207
- };
208
- };
209
- export { WorkspaceProvider, useWorkspace };
729
+ const useWorkspace = ()=>useContext(WorkspaceContext);
730
+ export { StaleWorkspaceScopeError, WorkspaceProvider, emptyWorkspaceSnapshot, useWorkspace };