@jxsuite/studio 10.4.0 → 10.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/2020-7sx2vexa.js +4 -0
- package/dist/chunks/adapter-12dxr3z7.js +4 -0
- package/dist/chunks/{add-new-flow-v0pm96mr.js → add-new-flow-any4pw2q.js} +3 -3
- package/dist/chunks/{cf-account-picker-xb8vvt2h.js → cf-account-picker-ajh60k54.js} +3 -3
- package/dist/chunks/cf-settings-hbj16kh3.js +4 -0
- package/dist/chunks/{color-area-h8dbasp1.js → color-area-gbcftzh3.js} +2 -2
- package/dist/chunks/{color-field-qv5r2b16.js → color-field-85136598.js} +3 -3
- package/dist/chunks/{color-slider-nz2mrzky.js → color-slider-w1s3ndd8.js} +2 -2
- package/dist/chunks/color-thzpppyc.js +4 -0
- package/dist/chunks/{combobox-6hs00996.js → combobox-dk98wc5x.js} +3 -3
- package/dist/chunks/components-a98hzqw9.js +4 -0
- package/dist/chunks/content-source-jq3fjs3w.js +4 -0
- package/dist/chunks/{convert-to-repeater-r8sfxjjm.js → convert-to-repeater-g66r6qp1.js} +3 -3
- package/dist/chunks/editor-rth1rzj8.js +4 -0
- package/dist/chunks/entry-commands-02j0s19w.js +4 -0
- package/dist/chunks/{extension-sections-vjzw46xb.js → extension-sections-rqrs7dr0.js} +3 -3
- package/dist/chunks/file-ops-6a49er4c.js +4 -0
- package/dist/chunks/files-b2p9y0yz.js +4 -0
- package/dist/chunks/format-host-7xe57k1s.js +4 -0
- package/dist/chunks/fs-events-0xsy168g.js +4 -0
- package/dist/chunks/grid-open-h3q4fjy5.js +4 -0
- package/dist/chunks/{grid-view-2f03wcgw.js → grid-view-0v4hjspb.js} +17 -17
- package/dist/chunks/{icons-gb967qc8.js → icons-j9eksgew.js} +2 -2
- package/dist/chunks/{iframe-entry-btck0e9s.js → iframe-entry-4zqtn3ry.js} +4 -4
- package/dist/chunks/{iframe-entry-b5qgggyp.js → iframe-entry-54kwwhmg.js} +4 -4
- package/dist/chunks/{iframe-entry-42hpmwz5.js → iframe-entry-m24fza30.js} +4 -4
- package/dist/chunks/iframe-host-8jcmaktg.js +4 -0
- package/dist/chunks/{image-bake-4afr4y6a.js → image-bake-m8gzaan9.js} +3 -3
- package/dist/chunks/{image-editor-x7t8jbjn.js → image-editor-0afyws7c.js} +3 -3
- package/dist/chunks/{index-yjrfeks8.js → index-2q88g57p.js} +3 -3
- package/dist/chunks/index-7nj2375a.js +4 -0
- package/dist/chunks/{javascript-hy2mws8v.js → javascript-5sxtr1g7.js} +3 -3
- package/dist/chunks/jsonMode-m6sf47hq.js +13 -0
- package/dist/chunks/jx-validate-808zgbgy.js +4 -0
- package/dist/chunks/layers-0ffdtqjn.js +4 -0
- package/dist/chunks/layers-panel-rw62wtvf.js +4 -0
- package/dist/chunks/{listbox-7gka1h86.js → listbox-k5p41049.js} +2 -2
- package/dist/chunks/lit-html-9bz9rcfv.js +4 -0
- package/dist/chunks/media-library-2a5hdbek.js +4 -0
- package/dist/chunks/media-open-069kzhj4.js +4 -0
- package/dist/chunks/media-picker-4p4pd28y.js +4 -0
- package/dist/chunks/menu-26nez152.js +4 -0
- package/dist/chunks/{monaco-binding-a51py33c.js → monaco-binding-1s2b8snq.js} +3 -3
- package/dist/chunks/monaco-buffer-m9fd2dka.js +4 -0
- package/dist/chunks/monaco-setup-e0crh3v6.js +4 -0
- package/dist/chunks/notify-re3js157.js +4 -0
- package/dist/chunks/platform-rywy3tkg.js +4 -0
- package/dist/chunks/popover-fhdnt4be.js +4 -0
- package/dist/chunks/publish-panel-czkpg8m4.js +4 -0
- package/dist/chunks/{publish-quick-4fwngwrw.js → publish-quick-b9wbhqj6.js} +3 -3
- package/dist/chunks/publish-workspace-yy6d23b7.js +4 -0
- package/dist/chunks/quick-search-tp7pjnmv.js +4 -0
- package/dist/chunks/references-wd2yj8s1.js +4 -0
- package/dist/chunks/right-panel-x971k0zc.js +4 -0
- package/dist/chunks/source-control-k4p00hpe.js +4 -0
- package/dist/chunks/{split-m5rwn31z.js → split-qh21j2b0.js} +2 -2
- package/dist/chunks/{studio-v5wktj6f.js → studio-151kgn69.js} +4 -4
- package/dist/chunks/studio-15mfah02.js +10 -0
- package/dist/chunks/studio-1a7zshnv.js +5 -0
- package/dist/chunks/studio-1d8m5s38.js +16 -0
- package/dist/chunks/studio-35pmfjtx.js +5 -0
- package/dist/chunks/{studio-3na1r48j.js → studio-3yr1zdp9.js} +8 -8
- package/dist/chunks/studio-43pynnhz.js +5 -0
- package/dist/chunks/studio-44xqe0b7.js +4 -0
- package/dist/chunks/{studio-hh394phe.js → studio-48yzqzqr.js} +4 -4
- package/dist/chunks/{studio-9gr7xs9r.js → studio-4jy967rj.js} +5 -5
- package/dist/chunks/{studio-4bgvvzhv.js → studio-4ncxzw5m.js} +4 -4
- package/dist/chunks/{studio-pm17wdc4.js → studio-4s5bka8t.js} +4 -4
- package/dist/chunks/{studio-5tet77de.js → studio-4z0cdh16.js} +3 -3
- package/dist/chunks/studio-5rq3e4q5.js +5 -0
- package/dist/chunks/{studio-wmjmsw0p.js → studio-69g6a0jn.js} +4 -4
- package/dist/chunks/studio-6hcfm9q3.js +5 -0
- package/dist/chunks/studio-6mf31e16.js +5 -0
- package/dist/chunks/studio-6sxnqqw8.js +5 -0
- package/dist/chunks/{studio-0939t1q1.js → studio-7qqb7hb8.js} +4 -4
- package/dist/chunks/{studio-bg8pzprg.js → studio-7qr8k0v2.js} +4 -4
- package/dist/chunks/{studio-0x6f3nk9.js → studio-8ncm9zwx.js} +4 -4
- package/dist/chunks/studio-8w5pa88p.js +7 -0
- package/dist/chunks/{studio-3qr08sxq.js → studio-9f5vcb4w.js} +3 -3
- package/dist/chunks/studio-aaw7wxv8.js +5 -0
- package/dist/chunks/studio-afx9brsc.js +5 -0
- package/dist/chunks/studio-ajfq4fmr.js +5 -0
- package/dist/chunks/{studio-evq47j2z.js → studio-ber1dgte.js} +4 -4
- package/dist/chunks/{studio-sce30r7b.js → studio-bk0anq3b.js} +3 -3
- package/dist/chunks/{studio-0gvvqj3h.js → studio-bmwgj8ez.js} +4 -4
- package/dist/chunks/{studio-s24mhq0z.js → studio-bpzwhn88.js} +4 -4
- package/dist/chunks/{studio-g9brbewz.js → studio-c72v1kwc.js} +4 -4
- package/dist/chunks/{studio-tpxcryf2.js → studio-cv6gnjqd.js} +4 -4
- package/dist/chunks/{studio-90zkgne5.js → studio-czpmzs9r.js} +7 -7
- package/dist/chunks/{studio-2pk7kc7z.js → studio-d5f7sskn.js} +4 -4
- package/dist/chunks/studio-dmgh2rrj.js +5 -0
- package/dist/chunks/{studio-nwf0c32p.js → studio-dst8y8vh.js} +4 -4
- package/dist/chunks/studio-ehj3csbm.js +6 -0
- package/dist/chunks/{studio-hfqpfmpk.js → studio-ek4fb4eb.js} +4 -4
- package/dist/chunks/studio-ep1384t9.js +5 -0
- package/dist/chunks/{studio-abh9kfdh.js → studio-eq5vqn1q.js} +4 -4
- package/dist/chunks/studio-erjccagy.js +5 -0
- package/dist/chunks/studio-ex1644g2.js +5 -0
- package/dist/chunks/studio-f3awr5nc.js +12 -0
- package/dist/chunks/studio-f69wcajk.js +9 -0
- package/dist/chunks/{studio-jma2q8r9.js → studio-g65t6svn.js} +4 -4
- package/dist/chunks/{studio-r6mx7vm5.js → studio-gw83z798.js} +5 -5
- package/dist/chunks/{studio-2x972v58.js → studio-hy936516.js} +4 -4
- package/dist/chunks/studio-j3bq30yz.js +5 -0
- package/dist/chunks/studio-j52ks8q7.js +5 -0
- package/dist/chunks/{studio-g0hxtcga.js → studio-jqvke4ac.js} +4 -4
- package/dist/chunks/{studio-bnc50f9h.js → studio-k05g7efh.js} +4 -4
- package/dist/chunks/studio-kefs4sz4.js +5 -0
- package/dist/chunks/{studio-2e4hry72.js → studio-ketrgtph.js} +5 -5
- package/dist/chunks/studio-m3jrjfhy.js +14 -0
- package/dist/chunks/{studio-45mj4jqe.js → studio-m8hesdrw.js} +4 -4
- package/dist/chunks/{studio-k0xzrse2.js → studio-n271vdsg.js} +36 -36
- package/dist/chunks/studio-n4pnf0sw.js +5 -0
- package/dist/chunks/{studio-gnazwtv3.js → studio-ncwx602y.js} +4 -4
- package/dist/chunks/{studio-amaz545p.js → studio-nngc45ct.js} +4 -4
- package/dist/chunks/{studio-eqcddvc2.js → studio-nv5eq0f7.js} +173 -173
- package/dist/chunks/{studio-az3wvyh2.js → studio-nzp0sqjg.js} +4 -4
- package/dist/chunks/{studio-k2evngrj.js → studio-pgze9p34.js} +5 -5
- package/dist/chunks/studio-ph17an19.js +5 -0
- package/dist/chunks/studio-pte5w7rf.js +9 -0
- package/dist/chunks/studio-pv1ts08p.js +6 -0
- package/dist/chunks/studio-q509sqt8.js +5 -0
- package/dist/chunks/{studio-rr00ex23.js → studio-qarg1xf9.js} +6 -6
- package/dist/chunks/studio-r5g7fx7q.js +9 -0
- package/dist/chunks/studio-s49y6m5b.js +5 -0
- package/dist/chunks/studio-s4k1v7dt.js +7 -0
- package/dist/chunks/studio-t46rt6gx.js +5 -0
- package/dist/chunks/{studio-61rg2cz5.js → studio-td3h1644.js} +4 -4
- package/dist/chunks/{studio-v7a7yb5w.js → studio-thgexj3w.js} +7 -7
- package/dist/chunks/{studio-vpevkfxw.js → studio-veh91hjv.js} +4 -4
- package/dist/chunks/studio-vswmz40d.js +6 -0
- package/dist/chunks/{studio-daksw8kh.js → studio-w3aafbpk.js} +4 -4
- package/dist/chunks/studio-w4mfaaam.js +5 -0
- package/dist/chunks/{studio-4zd6an2b.js → studio-wn2cmf9q.js} +4 -4
- package/dist/chunks/studio-x5ffyssw.js +5 -0
- package/dist/chunks/{studio-swedpd82.js → studio-x5py815g.js} +4 -4
- package/dist/chunks/{studio-4fnb5648.js → studio-xh6rf05g.js} +4 -4
- package/dist/chunks/studio-z6s56dzz.js +5 -0
- package/dist/chunks/{studio-nvcn2ykc.js → studio-zav9hgb3.js} +4 -4
- package/dist/chunks/{studio-a88ej93p.js → studio-ze1sm147.js} +6 -6
- package/dist/chunks/{studio-pj216fmn.js → studio-zkkt13qd.js} +4 -4
- package/dist/chunks/studio-zn7ysyzb.js +5 -0
- package/dist/chunks/{studio-xx3r3sb0.js → studio-ztzf8mce.js} +4 -4
- package/dist/chunks/{studio-1410q5dw.js → studio-zzgp5yka.js} +4 -4
- package/dist/chunks/{tooltip-dj1pvgz7.js → tooltip-gtx40bjg.js} +3 -3
- package/dist/chunks/{tree-j0ebwr5s.js → tree-a19s1c05.js} +2 -2
- package/dist/chunks/tsMode-3f116hth.js +14 -0
- package/dist/chunks/workspace-home-61n1jz3q.js +4 -0
- package/dist/chunks/workspace-sheek6zw.js +4 -0
- package/dist/chunks/{ws-client-arz2qsy9.js → ws-client-cb49m87s.js} +3 -3
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +82 -82
- package/dist/studio.js +97 -96
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +11 -11
- package/src/browse/library-layouts.ts +9 -12
- package/src/browse/library-pane.ts +22 -16
- package/src/browse/library-source.ts +3 -4
- package/src/canvas/canvas-live-render.ts +2 -5
- package/src/canvas/canvas-page-context.ts +5 -0
- package/src/content/entry-commands.ts +32 -27
- package/src/content/entry-editor.ts +46 -44
- package/src/content/entry-inspector.ts +15 -12
- package/src/content/entry-model.ts +3 -3
- package/src/content/entry-summaries.ts +5 -5
- package/src/content/hold-state.ts +172 -0
- package/src/content/page-summaries.ts +10 -14
- package/src/editor/inline-edit.ts +6 -1
- package/src/editor/repeater-scope.ts +333 -74
- package/src/grid/sources/content-source.ts +42 -66
- package/src/panels/block-action-bar.ts +3 -4
- package/src/panels/events-panel.ts +6 -2
- package/src/panels/formula-workspace.ts +9 -0
- package/src/panels/properties-panel.ts +127 -35
- package/src/panels/statement-editor.ts +4 -0
- package/src/panels/tab-strip.ts +29 -26
- package/src/services/ai-project-tools.ts +3 -3
- package/src/services/ai-tools.ts +5 -5
- package/src/services/ai-write-report.ts +22 -4
- package/src/services/context-manager.ts +110 -2
- package/src/services/document-assistant.ts +2 -2
- package/src/services/harness/chat-reducer.ts +9 -4
- package/src/services/render-critic.ts +234 -12
- package/src/services/tool-executor.ts +16 -8
- package/src/surfaces/collections-panel.json +1 -1
- package/src/surfaces/collections-panel.ts +1 -1
- package/src/surfaces/entry-editor.json +10 -10
- package/src/surfaces/entry-editor.ts +22 -19
- package/src/surfaces/entry-inspector.json +11 -11
- package/src/surfaces/entry-inspector.ts +17 -10
- package/src/surfaces/tab-strip.json +5 -5
- package/src/surfaces/tab-strip.ts +2 -2
- package/src/types.ts +2 -0
- package/src/ui/expression-editor.ts +10 -5
- package/src/ui/schema-form.ts +18 -1
- package/src/utils/edit-display.ts +27 -1
- package/dist/chunks/2020-906vx2dq.js +0 -4
- package/dist/chunks/adapter-hnvad61x.js +0 -4
- package/dist/chunks/cf-settings-mkbcz4z2.js +0 -4
- package/dist/chunks/color-04361ttk.js +0 -4
- package/dist/chunks/components-w5zqwva1.js +0 -4
- package/dist/chunks/content-source-g38w0d3f.js +0 -4
- package/dist/chunks/editor-wvf5e6j9.js +0 -4
- package/dist/chunks/entry-commands-t13dyw3c.js +0 -4
- package/dist/chunks/file-ops-1sghzgcr.js +0 -4
- package/dist/chunks/files-zrqtgt41.js +0 -4
- package/dist/chunks/format-host-9gy76erh.js +0 -4
- package/dist/chunks/fs-events-45rhg3me.js +0 -4
- package/dist/chunks/grid-open-t4fq6bbm.js +0 -4
- package/dist/chunks/iframe-host-9aq8ze3q.js +0 -4
- package/dist/chunks/index-f9qr7kzh.js +0 -4
- package/dist/chunks/jsonMode-vj30te2h.js +0 -13
- package/dist/chunks/jx-validate-r4ax11e3.js +0 -4
- package/dist/chunks/layers-m49nmwvx.js +0 -4
- package/dist/chunks/layers-panel-zr92pwdp.js +0 -4
- package/dist/chunks/lit-html-k2mb9k0r.js +0 -4
- package/dist/chunks/media-library-j74rt0w4.js +0 -4
- package/dist/chunks/media-open-mypr0k6b.js +0 -4
- package/dist/chunks/media-picker-pemtbsjv.js +0 -4
- package/dist/chunks/menu-d2jxpgma.js +0 -4
- package/dist/chunks/monaco-buffer-pa0g6ax7.js +0 -4
- package/dist/chunks/monaco-setup-0hw9f835.js +0 -4
- package/dist/chunks/notify-bwehrp7q.js +0 -4
- package/dist/chunks/platform-kv0k65cy.js +0 -4
- package/dist/chunks/popover-9tf4cjrk.js +0 -4
- package/dist/chunks/publish-panel-vg1n880b.js +0 -4
- package/dist/chunks/publish-workspace-jv4w2bx2.js +0 -4
- package/dist/chunks/quick-search-nh29rt8a.js +0 -4
- package/dist/chunks/references-vqyr5s9x.js +0 -4
- package/dist/chunks/right-panel-3ws9f2w0.js +0 -4
- package/dist/chunks/source-control-h3hcs5gy.js +0 -4
- package/dist/chunks/studio-0khd8xtx.js +0 -4
- package/dist/chunks/studio-1ctnw3xv.js +0 -5
- package/dist/chunks/studio-1ks5d9y4.js +0 -5
- package/dist/chunks/studio-1y9mab9m.js +0 -5
- package/dist/chunks/studio-2kpxm0eh.js +0 -7
- package/dist/chunks/studio-2zz4qbv9.js +0 -5
- package/dist/chunks/studio-3pz4t5a2.js +0 -5
- package/dist/chunks/studio-4f7thq0p.js +0 -5
- package/dist/chunks/studio-7rrdbvex.js +0 -5
- package/dist/chunks/studio-9dq7rwc7.js +0 -5
- package/dist/chunks/studio-acer0g8w.js +0 -5
- package/dist/chunks/studio-brh8d2pr.js +0 -5
- package/dist/chunks/studio-c2fa783p.js +0 -9
- package/dist/chunks/studio-cfnn70h1.js +0 -5
- package/dist/chunks/studio-cqxj4fcy.js +0 -5
- package/dist/chunks/studio-cw11h078.js +0 -5
- package/dist/chunks/studio-dn56z7sn.js +0 -5
- package/dist/chunks/studio-egnjpyde.js +0 -9
- package/dist/chunks/studio-ejrypvmq.js +0 -5
- package/dist/chunks/studio-frc16j8v.js +0 -10
- package/dist/chunks/studio-j0zf5x2x.js +0 -5
- package/dist/chunks/studio-j5fcp6gj.js +0 -5
- package/dist/chunks/studio-jd1qb1st.js +0 -7
- package/dist/chunks/studio-k1xqrgm5.js +0 -5
- package/dist/chunks/studio-k3we3kqb.js +0 -5
- package/dist/chunks/studio-knsjyx7n.js +0 -5
- package/dist/chunks/studio-mrjtnm62.js +0 -5
- package/dist/chunks/studio-n9qba0gx.js +0 -5
- package/dist/chunks/studio-nzemfbkk.js +0 -14
- package/dist/chunks/studio-pwctzka3.js +0 -16
- package/dist/chunks/studio-q80fg1d6.js +0 -12
- package/dist/chunks/studio-q9hx2qp0.js +0 -5
- package/dist/chunks/studio-qrzmfaf6.js +0 -5
- package/dist/chunks/studio-tmfcn347.js +0 -6
- package/dist/chunks/studio-wqfaqw4m.js +0 -5
- package/dist/chunks/studio-xpx91fby.js +0 -6
- package/dist/chunks/studio-xva3hzkv.js +0 -5
- package/dist/chunks/studio-z3fztxrp.js +0 -9
- package/dist/chunks/studio-z9453ypn.js +0 -6
- package/dist/chunks/tsMode-jg1dtphj.js +0 -14
- package/dist/chunks/workspace-6ztqcrsh.js +0 -4
- package/dist/chunks/workspace-home-rse9h8a0.js +0 -4
- package/src/content/draft-state.ts +0 -108
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Repeater scope resolution
|
|
3
|
-
* the
|
|
2
|
+
* Repeater scope resolution: what the item of a repeater (`$prototype: "Array"`) holds, deduced
|
|
3
|
+
* from the document and the project without running either.
|
|
4
|
+
*
|
|
5
|
+
* Two surfaces ask. Inline text edited INSIDE a repeater inserts a `${item.data.title}` token, and
|
|
6
|
+
* the `From data…` rung binds a `$map/item/data/title` pointer. They are the same field spelled for
|
|
7
|
+
* two grammars, so there is one walk and each field carries both spellings. A third surface asks
|
|
8
|
+
* for the one field that is neither text nor a value: `$children`, which an element draws by
|
|
9
|
+
* holding it as computed children (spec.md §8.4).
|
|
4
10
|
*
|
|
5
11
|
* There is no live `$map` in edit mode: `prepareForEditMode` renders a mapped array as a
|
|
6
12
|
* `repeater-perimeter` wrapping one glyph-ified template instance, so `renderMappedArrayInto`
|
|
@@ -9,23 +15,30 @@
|
|
|
9
15
|
* document path (which carries a real `"map"` segment — see canvas/path-mapping.ts
|
|
10
16
|
* `classifyRenderNode`).
|
|
11
17
|
*
|
|
12
|
-
* Pure and DOM-free: deps limited to `@jxsuite/schema` guards/types
|
|
18
|
+
* Pure and DOM-free: deps limited to `@jxsuite/schema` guards/types, the runtime's pointer grammar
|
|
19
|
+
*
|
|
20
|
+
* - {@link getNodeAtPath}.
|
|
21
|
+
*
|
|
22
|
+
* @docs studio/design/repeaters
|
|
13
23
|
*/
|
|
14
24
|
|
|
25
|
+
import { escapeToken, refSegments } from "@jxsuite/runtime/pointer";
|
|
15
26
|
import { getNodeAtPath } from "@jxsuite/schema/doc-ops";
|
|
16
27
|
import {
|
|
17
28
|
isJsonObject,
|
|
18
29
|
isMappedArray,
|
|
19
|
-
isNonNullObject,
|
|
20
30
|
isRef,
|
|
31
|
+
isSchemaOnlyDef,
|
|
21
32
|
isString,
|
|
22
33
|
} from "@jxsuite/schema/guards";
|
|
23
34
|
|
|
24
35
|
import type { JxPath } from "../state";
|
|
25
36
|
import type {
|
|
37
|
+
JsonObject,
|
|
38
|
+
JsonValue,
|
|
39
|
+
JxDocumentValue,
|
|
26
40
|
JxMappedArray,
|
|
27
41
|
JxMutableNode,
|
|
28
|
-
JxStateDefinition,
|
|
29
42
|
ProjectConfig,
|
|
30
43
|
} from "@jxsuite/schema/types";
|
|
31
44
|
|
|
@@ -58,93 +71,339 @@ export function findEnclosingRepeater(
|
|
|
58
71
|
return isMappedArray(node) ? node : null;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
74
|
+
/** One field of a repeater's item, in each grammar that can name it. */
|
|
75
|
+
export interface RepeaterField {
|
|
76
|
+
/** The members walked from the item, unescaped. */
|
|
77
|
+
path: string[];
|
|
78
|
+
/** What a template reads and a person is shown: `item.data.title`. */
|
|
79
|
+
token: string;
|
|
80
|
+
/** What a `$ref` holds: `$map/item/data/title`, with each member escaped (RFC 6901 §3). */
|
|
81
|
+
pointer: string;
|
|
82
|
+
/** Whether the field holds a list, which is all a nested repeater's Items can read. */
|
|
83
|
+
isArray: boolean;
|
|
84
|
+
/** Whether the field holds child nodes, which only an element's computed children can draw. */
|
|
85
|
+
holdsNodes: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The one `$`-prefixed member an item offers: the Jx tree a parsed body came out as. */
|
|
89
|
+
const NODES_MEMBER = "$children";
|
|
90
|
+
|
|
91
|
+
/** The content format whose loader parses an entry's body into {@link NODES_MEMBER}. */
|
|
92
|
+
const NODES_FORMAT = "Markdown";
|
|
93
|
+
|
|
94
|
+
/** What every step of the walk reads from. */
|
|
95
|
+
interface ItemSources {
|
|
96
|
+
doc: JxMutableNode;
|
|
97
|
+
config: ProjectConfig | null | undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Marks a schema as an entry of this content type, so a relationship cycle can be stopped. */
|
|
101
|
+
const ENTRY_TYPE = "$entryType";
|
|
102
|
+
|
|
103
|
+
/** Marks an entry's `data` member, which is a level of the path rather than a field to offer. */
|
|
104
|
+
const ENTRY_DATA = "$entryData";
|
|
105
|
+
|
|
106
|
+
/** How many members a sample list contributes to the shape it implies. */
|
|
107
|
+
const SAMPLE_CAP = 20;
|
|
108
|
+
|
|
109
|
+
/** How many members deep a field path may run, so a walk of a deep schema stays a short list. */
|
|
110
|
+
const DEPTH_CAP = 6;
|
|
111
|
+
|
|
112
|
+
/** How many pointers a `$ref` chain may follow, so `a → b → a` ends. */
|
|
113
|
+
const HOP_CAP = 8;
|
|
114
|
+
|
|
115
|
+
/** The shape of what a content type's loader hands back (`ContentLoaderEntry`). */
|
|
116
|
+
function entrySchema(contentType: string, ctx: ItemSources): JsonObject {
|
|
117
|
+
/* The `content` section is contributed by an extension and absorbed by the project config's index
|
|
118
|
+
signature, so each level is checked before it is read. */
|
|
119
|
+
const content: unknown = ctx.config?.content;
|
|
120
|
+
const type = isJsonObject(content) ? content[contentType] : undefined;
|
|
121
|
+
const data = isJsonObject(type) && isJsonObject(type.schema) ? type.schema : {};
|
|
122
|
+
// A CSV row has no body to parse. A type naming no format is not known to lack one.
|
|
123
|
+
const format = isJsonObject(type) ? type.format : undefined;
|
|
124
|
+
const hasNodes = !isString(format) || format === NODES_FORMAT;
|
|
125
|
+
// Built from entries because the order is what the picker lists: id and body, then the fields.
|
|
126
|
+
const members: [string, JsonValue][] = [
|
|
127
|
+
["id", {}],
|
|
128
|
+
["body", {}],
|
|
129
|
+
...(hasNodes ? ([[NODES_MEMBER, {}]] satisfies [string, JsonValue][]) : []),
|
|
130
|
+
["data", { ...data, [ENTRY_DATA]: true }],
|
|
131
|
+
];
|
|
132
|
+
return { [ENTRY_TYPE]: contentType, properties: Object.fromEntries(members) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether a schema's own keys say what is inside it, so a sample has nothing to add. */
|
|
136
|
+
function describesFields(schema: JsonObject | null, ctx: ItemSources): boolean {
|
|
137
|
+
return (
|
|
138
|
+
isJsonObject(schema?.properties) || isJsonObject(followPointers(schema?.items, ctx)?.properties)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Follow a schema's pointers to the schema they name, or `null` for one this walk cannot read.
|
|
144
|
+
*
|
|
145
|
+
* Three spellings reach another schema: `{ "$ref": "#/content/authors" }` is a relationship, whose
|
|
146
|
+
* value the loader resolves to the full entry; `{ "$ref": "#/$defs/Member" }` is a shared type; and
|
|
147
|
+
* `{ "type": { "$ref": … } }` is a typed value naming one.
|
|
148
|
+
*/
|
|
149
|
+
function followPointers(
|
|
150
|
+
schema: JxDocumentValue | undefined,
|
|
151
|
+
ctx: ItemSources,
|
|
152
|
+
hops = 0,
|
|
153
|
+
): JsonObject | null {
|
|
154
|
+
if (!isJsonObject(schema) || hops > HOP_CAP) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const pointer = isRef(schema) ? schema.$ref : isRef(schema.type) ? schema.type.$ref : null;
|
|
158
|
+
if (pointer === null) {
|
|
159
|
+
return schema;
|
|
160
|
+
}
|
|
161
|
+
if (pointer.startsWith("#/content/")) {
|
|
162
|
+
return entrySchema(pointer.slice("#/content/".length), ctx);
|
|
163
|
+
}
|
|
164
|
+
if (pointer.startsWith("#/$defs/")) {
|
|
165
|
+
const [name, ...rest] = refSegments(pointer.slice("#/$defs/".length));
|
|
166
|
+
const projectDefs: unknown = ctx.config?.$defs;
|
|
167
|
+
const declared =
|
|
168
|
+
name === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: (ctx.doc.$defs?.[name] ?? (isJsonObject(projectDefs) ? projectDefs[name] : undefined));
|
|
171
|
+
return walkSchema(followPointers(declared, ctx, hops + 1), rest, ctx);
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The union of two implied shapes, so an optional field the first row lacks is still found. */
|
|
177
|
+
function mergeSamples(a: JsonObject, b: JsonObject): JsonObject {
|
|
178
|
+
const merged: JsonObject = { ...a, ...b };
|
|
179
|
+
if (isJsonObject(a.items) && isJsonObject(b.items)) {
|
|
180
|
+
merged.items = mergeSamples(a.items, b.items);
|
|
181
|
+
}
|
|
182
|
+
if (isJsonObject(a.properties) && isJsonObject(b.properties)) {
|
|
183
|
+
const properties: JsonObject = { ...a.properties, ...b.properties };
|
|
184
|
+
for (const [key, left] of Object.entries(a.properties)) {
|
|
185
|
+
const right = b.properties[key];
|
|
186
|
+
if (isJsonObject(left) && isJsonObject(right)) {
|
|
187
|
+
properties[key] = mergeSamples(left, right);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
merged.properties = properties;
|
|
191
|
+
}
|
|
192
|
+
return merged;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The shape a sample value implies. */
|
|
196
|
+
function sampleSchema(value: JsonValue | undefined): JsonObject {
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
let items: JsonObject = {};
|
|
199
|
+
for (const member of value.slice(0, SAMPLE_CAP)) {
|
|
200
|
+
items = mergeSamples(items, sampleSchema(member));
|
|
201
|
+
}
|
|
202
|
+
return { items };
|
|
203
|
+
}
|
|
204
|
+
if (isJsonObject(value)) {
|
|
205
|
+
return {
|
|
206
|
+
properties: Object.fromEntries(
|
|
207
|
+
Object.entries(value).map(([key, member]) => [key, sampleSchema(member)]),
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The shape of a state entry's value, or `null` when only running it would say. */
|
|
215
|
+
function stateSchema(def: JxDocumentValue | undefined, ctx: ItemSources): JsonObject | null {
|
|
216
|
+
if (Array.isArray(def)) {
|
|
217
|
+
// SAFETY: a naked array state entry is the JSON value it reads as (schema/types `JxStateDefinition`).
|
|
218
|
+
return sampleSchema(def as JsonValue[]);
|
|
219
|
+
}
|
|
220
|
+
if (!isJsonObject(def)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/* Detected on the `contentType` string, not the prototype's name, so an import-aliased
|
|
224
|
+
`$prototype` still matches. An `id` is what asks for one entry rather than the list. */
|
|
225
|
+
if (isString(def.contentType)) {
|
|
226
|
+
const entry = entrySchema(def.contentType, ctx);
|
|
227
|
+
return def.$prototype === "ContentEntry" || def.id !== undefined ? entry : { items: entry };
|
|
228
|
+
}
|
|
229
|
+
const declared = followPointers(def, ctx);
|
|
230
|
+
if (describesFields(declared, ctx)) {
|
|
231
|
+
return declared;
|
|
232
|
+
}
|
|
233
|
+
if ("default" in def) {
|
|
234
|
+
return sampleSchema(def.default);
|
|
235
|
+
}
|
|
236
|
+
const isNakedObject = !isSchemaOnlyDef(def) && !Object.keys(def).some((k) => k.startsWith("$"));
|
|
237
|
+
return isNakedObject ? sampleSchema(def) : declared;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Follow members down a schema, or `null` where the schema stops saying. */
|
|
241
|
+
function walkSchema(
|
|
242
|
+
schema: JsonObject | null,
|
|
243
|
+
segments: readonly string[],
|
|
244
|
+
ctx: ItemSources,
|
|
245
|
+
): JsonObject | null {
|
|
246
|
+
let current = followPointers(schema, ctx);
|
|
247
|
+
for (const segment of segments) {
|
|
248
|
+
const member = isJsonObject(current?.properties) ? current.properties[segment] : undefined;
|
|
249
|
+
current = followPointers(member ?? (/^\d+$/.test(segment) ? current?.items : undefined), ctx);
|
|
250
|
+
}
|
|
251
|
+
return current;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The shape of what a repeater's `items` binding reads, in the scope of the repeater itself. */
|
|
255
|
+
function bindingSchema(
|
|
256
|
+
binding: JxMappedArray["items"],
|
|
257
|
+
repeaterPath: JxPath,
|
|
258
|
+
ctx: ItemSources,
|
|
259
|
+
): JsonObject | null {
|
|
260
|
+
if (Array.isArray(binding)) {
|
|
261
|
+
// SAFETY: an inline `items` list is read straight out of the JSON document.
|
|
262
|
+
return sampleSchema(binding as JsonValue[]);
|
|
263
|
+
}
|
|
63
264
|
if (!isRef(binding)) {
|
|
64
265
|
return null;
|
|
65
266
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
267
|
+
const ref = binding.$ref.replace(/^#\/\$map\//, "$map/");
|
|
268
|
+
if (ref.startsWith("#/state/")) {
|
|
269
|
+
const [name, ...rest] = refSegments(ref.slice("#/state/".length));
|
|
270
|
+
const def = name === undefined ? undefined : ctx.doc.state?.[name];
|
|
271
|
+
return walkSchema(stateSchema(def, ctx), rest, ctx);
|
|
272
|
+
}
|
|
273
|
+
// A nested repeater reads a member of the repeater around it, so the walk starts from that item.
|
|
274
|
+
if (ref === "$map/item" || ref.startsWith("$map/item/")) {
|
|
275
|
+
const rest = refSegments(ref.slice("$map/item/".length));
|
|
276
|
+
return walkSchema(itemSchemaAt(repeaterPath, ctx), rest, ctx);
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The shape of the item in scope at `path`, or `null` outside a repeater or past what is known. */
|
|
282
|
+
function itemSchemaAt(path: JxPath, ctx: ItemSources): JsonObject | null {
|
|
283
|
+
const repeater = findEnclosingRepeater(ctx.doc, path);
|
|
284
|
+
if (!repeater) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const list = bindingSchema(repeater.items, path.slice(0, path.lastIndexOf("map")), ctx);
|
|
288
|
+
return followPointers(list?.items, ctx);
|
|
68
289
|
}
|
|
69
290
|
|
|
70
|
-
/**
|
|
71
|
-
function
|
|
72
|
-
|
|
291
|
+
/** A field name as a template reads it: a member access where a name allows one, else a bracket. */
|
|
292
|
+
function fieldToken(path: readonly string[]): string {
|
|
293
|
+
let token = "item";
|
|
294
|
+
for (const member of path) {
|
|
295
|
+
token += /^[A-Za-z_$][\w$]*$/.test(member) ? `.${member}` : `[${JSON.stringify(member)}]`;
|
|
296
|
+
}
|
|
297
|
+
return token;
|
|
73
298
|
}
|
|
74
299
|
|
|
75
300
|
/**
|
|
76
|
-
*
|
|
77
|
-
* `index`; appends field tokens resolved from the repeater's binding:
|
|
301
|
+
* Every field the item in scope at `path` is known to hold, nearest the item first.
|
|
78
302
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* declared `items.properties`, else are inferred from a sample `default[0]`. Emitted FLAT
|
|
85
|
-
* (`item.<f>`, no `.data.`).
|
|
86
|
-
* - **Inline array** — `items` is a literal array; fields inferred from `items[0]` object keys.
|
|
87
|
-
* - **Fallback** — anything unresolvable yields exactly `["item", "index"]`.
|
|
303
|
+
* Walked a level at a time so {@link FIELD_CAP} cuts the deepest fields and never a top-level one.
|
|
304
|
+
* An object is offered as well as its members, because a component can take the whole value. A list
|
|
305
|
+
* is offered and not entered: one of its members has no single value to bind. A relationship is
|
|
306
|
+
* entered unless the branch already passed through that content type, which ends a post → author →
|
|
307
|
+
* post cycle.
|
|
88
308
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
309
|
+
* `$children` is left out unless asked for: it is a tree of nodes, so a text position, an attribute
|
|
310
|
+
* or a formula operand that read it would draw `[object Object]`.
|
|
311
|
+
*
|
|
312
|
+
* @param doc - The tab's document root (`tab.doc.document`).
|
|
313
|
+
* @param path - The path of the node asking.
|
|
91
314
|
* @param projectConfig - Parsed `project.json` (`projectState?.projectConfig`).
|
|
92
|
-
* @
|
|
315
|
+
* @param options - `withNodes` also lists the fields that hold child nodes.
|
|
316
|
+
* @returns The fields, empty outside a repeater and wherever the item's shape cannot be deduced.
|
|
93
317
|
*/
|
|
94
|
-
export function
|
|
95
|
-
|
|
96
|
-
|
|
318
|
+
export function repeaterItemFields(
|
|
319
|
+
doc: JxMutableNode,
|
|
320
|
+
path: JxPath | null | undefined,
|
|
97
321
|
projectConfig: ProjectConfig | null | undefined,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const out = [...base, "item.id", "item.body"];
|
|
117
|
-
if (isNonNullObject(props)) {
|
|
118
|
-
for (const f of safeFields(Object.keys(props))) {
|
|
119
|
-
out.push(`item.data.${f}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
322
|
+
{ withNodes = false }: { withNodes?: boolean } = {},
|
|
323
|
+
): RepeaterField[] {
|
|
324
|
+
if (!Array.isArray(path)) {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
const ctx: ItemSources = { config: projectConfig, doc };
|
|
328
|
+
const fields: RepeaterField[] = [];
|
|
329
|
+
const pending: { at: string[]; schema: JsonObject | null; via: string[] }[] = [
|
|
330
|
+
{ at: [], schema: itemSchemaAt(path, ctx), via: [] },
|
|
331
|
+
];
|
|
332
|
+
for (const { at, schema, via } of pending) {
|
|
333
|
+
const entryType = isString(schema?.[ENTRY_TYPE]) ? schema[ENTRY_TYPE] : null;
|
|
334
|
+
const passed = entryType === null ? via : [...via, entryType];
|
|
335
|
+
const members = isJsonObject(schema?.properties) ? Object.entries(schema.properties) : [];
|
|
336
|
+
for (const [key, member] of members) {
|
|
337
|
+
const holdsNodes = key === NODES_MEMBER;
|
|
338
|
+
if ((key.startsWith("$") && !(holdsNodes && withNodes)) || fields.length >= FIELD_CAP) {
|
|
339
|
+
continue;
|
|
123
340
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
341
|
+
const memberPath = [...at, key];
|
|
342
|
+
const resolved = followPointers(member, ctx);
|
|
343
|
+
if (resolved?.[ENTRY_DATA] !== true) {
|
|
344
|
+
fields.push({
|
|
345
|
+
holdsNodes,
|
|
346
|
+
isArray: resolved?.type === "array" || isJsonObject(resolved?.items),
|
|
347
|
+
path: memberPath,
|
|
348
|
+
pointer: `$map/item/${memberPath.map((m) => escapeToken(m)).join("/")}`,
|
|
349
|
+
token: fieldToken(memberPath),
|
|
350
|
+
});
|
|
130
351
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return [...base, ...safeFields(Object.keys(sample)).map((f) => `item.${f}`)];
|
|
352
|
+
const type = isString(resolved?.[ENTRY_TYPE]) ? resolved[ENTRY_TYPE] : null;
|
|
353
|
+
if (memberPath.length < DEPTH_CAP && !(type !== null && passed.includes(type))) {
|
|
354
|
+
pending.push({ at: memberPath, schema: resolved, via: passed });
|
|
135
355
|
}
|
|
136
356
|
}
|
|
137
|
-
return base;
|
|
138
357
|
}
|
|
358
|
+
return fields;
|
|
359
|
+
}
|
|
139
360
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
361
|
+
/**
|
|
362
|
+
* The local scope tokens for text edited inside a repeater: `item` and `index`, then the fields of
|
|
363
|
+
* the item in scope (see {@link repeaterItemFields}).
|
|
364
|
+
*
|
|
365
|
+
* @returns Token strings, e.g. `["item", "index", "item.id", "item.body", "item.data.title"]`.
|
|
366
|
+
*/
|
|
367
|
+
export function resolveRepeaterItemFields(
|
|
368
|
+
doc: JxMutableNode,
|
|
369
|
+
path: JxPath | null | undefined,
|
|
370
|
+
projectConfig: ProjectConfig | null | undefined,
|
|
371
|
+
): string[] {
|
|
372
|
+
return ["item", "index", ...repeaterItemFields(doc, path, projectConfig).map((f) => f.token)];
|
|
373
|
+
}
|
|
147
374
|
|
|
148
|
-
|
|
149
|
-
|
|
375
|
+
/**
|
|
376
|
+
* The `children` template that draws a field's nodes. The fallback is part of it because the field
|
|
377
|
+
* is only known to be possible: an entry loaded without a body has no `$children` to read.
|
|
378
|
+
*/
|
|
379
|
+
export function childrenTemplate(field: RepeaterField): string {
|
|
380
|
+
return `\${${field.token} ?? []}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The field a `children` template reads, with or without the fallback {@link childrenTemplate}
|
|
385
|
+
* writes. Anything more than the bare field is the author's own expression and is left as theirs.
|
|
386
|
+
*/
|
|
387
|
+
export function fieldOfChildrenTemplate(
|
|
388
|
+
template: string,
|
|
389
|
+
fields: readonly RepeaterField[],
|
|
390
|
+
): RepeaterField | undefined {
|
|
391
|
+
const token = /^\$\{\s*(.+?)\s*(?:\?\?\s*\[\s*\]\s*)?\}$/u.exec(template)?.[1];
|
|
392
|
+
return fields.find((field) => field.token === token);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* A `$map/…` pointer in the words {@link repeaterItemFields} offers it under, or `null` for any
|
|
397
|
+
* other pointer. A bound chip says the same name the picker did: `item.data.title`.
|
|
398
|
+
*/
|
|
399
|
+
export function mapRefLabel(ref: string): string | null {
|
|
400
|
+
if (ref === "$map/index") {
|
|
401
|
+
return "index";
|
|
402
|
+
}
|
|
403
|
+
if (ref === "$map/item") {
|
|
404
|
+
return "item";
|
|
405
|
+
}
|
|
406
|
+
return ref.startsWith("$map/item/")
|
|
407
|
+
? fieldToken(refSegments(ref.slice("$map/item/".length)))
|
|
408
|
+
: null;
|
|
150
409
|
}
|
|
@@ -12,22 +12,18 @@
|
|
|
12
12
|
* Collections type their columns from the content-type schema (plus inferred extras found in the
|
|
13
13
|
* data); the pages grid is fully inference-driven.
|
|
14
14
|
*
|
|
15
|
-
* **A collection listing obeys the
|
|
16
|
-
* `content/
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* **A collection listing obeys the hold perspective (site-architecture.md §7.6).**
|
|
16
|
+
* `content/hold-state.ts` owns both the one flag and the reading of a project's rules, and this
|
|
17
|
+
* source calls it rather than reading fields itself, which is what makes "including held entries"
|
|
18
|
+
* mean the same thing here as it does on the tab chip. It can, because that module does not import
|
|
19
|
+
* the richer `content/entry-model.ts`, which asks THIS file where a collection's entries live, so
|
|
20
|
+
* an edge back to it would be the cycle `import/no-cycle` rejects.
|
|
21
21
|
*/
|
|
22
22
|
import { getPlatform } from "../../platform";
|
|
23
23
|
import { projectState } from "../../store";
|
|
24
24
|
import { workspace } from "../../workspace/workspace";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
applyDraftFilter,
|
|
28
|
-
hasDraftAxis,
|
|
29
|
-
includingDrafts,
|
|
30
|
-
} from "../../content/draft-state";
|
|
25
|
+
import { applyHoldFilter, holdSwitch, includingHeld } from "../../content/hold-state";
|
|
26
|
+
import type { HoldBearer } from "../../content/hold-state";
|
|
31
27
|
import { getGridController } from "../grid-controller";
|
|
32
28
|
import { isRecentLocal, markLocalMutation } from "../../files/fs-events";
|
|
33
29
|
import {
|
|
@@ -275,39 +271,38 @@ interface EntryFileSourceOptions {
|
|
|
275
271
|
/** Inferred columns to front-load, in order (e.g. title/description for pages). */
|
|
276
272
|
priorityFields?: string[];
|
|
277
273
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* to explain why.
|
|
274
|
+
* The collection's definition, when this listing is subject to the hold perspective
|
|
275
|
+
* (site-architecture.md §7.6). Collections are and the pages tree is not: `hold` rules are
|
|
276
|
+
* written on a content type, and a page belongs to none.
|
|
282
277
|
*/
|
|
283
|
-
|
|
278
|
+
holdDef?: () => HoldBearer | null | undefined;
|
|
284
279
|
}
|
|
285
280
|
|
|
286
281
|
/**
|
|
287
|
-
* The live sources that obey the
|
|
282
|
+
* The live sources that obey the hold perspective.
|
|
288
283
|
*
|
|
289
284
|
* A `WeakSet` of the source objects rather than a list of tab-id prefixes: whether a listing is
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
285
|
+
* filtered is decided by one `holdDef` in this file, and re-deriving it from `"grid://collection/"`
|
|
286
|
+
* in {@link reloadHoldAwareGrids} would be a second answer free to drift. Membership is added by the
|
|
287
|
+
* engine off that same option, so the two cannot disagree. Weak, so a closed tab's source is
|
|
288
|
+
* collectable.
|
|
294
289
|
*/
|
|
295
|
-
const
|
|
290
|
+
const holdAwareSources = new WeakSet<GridSource>();
|
|
296
291
|
|
|
297
292
|
/**
|
|
298
|
-
* Reload every open grid whose rows depend on the
|
|
293
|
+
* Reload every open grid whose rows depend on the hold perspective.
|
|
299
294
|
*
|
|
300
295
|
* The perspective is a flag, and a grid holds its rows in controller state that was loaded once —
|
|
301
|
-
* so `content.
|
|
296
|
+
* so `content.setIncludeHeld` changing the flag has to say so. `load()` (not `refresh()`) is the
|
|
302
297
|
* right verb: it re-pulls columns and rows and leaves the edit buffer alone, whereas `refresh()`
|
|
303
298
|
* would stop to ask an author with pending edits whether to discard them, which is not a question a
|
|
304
299
|
* change of perspective should provoke.
|
|
305
300
|
*/
|
|
306
|
-
export async function
|
|
301
|
+
export async function reloadHoldAwareGrids(): Promise<void> {
|
|
307
302
|
const reloads: Promise<void>[] = [];
|
|
308
303
|
for (const tab of workspace.tabs.values()) {
|
|
309
304
|
const controller = getGridController(tab);
|
|
310
|
-
if (controller &&
|
|
305
|
+
if (controller && holdAwareSources.has(controller.source)) {
|
|
311
306
|
reloads.push(controller.load());
|
|
312
307
|
}
|
|
313
308
|
}
|
|
@@ -317,8 +312,8 @@ export async function reloadDraftAwareGrids(): Promise<void> {
|
|
|
317
312
|
/** Shared engine for frontmatter-file sources (collections, pages). */
|
|
318
313
|
function createEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
319
314
|
const source = buildEntryFileSource(opts);
|
|
320
|
-
if (opts.
|
|
321
|
-
|
|
315
|
+
if (opts.holdDef) {
|
|
316
|
+
holdAwareSources.add(source);
|
|
322
317
|
}
|
|
323
318
|
return source;
|
|
324
319
|
}
|
|
@@ -420,43 +415,26 @@ function buildEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
|
420
415
|
extraColumns.sort((a, b) => rank(a) - rank(b));
|
|
421
416
|
}
|
|
422
417
|
|
|
423
|
-
columnsCache =
|
|
418
|
+
columnsCache = withHoldColumn([pathColumn, ...schemaColumns, ...extraColumns]);
|
|
424
419
|
return columnsCache;
|
|
425
420
|
};
|
|
426
421
|
|
|
427
|
-
/** Whether this listing has a draft axis at all — the schema declares one, or an entry claims one. */
|
|
428
|
-
const draftAxis = (): boolean => {
|
|
429
|
-
if (!opts.drafts) {
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
const schema = opts.schema();
|
|
433
|
-
return (
|
|
434
|
-
hasDraftAxis(schema) ||
|
|
435
|
-
[...entries.values()].some((entry) => hasDraftAxis(schema, entry.frontmatter))
|
|
436
|
-
);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
422
|
/**
|
|
440
|
-
* Put the
|
|
441
|
-
*
|
|
442
|
-
* A MOVE, and deliberately not a synthesis: wherever there is an axis there is already a column.
|
|
443
|
-
* A schema that declares `draft` gave `columnsFromSchema` one, and an entry that carries the
|
|
444
|
-
* field without the schema declaring it gave `inferColumnsFromRows` one — the two conditions of
|
|
445
|
-
* {@link hasDraftAxis} are exactly the two conditions that produce a column. Building a third here
|
|
446
|
-
* would be an unreachable branch pretending to be a safety net.
|
|
423
|
+
* Put the hold switch's column immediately after Path, when the collection's rules offer one.
|
|
447
424
|
*
|
|
425
|
+
* A MOVE, and deliberately not a synthesis: a field the rules name but neither the schema nor any
|
|
426
|
+
* entry states has no column to move, and inventing one would show a state nobody wrote down.
|
|
448
427
|
* What the move buys is that the one field deciding whether an entry appears in its own list is
|
|
449
428
|
* not sitting out at column nine, where declaration order or first-sight order happened to put
|
|
450
|
-
* it.
|
|
451
|
-
* column is ordinary data there, and promoting it would invent a workflow the project never
|
|
452
|
-
* declared.
|
|
429
|
+
* it.
|
|
453
430
|
*/
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
|
|
431
|
+
const withHoldColumn = (columns: GridColumn[]): GridColumn[] => {
|
|
432
|
+
const field = holdSwitch(opts.holdDef?.())?.field;
|
|
433
|
+
const index = columns.findIndex((column) => column.field === field);
|
|
434
|
+
if (index < 1) {
|
|
457
435
|
return columns;
|
|
458
436
|
}
|
|
459
|
-
const rest = columns.filter((column) => column.field !==
|
|
437
|
+
const rest = columns.filter((column) => column.field !== field);
|
|
460
438
|
return [rest[0]!, columns[index]!, ...rest.slice(1)];
|
|
461
439
|
};
|
|
462
440
|
|
|
@@ -661,15 +639,13 @@ function buildEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
|
661
639
|
async rows(): Promise<GridRowsResult> {
|
|
662
640
|
await load();
|
|
663
641
|
// The perspective is applied to the LISTING, not the load: `entries` keeps every parsed file,
|
|
664
|
-
// So the
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
)
|
|
672
|
-
: [...entries.entries()];
|
|
642
|
+
// So turning the perspective back on is a re-list rather than a re-read of the directory.
|
|
643
|
+
const listed = applyHoldFilter(
|
|
644
|
+
[...entries.entries()],
|
|
645
|
+
opts.holdDef?.(),
|
|
646
|
+
([, entry]) => entry.frontmatter,
|
|
647
|
+
includingHeld(),
|
|
648
|
+
);
|
|
673
649
|
const rows = listed.map(([path, entry]) => rowFor(path, entry));
|
|
674
650
|
return { rows, total: rows.length };
|
|
675
651
|
},
|
|
@@ -687,7 +663,7 @@ export function createCollectionSource(typeName: string): GridSource {
|
|
|
687
663
|
};
|
|
688
664
|
|
|
689
665
|
return createEntryFileSource({
|
|
690
|
-
|
|
666
|
+
holdDef: () => collectionInfo(typeName)?.def,
|
|
691
667
|
id: makeGridTabId({ kind: "collection", name: typeName }),
|
|
692
668
|
insertFormatName: (path) => {
|
|
693
669
|
const info = requireInfo();
|
|
@@ -1193,11 +1193,10 @@ function openMergeTagMenu(anchor: HTMLElement): void {
|
|
|
1193
1193
|
// No live `$map` in edit mode — the perimeter renders one glyph template — so fields resolve
|
|
1194
1194
|
// Parent-side from schema, keyed off the selected element's doc path (which carries a `map` segment).
|
|
1195
1195
|
const selPath = getEditSnapshot().snapshot?.path ?? primarySelection(tab?.session.selection);
|
|
1196
|
-
|
|
1197
|
-
if (arrayNode && tab) {
|
|
1196
|
+
if (tab && selPath && findEnclosingRepeater(tab.doc.document, selPath)) {
|
|
1198
1197
|
const tokens = resolveRepeaterItemFields(
|
|
1199
|
-
|
|
1200
|
-
|
|
1198
|
+
tab.doc.document,
|
|
1199
|
+
selPath,
|
|
1201
1200
|
projectState?.projectConfig,
|
|
1202
1201
|
);
|
|
1203
1202
|
tags.push(...buildRepeaterTagsFromFields(tokens));
|