@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
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { html, nothing } from "lit-html";
|
|
9
9
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
10
|
-
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
11
10
|
import { live } from "lit-html/directives/live.js";
|
|
12
|
-
import { styleMap } from "lit-html/directives/style-map.js";
|
|
13
11
|
import { isRef } from "@jxsuite/schema/guards";
|
|
14
12
|
import { dynamicRouteParams } from "../page-params";
|
|
15
13
|
import { projectState } from "../state";
|
|
@@ -25,15 +23,23 @@ import {
|
|
|
25
23
|
import { renderFieldRow } from "../ui/field-row";
|
|
26
24
|
import { rawTextArea, spTextField } from "../ui/field-input";
|
|
27
25
|
import { expressionHint, renderExpressionEditor } from "../ui/expression-editor";
|
|
26
|
+
import { renderStatementEditor } from "./statement-editor";
|
|
27
|
+
import { livePreviewExpression } from "../services/live-preview";
|
|
28
28
|
import { renderMediaPicker } from "../ui/media-picker";
|
|
29
|
+
import { renderOnly } from "../store";
|
|
30
|
+
import { registerFormControl, renderForm } from "../ui/schema-form";
|
|
31
|
+
import { resolveContextPointer } from "../services/context-resolver";
|
|
32
|
+
import type { JsonSchema } from "../ui/schema-form";
|
|
29
33
|
import type { TabUi } from "../tabs/tab";
|
|
30
34
|
import type {
|
|
31
35
|
CemEvent,
|
|
32
36
|
CemParameter,
|
|
33
37
|
JxMutableNode,
|
|
38
|
+
JxStatement,
|
|
34
39
|
JxStateDefinition,
|
|
35
40
|
} from "@jxsuite/schema/types";
|
|
36
41
|
import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services";
|
|
42
|
+
import { getExtensions, loadExtensions } from "../format/format-host";
|
|
37
43
|
import type { TemplateResult } from "lit-html";
|
|
38
44
|
|
|
39
45
|
interface SignalsPanelState {
|
|
@@ -52,30 +58,6 @@ interface SignalsPanelCtx {
|
|
|
52
58
|
updateSession: (patch: Record<string, unknown>) => void;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
interface JsonSchema {
|
|
56
|
-
type?: string;
|
|
57
|
-
properties?: Record<string, SchemaProperty>;
|
|
58
|
-
required?: string[];
|
|
59
|
-
description?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface SchemaProperty {
|
|
63
|
-
type?: string;
|
|
64
|
-
enum?: string[];
|
|
65
|
-
default?: unknown;
|
|
66
|
-
format?: string;
|
|
67
|
-
minimum?: number;
|
|
68
|
-
maximum?: number;
|
|
69
|
-
description?: string;
|
|
70
|
-
examples?: string[];
|
|
71
|
-
name?: string;
|
|
72
|
-
items?: {
|
|
73
|
-
type?: string;
|
|
74
|
-
properties?: Record<string, Record<string, unknown>>;
|
|
75
|
-
required?: string[];
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
61
|
export interface SignalDef {
|
|
80
62
|
$prototype?: string;
|
|
81
63
|
$src?: string;
|
|
@@ -86,7 +68,7 @@ export interface SignalDef {
|
|
|
86
68
|
$handler?: string;
|
|
87
69
|
type?: string;
|
|
88
70
|
default?: unknown;
|
|
89
|
-
body?: string;
|
|
71
|
+
body?: string | unknown[];
|
|
90
72
|
parameters?: string[];
|
|
91
73
|
timing?: string;
|
|
92
74
|
description?: string;
|
|
@@ -158,6 +140,29 @@ const STUDIO_RESERVED_KEYS = new Set([
|
|
|
158
140
|
|
|
159
141
|
// ─── Signals / defs helpers ──────────────────────────────────────────────────
|
|
160
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Extension-manifest state classes for the add-state picker: plain `$prototype` targets (no
|
|
145
|
+
* admission blocks) across the enabled extensions, each with its `$studio.stateDefaults` hint
|
|
146
|
+
* (specs/extensions.md §10). Manifest classes need no `$src` — the registry resolves them.
|
|
147
|
+
*/
|
|
148
|
+
export function extensionStateClasses(): {
|
|
149
|
+
name: string;
|
|
150
|
+
stateDefaults?: Record<string, unknown>;
|
|
151
|
+
}[] {
|
|
152
|
+
const out: { name: string; stateDefaults?: Record<string, unknown> }[] = [];
|
|
153
|
+
for (const ext of getExtensions()) {
|
|
154
|
+
for (const cls of ext.classes ?? []) {
|
|
155
|
+
if (cls.state) {
|
|
156
|
+
out.push({
|
|
157
|
+
name: cls.name,
|
|
158
|
+
...(cls.stateDefaults === undefined ? {} : { stateDefaults: cls.stateDefaults }),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
161
166
|
/**
|
|
162
167
|
* View a state entry through the panel's flattened editing lens. Naked primitive and array entries
|
|
163
168
|
* surface as an empty view — the renderers guard every field access.
|
|
@@ -233,6 +238,9 @@ export function defHint(_name: string, def: SignalDef | null | undefined) {
|
|
|
233
238
|
return expressionHint(def.$expression);
|
|
234
239
|
}
|
|
235
240
|
if (def.$prototype === "Function") {
|
|
241
|
+
if (Array.isArray(def.body)) {
|
|
242
|
+
return `${def.body.length} statement${def.body.length === 1 ? "" : "s"}`;
|
|
243
|
+
}
|
|
236
244
|
if (def.body) {
|
|
237
245
|
return def.body.length > 20 ? `${def.body.slice(0, 20)}...` : def.body;
|
|
238
246
|
}
|
|
@@ -415,6 +423,10 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
|
|
|
415
423
|
const defs = S.document.state || {};
|
|
416
424
|
const entries = Object.entries(defs);
|
|
417
425
|
|
|
426
|
+
// Warm the extensions payload so manifest state classes appear in the add picker (the panel
|
|
427
|
+
// Re-renders constantly; loadExtensions memoizes, so this is a one-time fetch per project).
|
|
428
|
+
void loadExtensions();
|
|
429
|
+
|
|
418
430
|
// Group by category
|
|
419
431
|
const groups = {
|
|
420
432
|
computed: [],
|
|
@@ -504,6 +516,29 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
|
|
|
504
516
|
return;
|
|
505
517
|
}
|
|
506
518
|
|
|
519
|
+
// Extension-manifest state classes ("ext:Session"): no $src needed — the registry
|
|
520
|
+
// Resolves them; the descriptor's stateDefaults seed the def (e.g. timing "client").
|
|
521
|
+
if (type.startsWith("ext:")) {
|
|
522
|
+
const protoName = type.slice(4);
|
|
523
|
+
const cls = extensionStateClasses().find((c) => c.name === protoName);
|
|
524
|
+
let n = `$${protoName.charAt(0).toLowerCase()}${protoName.slice(1)}`;
|
|
525
|
+
let i = 1;
|
|
526
|
+
const base = n;
|
|
527
|
+
while (S.document.state && S.document.state[n]) {
|
|
528
|
+
n = base + i;
|
|
529
|
+
i += 1;
|
|
530
|
+
}
|
|
531
|
+
transactDoc(activeTab.value, (t) =>
|
|
532
|
+
mutateAddDef(t, n, {
|
|
533
|
+
$prototype: protoName,
|
|
534
|
+
...cls?.stateDefaults,
|
|
535
|
+
} as Record<string, JsonValue>),
|
|
536
|
+
);
|
|
537
|
+
expandedSignal = n;
|
|
538
|
+
ctx.renderLeftPanel();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
507
542
|
// Handle import-based prototypes (e.g., "import:ContentCollection")
|
|
508
543
|
if (type.startsWith("import:")) {
|
|
509
544
|
const protoName = type.slice(7);
|
|
@@ -576,6 +611,11 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
|
|
|
576
611
|
projectState.projectConfig.imports,
|
|
577
612
|
).map((k: string) => html`<sp-menu-item value="import:${k}">${k}</sp-menu-item>`)}`
|
|
578
613
|
: nothing}
|
|
614
|
+
${extensionStateClasses().length > 0
|
|
615
|
+
? html`<sp-menu-divider></sp-menu-divider>${extensionStateClasses().map(
|
|
616
|
+
(cls) => html`<sp-menu-item value="ext:${cls.name}">${cls.name}</sp-menu-item>`,
|
|
617
|
+
)}`
|
|
618
|
+
: nothing}
|
|
579
619
|
<sp-menu-divider></sp-menu-divider>
|
|
580
620
|
<sp-menu-item value="expression">Expression</sp-menu-item>
|
|
581
621
|
<sp-menu-item value="function">Function</sp-menu-item>
|
|
@@ -786,6 +826,22 @@ function renderSignalEditorTemplate(
|
|
|
786
826
|
} else if (cat === "expression") {
|
|
787
827
|
const exprNode = def.$expression || { operator: "=", target: null };
|
|
788
828
|
fields = html`
|
|
829
|
+
<div style="display:flex;align-items:center;gap:4px">
|
|
830
|
+
<span class="field-label" style="flex:1">Expression</span>
|
|
831
|
+
<sp-action-button
|
|
832
|
+
size="xs"
|
|
833
|
+
quiet
|
|
834
|
+
title="Open in formula workspace"
|
|
835
|
+
@click=${() => {
|
|
836
|
+
ctx.updateSession({
|
|
837
|
+
ui: { editingFormula: { defName: name, type: "def" } },
|
|
838
|
+
});
|
|
839
|
+
ctx.renderCanvas();
|
|
840
|
+
}}
|
|
841
|
+
>
|
|
842
|
+
<sp-icon-full-screen slot="icon"></sp-icon-full-screen>
|
|
843
|
+
</sp-action-button>
|
|
844
|
+
</div>
|
|
789
845
|
${renderExpressionEditor(
|
|
790
846
|
exprNode,
|
|
791
847
|
(newNode: unknown) =>
|
|
@@ -795,7 +851,17 @@ function renderSignalEditorTemplate(
|
|
|
795
851
|
),
|
|
796
852
|
{
|
|
797
853
|
allowEventRef: false,
|
|
854
|
+
// Live-context evaluation in the canvas iframe, snapshot fallback (M6). The signals
|
|
855
|
+
// Panel lives in the left panel — re-render it when a fresh live result lands.
|
|
856
|
+
preview: livePreviewExpression(activeTab.value, `def:${name}`, exprNode, null, () =>
|
|
857
|
+
renderOnly("leftPanel"),
|
|
858
|
+
),
|
|
859
|
+
onInsertDef: (defName, vendored) =>
|
|
860
|
+
transactDoc(activeTab.value, (t) =>
|
|
861
|
+
mutateAddDef(t, defName, vendored as Record<string, JsonValue>),
|
|
862
|
+
),
|
|
798
863
|
stateDefs: Object.keys(S.document.state || {}),
|
|
864
|
+
stateEntries: S.document.state || {},
|
|
799
865
|
},
|
|
800
866
|
)}
|
|
801
867
|
`;
|
|
@@ -924,6 +990,42 @@ function renderFunctionFields(
|
|
|
924
990
|
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { description: v || undefined })),
|
|
925
991
|
);
|
|
926
992
|
|
|
993
|
+
// Structured bodies (spec §20) are a body MODE of the function category, not a new entity:
|
|
994
|
+
// "Statements" renders the statement-card editor over `body: JxStatement[]`; "Code" is the
|
|
995
|
+
// Textarea/Monaco path over `body: string`. Switching modes replaces the body with the other
|
|
996
|
+
// Representation's empty seed — an explicit mode change, nothing is converted.
|
|
997
|
+
const bodyIsStatements = Array.isArray(def.body);
|
|
998
|
+
const bodyModeToggle = html`
|
|
999
|
+
<sp-action-group size="s" compact class="body-mode-toggle">
|
|
1000
|
+
<sp-action-button
|
|
1001
|
+
size="s"
|
|
1002
|
+
class="body-mode-statements"
|
|
1003
|
+
?selected=${bodyIsStatements}
|
|
1004
|
+
@click=${() => {
|
|
1005
|
+
if (!bodyIsStatements) {
|
|
1006
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: [] }));
|
|
1007
|
+
ctx.renderLeftPanel();
|
|
1008
|
+
}
|
|
1009
|
+
}}
|
|
1010
|
+
>
|
|
1011
|
+
Statements
|
|
1012
|
+
</sp-action-button>
|
|
1013
|
+
<sp-action-button
|
|
1014
|
+
size="s"
|
|
1015
|
+
class="body-mode-code"
|
|
1016
|
+
?selected=${!bodyIsStatements}
|
|
1017
|
+
@click=${() => {
|
|
1018
|
+
if (bodyIsStatements) {
|
|
1019
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: "" }));
|
|
1020
|
+
ctx.renderLeftPanel();
|
|
1021
|
+
}
|
|
1022
|
+
}}
|
|
1023
|
+
>
|
|
1024
|
+
Code
|
|
1025
|
+
</sp-action-button>
|
|
1026
|
+
</sp-action-group>
|
|
1027
|
+
`;
|
|
1028
|
+
|
|
927
1029
|
const bodyField = def.$src
|
|
928
1030
|
? html`
|
|
929
1031
|
${signalFieldRow("Source", def.$src || "", (v: string) =>
|
|
@@ -938,29 +1040,52 @@ function renderFunctionFields(
|
|
|
938
1040
|
: html`
|
|
939
1041
|
<div style="display:flex;align-items:center;gap:4px">
|
|
940
1042
|
<span class="field-label" style="flex:1">Body</span>
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1043
|
+
${bodyModeToggle}
|
|
1044
|
+
${bodyIsStatements
|
|
1045
|
+
? nothing
|
|
1046
|
+
: html`
|
|
1047
|
+
<sp-action-button
|
|
1048
|
+
size="xs"
|
|
1049
|
+
quiet
|
|
1050
|
+
title="Open in code editor"
|
|
1051
|
+
@click=${() => {
|
|
1052
|
+
ctx.updateSession({
|
|
1053
|
+
ui: { editingFunction: { defName: name, type: "def" } },
|
|
1054
|
+
});
|
|
1055
|
+
ctx.renderCanvas();
|
|
1056
|
+
}}
|
|
1057
|
+
>
|
|
1058
|
+
<sp-icon-code slot="icon"></sp-icon-code>
|
|
1059
|
+
</sp-action-button>
|
|
1060
|
+
`}
|
|
954
1061
|
</div>
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1062
|
+
${bodyIsStatements
|
|
1063
|
+
? renderStatementEditor(
|
|
1064
|
+
def.body as JxStatement[],
|
|
1065
|
+
(next) => {
|
|
1066
|
+
transactDoc(activeTab.value, (t) =>
|
|
1067
|
+
mutateUpdateDef(t, name, { body: next as unknown as JsonValue }),
|
|
1068
|
+
);
|
|
1069
|
+
ctx.renderLeftPanel();
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
allowEventRef: true,
|
|
1073
|
+
emits: def.emits ?? [],
|
|
1074
|
+
stateDefs: Object.keys(S.document.state || {}),
|
|
1075
|
+
stateEntries: S.document.state || {},
|
|
1076
|
+
},
|
|
1077
|
+
)
|
|
1078
|
+
: html`
|
|
1079
|
+
<textarea
|
|
1080
|
+
class="field-input"
|
|
1081
|
+
style="min-height:60px;font-family:var(--font-mono);font-size:var(--spectrum-font-size-50, 11px)"
|
|
1082
|
+
.value=${typeof def.body === "string" ? def.body : ""}
|
|
1083
|
+
@input=${(e: Event) => {
|
|
1084
|
+
const v = (e.target as HTMLInputElement).value;
|
|
1085
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: v }));
|
|
1086
|
+
}}
|
|
1087
|
+
></textarea>
|
|
1088
|
+
`}
|
|
964
1089
|
`;
|
|
965
1090
|
|
|
966
1091
|
return html`
|
|
@@ -1234,137 +1359,27 @@ function renderEmitsEditorTemplate(S: SignalsPanelState, name: string, def: Sign
|
|
|
1234
1359
|
// ─── Plugin schema-driven form rendering ────────────────────────────────────
|
|
1235
1360
|
|
|
1236
1361
|
/**
|
|
1237
|
-
* Resolve a schema
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
* string sentinel
|
|
1362
|
+
* Resolve a schema context pointer for signal config forms — a thin wrapper over the generic
|
|
1363
|
+
* `resolveContextPointer` making the content-type roots always-present (a missing section resolves
|
|
1364
|
+
* to `{}` → empty choices rather than a plain textfield): the parser's real descriptors point at
|
|
1365
|
+
* the `#/$context/content` root, while the deprecated `"$contentTypes"` string sentinel and the
|
|
1366
|
+
* legacy `#/$context/contentTypes` root keep resolving bit-for-bit for old class descriptors.
|
|
1241
1367
|
*
|
|
1242
|
-
* @param {
|
|
1243
|
-
* @param {Record<string, unknown>} [
|
|
1244
|
-
* @returns {
|
|
1368
|
+
* @param {string} pointer
|
|
1369
|
+
* @param {Record<string, unknown>} [scope] - Parent def for `{@param}` substitution
|
|
1370
|
+
* @returns {unknown}
|
|
1245
1371
|
*/
|
|
1246
|
-
function
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
): string[] | undefined {
|
|
1250
|
-
if (Array.isArray(enumDef)) {
|
|
1251
|
-
return enumDef;
|
|
1372
|
+
function resolveSignalsContextPointer(pointer: string, scope?: Record<string, unknown>): unknown {
|
|
1373
|
+
if (pointer === "#/$context/content") {
|
|
1374
|
+
return projectState?.projectConfig?.content ?? {};
|
|
1252
1375
|
}
|
|
1253
|
-
if (
|
|
1254
|
-
|
|
1255
|
-
if (ref === "#/$context/contentTypes") {
|
|
1256
|
-
return Object.keys(projectState?.projectConfig?.contentTypes ?? {});
|
|
1257
|
-
}
|
|
1258
|
-
if (typeof ref === "string" && ref.startsWith("#/$context/contentTypes/{@")) {
|
|
1259
|
-
const match = ref.match(/#\/\$context\/contentTypes\/\{@(\w+)\}\/schema\/properties/);
|
|
1260
|
-
if (match && parentDef) {
|
|
1261
|
-
const [, paramName] = match;
|
|
1262
|
-
const typeName = parentDef[paramName!] as string | undefined;
|
|
1263
|
-
if (typeName) {
|
|
1264
|
-
const ct = projectState?.projectConfig?.contentTypes?.[typeName] as
|
|
1265
|
-
| Record<string, unknown>
|
|
1266
|
-
| undefined;
|
|
1267
|
-
const schema = ct?.schema as Record<string, unknown> | undefined;
|
|
1268
|
-
const props = schema?.properties as Record<string, unknown> | undefined;
|
|
1269
|
-
if (props) {
|
|
1270
|
-
return Object.keys(props);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
return undefined;
|
|
1275
|
-
}
|
|
1376
|
+
if (pointer === "$contentTypes" || pointer === "#/$context/contentTypes") {
|
|
1377
|
+
return projectState?.projectConfig?.contentTypes ?? {};
|
|
1276
1378
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
/**
|
|
1284
|
-
* Render a single inline field within an array-of-objects row. Dispatches by schema type: enum →
|
|
1285
|
-
* picker, boolean → switch, number → number-field, else → textfield.
|
|
1286
|
-
*
|
|
1287
|
-
* @param {string} key
|
|
1288
|
-
* @param {Record<string, unknown>} schema
|
|
1289
|
-
* @param {unknown} value
|
|
1290
|
-
* @param {(val: unknown) => void} onChange
|
|
1291
|
-
* @param {Record<string, unknown>} [parentDef] - Parent def for resolving dependent enum refs
|
|
1292
|
-
*/
|
|
1293
|
-
/** Parse a numeric field value, returning NaN for blank input (so callers can treat it as unset). */
|
|
1294
|
-
function parseNumericField(raw: string, integer: boolean): number {
|
|
1295
|
-
if (raw.trim() === "") {
|
|
1296
|
-
return Number.NaN;
|
|
1297
|
-
}
|
|
1298
|
-
return integer ? Math.trunc(Number(raw)) : Number(raw);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
function renderInlineField(
|
|
1302
|
-
key: string,
|
|
1303
|
-
schema: Record<string, unknown>,
|
|
1304
|
-
value: unknown,
|
|
1305
|
-
onChange: (val: unknown) => void,
|
|
1306
|
-
parentDef?: Record<string, unknown>,
|
|
1307
|
-
) {
|
|
1308
|
-
if (isRef(value)) {
|
|
1309
|
-
return html`<sp-textfield
|
|
1310
|
-
size="s"
|
|
1311
|
-
label=${key}
|
|
1312
|
-
placeholder=${key}
|
|
1313
|
-
.value=${live(value.$ref)}
|
|
1314
|
-
@change=${(e: Event) => {
|
|
1315
|
-
const v = (e.target as HTMLInputElement).value.trim();
|
|
1316
|
-
onChange(v ? { $ref: v } : undefined);
|
|
1317
|
-
}}
|
|
1318
|
-
></sp-textfield>`;
|
|
1319
|
-
}
|
|
1320
|
-
const enumValues = resolveSchemaEnum(schema.enum, parentDef);
|
|
1321
|
-
|
|
1322
|
-
if (enumValues) {
|
|
1323
|
-
return html`<sp-picker
|
|
1324
|
-
size="s"
|
|
1325
|
-
label=${key}
|
|
1326
|
-
value=${value !== undefined ? String(value) : "__none__"}
|
|
1327
|
-
@change=${(e: Event) =>
|
|
1328
|
-
onChange(
|
|
1329
|
-
(e.target as HTMLInputElement).value === "__none__"
|
|
1330
|
-
? undefined
|
|
1331
|
-
: (e.target as HTMLInputElement).value,
|
|
1332
|
-
)}
|
|
1333
|
-
>
|
|
1334
|
-
<sp-menu-item value="__none__">—</sp-menu-item>
|
|
1335
|
-
${enumValues.map((v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`)}
|
|
1336
|
-
</sp-picker>`;
|
|
1337
|
-
}
|
|
1338
|
-
if (schema.type === "boolean") {
|
|
1339
|
-
return html`<sp-switch
|
|
1340
|
-
size="s"
|
|
1341
|
-
?checked=${Boolean(value)}
|
|
1342
|
-
@change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
|
|
1343
|
-
>${key}</sp-switch
|
|
1344
|
-
>`;
|
|
1345
|
-
}
|
|
1346
|
-
if (schema.type === "integer" || schema.type === "number") {
|
|
1347
|
-
return html`<sp-number-field
|
|
1348
|
-
size="s"
|
|
1349
|
-
label=${key}
|
|
1350
|
-
.value=${value !== undefined ? value : nothing}
|
|
1351
|
-
step=${schema.type === "integer" ? "1" : nothing}
|
|
1352
|
-
@change=${(e: Event) => {
|
|
1353
|
-
const parsed = parseNumericField(
|
|
1354
|
-
(e.target as HTMLInputElement).value,
|
|
1355
|
-
schema.type === "integer",
|
|
1356
|
-
);
|
|
1357
|
-
onChange(Number.isNaN(parsed) ? undefined : parsed);
|
|
1358
|
-
}}
|
|
1359
|
-
></sp-number-field>`;
|
|
1360
|
-
}
|
|
1361
|
-
return html`<sp-textfield
|
|
1362
|
-
size="s"
|
|
1363
|
-
label=${key}
|
|
1364
|
-
placeholder=${key}
|
|
1365
|
-
.value=${value ?? ""}
|
|
1366
|
-
@input=${(e: Event) => onChange((e.target as HTMLInputElement).value || undefined)}
|
|
1367
|
-
></sp-textfield>`;
|
|
1379
|
+
return resolveContextPointer(pointer, {
|
|
1380
|
+
projectConfig: (projectState?.projectConfig ?? {}) as Record<string, unknown>,
|
|
1381
|
+
...(scope !== undefined && { scope }),
|
|
1382
|
+
});
|
|
1368
1383
|
}
|
|
1369
1384
|
|
|
1370
1385
|
/**
|
|
@@ -1424,39 +1439,22 @@ function renderBindingControl(opts: {
|
|
|
1424
1439
|
`;
|
|
1425
1440
|
}
|
|
1426
1441
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
size="s"
|
|
1439
|
-
style="min-height:40px"
|
|
1440
|
-
.value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
|
|
1441
|
-
placeholder=${ps.default !== undefined ? JSON.stringify(ps.default) : nothing}
|
|
1442
|
-
@input=${(e: Event) => {
|
|
1443
|
-
clearTimeout(debounce);
|
|
1444
|
-
debounce = setTimeout(() => {
|
|
1445
|
-
try {
|
|
1446
|
-
transactDoc(activeTab.value, (t) =>
|
|
1447
|
-
mutateUpdateDef(t, name, {
|
|
1448
|
-
[prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
|
|
1449
|
-
}),
|
|
1450
|
-
);
|
|
1451
|
-
} catch {}
|
|
1452
|
-
}, 500);
|
|
1453
|
-
}}
|
|
1454
|
-
></sp-textfield>`;
|
|
1455
|
-
}
|
|
1442
|
+
// The "binding" control registers here rather than in ui/form-controls.ts because it owns
|
|
1443
|
+
// Panel-local ephemeral UI state (bindingCustomOpen) and the route-param picker semantics.
|
|
1444
|
+
registerFormControl("binding", ({ key, value, onChange, ctx, rerender }) =>
|
|
1445
|
+
renderBindingControl({
|
|
1446
|
+
commit: onChange,
|
|
1447
|
+
fieldKey: `${ctx.fieldKeyPrefix ?? ""}.${key}`,
|
|
1448
|
+
params: ctx.params ?? [],
|
|
1449
|
+
refVal: isRef(value) ? value.$ref : "",
|
|
1450
|
+
rerender,
|
|
1451
|
+
}),
|
|
1452
|
+
);
|
|
1456
1453
|
|
|
1457
1454
|
/**
|
|
1458
|
-
* Render config form fields from a JSON Schema `properties` object
|
|
1459
|
-
*
|
|
1455
|
+
* Render config form fields from a JSON Schema `properties` object — a thin wrapper over the shared
|
|
1456
|
+
* schema-form engine. Skips studio-reserved keys, resolves enum/context refs against the project
|
|
1457
|
+
* config, and commits every patch through transactDoc/mutateUpdateDef.
|
|
1460
1458
|
*/
|
|
1461
1459
|
export function renderSchemaFieldsTemplate(
|
|
1462
1460
|
schema: JsonSchema | null | undefined,
|
|
@@ -1469,260 +1467,19 @@ export function renderSchemaFieldsTemplate(
|
|
|
1469
1467
|
return nothing;
|
|
1470
1468
|
}
|
|
1471
1469
|
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const propertyFields = Object.entries(schema.properties)
|
|
1476
|
-
.filter(([prop]) => !STUDIO_RESERVED_KEYS.has(prop))
|
|
1477
|
-
.map(([prop, ps]) => {
|
|
1478
|
-
const currentValue = def[prop];
|
|
1479
|
-
const labelText = prop + (required.has(prop) ? " *" : "");
|
|
1480
|
-
|
|
1481
|
-
let control;
|
|
1482
|
-
const enumValues = resolveSchemaEnum(ps.enum, def);
|
|
1483
|
-
if (
|
|
1484
|
-
isRef(currentValue) &&
|
|
1485
|
-
ps.format !== "json-schema" &&
|
|
1486
|
-
ps.type !== "object" &&
|
|
1487
|
-
ps.type !== "array"
|
|
1488
|
-
) {
|
|
1489
|
-
control = renderBindingControl({
|
|
1490
|
-
refVal: currentValue.$ref,
|
|
1491
|
-
params,
|
|
1492
|
-
fieldKey: `${name}.${prop}`,
|
|
1493
|
-
commit: (next) =>
|
|
1494
|
-
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { [prop]: next })),
|
|
1495
|
-
rerender: ctx ? () => ctx.renderLeftPanel() : undefined,
|
|
1496
|
-
});
|
|
1497
|
-
} else if (enumValues) {
|
|
1498
|
-
control = html`
|
|
1499
|
-
<sp-picker
|
|
1500
|
-
size="s"
|
|
1501
|
-
value=${currentValue !== undefined
|
|
1502
|
-
? String(currentValue)
|
|
1503
|
-
: ps.default !== undefined
|
|
1504
|
-
? String(ps.default)
|
|
1505
|
-
: "__none__"}
|
|
1506
|
-
@change=${(e: Event) =>
|
|
1507
|
-
transactDoc(activeTab.value, (t) =>
|
|
1508
|
-
mutateUpdateDef(t, name, {
|
|
1509
|
-
[prop]:
|
|
1510
|
-
(e.target as HTMLInputElement).value === "__none__"
|
|
1511
|
-
? undefined
|
|
1512
|
-
: (e.target as HTMLInputElement).value,
|
|
1513
|
-
}),
|
|
1514
|
-
)}
|
|
1515
|
-
>
|
|
1516
|
-
${!required.has(prop) ? html`<sp-menu-item value="__none__">—</sp-menu-item>` : nothing}
|
|
1517
|
-
${enumValues.map(
|
|
1518
|
-
(val: string) => html`<sp-menu-item value=${val}>${val}</sp-menu-item>`,
|
|
1519
|
-
)}
|
|
1520
|
-
</sp-picker>
|
|
1521
|
-
`;
|
|
1522
|
-
} else if (ps.type === "boolean") {
|
|
1523
|
-
control = html`<sp-checkbox
|
|
1524
|
-
?checked=${currentValue ?? ps.default ?? false}
|
|
1525
|
-
@change=${(e: Event) =>
|
|
1526
|
-
transactDoc(activeTab.value, (t) =>
|
|
1527
|
-
mutateUpdateDef(t, name, {
|
|
1528
|
-
[prop]: (e.target as HTMLInputElement).checked,
|
|
1529
|
-
}),
|
|
1530
|
-
)}
|
|
1531
|
-
></sp-checkbox>`;
|
|
1532
|
-
} else if (ps.type === "integer" || ps.type === "number") {
|
|
1533
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
1534
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
1535
|
-
control = html`<sp-number-field
|
|
1536
|
-
size="s"
|
|
1537
|
-
min=${ifDefined(ps.minimum)}
|
|
1538
|
-
max=${ifDefined(ps.maximum)}
|
|
1539
|
-
step=${ps.type === "integer" ? "1" : nothing}
|
|
1540
|
-
.value=${currentValue !== undefined ? currentValue : nothing}
|
|
1541
|
-
placeholder=${ps.default != null ? String(ps.default) : nothing}
|
|
1542
|
-
@change=${(e: Event) => {
|
|
1543
|
-
clearTimeout(debounce);
|
|
1544
|
-
debounce = setTimeout(() => {
|
|
1545
|
-
const parsed = parseNumericField(
|
|
1546
|
-
(e.target as HTMLInputElement).value,
|
|
1547
|
-
ps.type === "integer",
|
|
1548
|
-
);
|
|
1549
|
-
transactDoc(activeTab.value, (t) =>
|
|
1550
|
-
mutateUpdateDef(t, name, {
|
|
1551
|
-
[prop]: Number.isNaN(parsed) ? undefined : parsed,
|
|
1552
|
-
}),
|
|
1553
|
-
);
|
|
1554
|
-
}, 400);
|
|
1555
|
-
}}
|
|
1556
|
-
></sp-number-field>`;
|
|
1557
|
-
} else if (ps.format === "json-schema") {
|
|
1558
|
-
const hasValue =
|
|
1559
|
-
currentValue && typeof currentValue === "object" && Object.keys(currentValue).length > 0;
|
|
1560
|
-
const cv = currentValue as Record<string, unknown>;
|
|
1561
|
-
const isSchemaRef = hasValue && cv.$ref;
|
|
1562
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
1563
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
1564
|
-
control = html`
|
|
1565
|
-
<div class="schema-param-editor">
|
|
1566
|
-
${hasValue && !isSchemaRef && cv.properties
|
|
1567
|
-
? html`
|
|
1568
|
-
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px">
|
|
1569
|
-
${Object.entries(cv.properties as Record<string, Record<string, unknown>>).map(
|
|
1570
|
-
([k, v]) => html`
|
|
1571
|
-
<span
|
|
1572
|
-
style="background:var(--bg);padding:1px 6px;border-radius:var(--radius);font-size:10px;color:var(--fg-dim)"
|
|
1573
|
-
>${k}: ${v.type ?? "any"}</span
|
|
1574
|
-
>
|
|
1575
|
-
`,
|
|
1576
|
-
)}
|
|
1577
|
-
</div>
|
|
1578
|
-
`
|
|
1579
|
-
: nothing}
|
|
1580
|
-
<sp-textfield
|
|
1581
|
-
multiline
|
|
1582
|
-
size="s"
|
|
1583
|
-
style=${styleMap({
|
|
1584
|
-
fontFamily: "monospace",
|
|
1585
|
-
fontSize: "11px",
|
|
1586
|
-
minHeight: hasValue ? "80px" : "40px",
|
|
1587
|
-
})}
|
|
1588
|
-
.value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
|
|
1589
|
-
placeholder=${ps.description ?? "JSON Schema defining the data shape\u2026"}
|
|
1590
|
-
@input=${(e: Event) => {
|
|
1591
|
-
clearTimeout(debounce);
|
|
1592
|
-
debounce = setTimeout(() => {
|
|
1593
|
-
try {
|
|
1594
|
-
transactDoc(activeTab.value, (t) =>
|
|
1595
|
-
mutateUpdateDef(t, name, {
|
|
1596
|
-
[prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
|
|
1597
|
-
}),
|
|
1598
|
-
);
|
|
1599
|
-
} catch {}
|
|
1600
|
-
}, 500);
|
|
1601
|
-
}}
|
|
1602
|
-
></sp-textfield>
|
|
1603
|
-
</div>
|
|
1604
|
-
`;
|
|
1605
|
-
} else if (ps.type === "array" && ps.items?.type === "object" && ps.items?.properties) {
|
|
1606
|
-
// Array of objects with defined schema → multi-row inline form
|
|
1607
|
-
const rows: Record<string, unknown>[] = Array.isArray(currentValue)
|
|
1608
|
-
? (currentValue as Record<string, unknown>[])
|
|
1609
|
-
: [];
|
|
1610
|
-
const itemProps = ps.items.properties as Record<string, Record<string, unknown>>;
|
|
1611
|
-
control = html`
|
|
1612
|
-
<div class="array-object-field">
|
|
1613
|
-
${rows.map(
|
|
1614
|
-
(row: Record<string, unknown>, idx: number) => html`
|
|
1615
|
-
<div
|
|
1616
|
-
class="array-object-row"
|
|
1617
|
-
style="display:flex;gap:4px;align-items:center;margin-bottom:4px"
|
|
1618
|
-
>
|
|
1619
|
-
${Object.entries(itemProps).map(([propKey, propSchema]) =>
|
|
1620
|
-
renderInlineField(
|
|
1621
|
-
propKey,
|
|
1622
|
-
propSchema,
|
|
1623
|
-
row[propKey],
|
|
1624
|
-
(val) => {
|
|
1625
|
-
const updated = [...rows];
|
|
1626
|
-
updated[idx] = { ...updated[idx], [propKey]: val };
|
|
1627
|
-
transactDoc(activeTab.value, (t) =>
|
|
1628
|
-
mutateUpdateDef(t, name, { [prop]: updated }),
|
|
1629
|
-
);
|
|
1630
|
-
},
|
|
1631
|
-
def,
|
|
1632
|
-
),
|
|
1633
|
-
)}
|
|
1634
|
-
<sp-action-button
|
|
1635
|
-
quiet
|
|
1636
|
-
size="s"
|
|
1637
|
-
@click=${() => {
|
|
1638
|
-
const updated = rows.filter((_: unknown, i: number) => i !== idx);
|
|
1639
|
-
transactDoc(activeTab.value, (t) =>
|
|
1640
|
-
mutateUpdateDef(t, name, {
|
|
1641
|
-
[prop]: updated.length > 0 ? updated : undefined,
|
|
1642
|
-
}),
|
|
1643
|
-
);
|
|
1644
|
-
ctx?.renderLeftPanel();
|
|
1645
|
-
}}
|
|
1646
|
-
>
|
|
1647
|
-
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
1648
|
-
</sp-action-button>
|
|
1649
|
-
</div>
|
|
1650
|
-
`,
|
|
1651
|
-
)}
|
|
1652
|
-
<sp-action-button
|
|
1653
|
-
quiet
|
|
1654
|
-
size="s"
|
|
1655
|
-
@click=${(e: Event) => {
|
|
1656
|
-
e.stopPropagation();
|
|
1657
|
-
const newRow: Record<string, unknown> = {};
|
|
1658
|
-
for (const [k, v] of Object.entries(itemProps)) {
|
|
1659
|
-
if ((v as Record<string, unknown>).default !== undefined) {
|
|
1660
|
-
newRow[k] = (v as Record<string, unknown>).default;
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
transactDoc(activeTab.value, (t) =>
|
|
1664
|
-
mutateUpdateDef(t, name, { [prop]: [...rows, newRow] }),
|
|
1665
|
-
);
|
|
1666
|
-
ctx?.renderLeftPanel();
|
|
1667
|
-
}}
|
|
1668
|
-
>+ Add</sp-action-button
|
|
1669
|
-
>
|
|
1670
|
-
</div>
|
|
1671
|
-
`;
|
|
1672
|
-
} else if (ps.type === "array" || ps.type === "object") {
|
|
1673
|
-
control = renderJsonTextField(currentValue, ps, name, prop);
|
|
1674
|
-
} else {
|
|
1675
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
1676
|
-
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
1677
|
-
const ph = ps.default !== undefined ? String(ps.default) : (ps.examples?.[0] ?? "");
|
|
1678
|
-
control = html`<div style="display:flex;gap:4px;align-items:center">
|
|
1679
|
-
<sp-textfield
|
|
1680
|
-
size="s"
|
|
1681
|
-
style="flex:1"
|
|
1682
|
-
.value=${currentValue ?? ""}
|
|
1683
|
-
placeholder=${ph || nothing}
|
|
1684
|
-
title=${ps.description || nothing}
|
|
1685
|
-
@input=${(e: Event) => {
|
|
1686
|
-
clearTimeout(debounce);
|
|
1687
|
-
debounce = setTimeout(
|
|
1688
|
-
() =>
|
|
1689
|
-
transactDoc(activeTab.value, (t) =>
|
|
1690
|
-
mutateUpdateDef(t, name, {
|
|
1691
|
-
[prop]: (e.target as HTMLInputElement).value || undefined,
|
|
1692
|
-
}),
|
|
1693
|
-
),
|
|
1694
|
-
400,
|
|
1695
|
-
);
|
|
1696
|
-
}}
|
|
1697
|
-
></sp-textfield>
|
|
1698
|
-
${params.length > 0
|
|
1699
|
-
? html`<sp-action-button
|
|
1700
|
-
quiet
|
|
1701
|
-
size="s"
|
|
1702
|
-
title="Bind to route param"
|
|
1703
|
-
@click=${() => {
|
|
1704
|
-
transactDoc(activeTab.value, (t) =>
|
|
1705
|
-
mutateUpdateDef(t, name, {
|
|
1706
|
-
[prop]: { $ref: `#/$params/${params[0]}` },
|
|
1707
|
-
}),
|
|
1708
|
-
);
|
|
1709
|
-
ctx?.renderLeftPanel();
|
|
1710
|
-
}}
|
|
1711
|
-
><sp-icon-link slot="icon"></sp-icon-link
|
|
1712
|
-
></sp-action-button>`
|
|
1713
|
-
: nothing}
|
|
1714
|
-
</div>`;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
return renderFieldRow({
|
|
1718
|
-
hasValue: false,
|
|
1719
|
-
label: labelText,
|
|
1720
|
-
prop: ps.name || prop,
|
|
1721
|
-
widget: control,
|
|
1722
|
-
});
|
|
1723
|
-
});
|
|
1470
|
+
const properties = Object.fromEntries(
|
|
1471
|
+
Object.entries(schema.properties).filter(([prop]) => !STUDIO_RESERVED_KEYS.has(prop)),
|
|
1472
|
+
);
|
|
1724
1473
|
|
|
1725
|
-
return
|
|
1474
|
+
return renderForm({ ...schema, properties }, def as Record<string, unknown>, {
|
|
1475
|
+
context: {
|
|
1476
|
+
fieldKeyPrefix: name,
|
|
1477
|
+
params: dynamicRouteParams(S.documentPath),
|
|
1478
|
+
resolvePointer: resolveSignalsContextPointer,
|
|
1479
|
+
},
|
|
1480
|
+
onChange: (patch) => transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, patch)),
|
|
1481
|
+
...(ctx && { rerender: () => ctx.renderLeftPanel() }),
|
|
1482
|
+
});
|
|
1726
1483
|
}
|
|
1727
1484
|
|
|
1728
1485
|
/**
|