@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chat-markdown.js — memoized markdown rendering for assistant chat messages.
|
|
3
3
|
*
|
|
4
|
-
* Wraps @jxsuite/
|
|
4
|
+
* Wraps @jxsuite/markup/md-html (sanitized markdown → HTML) with a per-message cache
|
|
5
5
|
* keyed by message id + content length, so re-renders during streaming only re-parse
|
|
6
6
|
* the message that actually grew. The HTML goes through unsafeHTML, which is safe here
|
|
7
7
|
* because md-html sanitizes (raw HTML dropped, javascript: URLs stripped).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { html } from "lit-html";
|
|
13
13
|
import type { TemplateResult } from "lit-html";
|
|
14
14
|
import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
|
|
15
|
-
import { markdownToHtml } from "@jxsuite/
|
|
15
|
+
import { markdownToHtml } from "@jxsuite/markup/md-html";
|
|
16
16
|
|
|
17
17
|
const cache = new Map<string, { len: number; html: string }>();
|
|
18
18
|
|
package/src/panels/ai-panel.ts
CHANGED
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* @license MIT
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { html, render as litRender } from "lit-html";
|
|
19
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
20
20
|
import type { TemplateResult } from "lit-html";
|
|
21
|
+
import { getPlatform } from "../platform";
|
|
21
22
|
import { effect, effectScope } from "../reactivity";
|
|
22
23
|
import { createDocumentAssistant } from "../services/document-assistant";
|
|
23
24
|
import { hasOpenAiKey } from "../services/ai-settings";
|
|
24
|
-
import { fetchAvailableModels, isProxyConfigured } from "../services/ai-models";
|
|
25
|
+
import { fetchAvailableModels, isManagedProxy, isProxyConfigured } from "../services/ai-models";
|
|
25
26
|
import { createAiCredentialsForm } from "../ui/ai-credentials-form";
|
|
26
27
|
import { clearMarkdownCache } from "./ai-chat/chat-markdown";
|
|
27
28
|
import { renderChatHeader, renderMessageList } from "./ai-chat/chat-view";
|
|
@@ -168,11 +169,67 @@ function startEditApiKey() {
|
|
|
168
169
|
credsForm.startEdit();
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
|
|
172
|
+
// ─── Managed Cloudflare connect (Workers AI) ─────────────────────────────────
|
|
173
|
+
|
|
174
|
+
let cfConnectBusy = false;
|
|
175
|
+
let cfConnectError = "";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Managed platforms broker Workers AI on the user's own Cloudflare account: offer connecting it as
|
|
179
|
+
* the keyless alternative whenever the proxy says managed-but-unconfigured and the platform can run
|
|
180
|
+
* the hosted OAuth flow (the PAL seam — desktop shells can implement cfConnect later).
|
|
181
|
+
*/
|
|
182
|
+
function canOfferManagedConnect(): boolean {
|
|
183
|
+
return isManagedProxy() && !isProxyConfigured() && Boolean(getPlatform().cfConnect);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function connectCloudflareForAi() {
|
|
187
|
+
if (cfConnectBusy) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
cfConnectBusy = true;
|
|
191
|
+
cfConnectError = "";
|
|
192
|
+
scheduleAiRender();
|
|
193
|
+
try {
|
|
194
|
+
const connection = await getPlatform().cfConnect?.();
|
|
195
|
+
if (connection) {
|
|
196
|
+
// Re-probe: /models flips to configured once the connection lands, opening the gate.
|
|
197
|
+
await fetchAvailableModels({ force: true });
|
|
198
|
+
} else {
|
|
199
|
+
cfConnectError = "Cloudflare connection was not completed.";
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
cfConnectError = error instanceof Error ? error.message : String(error);
|
|
203
|
+
}
|
|
204
|
+
cfConnectBusy = false;
|
|
205
|
+
scheduleAiRender();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderManagedConnect() {
|
|
209
|
+
return html`
|
|
210
|
+
<div class="ai-managed-connect">
|
|
211
|
+
<div>Use Workers AI on your own Cloudflare account — no API key needed.</div>
|
|
212
|
+
<sp-button size="s" ?disabled=${cfConnectBusy} @click=${() => void connectCloudflareForAi()}>
|
|
213
|
+
${cfConnectBusy ? "Connecting…" : "Connect Cloudflare"}
|
|
214
|
+
</sp-button>
|
|
215
|
+
${cfConnectError
|
|
216
|
+
? html`<div class="ai-managed-connect-error">${cfConnectError}</div>`
|
|
217
|
+
: nothing}
|
|
218
|
+
<div class="ai-managed-connect-divider">— or bring your own key —</div>
|
|
219
|
+
</div>
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The credentials gate: on managed platforms a keyless "Connect Cloudflare" (Workers AI) option
|
|
225
|
+
* sits above the OpenAI-compatible key form — both are real, working paths.
|
|
226
|
+
*/
|
|
172
227
|
function renderKeyGate() {
|
|
173
228
|
return html`
|
|
174
229
|
<div class="ai-tab-body">
|
|
175
|
-
<div class="ai-status-center"
|
|
230
|
+
<div class="ai-status-center">
|
|
231
|
+
${canOfferManagedConnect() ? renderManagedConnect() : nothing} ${credsForm.render()}
|
|
232
|
+
</div>
|
|
176
233
|
</div>
|
|
177
234
|
`;
|
|
178
235
|
}
|
|
@@ -606,10 +606,12 @@ export function renderBlockActionBar() {
|
|
|
606
606
|
const tag = (node.tagName ?? "div").toLowerCase();
|
|
607
607
|
|
|
608
608
|
// Inline format state, sourced from the iframe's selection snapshot.
|
|
609
|
-
const { editing, snapshot } = getEditSnapshot();
|
|
609
|
+
const { editing, editingProp, snapshot } = getEditSnapshot();
|
|
610
610
|
const inlineEditing = editing;
|
|
611
611
|
const actions = getInlineActions(tag) || [];
|
|
612
|
-
|
|
612
|
+
// A prop-bound plain session edits a single plain string — never show the format group (belt and
|
|
613
|
+
// Braces: component tags have no $inlineActions, so `actions` is empty there anyway).
|
|
614
|
+
const showFormat = inlineEditing && !editingProp && actions.length > 0;
|
|
613
615
|
const activeValues =
|
|
614
616
|
showFormat && snapshot
|
|
615
617
|
? actions.filter((a) => snapshot.activeTags.includes(a.tag)).map((a) => a.tag)
|
|
@@ -646,7 +648,10 @@ export function renderBlockActionBar() {
|
|
|
646
648
|
@click=${badgeInteractive
|
|
647
649
|
? (e: MouseEvent) => onTagBadgeClick(e, convertTargets, selection)
|
|
648
650
|
: nothing}
|
|
649
|
-
>${isRepeater ? nodeLabel(node) : node.$id || (node.tagName ?? "div")}
|
|
651
|
+
>${isRepeater ? nodeLabel(node) : node.$id || (node.tagName ?? "div")}${inlineEditing &&
|
|
652
|
+
editingProp
|
|
653
|
+
? ` · ${editingProp}`
|
|
654
|
+
: ""}</span
|
|
650
655
|
>
|
|
651
656
|
|
|
652
657
|
${selection.length >= 2
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Chat-panel.ts — the persistent AI chat sidebar (`#chat-panel` shell region).
|
|
4
|
+
*
|
|
5
|
+
* Hosts the assistant UI from ai-panel.ts unconditionally: with no project (welcome screen), with a
|
|
6
|
+
* project but no open document, and with a document open. The panel is mounted once at studio boot
|
|
7
|
+
* and never tears down on tab switches — the assistant's module state and DOM (composer draft,
|
|
8
|
+
* scroll position) persist.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately NOT built on createPanelScheduler: ai-panel owns a focus-guard-free rAF render loop
|
|
11
|
+
* (streaming must repaint while the composer is focused). This module only provides the host
|
|
12
|
+
* container, the initial paint, and the pending-agent-prompt handoff; ai-panel's watcher drives all
|
|
13
|
+
* chat-state repaints through the same lit part cache.
|
|
14
|
+
*
|
|
15
|
+
* @license MIT
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { render as litRender } from "lit-html";
|
|
19
|
+
import { effect, effectScope } from "../reactivity";
|
|
20
|
+
import { applyPanelCollapse, view } from "../view";
|
|
21
|
+
import { workspace } from "../workspace/workspace";
|
|
22
|
+
import { consumePendingAgentPrompt, hasPendingAgentPrompt } from "../services/agent-seed";
|
|
23
|
+
import {
|
|
24
|
+
bindAiPanelHost,
|
|
25
|
+
mountAiPanel,
|
|
26
|
+
renderAiPanelTemplate,
|
|
27
|
+
seedAssistantPrompt,
|
|
28
|
+
} from "./ai-panel";
|
|
29
|
+
|
|
30
|
+
import type { EffectScope } from "@vue/reactivity";
|
|
31
|
+
|
|
32
|
+
let _host: HTMLElement | null = null;
|
|
33
|
+
let _container: HTMLElement | null = null;
|
|
34
|
+
let _scope: EffectScope | null = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mount the chat sidebar into its shell region. Idempotent per host: the persistent `.panel-body`
|
|
38
|
+
* container is created once and bound as the ai-panel render host (lit needs a single render target
|
|
39
|
+
* for its part cache). A missing host (shell without a #chat-panel region, e.g. reduced test
|
|
40
|
+
* fixtures) is a no-op.
|
|
41
|
+
*
|
|
42
|
+
* @param {HTMLElement | null} host
|
|
43
|
+
*/
|
|
44
|
+
export function mount(host: HTMLElement | null) {
|
|
45
|
+
if (!host || (_host === host && _container)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
_host = host;
|
|
49
|
+
_container = document.createElement("div");
|
|
50
|
+
_container.className = "panel-body";
|
|
51
|
+
host.textContent = "";
|
|
52
|
+
host.append(_container);
|
|
53
|
+
|
|
54
|
+
mountAiPanel();
|
|
55
|
+
// The AI panel owns a focus-guard-free rAF render loop into this container so
|
|
56
|
+
// Streaming repaints while the composer is focused (see ai-panel.ts).
|
|
57
|
+
bindAiPanelHost(_container);
|
|
58
|
+
render();
|
|
59
|
+
|
|
60
|
+
_scope?.stop();
|
|
61
|
+
_scope = effectScope();
|
|
62
|
+
_scope.run(() => {
|
|
63
|
+
effect(() => {
|
|
64
|
+
// A pending agent prompt (stored by the New Project flow, possibly from another window) is
|
|
65
|
+
// Keyed by the absolute project root — consume it as soon as this window adopts that root.
|
|
66
|
+
const root = workspace.projectRoot;
|
|
67
|
+
if (!root || !hasPendingAgentPrompt(root)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (view.chatPanelCollapsed) {
|
|
71
|
+
view.chatPanelCollapsed = false;
|
|
72
|
+
applyPanelCollapse();
|
|
73
|
+
}
|
|
74
|
+
const prompt = consumePendingAgentPrompt(root);
|
|
75
|
+
if (prompt) {
|
|
76
|
+
// Defer past the current render so the assistant machinery is in place before the send.
|
|
77
|
+
requestAnimationFrame(() => void seedAssistantPrompt(prompt));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function unmount() {
|
|
84
|
+
_scope?.stop();
|
|
85
|
+
_scope = null;
|
|
86
|
+
if (_host) {
|
|
87
|
+
_host.textContent = "";
|
|
88
|
+
}
|
|
89
|
+
_host = null;
|
|
90
|
+
_container = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Repaint the assistant template into the persistent container (no-op before mount). */
|
|
94
|
+
export function render() {
|
|
95
|
+
if (_container) {
|
|
96
|
+
litRender(renderAiPanelTemplate(), _container);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Data section actions — the owner console entry points over the platform's data surface.
|
|
4
|
+
*
|
|
5
|
+
* Integration: the settings modal's contributed sections stay fully generic — extension-sections
|
|
6
|
+
* passes this module's {@link dataSectionActions} into the ContributedSectionOptions.actions slot
|
|
7
|
+
* for the data-domain sections ("connections"/"data") whenever the platform implements the
|
|
8
|
+
* protocol's data routes. Those actions surface Test Connection, Push Schema (dry-run plan
|
|
9
|
+
* confirmation before apply), and Open Data Grid — which opens the grid-tab source picker; table
|
|
10
|
+
* editing itself lives in the spreadsheet grid tabs (src/grid/), which replaced the old modal grid
|
|
11
|
+
* at feature parity (paging, cell edit, add/delete row) and added batch save with undo.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { html, nothing } from "lit-html";
|
|
15
|
+
import { openModal } from "../ui/layers";
|
|
16
|
+
import { dataSurfaceAvailable, pushSchema, testConnection } from "../services/data-service";
|
|
17
|
+
import { openGridSourcePicker } from "../grid/grid-open";
|
|
18
|
+
import type { TemplateResult } from "lit-html";
|
|
19
|
+
import type { DataConnectionTestResult, DataPushResult } from "../types";
|
|
20
|
+
import type { SectionActionsContext } from "../settings/contributed-section";
|
|
21
|
+
|
|
22
|
+
// ─── Grid opening (delegates to the grid-tab source picker) ──────────────────
|
|
23
|
+
|
|
24
|
+
/** Reset all module UI state and close any open surfaces (test hook / project switch). */
|
|
25
|
+
export function resetDataGridState(): void {
|
|
26
|
+
actionsState = { pushing: false, testResult: null, testing: null };
|
|
27
|
+
closePushDialog();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True when the platform serves the data grid. */
|
|
31
|
+
export function isDataGridAvailable(): boolean {
|
|
32
|
+
return dataSurfaceAvailable();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Push dialog (dry-run plan confirmation before apply) ─────────────────────
|
|
36
|
+
|
|
37
|
+
interface PushDialogState {
|
|
38
|
+
handle: ReturnType<typeof openModal>;
|
|
39
|
+
connection: string | undefined;
|
|
40
|
+
phase: "loading" | "confirm" | "applying" | "done";
|
|
41
|
+
plan: DataPushResult | null;
|
|
42
|
+
result: DataPushResult | null;
|
|
43
|
+
onDone: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let pushDialog: PushDialogState | null = null;
|
|
47
|
+
|
|
48
|
+
/** Close the push dialog (also part of resetDataGridState). */
|
|
49
|
+
function closePushDialog(): void {
|
|
50
|
+
pushDialog?.handle.close();
|
|
51
|
+
pushDialog = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Start a push: dry-run first, then a confirmation dialog gates the apply. */
|
|
55
|
+
export async function startPush(connection: string | undefined, onDone: () => void) {
|
|
56
|
+
if (pushDialog) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const handle = openModal(html``);
|
|
60
|
+
pushDialog = { connection, handle, onDone, phase: "loading", plan: null, result: null };
|
|
61
|
+
renderPushDialog();
|
|
62
|
+
const plan = await pushSchema({
|
|
63
|
+
dryRun: true,
|
|
64
|
+
...(connection === undefined ? {} : { connection }),
|
|
65
|
+
});
|
|
66
|
+
if (!pushDialog) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
pushDialog.plan = plan;
|
|
70
|
+
pushDialog.phase = "confirm";
|
|
71
|
+
renderPushDialog();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function applyPush(): Promise<void> {
|
|
75
|
+
if (!pushDialog) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
pushDialog.phase = "applying";
|
|
79
|
+
renderPushDialog();
|
|
80
|
+
const result = await pushSchema(
|
|
81
|
+
pushDialog.connection === undefined ? {} : { connection: pushDialog.connection },
|
|
82
|
+
);
|
|
83
|
+
if (!pushDialog) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
pushDialog.result = result;
|
|
87
|
+
pushDialog.phase = "done";
|
|
88
|
+
renderPushDialog();
|
|
89
|
+
pushDialog.onDone();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pushDialogBody(state: PushDialogState): TemplateResult {
|
|
93
|
+
if (state.phase === "loading" || state.phase === "applying") {
|
|
94
|
+
return html`<div class="push-dialog-status">
|
|
95
|
+
${state.phase === "loading" ? "Compiling plan…" : "Applying…"}
|
|
96
|
+
</div>`;
|
|
97
|
+
}
|
|
98
|
+
const shown = state.phase === "done" ? state.result : state.plan;
|
|
99
|
+
const steps = shown?.plan ?? [];
|
|
100
|
+
const warnings = shown?.warnings ?? [];
|
|
101
|
+
const errors = shown?.errors ?? [];
|
|
102
|
+
return html`
|
|
103
|
+
<div class="push-dialog-plan">
|
|
104
|
+
${state.phase === "done"
|
|
105
|
+
? html`<div class="push-dialog-status">
|
|
106
|
+
${shown?.applied ? "Schema applied." : "Push failed."}
|
|
107
|
+
</div>`
|
|
108
|
+
: steps.length === 0 && errors.length === 0
|
|
109
|
+
? html`<div class="push-dialog-status">Nothing to push — the schema is up to date.</div>`
|
|
110
|
+
: nothing}
|
|
111
|
+
<ul class="push-dialog-steps">
|
|
112
|
+
${steps.map(
|
|
113
|
+
(step) => html`<li class="push-step push-step-${step.kind}">${step.summary}</li>`,
|
|
114
|
+
)}
|
|
115
|
+
</ul>
|
|
116
|
+
${warnings.map((w) => html`<div class="push-dialog-warning">${w}</div>`)}
|
|
117
|
+
${errors.map((e) => html`<div class="push-dialog-error">${e}</div>`)}
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderPushDialog(): void {
|
|
123
|
+
if (!pushDialog) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const state = pushDialog;
|
|
127
|
+
const confirmable = state.phase === "confirm" && (state.plan?.plan.length ?? 0) > 0;
|
|
128
|
+
const tpl = html`
|
|
129
|
+
<sp-underlay open @close=${closePushDialog}></sp-underlay>
|
|
130
|
+
<div class="settings-modal push-dialog">
|
|
131
|
+
<div class="settings-modal-header">
|
|
132
|
+
<h2 class="settings-modal-title">
|
|
133
|
+
Push Schema${state.connection ? html` — ${state.connection}` : nothing}
|
|
134
|
+
</h2>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="settings-modal-body">${pushDialogBody(state)}</div>
|
|
137
|
+
<div class="push-dialog-actions">
|
|
138
|
+
<sp-action-button size="s" class="push-cancel" @click=${closePushDialog}>
|
|
139
|
+
${state.phase === "done" ? "Close" : "Cancel"}
|
|
140
|
+
</sp-action-button>
|
|
141
|
+
${confirmable
|
|
142
|
+
? html`<sp-action-button
|
|
143
|
+
size="s"
|
|
144
|
+
emphasized
|
|
145
|
+
class="push-apply"
|
|
146
|
+
@click=${() => void applyPush()}
|
|
147
|
+
>Apply</sp-action-button
|
|
148
|
+
>`
|
|
149
|
+
: nothing}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
`;
|
|
153
|
+
state.handle.update(tpl);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Contributed-section actions (Test / Push / Open grid) ────────────────────
|
|
157
|
+
|
|
158
|
+
interface ActionsState {
|
|
159
|
+
/** Connection currently being tested, when any. */
|
|
160
|
+
testing: string | null;
|
|
161
|
+
testResult: (DataConnectionTestResult & { connection: string }) | null;
|
|
162
|
+
pushing: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let actionsState: ActionsState = { pushing: false, testResult: null, testing: null };
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The actions renderer for a data-domain contributed section, or null when the section is not
|
|
169
|
+
* data-domain or the platform lacks the data routes. Section keys "connections"/"data" are the
|
|
170
|
+
* connector's host wire contract (the same literals the backend's data routes serve) — the generic
|
|
171
|
+
* contributed-section renderer itself stays extension-agnostic.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} sectionKey
|
|
174
|
+
* @returns {((ctx: SectionActionsContext) => TemplateResult) | null}
|
|
175
|
+
*/
|
|
176
|
+
export function dataSectionActions(
|
|
177
|
+
sectionKey: string,
|
|
178
|
+
): ((ctx: SectionActionsContext) => TemplateResult) | null {
|
|
179
|
+
if ((sectionKey !== "connections" && sectionKey !== "data") || !dataSurfaceAvailable()) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return (ctx) => renderSectionActions(sectionKey, ctx);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function runTest(connection: string, rerender: () => void): Promise<void> {
|
|
186
|
+
actionsState.testing = connection;
|
|
187
|
+
actionsState.testResult = null;
|
|
188
|
+
rerender();
|
|
189
|
+
const result = await testConnection(connection);
|
|
190
|
+
actionsState.testing = null;
|
|
191
|
+
actionsState.testResult = { ...result, connection };
|
|
192
|
+
rerender();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderSectionActions(sectionKey: string, ctx: SectionActionsContext): TemplateResult {
|
|
196
|
+
const { selected, rerender } = ctx;
|
|
197
|
+
const { testResult } = actionsState;
|
|
198
|
+
// On the connections section a selected entry scopes both Test and Push to that connection.
|
|
199
|
+
const pushTarget = sectionKey === "connections" && selected ? selected : undefined;
|
|
200
|
+
return html`
|
|
201
|
+
<div class="data-section-actions">
|
|
202
|
+
${sectionKey === "connections"
|
|
203
|
+
? html`<sp-action-button
|
|
204
|
+
size="s"
|
|
205
|
+
class="data-action-test"
|
|
206
|
+
?disabled=${!selected || actionsState.testing !== null}
|
|
207
|
+
@click=${() => {
|
|
208
|
+
if (selected) {
|
|
209
|
+
void runTest(selected, rerender);
|
|
210
|
+
}
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
${actionsState.testing ? "Testing…" : "Test Connection"}
|
|
214
|
+
</sp-action-button>`
|
|
215
|
+
: nothing}
|
|
216
|
+
<sp-action-button
|
|
217
|
+
size="s"
|
|
218
|
+
class="data-action-push"
|
|
219
|
+
@click=${() => void startPush(pushTarget, rerender)}
|
|
220
|
+
>
|
|
221
|
+
Push Schema
|
|
222
|
+
</sp-action-button>
|
|
223
|
+
<sp-action-button
|
|
224
|
+
size="s"
|
|
225
|
+
class="data-action-grid"
|
|
226
|
+
@click=${() => void openGridSourcePicker()}
|
|
227
|
+
>
|
|
228
|
+
Open Data Grid
|
|
229
|
+
</sp-action-button>
|
|
230
|
+
${testResult
|
|
231
|
+
? html`<span
|
|
232
|
+
class="data-test-result ${testResult.ok ? "ok" : "failed"}"
|
|
233
|
+
title=${testResult.error ?? ""}
|
|
234
|
+
>
|
|
235
|
+
${testResult.connection}:
|
|
236
|
+
${testResult.ok ? "connected" : (testResult.error ?? "failed")}
|
|
237
|
+
</span>`
|
|
238
|
+
: nothing}
|
|
239
|
+
</div>
|
|
240
|
+
`;
|
|
241
|
+
}
|
package/src/panels/editors.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { activeTab } from "../workspace/workspace";
|
|
|
14
14
|
import { mutateUpdateDef, mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
15
15
|
import { view } from "../view";
|
|
16
16
|
import { codeService, getFunctionArgs, setLintMarkers } from "../services/code-services";
|
|
17
|
+
import { globalEntries, namedFormulaEntries } from "../ui/formula-catalog";
|
|
17
18
|
|
|
18
19
|
import type { OxLintDiagnostic } from "../services/code-services";
|
|
19
20
|
import type { JxMutableNode, JxPrototypeDef } from "@jxsuite/schema/types";
|
|
@@ -185,7 +186,10 @@ export function registerFunctionCompletions() {
|
|
|
185
186
|
view._completionRegistered = true;
|
|
186
187
|
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
187
188
|
provideCompletionItems(model, position) {
|
|
188
|
-
|
|
189
|
+
if (!activeTab.value) {
|
|
190
|
+
return { suggestions: [] };
|
|
191
|
+
}
|
|
192
|
+
const defs = activeTab.value.doc.document?.state || {};
|
|
189
193
|
const word = model.getWordUntilPosition(position);
|
|
190
194
|
const range = {
|
|
191
195
|
endColumn: word.endColumn,
|
|
@@ -194,23 +198,45 @@ export function registerFunctionCompletions() {
|
|
|
194
198
|
startLineNumber: position.lineNumber,
|
|
195
199
|
};
|
|
196
200
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
201
|
+
// Named-formula catalog metadata enriches their completions with documentation.
|
|
202
|
+
const formulaDocs = new Map(namedFormulaEntries(defs).map((e) => [e.name, e.description]));
|
|
203
|
+
|
|
204
|
+
const suggestions: monaco.languages.CompletionItem[] = Object.entries(defs).map(
|
|
205
|
+
([key, def]) => {
|
|
206
|
+
let kind = monaco.languages.CompletionItemKind.Variable;
|
|
207
|
+
if (
|
|
208
|
+
(def as JxPrototypeDef)?.$prototype === "Function" ||
|
|
209
|
+
(def as Record<string, unknown>)?.$handler ||
|
|
210
|
+
formulaDocs.has(key)
|
|
211
|
+
) {
|
|
212
|
+
kind = monaco.languages.CompletionItemKind.Function;
|
|
213
|
+
} else if ((def as JxPrototypeDef)?.$prototype) {
|
|
214
|
+
kind = monaco.languages.CompletionItemKind.Property;
|
|
215
|
+
}
|
|
216
|
+
const item: monaco.languages.CompletionItem = {
|
|
217
|
+
insertText: `state.${key}`,
|
|
218
|
+
kind,
|
|
219
|
+
label: `state.${key}`,
|
|
220
|
+
range,
|
|
221
|
+
};
|
|
222
|
+
const documentation = formulaDocs.get(key);
|
|
223
|
+
if (documentation) {
|
|
224
|
+
item.documentation = documentation;
|
|
225
|
+
}
|
|
226
|
+
return item;
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Blessed pure globals from the formula catalog (Math.*, JSON.*, Object.*, …).
|
|
231
|
+
for (const entry of globalEntries()) {
|
|
232
|
+
suggestions.push({
|
|
233
|
+
documentation: entry.description,
|
|
234
|
+
insertText: `window.${entry.label}`,
|
|
235
|
+
kind: monaco.languages.CompletionItemKind.Function,
|
|
236
|
+
label: entry.label,
|
|
211
237
|
range,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
214
240
|
return { suggestions };
|
|
215
241
|
},
|
|
216
242
|
triggerCharacters: ["."],
|