@workbench-kit/shell-react 0.0.2-prototype.0.2.26 → 0.0.2-prototype.0.2.28
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 +18 -2
- package/package.json +10 -10
- package/src/chat/command-policy.ts +3 -17
- package/src/commands/use-command-descriptors.ts +2 -30
- package/src/commands/use-context-key-revision.ts +12 -12
- package/src/commands/use-extension-registry-command-descriptors.ts +46 -0
- package/src/devtools/use-workbench-devtools-snapshot.ts +1 -1
- package/src/editor/pane-visibility.ts +0 -23
- package/src/editor/view-providers.tsx +0 -7
- package/src/explorer/reveal.ts +1 -12
- package/src/extensions/builtin/commands/src/command-inspector-editor-host.ts +1 -19
- package/src/extensions/builtin/commands/src/index.ts +2 -2
- package/src/field-remap/convert-note-editor.tsx +2 -4
- package/src/field-remap/convert-palette.tsx +17 -20
- package/src/field-remap/flow-adapter.ts +21 -57
- package/src/field-remap/flow-ops.ts +0 -24
- package/src/field-remap/index.ts +0 -1
- package/src/field-remap/io-class-browse.tsx +29 -6
- package/src/field-remap/jsonata-transform.ts +9 -39
- package/src/field-remap/shape-io-editor.tsx +1 -1
- package/src/field-remap/transform-options-editor.tsx +1 -2
- package/src/field-remap/view.css +4 -29
- package/src/index.ts +1 -10
- package/src/jdw/widget-form-view.tsx +3 -20
- package/src/jdw/widget-preview-view.tsx +2 -20
- package/src/management/use-command-management.ts +22 -7
- package/src/management/use-keybinding-management.ts +1 -10
- package/src/shell/provider.tsx +19 -9
- package/src/shell/shell.tsx +11 -4
- package/src/workbench/command-host.tsx +46 -7
- package/src/workbench/command-palette.ts +12 -45
- package/src/workbench/workspace-view-state.ts +18 -1
- package/src/field-remap/graph.tsx +0 -559
- package/src/field-remap/table-mapping-adapter.ts +0 -59
- package/src/field-remap/tree.tsx +0 -404
|
@@ -1,559 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState, type JSX } from 'react';
|
|
2
|
-
import { Badge, Button } from '@workbench-kit/react/primitives';
|
|
3
|
-
import {
|
|
4
|
-
MAX_TRANSFORM_CHAIN,
|
|
5
|
-
flattenSourceFields,
|
|
6
|
-
flattenTargetSlots,
|
|
7
|
-
type MappingEdge,
|
|
8
|
-
type SourceField,
|
|
9
|
-
type TargetSlot,
|
|
10
|
-
type ValueTransformDefinition,
|
|
11
|
-
type ValueTransformRegistry,
|
|
12
|
-
} from '@workbench-kit/field-remap';
|
|
13
|
-
|
|
14
|
-
interface KeyRow {
|
|
15
|
-
readonly id: string;
|
|
16
|
-
readonly label: string;
|
|
17
|
-
readonly path?: string;
|
|
18
|
-
readonly dataType?: string;
|
|
19
|
-
readonly depth: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function collectKeys(
|
|
23
|
-
nodes: readonly {
|
|
24
|
-
id: string;
|
|
25
|
-
label: string;
|
|
26
|
-
path?: string;
|
|
27
|
-
dataType?: string;
|
|
28
|
-
children?: readonly unknown[];
|
|
29
|
-
}[],
|
|
30
|
-
depth = 0,
|
|
31
|
-
): KeyRow[] {
|
|
32
|
-
const rows: KeyRow[] = [];
|
|
33
|
-
for (const node of nodes) {
|
|
34
|
-
rows.push({
|
|
35
|
-
id: node.id,
|
|
36
|
-
label: node.label,
|
|
37
|
-
path: node.path,
|
|
38
|
-
dataType: node.dataType,
|
|
39
|
-
depth,
|
|
40
|
-
});
|
|
41
|
-
if (node.children && Array.isArray(node.children) && node.children.length > 0) {
|
|
42
|
-
rows.push(
|
|
43
|
-
...collectKeys(
|
|
44
|
-
node.children as readonly {
|
|
45
|
-
id: string;
|
|
46
|
-
label: string;
|
|
47
|
-
path?: string;
|
|
48
|
-
dataType?: string;
|
|
49
|
-
children?: readonly unknown[];
|
|
50
|
-
}[],
|
|
51
|
-
depth + 1,
|
|
52
|
-
),
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return rows;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function labelFor(
|
|
60
|
-
id: string,
|
|
61
|
-
sources: readonly SourceField[],
|
|
62
|
-
targets: readonly TargetSlot[],
|
|
63
|
-
): string {
|
|
64
|
-
const source = flattenSourceFields(sources).find((field) => field.id === id);
|
|
65
|
-
if (source) {
|
|
66
|
-
return source.path ?? source.label;
|
|
67
|
-
}
|
|
68
|
-
const target = flattenTargetSlots(targets).find((slot) => slot.id === id);
|
|
69
|
-
return target?.path ?? target?.label ?? id;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function findSource(sources: readonly SourceField[], id: string): SourceField | undefined {
|
|
73
|
-
return flattenSourceFields(sources).find((field) => field.id === id);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function findTarget(targets: readonly TargetSlot[], id: string): TargetSlot | undefined {
|
|
77
|
-
return flattenTargetSlots(targets).find((slot) => slot.id === id);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface FieldRemapGraphMapperProps {
|
|
81
|
-
readonly sources: readonly SourceField[];
|
|
82
|
-
readonly targets: readonly TargetSlot[];
|
|
83
|
-
readonly edges: readonly MappingEdge[];
|
|
84
|
-
readonly transforms: ValueTransformRegistry;
|
|
85
|
-
readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Left/right key columns + middle transform nodes per binding (format / reduce chain).
|
|
90
|
-
* Edges stay `MappingEdge` (`transformIds` ≤ {@link MAX_TRANSFORM_CHAIN}).
|
|
91
|
-
*/
|
|
92
|
-
export function FieldRemapGraphMapper({
|
|
93
|
-
sources,
|
|
94
|
-
targets,
|
|
95
|
-
edges,
|
|
96
|
-
transforms,
|
|
97
|
-
onEdgesChange,
|
|
98
|
-
}: FieldRemapGraphMapperProps): JSX.Element {
|
|
99
|
-
const sourceKeys = useMemo(() => collectKeys(sources), [sources]);
|
|
100
|
-
const targetKeys = useMemo(() => collectKeys(targets), [targets]);
|
|
101
|
-
const catalog = useMemo(
|
|
102
|
-
() =>
|
|
103
|
-
transforms
|
|
104
|
-
.list()
|
|
105
|
-
.filter((item) => item.id !== 'identity')
|
|
106
|
-
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
107
|
-
[transforms],
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const [pendingSourceId, setPendingSourceId] = useState<string | null>(null);
|
|
111
|
-
const [listContextEdgeId, setListContextEdgeId] = useState<string | null>(null);
|
|
112
|
-
const [pendingItemSourceId, setPendingItemSourceId] = useState<string | null>(null);
|
|
113
|
-
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
|
|
114
|
-
|
|
115
|
-
const mappedSourceIds = useMemo(() => new Set(edges.map((edge) => edge.sourceFieldId)), [edges]);
|
|
116
|
-
const mappedTargetIds = useMemo(() => new Set(edges.map((edge) => edge.targetSlotId)), [edges]);
|
|
117
|
-
|
|
118
|
-
const listContextEdge = edges.find((edge) => edge.id === listContextEdgeId) ?? null;
|
|
119
|
-
const listSource = listContextEdge
|
|
120
|
-
? findSource(sources, listContextEdge.sourceFieldId)
|
|
121
|
-
: undefined;
|
|
122
|
-
const listTarget = listContextEdge
|
|
123
|
-
? findTarget(targets, listContextEdge.targetSlotId)
|
|
124
|
-
: undefined;
|
|
125
|
-
|
|
126
|
-
const patchEdge = (edgeId: string, patch: Partial<MappingEdge>) => {
|
|
127
|
-
onEdgesChange(edges.map((edge) => (edge.id === edgeId ? { ...edge, ...patch } : edge)));
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const connect = (sourceId: string, targetId: string) => {
|
|
131
|
-
const source = findSource(sources, sourceId);
|
|
132
|
-
const target = findTarget(targets, targetId);
|
|
133
|
-
if (!source || !target) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const isListContext =
|
|
137
|
-
source.dataType === 'array' &&
|
|
138
|
-
target.dataType === 'array' &&
|
|
139
|
-
Boolean(source.children?.length && target.children?.length);
|
|
140
|
-
|
|
141
|
-
const nextEdge: MappingEdge = {
|
|
142
|
-
id: `e-${sourceId}-${targetId}-${Date.now()}`,
|
|
143
|
-
sourceFieldId: sourceId,
|
|
144
|
-
targetSlotId: targetId,
|
|
145
|
-
...(isListContext ? { itemEdges: [] as MappingEdge[] } : {}),
|
|
146
|
-
};
|
|
147
|
-
const withoutTarget = edges.filter((edge) => edge.targetSlotId !== targetId);
|
|
148
|
-
onEdgesChange([...withoutTarget, nextEdge]);
|
|
149
|
-
setPendingSourceId(null);
|
|
150
|
-
setSelectedEdgeId(nextEdge.id);
|
|
151
|
-
if (isListContext) {
|
|
152
|
-
setListContextEdgeId(nextEdge.id);
|
|
153
|
-
setPendingItemSourceId(null);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const handleSourceSelect = (id: string) => {
|
|
158
|
-
if (listContextEdgeId) {
|
|
159
|
-
setPendingItemSourceId(id);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
setPendingSourceId(id);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const handleTargetSelect = (id: string) => {
|
|
166
|
-
if (listContextEdge && pendingItemSourceId) {
|
|
167
|
-
const itemEdge: MappingEdge = {
|
|
168
|
-
id: `ie-${pendingItemSourceId}-${id}-${Date.now()}`,
|
|
169
|
-
sourceFieldId: pendingItemSourceId,
|
|
170
|
-
targetSlotId: id,
|
|
171
|
-
};
|
|
172
|
-
onEdgesChange(
|
|
173
|
-
edges.map((edge) =>
|
|
174
|
-
edge.id === listContextEdge.id
|
|
175
|
-
? {
|
|
176
|
-
...edge,
|
|
177
|
-
itemEdges: [
|
|
178
|
-
...(edge.itemEdges ?? []).filter((child) => child.targetSlotId !== id),
|
|
179
|
-
itemEdge,
|
|
180
|
-
],
|
|
181
|
-
}
|
|
182
|
-
: edge,
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
setPendingItemSourceId(null);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (!pendingSourceId) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
connect(pendingSourceId, id);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const addTransformNode = (edgeId: string, transformId: string) => {
|
|
195
|
-
const edge = edges.find((item) => item.id === edgeId);
|
|
196
|
-
if (!edge) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const current = edge.transformIds ?? [];
|
|
200
|
-
if (current.length >= MAX_TRANSFORM_CHAIN) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
patchEdge(edgeId, { transformIds: [...current, transformId] });
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const setTransformAt = (edgeId: string, index: number, transformId: string) => {
|
|
207
|
-
const edge = edges.find((item) => item.id === edgeId);
|
|
208
|
-
if (!edge) {
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const next = [...(edge.transformIds ?? [])];
|
|
212
|
-
next[index] = transformId;
|
|
213
|
-
patchEdge(edgeId, { transformIds: next });
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const removeTransformAt = (edgeId: string, index: number) => {
|
|
217
|
-
const edge = edges.find((item) => item.id === edgeId);
|
|
218
|
-
if (!edge) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const next = [...(edge.transformIds ?? [])];
|
|
222
|
-
next.splice(index, 1);
|
|
223
|
-
const optionSteps = edge.transformOptionSteps ? [...edge.transformOptionSteps] : undefined;
|
|
224
|
-
if (optionSteps) {
|
|
225
|
-
optionSteps.splice(index, 1);
|
|
226
|
-
}
|
|
227
|
-
patchEdge(edgeId, {
|
|
228
|
-
transformIds: next.length > 0 ? next : undefined,
|
|
229
|
-
transformOptionSteps: optionSteps && optionSteps.length > 0 ? optionSteps : undefined,
|
|
230
|
-
});
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const setNodeOption = (edgeId: string, index: number, key: string, value: string) => {
|
|
234
|
-
const edge = edges.find((item) => item.id === edgeId);
|
|
235
|
-
if (!edge) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const chain = edge.transformIds ?? [];
|
|
239
|
-
const steps = Array.from({ length: chain.length }, (_, i) => ({
|
|
240
|
-
...(edge.transformOptionSteps?.[i] ?? {}),
|
|
241
|
-
}));
|
|
242
|
-
steps[index] = { ...steps[index], [key]: value };
|
|
243
|
-
patchEdge(edgeId, { transformOptionSteps: steps });
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const removeEdge = (edgeId: string) => {
|
|
247
|
-
onEdgesChange(edges.filter((edge) => edge.id !== edgeId));
|
|
248
|
-
if (listContextEdgeId === edgeId) {
|
|
249
|
-
setListContextEdgeId(null);
|
|
250
|
-
setPendingItemSourceId(null);
|
|
251
|
-
}
|
|
252
|
-
if (selectedEdgeId === edgeId) {
|
|
253
|
-
setSelectedEdgeId(null);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const visibleSourceKeys =
|
|
258
|
-
listContextEdge && listSource?.children ? collectKeys(listSource.children) : sourceKeys;
|
|
259
|
-
const visibleTargetKeys =
|
|
260
|
-
listContextEdge && listTarget?.children ? collectKeys(listTarget.children) : targetKeys;
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<div className="workbench-field-remap-mapper" data-testid="field-remap-mapper">
|
|
264
|
-
<p className="workbench-field-remap-mapper__hint" data-testid="field-remap-hint">
|
|
265
|
-
{listContextEdge
|
|
266
|
-
? pendingItemSourceId
|
|
267
|
-
? 'List context: select a target item field.'
|
|
268
|
-
: 'List context: select a source item field, then a target item field.'
|
|
269
|
-
: pendingSourceId
|
|
270
|
-
? 'Select a target key to create a binding. Add middle nodes for formatting.'
|
|
271
|
-
: 'Select a source key, then a target key. Insert transform nodes in the middle when values need formatting.'}
|
|
272
|
-
</p>
|
|
273
|
-
|
|
274
|
-
<div className="workbench-field-remap-graph" data-testid="field-remap-graph">
|
|
275
|
-
<KeyColumn
|
|
276
|
-
title={listContextEdge ? `A item · ${listSource?.label ?? 'source'}` : 'A keys'}
|
|
277
|
-
keys={visibleSourceKeys}
|
|
278
|
-
selectedId={listContextEdge ? pendingItemSourceId : pendingSourceId}
|
|
279
|
-
mappedIds={
|
|
280
|
-
listContextEdge
|
|
281
|
-
? new Set((listContextEdge.itemEdges ?? []).map((edge) => edge.sourceFieldId))
|
|
282
|
-
: mappedSourceIds
|
|
283
|
-
}
|
|
284
|
-
onSelect={handleSourceSelect}
|
|
285
|
-
side="source"
|
|
286
|
-
/>
|
|
287
|
-
|
|
288
|
-
<div className="workbench-field-remap-graph__center" data-testid="field-remap-lanes">
|
|
289
|
-
<h4 className="workbench-field-remap-tree__title">Bindings / nodes</h4>
|
|
290
|
-
{edges.length === 0 ? (
|
|
291
|
-
<p className="workbench-field-remap-graph__empty">No bindings yet.</p>
|
|
292
|
-
) : (
|
|
293
|
-
<ul className="workbench-field-remap-graph__lanes">
|
|
294
|
-
{edges.map((edge) => (
|
|
295
|
-
<BindingLane
|
|
296
|
-
key={edge.id}
|
|
297
|
-
edge={edge}
|
|
298
|
-
selected={selectedEdgeId === edge.id}
|
|
299
|
-
sourceLabel={labelFor(edge.sourceFieldId, sources, targets)}
|
|
300
|
-
targetLabel={labelFor(edge.targetSlotId, sources, targets)}
|
|
301
|
-
catalog={catalog}
|
|
302
|
-
onSelect={() => setSelectedEdgeId(edge.id)}
|
|
303
|
-
onAddNode={(transformId) => addTransformNode(edge.id, transformId)}
|
|
304
|
-
onChangeNode={(index, transformId) => setTransformAt(edge.id, index, transformId)}
|
|
305
|
-
onRemoveNode={(index) => removeTransformAt(edge.id, index)}
|
|
306
|
-
onOptionChange={(index, key, value) => setNodeOption(edge.id, index, key, value)}
|
|
307
|
-
onEditItems={
|
|
308
|
-
edge.itemEdges
|
|
309
|
-
? () => {
|
|
310
|
-
setListContextEdgeId(edge.id);
|
|
311
|
-
setPendingItemSourceId(null);
|
|
312
|
-
setPendingSourceId(null);
|
|
313
|
-
}
|
|
314
|
-
: undefined
|
|
315
|
-
}
|
|
316
|
-
onRemove={() => removeEdge(edge.id)}
|
|
317
|
-
/>
|
|
318
|
-
))}
|
|
319
|
-
</ul>
|
|
320
|
-
)}
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
<KeyColumn
|
|
324
|
-
title={listContextEdge ? `B item · ${listTarget?.label ?? 'target'}` : 'B keys'}
|
|
325
|
-
keys={visibleTargetKeys}
|
|
326
|
-
selectedId={null}
|
|
327
|
-
mappedIds={
|
|
328
|
-
listContextEdge
|
|
329
|
-
? new Set((listContextEdge.itemEdges ?? []).map((edge) => edge.targetSlotId))
|
|
330
|
-
: mappedTargetIds
|
|
331
|
-
}
|
|
332
|
-
onSelect={handleTargetSelect}
|
|
333
|
-
side="target"
|
|
334
|
-
/>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
{listContextEdge ? (
|
|
338
|
-
<div
|
|
339
|
-
className="workbench-field-remap-mapper__list-context"
|
|
340
|
-
data-testid="field-remap-list-context"
|
|
341
|
-
>
|
|
342
|
-
<div className="workbench-field-remap-mapper__list-context-bar">
|
|
343
|
-
<strong>List context</strong>
|
|
344
|
-
<span>
|
|
345
|
-
{listContextEdge.sourceFieldId} → {listContextEdge.targetSlotId}
|
|
346
|
-
</span>
|
|
347
|
-
<Button
|
|
348
|
-
compact
|
|
349
|
-
type="button"
|
|
350
|
-
onClick={() => {
|
|
351
|
-
setListContextEdgeId(null);
|
|
352
|
-
setPendingItemSourceId(null);
|
|
353
|
-
}}
|
|
354
|
-
>
|
|
355
|
-
Done
|
|
356
|
-
</Button>
|
|
357
|
-
</div>
|
|
358
|
-
<ul className="workbench-field-remap-mapper__edges" data-testid="field-remap-edges">
|
|
359
|
-
{(listContextEdge.itemEdges ?? []).map((edge) => (
|
|
360
|
-
<li key={edge.id}>
|
|
361
|
-
<code>
|
|
362
|
-
{edge.sourceFieldId} → {edge.targetSlotId}
|
|
363
|
-
</code>
|
|
364
|
-
</li>
|
|
365
|
-
))}
|
|
366
|
-
</ul>
|
|
367
|
-
</div>
|
|
368
|
-
) : (
|
|
369
|
-
<ul
|
|
370
|
-
className="workbench-field-remap-mapper__edges visually-hidden"
|
|
371
|
-
data-testid="field-remap-edges"
|
|
372
|
-
>
|
|
373
|
-
{edges.map((edge) => (
|
|
374
|
-
<li key={edge.id}>
|
|
375
|
-
{edge.sourceFieldId} → {edge.targetSlotId}
|
|
376
|
-
{edge.transformIds?.length ? ` · ${edge.transformIds.join(' → ')}` : ''}
|
|
377
|
-
</li>
|
|
378
|
-
))}
|
|
379
|
-
</ul>
|
|
380
|
-
)}
|
|
381
|
-
</div>
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function KeyColumn({
|
|
386
|
-
title,
|
|
387
|
-
keys,
|
|
388
|
-
selectedId,
|
|
389
|
-
mappedIds,
|
|
390
|
-
onSelect,
|
|
391
|
-
side,
|
|
392
|
-
}: {
|
|
393
|
-
readonly title: string;
|
|
394
|
-
readonly keys: readonly KeyRow[];
|
|
395
|
-
readonly selectedId: string | null;
|
|
396
|
-
readonly mappedIds: ReadonlySet<string>;
|
|
397
|
-
readonly onSelect: (id: string) => void;
|
|
398
|
-
readonly side: 'source' | 'target';
|
|
399
|
-
}): JSX.Element {
|
|
400
|
-
return (
|
|
401
|
-
<div className="workbench-field-remap-tree" data-side={side}>
|
|
402
|
-
<h4 className="workbench-field-remap-tree__title">{title}</h4>
|
|
403
|
-
<ul className="workbench-field-remap-graph__keys" role="list">
|
|
404
|
-
{keys.map((key) => {
|
|
405
|
-
const badge = key.dataType === 'object' || key.dataType === 'array' ? key.dataType : null;
|
|
406
|
-
return (
|
|
407
|
-
<li key={key.id}>
|
|
408
|
-
<button
|
|
409
|
-
type="button"
|
|
410
|
-
className={[
|
|
411
|
-
'workbench-field-remap-graph__key',
|
|
412
|
-
selectedId === key.id ? 'is-selected' : '',
|
|
413
|
-
mappedIds.has(key.id) ? 'is-mapped' : '',
|
|
414
|
-
]
|
|
415
|
-
.filter(Boolean)
|
|
416
|
-
.join(' ')}
|
|
417
|
-
style={{ paddingLeft: `${0.5 + key.depth * 0.75}rem` }}
|
|
418
|
-
data-testid={`field-remap-node-${key.id}`}
|
|
419
|
-
onClick={() => onSelect(key.id)}
|
|
420
|
-
>
|
|
421
|
-
<span className="workbench-field-remap-tree__label">{key.label}</span>
|
|
422
|
-
{badge ? <Badge variant="muted">{badge}</Badge> : null}
|
|
423
|
-
</button>
|
|
424
|
-
</li>
|
|
425
|
-
);
|
|
426
|
-
})}
|
|
427
|
-
</ul>
|
|
428
|
-
</div>
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function BindingLane({
|
|
433
|
-
edge,
|
|
434
|
-
selected,
|
|
435
|
-
sourceLabel,
|
|
436
|
-
targetLabel,
|
|
437
|
-
catalog,
|
|
438
|
-
onSelect,
|
|
439
|
-
onAddNode,
|
|
440
|
-
onChangeNode,
|
|
441
|
-
onRemoveNode,
|
|
442
|
-
onOptionChange,
|
|
443
|
-
onEditItems,
|
|
444
|
-
onRemove,
|
|
445
|
-
}: {
|
|
446
|
-
readonly edge: MappingEdge;
|
|
447
|
-
readonly selected: boolean;
|
|
448
|
-
readonly sourceLabel: string;
|
|
449
|
-
readonly targetLabel: string;
|
|
450
|
-
readonly catalog: readonly ValueTransformDefinition[];
|
|
451
|
-
readonly onSelect: () => void;
|
|
452
|
-
readonly onAddNode: (transformId: string) => void;
|
|
453
|
-
readonly onChangeNode: (index: number, transformId: string) => void;
|
|
454
|
-
readonly onRemoveNode: (index: number) => void;
|
|
455
|
-
readonly onOptionChange: (index: number, key: string, value: string) => void;
|
|
456
|
-
readonly onEditItems?: (() => void) | undefined;
|
|
457
|
-
readonly onRemove: () => void;
|
|
458
|
-
}): JSX.Element {
|
|
459
|
-
const chain = edge.transformIds ?? [];
|
|
460
|
-
const defaultAddId = catalog[0]?.id ?? 'string:trim';
|
|
461
|
-
|
|
462
|
-
return (
|
|
463
|
-
<li
|
|
464
|
-
className={['workbench-field-remap-graph__lane', selected ? 'is-selected' : '']
|
|
465
|
-
.filter(Boolean)
|
|
466
|
-
.join(' ')}
|
|
467
|
-
data-testid={`field-remap-lane-${edge.id}`}
|
|
468
|
-
>
|
|
469
|
-
<button type="button" className="workbench-field-remap-graph__endpoint" onClick={onSelect}>
|
|
470
|
-
{sourceLabel}
|
|
471
|
-
</button>
|
|
472
|
-
|
|
473
|
-
<span className="workbench-field-remap-graph__arrow" aria-hidden="true">
|
|
474
|
-
→
|
|
475
|
-
</span>
|
|
476
|
-
|
|
477
|
-
<div className="workbench-field-remap-graph__nodes">
|
|
478
|
-
{chain.length === 0 ? (
|
|
479
|
-
<span className="workbench-field-remap-graph__passthrough" title="Pass-through">
|
|
480
|
-
direct
|
|
481
|
-
</span>
|
|
482
|
-
) : (
|
|
483
|
-
chain.map((transformId, index) => {
|
|
484
|
-
const definition = catalog.find((item) => item.id === transformId);
|
|
485
|
-
const optionFields = definition?.optionFields ?? [];
|
|
486
|
-
return (
|
|
487
|
-
<div
|
|
488
|
-
key={`${edge.id}-${index}-${transformId}`}
|
|
489
|
-
className="workbench-field-remap-graph__node"
|
|
490
|
-
data-testid={`field-remap-node-transform-${edge.id}-${index}`}
|
|
491
|
-
>
|
|
492
|
-
<select
|
|
493
|
-
aria-label={`Transform step ${index + 1}`}
|
|
494
|
-
value={transformId}
|
|
495
|
-
onChange={(event) => onChangeNode(index, event.target.value)}
|
|
496
|
-
>
|
|
497
|
-
{!catalog.some((item) => item.id === transformId) ? (
|
|
498
|
-
<option value={transformId}>{transformId}</option>
|
|
499
|
-
) : null}
|
|
500
|
-
{catalog.map((item) => (
|
|
501
|
-
<option key={item.id} value={item.id}>
|
|
502
|
-
{item.label}
|
|
503
|
-
</option>
|
|
504
|
-
))}
|
|
505
|
-
</select>
|
|
506
|
-
{optionFields.map((field) => (
|
|
507
|
-
<input
|
|
508
|
-
key={field.key}
|
|
509
|
-
type="text"
|
|
510
|
-
aria-label={field.label}
|
|
511
|
-
placeholder={field.label}
|
|
512
|
-
value={String(edge.transformOptionSteps?.[index]?.[field.key] ?? '')}
|
|
513
|
-
onChange={(event) => onOptionChange(index, field.key, event.target.value)}
|
|
514
|
-
/>
|
|
515
|
-
))}
|
|
516
|
-
<Button compact type="button" onClick={() => onRemoveNode(index)}>
|
|
517
|
-
×
|
|
518
|
-
</Button>
|
|
519
|
-
</div>
|
|
520
|
-
);
|
|
521
|
-
})
|
|
522
|
-
)}
|
|
523
|
-
|
|
524
|
-
{chain.length < MAX_TRANSFORM_CHAIN ? (
|
|
525
|
-
<Button
|
|
526
|
-
compact
|
|
527
|
-
type="button"
|
|
528
|
-
data-testid={`field-remap-add-node-${edge.id}`}
|
|
529
|
-
onClick={() => onAddNode(defaultAddId)}
|
|
530
|
-
>
|
|
531
|
-
+ node
|
|
532
|
-
</Button>
|
|
533
|
-
) : null}
|
|
534
|
-
|
|
535
|
-
{edge.itemSourcePath ? <Badge variant="muted">project {edge.itemSourcePath}</Badge> : null}
|
|
536
|
-
{edge.itemEdges ? <Badge variant="muted">{edge.itemEdges.length} item fields</Badge> : null}
|
|
537
|
-
</div>
|
|
538
|
-
|
|
539
|
-
<span className="workbench-field-remap-graph__arrow" aria-hidden="true">
|
|
540
|
-
→
|
|
541
|
-
</span>
|
|
542
|
-
|
|
543
|
-
<button type="button" className="workbench-field-remap-graph__endpoint" onClick={onSelect}>
|
|
544
|
-
{targetLabel}
|
|
545
|
-
</button>
|
|
546
|
-
|
|
547
|
-
<span className="workbench-field-remap-mapper__edge-actions">
|
|
548
|
-
{onEditItems ? (
|
|
549
|
-
<Button compact type="button" onClick={onEditItems}>
|
|
550
|
-
Edit items
|
|
551
|
-
</Button>
|
|
552
|
-
) : null}
|
|
553
|
-
<Button compact type="button" onClick={onRemove}>
|
|
554
|
-
Remove
|
|
555
|
-
</Button>
|
|
556
|
-
</span>
|
|
557
|
-
</li>
|
|
558
|
-
);
|
|
559
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { MappingEdge } from '@workbench-kit/field-remap';
|
|
2
|
-
import type { FieldItemInput, Mapping } from 'react-table-mapping';
|
|
3
|
-
|
|
4
|
-
/** One column: field name (static string cell). */
|
|
5
|
-
export const FIELD_REMAP_NAME_COLUMNS = [{ title: 'Field', key: 'name' }] as const;
|
|
6
|
-
|
|
7
|
-
export interface FieldRemapLeaf {
|
|
8
|
-
/** Kit field/slot id (may contain dots, e.g. `a.user_name`). */
|
|
9
|
-
readonly id: string;
|
|
10
|
-
readonly label: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* react-table-mapping builds connectors with `querySelector('#connector-…'+id)`.
|
|
15
|
-
* Dots in ids break CSS selectors (`#a.user_name` → id=a + class=user_name).
|
|
16
|
-
* Encode kit ids for the OSS UI and decode when adapting mappings.
|
|
17
|
-
*/
|
|
18
|
-
export function toTableMappingFieldId(kitFieldId: string): string {
|
|
19
|
-
return kitFieldId.split('.').join('__');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function fromTableMappingFieldId(tableFieldId: string): string {
|
|
23
|
-
return tableFieldId.split('__').join('.');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Build react-table-mapping source/target rows from leaf field descriptors. */
|
|
27
|
-
export function toTableMappingFields(leaves: readonly FieldRemapLeaf[]): FieldItemInput[] {
|
|
28
|
-
return leaves.map((leaf) => {
|
|
29
|
-
const id = toTableMappingFieldId(leaf.id);
|
|
30
|
-
return {
|
|
31
|
-
id,
|
|
32
|
-
key: id,
|
|
33
|
-
name: {
|
|
34
|
-
type: 'string' as const,
|
|
35
|
-
columnKey: 'name',
|
|
36
|
-
value: leaf.label,
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** OSS Mapping[] → kit MappingEdge[] (identity / pass-through). */
|
|
43
|
-
export function tableMappingsToEdges(mappings: readonly Mapping[]): MappingEdge[] {
|
|
44
|
-
return mappings.map((mapping) => ({
|
|
45
|
-
id: mapping.id,
|
|
46
|
-
sourceFieldId: fromTableMappingFieldId(mapping.source),
|
|
47
|
-
targetSlotId: fromTableMappingFieldId(mapping.target),
|
|
48
|
-
transformIds: ['identity'] as const,
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** kit MappingEdge[] → OSS Mapping[] for controlled mappings. */
|
|
53
|
-
export function edgesToTableMappings(edges: readonly MappingEdge[]): Mapping[] {
|
|
54
|
-
return edges.map((edge) => ({
|
|
55
|
-
id: edge.id,
|
|
56
|
-
source: toTableMappingFieldId(edge.sourceFieldId),
|
|
57
|
-
target: toTableMappingFieldId(edge.targetSlotId),
|
|
58
|
-
}));
|
|
59
|
-
}
|