@workbench-kit/shell-react 0.0.2-prototype.0.2.40 → 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
|
@@ -204,6 +204,8 @@ export function mappingToFlowGraph(input: {
|
|
|
204
204
|
readonly sourceFieldId?: string;
|
|
205
205
|
readonly targetSlotId?: string;
|
|
206
206
|
}[];
|
|
207
|
+
/** Mapper-local positions for ephemeral drafts created by canvas drop. */
|
|
208
|
+
readonly draftPositions?: ReadonlyMap<string, { readonly x: number; readonly y: number }>;
|
|
207
209
|
/** Document v2 n→m operators (display / multi-port Flow nodes). */
|
|
208
210
|
readonly operators?: readonly MappingOperator[];
|
|
209
211
|
}): { nodes: Node<FieldRemapFlowNodeData>[]; edges: Edge<FieldRemapFlowEdgeData>[] } {
|
|
@@ -221,7 +223,6 @@ export function mappingToFlowGraph(input: {
|
|
|
221
223
|
schemaRole: 'Source schema',
|
|
222
224
|
ports: sourcePorts,
|
|
223
225
|
},
|
|
224
|
-
draggable: true,
|
|
225
226
|
},
|
|
226
227
|
{
|
|
227
228
|
id: TARGET_OBJECT_NODE_ID,
|
|
@@ -233,7 +234,6 @@ export function mappingToFlowGraph(input: {
|
|
|
233
234
|
schemaRole: 'Target schema',
|
|
234
235
|
ports: targetPorts,
|
|
235
236
|
},
|
|
236
|
-
draggable: true,
|
|
237
237
|
},
|
|
238
238
|
];
|
|
239
239
|
|
|
@@ -272,7 +272,6 @@ export function mappingToFlowGraph(input: {
|
|
|
272
272
|
transformId,
|
|
273
273
|
label: definition?.label ?? transformId,
|
|
274
274
|
},
|
|
275
|
-
draggable: true,
|
|
276
275
|
});
|
|
277
276
|
});
|
|
278
277
|
|
|
@@ -315,7 +314,7 @@ export function mappingToFlowGraph(input: {
|
|
|
315
314
|
nodes.push({
|
|
316
315
|
id: nodeId,
|
|
317
316
|
type: 'fieldRemapDraftTransform',
|
|
318
|
-
position: {
|
|
317
|
+
position: input.draftPositions?.get(draft.localId) ?? {
|
|
319
318
|
x: TRANSFORM_X,
|
|
320
319
|
y: 40 + (input.edges.length + draftIndex) * TRANSFORM_ROW_GAP,
|
|
321
320
|
},
|
|
@@ -327,7 +326,6 @@ export function mappingToFlowGraph(input: {
|
|
|
327
326
|
sourceFieldId: draft.sourceFieldId,
|
|
328
327
|
targetSlotId: draft.targetSlotId,
|
|
329
328
|
},
|
|
330
|
-
draggable: true,
|
|
331
329
|
});
|
|
332
330
|
|
|
333
331
|
if (draft.sourceFieldId) {
|
|
@@ -372,7 +370,6 @@ export function mappingToFlowGraph(input: {
|
|
|
372
370
|
inputFieldIds: operator.inputFieldIds,
|
|
373
371
|
outputSlotId: operator.outputSlotId,
|
|
374
372
|
},
|
|
375
|
-
draggable: true,
|
|
376
373
|
});
|
|
377
374
|
for (const fieldId of operator.inputFieldIds) {
|
|
378
375
|
flowEdges.push({
|
|
@@ -410,7 +407,6 @@ export function mappingToFlowGraph(input: {
|
|
|
410
407
|
inputFieldId: operator.inputFieldId,
|
|
411
408
|
outputSlotIds: operator.outputSlotIds,
|
|
412
409
|
},
|
|
413
|
-
draggable: true,
|
|
414
410
|
});
|
|
415
411
|
if (operator.inputFieldId) {
|
|
416
412
|
flowEdges.push({
|
|
@@ -443,13 +439,82 @@ export function mappingToFlowGraph(input: {
|
|
|
443
439
|
* Optional field/slot lookup for type-gated connects.
|
|
444
440
|
* When omitted, only the topology allowlist is applied (tests / callers without shapes).
|
|
445
441
|
*/
|
|
442
|
+
export type FieldRemapFlowConnectionInput = {
|
|
443
|
+
readonly source: string | null | undefined;
|
|
444
|
+
readonly target: string | null | undefined;
|
|
445
|
+
readonly sourceHandle?: string | null;
|
|
446
|
+
readonly targetHandle?: string | null;
|
|
447
|
+
};
|
|
448
|
+
|
|
446
449
|
export type IsValidFieldRemapFlowConnectionContext = {
|
|
447
450
|
readonly sources: readonly SourceField[];
|
|
448
451
|
readonly targets: readonly TargetSlot[];
|
|
449
452
|
readonly edges: readonly MappingEdge[];
|
|
450
453
|
readonly transforms: ValueTransformRegistry;
|
|
454
|
+
readonly drafts?: readonly {
|
|
455
|
+
readonly localId: string;
|
|
456
|
+
readonly transformId: string;
|
|
457
|
+
readonly sourceFieldId?: string;
|
|
458
|
+
readonly targetSlotId?: string;
|
|
459
|
+
}[];
|
|
451
460
|
};
|
|
452
461
|
|
|
462
|
+
export type FieldRemapFlowConnectionRejectionReason =
|
|
463
|
+
| 'incomplete-connection'
|
|
464
|
+
| 'unsupported-topology'
|
|
465
|
+
| 'missing-port-endpoint'
|
|
466
|
+
| 'missing-draft-endpoint'
|
|
467
|
+
| 'missing-transform-edge'
|
|
468
|
+
| 'missing-transform-step'
|
|
469
|
+
| 'missing-transform-definition'
|
|
470
|
+
| 'same-edge-transform-splice'
|
|
471
|
+
| 'transform-chain-limit'
|
|
472
|
+
| 'incompatible-port-types';
|
|
473
|
+
|
|
474
|
+
export type FieldRemapFlowConnectionEvaluation =
|
|
475
|
+
| { readonly status: 'accepted' }
|
|
476
|
+
| { readonly status: 'rewire'; readonly impactedEdgeIds: readonly string[] }
|
|
477
|
+
| {
|
|
478
|
+
readonly status: 'rejected';
|
|
479
|
+
readonly reason: FieldRemapFlowConnectionRejectionReason;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const acceptedConnection = { status: 'accepted' } as const;
|
|
483
|
+
|
|
484
|
+
function rejectConnection(
|
|
485
|
+
reason: FieldRemapFlowConnectionRejectionReason,
|
|
486
|
+
): FieldRemapFlowConnectionEvaluation {
|
|
487
|
+
return { status: 'rejected', reason };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function acceptOrRewire(impactedEdgeIds: readonly string[]): FieldRemapFlowConnectionEvaluation {
|
|
491
|
+
return impactedEdgeIds.length > 0 ? { status: 'rewire', impactedEdgeIds } : acceptedConnection;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function evaluatePortCompatibility(
|
|
495
|
+
context: Pick<IsValidFieldRemapFlowConnectionContext, 'transforms'>,
|
|
496
|
+
sourceType: SourceField['dataType'],
|
|
497
|
+
targetType: TargetSlot['dataType'],
|
|
498
|
+
transformIds: readonly string[],
|
|
499
|
+
): FieldRemapFlowConnectionEvaluation | undefined {
|
|
500
|
+
if (!transformIds.every((transformId) => context.transforms.get(transformId) !== undefined)) {
|
|
501
|
+
return rejectConnection('missing-transform-definition');
|
|
502
|
+
}
|
|
503
|
+
return arePortsCompatible({ sourceType, targetType, transformIds, registry: context.transforms })
|
|
504
|
+
? undefined
|
|
505
|
+
: rejectConnection('incompatible-port-types');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function targetRewireEdgeIds(
|
|
509
|
+
edges: readonly MappingEdge[],
|
|
510
|
+
targetSlotId: string,
|
|
511
|
+
excludedEdgeId?: string,
|
|
512
|
+
): string[] {
|
|
513
|
+
return edges
|
|
514
|
+
.filter((edge) => edge.targetSlotId === targetSlotId && edge.id !== excludedEdgeId)
|
|
515
|
+
.map((edge) => edge.id);
|
|
516
|
+
}
|
|
517
|
+
|
|
453
518
|
function resolveConnectionPortIds(connection: {
|
|
454
519
|
readonly source: string | null | undefined;
|
|
455
520
|
readonly target: string | null | undefined;
|
|
@@ -490,26 +555,14 @@ function resolveConnectionPortIds(connection: {
|
|
|
490
555
|
* mismatches (including transform-mediated chains on an existing target edge)
|
|
491
556
|
* are rejected.
|
|
492
557
|
*/
|
|
493
|
-
export function
|
|
494
|
-
connection:
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
readonly sourceHandle?: string | null;
|
|
498
|
-
readonly targetHandle?: string | null;
|
|
499
|
-
},
|
|
500
|
-
context?: IsValidFieldRemapFlowConnectionContext & {
|
|
501
|
-
readonly drafts?: readonly {
|
|
502
|
-
readonly localId: string;
|
|
503
|
-
readonly transformId: string;
|
|
504
|
-
readonly sourceFieldId?: string;
|
|
505
|
-
readonly targetSlotId?: string;
|
|
506
|
-
}[];
|
|
507
|
-
},
|
|
508
|
-
): boolean {
|
|
558
|
+
export function evaluateFieldRemapFlowConnection(
|
|
559
|
+
connection: FieldRemapFlowConnectionInput,
|
|
560
|
+
context?: IsValidFieldRemapFlowConnectionContext,
|
|
561
|
+
): FieldRemapFlowConnectionEvaluation {
|
|
509
562
|
const source = connection.source;
|
|
510
563
|
const target = connection.target;
|
|
511
564
|
if (!source || !target) {
|
|
512
|
-
return
|
|
565
|
+
return rejectConnection('incomplete-connection');
|
|
513
566
|
}
|
|
514
567
|
|
|
515
568
|
const draftTargetId = parseDraftTransformNodeId(target);
|
|
@@ -517,39 +570,72 @@ export function isValidFieldRemapFlowConnection(
|
|
|
517
570
|
|
|
518
571
|
if (source === SOURCE_OBJECT_NODE_ID && draftTargetId && connection.sourceHandle) {
|
|
519
572
|
if (!context) {
|
|
520
|
-
return
|
|
573
|
+
return acceptedConnection;
|
|
521
574
|
}
|
|
522
575
|
const draft = context.drafts?.find((item) => item.localId === draftTargetId);
|
|
523
576
|
if (!draft) {
|
|
524
|
-
return
|
|
577
|
+
return rejectConnection('missing-draft-endpoint');
|
|
525
578
|
}
|
|
526
579
|
const sourceField = findSourceField(context.sources, connection.sourceHandle);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
580
|
+
if (!sourceField) {
|
|
581
|
+
return rejectConnection('missing-port-endpoint');
|
|
582
|
+
}
|
|
583
|
+
const targetSlot = draft.targetSlotId
|
|
584
|
+
? findTargetSlot(context.targets, draft.targetSlotId)
|
|
585
|
+
: undefined;
|
|
586
|
+
if (draft.targetSlotId && !targetSlot) {
|
|
587
|
+
return rejectConnection('missing-draft-endpoint');
|
|
588
|
+
}
|
|
589
|
+
const rejection = evaluatePortCompatibility(
|
|
590
|
+
context,
|
|
591
|
+
sourceField.dataType,
|
|
592
|
+
targetSlot?.dataType ?? 'unknown',
|
|
593
|
+
[draft.transformId],
|
|
594
|
+
);
|
|
595
|
+
if (rejection) {
|
|
596
|
+
return rejection;
|
|
597
|
+
}
|
|
598
|
+
return acceptOrRewire(
|
|
599
|
+
draft.targetSlotId ? targetRewireEdgeIds(context.edges, draft.targetSlotId) : [],
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (source === SOURCE_OBJECT_NODE_ID && draftTargetId) {
|
|
604
|
+
return rejectConnection('incomplete-connection');
|
|
533
605
|
}
|
|
534
606
|
|
|
535
607
|
if (draftSourceId && target === TARGET_OBJECT_NODE_ID && connection.targetHandle) {
|
|
536
608
|
if (!context) {
|
|
537
|
-
return
|
|
609
|
+
return acceptedConnection;
|
|
538
610
|
}
|
|
539
611
|
const draft = context.drafts?.find((item) => item.localId === draftSourceId);
|
|
540
612
|
if (!draft) {
|
|
541
|
-
return
|
|
613
|
+
return rejectConnection('missing-draft-endpoint');
|
|
542
614
|
}
|
|
543
|
-
const sourceType = draft.sourceFieldId
|
|
544
|
-
? findSourceField(context.sources, draft.sourceFieldId)?.dataType
|
|
545
|
-
: undefined;
|
|
546
615
|
const targetSlot = findTargetSlot(context.targets, connection.targetHandle);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
616
|
+
if (!targetSlot) {
|
|
617
|
+
return rejectConnection('missing-port-endpoint');
|
|
618
|
+
}
|
|
619
|
+
const sourceField = draft.sourceFieldId
|
|
620
|
+
? findSourceField(context.sources, draft.sourceFieldId)
|
|
621
|
+
: undefined;
|
|
622
|
+
if (draft.sourceFieldId && !sourceField) {
|
|
623
|
+
return rejectConnection('missing-draft-endpoint');
|
|
624
|
+
}
|
|
625
|
+
const rejection = evaluatePortCompatibility(
|
|
626
|
+
context,
|
|
627
|
+
sourceField?.dataType,
|
|
628
|
+
targetSlot.dataType,
|
|
629
|
+
[draft.transformId],
|
|
630
|
+
);
|
|
631
|
+
if (rejection) {
|
|
632
|
+
return rejection;
|
|
633
|
+
}
|
|
634
|
+
return acceptOrRewire(targetRewireEdgeIds(context.edges, connection.targetHandle));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (draftSourceId && target === TARGET_OBJECT_NODE_ID) {
|
|
638
|
+
return rejectConnection('incomplete-connection');
|
|
553
639
|
}
|
|
554
640
|
|
|
555
641
|
const xfTarget = parseTransformNodeId(target);
|
|
@@ -557,80 +643,109 @@ export function isValidFieldRemapFlowConnection(
|
|
|
557
643
|
|
|
558
644
|
if (source === SOURCE_OBJECT_NODE_ID && xfTarget && connection.sourceHandle) {
|
|
559
645
|
if (!context) {
|
|
560
|
-
return
|
|
646
|
+
return acceptedConnection;
|
|
561
647
|
}
|
|
562
648
|
const edge = context.edges.find((item) => item.id === xfTarget.mappingEdgeId);
|
|
563
649
|
if (!edge) {
|
|
564
|
-
return
|
|
650
|
+
return rejectConnection('missing-transform-edge');
|
|
565
651
|
}
|
|
566
652
|
const chain = edge.transformIds ?? [];
|
|
567
653
|
if (xfTarget.stepIndex >= chain.length) {
|
|
568
|
-
return
|
|
654
|
+
return rejectConnection('missing-transform-step');
|
|
569
655
|
}
|
|
570
656
|
const sourceField = findSourceField(context.sources, connection.sourceHandle);
|
|
571
657
|
const targetSlot = findTargetSlot(context.targets, edge.targetSlotId);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
658
|
+
if (!sourceField || !targetSlot) {
|
|
659
|
+
return rejectConnection('missing-port-endpoint');
|
|
660
|
+
}
|
|
661
|
+
const retainedChain = chain.slice(xfTarget.stepIndex);
|
|
662
|
+
return (
|
|
663
|
+
evaluatePortCompatibility(
|
|
664
|
+
context,
|
|
665
|
+
sourceField.dataType,
|
|
666
|
+
targetSlot.dataType,
|
|
667
|
+
retainedChain,
|
|
668
|
+
) ?? acceptedConnection
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (source === SOURCE_OBJECT_NODE_ID && xfTarget) {
|
|
673
|
+
return rejectConnection('incomplete-connection');
|
|
578
674
|
}
|
|
579
675
|
|
|
580
676
|
if (xfSource && target === TARGET_OBJECT_NODE_ID && connection.targetHandle) {
|
|
581
677
|
if (!context) {
|
|
582
|
-
return
|
|
678
|
+
return acceptedConnection;
|
|
583
679
|
}
|
|
584
680
|
const edge = context.edges.find((item) => item.id === xfSource.mappingEdgeId);
|
|
585
681
|
if (!edge) {
|
|
586
|
-
return
|
|
682
|
+
return rejectConnection('missing-transform-edge');
|
|
587
683
|
}
|
|
588
684
|
const chain = edge.transformIds ?? [];
|
|
589
685
|
if (xfSource.stepIndex >= chain.length) {
|
|
590
|
-
return
|
|
686
|
+
return rejectConnection('missing-transform-step');
|
|
591
687
|
}
|
|
592
688
|
const sourceField = findSourceField(context.sources, edge.sourceFieldId);
|
|
593
689
|
const targetSlot = findTargetSlot(context.targets, connection.targetHandle);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
690
|
+
if (!sourceField || !targetSlot) {
|
|
691
|
+
return rejectConnection('missing-port-endpoint');
|
|
692
|
+
}
|
|
693
|
+
const retainedChain = chain.slice(0, xfSource.stepIndex + 1);
|
|
694
|
+
const rejection = evaluatePortCompatibility(
|
|
695
|
+
context,
|
|
696
|
+
sourceField.dataType,
|
|
697
|
+
targetSlot.dataType,
|
|
698
|
+
retainedChain,
|
|
699
|
+
);
|
|
700
|
+
if (rejection) {
|
|
701
|
+
return rejection;
|
|
702
|
+
}
|
|
703
|
+
return acceptOrRewire(targetRewireEdgeIds(context.edges, connection.targetHandle, edge.id));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (xfSource && target === TARGET_OBJECT_NODE_ID) {
|
|
707
|
+
return rejectConnection('incomplete-connection');
|
|
600
708
|
}
|
|
601
709
|
|
|
602
710
|
if (xfSource && xfTarget) {
|
|
603
711
|
if (xfSource.mappingEdgeId === xfTarget.mappingEdgeId) {
|
|
604
|
-
return
|
|
712
|
+
return rejectConnection('same-edge-transform-splice');
|
|
605
713
|
}
|
|
606
714
|
if (!context) {
|
|
607
|
-
return
|
|
715
|
+
return acceptedConnection;
|
|
608
716
|
}
|
|
609
717
|
const edgeA = context.edges.find((item) => item.id === xfSource.mappingEdgeId);
|
|
610
718
|
const edgeB = context.edges.find((item) => item.id === xfTarget.mappingEdgeId);
|
|
611
719
|
if (!edgeA || !edgeB) {
|
|
612
|
-
return
|
|
720
|
+
return rejectConnection('missing-transform-edge');
|
|
613
721
|
}
|
|
614
722
|
const chainA = edgeA.transformIds ?? [];
|
|
615
723
|
const chainB = edgeB.transformIds ?? [];
|
|
616
724
|
if (xfSource.stepIndex >= chainA.length || xfTarget.stepIndex >= chainB.length) {
|
|
617
|
-
return
|
|
725
|
+
return rejectConnection('missing-transform-step');
|
|
618
726
|
}
|
|
619
727
|
const merged = [
|
|
620
728
|
...chainA.slice(0, xfSource.stepIndex + 1),
|
|
621
729
|
...chainB.slice(xfTarget.stepIndex),
|
|
622
730
|
];
|
|
623
731
|
if (merged.length === 0 || merged.length > MAX_TRANSFORM_CHAIN) {
|
|
624
|
-
return
|
|
732
|
+
return rejectConnection('transform-chain-limit');
|
|
625
733
|
}
|
|
626
734
|
const sourceField = findSourceField(context.sources, edgeA.sourceFieldId);
|
|
627
735
|
const targetSlot = findTargetSlot(context.targets, edgeB.targetSlotId);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
736
|
+
if (!sourceField || !targetSlot) {
|
|
737
|
+
return rejectConnection('missing-port-endpoint');
|
|
738
|
+
}
|
|
739
|
+
const rejection = evaluatePortCompatibility(
|
|
740
|
+
context,
|
|
741
|
+
sourceField.dataType,
|
|
742
|
+
targetSlot.dataType,
|
|
743
|
+
merged,
|
|
744
|
+
);
|
|
745
|
+
if (rejection) {
|
|
746
|
+
return rejection;
|
|
747
|
+
}
|
|
748
|
+
return acceptOrRewire([edgeA.id]);
|
|
634
749
|
}
|
|
635
750
|
|
|
636
751
|
const topologyOk =
|
|
@@ -639,28 +754,44 @@ export function isValidFieldRemapFlowConnection(
|
|
|
639
754
|
Boolean(connection.sourceHandle && connection.targetHandle);
|
|
640
755
|
|
|
641
756
|
if (!topologyOk) {
|
|
642
|
-
return
|
|
757
|
+
return source === SOURCE_OBJECT_NODE_ID && target === TARGET_OBJECT_NODE_ID
|
|
758
|
+
? rejectConnection('incomplete-connection')
|
|
759
|
+
: rejectConnection('unsupported-topology');
|
|
643
760
|
}
|
|
644
761
|
|
|
645
762
|
if (!context) {
|
|
646
|
-
return
|
|
763
|
+
return acceptedConnection;
|
|
647
764
|
}
|
|
648
765
|
|
|
649
766
|
const { sourceFieldId, targetSlotId } = resolveConnectionPortIds(connection);
|
|
650
767
|
if (!sourceFieldId || !targetSlotId) {
|
|
651
|
-
return
|
|
768
|
+
return rejectConnection('incomplete-connection');
|
|
652
769
|
}
|
|
653
770
|
|
|
654
771
|
const sourceField = findSourceField(context.sources, sourceFieldId);
|
|
655
772
|
const targetSlot = findTargetSlot(context.targets, targetSlotId);
|
|
773
|
+
if (!sourceField || !targetSlot) {
|
|
774
|
+
return rejectConnection('missing-port-endpoint');
|
|
775
|
+
}
|
|
656
776
|
const existing = context.edges.find((edge) => edge.targetSlotId === targetSlotId);
|
|
777
|
+
const retainedChain = existing?.transformIds ?? [];
|
|
778
|
+
const rejection = evaluatePortCompatibility(
|
|
779
|
+
context,
|
|
780
|
+
sourceField.dataType,
|
|
781
|
+
targetSlot.dataType,
|
|
782
|
+
retainedChain,
|
|
783
|
+
);
|
|
784
|
+
if (rejection) {
|
|
785
|
+
return rejection;
|
|
786
|
+
}
|
|
787
|
+
return acceptOrRewire(targetRewireEdgeIds(context.edges, targetSlotId));
|
|
788
|
+
}
|
|
657
789
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
});
|
|
790
|
+
export function isValidFieldRemapFlowConnection(
|
|
791
|
+
connection: FieldRemapFlowConnectionInput,
|
|
792
|
+
context?: IsValidFieldRemapFlowConnectionContext,
|
|
793
|
+
): boolean {
|
|
794
|
+
return evaluateFieldRemapFlowConnection(connection, context).status !== 'rejected';
|
|
664
795
|
}
|
|
665
796
|
|
|
666
797
|
/** Result of a persisted (non-draft) Flow connect that updates mapping edges. */
|
|
@@ -690,16 +821,13 @@ export function connectionToMappingEdge(input: {
|
|
|
690
821
|
}
|
|
691
822
|
|
|
692
823
|
const existing = input.existing.find((edge) => edge.targetSlotId === targetSlotId);
|
|
824
|
+
if (existing) {
|
|
825
|
+
return { ...existing, sourceFieldId, targetSlotId };
|
|
826
|
+
}
|
|
693
827
|
return {
|
|
694
|
-
id:
|
|
828
|
+
id: `e-${sourceFieldId}-${targetSlotId}-${Date.now()}`,
|
|
695
829
|
sourceFieldId,
|
|
696
830
|
targetSlotId,
|
|
697
|
-
...(existing?.transformIds ? { transformIds: existing.transformIds } : {}),
|
|
698
|
-
...(existing?.transformOptionSteps
|
|
699
|
-
? { transformOptionSteps: existing.transformOptionSteps }
|
|
700
|
-
: {}),
|
|
701
|
-
...(existing?.itemSourcePath ? { itemSourcePath: existing.itemSourcePath } : {}),
|
|
702
|
-
...(existing?.itemEdges ? { itemEdges: existing.itemEdges } : {}),
|
|
703
831
|
};
|
|
704
832
|
}
|
|
705
833
|
|
|
@@ -28,6 +28,156 @@ export type FieldRemapSelection =
|
|
|
28
28
|
| { readonly kind: 'operator'; readonly operatorId: string }
|
|
29
29
|
| null;
|
|
30
30
|
|
|
31
|
+
/** Mapper-local durable membership. It is intentionally not part of the public Flow props. */
|
|
32
|
+
export type FieldRemapBulkSelectionRef =
|
|
33
|
+
| { readonly kind: 'edge'; readonly edgeId: string }
|
|
34
|
+
| { readonly kind: 'transformStep'; readonly edgeId: string; readonly stepIndex: number };
|
|
35
|
+
|
|
36
|
+
export type FieldRemapSelectionGesture = 'plain' | 'toggle' | 'add';
|
|
37
|
+
|
|
38
|
+
export function fieldRemapBulkSelectionKey(ref: FieldRemapBulkSelectionRef): string {
|
|
39
|
+
return ref.kind === 'edge'
|
|
40
|
+
? `edge:${ref.edgeId}`
|
|
41
|
+
: `transformStep:${ref.edgeId}:${ref.stepIndex}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function fieldRemapSelectionKey(selection: FieldRemapSelection): string {
|
|
45
|
+
if (selection === null) {
|
|
46
|
+
return 'none';
|
|
47
|
+
}
|
|
48
|
+
if (selection.kind === 'draft') {
|
|
49
|
+
return `draft:${selection.localId}`;
|
|
50
|
+
}
|
|
51
|
+
if (selection.kind === 'operator') {
|
|
52
|
+
return `operator:${selection.operatorId}`;
|
|
53
|
+
}
|
|
54
|
+
return fieldRemapBulkSelectionKey(selection);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function asFieldRemapBulkSelectionRef(
|
|
58
|
+
selection: FieldRemapSelection,
|
|
59
|
+
): FieldRemapBulkSelectionRef | undefined {
|
|
60
|
+
return selection?.kind === 'edge' || selection?.kind === 'transformStep' ? selection : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One semantic order shared by selection, stale reconciliation, deletion and focus. */
|
|
64
|
+
export function listFieldRemapBulkSelectionRefs(
|
|
65
|
+
edges: readonly MappingEdge[],
|
|
66
|
+
): FieldRemapBulkSelectionRef[] {
|
|
67
|
+
const refs: FieldRemapBulkSelectionRef[] = [];
|
|
68
|
+
for (const edge of edges) {
|
|
69
|
+
refs.push({ kind: 'edge', edgeId: edge.id });
|
|
70
|
+
for (let stepIndex = 0; stepIndex < (edge.transformIds?.length ?? 0); stepIndex += 1) {
|
|
71
|
+
refs.push({ kind: 'transformStep', edgeId: edge.id, stepIndex });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return refs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function normalizeFieldRemapBulkSelection(
|
|
78
|
+
edges: readonly MappingEdge[],
|
|
79
|
+
refs: readonly FieldRemapBulkSelectionRef[],
|
|
80
|
+
): FieldRemapBulkSelectionRef[] {
|
|
81
|
+
const requested = new Set(refs.map(fieldRemapBulkSelectionKey));
|
|
82
|
+
return listFieldRemapBulkSelectionRefs(edges).filter((ref) =>
|
|
83
|
+
requested.has(fieldRemapBulkSelectionKey(ref)),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function updateFieldRemapBulkSelection(input: {
|
|
88
|
+
readonly edges: readonly MappingEdge[];
|
|
89
|
+
readonly membership: readonly FieldRemapBulkSelectionRef[];
|
|
90
|
+
readonly primary: FieldRemapSelection;
|
|
91
|
+
readonly target: FieldRemapBulkSelectionRef;
|
|
92
|
+
readonly gesture: FieldRemapSelectionGesture;
|
|
93
|
+
}): {
|
|
94
|
+
readonly membership: readonly FieldRemapBulkSelectionRef[];
|
|
95
|
+
readonly primary: FieldRemapSelection;
|
|
96
|
+
} {
|
|
97
|
+
const targetKey = fieldRemapBulkSelectionKey(input.target);
|
|
98
|
+
const canonical = listFieldRemapBulkSelectionRefs(input.edges);
|
|
99
|
+
if (!canonical.some((ref) => fieldRemapBulkSelectionKey(ref) === targetKey)) {
|
|
100
|
+
return {
|
|
101
|
+
membership: normalizeFieldRemapBulkSelection(input.edges, input.membership),
|
|
102
|
+
primary: input.primary,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const primaryRef = asFieldRemapBulkSelectionRef(input.primary);
|
|
107
|
+
const primaryKey = primaryRef ? fieldRemapBulkSelectionKey(primaryRef) : undefined;
|
|
108
|
+
const primaryIsValid = primaryKey
|
|
109
|
+
? canonical.some((ref) => fieldRemapBulkSelectionKey(ref) === primaryKey)
|
|
110
|
+
: false;
|
|
111
|
+
if (input.gesture === 'plain' || primaryKey === undefined || !primaryIsValid) {
|
|
112
|
+
return { membership: [input.target], primary: input.target };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const membership = normalizeFieldRemapBulkSelection(input.edges, input.membership);
|
|
116
|
+
const keys = new Set(membership.map(fieldRemapBulkSelectionKey));
|
|
117
|
+
if (input.gesture === 'add') {
|
|
118
|
+
keys.add(targetKey);
|
|
119
|
+
} else if (targetKey !== primaryKey) {
|
|
120
|
+
if (keys.has(targetKey)) {
|
|
121
|
+
keys.delete(targetKey);
|
|
122
|
+
} else {
|
|
123
|
+
keys.add(targetKey);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
keys.add(primaryKey);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
membership: canonical.filter((ref) => keys.has(fieldRemapBulkSelectionKey(ref))),
|
|
130
|
+
primary: input.primary,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type FieldRemapBulkDeletePlan =
|
|
135
|
+
| { readonly status: 'invalid' | 'unchanged' }
|
|
136
|
+
| { readonly status: 'changed'; readonly edges: readonly MappingEdge[] };
|
|
137
|
+
|
|
138
|
+
/** Plan one immutable, all-or-nothing durable removal from one edge snapshot. */
|
|
139
|
+
export function planFieldRemapBulkDelete(
|
|
140
|
+
edges: readonly MappingEdge[],
|
|
141
|
+
refs: readonly FieldRemapBulkSelectionRef[],
|
|
142
|
+
): FieldRemapBulkDeletePlan {
|
|
143
|
+
if (refs.length === 0) {
|
|
144
|
+
return { status: 'unchanged' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const validKeys = new Set(listFieldRemapBulkSelectionRefs(edges).map(fieldRemapBulkSelectionKey));
|
|
148
|
+
if (refs.some((ref) => !validKeys.has(fieldRemapBulkSelectionKey(ref)))) {
|
|
149
|
+
return { status: 'invalid' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const selectedEdges = new Set(refs.filter((ref) => ref.kind === 'edge').map((ref) => ref.edgeId));
|
|
153
|
+
const selectedSteps = new Map<string, Set<number>>();
|
|
154
|
+
for (const ref of refs) {
|
|
155
|
+
if (ref.kind === 'transformStep' && !selectedEdges.has(ref.edgeId)) {
|
|
156
|
+
const indices = selectedSteps.get(ref.edgeId) ?? new Set<number>();
|
|
157
|
+
indices.add(ref.stepIndex);
|
|
158
|
+
selectedSteps.set(ref.edgeId, indices);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const nextEdges = edges
|
|
163
|
+
.filter((edge) => !selectedEdges.has(edge.id))
|
|
164
|
+
.map((edge) => {
|
|
165
|
+
const indices = selectedSteps.get(edge.id);
|
|
166
|
+
if (!indices) {
|
|
167
|
+
return edge;
|
|
168
|
+
}
|
|
169
|
+
const transformIds = (edge.transformIds ?? []).filter((_, index) => !indices.has(index));
|
|
170
|
+
const optionSteps = edge.transformOptionSteps?.filter((_, index) => !indices.has(index));
|
|
171
|
+
return {
|
|
172
|
+
...edge,
|
|
173
|
+
transformIds: transformIds.length > 0 ? transformIds : undefined,
|
|
174
|
+
transformOptionSteps: resizeOptionSteps(optionSteps, transformIds.length),
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return { status: 'changed', edges: nextEdges };
|
|
179
|
+
}
|
|
180
|
+
|
|
31
181
|
/**
|
|
32
182
|
* Ephemeral place-then-wire transform. Not part of `FieldRemapDocument` until
|
|
33
183
|
* both ports are bound and {@link finalizeDraftTransform} succeeds.
|