@workbench-kit/shell-react 0.0.2-prototype.0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/package.json +51 -0
- package/src/chat/ai-mock-proposals.ts +39 -0
- package/src/chat/command-input.ts +52 -0
- package/src/chat/command-policy.ts +54 -0
- package/src/chat/command-surface.tsx +155 -0
- package/src/chat/use-chat-command-proposals.ts +194 -0
- package/src/chat/view-data.ts +20 -0
- package/src/chat/view.tsx +220 -0
- package/src/commands/inspector-surface.tsx +46 -0
- package/src/commands/use-command-descriptors.ts +41 -0
- package/src/commands/use-context-key-revision.ts +18 -0
- package/src/commands/view-data.ts +17 -0
- package/src/commands/view.tsx +43 -0
- package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -0
- package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -0
- package/src/devtools/index.ts +11 -0
- package/src/devtools/use-workbench-devtools-snapshot.ts +84 -0
- package/src/devtools/workbench-devtools-snapshot.ts +140 -0
- package/src/devtools/workbench-devtools.css +82 -0
- package/src/editor/area-dnd.ts +69 -0
- package/src/editor/area-model.ts +71 -0
- package/src/editor/area.css +454 -0
- package/src/editor/area.tsx +676 -0
- package/src/editor/host-surface.tsx +691 -0
- package/src/editor/markdown-document-view-data.ts +16 -0
- package/src/editor/pane-visibility.ts +130 -0
- package/src/editor/resource.ts +36 -0
- package/src/editor/state-storage.ts +182 -0
- package/src/editor/tab-context-menu.ts +118 -0
- package/src/editor/use-editor.ts +138 -0
- package/src/editor/view-providers.tsx +504 -0
- package/src/editor/workspace-file-availability.ts +15 -0
- package/src/editor/workspace-reconcile.tsx +31 -0
- package/src/explorer/context-menu.ts +114 -0
- package/src/explorer/create-command-workspace-explorer-port.ts +86 -0
- package/src/explorer/reveal.ts +112 -0
- package/src/explorer/search-view-data.ts +15 -0
- package/src/explorer/search-view.tsx +65 -0
- package/src/explorer/use-active-workspace-path.ts +15 -0
- package/src/explorer/view-data.ts +58 -0
- package/src/explorer/view.tsx +227 -0
- package/src/extensions/context-menu.ts +52 -0
- package/src/extensions/management-model.ts +313 -0
- package/src/extensions/use-extension-management.ts +241 -0
- package/src/extensions/view-data.ts +19 -0
- package/src/extensions/view.tsx +63 -0
- package/src/field-remap/chrome-labels.ts +74 -0
- package/src/field-remap/convert-note-editor.tsx +206 -0
- package/src/field-remap/convert-palette.tsx +126 -0
- package/src/field-remap/demo.tsx +105 -0
- package/src/field-remap/detail-panel.tsx +659 -0
- package/src/field-remap/editor-surface.tsx +36 -0
- package/src/field-remap/flow-adapter.ts +838 -0
- package/src/field-remap/flow-ops.ts +442 -0
- package/src/field-remap/flow.tsx +948 -0
- package/src/field-remap/graph.tsx +559 -0
- package/src/field-remap/index.ts +88 -0
- package/src/field-remap/io-class-browse.tsx +145 -0
- package/src/field-remap/jsonata-transform.ts +166 -0
- package/src/field-remap/panel.tsx +466 -0
- package/src/field-remap/samples.ts +507 -0
- package/src/field-remap/shape-io-editor.tsx +170 -0
- package/src/field-remap/table-mapping-adapter.ts +59 -0
- package/src/field-remap/transform-options-editor.tsx +312 -0
- package/src/field-remap/tree.tsx +404 -0
- package/src/field-remap/view-data.ts +16 -0
- package/src/field-remap/view.css +1367 -0
- package/src/field-remap/view.tsx +86 -0
- package/src/index.ts +294 -0
- package/src/jdw/document-view-data.ts +31 -0
- package/src/jdw/json-form-source-patch.ts +323 -0
- package/src/jdw/lab-view-data.ts +25 -0
- package/src/jdw/lab-view.css +21 -0
- package/src/jdw/lab-view.tsx +79 -0
- package/src/jdw/widget-form-view.tsx +68 -0
- package/src/jdw/widget-preview-view.tsx +54 -0
- package/src/management/keybinding-overrides-storage.ts +49 -0
- package/src/management/keybinding-settings.tsx +16 -0
- package/src/management/palette-commands.ts +38 -0
- package/src/management/preference-settings-storage.ts +50 -0
- package/src/management/settings-ids.ts +12 -0
- package/src/management/settings.tsx +96 -0
- package/src/management/use-command-management.ts +191 -0
- package/src/management/use-keybinding-management.ts +130 -0
- package/src/shell/model.tsx +149 -0
- package/src/shell/provider.tsx +606 -0
- package/src/shell/secondary-actions.tsx +59 -0
- package/src/shell/settings-constants.ts +9 -0
- package/src/shell/settings.tsx +599 -0
- package/src/shell/shell.tsx +680 -0
- package/src/shell/titlebar-layout-controls.tsx +4 -0
- package/src/shell/view-host.tsx +212 -0
- package/src/workbench/appearance-storage.ts +107 -0
- package/src/workbench/command-host.tsx +243 -0
- package/src/workbench/command-palette.ts +208 -0
- package/src/workbench/keybinding-bridge.ts +54 -0
- package/src/workbench/layout-storage.ts +134 -0
- package/src/workbench/profile-modal.tsx +118 -0
- package/src/workbench/shell-command-registration.ts +58 -0
- package/src/workbench/startup-gate.tsx +136 -0
- package/src/workbench/status-sections.ts +43 -0
- package/src/workbench/use-persisted-appearance.ts +33 -0
- package/src/workbench/user-commands.ts +46 -0
- package/src/workbench/workspace-view-state.ts +35 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { useMemo, useState, type JSX } from 'react';
|
|
2
|
+
import { Button } from '@workbench-kit/react/primitives';
|
|
3
|
+
import {
|
|
4
|
+
MAX_TRANSFORM_CHAIN,
|
|
5
|
+
findSourceField,
|
|
6
|
+
findTargetSlot,
|
|
7
|
+
flattenSourceFields,
|
|
8
|
+
flattenTargetSlots,
|
|
9
|
+
type MappingEdge,
|
|
10
|
+
type MappingOperator,
|
|
11
|
+
type SourceField,
|
|
12
|
+
type TargetSlot,
|
|
13
|
+
type ValueTransformRegistry,
|
|
14
|
+
} from '@workbench-kit/field-remap';
|
|
15
|
+
|
|
16
|
+
import { ConvertNoteEditor } from './convert-note-editor.js';
|
|
17
|
+
import {
|
|
18
|
+
addTransformStepToEdge,
|
|
19
|
+
bindOperatorInput,
|
|
20
|
+
bindOperatorOutput,
|
|
21
|
+
canEditListContext,
|
|
22
|
+
edgePortTypes,
|
|
23
|
+
enableListContextOnEdge,
|
|
24
|
+
listCompatibleTransforms,
|
|
25
|
+
removeItemEdgeFromParent,
|
|
26
|
+
removeMappingOperator,
|
|
27
|
+
removeOperatorInput,
|
|
28
|
+
removeOperatorOutput,
|
|
29
|
+
removeTransformStepFromEdge,
|
|
30
|
+
updateMappingOperator,
|
|
31
|
+
upsertItemEdgeOnParent,
|
|
32
|
+
type FieldRemapDraftTransform,
|
|
33
|
+
type FieldRemapSelection,
|
|
34
|
+
} from './flow-ops.js';
|
|
35
|
+
|
|
36
|
+
export interface FieldRemapDetailPanelProps {
|
|
37
|
+
readonly selection: FieldRemapSelection;
|
|
38
|
+
readonly edges: readonly MappingEdge[];
|
|
39
|
+
readonly sources: readonly SourceField[];
|
|
40
|
+
readonly targets: readonly TargetSlot[];
|
|
41
|
+
readonly transforms: ValueTransformRegistry;
|
|
42
|
+
readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
|
|
43
|
+
readonly onSelectionChange: (next: FieldRemapSelection) => void;
|
|
44
|
+
readonly drafts?: readonly FieldRemapDraftTransform[] | undefined;
|
|
45
|
+
readonly onDiscardDraft?: ((localId: string) => void) | undefined;
|
|
46
|
+
readonly operators?: readonly MappingOperator[] | undefined;
|
|
47
|
+
readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fieldLabel(
|
|
51
|
+
id: string,
|
|
52
|
+
sources: readonly SourceField[],
|
|
53
|
+
targets: readonly TargetSlot[],
|
|
54
|
+
): string {
|
|
55
|
+
const source = findSourceField(sources, id);
|
|
56
|
+
if (source) {
|
|
57
|
+
return source.path ?? source.label;
|
|
58
|
+
}
|
|
59
|
+
const target = findTargetSlot(targets, id);
|
|
60
|
+
return target?.path ?? target?.label ?? id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Selection-driven side rail for Flow:
|
|
65
|
+
* - edge / empty → light binding detail (chain overview, palette, list context)
|
|
66
|
+
* - transformStep → dedicated {@link ConvertNoteEditor} surface
|
|
67
|
+
*/
|
|
68
|
+
export function FieldRemapDetailPanel({
|
|
69
|
+
selection,
|
|
70
|
+
edges,
|
|
71
|
+
sources,
|
|
72
|
+
targets,
|
|
73
|
+
transforms,
|
|
74
|
+
onEdgesChange,
|
|
75
|
+
onSelectionChange,
|
|
76
|
+
drafts = [],
|
|
77
|
+
onDiscardDraft,
|
|
78
|
+
operators = [],
|
|
79
|
+
onOperatorsChange,
|
|
80
|
+
}: FieldRemapDetailPanelProps): JSX.Element | null {
|
|
81
|
+
const edge = useMemo(() => {
|
|
82
|
+
if (!selection || (selection.kind !== 'edge' && selection.kind !== 'transformStep')) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return edges.find((item) => item.id === selection.edgeId);
|
|
86
|
+
}, [edges, selection]);
|
|
87
|
+
|
|
88
|
+
const draft = useMemo(() => {
|
|
89
|
+
if (!selection || selection.kind !== 'draft') {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return drafts.find((item) => item.localId === selection.localId);
|
|
93
|
+
}, [drafts, selection]);
|
|
94
|
+
|
|
95
|
+
const operator = useMemo(() => {
|
|
96
|
+
if (!selection || selection.kind !== 'operator') {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return operators.find((item) => item.id === selection.operatorId);
|
|
100
|
+
}, [operators, selection]);
|
|
101
|
+
|
|
102
|
+
const [pendingItemSourceId, setPendingItemSourceId] = useState<string | null>(null);
|
|
103
|
+
const [paletteId, setPaletteId] = useState<string>('');
|
|
104
|
+
const [pendingOperatorFieldId, setPendingOperatorFieldId] = useState('');
|
|
105
|
+
const [pendingOperatorSlotId, setPendingOperatorSlotId] = useState('');
|
|
106
|
+
const flatSources = useMemo(() => flattenSourceFields(sources), [sources]);
|
|
107
|
+
const flatTargets = useMemo(() => flattenTargetSlots(targets), [targets]);
|
|
108
|
+
|
|
109
|
+
if (!selection) {
|
|
110
|
+
return (
|
|
111
|
+
<aside
|
|
112
|
+
className="workbench-field-remap-detail workbench-field-remap-detail--empty"
|
|
113
|
+
data-testid="field-remap-detail"
|
|
114
|
+
aria-label="Binding details"
|
|
115
|
+
>
|
|
116
|
+
<h4>Start with a convert</h4>
|
|
117
|
+
<p data-testid="field-remap-detail-empty-hint">
|
|
118
|
+
Use the Convert palette to place a convert, then wire source → draft → target. Or select
|
|
119
|
+
an existing binding / convert note on the canvas.
|
|
120
|
+
</p>
|
|
121
|
+
</aside>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (selection.kind === 'draft') {
|
|
126
|
+
const definition = draft ? transforms.get(draft.transformId) : undefined;
|
|
127
|
+
return (
|
|
128
|
+
<aside
|
|
129
|
+
className="workbench-field-remap-detail workbench-field-remap-detail--draft"
|
|
130
|
+
data-testid="field-remap-detail"
|
|
131
|
+
aria-label="Draft convert"
|
|
132
|
+
>
|
|
133
|
+
<div className="workbench-field-remap-detail__header">
|
|
134
|
+
<div>
|
|
135
|
+
<h4>Draft convert</h4>
|
|
136
|
+
<code data-testid="field-remap-detail-draft-id">
|
|
137
|
+
{definition?.label ?? draft?.transformId ?? selection.localId}
|
|
138
|
+
</code>
|
|
139
|
+
</div>
|
|
140
|
+
<Button
|
|
141
|
+
compact
|
|
142
|
+
type="button"
|
|
143
|
+
data-testid="field-remap-detail-discard-draft"
|
|
144
|
+
onClick={() => {
|
|
145
|
+
onDiscardDraft?.(selection.localId);
|
|
146
|
+
onSelectionChange(null);
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
Discard
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
<p className="workbench-field-remap-detail__muted">
|
|
153
|
+
Wire source then target ports. When both bind, the convert note editor opens.
|
|
154
|
+
</p>
|
|
155
|
+
<dl
|
|
156
|
+
className="workbench-field-remap-detail__draft-ports"
|
|
157
|
+
data-testid="field-remap-detail-draft-ports"
|
|
158
|
+
>
|
|
159
|
+
<div>
|
|
160
|
+
<dt>Source</dt>
|
|
161
|
+
<dd>
|
|
162
|
+
<code>{draft?.sourceFieldId ?? 'Unwired'}</code>
|
|
163
|
+
</dd>
|
|
164
|
+
</div>
|
|
165
|
+
<div>
|
|
166
|
+
<dt>Target</dt>
|
|
167
|
+
<dd>
|
|
168
|
+
<code>{draft?.targetSlotId ?? 'Unwired'}</code>
|
|
169
|
+
</dd>
|
|
170
|
+
</div>
|
|
171
|
+
</dl>
|
|
172
|
+
</aside>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (selection.kind === 'operator') {
|
|
177
|
+
if (!operator || !onOperatorsChange) {
|
|
178
|
+
return (
|
|
179
|
+
<aside className="workbench-field-remap-detail" data-testid="field-remap-detail">
|
|
180
|
+
<h4>Operator</h4>
|
|
181
|
+
<code data-testid="field-remap-detail-operator-id">{selection.operatorId}</code>
|
|
182
|
+
<p className="workbench-field-remap-detail__muted">
|
|
183
|
+
Host must wire onOperatorsChange for authoring.
|
|
184
|
+
</p>
|
|
185
|
+
</aside>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return (
|
|
189
|
+
<aside
|
|
190
|
+
className="workbench-field-remap-detail workbench-field-remap-detail--operator"
|
|
191
|
+
data-testid="field-remap-detail"
|
|
192
|
+
aria-label="Operator details"
|
|
193
|
+
>
|
|
194
|
+
<div className="workbench-field-remap-detail__header">
|
|
195
|
+
<div>
|
|
196
|
+
<h4>{operator.kind === 'combine' ? 'Combine (n→1)' : 'Split (1→n)'}</h4>
|
|
197
|
+
<code data-testid="field-remap-detail-operator-id">{operator.id}</code>
|
|
198
|
+
</div>
|
|
199
|
+
<Button
|
|
200
|
+
compact
|
|
201
|
+
type="button"
|
|
202
|
+
data-testid="field-remap-detail-delete-operator"
|
|
203
|
+
onClick={() => {
|
|
204
|
+
onOperatorsChange(removeMappingOperator(operators, operator.id));
|
|
205
|
+
onSelectionChange(null);
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
Delete
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
{operator.kind === 'combine' ? (
|
|
212
|
+
<section className="workbench-field-remap-detail__section">
|
|
213
|
+
<h5>Inputs</h5>
|
|
214
|
+
<ul className="workbench-field-remap-detail__item-edges">
|
|
215
|
+
{operator.inputFieldIds.map((fieldId) => (
|
|
216
|
+
<li key={fieldId}>
|
|
217
|
+
<code>{fieldLabel(fieldId, sources, targets)}</code>
|
|
218
|
+
<Button
|
|
219
|
+
compact
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={() =>
|
|
222
|
+
onOperatorsChange(
|
|
223
|
+
updateMappingOperator(operators, operator.id, (current) =>
|
|
224
|
+
removeOperatorInput(current, fieldId),
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
>
|
|
229
|
+
Remove
|
|
230
|
+
</Button>
|
|
231
|
+
</li>
|
|
232
|
+
))}
|
|
233
|
+
</ul>
|
|
234
|
+
<div className="workbench-field-remap-detail__field">
|
|
235
|
+
<label>
|
|
236
|
+
<span>Add input</span>
|
|
237
|
+
<select
|
|
238
|
+
data-testid="field-remap-operator-add-input"
|
|
239
|
+
value={pendingOperatorFieldId}
|
|
240
|
+
onChange={(event) => setPendingOperatorFieldId(event.target.value)}
|
|
241
|
+
>
|
|
242
|
+
<option value="">Select…</option>
|
|
243
|
+
{flatSources.map((field) => (
|
|
244
|
+
<option key={field.id} value={field.id}>
|
|
245
|
+
{field.path ?? field.label}
|
|
246
|
+
</option>
|
|
247
|
+
))}
|
|
248
|
+
</select>
|
|
249
|
+
</label>
|
|
250
|
+
<Button
|
|
251
|
+
compact
|
|
252
|
+
type="button"
|
|
253
|
+
data-testid="field-remap-operator-bind-input"
|
|
254
|
+
disabled={!pendingOperatorFieldId}
|
|
255
|
+
onClick={() => {
|
|
256
|
+
if (!pendingOperatorFieldId) return;
|
|
257
|
+
onOperatorsChange(
|
|
258
|
+
updateMappingOperator(operators, operator.id, (current) =>
|
|
259
|
+
bindOperatorInput(current, pendingOperatorFieldId),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
setPendingOperatorFieldId('');
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
Add
|
|
266
|
+
</Button>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="workbench-field-remap-detail__field">
|
|
269
|
+
<label>
|
|
270
|
+
<span>Output slot</span>
|
|
271
|
+
<select
|
|
272
|
+
data-testid="field-remap-operator-output"
|
|
273
|
+
value={operator.outputSlotId}
|
|
274
|
+
onChange={(event) => {
|
|
275
|
+
const slotId = event.target.value;
|
|
276
|
+
onOperatorsChange(
|
|
277
|
+
updateMappingOperator(operators, operator.id, (current) =>
|
|
278
|
+
slotId
|
|
279
|
+
? bindOperatorOutput(current, slotId)
|
|
280
|
+
: removeOperatorOutput(current, operator.outputSlotId),
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
<option value="">Unwired…</option>
|
|
286
|
+
{flatTargets.map((slot) => (
|
|
287
|
+
<option key={slot.id} value={slot.id}>
|
|
288
|
+
{slot.path ?? slot.label}
|
|
289
|
+
</option>
|
|
290
|
+
))}
|
|
291
|
+
</select>
|
|
292
|
+
</label>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
) : (
|
|
296
|
+
<section className="workbench-field-remap-detail__section">
|
|
297
|
+
<div className="workbench-field-remap-detail__field">
|
|
298
|
+
<label>
|
|
299
|
+
<span>Input field</span>
|
|
300
|
+
<select
|
|
301
|
+
data-testid="field-remap-operator-input"
|
|
302
|
+
value={operator.inputFieldId}
|
|
303
|
+
onChange={(event) => {
|
|
304
|
+
const fieldId = event.target.value;
|
|
305
|
+
onOperatorsChange(
|
|
306
|
+
updateMappingOperator(operators, operator.id, (current) =>
|
|
307
|
+
fieldId
|
|
308
|
+
? bindOperatorInput(current, fieldId)
|
|
309
|
+
: removeOperatorInput(current, operator.inputFieldId),
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<option value="">Unwired…</option>
|
|
315
|
+
{flatSources.map((field) => (
|
|
316
|
+
<option key={field.id} value={field.id}>
|
|
317
|
+
{field.path ?? field.label}
|
|
318
|
+
</option>
|
|
319
|
+
))}
|
|
320
|
+
</select>
|
|
321
|
+
</label>
|
|
322
|
+
</div>
|
|
323
|
+
<h5>Outputs</h5>
|
|
324
|
+
<ul className="workbench-field-remap-detail__item-edges">
|
|
325
|
+
{operator.outputSlotIds.map((slotId) => (
|
|
326
|
+
<li key={slotId}>
|
|
327
|
+
<code>{fieldLabel(slotId, sources, targets)}</code>
|
|
328
|
+
<Button
|
|
329
|
+
compact
|
|
330
|
+
type="button"
|
|
331
|
+
onClick={() =>
|
|
332
|
+
onOperatorsChange(
|
|
333
|
+
updateMappingOperator(operators, operator.id, (current) =>
|
|
334
|
+
removeOperatorOutput(current, slotId),
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
>
|
|
339
|
+
Remove
|
|
340
|
+
</Button>
|
|
341
|
+
</li>
|
|
342
|
+
))}
|
|
343
|
+
</ul>
|
|
344
|
+
<div className="workbench-field-remap-detail__field">
|
|
345
|
+
<label>
|
|
346
|
+
<span>Add output</span>
|
|
347
|
+
<select
|
|
348
|
+
data-testid="field-remap-operator-add-output"
|
|
349
|
+
value={pendingOperatorSlotId}
|
|
350
|
+
onChange={(event) => setPendingOperatorSlotId(event.target.value)}
|
|
351
|
+
>
|
|
352
|
+
<option value="">Select…</option>
|
|
353
|
+
{flatTargets.map((slot) => (
|
|
354
|
+
<option key={slot.id} value={slot.id}>
|
|
355
|
+
{slot.path ?? slot.label}
|
|
356
|
+
</option>
|
|
357
|
+
))}
|
|
358
|
+
</select>
|
|
359
|
+
</label>
|
|
360
|
+
<Button
|
|
361
|
+
compact
|
|
362
|
+
type="button"
|
|
363
|
+
data-testid="field-remap-operator-bind-output"
|
|
364
|
+
disabled={!pendingOperatorSlotId}
|
|
365
|
+
onClick={() => {
|
|
366
|
+
if (!pendingOperatorSlotId) return;
|
|
367
|
+
onOperatorsChange(
|
|
368
|
+
updateMappingOperator(operators, operator.id, (current) =>
|
|
369
|
+
bindOperatorOutput(current, pendingOperatorSlotId),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
setPendingOperatorSlotId('');
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
Add
|
|
376
|
+
</Button>
|
|
377
|
+
</div>
|
|
378
|
+
</section>
|
|
379
|
+
)}
|
|
380
|
+
</aside>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!edge) {
|
|
385
|
+
return (
|
|
386
|
+
<aside
|
|
387
|
+
className="workbench-field-remap-detail workbench-field-remap-detail--empty"
|
|
388
|
+
data-testid="field-remap-detail"
|
|
389
|
+
aria-label="Binding details"
|
|
390
|
+
>
|
|
391
|
+
<p>Select a binding for mapping details, or a convert node to open the convert editor.</p>
|
|
392
|
+
</aside>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (selection.kind === 'transformStep') {
|
|
397
|
+
return (
|
|
398
|
+
<ConvertNoteEditor
|
|
399
|
+
edge={edge}
|
|
400
|
+
stepIndex={selection.stepIndex}
|
|
401
|
+
sources={sources}
|
|
402
|
+
targets={targets}
|
|
403
|
+
transforms={transforms}
|
|
404
|
+
edges={edges}
|
|
405
|
+
onEdgesChange={onEdgesChange}
|
|
406
|
+
onSelectionChange={onSelectionChange}
|
|
407
|
+
/>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const portTypes = edgePortTypes(edge, sources, targets);
|
|
412
|
+
const chain = edge.transformIds ?? [];
|
|
413
|
+
|
|
414
|
+
const appendCatalog = listCompatibleTransforms({
|
|
415
|
+
registry: transforms,
|
|
416
|
+
edge,
|
|
417
|
+
stepIndex: chain.length,
|
|
418
|
+
sourceType: portTypes.sourceType,
|
|
419
|
+
targetType: portTypes.targetType,
|
|
420
|
+
mode: 'append',
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const defaultPaletteId = appendCatalog[0]?.id ?? '';
|
|
424
|
+
const effectivePaletteId =
|
|
425
|
+
paletteId && appendCatalog.some((item) => item.id === paletteId) ? paletteId : defaultPaletteId;
|
|
426
|
+
|
|
427
|
+
const listContextEnabled = canEditListContext(edge, sources, targets);
|
|
428
|
+
const sourceField = findSourceField(sources, edge.sourceFieldId);
|
|
429
|
+
const targetSlot = findTargetSlot(targets, edge.targetSlotId);
|
|
430
|
+
const itemSourceChildren = sourceField?.children ?? [];
|
|
431
|
+
const itemTargetChildren = targetSlot?.children ?? [];
|
|
432
|
+
|
|
433
|
+
const applyEdge = (next: MappingEdge | null) => {
|
|
434
|
+
if (!next) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<aside
|
|
442
|
+
className="workbench-field-remap-detail"
|
|
443
|
+
data-testid="field-remap-detail"
|
|
444
|
+
aria-label="Binding details"
|
|
445
|
+
>
|
|
446
|
+
<div className="workbench-field-remap-detail__header">
|
|
447
|
+
<div>
|
|
448
|
+
<h4>Binding</h4>
|
|
449
|
+
<code data-testid="field-remap-detail-binding">
|
|
450
|
+
{edge.sourceFieldId} → {edge.targetSlotId}
|
|
451
|
+
</code>
|
|
452
|
+
</div>
|
|
453
|
+
<Button
|
|
454
|
+
compact
|
|
455
|
+
type="button"
|
|
456
|
+
data-testid="field-remap-detail-clear"
|
|
457
|
+
onClick={() => onSelectionChange(null)}
|
|
458
|
+
>
|
|
459
|
+
Clear
|
|
460
|
+
</Button>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<section
|
|
464
|
+
className="workbench-field-remap-detail__section"
|
|
465
|
+
aria-labelledby="field-remap-detail-chain"
|
|
466
|
+
>
|
|
467
|
+
<h5 id="field-remap-detail-chain">Convert chain</h5>
|
|
468
|
+
{chain.length === 0 ? (
|
|
469
|
+
<p className="workbench-field-remap-detail__muted">Direct (no convert)</p>
|
|
470
|
+
) : (
|
|
471
|
+
<ol className="workbench-field-remap-detail__steps">
|
|
472
|
+
{chain.map((transformId, index) => {
|
|
473
|
+
const definition = transforms.get(transformId);
|
|
474
|
+
return (
|
|
475
|
+
<li key={`${edge.id}-${index}-${transformId}`}>
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
className="workbench-field-remap-detail__step"
|
|
479
|
+
data-testid={`field-remap-detail-step-${index}`}
|
|
480
|
+
onClick={() =>
|
|
481
|
+
onSelectionChange({
|
|
482
|
+
kind: 'transformStep',
|
|
483
|
+
edgeId: edge.id,
|
|
484
|
+
stepIndex: index,
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
>
|
|
488
|
+
<strong>{definition?.label ?? transformId}</strong>
|
|
489
|
+
<code>{transformId}</code>
|
|
490
|
+
<span className="workbench-field-remap-detail__step-hint">Edit convert</span>
|
|
491
|
+
</button>
|
|
492
|
+
<Button
|
|
493
|
+
compact
|
|
494
|
+
type="button"
|
|
495
|
+
aria-label={`Remove convert step ${index + 1}`}
|
|
496
|
+
data-testid={`field-remap-detail-remove-step-${index}`}
|
|
497
|
+
onClick={() => {
|
|
498
|
+
const next = removeTransformStepFromEdge(edge, index);
|
|
499
|
+
applyEdge(next);
|
|
500
|
+
}}
|
|
501
|
+
>
|
|
502
|
+
×
|
|
503
|
+
</Button>
|
|
504
|
+
</li>
|
|
505
|
+
);
|
|
506
|
+
})}
|
|
507
|
+
</ol>
|
|
508
|
+
)}
|
|
509
|
+
|
|
510
|
+
{chain.length < MAX_TRANSFORM_CHAIN ? (
|
|
511
|
+
<div
|
|
512
|
+
className="workbench-field-remap-detail__palette"
|
|
513
|
+
data-testid="field-remap-transform-palette"
|
|
514
|
+
>
|
|
515
|
+
<label>
|
|
516
|
+
<span>Add convert</span>
|
|
517
|
+
<select
|
|
518
|
+
aria-label="Convert palette"
|
|
519
|
+
data-testid="field-remap-palette-select"
|
|
520
|
+
value={effectivePaletteId}
|
|
521
|
+
disabled={appendCatalog.length === 0}
|
|
522
|
+
onChange={(event) => setPaletteId(event.target.value)}
|
|
523
|
+
>
|
|
524
|
+
{appendCatalog.length === 0 ? (
|
|
525
|
+
<option value="">No compatible converts</option>
|
|
526
|
+
) : null}
|
|
527
|
+
{appendCatalog.map((item) => (
|
|
528
|
+
<option key={item.id} value={item.id}>
|
|
529
|
+
{item.label}
|
|
530
|
+
</option>
|
|
531
|
+
))}
|
|
532
|
+
</select>
|
|
533
|
+
</label>
|
|
534
|
+
<Button
|
|
535
|
+
compact
|
|
536
|
+
type="button"
|
|
537
|
+
data-testid="field-remap-palette-add"
|
|
538
|
+
disabled={!effectivePaletteId}
|
|
539
|
+
onClick={() => {
|
|
540
|
+
const next = addTransformStepToEdge(edge, effectivePaletteId, {
|
|
541
|
+
registry: transforms,
|
|
542
|
+
sourceType: portTypes.sourceType,
|
|
543
|
+
targetType: portTypes.targetType,
|
|
544
|
+
});
|
|
545
|
+
if (!next) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
applyEdge(next);
|
|
549
|
+
onSelectionChange({
|
|
550
|
+
kind: 'transformStep',
|
|
551
|
+
edgeId: edge.id,
|
|
552
|
+
stepIndex: (next.transformIds?.length ?? 1) - 1,
|
|
553
|
+
});
|
|
554
|
+
}}
|
|
555
|
+
>
|
|
556
|
+
Add
|
|
557
|
+
</Button>
|
|
558
|
+
</div>
|
|
559
|
+
) : null}
|
|
560
|
+
</section>
|
|
561
|
+
|
|
562
|
+
{listContextEnabled ? (
|
|
563
|
+
<section
|
|
564
|
+
className="workbench-field-remap-detail__section"
|
|
565
|
+
aria-labelledby="field-remap-detail-list-context"
|
|
566
|
+
data-testid="field-remap-list-context"
|
|
567
|
+
>
|
|
568
|
+
<div className="workbench-field-remap-detail__list-context-bar">
|
|
569
|
+
<h5 id="field-remap-detail-list-context">List context</h5>
|
|
570
|
+
{!edge.itemEdges ? (
|
|
571
|
+
<Button
|
|
572
|
+
compact
|
|
573
|
+
type="button"
|
|
574
|
+
data-testid="field-remap-enable-list-context"
|
|
575
|
+
onClick={() => applyEdge(enableListContextOnEdge(edge))}
|
|
576
|
+
>
|
|
577
|
+
Enable item fields
|
|
578
|
+
</Button>
|
|
579
|
+
) : null}
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
{edge.itemEdges ? (
|
|
583
|
+
<>
|
|
584
|
+
<p className="workbench-field-remap-detail__muted">
|
|
585
|
+
{pendingItemSourceId
|
|
586
|
+
? 'Select a target item field to complete the item binding.'
|
|
587
|
+
: 'Select a source item field, then a target item field.'}
|
|
588
|
+
</p>
|
|
589
|
+
<div className="workbench-field-remap-detail__item-pickers">
|
|
590
|
+
<label>
|
|
591
|
+
<span>Source item field</span>
|
|
592
|
+
<select
|
|
593
|
+
aria-label="Source item field"
|
|
594
|
+
data-testid="field-remap-item-source"
|
|
595
|
+
value={pendingItemSourceId ?? ''}
|
|
596
|
+
onChange={(event) => setPendingItemSourceId(event.target.value || null)}
|
|
597
|
+
>
|
|
598
|
+
<option value="">Select…</option>
|
|
599
|
+
{itemSourceChildren.map((child) => (
|
|
600
|
+
<option key={child.id} value={child.id}>
|
|
601
|
+
{child.path ?? child.label}
|
|
602
|
+
</option>
|
|
603
|
+
))}
|
|
604
|
+
</select>
|
|
605
|
+
</label>
|
|
606
|
+
<label>
|
|
607
|
+
<span>Target item field</span>
|
|
608
|
+
<select
|
|
609
|
+
aria-label="Target item field"
|
|
610
|
+
data-testid="field-remap-item-target"
|
|
611
|
+
value=""
|
|
612
|
+
disabled={!pendingItemSourceId}
|
|
613
|
+
onChange={(event) => {
|
|
614
|
+
const targetId = event.target.value;
|
|
615
|
+
if (!pendingItemSourceId || !targetId) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const itemEdge: MappingEdge = {
|
|
619
|
+
id: `ie-${pendingItemSourceId}-${targetId}-${Date.now()}`,
|
|
620
|
+
sourceFieldId: pendingItemSourceId,
|
|
621
|
+
targetSlotId: targetId,
|
|
622
|
+
};
|
|
623
|
+
applyEdge(upsertItemEdgeOnParent(edge, itemEdge));
|
|
624
|
+
setPendingItemSourceId(null);
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
<option value="">Select…</option>
|
|
628
|
+
{itemTargetChildren.map((child) => (
|
|
629
|
+
<option key={child.id} value={child.id}>
|
|
630
|
+
{child.path ?? child.label}
|
|
631
|
+
</option>
|
|
632
|
+
))}
|
|
633
|
+
</select>
|
|
634
|
+
</label>
|
|
635
|
+
</div>
|
|
636
|
+
<ul className="workbench-field-remap-detail__item-edges">
|
|
637
|
+
{(edge.itemEdges ?? []).map((itemEdge) => (
|
|
638
|
+
<li key={itemEdge.id} data-testid={`field-remap-item-edge-${itemEdge.id}`}>
|
|
639
|
+
<code>
|
|
640
|
+
{fieldLabel(itemEdge.sourceFieldId, sources, targets)} →{' '}
|
|
641
|
+
{fieldLabel(itemEdge.targetSlotId, sources, targets)}
|
|
642
|
+
</code>
|
|
643
|
+
<Button
|
|
644
|
+
compact
|
|
645
|
+
type="button"
|
|
646
|
+
onClick={() => applyEdge(removeItemEdgeFromParent(edge, itemEdge.id))}
|
|
647
|
+
>
|
|
648
|
+
Remove
|
|
649
|
+
</Button>
|
|
650
|
+
</li>
|
|
651
|
+
))}
|
|
652
|
+
</ul>
|
|
653
|
+
</>
|
|
654
|
+
) : null}
|
|
655
|
+
</section>
|
|
656
|
+
) : null}
|
|
657
|
+
</aside>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import { ScrollArea } from '@workbench-kit/react/primitives';
|
|
3
|
+
|
|
4
|
+
import { SampleFieldRemapDemo } from './demo.js';
|
|
5
|
+
import { resolveFieldRemapSampleId } from './samples.js';
|
|
6
|
+
import './view.css';
|
|
7
|
+
|
|
8
|
+
export type FieldRemapEditorSurfaceId = string;
|
|
9
|
+
|
|
10
|
+
export interface FieldRemapEditorSurfaceProps {
|
|
11
|
+
readonly resourceUri: string;
|
|
12
|
+
readonly surfaceId: FieldRemapEditorSurfaceId;
|
|
13
|
+
readonly tabId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function FieldRemapEditorSurface({
|
|
17
|
+
resourceUri,
|
|
18
|
+
surfaceId,
|
|
19
|
+
tabId,
|
|
20
|
+
}: FieldRemapEditorSurfaceProps): JSX.Element {
|
|
21
|
+
const sampleId = resolveFieldRemapSampleId(surfaceId);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ScrollArea
|
|
25
|
+
aria-label={`${sampleId} field remap`}
|
|
26
|
+
className="workbench-field-remap-editor-surface"
|
|
27
|
+
data-editor-host-id={tabId}
|
|
28
|
+
data-resource-uri={resourceUri}
|
|
29
|
+
data-sample-id={sampleId}
|
|
30
|
+
data-testid="field-remap-editor-surface"
|
|
31
|
+
orientation="vertical"
|
|
32
|
+
>
|
|
33
|
+
<SampleFieldRemapDemo sampleId={sampleId} />
|
|
34
|
+
</ScrollArea>
|
|
35
|
+
);
|
|
36
|
+
}
|