@workbench-kit/shell-react 0.0.2-prototype.0.2.32 → 0.0.2-prototype.0.2.34

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.
Files changed (52) hide show
  1. package/README.md +32 -0
  2. package/package.json +9 -9
  3. package/src/commands/use-command-descriptors.ts +7 -3
  4. package/src/commands/use-extension-registry-command-descriptors.ts +23 -6
  5. package/src/devtools/use-workbench-devtools-snapshot.ts +39 -14
  6. package/src/devtools/workbench-devtools-snapshot.ts +32 -13
  7. package/src/editor/area.tsx +3 -2
  8. package/src/editor/state-storage.ts +31 -0
  9. package/src/editor/tab-context-menu.ts +8 -8
  10. package/src/editor/use-editor.ts +6 -6
  11. package/src/explorer/context-menu.ts +8 -4
  12. package/src/explorer/view.tsx +4 -3
  13. package/src/extensions/canonical-extension-descriptions.ts +58 -0
  14. package/src/extensions/context-menu.ts +9 -6
  15. package/src/extensions/extension-enablement-context.ts +15 -0
  16. package/src/extensions/extension-enablement-controller.ts +453 -0
  17. package/src/extensions/management-model.ts +111 -47
  18. package/src/extensions/theme-selection-protection.ts +98 -0
  19. package/src/extensions/uninstall-eligibility.ts +103 -0
  20. package/src/extensions/use-extension-management.ts +151 -67
  21. package/src/extensions/view.tsx +4 -0
  22. package/src/field-remap/chrome-labels.ts +87 -2
  23. package/src/field-remap/convert-palette.tsx +164 -18
  24. package/src/field-remap/demo.tsx +48 -3
  25. package/src/field-remap/detail-panel.tsx +6 -5
  26. package/src/field-remap/flow.tsx +341 -155
  27. package/src/field-remap/history.ts +99 -0
  28. package/src/field-remap/index.ts +9 -1
  29. package/src/field-remap/panel.tsx +388 -97
  30. package/src/field-remap/preview-controller.ts +122 -0
  31. package/src/field-remap/preview.tsx +171 -0
  32. package/src/field-remap/view.css +154 -2
  33. package/src/index.ts +16 -1
  34. package/src/management/keybinding-overrides-storage.ts +28 -0
  35. package/src/management/preference-settings-storage.ts +28 -0
  36. package/src/management/settings.tsx +2 -0
  37. package/src/management/use-command-management.ts +31 -26
  38. package/src/management/use-keybinding-management.ts +25 -12
  39. package/src/shell/focused-extension-services.ts +138 -0
  40. package/src/shell/host-shell.tsx +13 -10
  41. package/src/shell/persistence-diagnostic-context.ts +11 -0
  42. package/src/shell/provider.tsx +373 -69
  43. package/src/shell/settings.tsx +25 -16
  44. package/src/shell/shell.tsx +119 -78
  45. package/src/shell/view-host.tsx +23 -22
  46. package/src/storage/local-json-storage.ts +43 -29
  47. package/src/storage/persistence-diagnostics.ts +72 -0
  48. package/src/workbench/appearance-storage.ts +28 -0
  49. package/src/workbench/command-host.tsx +19 -22
  50. package/src/workbench/command-palette.ts +14 -13
  51. package/src/workbench/layout-storage.ts +52 -5
  52. package/src/workbench/use-persisted-appearance.ts +38 -12
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Children,
2
3
  useCallback,
3
4
  useEffect,
4
5
  useImperativeHandle,
@@ -9,6 +10,7 @@ import {
9
10
  type KeyboardEvent,
10
11
  type MouseEvent,
11
12
  type Ref,
13
+ type ReactNode,
12
14
  } from 'react';
