@workbench-kit/shell-react 0.0.2-prototype.0.2.41 → 0.0.2-prototype.0.2.43
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 +42 -2
- package/package.json +13 -10
- 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 +18 -0
- package/src/field-remap/convert-note-editor.tsx +30 -24
- package/src/field-remap/convert-palette.tsx +6 -0
- package/src/field-remap/demo.tsx +8 -2
- package/src/field-remap/detail-panel.tsx +225 -192
- 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 +1055 -162
- 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 +50 -14
- package/src/field-remap/transform-options-editor.tsx +5 -1
- package/src/field-remap/view.css +61 -42
- package/src/index.ts +7 -0
- package/src/management/keybinding-overrides-storage.ts +48 -23
- package/src/management/keybinding-settings.tsx +10 -1
- package/src/management/use-keybinding-management.ts +69 -19
- 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 +147 -18
- 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;
|
|
@@ -327,6 +403,16 @@ export interface FieldRemapFlowMapperProps {
|
|
|
327
403
|
readonly edges: readonly MappingEdge[];
|
|
328
404
|
readonly transforms: ValueTransformRegistry;
|
|
329
405
|
readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
|
|
406
|
+
/** Existing target/donor edges are replaced by default; `reject` preserves them unchanged. */
|
|
407
|
+
readonly rewirePolicy?: 'replace' | 'reject' | undefined;
|
|
408
|
+
/** Structured completed-attempt feedback; hover validation never invokes this callback. */
|
|
409
|
+
readonly onConnectionFeedback?:
|
|
410
|
+
((feedback: FieldRemapConnectionFeedback | null) => void) | undefined;
|
|
411
|
+
/**
|
|
412
|
+
* Authoritative parent/child conflict projection. `undefined` derives from supplied Flow inputs;
|
|
413
|
+
* an explicit empty array suppresses fallback derivation.
|
|
414
|
+
*/
|
|
415
|
+
readonly parentChildConflicts?: readonly MappingConflict[] | undefined;
|
|
330
416
|
/**
|
|
331
417
|
* `card` preserves the demo chrome. `embed` omits the flow hint and binding list;
|
|
332
418
|
* explicit `show*` props below take precedence.
|
|
@@ -334,6 +420,8 @@ export interface FieldRemapFlowMapperProps {
|
|
|
334
420
|
readonly chrome?: 'card' | 'embed' | undefined;
|
|
335
421
|
/** Show the empty-selection detail hint, or collapse that rail until a selection exists. */
|
|
336
422
|
readonly emptyDetail?: 'hint' | 'collapse' | undefined;
|
|
423
|
+
/** Render selection detail in the resizable rail (default) or the shared workbench Modal. */
|
|
424
|
+
readonly detailPresentation?: 'rail' | 'modal' | undefined;
|
|
337
425
|
/** Show the convert-first hint. Defaults to true for `card`, false for `embed`. */
|
|
338
426
|
readonly showFlowHint?: boolean | undefined;
|
|
339
427
|
/** Show the bottom binding list. Defaults to true for `card`, false for `embed`. */
|
|
@@ -343,6 +431,11 @@ export interface FieldRemapFlowMapperProps {
|
|
|
343
431
|
* the workspace expands without leaving an empty grid track.
|
|
344
432
|
*/
|
|
345
433
|
readonly showConvertPalette?: boolean | undefined;
|
|
434
|
+
/**
|
|
435
|
+
* View-only authoring guard. Existing mappings remain inspectable, while durable mutations and
|
|
436
|
+
* mapper-local drafts are disabled. This is not an authorization boundary.
|
|
437
|
+
*/
|
|
438
|
+
readonly readOnly?: boolean | undefined;
|
|
346
439
|
/** Document v2 n→m operators (display + authoring when onOperatorsChange is set). */
|
|
347
440
|
readonly operators?: readonly MappingOperator[] | undefined;
|
|
348
441
|
readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
|
|
@@ -404,9 +497,72 @@ export interface FieldRemapFlowMapperProps {
|
|
|
404
497
|
readonly t?: FieldRemapTranslate | undefined;
|
|
405
498
|
}
|
|
406
499
|
|
|
500
|
+
export type FieldRemapConnectionFeedbackReason =
|
|
501
|
+
FieldRemapFlowConnectionRejectionReason | 'rewire-policy-rejected';
|
|
502
|
+
|
|
503
|
+
export interface FieldRemapConnectionFeedback {
|
|
504
|
+
readonly reason: FieldRemapConnectionFeedbackReason;
|
|
505
|
+
readonly impactedEdgeIds?: readonly string[];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function areFieldRemapBulkSelectionsEqual(
|
|
509
|
+
left: readonly FieldRemapBulkSelectionRef[],
|
|
510
|
+
right: readonly FieldRemapBulkSelectionRef[],
|
|
511
|
+
): boolean {
|
|
512
|
+
return (
|
|
513
|
+
left.length === right.length &&
|
|
514
|
+
left.every(
|
|
515
|
+
(ref, index) => fieldRemapBulkSelectionKey(ref) === fieldRemapBulkSelectionKey(right[index]!),
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function fieldRemapBulkDomainKey(refs: readonly FieldRemapBulkSelectionRef[]): string {
|
|
521
|
+
return refs
|
|
522
|
+
.map(fieldRemapBulkSelectionKey)
|
|
523
|
+
.map((key) => `${key.length}:${key}`)
|
|
524
|
+
.join('');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function fieldRemapBulkRefFromKeyboardTarget(
|
|
528
|
+
target: EventTarget | null,
|
|
529
|
+
): FieldRemapBulkSelectionRef | undefined {
|
|
530
|
+
if (!(target instanceof Element)) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const explicit = target.closest<HTMLElement>('[data-field-remap-bulk-kind]');
|
|
535
|
+
const explicitKind = explicit?.dataset.fieldRemapBulkKind;
|
|
536
|
+
const explicitEdgeId = explicit?.dataset.fieldRemapBulkEdgeId;
|
|
537
|
+
if (explicitKind === 'edge' && explicitEdgeId) {
|
|
538
|
+
return { kind: 'edge', edgeId: explicitEdgeId };
|
|
539
|
+
}
|
|
540
|
+
if (explicitKind === 'transformStep' && explicitEdgeId) {
|
|
541
|
+
const stepIndex = Number(explicit.dataset.fieldRemapBulkStepIndex);
|
|
542
|
+
if (Number.isInteger(stepIndex) && stepIndex >= 0) {
|
|
543
|
+
return { kind: 'transformStep', edgeId: explicitEdgeId, stepIndex };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function connectionFromFinalState(state: FinalConnectionState): Connection | null {
|
|
550
|
+
if (!state.fromNode || !state.fromHandle || !state.toNode || !state.toHandle) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const reversed = state.fromHandle.type === 'target';
|
|
554
|
+
return {
|
|
555
|
+
source: (reversed ? state.toNode : state.fromNode).id,
|
|
556
|
+
sourceHandle: (reversed ? state.toHandle : state.fromHandle).id ?? null,
|
|
557
|
+
target: (reversed ? state.fromNode : state.toNode).id,
|
|
558
|
+
targetHandle: (reversed ? state.fromHandle : state.toHandle).id ?? null,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
407
562
|
interface FieldRemapSplitWorkspaceProps {
|
|
408
563
|
readonly children: ReactNode;
|
|
409
564
|
readonly layout: 'wide' | 'medium' | 'narrow';
|
|
565
|
+
readonly reserveHiddenDetailSplit: boolean;
|
|
410
566
|
readonly showConvertPalette: boolean;
|
|
411
567
|
readonly showDetail: boolean;
|
|
412
568
|
readonly surface: 'binding' | 'convert-note' | 'draft-convert' | 'operator';
|
|
@@ -420,6 +576,7 @@ interface FieldRemapSplitWorkspaceProps {
|
|
|
420
576
|
function FieldRemapSplitWorkspace({
|
|
421
577
|
children,
|
|
422
578
|
layout,
|
|
579
|
+
reserveHiddenDetailSplit,
|
|
423
580
|
showConvertPalette,
|
|
424
581
|
showDetail,
|
|
425
582
|
surface,
|
|
@@ -438,26 +595,29 @@ function FieldRemapSplitWorkspace({
|
|
|
438
595
|
});
|
|
439
596
|
const paletteSizePx = paletteSizeByLayout[layout];
|
|
440
597
|
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
|
-
|
|
598
|
+
const canvasWithDetail =
|
|
599
|
+
showDetail || reserveHiddenDetailSplit ? (
|
|
600
|
+
<SplitView
|
|
601
|
+
className={
|
|
602
|
+
showDetail
|
|
603
|
+
? 'workbench-field-remap-flow__canvas-detail-split'
|
|
604
|
+
: 'workbench-field-remap-flow__canvas-detail-split ui-workbench-split-view--secondary-collapsed'
|
|
605
|
+
}
|
|
606
|
+
layoutMode="secondary-fixed"
|
|
607
|
+
maxSecondarySizePx={isNarrow ? 320 : 480}
|
|
608
|
+
minPrimarySizePx={isNarrow ? 200 : 280}
|
|
609
|
+
minSecondarySizePx={isNarrow ? 160 : 256}
|
|
610
|
+
onSecondarySizePxChange={(nextSize) => {
|
|
611
|
+
setDetailSizeByLayout((current) => ({ ...current, [layout]: nextSize }));
|
|
612
|
+
}}
|
|
613
|
+
orientation={isNarrow ? 'vertical' : 'horizontal'}
|
|
614
|
+
primary={canvas}
|
|
615
|
+
secondary={detail}
|
|
616
|
+
secondarySizePx={detailSizePx}
|
|
617
|
+
/>
|
|
618
|
+
) : (
|
|
619
|
+
canvas
|
|
620
|
+
);
|
|
461
621
|
|
|
462
622
|
const content = (
|
|
463
623
|
<SplitView
|
|
@@ -502,11 +662,16 @@ function FieldRemapFlowCanvas({
|
|
|
502
662
|
edges,
|
|
503
663
|
transforms,
|
|
504
664
|
onEdgesChange,
|
|
665
|
+
rewirePolicy = 'replace',
|
|
666
|
+
onConnectionFeedback,
|
|
667
|
+
parentChildConflicts,
|
|
505
668
|
chrome = 'card',
|
|
506
669
|
emptyDetail: emptyDetailProp,
|
|
670
|
+
detailPresentation = 'rail',
|
|
507
671
|
showFlowHint: showFlowHintProp,
|
|
508
672
|
showBindingsList: showBindingsListProp,
|
|
509
673
|
showConvertPalette = true,
|
|
674
|
+
readOnly = false,
|
|
510
675
|
operators = [],
|
|
511
676
|
onOperatorsChange,
|
|
512
677
|
sourceTitle,
|
|
@@ -530,21 +695,269 @@ function FieldRemapFlowCanvas({
|
|
|
530
695
|
() => resolveFieldRemapChromeLabels(labelOverrides, t),
|
|
531
696
|
[labelOverrides, t],
|
|
532
697
|
);
|
|
533
|
-
const showFlowHint = showFlowHintProp ?? chrome !== 'embed';
|
|
698
|
+
const showFlowHint = !readOnly && (showFlowHintProp ?? chrome !== 'embed');
|
|
534
699
|
const showBindingsList = showBindingsListProp ?? chrome !== 'embed';
|
|
700
|
+
const showAuthoringPalette = showConvertPalette && !readOnly;
|
|
535
701
|
const emptyDetail = emptyDetailProp ?? (chrome === 'embed' ? 'collapse' : 'hint');
|
|
536
702
|
const mapperRef = useRef<HTMLDivElement>(null);
|
|
703
|
+
const restoreMapperFocusRef = useRef(false);
|
|
537
704
|
const [workspaceLayout, setWorkspaceLayout] = useState<'wide' | 'medium' | 'narrow'>('wide');
|
|
538
705
|
const [internalSelection, setInternalSelection] = useState<FieldRemapSelection>(null);
|
|
539
|
-
const
|
|
540
|
-
const
|
|
706
|
+
const authoritativeSelection = selectionProp !== undefined ? selectionProp : internalSelection;
|
|
707
|
+
const selectionExternallyManaged =
|
|
708
|
+
selectionProp !== undefined || onSelectionChangeProp !== undefined;
|
|
709
|
+
const [bulkSelection, setBulkSelection] = useState<readonly FieldRemapBulkSelectionRef[]>(() => {
|
|
710
|
+
const initial = asFieldRemapBulkSelectionRef(selectionProp ?? null);
|
|
711
|
+
return initial ? normalizeFieldRemapBulkSelection(edges, [initial]) : [];
|
|
712
|
+
});
|
|
713
|
+
const selection = authoritativeSelection;
|
|
714
|
+
const selectionRef = useRef(authoritativeSelection);
|
|
715
|
+
selectionRef.current = authoritativeSelection;
|
|
716
|
+
const setSelection = useCallback(
|
|
717
|
+
(next: FieldRemapSelection) => {
|
|
718
|
+
if (fieldRemapSelectionKey(selectionRef.current) === fieldRemapSelectionKey(next)) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (!selectionExternallyManaged) {
|
|
722
|
+
setInternalSelection(next);
|
|
723
|
+
const nextRef = asFieldRemapBulkSelectionRef(next);
|
|
724
|
+
setBulkSelection(nextRef ? normalizeFieldRemapBulkSelection(edges, [nextRef]) : []);
|
|
725
|
+
}
|
|
726
|
+
onSelectionChangeProp?.(next);
|
|
727
|
+
},
|
|
728
|
+
[edges, onSelectionChangeProp, selectionExternallyManaged],
|
|
729
|
+
);
|
|
730
|
+
const setSelectionRef = useRef(setSelection);
|
|
731
|
+
setSelectionRef.current = setSelection;
|
|
732
|
+
const closeModalDetail = useCallback(() => setSelectionRef.current(null), []);
|
|
733
|
+
const canonicalBulkRefs = useMemo(() => listFieldRemapBulkSelectionRefs(edges), [edges]);
|
|
734
|
+
const bulkDomainKey = useMemo(
|
|
735
|
+
() => fieldRemapBulkDomainKey(canonicalBulkRefs),
|
|
736
|
+
[canonicalBulkRefs],
|
|
737
|
+
);
|
|
738
|
+
const bulkSelectionKeys = useMemo(
|
|
739
|
+
() => new Set(bulkSelection.map(fieldRemapBulkSelectionKey)),
|
|
740
|
+
[bulkSelection],
|
|
741
|
+
);
|
|
742
|
+
const authoritativeSelectionKey = fieldRemapSelectionKey(authoritativeSelection);
|
|
743
|
+
const previousAuthoritativeSelectionKeyRef = useRef(authoritativeSelectionKey);
|
|
744
|
+
const bulkFocusTargetsRef = useRef(
|
|
745
|
+
new Map<string, Partial<Record<FieldRemapFocusSurface, FieldRemapFocusableElement>>>(),
|
|
746
|
+
);
|
|
747
|
+
const registerBulkFocusTarget = useCallback<RegisterFieldRemapFocusTarget>(
|
|
748
|
+
(key, surface, element) => {
|
|
749
|
+
const current = bulkFocusTargetsRef.current.get(key) ?? {};
|
|
750
|
+
if (element) {
|
|
751
|
+
current[surface] = element;
|
|
752
|
+
bulkFocusTargetsRef.current.set(key, current);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
delete current[surface];
|
|
756
|
+
if (current.graph || current.list) {
|
|
757
|
+
bulkFocusTargetsRef.current.set(key, current);
|
|
758
|
+
} else {
|
|
759
|
+
bulkFocusTargetsRef.current.delete(key);
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
[],
|
|
763
|
+
);
|
|
764
|
+
const focusBulkTarget = useCallback(
|
|
765
|
+
(ref: FieldRemapBulkSelectionRef, preferredSurface?: FieldRemapFocusSurface) => {
|
|
766
|
+
const targets = bulkFocusTargetsRef.current.get(fieldRemapBulkSelectionKey(ref));
|
|
767
|
+
const target =
|
|
768
|
+
(preferredSurface ? targets?.[preferredSurface] : undefined) ??
|
|
769
|
+
targets?.list ??
|
|
770
|
+
targets?.graph;
|
|
771
|
+
target?.focus({ preventScroll: true });
|
|
772
|
+
},
|
|
773
|
+
[],
|
|
774
|
+
);
|
|
775
|
+
const lastPrimaryCorrectionRef = useRef<string | undefined>(undefined);
|
|
776
|
+
const pendingPrimaryCorrectionAckRef = useRef<
|
|
777
|
+
{ readonly domainKey: string; readonly selectionKey: string } | undefined
|
|
778
|
+
>(undefined);
|
|
779
|
+
const pendingBulkFocusRef = useRef<
|
|
780
|
+
| {
|
|
781
|
+
readonly domainKey: string;
|
|
782
|
+
readonly target: FieldRemapBulkSelectionRef | null;
|
|
783
|
+
}
|
|
784
|
+
| undefined
|
|
785
|
+
>(undefined);
|
|
786
|
+
const pendingBulkCommitRef = useRef<
|
|
787
|
+
| {
|
|
788
|
+
readonly domainKey: string;
|
|
789
|
+
readonly membership: readonly FieldRemapBulkSelectionRef[];
|
|
790
|
+
readonly primary: FieldRemapSelection;
|
|
791
|
+
}
|
|
792
|
+
| undefined
|
|
793
|
+
>(undefined);
|
|
541
794
|
const previewVisible = showPreview && preview !== undefined && preview.status !== 'unavailable';
|
|
795
|
+
const flowAriaLabelConfig = useMemo(
|
|
796
|
+
() => ({
|
|
797
|
+
'node.a11yDescription.default': readOnly
|
|
798
|
+
? 'Press Enter or Space to inspect this item. Control or Command toggles a non-primary item; Shift adds it.'
|
|
799
|
+
: 'Press Enter or Space to select this item. Control or Command toggles a non-primary item; Shift adds it.',
|
|
800
|
+
'node.a11yDescription.keyboardDisabled': readOnly
|
|
801
|
+
? 'Press Enter or Space to inspect this item. Workbench editing is read only.'
|
|
802
|
+
: 'Press Enter or Space to select this item. Use the Workbench controls to edit it.',
|
|
803
|
+
'edge.a11yDescription.default': readOnly
|
|
804
|
+
? 'Press Enter or Space to inspect this mapping. Control or Command toggles a non-primary mapping; Shift adds it.'
|
|
805
|
+
: 'Press Enter or Space to select this mapping. Control or Command toggles a non-primary mapping; Shift adds it.',
|
|
806
|
+
}),
|
|
807
|
+
[readOnly],
|
|
808
|
+
);
|
|
809
|
+
const detailVisible = emptyDetail === 'hint' || selection !== null;
|
|
810
|
+
const sideRailVisible = previewVisible || (detailPresentation === 'rail' && detailVisible);
|
|
542
811
|
const [drafts, setDrafts] = useState<readonly FieldRemapDraftTransform[]>([]);
|
|
812
|
+
const [draftPositions, setDraftPositions] = useState<ReadonlyMap<string, XYPosition>>(
|
|
813
|
+
() => new Map(),
|
|
814
|
+
);
|
|
815
|
+
const [connectionFeedback, setConnectionFeedback] = useState<FieldRemapConnectionFeedback | null>(
|
|
816
|
+
null,
|
|
817
|
+
);
|
|
818
|
+
const connectionAttemptCompletedRef = useRef(false);
|
|
543
819
|
const [placeTransformId, setPlaceTransformId] = useState(() => {
|
|
544
820
|
const first = transforms.list().find((definition) => definition.id !== 'identity');
|
|
545
821
|
return first?.id ?? '';
|
|
546
822
|
});
|
|
547
823
|
const transformRegistrySignature = createTransformRegistrySignature(transforms);
|
|
824
|
+
const { screenToFlowPosition } = useReactFlow();
|
|
825
|
+
|
|
826
|
+
const removeDraftPosition = useCallback((localId: string) => {
|
|
827
|
+
setDraftPositions((current) => {
|
|
828
|
+
if (!current.has(localId)) {
|
|
829
|
+
return current;
|
|
830
|
+
}
|
|
831
|
+
const next = new Map(current);
|
|
832
|
+
next.delete(localId);
|
|
833
|
+
return next;
|
|
834
|
+
});
|
|
835
|
+
}, []);
|
|
836
|
+
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
if (detailPresentation === 'modal') {
|
|
839
|
+
void loadFieldRemapModalDetail().catch(() => undefined);
|
|
840
|
+
}
|
|
841
|
+
}, [detailPresentation]);
|
|
842
|
+
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
if (!readOnly) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
setDrafts([]);
|
|
848
|
+
setDraftPositions(new Map());
|
|
849
|
+
setConnectionFeedback(null);
|
|
850
|
+
connectionAttemptCompletedRef.current = false;
|
|
851
|
+
if (selectionProp === undefined) {
|
|
852
|
+
setInternalSelection((current) => (current?.kind === 'draft' ? null : current));
|
|
853
|
+
}
|
|
854
|
+
}, [readOnly, selectionProp]);
|
|
855
|
+
|
|
856
|
+
useEffect(() => {
|
|
857
|
+
let authoritativeReset: readonly FieldRemapBulkSelectionRef[] | undefined;
|
|
858
|
+
if (previousAuthoritativeSelectionKeyRef.current !== authoritativeSelectionKey) {
|
|
859
|
+
previousAuthoritativeSelectionKeyRef.current = authoritativeSelectionKey;
|
|
860
|
+
lastPrimaryCorrectionRef.current = undefined;
|
|
861
|
+
const pendingAck = pendingPrimaryCorrectionAckRef.current;
|
|
862
|
+
const acceptsCorrection =
|
|
863
|
+
pendingAck?.selectionKey === authoritativeSelectionKey &&
|
|
864
|
+
(pendingAck.domainKey === bulkDomainKey ||
|
|
865
|
+
pendingAck.domainKey === pendingBulkCommitRef.current?.domainKey);
|
|
866
|
+
pendingPrimaryCorrectionAckRef.current = undefined;
|
|
867
|
+
if (!acceptsCorrection) {
|
|
868
|
+
pendingBulkCommitRef.current = undefined;
|
|
869
|
+
const authoritativeRef = asFieldRemapBulkSelectionRef(authoritativeSelection);
|
|
870
|
+
const next = authoritativeRef
|
|
871
|
+
? normalizeFieldRemapBulkSelection(edges, [authoritativeRef])
|
|
872
|
+
: [];
|
|
873
|
+
authoritativeReset = next;
|
|
874
|
+
setBulkSelection((current) =>
|
|
875
|
+
areFieldRemapBulkSelectionsEqual(current, next) ? current : next,
|
|
876
|
+
);
|
|
877
|
+
if (!authoritativeRef || next.length > 0) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const pendingCommit = pendingBulkCommitRef.current;
|
|
884
|
+
if (pendingCommit) {
|
|
885
|
+
const primaryAccepted =
|
|
886
|
+
fieldRemapSelectionKey(authoritativeSelection) ===
|
|
887
|
+
fieldRemapSelectionKey(pendingCommit.primary);
|
|
888
|
+
if (primaryAccepted || bulkDomainKey === pendingCommit.domainKey) {
|
|
889
|
+
setBulkSelection((current) =>
|
|
890
|
+
areFieldRemapBulkSelectionsEqual(current, pendingCommit.membership)
|
|
891
|
+
? current
|
|
892
|
+
: pendingCommit.membership,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (bulkDomainKey !== pendingCommit.domainKey) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
pendingBulkCommitRef.current = undefined;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const primaryRef = asFieldRemapBulkSelectionRef(authoritativeSelection);
|
|
902
|
+
if (!primaryRef) {
|
|
903
|
+
setBulkSelection((current) => (current.length === 0 ? current : []));
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const primaryKey = fieldRemapBulkSelectionKey(primaryRef);
|
|
908
|
+
const primaryIsValid = canonicalBulkRefs.some(
|
|
909
|
+
(ref) => fieldRemapBulkSelectionKey(ref) === primaryKey,
|
|
910
|
+
);
|
|
911
|
+
let next = authoritativeReset ?? normalizeFieldRemapBulkSelection(edges, bulkSelection);
|
|
912
|
+
if (primaryIsValid && !next.some((ref) => fieldRemapBulkSelectionKey(ref) === primaryKey)) {
|
|
913
|
+
next = normalizeFieldRemapBulkSelection(edges, [...next, primaryRef]);
|
|
914
|
+
}
|
|
915
|
+
setBulkSelection((current) =>
|
|
916
|
+
areFieldRemapBulkSelectionsEqual(current, next) ? current : next,
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
if (primaryIsValid) {
|
|
920
|
+
lastPrimaryCorrectionRef.current = undefined;
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const nextPrimary = next[0] ?? null;
|
|
925
|
+
const correctionKey = `${bulkDomainKey}\u0001${fieldRemapSelectionKey(authoritativeSelection)}\u0001${fieldRemapSelectionKey(nextPrimary)}`;
|
|
926
|
+
if (lastPrimaryCorrectionRef.current === correctionKey) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
lastPrimaryCorrectionRef.current = correctionKey;
|
|
930
|
+
pendingPrimaryCorrectionAckRef.current = onSelectionChangeProp
|
|
931
|
+
? {
|
|
932
|
+
domainKey: bulkDomainKey,
|
|
933
|
+
selectionKey: fieldRemapSelectionKey(nextPrimary),
|
|
934
|
+
}
|
|
935
|
+
: undefined;
|
|
936
|
+
if (!selectionExternallyManaged) {
|
|
937
|
+
setInternalSelection(nextPrimary);
|
|
938
|
+
} else {
|
|
939
|
+
setSelection(nextPrimary);
|
|
940
|
+
}
|
|
941
|
+
}, [
|
|
942
|
+
bulkDomainKey,
|
|
943
|
+
bulkSelection,
|
|
944
|
+
canonicalBulkRefs,
|
|
945
|
+
authoritativeSelectionKey,
|
|
946
|
+
edges,
|
|
947
|
+
onSelectionChangeProp,
|
|
948
|
+
authoritativeSelection,
|
|
949
|
+
selection,
|
|
950
|
+
selectionExternallyManaged,
|
|
951
|
+
setSelection,
|
|
952
|
+
]);
|
|
953
|
+
|
|
954
|
+
useEffect(() => {
|
|
955
|
+
if (!restoreMapperFocusRef.current || selection !== null || drafts.length > 0) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
restoreMapperFocusRef.current = false;
|
|
959
|
+
mapperRef.current?.focus({ preventScroll: true });
|
|
960
|
+
}, [drafts.length, selection]);
|
|
548
961
|
|
|
549
962
|
useEffect(() => {
|
|
550
963
|
const element = mapperRef.current;
|
|
@@ -580,6 +993,7 @@ function FieldRemapFlowCanvas({
|
|
|
580
993
|
sourceTitle,
|
|
581
994
|
targetTitle,
|
|
582
995
|
drafts,
|
|
996
|
+
draftPositions,
|
|
583
997
|
}),
|
|
584
998
|
[
|
|
585
999
|
sources,
|
|
@@ -590,22 +1004,68 @@ function FieldRemapFlowCanvas({
|
|
|
590
1004
|
sourceTitle,
|
|
591
1005
|
targetTitle,
|
|
592
1006
|
drafts,
|
|
1007
|
+
draftPositions,
|
|
593
1008
|
transformRegistrySignature,
|
|
594
1009
|
],
|
|
595
1010
|
);
|
|
596
1011
|
|
|
1012
|
+
const applyBulkSelectionGesture = useCallback(
|
|
1013
|
+
(
|
|
1014
|
+
target: FieldRemapBulkSelectionRef,
|
|
1015
|
+
modifiers: {
|
|
1016
|
+
readonly ctrlKey: boolean;
|
|
1017
|
+
readonly metaKey: boolean;
|
|
1018
|
+
readonly shiftKey: boolean;
|
|
1019
|
+
},
|
|
1020
|
+
) => {
|
|
1021
|
+
pendingPrimaryCorrectionAckRef.current = undefined;
|
|
1022
|
+
const gesture =
|
|
1023
|
+
modifiers.ctrlKey || modifiers.metaKey ? 'toggle' : modifiers.shiftKey ? 'add' : 'plain';
|
|
1024
|
+
const next = updateFieldRemapBulkSelection({
|
|
1025
|
+
edges,
|
|
1026
|
+
membership: bulkSelection,
|
|
1027
|
+
primary: selection,
|
|
1028
|
+
target,
|
|
1029
|
+
gesture,
|
|
1030
|
+
});
|
|
1031
|
+
const primaryChanged =
|
|
1032
|
+
fieldRemapSelectionKey(next.primary) !== fieldRemapSelectionKey(selection);
|
|
1033
|
+
if (!primaryChanged || !selectionExternallyManaged) {
|
|
1034
|
+
setBulkSelection((current) =>
|
|
1035
|
+
areFieldRemapBulkSelectionsEqual(current, next.membership) ? current : next.membership,
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
if (primaryChanged) {
|
|
1039
|
+
setSelection(next.primary);
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
[bulkSelection, edges, selection, selectionExternallyManaged, setSelection],
|
|
1043
|
+
);
|
|
1044
|
+
|
|
597
1045
|
const nodesWithSelection = useMemo(
|
|
598
1046
|
() =>
|
|
599
1047
|
graph.nodes.map((node) => {
|
|
600
1048
|
if (node.data.kind === 'transform') {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1049
|
+
const ref = {
|
|
1050
|
+
kind: 'transformStep',
|
|
1051
|
+
edgeId: node.data.mappingEdgeId,
|
|
1052
|
+
stepIndex: node.data.stepIndex,
|
|
1053
|
+
} as const;
|
|
1054
|
+
const selected = bulkSelectionKeys.has(fieldRemapBulkSelectionKey(ref));
|
|
605
1055
|
return {
|
|
606
1056
|
...node,
|
|
607
|
-
data: { ...node.data, selected },
|
|
1057
|
+
data: { ...node.data, selected, registerFocusTarget: registerBulkFocusTarget },
|
|
608
1058
|
selected,
|
|
1059
|
+
selectable: false,
|
|
1060
|
+
focusable: true,
|
|
1061
|
+
ariaRole: 'button' as const,
|
|
1062
|
+
ariaLabel: `${node.data.label} convert step`,
|
|
1063
|
+
domAttributes: {
|
|
1064
|
+
'aria-pressed': selected,
|
|
1065
|
+
'data-field-remap-bulk-kind': ref.kind,
|
|
1066
|
+
'data-field-remap-bulk-edge-id': ref.edgeId,
|
|
1067
|
+
'data-field-remap-bulk-step-index': ref.stepIndex,
|
|
1068
|
+
},
|
|
609
1069
|
};
|
|
610
1070
|
}
|
|
611
1071
|
if (node.data.kind === 'draft-transform') {
|
|
@@ -619,50 +1079,168 @@ function FieldRemapFlowCanvas({
|
|
|
619
1079
|
}
|
|
620
1080
|
return node;
|
|
621
1081
|
}),
|
|
622
|
-
[graph.nodes, selection],
|
|
1082
|
+
[bulkSelectionKeys, graph.nodes, registerBulkFocusTarget, selection],
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
const flowEdgesWithSelection = useMemo(
|
|
1086
|
+
() =>
|
|
1087
|
+
graph.edges.map((edge) => {
|
|
1088
|
+
const data = edge.data as FieldRemapFlowEdgeData | undefined;
|
|
1089
|
+
const mappingEdgeId = data?.mappingEdgeId;
|
|
1090
|
+
const selected = mappingEdgeId
|
|
1091
|
+
? bulkSelectionKeys.has(
|
|
1092
|
+
fieldRemapBulkSelectionKey({ kind: 'edge', edgeId: mappingEdgeId }),
|
|
1093
|
+
)
|
|
1094
|
+
: false;
|
|
1095
|
+
if (!mappingEdgeId) {
|
|
1096
|
+
return { ...edge, selected };
|
|
1097
|
+
}
|
|
1098
|
+
const canonicalSegment = data?.segment === 'direct' || data?.segment === 'in';
|
|
1099
|
+
return {
|
|
1100
|
+
...edge,
|
|
1101
|
+
data: { ...data, registerFocusTarget: registerBulkFocusTarget },
|
|
1102
|
+
selected,
|
|
1103
|
+
selectable: false,
|
|
1104
|
+
focusable: canonicalSegment,
|
|
1105
|
+
ariaRole: canonicalSegment ? ('button' as const) : ('presentation' as const),
|
|
1106
|
+
ariaLabel: canonicalSegment ? `Mapping ${mappingEdgeId}` : undefined,
|
|
1107
|
+
domAttributes: canonicalSegment
|
|
1108
|
+
? {
|
|
1109
|
+
'aria-pressed': selected,
|
|
1110
|
+
'data-field-remap-bulk-kind': 'edge',
|
|
1111
|
+
'data-field-remap-bulk-edge-id': mappingEdgeId,
|
|
1112
|
+
}
|
|
1113
|
+
: { 'aria-hidden': true },
|
|
1114
|
+
};
|
|
1115
|
+
}),
|
|
1116
|
+
[bulkSelectionKeys, graph.edges, registerBulkFocusTarget],
|
|
623
1117
|
);
|
|
624
1118
|
|
|
625
1119
|
const [nodes, setNodes, onNodesChange] = useNodesState(nodesWithSelection);
|
|
626
|
-
const [flowEdges, setFlowEdges, onFlowEdgesChange] = useEdgesState(
|
|
1120
|
+
const [flowEdges, setFlowEdges, onFlowEdgesChange] = useEdgesState(flowEdgesWithSelection);
|
|
1121
|
+
const onProjectedNodesChange = useCallback(
|
|
1122
|
+
(changes: Parameters<typeof onNodesChange>[0]) => {
|
|
1123
|
+
onNodesChange(changes.filter((change) => change.type !== 'select'));
|
|
1124
|
+
},
|
|
1125
|
+
[onNodesChange],
|
|
1126
|
+
);
|
|
1127
|
+
const onProjectedFlowEdgesChange = useCallback(
|
|
1128
|
+
(changes: Parameters<typeof onFlowEdgesChange>[0]) => {
|
|
1129
|
+
onFlowEdgesChange(changes.filter((change) => change.type !== 'select'));
|
|
1130
|
+
},
|
|
1131
|
+
[onFlowEdgesChange],
|
|
1132
|
+
);
|
|
627
1133
|
|
|
628
1134
|
// Depending directly on `nodesWithSelection` (a new array after each graph
|
|
629
1135
|
// calculation) re-enters XYFlow's StoreUpdater. The explicit signature keeps
|
|
630
1136
|
// that loop guard while still tracking every value copied into rendered nodes.
|
|
631
1137
|
const graphSyncKey = createFieldRemapGraphSyncKey({
|
|
632
1138
|
nodes: nodesWithSelection,
|
|
633
|
-
edges:
|
|
1139
|
+
edges: flowEdgesWithSelection,
|
|
634
1140
|
selection,
|
|
635
1141
|
transformRegistrySignature,
|
|
636
1142
|
});
|
|
637
1143
|
const nodesWithSelectionRef = useRef(nodesWithSelection);
|
|
638
|
-
const graphEdgesRef = useRef(
|
|
1144
|
+
const graphEdgesRef = useRef(flowEdgesWithSelection);
|
|
639
1145
|
nodesWithSelectionRef.current = nodesWithSelection;
|
|
640
|
-
graphEdgesRef.current =
|
|
1146
|
+
graphEdgesRef.current = flowEdgesWithSelection;
|
|
641
1147
|
|
|
642
1148
|
useEffect(() => {
|
|
643
1149
|
setNodes(nodesWithSelectionRef.current);
|
|
644
1150
|
setFlowEdges(graphEdgesRef.current);
|
|
645
1151
|
}, [graphSyncKey, setFlowEdges, setNodes]);
|
|
646
1152
|
|
|
1153
|
+
useEffect(() => {
|
|
1154
|
+
const pending = pendingBulkFocusRef.current;
|
|
1155
|
+
const mapper = mapperRef.current;
|
|
1156
|
+
if (!pending || !mapper || pending.domainKey !== bulkDomainKey) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
pendingBulkFocusRef.current = undefined;
|
|
1160
|
+
if (!pending.target) {
|
|
1161
|
+
mapper.focus({ preventScroll: true });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const targets = bulkFocusTargetsRef.current.get(fieldRemapBulkSelectionKey(pending.target));
|
|
1165
|
+
(targets?.list ?? targets?.graph ?? mapper).focus({ preventScroll: true });
|
|
1166
|
+
}, [bulkDomainKey]);
|
|
1167
|
+
|
|
647
1168
|
const connectionContext = useMemo(
|
|
648
1169
|
() => ({ sources, targets, edges, transforms, drafts, operators }),
|
|
649
1170
|
[sources, targets, edges, transforms, drafts, operators],
|
|
650
1171
|
);
|
|
651
1172
|
|
|
1173
|
+
const conflicts = useMemo(
|
|
1174
|
+
() => parentChildConflicts ?? findParentChildMappingConflicts(edges, sources, targets),
|
|
1175
|
+
[edges, parentChildConflicts, sources, targets],
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
const publishConnectionFeedback = useCallback(
|
|
1179
|
+
(feedback: FieldRemapConnectionFeedback | null) => {
|
|
1180
|
+
setConnectionFeedback(feedback);
|
|
1181
|
+
onConnectionFeedback?.(feedback);
|
|
1182
|
+
},
|
|
1183
|
+
[onConnectionFeedback],
|
|
1184
|
+
);
|
|
1185
|
+
|
|
652
1186
|
const isValidConnection = useCallback(
|
|
653
1187
|
(connection: Connection | Edge) =>
|
|
654
|
-
isValidFieldRemapFlowConnection(connection, connectionContext),
|
|
655
|
-
[connectionContext],
|
|
1188
|
+
!readOnly && isValidFieldRemapFlowConnection(connection, connectionContext),
|
|
1189
|
+
[connectionContext, readOnly],
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
const onConnectStart = useCallback(() => {
|
|
1193
|
+
if (readOnly) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
// Clearing at attempt start lets an identical later rejection be announced once at completion.
|
|
1197
|
+
connectionAttemptCompletedRef.current = false;
|
|
1198
|
+
setConnectionFeedback(null);
|
|
1199
|
+
}, [readOnly]);
|
|
1200
|
+
|
|
1201
|
+
const onConnectEnd = useCallback(
|
|
1202
|
+
(_event: globalThis.MouseEvent | TouchEvent, state: FinalConnectionState) => {
|
|
1203
|
+
if (readOnly) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (connectionAttemptCompletedRef.current) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
connectionAttemptCompletedRef.current = true;
|
|
1210
|
+
const connection = connectionFromFinalState(state);
|
|
1211
|
+
if (!connection) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const evaluation = evaluateFieldRemapFlowConnection(connection, connectionContext);
|
|
1215
|
+
if (evaluation.status === 'rejected') {
|
|
1216
|
+
publishConnectionFeedback({ reason: evaluation.reason });
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
[connectionContext, publishConnectionFeedback, readOnly],
|
|
656
1220
|
);
|
|
657
1221
|
|
|
658
1222
|
const onConnect = useCallback(
|
|
659
1223
|
(connection: Connection) => {
|
|
1224
|
+
if (readOnly) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
660
1227
|
if (!connection.source || !connection.target) {
|
|
661
1228
|
return;
|
|
662
1229
|
}
|
|
663
|
-
|
|
1230
|
+
const evaluation = evaluateFieldRemapFlowConnection(connection, connectionContext);
|
|
1231
|
+
if (evaluation.status === 'rejected') {
|
|
664
1232
|
return;
|
|
665
1233
|
}
|
|
1234
|
+
if (evaluation.status === 'rewire' && rewirePolicy === 'reject') {
|
|
1235
|
+
connectionAttemptCompletedRef.current = true;
|
|
1236
|
+
publishConnectionFeedback({
|
|
1237
|
+
reason: 'rewire-policy-rejected',
|
|
1238
|
+
impactedEdgeIds: evaluation.impactedEdgeIds,
|
|
1239
|
+
});
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
connectionAttemptCompletedRef.current = true;
|
|
1243
|
+
publishConnectionFeedback(null);
|
|
666
1244
|
|
|
667
1245
|
const draftAsTarget = parseDraftTransformNodeId(connection.target);
|
|
668
1246
|
if (draftAsTarget && connection.sourceHandle) {
|
|
@@ -683,6 +1261,7 @@ function FieldRemapFlowCanvas({
|
|
|
683
1261
|
);
|
|
684
1262
|
onEdgesChange([...withoutTarget, finalized]);
|
|
685
1263
|
setDrafts(drafts.filter((item) => item.localId !== draft.localId));
|
|
1264
|
+
removeDraftPosition(draft.localId);
|
|
686
1265
|
setSelection({
|
|
687
1266
|
kind: 'transformStep',
|
|
688
1267
|
edgeId: finalized.id,
|
|
@@ -714,6 +1293,7 @@ function FieldRemapFlowCanvas({
|
|
|
714
1293
|
);
|
|
715
1294
|
onEdgesChange([...withoutTarget, finalized]);
|
|
716
1295
|
setDrafts(drafts.filter((item) => item.localId !== draft.localId));
|
|
1296
|
+
removeDraftPosition(draft.localId);
|
|
717
1297
|
setSelection({
|
|
718
1298
|
kind: 'transformStep',
|
|
719
1299
|
edgeId: finalized.id,
|
|
@@ -775,6 +1355,10 @@ function FieldRemapFlowCanvas({
|
|
|
775
1355
|
onEdgesChange,
|
|
776
1356
|
onOperatorsChange,
|
|
777
1357
|
operators,
|
|
1358
|
+
publishConnectionFeedback,
|
|
1359
|
+
readOnly,
|
|
1360
|
+
removeDraftPosition,
|
|
1361
|
+
rewirePolicy,
|
|
778
1362
|
setSelection,
|
|
779
1363
|
sources,
|
|
780
1364
|
targets,
|
|
@@ -782,8 +1366,107 @@ function FieldRemapFlowCanvas({
|
|
|
782
1366
|
],
|
|
783
1367
|
);
|
|
784
1368
|
|
|
1369
|
+
const commitBulkDelete = useCallback(
|
|
1370
|
+
(refs: readonly FieldRemapBulkSelectionRef[]): boolean => {
|
|
1371
|
+
const plan = planFieldRemapBulkDelete(edges, refs);
|
|
1372
|
+
if (plan.status !== 'changed') {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const removedEdgeIds = new Set(
|
|
1377
|
+
refs.filter((ref) => ref.kind === 'edge').map((ref) => ref.edgeId),
|
|
1378
|
+
);
|
|
1379
|
+
const removedKeys = new Set(refs.map(fieldRemapBulkSelectionKey));
|
|
1380
|
+
let survivingMembership = normalizeFieldRemapBulkSelection(
|
|
1381
|
+
plan.edges,
|
|
1382
|
+
bulkSelection.filter(
|
|
1383
|
+
(ref) =>
|
|
1384
|
+
!removedKeys.has(fieldRemapBulkSelectionKey(ref)) && !removedEdgeIds.has(ref.edgeId),
|
|
1385
|
+
),
|
|
1386
|
+
);
|
|
1387
|
+
const primaryRef = asFieldRemapBulkSelectionRef(selection);
|
|
1388
|
+
const primaryRemoved = primaryRef
|
|
1389
|
+
? removedKeys.has(fieldRemapBulkSelectionKey(primaryRef)) ||
|
|
1390
|
+
removedEdgeIds.has(primaryRef.edgeId)
|
|
1391
|
+
: false;
|
|
1392
|
+
const preservesSingleStepFallback =
|
|
1393
|
+
refs.length === 1 &&
|
|
1394
|
+
refs[0]?.kind === 'transformStep' &&
|
|
1395
|
+
primaryRef?.kind === 'transformStep' &&
|
|
1396
|
+
fieldRemapBulkSelectionKey(refs[0]) === fieldRemapBulkSelectionKey(primaryRef);
|
|
1397
|
+
let nextPrimary = primaryRemoved ? (survivingMembership[0] ?? null) : selection;
|
|
1398
|
+
if (preservesSingleStepFallback) {
|
|
1399
|
+
const removedStep = refs[0] as Extract<
|
|
1400
|
+
FieldRemapBulkSelectionRef,
|
|
1401
|
+
{ readonly kind: 'transformStep' }
|
|
1402
|
+
>;
|
|
1403
|
+
const nextEdge = plan.edges.find((edge) => edge.id === removedStep.edgeId);
|
|
1404
|
+
nextPrimary =
|
|
1405
|
+
(nextEdge?.transformIds?.length ?? 0) > 0
|
|
1406
|
+
? {
|
|
1407
|
+
kind: 'transformStep',
|
|
1408
|
+
edgeId: removedStep.edgeId,
|
|
1409
|
+
stepIndex: Math.min(
|
|
1410
|
+
removedStep.stepIndex,
|
|
1411
|
+
(nextEdge?.transformIds?.length ?? 1) - 1,
|
|
1412
|
+
),
|
|
1413
|
+
}
|
|
1414
|
+
: nextEdge
|
|
1415
|
+
? { kind: 'edge', edgeId: nextEdge.id }
|
|
1416
|
+
: null;
|
|
1417
|
+
const fallbackRef = asFieldRemapBulkSelectionRef(nextPrimary);
|
|
1418
|
+
survivingMembership = fallbackRef
|
|
1419
|
+
? normalizeFieldRemapBulkSelection(plan.edges, [fallbackRef])
|
|
1420
|
+
: [];
|
|
1421
|
+
}
|
|
1422
|
+
const nextDomainKey = fieldRemapBulkDomainKey(listFieldRemapBulkSelectionRefs(plan.edges));
|
|
1423
|
+
const primaryKeyChanged =
|
|
1424
|
+
fieldRemapSelectionKey(nextPrimary) !== fieldRemapSelectionKey(selection);
|
|
1425
|
+
|
|
1426
|
+
pendingBulkCommitRef.current = {
|
|
1427
|
+
domainKey: nextDomainKey,
|
|
1428
|
+
membership: survivingMembership,
|
|
1429
|
+
primary: nextPrimary,
|
|
1430
|
+
};
|
|
1431
|
+
pendingBulkFocusRef.current = {
|
|
1432
|
+
domainKey: nextDomainKey,
|
|
1433
|
+
target: asFieldRemapBulkSelectionRef(nextPrimary) ?? null,
|
|
1434
|
+
};
|
|
1435
|
+
if (primaryKeyChanged) {
|
|
1436
|
+
lastPrimaryCorrectionRef.current = `${nextDomainKey}\u0001${fieldRemapSelectionKey(selection)}\u0001${fieldRemapSelectionKey(nextPrimary)}`;
|
|
1437
|
+
pendingPrimaryCorrectionAckRef.current = onSelectionChangeProp
|
|
1438
|
+
? {
|
|
1439
|
+
domainKey: nextDomainKey,
|
|
1440
|
+
selectionKey: fieldRemapSelectionKey(nextPrimary),
|
|
1441
|
+
}
|
|
1442
|
+
: undefined;
|
|
1443
|
+
}
|
|
1444
|
+
onEdgesChange(plan.edges);
|
|
1445
|
+
|
|
1446
|
+
if (primaryKeyChanged) {
|
|
1447
|
+
if (!selectionExternallyManaged) {
|
|
1448
|
+
setInternalSelection(nextPrimary);
|
|
1449
|
+
} else {
|
|
1450
|
+
onSelectionChangeProp?.(nextPrimary);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return true;
|
|
1454
|
+
},
|
|
1455
|
+
[
|
|
1456
|
+
bulkSelection,
|
|
1457
|
+
edges,
|
|
1458
|
+
onEdgesChange,
|
|
1459
|
+
onSelectionChangeProp,
|
|
1460
|
+
selection,
|
|
1461
|
+
selectionExternallyManaged,
|
|
1462
|
+
],
|
|
1463
|
+
);
|
|
1464
|
+
|
|
785
1465
|
const onEdgesDelete = useCallback(
|
|
786
1466
|
(deleted: Edge[]) => {
|
|
1467
|
+
if (readOnly) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
787
1470
|
const mappingIds = new Set(
|
|
788
1471
|
deleted
|
|
789
1472
|
.map((edge) => {
|
|
@@ -795,26 +1478,64 @@ function FieldRemapFlowCanvas({
|
|
|
795
1478
|
if (mappingIds.size === 0) {
|
|
796
1479
|
return;
|
|
797
1480
|
}
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
selection &&
|
|
801
|
-
(selection.kind === 'edge' || selection.kind === 'transformStep') &&
|
|
802
|
-
mappingIds.has(selection.edgeId)
|
|
803
|
-
) {
|
|
804
|
-
setSelection(null);
|
|
805
|
-
}
|
|
1481
|
+
commitBulkDelete([...mappingIds].map((edgeId) => ({ kind: 'edge' as const, edgeId })));
|
|
806
1482
|
},
|
|
807
|
-
[
|
|
1483
|
+
[commitBulkDelete, readOnly],
|
|
808
1484
|
);
|
|
809
1485
|
|
|
810
1486
|
const placeDraft = useCallback(
|
|
811
|
-
(transformId: string) => {
|
|
1487
|
+
(transformId: string, position?: XYPosition) => {
|
|
1488
|
+
if (readOnly) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
812
1491
|
const draft = createDraftTransform(transformId);
|
|
813
1492
|
setDrafts((current) => [...current, draft]);
|
|
1493
|
+
if (position) {
|
|
1494
|
+
setDraftPositions((current) => new Map(current).set(draft.localId, position));
|
|
1495
|
+
}
|
|
814
1496
|
setSelection({ kind: 'draft', localId: draft.localId });
|
|
815
1497
|
setPlaceTransformId(transformId);
|
|
816
1498
|
},
|
|
817
|
-
[setSelection],
|
|
1499
|
+
[readOnly, setSelection],
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
const resolveDroppedTransformId = useCallback(
|
|
1503
|
+
(dataTransfer: DataTransfer) => {
|
|
1504
|
+
const transformId = readFieldRemapTransformDragData(dataTransfer);
|
|
1505
|
+
const definition = transformId ? transforms.get(transformId) : undefined;
|
|
1506
|
+
if (!transformId || transformId === 'identity' || definition?.id !== transformId) {
|
|
1507
|
+
return undefined;
|
|
1508
|
+
}
|
|
1509
|
+
return transformId;
|
|
1510
|
+
},
|
|
1511
|
+
[transforms],
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
const onCanvasDragOver = useCallback(
|
|
1515
|
+
(event: DragEvent<HTMLDivElement>) => {
|
|
1516
|
+
if (readOnly || !hasFieldRemapTransformDragType(event.dataTransfer)) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
event.preventDefault();
|
|
1520
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
1521
|
+
},
|
|
1522
|
+
[readOnly],
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1525
|
+
const onCanvasDrop = useCallback(
|
|
1526
|
+
(event: DragEvent<HTMLDivElement>) => {
|
|
1527
|
+
if (readOnly) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const transformId = resolveDroppedTransformId(event.dataTransfer);
|
|
1531
|
+
if (!transformId) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
event.preventDefault();
|
|
1535
|
+
event.stopPropagation();
|
|
1536
|
+
placeDraft(transformId, screenToFlowPosition({ x: event.clientX, y: event.clientY }));
|
|
1537
|
+
},
|
|
1538
|
+
[placeDraft, readOnly, resolveDroppedTransformId, screenToFlowPosition],
|
|
818
1539
|
);
|
|
819
1540
|
|
|
820
1541
|
const onNodeClick = useCallback(
|
|
@@ -825,7 +1546,7 @@ function FieldRemapFlowCanvas({
|
|
|
825
1546
|
return;
|
|
826
1547
|
}
|
|
827
1548
|
if (data.kind === 'combine-operator' || data.kind === 'split-operator') {
|
|
828
|
-
if (event.altKey && onOperatorsChange) {
|
|
1549
|
+
if (!readOnly && event.altKey && onOperatorsChange) {
|
|
829
1550
|
onOperatorsChange(removeMappingOperator(operators, data.operatorId));
|
|
830
1551
|
if (selection?.kind === 'operator' && selection.operatorId === data.operatorId) {
|
|
831
1552
|
setSelection(null);
|
|
@@ -838,41 +1559,148 @@ function FieldRemapFlowCanvas({
|
|
|
838
1559
|
if (data.kind !== 'transform') {
|
|
839
1560
|
return;
|
|
840
1561
|
}
|
|
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({
|
|
1562
|
+
const target = {
|
|
860
1563
|
kind: 'transformStep',
|
|
861
1564
|
edgeId: data.mappingEdgeId,
|
|
862
1565
|
stepIndex: data.stepIndex,
|
|
863
|
-
}
|
|
1566
|
+
} as const;
|
|
1567
|
+
if (!readOnly && event.altKey) {
|
|
1568
|
+
commitBulkDelete([target]);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
applyBulkSelectionGesture(target, event);
|
|
1572
|
+
focusBulkTarget(target, 'graph');
|
|
864
1573
|
},
|
|
865
|
-
[
|
|
1574
|
+
[
|
|
1575
|
+
applyBulkSelectionGesture,
|
|
1576
|
+
commitBulkDelete,
|
|
1577
|
+
focusBulkTarget,
|
|
1578
|
+
onOperatorsChange,
|
|
1579
|
+
operators,
|
|
1580
|
+
readOnly,
|
|
1581
|
+
selection,
|
|
1582
|
+
setSelection,
|
|
1583
|
+
],
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
const onEdgeClick = useCallback(
|
|
1587
|
+
(event: MouseEvent, edge: Edge) => {
|
|
1588
|
+
const mappingEdgeId = (edge.data as FieldRemapFlowEdgeData | undefined)?.mappingEdgeId;
|
|
1589
|
+
if (!mappingEdgeId) {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const target = { kind: 'edge', edgeId: mappingEdgeId } as const;
|
|
1593
|
+
applyBulkSelectionGesture(target, event);
|
|
1594
|
+
focusBulkTarget(target, 'graph');
|
|
1595
|
+
},
|
|
1596
|
+
[applyBulkSelectionGesture, focusBulkTarget],
|
|
1597
|
+
);
|
|
1598
|
+
|
|
1599
|
+
const onBulkSelectionKeyDownCapture = useCallback(
|
|
1600
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
1601
|
+
if ((event.key !== 'Enter' && event.key !== ' ') || event.defaultPrevented || event.altKey) {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const target = fieldRemapBulkRefFromKeyboardTarget(event.target);
|
|
1605
|
+
if (!target) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
event.preventDefault();
|
|
1609
|
+
event.stopPropagation();
|
|
1610
|
+
pendingPrimaryCorrectionAckRef.current = undefined;
|
|
1611
|
+
applyBulkSelectionGesture(target, event);
|
|
1612
|
+
},
|
|
1613
|
+
[applyBulkSelectionGesture],
|
|
866
1614
|
);
|
|
867
1615
|
|
|
868
1616
|
const onKeyDown = useCallback(
|
|
869
1617
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
870
1618
|
if (event.key === 'Escape') {
|
|
1619
|
+
if (event.defaultPrevented || (selection === null && drafts.length === 0)) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const mapper = mapperRef.current;
|
|
1623
|
+
const focused = mapper?.ownerDocument.activeElement;
|
|
1624
|
+
const detail = mapper?.querySelector<HTMLElement>(
|
|
1625
|
+
'[data-testid="field-remap-detail"], [data-testid="field-remap-convert-note"]',
|
|
1626
|
+
);
|
|
1627
|
+
const detailSeparator = mapper?.querySelector<HTMLElement>(
|
|
1628
|
+
'.workbench-field-remap-flow__canvas-detail-split > [role="separator"]',
|
|
1629
|
+
);
|
|
1630
|
+
restoreMapperFocusRef.current =
|
|
1631
|
+
detailPresentation === 'rail' &&
|
|
1632
|
+
emptyDetail === 'collapse' &&
|
|
1633
|
+
focused instanceof Element &&
|
|
1634
|
+
(detail?.contains(focused) === true ||
|
|
1635
|
+
(!previewVisible && detailSeparator?.contains(focused) === true));
|
|
1636
|
+
event.preventDefault();
|
|
1637
|
+
event.stopPropagation();
|
|
871
1638
|
setSelection(null);
|
|
872
1639
|
setDrafts([]);
|
|
1640
|
+
setDraftPositions(new Map());
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (readOnly) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (
|
|
1649
|
+
(event.key !== 'Delete' && event.key !== 'Backspace') ||
|
|
1650
|
+
event.ctrlKey ||
|
|
1651
|
+
event.metaKey ||
|
|
1652
|
+
event.altKey ||
|
|
1653
|
+
event.defaultPrevented ||
|
|
1654
|
+
isFieldRemapEditableShortcutTarget(event.target) ||
|
|
1655
|
+
selection === null
|
|
1656
|
+
) {
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
let consumed = false;
|
|
1661
|
+
if (selection.kind === 'edge' || selection.kind === 'transformStep') {
|
|
1662
|
+
const primaryKey = fieldRemapBulkSelectionKey(selection);
|
|
1663
|
+
const refs = bulkSelection.some((ref) => fieldRemapBulkSelectionKey(ref) === primaryKey)
|
|
1664
|
+
? bulkSelection
|
|
1665
|
+
: [...bulkSelection, selection];
|
|
1666
|
+
consumed = commitBulkDelete(refs);
|
|
1667
|
+
} else if (selection.kind === 'operator') {
|
|
1668
|
+
if (
|
|
1669
|
+
onOperatorsChange &&
|
|
1670
|
+
operators.some((operator) => operator.id === selection.operatorId)
|
|
1671
|
+
) {
|
|
1672
|
+
onOperatorsChange(removeMappingOperator(operators, selection.operatorId));
|
|
1673
|
+
setSelection(null);
|
|
1674
|
+
consumed = true;
|
|
1675
|
+
}
|
|
1676
|
+
} else if (selection.kind === 'draft') {
|
|
1677
|
+
if (drafts.some((draft) => draft.localId === selection.localId)) {
|
|
1678
|
+
setDrafts((current) => current.filter((draft) => draft.localId !== selection.localId));
|
|
1679
|
+
removeDraftPosition(selection.localId);
|
|
1680
|
+
setSelection(null);
|
|
1681
|
+
consumed = true;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (consumed) {
|
|
1686
|
+
event.preventDefault();
|
|
1687
|
+
event.stopPropagation();
|
|
873
1688
|
}
|
|
874
1689
|
},
|
|
875
|
-
[
|
|
1690
|
+
[
|
|
1691
|
+
drafts,
|
|
1692
|
+
bulkSelection,
|
|
1693
|
+
commitBulkDelete,
|
|
1694
|
+
detailPresentation,
|
|
1695
|
+
emptyDetail,
|
|
1696
|
+
onOperatorsChange,
|
|
1697
|
+
operators,
|
|
1698
|
+
previewVisible,
|
|
1699
|
+
readOnly,
|
|
1700
|
+
removeDraftPosition,
|
|
1701
|
+
selection,
|
|
1702
|
+
setSelection,
|
|
1703
|
+
],
|
|
876
1704
|
);
|
|
877
1705
|
|
|
878
1706
|
const handlePaneContextMenu = useCallback(
|
|
@@ -896,6 +1724,32 @@ function FieldRemapFlowCanvas({
|
|
|
896
1724
|
[onEdgeContextMenu, selection],
|
|
897
1725
|
);
|
|
898
1726
|
|
|
1727
|
+
const detailPanel = detailVisible ? (
|
|
1728
|
+
<FieldRemapDetailPanel
|
|
1729
|
+
selection={selection}
|
|
1730
|
+
edges={edges}
|
|
1731
|
+
sources={sources}
|
|
1732
|
+
targets={targets}
|
|
1733
|
+
transforms={transforms}
|
|
1734
|
+
readOnly={readOnly}
|
|
1735
|
+
onEdgesChange={onEdgesChange}
|
|
1736
|
+
onSelectionChange={setSelection}
|
|
1737
|
+
drafts={drafts}
|
|
1738
|
+
onDiscardDraft={(localId) => {
|
|
1739
|
+
setDrafts((current) => current.filter((item) => item.localId !== localId));
|
|
1740
|
+
removeDraftPosition(localId);
|
|
1741
|
+
}}
|
|
1742
|
+
operators={operators}
|
|
1743
|
+
onOperatorsChange={onOperatorsChange}
|
|
1744
|
+
emptyDetailTitle={
|
|
1745
|
+
readOnly ? chromeLabels.readOnlyEmptyDetailTitle : chromeLabels.emptyDetailTitle
|
|
1746
|
+
}
|
|
1747
|
+
emptyDetailDescription={
|
|
1748
|
+
readOnly ? chromeLabels.readOnlyEmptyDetailDescription : chromeLabels.emptyDetailDescription
|
|
1749
|
+
}
|
|
1750
|
+
/>
|
|
1751
|
+
) : null;
|
|
1752
|
+
|
|
899
1753
|
return (
|
|
900
1754
|
<div
|
|
901
1755
|
ref={mapperRef}
|
|
@@ -904,11 +1758,15 @@ function FieldRemapFlowCanvas({
|
|
|
904
1758
|
data-chrome={chrome}
|
|
905
1759
|
data-flow-hint={showFlowHint ? 'on' : 'off'}
|
|
906
1760
|
data-bindings-list={showBindingsList ? 'on' : 'off'}
|
|
907
|
-
data-convert-palette={
|
|
1761
|
+
data-convert-palette={showAuthoringPalette ? 'on' : 'off'}
|
|
1762
|
+
data-read-only={readOnly ? 'true' : 'false'}
|
|
908
1763
|
data-empty-detail={emptyDetail}
|
|
1764
|
+
data-detail-presentation={detailPresentation}
|
|
909
1765
|
data-minimap={showMinimap ? 'on' : 'off'}
|
|
910
1766
|
data-hidden-fields={includeHidden ? 'on' : 'off'}
|
|
911
1767
|
data-preview={previewVisible ? 'on' : 'off'}
|
|
1768
|
+
tabIndex={-1}
|
|
1769
|
+
onKeyDownCapture={onBulkSelectionKeyDownCapture}
|
|
912
1770
|
onKeyDown={onKeyDown}
|
|
913
1771
|
>
|
|
914
1772
|
{showFlowHint ? (
|
|
@@ -920,10 +1778,25 @@ function FieldRemapFlowCanvas({
|
|
|
920
1778
|
</p>
|
|
921
1779
|
) : null}
|
|
922
1780
|
|
|
1781
|
+
{connectionFeedback ? (
|
|
1782
|
+
<p className="workbench-field-remap-demo__warn" role="status">
|
|
1783
|
+
{connectionFeedback.reason}
|
|
1784
|
+
</p>
|
|
1785
|
+
) : null}
|
|
1786
|
+
|
|
1787
|
+
{conflicts.length > 0 ? (
|
|
1788
|
+
<p className="workbench-field-remap-demo__warn" role="status">
|
|
1789
|
+
Warning: parent and child fields are both mapped (
|
|
1790
|
+
{conflicts.map((item) => `${item.parentId} / ${item.childId}`).join('; ')}). Prefer one
|
|
1791
|
+
level.
|
|
1792
|
+
</p>
|
|
1793
|
+
) : null}
|
|
1794
|
+
|
|
923
1795
|
<FieldRemapSplitWorkspace
|
|
924
1796
|
layout={workspaceLayout}
|
|
925
|
-
|
|
926
|
-
|
|
1797
|
+
reserveHiddenDetailSplit={detailPresentation === 'rail'}
|
|
1798
|
+
showConvertPalette={showAuthoringPalette}
|
|
1799
|
+
showDetail={sideRailVisible}
|
|
927
1800
|
surface={
|
|
928
1801
|
selection?.kind === 'transformStep'
|
|
929
1802
|
? 'convert-note'
|
|
@@ -935,7 +1808,7 @@ function FieldRemapFlowCanvas({
|
|
|
935
1808
|
}
|
|
936
1809
|
>
|
|
937
1810
|
<>
|
|
938
|
-
{
|
|
1811
|
+
{showAuthoringPalette ? (
|
|
939
1812
|
<FieldRemapConvertPalette
|
|
940
1813
|
transforms={transforms}
|
|
941
1814
|
selectedTransformId={placeTransformId}
|
|
@@ -969,15 +1842,27 @@ function FieldRemapFlowCanvas({
|
|
|
969
1842
|
nodes={nodes}
|
|
970
1843
|
edges={flowEdges}
|
|
971
1844
|
nodeTypes={nodeTypes}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1845
|
+
edgeTypes={edgeTypes}
|
|
1846
|
+
onNodesChange={onProjectedNodesChange}
|
|
1847
|
+
onEdgesChange={onProjectedFlowEdgesChange}
|
|
1848
|
+
onConnect={readOnly ? undefined : onConnect}
|
|
1849
|
+
onConnectStart={readOnly ? undefined : onConnectStart}
|
|
1850
|
+
onConnectEnd={readOnly ? undefined : onConnectEnd}
|
|
1851
|
+
onEdgesDelete={readOnly ? undefined : onEdgesDelete}
|
|
1852
|
+
onDragOver={onCanvasDragOver}
|
|
1853
|
+
onDrop={onCanvasDrop}
|
|
976
1854
|
onNodeClick={onNodeClick}
|
|
1855
|
+
onEdgeClick={onEdgeClick}
|
|
977
1856
|
onPaneContextMenu={onPaneContextMenu ? handlePaneContextMenu : undefined}
|
|
978
1857
|
onNodeContextMenu={onNodeContextMenu ? handleNodeContextMenu : undefined}
|
|
979
1858
|
onEdgeContextMenu={onEdgeContextMenu ? handleEdgeContextMenu : undefined}
|
|
980
1859
|
isValidConnection={isValidConnection}
|
|
1860
|
+
nodesDraggable={!readOnly}
|
|
1861
|
+
nodesConnectable={!readOnly}
|
|
1862
|
+
edgesReconnectable={!readOnly}
|
|
1863
|
+
elementsSelectable={false}
|
|
1864
|
+
ariaLabelConfig={flowAriaLabelConfig}
|
|
1865
|
+
deleteKeyCode={null}
|
|
981
1866
|
fitView
|
|
982
1867
|
fitViewOptions={DEFAULT_FIT_VIEW_OPTIONS}
|
|
983
1868
|
proOptions={{ hideAttribution: true }}
|
|
@@ -1094,25 +1979,7 @@ function FieldRemapFlowCanvas({
|
|
|
1094
1979
|
</div>
|
|
1095
1980
|
|
|
1096
1981
|
<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}
|
|
1982
|
+
{detailPresentation === 'rail' ? detailPanel : null}
|
|
1116
1983
|
{previewVisible && preview ? (
|
|
1117
1984
|
<FieldRemapPreviewRail
|
|
1118
1985
|
preview={preview}
|
|
@@ -1129,6 +1996,18 @@ function FieldRemapFlowCanvas({
|
|
|
1129
1996
|
</div>
|
|
1130
1997
|
</FieldRemapSplitWorkspace>
|
|
1131
1998
|
|
|
1999
|
+
{detailPresentation === 'modal' && selection !== null ? (
|
|
2000
|
+
<Suspense fallback={null}>
|
|
2001
|
+
<FieldRemapModalDetail
|
|
2002
|
+
closeLabel={chromeLabels.closeDetailModal ?? 'Close details'}
|
|
2003
|
+
title={chromeLabels.detailModalTitle ?? 'Mapping details'}
|
|
2004
|
+
onClose={closeModalDetail}
|
|
2005
|
+
>
|
|
2006
|
+
{detailPanel}
|
|
2007
|
+
</FieldRemapModalDetail>
|
|
2008
|
+
</Suspense>
|
|
2009
|
+
) : null}
|
|
2010
|
+
|
|
1132
2011
|
{showBindingsList ? (
|
|
1133
2012
|
<div className="workbench-field-remap-flow__bindings" data-testid="field-remap-edges">
|
|
1134
2013
|
<h4>{chromeLabels.bindingsTitle}</h4>
|
|
@@ -1145,21 +2024,36 @@ function FieldRemapFlowCanvas({
|
|
|
1145
2024
|
});
|
|
1146
2025
|
const defaultAddId = appendCatalog[0]?.id;
|
|
1147
2026
|
const listContext = canEditListContext(edge, sources, targets);
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
2027
|
+
const edgeRef = { kind: 'edge', edgeId: edge.id } as const;
|
|
2028
|
+
const edgeKey = fieldRemapBulkSelectionKey(edgeRef);
|
|
2029
|
+
const edgeSelected = bulkSelectionKeys.has(edgeKey);
|
|
2030
|
+
const visibleRemoveRefs = edgeSelected ? bulkSelection : [edgeRef];
|
|
2031
|
+
const laneSelected = canonicalBulkRefs.some(
|
|
2032
|
+
(ref) =>
|
|
2033
|
+
ref.edgeId === edge.id && bulkSelectionKeys.has(fieldRemapBulkSelectionKey(ref)),
|
|
2034
|
+
);
|
|
2035
|
+
const edgeIsPrimary = selection?.kind === 'edge' && selection.edgeId === edge.id;
|
|
1151
2036
|
|
|
1152
2037
|
return (
|
|
1153
2038
|
<li
|
|
1154
2039
|
key={edge.id}
|
|
1155
|
-
className={
|
|
2040
|
+
className={laneSelected ? 'is-selected' : undefined}
|
|
1156
2041
|
data-testid={`field-remap-lane-${edge.id}`}
|
|
1157
2042
|
>
|
|
1158
2043
|
<button
|
|
2044
|
+
ref={(element) => registerBulkFocusTarget(edgeKey, 'list', element)}
|
|
1159
2045
|
type="button"
|
|
1160
|
-
|
|
2046
|
+
aria-pressed={edgeSelected}
|
|
2047
|
+
className={
|
|
2048
|
+
edgeSelected
|
|
2049
|
+
? 'workbench-field-remap-flow__binding-select is-selected'
|
|
2050
|
+
: 'workbench-field-remap-flow__binding-select'
|
|
2051
|
+
}
|
|
2052
|
+
data-field-remap-bulk-edge-id={edge.id}
|
|
2053
|
+
data-field-remap-bulk-kind="edge"
|
|
2054
|
+
data-primary={edgeIsPrimary ? 'true' : 'false'}
|
|
1161
2055
|
data-testid={`field-remap-select-edge-${edge.id}`}
|
|
1162
|
-
onClick={() =>
|
|
2056
|
+
onClick={(event) => applyBulkSelectionGesture(edgeRef, event)}
|
|
1163
2057
|
>
|
|
1164
2058
|
<code>
|
|
1165
2059
|
{edge.sourceFieldId} →{' '}
|
|
@@ -1170,70 +2064,69 @@ function FieldRemapFlowCanvas({
|
|
|
1170
2064
|
{edge.itemEdges ? ` · ${edge.itemEdges.length} item fields` : ''}
|
|
1171
2065
|
</code>
|
|
1172
2066
|
</button>
|
|
1173
|
-
|
|
1174
|
-
|
|
2067
|
+
{!readOnly ? (
|
|
2068
|
+
<span className="workbench-field-remap-mapper__edge-actions">
|
|
2069
|
+
{(edge.transformIds?.length ?? 0) < MAX_TRANSFORM_CHAIN && defaultAddId ? (
|
|
2070
|
+
<IconButton
|
|
2071
|
+
compact
|
|
2072
|
+
type="button"
|
|
2073
|
+
data-testid={`field-remap-add-node-${edge.id}`}
|
|
2074
|
+
icon="codicon-add"
|
|
2075
|
+
label={chromeLabels.addTransform}
|
|
2076
|
+
onClick={() => {
|
|
2077
|
+
const next = addTransformStepToEdge(edge, defaultAddId, {
|
|
2078
|
+
registry: transforms,
|
|
2079
|
+
sourceType: portTypes.sourceType,
|
|
2080
|
+
targetType: portTypes.targetType,
|
|
2081
|
+
});
|
|
2082
|
+
if (!next) {
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
2086
|
+
setSelection({
|
|
2087
|
+
kind: 'transformStep',
|
|
2088
|
+
edgeId: edge.id,
|
|
2089
|
+
stepIndex: (next.transformIds?.length ?? 1) - 1,
|
|
2090
|
+
});
|
|
2091
|
+
}}
|
|
2092
|
+
/>
|
|
2093
|
+
) : null}
|
|
2094
|
+
{listContext ? (
|
|
2095
|
+
<IconButton
|
|
2096
|
+
compact
|
|
2097
|
+
type="button"
|
|
2098
|
+
data-testid={`field-remap-edit-items-${edge.id}`}
|
|
2099
|
+
icon="codicon-edit"
|
|
2100
|
+
label={chromeLabels.editItems}
|
|
2101
|
+
onClick={() => {
|
|
2102
|
+
if (!edge.itemEdges) {
|
|
2103
|
+
onEdgesChange(
|
|
2104
|
+
edges.map((item) =>
|
|
2105
|
+
item.id === edge.id ? enableListContextOnEdge(item) : item,
|
|
2106
|
+
),
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
setSelection({ kind: 'edge', edgeId: edge.id });
|
|
2110
|
+
}}
|
|
2111
|
+
/>
|
|
2112
|
+
) : null}
|
|
1175
2113
|
<IconButton
|
|
1176
2114
|
compact
|
|
1177
2115
|
type="button"
|
|
1178
|
-
data-testid={`field-remap-
|
|
1179
|
-
icon="codicon-
|
|
1180
|
-
label={
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
});
|
|
1187
|
-
if (!next) {
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
1191
|
-
setSelection({
|
|
1192
|
-
kind: 'transformStep',
|
|
1193
|
-
edgeId: edge.id,
|
|
1194
|
-
stepIndex: (next.transformIds?.length ?? 1) - 1,
|
|
1195
|
-
});
|
|
1196
|
-
}}
|
|
1197
|
-
/>
|
|
1198
|
-
) : null}
|
|
1199
|
-
{listContext ? (
|
|
1200
|
-
<IconButton
|
|
1201
|
-
compact
|
|
1202
|
-
type="button"
|
|
1203
|
-
data-testid={`field-remap-edit-items-${edge.id}`}
|
|
1204
|
-
icon="codicon-edit"
|
|
1205
|
-
label={chromeLabels.editItems}
|
|
2116
|
+
data-testid={`field-remap-remove-edge-${edge.id}`}
|
|
2117
|
+
icon="codicon-trash"
|
|
2118
|
+
label={
|
|
2119
|
+
visibleRemoveRefs.length > 1
|
|
2120
|
+
? `Remove ${visibleRemoveRefs.length} selected items`
|
|
2121
|
+
: chromeLabels.removeBinding
|
|
2122
|
+
}
|
|
2123
|
+
variant="danger"
|
|
1206
2124
|
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 });
|
|
2125
|
+
commitBulkDelete(visibleRemoveRefs);
|
|
1215
2126
|
}}
|
|
1216
2127
|
/>
|
|
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>
|
|
2128
|
+
</span>
|
|
2129
|
+
) : null}
|
|
1237
2130
|
</li>
|
|
1238
2131
|
);
|
|
1239
2132
|
})}
|