@jxsuite/studio 1.4.0 → 1.5.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/canvas.html +51 -0
- package/dist/iframe-entry.js +4 -1
- package/dist/iframe-entry.js.map +3 -3
- package/dist/studio.js +115 -25
- package/dist/studio.js.map +9 -9
- package/fonts/OFL.txt +93 -0
- package/fonts/jetbrains-mono-400.woff2 +0 -0
- package/fonts/jetbrains-mono-500.woff2 +0 -0
- package/fonts/jetbrains-mono-700.woff2 +0 -0
- package/index.html +4631 -0
- package/package.json +13 -10
- package/src/editor/convert-to-component.ts +1 -1
- package/src/editor/convert-to-repeater.ts +1 -1
- package/src/new-project/add-repo-modal.ts +75 -14
- package/src/new-project/new-project-modal.ts +45 -18
- package/src/panels/welcome-screen.ts +15 -1
- package/src/platforms/cloud.ts +5 -3
- package/src/studio.ts +20 -3
- package/src/types.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"*.js",
|
|
13
13
|
"src",
|
|
14
|
-
"dist"
|
|
14
|
+
"dist",
|
|
15
|
+
"index.html",
|
|
16
|
+
"canvas.html",
|
|
17
|
+
"fonts"
|
|
15
18
|
],
|
|
16
19
|
"type": "module",
|
|
17
20
|
"exports": {
|
|
@@ -39,13 +42,13 @@
|
|
|
39
42
|
"@atlaskit/pragmatic-drag-and-drop": "^2.0.1",
|
|
40
43
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.0",
|
|
41
44
|
"@jxsuite/ai": "^0.34.0",
|
|
42
|
-
"@jxsuite/collab": "^0.5.
|
|
43
|
-
"@jxsuite/create": "^1.2.
|
|
44
|
-
"@jxsuite/formulas": "^0.0.
|
|
45
|
-
"@jxsuite/markup": "^0.4.
|
|
46
|
-
"@jxsuite/protocol": "^0.6.
|
|
47
|
-
"@jxsuite/runtime": "^1.3.
|
|
48
|
-
"@jxsuite/schema": "^1.
|
|
45
|
+
"@jxsuite/collab": "^0.5.1",
|
|
46
|
+
"@jxsuite/create": "^1.2.1",
|
|
47
|
+
"@jxsuite/formulas": "^0.0.5",
|
|
48
|
+
"@jxsuite/markup": "^0.4.1",
|
|
49
|
+
"@jxsuite/protocol": "^0.6.1",
|
|
50
|
+
"@jxsuite/runtime": "^1.3.1",
|
|
51
|
+
"@jxsuite/schema": "^1.4.0",
|
|
49
52
|
"@spectrum-web-components/accordion": "^1.12.2",
|
|
50
53
|
"@spectrum-web-components/action-bar": "1.12.2",
|
|
51
54
|
"@spectrum-web-components/action-button": "^1.12.2",
|
|
@@ -93,7 +96,7 @@
|
|
|
93
96
|
"yaml": "^2.9.0"
|
|
94
97
|
},
|
|
95
98
|
"devDependencies": {
|
|
96
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
99
|
+
"@happy-dom/global-registrator": "^20.11.1",
|
|
97
100
|
"@types/mdast": "^4.0.4",
|
|
98
101
|
"@webref/css": "^8.7.0",
|
|
99
102
|
"@webref/elements": "^2.7.1",
|
|
@@ -200,7 +200,7 @@ async function promptRepeaterConfig(defs: Record<string, unknown>) {
|
|
|
200
200
|
size="s"
|
|
201
201
|
placeholder="myItems"
|
|
202
202
|
.value=${newDefName}
|
|
203
|
-
?
|
|
203
|
+
?invalid=${Boolean(error)}
|
|
204
204
|
@input=${(e: Event) => {
|
|
205
205
|
newDefName = (e.target as HTMLInputElement).value || "";
|
|
206
206
|
error = "";
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* platform's account link can reach, personal and organization).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* Repository picker modal — a filterable picker over `platform.listRepos` (every repo the
|
|
4
|
+
* platform's account link can reach, personal and organization). Two modes share the dialog:
|
|
5
|
+
*
|
|
6
|
+
* - "add" (Add Existing Repository): the unfiltered adoption path.
|
|
7
|
+
* - "open" (Open Project on `openProjectPicker: "repo-list"` platforms): only write-access
|
|
8
|
+
* repositories, Jx-tagged ones first.
|
|
9
|
+
*
|
|
10
|
+
* Choosing a repo runs `platform.importProject`, which adopts it as a Jx project (probes
|
|
11
|
+
* project.json, tags + catalogues it) and resolves with the catalogue root key; the caller opens it
|
|
12
|
+
* through the same path as a recent project. Repos without a project.json fail with the backend's
|
|
13
|
+
* structured message, shown inline.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import { html, nothing } from "lit-html";
|
|
12
17
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
18
|
+
import { getAccountStatus, needsAppInstall } from "../account-status";
|
|
13
19
|
import { getPlatform } from "../platform";
|
|
14
20
|
import { openModal } from "../ui/layers";
|
|
15
21
|
import type { RepoInfo } from "../types";
|
|
16
22
|
|
|
23
|
+
type PickerMode = "add" | "open";
|
|
24
|
+
|
|
17
25
|
let _handle: ReturnType<typeof openModal> | null = null;
|
|
26
|
+
let _mode: PickerMode = "add";
|
|
18
27
|
let _repos: RepoInfo[] | null = null;
|
|
19
28
|
let _filter = "";
|
|
20
29
|
let _error = "";
|
|
@@ -28,11 +37,29 @@ export function platformSupportsAddRepo(): boolean {
|
|
|
28
37
|
return typeof platform.listRepos === "function" && typeof platform.importProject === "function";
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
/**
|
|
40
|
+
/** True when the active platform routes Open Project through this repo picker. */
|
|
41
|
+
export function platformUsesRepoPicker(): boolean {
|
|
42
|
+
return getPlatform().openProjectPicker === "repo-list" && platformSupportsAddRepo();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Open the adoption picker. Resolves with the imported project's catalogue root key, or null when
|
|
47
|
+
* cancelled.
|
|
48
|
+
*/
|
|
32
49
|
export function openAddRepoModal(): Promise<{ root: string } | null> {
|
|
50
|
+
return openPicker("add");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Open Project as a repo picker (write-access repositories only). Null when cancelled. */
|
|
54
|
+
export function openProjectPickerModal(): Promise<{ root: string } | null> {
|
|
55
|
+
return openPicker("open");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function openPicker(mode: PickerMode): Promise<{ root: string } | null> {
|
|
33
59
|
if (_handle) {
|
|
34
60
|
return Promise.resolve(null);
|
|
35
61
|
}
|
|
62
|
+
_mode = mode;
|
|
36
63
|
_repos = null;
|
|
37
64
|
_filter = "";
|
|
38
65
|
_error = "";
|
|
@@ -104,7 +131,13 @@ async function chooseRepo(repo: RepoInfo) {
|
|
|
104
131
|
|
|
105
132
|
function visibleRepos(): RepoInfo[] {
|
|
106
133
|
const query = _filter.trim().toLowerCase();
|
|
107
|
-
|
|
134
|
+
let repos = _repos ?? [];
|
|
135
|
+
if (_mode === "open") {
|
|
136
|
+
// Open Project offers only repos the user can write to; Jx-tagged repos surface first.
|
|
137
|
+
// The topic is an accelerator, not ground truth — untagged repos still open via importProject.
|
|
138
|
+
const writable = repos.filter((r) => r.permission === "admin" || r.permission === "write");
|
|
139
|
+
repos = [...writable.filter((r) => r.isJxProject), ...writable.filter((r) => !r.isJxProject)];
|
|
140
|
+
}
|
|
108
141
|
return query ? repos.filter((r) => r.fullName.toLowerCase().includes(query)) : repos;
|
|
109
142
|
}
|
|
110
143
|
|
|
@@ -129,17 +162,43 @@ function repoRowTpl(repo: RepoInfo) {
|
|
|
129
162
|
`;
|
|
130
163
|
}
|
|
131
164
|
|
|
165
|
+
function emptyTpl() {
|
|
166
|
+
if (_filter) {
|
|
167
|
+
return html`<div class="add-repo-empty">No repositories match the filter.</div>`;
|
|
168
|
+
}
|
|
169
|
+
if (_mode === "open" && (_repos ?? []).length > 0) {
|
|
170
|
+
return html`<div class="add-repo-empty">
|
|
171
|
+
No repositories with write access. Widen the Jx Suite GitHub App's repository access, or ask a
|
|
172
|
+
repository admin for write access.
|
|
173
|
+
</div>`;
|
|
174
|
+
}
|
|
175
|
+
if (needsAppInstall()) {
|
|
176
|
+
return html`<div class="add-repo-empty">
|
|
177
|
+
No repositories are reachable yet.
|
|
178
|
+
<a
|
|
179
|
+
class="add-repo-install"
|
|
180
|
+
href=${getAccountStatus()?.appInstallUrl ?? "#"}
|
|
181
|
+
target="_blank"
|
|
182
|
+
rel="noreferrer"
|
|
183
|
+
>
|
|
184
|
+
Install the Jx Suite GitHub App
|
|
185
|
+
</a>
|
|
186
|
+
to grant repository access, then reopen this dialog.
|
|
187
|
+
</div>`;
|
|
188
|
+
}
|
|
189
|
+
return html`<div class="add-repo-empty">
|
|
190
|
+
No repositories are reachable. Install the GitHub App (or widen its repository access) and try
|
|
191
|
+
again.
|
|
192
|
+
</div>`;
|
|
193
|
+
}
|
|
194
|
+
|
|
132
195
|
function bodyTpl() {
|
|
133
196
|
if (_repos === null) {
|
|
134
197
|
return html`<div class="add-repo-empty">Loading repositories…</div>`;
|
|
135
198
|
}
|
|
136
199
|
const repos = visibleRepos();
|
|
137
200
|
if (repos.length === 0) {
|
|
138
|
-
return
|
|
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>`;
|
|
201
|
+
return emptyTpl();
|
|
143
202
|
}
|
|
144
203
|
return html`<div class="add-repo-list">${repos.map((repo) => repoRowTpl(repo))}</div>`;
|
|
145
204
|
}
|
|
@@ -156,7 +215,9 @@ function renderModal() {
|
|
|
156
215
|
}}
|
|
157
216
|
>
|
|
158
217
|
<div class="new-project-modal-header">
|
|
159
|
-
<h2 class="new-project-modal-title">
|
|
218
|
+
<h2 class="new-project-modal-title">
|
|
219
|
+
${_mode === "open" ? "Open Project" : "Add existing repository"}
|
|
220
|
+
</h2>
|
|
160
221
|
<sp-action-button quiet size="s" @click=${closeAddRepoModal} title="Close">
|
|
161
222
|
<sp-icon-close slot="icon"></sp-icon-close>
|
|
162
223
|
</sp-action-button>
|
|
@@ -58,6 +58,12 @@ let _paramsSeededFor = "";
|
|
|
58
58
|
|
|
59
59
|
let _error = "";
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Inline validation error shown at the Project Name field (the global strip is for backend
|
|
63
|
+
* failures).
|
|
64
|
+
*/
|
|
65
|
+
let _nameError = "";
|
|
66
|
+
|
|
61
67
|
/** GitHub-App install link carried by a structured needs_installation_access failure. */
|
|
62
68
|
let _errorInstallUrl = "";
|
|
63
69
|
|
|
@@ -66,6 +72,15 @@ function captureError(error: unknown) {
|
|
|
66
72
|
_errorInstallUrl = installUrlOf(error) ?? "";
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
/** After a failed submit, bring the params step back to the top and focus the name field. */
|
|
76
|
+
function focusNameField() {
|
|
77
|
+
requestAnimationFrame(() => {
|
|
78
|
+
const body = document.querySelector(".new-project-modal-body");
|
|
79
|
+
body?.scrollTo?.(0, 0);
|
|
80
|
+
body?.querySelector<HTMLElement>("sp-textfield")?.focus?.();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
let _creating = false;
|
|
70
85
|
|
|
71
86
|
/** Starter templates offered in the picker (empty until loaded / on platforms without starters). */
|
|
@@ -99,7 +114,7 @@ function credsForm() {
|
|
|
99
114
|
*
|
|
100
115
|
* @returns {Promise<{ root: string; config: object } | null>}
|
|
101
116
|
*/
|
|
102
|
-
export function openNewProjectModal(): Promise<{
|
|
117
|
+
export function openNewProjectModal(options?: { tab?: "template" | "starter" }): Promise<{
|
|
103
118
|
root: string;
|
|
104
119
|
config: ProjectConfig;
|
|
105
120
|
} | null> {
|
|
@@ -113,13 +128,14 @@ export function openNewProjectModal(): Promise<{
|
|
|
113
128
|
name: "",
|
|
114
129
|
url: "",
|
|
115
130
|
};
|
|
116
|
-
_tab = "template";
|
|
131
|
+
_tab = options?.tab ?? "template";
|
|
117
132
|
_step = "source";
|
|
118
133
|
_template = "blank";
|
|
119
134
|
_starter = "";
|
|
120
135
|
_agentPrompt = "";
|
|
121
136
|
_paramsSeededFor = "";
|
|
122
137
|
_error = "";
|
|
138
|
+
_nameError = "";
|
|
123
139
|
_errorInstallUrl = "";
|
|
124
140
|
_creating = false;
|
|
125
141
|
_starters = [];
|
|
@@ -215,6 +231,9 @@ function renderModal() {
|
|
|
215
231
|
const onInput =
|
|
216
232
|
(field: "name" | "description" | "url" | "adapter" | "directory") => (e: Event) => {
|
|
217
233
|
_form[field] = (e.target as HTMLInputElement).value;
|
|
234
|
+
if (field === "name") {
|
|
235
|
+
_nameError = "";
|
|
236
|
+
}
|
|
218
237
|
if (field === "name" && !_form.directory) {
|
|
219
238
|
// Auto-derive directory slug from name while user hasn't manually typed one
|
|
220
239
|
_dirDerived = true;
|
|
@@ -301,6 +320,7 @@ function renderModal() {
|
|
|
301
320
|
}
|
|
302
321
|
_step = "source";
|
|
303
322
|
_error = "";
|
|
323
|
+
_nameError = "";
|
|
304
324
|
renderModal();
|
|
305
325
|
};
|
|
306
326
|
|
|
@@ -308,8 +328,9 @@ function renderModal() {
|
|
|
308
328
|
|
|
309
329
|
const onSubmit = async () => {
|
|
310
330
|
if (!_form.name.trim()) {
|
|
311
|
-
|
|
331
|
+
_nameError = "Project name is required";
|
|
312
332
|
renderModal();
|
|
333
|
+
focusNameField();
|
|
313
334
|
return;
|
|
314
335
|
}
|
|
315
336
|
if (!_form.directory.trim()) {
|
|
@@ -318,6 +339,7 @@ function renderModal() {
|
|
|
318
339
|
|
|
319
340
|
_creating = true;
|
|
320
341
|
_error = "";
|
|
342
|
+
_nameError = "";
|
|
321
343
|
renderModal();
|
|
322
344
|
|
|
323
345
|
try {
|
|
@@ -337,8 +359,9 @@ function renderModal() {
|
|
|
337
359
|
|
|
338
360
|
const onAgentSubmit = async () => {
|
|
339
361
|
if (!_form.name.trim()) {
|
|
340
|
-
|
|
362
|
+
_nameError = "Project name is required";
|
|
341
363
|
renderModal();
|
|
364
|
+
focusNameField();
|
|
342
365
|
return;
|
|
343
366
|
}
|
|
344
367
|
if (!_form.directory.trim()) {
|
|
@@ -347,6 +370,7 @@ function renderModal() {
|
|
|
347
370
|
|
|
348
371
|
_creating = true;
|
|
349
372
|
_error = "";
|
|
373
|
+
_nameError = "";
|
|
350
374
|
renderModal();
|
|
351
375
|
|
|
352
376
|
try {
|
|
@@ -468,9 +492,14 @@ function renderModal() {
|
|
|
468
492
|
<sp-textfield
|
|
469
493
|
placeholder="My Site"
|
|
470
494
|
.value=${_form.name}
|
|
495
|
+
?invalid=${Boolean(_nameError)}
|
|
471
496
|
@input=${onInput("name")}
|
|
472
497
|
style="width: 100%"
|
|
473
|
-
|
|
498
|
+
>
|
|
499
|
+
${_nameError
|
|
500
|
+
? html`<sp-help-text slot="negative-help-text">${_nameError}</sp-help-text>`
|
|
501
|
+
: ""}
|
|
502
|
+
</sp-textfield>
|
|
474
503
|
</label>
|
|
475
504
|
|
|
476
505
|
<label class="new-project-field">
|
|
@@ -601,19 +630,17 @@ function renderModal() {
|
|
|
601
630
|
</div>
|
|
602
631
|
`
|
|
603
632
|
: ""}
|
|
604
|
-
<div class="new-project-modal-body">
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
: ""}
|
|
616
|
-
</div>
|
|
633
|
+
<div class="new-project-modal-body">${bodyTpl()}</div>
|
|
634
|
+
${_tab !== "import" && _error
|
|
635
|
+
? html`<div class="new-project-error new-project-error--global">
|
|
636
|
+
${_error}
|
|
637
|
+
${_errorInstallUrl
|
|
638
|
+
? html`<a href=${_errorInstallUrl} target="_blank" rel="noreferrer">
|
|
639
|
+
Install the Jx Suite GitHub App →
|
|
640
|
+
</a>`
|
|
641
|
+
: ""}
|
|
642
|
+
</div>`
|
|
643
|
+
: ""}
|
|
617
644
|
<div class="new-project-modal-footer">${footerTpl()}</div>
|
|
618
645
|
</div>
|
|
619
646
|
`;
|
|
@@ -15,7 +15,7 @@ import { platformSupportsClone } from "./git-panel";
|
|
|
15
15
|
interface WelcomeCtx {
|
|
16
16
|
openProject: () => void;
|
|
17
17
|
openRecentProject: (root: string) => void;
|
|
18
|
-
openNewProject: () => void;
|
|
18
|
+
openNewProject: (options?: { tab?: "starter" }) => void;
|
|
19
19
|
cloneRepository: () => void;
|
|
20
20
|
addExistingRepo: () => void;
|
|
21
21
|
}
|
|
@@ -59,6 +59,20 @@ export function renderWelcome(host: HTMLElement) {
|
|
|
59
59
|
</svg>
|
|
60
60
|
New Project...
|
|
61
61
|
</button>
|
|
62
|
+
<button class="welcome-action" @click=${() => ctx.openNewProject({ tab: "starter" })}>
|
|
63
|
+
<svg
|
|
64
|
+
class="welcome-action-icon"
|
|
65
|
+
width="16"
|
|
66
|
+
height="16"
|
|
67
|
+
viewBox="0 0 16 16"
|
|
68
|
+
fill="currentColor"
|
|
69
|
+
>
|
|
70
|
+
<path
|
|
71
|
+
d="M1 1h6v6H1V1zm1 1v4h4V2H2zm7-1h6v6H9V1zm1 1v4h4V2h-4zM1 9h6v6H1V9zm1 1v4h4v-4H2zm7-1h6v6H9V9zm1 1v4h4v-4h-4z"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
Start from an Example...
|
|
75
|
+
</button>
|
|
62
76
|
<button class="welcome-action" @click=${() => ctx.openProject()}>
|
|
63
77
|
<svg
|
|
64
78
|
class="welcome-action-icon"
|
package/src/platforms/cloud.ts
CHANGED
|
@@ -209,6 +209,9 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
|
|
|
209
209
|
id: "cloud",
|
|
210
210
|
projectRoot: root,
|
|
211
211
|
canvasUrl: "/canvas.html",
|
|
212
|
+
/* Open Project routes through Studio's repo picker (write-access repos via listRepos +
|
|
213
|
+
importProject) — sessions are URL-bound, so openProject() below never opens a dialog. */
|
|
214
|
+
openProjectPicker: "repo-list",
|
|
212
215
|
|
|
213
216
|
async activate() {
|
|
214
217
|
if (!project) {
|
|
@@ -377,9 +380,8 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
|
|
|
377
380
|
|
|
378
381
|
/**
|
|
379
382
|
* The project's format registry, served by the session gateway (the dev server's `GET
|
|
380
|
-
* /__studio/formats` under this session's base path).
|
|
381
|
-
*
|
|
382
|
-
* open) until the backend lands it.
|
|
383
|
+
* /__studio/formats` under this session's base path). Degrades to an empty registry (only .json
|
|
384
|
+
* documents open) against backends that predate the route.
|
|
383
385
|
*/
|
|
384
386
|
async listFormats() {
|
|
385
387
|
try {
|
package/src/studio.ts
CHANGED
|
@@ -155,7 +155,11 @@ import { hydrateProjectList } from "./project-list";
|
|
|
155
155
|
import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
|
|
156
156
|
import { hydrateSettings } from "./services/settings-store";
|
|
157
157
|
import { initWelcome } from "./panels/welcome-screen";
|
|
158
|
-
import {
|
|
158
|
+
import {
|
|
159
|
+
openAddRepoModal,
|
|
160
|
+
openProjectPickerModal,
|
|
161
|
+
platformUsesRepoPicker,
|
|
162
|
+
} from "./new-project/add-repo-modal";
|
|
159
163
|
import { openNewProjectModal } from "./new-project/new-project-modal";
|
|
160
164
|
import type { DocumentStackEntry, GitDiffState } from "./types";
|
|
161
165
|
import type { Tab } from "./tabs/tab";
|
|
@@ -546,8 +550,8 @@ initWelcome({
|
|
|
546
550
|
}
|
|
547
551
|
},
|
|
548
552
|
cloneRepository: () => cloneRepository({ openRecentProject }),
|
|
549
|
-
openNewProject: async () => {
|
|
550
|
-
const result = await openNewProjectModal();
|
|
553
|
+
openNewProject: async (options) => {
|
|
554
|
+
const result = await openNewProjectModal(options);
|
|
551
555
|
if (result) {
|
|
552
556
|
void openRecentProject(result.root);
|
|
553
557
|
}
|
|
@@ -888,6 +892,19 @@ function loadProject() {
|
|
|
888
892
|
return _loadProject();
|
|
889
893
|
}
|
|
890
894
|
async function openProject() {
|
|
895
|
+
// Repo-list platforms (cloud) pick from the user's writable repositories instead of a
|
|
896
|
+
// Backend dialog; the choice opens through the same path as a recent project.
|
|
897
|
+
if (platformUsesRepoPicker()) {
|
|
898
|
+
const picked = await openProjectPickerModal();
|
|
899
|
+
if (picked) {
|
|
900
|
+
// The catalogue may have gained an entry; refresh it before navigating into the project.
|
|
901
|
+
void hydrateProjectList().then(() => {
|
|
902
|
+
render();
|
|
903
|
+
});
|
|
904
|
+
void openRecentProject(picked.root);
|
|
905
|
+
}
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
891
908
|
const result = await _openProject({
|
|
892
909
|
renderActivityBar: () => renderActivityBar(),
|
|
893
910
|
renderLeftPanel,
|
package/src/types.ts
CHANGED
|
@@ -127,6 +127,14 @@ export interface StudioPlatform {
|
|
|
127
127
|
config: ProjectConfig;
|
|
128
128
|
handle: { root: string; name: string; projectConfig: ProjectConfig };
|
|
129
129
|
} | null>;
|
|
130
|
+
/**
|
|
131
|
+
* How "Open Project" picks a project. Absent: `openProject()` owns picking (native dialog /
|
|
132
|
+
* showDirectoryPicker). `"repo-list"`: Studio shows its repository picker over `listRepos` +
|
|
133
|
+
* `importProject` (write-access repositories) and opens the choice through the recent-projects
|
|
134
|
+
* path — `openProject()` is never called. Cloud sets this: its sessions are URL-bound, so there
|
|
135
|
+
* is no backend dialog to delegate to.
|
|
136
|
+
*/
|
|
137
|
+
openProjectPicker?: "repo-list";
|
|
130
138
|
probeRootProject: () => Promise<{
|
|
131
139
|
meta: { root: string; name: string };
|
|
132
140
|
info: {
|