@treasuryspatial/viewer-react 0.1.44 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AccessAssetsMenu.d.ts.map +1 -1
- package/dist/AccessAssetsMenu.js +53 -45
- package/dist/AssetUploadCard.d.ts +2 -1
- package/dist/AssetUploadCard.d.ts.map +1 -1
- package/dist/AssetUploadCard.js +3 -2
- package/dist/ViewControls.d.ts +36 -2
- package/dist/ViewControls.d.ts.map +1 -1
- package/dist/ViewControls.js +154 -45
- package/dist/ViewerCanvas.d.ts.map +1 -1
- package/dist/ViewerCanvas.js +15 -40
- package/dist/composerRouting.d.ts +6 -0
- package/dist/composerRouting.d.ts.map +1 -1
- package/dist/composerRouting.js +18 -9
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/useAdminGallery.d.ts +2 -30
- package/dist/useAdminGallery.d.ts.map +1 -1
- package/dist/useAdminGallery.js +15 -6
- package/dist/useAdminImageStream.d.ts +3 -31
- package/dist/useAdminImageStream.d.ts.map +1 -1
- package/dist/useAdminImageStream.js +22 -10
- package/dist/useComposerAccessGate.js +4 -4
- package/dist/useComposerAuthState.d.ts +1 -0
- package/dist/useComposerAuthState.d.ts.map +1 -1
- package/dist/useComposerAuthState.js +4 -1
- package/dist/useComposerModeDraftState.d.ts +21 -0
- package/dist/useComposerModeDraftState.d.ts.map +1 -0
- package/dist/useComposerModeDraftState.js +89 -0
- package/dist/useComposerUiState.d.ts +11 -4
- package/dist/useComposerUiState.d.ts.map +1 -1
- package/dist/useComposerUiState.js +118 -80
- package/dist/useComposerWorkspaceSession.d.ts +21 -0
- package/dist/useComposerWorkspaceSession.d.ts.map +1 -0
- package/dist/useComposerWorkspaceSession.js +291 -0
- package/dist/useGrasshopperSession.d.ts.map +1 -1
- package/dist/useGrasshopperSession.js +10 -4
- package/dist/useModeShellState.d.ts.map +1 -1
- package/dist/useModeShellState.js +52 -6
- package/dist/usePromptPackSelection.d.ts +20 -0
- package/dist/usePromptPackSelection.d.ts.map +1 -0
- package/dist/usePromptPackSelection.js +81 -0
- package/dist/useRightRailTabs.d.ts.map +1 -1
- package/dist/useRightRailTabs.js +4 -3
- package/dist/useSessionLogout.d.ts.map +1 -1
- package/dist/useSessionLogout.js +12 -1
- package/dist/useSessionRevocationPing.d.ts.map +1 -1
- package/dist/useSessionRevocationPing.js +15 -2
- package/dist/useShellPanelToggle.d.ts +7 -1
- package/dist/useShellPanelToggle.d.ts.map +1 -1
- package/dist/useShellPanelToggle.js +14 -6
- package/dist/workspaceSession.d.ts +36 -0
- package/dist/workspaceSession.d.ts.map +1 -0
- package/dist/workspaceSession.js +141 -0
- package/package.json +9 -8
- package/dist/usePromptPackSelectionByMode.d.ts +0 -26
- package/dist/usePromptPackSelectionByMode.d.ts.map +0 -1
- package/dist/usePromptPackSelectionByMode.js +0 -111
|
@@ -1,119 +1,157 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
const resolveString = (value, fallback) => typeof value === "string" && value.length > 0 ? value : fallback;
|
|
3
|
-
const modeKey = (mode, key) => `ui${key}_${mode}`;
|
|
4
3
|
const resolveBoolean = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
5
4
|
const resolvePresetId = (value) => (typeof value === "string" ? value : null);
|
|
5
|
+
const resolveRightPanelTab = (value, fallback) => {
|
|
6
|
+
if (value === "stream")
|
|
7
|
+
return "feed";
|
|
8
|
+
return resolveString(value, fallback);
|
|
9
|
+
};
|
|
6
10
|
const resolveDefaultPrimaryTab = (mode, defaultPrimaryTabByMode) => defaultPrimaryTabByMode?.[mode] ?? (mode === "imagine" ? "render" : "controls");
|
|
7
|
-
export function useComposerUiState({ mode,
|
|
11
|
+
export function useComposerUiState({ mode, workspaceSession, setWorkspaceSession, defaults, promptKitModes = ["imagine"], moduleKitModes = ["design", "map"], defaultPrimaryTabByMode, }) {
|
|
8
12
|
const modeDefaultTab = resolveDefaultPrimaryTab(mode, defaultPrimaryTabByMode);
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const [
|
|
23
|
-
const [
|
|
24
|
-
const [
|
|
25
|
-
const
|
|
13
|
+
const resolveWorkspaceValue = useCallback(() => ({
|
|
14
|
+
primaryTab: resolveString(workspaceSession?.uiState.primaryTabByMode?.[mode], defaults?.primaryTab ?? modeDefaultTab),
|
|
15
|
+
promptKitOpen: resolveBoolean(workspaceSession?.uiState.promptKitOpenByMode?.[mode], defaults?.promptKitOpen ?? false),
|
|
16
|
+
moduleKitOpen: resolveBoolean(workspaceSession?.uiState.moduleKitOpenByMode?.[mode], defaults?.moduleKitOpen ?? false),
|
|
17
|
+
activeImaginePresetId: resolvePresetId(workspaceSession?.uiState.activeImaginePresetIdByMode?.[mode]) ??
|
|
18
|
+
defaults?.activeImaginePresetId ??
|
|
19
|
+
null,
|
|
20
|
+
autoGenerateEnabled: resolveBoolean(workspaceSession?.uiState.autoGenerateEnabledByMode?.[mode], defaults?.autoGenerateEnabled ?? false),
|
|
21
|
+
controlPanelOpen: resolveBoolean(workspaceSession?.uiState.controlPanelOpenByMode?.[mode], defaults?.controlPanelOpen ?? true),
|
|
22
|
+
rightPanelOpen: resolveBoolean(workspaceSession?.uiState.rightPanelOpenByMode?.[mode], defaults?.rightPanelOpen ?? false),
|
|
23
|
+
rightPanelTab: resolveRightPanelTab(workspaceSession?.uiState.rightPanelTabByMode?.[mode], defaults?.rightPanelTab ?? "feed"),
|
|
24
|
+
}), [defaults, mode, modeDefaultTab, workspaceSession]);
|
|
25
|
+
const initialValue = resolveWorkspaceValue();
|
|
26
|
+
const [primaryTab, setPrimaryTabState] = useState(initialValue.primaryTab);
|
|
27
|
+
const [promptKitOpen, setPromptKitOpenState] = useState(initialValue.promptKitOpen);
|
|
28
|
+
const [moduleKitOpen, setModuleKitOpenState] = useState(initialValue.moduleKitOpen);
|
|
29
|
+
const [activeImaginePresetId, setActiveImaginePresetIdState] = useState(initialValue.activeImaginePresetId);
|
|
30
|
+
const [autoGenerateEnabled, setAutoGenerateEnabledState] = useState(initialValue.autoGenerateEnabled);
|
|
31
|
+
const [controlPanelOpen, setControlPanelOpenState] = useState(initialValue.controlPanelOpen);
|
|
32
|
+
const [rightPanelOpen, setRightPanelOpenState] = useState(initialValue.rightPanelOpen);
|
|
33
|
+
const [rightPanelTab, setRightPanelTabState] = useState(initialValue.rightPanelTab);
|
|
34
|
+
// Per-field delta sync: only apply a session value when the SESSION
|
|
35
|
+
// changed that field. The previous whole-snapshot stomp raced local
|
|
36
|
+
// writes — clicking a toggle (e.g. the module chooser) opened it locally
|
|
37
|
+
// while the session write was still in flight; any unrelated session
|
|
38
|
+
// churn (draft saves, hydration) then re-applied the stale session value
|
|
39
|
+
// and instantly reverted the click.
|
|
40
|
+
const lastResolvedRef = useRef(null);
|
|
26
41
|
useEffect(() => {
|
|
27
|
-
|
|
42
|
+
const next = resolveWorkspaceValue();
|
|
43
|
+
const prev = lastResolvedRef.current;
|
|
44
|
+
lastResolvedRef.current = next;
|
|
45
|
+
if (!prev)
|
|
46
|
+
return; // initial useState already seeded from the session
|
|
47
|
+
if (next.primaryTab !== prev.primaryTab)
|
|
48
|
+
setPrimaryTabState(next.primaryTab);
|
|
49
|
+
if (next.promptKitOpen !== prev.promptKitOpen)
|
|
50
|
+
setPromptKitOpenState(next.promptKitOpen);
|
|
51
|
+
if (next.moduleKitOpen !== prev.moduleKitOpen)
|
|
52
|
+
setModuleKitOpenState(next.moduleKitOpen);
|
|
53
|
+
if (next.activeImaginePresetId !== prev.activeImaginePresetId)
|
|
54
|
+
setActiveImaginePresetIdState(next.activeImaginePresetId);
|
|
55
|
+
if (next.autoGenerateEnabled !== prev.autoGenerateEnabled)
|
|
56
|
+
setAutoGenerateEnabledState(next.autoGenerateEnabled);
|
|
57
|
+
if (next.controlPanelOpen !== prev.controlPanelOpen)
|
|
58
|
+
setControlPanelOpenState(next.controlPanelOpen);
|
|
59
|
+
if (next.rightPanelOpen !== prev.rightPanelOpen)
|
|
60
|
+
setRightPanelOpenState(next.rightPanelOpen);
|
|
61
|
+
if (next.rightPanelTab !== prev.rightPanelTab)
|
|
62
|
+
setRightPanelTabState(next.rightPanelTab);
|
|
63
|
+
}, [resolveWorkspaceValue]);
|
|
64
|
+
const updateWorkspaceUiState = useCallback((updater) => {
|
|
65
|
+
if (!setWorkspaceSession)
|
|
28
66
|
return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
setModuleKitOpenState(resolveBoolean(sessionContext.uiModuleKitOpen, initialModuleOpen));
|
|
35
|
-
setActiveImaginePresetIdState(resolvePresetId(sessionContext.uiActiveImaginePresetId) ?? initialPresetId);
|
|
36
|
-
setControlPanelOpenState(resolveBoolean(sessionContext.uiControlPanelOpen, initialControlOpen));
|
|
37
|
-
setRightPanelOpenState(resolveBoolean(sessionContext.uiRightPanelOpen, initialRightOpen));
|
|
38
|
-
setRightPanelTabState(resolveString(sessionContext.uiRightPanelTab, initialRightTab));
|
|
39
|
-
}, [
|
|
40
|
-
initialControlOpen,
|
|
41
|
-
initialModuleOpen,
|
|
42
|
-
initialPresetId,
|
|
43
|
-
initialPrimaryTab,
|
|
44
|
-
initialPromptOpen,
|
|
45
|
-
initialRightOpen,
|
|
46
|
-
initialRightTab,
|
|
47
|
-
sessionContext,
|
|
48
|
-
]);
|
|
49
|
-
const setContext = useCallback((partial) => {
|
|
50
|
-
if (!setSessionContext)
|
|
51
|
-
return;
|
|
52
|
-
setSessionContext(partial);
|
|
53
|
-
}, [setSessionContext]);
|
|
67
|
+
setWorkspaceSession((session) => ({
|
|
68
|
+
...session,
|
|
69
|
+
uiState: updater(session.uiState),
|
|
70
|
+
}));
|
|
71
|
+
}, [setWorkspaceSession]);
|
|
54
72
|
const setPrimaryTab = useCallback((next) => {
|
|
55
73
|
setPrimaryTabState(next);
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
updateWorkspaceUiState((uiState) => ({
|
|
75
|
+
...uiState,
|
|
76
|
+
primaryTabByMode: { ...uiState.primaryTabByMode, [mode]: next },
|
|
77
|
+
}));
|
|
78
|
+
}, [mode, updateWorkspaceUiState]);
|
|
58
79
|
const setPromptKitOpen = useCallback((next) => {
|
|
59
80
|
setPromptKitOpenState(next);
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
updateWorkspaceUiState((uiState) => ({
|
|
82
|
+
...uiState,
|
|
83
|
+
promptKitOpenByMode: { ...uiState.promptKitOpenByMode, [mode]: next },
|
|
84
|
+
}));
|
|
85
|
+
}, [mode, updateWorkspaceUiState]);
|
|
62
86
|
const setModuleKitOpen = useCallback((next) => {
|
|
63
87
|
setModuleKitOpenState(next);
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
updateWorkspaceUiState((uiState) => ({
|
|
89
|
+
...uiState,
|
|
90
|
+
moduleKitOpenByMode: { ...uiState.moduleKitOpenByMode, [mode]: next },
|
|
91
|
+
}));
|
|
92
|
+
}, [mode, updateWorkspaceUiState]);
|
|
66
93
|
const setActiveImaginePresetId = useCallback((next) => {
|
|
67
94
|
setActiveImaginePresetIdState(next);
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
updateWorkspaceUiState((uiState) => ({
|
|
96
|
+
...uiState,
|
|
97
|
+
activeImaginePresetIdByMode: { ...uiState.activeImaginePresetIdByMode, [mode]: next },
|
|
98
|
+
}));
|
|
99
|
+
}, [mode, updateWorkspaceUiState]);
|
|
100
|
+
const setAutoGenerateEnabled = useCallback((next) => {
|
|
101
|
+
setAutoGenerateEnabledState(next);
|
|
102
|
+
updateWorkspaceUiState((uiState) => ({
|
|
103
|
+
...uiState,
|
|
104
|
+
autoGenerateEnabledByMode: { ...uiState.autoGenerateEnabledByMode, [mode]: next },
|
|
105
|
+
}));
|
|
106
|
+
}, [mode, updateWorkspaceUiState]);
|
|
70
107
|
const setControlPanelOpen = useCallback((next) => {
|
|
71
108
|
setControlPanelOpenState(next);
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
updateWorkspaceUiState((uiState) => ({
|
|
110
|
+
...uiState,
|
|
111
|
+
controlPanelOpenByMode: { ...uiState.controlPanelOpenByMode, [mode]: next },
|
|
112
|
+
}));
|
|
113
|
+
}, [mode, updateWorkspaceUiState]);
|
|
74
114
|
const setRightPanelOpen = useCallback((next) => {
|
|
75
115
|
setRightPanelOpenState(next);
|
|
76
|
-
|
|
77
|
-
|
|
116
|
+
updateWorkspaceUiState((uiState) => ({
|
|
117
|
+
...uiState,
|
|
118
|
+
rightPanelOpenByMode: { ...uiState.rightPanelOpenByMode, [mode]: next },
|
|
119
|
+
}));
|
|
120
|
+
}, [mode, updateWorkspaceUiState]);
|
|
78
121
|
const setRightPanelTab = useCallback((next) => {
|
|
79
122
|
setRightPanelTabState(next);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
modeRef.current = mode;
|
|
87
|
-
if (!sessionContext)
|
|
88
|
-
return;
|
|
89
|
-
setPrimaryTabState(resolveString(sessionContext?.[modeKey(mode, "primaryTab")] ?? sessionContext?.uiPrimaryTab, resolveDefaultPrimaryTab(mode, defaultPrimaryTabByMode)));
|
|
90
|
-
setPromptKitOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "promptKitOpen")] ??
|
|
91
|
-
sessionContext?.uiPromptKitOpen, false));
|
|
92
|
-
setModuleKitOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "moduleKitOpen")] ??
|
|
93
|
-
sessionContext?.uiModuleKitOpen, false));
|
|
94
|
-
setActiveImaginePresetIdState(resolvePresetId(sessionContext?.[modeKey(mode, "activeImaginePresetId")] ??
|
|
95
|
-
sessionContext?.uiActiveImaginePresetId) ?? null);
|
|
96
|
-
setControlPanelOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "controlPanelOpen")] ??
|
|
97
|
-
sessionContext?.uiControlPanelOpen, true));
|
|
98
|
-
setRightPanelOpenState(resolveBoolean(sessionContext?.[modeKey(mode, "rightPanelOpen")] ??
|
|
99
|
-
sessionContext?.uiRightPanelOpen, false));
|
|
100
|
-
setRightPanelTabState(resolveString(sessionContext?.[modeKey(mode, "rightPanelTab")] ??
|
|
101
|
-
sessionContext?.uiRightPanelTab, "images"));
|
|
102
|
-
}, [defaultPrimaryTabByMode, mode, sessionContext]);
|
|
123
|
+
updateWorkspaceUiState((uiState) => ({
|
|
124
|
+
...uiState,
|
|
125
|
+
rightPanelTabByMode: { ...uiState.rightPanelTabByMode, [mode]: next },
|
|
126
|
+
}));
|
|
127
|
+
}, [mode, updateWorkspaceUiState]);
|
|
103
128
|
const gatedPromptKitOpen = useMemo(() => (promptKitModes.includes(mode) ? promptKitOpen : false), [mode, promptKitOpen, promptKitModes]);
|
|
104
129
|
const gatedModuleKitOpen = useMemo(() => (moduleKitModes.includes(mode) ? moduleKitOpen : false), [mode, moduleKitOpen, moduleKitModes]);
|
|
130
|
+
const chooserOpen = gatedPromptKitOpen || gatedModuleKitOpen;
|
|
131
|
+
const setChooserOpen = useCallback((next) => {
|
|
132
|
+
if (promptKitModes.includes(mode)) {
|
|
133
|
+
setPromptKitOpen(next);
|
|
134
|
+
}
|
|
135
|
+
if (moduleKitModes.includes(mode)) {
|
|
136
|
+
setModuleKitOpen(next);
|
|
137
|
+
}
|
|
138
|
+
}, [mode, moduleKitModes, promptKitModes, setModuleKitOpen, setPromptKitOpen]);
|
|
105
139
|
return {
|
|
106
140
|
primaryTab,
|
|
107
141
|
promptKitOpen: gatedPromptKitOpen,
|
|
108
142
|
moduleKitOpen: gatedModuleKitOpen,
|
|
143
|
+
chooserOpen,
|
|
109
144
|
activeImaginePresetId,
|
|
145
|
+
autoGenerateEnabled,
|
|
110
146
|
controlPanelOpen,
|
|
111
147
|
rightPanelOpen,
|
|
112
148
|
rightPanelTab,
|
|
113
149
|
setPrimaryTab,
|
|
114
150
|
setPromptKitOpen,
|
|
115
151
|
setModuleKitOpen,
|
|
152
|
+
setChooserOpen,
|
|
116
153
|
setActiveImaginePresetId,
|
|
154
|
+
setAutoGenerateEnabled,
|
|
117
155
|
setControlPanelOpen,
|
|
118
156
|
setRightPanelOpen,
|
|
119
157
|
setRightPanelTab,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ComposerWorkspaceSession, type ComposerWorkspaceSessionUpdater } from './workspaceSession';
|
|
2
|
+
export type WorkspaceSessionRemoteStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
3
|
+
export type UseComposerWorkspaceSessionOptions<TDraft> = {
|
|
4
|
+
activeMode: string;
|
|
5
|
+
modeIds?: string[];
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
remoteEndpoint?: string;
|
|
8
|
+
storageKey?: string;
|
|
9
|
+
createDraft: () => TDraft;
|
|
10
|
+
restoreDraft: (raw: unknown) => TDraft;
|
|
11
|
+
};
|
|
12
|
+
export declare function useComposerWorkspaceSession<TDraft>({ activeMode, modeIds, enabled, remoteEndpoint, storageKey, createDraft, restoreDraft, }: UseComposerWorkspaceSessionOptions<TDraft>): {
|
|
13
|
+
workspaceSession: ComposerWorkspaceSession<TDraft>;
|
|
14
|
+
workspaceSessionHydrated: boolean;
|
|
15
|
+
remoteHydrated: boolean;
|
|
16
|
+
remoteStatus: WorkspaceSessionRemoteStatus;
|
|
17
|
+
remoteError: string | null;
|
|
18
|
+
setWorkspaceSession: (updater: ComposerWorkspaceSessionUpdater<TDraft>) => void;
|
|
19
|
+
clearWorkspaceSession: () => void;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=useComposerWorkspaceSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useComposerWorkspaceSession.d.ts","sourceRoot":"","sources":["../src/useComposerWorkspaceSession.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACrC,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,4BAA4B,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAElF,MAAM,MAAM,kCAAkC,CAAC,MAAM,IAAI;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,MAAM,CAAC;IAC1B,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;CACxC,CAAC;AA0CF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,EAClD,UAAU,EACV,OAAY,EACZ,OAAe,EACf,cAAyC,EACzC,UAAgC,EAChC,WAAW,EACX,YAAY,GACb,EAAE,kCAAkC,CAAC,MAAM,CAAC,GAAG;IAC9C,gBAAgB,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACnD,wBAAwB,EAAE,OAAO,CAAC;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,4BAA4B,CAAC;IAC3C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,mBAAmB,EAAE,CAAC,OAAO,EAAE,+BAA+B,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IAChF,qBAAqB,EAAE,MAAM,IAAI,CAAC;CACnC,CA2QA"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { createEmptyComposerWorkspaceSession, restoreComposerWorkspaceSession, } from './workspaceSession';
|
|
4
|
+
const DEFAULT_STORAGE_KEY = 'treasury:composer-workspace-session:v1';
|
|
5
|
+
const REMOTE_SAVE_DEBOUNCE_MS = 800;
|
|
6
|
+
const isAbortError = (error) => error instanceof Error && error.name === 'AbortError';
|
|
7
|
+
const readUpdatedAtMs = (session) => {
|
|
8
|
+
const parsed = Date.parse(session.updatedAt ?? '');
|
|
9
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
10
|
+
};
|
|
11
|
+
const shouldPreferLocalSession = (localSession, remoteSession) => {
|
|
12
|
+
const localUpdatedAtMs = readUpdatedAtMs(localSession);
|
|
13
|
+
const remoteUpdatedAtMs = readUpdatedAtMs(remoteSession);
|
|
14
|
+
if (localUpdatedAtMs !== null && remoteUpdatedAtMs !== null) {
|
|
15
|
+
return localUpdatedAtMs > remoteUpdatedAtMs;
|
|
16
|
+
}
|
|
17
|
+
if (localUpdatedAtMs !== null && remoteUpdatedAtMs === null) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
};
|
|
22
|
+
const resolveRemoteError = (payload, status, fallback) => {
|
|
23
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
24
|
+
const record = payload;
|
|
25
|
+
const error = record.error;
|
|
26
|
+
if (typeof error === 'string' && error.trim())
|
|
27
|
+
return error.trim();
|
|
28
|
+
if (error && typeof error === 'object' && !Array.isArray(error)) {
|
|
29
|
+
const message = error.message;
|
|
30
|
+
if (typeof message === 'string' && message.trim())
|
|
31
|
+
return message.trim();
|
|
32
|
+
}
|
|
33
|
+
if (typeof record.message === 'string' && record.message.trim())
|
|
34
|
+
return record.message.trim();
|
|
35
|
+
}
|
|
36
|
+
return `${fallback} (${status || 0})`;
|
|
37
|
+
};
|
|
38
|
+
export function useComposerWorkspaceSession({ activeMode, modeIds = [], enabled = false, remoteEndpoint = '/api/workspace/session', storageKey = DEFAULT_STORAGE_KEY, createDraft, restoreDraft, }) {
|
|
39
|
+
const restoreOptions = useMemo(() => ({
|
|
40
|
+
createDraft,
|
|
41
|
+
restoreDraft,
|
|
42
|
+
activeMode,
|
|
43
|
+
modeIds,
|
|
44
|
+
}), [activeMode, createDraft, modeIds, restoreDraft]);
|
|
45
|
+
const normalizeSession = useCallback((value) => restoreComposerWorkspaceSession(value, restoreOptions), [restoreOptions]);
|
|
46
|
+
const createEmptySession = useCallback(() => createEmptyComposerWorkspaceSession(createDraft, { activeMode, modeIds }), [activeMode, createDraft, modeIds]);
|
|
47
|
+
const [workspaceSession, setWorkspaceSessionState] = useState(() => createEmptyComposerWorkspaceSession(createDraft, { activeMode, modeIds }));
|
|
48
|
+
const [workspaceSessionHydrated, setWorkspaceSessionHydrated] = useState(false);
|
|
49
|
+
const [remoteHydrated, setRemoteHydrated] = useState(!enabled);
|
|
50
|
+
const [remoteStatus, setRemoteStatus] = useState(enabled ? 'loading' : 'idle');
|
|
51
|
+
const [remoteError, setRemoteError] = useState(null);
|
|
52
|
+
const lastSyncedSignatureRef = useRef('');
|
|
53
|
+
const remoteSaveTimerRef = useRef(null);
|
|
54
|
+
const setWorkspaceSession = useCallback((updater) => {
|
|
55
|
+
setWorkspaceSessionState((prev) => {
|
|
56
|
+
const normalizedPrev = normalizeSession(prev);
|
|
57
|
+
const nextCandidate = updater(normalizedPrev);
|
|
58
|
+
const normalizedNext = normalizeSession({
|
|
59
|
+
...nextCandidate,
|
|
60
|
+
activeMode: nextCandidate.activeMode || activeMode,
|
|
61
|
+
});
|
|
62
|
+
const previousSignature = JSON.stringify(normalizedPrev);
|
|
63
|
+
const nextSignature = JSON.stringify(normalizedNext);
|
|
64
|
+
if (previousSignature === nextSignature) {
|
|
65
|
+
return prev;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...normalizedNext,
|
|
69
|
+
updatedAt: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}, [activeMode, normalizeSession]);
|
|
73
|
+
const clearWorkspaceSession = useCallback(() => {
|
|
74
|
+
const next = createEmptySession();
|
|
75
|
+
const nextSignature = JSON.stringify(normalizeSession(next));
|
|
76
|
+
if (typeof window !== 'undefined') {
|
|
77
|
+
window.sessionStorage.removeItem(storageKey);
|
|
78
|
+
}
|
|
79
|
+
setWorkspaceSessionState(next);
|
|
80
|
+
setRemoteError(null);
|
|
81
|
+
lastSyncedSignatureRef.current = '';
|
|
82
|
+
if (!enabled) {
|
|
83
|
+
setRemoteStatus('idle');
|
|
84
|
+
setRemoteHydrated(true);
|
|
85
|
+
lastSyncedSignatureRef.current = nextSignature;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setRemoteStatus('loading');
|
|
89
|
+
setRemoteHydrated(false);
|
|
90
|
+
void (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(remoteEndpoint, {
|
|
93
|
+
method: 'DELETE',
|
|
94
|
+
cache: 'no-store',
|
|
95
|
+
credentials: 'same-origin',
|
|
96
|
+
});
|
|
97
|
+
const payload = await response.json().catch(() => null);
|
|
98
|
+
if (!response.ok || !payload?.success) {
|
|
99
|
+
throw new Error(resolveRemoteError(payload, response.status, 'Workspace session delete failed'));
|
|
100
|
+
}
|
|
101
|
+
lastSyncedSignatureRef.current = nextSignature;
|
|
102
|
+
setRemoteStatus('ready');
|
|
103
|
+
setRemoteError(null);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
setRemoteStatus('error');
|
|
107
|
+
setRemoteError(error instanceof Error ? error.message : 'Workspace session delete failed');
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
setRemoteHydrated(true);
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
}, [createEmptySession, enabled, normalizeSession, remoteEndpoint, storageKey]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (typeof window === 'undefined') {
|
|
116
|
+
setWorkspaceSessionHydrated(true);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const raw = window.sessionStorage.getItem(storageKey);
|
|
121
|
+
if (!raw) {
|
|
122
|
+
setWorkspaceSessionState(createEmptySession());
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
setWorkspaceSessionState(normalizeSession(parsed));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
setWorkspaceSessionState(createEmptySession());
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
setWorkspaceSessionHydrated(true);
|
|
134
|
+
}
|
|
135
|
+
}, [createEmptySession, normalizeSession, storageKey]);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!workspaceSessionHydrated)
|
|
138
|
+
return;
|
|
139
|
+
setWorkspaceSession((session) => {
|
|
140
|
+
if (session.activeMode === activeMode)
|
|
141
|
+
return session;
|
|
142
|
+
return { ...session, activeMode };
|
|
143
|
+
});
|
|
144
|
+
}, [activeMode, setWorkspaceSession, workspaceSessionHydrated]);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!workspaceSessionHydrated || typeof window === 'undefined')
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
window.sessionStorage.setItem(storageKey, JSON.stringify(workspaceSession));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Ignore crash-cache persistence failures.
|
|
153
|
+
}
|
|
154
|
+
}, [storageKey, workspaceSession, workspaceSessionHydrated]);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!workspaceSessionHydrated)
|
|
157
|
+
return;
|
|
158
|
+
if (!enabled) {
|
|
159
|
+
setRemoteHydrated(true);
|
|
160
|
+
setRemoteStatus('idle');
|
|
161
|
+
setRemoteError(null);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let cancelled = false;
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
setRemoteHydrated(false);
|
|
167
|
+
setRemoteStatus('loading');
|
|
168
|
+
setRemoteError(null);
|
|
169
|
+
const loadRemoteWorkspaceSession = async () => {
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch(remoteEndpoint, {
|
|
172
|
+
method: 'GET',
|
|
173
|
+
cache: 'no-store',
|
|
174
|
+
credentials: 'same-origin',
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
});
|
|
177
|
+
const payload = await response.json().catch(() => null);
|
|
178
|
+
if (!response.ok || !payload?.success) {
|
|
179
|
+
throw new Error(resolveRemoteError(payload, response.status, 'Workspace session load failed'));
|
|
180
|
+
}
|
|
181
|
+
if (cancelled)
|
|
182
|
+
return;
|
|
183
|
+
const remoteSessionRaw = payload?.session ?? null;
|
|
184
|
+
setWorkspaceSessionState((prev) => {
|
|
185
|
+
const localSession = normalizeSession(prev);
|
|
186
|
+
if (!remoteSessionRaw) {
|
|
187
|
+
return localSession;
|
|
188
|
+
}
|
|
189
|
+
const remoteSession = normalizeSession(remoteSessionRaw);
|
|
190
|
+
if (shouldPreferLocalSession(localSession, remoteSession)) {
|
|
191
|
+
return localSession;
|
|
192
|
+
}
|
|
193
|
+
lastSyncedSignatureRef.current = JSON.stringify(remoteSession);
|
|
194
|
+
const localSignature = JSON.stringify(localSession);
|
|
195
|
+
const remoteSignature = lastSyncedSignatureRef.current;
|
|
196
|
+
return localSignature === remoteSignature ? prev : remoteSession;
|
|
197
|
+
});
|
|
198
|
+
setRemoteStatus('ready');
|
|
199
|
+
setRemoteError(null);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (cancelled || isAbortError(error))
|
|
203
|
+
return;
|
|
204
|
+
setRemoteStatus('error');
|
|
205
|
+
setRemoteError(error instanceof Error ? error.message : 'Workspace session load failed');
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
if (!cancelled) {
|
|
209
|
+
setRemoteHydrated(true);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
void loadRemoteWorkspaceSession();
|
|
214
|
+
return () => {
|
|
215
|
+
cancelled = true;
|
|
216
|
+
controller.abort();
|
|
217
|
+
};
|
|
218
|
+
}, [enabled, normalizeSession, remoteEndpoint, workspaceSessionHydrated]);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!workspaceSessionHydrated)
|
|
221
|
+
return;
|
|
222
|
+
if (!enabled || !remoteHydrated)
|
|
223
|
+
return;
|
|
224
|
+
const normalizedSession = normalizeSession(workspaceSession);
|
|
225
|
+
const nextSignature = JSON.stringify(normalizedSession);
|
|
226
|
+
if (nextSignature === lastSyncedSignatureRef.current) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (remoteSaveTimerRef.current) {
|
|
230
|
+
clearTimeout(remoteSaveTimerRef.current);
|
|
231
|
+
remoteSaveTimerRef.current = null;
|
|
232
|
+
}
|
|
233
|
+
remoteSaveTimerRef.current = setTimeout(() => {
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
const persistRemoteWorkspaceSession = async () => {
|
|
236
|
+
try {
|
|
237
|
+
const response = await fetch(remoteEndpoint, {
|
|
238
|
+
method: 'PUT',
|
|
239
|
+
cache: 'no-store',
|
|
240
|
+
credentials: 'same-origin',
|
|
241
|
+
headers: {
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify({ session: normalizedSession }),
|
|
245
|
+
signal: controller.signal,
|
|
246
|
+
});
|
|
247
|
+
const payload = await response.json().catch(() => null);
|
|
248
|
+
if (!response.ok || !payload?.success) {
|
|
249
|
+
throw new Error(resolveRemoteError(payload, response.status, 'Workspace session save failed'));
|
|
250
|
+
}
|
|
251
|
+
const savedSession = normalizeSession(payload?.session ?? normalizedSession);
|
|
252
|
+
const savedSignature = JSON.stringify(savedSession);
|
|
253
|
+
lastSyncedSignatureRef.current = savedSignature;
|
|
254
|
+
setRemoteStatus('ready');
|
|
255
|
+
setRemoteError(null);
|
|
256
|
+
setWorkspaceSessionState((prev) => {
|
|
257
|
+
const localSession = normalizeSession(prev);
|
|
258
|
+
const localSignature = JSON.stringify(localSession);
|
|
259
|
+
if (localSignature === savedSignature || shouldPreferLocalSession(localSession, savedSession)) {
|
|
260
|
+
return prev;
|
|
261
|
+
}
|
|
262
|
+
return savedSession;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (isAbortError(error))
|
|
267
|
+
return;
|
|
268
|
+
setRemoteStatus('error');
|
|
269
|
+
setRemoteError(error instanceof Error ? error.message : 'Workspace session save failed');
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
void persistRemoteWorkspaceSession();
|
|
273
|
+
return () => controller.abort();
|
|
274
|
+
}, REMOTE_SAVE_DEBOUNCE_MS);
|
|
275
|
+
return () => {
|
|
276
|
+
if (remoteSaveTimerRef.current) {
|
|
277
|
+
clearTimeout(remoteSaveTimerRef.current);
|
|
278
|
+
remoteSaveTimerRef.current = null;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}, [enabled, normalizeSession, remoteEndpoint, remoteHydrated, workspaceSession, workspaceSessionHydrated]);
|
|
282
|
+
return {
|
|
283
|
+
workspaceSession,
|
|
284
|
+
workspaceSessionHydrated,
|
|
285
|
+
remoteHydrated,
|
|
286
|
+
remoteStatus,
|
|
287
|
+
remoteError,
|
|
288
|
+
setWorkspaceSession,
|
|
289
|
+
clearWorkspaceSession,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGrasshopperSession.d.ts","sourceRoot":"","sources":["../src/useGrasshopperSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACb,MAAM,6BAA6B,CAAC;AAQrC,KAAK,iBAAiB,GAAG,gBAAgB,GAAG;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"useGrasshopperSession.d.ts","sourceRoot":"","sources":["../src/useGrasshopperSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACb,MAAM,6BAA6B,CAAC;AAQrC,KAAK,iBAAiB,GAAG,gBAAgB,GAAG;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAuGF,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;QACnB,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC5B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;IACnD,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACrD,KAAK,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IAC5F,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EACpC,QAA0B,EAC1B,OAAO,EACP,kBAAkB,EAClB,UAAU,EACV,OAAO,GACR,GAAE,yBAA8B,GAAG,uBAAuB,CAgI1D"}
|
|
@@ -2,6 +2,8 @@ import { useCallback, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { buildGrasshopperDefaults, buildGrasshopperValues, solveGrasshopperFile, warmGrasshopperSolver, } from "@treasuryspatial/viewer-kit";
|
|
3
3
|
const sanitizeGhKey = (value) => value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4
4
|
const resolveGhParamName = (input, index) => {
|
|
5
|
+
if (input.paramName)
|
|
6
|
+
return input.paramName;
|
|
5
7
|
if (input.name)
|
|
6
8
|
return input.name;
|
|
7
9
|
if (input.nickname)
|
|
@@ -56,10 +58,14 @@ const buildGhPanelField = (input, index) => {
|
|
|
56
58
|
const label = resolveGhLabel(input, index);
|
|
57
59
|
const type = (input.type ?? "").toLowerCase();
|
|
58
60
|
const options = Array.isArray(input.options)
|
|
59
|
-
? input.options.map((option, optionIndex) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
? input.options.map((option, optionIndex) => {
|
|
62
|
+
const optionRecord = typeof option === "object" && option !== null ? option : null;
|
|
63
|
+
const value = optionRecord?.value ?? optionIndex;
|
|
64
|
+
return {
|
|
65
|
+
value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : optionIndex,
|
|
66
|
+
label: typeof optionRecord?.label === "string" ? optionRecord.label : String(value),
|
|
67
|
+
};
|
|
68
|
+
})
|
|
63
69
|
: undefined;
|
|
64
70
|
if (options && options.length > 0) {
|
|
65
71
|
return { key, label, control: "select", options };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useModeShellState.d.ts","sourceRoot":"","sources":["../src/useModeShellState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"useModeShellState.d.ts","sourceRoot":"","sources":["../src/useModeShellState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAKtE,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAWF,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,MAAM,EACN,gBAAuB,EACvB,kBAA0B,EAC1B,oBAA4B,EAC5B,OAAc,GACf,EAAE,wBAAwB;;;;;;;;;;EAyH1B"}
|