@jxsuite/studio 0.37.1 → 1.1.0
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/dist/iframe-entry.js +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +103709 -81001
- package/dist/studio.js.map +201 -157
- package/package.json +47 -45
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +132 -6
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/context-menu.ts +1 -1
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +56 -1
- package/src/format/format-host.ts +63 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/new-project/add-repo-modal.ts +183 -0
- package/src/new-project/new-project-modal.ts +22 -3
- package/src/page-params.ts +34 -8
- package/src/panels/ai-chat/chat-markdown.ts +2 -2
- package/src/panels/ai-panel.ts +61 -4
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +241 -0
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +217 -460
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/statusbar.ts +1 -1
- package/src/panels/style-panel.ts +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/panels/welcome-screen.ts +50 -0
- package/src/platform-errors.ts +30 -0
- package/src/platforms/cloud.ts +172 -89
- package/src/platforms/devserver.ts +172 -0
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/monaco-setup.ts +75 -26
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/settings/contributed-section.ts +406 -0
- package/src/settings/extension-sections.ts +145 -0
- package/src/settings/schema-field-ui.ts +4 -2
- package/src/settings/settings-modal.ts +101 -42
- package/src/site-context.ts +12 -1
- package/src/store.ts +4 -0
- package/src/studio.ts +107 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +61 -14
- package/src/types.ts +132 -1
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/form-controls.ts +322 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/preview-format.ts +26 -0
- package/src/utils/studio-utils.ts +4 -3
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
- package/src/settings/content-types-editor.ts +0 -599
package/src/editor/shortcuts.ts
CHANGED
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
undo as tabUndo,
|
|
17
17
|
transactDoc,
|
|
18
18
|
} from "../tabs/transact";
|
|
19
|
+
import { applyEditZoom, requestEditZoom, setEditZoom } from "../canvas/canvas-utils";
|
|
19
20
|
import { isEditing, stopEditing } from "./inline-edit";
|
|
20
21
|
import { copyNode, cutNode, pasteNode } from "./context-menu";
|
|
21
22
|
import { openQuickSearch } from "../panels/quick-search";
|
|
23
|
+
import { shouldWarnOnClose } from "../panels/tab-strip";
|
|
22
24
|
import { showConfirmDialog } from "../ui/layers";
|
|
23
25
|
import { rectOf } from "../utils/geometry";
|
|
24
26
|
|
|
@@ -33,7 +35,6 @@ import type { JxPath } from "../state";
|
|
|
33
35
|
* panY: number;
|
|
34
36
|
* setPan: (x: number, y: number) => void;
|
|
35
37
|
* applyTransform: () => void;
|
|
36
|
-
* positionZoomIndicator: () => void;
|
|
37
38
|
* saveFile: () => void;
|
|
38
39
|
* openProject: () => void;
|
|
39
40
|
* }} getContext
|
|
@@ -45,7 +46,6 @@ export function initShortcuts(
|
|
|
45
46
|
panY: number;
|
|
46
47
|
setPan: (x: number, y: number) => void;
|
|
47
48
|
applyTransform: () => void;
|
|
48
|
-
positionZoomIndicator: () => void;
|
|
49
49
|
saveFile: () => void;
|
|
50
50
|
openProject: () => void;
|
|
51
51
|
},
|
|
@@ -55,10 +55,18 @@ export function initShortcuts(
|
|
|
55
55
|
"wheel",
|
|
56
56
|
(e: WheelEvent) => {
|
|
57
57
|
const { canvasMode, panX, panY, setPan, applyTransform } = getContext();
|
|
58
|
-
// Edit (content) mode:
|
|
59
|
-
//
|
|
60
|
-
//
|
|
58
|
+
// Edit (content) mode: ctrl/cmd+wheel drives the content zoom (browser-page-zoom semantics —
|
|
59
|
+
// The footprint stays fixed, content reflows); plain wheel scrolls the edit-mode container
|
|
60
|
+
// Ourselves. The canvas iframe is sized to its content (no internal scroll) and a cross-origin
|
|
61
|
+
// OOPIF doesn't bubble wheel to the parent, so the wheel reaches us forwarded (or over the
|
|
62
|
+
// Canvas chrome) but never triggers native scroll.
|
|
61
63
|
if (canvasMode === "edit") {
|
|
64
|
+
if (e.ctrlKey || e.metaKey) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
const editZoom = activeTab.value?.session.ui.editZoom ?? 1;
|
|
67
|
+
requestEditZoom(editZoom * (1 + -e.deltaY * 0.005));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
62
70
|
const sc = canvasWrap.querySelector<HTMLElement>(".content-edit-canvas");
|
|
63
71
|
if (sc) {
|
|
64
72
|
e.preventDefault();
|
|
@@ -125,8 +133,13 @@ export function initShortcuts(
|
|
|
125
133
|
canvasWrap.addEventListener("pointerup", onUp);
|
|
126
134
|
});
|
|
127
135
|
|
|
128
|
-
//
|
|
129
|
-
|
|
136
|
+
// Re-fit the edit-mode content zoom on resize: its layout width derives from the LIVE column
|
|
137
|
+
// Width, which tracks the studio window.
|
|
138
|
+
window.addEventListener("resize", () => {
|
|
139
|
+
if (getContext().canvasMode === "edit") {
|
|
140
|
+
applyEditZoom();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
130
143
|
|
|
131
144
|
document.addEventListener("keydown", (e) => {
|
|
132
145
|
const { canvasMode, setPan, applyTransform, saveFile, openProject } = getContext();
|
|
@@ -161,12 +174,17 @@ export function initShortcuts(
|
|
|
161
174
|
return;
|
|
162
175
|
}
|
|
163
176
|
if (mod) {
|
|
177
|
+
// Grid mode: copy/paste/duplicate/zoom belong to the grid engine (Tabulator clipboard
|
|
178
|
+
// Needs the native events); only tab/app-level chords pass through.
|
|
179
|
+
if (canvasMode === "grid" && !["o", "p", "s", "w", "z", "Z"].includes(e.key)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
164
182
|
switch (e.key) {
|
|
165
183
|
case "w": {
|
|
166
184
|
e.preventDefault();
|
|
167
185
|
if (workspace.activeTabId && workspace.tabOrder.length > 1) {
|
|
168
186
|
const tabToClose = workspace.tabs.get(workspace.activeTabId);
|
|
169
|
-
if (tabToClose
|
|
187
|
+
if (tabToClose && shouldWarnOnClose(tabToClose)) {
|
|
170
188
|
const name = tabToClose.documentPath?.split("/").pop() || "Untitled";
|
|
171
189
|
void showConfirmDialog(
|
|
172
190
|
"Unsaved Changes",
|
|
@@ -198,7 +216,9 @@ export function initShortcuts(
|
|
|
198
216
|
saveFile();
|
|
199
217
|
break;
|
|
200
218
|
}
|
|
201
|
-
|
|
219
|
+
// With Shift held e.key is "Z", so redo needs the uppercase case too.
|
|
220
|
+
case "z":
|
|
221
|
+
case "Z": {
|
|
202
222
|
e.preventDefault();
|
|
203
223
|
if (e.shiftKey) {
|
|
204
224
|
tabRedo(activeTab.value!);
|
|
@@ -231,10 +251,11 @@ export function initShortcuts(
|
|
|
231
251
|
break;
|
|
232
252
|
}
|
|
233
253
|
case "0": {
|
|
254
|
+
e.preventDefault();
|
|
234
255
|
if (canvasMode === "edit") {
|
|
256
|
+
setEditZoom(1);
|
|
235
257
|
break;
|
|
236
258
|
}
|
|
237
|
-
e.preventDefault();
|
|
238
259
|
activeTab.value!.session.ui.zoom = 1;
|
|
239
260
|
setPan(16, 16);
|
|
240
261
|
applyTransform();
|
|
@@ -242,19 +263,21 @@ export function initShortcuts(
|
|
|
242
263
|
}
|
|
243
264
|
case "=":
|
|
244
265
|
case "+": {
|
|
266
|
+
e.preventDefault();
|
|
245
267
|
if (canvasMode === "edit") {
|
|
268
|
+
setEditZoom((tab?.session.ui.editZoom ?? 1) * 1.2);
|
|
246
269
|
break;
|
|
247
270
|
}
|
|
248
|
-
e.preventDefault();
|
|
249
271
|
activeTab.value!.session.ui.zoom = Math.min(5, (tab?.session.ui.zoom ?? 1) * 1.2);
|
|
250
272
|
applyTransform();
|
|
251
273
|
break;
|
|
252
274
|
}
|
|
253
275
|
case "-": {
|
|
276
|
+
e.preventDefault();
|
|
254
277
|
if (canvasMode === "edit") {
|
|
278
|
+
setEditZoom((tab?.session.ui.editZoom ?? 1) / 1.2);
|
|
255
279
|
break;
|
|
256
280
|
}
|
|
257
|
-
e.preventDefault();
|
|
258
281
|
activeTab.value!.session.ui.zoom = Math.max(0.05, (tab?.session.ui.zoom ?? 1) / 1.2);
|
|
259
282
|
applyTransform();
|
|
260
283
|
break;
|
|
@@ -266,6 +289,12 @@ export function initShortcuts(
|
|
|
266
289
|
return;
|
|
267
290
|
}
|
|
268
291
|
|
|
292
|
+
// Grid mode: Delete/Escape/Enter/arrows drive the grid's own range clearing and cell
|
|
293
|
+
// Navigation — never the canvas document.
|
|
294
|
+
if (canvasMode === "grid") {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
269
298
|
switch (e.key) {
|
|
270
299
|
case "Delete":
|
|
271
300
|
case "Backspace": {
|
package/src/files/file-ops.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { errorMessage } from "@jxsuite/schema/parse";
|
|
|
12
12
|
import { statusMessage } from "../panels/statusbar";
|
|
13
13
|
import { validateComponentSlots } from "../services/cem-export";
|
|
14
14
|
import { getPlatform } from "../platform";
|
|
15
|
+
import { getGridController } from "../grid/grid-controller";
|
|
15
16
|
import { activeTab, openTab } from "../workspace/workspace";
|
|
16
17
|
import { collabSave } from "../collab/collab-session";
|
|
17
18
|
import { isEditing, stopEditing } from "../editor/inline-edit";
|
|
@@ -162,6 +163,12 @@ export async function saveFile() {
|
|
|
162
163
|
if (!tab) {
|
|
163
164
|
return;
|
|
164
165
|
}
|
|
166
|
+
// Grid tabs batch-save through their controller (per-source commit semantics, not a doc write).
|
|
167
|
+
const grid = getGridController(tab);
|
|
168
|
+
if (grid) {
|
|
169
|
+
await grid.save();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
165
172
|
try {
|
|
166
173
|
// A co-edited tab persists through its provider (a direct file write would reset the room).
|
|
167
174
|
if (await collabSave(tab)) {
|
|
@@ -264,6 +271,13 @@ export async function exportFile() {
|
|
|
264
271
|
* @returns {Promise<string>}
|
|
265
272
|
*/
|
|
266
273
|
export async function serializeDocument(tab: Tab): Promise<string> {
|
|
274
|
+
// Grid tabs serialize through their source (pending edits included) — e.g. the Monaco source
|
|
275
|
+
// View of a CSV grid tab shows the live file text.
|
|
276
|
+
const grid = getGridController(tab);
|
|
277
|
+
const gridText = grid?.serializeForSource();
|
|
278
|
+
if (gridText) {
|
|
279
|
+
return gridText;
|
|
280
|
+
}
|
|
267
281
|
await loadFormats();
|
|
268
282
|
const sourceFormat = formatByName(tab.doc.sourceFormat);
|
|
269
283
|
if (sourceFormat?.capabilities.serialize) {
|
package/src/files/files.ts
CHANGED
|
@@ -27,13 +27,23 @@ import {
|
|
|
27
27
|
monitorForElements,
|
|
28
28
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
29
29
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
activateTab,
|
|
32
|
+
openTab,
|
|
33
|
+
renameTab,
|
|
34
|
+
replaceAllTabs,
|
|
35
|
+
setWorkspaceProject,
|
|
36
|
+
workspace,
|
|
37
|
+
} from "../workspace/workspace";
|
|
38
|
+
import { openCollectionGrid, openCsvGridTab, openPagesGrid } from "../grid/grid-open";
|
|
39
|
+
import { collectionDirs } from "../grid/sources/content-source";
|
|
31
40
|
import { parseSourceForPath, serializeDocument } from "./file-ops";
|
|
32
41
|
import {
|
|
33
42
|
documentExtensions,
|
|
34
43
|
formatForPath,
|
|
35
44
|
loadFormats,
|
|
36
45
|
noFormatError,
|
|
46
|
+
refreshExtensionUi,
|
|
37
47
|
refreshFormats,
|
|
38
48
|
} from "../format/format-host";
|
|
39
49
|
import { view } from "../view";
|
|
@@ -83,6 +93,7 @@ export async function loadProject() {
|
|
|
83
93
|
|
|
84
94
|
refreshFormats();
|
|
85
95
|
void loadFormats();
|
|
96
|
+
refreshExtensionUi(platform);
|
|
86
97
|
|
|
87
98
|
setProjectState({
|
|
88
99
|
dirs: new Map(),
|
|
@@ -96,6 +107,11 @@ export async function loadProject() {
|
|
|
96
107
|
searchQuery: "",
|
|
97
108
|
selectedPath: null,
|
|
98
109
|
});
|
|
110
|
+
// Only a site project counts as an open project for the workspace (a bare monorepo root keeps
|
|
111
|
+
// The assistant in bootstrap mode).
|
|
112
|
+
if (info.isSiteProject) {
|
|
113
|
+
setWorkspaceProject(meta.root || ".", info.projectConfig || null);
|
|
114
|
+
}
|
|
99
115
|
|
|
100
116
|
if (info.isSiteProject) {
|
|
101
117
|
addRecentProject(requireProjectState().name, meta.root);
|
|
@@ -145,6 +161,7 @@ export async function openProject({
|
|
|
145
161
|
|
|
146
162
|
refreshFormats();
|
|
147
163
|
void loadFormats();
|
|
164
|
+
refreshExtensionUi(platform);
|
|
148
165
|
|
|
149
166
|
setProjectState({
|
|
150
167
|
.../** @type {ProjectState} */ projectState,
|
|
@@ -157,6 +174,7 @@ export async function openProject({
|
|
|
157
174
|
searchQuery: "",
|
|
158
175
|
selectedPath: null,
|
|
159
176
|
});
|
|
177
|
+
setWorkspaceProject(handle.root, config);
|
|
160
178
|
|
|
161
179
|
await autoSyncProjectOnOpen();
|
|
162
180
|
await ensureDependenciesInstalled();
|
|
@@ -716,6 +734,26 @@ function showFileContextMenu(
|
|
|
716
734
|
action: () => createNewFile(entry.path, ctx.renderLeftPanel),
|
|
717
735
|
label: "New File\u2026",
|
|
718
736
|
});
|
|
737
|
+
// Directories backing a content collection get a bulk-edit affordance.
|
|
738
|
+
const collection = collectionDirs().find(
|
|
739
|
+
({ dir }) => entry.path === dir || entry.path.endsWith(`/${dir}`),
|
|
740
|
+
);
|
|
741
|
+
if (collection) {
|
|
742
|
+
items.push({
|
|
743
|
+
action: () => {
|
|
744
|
+
openCollectionGrid(collection.name);
|
|
745
|
+
},
|
|
746
|
+
label: "Edit Collection in Grid",
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if (entry.path === "pages" || entry.path.endsWith("/pages")) {
|
|
750
|
+
items.push({
|
|
751
|
+
action: () => {
|
|
752
|
+
openPagesGrid();
|
|
753
|
+
},
|
|
754
|
+
label: "Edit Pages in Grid",
|
|
755
|
+
});
|
|
756
|
+
}
|
|
719
757
|
}
|
|
720
758
|
items.push(
|
|
721
759
|
{ label: "\u2014" },
|
|
@@ -1029,6 +1067,23 @@ export async function openFileInTab(path: string) {
|
|
|
1029
1067
|
}
|
|
1030
1068
|
}
|
|
1031
1069
|
|
|
1070
|
+
// CSV files open in the grid editor (source mode remains as the raw-text alternate).
|
|
1071
|
+
if (path.toLowerCase().endsWith(".csv")) {
|
|
1072
|
+
try {
|
|
1073
|
+
await openCsvGridTab(path);
|
|
1074
|
+
requireProjectState().selectedPath = path;
|
|
1075
|
+
trackRecentFile({
|
|
1076
|
+
name: path.split("/").pop() || path,
|
|
1077
|
+
path,
|
|
1078
|
+
root: requireProjectState().projectRoot,
|
|
1079
|
+
});
|
|
1080
|
+
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
statusMessage(`Error: ${errorMessage(error)}`);
|
|
1083
|
+
}
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1032
1087
|
const platform = getPlatform();
|
|
1033
1088
|
try {
|
|
1034
1089
|
const content = await platform.readFile(path);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getPlatform } from "../platform";
|
|
11
|
+
import type { ExtensionsInfo } from "../types";
|
|
11
12
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
12
13
|
|
|
13
14
|
export interface StudioFormatHints {
|
|
@@ -68,10 +69,12 @@ export function loadFormats(): Promise<StudioFormat[]> {
|
|
|
68
69
|
return _loaded;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/** Invalidate the cached registry (call on project switch). */
|
|
72
|
+
/** Invalidate the cached registry and extensions payload (call on project switch). */
|
|
72
73
|
export function refreshFormats() {
|
|
73
74
|
_loaded = null;
|
|
74
75
|
_formats = [];
|
|
76
|
+
_extensionsLoaded = null;
|
|
77
|
+
_extensions = [];
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/** Seed the registry directly (tests and hosts that preload format metadata). */
|
|
@@ -80,6 +83,65 @@ export function setFormats(formats: StudioFormat[]) {
|
|
|
80
83
|
_loaded = Promise.resolve(formats);
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// ─── Extensions payload (specs/extensions.md §9/§9.1) ────────────────────────
|
|
87
|
+
|
|
88
|
+
let _extensions: ExtensionsInfo[] = [];
|
|
89
|
+
let _extensionsLoaded: Promise<ExtensionsInfo[]> | null = null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load (and cache) the project's enabled extensions with their project-section contributions.
|
|
93
|
+
* Backed by the platform's optional `listExtensions` member; platforms without it (or failures)
|
|
94
|
+
* degrade to an empty list, hiding descriptor-contributed settings sections.
|
|
95
|
+
*/
|
|
96
|
+
export function loadExtensions(): Promise<ExtensionsInfo[]> {
|
|
97
|
+
if (!_extensionsLoaded) {
|
|
98
|
+
_extensionsLoaded = (async () => {
|
|
99
|
+
try {
|
|
100
|
+
const platform = getPlatform() as {
|
|
101
|
+
listExtensions?: () => Promise<ExtensionsInfo[]>;
|
|
102
|
+
};
|
|
103
|
+
_extensions = (await platform.listExtensions?.()) ?? [];
|
|
104
|
+
} catch {
|
|
105
|
+
_extensions = [];
|
|
106
|
+
}
|
|
107
|
+
return _extensions;
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
return _extensionsLoaded;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The last-loaded extensions payload (synchronous access for render paths). */
|
|
114
|
+
export function getExtensions(): ExtensionsInfo[] {
|
|
115
|
+
return _extensions;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Seed the extensions payload directly (tests and hosts that preload it). */
|
|
119
|
+
export function setExtensions(extensions: ExtensionsInfo[]) {
|
|
120
|
+
_extensions = extensions;
|
|
121
|
+
_extensionsLoaded = Promise.resolve(extensions);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fire-and-forget refresh of the extension-facing editor surface after project (re)activation or an
|
|
126
|
+
* `extensions` change: Monaco's per-project schemas and the descriptor-contributed settings
|
|
127
|
+
* sections. Both modules load lazily — monaco is heavy and the settings registry pulls DOM
|
|
128
|
+
* templates — and both degrade silently (bundled core schemas, built-in sections only).
|
|
129
|
+
*
|
|
130
|
+
* @param {object} platform - The studio platform (only `fetchProjectSchemas` is consulted here)
|
|
131
|
+
*/
|
|
132
|
+
export function refreshExtensionUi(platform: {
|
|
133
|
+
fetchProjectSchemas?: () => Promise<{ project?: unknown; document?: unknown }>;
|
|
134
|
+
}): void {
|
|
135
|
+
void import("../services/monaco-setup")
|
|
136
|
+
.then(({ refreshProjectSchemas }) => refreshProjectSchemas(platform))
|
|
137
|
+
.catch(() => false);
|
|
138
|
+
void import("../settings/extension-sections")
|
|
139
|
+
.then(({ syncExtensionSettingsSections }) => syncExtensionSettingsSections())
|
|
140
|
+
.catch(() => {
|
|
141
|
+
// Contributed sections also refresh on the next settings-modal open.
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
83
145
|
/** The last-loaded registry (synchronous access for render paths). */
|
|
84
146
|
export function getFormats(): StudioFormat[] {
|
|
85
147
|
return _formats;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid cell editors and formatters — the lit → Tabulator bridge.
|
|
3
|
+
*
|
|
4
|
+
* Tabulator's editor contract is `(cell, onRendered, success, cancel) => element`: it parents the
|
|
5
|
+
* returned element into the cell and expects `success(value)`/`cancel()` calls. Each factory here
|
|
6
|
+
* lit-renders a control into a host element supplied by grid-view's single sanctioned element
|
|
7
|
+
* factory — this module itself never creates DOM imperatively. Formatters likewise return a
|
|
8
|
+
* rendered element (never an HTML string, which Tabulator would inject as innerHTML).
|
|
9
|
+
*
|
|
10
|
+
* Native inputs (the data-grid precedent) rather than Spectrum controls: cells are 24px
|
|
11
|
+
* micro-controls where SWC shadow focus handling fights Tabulator's editor lifecycle. Rich popover
|
|
12
|
+
* editors (media, relationship pickers) layer on in a later phase.
|
|
13
|
+
*/
|
|
14
|
+
import { html, render } from "lit-html";
|
|
15
|
+
import { cellToText, coerceCellInput } from "./schema-columns";
|
|
16
|
+
import type { GridCellValue, GridColumn } from "./grid-source";
|
|
17
|
+
|
|
18
|
+
/** The subset of Tabulator's CellComponent the editors/formatters touch. */
|
|
19
|
+
export interface CellLike {
|
|
20
|
+
getValue: () => unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CellEditorFn = (
|
|
24
|
+
cell: CellLike,
|
|
25
|
+
onRendered: (fn: () => void) => void,
|
|
26
|
+
success: (value: unknown) => void,
|
|
27
|
+
cancel: () => void,
|
|
28
|
+
) => HTMLElement;
|
|
29
|
+
|
|
30
|
+
export type CellFormatterFn = (cell: CellLike) => HTMLElement;
|
|
31
|
+
|
|
32
|
+
/** Detached-host factory — grid-view owns the one sanctioned document.createElement. */
|
|
33
|
+
export type HostFactory = (className: string) => HTMLElement;
|
|
34
|
+
|
|
35
|
+
/** Commit/cancel wiring shared by every text-ish editor. */
|
|
36
|
+
function inputEditor(makeHost: HostFactory, column: GridColumn, inputType: string): CellEditorFn {
|
|
37
|
+
return (cell, onRendered, success, cancel) => {
|
|
38
|
+
const host = makeHost("jx-grid-editor");
|
|
39
|
+
let done = false;
|
|
40
|
+
const commit = (raw: string) => {
|
|
41
|
+
if (!done) {
|
|
42
|
+
done = true;
|
|
43
|
+
success(coerceCellInput(raw, column));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const abort = () => {
|
|
47
|
+
if (!done) {
|
|
48
|
+
done = true;
|
|
49
|
+
cancel();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const initial =
|
|
53
|
+
column.kind === "date"
|
|
54
|
+
? cellToText(cell.getValue() as GridCellValue).slice(0, 10)
|
|
55
|
+
: cellToText(cell.getValue() as GridCellValue);
|
|
56
|
+
render(
|
|
57
|
+
html`<input
|
|
58
|
+
class="jx-grid-input"
|
|
59
|
+
type=${inputType}
|
|
60
|
+
.value=${initial}
|
|
61
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
62
|
+
if (e.key === "Enter") {
|
|
63
|
+
commit((e.target as HTMLInputElement).value);
|
|
64
|
+
} else if (e.key === "Escape") {
|
|
65
|
+
abort();
|
|
66
|
+
}
|
|
67
|
+
}}
|
|
68
|
+
@blur=${(e: Event) => commit((e.target as HTMLInputElement).value)}
|
|
69
|
+
/>`,
|
|
70
|
+
host,
|
|
71
|
+
);
|
|
72
|
+
onRendered(() => {
|
|
73
|
+
const input = host.querySelector("input");
|
|
74
|
+
input?.focus();
|
|
75
|
+
input?.select();
|
|
76
|
+
});
|
|
77
|
+
return host;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkboxEditor(makeHost: HostFactory): CellEditorFn {
|
|
82
|
+
return (cell, onRendered, success, cancel) => {
|
|
83
|
+
const host = makeHost("jx-grid-editor");
|
|
84
|
+
let done = false;
|
|
85
|
+
render(
|
|
86
|
+
html`<input
|
|
87
|
+
class="jx-grid-checkbox"
|
|
88
|
+
type="checkbox"
|
|
89
|
+
.checked=${cell.getValue() === true}
|
|
90
|
+
@change=${(e: Event) => {
|
|
91
|
+
if (!done) {
|
|
92
|
+
done = true;
|
|
93
|
+
success((e.target as HTMLInputElement).checked);
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
97
|
+
if (e.key === "Escape" && !done) {
|
|
98
|
+
done = true;
|
|
99
|
+
cancel();
|
|
100
|
+
} else if (e.key === "Enter" && !done) {
|
|
101
|
+
done = true;
|
|
102
|
+
success((e.target as HTMLInputElement).checked);
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
@blur=${(e: Event) => {
|
|
106
|
+
if (!done) {
|
|
107
|
+
done = true;
|
|
108
|
+
success((e.target as HTMLInputElement).checked);
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
111
|
+
/>`,
|
|
112
|
+
host,
|
|
113
|
+
);
|
|
114
|
+
onRendered(() => host.querySelector("input")?.focus());
|
|
115
|
+
return host;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function selectEditor(makeHost: HostFactory, column: GridColumn): CellEditorFn {
|
|
120
|
+
const options = Array.isArray(column.schema?.enum)
|
|
121
|
+
? (column.schema.enum as unknown[]).map(String)
|
|
122
|
+
: [];
|
|
123
|
+
return (cell, onRendered, success, cancel) => {
|
|
124
|
+
const host = makeHost("jx-grid-editor");
|
|
125
|
+
let done = false;
|
|
126
|
+
const current = cellToText(cell.getValue() as GridCellValue);
|
|
127
|
+
render(
|
|
128
|
+
html`<select
|
|
129
|
+
class="jx-grid-select"
|
|
130
|
+
@change=${(e: Event) => {
|
|
131
|
+
if (!done) {
|
|
132
|
+
done = true;
|
|
133
|
+
const { value } = e.target as HTMLSelectElement;
|
|
134
|
+
success(value === "" ? null : value);
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
138
|
+
if (e.key === "Escape" && !done) {
|
|
139
|
+
done = true;
|
|
140
|
+
cancel();
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
@blur=${() => {
|
|
144
|
+
if (!done) {
|
|
145
|
+
done = true;
|
|
146
|
+
cancel();
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<option value="" ?selected=${current === ""}>—</option>
|
|
151
|
+
${options.map(
|
|
152
|
+
(option) =>
|
|
153
|
+
html`<option value=${option} ?selected=${option === current}>${option}</option>`,
|
|
154
|
+
)}
|
|
155
|
+
</select>`,
|
|
156
|
+
host,
|
|
157
|
+
);
|
|
158
|
+
onRendered(() => host.querySelector("select")?.focus());
|
|
159
|
+
return host;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Pill/chip editor for string arrays: Enter/comma adds, Backspace pops, × removes. */
|
|
164
|
+
function pillEditor(makeHost: HostFactory, column: GridColumn): CellEditorFn {
|
|
165
|
+
return (cell, onRendered, success, cancel) => {
|
|
166
|
+
const host = makeHost("jx-grid-editor jx-grid-pill-editor");
|
|
167
|
+
const initial = coerceCellInput(cell.getValue(), column);
|
|
168
|
+
const chips: string[] = Array.isArray(initial) ? [...initial] : [];
|
|
169
|
+
let done = false;
|
|
170
|
+
const finish = (committed: string[] | null) => {
|
|
171
|
+
if (done) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
done = true;
|
|
175
|
+
if (committed) {
|
|
176
|
+
success(committed);
|
|
177
|
+
} else {
|
|
178
|
+
cancel();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const inputEl = () => host.querySelector("input");
|
|
182
|
+
const addFromInput = (input: HTMLInputElement): boolean => {
|
|
183
|
+
const text = input.value.trim();
|
|
184
|
+
if (text === "") {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
chips.push(text);
|
|
188
|
+
input.value = "";
|
|
189
|
+
return true;
|
|
190
|
+
};
|
|
191
|
+
const doRender = () => {
|
|
192
|
+
render(
|
|
193
|
+
html`<span class="jx-grid-pills">
|
|
194
|
+
${chips.map(
|
|
195
|
+
(chip, i) => html`<span class="jx-grid-chip"
|
|
196
|
+
>${chip}<button
|
|
197
|
+
class="jx-grid-chip-x"
|
|
198
|
+
title="Remove"
|
|
199
|
+
@mousedown=${(e: Event) => e.preventDefault()}
|
|
200
|
+
@click=${() => {
|
|
201
|
+
chips.splice(i, 1);
|
|
202
|
+
doRender();
|
|
203
|
+
inputEl()?.focus();
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
×
|
|
207
|
+
</button></span
|
|
208
|
+
>`,
|
|
209
|
+
)}
|
|
210
|
+
<input
|
|
211
|
+
class="jx-grid-pill-input"
|
|
212
|
+
placeholder=${chips.length === 0 ? "add…" : ""}
|
|
213
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
214
|
+
const input = e.target as HTMLInputElement;
|
|
215
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
if (addFromInput(input)) {
|
|
218
|
+
doRender();
|
|
219
|
+
inputEl()?.focus();
|
|
220
|
+
} else if (e.key === "Enter") {
|
|
221
|
+
finish([...chips]);
|
|
222
|
+
}
|
|
223
|
+
} else if (e.key === "Backspace" && input.value === "" && chips.length > 0) {
|
|
224
|
+
chips.pop();
|
|
225
|
+
doRender();
|
|
226
|
+
inputEl()?.focus();
|
|
227
|
+
} else if (e.key === "Escape") {
|
|
228
|
+
finish(null);
|
|
229
|
+
}
|
|
230
|
+
}}
|
|
231
|
+
@blur=${(e: Event) => {
|
|
232
|
+
addFromInput(e.target as HTMLInputElement);
|
|
233
|
+
finish([...chips]);
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
</span>`,
|
|
237
|
+
host,
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
doRender();
|
|
241
|
+
onRendered(() => inputEl()?.focus());
|
|
242
|
+
return host;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Editor factory for a column; undefined means the column is not editable in place. */
|
|
247
|
+
export function editorForColumn(
|
|
248
|
+
column: GridColumn,
|
|
249
|
+
makeHost: HostFactory,
|
|
250
|
+
): CellEditorFn | undefined {
|
|
251
|
+
if (column.kind === "readonly" || (!column.editable && !column.insertOnly)) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
switch (column.kind) {
|
|
255
|
+
case "boolean": {
|
|
256
|
+
return checkboxEditor(makeHost);
|
|
257
|
+
}
|
|
258
|
+
case "enum": {
|
|
259
|
+
return selectEditor(makeHost, column);
|
|
260
|
+
}
|
|
261
|
+
case "date": {
|
|
262
|
+
return inputEditor(makeHost, column, "date");
|
|
263
|
+
}
|
|
264
|
+
case "array": {
|
|
265
|
+
return pillEditor(makeHost, column);
|
|
266
|
+
}
|
|
267
|
+
default: {
|
|
268
|
+
// String/text/number/image/reference — text input + column-typed coercion.
|
|
269
|
+
return inputEditor(makeHost, column, "text");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Formatter factory — plain-text projection rendered as a real element (innerHTML-safe). */
|
|
275
|
+
export function formatterForColumn(column: GridColumn, makeHost: HostFactory): CellFormatterFn {
|
|
276
|
+
return (cell) => {
|
|
277
|
+
const value = cell.getValue() as GridCellValue;
|
|
278
|
+
const host = makeHost(`jx-grid-cell-content jx-grid-kind-${column.kind}`);
|
|
279
|
+
if (column.kind === "boolean") {
|
|
280
|
+
render(html`${value === true ? "✓" : ""}`, host);
|
|
281
|
+
} else if (column.kind === "array" && Array.isArray(value)) {
|
|
282
|
+
render(html`${value.map((item) => html`<span class="jx-grid-chip">${item}</span>`)}`, host);
|
|
283
|
+
} else {
|
|
284
|
+
render(html`${cellToText(value)}`, host);
|
|
285
|
+
}
|
|
286
|
+
return host;
|
|
287
|
+
};
|
|
288
|
+
}
|