domma-cms 1.6.60 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4349 -0
- package/README.md +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/mail-compose.js +1 -0
- package/admin/js/lib/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/call-new.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- package/admin/js/views/call-new.js +1 -1
- package/admin/js/views/contacts.js +15 -15
- package/admin/js/views/form-submissions.js +2 -2
- package/docs/DommaFeatures.md +3005 -0
- package/docs/MultiTenant.md +176 -0
- package/docs/api-reference.md +1667 -0
- package/docs/cache.md +96 -0
- package/docs/configuration.md +1811 -0
- package/docs/editions-and-licences.md +190 -0
- package/docs/getting-started.md +201 -0
- package/docs/guides/form-triggers.md +260 -0
- package/docs/guides/mongodb-storage-adapter.md +282 -0
- package/docs/guides/multi-page-forms.md +121 -0
- package/docs/images/scribe-composer.png +0 -0
- package/docs/markdown-shortcodes.md +1738 -0
- package/docs/pentest-report-2026-04-03.md +181 -0
- package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
- package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
- package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
- package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
- package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
- package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
- package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
- package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
- package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
- package/docs/plans/2026-03-05-user-types-design.md +139 -0
- package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
- package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
- package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
- package/docs/plans/2026-06-10-core-project-design.md +240 -0
- package/docs/plugin-development.md +827 -0
- package/docs/recruitment-recipe.md +501 -0
- package/docs/scaffolding.md +312 -0
- package/docs/screenshots/calculator-floating.png +0 -0
- package/docs/screenshots/calculator-standard.png +0 -0
- package/docs/screenshots/email-settings.png +0 -0
- package/docs/screenshots/messages-count.png +0 -0
- package/docs/structure/user-types.md +107 -0
- package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
- package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
- package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
- package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
- package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
- package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
- package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
- package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
- package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
- package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
- package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
- package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
- package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
- package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
- package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
- package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
- package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
- package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
- package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
- package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
- package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
- package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
- package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
- package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
- package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
- package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
- package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
- package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
- package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
- package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
- package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
- package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
- package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
- package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
- package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
- package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
- package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
- package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
- package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
- package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
- package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
- package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
- package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
- package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
- package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
- package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
- package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
- package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
- package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
- package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
- package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
- package/docs/theming.md +320 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/compose-window.js +3 -1
- package/plugins/_lib/admin/mail/identity.js +18 -3
- package/plugins/_lib/admin/mail/reader-view.js +32 -3
- package/plugins/blog/CLAUDE.md +4 -0
- package/plugins/blog/admin/lib/email.js +191 -0
- package/plugins/blog/admin/views/comments.js +3 -1
- package/plugins/blog/plugin.json +2 -2
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/email.test.js +69 -0
- package/plugins/booking/CLAUDE.md +4 -0
- package/plugins/booking/admin/lib/email.js +191 -0
- package/plugins/booking/admin/views/panels.js +7 -3
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/email.test.js +69 -0
- package/plugins/free-tier.lock.json +27 -19
- package/plugins/shopping-cart/CLAUDE.md +4 -0
- package/plugins/shopping-cart/admin/lib/email.js +191 -0
- package/plugins/shopping-cart/admin/views/orders.js +11 -3
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/email.test.js +69 -0
- package/plugins/support/CLAUDE.md +4 -0
- package/plugins/support/admin/lib/email.js +191 -0
- package/plugins/support/admin/views/desk.js +4 -1
- package/plugins/support/admin/views/inbox.js +6 -2
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/email.test.js +69 -0
- package/public/js/celebrations/core/canvas.js +1 -1
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
- package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
- package/public/js/celebrations/message-attrs.mjs +1 -0
- package/public/js/celebrations/themes/anniversary.js +1 -0
- package/public/js/celebrations/themes/autumn.js +1 -0
- package/public/js/celebrations/themes/birthday.js +1 -0
- package/public/js/celebrations/themes/butterfly-flight.js +1 -0
- package/public/js/celebrations/themes/calendar.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year.js +1 -0
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/celebrations/themes/easter.js +1 -0
- package/public/js/celebrations/themes/garden-drawing.js +1 -0
- package/public/js/celebrations/themes/garden.js +1 -0
- package/public/js/celebrations/themes/graduation.js +1 -0
- package/public/js/celebrations/themes/greeting.js +1 -0
- package/public/js/celebrations/themes/make-it-rain.js +1 -0
- package/public/js/celebrations/themes/money-rain.js +1 -0
- package/public/js/celebrations/themes/new-year.js +1 -0
- package/public/js/celebrations/themes/occasion-drawing.js +1 -0
- package/public/js/celebrations/themes/occasions.js +1 -0
- package/public/js/celebrations/themes/rain-control.js +1 -0
- package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
- package/public/js/celebrations/themes/sparkler-font.js +1 -0
- package/public/js/celebrations/themes/sparkler-writing.js +1 -0
- package/public/js/celebrations/themes/summer-garden.js +1 -0
- package/public/js/celebrations/themes/winter.js +1 -0
- package/public/js/celebrations/trait-data.mjs +1 -1
- package/public/js/effects.js +1 -1
- package/scripts/build.js +4 -0
- package/server/services/markdown.js +5 -2
- package/server/templates/page.html +1 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Collection context menu - sort, group, export & copy - design
|
|
2
|
+
|
|
3
|
+
Extends the public-site right-click menu (`public/js/collection-context.js`) from filter-only
|
|
4
|
+
to filter + sort + group + export + copy + print, with schema-level export permissions
|
|
5
|
+
configurable from the collection editor.
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
|
|
9
|
+
The right-click menu on `[collection]` / `[view]` displays currently offers filters and free-text
|
|
10
|
+
search. Add sorting, grouping, export, clipboard copy, print, and a shareable-view link - without
|
|
11
|
+
regressing the one-click speed of the filter path that makes the current menu good.
|
|
12
|
+
|
|
13
|
+
Secondary goal, surfaced during brainstorming: make export **coherent**. Two export surfaces
|
|
14
|
+
already exist and disagree with each other (see Prior art). This design unifies the dialect.
|
|
15
|
+
|
|
16
|
+
## Prior art found during brainstorming
|
|
17
|
+
|
|
18
|
+
Three discoveries reshaped the design. Recorded here because none are visible from the feature's
|
|
19
|
+
own vantage point.
|
|
20
|
+
|
|
21
|
+
1. **`exportable="true"` already exists.** `server/services/markdown.js:474` (tables) and
|
|
22
|
+
`:1122`/`:1231` (Collection Browser) already accept a per-placement export attribute, passed
|
|
23
|
+
through to `T.create` by `public/js/site.js:764`. A schema-level gate must not regress it.
|
|
24
|
+
|
|
25
|
+
2. **`exportEntries()` already exists.** `server/services/collections.js:475` has served the
|
|
26
|
+
admin's CSV/JSON export since the collections feature landed. It quotes every cell and escapes
|
|
27
|
+
`"` as `""` - and nothing else. Object-valued fields export as the literal string
|
|
28
|
+
`[object Object]`; this is a live bug affecting e.g. `blog-posts.seo`.
|
|
29
|
+
|
|
30
|
+
3. **Seven admin view files are committed mangled.** `collection-editor.js`, `form-editor.js`,
|
|
31
|
+
`block-editor.js`, `action-editor.js`, `page-editor.js`, `view-editor.js` and `navigation.js`
|
|
32
|
+
were readable at `35f0bdd` (2026-03-10) and mangled by `d492e38` (2026-03-12) - identifiers
|
|
33
|
+
reduced to single letters, JSDoc stripped. The other 81 files under `admin/js` are unaffected,
|
|
34
|
+
and `scripts/build.js` writes only to `_publish/`, so the current build is not the cause.
|
|
35
|
+
Restoration is out of scope here (see Deliberately deferred).
|
|
36
|
+
|
|
37
|
+
## Decisions (from brainstorming)
|
|
38
|
+
|
|
39
|
+
1. **Export permission lives in the collection schema**, as an `export` block alongside `api`.
|
|
40
|
+
2. **Default on, but role-gated.** Absent config means signed-in users get export and anonymous
|
|
41
|
+
visitors do not. Public export is an explicit opt-in.
|
|
42
|
+
3. **`exportable="true"` is grandfathered as an explicit public opt-in**, because that is exactly
|
|
43
|
+
what it means today. No existing page loses its CSV button on update.
|
|
44
|
+
4. **Scope is the wide tier**: sort, group, export, copy, print, copy-link-to-view.
|
|
45
|
+
5. **Menu structure is drill-in panels**, not hover flyouts or tabs.
|
|
46
|
+
6. **The export gate is client-side and soft**, and is documented as such.
|
|
47
|
+
7. **One shared CSV dialect** across the public export and the server's `exportEntries()`,
|
|
48
|
+
in one shared `.mjs` module imported by both. Fixes the `[object Object]` bug as a side effect.
|
|
49
|
+
|
|
50
|
+
## Schema
|
|
51
|
+
|
|
52
|
+
New optional block in `content/collections/<slug>/schema.json`, parallel to `api`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
"export": {
|
|
56
|
+
"enabled": true,
|
|
57
|
+
"access": "subscriber",
|
|
58
|
+
"formats": ["csv", "json"]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `enabled` - default `true`.
|
|
63
|
+
- `access` - `"public"` or a role name. Default: the least-privileged role in
|
|
64
|
+
`userTypes.getRoleHierarchy()` (the highest level number), so the default is effectively
|
|
65
|
+
"any authenticated user". Never `"token"`; API tokens are not a browser concept.
|
|
66
|
+
- `formats` - subset of `["csv", "json"]`. Default: both.
|
|
67
|
+
|
|
68
|
+
### Resolution order
|
|
69
|
+
|
|
70
|
+
Evaluated per display, first match wins:
|
|
71
|
+
|
|
72
|
+
1. `export.enabled === false` → no export. `exportable="true"` is ignored.
|
|
73
|
+
2. `exportable="true"` on the shortcode → export offered to everyone (grandfathering).
|
|
74
|
+
3. `export.access === "public"` → offered to everyone.
|
|
75
|
+
4. Otherwise → offered only when `GET /api/auth/me` resolves a user whose role level is at or
|
|
76
|
+
above `export.access`.
|
|
77
|
+
|
|
78
|
+
`schema.export` answers *may this collection be exported, and by whom*. `exportable="true"`
|
|
79
|
+
answers *does this placement render a visible button*. They are not redundant; both survive.
|
|
80
|
+
|
|
81
|
+
### Why the gate is client-side
|
|
82
|
+
|
|
83
|
+
`server/routes/public.js:138-149` deliberately skips `jwtVerify()` when a page's visibility is
|
|
84
|
+
effectively public, and keys the response cache `role:anon` so all anonymous traffic shares one
|
|
85
|
+
entry. Server-rendering a per-visitor export flag would force JWT verification on every public
|
|
86
|
+
page request and fragment that cache.
|
|
87
|
+
|
|
88
|
+
This costs nothing, because the gate was never a security boundary: the full dataset already
|
|
89
|
+
ships inside the base64 `data-ctx` attribute. The gate stops a casual visitor being handed a
|
|
90
|
+
download button; it does not stop a determined one reading DevTools. **This must be stated in
|
|
91
|
+
`docs/configuration.md`** so no operator mistakes it for enforcement. A hard gate would require a
|
|
92
|
+
server export endpoint re-deriving rows from posted filter state - deferred.
|
|
93
|
+
|
|
94
|
+
## Delivery to the client
|
|
95
|
+
|
|
96
|
+
Two routes, because tables already differ:
|
|
97
|
+
|
|
98
|
+
- **`data-ctx` displays** - `buildCtxAttr(fields, entries, opts)` gains a third parameter
|
|
99
|
+
carrying `{slug, export: {allowed, access, formats}}`. Threaded explicitly through all 8 call
|
|
100
|
+
sites (`markdown.js:289, 508, 535, 566, 604, 666, 759`). **Not** module-level state:
|
|
101
|
+
`markdown.js` renders shortcodes recursively, so a shared variable is clobbered by a
|
|
102
|
+
`[collection]` nested inside a `[card]`.
|
|
103
|
+
- **`[data-collection-table]`** - rides the existing base64 `payload` object that
|
|
104
|
+
`site.js:758` already decodes.
|
|
105
|
+
|
|
106
|
+
## Menu structure
|
|
107
|
+
|
|
108
|
+
Drill-in panels: one panel whose content swaps, with a `‹ Back` row. Chosen over hover flyouts
|
|
109
|
+
because it reuses the existing viewport clamp (`collection-context.js:393-395`) unchanged, needs
|
|
110
|
+
no hover-intent timing, survives long-press on touch, and is keyboard-navigable for free.
|
|
111
|
+
|
|
112
|
+
Root panel, split by scope - `resolveEntry()` already distinguishes the two:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
ENTRY-SCOPED (heading = clicked field label)
|
|
116
|
+
Filter = "value"
|
|
117
|
+
Filter ≠ "value"
|
|
118
|
+
Filter > / < "value" (numeric/date values only, as today)
|
|
119
|
+
Copy value
|
|
120
|
+
DISPLAY-SCOPED
|
|
121
|
+
Sort…
|
|
122
|
+
Group by… (DOM-toggle display types only - see Grouping)
|
|
123
|
+
Copy…
|
|
124
|
+
Export… (only when resolution order permits)
|
|
125
|
+
Print
|
|
126
|
+
Copy link to this view
|
|
127
|
+
Clear all filters (n) (only when filters active, as today)
|
|
128
|
+
[ Search… ]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The free-text search input stays pinned at the bottom of the root panel and keeps autofocus.
|
|
132
|
+
Filter remains one click. Sort becomes two.
|
|
133
|
+
|
|
134
|
+
## Sort
|
|
135
|
+
|
|
136
|
+
State: `wrapper._dmCtxSort = {field, label, dir}` or `null`. Applied in `apply()` directly after
|
|
137
|
+
the existing filter step:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
let allowedEntries = model.entries.filter(e => filters.every(f => matches(e, f)));
|
|
141
|
+
if (sort) allowedEntries = allowedEntries.slice().sort(comparator(sort));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`.slice()` is required - `model.entries` is the pristine source order and clearing the sort must
|
|
145
|
+
return to it.
|
|
146
|
+
|
|
147
|
+
Three of the four existing strategies inherit sort with no further work, because each takes its
|
|
148
|
+
order from the array it is handed: table (`_dmTable.render`), timeline (`instance.setItems`),
|
|
149
|
+
carousel (rebuild from pristine slides). Only the DOM-toggle path needs new code: reorder the
|
|
150
|
+
`[data-entry-id]` siblings with `appendChild` in sorted order. Moving nodes rather than cloning
|
|
151
|
+
preserves event listeners and Domma instance references.
|
|
152
|
+
|
|
153
|
+
**Comparator.** Extract the type-detection ladder currently inlined in `matches()`
|
|
154
|
+
(`collection-context.js:92-99`) into a shared `compareValues(a, b)`: try `parseFloat`, then
|
|
155
|
+
`Date.parse`, then string compare. Both `gt`/`lt` filtering and sorting then share one definition
|
|
156
|
+
of "which value is larger", which otherwise drifts.
|
|
157
|
+
|
|
158
|
+
**Direction labels follow the detected type** - `A → Z` for strings, `newest → oldest` for dates,
|
|
159
|
+
`low → high` for numbers.
|
|
160
|
+
|
|
161
|
+
## Grouping
|
|
162
|
+
|
|
163
|
+
Available **only on display types that use the DOM-toggle strategy**: cards, list, listgroup,
|
|
164
|
+
accordion, block. These render entries as siblings, so a heading element can be inserted between
|
|
165
|
+
groups.
|
|
166
|
+
|
|
167
|
+
The other three cannot take it, for three distinct reasons:
|
|
168
|
+
|
|
169
|
+
- **table** - `T.create` has no grouping API; injected heading rows would fight its pagination.
|
|
170
|
+
- **timeline** - `Progression.setItems()` accepts items, not section headers.
|
|
171
|
+
- **carousel** - slides are a sequence; grouped slides are not meaningful.
|
|
172
|
+
|
|
173
|
+
The `Group by…` row is **hidden** on those types, not disabled - a greyed row invites "why?" on
|
|
174
|
+
every open.
|
|
175
|
+
|
|
176
|
+
Implementation: group field becomes the primary sort; a `.dm-ctx-group-heading` div is inserted
|
|
177
|
+
before the first entry of each distinct value. An active sort becomes the secondary sort within
|
|
178
|
+
each group. Clearing the group leaves the sort intact.
|
|
179
|
+
|
|
180
|
+
## Export, copy, print
|
|
181
|
+
|
|
182
|
+
One serialiser, several sinks:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
serialise(entries, fields, format) // format: 'csv' | 'json' | 'markdown' → string
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Menu row | Format | Sink |
|
|
189
|
+
|----------------------------|----------|-----------------|
|
|
190
|
+
| Export → CSV | csv | Blob download |
|
|
191
|
+
| Export → JSON | json | Blob download |
|
|
192
|
+
| Copy → this value | - | clipboard (raw) |
|
|
193
|
+
| Copy → this row | json | clipboard |
|
|
194
|
+
| Copy → visible as table | markdown | clipboard |
|
|
195
|
+
|
|
196
|
+
**Row set:** the filtered-and-sorted entries - exactly what is on screen. No "all vs visible"
|
|
197
|
+
choice; the chips above the display already state what has been filtered out. Grouping is
|
|
198
|
+
presentational and does not affect the row set.
|
|
199
|
+
|
|
200
|
+
**Column set:** `model.fields`, which is already the rendered-field allowlist produced by
|
|
201
|
+
`buildCtxAttr`. Export cannot emit a column the page did not render.
|
|
202
|
+
|
|
203
|
+
**Filename:** `<slug>-<YYYY-MM-DD>.<ext>`. Requires adding `slug` to the ctx payload.
|
|
204
|
+
|
|
205
|
+
**Print:** stamp `data-dm-print` on the clicked wrapper, apply a print stylesheet that hides
|
|
206
|
+
everything outside that subtree, call `window.print()`, then remove the attribute. Accordions
|
|
207
|
+
must be force-expanded in print CSS or the output is a stack of closed headers.
|
|
208
|
+
|
|
209
|
+
**Copy link to this view:** call `syncHash()`, then copy `location.href`. Confirmation via
|
|
210
|
+
`E.toast` when Domma is present, falling back to a transient inline note.
|
|
211
|
+
|
|
212
|
+
## The shared CSV dialect
|
|
213
|
+
|
|
214
|
+
Both `serialise(..., 'csv')` (browser) and `exportEntries(slug, 'csv')` (server) must produce
|
|
215
|
+
byte-identical output for the same values. **They share one module** - verified during planning:
|
|
216
|
+
root `package.json` is `"type": "module"`, and although `public/js/package.json` pins
|
|
217
|
+
`"type": "commonjs"`, a `.mjs` file is ESM unconditionally and imports cleanly from server code.
|
|
218
|
+
`tests/services/menu-decor-client.test.js:7` is the existing precedent for the test side; a
|
|
219
|
+
`node -e` import from the server side was confirmed working.
|
|
220
|
+
|
|
221
|
+
So `public/js/collection-export.mjs` is the single source of truth, imported by
|
|
222
|
+
`server/services/collections.js` *and* served to the browser. No duplication, so no parity
|
|
223
|
+
fixture is needed - the dialect simply cannot diverge.
|
|
224
|
+
|
|
225
|
+
The **dialect** is shared. The **column set** legitimately differs: the admin export includes
|
|
226
|
+
`id` and `createdAt` and every schema field; the public export emits only the rendered allowlist.
|
|
227
|
+
|
|
228
|
+
Rules, in order:
|
|
229
|
+
|
|
230
|
+
1. **Coercion.**
|
|
231
|
+
- `null` / `undefined` → `''`
|
|
232
|
+
- `Array` → elements coerced individually, joined with `'; '`
|
|
233
|
+
- `Date` → ISO 8601 string
|
|
234
|
+
- non-null non-array `object` → `JSON.stringify(v)`
|
|
235
|
+
- otherwise → `String(v)`
|
|
236
|
+
2. **Formula-injection sanitising.** If the coerced string matches `/^[=+\-@\t\r]/` **and is not
|
|
237
|
+
a finite number**, prefix a single apostrophe. The numeric exemption is essential - without it
|
|
238
|
+
every negative number exports as `'-5`. Applied to `csv` only; `json` and `markdown` are
|
|
239
|
+
unaffected.
|
|
240
|
+
3. **Quoting.** Every cell is wrapped in double quotes, with `"` escaped as `""`. Matches the
|
|
241
|
+
existing server behaviour; quoting unconditionally avoids a needs-quoting predicate.
|
|
242
|
+
4. **Line ending:** `\n`. Deviates from RFC 4180's CRLF, but matches what `exportEntries()`
|
|
243
|
+
already emits - changing it risks breaking operators' existing importers for no user-visible
|
|
244
|
+
gain. Deliberate.
|
|
245
|
+
5. **Byte order mark:** output is prefixed with `\uFEFF` (U+FEFF) so Excel reads UTF-8 correctly.
|
|
246
|
+
|
|
247
|
+
Rule 1 fixes the `[object Object]` bug. Rules 2 and 5 are new to both surfaces.
|
|
248
|
+
|
|
249
|
+
## URL state
|
|
250
|
+
|
|
251
|
+
The `dmf=` hash payload currently maps display index → filter array. It becomes display index →
|
|
252
|
+
`{f, s, g}` (filters, sort, group). Back-compat is three lines in `readHash()`: if the per-index
|
|
253
|
+
value is an `Array`, treat it as a legacy filter list. Existing shared links keep working.
|
|
254
|
+
|
|
255
|
+
## Admin UI
|
|
256
|
+
|
|
257
|
+
The collection editor (`admin/js/templates/collection-editor.html:13-18`) has tabs
|
|
258
|
+
Settings / Fields / API Access / Storage. Export joins the **API Access** panel as a fifth row
|
|
259
|
+
below the four verbs, with a Formats sub-row:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
Export ☑ Allow export [ subscriber ▾ ]
|
|
263
|
+
Formats ☑ CSV ☑ JSON
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The existing **Read fields** sub-row (`collection-editor.js:236-247`) is the precedent for a row
|
|
267
|
+
carrying an extra control, so Formats introduces no new pattern.
|
|
268
|
+
|
|
269
|
+
Wiring mirrors the existing shape: `readApiRows()` (`:297-305`) reads `api-<verb>-enabled` /
|
|
270
|
+
`api-<verb>-access` by DOM id; export reads `export-enabled`, `export-access`, and two format
|
|
271
|
+
checkboxes. The three save paths (`:432`, `:463`, `:480`) currently send `api: b` and gain
|
|
272
|
+
`export: <obj>` alongside.
|
|
273
|
+
|
|
274
|
+
The tab is renamed **"API & Export"** - a user hunting for an export setting will not open a tab
|
|
275
|
+
labelled "API Access".
|
|
276
|
+
|
|
277
|
+
New code in this file uses **readable identifiers** despite the surrounding mangled style. A file
|
|
278
|
+
that is 5% legible is better than one that is 0%.
|
|
279
|
+
|
|
280
|
+
## File layout
|
|
281
|
+
|
|
282
|
+
| File | Change |
|
|
283
|
+
|---|---|
|
|
284
|
+
| `public/js/collection-export.mjs` | **new** - `serialise()`, CSV dialect, download + clipboard sinks. `.mjs` so both the browser and `collections.js` import it |
|
|
285
|
+
| `public/js/collection-context.js` | drill-in panels, sort state, grouping, comparator extraction, menu rows |
|
|
286
|
+
| `public/js/site.js` | pass export config from table payload to the wrapper |
|
|
287
|
+
| `public/css/site.css` | group headings, print stylesheet, panel transition |
|
|
288
|
+
| `server/services/markdown.js` | `buildCtxAttr()` third parameter, threaded through 8 call sites |
|
|
289
|
+
| `server/services/collections.js` | `exportEntries()` imports and uses the shared dialect |
|
|
290
|
+
| `admin/js/templates/collection-editor.html` | tab rename, export rows |
|
|
291
|
+
| `admin/js/views/collection-editor.js` | render + read + save the export block |
|
|
292
|
+
| `docs/configuration.md` | `export` schema block; explicit note that the gate is soft |
|
|
293
|
+
| `docs/markdown-shortcodes.md` | menu capabilities per display type |
|
|
294
|
+
|
|
295
|
+
`collection-context.js` is 421 lines today and would approach 700 with all of this. Splitting the
|
|
296
|
+
serialiser into `collection-export.mjs` keeps each file focused: one owns menu and display state,
|
|
297
|
+
the other owns turning entries into text.
|
|
298
|
+
|
|
299
|
+
**Load-order consequence:** `page.html:120` currently loads `collection-context.js` as a classic
|
|
300
|
+
`defer` script, which cannot `import`. It becomes `type="module"` - matching `site.js` (`:119`)
|
|
301
|
+
and `forms.js` (`:128`), which already do exactly this. Module scripts defer by default, so the
|
|
302
|
+
`defer` attribute is dropped. The file is already an IIFE under `'use strict'`, so no other
|
|
303
|
+
semantics change. The `?v=` cache-buster string must be bumped on every touched public asset.
|
|
304
|
+
|
|
305
|
+
## Testing
|
|
306
|
+
|
|
307
|
+
Existing suites live under `tests/` by area (`node --test`, concurrency 1).
|
|
308
|
+
|
|
309
|
+
- **`tests/services/csv-dialect.test.js`** - unit tests for the single shared dialect: ~9
|
|
310
|
+
awkward values covering formula-leading string, negative number, embedded double quote,
|
|
311
|
+
embedded comma, embedded newline, array, nested object, accented text, empty.
|
|
312
|
+
- **`tests/services/collections-export.test.js`** - extend for the `[object Object]` regression.
|
|
313
|
+
- **`tests/markdown/collection-ctx.test.js`** - `buildCtxAttr` emits export config; respects
|
|
314
|
+
`enabled: false`; grandfathers `exportable="true"`; never emits an unrendered field.
|
|
315
|
+
- **Manual, in a real browser** - sort across all 8 display types; grouping hidden on
|
|
316
|
+
table/timeline/carousel; drill-in on long-press; legacy `dmf=` link still restores; print
|
|
317
|
+
output; export gate visible signed-in and absent signed-out.
|
|
318
|
+
|
|
319
|
+
Per project convention, UI behaviour is not claimed as verified without a real browser load.
|
|
320
|
+
|
|
321
|
+
## Deliberately deferred
|
|
322
|
+
|
|
323
|
+
- **Restoring the seven mangled admin view files.** Readable source exists at `35f0bdd`, but five
|
|
324
|
+
months of changes have landed on the mangled lineage, so it is a rename-against-reference job,
|
|
325
|
+
not a revert. Its own task - it should not ride along with a feature.
|
|
326
|
+
- **A hard server-side export gate.** Would need `/api/collections/:slug/export` re-deriving rows
|
|
327
|
+
from posted filter state. Only worth it if an operator needs genuine enforcement.
|
|
328
|
+
- **Column visibility toggles** for table displays.
|
|
329
|
+
- **Multi-field sort.** Single field plus an optional group is enough.
|
|
330
|
+
- **Export of grouped structure.** Groups are presentational; exports stay flat.
|