@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
package/README.md
CHANGED
|
@@ -51,6 +51,28 @@ registry can use `registry-command-descriptors` without importing Provider
|
|
|
51
51
|
context into that leaf bundle. The root barrel remains the discovery surface,
|
|
52
52
|
not the default runtime import graph.
|
|
53
53
|
|
|
54
|
+
### Provider-free command host
|
|
55
|
+
|
|
56
|
+
Hosts that already own command descriptors and execution can compose the canonical
|
|
57
|
+
Command Palette and Quick Open without `WorkbenchProvider`:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { WorkbenchCommandHostController } from '@workbench-kit/shell-react/command-host-controller';
|
|
61
|
+
|
|
62
|
+
<WorkbenchCommandHostController
|
|
63
|
+
commands={commands}
|
|
64
|
+
executeCommand={executeCommand}
|
|
65
|
+
quickOpenProviders={quickOpenProviders}
|
|
66
|
+
/>;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`commands` is the complete palette descriptor set. The controller owns only overlay
|
|
70
|
+
state, hard Palette/Quick Open shortcuts, selection routing, and completion-driven
|
|
71
|
+
closing. Hosts continue to own command registration, descriptor projection, Quick Open
|
|
72
|
+
provider construction, persistence, and error reporting. Pass `shortcutBridge` only
|
|
73
|
+
when the host also wants the generic keybinding bridge; omit it or pass `false` to keep
|
|
74
|
+
that routing host-owned.
|
|
75
|
+
|
|
54
76
|
### Focused extension context migration
|
|
55
77
|
|
|
56
78
|
`WorkbenchContextValue` no longer exposes the aggregate `ExtensionRegistry`.
|
|
@@ -117,8 +139,19 @@ for Flow-only embeds and custom bundler setups. The full barrel
|
|
|
117
139
|
when both durable channels are uncontrolled. If either channel is controlled, pass one
|
|
118
140
|
`historyOwner` for the complete composite state; the Panel never creates a partial stack.
|
|
119
141
|
`historyActionsRef` exposes host-chrome actions and
|
|
120
|
-
`onHistoryAvailabilityChange` reports whether those actions are available.
|
|
121
|
-
|
|
142
|
+
`onHistoryAvailabilityChange` reports whether those actions are available. The Panel routes
|
|
143
|
+
available undo/redo chords through that same owner; direct `FieldRemapFlowMapper` consumers
|
|
144
|
+
remain responsible for host-owned history routing.
|
|
145
|
+
|
|
146
|
+
| Surface | Shortcut | Behavior |
|
|
147
|
+
| ------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
148
|
+
| Flow | `Escape` | Clears the active selection and unfinished drafts. If collapsed detail chrome owned focus, focus moves to the programmatic-only mapper root. |
|
|
149
|
+
| Flow | `Delete` / `Backspace` | Removes the eligible selected edge, convert step, operator, or draft through the existing mutation path. |
|
|
150
|
+
| Panel | `Ctrl/Cmd+Z` | Invokes the existing composite history owner's available undo action. |
|
|
151
|
+
| Panel | `Ctrl/Cmd+Shift+Z`, `Ctrl+Y` | Invokes the existing composite history owner's available redo action. |
|
|
152
|
+
|
|
153
|
+
Editable inputs, textareas, selects, contenteditable elements, and transform option editors
|
|
154
|
+
retain their native key behavior. Unavailable or read-only actions leave the event unconsumed.
|
|
122
155
|
|
|
123
156
|
Only semantic edits coming from the Flow mapper create entries. Hidden mappings are
|
|
124
157
|
reconstructed before an entry is recorded, so undo does not discard filtered state.
|
|
@@ -140,6 +173,13 @@ hooks so hosts avoid CSS/DOM workarounds:
|
|
|
140
173
|
| `flowActionsRef` | `{ fitView(options?) }` using the same defaults as Controls fit-view. |
|
|
141
174
|
| `labels` / `t` | Override edge-list / Convert palette chrome (e.g. “Field maps”). |
|
|
142
175
|
| `ioChrome` (Panel) | `'browse' \| 'edit' \| 'none'` — prefer browse for inspect-only I/O. |
|
|
176
|
+
| `rewirePolicy` (Flow) | `'replace'` by default; `'reject'` preserves prior edges and reports impacted edge IDs. |
|
|
177
|
+
| `onConnectionFeedback` (Flow) | Receives one structured result at connection-attempt completion; hover validation stays silent. |
|
|
178
|
+
| `parentChildConflicts` (Flow) | Optional authoritative conflict projection; `undefined` derives from the supplied Flow inputs. |
|
|
179
|
+
|
|
180
|
+
Rejected attempts render one compact `role="status"` message. Standalone Flow embeds derive
|
|
181
|
+
parent/child conflicts with the domain detector; Panel computes the same conflicts from full shapes
|
|
182
|
+
before hidden-field projection and supplies that authoritative result, so it renders only once.
|
|
143
183
|
|
|
144
184
|
```tsx
|
|
145
185
|
const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workbench-kit/shell-react",
|
|
3
|
-
"version": "0.0.2-prototype.0.2.
|
|
3
|
+
"version": "0.0.2-prototype.0.2.43",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"./command-descriptors": "./src/commands/use-command-descriptors.ts",
|
|
12
12
|
"./registry-command-descriptors": "./src/commands/use-extension-registry-command-descriptors.ts",
|
|
13
13
|
"./command-host": "./src/workbench/command-host.tsx",
|
|
14
|
+
"./command-host-controller": "./src/workbench/command-host-controller.tsx",
|
|
14
15
|
"./command-palette": "./src/workbench/command-palette.ts",
|
|
15
16
|
"./field-remap": "./src/field-remap/index.ts",
|
|
16
17
|
"./field-remap/view.css": "./src/field-remap/view.css",
|
|
@@ -31,14 +32,14 @@
|
|
|
31
32
|
"@radix-ui/react-slot": "^1.3.0",
|
|
32
33
|
"@xyflow/react": "^12.11.2",
|
|
33
34
|
"jsonata": "^2.2.0",
|
|
34
|
-
"@workbench-kit/
|
|
35
|
-
"@workbench-kit/
|
|
36
|
-
"@workbench-kit/
|
|
37
|
-
"@workbench-kit/
|
|
38
|
-
"@workbench-kit/workbench-
|
|
39
|
-
"@workbench-kit/workbench-
|
|
40
|
-
"@workbench-kit/
|
|
41
|
-
"@workbench-kit/
|
|
35
|
+
"@workbench-kit/platform": "0.0.2-prototype.0.2.43",
|
|
36
|
+
"@workbench-kit/field-remap": "0.0.2-prototype.0.2.43",
|
|
37
|
+
"@workbench-kit/tokens": "0.0.2-prototype.0.2.43",
|
|
38
|
+
"@workbench-kit/react": "0.0.2-prototype.0.2.43",
|
|
39
|
+
"@workbench-kit/workbench-core": "0.0.2-prototype.0.2.43",
|
|
40
|
+
"@workbench-kit/workbench-extension-sdk": "0.0.2-prototype.0.2.43",
|
|
41
|
+
"@workbench-kit/workspace": "0.0.2-prototype.0.2.43",
|
|
42
|
+
"@workbench-kit/workbench-config": "0.0.2-prototype.0.2.43"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"react": "^19.0.0",
|
|
@@ -57,6 +58,8 @@
|
|
|
57
58
|
},
|
|
58
59
|
"scripts": {
|
|
59
60
|
"test": "vitest run --config vitest.config.ts src",
|
|
60
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
61
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
62
|
+
"typecheck:exact-optional": "pnpm --filter @workbench-kit/react exec tsc -p typecheck-exact-optional/tsconfig.emit.json && pnpm typecheck:exact-optional:consumer",
|
|
63
|
+
"typecheck:exact-optional:consumer": "tsc -p typecheck-exact-optional/tsconfig.emit.json && tsc -p typecheck-exact-optional/tsconfig.json --noEmit"
|
|
61
64
|
}
|
|
62
65
|
}
|
|
@@ -76,7 +76,11 @@ export class ExtensionEnablementController implements DisposableLike {
|
|
|
76
76
|
private readonly storage: WorkbenchStorageAdapter | undefined;
|
|
77
77
|
private readonly storageKey: string;
|
|
78
78
|
private installedRecords: readonly InstalledExtensionRecord[];
|
|
79
|
-
private
|
|
79
|
+
private readonly themeSelectionProtectionOwners = new Map<
|
|
80
|
+
number,
|
|
81
|
+
ThemeSelectionProtectionSnapshot
|
|
82
|
+
>();
|
|
83
|
+
private themeSelectionProtectionGeneration = 0;
|
|
80
84
|
private disposed = false;
|
|
81
85
|
|
|
82
86
|
constructor({
|
|
@@ -123,11 +127,27 @@ export class ExtensionEnablementController implements DisposableLike {
|
|
|
123
127
|
};
|
|
124
128
|
};
|
|
125
129
|
|
|
126
|
-
setThemeSelectionProtection(snapshot: ThemeSelectionProtectionSnapshot | undefined): void {
|
|
127
|
-
this.
|
|
128
|
-
|
|
130
|
+
setThemeSelectionProtection(snapshot: ThemeSelectionProtectionSnapshot | undefined): () => void {
|
|
131
|
+
const generation = ++this.themeSelectionProtectionGeneration;
|
|
132
|
+
if (snapshot === undefined) {
|
|
133
|
+
this.themeSelectionProtectionOwners.clear();
|
|
134
|
+
return () => undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.themeSelectionProtectionOwners.set(
|
|
138
|
+
generation,
|
|
139
|
+
snapshot.kind === 'known'
|
|
129
140
|
? { ...snapshot, protectedThemeIds: [...snapshot.protectedThemeIds] }
|
|
130
|
-
: snapshot
|
|
141
|
+
: snapshot,
|
|
142
|
+
);
|
|
143
|
+
let disposed = false;
|
|
144
|
+
return () => {
|
|
145
|
+
if (disposed) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
disposed = true;
|
|
149
|
+
this.themeSelectionProtectionOwners.delete(generation);
|
|
150
|
+
};
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
commitInstalledRecords(
|
|
@@ -228,6 +248,7 @@ export class ExtensionEnablementController implements DisposableLike {
|
|
|
228
248
|
this.disposed = true;
|
|
229
249
|
this.listeners.clear();
|
|
230
250
|
this.registrationHandles.clear();
|
|
251
|
+
this.themeSelectionProtectionOwners.clear();
|
|
231
252
|
this.registrationLifetime.dispose();
|
|
232
253
|
}
|
|
233
254
|
|
|
@@ -345,15 +366,25 @@ export class ExtensionEnablementController implements DisposableLike {
|
|
|
345
366
|
return { commitRequestedState: true, eligible: false, kind: 'reloadRequired' };
|
|
346
367
|
}
|
|
347
368
|
|
|
348
|
-
const
|
|
369
|
+
const selectionProtections = [...this.themeSelectionProtectionOwners.values()];
|
|
370
|
+
const themeRegistryRevision = this.registry.themes.getRevision();
|
|
349
371
|
if (
|
|
350
|
-
|
|
351
|
-
|
|
372
|
+
selectionProtections.length === 0 ||
|
|
373
|
+
selectionProtections.some(
|
|
374
|
+
(selectionProtection) =>
|
|
375
|
+
selectionProtection.kind !== 'known' ||
|
|
376
|
+
selectionProtection.themeRegistryRevision !== themeRegistryRevision ||
|
|
377
|
+
!selectionProtection.isCurrent(),
|
|
378
|
+
)
|
|
352
379
|
) {
|
|
353
380
|
return { commitRequestedState: false, eligible: false, kind: 'reloadRequired' };
|
|
354
381
|
}
|
|
355
382
|
|
|
356
|
-
const protectedThemeIds = new Set(
|
|
383
|
+
const protectedThemeIds = new Set(
|
|
384
|
+
selectionProtections.flatMap((selectionProtection) =>
|
|
385
|
+
selectionProtection.kind === 'known' ? selectionProtection.protectedThemeIds : [],
|
|
386
|
+
),
|
|
387
|
+
);
|
|
357
388
|
|
|
358
389
|
const themes = description.manifest.contributes?.themes ?? [];
|
|
359
390
|
if (themes.some((theme) => protectedThemeIds.has(theme.id))) {
|
|
@@ -1,98 +1,123 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DARK_THEME_PRESET_OPTIONS,
|
|
3
|
-
LIGHT_THEME_PRESET_OPTIONS,
|
|
4
|
-
WORKBENCH_COLOR_SCHEME_OPTIONS,
|
|
5
|
-
} from '@workbench-kit/react/workbench';
|
|
6
1
|
import type { ThemeRegistry } from '@workbench-kit/workbench-core';
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import {
|
|
4
|
+
createWorkbenchAppearanceCatalogSnapshot,
|
|
5
|
+
resolveWorkbenchAppearanceSelection,
|
|
6
|
+
type WorkbenchAppearanceCatalogSnapshot,
|
|
7
|
+
type WorkbenchAppearanceHostOptionInput,
|
|
8
|
+
} from '../shell/appearance-catalog.js';
|
|
9
|
+
import { classifyWorkbenchAppearanceThemeSelection } from '../shell/appearance-presentation.js';
|
|
10
|
+
|
|
11
|
+
interface ThemeSelectionProtectionBase {
|
|
12
|
+
readonly isCurrent: () => boolean;
|
|
13
|
+
readonly sourceFingerprint: string;
|
|
14
|
+
readonly themeRegistryRevision: number;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export type ThemeSelectionProtectionSnapshot =
|
|
13
|
-
| {
|
|
18
|
+
| (ThemeSelectionProtectionBase & {
|
|
14
19
|
readonly kind: 'known';
|
|
15
20
|
readonly protectedThemeIds: readonly string[];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
| {
|
|
21
|
+
})
|
|
22
|
+
| (ThemeSelectionProtectionBase & {
|
|
19
23
|
readonly kind: 'unknown';
|
|
20
|
-
|
|
21
|
-
};
|
|
24
|
+
});
|
|
22
25
|
|
|
23
26
|
export interface ThemeSelectionProtectionInput {
|
|
27
|
+
readonly catalog?: WorkbenchAppearanceCatalogSnapshot | undefined;
|
|
24
28
|
readonly darkPreset: string | undefined;
|
|
25
29
|
readonly lightPreset: string | undefined;
|
|
26
30
|
readonly theme: string | undefined;
|
|
27
|
-
readonly themeOptions: readonly
|
|
31
|
+
readonly themeOptions: readonly WorkbenchAppearanceHostOptionInput[] | undefined;
|
|
28
32
|
readonly themes: ThemeRegistry;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
|
-
* Captures only selections that resolve to exactly one
|
|
33
|
-
*
|
|
34
|
-
*
|
|
36
|
+
* Captures only selections that resolve to exactly one eligible catalog row. The action guard
|
|
37
|
+
* reconstructs current own data and compares both revision and the canonical source fingerprint,
|
|
38
|
+
* so writable public contributions cannot reuse stale lifecycle protection.
|
|
35
39
|
*/
|
|
36
40
|
export function createThemeSelectionProtectionSnapshot({
|
|
41
|
+
catalog: suppliedCatalog,
|
|
37
42
|
darkPreset,
|
|
38
43
|
lightPreset,
|
|
39
44
|
theme,
|
|
40
45
|
themeOptions,
|
|
41
46
|
themes,
|
|
42
47
|
}: ThemeSelectionProtectionInput): ThemeSelectionProtectionSnapshot {
|
|
43
|
-
const
|
|
44
|
-
|
|
48
|
+
const catalog =
|
|
49
|
+
suppliedCatalog ??
|
|
50
|
+
createWorkbenchAppearanceCatalogSnapshot({ hostOptions: themeOptions, themes });
|
|
51
|
+
const base: ThemeSelectionProtectionBase = {
|
|
52
|
+
isCurrent: () => isCatalogCurrent(catalog, themes, themeOptions),
|
|
53
|
+
sourceFingerprint: catalog.sourceFingerprint,
|
|
54
|
+
themeRegistryRevision: catalog.themeRegistryRevision,
|
|
55
|
+
};
|
|
45
56
|
const hasLightPreset = lightPreset !== undefined;
|
|
46
57
|
const hasDarkPreset = darkPreset !== undefined;
|
|
47
|
-
|
|
48
|
-
if (hasLightPreset !== hasDarkPreset) {
|
|
49
|
-
return { kind: 'unknown', themeRegistryRevision };
|
|
50
|
-
}
|
|
58
|
+
const themeSelection = classifyWorkbenchAppearanceThemeSelection(theme);
|
|
51
59
|
|
|
52
60
|
if (hasLightPreset && hasDarkPreset) {
|
|
53
|
-
|
|
54
|
-
...
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if (themeSelection.kind !== 'base-preference') {
|
|
62
|
+
return Object.freeze({ ...base, kind: 'unknown' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lightResolution = resolveWorkbenchAppearanceSelection(
|
|
66
|
+
catalog,
|
|
67
|
+
'light-preset',
|
|
68
|
+
lightPreset,
|
|
69
|
+
);
|
|
70
|
+
const darkResolution = resolveWorkbenchAppearanceSelection(catalog, 'dark-preset', darkPreset);
|
|
61
71
|
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
!hasExactlyOneOption(lightPreset, lightOptions) ||
|
|
65
|
-
!hasExactlyOneOption(darkPreset, darkOptions)
|
|
66
|
-
) {
|
|
67
|
-
return { kind: 'unknown', themeRegistryRevision };
|
|
72
|
+
if (lightResolution.status !== 'resolved' || darkResolution.status !== 'resolved') {
|
|
73
|
+
return Object.freeze({ ...base, kind: 'unknown' });
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
return {
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
...base,
|
|
71
78
|
kind: 'known',
|
|
72
|
-
protectedThemeIds: [
|
|
73
|
-
|
|
74
|
-
};
|
|
79
|
+
protectedThemeIds: Object.freeze([lightPreset, darkPreset]),
|
|
80
|
+
});
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
if (themeSelection.kind === 'base-preference') {
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
...base,
|
|
86
|
+
kind: 'known',
|
|
87
|
+
protectedThemeIds: Object.freeze([]),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const themeResolution = resolveWorkbenchAppearanceSelection(
|
|
91
|
+
catalog,
|
|
92
|
+
'flat-theme',
|
|
93
|
+
themeSelection.rawTheme,
|
|
94
|
+
);
|
|
95
|
+
if (themeResolution.status !== 'resolved') {
|
|
96
|
+
return Object.freeze({ ...base, kind: 'unknown' });
|
|
80
97
|
}
|
|
81
98
|
|
|
82
|
-
return {
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
...base,
|
|
83
101
|
kind: 'known',
|
|
84
|
-
protectedThemeIds: [
|
|
85
|
-
|
|
86
|
-
};
|
|
102
|
+
protectedThemeIds: Object.freeze([themeSelection.rawTheme]),
|
|
103
|
+
});
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
function isCatalogCurrent(
|
|
107
|
+
captured: WorkbenchAppearanceCatalogSnapshot,
|
|
108
|
+
themes: ThemeRegistry,
|
|
109
|
+
themeOptions: readonly WorkbenchAppearanceHostOptionInput[] | undefined,
|
|
110
|
+
): boolean {
|
|
111
|
+
try {
|
|
112
|
+
const current = createWorkbenchAppearanceCatalogSnapshot({
|
|
113
|
+
hostOptions: themeOptions,
|
|
114
|
+
themes,
|
|
115
|
+
});
|
|
116
|
+
return (
|
|
117
|
+
current.themeRegistryRevision === captured.themeRegistryRevision &&
|
|
118
|
+
current.sourceFingerprint === captured.sourceFingerprint
|
|
119
|
+
);
|
|
120
|
+
} catch {
|
|
94
121
|
return false;
|
|
95
122
|
}
|
|
96
|
-
|
|
97
|
-
return options.filter((option) => option.id === selection).length === 1;
|
|
98
123
|
}
|
|
@@ -37,6 +37,12 @@ export interface FieldRemapChromeLabels {
|
|
|
37
37
|
readonly hideHiddenFields: string;
|
|
38
38
|
readonly emptyDetailTitle: string;
|
|
39
39
|
readonly emptyDetailDescription: string;
|
|
40
|
+
/** Additive inspection-only empty-detail copy. Omitted hosts use the English defaults. */
|
|
41
|
+
readonly readOnlyEmptyDetailTitle?: string;
|
|
42
|
+
readonly readOnlyEmptyDetailDescription?: string;
|
|
43
|
+
/** Additive selection-detail Modal copy. Omitted legacy label objects use English defaults. */
|
|
44
|
+
readonly detailModalTitle?: string;
|
|
45
|
+
readonly closeDetailModal?: string;
|
|
40
46
|
/** Additive Flow preview copy. Omitted legacy label objects use English defaults. */
|
|
41
47
|
readonly previewTitle?: string;
|
|
42
48
|
readonly previewLoading?: string;
|
|
@@ -76,6 +82,10 @@ export const defaultFieldRemapChromeLabels = {
|
|
|
76
82
|
emptyDetailTitle: 'Start with a convert',
|
|
77
83
|
emptyDetailDescription:
|
|
78
84
|
'Use the Convert palette to place a convert, then wire source → draft → target. Or select an existing binding / convert note on the canvas.',
|
|
85
|
+
readOnlyEmptyDetailTitle: 'Inspect mappings',
|
|
86
|
+
readOnlyEmptyDetailDescription: 'Select a mapping to inspect its details.',
|
|
87
|
+
detailModalTitle: 'Mapping details',
|
|
88
|
+
closeDetailModal: 'Close details',
|
|
79
89
|
previewTitle: 'Sample preview',
|
|
80
90
|
previewLoading: 'Updating preview…',
|
|
81
91
|
previewError: 'Preview failed',
|
|
@@ -114,6 +124,10 @@ export const fieldRemapChromeLabelKeys = {
|
|
|
114
124
|
hideHiddenFields: 'fieldRemap.hideHiddenFields',
|
|
115
125
|
emptyDetailTitle: 'fieldRemap.emptyDetailTitle',
|
|
116
126
|
emptyDetailDescription: 'fieldRemap.emptyDetailDescription',
|
|
127
|
+
readOnlyEmptyDetailTitle: 'fieldRemap.readOnlyEmptyDetailTitle',
|
|
128
|
+
readOnlyEmptyDetailDescription: 'fieldRemap.readOnlyEmptyDetailDescription',
|
|
129
|
+
detailModalTitle: 'fieldRemap.detailModalTitle',
|
|
130
|
+
closeDetailModal: 'fieldRemap.closeDetailModal',
|
|
117
131
|
previewTitle: 'fieldRemap.previewTitle',
|
|
118
132
|
previewLoading: 'fieldRemap.previewLoading',
|
|
119
133
|
previewError: 'fieldRemap.previewError',
|
|
@@ -162,6 +176,10 @@ export function resolveFieldRemapChromeLabels(
|
|
|
162
176
|
hideHiddenFields: resolve('hideHiddenFields'),
|
|
163
177
|
emptyDetailTitle: resolve('emptyDetailTitle'),
|
|
164
178
|
emptyDetailDescription: resolve('emptyDetailDescription'),
|
|
179
|
+
readOnlyEmptyDetailTitle: resolve('readOnlyEmptyDetailTitle'),
|
|
180
|
+
readOnlyEmptyDetailDescription: resolve('readOnlyEmptyDetailDescription'),
|
|
181
|
+
detailModalTitle: resolve('detailModalTitle'),
|
|
182
|
+
closeDetailModal: resolve('closeDetailModal'),
|
|
165
183
|
previewTitle: resolve('previewTitle'),
|
|
166
184
|
previewLoading: resolve('previewLoading'),
|
|
167
185
|
previewError: resolve('previewError'),
|
|
@@ -28,6 +28,7 @@ export interface ConvertNoteEditorProps {
|
|
|
28
28
|
readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
|
|
29
29
|
readonly edges: readonly MappingEdge[];
|
|
30
30
|
readonly onSelectionChange: (next: FieldRemapSelection) => void;
|
|
31
|
+
readonly readOnly?: boolean | undefined;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -43,6 +44,7 @@ export function ConvertNoteEditor({
|
|
|
43
44
|
edges,
|
|
44
45
|
onEdgesChange,
|
|
45
46
|
onSelectionChange,
|
|
47
|
+
readOnly = false,
|
|
46
48
|
}: ConvertNoteEditorProps): JSX.Element | null {
|
|
47
49
|
const chain = edge.transformIds ?? [];
|
|
48
50
|
const transformId = chain[stepIndex];
|
|
@@ -77,7 +79,7 @@ export function ConvertNoteEditor({
|
|
|
77
79
|
const stepLabel = definition?.label ?? transformId;
|
|
78
80
|
|
|
79
81
|
const applyEdge = (next: MappingEdge | null) => {
|
|
80
|
-
if (!next) {
|
|
82
|
+
if (readOnly || !next) {
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
83
85
|
onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
|
|
@@ -135,6 +137,7 @@ export function ConvertNoteEditor({
|
|
|
135
137
|
aria-label="Convert transform id"
|
|
136
138
|
data-testid="field-remap-step-id"
|
|
137
139
|
value={transformId}
|
|
140
|
+
disabled={readOnly}
|
|
138
141
|
onChange={(event) => {
|
|
139
142
|
applyEdge(
|
|
140
143
|
setTransformStepIdOnEdge(edge, stepIndex, event.target.value, {
|
|
@@ -165,35 +168,38 @@ export function ConvertNoteEditor({
|
|
|
165
168
|
<TransformOptionsEditor
|
|
166
169
|
fields={optionFields}
|
|
167
170
|
value={optionValue}
|
|
171
|
+
disabled={readOnly}
|
|
168
172
|
onChange={(nextOptions) => {
|
|
169
173
|
applyEdge(replaceTransformStepOptionsOnEdge(edge, stepIndex, nextOptions));
|
|
170
174
|
}}
|
|
171
175
|
/>
|
|
172
176
|
</section>
|
|
173
177
|
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
178
|
+
{!readOnly ? (
|
|
179
|
+
<div className="workbench-field-remap-convert-note__footer">
|
|
180
|
+
<Button
|
|
181
|
+
compact
|
|
182
|
+
type="button"
|
|
183
|
+
data-testid="field-remap-convert-note-remove"
|
|
184
|
+
onClick={() => {
|
|
185
|
+
const next = removeTransformStepFromEdge(edge, stepIndex);
|
|
186
|
+
applyEdge(next);
|
|
187
|
+
const nextLen = next.transformIds?.length ?? 0;
|
|
188
|
+
if (nextLen === 0) {
|
|
189
|
+
onSelectionChange({ kind: 'edge', edgeId: edge.id });
|
|
190
|
+
} else {
|
|
191
|
+
onSelectionChange({
|
|
192
|
+
kind: 'transformStep',
|
|
193
|
+
edgeId: edge.id,
|
|
194
|
+
stepIndex: Math.min(stepIndex, nextLen - 1),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
Remove convert
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
) : null}
|
|
197
203
|
</aside>
|
|
198
204
|
);
|
|
199
205
|
}
|
|
@@ -4,6 +4,7 @@ import { ClearableTextInput, IconButton } from '@workbench-kit/react/primitives'
|
|
|
4
4
|
import type { ValueTransformRegistry } from '@workbench-kit/field-remap';
|
|
5
5
|
|
|
6
6
|
import { defaultFieldRemapChromeLabels, type FieldRemapChromeLabels } from './chrome-labels.js';
|
|
7
|
+
import { writeFieldRemapTransformDragData } from './drag-payload.js';
|
|
7
8
|
|
|
8
9
|
export interface FieldRemapConvertPaletteProps {
|
|
9
10
|
readonly transforms: ValueTransformRegistry;
|
|
@@ -207,6 +208,7 @@ export function FieldRemapConvertPalette({
|
|
|
207
208
|
key={definition.id}
|
|
208
209
|
aria-selected={selected}
|
|
209
210
|
data-testid={`field-remap-palette-item-${definition.id}`}
|
|
211
|
+
draggable
|
|
210
212
|
role="option"
|
|
211
213
|
selected={selected}
|
|
212
214
|
tabIndex={definition.id === rovingTransformId ? 0 : -1}
|
|
@@ -217,6 +219,10 @@ export function FieldRemapConvertPalette({
|
|
|
217
219
|
onSelectedTransformIdChange(definition.id);
|
|
218
220
|
}}
|
|
219
221
|
onDoubleClick={() => onPlaceDraft(definition.id)}
|
|
222
|
+
onDragStart={(event) => {
|
|
223
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
224
|
+
writeFieldRemapTransformDragData(event.dataTransfer, definition.id);
|
|
225
|
+
}}
|
|
220
226
|
onFocus={() => setRequestedRovingTransformId(definition.id)}
|
|
221
227
|
onKeyDown={(event) => handleOptionKeyDown(event, definition.id)}
|
|
222
228
|
>
|
package/src/field-remap/demo.tsx
CHANGED
|
@@ -25,6 +25,8 @@ export interface SampleFieldRemapDemoProps {
|
|
|
25
25
|
readonly showBindingsList?: FieldRemapFlowMapperProps['showBindingsList'];
|
|
26
26
|
readonly showConvertPalette?: FieldRemapFlowMapperProps['showConvertPalette'];
|
|
27
27
|
readonly emptyDetail?: FieldRemapFlowMapperProps['emptyDetail'];
|
|
28
|
+
readonly detailPresentation?: FieldRemapFlowMapperProps['detailPresentation'];
|
|
29
|
+
readonly readOnly?: FieldRemapFlowMapperProps['readOnly'];
|
|
28
30
|
/** When true, show host-chrome demo controls (history + fit view). */
|
|
29
31
|
readonly showHostChromeDemo?: boolean | undefined;
|
|
30
32
|
/** Prefer `browse` for I/O class/field inspection demos. */
|
|
@@ -46,6 +48,8 @@ export function SampleFieldRemapDemo({
|
|
|
46
48
|
showBindingsList,
|
|
47
49
|
showConvertPalette,
|
|
48
50
|
emptyDetail,
|
|
51
|
+
detailPresentation,
|
|
52
|
+
readOnly,
|
|
49
53
|
showHostChromeDemo = false,
|
|
50
54
|
ioChrome,
|
|
51
55
|
browseSeedShapes = false,
|
|
@@ -89,7 +93,7 @@ export function SampleFieldRemapDemo({
|
|
|
89
93
|
compact
|
|
90
94
|
type="button"
|
|
91
95
|
data-testid="field-remap-undo"
|
|
92
|
-
disabled={!historyAvailability.canUndo}
|
|
96
|
+
disabled={readOnly || !historyAvailability.canUndo}
|
|
93
97
|
onClick={() => historyActionsRef.current?.undo()}
|
|
94
98
|
>
|
|
95
99
|
Undo
|
|
@@ -98,7 +102,7 @@ export function SampleFieldRemapDemo({
|
|
|
98
102
|
compact
|
|
99
103
|
type="button"
|
|
100
104
|
data-testid="field-remap-redo"
|
|
101
|
-
disabled={!historyAvailability.canRedo}
|
|
105
|
+
disabled={readOnly || !historyAvailability.canRedo}
|
|
102
106
|
onClick={() => historyActionsRef.current?.redo()}
|
|
103
107
|
>
|
|
104
108
|
Redo
|
|
@@ -119,6 +123,8 @@ export function SampleFieldRemapDemo({
|
|
|
119
123
|
showBindingsList={showBindingsList}
|
|
120
124
|
showConvertPalette={showConvertPalette}
|
|
121
125
|
emptyDetail={emptyDetail}
|
|
126
|
+
detailPresentation={detailPresentation}
|
|
127
|
+
readOnly={readOnly}
|
|
122
128
|
onShowMinimapChange={setShowMinimap}
|
|
123
129
|
ioChrome={ioChrome}
|
|
124
130
|
editableShapes={ioChrome === 'browse' ? false : undefined}
|