@signalsafe/tree-spec-editor-react 0.1.1 → 0.1.2
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/LICENSE +21 -0
- package/README.md +28 -7
- package/dist/GraphEditorCanvasContext.d.ts +26 -0
- package/dist/GraphEditorCanvasContext.d.ts.map +1 -0
- package/dist/GraphEditorCanvasContext.js +20 -0
- package/dist/TreeSpecGraphEditor.d.ts +22 -1
- package/dist/TreeSpecGraphEditor.d.ts.map +1 -1
- package/dist/TreeSpecGraphEditor.js +133 -260
- package/dist/canvas/constants.d.ts +41 -0
- package/dist/canvas/constants.d.ts.map +1 -0
- package/dist/canvas/constants.js +40 -0
- package/dist/canvas/edgeBuilders.d.ts +6 -0
- package/dist/canvas/edgeBuilders.d.ts.map +1 -0
- package/dist/canvas/edgeBuilders.js +57 -0
- package/dist/canvas/edgeStyle.d.ts +16 -0
- package/dist/canvas/edgeStyle.d.ts.map +1 -0
- package/dist/canvas/edgeStyle.js +44 -0
- package/dist/canvas/focusChoice.d.ts +3 -0
- package/dist/canvas/focusChoice.d.ts.map +1 -0
- package/dist/canvas/focusChoice.js +12 -0
- package/dist/canvas/typeGuards.d.ts +3 -0
- package/dist/canvas/typeGuards.d.ts.map +1 -0
- package/dist/canvas/typeGuards.js +16 -0
- package/dist/contextMenu/GraphCanvasContextMenu.d.ts +10 -0
- package/dist/contextMenu/GraphCanvasContextMenu.d.ts.map +1 -0
- package/dist/contextMenu/GraphCanvasContextMenu.js +39 -0
- package/dist/contextMenu/types.d.ts +11 -0
- package/dist/contextMenu/types.d.ts.map +1 -0
- package/dist/contextMenu/types.js +1 -0
- package/dist/hooks/keyboardShortcutDispatch.d.ts +30 -0
- package/dist/hooks/keyboardShortcutDispatch.d.ts.map +1 -0
- package/dist/hooks/keyboardShortcutDispatch.js +88 -0
- package/dist/hooks/types.d.ts +32 -2
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/hooks/useCanvasContextMenu.d.ts +15 -0
- package/dist/hooks/useCanvasContextMenu.d.ts.map +1 -0
- package/dist/hooks/useCanvasContextMenu.js +50 -0
- package/dist/hooks/useCanvasGraphState.d.ts +29 -0
- package/dist/hooks/useCanvasGraphState.d.ts.map +1 -0
- package/dist/hooks/useCanvasGraphState.js +150 -0
- package/dist/hooks/useCanvasIssueIndex.d.ts +12 -0
- package/dist/hooks/useCanvasIssueIndex.d.ts.map +1 -0
- package/dist/hooks/useCanvasIssueIndex.js +32 -0
- package/dist/hooks/useCanvasNodeResize.d.ts +17 -0
- package/dist/hooks/useCanvasNodeResize.d.ts.map +1 -0
- package/dist/hooks/useCanvasNodeResize.js +53 -0
- package/dist/hooks/useCanvasViewport.d.ts +12 -0
- package/dist/hooks/useCanvasViewport.d.ts.map +1 -0
- package/dist/hooks/useCanvasViewport.js +84 -0
- package/dist/hooks/useChoiceDragDrop.d.ts +25 -0
- package/dist/hooks/useChoiceDragDrop.d.ts.map +1 -0
- package/dist/hooks/useChoiceDragDrop.js +40 -0
- package/dist/hooks/useEditorAdapter.d.ts +46 -0
- package/dist/hooks/useEditorAdapter.d.ts.map +1 -0
- package/dist/hooks/useEditorAdapter.js +281 -0
- package/dist/hooks/useEditorAutosave.d.ts +18 -0
- package/dist/hooks/useEditorAutosave.d.ts.map +1 -0
- package/dist/hooks/useEditorAutosave.js +37 -0
- package/dist/hooks/useEditorHistory.d.ts +16 -0
- package/dist/hooks/useEditorHistory.d.ts.map +1 -0
- package/dist/hooks/useEditorHistory.js +103 -0
- package/dist/hooks/useEditorSelection.d.ts +22 -0
- package/dist/hooks/useEditorSelection.d.ts.map +1 -0
- package/dist/hooks/useEditorSelection.js +76 -0
- package/dist/hooks/useGraphConnect.d.ts +22 -0
- package/dist/hooks/useGraphConnect.d.ts.map +1 -0
- package/dist/hooks/useGraphConnect.js +75 -0
- package/dist/hooks/useTreeSpecEditor.d.ts +0 -8
- package/dist/hooks/useTreeSpecEditor.d.ts.map +1 -1
- package/dist/hooks/useTreeSpecEditor.js +231 -462
- package/dist/nodes/ChoiceCanvasRow.d.ts +10 -0
- package/dist/nodes/ChoiceCanvasRow.d.ts.map +1 -0
- package/dist/nodes/ChoiceCanvasRow.js +55 -0
- package/dist/nodes/EndNode.d.ts +3 -0
- package/dist/nodes/EndNode.d.ts.map +1 -0
- package/dist/nodes/EndNode.js +7 -0
- package/dist/nodes/PromptNode.d.ts +6 -0
- package/dist/nodes/PromptNode.d.ts.map +1 -0
- package/dist/nodes/PromptNode.js +51 -0
- package/dist/nodes/PromptNodeChoicesList.d.ts +14 -0
- package/dist/nodes/PromptNodeChoicesList.d.ts.map +1 -0
- package/dist/nodes/PromptNodeChoicesList.js +24 -0
- package/dist/nodes/PromptNodeHeader.d.ts +10 -0
- package/dist/nodes/PromptNodeHeader.d.ts.map +1 -0
- package/dist/nodes/PromptNodeHeader.js +6 -0
- package/dist/nodes/PromptNodeIssueBadges.d.ts +7 -0
- package/dist/nodes/PromptNodeIssueBadges.d.ts.map +1 -0
- package/dist/nodes/PromptNodeIssueBadges.js +6 -0
- package/dist/nodes/PromptNodeToolbar.d.ts +6 -0
- package/dist/nodes/PromptNodeToolbar.d.ts.map +1 -0
- package/dist/nodes/PromptNodeToolbar.js +5 -0
- package/dist/nodes/types.d.ts +13 -0
- package/dist/nodes/types.d.ts.map +1 -0
- package/dist/nodes/types.js +1 -0
- package/dist/utils/joinClasses.d.ts +2 -0
- package/dist/utils/joinClasses.d.ts.map +1 -0
- package/dist/utils/joinClasses.js +3 -0
- package/package.json +30 -12
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { compileTreeSpec, decompileTreeSpec,
|
|
3
|
-
import { applyTreeTemplate, autoLayoutTree, AUTOSAVE_STATUS,
|
|
2
|
+
import { compileTreeSpec, decompileTreeSpec, TERMINAL_OUTCOME, TREE_SPEC_ISSUE_SEVERITY, } from '@signalsafe/tree-spec';
|
|
3
|
+
import { applyTreeTemplate, autoLayoutTree, AUTOSAVE_STATUS, deleteNode, deleteTransitionsForChoice, END_NODE_ID, getNextSpawnPosition, getTransition, GRAPH_SELECTION_KIND, moveChoiceInTree, moveNodeChoice, patchChoiceEdgeHints, patchGraphEditorMeta, renameNodeChoiceId, safeUUID, upsertTransition, } from '@signalsafe/tree-spec-editor-core';
|
|
4
|
+
import { useEditorAdapter } from './useEditorAdapter';
|
|
5
|
+
import { useEditorAutosave } from './useEditorAutosave';
|
|
6
|
+
import { useEditorHistory } from './useEditorHistory';
|
|
7
|
+
import { useEditorSelection } from './useEditorSelection';
|
|
8
|
+
import { dispatchEditorKeyboardShortcut, resolveEditorKeyboardShortcutAction } from './keyboardShortcutDispatch';
|
|
4
9
|
const DEFAULT_AUTOSAVE_DEBOUNCE_MS = 2500;
|
|
5
10
|
function isTextFieldTarget(target) {
|
|
6
11
|
const tag = target instanceof HTMLElement ? target.tagName.toLowerCase() : '';
|
|
@@ -14,69 +19,56 @@ function isTextFieldTarget(target) {
|
|
|
14
19
|
* audit, clone-to-draft, selection, focus + fit-view, node/choice operations,
|
|
15
20
|
* and keyboard shortcuts. Consumers compose their own UI (toolbar, panels,
|
|
16
21
|
* modals, layout) on top of the returned state + actions.
|
|
17
|
-
*
|
|
18
|
-
* Boundary commitments:
|
|
19
|
-
* - **No router**: routing is host-injected via `onPreview` / `onCloneNavigate`.
|
|
20
|
-
* - **No UI library**: returns data only; never renders JSX.
|
|
21
|
-
* - **No simulator runtime**: hosts inject `computeRuntimeIssues` if needed.
|
|
22
|
-
*
|
|
23
|
-
* See {@link UseTreeSpecEditorOptions} for full option documentation and
|
|
24
|
-
* {@link UseTreeSpecEditorResult} for the returned shape.
|
|
25
22
|
*/
|
|
26
23
|
export function useTreeSpecEditor(options) {
|
|
27
|
-
const { adapter, entityId, autosaveDebounceMs = DEFAULT_AUTOSAVE_DEBOUNCE_MS, enableAutosave = true, enableKeyboardShortcuts = true,
|
|
28
|
-
|
|
29
|
-
// Core editor state.
|
|
30
|
-
// -----------------------------------------------------------------------
|
|
31
|
-
const [loading, setLoading] = useState(true);
|
|
32
|
-
const [saving, setSaving] = useState(false);
|
|
33
|
-
const [publishing, setPublishing] = useState(false);
|
|
24
|
+
const { adapter, entityId, autosaveDebounceMs = DEFAULT_AUTOSAVE_DEBOUNCE_MS, enableAutosave = true, enableKeyboardShortcuts = true, computeRuntimeIssues, debugMode = false, onPreview, } = options;
|
|
25
|
+
const lastSavedKeyRef = useRef('');
|
|
34
26
|
const [autosaveStatus, setAutosaveStatus] = useState(AUTOSAVE_STATUS.IDLE);
|
|
35
|
-
const [rawTreeSpec, setRawTreeSpec] = useState(null);
|
|
36
|
-
const [tree, setTree] = useState(null);
|
|
37
27
|
const [isPublished, setIsPublished] = useState(false);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
28
|
+
const history = useEditorHistory(isPublished);
|
|
29
|
+
const { tree, canUndo, canRedo, hasCopiedNode, commitTree, replaceTreeWithoutHistory, undo, redo, copySelectedNode: copySelectedNodeInternal, pasteCopiedNode: pasteCopiedNodeInternal, duplicateNodeById: duplicateNodeByIdInternal, } = history;
|
|
30
|
+
const adapterState = useEditorAdapter({
|
|
31
|
+
options,
|
|
32
|
+
tree,
|
|
33
|
+
isPublished,
|
|
34
|
+
setIsPublished,
|
|
35
|
+
replaceTreeWithoutHistory,
|
|
36
|
+
lastSavedKeyRef,
|
|
37
|
+
setAutosaveStatus,
|
|
38
|
+
});
|
|
39
|
+
const selectionState = useEditorSelection(tree);
|
|
40
|
+
const { selection, focusNodeId, focusChoiceId, fitViewNonce, selectedNode, selectedEdge, inspectorNode, selectChoice, applySelection, setFocusNodeId, setFocusChoiceId, triggerResetView, selectIssue, setSelection, } = selectionState;
|
|
41
|
+
useEditorAutosave({
|
|
42
|
+
enableAutosave,
|
|
43
|
+
autosaveDebounceMs,
|
|
44
|
+
entityId,
|
|
45
|
+
tree,
|
|
46
|
+
isPublished,
|
|
47
|
+
saving: adapterState.saving,
|
|
48
|
+
publishing: adapterState.publishing,
|
|
49
|
+
autosaveStatus,
|
|
50
|
+
setAutosaveStatus,
|
|
51
|
+
lastSavedKeyRef,
|
|
52
|
+
saveDraftRef: adapterState.saveDraftRef,
|
|
53
|
+
});
|
|
64
54
|
const baselineTree = useMemo(() => {
|
|
65
|
-
if (!rawTreeSpec)
|
|
55
|
+
if (!adapterState.rawTreeSpec)
|
|
66
56
|
return null;
|
|
67
57
|
try {
|
|
68
|
-
return decompileTreeSpec(rawTreeSpec);
|
|
58
|
+
return decompileTreeSpec(adapterState.rawTreeSpec);
|
|
69
59
|
}
|
|
70
60
|
catch {
|
|
71
61
|
return null;
|
|
72
62
|
}
|
|
73
|
-
}, [rawTreeSpec]);
|
|
63
|
+
}, [adapterState.rawTreeSpec]);
|
|
74
64
|
const compiledTreeSpec = useMemo(() => (tree ? compileTreeSpec(tree) : null), [tree]);
|
|
75
65
|
const runtimeIssues = useMemo(() => (compiledTreeSpec && computeRuntimeIssues ? computeRuntimeIssues(compiledTreeSpec) : []), [compiledTreeSpec, computeRuntimeIssues]);
|
|
76
66
|
const issues = useMemo(() => {
|
|
77
67
|
const seen = new Set();
|
|
78
68
|
const out = [];
|
|
79
|
-
const groups = debugMode
|
|
69
|
+
const groups = debugMode
|
|
70
|
+
? [adapterState.localIssues, adapterState.serverIssues]
|
|
71
|
+
: [adapterState.localIssues, runtimeIssues, adapterState.serverIssues];
|
|
80
72
|
for (const arr of groups) {
|
|
81
73
|
for (const i of arr) {
|
|
82
74
|
const key = `${i.severity}|${i.message}|${i.node_id ?? ''}|${i.choice_id ?? ''}`;
|
|
@@ -87,201 +79,15 @@ export function useTreeSpecEditor(options) {
|
|
|
87
79
|
}
|
|
88
80
|
}
|
|
89
81
|
return out;
|
|
90
|
-
}, [localIssues, serverIssues, runtimeIssues, debugMode]);
|
|
82
|
+
}, [adapterState.localIssues, adapterState.serverIssues, runtimeIssues, debugMode]);
|
|
91
83
|
const canPublish = useMemo(() => !issues.some((i) => i.severity === TREE_SPEC_ISSUE_SEVERITY.ERROR), [issues]);
|
|
92
|
-
const selectedNode = useMemo(() => {
|
|
93
|
-
if (!tree || selection.kind !== GRAPH_SELECTION_KIND.NODE || !selection.id)
|
|
94
|
-
return null;
|
|
95
|
-
if (selection.id === END_NODE_ID)
|
|
96
|
-
return null;
|
|
97
|
-
return tree.nodes[selection.id] ?? null;
|
|
98
|
-
}, [tree, selection]);
|
|
99
|
-
const selectedEdge = useMemo(() => {
|
|
100
|
-
if (!tree || selection.kind !== GRAPH_SELECTION_KIND.EDGE || !selection.id)
|
|
101
|
-
return null;
|
|
102
|
-
return tree.transitions.find((t) => t.id === selection.id) ?? null;
|
|
103
|
-
}, [tree, selection]);
|
|
104
|
-
// -----------------------------------------------------------------------
|
|
105
|
-
// Initial load + local lint recomputation.
|
|
106
|
-
// -----------------------------------------------------------------------
|
|
107
|
-
// Stash latest validate in a ref so the keyboard effect can call it
|
|
108
|
-
// without re-subscribing on every tree change. Populated below.
|
|
109
|
-
const validateRef = useRef(async () => undefined);
|
|
110
|
-
const saveDraftRef = useRef(async () => undefined);
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
let cancelled = false;
|
|
113
|
-
async function load() {
|
|
114
|
-
if (!entityId)
|
|
115
|
-
return;
|
|
116
|
-
setLoading(true);
|
|
117
|
-
setVersionInfo(null);
|
|
118
|
-
try {
|
|
119
|
-
const raw = await adapter.getVersion(entityId);
|
|
120
|
-
if (cancelled)
|
|
121
|
-
return;
|
|
122
|
-
setIsPublished(Boolean(raw?.is_published));
|
|
123
|
-
setVersionInfo(raw?.info ?? null);
|
|
124
|
-
const rawSpec = raw?.tree_spec;
|
|
125
|
-
const spec = coerceRawSpec(rawSpec);
|
|
126
|
-
if (spec == null) {
|
|
127
|
-
setRawTreeSpec(null);
|
|
128
|
-
setTree(null);
|
|
129
|
-
setVersionInfo(raw?.info ?? null);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
setRawTreeSpec(spec);
|
|
133
|
-
const nextTree = decompileTreeSpec(spec);
|
|
134
|
-
setTree(nextTree);
|
|
135
|
-
setLocalIssues([...lintTreeSpecWire(spec), ...lintEditorTree(nextTree)]);
|
|
136
|
-
if (shouldQueueInitialValidation(raw?.is_published)) {
|
|
137
|
-
queueMicrotask(() => {
|
|
138
|
-
void validateRef.current(spec);
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
lastSavedKeyRef.current = JSON.stringify(spec);
|
|
142
|
-
setAutosaveStatus(AUTOSAVE_STATUS.IDLE);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
if (cancelled)
|
|
147
|
-
return;
|
|
148
|
-
setRawTreeSpec(null);
|
|
149
|
-
setTree(null);
|
|
150
|
-
setVersionInfo(null);
|
|
151
|
-
}
|
|
152
|
-
finally {
|
|
153
|
-
if (!cancelled)
|
|
154
|
-
setLoading(false);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (entityId)
|
|
158
|
-
void load();
|
|
159
|
-
return () => {
|
|
160
|
-
cancelled = true;
|
|
161
|
-
};
|
|
162
|
-
}, [entityId, adapter, coerceRawSpec, shouldQueueInitialValidation]);
|
|
163
|
-
// Local issue list stays focused on structural/editor checks; recomputed
|
|
164
|
-
// every time the compiled spec changes (which happens on every tree edit).
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (!tree || !compiledTreeSpec)
|
|
167
|
-
return;
|
|
168
|
-
setLocalIssues([...lintTreeSpecWire(compiledTreeSpec), ...lintEditorTree(tree)]);
|
|
169
|
-
}, [tree, compiledTreeSpec]);
|
|
170
|
-
// -----------------------------------------------------------------------
|
|
171
|
-
// Validation.
|
|
172
|
-
// -----------------------------------------------------------------------
|
|
173
|
-
const validate = useCallback(async (specOverride) => {
|
|
174
|
-
if (!entityId)
|
|
175
|
-
return undefined;
|
|
176
|
-
const spec = specOverride ?? (tree ? compileTreeSpec(tree) : rawTreeSpec);
|
|
177
|
-
if (!spec)
|
|
178
|
-
return undefined;
|
|
179
|
-
if (!adapter.validate) {
|
|
180
|
-
setLastValidatedAt(new Date().toISOString());
|
|
181
|
-
return { valid: true };
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const payload = await adapter.validate(entityId, spec);
|
|
185
|
-
const nextIssues = (payload?.issues ?? []).map((i) => {
|
|
186
|
-
const severity = i.severity ?? i.level ?? TREE_SPEC_ISSUE_SEVERITY.ERROR;
|
|
187
|
-
return {
|
|
188
|
-
severity,
|
|
189
|
-
message: String(i.message ?? 'Issue'),
|
|
190
|
-
node_id: i.node_id ?? undefined,
|
|
191
|
-
choice_id: i.choice_id ?? undefined,
|
|
192
|
-
};
|
|
193
|
-
});
|
|
194
|
-
setServerIssues(nextIssues);
|
|
195
|
-
setLastValidatedAt(new Date().toISOString());
|
|
196
|
-
return payload;
|
|
197
|
-
}
|
|
198
|
-
catch (err) {
|
|
199
|
-
const ex = err;
|
|
200
|
-
const errData = ex?.response?.data;
|
|
201
|
-
const msg = errData?.detail ??
|
|
202
|
-
errData?.error ??
|
|
203
|
-
err?.message ??
|
|
204
|
-
'Validation failed';
|
|
205
|
-
const parsed = parseServerErrorMessage(String(msg));
|
|
206
|
-
setServerIssues(parsed ?? [{ severity: TREE_SPEC_ISSUE_SEVERITY.ERROR, message: String(msg) }]);
|
|
207
|
-
setLastValidatedAt(new Date().toISOString());
|
|
208
|
-
return {
|
|
209
|
-
valid: false,
|
|
210
|
-
issues: [{ severity: TREE_SPEC_ISSUE_SEVERITY.ERROR, message: String(msg) }],
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
}, [adapter, entityId, parseServerErrorMessage, rawTreeSpec, tree]);
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
validateRef.current = validate;
|
|
216
|
-
}, [validate]);
|
|
217
|
-
// -----------------------------------------------------------------------
|
|
218
|
-
// Save draft + autosave.
|
|
219
|
-
// -----------------------------------------------------------------------
|
|
220
|
-
const saveDraft = useCallback(async () => {
|
|
221
|
-
if (!entityId || !tree)
|
|
222
|
-
return;
|
|
223
|
-
if (isPublished)
|
|
224
|
-
return;
|
|
225
|
-
setSaving(true);
|
|
226
|
-
setAutosaveStatus(AUTOSAVE_STATUS.SAVING);
|
|
227
|
-
try {
|
|
228
|
-
const compiled = compileTreeSpec(tree);
|
|
229
|
-
await adapter.updateVersion(entityId, { tree_spec: compiled });
|
|
230
|
-
setRawTreeSpec(compiled);
|
|
231
|
-
lastSavedKeyRef.current = JSON.stringify(compiled);
|
|
232
|
-
setAutosaveStatus(AUTOSAVE_STATUS.SAVED);
|
|
233
|
-
}
|
|
234
|
-
finally {
|
|
235
|
-
setSaving(false);
|
|
236
|
-
}
|
|
237
|
-
}, [adapter, entityId, isPublished, tree]);
|
|
238
|
-
useEffect(() => {
|
|
239
|
-
saveDraftRef.current = saveDraft;
|
|
240
|
-
}, [saveDraft]);
|
|
241
|
-
useEffect(() => {
|
|
242
|
-
if (!enableAutosave)
|
|
243
|
-
return;
|
|
244
|
-
if (!entityId || !tree)
|
|
245
|
-
return;
|
|
246
|
-
if (isPublished)
|
|
247
|
-
return;
|
|
248
|
-
if (saving || publishing)
|
|
249
|
-
return;
|
|
250
|
-
const compiled = compileTreeSpec(tree);
|
|
251
|
-
const key = JSON.stringify(compiled);
|
|
252
|
-
if (key === lastSavedKeyRef.current) {
|
|
253
|
-
if (autosaveStatus !== AUTOSAVE_STATUS.SAVED)
|
|
254
|
-
setAutosaveStatus(AUTOSAVE_STATUS.IDLE);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
setAutosaveStatus(AUTOSAVE_STATUS.DIRTY);
|
|
258
|
-
if (autosaveTimerRef.current) {
|
|
259
|
-
globalThis.clearTimeout(autosaveTimerRef.current);
|
|
260
|
-
autosaveTimerRef.current = null;
|
|
261
|
-
}
|
|
262
|
-
autosaveTimerRef.current = globalThis.setTimeout(() => {
|
|
263
|
-
void saveDraftRef.current();
|
|
264
|
-
}, autosaveDebounceMs);
|
|
265
|
-
return () => {
|
|
266
|
-
if (autosaveTimerRef.current) {
|
|
267
|
-
globalThis.clearTimeout(autosaveTimerRef.current);
|
|
268
|
-
autosaveTimerRef.current = null;
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
// NOTE: autosaveStatus is intentionally NOT in deps. The dirty/idle
|
|
272
|
-
// bookkeeping above flips status, and re-running the effect on every
|
|
273
|
-
// status change would reset the debounce timer.
|
|
274
|
-
}, [tree, entityId, isPublished, saving, publishing, autosaveDebounceMs, enableAutosave]);
|
|
275
|
-
// -----------------------------------------------------------------------
|
|
276
|
-
// Publish.
|
|
277
|
-
// -----------------------------------------------------------------------
|
|
278
84
|
const publish = useCallback(async () => {
|
|
279
85
|
if (!entityId || !tree || !adapter.publish)
|
|
280
86
|
return;
|
|
281
|
-
setPublishing(true);
|
|
87
|
+
adapterState.setPublishing(true);
|
|
282
88
|
try {
|
|
283
89
|
const compiled = compileTreeSpec(tree);
|
|
284
|
-
const vr = await validate(compiled);
|
|
90
|
+
const vr = await adapterState.validate(compiled);
|
|
285
91
|
if (vr?.valid === false)
|
|
286
92
|
return;
|
|
287
93
|
if (!canPublish)
|
|
@@ -290,110 +96,12 @@ export function useTreeSpecEditor(options) {
|
|
|
290
96
|
setIsPublished(true);
|
|
291
97
|
}
|
|
292
98
|
finally {
|
|
293
|
-
setPublishing(false);
|
|
294
|
-
}
|
|
295
|
-
}, [adapter, canPublish, entityId, tree, validate]);
|
|
296
|
-
// -----------------------------------------------------------------------
|
|
297
|
-
// Snapshots: list (when modal opens), create, restore.
|
|
298
|
-
// -----------------------------------------------------------------------
|
|
299
|
-
useEffect(() => {
|
|
300
|
-
const listSnapshots = adapter.listSnapshots;
|
|
301
|
-
if (!listSnapshots || !showDraftHistory || !entityId)
|
|
302
|
-
return;
|
|
303
|
-
let cancelled = false;
|
|
304
|
-
setLoadingSnapshots(true);
|
|
305
|
-
void (async () => {
|
|
306
|
-
try {
|
|
307
|
-
const list = await listSnapshots(entityId);
|
|
308
|
-
if (!cancelled)
|
|
309
|
-
setSnapshots(list);
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
if (!cancelled)
|
|
313
|
-
setLoadingSnapshots(false);
|
|
314
|
-
}
|
|
315
|
-
})();
|
|
316
|
-
return () => {
|
|
317
|
-
cancelled = true;
|
|
318
|
-
};
|
|
319
|
-
}, [adapter, showDraftHistory, entityId]);
|
|
320
|
-
const createSnapshot = useCallback(async () => {
|
|
321
|
-
if (!entityId || !tree || !adapter.createSnapshot || !adapter.listSnapshots)
|
|
322
|
-
return;
|
|
323
|
-
setCreatingSnapshot(true);
|
|
324
|
-
try {
|
|
325
|
-
const compiled = compileTreeSpec(tree);
|
|
326
|
-
await adapter.createSnapshot(entityId, { label: '', tree_spec: compiled });
|
|
327
|
-
const list = await adapter.listSnapshots(entityId);
|
|
328
|
-
setSnapshots(list);
|
|
329
|
-
}
|
|
330
|
-
finally {
|
|
331
|
-
setCreatingSnapshot(false);
|
|
332
|
-
}
|
|
333
|
-
}, [adapter, entityId, tree]);
|
|
334
|
-
const restoreSnapshot = useCallback(async (snapshotId) => {
|
|
335
|
-
if (!entityId || !adapter.restoreSnapshot)
|
|
336
|
-
return;
|
|
337
|
-
setRestoringSnapshotId(snapshotId);
|
|
338
|
-
try {
|
|
339
|
-
const { tree_spec: rawSpec } = await adapter.restoreSnapshot(entityId, snapshotId);
|
|
340
|
-
const spec = coerceRawSpec(rawSpec);
|
|
341
|
-
if (spec != null) {
|
|
342
|
-
setRawTreeSpec(spec);
|
|
343
|
-
setTree(decompileTreeSpec(spec));
|
|
344
|
-
lastSavedKeyRef.current = JSON.stringify(spec);
|
|
345
|
-
setAutosaveStatus(AUTOSAVE_STATUS.IDLE);
|
|
346
|
-
}
|
|
347
|
-
setShowDraftHistory(false);
|
|
348
|
-
}
|
|
349
|
-
finally {
|
|
350
|
-
setRestoringSnapshotId(null);
|
|
351
|
-
}
|
|
352
|
-
}, [adapter, coerceRawSpec, entityId]);
|
|
353
|
-
// -----------------------------------------------------------------------
|
|
354
|
-
// Audit: list (when modal opens).
|
|
355
|
-
// -----------------------------------------------------------------------
|
|
356
|
-
useEffect(() => {
|
|
357
|
-
const listAudit = adapter.listAudit;
|
|
358
|
-
if (!listAudit || !showAudit || !entityId)
|
|
359
|
-
return;
|
|
360
|
-
let cancelled = false;
|
|
361
|
-
setLoadingAudit(true);
|
|
362
|
-
void (async () => {
|
|
363
|
-
try {
|
|
364
|
-
const list = await listAudit(entityId);
|
|
365
|
-
if (!cancelled)
|
|
366
|
-
setAuditEvents(list);
|
|
367
|
-
}
|
|
368
|
-
finally {
|
|
369
|
-
if (!cancelled)
|
|
370
|
-
setLoadingAudit(false);
|
|
371
|
-
}
|
|
372
|
-
})();
|
|
373
|
-
return () => {
|
|
374
|
-
cancelled = true;
|
|
375
|
-
};
|
|
376
|
-
}, [adapter, showAudit, entityId]);
|
|
377
|
-
// -----------------------------------------------------------------------
|
|
378
|
-
// Clone to Draft.
|
|
379
|
-
// -----------------------------------------------------------------------
|
|
380
|
-
const cloneToDraft = useCallback(async () => {
|
|
381
|
-
if (!entityId || !adapter.cloneToDraft)
|
|
382
|
-
return;
|
|
383
|
-
setCloning(true);
|
|
384
|
-
try {
|
|
385
|
-
const { id: newId } = await adapter.cloneToDraft(entityId);
|
|
386
|
-
if (newId)
|
|
387
|
-
onCloneNavigate?.(newId);
|
|
388
|
-
}
|
|
389
|
-
finally {
|
|
390
|
-
setCloning(false);
|
|
99
|
+
adapterState.setPublishing(false);
|
|
391
100
|
}
|
|
392
|
-
}, [adapter, entityId,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const triggerResetView = useCallback(() => setFitViewNonce((n) => n + 1), []);
|
|
101
|
+
}, [adapter, adapterState, canPublish, entityId, tree]);
|
|
102
|
+
const [nodeSearch, setNodeSearch] = useState('');
|
|
103
|
+
const [issueSearch, setIssueSearch] = useState('');
|
|
104
|
+
const [showMiniMap, setShowMiniMap] = useState(true);
|
|
397
105
|
const addNodeOfType = useCallback((type, patch) => {
|
|
398
106
|
if (!tree)
|
|
399
107
|
return undefined;
|
|
@@ -406,34 +114,34 @@ export function useTreeSpecEditor(options) {
|
|
|
406
114
|
choices: patch?.choices ?? [],
|
|
407
115
|
position: patch?.position ?? p,
|
|
408
116
|
};
|
|
409
|
-
|
|
117
|
+
commitTree({ ...tree, nodes: { ...tree.nodes, [id]: nextNode } });
|
|
410
118
|
setSelection({ kind: GRAPH_SELECTION_KIND.NODE, id });
|
|
411
119
|
setFocusNodeId(id);
|
|
412
120
|
return id;
|
|
413
|
-
}, [tree]);
|
|
121
|
+
}, [tree, commitTree, setSelection, setFocusNodeId]);
|
|
414
122
|
const autoLayout = useCallback(() => {
|
|
415
123
|
if (!tree)
|
|
416
124
|
return;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}, [tree]);
|
|
125
|
+
commitTree(autoLayoutTree(tree));
|
|
126
|
+
triggerResetView();
|
|
127
|
+
}, [tree, commitTree, triggerResetView]);
|
|
420
128
|
const insertTemplate = useCallback((spec) => {
|
|
421
129
|
if (!tree)
|
|
422
130
|
return;
|
|
423
131
|
const { nextTree, focusNodeId: spawnedFocusNodeId } = applyTreeTemplate(tree, spec);
|
|
424
|
-
|
|
132
|
+
commitTree(nextTree);
|
|
425
133
|
setSelection({ kind: GRAPH_SELECTION_KIND.NODE, id: spawnedFocusNodeId });
|
|
426
134
|
setFocusNodeId(spawnedFocusNodeId);
|
|
427
|
-
}, [tree]);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
135
|
+
}, [tree, commitTree, setSelection, setFocusNodeId]);
|
|
136
|
+
const copySelectedNode = useCallback(() => copySelectedNodeInternal(selection), [copySelectedNodeInternal, selection]);
|
|
137
|
+
const pasteCopiedNode = useCallback(() => pasteCopiedNodeInternal(setSelection, setFocusNodeId), [pasteCopiedNodeInternal, setSelection, setFocusNodeId]);
|
|
138
|
+
const duplicateNodeById = useCallback((nodeId) => duplicateNodeByIdInternal(nodeId, setSelection, setFocusNodeId), [duplicateNodeByIdInternal, setSelection, setFocusNodeId]);
|
|
431
139
|
const updateSelectedNode = useCallback((patch) => {
|
|
432
140
|
if (!tree || !selectedNode)
|
|
433
141
|
return;
|
|
434
142
|
const next = { ...selectedNode, ...patch };
|
|
435
|
-
|
|
436
|
-
}, [tree, selectedNode]);
|
|
143
|
+
commitTree({ ...tree, nodes: { ...tree.nodes, [selectedNode.id]: next } });
|
|
144
|
+
}, [tree, selectedNode, commitTree]);
|
|
437
145
|
const addChoice = useCallback(() => {
|
|
438
146
|
if (!tree || !selectedNode)
|
|
439
147
|
return;
|
|
@@ -441,19 +149,67 @@ export function useTreeSpecEditor(options) {
|
|
|
441
149
|
const nextChoice = { id: choiceId, label: 'New choice' };
|
|
442
150
|
updateSelectedNode({ choices: [...(selectedNode.choices ?? []), nextChoice] });
|
|
443
151
|
}, [tree, selectedNode, updateSelectedNode]);
|
|
152
|
+
const setChoiceType = useCallback((choiceId, typeId, defaultLabel) => {
|
|
153
|
+
if (!tree || !selectedNode)
|
|
154
|
+
return;
|
|
155
|
+
const next = renameNodeChoiceId(tree, selectedNode.id, choiceId, typeId);
|
|
156
|
+
if (!next)
|
|
157
|
+
return;
|
|
158
|
+
if (defaultLabel) {
|
|
159
|
+
const node = next.nodes[selectedNode.id];
|
|
160
|
+
if (node) {
|
|
161
|
+
const choices = (node.choices ?? []).map((choice) => {
|
|
162
|
+
if (choice.id !== typeId)
|
|
163
|
+
return choice;
|
|
164
|
+
if (choice.label === 'New choice' || !choice.label.trim()) {
|
|
165
|
+
return { ...choice, label: defaultLabel };
|
|
166
|
+
}
|
|
167
|
+
return choice;
|
|
168
|
+
});
|
|
169
|
+
next.nodes[selectedNode.id] = { ...node, choices };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
commitTree(next);
|
|
173
|
+
if (focusChoiceId === choiceId) {
|
|
174
|
+
selectChoice(selectedNode.id, typeId);
|
|
175
|
+
}
|
|
176
|
+
}, [tree, selectedNode, commitTree, focusChoiceId, selectChoice]);
|
|
444
177
|
const deleteChoice = useCallback((choiceId) => {
|
|
445
178
|
if (!tree || !selectedNode)
|
|
446
179
|
return;
|
|
447
180
|
const nextChoices = (selectedNode.choices ?? []).filter((c) => c.id !== choiceId);
|
|
448
181
|
const nextTree = deleteTransitionsForChoice(tree, selectedNode.id, choiceId);
|
|
449
|
-
|
|
182
|
+
commitTree({
|
|
450
183
|
...nextTree,
|
|
451
184
|
nodes: {
|
|
452
185
|
...nextTree.nodes,
|
|
453
186
|
[selectedNode.id]: { ...selectedNode, choices: nextChoices },
|
|
454
187
|
},
|
|
455
188
|
});
|
|
456
|
-
}, [tree, selectedNode]);
|
|
189
|
+
}, [tree, selectedNode, commitTree]);
|
|
190
|
+
const moveChoice = useCallback((choiceId, direction) => {
|
|
191
|
+
if (!tree || !selectedNode)
|
|
192
|
+
return;
|
|
193
|
+
const nextChoices = moveNodeChoice(selectedNode.choices ?? [], choiceId, direction);
|
|
194
|
+
if (!nextChoices)
|
|
195
|
+
return;
|
|
196
|
+
commitTree({
|
|
197
|
+
...tree,
|
|
198
|
+
nodes: {
|
|
199
|
+
...tree.nodes,
|
|
200
|
+
[selectedNode.id]: { ...selectedNode, choices: nextChoices },
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}, [tree, selectedNode, commitTree]);
|
|
204
|
+
const repositionChoice = useCallback((fromNodeId, choiceId, toNodeId, toIndex) => {
|
|
205
|
+
if (!tree)
|
|
206
|
+
return;
|
|
207
|
+
const next = moveChoiceInTree(tree, fromNodeId, choiceId, toNodeId, toIndex);
|
|
208
|
+
if (!next)
|
|
209
|
+
return;
|
|
210
|
+
commitTree(next);
|
|
211
|
+
selectChoice(toNodeId, choiceId);
|
|
212
|
+
}, [tree, commitTree, selectChoice]);
|
|
457
213
|
const setChoiceTarget = useCallback((choiceId, targetNodeId) => {
|
|
458
214
|
if (!tree || !selectedNode)
|
|
459
215
|
return;
|
|
@@ -467,8 +223,8 @@ export function useTreeSpecEditor(options) {
|
|
|
467
223
|
? (existing?.outcome ?? TERMINAL_OUTCOME.AT_RISK)
|
|
468
224
|
: undefined,
|
|
469
225
|
};
|
|
470
|
-
|
|
471
|
-
}, [tree, selectedNode]);
|
|
226
|
+
commitTree(upsertTransition(tree, next));
|
|
227
|
+
}, [tree, selectedNode, commitTree]);
|
|
472
228
|
const setChoiceOutcome = useCallback((choiceId, outcome) => {
|
|
473
229
|
if (!tree || !selectedNode)
|
|
474
230
|
return;
|
|
@@ -481,26 +237,35 @@ export function useTreeSpecEditor(options) {
|
|
|
481
237
|
outcome !== TERMINAL_OUTCOME.AT_RISK &&
|
|
482
238
|
outcome !== TERMINAL_OUTCOME.COMPROMISED)
|
|
483
239
|
return;
|
|
484
|
-
|
|
485
|
-
}, [tree, selectedNode]);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (!
|
|
240
|
+
commitTree(upsertTransition(tree, { ...existing, outcome }));
|
|
241
|
+
}, [tree, selectedNode, commitTree]);
|
|
242
|
+
const updateChoiceEdgeHints = useCallback((nodeId, choiceId, patch) => {
|
|
243
|
+
if (!tree)
|
|
244
|
+
return;
|
|
245
|
+
const node = tree.nodes[nodeId];
|
|
246
|
+
if (!node)
|
|
491
247
|
return;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
248
|
+
const choices = (node.choices ?? []).map((choice) => choice.id === choiceId ? patchChoiceEdgeHints(choice, patch) : choice);
|
|
249
|
+
commitTree({
|
|
250
|
+
...tree,
|
|
251
|
+
nodes: {
|
|
252
|
+
...tree.nodes,
|
|
253
|
+
[nodeId]: { ...node, choices },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}, [tree, commitTree]);
|
|
257
|
+
const setDefaultEdgeType = useCallback((edgeType) => {
|
|
258
|
+
if (!tree)
|
|
259
|
+
return;
|
|
260
|
+
commitTree(patchGraphEditorMeta(tree, { default_edge_type: edgeType }));
|
|
261
|
+
}, [tree, commitTree]);
|
|
497
262
|
const deleteNodeById = useCallback((nodeId) => {
|
|
498
263
|
if (!tree || isPublished)
|
|
499
264
|
return false;
|
|
500
265
|
const nextTree = deleteNode(tree, nodeId);
|
|
501
266
|
if (!nextTree)
|
|
502
267
|
return false;
|
|
503
|
-
|
|
268
|
+
commitTree(nextTree);
|
|
504
269
|
const wasSelected = selection.kind === GRAPH_SELECTION_KIND.NODE && selection.id === nodeId;
|
|
505
270
|
if (wasSelected) {
|
|
506
271
|
setSelection({ kind: null, id: null });
|
|
@@ -510,26 +275,21 @@ export function useTreeSpecEditor(options) {
|
|
|
510
275
|
setFocusNodeId(null);
|
|
511
276
|
}
|
|
512
277
|
return true;
|
|
513
|
-
}, [
|
|
278
|
+
}, [
|
|
279
|
+
tree,
|
|
280
|
+
isPublished,
|
|
281
|
+
selection,
|
|
282
|
+
focusNodeId,
|
|
283
|
+
commitTree,
|
|
284
|
+
setSelection,
|
|
285
|
+
setFocusNodeId,
|
|
286
|
+
setFocusChoiceId,
|
|
287
|
+
]);
|
|
514
288
|
const deleteSelectedNode = useCallback(() => {
|
|
515
289
|
if (selection.kind !== GRAPH_SELECTION_KIND.NODE || !selection.id)
|
|
516
290
|
return false;
|
|
517
291
|
return deleteNodeById(selection.id);
|
|
518
292
|
}, [deleteNodeById, selection]);
|
|
519
|
-
// -----------------------------------------------------------------------
|
|
520
|
-
// Scroll the focused choice into view when it changes.
|
|
521
|
-
// -----------------------------------------------------------------------
|
|
522
|
-
useEffect(() => {
|
|
523
|
-
if (!selectedNode || !focusChoiceId)
|
|
524
|
-
return;
|
|
525
|
-
if (typeof document === 'undefined')
|
|
526
|
-
return;
|
|
527
|
-
const el = document.getElementById(`choice-editor-${selectedNode.id}-${focusChoiceId}`);
|
|
528
|
-
el?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
529
|
-
}, [selectedNode, focusChoiceId]);
|
|
530
|
-
// -----------------------------------------------------------------------
|
|
531
|
-
// Keyboard shortcuts (Save/Validate/Preview/Duplicate/Delete).
|
|
532
|
-
// -----------------------------------------------------------------------
|
|
533
293
|
useEffect(() => {
|
|
534
294
|
if (!enableKeyboardShortcuts)
|
|
535
295
|
return;
|
|
@@ -538,77 +298,71 @@ export function useTreeSpecEditor(options) {
|
|
|
538
298
|
const onKeyDown = (e) => {
|
|
539
299
|
if (isTextFieldTarget(e.target))
|
|
540
300
|
return;
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
301
|
+
const shortcutAction = resolveEditorKeyboardShortcutAction(e, {
|
|
302
|
+
tree,
|
|
303
|
+
selection,
|
|
304
|
+
canUndo,
|
|
305
|
+
canRedo,
|
|
306
|
+
hasCopiedNode,
|
|
307
|
+
isPublished,
|
|
308
|
+
});
|
|
309
|
+
dispatchEditorKeyboardShortcut(shortcutAction, e, {
|
|
310
|
+
tree,
|
|
311
|
+
selection,
|
|
312
|
+
isPublished,
|
|
313
|
+
onPreview,
|
|
314
|
+
undo,
|
|
315
|
+
redo,
|
|
316
|
+
copySelectedNode,
|
|
317
|
+
pasteCopiedNode,
|
|
318
|
+
deleteSelectedNode,
|
|
319
|
+
saveDraft: () => void adapterState.saveDraftRef.current?.(),
|
|
320
|
+
validate: () => void adapterState.validateRef.current?.(),
|
|
321
|
+
commitTree,
|
|
322
|
+
setSelection: applySelection,
|
|
323
|
+
setFocusNodeId,
|
|
548
324
|
});
|
|
549
|
-
switch (shortcutAction) {
|
|
550
|
-
case KEYBOARD_SHORTCUT_ACTION.SAVE:
|
|
551
|
-
e.preventDefault();
|
|
552
|
-
void saveDraftRef.current();
|
|
553
|
-
return;
|
|
554
|
-
case KEYBOARD_SHORTCUT_ACTION.VALIDATE:
|
|
555
|
-
e.preventDefault();
|
|
556
|
-
void validateRef.current();
|
|
557
|
-
return;
|
|
558
|
-
case KEYBOARD_SHORTCUT_ACTION.PREVIEW:
|
|
559
|
-
if (!onPreview)
|
|
560
|
-
return;
|
|
561
|
-
e.preventDefault();
|
|
562
|
-
onPreview();
|
|
563
|
-
return;
|
|
564
|
-
case KEYBOARD_SHORTCUT_ACTION.DUPLICATE: {
|
|
565
|
-
if (!tree || !selectedNodeId)
|
|
566
|
-
return;
|
|
567
|
-
e.preventDefault();
|
|
568
|
-
const duplicated = duplicateNode(tree, selectedNodeId);
|
|
569
|
-
if (!duplicated)
|
|
570
|
-
return;
|
|
571
|
-
setTree(duplicated.nextTree);
|
|
572
|
-
setSelection({ kind: GRAPH_SELECTION_KIND.NODE, id: duplicated.nextNodeId });
|
|
573
|
-
setFocusNodeId(duplicated.nextNodeId);
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
case KEYBOARD_SHORTCUT_ACTION.DELETE: {
|
|
577
|
-
if (!tree || !selectedNodeId)
|
|
578
|
-
return;
|
|
579
|
-
if (deleteSelectedNode()) {
|
|
580
|
-
e.preventDefault();
|
|
581
|
-
}
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
default:
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
325
|
};
|
|
588
326
|
globalThis.addEventListener('keydown', onKeyDown);
|
|
589
327
|
return () => globalThis.removeEventListener('keydown', onKeyDown);
|
|
590
|
-
}, [
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
328
|
+
}, [
|
|
329
|
+
enableKeyboardShortcuts,
|
|
330
|
+
onPreview,
|
|
331
|
+
tree,
|
|
332
|
+
selection,
|
|
333
|
+
deleteSelectedNode,
|
|
334
|
+
canUndo,
|
|
335
|
+
canRedo,
|
|
336
|
+
hasCopiedNode,
|
|
337
|
+
isPublished,
|
|
338
|
+
undo,
|
|
339
|
+
redo,
|
|
340
|
+
copySelectedNode,
|
|
341
|
+
pasteCopiedNode,
|
|
342
|
+
commitTree,
|
|
343
|
+
applySelection,
|
|
344
|
+
setFocusNodeId,
|
|
345
|
+
adapterState.saveDraftRef,
|
|
346
|
+
adapterState.validateRef,
|
|
347
|
+
]);
|
|
594
348
|
const state = {
|
|
595
|
-
loading,
|
|
596
|
-
saving,
|
|
597
|
-
publishing,
|
|
598
|
-
creatingSnapshot,
|
|
599
|
-
cloning,
|
|
600
|
-
restoringSnapshotId,
|
|
349
|
+
loading: adapterState.loading,
|
|
350
|
+
saving: adapterState.saving,
|
|
351
|
+
publishing: adapterState.publishing,
|
|
352
|
+
creatingSnapshot: adapterState.creatingSnapshot,
|
|
353
|
+
cloning: adapterState.cloning,
|
|
354
|
+
restoringSnapshotId: adapterState.restoringSnapshotId,
|
|
601
355
|
autosaveStatus,
|
|
602
|
-
lastValidatedAt,
|
|
603
|
-
rawTreeSpec,
|
|
356
|
+
lastValidatedAt: adapterState.lastValidatedAt,
|
|
357
|
+
rawTreeSpec: adapterState.rawTreeSpec,
|
|
604
358
|
tree,
|
|
605
359
|
baselineTree,
|
|
606
360
|
compiledTreeSpec,
|
|
607
361
|
isPublished,
|
|
608
|
-
versionInfo,
|
|
362
|
+
versionInfo: adapterState.versionInfo,
|
|
609
363
|
hasTree: Boolean(tree),
|
|
610
|
-
localIssues,
|
|
611
|
-
serverIssues,
|
|
364
|
+
localIssues: adapterState.localIssues,
|
|
365
|
+
serverIssues: adapterState.serverIssues,
|
|
612
366
|
runtimeIssues,
|
|
613
367
|
issues,
|
|
614
368
|
canPublish,
|
|
@@ -618,46 +372,61 @@ export function useTreeSpecEditor(options) {
|
|
|
618
372
|
fitViewNonce,
|
|
619
373
|
selectedNode,
|
|
620
374
|
selectedEdge,
|
|
375
|
+
inspectorNode,
|
|
621
376
|
nodeSearch,
|
|
622
377
|
issueSearch,
|
|
623
378
|
showMiniMap,
|
|
624
|
-
snapshots,
|
|
625
|
-
auditEvents,
|
|
626
|
-
loadingSnapshots,
|
|
627
|
-
loadingAudit,
|
|
628
|
-
showDraftHistory,
|
|
629
|
-
showAudit,
|
|
630
|
-
showPublishModal,
|
|
379
|
+
snapshots: adapterState.snapshots,
|
|
380
|
+
auditEvents: adapterState.auditEvents,
|
|
381
|
+
loadingSnapshots: adapterState.loadingSnapshots,
|
|
382
|
+
loadingAudit: adapterState.loadingAudit,
|
|
383
|
+
showDraftHistory: adapterState.showDraftHistory,
|
|
384
|
+
showAudit: adapterState.showAudit,
|
|
385
|
+
showPublishModal: adapterState.showPublishModal,
|
|
386
|
+
canUndo,
|
|
387
|
+
canRedo,
|
|
388
|
+
hasCopiedNode,
|
|
631
389
|
};
|
|
632
390
|
const actions = {
|
|
633
|
-
setTree,
|
|
634
|
-
setSelection,
|
|
391
|
+
setTree: commitTree,
|
|
392
|
+
setSelection: applySelection,
|
|
393
|
+
selectChoice,
|
|
635
394
|
setFocusNodeId,
|
|
636
395
|
setFocusChoiceId,
|
|
637
396
|
triggerResetView,
|
|
638
397
|
setNodeSearch,
|
|
639
398
|
setIssueSearch,
|
|
640
399
|
setShowMiniMap,
|
|
641
|
-
setShowDraftHistory,
|
|
642
|
-
setShowAudit,
|
|
643
|
-
setShowPublishModal,
|
|
400
|
+
setShowDraftHistory: adapterState.setShowDraftHistory,
|
|
401
|
+
setShowAudit: adapterState.setShowAudit,
|
|
402
|
+
setShowPublishModal: adapterState.setShowPublishModal,
|
|
644
403
|
addNodeOfType,
|
|
645
404
|
deleteSelectedNode,
|
|
646
405
|
deleteNodeById,
|
|
647
406
|
autoLayout,
|
|
648
407
|
insertTemplate,
|
|
649
|
-
validate,
|
|
650
|
-
saveDraft,
|
|
408
|
+
validate: adapterState.validate,
|
|
409
|
+
saveDraft: adapterState.saveDraft,
|
|
651
410
|
publish,
|
|
652
|
-
createSnapshot,
|
|
653
|
-
restoreSnapshot,
|
|
654
|
-
cloneToDraft,
|
|
411
|
+
createSnapshot: adapterState.createSnapshot,
|
|
412
|
+
restoreSnapshot: adapterState.restoreSnapshot,
|
|
413
|
+
cloneToDraft: adapterState.cloneToDraft,
|
|
655
414
|
updateSelectedNode,
|
|
656
415
|
addChoice,
|
|
416
|
+
setChoiceType,
|
|
657
417
|
deleteChoice,
|
|
418
|
+
moveChoice,
|
|
419
|
+
repositionChoice,
|
|
658
420
|
setChoiceTarget,
|
|
659
421
|
setChoiceOutcome,
|
|
422
|
+
updateChoiceEdgeHints,
|
|
423
|
+
setDefaultEdgeType,
|
|
660
424
|
selectIssue,
|
|
425
|
+
undo,
|
|
426
|
+
redo,
|
|
427
|
+
copySelectedNode,
|
|
428
|
+
pasteCopiedNode,
|
|
429
|
+
duplicateNodeById,
|
|
661
430
|
};
|
|
662
431
|
return { ...state, actions };
|
|
663
432
|
}
|