@jxsuite/studio 0.37.0 → 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,48 +39,48 @@
|
|
|
39
39
|
"@atlaskit/pragmatic-drag-and-drop": "^2.0.1",
|
|
40
40
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.0",
|
|
41
41
|
"@jxsuite/ai": "^0.33.0",
|
|
42
|
-
"@jxsuite/collab": "^0.
|
|
43
|
-
"@jxsuite/create": "^0.
|
|
44
|
-
"@jxsuite/
|
|
45
|
-
"@jxsuite/protocol": "^0.
|
|
46
|
-
"@jxsuite/runtime": "^0.
|
|
47
|
-
"@jxsuite/schema": "^0.
|
|
48
|
-
"@spectrum-web-components/accordion": "^1.12.
|
|
49
|
-
"@spectrum-web-components/action-bar": "1.12.
|
|
50
|
-
"@spectrum-web-components/action-button": "^1.12.
|
|
51
|
-
"@spectrum-web-components/action-group": "^1.12.
|
|
52
|
-
"@spectrum-web-components/base": "^1.12.
|
|
53
|
-
"@spectrum-web-components/button": "^1.12.
|
|
54
|
-
"@spectrum-web-components/checkbox": "^1.12.
|
|
55
|
-
"@spectrum-web-components/color-area": "^1.12.
|
|
56
|
-
"@spectrum-web-components/color-handle": "^1.12.
|
|
57
|
-
"@spectrum-web-components/color-slider": "^1.12.
|
|
58
|
-
"@spectrum-web-components/combobox": "^1.12.
|
|
59
|
-
"@spectrum-web-components/dialog": "^1.12.
|
|
60
|
-
"@spectrum-web-components/divider": "^1.12.
|
|
61
|
-
"@spectrum-web-components/field-label": "^1.12.
|
|
62
|
-
"@spectrum-web-components/help-text": "^1.12.
|
|
63
|
-
"@spectrum-web-components/icon": "^1.12.
|
|
64
|
-
"@spectrum-web-components/icons-ui": "^1.12.
|
|
65
|
-
"@spectrum-web-components/icons-workflow": "^1.12.
|
|
66
|
-
"@spectrum-web-components/menu": "^1.12.
|
|
67
|
-
"@spectrum-web-components/number-field": "^1.12.
|
|
68
|
-
"@spectrum-web-components/overlay": "^1.12.
|
|
42
|
+
"@jxsuite/collab": "^0.3.0",
|
|
43
|
+
"@jxsuite/create": "^1.0.0",
|
|
44
|
+
"@jxsuite/markup": "^0.2.0",
|
|
45
|
+
"@jxsuite/protocol": "^0.4.0",
|
|
46
|
+
"@jxsuite/runtime": "^1.0.0",
|
|
47
|
+
"@jxsuite/schema": "^1.0.0",
|
|
48
|
+
"@spectrum-web-components/accordion": "^1.12.2",
|
|
49
|
+
"@spectrum-web-components/action-bar": "1.12.2",
|
|
50
|
+
"@spectrum-web-components/action-button": "^1.12.2",
|
|
51
|
+
"@spectrum-web-components/action-group": "^1.12.2",
|
|
52
|
+
"@spectrum-web-components/base": "^1.12.2",
|
|
53
|
+
"@spectrum-web-components/button": "^1.12.2",
|
|
54
|
+
"@spectrum-web-components/checkbox": "^1.12.2",
|
|
55
|
+
"@spectrum-web-components/color-area": "^1.12.2",
|
|
56
|
+
"@spectrum-web-components/color-handle": "^1.12.2",
|
|
57
|
+
"@spectrum-web-components/color-slider": "^1.12.2",
|
|
58
|
+
"@spectrum-web-components/combobox": "^1.12.2",
|
|
59
|
+
"@spectrum-web-components/dialog": "^1.12.2",
|
|
60
|
+
"@spectrum-web-components/divider": "^1.12.2",
|
|
61
|
+
"@spectrum-web-components/field-label": "^1.12.2",
|
|
62
|
+
"@spectrum-web-components/help-text": "^1.12.2",
|
|
63
|
+
"@spectrum-web-components/icon": "^1.12.2",
|
|
64
|
+
"@spectrum-web-components/icons-ui": "^1.12.2",
|
|
65
|
+
"@spectrum-web-components/icons-workflow": "^1.12.2",
|
|
66
|
+
"@spectrum-web-components/menu": "^1.12.2",
|
|
67
|
+
"@spectrum-web-components/number-field": "^1.12.2",
|
|
68
|
+
"@spectrum-web-components/overlay": "^1.12.2",
|
|
69
69
|
"@spectrum-web-components/overlay-trigger": "^0.1.7",
|
|
70
|
-
"@spectrum-web-components/picker": "^1.12.
|
|
71
|
-
"@spectrum-web-components/picker-button": "^1.12.
|
|
72
|
-
"@spectrum-web-components/popover": "^1.12.
|
|
73
|
-
"@spectrum-web-components/progress-circle": "^1.12.
|
|
74
|
-
"@spectrum-web-components/search": "^1.12.
|
|
75
|
-
"@spectrum-web-components/swatch": "^1.12.
|
|
76
|
-
"@spectrum-web-components/switch": "^1.12.
|
|
77
|
-
"@spectrum-web-components/table": "^1.12.
|
|
78
|
-
"@spectrum-web-components/tabs": "^1.12.
|
|
79
|
-
"@spectrum-web-components/textfield": "^1.12.
|
|
80
|
-
"@spectrum-web-components/theme": "^1.12.
|
|
81
|
-
"@spectrum-web-components/toast": "^1.12.
|
|
82
|
-
"@spectrum-web-components/tooltip": "^1.12.
|
|
83
|
-
"@spectrum-web-components/underlay": "^1.12.
|
|
70
|
+
"@spectrum-web-components/picker": "^1.12.2",
|
|
71
|
+
"@spectrum-web-components/picker-button": "^1.12.2",
|
|
72
|
+
"@spectrum-web-components/popover": "^1.12.2",
|
|
73
|
+
"@spectrum-web-components/progress-circle": "^1.12.2",
|
|
74
|
+
"@spectrum-web-components/search": "^1.12.2",
|
|
75
|
+
"@spectrum-web-components/swatch": "^1.12.2",
|
|
76
|
+
"@spectrum-web-components/switch": "^1.12.2",
|
|
77
|
+
"@spectrum-web-components/table": "^1.12.2",
|
|
78
|
+
"@spectrum-web-components/tabs": "^1.12.2",
|
|
79
|
+
"@spectrum-web-components/textfield": "^1.12.2",
|
|
80
|
+
"@spectrum-web-components/theme": "^1.12.2",
|
|
81
|
+
"@spectrum-web-components/toast": "^1.12.2",
|
|
82
|
+
"@spectrum-web-components/tooltip": "^1.12.2",
|
|
83
|
+
"@spectrum-web-components/underlay": "^1.12.2",
|
|
84
84
|
"@vue/reactivity": "3.5.39",
|
|
85
85
|
"ajv": "^8.20.0",
|
|
86
86
|
"ajv-formats": "^3.0.1",
|
|
@@ -93,9 +93,9 @@
|
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
95
95
|
"@types/mdast": "^4.0.4",
|
|
96
|
-
"@webref/css": "^8.
|
|
96
|
+
"@webref/css": "^8.7.0",
|
|
97
97
|
"@webref/elements": "^2.7.1",
|
|
98
|
-
"@webref/idl": "^3.81.
|
|
98
|
+
"@webref/idl": "^3.81.3"
|
|
99
99
|
},
|
|
100
100
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
101
101
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account onboarding status — a synchronous render cache over the optional
|
|
3
|
+
* `platform.getAccountStatus` PAL member (cloud GitHub-App installations). Hydrated once at boot
|
|
4
|
+
* (studio.ts) like the project-list cache; the welcome screen reads it synchronously to prompt
|
|
5
|
+
* "install the GitHub App" when the user has no repository access yet.
|
|
6
|
+
*/
|
|
7
|
+
import { getPlatform, hasPlatform } from "./platform";
|
|
8
|
+
import type { AccountStatus } from "./types";
|
|
9
|
+
|
|
10
|
+
let cache: AccountStatus | null = null;
|
|
11
|
+
|
|
12
|
+
/** Refresh the cache from the platform; null when unsupported, failing, or unknown. */
|
|
13
|
+
export async function hydrateAccountStatus(): Promise<void> {
|
|
14
|
+
if (!hasPlatform() || typeof getPlatform().getAccountStatus !== "function") {
|
|
15
|
+
cache = null;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
cache = (await getPlatform().getAccountStatus?.()) ?? null;
|
|
20
|
+
} catch {
|
|
21
|
+
// Onboarding prompts are progressive enhancement; unknown status never nags.
|
|
22
|
+
cache = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Synchronous snapshot for render paths (never awaits); null = unknown. */
|
|
27
|
+
export function getAccountStatus(): AccountStatus | null {
|
|
28
|
+
return cache;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the account verifiably has no repository access yet and we know where to fix it. */
|
|
32
|
+
export function needsAppInstall(): boolean {
|
|
33
|
+
return cache !== null && cache.installations.length === 0 && Boolean(cache.appInstallUrl);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Reset seam for tests. */
|
|
37
|
+
export function resetAccountStatus(): void {
|
|
38
|
+
cache = null;
|
|
39
|
+
}
|
package/src/browse/browse.ts
CHANGED
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
} from "../format/format-host";
|
|
35
35
|
|
|
36
36
|
import type { ComponentEntry } from "../files/components";
|
|
37
|
-
import type {
|
|
37
|
+
import type { ContentSectionEntry } from "../types";
|
|
38
|
+
import type { JxDocument } from "@jxsuite/schema/types";
|
|
38
39
|
|
|
39
40
|
// ─── Category definitions ────────────────────────────────────────────────────
|
|
40
41
|
|
|
@@ -184,19 +185,16 @@ async function collectFiles(
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
/**
|
|
187
|
-
* Match a file path against project
|
|
188
|
-
* Returns the content type name (capitalized) or null if no match.
|
|
188
|
+
* Match a file path against the project `content` section's source directories to find its content
|
|
189
|
+
* type name. Returns the content type name (capitalized) or null if no match.
|
|
189
190
|
*
|
|
190
191
|
* @param {string} filePath
|
|
191
192
|
* @returns {string | null}
|
|
192
193
|
*/
|
|
193
194
|
function contentTypeFor(filePath: string) {
|
|
194
195
|
const config = projectState?.projectConfig;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
199
|
-
const d = def as ContentTypeDef;
|
|
196
|
+
const content = (config?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
197
|
+
for (const [name, d] of Object.entries(content)) {
|
|
200
198
|
if (!d.source) {
|
|
201
199
|
continue;
|
|
202
200
|
}
|
|
@@ -264,7 +262,8 @@ function getEntityTypes() {
|
|
|
264
262
|
*/
|
|
265
263
|
function buildFrontmatterYaml(contentTypeName: string) {
|
|
266
264
|
const config = projectState?.projectConfig;
|
|
267
|
-
const
|
|
265
|
+
const content = (config?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
266
|
+
const col = content[contentTypeName];
|
|
268
267
|
if (!col?.schema?.properties) {
|
|
269
268
|
return "title: Untitled\n";
|
|
270
269
|
}
|
|
@@ -284,11 +283,8 @@ function buildFrontmatterYaml(contentTypeName: string) {
|
|
|
284
283
|
*/
|
|
285
284
|
function getContentTypeTypes() {
|
|
286
285
|
const config = projectState?.projectConfig;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
291
|
-
const d = def as ContentTypeDef;
|
|
286
|
+
const content = (config?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
287
|
+
return Object.entries(content).map(([name, d]) => {
|
|
292
288
|
const dir = d.source ? d.source.replace(/^\.\//, "").replace(/\/$/, "") : name;
|
|
293
289
|
return {
|
|
294
290
|
contentTypeName: name,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { html } from "lit-html";
|
|
4
4
|
import { jsonClone } from "../utils/studio-utils";
|
|
5
5
|
import { ref } from "lit-html/directives/ref.js";
|
|
6
|
-
import { htmlToJx } from "@jxsuite/
|
|
6
|
+
import { htmlToJx } from "@jxsuite/markup/html-to-jx";
|
|
7
7
|
import { childIndex, getNodeAtPath, parentElementPath } from "../store";
|
|
8
8
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
9
9
|
import {
|
|
@@ -16,12 +16,8 @@ import { isRef } from "@jxsuite/schema/guards";
|
|
|
16
16
|
import { getNodeAtPath } from "../state";
|
|
17
17
|
|
|
18
18
|
import type { JxPath } from "../state";
|
|
19
|
-
import type {
|
|
20
|
-
|
|
21
|
-
JxMutableNode,
|
|
22
|
-
ProjectConfig,
|
|
23
|
-
ContentTypeSchema,
|
|
24
|
-
} from "@jxsuite/schema/types";
|
|
19
|
+
import type { ContentSectionEntry } from "../types";
|
|
20
|
+
import type { JxMappedArray, JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
|
|
25
21
|
|
|
26
22
|
/** Cap on how many inferred/declared fields we enumerate (mirrors merge-tags BREADTH_CAP breadth). */
|
|
27
23
|
const FIELD_CAP = 50;
|
|
@@ -73,10 +69,9 @@ function safeFields(names: string[]): string[] {
|
|
|
73
69
|
* `index`; appends field tokens resolved from the repeater's binding:
|
|
74
70
|
*
|
|
75
71
|
* - **Content collection** — `items` refs a `{ contentType }` state def. Item shape is `{ id, data: {
|
|
76
|
-
* …fields }, body }`, so fields come from
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* / `item.body`.
|
|
72
|
+
* …fields }, body }`, so fields come from `projectConfig.content[contentType].schema.properties`
|
|
73
|
+
* and emit as `item.data.<f>` (the `.data.` nesting is LOAD-BEARING — runtime templates use
|
|
74
|
+
* `${item.data.title}`), plus `item.id` / `item.body`.
|
|
80
75
|
* - **State array** — `items` refs a state def that is an array of objects. Fields come from a
|
|
81
76
|
* declared `items.properties`, else are inferred from a sample `default[0]`. Emitted FLAT
|
|
82
77
|
* (`item.<f>`, no `.data.`).
|
|
@@ -104,9 +99,9 @@ export function resolveRepeaterItemFields(
|
|
|
104
99
|
// (a) Content collection — `{ contentType: "<name>" }`. Import-aliased `$prototype` still
|
|
105
100
|
// Matches because we detect on the `contentType` string, not the prototype name.
|
|
106
101
|
if (typeof def.contentType === "string") {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const props = schema?.properties;
|
|
102
|
+
const content = (projectConfig?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
103
|
+
const ct = content[def.contentType];
|
|
104
|
+
const props = ct?.schema?.properties;
|
|
110
105
|
const out = [...base, "item.id", "item.body"];
|
|
111
106
|
if (props && typeof props === "object") {
|
|
112
107
|
for (const f of safeFields(Object.keys(props))) {
|
package/src/files/files.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
formatForPath,
|
|
35
35
|
loadFormats,
|
|
36
36
|
noFormatError,
|
|
37
|
+
refreshExtensionUi,
|
|
37
38
|
refreshFormats,
|
|
38
39
|
} from "../format/format-host";
|
|
39
40
|
import { view } from "../view";
|
|
@@ -83,6 +84,7 @@ export async function loadProject() {
|
|
|
83
84
|
|
|
84
85
|
refreshFormats();
|
|
85
86
|
void loadFormats();
|
|
87
|
+
refreshExtensionUi(platform);
|
|
86
88
|
|
|
87
89
|
setProjectState({
|
|
88
90
|
dirs: new Map(),
|
|
@@ -145,6 +147,7 @@ export async function openProject({
|
|
|
145
147
|
|
|
146
148
|
refreshFormats();
|
|
147
149
|
void loadFormats();
|
|
150
|
+
refreshExtensionUi(platform);
|
|
148
151
|
|
|
149
152
|
setProjectState({
|
|
150
153
|
.../** @type {ProjectState} */ projectState,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getPlatform } from "../platform";
|
|
11
|
+
import type { ExtensionsInfo } from "../types";
|
|
11
12
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
12
13
|
|
|
13
14
|
export interface StudioFormatHints {
|
|
@@ -68,10 +69,12 @@ export function loadFormats(): Promise<StudioFormat[]> {
|
|
|
68
69
|
return _loaded;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/** Invalidate the cached registry (call on project switch). */
|
|
72
|
+
/** Invalidate the cached registry and extensions payload (call on project switch). */
|
|
72
73
|
export function refreshFormats() {
|
|
73
74
|
_loaded = null;
|
|
74
75
|
_formats = [];
|
|
76
|
+
_extensionsLoaded = null;
|
|
77
|
+
_extensions = [];
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/** Seed the registry directly (tests and hosts that preload format metadata). */
|
|
@@ -80,6 +83,65 @@ export function setFormats(formats: StudioFormat[]) {
|
|
|
80
83
|
_loaded = Promise.resolve(formats);
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// ─── Extensions payload (specs/extensions.md §9/§9.1) ────────────────────────
|
|
87
|
+
|
|
88
|
+
let _extensions: ExtensionsInfo[] = [];
|
|
89
|
+
let _extensionsLoaded: Promise<ExtensionsInfo[]> | null = null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load (and cache) the project's enabled extensions with their project-section contributions.
|
|
93
|
+
* Backed by the platform's optional `listExtensions` member; platforms without it (or failures)
|
|
94
|
+
* degrade to an empty list, hiding descriptor-contributed settings sections.
|
|
95
|
+
*/
|
|
96
|
+
export function loadExtensions(): Promise<ExtensionsInfo[]> {
|
|
97
|
+
if (!_extensionsLoaded) {
|
|
98
|
+
_extensionsLoaded = (async () => {
|
|
99
|
+
try {
|
|
100
|
+
const platform = getPlatform() as {
|
|
101
|
+
listExtensions?: () => Promise<ExtensionsInfo[]>;
|
|
102
|
+
};
|
|
103
|
+
_extensions = (await platform.listExtensions?.()) ?? [];
|
|
104
|
+
} catch {
|
|
105
|
+
_extensions = [];
|
|
106
|
+
}
|
|
107
|
+
return _extensions;
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
return _extensionsLoaded;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The last-loaded extensions payload (synchronous access for render paths). */
|
|
114
|
+
export function getExtensions(): ExtensionsInfo[] {
|
|
115
|
+
return _extensions;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Seed the extensions payload directly (tests and hosts that preload it). */
|
|
119
|
+
export function setExtensions(extensions: ExtensionsInfo[]) {
|
|
120
|
+
_extensions = extensions;
|
|
121
|
+
_extensionsLoaded = Promise.resolve(extensions);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fire-and-forget refresh of the extension-facing editor surface after project (re)activation or an
|
|
126
|
+
* `extensions` change: Monaco's per-project schemas and the descriptor-contributed settings
|
|
127
|
+
* sections. Both modules load lazily — monaco is heavy and the settings registry pulls DOM
|
|
128
|
+
* templates — and both degrade silently (bundled core schemas, built-in sections only).
|
|
129
|
+
*
|
|
130
|
+
* @param {object} platform - The studio platform (only `fetchProjectSchemas` is consulted here)
|
|
131
|
+
*/
|
|
132
|
+
export function refreshExtensionUi(platform: {
|
|
133
|
+
fetchProjectSchemas?: () => Promise<{ project?: unknown; document?: unknown }>;
|
|
134
|
+
}): void {
|
|
135
|
+
void import("../services/monaco-setup")
|
|
136
|
+
.then(({ refreshProjectSchemas }) => refreshProjectSchemas(platform))
|
|
137
|
+
.catch(() => false);
|
|
138
|
+
void import("../settings/extension-sections")
|
|
139
|
+
.then(({ syncExtensionSettingsSections }) => syncExtensionSettingsSections())
|
|
140
|
+
.catch(() => {
|
|
141
|
+
// Contributed sections also refresh on the next settings-modal open.
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
83
145
|
/** The last-loaded registry (synchronous access for render paths). */
|
|
84
146
|
export function getFormats(): StudioFormat[] {
|
|
85
147
|
return _formats;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Add Existing Repository modal — a filterable picker over `platform.listRepos` (every repo the
|
|
4
|
+
* platform's account link can reach, personal and organization). Choosing a repo runs
|
|
5
|
+
* `platform.importProject`, which adopts it as a Jx project (probes project.json, tags + catalogues
|
|
6
|
+
* it) and resolves with the catalogue root key; the caller opens it through the same path as a
|
|
7
|
+
* recent project. Repos without a project.json fail with the backend's structured message, shown
|
|
8
|
+
* inline.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { html, nothing } from "lit-html";
|
|
12
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
13
|
+
import { getPlatform } from "../platform";
|
|
14
|
+
import { openModal } from "../ui/layers";
|
|
15
|
+
import type { RepoInfo } from "../types";
|
|
16
|
+
|
|
17
|
+
let _handle: ReturnType<typeof openModal> | null = null;
|
|
18
|
+
let _repos: RepoInfo[] | null = null;
|
|
19
|
+
let _filter = "";
|
|
20
|
+
let _error = "";
|
|
21
|
+
/** FullName of the repo currently importing ("" = idle). */
|
|
22
|
+
let _importing = "";
|
|
23
|
+
let _resolve: ((result: { root: string } | null) => void) | null = null;
|
|
24
|
+
|
|
25
|
+
/** True when the active platform can browse + adopt existing repositories. */
|
|
26
|
+
export function platformSupportsAddRepo(): boolean {
|
|
27
|
+
const platform = getPlatform();
|
|
28
|
+
return typeof platform.listRepos === "function" && typeof platform.importProject === "function";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Open the picker. Resolves with the imported project's catalogue root key, or null when cancelled. */
|
|
32
|
+
export function openAddRepoModal(): Promise<{ root: string } | null> {
|
|
33
|
+
if (_handle) {
|
|
34
|
+
return Promise.resolve(null);
|
|
35
|
+
}
|
|
36
|
+
_repos = null;
|
|
37
|
+
_filter = "";
|
|
38
|
+
_error = "";
|
|
39
|
+
_importing = "";
|
|
40
|
+
|
|
41
|
+
void getPlatform()
|
|
42
|
+
.listRepos?.()
|
|
43
|
+
.then((repos) => {
|
|
44
|
+
_repos = repos;
|
|
45
|
+
})
|
|
46
|
+
.catch((error: unknown) => {
|
|
47
|
+
_repos = [];
|
|
48
|
+
_error = errorMessage(error);
|
|
49
|
+
})
|
|
50
|
+
.finally(() => {
|
|
51
|
+
renderModal();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
_resolve = resolve;
|
|
56
|
+
renderModal();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function closeAddRepoModal() {
|
|
61
|
+
if (!_handle || _importing) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
_handle.close();
|
|
65
|
+
_handle = null;
|
|
66
|
+
if (_resolve) {
|
|
67
|
+
_resolve(null);
|
|
68
|
+
_resolve = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function finish(result: { root: string }) {
|
|
73
|
+
_importing = "";
|
|
74
|
+
if (_handle) {
|
|
75
|
+
_handle.close();
|
|
76
|
+
_handle = null;
|
|
77
|
+
}
|
|
78
|
+
if (_resolve) {
|
|
79
|
+
_resolve(result);
|
|
80
|
+
_resolve = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function chooseRepo(repo: RepoInfo) {
|
|
85
|
+
if (_importing) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
_importing = repo.fullName;
|
|
89
|
+
_error = "";
|
|
90
|
+
renderModal();
|
|
91
|
+
try {
|
|
92
|
+
const imported = await getPlatform().importProject?.({ name: repo.name, owner: repo.owner });
|
|
93
|
+
if (imported) {
|
|
94
|
+
finish(imported);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
_error = "This platform cannot import repositories.";
|
|
98
|
+
} catch (error) {
|
|
99
|
+
_error = errorMessage(error);
|
|
100
|
+
}
|
|
101
|
+
_importing = "";
|
|
102
|
+
renderModal();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function visibleRepos(): RepoInfo[] {
|
|
106
|
+
const query = _filter.trim().toLowerCase();
|
|
107
|
+
const repos = _repos ?? [];
|
|
108
|
+
return query ? repos.filter((r) => r.fullName.toLowerCase().includes(query)) : repos;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function repoRowTpl(repo: RepoInfo) {
|
|
112
|
+
return html`
|
|
113
|
+
<button
|
|
114
|
+
class="add-repo-row"
|
|
115
|
+
?disabled=${Boolean(_importing)}
|
|
116
|
+
title=${repo.fullName}
|
|
117
|
+
@click=${() => void chooseRepo(repo)}
|
|
118
|
+
>
|
|
119
|
+
<span class="add-repo-name">${repo.fullName}</span>
|
|
120
|
+
<span class="add-repo-meta">
|
|
121
|
+
${repo.isJxProject ? html`<span class="add-repo-badge">Jx</span>` : nothing}
|
|
122
|
+
${repo.private ? html`<span class="add-repo-badge">private</span>` : nothing}
|
|
123
|
+
<span>${repo.defaultBranch} · ${repo.permission}</span>
|
|
124
|
+
</span>
|
|
125
|
+
${_importing === repo.fullName
|
|
126
|
+
? html`<span class="add-repo-busy">Importing…</span>`
|
|
127
|
+
: nothing}
|
|
128
|
+
</button>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function bodyTpl() {
|
|
133
|
+
if (_repos === null) {
|
|
134
|
+
return html`<div class="add-repo-empty">Loading repositories…</div>`;
|
|
135
|
+
}
|
|
136
|
+
const repos = visibleRepos();
|
|
137
|
+
if (repos.length === 0) {
|
|
138
|
+
return html`<div class="add-repo-empty">
|
|
139
|
+
${_filter
|
|
140
|
+
? "No repositories match the filter."
|
|
141
|
+
: "No repositories are reachable. Install the GitHub App (or widen its repository access) and try again."}
|
|
142
|
+
</div>`;
|
|
143
|
+
}
|
|
144
|
+
return html`<div class="add-repo-list">${repos.map((repo) => repoRowTpl(repo))}</div>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderModal() {
|
|
148
|
+
const tpl = html`
|
|
149
|
+
<sp-underlay open @close=${closeAddRepoModal}></sp-underlay>
|
|
150
|
+
<div
|
|
151
|
+
class="new-project-modal add-repo-modal"
|
|
152
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
153
|
+
if (e.key === "Escape") {
|
|
154
|
+
closeAddRepoModal();
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
<div class="new-project-modal-header">
|
|
159
|
+
<h2 class="new-project-modal-title">Add existing repository</h2>
|
|
160
|
+
<sp-action-button quiet size="s" @click=${closeAddRepoModal} title="Close">
|
|
161
|
+
<sp-icon-close slot="icon"></sp-icon-close>
|
|
162
|
+
</sp-action-button>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="new-project-modal-body">
|
|
165
|
+
<sp-textfield
|
|
166
|
+
class="add-repo-filter"
|
|
167
|
+
placeholder="Filter repositories…"
|
|
168
|
+
value=${_filter}
|
|
169
|
+
@input=${(e: Event) => {
|
|
170
|
+
_filter = (e.target as HTMLInputElement).value;
|
|
171
|
+
renderModal();
|
|
172
|
+
}}
|
|
173
|
+
></sp-textfield>
|
|
174
|
+
${bodyTpl()} ${_error ? html`<div class="new-project-error">${_error}</div>` : nothing}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
`;
|
|
178
|
+
if (_handle) {
|
|
179
|
+
_handle.update(tpl);
|
|
180
|
+
} else {
|
|
181
|
+
_handle = openModal(tpl);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -13,6 +13,7 @@ import { html } from "lit-html";
|
|
|
13
13
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
14
14
|
import { openModal } from "../ui/layers";
|
|
15
15
|
import { getPlatform } from "../platform";
|
|
16
|
+
import { installUrlOf } from "../platform-errors";
|
|
16
17
|
import { hasOpenAiKey } from "../services/ai-settings";
|
|
17
18
|
import { setPendingAgentPrompt } from "../services/agent-seed";
|
|
18
19
|
import { createAiCredentialsForm } from "../ui/ai-credentials-form";
|
|
@@ -57,6 +58,14 @@ let _paramsSeededFor = "";
|
|
|
57
58
|
|
|
58
59
|
let _error = "";
|
|
59
60
|
|
|
61
|
+
/** GitHub-App install link carried by a structured needs_installation_access failure. */
|
|
62
|
+
let _errorInstallUrl = "";
|
|
63
|
+
|
|
64
|
+
function captureError(error: unknown) {
|
|
65
|
+
_error = errorMessage(error);
|
|
66
|
+
_errorInstallUrl = installUrlOf(error) ?? "";
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
let _creating = false;
|
|
61
70
|
|
|
62
71
|
/** Starter templates offered in the picker (empty until loaded / on platforms without starters). */
|
|
@@ -111,6 +120,7 @@ export function openNewProjectModal(): Promise<{
|
|
|
111
120
|
_agentPrompt = "";
|
|
112
121
|
_paramsSeededFor = "";
|
|
113
122
|
_error = "";
|
|
123
|
+
_errorInstallUrl = "";
|
|
114
124
|
_creating = false;
|
|
115
125
|
_starters = [];
|
|
116
126
|
_dirDerived = true;
|
|
@@ -320,7 +330,7 @@ function renderModal() {
|
|
|
320
330
|
finish(result);
|
|
321
331
|
} catch (error) {
|
|
322
332
|
_creating = false;
|
|
323
|
-
|
|
333
|
+
captureError(error);
|
|
324
334
|
renderModal();
|
|
325
335
|
}
|
|
326
336
|
};
|
|
@@ -351,7 +361,7 @@ function renderModal() {
|
|
|
351
361
|
finish(result);
|
|
352
362
|
} catch (error) {
|
|
353
363
|
_creating = false;
|
|
354
|
-
|
|
364
|
+
captureError(error);
|
|
355
365
|
renderModal();
|
|
356
366
|
}
|
|
357
367
|
};
|
|
@@ -593,7 +603,16 @@ function renderModal() {
|
|
|
593
603
|
: ""}
|
|
594
604
|
<div class="new-project-modal-body">
|
|
595
605
|
${bodyTpl()}
|
|
596
|
-
${_tab !== "import" && _error
|
|
606
|
+
${_tab !== "import" && _error
|
|
607
|
+
? html`<div class="new-project-error">
|
|
608
|
+
${_error}
|
|
609
|
+
${_errorInstallUrl
|
|
610
|
+
? html`<a href=${_errorInstallUrl} target="_blank" rel="noreferrer">
|
|
611
|
+
Install the Jx Suite GitHub App →
|
|
612
|
+
</a>`
|
|
613
|
+
: ""}
|
|
614
|
+
</div>`
|
|
615
|
+
: ""}
|
|
597
616
|
</div>
|
|
598
617
|
<div class="new-project-modal-footer">${footerTpl()}</div>
|
|
599
618
|
</div>
|