@jxsuite/studio 6.0.0 → 6.1.1
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/chunks/2020-va2wwra4.js +4 -0
- package/dist/chunks/adapter-f93pafd7.js +4 -0
- package/dist/chunks/{cf-account-picker-bb2yq946.js → cf-account-picker-af1h381v.js} +3 -3
- package/dist/chunks/cf-settings-125tdm0w.js +4 -0
- package/dist/chunks/components-ax2f1fnn.js +4 -0
- package/dist/chunks/content-source-108aqtk1.js +4 -0
- package/dist/chunks/{convert-to-repeater-vqkt7s3s.js → convert-to-repeater-ry5rmppr.js} +3 -3
- package/dist/chunks/editor-pfgdmkb0.js +4 -0
- package/dist/chunks/entry-commands-t0k1q0kx.js +4 -0
- package/dist/chunks/{extension-sections-8beq9ac3.js → extension-sections-6mwh2hf8.js} +3 -3
- package/dist/chunks/file-ops-meej5mqf.js +4 -0
- package/dist/chunks/files-44vp8a2s.js +4 -0
- package/dist/chunks/format-host-7q1petb9.js +4 -0
- package/dist/chunks/fs-events-h2mwnxaz.js +4 -0
- package/dist/chunks/git-panel-zngbymvd.js +4 -0
- package/dist/chunks/{grid-view-ca10def6.js → grid-view-sx9vynsp.js} +15 -15
- package/dist/chunks/icons-p44xhjf0.js +4 -0
- package/dist/chunks/iframe-host-qk3ycq87.js +4 -0
- package/dist/chunks/{index-9ydfe04y.js → index-2veqmnhr.js} +3 -3
- package/dist/chunks/index-wkyfjeqz.js +4 -0
- package/dist/chunks/{javascript-d9pqgc2p.js → javascript-8nbvs12s.js} +3 -3
- package/dist/chunks/jsonMode-s8kt9dsj.js +13 -0
- package/dist/chunks/jx-validate-psgd7vsx.js +4 -0
- package/dist/chunks/layers-panel-1kq2jyfk.js +4 -0
- package/dist/chunks/layers-xd92fg8d.js +4 -0
- package/dist/chunks/lit-html-ac4cwwr4.js +4 -0
- package/dist/chunks/media-picker-0svzkdyb.js +4 -0
- package/dist/chunks/menu-5c55ea3d.js +4 -0
- package/dist/chunks/{monaco-binding-62dtmgbf.js → monaco-binding-z6r70pdt.js} +3 -3
- package/dist/chunks/monaco-buffer-fz6gq5y7.js +4 -0
- package/dist/chunks/monaco-setup-q4axkgj7.js +4 -0
- package/dist/chunks/notify-0xgtn8q1.js +4 -0
- package/dist/chunks/platform-v1qctbe6.js +4 -0
- package/dist/chunks/publish-panel-ym1fmgq5.js +4 -0
- package/dist/chunks/quick-search-a54e6t5n.js +4 -0
- package/dist/chunks/references-gmqp82fs.js +4 -0
- package/dist/chunks/right-panel-v7t9awbk.js +4 -0
- package/dist/chunks/section-registry-vhyf3zsr.js +4 -0
- package/dist/chunks/studio-0bhx5hz6.js +5 -0
- package/dist/chunks/studio-0k1ddnyy.js +6 -0
- package/dist/chunks/studio-1b04pd0j.js +64 -0
- package/dist/chunks/{studio-sxc0zvpt.js → studio-1q1sh2ax.js} +3 -3
- package/dist/chunks/studio-4dwbj6gb.js +5 -0
- package/dist/chunks/studio-4tanh828.js +5 -0
- package/dist/chunks/{studio-fqq890ap.js → studio-55h4mstj.js} +3 -3
- package/dist/chunks/studio-5tnvjkz4.js +5 -0
- package/dist/chunks/studio-60dqgb0c.js +5 -0
- package/dist/chunks/studio-61dp09qg.js +9 -0
- package/dist/chunks/studio-63epcrj2.js +5 -0
- package/dist/chunks/studio-74k29dy9.js +5 -0
- package/dist/chunks/{studio-qa73881a.js → studio-7vg6jcd3.js} +4 -4
- package/dist/chunks/studio-7zx31knw.js +9 -0
- package/dist/chunks/studio-8bxk97km.js +5 -0
- package/dist/chunks/{studio-edbtvcd1.js → studio-8grwxtqk.js} +4 -4
- package/dist/chunks/studio-933e30n4.js +5 -0
- package/dist/chunks/{studio-rr4xkv81.js → studio-98wnwhbg.js} +4 -4
- package/dist/chunks/studio-9mccjwjh.js +14 -0
- package/dist/chunks/studio-a2ke1e4t.js +5 -0
- package/dist/chunks/studio-e3a2a6v3.js +4 -0
- package/dist/chunks/{studio-pwqfmt4p.js → studio-e81q1vb9.js} +4 -4
- package/dist/chunks/{studio-7pgy3xaf.js → studio-epgrmqqm.js} +4 -4
- package/dist/chunks/studio-g1shz58g.js +5 -0
- package/dist/chunks/studio-gf0fz03c.js +5 -0
- package/dist/chunks/studio-gjtpe62q.js +6 -0
- package/dist/chunks/{studio-tzegmy4w.js → studio-gkskhg7m.js} +4 -4
- package/dist/chunks/studio-gx4dnjzd.js +5 -0
- package/dist/chunks/{studio-8xcwewjq.js → studio-h46vtzk8.js} +4 -4
- package/dist/chunks/studio-j03zga3y.js +9 -0
- package/dist/chunks/{studio-4r0sjtx8.js → studio-jhpxeckz.js} +4 -4
- package/dist/chunks/{studio-zd4kayn2.js → studio-k01r7ykp.js} +4 -4
- package/dist/chunks/{studio-03wwfvyt.js → studio-kggey1jt.js} +4 -4
- package/dist/chunks/{studio-hnvnfbdv.js → studio-kk7h30ne.js} +6 -6
- package/dist/chunks/{studio-01qza1n3.js → studio-kt1zqney.js} +4 -4
- package/dist/chunks/{studio-r3kaka4d.js → studio-m3cwhvye.js} +4 -4
- package/dist/chunks/{studio-evwgp1ym.js → studio-mcayt6hz.js} +4 -4
- package/dist/chunks/{studio-5vy2wfm5.js → studio-myjcghv3.js} +4 -4
- package/dist/chunks/studio-n8bxwc3y.js +4 -0
- package/dist/chunks/{studio-ztvcxm3w.js → studio-ngm8gvpb.js} +4 -4
- package/dist/chunks/{studio-01kbbqg3.js → studio-nj5a01jy.js} +196 -196
- package/dist/chunks/{studio-xkfs0dp2.js → studio-pcgz8heb.js} +4 -4
- package/dist/chunks/{studio-0bbxhhce.js → studio-pre0skq6.js} +4 -4
- package/dist/chunks/{studio-n78gjfy8.js → studio-qcsq0t15.js} +4 -4
- package/dist/chunks/studio-qkstvve9.js +5 -0
- package/dist/chunks/studio-qv1pwd3c.js +5 -0
- package/dist/chunks/{studio-rmn2dz59.js → studio-rc6dvxjb.js} +5 -5
- package/dist/chunks/{studio-st75kn99.js → studio-rv11kfwa.js} +7 -7
- package/dist/chunks/studio-s8dmmz66.js +5 -0
- package/dist/chunks/studio-sp5xzbj4.js +6 -0
- package/dist/chunks/{studio-tmeq6mjb.js → studio-szsjbj0g.js} +4 -4
- package/dist/chunks/studio-t1xzawhr.js +16 -0
- package/dist/chunks/{studio-mk00zd5d.js → studio-t6b6qe31.js} +4 -4
- package/dist/chunks/{studio-bftp4n4b.js → studio-tdcprbqb.js} +4 -4
- package/dist/chunks/{studio-ge5fqjth.js → studio-vrmcjf9g.js} +5 -5
- package/dist/chunks/studio-vyc0b4he.js +10 -0
- package/dist/chunks/{studio-semd1s8s.js → studio-w0cgf6jg.js} +4 -4
- package/dist/chunks/{studio-gp02nqnw.js → studio-w3hskzqk.js} +4 -4
- package/dist/chunks/{studio-1wvfxkyy.js → studio-wszjyh0j.js} +4 -4
- package/dist/chunks/studio-xgrgcdyh.js +9 -0
- package/dist/chunks/{studio-y7jehjrp.js → studio-xz2yxfcb.js} +4 -4
- package/dist/chunks/{studio-aje966s9.js → studio-y6n42tgs.js} +4 -4
- package/dist/chunks/{studio-yr04mv2j.js → studio-yb560vxr.js} +4 -4
- package/dist/chunks/{studio-tp642n5g.js → studio-yn9f8vtx.js} +4 -4
- package/dist/chunks/studio-yw05w8f0.js +7 -0
- package/dist/chunks/{studio-cbr9czkn.js → studio-z16sgavc.js} +4 -4
- package/dist/chunks/studio-zey7zsdw.js +12 -0
- package/dist/chunks/{studio-1q6amtph.js → studio-zyqn40w4.js} +4 -4
- package/dist/chunks/tsMode-2frah6h1.js +14 -0
- package/dist/chunks/workspace-zt81g1ms.js +4 -0
- package/dist/chunks/ws-client-n2bh4hkf.js +4 -0
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +62 -62
- package/dist/studio.js +87 -87
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +10 -10
- package/src/canvas/canvas-render.ts +4 -1
- package/src/canvas/canvas-utils.ts +73 -15
- package/src/canvas/iframe-host.ts +67 -21
- package/src/canvas/iframe-inline-edit.ts +22 -4
- package/src/canvas/iframe-interaction.ts +7 -1
- package/src/canvas/iframe-protocol.ts +3 -0
- package/src/canvas/iframe-render.ts +5 -2
- package/src/commands/defaults.ts +4 -1
- package/src/commands/live-context.ts +3 -1
- package/src/editor/shortcuts.ts +55 -27
- package/src/files/media-paths.ts +3 -1
- package/src/new-project/add-repo-modal.ts +459 -59
- package/src/panels/editors.ts +3 -1
- package/src/panels/style-panel.ts +36 -11
- package/src/platforms/cloud.ts +95 -18
- package/src/platforms/devserver.ts +47 -12
- package/src/publish/pages-service.ts +7 -0
- package/src/publish/publish-panel.ts +16 -2
- package/src/studio.ts +14 -2
- package/src/surfaces/add-repo.json +721 -224
- package/src/surfaces/add-repo.ts +196 -31
- package/src/surfaces/library-pane.ts +6 -0
- package/src/surfaces/rail.json +126 -73
- package/src/surfaces/rail.ts +31 -5
- package/src/tabs/typing.ts +31 -0
- package/src/types.ts +42 -3
- package/src/utils/edit-display.ts +30 -1
- package/src/utils/project-dir.ts +24 -0
- package/src/workspace/workspace.ts +7 -0
- package/dist/chunks/2020-cs2sb9t1.js +0 -4
- package/dist/chunks/adapter-y5gdq19c.js +0 -4
- package/dist/chunks/cf-settings-20dznc3j.js +0 -4
- package/dist/chunks/components-q037dya7.js +0 -4
- package/dist/chunks/content-source-t0q184js.js +0 -4
- package/dist/chunks/editor-j5sq149q.js +0 -4
- package/dist/chunks/entry-commands-fxzgcc4k.js +0 -4
- package/dist/chunks/file-ops-2n77759h.js +0 -4
- package/dist/chunks/files-fjy7zpxt.js +0 -4
- package/dist/chunks/format-host-wj3mf3n8.js +0 -4
- package/dist/chunks/fs-events-7kcgn4e9.js +0 -4
- package/dist/chunks/git-panel-7z2mpt2d.js +0 -4
- package/dist/chunks/icons-qbh8akb1.js +0 -4
- package/dist/chunks/iframe-host-a0zjd6xp.js +0 -4
- package/dist/chunks/index-m0x5ztax.js +0 -4
- package/dist/chunks/jsonMode-800yvbem.js +0 -13
- package/dist/chunks/jx-validate-zc9zp77s.js +0 -4
- package/dist/chunks/layers-p677nrh5.js +0 -4
- package/dist/chunks/layers-panel-y4w1dq90.js +0 -4
- package/dist/chunks/lit-html-cfn61dn7.js +0 -4
- package/dist/chunks/media-picker-xg7yjpz9.js +0 -4
- package/dist/chunks/menu-fpekzj6w.js +0 -4
- package/dist/chunks/monaco-buffer-nn4dtx27.js +0 -4
- package/dist/chunks/monaco-setup-tj13ndtm.js +0 -4
- package/dist/chunks/notify-ahs57a2z.js +0 -4
- package/dist/chunks/platform-v9xyfkbx.js +0 -4
- package/dist/chunks/publish-panel-xtbfgg2r.js +0 -4
- package/dist/chunks/quick-search-d3a6m7j3.js +0 -4
- package/dist/chunks/references-99ws0b0m.js +0 -4
- package/dist/chunks/right-panel-31vbk5xp.js +0 -4
- package/dist/chunks/section-registry-b2yzp9sw.js +0 -4
- package/dist/chunks/studio-1a99w39s.js +0 -5
- package/dist/chunks/studio-3ck2a5c4.js +0 -10
- package/dist/chunks/studio-4fde4a0x.js +0 -5
- package/dist/chunks/studio-4jj9gme2.js +0 -6
- package/dist/chunks/studio-4m50vh74.js +0 -16
- package/dist/chunks/studio-4xppwncr.js +0 -4
- package/dist/chunks/studio-5d7wfmnp.js +0 -5
- package/dist/chunks/studio-5e9zsamh.js +0 -9
- package/dist/chunks/studio-5gs9th4b.js +0 -5
- package/dist/chunks/studio-7qvs7v0m.js +0 -5
- package/dist/chunks/studio-84rtc120.js +0 -5
- package/dist/chunks/studio-aekhcdzw.js +0 -7
- package/dist/chunks/studio-aqmph6k4.js +0 -5
- package/dist/chunks/studio-c6zdm25z.js +0 -9
- package/dist/chunks/studio-capk0583.js +0 -9
- package/dist/chunks/studio-chdmwwzw.js +0 -5
- package/dist/chunks/studio-cp1gbff9.js +0 -14
- package/dist/chunks/studio-cqmtbg2p.js +0 -5
- package/dist/chunks/studio-e47215hh.js +0 -5
- package/dist/chunks/studio-fbx6twps.js +0 -6
- package/dist/chunks/studio-fthwn4kf.js +0 -5
- package/dist/chunks/studio-g8dsj3d2.js +0 -6
- package/dist/chunks/studio-gjknvzam.js +0 -9
- package/dist/chunks/studio-hjbg8d5y.js +0 -12
- package/dist/chunks/studio-nbxqcgvf.js +0 -5
- package/dist/chunks/studio-t1eedsyx.js +0 -4
- package/dist/chunks/studio-vavmwshr.js +0 -5
- package/dist/chunks/studio-x5g1xk77.js +0 -5
- package/dist/chunks/studio-x92tgnee.js +0 -64
- package/dist/chunks/studio-xg1942as.js +0 -5
- package/dist/chunks/studio-zev8ssp1.js +0 -5
- package/dist/chunks/tsMode-k9s9fcag.js +0 -14
- package/dist/chunks/workspace-5vmgsagj.js +0 -4
- package/dist/chunks/ws-client-0ja08haa.js +0 -4
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
3
|
* Repository picker flow — a filterable picker over `platform.listRepos` (every repo the platform's
|
|
4
|
-
* account link can reach, personal and organization)
|
|
4
|
+
* account link can reach, personal and organization), with a second step for the folder the project
|
|
5
|
+
* lives in. Two modes share the dialog:
|
|
5
6
|
*
|
|
6
7
|
* - "add" (Add Existing Repository): the unfiltered adoption path.
|
|
7
8
|
* - "open" (Open Project on `openProjectPicker: "repo-list"` platforms): only write-access
|
|
8
9
|
* repositories, Jx-tagged ones first.
|
|
9
10
|
*
|
|
10
|
-
* Choosing a
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Choosing a repository asks `platform.listRepoProjects` which of its folders hold a `project.json`
|
|
12
|
+
* and preselects one — the root when it is a project, else the first — so a monorepo's
|
|
13
|
+
* `sites/marketing` opens as itself rather than failing because the root has no config. The folder
|
|
14
|
+
* field takes any folder too, which is the answer for a repository too large to scan and for a
|
|
15
|
+
* platform that cannot scan at all. Confirming runs `platform.importProject` with that folder,
|
|
16
|
+
* which adopts it as a Jx project (probes its project.json, tags + catalogues the repository) and
|
|
17
|
+
* resolves with the catalogue root key; the caller opens it through the same path as a recent
|
|
18
|
+
* project. A folder without a project.json fails with the backend's structured message, shown
|
|
19
|
+
* inline.
|
|
20
|
+
*
|
|
21
|
+
* Scans are cached per repository for the life of the dialog, and one the CARET asks for (an arrow
|
|
22
|
+
* key in the filter) waits {@link SCAN_DELAY_MS} first: arrowing past twenty repositories must not
|
|
23
|
+
* be twenty tree reads against the reader's GitHub rate limit. A click asks at once.
|
|
14
24
|
*
|
|
15
25
|
* The list only ever shows what the Jx Suite GitHub App can reach, so the dialog also carries an
|
|
16
26
|
* access footer: per-installation links to widen the App's repository selection, a link to install
|
|
@@ -18,17 +28,9 @@
|
|
|
18
28
|
* reloads the list without losing the dialog.
|
|
19
29
|
*
|
|
20
30
|
* **The dialog is a document.** `surfaces/add-repo.json` draws it and `surfaces/add-repo.ts` mounts
|
|
21
|
-
* it; this module keeps the listing, the ordering, the adoption and the promise.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* beside an `<sp-underlay>` and needed `z-index: 1000` to climb back out from under its own scrim.
|
|
25
|
-
* A modal `<dialog>` is in the top layer by construction, so none of that stacking is left to get
|
|
26
|
-
* wrong, and a filter keystroke now moves one binding rather than rebuilding every row.
|
|
27
|
-
*
|
|
28
|
-
* **`closeAddRepoModal` went with it.** It existed because the lit card had to answer its own
|
|
29
|
-
* Escape key, its own underlay click and its own close button, and all three of those belong to the
|
|
30
|
-
* platform now: every way out raises the dialog's `cancel`, which arrives here as `onClosed`. A
|
|
31
|
-
* closer nothing in the app called was left over from drawing the box by hand.
|
|
31
|
+
* it; this module keeps the listing, the ordering, the scans, the adoption and the promise. Every
|
|
32
|
+
* way out of it — Escape, Cancel, a programmatic close — is the platform's `cancel` on the dialog,
|
|
33
|
+
* which arrives here as `onClosed`.
|
|
32
34
|
*/
|
|
33
35
|
|
|
34
36
|
import { actIfRequired } from "../account/action-flow";
|
|
@@ -41,19 +43,48 @@ import {
|
|
|
41
43
|
} from "../account-status";
|
|
42
44
|
import { getPlatform } from "../platform";
|
|
43
45
|
import { layerHost } from "../ui/layers";
|
|
46
|
+
import { normalizeProjectDir } from "../utils/project-dir";
|
|
44
47
|
import { openAddRepoSurface } from "../surfaces/add-repo";
|
|
45
|
-
import type {
|
|
46
|
-
|
|
48
|
+
import type {
|
|
49
|
+
AddRepoLocation,
|
|
50
|
+
AddRepoRow,
|
|
51
|
+
AddRepoSurfaceHandle,
|
|
52
|
+
AddRepoView,
|
|
53
|
+
} from "../surfaces/add-repo";
|
|
54
|
+
import type { RepoInfo, RepoProjects } from "../types";
|
|
47
55
|
|
|
48
56
|
type PickerMode = "add" | "open";
|
|
49
57
|
|
|
58
|
+
/** How long a scan the caret asked for waits for the caret to move on. */
|
|
59
|
+
export const SCAN_DELAY_MS = 250;
|
|
60
|
+
|
|
61
|
+
/** What the dialog knows about one repository's projects. */
|
|
62
|
+
type Scan =
|
|
63
|
+
| { status: "loading" }
|
|
64
|
+
| { status: "done"; projects: RepoProjects }
|
|
65
|
+
| { status: "failed"; message: string };
|
|
66
|
+
|
|
50
67
|
let _handle: AddRepoSurfaceHandle | null = null;
|
|
51
68
|
let _mode: PickerMode = "add";
|
|
52
69
|
let _repos: RepoInfo[] | null = null;
|
|
53
70
|
let _filter = "";
|
|
71
|
+
/** The account the list is narrowed to; "" = every account. */
|
|
72
|
+
let _owner = "";
|
|
73
|
+
/** FullName of the chosen repository ("" = none). */
|
|
74
|
+
let _selected = "";
|
|
75
|
+
/** The folder field's text. */
|
|
76
|
+
let _folder = "";
|
|
77
|
+
/** The reader typed or picked a folder, so a scan landing late must not replace it. */
|
|
78
|
+
let _folderTouched = false;
|
|
79
|
+
/** A repository double-clicked before its scan landed: open it once the folder is unambiguous. */
|
|
80
|
+
let _pendingOpen = "";
|
|
81
|
+
let _scans = new Map<string, Scan>();
|
|
82
|
+
let _scanTimer: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
/** The repository whose scan is waiting on `_scanTimer` ("" = none). */
|
|
84
|
+
let _scanQueued = "";
|
|
54
85
|
let _error = "";
|
|
55
|
-
/**
|
|
56
|
-
let
|
|
86
|
+
/** An adoption is in flight. */
|
|
87
|
+
let _busy = false;
|
|
57
88
|
let _resolve: ((result: { root: string } | null) => void) | null = null;
|
|
58
89
|
|
|
59
90
|
/** True when the active platform can browse + adopt existing repositories. */
|
|
@@ -87,31 +118,65 @@ function openPicker(mode: PickerMode): Promise<{ root: string } | null> {
|
|
|
87
118
|
_mode = mode;
|
|
88
119
|
_repos = null;
|
|
89
120
|
_filter = "";
|
|
121
|
+
_owner = "";
|
|
122
|
+
_selected = "";
|
|
123
|
+
_folder = "";
|
|
124
|
+
_folderTouched = false;
|
|
125
|
+
_pendingOpen = "";
|
|
126
|
+
_scans = new Map();
|
|
90
127
|
_error = "";
|
|
91
|
-
|
|
128
|
+
_busy = false;
|
|
92
129
|
|
|
93
130
|
return new Promise((resolve) => {
|
|
94
131
|
_resolve = resolve;
|
|
95
132
|
_handle = openAddRepoSurface({
|
|
96
133
|
layer: layerHost("dialog"),
|
|
97
|
-
onChoose: (fullName) => {
|
|
98
|
-
void chooseRepo(fullName);
|
|
99
|
-
},
|
|
100
|
-
/* Escape, the Cancel button and a programmatic close all arrive here, and all of them mean
|
|
101
|
-
the same thing: nothing was adopted. A native `<dialog>` has already closed by the time it
|
|
102
|
-
says so, which is why this settles rather than deciding whether to allow it — an adoption
|
|
103
|
-
still in flight is dropped on the way out (`chooseRepo` finds the picker gone), exactly as
|
|
104
|
-
a listing that lands after a dismissal is. */
|
|
105
134
|
onClosed: () => {
|
|
135
|
+
/* Escape, the Cancel button and a programmatic close all arrive here, and all of them mean
|
|
136
|
+
the same thing: nothing was adopted. A native `<dialog>` has already closed by the time
|
|
137
|
+
it says so, which is why this settles rather than deciding whether to allow it — an
|
|
138
|
+
adoption still in flight is dropped on the way out (`adopt` finds the picker gone),
|
|
139
|
+
exactly as a listing or a scan that lands after a dismissal is. */
|
|
106
140
|
settle(null);
|
|
107
141
|
},
|
|
142
|
+
onConfirm: () => {
|
|
143
|
+
confirm();
|
|
144
|
+
},
|
|
108
145
|
onFilter: (value) => {
|
|
109
146
|
_filter = value;
|
|
110
147
|
redraw();
|
|
111
148
|
},
|
|
149
|
+
onFolder: (value) => {
|
|
150
|
+
_folder = value;
|
|
151
|
+
_folderTouched = true;
|
|
152
|
+
_error = "";
|
|
153
|
+
redraw();
|
|
154
|
+
},
|
|
155
|
+
onLocation: (dir) => {
|
|
156
|
+
_folder = dir;
|
|
157
|
+
_folderTouched = true;
|
|
158
|
+
_error = "";
|
|
159
|
+
redraw();
|
|
160
|
+
},
|
|
161
|
+
onMove: (delta) => {
|
|
162
|
+
move(delta);
|
|
163
|
+
},
|
|
164
|
+
onMoveLocation: (delta) => {
|
|
165
|
+
moveLocation(delta);
|
|
166
|
+
},
|
|
167
|
+
onOpenRepo: (fullName) => {
|
|
168
|
+
openRepo(fullName);
|
|
169
|
+
},
|
|
170
|
+
onOwner: (value) => {
|
|
171
|
+
_owner = value;
|
|
172
|
+
redraw();
|
|
173
|
+
},
|
|
112
174
|
onRefresh: () => {
|
|
113
175
|
loadRepos();
|
|
114
176
|
},
|
|
177
|
+
onSelect: (fullName) => {
|
|
178
|
+
select(fullName, "now");
|
|
179
|
+
},
|
|
115
180
|
view: viewOf(),
|
|
116
181
|
});
|
|
117
182
|
loadRepos();
|
|
@@ -120,11 +185,15 @@ function openPicker(mode: PickerMode): Promise<{ root: string } | null> {
|
|
|
120
185
|
|
|
121
186
|
/**
|
|
122
187
|
* (Re)load the repository list and the App's installation coverage. Both feed the same question —
|
|
123
|
-
* "which repositories can Jx see?" — so a refresh after a permission change re-reads both.
|
|
188
|
+
* "which repositories can Jx see?" — so a refresh after a permission change re-reads both. The
|
|
189
|
+
* scans go too: what a refresh is for is usually a change the reader just made on GitHub, and a
|
|
190
|
+
* `project.json` they just pushed is one.
|
|
124
191
|
*/
|
|
125
192
|
function loadRepos(): void {
|
|
126
193
|
_repos = null;
|
|
127
194
|
_error = "";
|
|
195
|
+
cancelQueuedScan();
|
|
196
|
+
_scans = new Map();
|
|
128
197
|
// Paints the loading state on a refresh; a no-op on open, where the surface opens on this view.
|
|
129
198
|
redraw();
|
|
130
199
|
|
|
@@ -134,6 +203,9 @@ function loadRepos(): void {
|
|
|
134
203
|
.listRepos?.()
|
|
135
204
|
.then((repos) => {
|
|
136
205
|
_repos = repos;
|
|
206
|
+
if (_selected) {
|
|
207
|
+
requestScan(_selected, "now");
|
|
208
|
+
}
|
|
137
209
|
})
|
|
138
210
|
.catch((error: unknown) => {
|
|
139
211
|
_repos = [];
|
|
@@ -163,67 +235,310 @@ function settle(result: { root: string } | null): void {
|
|
|
163
235
|
const resolve = _resolve;
|
|
164
236
|
_handle = null;
|
|
165
237
|
_resolve = null;
|
|
166
|
-
|
|
238
|
+
_busy = false;
|
|
239
|
+
cancelQueuedScan();
|
|
167
240
|
resolve?.(result);
|
|
168
241
|
handle?.close();
|
|
169
242
|
}
|
|
170
243
|
|
|
244
|
+
/** The repositories the current mode offers, before the account and the filter narrow them. */
|
|
245
|
+
function offeredRepos(): RepoInfo[] {
|
|
246
|
+
const repos = _repos ?? [];
|
|
247
|
+
if (_mode !== "open") {
|
|
248
|
+
return repos;
|
|
249
|
+
}
|
|
250
|
+
// Open Project offers only repos the user can write to; Jx-tagged repos surface first.
|
|
251
|
+
// The topic is an accelerator, not ground truth — untagged repos still open via importProject.
|
|
252
|
+
const writable = repos.filter((r) => r.permission === "admin" || r.permission === "write");
|
|
253
|
+
return [...writable.filter((r) => r.isJxProject), ...writable.filter((r) => !r.isJxProject)];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function visibleRepos(): RepoInfo[] {
|
|
257
|
+
const query = _filter.trim().toLowerCase();
|
|
258
|
+
const repos = offeredRepos().filter((r) => !_owner || r.owner === _owner);
|
|
259
|
+
return query ? repos.filter((r) => r.fullName.toLowerCase().includes(query)) : repos;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function selectedRepo(): RepoInfo | undefined {
|
|
263
|
+
return _selected ? (_repos ?? []).find((repo) => repo.fullName === _selected) : undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
171
266
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
267
|
+
* Choose a repository, and ask what it holds.
|
|
268
|
+
*
|
|
269
|
+
* `"now"` for a pointer, which means it; `"soon"` for the caret, which may only be passing through.
|
|
270
|
+
* The folder goes back to untouched, so the new repository's own default can land in it.
|
|
174
271
|
*/
|
|
175
|
-
|
|
176
|
-
if (
|
|
272
|
+
function select(fullName: string, when: "now" | "soon"): void {
|
|
273
|
+
if (fullName === _selected || _busy) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
_selected = fullName;
|
|
277
|
+
_folder = "";
|
|
278
|
+
_folderTouched = false;
|
|
279
|
+
_pendingOpen = "";
|
|
280
|
+
_error = "";
|
|
281
|
+
requestScan(fullName, when);
|
|
282
|
+
redraw();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Move the repository caret by `delta` rows; from nothing, Down lands on the first and Up the last. */
|
|
286
|
+
function move(delta: number): void {
|
|
287
|
+
const rows = visibleRepos();
|
|
288
|
+
if (rows.length === 0) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const at = rows.findIndex((repo) => repo.fullName === _selected);
|
|
292
|
+
const next =
|
|
293
|
+
at === -1
|
|
294
|
+
? delta > 0
|
|
295
|
+
? 0
|
|
296
|
+
: rows.length - 1
|
|
297
|
+
: Math.min(rows.length - 1, Math.max(0, at + delta));
|
|
298
|
+
const target = rows[next];
|
|
299
|
+
if (target) {
|
|
300
|
+
select(target.fullName, "soon");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The folders the chosen repository's scan found; [] until one has. */
|
|
305
|
+
function scannedLocations(): RepoProjects["locations"] {
|
|
306
|
+
const scanned = _scans.get(_selected);
|
|
307
|
+
return scanned?.status === "done" ? scanned.projects.locations : [];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Move the folder caret by `delta` rows, writing the row's folder into the field as it goes. */
|
|
311
|
+
function moveLocation(delta: number): void {
|
|
312
|
+
const locations = scannedLocations();
|
|
313
|
+
if (locations.length === 0) {
|
|
177
314
|
return;
|
|
178
315
|
}
|
|
316
|
+
const current = normalizeProjectDir(_folder);
|
|
317
|
+
const at = locations.findIndex((location) => location.dir === current);
|
|
318
|
+
const next =
|
|
319
|
+
at === -1
|
|
320
|
+
? delta > 0
|
|
321
|
+
? 0
|
|
322
|
+
: locations.length - 1
|
|
323
|
+
: (at + delta + locations.length) % locations.length;
|
|
324
|
+
const target = locations[next];
|
|
325
|
+
if (target) {
|
|
326
|
+
_folder = target.dir;
|
|
327
|
+
_folderTouched = true;
|
|
328
|
+
_error = "";
|
|
329
|
+
redraw();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Ask the platform which folders of `fullName` hold a project.json, unless it already said. */
|
|
334
|
+
function requestScan(fullName: string, when: "now" | "soon"): void {
|
|
335
|
+
const platform = getPlatform();
|
|
179
336
|
const repo = (_repos ?? []).find((candidate) => candidate.fullName === fullName);
|
|
180
|
-
if (!repo) {
|
|
337
|
+
if (!platform.listRepoProjects || !repo) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const known = _scans.get(fullName);
|
|
341
|
+
if (known) {
|
|
342
|
+
if (known.status === "done") {
|
|
343
|
+
applyDefaultFolder(fullName);
|
|
344
|
+
}
|
|
181
345
|
return;
|
|
182
346
|
}
|
|
183
|
-
|
|
347
|
+
cancelQueuedScan();
|
|
348
|
+
_scans.set(fullName, { status: "loading" });
|
|
349
|
+
if (when === "now") {
|
|
350
|
+
void scan(repo);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
_scanQueued = fullName;
|
|
354
|
+
_scanTimer = setTimeout(() => {
|
|
355
|
+
_scanTimer = null;
|
|
356
|
+
_scanQueued = "";
|
|
357
|
+
void scan(repo);
|
|
358
|
+
}, SCAN_DELAY_MS);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Drop the scan waiting on the timer, if one is. Its "loading" entry goes with it: the caret moved
|
|
363
|
+
* on before the wait was over, and a repository left marked as loading would never be asked again
|
|
364
|
+
* when the caret came back to it.
|
|
365
|
+
*/
|
|
366
|
+
function cancelQueuedScan(): void {
|
|
367
|
+
if (_scanTimer === null) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
clearTimeout(_scanTimer);
|
|
371
|
+
_scanTimer = null;
|
|
372
|
+
if (_scanQueued) {
|
|
373
|
+
_scans.delete(_scanQueued);
|
|
374
|
+
_scanQueued = "";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function scan(repo: RepoInfo): Promise<void> {
|
|
379
|
+
const handle = _handle;
|
|
380
|
+
let outcome: Scan;
|
|
381
|
+
try {
|
|
382
|
+
const projects = await getPlatform().listRepoProjects!({ name: repo.name, owner: repo.owner });
|
|
383
|
+
outcome = { projects, status: "done" };
|
|
384
|
+
} catch (error) {
|
|
385
|
+
outcome = { message: errorMessage(error), status: "failed" };
|
|
386
|
+
}
|
|
387
|
+
// A scan that lands after the dialog closed — or after a refresh dropped it — has nowhere to go.
|
|
388
|
+
if (_handle !== handle || _scans.get(repo.fullName)?.status !== "loading") {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
_scans.set(repo.fullName, outcome);
|
|
392
|
+
if (repo.fullName === _selected) {
|
|
393
|
+
applyDefaultFolder(repo.fullName);
|
|
394
|
+
if (_pendingOpen === repo.fullName) {
|
|
395
|
+
_pendingOpen = "";
|
|
396
|
+
if (outcome.status === "done" && outcome.projects.locations.length === 1) {
|
|
397
|
+
confirm();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
redraw();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The folder a scan suggests, written into the field the reader has not touched: the root when it
|
|
407
|
+
* is a project, since that is what the repository is; otherwise the first, which the scan orders
|
|
408
|
+
* shallowest-first. A scan that found nothing leaves the field empty.
|
|
409
|
+
*/
|
|
410
|
+
function applyDefaultFolder(fullName: string): void {
|
|
411
|
+
const scanned = _scans.get(fullName);
|
|
412
|
+
if (_folderTouched || fullName !== _selected || scanned?.status !== "done") {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const { locations } = scanned.projects;
|
|
416
|
+
_folder = (locations.find((location) => location.dir === "") ?? locations[0])?.dir ?? "";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* A double-click on a repository row: choose it, and open it if the folder is already settled. A
|
|
421
|
+
* scan still running settles it later — and opens only if it finds exactly one project, because a
|
|
422
|
+
* repository with several needs the reader to say which.
|
|
423
|
+
*/
|
|
424
|
+
function openRepo(fullName: string): void {
|
|
425
|
+
select(fullName, "now");
|
|
426
|
+
const scanned = _scans.get(fullName);
|
|
427
|
+
if (scanned?.status === "loading") {
|
|
428
|
+
_pendingOpen = fullName;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (scanned?.status === "done" && scanned.projects.locations.length > 1 && !_folderTouched) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
confirm();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Why the folder field's text cannot be opened; "" when it can. */
|
|
438
|
+
function folderErrorOf(): string {
|
|
439
|
+
return normalizeProjectDir(_folder) === null
|
|
440
|
+
? "A project folder is a path inside the repository, without . or .. segments."
|
|
441
|
+
: "";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Whether confirming would adopt something, and the one reason it would not when it cannot. */
|
|
445
|
+
function canConfirm(): boolean {
|
|
446
|
+
const repo = selectedRepo();
|
|
447
|
+
if (!repo || _busy || folderErrorOf() !== "") {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const scanned = _scans.get(repo.fullName);
|
|
451
|
+
if (_folderTouched || !scanned) {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
// Untouched, the folder is the scan's suggestion: wait for it, and offer nothing when it found none.
|
|
455
|
+
return (
|
|
456
|
+
scanned.status === "failed" ||
|
|
457
|
+
(scanned.status === "done" &&
|
|
458
|
+
(scanned.projects.locations.length > 0 || scanned.projects.truncated))
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The primary answer: the confirm button, Enter in either field, a double-click. With nothing
|
|
464
|
+
* chosen, Enter in the filter chooses the first visible repository instead — the one a reader who
|
|
465
|
+
* typed its name and pressed Enter meant.
|
|
466
|
+
*/
|
|
467
|
+
function confirm(): void {
|
|
468
|
+
if (!_selected) {
|
|
469
|
+
const [first] = visibleRepos();
|
|
470
|
+
if (first) {
|
|
471
|
+
select(first.fullName, "now");
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (!canConfirm()) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
void adopt(_selected, normalizeProjectDir(_folder) ?? "");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Adopt the chosen repository at `dir`. A refusal that offers an action (desktop.md §10.4) is
|
|
483
|
+
* offered, and an action that is done runs the import again, once.
|
|
484
|
+
*/
|
|
485
|
+
async function adopt(fullName: string, dir: string, retried = false): Promise<void> {
|
|
486
|
+
const repo = (_repos ?? []).find((candidate) => candidate.fullName === fullName);
|
|
487
|
+
if (!repo || (_busy && !retried)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const handle = _handle;
|
|
491
|
+
_busy = true;
|
|
184
492
|
_error = "";
|
|
185
493
|
redraw();
|
|
186
494
|
try {
|
|
187
|
-
const imported = await getPlatform().importProject?.({
|
|
495
|
+
const imported = await getPlatform().importProject?.({
|
|
496
|
+
name: repo.name,
|
|
497
|
+
owner: repo.owner,
|
|
498
|
+
...(dir ? { dir } : {}),
|
|
499
|
+
});
|
|
500
|
+
if (_handle !== handle) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
188
503
|
if (imported) {
|
|
189
504
|
settle(imported);
|
|
190
505
|
return;
|
|
191
506
|
}
|
|
192
507
|
_error = "This platform cannot import repositories.";
|
|
193
508
|
} catch (error) {
|
|
509
|
+
if (_handle !== handle) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
194
512
|
_error = errorMessage(error);
|
|
195
|
-
|
|
513
|
+
_busy = false;
|
|
196
514
|
redraw();
|
|
197
|
-
if (!retried && (await actIfRequired(error))) {
|
|
198
|
-
await
|
|
515
|
+
if (!retried && (await actIfRequired(error)) && _handle === handle) {
|
|
516
|
+
await adopt(fullName, dir, true);
|
|
199
517
|
}
|
|
200
518
|
return;
|
|
201
519
|
}
|
|
202
|
-
|
|
520
|
+
_busy = false;
|
|
203
521
|
redraw();
|
|
204
522
|
}
|
|
205
523
|
|
|
206
|
-
function visibleRepos(): RepoInfo[] {
|
|
207
|
-
const query = _filter.trim().toLowerCase();
|
|
208
|
-
let repos = _repos ?? [];
|
|
209
|
-
if (_mode === "open") {
|
|
210
|
-
// Open Project offers only repos the user can write to; Jx-tagged repos surface first.
|
|
211
|
-
// The topic is an accelerator, not ground truth — untagged repos still open via importProject.
|
|
212
|
-
const writable = repos.filter((r) => r.permission === "admin" || r.permission === "write");
|
|
213
|
-
repos = [...writable.filter((r) => r.isJxProject), ...writable.filter((r) => !r.isJxProject)];
|
|
214
|
-
}
|
|
215
|
-
return query ? repos.filter((r) => r.fullName.toLowerCase().includes(query)) : repos;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
524
|
/** One repository as the list draws it: strings and flags, nothing the document has to interpret. */
|
|
219
525
|
function rowOf(repo: RepoInfo): AddRepoRow {
|
|
220
526
|
return {
|
|
221
|
-
disabled: _importing !== "",
|
|
222
527
|
fullName: repo.fullName,
|
|
223
|
-
importing: _importing === repo.fullName,
|
|
224
528
|
isJx: repo.isJxProject,
|
|
225
529
|
isPrivate: repo.private,
|
|
226
|
-
|
|
530
|
+
// The branch alone: the permission is the chosen repository's detail, not every row's.
|
|
531
|
+
meta: repo.defaultBranch,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** One scanned folder as the folder list draws it. */
|
|
536
|
+
function locationOf(location: RepoProjects["locations"][number], repo: RepoInfo): AddRepoLocation {
|
|
537
|
+
const folderName = location.dir.slice(location.dir.lastIndexOf("/") + 1);
|
|
538
|
+
return {
|
|
539
|
+
dir: location.dir,
|
|
540
|
+
label: location.name ?? (location.dir ? folderName : repo.name),
|
|
541
|
+
path: location.dir ? `${location.dir}/` : "/",
|
|
227
542
|
};
|
|
228
543
|
}
|
|
229
544
|
|
|
@@ -235,7 +550,7 @@ function rowOf(repo: RepoInfo): AddRepoRow {
|
|
|
235
550
|
* an App that was never installed, and an App that is installed and reaches nothing.
|
|
236
551
|
*/
|
|
237
552
|
function emptyStateOf(): AddRepoView["emptyState"] {
|
|
238
|
-
if (_filter) {
|
|
553
|
+
if (_filter || _owner) {
|
|
239
554
|
return "filter";
|
|
240
555
|
}
|
|
241
556
|
if (_mode === "open" && (_repos ?? []).length > 0) {
|
|
@@ -244,6 +559,23 @@ function emptyStateOf(): AddRepoView["emptyState"] {
|
|
|
244
559
|
return needsAppInstall() ? "install" : "none";
|
|
245
560
|
}
|
|
246
561
|
|
|
562
|
+
/**
|
|
563
|
+
* The account filter: every account the offered repositories belong to, after "All accounts" — or
|
|
564
|
+
* nothing at all when they all belong to one, since there is nothing to narrow by.
|
|
565
|
+
*/
|
|
566
|
+
function ownersOf(): AddRepoView["owners"] {
|
|
567
|
+
const owners = [...new Set(offeredRepos().map((repo) => repo.owner))].toSorted((a, b) =>
|
|
568
|
+
a.localeCompare(b),
|
|
569
|
+
);
|
|
570
|
+
if (owners.length < 2) {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
return [
|
|
574
|
+
{ label: "All accounts", value: "" },
|
|
575
|
+
...owners.map((owner) => ({ label: owner, value: owner })),
|
|
576
|
+
];
|
|
577
|
+
}
|
|
578
|
+
|
|
247
579
|
/**
|
|
248
580
|
* Repository-access footer: the list is bounded by what the Jx Suite App was granted, so every mode
|
|
249
581
|
* offers a way out of that boundary — widen an existing installation, install on another account,
|
|
@@ -272,16 +604,84 @@ function accessOf(): AddRepoView["access"] {
|
|
|
272
604
|
];
|
|
273
605
|
}
|
|
274
606
|
|
|
607
|
+
/** The chosen repository's pane: its state, its rows, and the sentence under the folder field. */
|
|
608
|
+
function detailOf(repo: RepoInfo | undefined): Pick<
|
|
609
|
+
AddRepoView,
|
|
610
|
+
"detailState" | "detailName" | "detailMeta" | "locations" | "folderHelp"
|
|
611
|
+
> & {
|
|
612
|
+
branch: string;
|
|
613
|
+
} {
|
|
614
|
+
if (!repo) {
|
|
615
|
+
return {
|
|
616
|
+
branch: "",
|
|
617
|
+
detailMeta: "",
|
|
618
|
+
detailName: "",
|
|
619
|
+
detailState: "none",
|
|
620
|
+
folderHelp: "",
|
|
621
|
+
locations: [],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const scanned = _scans.get(repo.fullName);
|
|
625
|
+
const branch = scanned?.status === "done" ? scanned.projects.branch : repo.defaultBranch;
|
|
626
|
+
const base = {
|
|
627
|
+
branch,
|
|
628
|
+
detailMeta: [branch, repo.permission, ...(repo.private ? ["private"] : [])].join(" · "),
|
|
629
|
+
detailName: repo.fullName,
|
|
630
|
+
folderHelp: "The folder that holds project.json. Leave it empty for the repository root.",
|
|
631
|
+
locations: [] as AddRepoLocation[],
|
|
632
|
+
};
|
|
633
|
+
if (!getPlatform().listRepoProjects) {
|
|
634
|
+
return { ...base, detailState: "unlisted" };
|
|
635
|
+
}
|
|
636
|
+
if (!scanned || scanned.status === "loading") {
|
|
637
|
+
return { ...base, detailState: "loading" };
|
|
638
|
+
}
|
|
639
|
+
if (scanned.status === "failed") {
|
|
640
|
+
return { ...base, detailState: "failed" };
|
|
641
|
+
}
|
|
642
|
+
const { locations, truncated } = scanned.projects;
|
|
643
|
+
return {
|
|
644
|
+
...base,
|
|
645
|
+
detailState: locations.length > 0 ? "listed" : "empty",
|
|
646
|
+
...(truncated
|
|
647
|
+
? {
|
|
648
|
+
folderHelp:
|
|
649
|
+
"This repository is too large to scan in full. Name the folder if its project isn't listed.",
|
|
650
|
+
}
|
|
651
|
+
: {}),
|
|
652
|
+
locations: locations.map((location) => locationOf(location, repo)),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
275
656
|
/** Everything the dialog draws, as one record. The only place this module's state becomes a view. */
|
|
276
657
|
function viewOf(): AddRepoView {
|
|
658
|
+
const repo = selectedRepo();
|
|
659
|
+
const { branch, ...detail } = detailOf(repo);
|
|
660
|
+
const folderError = folderErrorOf();
|
|
661
|
+
const folder = normalizeProjectDir(_folder) ?? "";
|
|
662
|
+
const verb = _mode === "open" ? "Open" : "Add";
|
|
277
663
|
return {
|
|
664
|
+
...detail,
|
|
278
665
|
access: accessOf(),
|
|
666
|
+
busy: _busy,
|
|
667
|
+
canConfirm: canConfirm(),
|
|
668
|
+
confirmLabel: _busy ? (_mode === "open" ? "Opening…" : "Adding…") : verb,
|
|
279
669
|
emptyState: emptyStateOf(),
|
|
280
670
|
failure: _error,
|
|
671
|
+
chosenFolder: folder,
|
|
281
672
|
filter: _filter,
|
|
673
|
+
folder: _folder,
|
|
674
|
+
folderError,
|
|
282
675
|
installUrl: getAccountStatus()?.appInstallUrl ?? "",
|
|
283
676
|
loading: _repos === null,
|
|
284
|
-
|
|
677
|
+
owner: _owner,
|
|
678
|
+
owners: ownersOf(),
|
|
679
|
+
rows: visibleRepos().map((candidate) => rowOf(candidate)),
|
|
680
|
+
selected: _selected,
|
|
681
|
+
summary:
|
|
682
|
+
repo && folderError === ""
|
|
683
|
+
? `${verb}s ${repo.fullName} ${folder ? `at ${folder}/` : "at the repository root"}${branch ? ` on ${branch}` : ""}.`
|
|
684
|
+
: "",
|
|
285
685
|
title: _mode === "open" ? "Open Project" : "Add existing repository",
|
|
286
686
|
};
|
|
287
687
|
}
|
package/src/panels/editors.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { globalEntries, namedFormulaEntries } from "../ui/formula-catalog";
|
|
|
52
52
|
import { notify } from "../services/notify";
|
|
53
53
|
|
|
54
54
|
import type { BufferWrites } from "../services/monaco-buffer";
|
|
55
|
+
import { noteTyping } from "../tabs/typing";
|
|
55
56
|
import type { OxLintDiagnostic } from "../services/code-services";
|
|
56
57
|
import type { JxPrototypeDef } from "@jxsuite/schema/types";
|
|
57
58
|
import type { JxPath } from "../state";
|
|
@@ -500,8 +501,9 @@ async function mountFunctionEditor(
|
|
|
500
501
|
// Stated as a fact rather than left to be read off the timer, because the timer is only ONE of
|
|
501
502
|
// The ways a buffer gets ahead and the other one is invisible to it. `markTyped` rather than
|
|
502
503
|
// `markAhead`: this text is the AUTHOR'S, so it is also the text a close would destroy, and
|
|
503
|
-
// Format-on-open's (below) is not.
|
|
504
|
+
// Format-on-open's (below) is not. Noted for Save too, so it is enabled ahead of the commit.
|
|
504
505
|
writes.markTyped();
|
|
506
|
+
noteTyping(tab.id);
|
|
505
507
|
|
|
506
508
|
writes.arm(BUFFER_COMMIT, 500, () => {
|
|
507
509
|
editor._commitBody?.(editor.getValue());
|