@jxsuite/studio 5.5.0 → 5.6.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/LICENSE +1 -1
- package/dist/chunks/{2020-g8mwpbkq.js → 2020-zvax9fmb.js} +3 -3
- package/dist/chunks/adapter-6zhjbpmd.js +4 -0
- package/dist/chunks/cf-account-picker-zfycs4k2.js +4 -0
- package/dist/chunks/cf-settings-fpa485q3.js +4 -0
- package/dist/chunks/components-b0vh4sq8.js +4 -0
- package/dist/chunks/content-source-z5t0d0h9.js +4 -0
- package/dist/chunks/convert-to-repeater-x19jr6c7.js +4 -0
- package/dist/chunks/editor-7e23n4a8.js +4 -0
- package/dist/chunks/entry-commands-5cm6aba2.js +4 -0
- package/dist/chunks/extension-sections-y90sa0n4.js +4 -0
- package/dist/chunks/file-ops-hv7gf757.js +4 -0
- package/dist/chunks/files-kb0epdxf.js +4 -0
- package/dist/chunks/format-host-eg1wy2bp.js +4 -0
- package/dist/chunks/fs-events-ky3vxfmh.js +4 -0
- package/dist/chunks/git-panel-yyg3bzp2.js +4 -0
- package/dist/chunks/grid-view-5mqv1cbd.css +1 -0
- package/dist/chunks/grid-view-d9mybhn0.js +51 -0
- package/dist/chunks/iframe-host-9wdf0hes.js +4 -0
- package/dist/chunks/index-2tf0m2ne.js +4 -0
- package/dist/chunks/{index-arg188jq.js → index-vnwy9d44.js} +3 -3
- package/dist/chunks/{javascript-zcdmye2k.js → javascript-f0nrbdjb.js} +2 -2
- package/dist/chunks/{jsonMode-6x5e1nbf.js → jsonMode-nfa90dtf.js} +5 -5
- package/dist/chunks/jx-validate-7w11db9v.js +4 -0
- package/dist/chunks/layers-7zasrpse.js +4 -0
- package/dist/chunks/layers-panel-9nzc4fdq.js +4 -0
- package/dist/chunks/lit-html-5qr7nbng.js +4 -0
- package/dist/chunks/media-picker-maevn9d4.js +4 -0
- package/dist/chunks/menu-wq6q0v3y.js +4 -0
- package/dist/chunks/{monaco-binding-4az2kh5p.js → monaco-binding-antn79jf.js} +3 -3
- package/dist/chunks/monaco-buffer-9ry2rn5x.js +4 -0
- package/dist/chunks/{monaco-setup-d7mwkcx3.js → monaco-setup-k8s40vwg.js} +3 -3
- package/dist/chunks/notify-g91gjd6w.js +4 -0
- package/dist/chunks/platform-jdwjp2vt.js +4 -0
- package/dist/chunks/publish-panel-8yym447p.js +4 -0
- package/dist/chunks/quick-search-9bzm9kh3.js +4 -0
- package/dist/chunks/references-znssfqvq.js +4 -0
- package/dist/chunks/right-panel-czbzktm8.js +4 -0
- package/dist/chunks/section-registry-0rtq73qs.js +4 -0
- package/dist/chunks/studio-011em5cf.js +5 -0
- package/dist/chunks/studio-02y9cb4h.js +6 -0
- package/dist/chunks/{studio-7gqyhjns.js → studio-09n3fyy0.js} +4 -4
- package/dist/chunks/{studio-xcng8nx7.js → studio-0c3fpdqj.js} +5 -5
- package/dist/chunks/studio-0mm38y62.js +5 -0
- package/dist/chunks/studio-0nm2skex.js +4 -0
- package/dist/chunks/{studio-73czgk6a.js → studio-153mwkyq.js} +4 -4
- package/dist/chunks/{studio-f8jyp45j.js → studio-1pxvgx1t.js} +4 -4
- package/dist/chunks/studio-27tc8q0h.js +5 -0
- package/dist/chunks/studio-28pz4s1k.js +5 -0
- package/dist/chunks/{studio-x5esgzct.js → studio-36s6qjgq.js} +4 -4
- package/dist/chunks/studio-4yztve2t.js +5 -0
- package/dist/chunks/{studio-x9sagq2d.js → studio-5fc0728f.js} +7 -7
- package/dist/chunks/{studio-7jazdy6y.js → studio-654fxcr0.js} +4 -4
- package/dist/chunks/studio-6exg5z4x.js +4 -0
- package/dist/chunks/studio-6k1eswp9.js +5 -0
- package/dist/chunks/{studio-5wfrgee9.js → studio-6zyt56ek.js} +4 -4
- package/dist/chunks/studio-7djymzt2.js +5 -0
- package/dist/chunks/{studio-b4epb26c.js → studio-7w04zw8h.js} +7 -7
- package/dist/chunks/studio-805fwp9k.js +5 -0
- package/dist/chunks/{studio-k1eaqyzm.js → studio-81twpmzj.js} +4 -4
- package/dist/chunks/studio-891t1tv9.js +5 -0
- package/dist/chunks/{studio-5n1km79v.js → studio-9n2j9nqk.js} +4 -4
- package/dist/chunks/{studio-he2fhrgp.js → studio-9tkaykt5.js} +4 -4
- package/dist/chunks/studio-ant1zbp3.js +5 -0
- package/dist/chunks/studio-b29h7g7s.js +5 -0
- package/dist/chunks/studio-b5rxwkj2.js +5 -0
- package/dist/chunks/studio-d007j0z3.js +7 -0
- package/dist/chunks/studio-dknr8m7b.js +14 -0
- package/dist/chunks/{studio-eq27rrr0.js → studio-dmymvk2j.js} +3 -3
- package/dist/chunks/studio-dtmdjmqb.js +5 -0
- package/dist/chunks/studio-eb5sty2k.js +5 -0
- package/dist/chunks/{studio-hc9q4j24.js → studio-ebt49j3c.js} +5 -5
- package/dist/chunks/studio-eegyf5vg.js +5 -0
- package/dist/chunks/studio-frv7vjhw.js +5 -0
- package/dist/chunks/studio-ft8pfpp0.js +5 -0
- package/dist/chunks/{studio-mhhc9btt.js → studio-gj4j9e7k.js} +4 -4
- package/dist/chunks/{studio-hf4qdjc8.js → studio-gxna6yta.js} +4 -4
- package/dist/chunks/studio-je15p5v8.js +6 -0
- package/dist/chunks/{studio-21enxdr8.js → studio-jsr14n1s.js} +4 -4
- package/dist/chunks/studio-k9pwvmz8.js +5 -0
- package/dist/chunks/{studio-p1eggxcf.js → studio-ks0ecgjx.js} +3 -3
- package/dist/chunks/studio-mb8jhtr4.js +64 -0
- package/dist/chunks/studio-mf41717j.js +5 -0
- package/dist/chunks/{studio-625ezz59.js → studio-mm8x32gg.js} +4 -4
- package/dist/chunks/{studio-pkkw32j0.js → studio-n2e133d6.js} +4 -4
- package/dist/chunks/{studio-c4pnt1c6.js → studio-njmg0e73.js} +5 -5
- package/dist/chunks/studio-nvykrgbj.js +4 -0
- package/dist/chunks/studio-nw4q9sdn.js +5 -0
- package/dist/chunks/studio-p6avneaq.js +5 -0
- package/dist/chunks/studio-pvj970st.js +5 -0
- package/dist/chunks/studio-r7yeeqc4.js +5 -0
- package/dist/chunks/studio-s85vah66.js +6 -0
- package/dist/chunks/studio-sadckcpp.js +7 -0
- package/dist/chunks/studio-sgzss898.js +9 -0
- package/dist/chunks/{studio-7by9a93k.js → studio-t0v47dpc.js} +4 -4
- package/dist/chunks/{studio-jnb285we.js → studio-tj28pxa1.js} +4 -4
- package/dist/chunks/{studio-6j2jn55a.js → studio-tzs6njnb.js} +8 -8
- package/dist/chunks/studio-vz7b43ng.js +5 -0
- package/dist/chunks/studio-w8w9frjy.js +16 -0
- package/dist/chunks/{studio-xc8ebsts.js → studio-wr0pq67n.js} +4 -4
- package/dist/chunks/{studio-8r4y2x0z.js → studio-y164brht.js} +4 -4
- package/dist/chunks/studio-y17505ge.js +928 -0
- package/dist/chunks/studio-ymsjnvp9.js +5 -0
- package/dist/chunks/studio-z1vdxq0e.js +5 -0
- package/dist/chunks/studio-z47mm077.js +10 -0
- package/dist/chunks/studio-zkkdbbvd.js +9 -0
- package/dist/chunks/{studio-awqghqxv.js → studio-zkvyd3ev.js} +4 -4
- package/dist/chunks/{tsMode-rtwt7w1a.js → tsMode-eq26t9gs.js} +5 -5
- package/dist/chunks/workspace-emt8454d.js +4 -0
- package/dist/chunks/ws-client-3gxqp06s.js +4 -0
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +62 -59
- package/dist/studio.css +1 -1
- package/dist/studio.js +115 -117
- package/package.json +5 -5
- package/src/account/action-flow.ts +294 -0
- package/src/account/action-required.ts +68 -0
- package/src/account-status.ts +29 -1
- package/src/browse/library-model.ts +3 -1
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +3 -1
- package/src/canvas/asset-resolve.ts +79 -1
- package/src/canvas/iframe-entry.ts +38 -1
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-protocol.ts +13 -0
- package/src/files/asset-versions.ts +294 -0
- package/src/files/files.ts +61 -9
- package/src/files/fs-events.ts +279 -28
- package/src/files/media-meta.ts +2 -1
- package/src/files/media-paths.ts +5 -2
- package/src/files/media-upload.ts +5 -1
- package/src/new-project/add-repo-modal.ts +12 -1
- package/src/new-project/new-project-modal.ts +12 -1
- package/src/panels/git-panel.ts +25 -2
- package/src/platform-errors.ts +61 -1
- package/src/platforms/cloud.ts +389 -50
- package/src/platforms/devserver.ts +17 -1
- package/src/publish/pages-service.ts +17 -7
- package/src/services/ai-models.ts +30 -6
- package/src/services/ai-system-prompt.ts +81 -51
- package/src/settings/preferences-accounts.ts +42 -3
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/studio.ts +18 -1
- package/src/surfaces/dialog.ts +5 -0
- package/src/surfaces/welcome.json +86 -1
- package/src/surfaces/welcome.ts +61 -2
- package/src/types.ts +84 -2
- package/src/ui/ai-managed-connect.ts +41 -4
- package/src/ui/media-picker.ts +2 -1
- package/dist/chunks/adapter-bsbs78x9.js +0 -4
- package/dist/chunks/cf-account-picker-gnm7xeqb.js +0 -4
- package/dist/chunks/cf-settings-9y6jyscp.js +0 -4
- package/dist/chunks/components-s51h3b7c.js +0 -4
- package/dist/chunks/content-source-ty3be0qg.js +0 -4
- package/dist/chunks/convert-to-repeater-c3e7q61z.js +0 -4
- package/dist/chunks/editor-t002b41m.js +0 -4
- package/dist/chunks/entry-commands-sb72e9gy.js +0 -4
- package/dist/chunks/extension-sections-ktn2ntnj.js +0 -4
- package/dist/chunks/file-ops-78zeytkm.js +0 -4
- package/dist/chunks/files-gatzrz8v.js +0 -4
- package/dist/chunks/format-host-r59b1651.js +0 -4
- package/dist/chunks/fs-events-f8h91ndt.js +0 -4
- package/dist/chunks/git-panel-y12zf7nj.js +0 -4
- package/dist/chunks/grid-view-rfd8b2pg.js +0 -51
- package/dist/chunks/grid-view-tafenfm2.css +0 -1
- package/dist/chunks/iframe-host-vsrsj20g.js +0 -4
- package/dist/chunks/index-5aghyrfh.js +0 -4
- package/dist/chunks/jx-validate-653krkrf.js +0 -4
- package/dist/chunks/layers-32p8gtae.js +0 -4
- package/dist/chunks/layers-panel-rs58m105.js +0 -4
- package/dist/chunks/lit-html-54bmkwz3.js +0 -4
- package/dist/chunks/media-picker-5bb04wct.js +0 -4
- package/dist/chunks/menu-cbha7w2e.js +0 -4
- package/dist/chunks/monaco-buffer-9vbbyfr1.js +0 -4
- package/dist/chunks/notify-dc1nz5ar.js +0 -4
- package/dist/chunks/platform-2m49s1zf.js +0 -4
- package/dist/chunks/publish-panel-nx3ycmma.js +0 -4
- package/dist/chunks/quick-search-yzrzh9x3.js +0 -4
- package/dist/chunks/references-vx7mbj9f.js +0 -4
- package/dist/chunks/right-panel-7jzpbxja.js +0 -4
- package/dist/chunks/section-registry-gjryse41.js +0 -4
- package/dist/chunks/studio-3znfhcxg.js +0 -5
- package/dist/chunks/studio-58qam27v.js +0 -4
- package/dist/chunks/studio-6j59fwaw.js +0 -5
- package/dist/chunks/studio-7z69g1gx.js +0 -5
- package/dist/chunks/studio-93j3v6zx.js +0 -5
- package/dist/chunks/studio-9gf6v8zy.js +0 -5
- package/dist/chunks/studio-9rhmettf.js +0 -4
- package/dist/chunks/studio-ac5aq677.js +0 -10
- package/dist/chunks/studio-as65vbns.js +0 -5
- package/dist/chunks/studio-bkv0sga4.js +0 -6
- package/dist/chunks/studio-bttmqef4.js +0 -5
- package/dist/chunks/studio-c3y9d41k.js +0 -5
- package/dist/chunks/studio-ccegp7gz.js +0 -9
- package/dist/chunks/studio-chxabn98.js +0 -6
- package/dist/chunks/studio-cktcdsxz.js +0 -16
- package/dist/chunks/studio-cnspfke5.js +0 -5
- package/dist/chunks/studio-de3p1ktm.js +0 -5
- package/dist/chunks/studio-fsbcn530.js +0 -928
- package/dist/chunks/studio-gftga7a3.js +0 -64
- package/dist/chunks/studio-hf1rpcq0.js +0 -4
- package/dist/chunks/studio-hz02hq70.js +0 -5
- package/dist/chunks/studio-jsv3s3s1.js +0 -5
- package/dist/chunks/studio-ks7cksh0.js +0 -5
- package/dist/chunks/studio-nptx0dbs.js +0 -5
- package/dist/chunks/studio-pg0z7k98.js +0 -5
- package/dist/chunks/studio-pve0b2f2.js +0 -5
- package/dist/chunks/studio-q7zmjv3b.js +0 -6
- package/dist/chunks/studio-rpmvw7kj.js +0 -5
- package/dist/chunks/studio-rwhss52s.js +0 -5
- package/dist/chunks/studio-sbg3g3h0.js +0 -5
- package/dist/chunks/studio-sq1bn60g.js +0 -14
- package/dist/chunks/studio-vwz7rma2.js +0 -5
- package/dist/chunks/studio-wa55e8gf.js +0 -5
- package/dist/chunks/studio-x0mzdha2.js +0 -5
- package/dist/chunks/studio-xrgpakvc.js +0 -9
- package/dist/chunks/studio-xt3djk1w.js +0 -7
- package/dist/chunks/studio-yh3nggx1.js +0 -7
- package/dist/chunks/studio-zmecp6hq.js +0 -5
- package/dist/chunks/workspace-g6dvaete.js +0 -4
- package/dist/chunks/ws-client-shf41j5p.js +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
|
|
63
63
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^3.0.0",
|
|
64
|
-
"@jxsuite/ai": "^0.39.
|
|
65
|
-
"@jxsuite/collab": "^0.
|
|
64
|
+
"@jxsuite/ai": "^0.39.1",
|
|
65
|
+
"@jxsuite/collab": "^0.11.0",
|
|
66
66
|
"@jxsuite/create": "^1.3.12",
|
|
67
67
|
"@jxsuite/formulas": "^0.0.19",
|
|
68
68
|
"@jxsuite/markup": "^0.4.11",
|
|
69
|
-
"@jxsuite/protocol": "^2.
|
|
69
|
+
"@jxsuite/protocol": "^2.6.0",
|
|
70
70
|
"@jxsuite/runtime": "^4.0.2",
|
|
71
71
|
"@jxsuite/schema": "^2.3.0",
|
|
72
72
|
"@jxsuite/site": "^2.0.2",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"ajv-formats": "^3.0.1",
|
|
77
77
|
"lit-html": "^3.3.3",
|
|
78
78
|
"monaco-editor": "^0.57.0",
|
|
79
|
-
"tabulator-tables": "^6.
|
|
79
|
+
"tabulator-tables": "^6.6.0",
|
|
80
80
|
"yaml": "^2.9.1"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Action-flow.ts — what Studio does when the backend refuses something and offers what to do.
|
|
4
|
+
*
|
|
5
|
+
* A backend answers an action it will not allow yet with `action-required` (desktop.md §10.4): its
|
|
6
|
+
* own heading, its own sentence, the actions it offers and what to do once one is done. Studio does
|
|
7
|
+
* not know why — a role, a policy, a plan, a quota — and does not need to. This module shows the
|
|
8
|
+
* refusal in one dialog, runs the chosen action through `platform.performAction`, and follows the
|
|
9
|
+
* backend's `retry` hint. `surfaces/dialog.ts` draws each phase; every word about the refusal
|
|
10
|
+
* itself is the backend's, and the few this module supplies ("Not now", "Waiting…") are about the
|
|
11
|
+
* dialog.
|
|
12
|
+
*
|
|
13
|
+
* **One dialog at a time.** A refused project open is a burst — `activate`, `project-info`, a
|
|
14
|
+
* directory listing — and every one of them reports. Callers that ask while a dialog is up join
|
|
15
|
+
* it.
|
|
16
|
+
*
|
|
17
|
+
* **Declining is remembered for the page.** The reports that arrive by themselves
|
|
18
|
+
* ({@link wireActionReports}) stop offering once the user has said "Not now", because a session
|
|
19
|
+
* whose every request is refused would otherwise reopen the dialog on each one. An action the user
|
|
20
|
+
* takes on purpose — a commit, a New Project — still asks, because that is a new question.
|
|
21
|
+
*
|
|
22
|
+
* @docs studio/interface/welcome-screen
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { hydrateAccountStatus, takeToastNotices } from "../account-status";
|
|
26
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
27
|
+
import { platformErrorInfo } from "../platform-errors";
|
|
28
|
+
import { notify } from "../services/notify";
|
|
29
|
+
import { resetModelCache } from "../services/ai-models";
|
|
30
|
+
import { openDialogSurface } from "../surfaces/dialog";
|
|
31
|
+
import { layerHost } from "../ui/layers";
|
|
32
|
+
import { onActionRequired } from "./action-required";
|
|
33
|
+
import type { ActionRequiredReport } from "./action-required";
|
|
34
|
+
import type { ActionOutcome, OfferedAction, RetryHint } from "../types";
|
|
35
|
+
|
|
36
|
+
/** A refusal to offer: what the backend offered, in its own words. */
|
|
37
|
+
export interface ActionPrompt {
|
|
38
|
+
actions: OfferedAction[];
|
|
39
|
+
heading?: string | undefined;
|
|
40
|
+
detail?: string | undefined;
|
|
41
|
+
retry?: RetryHint | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether this platform can perform an offered action at all. */
|
|
45
|
+
export function canPerformActions(): boolean {
|
|
46
|
+
return hasPlatform() && typeof getPlatform().performAction === "function";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The action a surface leads with — the one marked primary, else the first — and one other. */
|
|
50
|
+
export function leadingActions(
|
|
51
|
+
actions: readonly OfferedAction[],
|
|
52
|
+
): { lead: OfferedAction; other?: OfferedAction } | null {
|
|
53
|
+
const lead = actions.find((action) => action.primary) ?? actions[0];
|
|
54
|
+
if (!lead) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const other = actions.find((action) => action !== lead);
|
|
58
|
+
return other ? { lead, other } : { lead };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The dialog currently up, joined by every caller that asks while it is. */
|
|
62
|
+
let running: Promise<boolean> | null = null;
|
|
63
|
+
|
|
64
|
+
/** Set once the user declines; silences the reports that arrive on their own until reload. */
|
|
65
|
+
let declined = false;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Offer a refusal's actions, and resolve whether one of them is now done.
|
|
69
|
+
*
|
|
70
|
+
* Resolves false at once on a platform that cannot perform actions, or for a refusal that offers
|
|
71
|
+
* nothing, and when the user declines or closes the dialog. Never rejects: an action that failed is
|
|
72
|
+
* drawn in the dialog, which stays up so the user can try again or give up.
|
|
73
|
+
*/
|
|
74
|
+
export function promptAction(prompt: ActionPrompt): Promise<boolean> {
|
|
75
|
+
const leading = leadingActions(prompt.actions);
|
|
76
|
+
if (!canPerformActions() || !leading) {
|
|
77
|
+
return Promise.resolve(false);
|
|
78
|
+
}
|
|
79
|
+
running ??= offer(prompt, leading).finally(() => {
|
|
80
|
+
running = null;
|
|
81
|
+
});
|
|
82
|
+
return running;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A caught error, offered when it is an `action-required` refusal with something to offer. Resolves
|
|
87
|
+
* whether the caller should now try again: false for any other error (nothing was offered) and for
|
|
88
|
+
* an offer declined.
|
|
89
|
+
*/
|
|
90
|
+
export async function actIfRequired(error: unknown): Promise<boolean> {
|
|
91
|
+
const info = platformErrorInfo(error);
|
|
92
|
+
if (info.code !== "action-required" || !info.actions?.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return promptAction({
|
|
96
|
+
actions: info.actions,
|
|
97
|
+
detail: error instanceof Error ? error.message : undefined,
|
|
98
|
+
heading: info.heading,
|
|
99
|
+
retry: info.retry,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Everything a done action may have changed, re-read: the account rows and Start-pane notices read
|
|
105
|
+
* the status cache, and the assistant's gate reads the models probe. Then whatever the platform now
|
|
106
|
+
* wants announced is announced — in its words, not Studio's.
|
|
107
|
+
*/
|
|
108
|
+
async function settleDone(): Promise<void> {
|
|
109
|
+
resetModelCache();
|
|
110
|
+
await hydrateAccountStatus();
|
|
111
|
+
showToastNotices();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Run one dialog to its end. */
|
|
115
|
+
function offer(
|
|
116
|
+
prompt: ActionPrompt,
|
|
117
|
+
leading: { lead: OfferedAction; other?: OfferedAction },
|
|
118
|
+
): Promise<boolean> {
|
|
119
|
+
const message = prompt.detail ?? "This needs one more step first.";
|
|
120
|
+
return new Promise<boolean>((resolve) => {
|
|
121
|
+
let phase: "offer" | "waiting" | "done" = "offer";
|
|
122
|
+
const finish = (done: boolean) => {
|
|
123
|
+
if (phase === "done") {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
phase = "done";
|
|
127
|
+
handle.close();
|
|
128
|
+
resolve(done);
|
|
129
|
+
};
|
|
130
|
+
const offerAgain = (sentence: string) => {
|
|
131
|
+
phase = "offer";
|
|
132
|
+
handle.update({
|
|
133
|
+
cancelLabel: "Not now",
|
|
134
|
+
confirmLabel: leading.lead.label,
|
|
135
|
+
message: sentence,
|
|
136
|
+
secondaryLabel: leading.other?.label ?? "",
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const begin = (action: OfferedAction) => {
|
|
141
|
+
if (phase !== "offer") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
phase = "waiting";
|
|
145
|
+
/* Called straight from the click, with nothing awaited first: `performAction` opens its
|
|
146
|
+
window synchronously, and only code still inside the click may. */
|
|
147
|
+
let flow: Promise<ActionOutcome | null>;
|
|
148
|
+
try {
|
|
149
|
+
flow = getPlatform().performAction!(action);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
offerAgain(error instanceof Error ? error.message : String(error));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
handle.update({
|
|
155
|
+
cancelLabel: "Cancel",
|
|
156
|
+
confirmLabel: "Waiting…",
|
|
157
|
+
message: "Finish in the window that opened. This closes by itself once it is done.",
|
|
158
|
+
secondaryLabel: "",
|
|
159
|
+
});
|
|
160
|
+
void flow.then(
|
|
161
|
+
async (outcome) => {
|
|
162
|
+
/* A done action is re-read even when the dialog is already gone — the user may have
|
|
163
|
+
dismissed the waiting dialog and finished in the window anyway, and the account must
|
|
164
|
+
not go on saying otherwise. */
|
|
165
|
+
if (outcome?.status === "done") {
|
|
166
|
+
await settleDone();
|
|
167
|
+
finish(true);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (phase !== "waiting" || !outcome || outcome.status === "redirect") {
|
|
171
|
+
// Dismissed meanwhile, or the page is navigating to the action: nothing to draw.
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Closed without saying: the account may have changed anyway, so re-read it, and offer again.
|
|
175
|
+
void hydrateAccountStatus();
|
|
176
|
+
offerAgain(message);
|
|
177
|
+
},
|
|
178
|
+
(error: unknown) => {
|
|
179
|
+
if (phase === "waiting") {
|
|
180
|
+
offerAgain(error instanceof Error ? error.message : String(error));
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const { lead, other } = leading;
|
|
187
|
+
const handle = openDialogSurface({
|
|
188
|
+
cancelLabel: "Not now",
|
|
189
|
+
confirmLabel: lead.label,
|
|
190
|
+
headline: prompt.heading ?? "One more step",
|
|
191
|
+
layer: layerHost("dialog"),
|
|
192
|
+
message,
|
|
193
|
+
onCancel: () => {
|
|
194
|
+
declined = true;
|
|
195
|
+
finish(false);
|
|
196
|
+
},
|
|
197
|
+
onClosed: () => {
|
|
198
|
+
finish(false);
|
|
199
|
+
},
|
|
200
|
+
onConfirm: () => {
|
|
201
|
+
begin(lead);
|
|
202
|
+
},
|
|
203
|
+
region: "account/action",
|
|
204
|
+
...(other
|
|
205
|
+
? {
|
|
206
|
+
onSecondary: () => {
|
|
207
|
+
begin(other);
|
|
208
|
+
},
|
|
209
|
+
secondaryLabel: other.label,
|
|
210
|
+
}
|
|
211
|
+
: {}),
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Follow a done refusal's retry hint: reload a session it broke, or ask for the action again. */
|
|
217
|
+
function afterDone(retry: RetryHint | undefined): void {
|
|
218
|
+
if (retry === "reload") {
|
|
219
|
+
location.reload();
|
|
220
|
+
} else if (retry === "repeat") {
|
|
221
|
+
notify.info("Done. Run that again now.", { key: "account.action.retry", source: "Account" });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Offer the actions of every refusal the platform adapter reports by itself — the refusals no
|
|
227
|
+
* caller asked to handle. Returns the unsubscribe. Called once, at boot.
|
|
228
|
+
*/
|
|
229
|
+
export function wireActionReports(): () => void {
|
|
230
|
+
return onActionRequired((report: ActionRequiredReport) => {
|
|
231
|
+
if (declined || running) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
void promptAction(report).then((done) => {
|
|
235
|
+
if (done) {
|
|
236
|
+
afterDone(report.retry);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Run an action offered outside a refusal — on a Start-pane notice or an account row. Resolves
|
|
244
|
+
* whether it is done; either way the account status is re-read, because an action is how it
|
|
245
|
+
* changes. Synchronous up to `performAction`, which opens its window from inside the click.
|
|
246
|
+
*/
|
|
247
|
+
export async function runOfferedAction(action: OfferedAction): Promise<boolean> {
|
|
248
|
+
if (!canPerformActions()) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const pending = getPlatform().performAction!(action);
|
|
252
|
+
let outcome: ActionOutcome | null;
|
|
253
|
+
try {
|
|
254
|
+
outcome = await pending;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// A toast, not a Problems entry: it answers the click, and nothing is left standing to fix.
|
|
257
|
+
notify.error(error instanceof Error ? error.message : String(error), {
|
|
258
|
+
key: "account.action",
|
|
259
|
+
source: "Account",
|
|
260
|
+
tier: "toast",
|
|
261
|
+
});
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (outcome?.status === "done") {
|
|
265
|
+
await settleDone();
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (outcome?.status !== "redirect") {
|
|
269
|
+
await hydrateAccountStatus();
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Show the platform's one-time notices — how an action that left the page ended, or anything else
|
|
276
|
+
* it wants said once. Called at boot after the account status is read, and after every done
|
|
277
|
+
* action.
|
|
278
|
+
*/
|
|
279
|
+
export function showToastNotices(): void {
|
|
280
|
+
for (const notice of takeToastNotices()) {
|
|
281
|
+
const options = { key: `account.notice.${notice.id}`, source: "Account" };
|
|
282
|
+
if (notice.level === "warning") {
|
|
283
|
+
notify.warn(notice.message, options);
|
|
284
|
+
} else {
|
|
285
|
+
notify.info(notice.message, options);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Forget the dialog and the decline — tests only. */
|
|
291
|
+
export function resetActionFlow(): void {
|
|
292
|
+
running = null;
|
|
293
|
+
declined = false;
|
|
294
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action-required.ts — the channel a platform adapter raises an `action-required` refusal on.
|
|
3
|
+
*
|
|
4
|
+
* A backend refuses the session's own requests with `action-required` (HTTP 403, desktop.md §10.4),
|
|
5
|
+
* and those requests are made deep inside an adapter by callers that have no business knowing about
|
|
6
|
+
* access rules: a file read, a directory listing, the boot-time `activate`. Each of them still
|
|
7
|
+
* throws its ordinary error, and this is the side channel that says, once, "and here is what the
|
|
8
|
+
* user can do about it" — to the one listener that can offer it (`action-flow.ts`).
|
|
9
|
+
*
|
|
10
|
+
* No DOM and no imports beyond types, so an adapter can raise on it from anywhere without pulling
|
|
11
|
+
* the dialog layer into its module graph.
|
|
12
|
+
*
|
|
13
|
+
* **A report raised before anyone listens is kept.** The boot-time `activate` runs as the studio
|
|
14
|
+
* module evaluates, and its refusal can land before the flow has subscribed; dropping it would be
|
|
15
|
+
* exactly the silent failure this channel exists to prevent. Only the latest one is kept — they all
|
|
16
|
+
* say the same thing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { OfferedAction, RetryHint } from "@jxsuite/protocol";
|
|
20
|
+
|
|
21
|
+
/** One refusal, in the backend's words. */
|
|
22
|
+
export interface ActionRequiredReport {
|
|
23
|
+
/** What the backend offers. A report is raised only when it offers something. */
|
|
24
|
+
actions: OfferedAction[];
|
|
25
|
+
/** The refusal's heading. */
|
|
26
|
+
heading?: string;
|
|
27
|
+
/** The refusal's `detail`. */
|
|
28
|
+
detail?: string;
|
|
29
|
+
/** What to do once an action is done. */
|
|
30
|
+
retry?: RetryHint;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Listener = (report: ActionRequiredReport) => void;
|
|
34
|
+
|
|
35
|
+
const listeners = new Set<Listener>();
|
|
36
|
+
|
|
37
|
+
/** The latest report raised while nobody was listening; delivered to the first subscriber. */
|
|
38
|
+
let pending: ActionRequiredReport | null = null;
|
|
39
|
+
|
|
40
|
+
/** Raise a refusal. Every listener hears it; with none yet, the first one to subscribe will. */
|
|
41
|
+
export function reportActionRequired(report: ActionRequiredReport): void {
|
|
42
|
+
if (listeners.size === 0) {
|
|
43
|
+
pending = report;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const listener of listeners) {
|
|
47
|
+
listener(report);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Hear every refusal from now on, and the one raised before this call, if any. */
|
|
52
|
+
export function onActionRequired(listener: Listener): () => void {
|
|
53
|
+
listeners.add(listener);
|
|
54
|
+
const missed = pending;
|
|
55
|
+
pending = null;
|
|
56
|
+
if (missed) {
|
|
57
|
+
listener(missed);
|
|
58
|
+
}
|
|
59
|
+
return () => {
|
|
60
|
+
listeners.delete(listener);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Drop every listener and any kept report — tests only. */
|
|
65
|
+
export function resetActionReports(): void {
|
|
66
|
+
listeners.clear();
|
|
67
|
+
pending = null;
|
|
68
|
+
}
|
package/src/account-status.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* "install the GitHub App" when the user has no repository access yet.
|
|
6
6
|
*/
|
|
7
7
|
import { getPlatform, hasPlatform } from "./platform";
|
|
8
|
-
import type { AccountStatus } from "./types";
|
|
8
|
+
import type { AccountEntry, AccountNotice, AccountStatus } from "./types";
|
|
9
9
|
|
|
10
10
|
let cache: AccountStatus | null = null;
|
|
11
11
|
|
|
@@ -62,7 +62,35 @@ export function getRepoAccessLinks(): RepoAccessLinks | null {
|
|
|
62
62
|
return { manage, ...(cache.appInstallUrl ? { installUrl: cache.appInstallUrl } : {}) };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** The rows the platform adds to Preferences › Accounts (desktop.md §10.4); none when absent. */
|
|
66
|
+
export function getAccountEntries(): AccountEntry[] {
|
|
67
|
+
return cache?.entries ?? [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The platform's notices to draw on the Start pane: every one that is not a one-time toast. */
|
|
71
|
+
export function getBannerNotices(): AccountNotice[] {
|
|
72
|
+
return (cache?.notices ?? []).filter((notice) => notice.display !== "toast");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Toast notices already shown, by id, so a repeated hydrate does not announce one twice. */
|
|
76
|
+
const shownToasts = new Set<string>();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The platform's one-time notices not yet shown, marked shown as they are taken. A platform that
|
|
80
|
+
* keeps sending one after it was shown — before it hears it was seen — does not repeat itself.
|
|
81
|
+
*/
|
|
82
|
+
export function takeToastNotices(): AccountNotice[] {
|
|
83
|
+
const fresh = (cache?.notices ?? []).filter(
|
|
84
|
+
(notice) => notice.display === "toast" && !shownToasts.has(notice.id),
|
|
85
|
+
);
|
|
86
|
+
for (const notice of fresh) {
|
|
87
|
+
shownToasts.add(notice.id);
|
|
88
|
+
}
|
|
89
|
+
return fresh;
|
|
90
|
+
}
|
|
91
|
+
|
|
65
92
|
/** Reset seam for tests. */
|
|
66
93
|
export function resetAccountStatus(): void {
|
|
67
94
|
cache = null;
|
|
95
|
+
shownToasts.clear();
|
|
68
96
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { MEDIA_EXTENSIONS, extensionOf } from "../files/media-upload";
|
|
22
22
|
import { projectState } from "../store";
|
|
23
|
+
import { notedListing } from "../files/asset-versions";
|
|
23
24
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
24
25
|
import { resolveI18n, servedLocaleOfPath } from "@jxsuite/schema/locale";
|
|
25
26
|
import type { ResolvedI18n } from "@jxsuite/schema/locale";
|
|
@@ -174,7 +175,8 @@ async function walk(
|
|
|
174
175
|
): Promise<void> {
|
|
175
176
|
let entries: DirEntry[];
|
|
176
177
|
try {
|
|
177
|
-
|
|
178
|
+
// Noted: the Library walks every media directory, so it is where most versions are learned.
|
|
179
|
+
entries = await notedListing(() => platform.listDirectory(dir));
|
|
178
180
|
} catch (error) {
|
|
179
181
|
failures.push({ dir, error: errorMessage(error) });
|
|
180
182
|
return;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
setSkipServerFunctions,
|
|
27
27
|
} from "@jxsuite/runtime";
|
|
28
28
|
import { assetContextFor, hostAssetDeclarations, resolveAssetRef } from "../canvas/asset-refs";
|
|
29
|
+
import { assetVersionsSnapshot } from "../files/asset-versions";
|
|
29
30
|
import { componentRegistry } from "../files/components";
|
|
30
31
|
import { getPlatform } from "../platform";
|
|
31
32
|
import { loadFormats, formatForPath } from "../format/format-host";
|
|
@@ -121,7 +122,7 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
|
|
|
121
122
|
try {
|
|
122
123
|
const content = await getPlatform().readFile(filePath);
|
|
123
124
|
setSkipServerFunctions(true);
|
|
124
|
-
const
|
|
125
|
+
const context = assetContextFor(filePath, hostAssetDeclarations());
|
|
125
126
|
await loadFormats();
|
|
126
127
|
let document_: JxDocument;
|
|
127
128
|
if (formatForPath(filePath)) {
|
|
@@ -134,7 +135,10 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
|
|
|
134
135
|
/* This render lands in the PARENT document, so an `<img src="/hero.jpg">` inside it resolves
|
|
135
136
|
against `index.html` — which is the shell, not the site. Same hook the canvas installs, same
|
|
136
137
|
context, restored afterwards because the next preview is a different document and a stale
|
|
137
|
-
`documentDir` would resolve its relative refs against the wrong entry.
|
|
138
|
+
`documentDir` would resolve its relative refs against the wrong entry. The version map is
|
|
139
|
+
read as the render starts, after the reads above: the Library is the many-images screen,
|
|
140
|
+
and a versioned src is what lets its cards repaint from the browser cache. */
|
|
141
|
+
const assets = context && { ...context, versions: assetVersionsSnapshot() };
|
|
138
142
|
setCanvasAssetResolver((value) => resolveAssetRef(value, assets));
|
|
139
143
|
try {
|
|
140
144
|
const element = renderNode(document_, scope);
|
package/src/canvas/asset-refs.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { contentMountFor, resolveAssetRef } from "./asset-resolve";
|
|
|
18
18
|
import { documentBase, loopbackAssetSrc } from "./canvas-origin";
|
|
19
19
|
import { getPlatform, hasPlatform } from "../platform";
|
|
20
20
|
import { projectState } from "../store";
|
|
21
|
+
import { assetVersionsSnapshot } from "../files/asset-versions";
|
|
21
22
|
import { activeTab } from "../workspace/workspace";
|
|
22
23
|
import type { AssetContext } from "./asset-resolve";
|
|
23
24
|
import type { ContentSectionEntry } from "../types";
|
|
@@ -94,7 +95,8 @@ export function previewAssetSrc(value: string): string {
|
|
|
94
95
|
return value;
|
|
95
96
|
}
|
|
96
97
|
const ctx = assetContextFor(activeTab.value?.documentPath, hostAssetDeclarations());
|
|
97
|
-
|
|
98
|
+
const versioned = ctx && { ...ctx, versions: assetVersionsSnapshot() };
|
|
99
|
+
return loopbackAssetSrc(resolveAssetRef(value, versioned) ?? value);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/**
|
|
@@ -98,6 +98,81 @@ export interface AssetContext {
|
|
|
98
98
|
* editing servers actually use, and why the two differ.
|
|
99
99
|
*/
|
|
100
100
|
lanes: readonly AssetLane[];
|
|
101
|
+
/**
|
|
102
|
+
* Content versions by normalized project path, for repo-space URLs (`?v=<version>`, see
|
|
103
|
+
* {@link withVersion}). Only paths the backend vouches for appear; a path absent here resolves
|
|
104
|
+
* unversioned, exactly as before versions existed.
|
|
105
|
+
*/
|
|
106
|
+
versions?: Readonly<Record<string, string>> | undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `url` with `v=<version>` in its query — appended to an existing query, inserted before any
|
|
111
|
+
* `#fragment`, and replacing every `v` parameter the URL already had. The host reads ONE `v`, and
|
|
112
|
+
* it decodes the key to find it (`URLSearchParams.get("v")` returns the FIRST match), so when
|
|
113
|
+
* Studio appends its own, an authored `v` — `?v=2`, `?v`, or a percent-encoded `?%76=…` — is
|
|
114
|
+
* dropped by its DECODED name rather than left to shadow it.
|
|
115
|
+
*
|
|
116
|
+
* With no version the URL comes back EXACTLY as given, authored `v` and all: an unversioned
|
|
117
|
+
* reference keeps working precisely as it did before versions existed. That is safe because a host
|
|
118
|
+
* may answer a `v` immutably only when it is a version the host issued, with exactly the bytes it
|
|
119
|
+
* names (specs/studio.md §3.4), so an authored cache-buster like `?v=2` gets the ordinary
|
|
120
|
+
* revalidating response. A version that is not a non-empty string is no version.
|
|
121
|
+
*
|
|
122
|
+
* The version is opaque and equal versions mean identical bytes, so a host that recognizes it may
|
|
123
|
+
* answer the URL with an immutable cache lifetime (specs/studio.md §3.4).
|
|
124
|
+
*
|
|
125
|
+
* @param {string} url - A URL, absolute or relative
|
|
126
|
+
* @param {string | undefined} version - The file's content version, if any
|
|
127
|
+
* @returns {string} The versioned URL
|
|
128
|
+
*/
|
|
129
|
+
export function withVersion(url: string, version: string | undefined): string {
|
|
130
|
+
if (typeof version !== "string" || version === "") {
|
|
131
|
+
return url;
|
|
132
|
+
}
|
|
133
|
+
const hashAt = url.indexOf("#");
|
|
134
|
+
const fragment = hashAt === -1 ? "" : url.slice(hashAt);
|
|
135
|
+
const beforeHash = hashAt === -1 ? url : url.slice(0, hashAt);
|
|
136
|
+
const queryAt = beforeHash.indexOf("?");
|
|
137
|
+
const base = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
|
|
138
|
+
const parts = queryAt === -1 ? [] : beforeHash.slice(queryAt + 1).split("&");
|
|
139
|
+
const kept = parts.filter((part) => part !== "" && !namesV(part));
|
|
140
|
+
kept.push(`v=${encodeURIComponent(version)}`);
|
|
141
|
+
return `${base}?${kept.join("&")}${fragment}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Whether one `key[=value]` query part's key decodes to `v`, the way `URLSearchParams` reads it. */
|
|
145
|
+
function namesV(part: string): boolean {
|
|
146
|
+
const eq = part.indexOf("=");
|
|
147
|
+
const key = eq === -1 ? part : part.slice(0, eq);
|
|
148
|
+
if (key === "v") {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (!key.includes("%")) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
return decodeURIComponent(key) === "v";
|
|
156
|
+
} catch {
|
|
157
|
+
// A malformed escape decodes to itself under `URLSearchParams`, which is never a bare `v`.
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The version `versions` holds for `path`, or undefined. Own keys only: the map is a plain object
|
|
164
|
+
* (and a structured clone of one inside the canvas iframe), so a path named `constructor` or
|
|
165
|
+
* `__proto__` would otherwise read a member of `Object.prototype` as its version.
|
|
166
|
+
*/
|
|
167
|
+
function ownVersion(
|
|
168
|
+
versions: Readonly<Record<string, string>> | undefined,
|
|
169
|
+
path: string,
|
|
170
|
+
): string | undefined {
|
|
171
|
+
if (!versions || !Object.hasOwn(versions, path)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const version: unknown = versions[path];
|
|
175
|
+
return typeof version === "string" ? version : undefined;
|
|
101
176
|
}
|
|
102
177
|
|
|
103
178
|
/**
|
|
@@ -221,7 +296,10 @@ export function resolveAssetRef(value: string, ctx: AssetContext | null): string
|
|
|
221
296
|
return null;
|
|
222
297
|
}
|
|
223
298
|
const { suffix } = splitRefSuffix(value);
|
|
224
|
-
return
|
|
299
|
+
return withVersion(
|
|
300
|
+
`${ctx.fileBaseUrl}${encodeProjectPath(path)}${suffix}`,
|
|
301
|
+
ownVersion(ctx.versions, path),
|
|
302
|
+
);
|
|
225
303
|
}
|
|
226
304
|
const [mount] = ctx.mounts;
|
|
227
305
|
return mount ? mountedRefFor(value, ctx.documentDir, mount) : null;
|
|
@@ -53,8 +53,11 @@ import {
|
|
|
53
53
|
reapplyStyle,
|
|
54
54
|
redefineElement,
|
|
55
55
|
resetDocumentStyles,
|
|
56
|
+
setCanvasAssetResolver,
|
|
56
57
|
setResolveToken,
|
|
57
58
|
} from "@jxsuite/runtime";
|
|
59
|
+
import { resolveAssetRef } from "./asset-resolve";
|
|
60
|
+
import type { AssetContext } from "./asset-resolve";
|
|
58
61
|
import { KIT_LOADERS } from "@jxsuite/ui/loaders";
|
|
59
62
|
import type { IframeChannel } from "./iframe-channel";
|
|
60
63
|
import type {
|
|
@@ -121,6 +124,25 @@ export function isExternalFileDrag(e: DragEvent): boolean {
|
|
|
121
124
|
*/
|
|
122
125
|
const FILE_DRAG_SRC: DragSrcKind = { type: "block" };
|
|
123
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The project's media versions, as the host last posted them (`assetVersions`). Module-level: one
|
|
129
|
+
* frame per realm, and the map describes the PROJECT, not any one render. Folded into every
|
|
130
|
+
* render's asset context by {@link versionedAssets}; empty until the host sends one, which resolves
|
|
131
|
+
* every reference unversioned — the behaviour from before versions existed.
|
|
132
|
+
*/
|
|
133
|
+
let assetVersions: Readonly<Record<string, string>> = {};
|
|
134
|
+
/**
|
|
135
|
+
* The asset context of the newest render accepted, WITHOUT versions — kept so a later
|
|
136
|
+
* `assetVersions` can rebuild the resolver around the new map. Null when that render resolves
|
|
137
|
+
* nothing.
|
|
138
|
+
*/
|
|
139
|
+
let liveAssets: AssetContext | null = null;
|
|
140
|
+
|
|
141
|
+
/** A render's asset context with the current versions folded in, or null for no resolution. */
|
|
142
|
+
function versionedAssets(assets: AssetContext | null): AssetContext | null {
|
|
143
|
+
return assets ? { ...assets, versions: assetVersions } : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
/**
|
|
125
147
|
* Describe the node under a file drag for the parent: its path, rect, and resolved tag. The tag
|
|
126
148
|
* comes from the SHADOW DOC (the document's own `tagName`) when the path resolves there, because
|
|
@@ -240,6 +262,9 @@ export function startCanvasIframe(opts: {
|
|
|
240
262
|
container: HTMLElement;
|
|
241
263
|
}): () => void {
|
|
242
264
|
const { channel, container } = opts;
|
|
265
|
+
// A frame starts knowing no versions; the host posts the map before the first render.
|
|
266
|
+
assetVersions = {};
|
|
267
|
+
liveAssets = null;
|
|
243
268
|
let handle: RenderHandle | null = null;
|
|
244
269
|
// Disposer for the live render's dataScope re-post effect (see the render handler); stopped
|
|
245
270
|
// Alongside the handle so a superseded render's refs can't keep posting.
|
|
@@ -869,6 +894,17 @@ export function startCanvasIframe(opts: {
|
|
|
869
894
|
stopAutoScroll();
|
|
870
895
|
return;
|
|
871
896
|
}
|
|
897
|
+
if (msg.kind === "assetVersions") {
|
|
898
|
+
/* Replaced wholesale, like the keymap. The resolver the last render installed closed over the
|
|
899
|
+
old map, and a patch draws through that resolver — so it is rebuilt around the new one
|
|
900
|
+
here, not left for the next render. */
|
|
901
|
+
assetVersions = msg.versions;
|
|
902
|
+
const assets = versionedAssets(liveAssets);
|
|
903
|
+
if (assets) {
|
|
904
|
+
setCanvasAssetResolver((value) => resolveAssetRef(value, assets));
|
|
905
|
+
}
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
872
908
|
if (msg.kind === "keymap") {
|
|
873
909
|
// Replaced wholesale, never merged: the host sends the whole live table, so a chord the
|
|
874
910
|
// Author unbound disappears here rather than lingering as a key the canvas still swallows.
|
|
@@ -946,6 +982,7 @@ export function startCanvasIframe(opts: {
|
|
|
946
982
|
stopEditing();
|
|
947
983
|
}
|
|
948
984
|
latestGen = msg.gen;
|
|
985
|
+
liveAssets = msg.assets ?? null;
|
|
949
986
|
// Adopt the document's caret vocabulary BEFORE rendering: which tags hold a caret depends on
|
|
950
987
|
// The format class, and a `.md` page and a native `.json` component do not agree. Absent means
|
|
951
988
|
// A document with no format, where the studio's own element metadata answers on its own.
|
|
@@ -966,7 +1003,7 @@ export function startCanvasIframe(opts: {
|
|
|
966
1003
|
element per render. */
|
|
967
1004
|
resetDocumentStyles(container.ownerDocument);
|
|
968
1005
|
handle = await renderResolvedDocument({
|
|
969
|
-
assets: msg.assets ?? null,
|
|
1006
|
+
assets: versionedAssets(msg.assets ?? null),
|
|
970
1007
|
container,
|
|
971
1008
|
doc: msg.doc as JxDocument,
|
|
972
1009
|
docBase: msg.docBase,
|