@workbench-kit/shell-react 0.0.2-prototype.0.2.41 → 0.0.2-prototype.0.2.44
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 +81 -2
- package/package.json +14 -10
- package/src/editor/workspace-reconcile.tsx +12 -7
- package/src/extensions/extension-enablement-controller.ts +40 -9
- package/src/extensions/theme-selection-protection.ts +80 -55
- package/src/field-remap/chrome-labels.ts +117 -0
- package/src/field-remap/convert-note-editor.tsx +119 -104
- package/src/field-remap/convert-palette.tsx +6 -0
- package/src/field-remap/demo.tsx +8 -2
- package/src/field-remap/detail-panel.tsx +535 -434
- package/src/field-remap/document-io.tsx +299 -0
- package/src/field-remap/drag-payload.ts +48 -0
- package/src/field-remap/flow-adapter.ts +216 -88
- package/src/field-remap/flow-ops.ts +150 -0
- package/src/field-remap/flow.tsx +1227 -272
- package/src/field-remap/index.ts +2 -0
- package/src/field-remap/io-class-browse.tsx +34 -22
- package/src/field-remap/keyboard.ts +16 -0
- package/src/field-remap/modal-detail.tsx +34 -0
- package/src/field-remap/panel.tsx +110 -14
- package/src/field-remap/transform-options-editor.tsx +68 -48
- package/src/field-remap/view.css +107 -189
- package/src/index.ts +7 -0
- package/src/keybinding-management-settings.ts +4 -0
- package/src/management/keybinding-overrides-storage.ts +48 -23
- package/src/management/keybinding-settings-view.tsx +31 -0
- package/src/management/keybinding-settings.tsx +4 -12
- package/src/management/use-keybinding-management.ts +72 -22
- package/src/shell/appearance-catalog.ts +453 -0
- package/src/shell/appearance-controller.ts +331 -0
- package/src/shell/appearance-presentation.ts +269 -0
- package/src/shell/provider.tsx +157 -19
- package/src/shell/settings.tsx +282 -153
- package/src/shell/shell.tsx +88 -18
- package/src/workbench/appearance-storage.ts +2 -2
- package/src/workbench/command-host-controller.tsx +223 -0
- package/src/workbench/command-host.tsx +100 -150
- package/src/workbench/keybinding-bridge.ts +84 -38
- package/src/workbench/shell-command-registration.ts +27 -2
package/src/field-remap/flow.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Children,
|
|
3
|
+
lazy,
|
|
4
|
+
Suspense,
|
|
3
5
|
useCallback,
|
|
4
6
|
useEffect,
|
|
5
7
|
useImperativeHandle,
|
|
@@ -7,6 +9,7 @@ import {
|
|
|
7
9
|
useRef,
|
|
8
10
|
useState,
|
|
9
11
|
type JSX,
|
|
12
|
+
type DragEvent,
|
|
10
13
|
type KeyboardEvent,
|
|
11
14
|
type MouseEvent,
|
|
12
15
|
type Ref,
|
|
@@ -21,19 +24,25 @@ import {
|
|
|
21
24
|
Position,
|
|
22
25
|
ReactFlow,
|
|
23
26
|
ReactFlowProvider,
|
|
27
|
+
SmoothStepEdge,
|
|
24
28
|
useEdgesState,
|
|
25
29
|
useNodesState,
|
|
26
30
|
useReactFlow,
|
|
27
31
|
type Connection,
|
|
28
32
|
type Edge,
|
|
33
|
+
type EdgeProps,
|
|
34
|
+
type FinalConnectionState,
|
|
29
35
|
type Node,
|
|
30
36
|
type NodeProps,
|
|
37
|
+
type XYPosition,
|
|
31
38
|
} from '@xyflow/react';
|
|
32
39
|
import '@xyflow/react/dist/style.css';
|
|
33
40
|
import { Badge, IconButton } from '@workbench-kit/react/primitives';
|
|
34
41
|
import { SplitView } from '@workbench-kit/react/workbench/split-view';
|
|
35
42
|
import {
|
|
36
43
|
MAX_TRANSFORM_CHAIN,
|
|
44
|
+
findParentChildMappingConflicts,
|
|
45
|
+
type MappingConflict,
|
|
37
46
|
type MappingEdge,
|
|
38
47
|
type MappingOperator,
|
|
39
48
|
type SourceField,
|
|
@@ -42,6 +51,7 @@ import {
|
|
|
42
51
|
} from '@workbench-kit/field-remap';
|
|
43
52
|
|
|
44
53
|
import { FieldRemapConvertPalette } from './convert-palette.js';
|
|
54
|
+
import { hasFieldRemapTransformDragType, readFieldRemapTransformDragData } from './drag-payload.js';
|
|
45
55
|
import {
|
|
46
56
|
resolveFieldRemapChromeLabels,
|
|
47
57
|
type FieldRemapChromeLabels,
|
|
@@ -50,6 +60,7 @@ import {
|
|
|
50
60
|
import { FieldRemapDetailPanel } from './detail-panel.js';
|
|
51
61
|
import {
|
|
52
62
|
applyFieldRemapFlowConnection,
|
|
63
|
+
evaluateFieldRemapFlowConnection,
|
|
53
64
|
isValidFieldRemapFlowConnection,
|
|
54
65
|
mappingToFlowGraph,
|
|
55
66
|
parseDraftTransformNodeId,
|
|
@@ -57,6 +68,7 @@ import {
|
|
|
57
68
|
type FieldRemapCombineOperatorNodeData,
|
|
58
69
|
type FieldRemapDraftTransformNodeData,
|
|
59
70
|
type FieldRemapFlowEdgeData,
|
|
71
|
+
type FieldRemapFlowConnectionRejectionReason,
|
|
60
72
|
type FieldRemapFlowNodeData,
|
|
61
73
|
type FieldRemapSourceObjectNodeData,
|
|
62
74
|
type FieldRemapSplitOperatorNodeData,
|
|
@@ -77,16 +89,38 @@ import {
|
|
|
77
89
|
edgePortTypes,
|
|
78
90
|
enableListContextOnEdge,
|
|
79
91
|
finalizeDraftTransform,
|
|
92
|
+
asFieldRemapBulkSelectionRef,
|
|
93
|
+
fieldRemapBulkSelectionKey,
|
|
94
|
+
fieldRemapSelectionKey,
|
|
80
95
|
listCompatibleTransforms,
|
|
96
|
+
listFieldRemapBulkSelectionRefs,
|
|
97
|
+
normalizeFieldRemapBulkSelection,
|
|
98
|
+
planFieldRemapBulkDelete,
|
|
81
99
|
removeMappingOperator,
|
|
82
|
-
|
|
100
|
+
updateFieldRemapBulkSelection,
|
|
83
101
|
updateMappingOperator,
|
|
102
|
+
type FieldRemapBulkSelectionRef,
|
|
84
103
|
type FieldRemapDraftTransform,
|
|
85
104
|
type FieldRemapSelection,
|
|
86
105
|
} from './flow-ops.js';
|
|
106
|
+
import { isFieldRemapEditableShortcutTarget } from './keyboard.js';
|
|
87
107
|
import { FieldRemapPreviewRail, type FieldRemapPreviewState } from './preview.js';
|
|
88
108
|
import './view.css';
|
|
89
109
|
|
|
110
|
+
type FieldRemapFocusSurface = 'graph' | 'list';
|
|
111
|
+
type FieldRemapFocusableElement = HTMLElement | SVGElement;
|
|
112
|
+
type RegisterFieldRemapFocusTarget = (
|
|
113
|
+
key: string,
|
|
114
|
+
surface: FieldRemapFocusSurface,
|
|
115
|
+
element: FieldRemapFocusableElement | null,
|
|
116
|
+
) => void;
|
|
117
|
+
|
|
118
|
+
const loadFieldRemapModalDetail = async () => {
|
|
119
|
+
const module = await import('./modal-detail.js');
|
|
120
|
+
return { default: module.FieldRemapModalDetail };
|
|
121
|
+
};
|
|
122
|
+
const FieldRemapModalDetail = lazy(loadFieldRemapModalDetail);
|
|
123
|
+
|
|
90
124
|
function TypeBadge({ dataType }: { readonly dataType?: string }): JSX.Element | null {
|
|
91
125
|
if (dataType !== 'object' && dataType !== 'array') {
|
|
92
126
|
return null;
|
|
@@ -159,8 +193,21 @@ function TargetObjectNode({ data }: NodeProps<Node<FieldRemapTargetObjectNodeDat
|
|
|
159
193
|
}
|
|
160
194
|
|
|
161
195
|
function TransformNode({ data }: NodeProps<Node<FieldRemapTransformNodeData>>): JSX.Element {
|
|
196
|
+
const registerFocusTarget = data.registerFocusTarget as RegisterFieldRemapFocusTarget | undefined;
|
|
197
|
+
const selectionKey = fieldRemapBulkSelectionKey({
|
|
198
|
+
kind: 'transformStep',
|
|
199
|
+
edgeId: data.mappingEdgeId,
|
|
200
|
+
stepIndex: data.stepIndex,
|
|
201
|
+
});
|
|
202
|
+
const registerRoot = useCallback(
|
|
203
|
+
(element: HTMLDivElement | null) => {
|
|
204
|
+
registerFocusTarget?.(selectionKey, 'graph', element?.parentElement ?? null);
|
|
205
|
+
},
|
|
206
|
+
[registerFocusTarget, selectionKey],
|
|
207
|
+
);
|
|
162
208
|
return (
|
|
163
209
|
<div
|
|
210
|
+
ref={registerRoot}
|
|
164
211
|
className={
|
|
165
212
|
data.selected
|
|
166
213
|
? 'workbench-field-remap-flow-node workbench-field-remap-flow-node--transform is-selected'
|
|
@@ -177,6 +224,31 @@ function TransformNode({ data }: NodeProps<Node<FieldRemapTransformNodeData>>):
|
|
|
177
224
|
);
|
|
178
225
|
}
|
|
179
226
|
|
|
227
|
+
function FieldRemapSmoothStepEdge(props: EdgeProps<Edge<FieldRemapFlowEdgeData>>): JSX.Element {
|
|
228
|
+
const registerFocusTarget = props.data?.registerFocusTarget as
|
|
229
|
+
RegisterFieldRemapFocusTarget | undefined;
|
|
230
|
+
const mappingEdgeId = props.data?.mappingEdgeId;
|
|
231
|
+
const canonicalSegment = props.data?.segment === 'direct' || props.data?.segment === 'in';
|
|
232
|
+
const selectionKey =
|
|
233
|
+
mappingEdgeId && canonicalSegment
|
|
234
|
+
? fieldRemapBulkSelectionKey({ kind: 'edge', edgeId: mappingEdgeId })
|
|
235
|
+
: undefined;
|
|
236
|
+
const registerRoot = useCallback(
|
|
237
|
+
(element: SVGGElement | null) => {
|
|
238
|
+
if (selectionKey) {
|
|
239
|
+
registerFocusTarget?.(selectionKey, 'graph', element?.parentElement ?? null);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
[registerFocusTarget, selectionKey],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<g ref={selectionKey ? registerRoot : undefined}>
|
|
247
|
+
<SmoothStepEdge {...props} />
|
|
248
|
+
</g>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
180
252
|
function DraftTransformNode({
|
|
181
253
|
data,
|
|
182
254
|
}: NodeProps<Node<FieldRemapDraftTransformNodeData>>): JSX.Element {
|
|
@@ -289,6 +361,10 @@ const nodeTypes = {
|
|
|
289
361
|
fieldRemapSplitOperator: SplitOperatorNode,
|
|
290
362
|
};
|
|
291
363
|
|
|
364
|
+
const edgeTypes = {
|
|
365
|
+
smoothstep: FieldRemapSmoothStepEdge,
|
|
366
|
+
};
|
|
367
|
+
|
|
292
368
|
/** Imperative Flow chrome actions for integrating hosts (fit-view without Controls DOM). */
|
|
293
369
|
export interface FieldRemapFlowActions {
|
|
294
370
|
fitView: (options?: { padding?: number; maxZoom?: number }) => void;
|
|
@@ -301,8 +377,10 @@ const DEFAULT_FIT_VIEW_OPTIONS = { padding: 0.12, maxZoom: 1.15 } as const;
|
|
|
301
377
|
* to the React Flow store (avoids update loops with controlled nodes).
|
|
302
378
|
*/
|
|
303
379
|
function FieldRemapFlowActionsBridge({
|
|
380
|
+
canFitView,
|
|
304
381
|
flowActionsRef,
|
|
305
382
|
}: {
|
|
383
|
+
readonly canFitView: () => boolean;
|
|
306
384
|
readonly flowActionsRef?: Ref<FieldRemapFlowActions | null> | undefined;
|
|
307
385
|
}): null {
|
|
308
386
|
const { fitView } = useReactFlow();
|
|
@@ -310,13 +388,16 @@ function FieldRemapFlowActionsBridge({
|
|
|
310
388
|
flowActionsRef,
|
|
311
389
|
() => ({
|
|
312
390
|
fitView: (options) => {
|
|
391
|
+
if (!canFitView()) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
313
394
|
void fitView({
|
|
314
395
|
padding: options?.padding ?? DEFAULT_FIT_VIEW_OPTIONS.padding,
|
|
315
396
|
maxZoom: options?.maxZoom ?? DEFAULT_FIT_VIEW_OPTIONS.maxZoom,
|
|
316
397
|
});
|
|
317
398
|
},
|
|
318
399
|
}),
|
|
319
|
-
[fitView],
|
|
400
|
+
[canFitView, fitView],
|
|
320
401
|
);
|
|
321
402
|
return null;
|
|
322
403
|
}
|
|
@@ -327,6 +408,16 @@ export interface FieldRemapFlowMapperProps {
|
|
|
327
408
|
readonly edges: readonly MappingEdge[];
|
|
328
409
|
readonly transforms: ValueTransformRegistry;
|
|
329
410
|
readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
|
|
411
|
+
/** Existing target/donor edges are replaced by default; `reject` preserves them unchanged. */
|
|
412
|
+
readonly rewirePolicy?: 'replace' | 'reject' | undefined;
|
|
413
|
+
/** Structured completed-attempt feedback; hover validation never invokes this callback. */
|
|
414
|
+
readonly onConnectionFeedback?:
|
|
415
|
+
((feedback: FieldRemapConnectionFeedback | null) => void) | undefined;
|
|
416
|
+
/**
|
|
417
|
+
* Authoritative parent/child conflict projection. `undefined` derives from supplied Flow inputs;
|
|
418
|
+
* an explicit empty array suppresses fallback derivation.
|
|
419
|
+
*/
|
|
420
|
+
readonly parentChildConflicts?: readonly MappingConflict[] | undefined;
|
|
330
421
|
/**
|
|
331
422
|
* `card` preserves the demo chrome. `embed` omits the flow hint and binding list;
|
|
332
423
|
* explicit `show*` props below take precedence.
|
|
@@ -334,6 +425,8 @@ export interface FieldRemapFlowMapperProps {
|
|
|
334
425
|
readonly chrome?: 'card' | 'embed' | undefined;
|
|
335
426
|
/** Show the empty-selection detail hint, or collapse that rail until a selection exists. */
|
|
336
427
|
readonly emptyDetail?: 'hint' | 'collapse' | undefined;
|
|
428
|
+
/** Render selection detail in the resizable rail (default) or the shared workbench Modal. */
|
|
429
|
+
readonly detailPresentation?: 'rail' | 'modal' | undefined;
|
|
337
430
|
/** Show the convert-first hint. Defaults to true for `card`, false for `embed`. */
|
|
338
431
|
readonly showFlowHint?: boolean | undefined;
|
|
339
432
|
/** Show the bottom binding list. Defaults to true for `card`, false for `embed`. */
|
|
@@ -343,6 +436,11 @@ export interface FieldRemapFlowMapperProps {
|
|
|
343
436
|
* the workspace expands without leaving an empty grid track.
|
|
344
437
|
*/
|
|
345
438
|
readonly showConvertPalette?: boolean | undefined;
|
|
439
|
+
/**
|
|
440
|
+
* View-only authoring guard. Existing mappings remain inspectable, while durable mutations and
|
|
441
|
+
* mapper-local drafts are disabled. This is not an authorization boundary.
|
|
442
|
+
*/
|
|
443
|
+
readonly readOnly?: boolean | undefined;
|
|
346
444
|
/** Document v2 n→m operators (display + authoring when onOperatorsChange is set). */
|
|
347
445
|
readonly operators?: readonly MappingOperator[] | undefined;
|
|
348
446
|
readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
|
|
@@ -404,9 +502,72 @@ export interface FieldRemapFlowMapperProps {
|
|
|
404
502
|
readonly t?: FieldRemapTranslate | undefined;
|
|
405
503
|
}
|
|
406
504
|
|
|
505
|
+
export type FieldRemapConnectionFeedbackReason =
|
|
506
|
+
FieldRemapFlowConnectionRejectionReason | 'rewire-policy-rejected';
|
|
507
|
+
|
|
508
|
+
export interface FieldRemapConnectionFeedback {
|
|
509
|
+
readonly reason: FieldRemapConnectionFeedbackReason;
|
|
510
|
+
readonly impactedEdgeIds?: readonly string[];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function areFieldRemapBulkSelectionsEqual(
|
|
514
|
+
left: readonly FieldRemapBulkSelectionRef[],
|
|
515
|
+
right: readonly FieldRemapBulkSelectionRef[],
|
|
516
|
+
): boolean {
|
|
517
|
+
return (
|
|
518
|
+
left.length === right.length &&
|
|
519
|
+
left.every(
|
|
520
|
+
(ref, index) => fieldRemapBulkSelectionKey(ref) === fieldRemapBulkSelectionKey(right[index]!),
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function fieldRemapBulkDomainKey(refs: readonly FieldRemapBulkSelectionRef[]): string {
|
|
526
|
+
return refs
|
|
527
|
+
.map(fieldRemapBulkSelectionKey)
|
|
528
|
+
.map((key) => `${key.length}:${key}`)
|
|
529
|
+
.join('');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function fieldRemapBulkRefFromKeyboardTarget(
|
|
533
|
+
target: EventTarget | null,
|
|
534
|
+
): FieldRemapBulkSelectionRef | undefined {
|
|
535
|
+
if (!(target instanceof Element)) {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const explicit = target.closest<HTMLElement>('[data-field-remap-bulk-kind]');
|
|
540
|
+
const explicitKind = explicit?.dataset.fieldRemapBulkKind;
|
|
541
|
+
const explicitEdgeId = explicit?.dataset.fieldRemapBulkEdgeId;
|
|
542
|
+
if (explicitKind === 'edge' && explicitEdgeId) {
|
|
543
|
+
return { kind: 'edge', edgeId: explicitEdgeId };
|
|
544
|
+
}
|
|
545
|
+
if (explicitKind === 'transformStep' && explicitEdgeId) {
|
|
546
|
+
const stepIndex = Number(explicit.dataset.fieldRemapBulkStepIndex);
|
|
547
|
+
if (Number.isInteger(stepIndex) && stepIndex >= 0) {
|
|
548
|
+
return { kind: 'transformStep', edgeId: explicitEdgeId, stepIndex };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function connectionFromFinalState(state: FinalConnectionState): Connection | null {
|
|
555
|
+
if (!state.fromNode || !state.fromHandle || !state.toNode || !state.toHandle) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const reversed = state.fromHandle.type === 'target';
|
|
559
|
+
return {
|
|
560
|
+
source: (reversed ? state.toNode : state.fromNode).id,
|
|
561
|
+
sourceHandle: (reversed ? state.toHandle : state.fromHandle).id ?? null,
|
|
562
|
+
target: (reversed ? state.fromNode : state.toNode).id,
|
|
563
|
+
targetHandle: (reversed ? state.fromHandle : state.toHandle).id ?? null,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
407
567
|
interface FieldRemapSplitWorkspaceProps {
|
|
408
568
|
readonly children: ReactNode;
|
|
409
569
|
readonly layout: 'wide' | 'medium' | 'narrow';
|
|
570
|
+
readonly reserveHiddenDetailSplit: boolean;
|
|
410
571
|
readonly showConvertPalette: boolean;
|
|
411
572
|
readonly showDetail: boolean;
|
|
412
573
|
readonly surface: 'binding' | 'convert-note' | 'draft-convert' | 'operator';
|
|
@@ -420,6 +581,7 @@ interface FieldRemapSplitWorkspaceProps {
|
|
|
420
581
|
function FieldRemapSplitWorkspace({
|
|
421
582
|
children,
|
|
422
583
|
layout,
|
|
584
|
+
reserveHiddenDetailSplit,
|
|
423
585
|
showConvertPalette,
|
|
424
586
|
showDetail,
|
|
425
587
|
surface,
|
|
@@ -438,26 +600,29 @@ function FieldRemapSplitWorkspace({
|
|
|
438
600
|
});
|
|
439
601
|
const paletteSizePx = paletteSizeByLayout[layout];
|
|
440
602
|
const detailSizePx = detailSizeByLayout[layout];
|
|
441
|
-
const canvasWithDetail =
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
603
|
+
const canvasWithDetail =
|
|
604
|
+
showDetail || reserveHiddenDetailSplit ? (
|
|
605
|
+
<SplitView
|
|
606
|
+
className={
|
|
607
|
+
showDetail
|
|
608
|
+
? 'workbench-field-remap-flow__canvas-detail-split'
|
|
609
|
+
: 'workbench-field-remap-flow__canvas-detail-split ui-workbench-split-view--secondary-collapsed'
|
|
610
|
+
}
|
|
611
|
+
layoutMode="secondary-fixed"
|
|
612
|
+
maxSecondarySizePx={isNarrow ? 320 : 480}
|
|
613
|
+
minPrimarySizePx={isNarrow ? 200 : 280}
|
|
614
|
+
minSecondarySizePx={isNarrow ? 160 : 256}
|
|
615
|
+
onSecondarySizePxChange={(nextSize) => {
|
|
616
|
+
setDetailSizeByLayout((current) => ({ ...current, [layout]: nextSize }));
|
|
617
|
+
}}
|
|
618
|
+
orientation={isNarrow ? 'vertical' : 'horizontal'}
|
|
619
|
+
primary={canvas}
|
|
620
|
+
secondary={detail}
|
|
621
|
+
secondarySizePx={detailSizePx}
|
|
622
|
+
/>
|
|
623
|
+
) : (
|
|
624
|
+
canvas
|
|
625
|
+
);
|
|
461
626
|
|
|
462
627
|
const content = (
|
|
463
628
|
<SplitView
|
|
@@ -502,11 +667,16 @@ function FieldRemapFlowCanvas({
|
|
|
502
667
|
edges,
|
|
503
668
|
transforms,
|
|
504
669
|
onEdgesChange,
|
|
670
|
+
rewirePolicy = 'replace',
|
|
671
|
+
onConnectionFeedback,
|
|
672
|
+
parentChildConflicts,
|
|
505
673
|
chrome = 'card',
|
|
506
674
|
emptyDetail: emptyDetailProp,
|
|
675
|
+
detailPresentation = 'rail',
|
|
507
676
|
showFlowHint: showFlowHintProp,
|
|
508
677
|
showBindingsList: showBindingsListProp,
|
|
509
678
|
showConvertPalette = true,
|
|
679
|
+
readOnly = false,
|
|
510
680
|
operators = [],
|
|
511
681
|
onOperatorsChange,
|
|
512
682
|
sourceTitle,
|
|
@@ -530,21 +700,271 @@ function FieldRemapFlowCanvas({
|
|
|
530
700
|
() => resolveFieldRemapChromeLabels(labelOverrides, t),
|
|
531
701
|
[labelOverrides, t],
|
|
532
702
|
);
|
|
533
|
-
const showFlowHint = showFlowHintProp ?? chrome !== 'embed';
|
|
703
|
+
const showFlowHint = !readOnly && (showFlowHintProp ?? chrome !== 'embed');
|
|
534
704
|
const showBindingsList = showBindingsListProp ?? chrome !== 'embed';
|
|
705
|
+
const showAuthoringPalette = showConvertPalette && !readOnly;
|
|
535
706
|
const emptyDetail = emptyDetailProp ?? (chrome === 'embed' ? 'collapse' : 'hint');
|
|
536
707
|
const mapperRef = useRef<HTMLDivElement>(null);
|
|
708
|
+
const canvasRef = useRef<HTMLDivElement>(null);
|
|
709
|
+
const restoreMapperFocusRef = useRef(false);
|
|
710
|
+
const [hasPositiveCanvasSize, setHasPositiveCanvasSize] = useState(false);
|
|
537
711
|
const [workspaceLayout, setWorkspaceLayout] = useState<'wide' | 'medium' | 'narrow'>('wide');
|
|
538
712
|
const [internalSelection, setInternalSelection] = useState<FieldRemapSelection>(null);
|
|
539
|
-
const
|
|
540
|
-
const
|
|
713
|
+
const authoritativeSelection = selectionProp !== undefined ? selectionProp : internalSelection;
|
|
714
|
+
const selectionExternallyManaged =
|
|
715
|
+
selectionProp !== undefined || onSelectionChangeProp !== undefined;
|
|
716
|
+
const [bulkSelection, setBulkSelection] = useState<readonly FieldRemapBulkSelectionRef[]>(() => {
|
|
717
|
+
const initial = asFieldRemapBulkSelectionRef(selectionProp ?? null);
|
|
718
|
+
return initial ? normalizeFieldRemapBulkSelection(edges, [initial]) : [];
|
|
719
|
+
});
|
|
720
|
+
const selection = authoritativeSelection;
|
|
721
|
+
const selectionRef = useRef(authoritativeSelection);
|
|
722
|
+
selectionRef.current = authoritativeSelection;
|
|
723
|
+
const setSelection = useCallback(
|
|
724
|
+
(next: FieldRemapSelection) => {
|
|
725
|
+
if (fieldRemapSelectionKey(selectionRef.current) === fieldRemapSelectionKey(next)) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (!selectionExternallyManaged) {
|
|
729
|
+
setInternalSelection(next);
|
|
730
|
+
const nextRef = asFieldRemapBulkSelectionRef(next);
|
|
731
|
+
setBulkSelection(nextRef ? normalizeFieldRemapBulkSelection(edges, [nextRef]) : []);
|
|
732
|
+
}
|
|
733
|
+
onSelectionChangeProp?.(next);
|
|
734
|
+
},
|
|
735
|
+
[edges, onSelectionChangeProp, selectionExternallyManaged],
|
|
736
|
+
);
|
|
737
|
+
const setSelectionRef = useRef(setSelection);
|
|
738
|
+
setSelectionRef.current = setSelection;
|
|
739
|
+
const closeModalDetail = useCallback(() => setSelectionRef.current(null), []);
|
|
740
|
+
const canonicalBulkRefs = useMemo(() => listFieldRemapBulkSelectionRefs(edges), [edges]);
|
|
741
|
+
const bulkDomainKey = useMemo(
|
|
742
|
+
() => fieldRemapBulkDomainKey(canonicalBulkRefs),
|
|
743
|
+
[canonicalBulkRefs],
|
|
744
|
+
);
|
|
745
|
+
const bulkSelectionKeys = useMemo(
|
|
746
|
+
() => new Set(bulkSelection.map(fieldRemapBulkSelectionKey)),
|
|
747
|
+
[bulkSelection],
|
|
748
|
+
);
|
|
749
|
+
const authoritativeSelectionKey = fieldRemapSelectionKey(authoritativeSelection);
|
|
750
|
+
const previousAuthoritativeSelectionKeyRef = useRef(authoritativeSelectionKey);
|
|
751
|
+
const bulkFocusTargetsRef = useRef(
|
|
752
|
+
new Map<string, Partial<Record<FieldRemapFocusSurface, FieldRemapFocusableElement>>>(),
|
|
753
|
+
);
|
|
754
|
+
const registerBulkFocusTarget = useCallback<RegisterFieldRemapFocusTarget>(
|
|
755
|
+
(key, surface, element) => {
|
|
756
|
+
const current = bulkFocusTargetsRef.current.get(key) ?? {};
|
|
757
|
+
if (element) {
|
|
758
|
+
current[surface] = element;
|
|
759
|
+
bulkFocusTargetsRef.current.set(key, current);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
delete current[surface];
|
|
763
|
+
if (current.graph || current.list) {
|
|
764
|
+
bulkFocusTargetsRef.current.set(key, current);
|
|
765
|
+
} else {
|
|
766
|
+
bulkFocusTargetsRef.current.delete(key);
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
[],
|
|
770
|
+
);
|
|
771
|
+
const focusBulkTarget = useCallback(
|
|
772
|
+
(ref: FieldRemapBulkSelectionRef, preferredSurface?: FieldRemapFocusSurface) => {
|
|
773
|
+
const targets = bulkFocusTargetsRef.current.get(fieldRemapBulkSelectionKey(ref));
|
|
774
|
+
const target =
|
|
775
|
+
(preferredSurface ? targets?.[preferredSurface] : undefined) ??
|
|
776
|
+
targets?.list ??
|
|
777
|
+
targets?.graph;
|
|
778
|
+
target?.focus({ preventScroll: true });
|
|
779
|
+
},
|
|
780
|
+
[],
|
|
781
|
+
);
|
|
782
|
+
const lastPrimaryCorrectionRef = useRef<string | undefined>(undefined);
|
|
783
|
+
const pendingPrimaryCorrectionAckRef = useRef<
|
|
784
|
+
{ readonly domainKey: string; readonly selectionKey: string } | undefined
|
|
785
|
+
>(undefined);
|
|
786
|
+
const pendingBulkFocusRef = useRef<
|
|
787
|
+
| {
|
|
788
|
+
readonly domainKey: string;
|
|
789
|
+
readonly target: FieldRemapBulkSelectionRef | null;
|
|
790
|
+
}
|
|
791
|
+
| undefined
|
|
792
|
+
>(undefined);
|
|
793
|
+
const pendingBulkCommitRef = useRef<
|
|
794
|
+
| {
|
|
795
|
+
readonly domainKey: string;
|
|
796
|
+
readonly membership: readonly FieldRemapBulkSelectionRef[];
|
|
797
|
+
readonly primary: FieldRemapSelection;
|
|
798
|
+
}
|
|
799
|
+
| undefined
|
|
800
|
+
>(undefined);
|
|
541
801
|
const previewVisible = showPreview && preview !== undefined && preview.status !== 'unavailable';
|
|
802
|
+
const flowAriaLabelConfig = useMemo(
|
|
803
|
+
() => ({
|
|
804
|
+
'node.a11yDescription.default': readOnly
|
|
805
|
+
? 'Press Enter or Space to inspect this item. Control or Command toggles a non-primary item; Shift adds it.'
|
|
806
|
+
: 'Press Enter or Space to select this item. Control or Command toggles a non-primary item; Shift adds it.',
|
|
807
|
+
'node.a11yDescription.keyboardDisabled': readOnly
|
|
808
|
+
? 'Press Enter or Space to inspect this item. Workbench editing is read only.'
|
|
809
|
+
: 'Press Enter or Space to select this item. Use the Workbench controls to edit it.',
|
|
810
|
+
'edge.a11yDescription.default': readOnly
|
|
811
|
+
? 'Press Enter or Space to inspect this mapping. Control or Command toggles a non-primary mapping; Shift adds it.'
|
|
812
|
+
: 'Press Enter or Space to select this mapping. Control or Command toggles a non-primary mapping; Shift adds it.',
|
|
813
|
+
}),
|
|
814
|
+
[readOnly],
|
|
815
|
+
);
|
|
816
|
+
const detailVisible = emptyDetail === 'hint' || selection !== null;
|
|
817
|
+
const sideRailVisible = previewVisible || (detailPresentation === 'rail' && detailVisible);
|
|
542
818
|
const [drafts, setDrafts] = useState<readonly FieldRemapDraftTransform[]>([]);
|
|
819
|
+
const [draftPositions, setDraftPositions] = useState<ReadonlyMap<string, XYPosition>>(
|
|
820
|
+
() => new Map(),
|
|
821
|
+
);
|
|
822
|
+
const [connectionFeedback, setConnectionFeedback] = useState<FieldRemapConnectionFeedback | null>(
|
|
823
|
+
null,
|
|
824
|
+
);
|
|
825
|
+
const connectionAttemptCompletedRef = useRef(false);
|
|
543
826
|
const [placeTransformId, setPlaceTransformId] = useState(() => {
|
|
544
827
|
const first = transforms.list().find((definition) => definition.id !== 'identity');
|
|
545
828
|
return first?.id ?? '';
|
|
546
829
|
});
|
|
547
830
|
const transformRegistrySignature = createTransformRegistrySignature(transforms);
|
|
831
|
+
const { screenToFlowPosition } = useReactFlow();
|
|
832
|
+
|
|
833
|
+
const removeDraftPosition = useCallback((localId: string) => {
|
|
834
|
+
setDraftPositions((current) => {
|
|
835
|
+
if (!current.has(localId)) {
|
|
836
|
+
return current;
|
|
837
|
+
}
|
|
838
|
+
const next = new Map(current);
|
|
839
|
+
next.delete(localId);
|
|
840
|
+
return next;
|
|
841
|
+
});
|
|
842
|
+
}, []);
|
|
843
|
+
|
|
844
|
+
useEffect(() => {
|
|
845
|
+
if (detailPresentation === 'modal') {
|
|
846
|
+
void loadFieldRemapModalDetail().catch(() => undefined);
|
|
847
|
+
}
|
|
848
|
+
}, [detailPresentation]);
|
|
849
|
+
|
|
850
|
+
useEffect(() => {
|
|
851
|
+
if (!readOnly) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
setDrafts([]);
|
|
855
|
+
setDraftPositions(new Map());
|
|
856
|
+
setConnectionFeedback(null);
|
|
857
|
+
connectionAttemptCompletedRef.current = false;
|
|
858
|
+
if (selectionProp === undefined) {
|
|
859
|
+
setInternalSelection((current) => (current?.kind === 'draft' ? null : current));
|
|
860
|
+
}
|
|
861
|
+
}, [readOnly, selectionProp]);
|
|
862
|
+
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
let authoritativeReset: readonly FieldRemapBulkSelectionRef[] | undefined;
|
|
865
|
+
if (previousAuthoritativeSelectionKeyRef.current !== authoritativeSelectionKey) {
|
|
866
|
+
previousAuthoritativeSelectionKeyRef.current = authoritativeSelectionKey;
|
|
867
|
+
lastPrimaryCorrectionRef.current = undefined;
|
|
868
|
+
const pendingAck = pendingPrimaryCorrectionAckRef.current;
|
|
869
|
+
const acceptsCorrection =
|
|
870
|
+
pendingAck?.selectionKey === authoritativeSelectionKey &&
|
|
871
|
+
(pendingAck.domainKey === bulkDomainKey ||
|
|
872
|
+
pendingAck.domainKey === pendingBulkCommitRef.current?.domainKey);
|
|
873
|
+
pendingPrimaryCorrectionAckRef.current = undefined;
|
|
874
|
+
if (!acceptsCorrection) {
|
|
875
|
+
pendingBulkCommitRef.current = undefined;
|
|
876
|
+
const authoritativeRef = asFieldRemapBulkSelectionRef(authoritativeSelection);
|
|
877
|
+
const next = authoritativeRef
|
|
878
|
+
? normalizeFieldRemapBulkSelection(edges, [authoritativeRef])
|
|
879
|
+
: [];
|
|
880
|
+
authoritativeReset = next;
|
|
881
|
+
setBulkSelection((current) =>
|
|
882
|
+
areFieldRemapBulkSelectionsEqual(current, next) ? current : next,
|
|
883
|
+
);
|
|
884
|
+
if (!authoritativeRef || next.length > 0) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const pendingCommit = pendingBulkCommitRef.current;
|
|
891
|
+
if (pendingCommit) {
|
|
892
|
+
const primaryAccepted =
|
|
893
|
+
fieldRemapSelectionKey(authoritativeSelection) ===
|
|
894
|
+
fieldRemapSelectionKey(pendingCommit.primary);
|
|
895
|
+
if (primaryAccepted || bulkDomainKey === pendingCommit.domainKey) {
|
|
896
|
+
setBulkSelection((current) =>
|
|
897
|
+
areFieldRemapBulkSelectionsEqual(current, pendingCommit.membership)
|
|
898
|
+
? current
|
|
899
|
+
: pendingCommit.membership,
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
if (bulkDomainKey !== pendingCommit.domainKey) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
pendingBulkCommitRef.current = undefined;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const primaryRef = asFieldRemapBulkSelectionRef(authoritativeSelection);
|
|
909
|
+
if (!primaryRef) {
|
|
910
|
+
setBulkSelection((current) => (current.length === 0 ? current : []));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const primaryKey = fieldRemapBulkSelectionKey(primaryRef);
|
|
915
|
+
const primaryIsValid = canonicalBulkRefs.some(
|
|
916
|
+
(ref) => fieldRemapBulkSelectionKey(ref) === primaryKey,
|
|
917
|
+
);
|
|
918
|
+
let next = authoritativeReset ?? normalizeFieldRemapBulkSelection(edges, bulkSelection);
|
|
919
|
+
if (primaryIsValid && !next.some((ref) => fieldRemapBulkSelectionKey(ref) === primaryKey)) {
|
|
920
|
+
next = normalizeFieldRemapBulkSelection(edges, [...next, primaryRef]);
|
|
921
|
+
}
|
|
922
|
+
setBulkSelection((current) =>
|
|
923
|
+
areFieldRemapBulkSelectionsEqual(current, next) ? current : next,
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
if (primaryIsValid) {
|
|
927
|
+
lastPrimaryCorrectionRef.current = undefined;
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const nextPrimary = next[0] ?? null;
|
|
932
|
+
const correctionKey = `${bulkDomainKey}\u0001${fieldRemapSelectionKey(authoritativeSelection)}\u0001${fieldRemapSelectionKey(nextPrimary)}`;
|
|
933
|
+
if (lastPrimaryCorrectionRef.current === correctionKey) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
lastPrimaryCorrectionRef.current = correctionKey;
|
|
937
|
+
pendingPrimaryCorrectionAckRef.current = onSelectionChangeProp
|
|
938
|
+
? {
|
|
939
|
+
domainKey: bulkDomainKey,
|
|
940
|
+
selectionKey: fieldRemapSelectionKey(nextPrimary),
|
|
941
|
+
}
|
|
942
|
+
: undefined;
|
|
943
|
+
if (!selectionExternallyManaged) {
|
|
944
|
+
setInternalSelection(nextPrimary);
|
|
945
|
+
} else {
|
|
946
|
+
setSelection(nextPrimary);
|
|
947
|
+
}
|
|
948
|
+
}, [
|
|
949
|
+
bulkDomainKey,
|
|
950
|
+
bulkSelection,
|
|
951
|
+
canonicalBulkRefs,
|
|
952
|
+
authoritativeSelectionKey,
|
|
953
|
+
edges,
|
|
954
|
+
onSelectionChangeProp,
|
|
955
|
+
authoritativeSelection,
|
|
956
|
+
selection,
|
|
957
|
+
selectionExternallyManaged,
|
|
958
|
+
setSelection,
|
|
959
|
+
]);
|
|
960
|
+
|
|
961
|
+
useEffect(() => {
|
|
962
|
+
if (!restoreMapperFocusRef.current || selection !== null || drafts.length > 0) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
restoreMapperFocusRef.current = false;
|
|
966
|
+
mapperRef.current?.focus({ preventScroll: true });
|
|
967
|
+
}, [drafts.length, selection]);
|
|
548
968
|
|
|
549
969
|
useEffect(() => {
|
|
550
970
|
const element = mapperRef.current;
|
|
@@ -569,6 +989,48 @@ function FieldRemapFlowCanvas({
|
|
|
569
989
|
return () => observer.disconnect();
|
|
570
990
|
}, []);
|
|
571
991
|
|
|
992
|
+
const canFitView = useCallback(() => {
|
|
993
|
+
const bounds = canvasRef.current?.getBoundingClientRect();
|
|
994
|
+
return bounds !== undefined && bounds.width > 0 && bounds.height > 0;
|
|
995
|
+
}, []);
|
|
996
|
+
|
|
997
|
+
useEffect(() => {
|
|
998
|
+
const element = canvasRef.current;
|
|
999
|
+
if (!element) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const updateMountEligibility = () => {
|
|
1004
|
+
const bounds = element.getBoundingClientRect();
|
|
1005
|
+
if (bounds.width > 0 && bounds.height > 0) {
|
|
1006
|
+
setHasPositiveCanvasSize(true);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
updateMountEligibility();
|
|
1011
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
let pendingFrame: number | undefined;
|
|
1015
|
+
const scheduleMountEligibilityUpdate = () => {
|
|
1016
|
+
if (pendingFrame !== undefined) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
pendingFrame = requestAnimationFrame(() => {
|
|
1020
|
+
pendingFrame = undefined;
|
|
1021
|
+
updateMountEligibility();
|
|
1022
|
+
});
|
|
1023
|
+
};
|
|
1024
|
+
const observer = new ResizeObserver(scheduleMountEligibilityUpdate);
|
|
1025
|
+
observer.observe(element);
|
|
1026
|
+
return () => {
|
|
1027
|
+
observer.disconnect();
|
|
1028
|
+
if (pendingFrame !== undefined) {
|
|
1029
|
+
cancelAnimationFrame(pendingFrame);
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
}, []);
|
|
1033
|
+
|
|
572
1034
|
const graph = useMemo(
|
|
573
1035
|
() =>
|
|
574
1036
|
mappingToFlowGraph({
|
|
@@ -580,6 +1042,7 @@ function FieldRemapFlowCanvas({
|
|
|
580
1042
|
sourceTitle,
|
|
581
1043
|
targetTitle,
|
|
582
1044
|
drafts,
|
|
1045
|
+
draftPositions,
|
|
583
1046
|
}),
|
|
584
1047
|
[
|
|
585
1048
|
sources,
|
|
@@ -590,22 +1053,68 @@ function FieldRemapFlowCanvas({
|
|
|
590
1053
|
sourceTitle,
|
|
591
1054
|
targetTitle,
|
|
592
1055
|
drafts,
|
|
1056
|
+
draftPositions,
|
|
593
1057
|
transformRegistrySignature,
|
|
594
1058
|
],
|
|
595
1059
|
);
|
|
596
1060
|
|
|
1061
|
+
const applyBulkSelectionGesture = useCallback(
|
|
1062
|
+
(
|
|
1063
|
+
target: FieldRemapBulkSelectionRef,
|
|
1064
|
+
modifiers: {
|
|
1065
|
+
readonly ctrlKey: boolean;
|
|
1066
|
+
readonly metaKey: boolean;
|
|
1067
|
+
readonly shiftKey: boolean;
|
|
1068
|
+
},
|
|
1069
|
+
) => {
|
|
1070
|
+
pendingPrimaryCorrectionAckRef.current = undefined;
|
|
1071
|
+
const gesture =
|
|
1072
|
+
modifiers.ctrlKey || modifiers.metaKey ? 'toggle' : modifiers.shiftKey ? 'add' : 'plain';
|
|
1073
|
+
const next = updateFieldRemapBulkSelection({
|
|
1074
|
+
edges,
|
|
1075
|
+
membership: bulkSelection,
|
|
1076
|
+
primary: selection,
|
|
1077
|
+
target,
|
|
1078
|
+
gesture,
|
|
1079
|
+
});
|
|
1080
|
+
const primaryChanged =
|
|
1081
|
+
fieldRemapSelectionKey(next.primary) !== fieldRemapSelectionKey(selection);
|
|
1082
|
+
if (!primaryChanged || !selectionExternallyManaged) {
|
|
1083
|
+
setBulkSelection((current) =>
|
|
1084
|
+
areFieldRemapBulkSelectionsEqual(current, next.membership) ? current : next.membership,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (primaryChanged) {
|
|
1088
|
+
setSelection(next.primary);
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
[bulkSelection, edges, selection, selectionExternallyManaged, setSelection],
|
|
1092
|
+
);
|
|
1093
|
+
|
|
597
1094
|
const nodesWithSelection = useMemo(
|
|
598
1095
|
() =>
|
|
599
1096
|
graph.nodes.map((node) => {
|
|
600
1097
|
if (node.data.kind === 'transform') {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1098
|
+
const ref = {
|
|
1099
|
+
kind: 'transformStep',
|
|
1100
|
+
edgeId: node.data.mappingEdgeId,
|
|
1101
|
+
stepIndex: node.data.stepIndex,
|
|
1102
|
+
} as const;
|
|
1103
|
+
const selected = bulkSelectionKeys.has(fieldRemapBulkSelectionKey(ref));
|
|
605
1104
|
return {
|
|
606
1105
|
...node,
|
|
607
|
-
data: { ...node.data, selected },
|
|
1106
|
+
data: { ...node.data, selected, registerFocusTarget: registerBulkFocusTarget },
|
|
608
1107
|
selected,
|
|
1108
|
+
selectable: false,
|
|
1109
|
+
focusable: true,
|
|
1110
|
+
ariaRole: 'button' as const,
|
|
1111
|
+
ariaLabel: `${node.data.label} convert step`,
|
|
1112
|
+
domAttributes: {
|
|
1113
|
+
'aria-pressed': selected,
|
|
1114
|
+
'data-field-remap-bulk-kind': ref.kind,
|
|
1115
|
+
'data-field-remap-bulk-edge-id': ref.edgeId,
|
|
1116
|
+
'data-field-remap-bulk-step-index': ref.stepIndex,
|
|
1117
|
+
},
|
|
609
1118
|
};
|
|
610
1119
|
}
|
|
611
1120
|
if (node.data.kind === 'draft-transform') {
|
|
@@ -619,50 +1128,168 @@ function FieldRemapFlowCanvas({
|
|
|
619
1128
|
}
|
|
620
1129
|
return node;
|
|
621
1130
|
}),
|
|
622
|
-
[graph.nodes, selection],
|
|
1131
|
+
[bulkSelectionKeys, graph.nodes, registerBulkFocusTarget, selection],
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
const flowEdgesWithSelection = useMemo(
|
|
1135
|
+
() =>
|
|
1136
|
+
graph.edges.map((edge) => {
|
|
1137
|
+
const data = edge.data as FieldRemapFlowEdgeData | undefined;
|
|
1138
|
+
const mappingEdgeId = data?.mappingEdgeId;
|
|
1139
|
+
const selected = mappingEdgeId
|
|
1140
|
+
? bulkSelectionKeys.has(
|
|
1141
|
+
fieldRemapBulkSelectionKey({ kind: 'edge', edgeId: mappingEdgeId }),
|
|
1142
|
+
)
|
|
1143
|
+
: false;
|
|
1144
|
+
if (!mappingEdgeId) {
|
|
1145
|
+
return { ...edge, selected };
|
|
1146
|
+
}
|
|
1147
|
+
const canonicalSegment = data?.segment === 'direct' || data?.segment === 'in';
|
|
1148
|
+
return {
|
|
1149
|
+
...edge,
|
|
1150
|
+
data: { ...data, registerFocusTarget: registerBulkFocusTarget },
|
|
1151
|
+
selected,
|
|
1152
|
+
selectable: false,
|
|
1153
|
+
focusable: canonicalSegment,
|
|
1154
|
+
ariaRole: canonicalSegment ? ('button' as const) : ('presentation' as const),
|
|
1155
|
+
ariaLabel: canonicalSegment ? `Mapping ${mappingEdgeId}` : undefined,
|
|
1156
|
+
domAttributes: canonicalSegment
|
|
1157
|
+
? {
|
|
1158
|
+
'aria-pressed': selected,
|
|
1159
|
+
'data-field-remap-bulk-kind': 'edge',
|
|
1160
|
+
'data-field-remap-bulk-edge-id': mappingEdgeId,
|
|
1161
|
+
}
|
|
1162
|
+
: { 'aria-hidden': true },
|
|
1163
|
+
};
|
|
1164
|
+
}),
|
|
1165
|
+
[bulkSelectionKeys, graph.edges, registerBulkFocusTarget],
|
|
623
1166
|
);
|
|
624
1167
|
|
|
625
1168
|
const [nodes, setNodes, onNodesChange] = useNodesState(nodesWithSelection);
|
|
626
|
-
const [flowEdges, setFlowEdges, onFlowEdgesChange] = useEdgesState(
|
|
1169
|
+
const [flowEdges, setFlowEdges, onFlowEdgesChange] = useEdgesState(flowEdgesWithSelection);
|
|
1170
|
+
const onProjectedNodesChange = useCallback(
|
|
1171
|
+
(changes: Parameters<typeof onNodesChange>[0]) => {
|
|
1172
|
+
onNodesChange(changes.filter((change) => change.type !== 'select'));
|
|
1173
|
+
},
|
|
1174
|
+
[onNodesChange],
|
|
1175
|
+
);
|
|
1176
|
+
const onProjectedFlowEdgesChange = useCallback(
|
|
1177
|
+
(changes: Parameters<typeof onFlowEdgesChange>[0]) => {
|
|
1178
|
+
onFlowEdgesChange(changes.filter((change) => change.type !== 'select'));
|
|
1179
|
+
},
|
|
1180
|
+
[onFlowEdgesChange],
|
|
1181
|
+
);
|
|
627
1182
|
|
|
628
1183
|
// Depending directly on `nodesWithSelection` (a new array after each graph
|
|
629
1184
|
// calculation) re-enters XYFlow's StoreUpdater. The explicit signature keeps
|
|
630
1185
|
// that loop guard while still tracking every value copied into rendered nodes.
|
|
631
1186
|
const graphSyncKey = createFieldRemapGraphSyncKey({
|
|
632
1187
|
nodes: nodesWithSelection,
|
|
633
|
-
edges:
|
|
1188
|
+
edges: flowEdgesWithSelection,
|
|
634
1189
|
selection,
|
|
635
1190
|
transformRegistrySignature,
|
|
636
1191
|
});
|
|
637
1192
|
const nodesWithSelectionRef = useRef(nodesWithSelection);
|
|
638
|
-
const graphEdgesRef = useRef(
|
|
1193
|
+
const graphEdgesRef = useRef(flowEdgesWithSelection);
|
|
639
1194
|
nodesWithSelectionRef.current = nodesWithSelection;
|
|
640
|
-
graphEdgesRef.current =
|
|
1195
|
+
graphEdgesRef.current = flowEdgesWithSelection;
|
|
641
1196
|
|
|
642
1197
|
useEffect(() => {
|
|
643
1198
|
setNodes(nodesWithSelectionRef.current);
|
|
644
1199
|
setFlowEdges(graphEdgesRef.current);
|
|
645
1200
|
}, [graphSyncKey, setFlowEdges, setNodes]);
|
|
646
1201
|
|
|
1202
|
+
useEffect(() => {
|
|
1203
|
+
const pending = pendingBulkFocusRef.current;
|
|
1204
|
+
const mapper = mapperRef.current;
|
|
1205
|
+
if (!pending || !mapper || pending.domainKey !== bulkDomainKey) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
pendingBulkFocusRef.current = undefined;
|
|
1209
|
+
if (!pending.target) {
|
|
1210
|
+
mapper.focus({ preventScroll: true });
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const targets = bulkFocusTargetsRef.current.get(fieldRemapBulkSelectionKey(pending.target));
|
|
1214
|
+
(targets?.list ?? targets?.graph ?? mapper).focus({ preventScroll: true });
|
|
1215
|
+
}, [bulkDomainKey]);
|
|
1216
|
+
|
|
647
1217
|
const connectionContext = useMemo(
|
|
648
1218
|
() => ({ sources, targets, edges, transforms, drafts, operators }),
|
|
649
1219
|
[sources, targets, edges, transforms, drafts, operators],
|
|
650
1220
|
);
|
|
651
1221
|
|
|
1222
|
+
const conflicts = useMemo(
|
|
1223
|
+
() => parentChildConflicts ?? findParentChildMappingConflicts(edges, sources, targets),
|
|
1224
|
+
[edges, parentChildConflicts, sources, targets],
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
const publishConnectionFeedback = useCallback(
|
|
1228
|
+
(feedback: FieldRemapConnectionFeedback | null) => {
|
|
1229
|
+
setConnectionFeedback(feedback);
|
|
1230
|
+
onConnectionFeedback?.(feedback);
|
|
1231
|
+
},
|
|
1232
|
+
[onConnectionFeedback],
|
|
1233
|
+
);
|
|
1234
|
+
|
|
652
1235
|
const isValidConnection = useCallback(
|
|
653
1236
|
(connection: Connection | Edge) =>
|
|
654
|
-
isValidFieldRemapFlowConnection(connection, connectionContext),
|
|
655
|
-
[connectionContext],
|
|
1237
|
+
!readOnly && isValidFieldRemapFlowConnection(connection, connectionContext),
|
|
1238
|
+
[connectionContext, readOnly],
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
const onConnectStart = useCallback(() => {
|
|
1242
|
+
if (readOnly) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
// Clearing at attempt start lets an identical later rejection be announced once at completion.
|
|
1246
|
+
connectionAttemptCompletedRef.current = false;
|
|
1247
|
+
setConnectionFeedback(null);
|
|
1248
|
+
}, [readOnly]);
|
|
1249
|
+
|
|
1250
|
+
const onConnectEnd = useCallback(
|
|
1251
|
+
(_event: globalThis.MouseEvent | TouchEvent, state: FinalConnectionState) => {
|
|
1252
|
+
if (readOnly) {
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (connectionAttemptCompletedRef.current) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
connectionAttemptCompletedRef.current = true;
|
|
1259
|
+
const connection = connectionFromFinalState(state);
|
|
1260
|
+
if (!connection) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const evaluation = evaluateFieldRemapFlowConnection(connection, connectionContext);
|
|
1264
|
+
if (evaluation.status === 'rejected') {
|
|
1265
|
+
publishConnectionFeedback({ reason: evaluation.reason });
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
[connectionContext, publishConnectionFeedback, readOnly],
|
|
656
1269
|
);
|
|
657
1270
|
|
|
658
1271
|
const onConnect = useCallback(
|
|
659
1272
|
(connection: Connection) => {
|
|
1273
|
+
if (readOnly) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
660
1276
|
if (!connection.source || !connection.target) {
|
|
661
1277
|
return;
|
|
662
1278
|
}
|
|
663
|
-
|
|
1279
|
+
const evaluation = evaluateFieldRemapFlowConnection(connection, connectionContext);
|
|
1280
|
+
if (evaluation.status === 'rejected') {
|
|
664
1281
|
return;
|
|
665
1282
|
}
|
|
1283
|
+
if (evaluation.status === 'rewire' && rewirePolicy === 'reject') {
|
|
1284
|
+
connectionAttemptCompletedRef.current = true;
|
|
1285
|
+
publishConnectionFeedback({
|
|
1286
|
+
reason: 'rewire-policy-rejected',
|
|
1287
|
+
impactedEdgeIds: evaluation.impactedEdgeIds,
|
|
1288
|
+
});
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
connectionAttemptCompletedRef.current = true;
|
|
1292
|
+
publishConnectionFeedback(null);
|
|
666
1293
|
|
|
667
1294
|
const draftAsTarget = parseDraftTransformNodeId(connection.target);
|
|
668
1295
|
if (draftAsTarget && connection.sourceHandle) {
|
|
@@ -683,6 +1310,7 @@ function FieldRemapFlowCanvas({
|
|
|
683
1310
|
);
|
|
684
1311
|
onEdgesChange([...withoutTarget, finalized]);
|
|
685
1312
|
setDrafts(drafts.filter((item) => item.localId !== draft.localId));
|
|
1313
|
+
removeDraftPosition(draft.localId);
|
|
686
1314
|
setSelection({
|
|
687
1315
|
kind: 'transformStep',
|
|
688
1316
|
edgeId: finalized.id,
|
|
@@ -714,6 +1342,7 @@ function FieldRemapFlowCanvas({
|
|
|
714
1342
|
);
|
|
715
1343
|
onEdgesChange([...withoutTarget, finalized]);
|
|
716
1344
|
setDrafts(drafts.filter((item) => item.localId !== draft.localId));
|
|
1345
|
+
removeDraftPosition(draft.localId);
|
|
717
1346
|
setSelection({
|
|
718
1347
|
kind: 'transformStep',
|
|
719
1348
|
edgeId: finalized.id,
|
|
@@ -775,6 +1404,10 @@ function FieldRemapFlowCanvas({
|
|
|
775
1404
|
onEdgesChange,
|
|
776
1405
|
onOperatorsChange,
|
|
777
1406
|
operators,
|
|
1407
|
+
publishConnectionFeedback,
|
|
1408
|
+
readOnly,
|
|
1409
|
+
removeDraftPosition,
|
|
1410
|
+
rewirePolicy,
|
|
778
1411
|
setSelection,
|
|
779
1412
|
sources,
|
|
780
1413
|
targets,
|
|
@@ -782,8 +1415,107 @@ function FieldRemapFlowCanvas({
|
|
|
782
1415
|
],
|
|
783
1416
|
);
|
|
784
1417
|
|
|
1418
|
+
const commitBulkDelete = useCallback(
|
|
1419
|
+
(refs: readonly FieldRemapBulkSelectionRef[]): boolean => {
|
|
1420
|
+
const plan = planFieldRemapBulkDelete(edges, refs);
|
|
1421
|
+
if (plan.status !== 'changed') {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const removedEdgeIds = new Set(
|
|
1426
|
+
refs.filter((ref) => ref.kind === 'edge').map((ref) => ref.edgeId),
|
|
1427
|
+
);
|
|
1428
|
+
const removedKeys = new Set(refs.map(fieldRemapBulkSelectionKey));
|
|
1429
|
+
let survivingMembership = normalizeFieldRemapBulkSelection(
|
|
1430
|
+
plan.edges,
|
|
1431
|
+
bulkSelection.filter(
|
|
1432
|
+
(ref) =>
|
|
1433
|
+
!removedKeys.has(fieldRemapBulkSelectionKey(ref)) && !removedEdgeIds.has(ref.edgeId),
|
|
1434
|
+
),
|
|
1435
|
+
);
|
|
1436
|
+
const primaryRef = asFieldRemapBulkSelectionRef(selection);
|
|
1437
|
+
const primaryRemoved = primaryRef
|
|
1438
|
+
? removedKeys.has(fieldRemapBulkSelectionKey(primaryRef)) ||
|
|
1439
|
+
removedEdgeIds.has(primaryRef.edgeId)
|
|
1440
|
+
: false;
|
|
1441
|
+
const preservesSingleStepFallback =
|
|
1442
|
+
refs.length === 1 &&
|
|
1443
|
+
refs[0]?.kind === 'transformStep' &&
|
|
1444
|
+
primaryRef?.kind === 'transformStep' &&
|
|
1445
|
+
fieldRemapBulkSelectionKey(refs[0]) === fieldRemapBulkSelectionKey(primaryRef);
|
|
1446
|
+
let nextPrimary = primaryRemoved ? (survivingMembership[0] ?? null) : selection;
|
|
1447
|
+
if (preservesSingleStepFallback) {
|
|
1448
|
+
const removedStep = refs[0] as Extract<
|
|
1449
|
+
FieldRemapBulkSelectionRef,
|
|
1450
|
+
{ readonly kind: 'transformStep' }
|
|
1451
|
+
>;
|
|
1452
|
+
const nextEdge = plan.edges.find((edge) => edge.id === removedStep.edgeId);
|
|
1453
|
+
nextPrimary =
|
|
1454
|
+
(nextEdge?.transformIds?.length ?? 0) > 0
|
|
1455
|
+
? {
|
|
1456
|
+
kind: 'transformStep',
|
|
1457
|
+
edgeId: removedStep.edgeId,
|
|
1458
|
+
stepIndex: Math.min(
|
|
1459
|
+
removedStep.stepIndex,
|
|
1460
|
+
(nextEdge?.transformIds?.length ?? 1) - 1,
|
|
1461
|
+
),
|
|
1462
|
+
}
|
|
1463
|
+
: nextEdge
|
|
1464
|
+
? { kind: 'edge', edgeId: nextEdge.id }
|
|
1465
|
+
: null;
|
|
1466
|
+
const fallbackRef = asFieldRemapBulkSelectionRef(nextPrimary);
|
|
1467
|
+
survivingMembership = fallbackRef
|
|
1468
|
+
? normalizeFieldRemapBulkSelection(plan.edges, [fallbackRef])
|
|
1469
|
+
: [];
|
|
1470
|
+
}
|
|
1471
|
+
const nextDomainKey = fieldRemapBulkDomainKey(listFieldRemapBulkSelectionRefs(plan.edges));
|
|
1472
|
+
const primaryKeyChanged =
|
|
1473
|
+
fieldRemapSelectionKey(nextPrimary) !== fieldRemapSelectionKey(selection);
|
|
1474
|
+
|
|
1475
|
+
pendingBulkCommitRef.current = {
|
|
1476
|
+
domainKey: nextDomainKey,
|
|
1477
|
+
membership: survivingMembership,
|
|
1478
|
+
primary: nextPrimary,
|
|
1479
|
+
};
|
|
1480
|
+
pendingBulkFocusRef.current = {
|
|
1481
|
+
domainKey: nextDomainKey,
|
|
1482
|
+
target: asFieldRemapBulkSelectionRef(nextPrimary) ?? null,
|
|
1483
|
+
};
|
|
1484
|
+
if (primaryKeyChanged) {
|
|
1485
|
+
lastPrimaryCorrectionRef.current = `${nextDomainKey}\u0001${fieldRemapSelectionKey(selection)}\u0001${fieldRemapSelectionKey(nextPrimary)}`;
|
|
1486
|
+
pendingPrimaryCorrectionAckRef.current = onSelectionChangeProp
|
|
1487
|
+
? {
|
|
1488
|
+
domainKey: nextDomainKey,
|
|
1489
|
+
selectionKey: fieldRemapSelectionKey(nextPrimary),
|
|
1490
|
+
}
|
|
1491
|
+
: undefined;
|
|
1492
|
+
}
|
|
1493
|
+
onEdgesChange(plan.edges);
|
|
1494
|
+
|
|
1495
|
+
if (primaryKeyChanged) {
|
|
1496
|
+
if (!selectionExternallyManaged) {
|
|
1497
|
+
setInternalSelection(nextPrimary);
|
|
1498
|
+
} else {
|
|
1499
|
+
onSelectionChangeProp?.(nextPrimary);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return true;
|
|
1503
|
+
},
|
|
1504
|
+
[
|
|
1505
|
+
bulkSelection,
|
|
1506
|
+
edges,
|
|
1507
|
+
onEdgesChange,
|
|
1508
|
+
onSelectionChangeProp,
|
|
1509
|
+
selection,
|
|
1510
|
+
selectionExternallyManaged,
|
|
1511
|
+
],
|
|
1512
|
+
);
|
|
1513
|
+
|
|
785
1514
|
const onEdgesDelete = useCallback(
|
|
786
1515
|
(deleted: Edge[]) => {
|
|
1516
|
+
if (readOnly) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
787
1519
|
const mappingIds = new Set(
|
|
788
1520
|
deleted
|
|
789
1521
|
.map((edge) => {
|
|
@@ -795,26 +1527,64 @@ function FieldRemapFlowCanvas({
|
|
|
795
1527
|
if (mappingIds.size === 0) {
|
|
796
1528
|
return;
|
|
797
1529
|
}
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
selection &&
|
|
801
|
-
(selection.kind === 'edge' || selection.kind === 'transformStep') &&
|
|
802
|
-
mappingIds.has(selection.edgeId)
|
|
803
|
-
) {
|
|
804
|
-
setSelection(null);
|
|
805
|
-
}
|
|
1530
|
+
commitBulkDelete([...mappingIds].map((edgeId) => ({ kind: 'edge' as const, edgeId })));
|
|
806
1531
|
},
|
|
807
|
-
[
|
|
1532
|
+
[commitBulkDelete, readOnly],
|
|
808
1533
|
);
|
|
809
1534
|
|
|
810
1535
|
const placeDraft = useCallback(
|
|
811
|
-
(transformId: string) => {
|
|
1536
|
+
(transformId: string, position?: XYPosition) => {
|
|
1537
|
+
if (readOnly) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
812
1540
|
const draft = createDraftTransform(transformId);
|
|
813
1541
|
setDrafts((current) => [...current, draft]);
|
|
1542
|
+
if (position) {
|
|
1543
|
+
setDraftPositions((current) => new Map(current).set(draft.localId, position));
|
|
1544
|
+
}
|
|
814
1545
|
setSelection({ kind: 'draft', localId: draft.localId });
|
|
815
1546
|
setPlaceTransformId(transformId);
|
|
816
1547
|
},
|
|
817
|
-
[setSelection],
|
|
1548
|
+
[readOnly, setSelection],
|
|
1549
|
+
);
|
|
1550
|
+
|
|
1551
|
+
const resolveDroppedTransformId = useCallback(
|
|
1552
|
+
(dataTransfer: DataTransfer) => {
|
|
1553
|
+
const transformId = readFieldRemapTransformDragData(dataTransfer);
|
|
1554
|
+
const definition = transformId ? transforms.get(transformId) : undefined;
|
|
1555
|
+
if (!transformId || transformId === 'identity' || definition?.id !== transformId) {
|
|
1556
|
+
return undefined;
|
|
1557
|
+
}
|
|
1558
|
+
return transformId;
|
|
1559
|
+
},
|
|
1560
|
+
[transforms],
|
|
1561
|
+
);
|
|
1562
|
+
|
|
1563
|
+
const onCanvasDragOver = useCallback(
|
|
1564
|
+
(event: DragEvent<HTMLDivElement>) => {
|
|
1565
|
+
if (readOnly || !hasFieldRemapTransformDragType(event.dataTransfer)) {
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
event.preventDefault();
|
|
1569
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
1570
|
+
},
|
|
1571
|
+
[readOnly],
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
const onCanvasDrop = useCallback(
|
|
1575
|
+
(event: DragEvent<HTMLDivElement>) => {
|
|
1576
|
+
if (readOnly) {
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
const transformId = resolveDroppedTransformId(event.dataTransfer);
|
|
1580
|
+
if (!transformId) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
event.preventDefault();
|
|
1584
|
+
event.stopPropagation();
|
|
1585
|
+
placeDraft(transformId, screenToFlowPosition({ x: event.clientX, y: event.clientY }));
|
|
1586
|
+
},
|
|
1587
|
+
[placeDraft, readOnly, resolveDroppedTransformId, screenToFlowPosition],
|
|
818
1588
|
);
|
|
819
1589
|
|
|
820
1590
|
const onNodeClick = useCallback(
|
|
@@ -825,7 +1595,7 @@ function FieldRemapFlowCanvas({
|
|
|
825
1595
|
return;
|
|
826
1596
|
}
|
|
827
1597
|
if (data.kind === 'combine-operator' || data.kind === 'split-operator') {
|
|
828
|
-
if (event.altKey && onOperatorsChange) {
|
|
1598
|
+
if (!readOnly && event.altKey && onOperatorsChange) {
|
|
829
1599
|
onOperatorsChange(removeMappingOperator(operators, data.operatorId));
|
|
830
1600
|
if (selection?.kind === 'operator' && selection.operatorId === data.operatorId) {
|
|
831
1601
|
setSelection(null);
|
|
@@ -838,41 +1608,148 @@ function FieldRemapFlowCanvas({
|
|
|
838
1608
|
if (data.kind !== 'transform') {
|
|
839
1609
|
return;
|
|
840
1610
|
}
|
|
841
|
-
|
|
842
|
-
const edge = edges.find((item) => item.id === data.mappingEdgeId);
|
|
843
|
-
if (!edge) {
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
const next = removeTransformStepFromEdge(edge, data.stepIndex);
|
|
847
|
-
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
848
|
-
setSelection(
|
|
849
|
-
(next.transformIds?.length ?? 0) > 0
|
|
850
|
-
? {
|
|
851
|
-
kind: 'transformStep',
|
|
852
|
-
edgeId: edge.id,
|
|
853
|
-
stepIndex: Math.min(data.stepIndex, (next.transformIds?.length ?? 1) - 1),
|
|
854
|
-
}
|
|
855
|
-
: { kind: 'edge', edgeId: edge.id },
|
|
856
|
-
);
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
setSelection({
|
|
1611
|
+
const target = {
|
|
860
1612
|
kind: 'transformStep',
|
|
861
1613
|
edgeId: data.mappingEdgeId,
|
|
862
1614
|
stepIndex: data.stepIndex,
|
|
863
|
-
}
|
|
1615
|
+
} as const;
|
|
1616
|
+
if (!readOnly && event.altKey) {
|
|
1617
|
+
commitBulkDelete([target]);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
applyBulkSelectionGesture(target, event);
|
|
1621
|
+
focusBulkTarget(target, 'graph');
|
|
864
1622
|
},
|
|
865
|
-
[
|
|
1623
|
+
[
|
|
1624
|
+
applyBulkSelectionGesture,
|
|
1625
|
+
commitBulkDelete,
|
|
1626
|
+
focusBulkTarget,
|
|
1627
|
+
onOperatorsChange,
|
|
1628
|
+
operators,
|
|
1629
|
+
readOnly,
|
|
1630
|
+
selection,
|
|
1631
|
+
setSelection,
|
|
1632
|
+
],
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
const onEdgeClick = useCallback(
|
|
1636
|
+
(event: MouseEvent, edge: Edge) => {
|
|
1637
|
+
const mappingEdgeId = (edge.data as FieldRemapFlowEdgeData | undefined)?.mappingEdgeId;
|
|
1638
|
+
if (!mappingEdgeId) {
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const target = { kind: 'edge', edgeId: mappingEdgeId } as const;
|
|
1642
|
+
applyBulkSelectionGesture(target, event);
|
|
1643
|
+
focusBulkTarget(target, 'graph');
|
|
1644
|
+
},
|
|
1645
|
+
[applyBulkSelectionGesture, focusBulkTarget],
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
const onBulkSelectionKeyDownCapture = useCallback(
|
|
1649
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
1650
|
+
if ((event.key !== 'Enter' && event.key !== ' ') || event.defaultPrevented || event.altKey) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const target = fieldRemapBulkRefFromKeyboardTarget(event.target);
|
|
1654
|
+
if (!target) {
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
event.preventDefault();
|
|
1658
|
+
event.stopPropagation();
|
|
1659
|
+
pendingPrimaryCorrectionAckRef.current = undefined;
|
|
1660
|
+
applyBulkSelectionGesture(target, event);
|
|
1661
|
+
},
|
|
1662
|
+
[applyBulkSelectionGesture],
|
|
866
1663
|
);
|
|
867
1664
|
|
|
868
1665
|
const onKeyDown = useCallback(
|
|
869
1666
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
870
1667
|
if (event.key === 'Escape') {
|
|
1668
|
+
if (event.defaultPrevented || (selection === null && drafts.length === 0)) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const mapper = mapperRef.current;
|
|
1672
|
+
const focused = mapper?.ownerDocument.activeElement;
|
|
1673
|
+
const detail = mapper?.querySelector<HTMLElement>(
|
|
1674
|
+
'[data-testid="field-remap-detail"], [data-testid="field-remap-convert-note"]',
|
|
1675
|
+
);
|
|
1676
|
+
const detailSeparator = mapper?.querySelector<HTMLElement>(
|
|
1677
|
+
'.workbench-field-remap-flow__canvas-detail-split > [role="separator"]',
|
|
1678
|
+
);
|
|
1679
|
+
restoreMapperFocusRef.current =
|
|
1680
|
+
detailPresentation === 'rail' &&
|
|
1681
|
+
emptyDetail === 'collapse' &&
|
|
1682
|
+
focused instanceof Element &&
|
|
1683
|
+
(detail?.contains(focused) === true ||
|
|
1684
|
+
(!previewVisible && detailSeparator?.contains(focused) === true));
|
|
1685
|
+
event.preventDefault();
|
|
1686
|
+
event.stopPropagation();
|
|
871
1687
|
setSelection(null);
|
|
872
1688
|
setDrafts([]);
|
|
1689
|
+
setDraftPositions(new Map());
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (readOnly) {
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
if (
|
|
1698
|
+
(event.key !== 'Delete' && event.key !== 'Backspace') ||
|
|
1699
|
+
event.ctrlKey ||
|
|
1700
|
+
event.metaKey ||
|
|
1701
|
+
event.altKey ||
|
|
1702
|
+
event.defaultPrevented ||
|
|
1703
|
+
isFieldRemapEditableShortcutTarget(event.target) ||
|
|
1704
|
+
selection === null
|
|
1705
|
+
) {
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
let consumed = false;
|
|
1710
|
+
if (selection.kind === 'edge' || selection.kind === 'transformStep') {
|
|
1711
|
+
const primaryKey = fieldRemapBulkSelectionKey(selection);
|
|
1712
|
+
const refs = bulkSelection.some((ref) => fieldRemapBulkSelectionKey(ref) === primaryKey)
|
|
1713
|
+
? bulkSelection
|
|
1714
|
+
: [...bulkSelection, selection];
|
|
1715
|
+
consumed = commitBulkDelete(refs);
|
|
1716
|
+
} else if (selection.kind === 'operator') {
|
|
1717
|
+
if (
|
|
1718
|
+
onOperatorsChange &&
|
|
1719
|
+
operators.some((operator) => operator.id === selection.operatorId)
|
|
1720
|
+
) {
|
|
1721
|
+
onOperatorsChange(removeMappingOperator(operators, selection.operatorId));
|
|
1722
|
+
setSelection(null);
|
|
1723
|
+
consumed = true;
|
|
1724
|
+
}
|
|
1725
|
+
} else if (selection.kind === 'draft') {
|
|
1726
|
+
if (drafts.some((draft) => draft.localId === selection.localId)) {
|
|
1727
|
+
setDrafts((current) => current.filter((draft) => draft.localId !== selection.localId));
|
|
1728
|
+
removeDraftPosition(selection.localId);
|
|
1729
|
+
setSelection(null);
|
|
1730
|
+
consumed = true;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (consumed) {
|
|
1735
|
+
event.preventDefault();
|
|
1736
|
+
event.stopPropagation();
|
|
873
1737
|
}
|
|
874
1738
|
},
|
|
875
|
-
[
|
|
1739
|
+
[
|
|
1740
|
+
drafts,
|
|
1741
|
+
bulkSelection,
|
|
1742
|
+
commitBulkDelete,
|
|
1743
|
+
detailPresentation,
|
|
1744
|
+
emptyDetail,
|
|
1745
|
+
onOperatorsChange,
|
|
1746
|
+
operators,
|
|
1747
|
+
previewVisible,
|
|
1748
|
+
readOnly,
|
|
1749
|
+
removeDraftPosition,
|
|
1750
|
+
selection,
|
|
1751
|
+
setSelection,
|
|
1752
|
+
],
|
|
876
1753
|
);
|
|
877
1754
|
|
|
878
1755
|
const handlePaneContextMenu = useCallback(
|
|
@@ -896,6 +1773,32 @@ function FieldRemapFlowCanvas({
|
|
|
896
1773
|
[onEdgeContextMenu, selection],
|
|
897
1774
|
);
|
|
898
1775
|
|
|
1776
|
+
const detailPanel = detailVisible ? (
|
|
1777
|
+
<FieldRemapDetailPanel
|
|
1778
|
+
selection={selection}
|
|
1779
|
+
edges={edges}
|
|
1780
|
+
sources={sources}
|
|
1781
|
+
targets={targets}
|
|
1782
|
+
transforms={transforms}
|
|
1783
|
+
readOnly={readOnly}
|
|
1784
|
+
onEdgesChange={onEdgesChange}
|
|
1785
|
+
onSelectionChange={setSelection}
|
|
1786
|
+
drafts={drafts}
|
|
1787
|
+
onDiscardDraft={(localId) => {
|
|
1788
|
+
setDrafts((current) => current.filter((item) => item.localId !== localId));
|
|
1789
|
+
removeDraftPosition(localId);
|
|
1790
|
+
}}
|
|
1791
|
+
operators={operators}
|
|
1792
|
+
onOperatorsChange={onOperatorsChange}
|
|
1793
|
+
emptyDetailTitle={
|
|
1794
|
+
readOnly ? chromeLabels.readOnlyEmptyDetailTitle : chromeLabels.emptyDetailTitle
|
|
1795
|
+
}
|
|
1796
|
+
emptyDetailDescription={
|
|
1797
|
+
readOnly ? chromeLabels.readOnlyEmptyDetailDescription : chromeLabels.emptyDetailDescription
|
|
1798
|
+
}
|
|
1799
|
+
/>
|
|
1800
|
+
) : null;
|
|
1801
|
+
|
|
899
1802
|
return (
|
|
900
1803
|
<div
|
|
901
1804
|
ref={mapperRef}
|
|
@@ -904,11 +1807,15 @@ function FieldRemapFlowCanvas({
|
|
|
904
1807
|
data-chrome={chrome}
|
|
905
1808
|
data-flow-hint={showFlowHint ? 'on' : 'off'}
|
|
906
1809
|
data-bindings-list={showBindingsList ? 'on' : 'off'}
|
|
907
|
-
data-convert-palette={
|
|
1810
|
+
data-convert-palette={showAuthoringPalette ? 'on' : 'off'}
|
|
1811
|
+
data-read-only={readOnly ? 'true' : 'false'}
|
|
908
1812
|
data-empty-detail={emptyDetail}
|
|
1813
|
+
data-detail-presentation={detailPresentation}
|
|
909
1814
|
data-minimap={showMinimap ? 'on' : 'off'}
|
|
910
1815
|
data-hidden-fields={includeHidden ? 'on' : 'off'}
|
|
911
1816
|
data-preview={previewVisible ? 'on' : 'off'}
|
|
1817
|
+
tabIndex={-1}
|
|
1818
|
+
onKeyDownCapture={onBulkSelectionKeyDownCapture}
|
|
912
1819
|
onKeyDown={onKeyDown}
|
|
913
1820
|
>
|
|
914
1821
|
{showFlowHint ? (
|
|
@@ -920,10 +1827,25 @@ function FieldRemapFlowCanvas({
|
|
|
920
1827
|
</p>
|
|
921
1828
|
) : null}
|
|
922
1829
|
|
|
1830
|
+
{connectionFeedback ? (
|
|
1831
|
+
<p className="workbench-field-remap-demo__warn" role="status">
|
|
1832
|
+
{connectionFeedback.reason}
|
|
1833
|
+
</p>
|
|
1834
|
+
) : null}
|
|
1835
|
+
|
|
1836
|
+
{conflicts.length > 0 ? (
|
|
1837
|
+
<p className="workbench-field-remap-demo__warn" role="status">
|
|
1838
|
+
Warning: parent and child fields are both mapped (
|
|
1839
|
+
{conflicts.map((item) => `${item.parentId} / ${item.childId}`).join('; ')}). Prefer one
|
|
1840
|
+
level.
|
|
1841
|
+
</p>
|
|
1842
|
+
) : null}
|
|
1843
|
+
|
|
923
1844
|
<FieldRemapSplitWorkspace
|
|
924
1845
|
layout={workspaceLayout}
|
|
925
|
-
|
|
926
|
-
|
|
1846
|
+
reserveHiddenDetailSplit={detailPresentation === 'rail'}
|
|
1847
|
+
showConvertPalette={showAuthoringPalette}
|
|
1848
|
+
showDetail={sideRailVisible}
|
|
927
1849
|
surface={
|
|
928
1850
|
selection?.kind === 'transformStep'
|
|
929
1851
|
? 'convert-note'
|
|
@@ -935,7 +1857,7 @@ function FieldRemapFlowCanvas({
|
|
|
935
1857
|
}
|
|
936
1858
|
>
|
|
937
1859
|
<>
|
|
938
|
-
{
|
|
1860
|
+
{showAuthoringPalette ? (
|
|
939
1861
|
<FieldRemapConvertPalette
|
|
940
1862
|
transforms={transforms}
|
|
941
1863
|
selectedTransformId={placeTransformId}
|
|
@@ -964,78 +1886,61 @@ function FieldRemapFlowCanvas({
|
|
|
964
1886
|
) : null}
|
|
965
1887
|
</>
|
|
966
1888
|
|
|
967
|
-
<div
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
{
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1889
|
+
<div
|
|
1890
|
+
ref={canvasRef}
|
|
1891
|
+
className="workbench-field-remap-flow__canvas"
|
|
1892
|
+
data-testid="field-remap-flow"
|
|
1893
|
+
>
|
|
1894
|
+
{hasPositiveCanvasSize ? (
|
|
1895
|
+
<ReactFlow
|
|
1896
|
+
nodes={nodes}
|
|
1897
|
+
edges={flowEdges}
|
|
1898
|
+
nodeTypes={nodeTypes}
|
|
1899
|
+
edgeTypes={edgeTypes}
|
|
1900
|
+
onNodesChange={onProjectedNodesChange}
|
|
1901
|
+
onEdgesChange={onProjectedFlowEdgesChange}
|
|
1902
|
+
onConnect={readOnly ? undefined : onConnect}
|
|
1903
|
+
onConnectStart={readOnly ? undefined : onConnectStart}
|
|
1904
|
+
onConnectEnd={readOnly ? undefined : onConnectEnd}
|
|
1905
|
+
onEdgesDelete={readOnly ? undefined : onEdgesDelete}
|
|
1906
|
+
onDragOver={onCanvasDragOver}
|
|
1907
|
+
onDrop={onCanvasDrop}
|
|
1908
|
+
onNodeClick={onNodeClick}
|
|
1909
|
+
onEdgeClick={onEdgeClick}
|
|
1910
|
+
onPaneContextMenu={onPaneContextMenu ? handlePaneContextMenu : undefined}
|
|
1911
|
+
onNodeContextMenu={onNodeContextMenu ? handleNodeContextMenu : undefined}
|
|
1912
|
+
onEdgeContextMenu={onEdgeContextMenu ? handleEdgeContextMenu : undefined}
|
|
1913
|
+
isValidConnection={isValidConnection}
|
|
1914
|
+
nodesDraggable={!readOnly}
|
|
1915
|
+
nodesConnectable={!readOnly}
|
|
1916
|
+
edgesReconnectable={!readOnly}
|
|
1917
|
+
elementsSelectable={false}
|
|
1918
|
+
ariaLabelConfig={flowAriaLabelConfig}
|
|
1919
|
+
deleteKeyCode={null}
|
|
1920
|
+
fitView
|
|
1921
|
+
fitViewOptions={DEFAULT_FIT_VIEW_OPTIONS}
|
|
1922
|
+
proOptions={{ hideAttribution: true }}
|
|
1923
|
+
>
|
|
1924
|
+
<FieldRemapFlowActionsBridge
|
|
1925
|
+
canFitView={canFitView}
|
|
1926
|
+
flowActionsRef={flowActionsRef}
|
|
1927
|
+
/>
|
|
1928
|
+
<Background gap={16} color="var(--xy-background-pattern-color)" />
|
|
1929
|
+
<Controls showInteractive={false} fitViewOptions={DEFAULT_FIT_VIEW_OPTIONS}>
|
|
1930
|
+
{onShowMinimapChange ? (
|
|
1931
|
+
<ControlButton
|
|
1932
|
+
aria-label={showMinimap ? chromeLabels.hideMinimap : chromeLabels.showMinimap}
|
|
1933
|
+
className={
|
|
1934
|
+
showMinimap
|
|
1935
|
+
? 'workbench-field-remap-flow__minimap-toggle is-active'
|
|
1936
|
+
: 'workbench-field-remap-flow__minimap-toggle'
|
|
1937
|
+
}
|
|
1938
|
+
data-testid="field-remap-toggle-minimap"
|
|
1939
|
+
title={showMinimap ? chromeLabels.hideMinimap : chromeLabels.showMinimap}
|
|
1940
|
+
onClick={() => {
|
|
1941
|
+
onShowMinimapChange(!showMinimap);
|
|
1942
|
+
}}
|
|
1012
1943
|
>
|
|
1013
|
-
<path d="M3 6.5 9 4l6 2.5L21 4v13.5L15 20l-6-2.5L3 20z" />
|
|
1014
|
-
<path d="M9 4v13.5" />
|
|
1015
|
-
<path d="M15 6.5V20" />
|
|
1016
|
-
</svg>
|
|
1017
|
-
</ControlButton>
|
|
1018
|
-
) : null}
|
|
1019
|
-
{onIncludeHiddenChange ? (
|
|
1020
|
-
<ControlButton
|
|
1021
|
-
aria-label={
|
|
1022
|
-
includeHidden ? chromeLabels.hideHiddenFields : chromeLabels.showHiddenFields
|
|
1023
|
-
}
|
|
1024
|
-
aria-pressed={includeHidden}
|
|
1025
|
-
className={
|
|
1026
|
-
includeHidden
|
|
1027
|
-
? 'workbench-field-remap-flow__hidden-toggle is-active'
|
|
1028
|
-
: 'workbench-field-remap-flow__hidden-toggle'
|
|
1029
|
-
}
|
|
1030
|
-
data-testid="field-remap-toggle-hidden-fields"
|
|
1031
|
-
title={
|
|
1032
|
-
includeHidden ? chromeLabels.hideHiddenFields : chromeLabels.showHiddenFields
|
|
1033
|
-
}
|
|
1034
|
-
onClick={() => {
|
|
1035
|
-
onIncludeHiddenChange(!includeHidden);
|
|
1036
|
-
}}
|
|
1037
|
-
>
|
|
1038
|
-
{includeHidden ? (
|
|
1039
1944
|
<svg
|
|
1040
1945
|
aria-hidden="true"
|
|
1041
1946
|
fill="none"
|
|
@@ -1047,72 +1952,96 @@ function FieldRemapFlowCanvas({
|
|
|
1047
1952
|
viewBox="0 0 24 24"
|
|
1048
1953
|
width="16"
|
|
1049
1954
|
>
|
|
1050
|
-
<path d="
|
|
1051
|
-
<
|
|
1955
|
+
<path d="M3 6.5 9 4l6 2.5L21 4v13.5L15 20l-6-2.5L3 20z" />
|
|
1956
|
+
<path d="M9 4v13.5" />
|
|
1957
|
+
<path d="M15 6.5V20" />
|
|
1052
1958
|
</svg>
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1959
|
+
</ControlButton>
|
|
1960
|
+
) : null}
|
|
1961
|
+
{onIncludeHiddenChange ? (
|
|
1962
|
+
<ControlButton
|
|
1963
|
+
aria-label={
|
|
1964
|
+
includeHidden ? chromeLabels.hideHiddenFields : chromeLabels.showHiddenFields
|
|
1965
|
+
}
|
|
1966
|
+
aria-pressed={includeHidden}
|
|
1967
|
+
className={
|
|
1968
|
+
includeHidden
|
|
1969
|
+
? 'workbench-field-remap-flow__hidden-toggle is-active'
|
|
1970
|
+
: 'workbench-field-remap-flow__hidden-toggle'
|
|
1971
|
+
}
|
|
1972
|
+
data-testid="field-remap-toggle-hidden-fields"
|
|
1973
|
+
title={
|
|
1974
|
+
includeHidden ? chromeLabels.hideHiddenFields : chromeLabels.showHiddenFields
|
|
1975
|
+
}
|
|
1976
|
+
onClick={() => {
|
|
1977
|
+
onIncludeHiddenChange(!includeHidden);
|
|
1978
|
+
}}
|
|
1979
|
+
>
|
|
1980
|
+
{includeHidden ? (
|
|
1981
|
+
<svg
|
|
1982
|
+
aria-hidden="true"
|
|
1983
|
+
fill="none"
|
|
1984
|
+
height="16"
|
|
1985
|
+
stroke="currentColor"
|
|
1986
|
+
strokeLinecap="round"
|
|
1987
|
+
strokeLinejoin="round"
|
|
1988
|
+
strokeWidth="1.75"
|
|
1989
|
+
viewBox="0 0 24 24"
|
|
1990
|
+
width="16"
|
|
1991
|
+
>
|
|
1992
|
+
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
|
|
1993
|
+
<circle cx="12" cy="12" r="3" />
|
|
1994
|
+
</svg>
|
|
1995
|
+
) : (
|
|
1996
|
+
<svg
|
|
1997
|
+
aria-hidden="true"
|
|
1998
|
+
fill="none"
|
|
1999
|
+
height="16"
|
|
2000
|
+
stroke="currentColor"
|
|
2001
|
+
strokeLinecap="round"
|
|
2002
|
+
strokeLinejoin="round"
|
|
2003
|
+
strokeWidth="1.75"
|
|
2004
|
+
viewBox="0 0 24 24"
|
|
2005
|
+
width="16"
|
|
2006
|
+
>
|
|
2007
|
+
<path d="M3 3l18 18" />
|
|
2008
|
+
<path d="M10.6 10.6a3 3 0 0 0 4.2 4.2" />
|
|
2009
|
+
<path d="M9.9 5.1A10.6 10.6 0 0 1 12 5c6.5 0 10 7 10 7a17.4 17.4 0 0 1-3.2 4.4" />
|
|
2010
|
+
<path d="M6.1 6.1C3.9 7.7 2 12 2 12s3.5 7 10 7a10.4 10.4 0 0 0 4.2-.9" />
|
|
2011
|
+
</svg>
|
|
2012
|
+
)}
|
|
2013
|
+
</ControlButton>
|
|
2014
|
+
) : null}
|
|
2015
|
+
</Controls>
|
|
2016
|
+
{showMinimap ? (
|
|
2017
|
+
<MiniMap
|
|
2018
|
+
pannable
|
|
2019
|
+
zoomable
|
|
2020
|
+
bgColor="var(--xy-minimap-background-color)"
|
|
2021
|
+
maskColor="var(--xy-minimap-mask-background-color)"
|
|
2022
|
+
nodeColor={(node) => {
|
|
2023
|
+
const kind = (node.data as FieldRemapFlowNodeData | undefined)?.kind;
|
|
2024
|
+
if (kind === 'source-object') {
|
|
2025
|
+
return 'var(--vscode-charts-blue, #3794ff)';
|
|
2026
|
+
}
|
|
2027
|
+
if (kind === 'target-object') {
|
|
2028
|
+
return 'var(--vscode-charts-green, #89d185)';
|
|
2029
|
+
}
|
|
2030
|
+
return 'var(--vscode-focusBorder, var(--color-accent, #3794ff))';
|
|
2031
|
+
}}
|
|
2032
|
+
nodeStrokeColor="var(--xy-minimap-node-stroke-color)"
|
|
2033
|
+
/>
|
|
1072
2034
|
) : null}
|
|
1073
|
-
</
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
maskColor="var(--xy-minimap-mask-background-color)"
|
|
1080
|
-
nodeColor={(node) => {
|
|
1081
|
-
const kind = (node.data as FieldRemapFlowNodeData | undefined)?.kind;
|
|
1082
|
-
if (kind === 'source-object') {
|
|
1083
|
-
return 'var(--vscode-charts-blue, #3794ff)';
|
|
1084
|
-
}
|
|
1085
|
-
if (kind === 'target-object') {
|
|
1086
|
-
return 'var(--vscode-charts-green, #89d185)';
|
|
1087
|
-
}
|
|
1088
|
-
return 'var(--vscode-focusBorder, var(--color-accent, #3794ff))';
|
|
1089
|
-
}}
|
|
1090
|
-
nodeStrokeColor="var(--xy-minimap-node-stroke-color)"
|
|
1091
|
-
/>
|
|
1092
|
-
) : null}
|
|
1093
|
-
</ReactFlow>
|
|
2035
|
+
</ReactFlow>
|
|
2036
|
+
) : (
|
|
2037
|
+
<p className="workbench-field-remap-demo__warn" role="status">
|
|
2038
|
+
Mapping canvas is waiting for available space.
|
|
2039
|
+
</p>
|
|
2040
|
+
)}
|
|
1094
2041
|
</div>
|
|
1095
2042
|
|
|
1096
2043
|
<div className="workbench-field-remap-flow__side-rail">
|
|
1097
|
-
{
|
|
1098
|
-
<FieldRemapDetailPanel
|
|
1099
|
-
selection={selection}
|
|
1100
|
-
edges={edges}
|
|
1101
|
-
sources={sources}
|
|
1102
|
-
targets={targets}
|
|
1103
|
-
transforms={transforms}
|
|
1104
|
-
onEdgesChange={onEdgesChange}
|
|
1105
|
-
onSelectionChange={setSelection}
|
|
1106
|
-
drafts={drafts}
|
|
1107
|
-
onDiscardDraft={(localId) => {
|
|
1108
|
-
setDrafts((current) => current.filter((item) => item.localId !== localId));
|
|
1109
|
-
}}
|
|
1110
|
-
operators={operators}
|
|
1111
|
-
onOperatorsChange={onOperatorsChange}
|
|
1112
|
-
emptyDetailTitle={chromeLabels.emptyDetailTitle}
|
|
1113
|
-
emptyDetailDescription={chromeLabels.emptyDetailDescription}
|
|
1114
|
-
/>
|
|
1115
|
-
) : null}
|
|
2044
|
+
{detailPresentation === 'rail' ? detailPanel : null}
|
|
1116
2045
|
{previewVisible && preview ? (
|
|
1117
2046
|
<FieldRemapPreviewRail
|
|
1118
2047
|
preview={preview}
|
|
@@ -1129,6 +2058,18 @@ function FieldRemapFlowCanvas({
|
|
|
1129
2058
|
</div>
|
|
1130
2059
|
</FieldRemapSplitWorkspace>
|
|
1131
2060
|
|
|
2061
|
+
{detailPresentation === 'modal' && selection !== null ? (
|
|
2062
|
+
<Suspense fallback={null}>
|
|
2063
|
+
<FieldRemapModalDetail
|
|
2064
|
+
closeLabel={chromeLabels.closeDetailModal ?? 'Close details'}
|
|
2065
|
+
title={chromeLabels.detailModalTitle ?? 'Mapping details'}
|
|
2066
|
+
onClose={closeModalDetail}
|
|
2067
|
+
>
|
|
2068
|
+
{detailPanel}
|
|
2069
|
+
</FieldRemapModalDetail>
|
|
2070
|
+
</Suspense>
|
|
2071
|
+
) : null}
|
|
2072
|
+
|
|
1132
2073
|
{showBindingsList ? (
|
|
1133
2074
|
<div className="workbench-field-remap-flow__bindings" data-testid="field-remap-edges">
|
|
1134
2075
|
<h4>{chromeLabels.bindingsTitle}</h4>
|
|
@@ -1145,21 +2086,36 @@ function FieldRemapFlowCanvas({
|
|
|
1145
2086
|
});
|
|
1146
2087
|
const defaultAddId = appendCatalog[0]?.id;
|
|
1147
2088
|
const listContext = canEditListContext(edge, sources, targets);
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
2089
|
+
const edgeRef = { kind: 'edge', edgeId: edge.id } as const;
|
|
2090
|
+
const edgeKey = fieldRemapBulkSelectionKey(edgeRef);
|
|
2091
|
+
const edgeSelected = bulkSelectionKeys.has(edgeKey);
|
|
2092
|
+
const visibleRemoveRefs = edgeSelected ? bulkSelection : [edgeRef];
|
|
2093
|
+
const laneSelected = canonicalBulkRefs.some(
|
|
2094
|
+
(ref) =>
|
|
2095
|
+
ref.edgeId === edge.id && bulkSelectionKeys.has(fieldRemapBulkSelectionKey(ref)),
|
|
2096
|
+
);
|
|
2097
|
+
const edgeIsPrimary = selection?.kind === 'edge' && selection.edgeId === edge.id;
|
|
1151
2098
|
|
|
1152
2099
|
return (
|
|
1153
2100
|
<li
|
|
1154
2101
|
key={edge.id}
|
|
1155
|
-
className={
|
|
2102
|
+
className={laneSelected ? 'is-selected' : undefined}
|
|
1156
2103
|
data-testid={`field-remap-lane-${edge.id}`}
|
|
1157
2104
|
>
|
|
1158
2105
|
<button
|
|
2106
|
+
ref={(element) => registerBulkFocusTarget(edgeKey, 'list', element)}
|
|
1159
2107
|
type="button"
|
|
1160
|
-
|
|
2108
|
+
aria-pressed={edgeSelected}
|
|
2109
|
+
className={
|
|
2110
|
+
edgeSelected
|
|
2111
|
+
? 'workbench-field-remap-flow__binding-select is-selected'
|
|
2112
|
+
: 'workbench-field-remap-flow__binding-select'
|
|
2113
|
+
}
|
|
2114
|
+
data-field-remap-bulk-edge-id={edge.id}
|
|
2115
|
+
data-field-remap-bulk-kind="edge"
|
|
2116
|
+
data-primary={edgeIsPrimary ? 'true' : 'false'}
|
|
1161
2117
|
data-testid={`field-remap-select-edge-${edge.id}`}
|
|
1162
|
-
onClick={() =>
|
|
2118
|
+
onClick={(event) => applyBulkSelectionGesture(edgeRef, event)}
|
|
1163
2119
|
>
|
|
1164
2120
|
<code>
|
|
1165
2121
|
{edge.sourceFieldId} →{' '}
|
|
@@ -1170,70 +2126,69 @@ function FieldRemapFlowCanvas({
|
|
|
1170
2126
|
{edge.itemEdges ? ` · ${edge.itemEdges.length} item fields` : ''}
|
|
1171
2127
|
</code>
|
|
1172
2128
|
</button>
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
<
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
2129
|
+
{!readOnly ? (
|
|
2130
|
+
<span className="workbench-field-remap-mapper__edge-actions">
|
|
2131
|
+
{(edge.transformIds?.length ?? 0) < MAX_TRANSFORM_CHAIN && defaultAddId ? (
|
|
2132
|
+
<IconButton
|
|
2133
|
+
compact
|
|
2134
|
+
type="button"
|
|
2135
|
+
data-testid={`field-remap-add-node-${edge.id}`}
|
|
2136
|
+
icon="codicon-add"
|
|
2137
|
+
label={chromeLabels.addTransform}
|
|
2138
|
+
onClick={() => {
|
|
2139
|
+
const next = addTransformStepToEdge(edge, defaultAddId, {
|
|
2140
|
+
registry: transforms,
|
|
2141
|
+
sourceType: portTypes.sourceType,
|
|
2142
|
+
targetType: portTypes.targetType,
|
|
2143
|
+
});
|
|
2144
|
+
if (!next) {
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
2148
|
+
setSelection({
|
|
2149
|
+
kind: 'transformStep',
|
|
2150
|
+
edgeId: edge.id,
|
|
2151
|
+
stepIndex: (next.transformIds?.length ?? 1) - 1,
|
|
2152
|
+
});
|
|
2153
|
+
}}
|
|
2154
|
+
/>
|
|
2155
|
+
) : null}
|
|
2156
|
+
{listContext ? (
|
|
2157
|
+
<IconButton
|
|
2158
|
+
compact
|
|
2159
|
+
type="button"
|
|
2160
|
+
data-testid={`field-remap-edit-items-${edge.id}`}
|
|
2161
|
+
icon="codicon-edit"
|
|
2162
|
+
label={chromeLabels.editItems}
|
|
2163
|
+
onClick={() => {
|
|
2164
|
+
if (!edge.itemEdges) {
|
|
2165
|
+
onEdgesChange(
|
|
2166
|
+
edges.map((item) =>
|
|
2167
|
+
item.id === edge.id ? enableListContextOnEdge(item) : item,
|
|
2168
|
+
),
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
setSelection({ kind: 'edge', edgeId: edge.id });
|
|
2172
|
+
}}
|
|
2173
|
+
/>
|
|
2174
|
+
) : null}
|
|
1200
2175
|
<IconButton
|
|
1201
2176
|
compact
|
|
1202
2177
|
type="button"
|
|
1203
|
-
data-testid={`field-remap-
|
|
1204
|
-
icon="codicon-
|
|
1205
|
-
label={
|
|
2178
|
+
data-testid={`field-remap-remove-edge-${edge.id}`}
|
|
2179
|
+
icon="codicon-trash"
|
|
2180
|
+
label={
|
|
2181
|
+
visibleRemoveRefs.length > 1
|
|
2182
|
+
? `Remove ${visibleRemoveRefs.length} selected items`
|
|
2183
|
+
: chromeLabels.removeBinding
|
|
2184
|
+
}
|
|
2185
|
+
variant="danger"
|
|
1206
2186
|
onClick={() => {
|
|
1207
|
-
|
|
1208
|
-
onEdgesChange(
|
|
1209
|
-
edges.map((item) =>
|
|
1210
|
-
item.id === edge.id ? enableListContextOnEdge(item) : item,
|
|
1211
|
-
),
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
setSelection({ kind: 'edge', edgeId: edge.id });
|
|
2187
|
+
commitBulkDelete(visibleRemoveRefs);
|
|
1215
2188
|
}}
|
|
1216
2189
|
/>
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
compact
|
|
1220
|
-
type="button"
|
|
1221
|
-
data-testid={`field-remap-remove-edge-${edge.id}`}
|
|
1222
|
-
icon="codicon-trash"
|
|
1223
|
-
label={chromeLabels.removeBinding}
|
|
1224
|
-
variant="danger"
|
|
1225
|
-
onClick={() => {
|
|
1226
|
-
onEdgesChange(edges.filter((item) => item.id !== edge.id));
|
|
1227
|
-
if (
|
|
1228
|
-
selection &&
|
|
1229
|
-
(selection.kind === 'edge' || selection.kind === 'transformStep') &&
|
|
1230
|
-
selection.edgeId === edge.id
|
|
1231
|
-
) {
|
|
1232
|
-
setSelection(null);
|
|
1233
|
-
}
|
|
1234
|
-
}}
|
|
1235
|
-
/>
|
|
1236
|
-
</span>
|
|
2190
|
+
</span>
|
|
2191
|
+
) : null}
|
|
1237
2192
|
</li>
|
|
1238
2193
|
);
|
|
1239
2194
|
})}
|