@jxsuite/studio 0.37.1 → 1.0.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/studio.js +77654 -76269
- package/dist/studio.js.map +128 -119
- package/package.json +44 -44
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- package/src/canvas/iframe-host.ts +1 -1
- package/src/editor/context-menu.ts +1 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/files/files.ts +3 -0
- package/src/format/format-host.ts +63 -1
- 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/data-grid.ts +619 -0
- package/src/panels/signals-panel.ts +102 -437
- package/src/panels/statusbar.ts +1 -1
- 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/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -0
- package/src/services/monaco-setup.ts +75 -26
- 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 +10 -1
- package/src/studio.ts +24 -0
- package/src/tabs/transact.ts +1 -1
- package/src/types.ts +120 -1
- package/src/ui/form-controls.ts +322 -0
- package/src/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/studio-utils.ts +4 -3
- package/src/settings/content-types-editor.ts +0 -599
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Built-in schema-form controls (specs/extensions.md §9.1): "schema-builder" (visual JSON-Schema
|
|
4
|
+
* field editor wrapping settings/schema-field-ui) and "secret" (value committed via the host's
|
|
5
|
+
* secret store, never project.json). The third built-in, "binding", registers from
|
|
6
|
+
* panels/signals-panel.ts because it owns panel-local ephemeral UI state (custom-ref mode) and the
|
|
7
|
+
* route-param picker semantics.
|
|
8
|
+
*
|
|
9
|
+
* Imported once for side effects from studio startup.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { html } from "lit-html";
|
|
13
|
+
import { repeat } from "lit-html/directives/repeat.js";
|
|
14
|
+
import { registerFormControl } from "./schema-form";
|
|
15
|
+
import {
|
|
16
|
+
addFieldFormTpl,
|
|
17
|
+
detectFieldFormat,
|
|
18
|
+
fieldCardTpl,
|
|
19
|
+
schemaForType,
|
|
20
|
+
} from "../settings/schema-field-ui";
|
|
21
|
+
import { toCamelCase } from "../utils/studio-utils";
|
|
22
|
+
|
|
23
|
+
import type { FieldHandlers, SchemaProperty } from "../settings/schema-field-ui";
|
|
24
|
+
import type { SchemaFormControlArgs } from "./schema-form";
|
|
25
|
+
|
|
26
|
+
// ─── Schema-builder control ──────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** The JSON-Schema object shape the schema-builder edits. */
|
|
29
|
+
interface BuilderSchema {
|
|
30
|
+
type?: string;
|
|
31
|
+
properties?: Record<string, SchemaProperty>;
|
|
32
|
+
required?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AddFieldState {
|
|
36
|
+
format: string;
|
|
37
|
+
name: string;
|
|
38
|
+
required: boolean;
|
|
39
|
+
type: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const blankAddFieldState = (): AddFieldState => ({
|
|
43
|
+
format: "",
|
|
44
|
+
name: "",
|
|
45
|
+
required: false,
|
|
46
|
+
type: "string",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Per-field add-field form state, keyed by `${fieldKeyPrefix}.${key}`. */
|
|
50
|
+
const addFieldOpen = new Set<string>();
|
|
51
|
+
const addFieldStates = new Map<string, AddFieldState>();
|
|
52
|
+
|
|
53
|
+
/** Reset schema-builder ephemeral UI state (test hook). */
|
|
54
|
+
export function resetFormControlUiState(): void {
|
|
55
|
+
addFieldOpen.clear();
|
|
56
|
+
addFieldStates.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Working clone of the schema value — JSON round-trip, as values may be reactive proxies. */
|
|
60
|
+
function cloneSchema(value: unknown): BuilderSchema {
|
|
61
|
+
const base =
|
|
62
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
63
|
+
? value
|
|
64
|
+
: { properties: {}, required: [], type: "object" };
|
|
65
|
+
// oxlint-disable-next-line unicorn/prefer-structured-clone
|
|
66
|
+
return JSON.parse(JSON.stringify(base)) as BuilderSchema;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Visual JSON-Schema field editor wrapping the shared schema-field-ui templates. */
|
|
70
|
+
function schemaBuilderControl({ key, value, onChange, ctx, rerender }: SchemaFormControlArgs) {
|
|
71
|
+
const schema =
|
|
72
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
73
|
+
? (value as BuilderSchema)
|
|
74
|
+
: ({ properties: {}, required: [], type: "object" } as BuilderSchema);
|
|
75
|
+
const properties = schema.properties ?? {};
|
|
76
|
+
const required = schema.required ?? [];
|
|
77
|
+
const stateKey = `${ctx.fieldKeyPrefix ?? ""}.${key}`;
|
|
78
|
+
|
|
79
|
+
const update = (mutate: (draft: BuilderSchema) => void) => {
|
|
80
|
+
const draft = cloneSchema(value);
|
|
81
|
+
draft.type ??= "object";
|
|
82
|
+
draft.properties ??= {};
|
|
83
|
+
draft.required ??= [];
|
|
84
|
+
mutate(draft);
|
|
85
|
+
onChange(draft);
|
|
86
|
+
rerender?.();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Content types available as reference targets, resolved through the host context
|
|
90
|
+
const contentTypesValue = ctx.resolvePointer("#/$context/content");
|
|
91
|
+
const contentTypeNames =
|
|
92
|
+
contentTypesValue && typeof contentTypesValue === "object"
|
|
93
|
+
? Object.keys(contentTypesValue)
|
|
94
|
+
: [];
|
|
95
|
+
|
|
96
|
+
const handlers: FieldHandlers = {
|
|
97
|
+
onAddNestedField: (parentName, fieldState) =>
|
|
98
|
+
update((draft) => {
|
|
99
|
+
const parent = draft.properties?.[parentName];
|
|
100
|
+
const name = toCamelCase(fieldState.name);
|
|
101
|
+
if (!parent || !name) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
parent.properties ??= {};
|
|
105
|
+
parent.properties[name] = schemaForType(fieldState.type);
|
|
106
|
+
if (fieldState.required) {
|
|
107
|
+
parent.required ??= [];
|
|
108
|
+
if (!parent.required.includes(name)) {
|
|
109
|
+
parent.required.push(name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
onChangeFormat: (name, format) =>
|
|
114
|
+
update((draft) => {
|
|
115
|
+
const prop = draft.properties?.[name];
|
|
116
|
+
if (!prop) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
draft.properties![name] = schemaForType(prop.type || "string", format || undefined);
|
|
120
|
+
}),
|
|
121
|
+
onChangeNestedFormat: (parentName, childName, format) =>
|
|
122
|
+
update((draft) => {
|
|
123
|
+
const parent = draft.properties?.[parentName];
|
|
124
|
+
const prop = parent?.properties?.[childName];
|
|
125
|
+
if (!prop) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
parent!.properties![childName] = schemaForType(prop.type || "string", format || undefined);
|
|
129
|
+
}),
|
|
130
|
+
onChangeNestedType: (parentName, childName, newType) =>
|
|
131
|
+
update((draft) => {
|
|
132
|
+
const parent = draft.properties?.[parentName];
|
|
133
|
+
const prop = parent?.properties?.[childName];
|
|
134
|
+
if (!prop) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const oldFormat =
|
|
138
|
+
newType === "string" || newType === "array" ? detectFieldFormat(prop) : undefined;
|
|
139
|
+
parent!.properties![childName] = schemaForType(newType, oldFormat || undefined);
|
|
140
|
+
}),
|
|
141
|
+
onChangeRefTarget: (name, target) =>
|
|
142
|
+
update((draft) => {
|
|
143
|
+
if (!draft.properties?.[name]) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
draft.properties[name] = { $ref: `#/content/${target}` };
|
|
147
|
+
}),
|
|
148
|
+
onChangeType: (name, newType) =>
|
|
149
|
+
update((draft) => {
|
|
150
|
+
const prop = draft.properties?.[name];
|
|
151
|
+
if (!prop) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const oldFormat =
|
|
155
|
+
newType === "string" || newType === "array" ? detectFieldFormat(prop) : undefined;
|
|
156
|
+
draft.properties![name] = schemaForType(newType, oldFormat || undefined);
|
|
157
|
+
}),
|
|
158
|
+
onDelete: (name) =>
|
|
159
|
+
update((draft) => {
|
|
160
|
+
delete draft.properties?.[name];
|
|
161
|
+
draft.required = (draft.required ?? []).filter((r) => r !== name);
|
|
162
|
+
}),
|
|
163
|
+
onDeleteNested: (parentName, childName) =>
|
|
164
|
+
update((draft) => {
|
|
165
|
+
const parent = draft.properties?.[parentName];
|
|
166
|
+
if (!parent?.properties) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
delete parent.properties[childName];
|
|
170
|
+
if (parent.required) {
|
|
171
|
+
parent.required = parent.required.filter((r) => r !== childName);
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
onRename: (oldName, newName) =>
|
|
175
|
+
update((draft) => {
|
|
176
|
+
const normalized = toCamelCase(newName);
|
|
177
|
+
if (!draft.properties || !normalized || draft.properties[normalized]) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const next: Record<string, SchemaProperty> = {};
|
|
181
|
+
for (const [k, v] of Object.entries(draft.properties)) {
|
|
182
|
+
next[k === oldName ? normalized : k] = v;
|
|
183
|
+
}
|
|
184
|
+
draft.properties = next;
|
|
185
|
+
draft.required = (draft.required ?? []).map((r) => (r === oldName ? normalized : r));
|
|
186
|
+
}),
|
|
187
|
+
onRenameNested: (parentName, oldChild, newChild) =>
|
|
188
|
+
update((draft) => {
|
|
189
|
+
const parent = draft.properties?.[parentName];
|
|
190
|
+
const normalized = toCamelCase(newChild);
|
|
191
|
+
if (!parent?.properties || !normalized || parent.properties[normalized]) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const next: Record<string, SchemaProperty> = {};
|
|
195
|
+
for (const [k, v] of Object.entries(parent.properties)) {
|
|
196
|
+
next[k === oldChild ? normalized : k] = v;
|
|
197
|
+
}
|
|
198
|
+
parent.properties = next;
|
|
199
|
+
if (parent.required) {
|
|
200
|
+
parent.required = parent.required.map((r) => (r === oldChild ? normalized : r));
|
|
201
|
+
}
|
|
202
|
+
}),
|
|
203
|
+
onToggleNestedRequired: (parentName, childName) =>
|
|
204
|
+
update((draft) => {
|
|
205
|
+
const parent = draft.properties?.[parentName];
|
|
206
|
+
if (!parent) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
parent.required ??= [];
|
|
210
|
+
const idx = parent.required.indexOf(childName);
|
|
211
|
+
if (idx === -1) {
|
|
212
|
+
parent.required.push(childName);
|
|
213
|
+
} else {
|
|
214
|
+
parent.required.splice(idx, 1);
|
|
215
|
+
}
|
|
216
|
+
}),
|
|
217
|
+
onToggleRequired: (name) =>
|
|
218
|
+
update((draft) => {
|
|
219
|
+
draft.required ??= [];
|
|
220
|
+
const idx = draft.required.indexOf(name);
|
|
221
|
+
if (idx === -1) {
|
|
222
|
+
draft.required.push(name);
|
|
223
|
+
} else {
|
|
224
|
+
draft.required.splice(idx, 1);
|
|
225
|
+
}
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const fieldCards = repeat(
|
|
230
|
+
Object.entries(properties),
|
|
231
|
+
([name]) => name,
|
|
232
|
+
([name, def]) => fieldCardTpl(name, def, required.includes(name), handlers, contentTypeNames),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const addFieldState = addFieldStates.get(stateKey) ?? blankAddFieldState();
|
|
236
|
+
|
|
237
|
+
return html`
|
|
238
|
+
<div class="schema-builder">
|
|
239
|
+
<div class="schema-field-list">${fieldCards}</div>
|
|
240
|
+
${addFieldOpen.has(stateKey)
|
|
241
|
+
? addFieldFormTpl(addFieldState, {
|
|
242
|
+
onCancel: () => {
|
|
243
|
+
addFieldOpen.delete(stateKey);
|
|
244
|
+
addFieldStates.delete(stateKey);
|
|
245
|
+
rerender?.();
|
|
246
|
+
},
|
|
247
|
+
onConfirm: () => {
|
|
248
|
+
const raw = addFieldState.name.trim();
|
|
249
|
+
const name = toCamelCase(raw);
|
|
250
|
+
if (!name) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// Close the form before update() rerenders with the committed value
|
|
254
|
+
addFieldOpen.delete(stateKey);
|
|
255
|
+
addFieldStates.delete(stateKey);
|
|
256
|
+
update((draft) => {
|
|
257
|
+
draft.properties![name] = schemaForType(
|
|
258
|
+
addFieldState.type,
|
|
259
|
+
addFieldState.format || undefined,
|
|
260
|
+
);
|
|
261
|
+
if (addFieldState.required && !draft.required!.includes(name)) {
|
|
262
|
+
draft.required!.push(name);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
onInput: (field, val) => {
|
|
267
|
+
addFieldStates.set(stateKey, { ...addFieldState, [field]: val });
|
|
268
|
+
rerender?.();
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
: html`
|
|
272
|
+
<sp-action-button
|
|
273
|
+
size="s"
|
|
274
|
+
quiet
|
|
275
|
+
@click=${() => {
|
|
276
|
+
addFieldOpen.add(stateKey);
|
|
277
|
+
addFieldStates.set(stateKey, blankAddFieldState());
|
|
278
|
+
rerender?.();
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
<sp-icon-add slot="icon"></sp-icon-add> Add Field
|
|
282
|
+
</sp-action-button>
|
|
283
|
+
`}
|
|
284
|
+
</div>
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
registerFormControl("schema-builder", schemaBuilderControl);
|
|
289
|
+
|
|
290
|
+
// ─── Secret control ──────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
// Secret VALUES commit through the host's secret store (ctx.commitSecret → platform setSecrets),
|
|
293
|
+
// Never project.json; the field itself persists only the derived env-var NAME commitSecret
|
|
294
|
+
// Returns. Hosts without a secrets surface leave the control disabled.
|
|
295
|
+
registerFormControl("secret", ({ key, value, onChange, ctx, rerender }) => {
|
|
296
|
+
const { commitSecret } = ctx;
|
|
297
|
+
const envName = typeof value === "string" && value ? value : null;
|
|
298
|
+
return html`<sp-textfield
|
|
299
|
+
size="s"
|
|
300
|
+
type="password"
|
|
301
|
+
class="secret-field"
|
|
302
|
+
placeholder=${envName ? `Stored as ${envName}` : "Not set"}
|
|
303
|
+
?disabled=${!commitSecret}
|
|
304
|
+
@change=${async (e: Event) => {
|
|
305
|
+
const target = e.target as HTMLInputElement;
|
|
306
|
+
const secretValue = target.value;
|
|
307
|
+
if (!commitSecret || !secretValue) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const name = await commitSecret(key, secretValue);
|
|
311
|
+
target.value = "";
|
|
312
|
+
if (typeof name === "string" && name && name !== envName) {
|
|
313
|
+
onChange(name);
|
|
314
|
+
} else {
|
|
315
|
+
rerender?.();
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
318
|
+
></sp-textfield>`;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Keep an explicit export so hosts can assert the built-ins module loaded
|
|
322
|
+
export const builtinFormControls = ["schema-builder", "secret"] as const;
|
package/src/ui/progress-modal.ts
CHANGED
|
@@ -57,8 +57,8 @@ function errorView(title: string, message: string, onClose: () => void): Templat
|
|
|
57
57
|
<pre
|
|
58
58
|
style="font-size:var(--spectrum-font-size-50, 11px);white-space:pre-wrap;overflow:auto;max-height:240px;margin:0;color:var(--fg-dim,#aaa)"
|
|
59
59
|
>
|
|
60
|
-
${message}
|
|
61
|
-
>
|
|
60
|
+
${message}
|
|
61
|
+
</pre>
|
|
62
62
|
</div>
|
|
63
63
|
<div style="display:flex;justify-content:flex-end">
|
|
64
64
|
<sp-button size="s" @click=${onClose}>Close</sp-button>
|