13
15
  import {
14
16
  Background,
@@ -28,7 +30,8 @@ import {
28
30
  type NodeProps,
29
31
  } from '@xyflow/react';
30
32
  import '@xyflow/react/dist/style.css';
31
- import { Badge, Button } from '@workbench-kit/react/primitives';
33
+ import { Badge, IconButton } from '@workbench-kit/react/primitives';
34
+ import { SplitView } from '@workbench-kit/react/workbench/split-view';
32
35
  import {
33
36
  MAX_TRANSFORM_CHAIN,
34
37
  type MappingEdge,
@@ -81,6 +84,7 @@ import {
81
84
  type FieldRemapDraftTransform,
82
85
  type FieldRemapSelection,
83
86
  } from './flow-ops.js';
87
+ import { FieldRemapPreviewRail, type FieldRemapPreviewState } from './preview.js';
84
88
  import './view.css';
85
89
 
86
90
  function TypeBadge({ dataType }: { readonly dataType?: string }): JSX.Element | null {
@@ -323,6 +327,22 @@ export interface FieldRemapFlowMapperProps {
323
327
  readonly edges: readonly MappingEdge[];
324
328
  readonly transforms: ValueTransformRegistry;
325
329
  readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
330
+ /**
331
+ * `card` preserves the demo chrome. `embed` omits the flow hint and binding list;
332
+ * explicit `show*` props below take precedence.
333
+ */
334
+ readonly chrome?: 'card' | 'embed' | undefined;
335
+ /** Show the empty-selection detail hint, or collapse that rail until a selection exists. */
336
+ readonly emptyDetail?: 'hint' | 'collapse' | undefined;
337
+ /** Show the convert-first hint. Defaults to true for `card`, false for `embed`. */
338
+ readonly showFlowHint?: boolean | undefined;
339
+ /** Show the bottom binding list. Defaults to true for `card`, false for `embed`. */
340
+ readonly showBindingsList?: boolean | undefined;
341
+ /**
342
+ * Mount the primary Convert palette. Defaults to true for both chrome modes; when false,
343
+ * the workspace expands without leaving an empty grid track.
344
+ */
345
+ readonly showConvertPalette?: boolean | undefined;
326
346
  /** Document v2 n→m operators (display + authoring when onOperatorsChange is set). */
327
347
  readonly operators?: readonly MappingOperator[] | undefined;
328
348
  readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
@@ -330,6 +350,10 @@ export interface FieldRemapFlowMapperProps {
330
350
  readonly targetTitle?: string | undefined;
331
351
  readonly selection?: FieldRemapSelection | undefined;
332
352
  readonly onSelectionChange?: ((next: FieldRemapSelection) => void) | undefined;
353
+ /** Precomputed runtime-only preview. Flow never evaluates mappings itself. */
354
+ readonly preview?: FieldRemapPreviewState;
355
+ /** Explicitly hide an injected preview without reserving a splitter track. */
356
+ readonly showPreview?: boolean;
333
357
  /**
334
358
  * When false, MiniMap is not mounted (not CSS-hidden). Defaults to true for
335
359
  * backward compatibility with existing samples.
@@ -380,18 +404,117 @@ export interface FieldRemapFlowMapperProps {
380
404
  readonly t?: FieldRemapTranslate | undefined;
381
405
  }
382
406
 
407
+ interface FieldRemapSplitWorkspaceProps {
408
+ readonly children: ReactNode;
409
+ readonly layout: 'wide' | 'medium' | 'narrow';
410
+ readonly showConvertPalette: boolean;
411
+ readonly showDetail: boolean;
412
+ readonly surface: 'binding' | 'convert-note' | 'draft-convert' | 'operator';
413
+ }
414
+
415
+ /**
416
+ * The Flow rails are deliberately composed from the shared SplitView primitive so keyboard and
417
+ * pointer resizing stay consistent with the rest of the workbench. At narrower host widths the
418
+ * palette and/or detail move above or below the canvas rather than squeezing its usable width.
419
+ */
420
+ function FieldRemapSplitWorkspace({
421
+ children,
422
+ layout,
423
+ showConvertPalette,
424
+ showDetail,
425
+ surface,
426
+ }: FieldRemapSplitWorkspaceProps): JSX.Element {
427
+ const [palette, canvas, detail] = Children.toArray(children);
428
+ const isNarrow = layout === 'narrow';
429
+ const [paletteSizeByLayout, setPaletteSizeByLayout] = useState({
430
+ wide: 240,
431
+ medium: 240,
432
+ narrow: 192,
433
+ });
434
+ const [detailSizeByLayout, setDetailSizeByLayout] = useState({
435
+ wide: 320,
436
+ medium: 320,
437
+ narrow: 220,
438
+ });
439
+ const paletteSizePx = paletteSizeByLayout[layout];
440
+ const detailSizePx = detailSizeByLayout[layout];
441
+ const canvasWithDetail = (
442
+ <SplitView
443
+ className={
444
+ showDetail
445
+ ? 'workbench-field-remap-flow__canvas-detail-split'
446
+ : 'workbench-field-remap-flow__canvas-detail-split ui-workbench-split-view--secondary-collapsed'
447
+ }
448
+ layoutMode="secondary-fixed"
449
+ maxSecondarySizePx={isNarrow ? 320 : 480}
450
+ minPrimarySizePx={isNarrow ? 200 : 280}
451
+ minSecondarySizePx={isNarrow ? 160 : 256}
452
+ onSecondarySizePxChange={(nextSize) => {
453
+ setDetailSizeByLayout((current) => ({ ...current, [layout]: nextSize }));
454
+ }}
455
+ orientation={isNarrow ? 'vertical' : 'horizontal'}
456
+ primary={canvas}
457
+ secondary={detail}
458
+ secondarySizePx={detailSizePx}
459
+ />
460
+ );
461
+
462
+ const content = (
463
+ <SplitView
464
+ className={
465
+ showConvertPalette
466
+ ? 'workbench-field-remap-flow__palette-split'
467
+ : 'workbench-field-remap-flow__palette-split ui-workbench-split-view--primary-collapsed'
468
+ }
469
+ maxPrimarySizePx={isNarrow ? 288 : 320}
470
+ minPrimarySizePx={isNarrow ? 160 : 192}
471
+ minSecondarySizePx={isNarrow ? 200 : 280}
472
+ onPrimarySizePxChange={(nextSize) => {
473
+ setPaletteSizeByLayout((current) => ({ ...current, [layout]: nextSize }));
474
+ }}
475
+ orientation={layout === 'wide' ? 'horizontal' : 'vertical'}
476
+ primary={palette}
477
+ primarySizePx={paletteSizePx}
478
+ primarySizeUnit="pixels"
479
+ secondary={canvasWithDetail}
480
+ />
481
+ );
482
+
483
+ return (
484
+ <div
485
+ className={
486
+ showConvertPalette
487
+ ? 'workbench-field-remap-flow__workspace workbench-field-remap-flow__workspace--split'
488
+ : 'workbench-field-remap-flow__workspace workbench-field-remap-flow__workspace--split workbench-field-remap-flow__workspace--without-palette'
489
+ }
490
+ data-layout={layout}
491
+ data-surface={surface}
492
+ data-testid="field-remap-workspace"
493
+ >
494
+ {content}
495
+ </div>
496
+ );
497
+ }
498
+
383
499
  function FieldRemapFlowCanvas({
384
500
  sources,
385
501
  targets,
386
502
  edges,
387
503
  transforms,
388
504
  onEdgesChange,
505
+ chrome = 'card',
506
+ emptyDetail: emptyDetailProp,
507
+ showFlowHint: showFlowHintProp,
508
+ showBindingsList: showBindingsListProp,
509
+ showConvertPalette = true,
389
510
  operators = [],
390
511
  onOperatorsChange,
391
512
  sourceTitle,
392
513
  targetTitle,
393
514
  selection: selectionProp,
394
515
  onSelectionChange: onSelectionChangeProp,
516
+ preview,
517
+ showPreview = true,
395
518
  showMinimap = true,
396
519
  onShowMinimapChange,
397
520
  includeHidden = false,
@@ -407,9 +530,15 @@ function FieldRemapFlowCanvas({
407
530
  () => resolveFieldRemapChromeLabels(labelOverrides, t),
408
531
  [labelOverrides, t],
409
532
  );
533
+ const showFlowHint = showFlowHintProp ?? chrome !== 'embed';
534
+ const showBindingsList = showBindingsListProp ?? chrome !== 'embed';
535
+ const emptyDetail = emptyDetailProp ?? (chrome === 'embed' ? 'collapse' : 'hint');
536
+ const mapperRef = useRef<HTMLDivElement>(null);
537
+ const [workspaceLayout, setWorkspaceLayout] = useState<'wide' | 'medium' | 'narrow'>('wide');
410
538
  const [internalSelection, setInternalSelection] = useState<FieldRemapSelection>(null);
411
539
  const selection = selectionProp !== undefined ? selectionProp : internalSelection;
412
540
  const setSelection = onSelectionChangeProp ?? setInternalSelection;
541
+ const previewVisible = showPreview && preview !== undefined && preview.status !== 'unavailable';
413
542
  const [drafts, setDrafts] = useState<readonly FieldRemapDraftTransform[]>([]);
414
543
  const [placeTransformId, setPlaceTransformId] = useState(() => {
415
544
  const first = transforms.list().find((definition) => definition.id !== 'identity');
@@ -417,6 +546,29 @@ function FieldRemapFlowCanvas({
417
546
  });
418
547
  const transformRegistrySignature = createTransformRegistrySignature(transforms);
419
548
 
549
+ useEffect(() => {
550
+ const element = mapperRef.current;
551
+ if (!element || typeof ResizeObserver === 'undefined') {
552
+ return;
553
+ }
554
+
555
+ const updateLayout = () => {
556
+ const remSize = Number.parseFloat(getComputedStyle(element).fontSize) || 16;
557
+ const width = element.getBoundingClientRect().width;
558
+ if (width <= 0) {
559
+ return;
560
+ }
561
+ const nextLayout =
562
+ width <= 60 * remSize ? 'narrow' : width <= 68.75 * remSize ? 'medium' : 'wide';
563
+ setWorkspaceLayout((current) => (current === nextLayout ? current : nextLayout));
564
+ };
565
+
566
+ updateLayout();
567
+ const observer = new ResizeObserver(updateLayout);
568
+ observer.observe(element);
569
+ return () => observer.disconnect();
570
+ }, []);
571
+
420
572
  const graph = useMemo(
421
573
  () =>
422
574
  mappingToFlowGraph({
@@ -746,26 +898,33 @@ function FieldRemapFlowCanvas({
746
898
 
747
899
  return (
748
900
  <div
901
+ ref={mapperRef}
749
902
  className="workbench-field-remap-flow"
750
903
  data-testid="field-remap-mapper"
904
+ data-chrome={chrome}
905
+ data-flow-hint={showFlowHint ? 'on' : 'off'}
906
+ data-bindings-list={showBindingsList ? 'on' : 'off'}
907
+ data-convert-palette={showConvertPalette ? 'on' : 'off'}
908
+ data-empty-detail={emptyDetail}
751
909
  data-minimap={showMinimap ? 'on' : 'off'}
752
910
  data-hidden-fields={includeHidden ? 'on' : 'off'}
911
+ data-preview={previewVisible ? 'on' : 'off'}
753
912
  onKeyDown={onKeyDown}
754
913
  >
755
- <p className="workbench-field-remap-mapper__hint" data-testid="field-remap-hint">
756
- Convert-first: pick a convert in the palette, place it, then wire source → draft → target.
757
- Select a convert note for the Convert editor; select a binding for lighter mapping detail.
758
- Use n→m actions to author combine/split. Alt-click removes a convert step or operator.
759
- Escape clears selection and unfinished drafts.
760
- </p>
914
+ {showFlowHint ? (
915
+ <p className="workbench-field-remap-mapper__hint" data-testid="field-remap-hint">
916
+ Convert-first: pick a convert in the palette, place it, then wire source draft → target.
917
+ Select a convert note for the Convert editor; select a binding for lighter mapping detail.
918
+ Use n→m actions to author combine/split. Alt-click removes a convert step or operator.
919
+ Escape clears selection and unfinished drafts.
920
+ </p>
921
+ ) : null}
761
922
 
762
- <div
763
- className={
764
- selection?.kind === 'transformStep'
765
- ? 'workbench-field-remap-flow__workspace workbench-field-remap-flow__workspace--convert'
766
- : 'workbench-field-remap-flow__workspace'
767
- }
768
- data-surface={
923
+ <FieldRemapSplitWorkspace
924
+ layout={workspaceLayout}
925
+ showConvertPalette={showConvertPalette}
926
+ showDetail={emptyDetail === 'hint' || selection !== null || previewVisible}
927
+ surface={
769
928
  selection?.kind === 'transformStep'
770
929
  ? 'convert-note'
771
930
  : selection?.kind === 'draft'
@@ -775,31 +934,35 @@ function FieldRemapFlowCanvas({
775
934
  : 'binding'
776
935
  }
777
936
  >
778
- <FieldRemapConvertPalette
779
- transforms={transforms}
780
- selectedTransformId={placeTransformId}
781
- onSelectedTransformIdChange={setPlaceTransformId}
782
- onPlaceDraft={placeDraft}
783
- chromeLabels={chromeLabels}
784
- onAddCombine={
785
- onOperatorsChange
786
- ? () => {
787
- const next = createCombineOperator();
788
- onOperatorsChange([...operators, next]);
789
- setSelection({ kind: 'operator', operatorId: next.id });
790
- }
791
- : undefined
792
- }
793
- onAddSplit={
794
- onOperatorsChange
795
- ? () => {
796
- const next = createSplitOperator();
797
- onOperatorsChange([...operators, next]);
798
- setSelection({ kind: 'operator', operatorId: next.id });
799
- }
800
- : undefined
801
- }
802
- />
937
+ <>
938
+ {showConvertPalette ? (
939
+ <FieldRemapConvertPalette
940
+ transforms={transforms}
941
+ selectedTransformId={placeTransformId}
942
+ onSelectedTransformIdChange={setPlaceTransformId}
943
+ onPlaceDraft={placeDraft}
944
+ chromeLabels={chromeLabels}
945
+ onAddCombine={
946
+ onOperatorsChange
947
+ ? () => {
948
+ const next = createCombineOperator();
949
+ onOperatorsChange([...operators, next]);
950
+ setSelection({ kind: 'operator', operatorId: next.id });
951
+ }
952
+ : undefined
953
+ }
954
+ onAddSplit={
955
+ onOperatorsChange
956
+ ? () => {
957
+ const next = createSplitOperator();
958
+ onOperatorsChange([...operators, next]);
959
+ setSelection({ kind: 'operator', operatorId: next.id });
960
+ }
961
+ : undefined
962
+ }
963
+ />
964
+ ) : null}
965
+ </>
803
966
 
804
967
  <div className="workbench-field-remap-flow__canvas" data-testid="field-remap-flow">
805
968
  <ReactFlow
@@ -930,130 +1093,153 @@ function FieldRemapFlowCanvas({
930
1093
  </ReactFlow>
931
1094
  </div>
932
1095
 
933
- <FieldRemapDetailPanel
934
- selection={selection}
935
- edges={edges}
936
- sources={sources}
937
- targets={targets}
938
- transforms={transforms}
939
- onEdgesChange={onEdgesChange}
940
- onSelectionChange={setSelection}
941
- drafts={drafts}
942
- onDiscardDraft={(localId) => {
943
- setDrafts((current) => current.filter((item) => item.localId !== localId));
944
- }}
945
- operators={operators}
946
- onOperatorsChange={onOperatorsChange}
947
- />
948
- </div>
1096
+ <div className="workbench-field-remap-flow__side-rail">
1097
+ {emptyDetail === 'hint' || selection !== null ? (
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}
1116
+ {previewVisible && preview ? (
1117
+ <FieldRemapPreviewRail
1118
+ preview={preview}
1119
+ selection={selection}
1120
+ edges={edges}
1121
+ operatorExists={
1122
+ selection?.kind === 'operator'
1123
+ ? operators.some((operator) => operator.id === selection.operatorId)
1124
+ : false
1125
+ }
1126
+ labels={chromeLabels}
1127
+ />
1128
+ ) : null}
1129
+ </div>
1130
+ </FieldRemapSplitWorkspace>
949
1131
 
950
- <div className="workbench-field-remap-flow__bindings" data-testid="field-remap-edges">
951
- <h4>{chromeLabels.bindingsTitle}</h4>
952
- <ul>
953
- {edges.map((edge) => {
954
- const portTypes = edgePortTypes(edge, sources, targets);
955
- const appendCatalog = listCompatibleTransforms({
956
- registry: transforms,
957
- edge,
958
- stepIndex: edge.transformIds?.length ?? 0,
959
- sourceType: portTypes.sourceType,
960
- targetType: portTypes.targetType,
961
- mode: 'append',
962
- });
963
- const defaultAddId = appendCatalog[0]?.id;
964
- const listContext = canEditListContext(edge, sources, targets);
965
- const selected =
966
- (selection?.kind === 'edge' || selection?.kind === 'transformStep') &&
967
- selection.edgeId === edge.id;
1132
+ {showBindingsList ? (
1133
+ <div className="workbench-field-remap-flow__bindings" data-testid="field-remap-edges">
1134
+ <h4>{chromeLabels.bindingsTitle}</h4>
1135
+ <ul>
1136
+ {edges.map((edge) => {
1137
+ const portTypes = edgePortTypes(edge, sources, targets);
1138
+ const appendCatalog = listCompatibleTransforms({
1139
+ registry: transforms,
1140
+ edge,
1141
+ stepIndex: edge.transformIds?.length ?? 0,
1142
+ sourceType: portTypes.sourceType,
1143
+ targetType: portTypes.targetType,
1144
+ mode: 'append',
1145
+ });
1146
+ const defaultAddId = appendCatalog[0]?.id;
1147
+ const listContext = canEditListContext(edge, sources, targets);
1148
+ const selected =
1149
+ (selection?.kind === 'edge' || selection?.kind === 'transformStep') &&
1150
+ selection.edgeId === edge.id;
968
1151
 
969
- return (
970
- <li
971
- key={edge.id}
972
- className={selected ? 'is-selected' : undefined}
973
- data-testid={`field-remap-lane-${edge.id}`}
974
- >
975
- <button
976
- type="button"
977
- className="workbench-field-remap-flow__binding-select"
978
- data-testid={`field-remap-select-edge-${edge.id}`}
979
- onClick={() => setSelection({ kind: 'edge', edgeId: edge.id })}
1152
+ return (
1153
+ <li
1154
+ key={edge.id}
1155
+ className={selected ? 'is-selected' : undefined}
1156
+ data-testid={`field-remap-lane-${edge.id}`}
980
1157
  >
981
- <code>
982
- {edge.sourceFieldId} →{' '}
983
- {(edge.transformIds ?? []).length > 0
984
- ? `${(edge.transformIds ?? []).join(' → ')}`
985
- : ''}
986
- {edge.targetSlotId}
987
- {edge.itemEdges ? ` · ${edge.itemEdges.length} item fields` : ''}
988
- </code>
989
- </button>
990
- <span className="workbench-field-remap-mapper__edge-actions">
991
- {(edge.transformIds?.length ?? 0) < MAX_TRANSFORM_CHAIN && defaultAddId ? (
992
- <Button
993
- compact
994
- type="button"
995
- data-testid={`field-remap-add-node-${edge.id}`}
996
- onClick={() => {
997
- const next = addTransformStepToEdge(edge, defaultAddId, {
998
- registry: transforms,
999
- sourceType: portTypes.sourceType,
1000
- targetType: portTypes.targetType,
1001
- });
1002
- if (!next) {
1003
- return;
1004
- }
1005
- onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
1006
- setSelection({
1007
- kind: 'transformStep',
1008
- edgeId: edge.id,
1009
- stepIndex: (next.transformIds?.length ?? 1) - 1,
1010
- });
1011
- }}
1012
- >
1013
- + node
1014
- </Button>
1015
- ) : null}
1016
- {listContext ? (
1017
- <Button
1158
+ <button
1159
+ type="button"
1160
+ className="workbench-field-remap-flow__binding-select"
1161
+ data-testid={`field-remap-select-edge-${edge.id}`}
1162
+ onClick={() => setSelection({ kind: 'edge', edgeId: edge.id })}
1163
+ >
1164
+ <code>
1165
+ {edge.sourceFieldId} →{' '}
1166
+ {(edge.transformIds ?? []).length > 0
1167
+ ? `${(edge.transformIds ?? []).join(' → ')} → `
1168
+ : ''}
1169
+ {edge.targetSlotId}
1170
+ {edge.itemEdges ? ` · ${edge.itemEdges.length} item fields` : ''}
1171
+ </code>
1172
+ </button>
1173
+ <span className="workbench-field-remap-mapper__edge-actions">
1174
+ {(edge.transformIds?.length ?? 0) < MAX_TRANSFORM_CHAIN && defaultAddId ? (
1175
+ <IconButton
1176
+ compact
1177
+ type="button"
1178
+ data-testid={`field-remap-add-node-${edge.id}`}
1179
+ icon="codicon-add"
1180
+ label={chromeLabels.addTransform}
1181
+ onClick={() => {
1182
+ const next = addTransformStepToEdge(edge, defaultAddId, {
1183
+ registry: transforms,
1184
+ sourceType: portTypes.sourceType,
1185
+ targetType: portTypes.targetType,
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}
1206
+ onClick={() => {
1207
+ if (!edge.itemEdges) {
1208
+ onEdgesChange(
1209
+ edges.map((item) =>
1210
+ item.id === edge.id ? enableListContextOnEdge(item) : item,
1211
+ ),
1212
+ );
1213
+ }
1214
+ setSelection({ kind: 'edge', edgeId: edge.id });
1215
+ }}
1216
+ />
1217
+ ) : null}
1218
+ <IconButton
1018
1219
  compact
1019
1220
  type="button"
1020
- data-testid={`field-remap-edit-items-${edge.id}`}
1221
+ data-testid={`field-remap-remove-edge-${edge.id}`}
1222
+ icon="codicon-trash"
1223
+ label={chromeLabels.removeBinding}
1224
+ variant="danger"
1021
1225
  onClick={() => {
1022
- if (!edge.itemEdges) {
1023
- onEdgesChange(
1024
- edges.map((item) =>
1025
- item.id === edge.id ? enableListContextOnEdge(item) : item,
1026
- ),
1027
- );
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);
1028
1233
  }
1029
- setSelection({ kind: 'edge', edgeId: edge.id });
1030
1234
  }}
1031
- >
1032
- Edit items
1033
- </Button>
1034
- ) : null}
1035
- <Button
1036
- compact
1037
- type="button"
1038
- onClick={() => {
1039
- onEdgesChange(edges.filter((item) => item.id !== edge.id));
1040
- if (
1041
- selection &&
1042
- (selection.kind === 'edge' || selection.kind === 'transformStep') &&
1043
- selection.edgeId === edge.id
1044
- ) {
1045
- setSelection(null);
1046
- }
1047
- }}
1048
- >
1049
- Remove
1050
- </Button>
1051
- </span>
1052
- </li>
1053
- );
1054
- })}
1055
- </ul>
1056
- </div>
1235
+ />
1236
+ </span>
1237
+ </li>
1238
+ );
1239
+ })}
1240
+ </ul>
1241
+ </div>
1242
+ ) : null}
1057
1243
  </div>
1058
1244
  );
1059
1245
  }