@jxsuite/studio 2.4.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
- package/dist/chunks/studio-2zkz8evn.js +3163 -0
- package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
- package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/studio-kyh9avvv.js +28 -0
- package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
- package/dist/chunks/studio-xrapxwwc.js +1539 -0
- package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +450 -48
- package/dist/studio.js +2455 -1761
- package/package.json +16 -15
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +59 -5
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +50 -16
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/file-drop-action.ts +2 -1
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -61
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +35 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +333 -160
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +21 -80
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/signals-panel.ts +2 -1
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +34 -3
- package/src/platforms/devserver.ts +72 -34
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-settings.ts +69 -71
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/cf-settings.ts +19 -37
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +49 -7
- package/src/services/idle.ts +6 -1
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +181 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/services/tool-executor.ts +34 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/settings/settings-document.ts +65 -9
- package/src/shell.ts +6 -14
- package/src/studio.ts +65 -7
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +164 -8
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +43 -31
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
- package/dist/chunks/content-source-k9724vcv.js +0 -39
- package/dist/chunks/files-yew27sn9.js +0 -69
- package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
- package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
- package/dist/chunks/quick-search-9avyc4pc.js +0 -54
- package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/dist/chunks/studio-ra7ar50f.js +0 -6682
- package/src/canvas/content-assets.ts +0 -263
- package/src/canvas/site-style-css.ts +0 -121
- package/src/services/settings-store.ts +0 -99
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format-choices — the two derived predicates behind "which format may I create?" and "which format
|
|
3
|
+
* may I convert to?".
|
|
4
|
+
*
|
|
5
|
+
* Both are computed from what a format class DECLARES — `capabilities.parse`,
|
|
6
|
+
* `capabilities.serialize`, `format.documentKinds` — so a third format extension appears in the New
|
|
7
|
+
* File picker and in Convert To with no edit to this file, which is the same contract
|
|
8
|
+
* `format/format-host.ts` already keeps for opening and saving.
|
|
9
|
+
*
|
|
10
|
+
* Pure and synchronous over the loaded registry and `projectState`, because the Files tree's
|
|
11
|
+
* context menu is built during a render and cannot await anything.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { collectionForFile } from "../content/collection-match";
|
|
15
|
+
import { documentExtensions, formatByExtension, formatForPath, getFormats } from "./format-host";
|
|
16
|
+
import { projectState } from "../store";
|
|
17
|
+
|
|
18
|
+
/** One row of a format picker: the extension it writes, and the name the reader sees. */
|
|
19
|
+
export interface FormatChoiceRow {
|
|
20
|
+
ext: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The document kinds that mean "this format's `parse` returns a Jx document".
|
|
26
|
+
*
|
|
27
|
+
* There is no separate declaration for the SHAPE `parse` returns, and none is needed: the compiler
|
|
28
|
+
* builds its page and component globs from `documentExtensions("page")` /
|
|
29
|
+
* `documentExtensions("component")` and casts every `parse` result to `JxDocument`
|
|
30
|
+
* (`packages/compiler/src/site/pages-discovery.ts`). So declaring either kind IS the claim, already
|
|
31
|
+
* relied upon by the build. A `content`-only format — Csv, whose `parse` returns
|
|
32
|
+
* `ContentLoaderEntry[]` rows — makes no such claim and is excluded by the same test.
|
|
33
|
+
*/
|
|
34
|
+
const DOCUMENT_KINDS = ["page", "component"] as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Where a Jx document lives.
|
|
38
|
+
*
|
|
39
|
+
* Both keys are conventions the build hard-codes rather than reads (`site-build.ts` resolves
|
|
40
|
+
* `pages` against the project root), so a convert may lean on them. It is deliberately
|
|
41
|
+
* conservative: a component kept in `shared/` is not offered a conversion, which is a smaller wrong
|
|
42
|
+
* than offering one for `package.json`, `tsconfig.json` or a `nav.json` that no format and no
|
|
43
|
+
* schema claims.
|
|
44
|
+
*/
|
|
45
|
+
const DOCUMENT_ROOTS = { components: "component", pages: "page" } as const;
|
|
46
|
+
|
|
47
|
+
/** Normalise a project-relative path for prefix comparison. */
|
|
48
|
+
function normalize(path: string): string {
|
|
49
|
+
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Which document root a path sits under, or null. */
|
|
53
|
+
function documentRootOf(path: string): "page" | "component" | null {
|
|
54
|
+
const lower = normalize(path).toLowerCase();
|
|
55
|
+
for (const [dir, kind] of Object.entries(DOCUMENT_ROOTS)) {
|
|
56
|
+
if (lower.startsWith(`${dir}/`)) {
|
|
57
|
+
return kind;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether a format declares at least one of the document kinds. */
|
|
64
|
+
function declaresDocumentKind(kinds: readonly string[]): boolean {
|
|
65
|
+
return DOCUMENT_KINDS.some((kind) => kinds.includes(kind));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether this path is (or is used as) a layout.
|
|
70
|
+
*
|
|
71
|
+
* Layouts are JSON at BOTH readers — `packages/compiler/src/site/layout-resolver.ts` `JSON.parse`s
|
|
72
|
+
* and throws, `packages/studio/src/site-context.ts` `JSON.parse`s and silently returns null — and
|
|
73
|
+
* there is no `"layout"` document kind for a format to declare, so this clause is hand-written and
|
|
74
|
+
* cannot be derived. `layouts/` is excluded by {@link DOCUMENT_ROOTS} already; what remains is a
|
|
75
|
+
* project whose `layout` points somewhere inside a document root.
|
|
76
|
+
*/
|
|
77
|
+
function isLayoutPath(path: string): boolean {
|
|
78
|
+
const declared = projectState?.projectConfig?.layout;
|
|
79
|
+
if (typeof declared !== "string") {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return normalize(declared).toLowerCase() === normalize(path).toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The rows the New File format picker offers, `.json` first.
|
|
87
|
+
*
|
|
88
|
+
* A format qualifies only when SOME registered class can both `parse` and `serialize` its
|
|
89
|
+
* extension. Both halves are load-bearing and neither is cosmetic:
|
|
90
|
+
*
|
|
91
|
+
* - Without `parse`, opening the file that was just created throws `noFormatError`
|
|
92
|
+
* (`files/file-ops.ts`'s `parseSourceForPath`);
|
|
93
|
+
* - Without `serialize`, the first save falls through to `defaultContentFormat()` and writes ANOTHER
|
|
94
|
+
* format's bytes into the file (`files/file-ops.ts`'s `serializeDocument`).
|
|
95
|
+
*
|
|
96
|
+
* Looked up per `(extension, capability)` rather than per format row because the registry
|
|
97
|
+
* deliberately legalises a split claim — one class parsing an extension, another serializing it.
|
|
98
|
+
*
|
|
99
|
+
* @param kind - Restrict to formats declaring this document kind (the Library's New ▸ Page).
|
|
100
|
+
* @returns The picker's format rows, without the caller's own sentinel rows.
|
|
101
|
+
*/
|
|
102
|
+
export function creationFormats(kind?: "page" | "component" | "content"): FormatChoiceRow[] {
|
|
103
|
+
const rows: FormatChoiceRow[] = [{ ext: ".json", label: "JSON (.json)" }];
|
|
104
|
+
for (const ext of documentExtensions(kind)) {
|
|
105
|
+
const reader = formatByExtension(ext, "parse");
|
|
106
|
+
if (reader && formatByExtension(ext, "serialize")) {
|
|
107
|
+
rows.push({ ext, label: `${reader.name} (${ext})` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return rows;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Every extension the studio recognises as a document — `.json` plus everything any registered
|
|
115
|
+
* format claims, whether or not it is creatable.
|
|
116
|
+
*
|
|
117
|
+
* This is the set a name is tested against, not {@link creationFormats}: a typed `notes.csv` must be
|
|
118
|
+
* recognised as naming a format the picker did not offer, rather than treated as an unremarkable
|
|
119
|
+
* suffix.
|
|
120
|
+
*/
|
|
121
|
+
export function knownDocumentExtensions(): Set<string> {
|
|
122
|
+
return new Set([".json", ...getFormats().flatMap((format) => format.extensions)]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The formats this file may be converted INTO, or an empty list when it may not be converted at
|
|
127
|
+
* all.
|
|
128
|
+
*
|
|
129
|
+
* Empty is the answer for, and only for:
|
|
130
|
+
*
|
|
131
|
+
* - A file outside `pages/` and `components/` — including every `.json` that is configuration rather
|
|
132
|
+
* than a document, and every `layouts/` file;
|
|
133
|
+
* - The project's declared `layout`, wherever it lives;
|
|
134
|
+
* - Anything inside a content collection, in EITHER direction. Converting an entry drops it out of
|
|
135
|
+
* its collection's discovery glob; converting a co-located file INTO the collection's format
|
|
136
|
+
* enlists it as an entry nobody seeded. Source and target share a directory, so one test covers
|
|
137
|
+
* both;
|
|
138
|
+
* - A source whose format cannot `parse`, or declares no document kind (Csv);
|
|
139
|
+
* - A source for which no OTHER format can `serialize` a document.
|
|
140
|
+
*
|
|
141
|
+
* @param path - Project-relative file path.
|
|
142
|
+
* @returns The target rows, or `[]`.
|
|
143
|
+
*/
|
|
144
|
+
export function convertTargets(path: string | null | undefined): FormatChoiceRow[] {
|
|
145
|
+
if (path === null || path === undefined || path === "") {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const root = documentRootOf(path);
|
|
149
|
+
if (root === null || isLayoutPath(path) || collectionForFile(path)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const source = formatForPath(path);
|
|
154
|
+
const readable = source
|
|
155
|
+
? Boolean(source.capabilities.parse) && declaresDocumentKind(source.documentKinds)
|
|
156
|
+
: normalize(path).toLowerCase().endsWith(".json");
|
|
157
|
+
if (!readable) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const rows: FormatChoiceRow[] = [];
|
|
162
|
+
if (source) {
|
|
163
|
+
rows.push({ ext: ".json", label: "JSON (.json)" });
|
|
164
|
+
}
|
|
165
|
+
for (const ext of documentExtensions()) {
|
|
166
|
+
const writer = formatByExtension(ext, "serialize");
|
|
167
|
+
if (
|
|
168
|
+
!writer ||
|
|
169
|
+
writer.name === source?.name ||
|
|
170
|
+
!declaresDocumentKind(writer.documentKinds) ||
|
|
171
|
+
!writer.documentKinds.includes(root)
|
|
172
|
+
) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
rows.push({ ext, label: `${writer.name} (${ext})` });
|
|
176
|
+
}
|
|
177
|
+
return rows;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Every extension that is a conversion target SOMEWHERE in this project — the enum behind
|
|
182
|
+
* `file.convertFormat`'s `format` argument.
|
|
183
|
+
*
|
|
184
|
+
* Necessarily broader than {@link convertTargets}, which is answered per file: a command argument's
|
|
185
|
+
* declared values cannot depend on another argument. The per-file rule still decides, and
|
|
186
|
+
* `convertFile` re-checks it, so this list is what may be ASKED for and not what will be accepted.
|
|
187
|
+
*/
|
|
188
|
+
export function convertTargetExtensions(): string[] {
|
|
189
|
+
const exts = new Set<string>([".json"]);
|
|
190
|
+
for (const format of getFormats()) {
|
|
191
|
+
if (!format.capabilities.serialize || !declaresDocumentKind(format.documentKinds)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const ext of format.extensions) {
|
|
195
|
+
exts.add(ext);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return [...exts].toSorted();
|
|
199
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid-idle.ts — the grid's quiescence registry, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A separate module from `grid-view.ts` for one reason: `services/idle.ts` must be able to ASK
|
|
5
|
+
* whether any grid is mid-flight without pulling Tabulator into its import graph. It read the
|
|
6
|
+
* probes from `grid-view.ts` first, and `grid-view.ts` imports `tabulator-tables` at module load —
|
|
7
|
+
* so every test that transitively reached `idle.ts` (which is most of them, through the shell)
|
|
8
|
+
* tried to evaluate the Tabulator ESM bundle and died with `Export named 'ClipboardModule' not
|
|
9
|
+
* found` unless it had thought to mock a module it never uses.
|
|
10
|
+
*
|
|
11
|
+
* So the dependency points the other way: this file imports nothing, `idle.ts` reads it, and
|
|
12
|
+
* `grid-view.ts` writes to it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Answers `null` when its grid is settled, or a human-readable reason when it is not. */
|
|
16
|
+
export type GridProbe = () => string | null;
|
|
17
|
+
|
|
18
|
+
const liveGrids = new Set<GridProbe>();
|
|
19
|
+
|
|
20
|
+
/** Register a live grid. The returned function deregisters it — call it from `destroy()`. */
|
|
21
|
+
export function registerGridProbe(probe: GridProbe): () => void {
|
|
22
|
+
liveGrids.add(probe);
|
|
23
|
+
return () => liveGrids.delete(probe);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Grids still building, or still laying out their selection range.
|
|
28
|
+
*
|
|
29
|
+
* A probe must never throw: it runs inside the idle predicate, and an exception there would be
|
|
30
|
+
* indistinguishable from a subsystem that never settles.
|
|
31
|
+
*/
|
|
32
|
+
export function gridIdleBlockers(): readonly string[] {
|
|
33
|
+
const blockers: string[] = [];
|
|
34
|
+
for (const probe of liveGrids) {
|
|
35
|
+
try {
|
|
36
|
+
const reason = probe();
|
|
37
|
+
if (reason) {
|
|
38
|
+
blockers.push(reason);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// A table torn down between registration and probe is settled by definition.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return blockers;
|
|
45
|
+
}
|
package/src/grid/grid-view.ts
CHANGED
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
} from "tabulator-tables";
|
|
41
41
|
import type { GridCellValue, GridColumn } from "./grid-source";
|
|
42
42
|
import type { GridController } from "./grid-controller";
|
|
43
|
+
import { registerGridProbe } from "./grid-idle";
|
|
43
44
|
|
|
44
45
|
export interface GridView {
|
|
45
46
|
/** Re-pull controller.effectiveRows() into the table (buffer → table sync). */
|
|
@@ -222,6 +223,37 @@ export function createGridView(host: HTMLElement, controller: GridController): G
|
|
|
222
223
|
}
|
|
223
224
|
});
|
|
224
225
|
|
|
226
|
+
/*
|
|
227
|
+
* Settled means BUILT AND LAID OUT, not merely constructed. `selectableRange: 1` gives a
|
|
228
|
+
* non-empty table an initial range, and that range's outline is a rendered element — so until it
|
|
229
|
+
* exists the picture is still changing. An empty table is exempt, because it never gets one and
|
|
230
|
+
* waiting for it would be waiting forever.
|
|
231
|
+
*
|
|
232
|
+
* The MODEL is not the picture, and that distinction is the whole bug this second clause fixes.
|
|
233
|
+
* `getRanges()` answers from Tabulator's range model, which is populated when the range is
|
|
234
|
+
* created; the outline is a `.tabulator-range` div the SelectRange module paints into
|
|
235
|
+
* `.tabulator-range-overlay` on a later frame. Between the two, the grid reports itself settled
|
|
236
|
+
* and the capture lands on a table whose selected cell has no outline — so `data-grid.png`
|
|
237
|
+
* alternated between an outlined "Elena M." and a bare one, 16 rewrites in ten days at a steady
|
|
238
|
+
* 0.089 %. Asking the DOM for a laid-out box closes the window that the model check leaves open.
|
|
239
|
+
*/
|
|
240
|
+
const probe = (): string | null => {
|
|
241
|
+
if (!built) {
|
|
242
|
+
return `grid[${gridId}]: building`;
|
|
243
|
+
}
|
|
244
|
+
if (controller.effectiveRows().length > 0) {
|
|
245
|
+
if (table.getRanges().length === 0) {
|
|
246
|
+
return `grid[${gridId}]: selection range not laid out`;
|
|
247
|
+
}
|
|
248
|
+
const outline = host.querySelector(".tabulator-range-overlay .tabulator-range");
|
|
249
|
+
if (!outline || rectOf(outline).width === 0) {
|
|
250
|
+
return `grid[${gridId}]: selection range outline not painted`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
const unregisterProbe = registerGridProbe(probe);
|
|
256
|
+
|
|
225
257
|
table.on("cellEdited", (cell: CellComponent) => {
|
|
226
258
|
if (suppress) {
|
|
227
259
|
return;
|
|
@@ -307,6 +339,9 @@ export function createGridView(host: HTMLElement, controller: GridController): G
|
|
|
307
339
|
|
|
308
340
|
return {
|
|
309
341
|
destroy() {
|
|
342
|
+
// Before `table.destroy()`: a probe left registered against a torn-down table would be asking
|
|
343
|
+
// Tabulator questions it can no longer answer.
|
|
344
|
+
unregisterProbe();
|
|
310
345
|
controller.bindView(null);
|
|
311
346
|
table.destroy();
|
|
312
347
|
},
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { GridCellValue, GridColumn, GridColumnKind } from "./grid-source";
|
|
12
12
|
import type { JsonSchema } from "../ui/schema-form";
|
|
13
|
+
import { isMediaFormat } from "../utils/studio-utils";
|
|
13
14
|
|
|
14
15
|
/** JSON-Schema property as it appears in content-type schemas (superset of ui JsonSchema). */
|
|
15
16
|
interface PropSchema extends JsonSchema {
|
|
@@ -44,7 +45,7 @@ export function kindForProp(prop: PropSchema): GridColumnKind {
|
|
|
44
45
|
return "enum";
|
|
45
46
|
}
|
|
46
47
|
const format = prop.type === "array" ? undefined : prop.format;
|
|
47
|
-
if (format
|
|
48
|
+
if (isMediaFormat(format)) {
|
|
48
49
|
return "image";
|
|
49
50
|
}
|
|
50
51
|
if (format === "date" || format === "date-time") {
|
|
@@ -269,20 +269,41 @@ function buildEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
|
269
269
|
const files = await opts.list();
|
|
270
270
|
entries.clear();
|
|
271
271
|
columnsCache = null;
|
|
272
|
-
|
|
272
|
+
/*
|
|
273
|
+
* Read concurrently, INSERT IN ORDER.
|
|
274
|
+
*
|
|
275
|
+
* `entries` is a Map and the grid renders it in insertion order, so setting it from inside
|
|
276
|
+
* the worker made row order the order the reads happened to FINISH in — which is a race
|
|
277
|
+
* between two `platform.readFile` calls, not a property of the collection. The two posts in
|
|
278
|
+
* the `first-collection` fixture swapped places between runs, and that alone rewrote
|
|
279
|
+
* `blog-grid.png` in 3 of the 21 images the screenshot lane pushed over 24 commits.
|
|
280
|
+
*
|
|
281
|
+
* `mapLimit` already documents that it preserves item order; the fix is to USE its result
|
|
282
|
+
* rather than to mutate shared state from inside it. The concurrency is worth keeping — a
|
|
283
|
+
* collection is a directory of files and reading it eight at a time is why the grid opens
|
|
284
|
+
* quickly.
|
|
285
|
+
*/
|
|
286
|
+
const loaded = await mapLimit(files, 8, async (path): Promise<EntryRecord | null> => {
|
|
273
287
|
try {
|
|
274
288
|
const text = await platform.readFile(path);
|
|
275
289
|
const parsed = await parseSourceForPath(path, text);
|
|
276
|
-
|
|
290
|
+
return {
|
|
277
291
|
document: parsed.document,
|
|
278
292
|
formatName: parsed.format.name,
|
|
279
293
|
frontmatter: parsed.frontmatter ?? {},
|
|
280
294
|
rawText: text,
|
|
281
|
-
}
|
|
295
|
+
};
|
|
282
296
|
} catch {
|
|
283
297
|
// Unparseable entry — leave it out rather than failing the whole grid.
|
|
298
|
+
return null;
|
|
284
299
|
}
|
|
285
300
|
});
|
|
301
|
+
for (const [index, path] of files.entries()) {
|
|
302
|
+
const record = loaded[index];
|
|
303
|
+
if (record) {
|
|
304
|
+
entries.set(path, record);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
286
307
|
})();
|
|
287
308
|
return loadPromise;
|
|
288
309
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a media file in the viewer.
|
|
3
|
+
*
|
|
4
|
+
* The shape of `grid/grid-open.ts`'s `openCsvGridTab`, and for the same reason: a real file tab
|
|
5
|
+
* keyed by its path, with a stub document, because the tab model wants A DOCUMENT and this file is
|
|
6
|
+
* not one. The viewer never reads it — it reads `tab.documentPath` and shows the bytes at that path
|
|
7
|
+
* through an `<img>`, a `<video>` or a font specimen.
|
|
8
|
+
*
|
|
9
|
+
* `.svg` gets a `source` mode beside the viewer: it is the one media format that is also text, and
|
|
10
|
+
* hand-editing one is an ordinary thing to want.
|
|
11
|
+
*
|
|
12
|
+
* The modes are set HERE rather than in `tabs/tab.ts`'s `inferModes`, which is the same division
|
|
13
|
+
* CSV already lives under: `inferModes` answers for DOCUMENTS, from the format registry, and an
|
|
14
|
+
* opener answers for the files that are not documents. Every media tab is created through this
|
|
15
|
+
* function — `openFileInTab` routes to it, and session restore goes through `openFileInTab` — so
|
|
16
|
+
* there is no path by which one is built without them.
|
|
17
|
+
*
|
|
18
|
+
* @docs studio/projects/media
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { activateTab, openTab, workspace } from "../workspace/workspace";
|
|
22
|
+
import { extensionOf, isMediaFile } from "../files/media-upload";
|
|
23
|
+
import { MEDIA_MODE } from "./media-pane";
|
|
24
|
+
import type { Tab } from "../tabs/tab";
|
|
25
|
+
|
|
26
|
+
/** Placeholder document for a media tab — the viewer never reads it, and there is nothing to save. */
|
|
27
|
+
const MEDIA_STUB_DOCUMENT = { children: [], tagName: "div" };
|
|
28
|
+
|
|
29
|
+
/** Whether this path opens in the media viewer rather than through a document parse. */
|
|
30
|
+
export function isViewableMedia(path: string): boolean {
|
|
31
|
+
return isMediaFile(path);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The modes a media tab offers. SVG is also text, so it keeps a source alternate. */
|
|
35
|
+
export function mediaTabModes(path: string): string[] {
|
|
36
|
+
return extensionOf(path) === ".svg" ? [MEDIA_MODE, "source"] : [MEDIA_MODE];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Open (or activate) a media file's viewer tab.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} path - Project-relative path of the file
|
|
43
|
+
* @returns {Tab}
|
|
44
|
+
*/
|
|
45
|
+
export function openMediaTab(path: string): Tab {
|
|
46
|
+
const existing = workspace.tabs.get(path);
|
|
47
|
+
if (existing) {
|
|
48
|
+
activateTab(path);
|
|
49
|
+
return existing;
|
|
50
|
+
}
|
|
51
|
+
return openTab({
|
|
52
|
+
capabilities: { modes: mediaTabModes(path) },
|
|
53
|
+
document: structuredClone(MEDIA_STUB_DOCUMENT),
|
|
54
|
+
documentPath: path,
|
|
55
|
+
id: path,
|
|
56
|
+
sourceFormat: null,
|
|
57
|
+
});
|
|
58
|
+
}
|