@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.
Files changed (38) hide show
  1. package/dist/studio.js +77654 -76269
  2. package/dist/studio.js.map +128 -119
  3. package/package.json +44 -44
  4. package/src/account-status.ts +39 -0
  5. package/src/browse/browse.ts +10 -14
  6. package/src/canvas/iframe-host.ts +1 -1
  7. package/src/editor/context-menu.ts +1 -1
  8. package/src/editor/repeater-scope.ts +8 -13
  9. package/src/files/files.ts +3 -0
  10. package/src/format/format-host.ts +63 -1
  11. package/src/new-project/add-repo-modal.ts +183 -0
  12. package/src/new-project/new-project-modal.ts +22 -3
  13. package/src/page-params.ts +34 -8
  14. package/src/panels/ai-chat/chat-markdown.ts +2 -2
  15. package/src/panels/ai-panel.ts +61 -4
  16. package/src/panels/data-grid.ts +619 -0
  17. package/src/panels/signals-panel.ts +102 -437
  18. package/src/panels/statusbar.ts +1 -1
  19. package/src/panels/welcome-screen.ts +50 -0
  20. package/src/platform-errors.ts +30 -0
  21. package/src/platforms/cloud.ts +172 -89
  22. package/src/platforms/devserver.ts +172 -0
  23. package/src/services/context-resolver.ts +73 -0
  24. package/src/services/data-service.ts +155 -0
  25. package/src/services/monaco-setup.ts +75 -26
  26. package/src/settings/contributed-section.ts +406 -0
  27. package/src/settings/extension-sections.ts +145 -0
  28. package/src/settings/schema-field-ui.ts +4 -2
  29. package/src/settings/settings-modal.ts +101 -42
  30. package/src/site-context.ts +10 -1
  31. package/src/studio.ts +24 -0
  32. package/src/tabs/transact.ts +1 -1
  33. package/src/types.ts +120 -1
  34. package/src/ui/form-controls.ts +322 -0
  35. package/src/ui/progress-modal.ts +2 -2
  36. package/src/ui/schema-form.ts +524 -0
  37. package/src/utils/studio-utils.ts +4 -3
  38. package/src/settings/content-types-editor.ts +0 -599
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Context resolver — generic `#/$context/<pointer>` resolution over the project config
3
+ * (specs/extensions.md §9.1).
4
+ *
5
+ * Pointers walk the project config segment by segment. `{@param}` segments substitute values from a
6
+ * caller-provided scope (e.g. the form's current value record), and the `$formats` virtual root
7
+ * resolves to the registered format names instead of a project.json key.
8
+ */
9
+
10
+ /** Options for {@link resolveContextPointer}. */
11
+ export interface ResolveContextOptions {
12
+ /** The project configuration object the pointer walks over. */
13
+ projectConfig: Record<string, unknown>;
14
+ /** Scope record supplying `{@param}` substitutions (e.g. the enclosing form value). */
15
+ scope?: Record<string, unknown> | undefined;
16
+ /** Registered formats backing the `$formats` virtual root. */
17
+ formats?: { name: string }[] | undefined;
18
+ }
19
+
20
+ /** Matches a `{@param}` pointer segment, capturing the param name. */
21
+ const PARAM_SEGMENT = /^\{@(\w+)\}$/;
22
+
23
+ /**
24
+ * Resolve a `#/$context/<seg>/<seg>…` pointer.
25
+ *
26
+ * - `{@param}` segments substitute from `opts.scope`; an unresolvable param yields `undefined`.
27
+ * - The `$formats` virtual root returns the format names from `opts.formats`.
28
+ * - Every other pointer is a plain JSON-pointer walk over `opts.projectConfig` — no key is
29
+ * special-cased, so both legacy (`contentTypes`) and newer (`content`) sections resolve alike.
30
+ *
31
+ * @param {string} pointer
32
+ * @param {ResolveContextOptions} opts
33
+ * @returns {unknown} The resolved value, or `undefined` when the pointer does not resolve.
34
+ */
35
+ export function resolveContextPointer(pointer: string, opts: ResolveContextOptions): unknown {
36
+ if (!pointer.startsWith("#/$context/")) {
37
+ return undefined;
38
+ }
39
+
40
+ const segments: string[] = [];
41
+ for (const raw of pointer.slice("#/$context/".length).split("/")) {
42
+ const match = raw.match(PARAM_SEGMENT);
43
+ if (match) {
44
+ const value = opts.scope?.[match[1]!];
45
+ if (typeof value !== "string" && typeof value !== "number") {
46
+ return undefined;
47
+ }
48
+ const substituted = String(value);
49
+ if (!substituted) {
50
+ return undefined;
51
+ }
52
+ segments.push(substituted);
53
+ } else {
54
+ segments.push(raw);
55
+ }
56
+ }
57
+
58
+ if (segments[0] === "$formats") {
59
+ if (segments.length !== 1) {
60
+ return undefined;
61
+ }
62
+ return (opts.formats ?? []).map((format) => format.name);
63
+ }
64
+
65
+ let node: unknown = opts.projectConfig;
66
+ for (const segment of segments) {
67
+ if (node === null || typeof node !== "object") {
68
+ return undefined;
69
+ }
70
+ node = (node as Record<string, unknown>)[segment];
71
+ }
72
+ return node;
73
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Data-service — thin PAL wrapper over the optional data-surface members
3
+ * (dataConnections/dataConnectionTest/dataPush/dataRows/row CRUD/listSecrets/setSecrets).
4
+ *
5
+ * Every helper degrades cleanly when the platform omits the member family (older backends, cloud
6
+ * without DB reach): list-shaped calls resolve null/empty, action-shaped calls resolve an error
7
+ * result instead of throwing. Secret VALUES only ever flow INTO setSecrets — reads are names-only
8
+ * (specs/extensions.md §13).
9
+ */
10
+
11
+ import { getPlatform } from "../platform";
12
+ import type {
13
+ DataConnectionsResponse,
14
+ DataConnectionTestResult,
15
+ DataPushResult,
16
+ DataRowDelete,
17
+ DataRowInsert,
18
+ DataRowsQuery,
19
+ DataRowsResult,
20
+ DataRowUpdate,
21
+ SecretsSetRequest,
22
+ SecretsSetResponse,
23
+ StudioPlatform,
24
+ } from "../types";
25
+
26
+ /** The registered platform, or null before registration (early boot, bare tests). */
27
+ function platformOrNull(): StudioPlatform | null {
28
+ try {
29
+ return getPlatform();
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** True when the platform serves the data grid (the family's gating member is dataRows). */
36
+ export function dataSurfaceAvailable(): boolean {
37
+ return typeof platformOrNull()?.dataRows === "function";
38
+ }
39
+
40
+ /** True when the platform can store secret values (backs the "secret" form control). */
41
+ export function secretsAvailable(): boolean {
42
+ return typeof platformOrNull()?.setSecrets === "function";
43
+ }
44
+
45
+ /** Connections with configured state and table names; null when the backend lacks the route. */
46
+ export async function fetchConnections(): Promise<DataConnectionsResponse | null> {
47
+ const platform = platformOrNull();
48
+ if (typeof platform?.dataConnections !== "function") {
49
+ return null;
50
+ }
51
+ return platform.dataConnections();
52
+ }
53
+
54
+ /** Probe one connection; degrades to an error result instead of throwing. */
55
+ export async function testConnection(connection: string): Promise<DataConnectionTestResult> {
56
+ const platform = platformOrNull();
57
+ if (typeof platform?.dataConnectionTest !== "function") {
58
+ return { error: "Connection tests are not supported by this backend", ok: false };
59
+ }
60
+ try {
61
+ return await platform.dataConnectionTest(connection);
62
+ } catch (error) {
63
+ return { error: error instanceof Error ? error.message : String(error), ok: false };
64
+ }
65
+ }
66
+
67
+ /** Push table schemas (dryRun compiles the plan only); degrades to an error result. */
68
+ export async function pushSchema(opts?: {
69
+ connection?: string;
70
+ dryRun?: boolean;
71
+ }): Promise<DataPushResult> {
72
+ const platform = platformOrNull();
73
+ if (typeof platform?.dataPush !== "function") {
74
+ return { applied: false, errors: ["Schema push is not supported by this backend"], plan: [] };
75
+ }
76
+ try {
77
+ return await platform.dataPush(opts);
78
+ } catch (error) {
79
+ return {
80
+ applied: false,
81
+ errors: [error instanceof Error ? error.message : String(error)],
82
+ plan: [],
83
+ };
84
+ }
85
+ }
86
+
87
+ /** A required data member, throwing a uniform degradation error when absent. */
88
+ function requireMember<K extends keyof StudioPlatform>(name: K): NonNullable<StudioPlatform[K]> {
89
+ const platform = platformOrNull();
90
+ const member = platform?.[name];
91
+ if (member === undefined || member === null) {
92
+ throw new Error(`The ${String(name)} surface is not supported by this backend`);
93
+ }
94
+ return member as NonNullable<StudioPlatform[K]>;
95
+ }
96
+
97
+ /** Page a table's rows (grid callers handle errors per load). */
98
+ export function fetchRows(query: DataRowsQuery): Promise<DataRowsResult> {
99
+ return requireMember("dataRows")(query);
100
+ }
101
+
102
+ export function insertRow(req: DataRowInsert): Promise<{ row: Record<string, unknown> }> {
103
+ return requireMember("dataInsertRow")(req);
104
+ }
105
+
106
+ export function updateRow(req: DataRowUpdate): Promise<{ row: Record<string, unknown> }> {
107
+ return requireMember("dataUpdateRow")(req);
108
+ }
109
+
110
+ export function deleteRow(req: DataRowDelete): Promise<{ ok: boolean }> {
111
+ return requireMember("dataDeleteRow")(req);
112
+ }
113
+
114
+ /** Configured secret env-var NAMES; null when the backend has no secrets surface. */
115
+ export async function listSecretNames(): Promise<string[] | null> {
116
+ const platform = platformOrNull();
117
+ if (typeof platform?.listSecrets !== "function") {
118
+ return null;
119
+ }
120
+ return platform.listSecrets();
121
+ }
122
+
123
+ /** Write/remove secrets through the platform store (values flow in, names come back). */
124
+ export function saveSecrets(req: SecretsSetRequest): Promise<SecretsSetResponse> {
125
+ return requireMember("setSecrets")(req);
126
+ }
127
+
128
+ /**
129
+ * Derive the env-var NAME a secret-marked field stores its value under: CONSTANT_CASE of the entry
130
+ * key (falling back to the section key) plus the field key minus its `Env` suffix — e.g. connection
131
+ * "main" field "urlEnv" → "MAIN_URL"; section "auth" field "secretEnv" → "AUTH_SECRET". The NAME
132
+ * goes into project.json; the VALUE goes to setSecrets.
133
+ *
134
+ * @param {string} sectionKey
135
+ * @param {string | null} entryKey - The map-layout entry (connection) name, when any
136
+ * @param {string} fieldKey
137
+ * @returns {string}
138
+ */
139
+ export function deriveSecretEnvName(
140
+ sectionKey: string,
141
+ entryKey: string | null,
142
+ fieldKey: string,
143
+ ): string {
144
+ const constant = (raw: string) =>
145
+ raw
146
+ .replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2")
147
+ .replaceAll(/[^A-Za-z0-9]+/g, "_")
148
+ .replaceAll(/^_+|_+$/g, "")
149
+ .toUpperCase();
150
+ const parts = [entryKey ?? sectionKey, fieldKey.replace(/Env$/, "")]
151
+ .map((part) => constant(part))
152
+ .filter(Boolean);
153
+ const name = parts.join("_") || "SECRET";
154
+ return /^\d/.test(name) ? `JX_${name}` : name;
155
+ }
@@ -35,29 +35,78 @@ if (typeof document !== "undefined" && document.fonts?.ready) {
35
35
  });
36
36
  }
37
37
 
38
- // oxlint-disable-next-line typescript/no-unsafe-call, typescript/no-unsafe-member-access -- jsonDefaults is imported from Monaco's untyped ESM contribution (see @ts-expect-error above); no type declarations exist for this named export
39
- jsonDefaults.setDiagnosticsOptions({
40
- allowComments: false,
41
- schemas: [
42
- {
43
- fileMatch: [
44
- "pages/*.json",
45
- "pages/**/*.json",
46
- "layouts/*.json",
47
- "layouts/**/*.json",
48
- "components/*.json",
49
- "components/**/*.json",
50
- "elements/*.json",
51
- "elements/**/*.json",
52
- ],
53
- schema: jxSchema,
54
- uri: "https://jxsuite.com/schema/v1",
55
- },
56
- {
57
- fileMatch: ["project.json"],
58
- schema: projectSchema,
59
- uri: "https://jxsuite.com/schema/project/v1",
60
- },
61
- ],
62
- validate: true,
63
- });
38
+ /** Folder globs whose JSON files validate against the (per-project or core) document schema. */
39
+ const DOCUMENT_FILE_MATCH = [
40
+ "pages/*.json",
41
+ "pages/**/*.json",
42
+ "layouts/*.json",
43
+ "layouts/**/*.json",
44
+ "components/*.json",
45
+ "components/**/*.json",
46
+ "elements/*.json",
47
+ "elements/**/*.json",
48
+ ];
49
+
50
+ /**
51
+ * Register the JSON diagnostics schemas. Fetched per-project schemas are inline OBJECTS (never URLs
52
+ * — `enableSchemaRequest` stays off), so backends must serve them PRE-BUNDLED; the bundled core
53
+ * schemas remain the offline fallback.
54
+ */
55
+ function applyJsonSchemas(documentSchema: unknown, projectJsonSchema: unknown) {
56
+ // oxlint-disable-next-line typescript/no-unsafe-call, typescript/no-unsafe-member-access -- jsonDefaults is imported from Monaco's untyped ESM contribution (see @ts-expect-error above); no type declarations exist for this named export
57
+ jsonDefaults.setDiagnosticsOptions({
58
+ allowComments: false,
59
+ schemas: [
60
+ {
61
+ fileMatch: DOCUMENT_FILE_MATCH,
62
+ schema: documentSchema,
63
+ uri: "https://jxsuite.com/schema/v1",
64
+ },
65
+ {
66
+ fileMatch: ["project.json"],
67
+ schema: projectJsonSchema,
68
+ uri: "https://jxsuite.com/schema/project/v1",
69
+ },
70
+ ],
71
+ validate: true,
72
+ });
73
+ }
74
+
75
+ // Bundled core schemas register at module init — the offline fallback every platform starts from.
76
+ applyJsonSchemas(jxSchema, projectSchema);
77
+
78
+ /**
79
+ * Swap Monaco's JSON schemas to the ACTIVE project's generated entry documents, fetched pre-bundled
80
+ * through the platform's optional `fetchProjectSchemas` member (dev server:
81
+ * `/__studio/project-schemas`; desktop: RPC into the project session). Call on project activation
82
+ * and after project.json `extensions` changes.
83
+ *
84
+ * @param {object} platform - The studio platform (only `fetchProjectSchemas` is consulted)
85
+ * @returns {Promise<boolean>} True when per-project schemas were applied; false on fallback
86
+ */
87
+ export async function refreshProjectSchemas(platform: {
88
+ fetchProjectSchemas?: () => Promise<{ project?: unknown; document?: unknown }>;
89
+ }): Promise<boolean> {
90
+ const fetcher = platform.fetchProjectSchemas;
91
+ if (!fetcher) {
92
+ return false;
93
+ }
94
+ let schemas: { project?: unknown; document?: unknown };
95
+ try {
96
+ schemas = (await fetcher.call(platform)) ?? {};
97
+ } catch {
98
+ // Editor degradation: keep the bundled core schemas.
99
+ return false;
100
+ }
101
+ const { document, project } = schemas;
102
+ if (!document && !project) {
103
+ return false;
104
+ }
105
+ applyJsonSchemas(document ?? jxSchema, project ?? projectSchema);
106
+ return true;
107
+ }
108
+
109
+ /** Restore the bundled core schemas (project closed / tests). */
110
+ export function resetProjectSchemas(): void {
111
+ applyJsonSchemas(jxSchema, projectSchema);
112
+ }