@workbench-kit/shell-react 0.0.2-prototype.0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/package.json +51 -0
- package/src/chat/ai-mock-proposals.ts +39 -0
- package/src/chat/command-input.ts +52 -0
- package/src/chat/command-policy.ts +54 -0
- package/src/chat/command-surface.tsx +155 -0
- package/src/chat/use-chat-command-proposals.ts +194 -0
- package/src/chat/view-data.ts +20 -0
- package/src/chat/view.tsx +220 -0
- package/src/commands/inspector-surface.tsx +46 -0
- package/src/commands/use-command-descriptors.ts +41 -0
- package/src/commands/use-context-key-revision.ts +18 -0
- package/src/commands/view-data.ts +17 -0
- package/src/commands/view.tsx +43 -0
- package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -0
- package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -0
- package/src/devtools/index.ts +11 -0
- package/src/devtools/use-workbench-devtools-snapshot.ts +84 -0
- package/src/devtools/workbench-devtools-snapshot.ts +140 -0
- package/src/devtools/workbench-devtools.css +82 -0
- package/src/editor/area-dnd.ts +69 -0
- package/src/editor/area-model.ts +71 -0
- package/src/editor/area.css +454 -0
- package/src/editor/area.tsx +676 -0
- package/src/editor/host-surface.tsx +691 -0
- package/src/editor/markdown-document-view-data.ts +16 -0
- package/src/editor/pane-visibility.ts +130 -0
- package/src/editor/resource.ts +36 -0
- package/src/editor/state-storage.ts +182 -0
- package/src/editor/tab-context-menu.ts +118 -0
- package/src/editor/use-editor.ts +138 -0
- package/src/editor/view-providers.tsx +504 -0
- package/src/editor/workspace-file-availability.ts +15 -0
- package/src/editor/workspace-reconcile.tsx +31 -0
- package/src/explorer/context-menu.ts +114 -0
- package/src/explorer/create-command-workspace-explorer-port.ts +86 -0
- package/src/explorer/reveal.ts +112 -0
- package/src/explorer/search-view-data.ts +15 -0
- package/src/explorer/search-view.tsx +65 -0
- package/src/explorer/use-active-workspace-path.ts +15 -0
- package/src/explorer/view-data.ts +58 -0
- package/src/explorer/view.tsx +227 -0
- package/src/extensions/context-menu.ts +52 -0
- package/src/extensions/management-model.ts +313 -0
- package/src/extensions/use-extension-management.ts +241 -0
- package/src/extensions/view-data.ts +19 -0
- package/src/extensions/view.tsx +63 -0
- package/src/field-remap/chrome-labels.ts +74 -0
- package/src/field-remap/convert-note-editor.tsx +206 -0
- package/src/field-remap/convert-palette.tsx +126 -0
- package/src/field-remap/demo.tsx +105 -0
- package/src/field-remap/detail-panel.tsx +659 -0
- package/src/field-remap/editor-surface.tsx +36 -0
- package/src/field-remap/flow-adapter.ts +838 -0
- package/src/field-remap/flow-ops.ts +442 -0
- package/src/field-remap/flow.tsx +948 -0
- package/src/field-remap/graph.tsx +559 -0
- package/src/field-remap/index.ts +88 -0
- package/src/field-remap/io-class-browse.tsx +145 -0
- package/src/field-remap/jsonata-transform.ts +166 -0
- package/src/field-remap/panel.tsx +466 -0
- package/src/field-remap/samples.ts +507 -0
- package/src/field-remap/shape-io-editor.tsx +170 -0
- package/src/field-remap/table-mapping-adapter.ts +59 -0
- package/src/field-remap/transform-options-editor.tsx +312 -0
- package/src/field-remap/tree.tsx +404 -0
- package/src/field-remap/view-data.ts +16 -0
- package/src/field-remap/view.css +1367 -0
- package/src/field-remap/view.tsx +86 -0
- package/src/index.ts +294 -0
- package/src/jdw/document-view-data.ts +31 -0
- package/src/jdw/json-form-source-patch.ts +323 -0
- package/src/jdw/lab-view-data.ts +25 -0
- package/src/jdw/lab-view.css +21 -0
- package/src/jdw/lab-view.tsx +79 -0
- package/src/jdw/widget-form-view.tsx +68 -0
- package/src/jdw/widget-preview-view.tsx +54 -0
- package/src/management/keybinding-overrides-storage.ts +49 -0
- package/src/management/keybinding-settings.tsx +16 -0
- package/src/management/palette-commands.ts +38 -0
- package/src/management/preference-settings-storage.ts +50 -0
- package/src/management/settings-ids.ts +12 -0
- package/src/management/settings.tsx +96 -0
- package/src/management/use-command-management.ts +191 -0
- package/src/management/use-keybinding-management.ts +130 -0
- package/src/shell/model.tsx +149 -0
- package/src/shell/provider.tsx +606 -0
- package/src/shell/secondary-actions.tsx +59 -0
- package/src/shell/settings-constants.ts +9 -0
- package/src/shell/settings.tsx +599 -0
- package/src/shell/shell.tsx +680 -0
- package/src/shell/titlebar-layout-controls.tsx +4 -0
- package/src/shell/view-host.tsx +212 -0
- package/src/workbench/appearance-storage.ts +107 -0
- package/src/workbench/command-host.tsx +243 -0
- package/src/workbench/command-palette.ts +208 -0
- package/src/workbench/keybinding-bridge.ts +54 -0
- package/src/workbench/layout-storage.ts +134 -0
- package/src/workbench/profile-modal.tsx +118 -0
- package/src/workbench/shell-command-registration.ts +58 -0
- package/src/workbench/startup-gate.tsx +136 -0
- package/src/workbench/status-sections.ts +43 -0
- package/src/workbench/use-persisted-appearance.ts +33 -0
- package/src/workbench/user-commands.ts +46 -0
- package/src/workbench/workspace-view-state.ts +35 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type JSX,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
type MouseEvent,
|
|
11
|
+
type Ref,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import {
|
|
14
|
+
Background,
|
|
15
|
+
Controls,
|
|
16
|
+
Handle,
|
|
17
|
+
MiniMap,
|
|
18
|
+
Position,
|
|
19
|
+
ReactFlow,
|
|
20
|
+
ReactFlowProvider,
|
|
21
|
+
useEdgesState,
|
|
22
|
+
useNodesState,
|
|
23
|
+
useReactFlow,
|
|
24
|
+
type Connection,
|
|
25
|
+
type Edge,
|
|
26
|
+
type Node,
|
|
27
|
+
type NodeProps,
|
|
28
|
+
} from '@xyflow/react';
|
|
29
|
+
import '@xyflow/react/dist/style.css';
|
|
30
|
+
import { Badge, Button } from '@workbench-kit/react/primitives';
|
|
31
|
+
import {
|
|
32
|
+
MAX_TRANSFORM_CHAIN,
|
|
33
|
+
type MappingEdge,
|
|
34
|
+
type MappingOperator,
|
|
35
|
+
type SourceField,
|
|
36
|
+
type TargetSlot,
|
|
37
|
+
type ValueTransformRegistry,
|
|
38
|
+
} from '@workbench-kit/field-remap';
|
|
39
|
+
|
|
40
|
+
import { FieldRemapConvertPalette } from './convert-palette.js';
|
|
41
|
+
import {
|
|
42
|
+
resolveFieldRemapChromeLabels,
|
|
43
|
+
type FieldRemapChromeLabels,
|
|
44
|
+
type FieldRemapTranslate,
|
|
45
|
+
} from './chrome-labels.js';
|
|
46
|
+
import { FieldRemapDetailPanel } from './detail-panel.js';
|
|
47
|
+
import {
|
|
48
|
+
applyFieldRemapFlowConnection,
|
|
49
|
+
isValidFieldRemapFlowConnection,
|
|
50
|
+
mappingToFlowGraph,
|
|
51
|
+
parseDraftTransformNodeId,
|
|
52
|
+
parseOperatorNodeId,
|
|
53
|
+
type FieldRemapCombineOperatorNodeData,
|
|
54
|
+
type FieldRemapDraftTransformNodeData,
|
|
55
|
+
type FieldRemapFlowEdgeData,
|
|
56
|
+
type FieldRemapFlowNodeData,
|
|
57
|
+
type FieldRemapSourceObjectNodeData,
|
|
58
|
+
type FieldRemapSplitOperatorNodeData,
|
|
59
|
+
type FieldRemapTargetObjectNodeData,
|
|
60
|
+
type FieldRemapTransformNodeData,
|
|
61
|
+
} from './flow-adapter.js';
|
|
62
|
+
import {
|
|
63
|
+
addTransformStepToEdge,
|
|
64
|
+
bindDraftSource,
|
|
65
|
+
bindDraftTarget,
|
|
66
|
+
bindOperatorInput,
|
|
67
|
+
bindOperatorOutput,
|
|
68
|
+
canEditListContext,
|
|
69
|
+
createCombineOperator,
|
|
70
|
+
createDraftTransform,
|
|
71
|
+
createSplitOperator,
|
|
72
|
+
edgePortTypes,
|
|
73
|
+
enableListContextOnEdge,
|
|
74
|
+
finalizeDraftTransform,
|
|
75
|
+
listCompatibleTransforms,
|
|
76
|
+
removeMappingOperator,
|
|
77
|
+
removeTransformStepFromEdge,
|
|
78
|
+
updateMappingOperator,
|
|
79
|
+
type FieldRemapDraftTransform,
|
|
80
|
+
type FieldRemapSelection,
|
|
81
|
+
} from './flow-ops.js';
|
|
82
|
+
import './view.css';
|
|
83
|
+
|
|
84
|
+
function TypeBadge({ dataType }: { readonly dataType?: string }): JSX.Element | null {
|
|
85
|
+
if (dataType !== 'object' && dataType !== 'array') {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return <Badge variant="muted">{dataType}</Badge>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function SourceObjectNode({ data }: NodeProps<Node<FieldRemapSourceObjectNodeData>>): JSX.Element {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className="workbench-field-remap-flow-node workbench-field-remap-flow-node--object workbench-field-remap-flow-node--source"
|
|
95
|
+
data-testid="field-remap-source-schema"
|
|
96
|
+
>
|
|
97
|
+
<div className="workbench-field-remap-flow-node__header">
|
|
98
|
+
<strong>{data.title}</strong>
|
|
99
|
+
<span className="workbench-field-remap-flow-node__hint">
|
|
100
|
+
{data.schemaRole ?? 'Source schema'}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
<ul className="workbench-field-remap-flow-node__ports">
|
|
104
|
+
{data.ports.map((port) => (
|
|
105
|
+
<li key={port.fieldId} className="workbench-field-remap-flow-node__port">
|
|
106
|
+
<span className="workbench-field-remap-flow-node__port-label">
|
|
107
|
+
<span>{port.label}</span>
|
|
108
|
+
<TypeBadge dataType={port.dataType} />
|
|
109
|
+
</span>
|
|
110
|
+
<Handle
|
|
111
|
+
type="source"
|
|
112
|
+
position={Position.Right}
|
|
113
|
+
id={port.fieldId}
|
|
114
|
+
className="workbench-field-remap-flow-node__handle"
|
|
115
|
+
/>
|
|
116
|
+
</li>
|
|
117
|
+
))}
|
|
118
|
+
</ul>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function TargetObjectNode({ data }: NodeProps<Node<FieldRemapTargetObjectNodeData>>): JSX.Element {
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
className="workbench-field-remap-flow-node workbench-field-remap-flow-node--object workbench-field-remap-flow-node--target"
|
|
127
|
+
data-testid="field-remap-target-schema"
|
|
128
|
+
>
|
|
129
|
+
<div className="workbench-field-remap-flow-node__header">
|
|
130
|
+
<strong>{data.title}</strong>
|
|
131
|
+
<span className="workbench-field-remap-flow-node__hint">
|
|
132
|
+
{data.schemaRole ?? 'Target schema'}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
<ul className="workbench-field-remap-flow-node__ports">
|
|
136
|
+
{data.ports.map((port) => (
|
|
137
|
+
<li key={port.fieldId} className="workbench-field-remap-flow-node__port">
|
|
138
|
+
<Handle
|
|
139
|
+
type="target"
|
|
140
|
+
position={Position.Left}
|
|
141
|
+
id={port.fieldId}
|
|
142
|
+
className="workbench-field-remap-flow-node__handle"
|
|
143
|
+
/>
|
|
144
|
+
<span className="workbench-field-remap-flow-node__port-label">
|
|
145
|
+
<span>{port.label}</span>
|
|
146
|
+
<TypeBadge dataType={port.dataType} />
|
|
147
|
+
</span>
|
|
148
|
+
</li>
|
|
149
|
+
))}
|
|
150
|
+
</ul>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function TransformNode({ data }: NodeProps<Node<FieldRemapTransformNodeData>>): JSX.Element {
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
className={
|
|
159
|
+
data.selected
|
|
160
|
+
? 'workbench-field-remap-flow-node workbench-field-remap-flow-node--transform is-selected'
|
|
161
|
+
: 'workbench-field-remap-flow-node workbench-field-remap-flow-node--transform'
|
|
162
|
+
}
|
|
163
|
+
>
|
|
164
|
+
<Handle type="target" position={Position.Left} id="in" />
|
|
165
|
+
<div className="workbench-field-remap-flow-node__title">
|
|
166
|
+
<strong>{data.label}</strong>
|
|
167
|
+
</div>
|
|
168
|
+
<code className="workbench-field-remap-flow-node__id">{data.transformId}</code>
|
|
169
|
+
<Handle type="source" position={Position.Right} id="out" />
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function DraftTransformNode({
|
|
175
|
+
data,
|
|
176
|
+
}: NodeProps<Node<FieldRemapDraftTransformNodeData>>): JSX.Element {
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className="workbench-field-remap-flow-node workbench-field-remap-flow-node--transform workbench-field-remap-flow-node--draft"
|
|
180
|
+
data-testid={`field-remap-draft-${data.localId}`}
|
|
181
|
+
>
|
|
182
|
+
<Handle type="target" position={Position.Left} id="in" />
|
|
183
|
+
<div className="workbench-field-remap-flow-node__title">
|
|
184
|
+
<strong>{data.label}</strong>
|
|
185
|
+
<Badge variant="muted">draft</Badge>
|
|
186
|
+
</div>
|
|
187
|
+
<code className="workbench-field-remap-flow-node__id">{data.transformId}</code>
|
|
188
|
+
<Handle type="source" position={Position.Right} id="out" />
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function CombineOperatorNode({
|
|
194
|
+
data,
|
|
195
|
+
}: NodeProps<Node<FieldRemapCombineOperatorNodeData>>): JSX.Element {
|
|
196
|
+
return (
|
|
197
|
+
<div
|
|
198
|
+
className="workbench-field-remap-flow-node workbench-field-remap-flow-node--operator workbench-field-remap-flow-node--combine"
|
|
199
|
+
data-testid={`field-remap-op-${data.operatorId}`}
|
|
200
|
+
>
|
|
201
|
+
<div className="workbench-field-remap-flow-node__title">
|
|
202
|
+
<strong>{data.label}</strong>
|
|
203
|
+
<Badge variant="muted">n→1</Badge>
|
|
204
|
+
</div>
|
|
205
|
+
<ul className="workbench-field-remap-flow-node__ports">
|
|
206
|
+
{data.inputFieldIds.map((fieldId) => (
|
|
207
|
+
<li key={fieldId} className="workbench-field-remap-flow-node__port">
|
|
208
|
+
<Handle
|
|
209
|
+
type="target"
|
|
210
|
+
position={Position.Left}
|
|
211
|
+
id={fieldId}
|
|
212
|
+
className="workbench-field-remap-flow-node__handle"
|
|
213
|
+
/>
|
|
214
|
+
<span className="workbench-field-remap-flow-node__port-label">
|
|
215
|
+
<code>{fieldId}</code>
|
|
216
|
+
</span>
|
|
217
|
+
</li>
|
|
218
|
+
))}
|
|
219
|
+
</ul>
|
|
220
|
+
<div className="workbench-field-remap-flow-node__port workbench-field-remap-flow-node__port--out">
|
|
221
|
+
<span className="workbench-field-remap-flow-node__port-label">
|
|
222
|
+
<code>{data.outputSlotId}</code>
|
|
223
|
+
</span>
|
|
224
|
+
<Handle
|
|
225
|
+
type="source"
|
|
226
|
+
position={Position.Right}
|
|
227
|
+
id="out"
|
|
228
|
+
className="workbench-field-remap-flow-node__handle"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function SplitOperatorNode({
|
|
236
|
+
data,
|
|
237
|
+
}: NodeProps<Node<FieldRemapSplitOperatorNodeData>>): JSX.Element {
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
className="workbench-field-remap-flow-node workbench-field-remap-flow-node--operator workbench-field-remap-flow-node--split"
|
|
241
|
+
data-testid={`field-remap-op-${data.operatorId}`}
|
|
242
|
+
>
|
|
243
|
+
<div className="workbench-field-remap-flow-node__title">
|
|
244
|
+
<strong>{data.label}</strong>
|
|
245
|
+
<Badge variant="muted">1→n</Badge>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="workbench-field-remap-flow-node__port workbench-field-remap-flow-node__port--in">
|
|
248
|
+
<Handle
|
|
249
|
+
type="target"
|
|
250
|
+
position={Position.Left}
|
|
251
|
+
id="in"
|
|
252
|
+
className="workbench-field-remap-flow-node__handle"
|
|
253
|
+
/>
|
|
254
|
+
<span className="workbench-field-remap-flow-node__port-label">
|
|
255
|
+
<code>{data.inputFieldId}</code>
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
<ul className="workbench-field-remap-flow-node__ports">
|
|
259
|
+
{data.outputSlotIds.map((slotId) => (
|
|
260
|
+
<li key={slotId} className="workbench-field-remap-flow-node__port">
|
|
261
|
+
<span className="workbench-field-remap-flow-node__port-label">
|
|
262
|
+
<code>{slotId}</code>
|
|
263
|
+
</span>
|
|
264
|
+
<Handle
|
|
265
|
+
type="source"
|
|
266
|
+
position={Position.Right}
|
|
267
|
+
id={slotId}
|
|
268
|
+
className="workbench-field-remap-flow-node__handle"
|
|
269
|
+
/>
|
|
270
|
+
</li>
|
|
271
|
+
))}
|
|
272
|
+
</ul>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const nodeTypes = {
|
|
278
|
+
fieldRemapSourceObject: SourceObjectNode,
|
|
279
|
+
fieldRemapTargetObject: TargetObjectNode,
|
|
280
|
+
fieldRemapTransform: TransformNode,
|
|
281
|
+
fieldRemapDraftTransform: DraftTransformNode,
|
|
282
|
+
fieldRemapCombineOperator: CombineOperatorNode,
|
|
283
|
+
fieldRemapSplitOperator: SplitOperatorNode,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Imperative Flow chrome actions for integrating hosts (fit-view without Controls DOM). */
|
|
287
|
+
export interface FieldRemapFlowActions {
|
|
288
|
+
fitView: (options?: { padding?: number; maxZoom?: number }) => void;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const DEFAULT_FIT_VIEW_OPTIONS = { padding: 0.12, maxZoom: 1.15 } as const;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Bridges imperative fit-view to hosts without subscribing the whole canvas
|
|
295
|
+
* to the React Flow store (avoids update loops with controlled nodes).
|
|
296
|
+
*/
|
|
297
|
+
function FieldRemapFlowActionsBridge({
|
|
298
|
+
flowActionsRef,
|
|
299
|
+
}: {
|
|
300
|
+
readonly flowActionsRef?: Ref<FieldRemapFlowActions | null> | undefined;
|
|
301
|
+
}): null {
|
|
302
|
+
const { fitView } = useReactFlow();
|
|
303
|
+
useImperativeHandle(
|
|
304
|
+
flowActionsRef,
|
|
305
|
+
() => ({
|
|
306
|
+
fitView: (options) => {
|
|
307
|
+
void fitView({
|
|
308
|
+
padding: options?.padding ?? DEFAULT_FIT_VIEW_OPTIONS.padding,
|
|
309
|
+
maxZoom: options?.maxZoom ?? DEFAULT_FIT_VIEW_OPTIONS.maxZoom,
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
[fitView],
|
|
314
|
+
);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export interface FieldRemapFlowMapperProps {
|
|
319
|
+
readonly sources: readonly SourceField[];
|
|
320
|
+
readonly targets: readonly TargetSlot[];
|
|
321
|
+
readonly edges: readonly MappingEdge[];
|
|
322
|
+
readonly transforms: ValueTransformRegistry;
|
|
323
|
+
readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
|
|
324
|
+
/** Document v2 n→m operators (display + authoring when onOperatorsChange is set). */
|
|
325
|
+
readonly operators?: readonly MappingOperator[] | undefined;
|
|
326
|
+
readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
|
|
327
|
+
readonly sourceTitle?: string | undefined;
|
|
328
|
+
readonly targetTitle?: string | undefined;
|
|
329
|
+
readonly selection?: FieldRemapSelection | undefined;
|
|
330
|
+
readonly onSelectionChange?: ((next: FieldRemapSelection) => void) | undefined;
|
|
331
|
+
/**
|
|
332
|
+
* When false, MiniMap is not mounted (not CSS-hidden). Defaults to true for
|
|
333
|
+
* backward compatibility with existing samples.
|
|
334
|
+
*/
|
|
335
|
+
readonly showMinimap?: boolean | undefined;
|
|
336
|
+
/** Pane context menu; host owns menu UI. Receives current Field Remap selection. */
|
|
337
|
+
readonly onPaneContextMenu?:
|
|
338
|
+
| ((event: MouseEvent | globalThis.MouseEvent, ctx: { selection: FieldRemapSelection }) => void)
|
|
339
|
+
| undefined;
|
|
340
|
+
/** Node context menu; host owns menu UI. */
|
|
341
|
+
readonly onNodeContextMenu?:
|
|
342
|
+
| ((
|
|
343
|
+
event: MouseEvent | globalThis.MouseEvent,
|
|
344
|
+
ctx: { nodeId: string; selection: FieldRemapSelection },
|
|
345
|
+
) => void)
|
|
346
|
+
| undefined;
|
|
347
|
+
/** Edge context menu; host owns menu UI. */
|
|
348
|
+
readonly onEdgeContextMenu?:
|
|
349
|
+
| ((
|
|
350
|
+
event: MouseEvent | globalThis.MouseEvent,
|
|
351
|
+
ctx: { edgeId: string; selection: FieldRemapSelection },
|
|
352
|
+
) => void)
|
|
353
|
+
| undefined;
|
|
354
|
+
/** Imperative fit-view (same defaults as Controls fit-view). */
|
|
355
|
+
readonly flowActionsRef?: Ref<FieldRemapFlowActions | null> | undefined;
|
|
356
|
+
/**
|
|
357
|
+
* Override high-visibility chrome strings (edge-list heading, Convert palette).
|
|
358
|
+
* Prefer this over CSS text hacks when the host product noun is not “Bindings”.
|
|
359
|
+
*/
|
|
360
|
+
readonly labels?: Partial<FieldRemapChromeLabels> | undefined;
|
|
361
|
+
/** Optional `t(key, fallback)` injection; `labels` wins when both are set. */
|
|
362
|
+
readonly t?: FieldRemapTranslate | undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function FieldRemapFlowCanvas({
|
|
366
|
+
sources,
|
|
367
|
+
targets,
|
|
368
|
+
edges,
|
|
369
|
+
transforms,
|
|
370
|
+
onEdgesChange,
|
|
371
|
+
operators = [],
|
|
372
|
+
onOperatorsChange,
|
|
373
|
+
sourceTitle,
|
|
374
|
+
targetTitle,
|
|
375
|
+
selection: selectionProp,
|
|
376
|
+
onSelectionChange: onSelectionChangeProp,
|
|
377
|
+
showMinimap = true,
|
|
378
|
+
onPaneContextMenu,
|
|
379
|
+
onNodeContextMenu,
|
|
380
|
+
onEdgeContextMenu,
|
|
381
|
+
flowActionsRef,
|
|
382
|
+
labels: labelOverrides,
|
|
383
|
+
t,
|
|
384
|
+
}: FieldRemapFlowMapperProps): JSX.Element {
|
|
385
|
+
const chromeLabels = useMemo(
|
|
386
|
+
() => resolveFieldRemapChromeLabels(labelOverrides, t),
|
|
387
|
+
[labelOverrides, t],
|
|
388
|
+
);
|
|
389
|
+
const [internalSelection, setInternalSelection] = useState<FieldRemapSelection>(null);
|
|
390
|
+
const selection = selectionProp !== undefined ? selectionProp : internalSelection;
|
|
391
|
+
const setSelection = onSelectionChangeProp ?? setInternalSelection;
|
|
392
|
+
const [drafts, setDrafts] = useState<readonly FieldRemapDraftTransform[]>([]);
|
|
393
|
+
const [placeTransformId, setPlaceTransformId] = useState(() => {
|
|
394
|
+
const first = transforms.list().find((definition) => definition.id !== 'identity');
|
|
395
|
+
return first?.id ?? '';
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const graph = useMemo(
|
|
399
|
+
() =>
|
|
400
|
+
mappingToFlowGraph({
|
|
401
|
+
sources,
|
|
402
|
+
targets,
|
|
403
|
+
edges,
|
|
404
|
+
transforms,
|
|
405
|
+
operators,
|
|
406
|
+
sourceTitle,
|
|
407
|
+
targetTitle,
|
|
408
|
+
drafts,
|
|
409
|
+
}),
|
|
410
|
+
[sources, targets, edges, transforms, operators, sourceTitle, targetTitle, drafts],
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const nodesWithSelection = useMemo(
|
|
414
|
+
() =>
|
|
415
|
+
graph.nodes.map((node) => {
|
|
416
|
+
if (node.data.kind === 'transform') {
|
|
417
|
+
const selected =
|
|
418
|
+
selection?.kind === 'transformStep' &&
|
|
419
|
+
selection.edgeId === node.data.mappingEdgeId &&
|
|
420
|
+
selection.stepIndex === node.data.stepIndex;
|
|
421
|
+
return {
|
|
422
|
+
...node,
|
|
423
|
+
data: { ...node.data, selected },
|
|
424
|
+
selected,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (node.data.kind === 'draft-transform') {
|
|
428
|
+
const selected = selection?.kind === 'draft' && selection.localId === node.data.localId;
|
|
429
|
+
return { ...node, selected };
|
|
430
|
+
}
|
|
431
|
+
if (node.data.kind === 'combine-operator' || node.data.kind === 'split-operator') {
|
|
432
|
+
const selected =
|
|
433
|
+
selection?.kind === 'operator' && selection.operatorId === node.data.operatorId;
|
|
434
|
+
return { ...node, selected };
|
|
435
|
+
}
|
|
436
|
+
return node;
|
|
437
|
+
}),
|
|
438
|
+
[graph.nodes, selection],
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const [nodes, setNodes, onNodesChange] = useNodesState(nodesWithSelection);
|
|
442
|
+
const [flowEdges, setFlowEdges, onFlowEdgesChange] = useEdgesState(graph.edges);
|
|
443
|
+
|
|
444
|
+
// Sync controlled graph by topology/selection signature only. Depending on
|
|
445
|
+
// `nodesWithSelection` (new array each paint) re-enters xyflow StoreUpdater and
|
|
446
|
+
// can hit "Maximum update depth exceeded" in embed hosts with tight flex layouts.
|
|
447
|
+
const graphSyncKey = useMemo(
|
|
448
|
+
() =>
|
|
449
|
+
[
|
|
450
|
+
...graph.nodes.map((node) => node.id),
|
|
451
|
+
...graph.edges.map(
|
|
452
|
+
(edge) =>
|
|
453
|
+
`${edge.id}:${edge.source}:${edge.target}:${edge.sourceHandle ?? ''}:${edge.targetHandle ?? ''}`,
|
|
454
|
+
),
|
|
455
|
+
selection ? JSON.stringify(selection) : '',
|
|
456
|
+
...drafts.map((draft) => draft.localId),
|
|
457
|
+
].join('|'),
|
|
458
|
+
[drafts, graph.edges, graph.nodes, selection],
|
|
459
|
+
);
|
|
460
|
+
const nodesWithSelectionRef = useRef(nodesWithSelection);
|
|
461
|
+
const graphEdgesRef = useRef(graph.edges);
|
|
462
|
+
nodesWithSelectionRef.current = nodesWithSelection;
|
|
463
|
+
graphEdgesRef.current = graph.edges;
|
|
464
|
+
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
setNodes(nodesWithSelectionRef.current);
|
|
467
|
+
setFlowEdges(graphEdgesRef.current);
|
|
468
|
+
}, [graphSyncKey, setFlowEdges, setNodes]);
|
|
469
|
+
|
|
470
|
+
const connectionContext = useMemo(
|
|
471
|
+
() => ({ sources, targets, edges, transforms, drafts, operators }),
|
|
472
|
+
[sources, targets, edges, transforms, drafts, operators],
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const isValidConnection = useCallback(
|
|
476
|
+
(connection: Connection | Edge) =>
|
|
477
|
+
isValidFieldRemapFlowConnection(connection, connectionContext),
|
|
478
|
+
[connectionContext],
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const onConnect = useCallback(
|
|
482
|
+
(connection: Connection) => {
|
|
483
|
+
if (!connection.source || !connection.target) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!isValidFieldRemapFlowConnection(connection, connectionContext)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const draftAsTarget = parseDraftTransformNodeId(connection.target);
|
|
491
|
+
if (draftAsTarget && connection.sourceHandle) {
|
|
492
|
+
const draft = drafts.find((item) => item.localId === draftAsTarget);
|
|
493
|
+
if (!draft) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const bound = bindDraftSource(draft, connection.sourceHandle);
|
|
497
|
+
const finalized = finalizeDraftTransform(bound, {
|
|
498
|
+
registry: transforms,
|
|
499
|
+
sources,
|
|
500
|
+
targets,
|
|
501
|
+
existing: edges,
|
|
502
|
+
});
|
|
503
|
+
if (finalized) {
|
|
504
|
+
const withoutTarget = edges.filter(
|
|
505
|
+
(edge) => edge.targetSlotId !== finalized.targetSlotId,
|
|
506
|
+
);
|
|
507
|
+
onEdgesChange([...withoutTarget, finalized]);
|
|
508
|
+
setDrafts(drafts.filter((item) => item.localId !== draft.localId));
|
|
509
|
+
setSelection({
|
|
510
|
+
kind: 'transformStep',
|
|
511
|
+
edgeId: finalized.id,
|
|
512
|
+
stepIndex: 0,
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
setDrafts(drafts.map((item) => (item.localId === draft.localId ? bound : item)));
|
|
517
|
+
setSelection({ kind: 'draft', localId: draft.localId });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const draftAsSource = parseDraftTransformNodeId(connection.source);
|
|
522
|
+
if (draftAsSource && connection.targetHandle) {
|
|
523
|
+
const draft = drafts.find((item) => item.localId === draftAsSource);
|
|
524
|
+
if (!draft) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const bound = bindDraftTarget(draft, connection.targetHandle);
|
|
528
|
+
const finalized = finalizeDraftTransform(bound, {
|
|
529
|
+
registry: transforms,
|
|
530
|
+
sources,
|
|
531
|
+
targets,
|
|
532
|
+
existing: edges,
|
|
533
|
+
});
|
|
534
|
+
if (finalized) {
|
|
535
|
+
const withoutTarget = edges.filter(
|
|
536
|
+
(edge) => edge.targetSlotId !== finalized.targetSlotId,
|
|
537
|
+
);
|
|
538
|
+
onEdgesChange([...withoutTarget, finalized]);
|
|
539
|
+
setDrafts(drafts.filter((item) => item.localId !== draft.localId));
|
|
540
|
+
setSelection({
|
|
541
|
+
kind: 'transformStep',
|
|
542
|
+
edgeId: finalized.id,
|
|
543
|
+
stepIndex: 0,
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
setDrafts(drafts.map((item) => (item.localId === draft.localId ? bound : item)));
|
|
548
|
+
setSelection({ kind: 'draft', localId: draft.localId });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const operatorAsTarget = parseOperatorNodeId(connection.target);
|
|
553
|
+
if (operatorAsTarget && connection.sourceHandle && onOperatorsChange) {
|
|
554
|
+
onOperatorsChange(
|
|
555
|
+
updateMappingOperator(operators, operatorAsTarget, (operator) =>
|
|
556
|
+
bindOperatorInput(operator, connection.sourceHandle!),
|
|
557
|
+
),
|
|
558
|
+
);
|
|
559
|
+
setSelection({ kind: 'operator', operatorId: operatorAsTarget });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const operatorAsSource = parseOperatorNodeId(connection.source);
|
|
564
|
+
if (operatorAsSource && connection.targetHandle && onOperatorsChange) {
|
|
565
|
+
onOperatorsChange(
|
|
566
|
+
updateMappingOperator(operators, operatorAsSource, (operator) =>
|
|
567
|
+
bindOperatorOutput(operator, connection.targetHandle!),
|
|
568
|
+
),
|
|
569
|
+
);
|
|
570
|
+
setSelection({ kind: 'operator', operatorId: operatorAsSource });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const result = applyFieldRemapFlowConnection({
|
|
575
|
+
sourceNodeId: connection.source,
|
|
576
|
+
targetNodeId: connection.target,
|
|
577
|
+
sourceHandle: connection.sourceHandle,
|
|
578
|
+
targetHandle: connection.targetHandle,
|
|
579
|
+
existing: edges,
|
|
580
|
+
});
|
|
581
|
+
if (!result) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const removeIds = new Set(result.removeEdgeIds ?? []);
|
|
585
|
+
const withoutTarget = edges.filter(
|
|
586
|
+
(edge) =>
|
|
587
|
+
edge.targetSlotId !== result.edge.targetSlotId &&
|
|
588
|
+
edge.id !== result.edge.id &&
|
|
589
|
+
!removeIds.has(edge.id),
|
|
590
|
+
);
|
|
591
|
+
onEdgesChange([...withoutTarget, result.edge]);
|
|
592
|
+
setSelection({ kind: 'edge', edgeId: result.edge.id });
|
|
593
|
+
},
|
|
594
|
+
[connectionContext, drafts, edges, onEdgesChange, setSelection, sources, targets, transforms],
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const onEdgesDelete = useCallback(
|
|
598
|
+
(deleted: Edge[]) => {
|
|
599
|
+
const mappingIds = new Set(
|
|
600
|
+
deleted
|
|
601
|
+
.map((edge) => {
|
|
602
|
+
const data = edge.data as FieldRemapFlowEdgeData | undefined;
|
|
603
|
+
return data?.mappingEdgeId;
|
|
604
|
+
})
|
|
605
|
+
.filter((id): id is string => typeof id === 'string'),
|
|
606
|
+
);
|
|
607
|
+
if (mappingIds.size === 0) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
onEdgesChange(edges.filter((edge) => !mappingIds.has(edge.id)));
|
|
611
|
+
if (
|
|
612
|
+
selection &&
|
|
613
|
+
(selection.kind === 'edge' || selection.kind === 'transformStep') &&
|
|
614
|
+
mappingIds.has(selection.edgeId)
|
|
615
|
+
) {
|
|
616
|
+
setSelection(null);
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
[edges, onEdgesChange, selection, setSelection],
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const placeDraft = useCallback(
|
|
623
|
+
(transformId: string) => {
|
|
624
|
+
const draft = createDraftTransform(transformId);
|
|
625
|
+
setDrafts((current) => [...current, draft]);
|
|
626
|
+
setSelection({ kind: 'draft', localId: draft.localId });
|
|
627
|
+
setPlaceTransformId(transformId);
|
|
628
|
+
},
|
|
629
|
+
[setSelection],
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const onNodeClick = useCallback(
|
|
633
|
+
(event: MouseEvent, node: Node) => {
|
|
634
|
+
const data = node.data as FieldRemapFlowNodeData;
|
|
635
|
+
if (data.kind === 'draft-transform') {
|
|
636
|
+
setSelection({ kind: 'draft', localId: data.localId });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (data.kind === 'combine-operator' || data.kind === 'split-operator') {
|
|
640
|
+
if (event.altKey && onOperatorsChange) {
|
|
641
|
+
onOperatorsChange(removeMappingOperator(operators, data.operatorId));
|
|
642
|
+
if (selection?.kind === 'operator' && selection.operatorId === data.operatorId) {
|
|
643
|
+
setSelection(null);
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
setSelection({ kind: 'operator', operatorId: data.operatorId });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (data.kind !== 'transform') {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (event.altKey) {
|
|
654
|
+
const edge = edges.find((item) => item.id === data.mappingEdgeId);
|
|
655
|
+
if (!edge) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const next = removeTransformStepFromEdge(edge, data.stepIndex);
|
|
659
|
+
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
660
|
+
setSelection(
|
|
661
|
+
(next.transformIds?.length ?? 0) > 0
|
|
662
|
+
? {
|
|
663
|
+
kind: 'transformStep',
|
|
664
|
+
edgeId: edge.id,
|
|
665
|
+
stepIndex: Math.min(data.stepIndex, (next.transformIds?.length ?? 1) - 1),
|
|
666
|
+
}
|
|
667
|
+
: { kind: 'edge', edgeId: edge.id },
|
|
668
|
+
);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
setSelection({
|
|
672
|
+
kind: 'transformStep',
|
|
673
|
+
edgeId: data.mappingEdgeId,
|
|
674
|
+
stepIndex: data.stepIndex,
|
|
675
|
+
});
|
|
676
|
+
},
|
|
677
|
+
[edges, onEdgesChange, setSelection],
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const onKeyDown = useCallback(
|
|
681
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
682
|
+
if (event.key === 'Escape') {
|
|
683
|
+
setSelection(null);
|
|
684
|
+
setDrafts([]);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
[setSelection],
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const handlePaneContextMenu = useCallback(
|
|
691
|
+
(event: MouseEvent | globalThis.MouseEvent) => {
|
|
692
|
+
onPaneContextMenu?.(event, { selection });
|
|
693
|
+
},
|
|
694
|
+
[onPaneContextMenu, selection],
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
const handleNodeContextMenu = useCallback(
|
|
698
|
+
(event: MouseEvent, node: Node) => {
|
|
699
|
+
onNodeContextMenu?.(event, { nodeId: node.id, selection });
|
|
700
|
+
},
|
|
701
|
+
[onNodeContextMenu, selection],
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const handleEdgeContextMenu = useCallback(
|
|
705
|
+
(event: MouseEvent, edge: Edge) => {
|
|
706
|
+
onEdgeContextMenu?.(event, { edgeId: edge.id, selection });
|
|
707
|
+
},
|
|
708
|
+
[onEdgeContextMenu, selection],
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
return (
|
|
712
|
+
<div
|
|
713
|
+
className="workbench-field-remap-flow"
|
|
714
|
+
data-testid="field-remap-mapper"
|
|
715
|
+
data-minimap={showMinimap ? 'on' : 'off'}
|
|
716
|
+
onKeyDown={onKeyDown}
|
|
717
|
+
>
|
|
718
|
+
<p className="workbench-field-remap-mapper__hint" data-testid="field-remap-hint">
|
|
719
|
+
Convert-first: pick a convert in the palette, place it, then wire source → draft → target.
|
|
720
|
+
Select a convert note for the Convert editor; select a binding for lighter mapping detail.
|
|
721
|
+
Use n→m actions to author combine/split. Alt-click removes a convert step or operator.
|
|
722
|
+
Escape clears selection and unfinished drafts.
|
|
723
|
+
</p>
|
|
724
|
+
|
|
725
|
+
<div
|
|
726
|
+
className={
|
|
727
|
+
selection?.kind === 'transformStep'
|
|
728
|
+
? 'workbench-field-remap-flow__workspace workbench-field-remap-flow__workspace--convert'
|
|
729
|
+
: 'workbench-field-remap-flow__workspace'
|
|
730
|
+
}
|
|
731
|
+
data-surface={
|
|
732
|
+
selection?.kind === 'transformStep'
|
|
733
|
+
? 'convert-note'
|
|
734
|
+
: selection?.kind === 'draft'
|
|
735
|
+
? 'draft-convert'
|
|
736
|
+
: selection?.kind === 'operator'
|
|
737
|
+
? 'operator'
|
|
738
|
+
: 'binding'
|
|
739
|
+
}
|
|
740
|
+
>
|
|
741
|
+
<FieldRemapConvertPalette
|
|
742
|
+
transforms={transforms}
|
|
743
|
+
selectedTransformId={placeTransformId}
|
|
744
|
+
onSelectedTransformIdChange={setPlaceTransformId}
|
|
745
|
+
onPlaceDraft={placeDraft}
|
|
746
|
+
chromeLabels={chromeLabels}
|
|
747
|
+
onAddCombine={
|
|
748
|
+
onOperatorsChange
|
|
749
|
+
? () => {
|
|
750
|
+
const next = createCombineOperator();
|
|
751
|
+
onOperatorsChange([...operators, next]);
|
|
752
|
+
setSelection({ kind: 'operator', operatorId: next.id });
|
|
753
|
+
}
|
|
754
|
+
: undefined
|
|
755
|
+
}
|
|
756
|
+
onAddSplit={
|
|
757
|
+
onOperatorsChange
|
|
758
|
+
? () => {
|
|
759
|
+
const next = createSplitOperator();
|
|
760
|
+
onOperatorsChange([...operators, next]);
|
|
761
|
+
setSelection({ kind: 'operator', operatorId: next.id });
|
|
762
|
+
}
|
|
763
|
+
: undefined
|
|
764
|
+
}
|
|
765
|
+
/>
|
|
766
|
+
|
|
767
|
+
<div className="workbench-field-remap-flow__canvas" data-testid="field-remap-flow">
|
|
768
|
+
<ReactFlow
|
|
769
|
+
nodes={nodes}
|
|
770
|
+
edges={flowEdges}
|
|
771
|
+
nodeTypes={nodeTypes}
|
|
772
|
+
onNodesChange={onNodesChange}
|
|
773
|
+
onEdgesChange={onFlowEdgesChange}
|
|
774
|
+
onConnect={onConnect}
|
|
775
|
+
onEdgesDelete={onEdgesDelete}
|
|
776
|
+
onNodeClick={onNodeClick}
|
|
777
|
+
onPaneContextMenu={onPaneContextMenu ? handlePaneContextMenu : undefined}
|
|
778
|
+
onNodeContextMenu={onNodeContextMenu ? handleNodeContextMenu : undefined}
|
|
779
|
+
onEdgeContextMenu={onEdgeContextMenu ? handleEdgeContextMenu : undefined}
|
|
780
|
+
isValidConnection={isValidConnection}
|
|
781
|
+
fitView
|
|
782
|
+
fitViewOptions={DEFAULT_FIT_VIEW_OPTIONS}
|
|
783
|
+
proOptions={{ hideAttribution: true }}
|
|
784
|
+
>
|
|
785
|
+
<FieldRemapFlowActionsBridge flowActionsRef={flowActionsRef} />
|
|
786
|
+
<Background gap={16} color="var(--xy-background-pattern-color)" />
|
|
787
|
+
<Controls showInteractive={false} fitViewOptions={DEFAULT_FIT_VIEW_OPTIONS} />
|
|
788
|
+
{showMinimap ? (
|
|
789
|
+
<MiniMap
|
|
790
|
+
pannable
|
|
791
|
+
zoomable
|
|
792
|
+
bgColor="var(--xy-minimap-background-color)"
|
|
793
|
+
maskColor="var(--xy-minimap-mask-background-color)"
|
|
794
|
+
nodeColor={(node) => {
|
|
795
|
+
const kind = (node.data as FieldRemapFlowNodeData | undefined)?.kind;
|
|
796
|
+
if (kind === 'source-object') {
|
|
797
|
+
return 'var(--vscode-charts-blue, #3794ff)';
|
|
798
|
+
}
|
|
799
|
+
if (kind === 'target-object') {
|
|
800
|
+
return 'var(--vscode-charts-green, #89d185)';
|
|
801
|
+
}
|
|
802
|
+
return 'var(--vscode-focusBorder, var(--color-accent, #3794ff))';
|
|
803
|
+
}}
|
|
804
|
+
nodeStrokeColor="var(--xy-minimap-node-stroke-color)"
|
|
805
|
+
/>
|
|
806
|
+
) : null}
|
|
807
|
+
</ReactFlow>
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<FieldRemapDetailPanel
|
|
811
|
+
selection={selection}
|
|
812
|
+
edges={edges}
|
|
813
|
+
sources={sources}
|
|
814
|
+
targets={targets}
|
|
815
|
+
transforms={transforms}
|
|
816
|
+
onEdgesChange={onEdgesChange}
|
|
817
|
+
onSelectionChange={setSelection}
|
|
818
|
+
drafts={drafts}
|
|
819
|
+
onDiscardDraft={(localId) => {
|
|
820
|
+
setDrafts((current) => current.filter((item) => item.localId !== localId));
|
|
821
|
+
}}
|
|
822
|
+
operators={operators}
|
|
823
|
+
onOperatorsChange={onOperatorsChange}
|
|
824
|
+
/>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<div className="workbench-field-remap-flow__bindings" data-testid="field-remap-edges">
|
|
828
|
+
<h4>{chromeLabels.bindingsTitle}</h4>
|
|
829
|
+
<ul>
|
|
830
|
+
{edges.map((edge) => {
|
|
831
|
+
const portTypes = edgePortTypes(edge, sources, targets);
|
|
832
|
+
const appendCatalog = listCompatibleTransforms({
|
|
833
|
+
registry: transforms,
|
|
834
|
+
edge,
|
|
835
|
+
stepIndex: edge.transformIds?.length ?? 0,
|
|
836
|
+
sourceType: portTypes.sourceType,
|
|
837
|
+
targetType: portTypes.targetType,
|
|
838
|
+
mode: 'append',
|
|
839
|
+
});
|
|
840
|
+
const defaultAddId = appendCatalog[0]?.id;
|
|
841
|
+
const listContext = canEditListContext(edge, sources, targets);
|
|
842
|
+
const selected =
|
|
843
|
+
(selection?.kind === 'edge' || selection?.kind === 'transformStep') &&
|
|
844
|
+
selection.edgeId === edge.id;
|
|
845
|
+
|
|
846
|
+
return (
|
|
847
|
+
<li
|
|
848
|
+
key={edge.id}
|
|
849
|
+
className={selected ? 'is-selected' : undefined}
|
|
850
|
+
data-testid={`field-remap-lane-${edge.id}`}
|
|
851
|
+
>
|
|
852
|
+
<button
|
|
853
|
+
type="button"
|
|
854
|
+
className="workbench-field-remap-flow__binding-select"
|
|
855
|
+
data-testid={`field-remap-select-edge-${edge.id}`}
|
|
856
|
+
onClick={() => setSelection({ kind: 'edge', edgeId: edge.id })}
|
|
857
|
+
>
|
|
858
|
+
<code>
|
|
859
|
+
{edge.sourceFieldId} →{' '}
|
|
860
|
+
{(edge.transformIds ?? []).length > 0
|
|
861
|
+
? `${(edge.transformIds ?? []).join(' → ')} → `
|
|
862
|
+
: ''}
|
|
863
|
+
{edge.targetSlotId}
|
|
864
|
+
{edge.itemEdges ? ` · ${edge.itemEdges.length} item fields` : ''}
|
|
865
|
+
</code>
|
|
866
|
+
</button>
|
|
867
|
+
<span className="workbench-field-remap-mapper__edge-actions">
|
|
868
|
+
{(edge.transformIds?.length ?? 0) < MAX_TRANSFORM_CHAIN && defaultAddId ? (
|
|
869
|
+
<Button
|
|
870
|
+
compact
|
|
871
|
+
type="button"
|
|
872
|
+
data-testid={`field-remap-add-node-${edge.id}`}
|
|
873
|
+
onClick={() => {
|
|
874
|
+
const next = addTransformStepToEdge(edge, defaultAddId, {
|
|
875
|
+
registry: transforms,
|
|
876
|
+
sourceType: portTypes.sourceType,
|
|
877
|
+
targetType: portTypes.targetType,
|
|
878
|
+
});
|
|
879
|
+
if (!next) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
883
|
+
setSelection({
|
|
884
|
+
kind: 'transformStep',
|
|
885
|
+
edgeId: edge.id,
|
|
886
|
+
stepIndex: (next.transformIds?.length ?? 1) - 1,
|
|
887
|
+
});
|
|
888
|
+
}}
|
|
889
|
+
>
|
|
890
|
+
+ node
|
|
891
|
+
</Button>
|
|
892
|
+
) : null}
|
|
893
|
+
{listContext ? (
|
|
894
|
+
<Button
|
|
895
|
+
compact
|
|
896
|
+
type="button"
|
|
897
|
+
data-testid={`field-remap-edit-items-${edge.id}`}
|
|
898
|
+
onClick={() => {
|
|
899
|
+
if (!edge.itemEdges) {
|
|
900
|
+
onEdgesChange(
|
|
901
|
+
edges.map((item) =>
|
|
902
|
+
item.id === edge.id ? enableListContextOnEdge(item) : item,
|
|
903
|
+
),
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
setSelection({ kind: 'edge', edgeId: edge.id });
|
|
907
|
+
}}
|
|
908
|
+
>
|
|
909
|
+
Edit items
|
|
910
|
+
</Button>
|
|
911
|
+
) : null}
|
|
912
|
+
<Button
|
|
913
|
+
compact
|
|
914
|
+
type="button"
|
|
915
|
+
onClick={() => {
|
|
916
|
+
onEdgesChange(edges.filter((item) => item.id !== edge.id));
|
|
917
|
+
if (
|
|
918
|
+
selection &&
|
|
919
|
+
(selection.kind === 'edge' || selection.kind === 'transformStep') &&
|
|
920
|
+
selection.edgeId === edge.id
|
|
921
|
+
) {
|
|
922
|
+
setSelection(null);
|
|
923
|
+
}
|
|
924
|
+
}}
|
|
925
|
+
>
|
|
926
|
+
Remove
|
|
927
|
+
</Button>
|
|
928
|
+
</span>
|
|
929
|
+
</li>
|
|
930
|
+
);
|
|
931
|
+
})}
|
|
932
|
+
</ul>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* React Flow mapper: multi-port source object → convert notes → multi-port target object.
|
|
940
|
+
* Selection gates the side rail: binding detail vs dedicated Convert note editor.
|
|
941
|
+
*/
|
|
942
|
+
export function FieldRemapFlowMapper(props: FieldRemapFlowMapperProps): JSX.Element {
|
|
943
|
+
return (
|
|
944
|
+
<ReactFlowProvider>
|
|
945
|
+
<FieldRemapFlowCanvas {...props} />
|
|
946
|
+
</ReactFlowProvider>
|
|
947
|
+
);
|
|
948
|
+
}
|