@bygghammar/cms-platform 0.80.0 → 0.90.0-m7.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 +251 -0
- package/docs/POPUPS.md +102 -0
- package/package.json +23 -3
- package/src/backoffice/SiteSettingsView.tsx +14 -15
- package/src/backoffice/routes/host.ts +30 -7
- package/src/backoffice/shell/nav-model.ts +32 -23
- package/src/config/modules.ts +184 -0
- package/src/config/site-config.ts +9 -31
- package/src/forms/FormRenderer.tsx +40 -3
- package/src/forms/submission-result.ts +35 -0
- package/src/forms/success-listeners.ts +46 -0
- package/src/host/index.tsx +10 -1
- package/src/modules/index.ts +28 -0
- package/src/modules/public-runtime.tsx +159 -0
- package/src/popups/admin.tsx +72 -0
- package/src/popups/cms-bridges.ts +200 -0
- package/src/popups/destinations.tsx +141 -0
- package/src/popups/host.ts +82 -0
- package/src/popups/media-bridge.tsx +84 -0
- package/src/popups/register.ts +23 -0
- package/src/popups/runtime.tsx +94 -0
- package/src/server/popup-optin-trigger.ts +111 -0
- package/src/server/popup-optin-webhook-core.ts +363 -0
- package/src/server/webhook-url-guard.ts +199 -0
- package/src/types/css-modules.d.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,214 @@ matches `package.json` exactly. See
|
|
|
8
8
|
Versions 0.1.0 through 0.78.0 predate this repository and were reconstructed
|
|
9
9
|
from the published npm tarballs.
|
|
10
10
|
|
|
11
|
+
## 0.90.0-m7.1
|
|
12
|
+
|
|
13
|
+
First version published to npm rather than vendored as a tarball. Same code as
|
|
14
|
+
`0.80.2-popup.15`, renumbered for a real prerelease: the minor moves because the
|
|
15
|
+
popup integration gained form-field discovery, and the `m7` prerelease says this
|
|
16
|
+
is still waiting on staging acceptance. Published under the `m7` dist-tag, so
|
|
17
|
+
`latest` keeps pointing at 0.80.1.
|
|
18
|
+
|
|
19
|
+
- The optional `@bygghammar/lightweight-popup` peer range now names a
|
|
20
|
+
prerelease. `>=0.0.0` does not accept `0.2.0-m7.1` — a prerelease only
|
|
21
|
+
satisfies a range that mentions one at the same version — so the peer would
|
|
22
|
+
have read as unmet on every install.
|
|
23
|
+
|
|
24
|
+
## 0.80.2-popup.15
|
|
25
|
+
|
|
26
|
+
- `cmsFormDirectory` can describe a form's fields, so the popup builder offers
|
|
27
|
+
real fields to tick instead of asking an administrator to type column names.
|
|
28
|
+
Built from the listing the forms screen already reads — no second host
|
|
29
|
+
contract and no second query.
|
|
30
|
+
- Disabled fields, removed ones and the spam trap never reach the selector; a
|
|
31
|
+
field's own label is shown while its stable key is what gets stored.
|
|
32
|
+
- Vendors popup 0.1.0-dev.9.
|
|
33
|
+
|
|
34
|
+
## 0.80.2-popup.14
|
|
35
|
+
|
|
36
|
+
- A test delivery that failed before any request was made is no longer reported
|
|
37
|
+
as "The endpoint refused it". The bridge defaulted an absent category to
|
|
38
|
+
`http-error`; a missing signing secret arrives exactly that way, so the screen
|
|
39
|
+
sent administrators to debug a receiver that had never been contacted. An
|
|
40
|
+
unnamed failure is now `config-error`.
|
|
41
|
+
|
|
42
|
+
## 0.80.2-popup.13
|
|
43
|
+
|
|
44
|
+
- The destinations screen imports the popup admin stylesheet. It is a sibling
|
|
45
|
+
route rather than a child of the builder, so it loads on its own and nothing
|
|
46
|
+
else pulled the styling in — the first time anyone could reach it, twelve
|
|
47
|
+
`lwp-` elements rendered as plain text.
|
|
48
|
+
- A test now requires every module that mounts one of the package's admin
|
|
49
|
+
screens to import that stylesheet beside it.
|
|
50
|
+
|
|
51
|
+
## 0.80.2-popup.12
|
|
52
|
+
|
|
53
|
+
- Documents the popup bridges in `docs/POPUPS.md`: which port each CMS system
|
|
54
|
+
fills, why an unfilled port is not an integration, what crosses the media
|
|
55
|
+
boundary, and which webhook responsibility sits in which layer.
|
|
56
|
+
|
|
57
|
+
## 0.80.2-popup.11
|
|
58
|
+
|
|
59
|
+
- A destinations screen for popup opt-in webhooks: `popups/destinations` binds
|
|
60
|
+
the package's provider-neutral `DestinationsAdmin` to the host's own server
|
|
61
|
+
functions. A site that registers none gets an honest empty screen.
|
|
62
|
+
- The backoffice host contract gains the remaining optional webhook slots
|
|
63
|
+
(save, delete, send test, list deliveries).
|
|
64
|
+
- Vendors popup 0.1.0-dev.5.
|
|
65
|
+
|
|
66
|
+
## 0.80.2-popup.10
|
|
67
|
+
|
|
68
|
+
- The popup builder now gets the site's media library. `cmsMediaPicker()`
|
|
69
|
+
renders the same `MediaPickerDialog` every other backoffice screen uses, and
|
|
70
|
+
is passed into the popup admin adapters — the line whose absence left
|
|
71
|
+
administrators typing storage URLs by hand.
|
|
72
|
+
- Only a stable public address, a filename and any stored alt text cross into
|
|
73
|
+
the popup package. Uploading, listing and deleting stay with the library.
|
|
74
|
+
- Vendors popup 0.1.0-dev.4.
|
|
75
|
+
|
|
76
|
+
## 0.80.2-popup.9
|
|
77
|
+
|
|
78
|
+
- The backoffice host contract gains an optional , so
|
|
79
|
+
a site can offer opt-in destinations to the popup editor. A site without one
|
|
80
|
+
registers nothing and the picker does not appear.
|
|
81
|
+
|
|
82
|
+
## 0.80.2-popup.8
|
|
83
|
+
|
|
84
|
+
- Vendors popup 0.1.0-dev.3, which adds the opt-in destination picker this
|
|
85
|
+
version bridges to.
|
|
86
|
+
|
|
87
|
+
## 0.80.2-popup.7
|
|
88
|
+
|
|
89
|
+
- Server-only popup opt-in webhooks. `server/popup-optin-webhook-core` signs a
|
|
90
|
+
versioned payload with HMAC-SHA256 over a timestamped string, re-validates
|
|
91
|
+
the endpoint on every redirect hop, bounds the response it will read, and
|
|
92
|
+
categorises failures without ever echoing the receiver's own words.
|
|
93
|
+
- `server/webhook-url-guard` refuses anything that is not public https:
|
|
94
|
+
loopback, private, link-local, carrier-grade NAT, unique-local, multicast and
|
|
95
|
+
cloud-metadata addresses, including the IPv4 forms hidden inside IPv6, plus
|
|
96
|
+
credentials embedded in the URL.
|
|
97
|
+
- `server/popup-optin-trigger` is the single predicate deciding what counts as
|
|
98
|
+
a genuine opt-in. The honeypot's `{ok: true, skipped: true}` is not one, and
|
|
99
|
+
neither is a Yes click, a dismissal, a preview or a test-mode popup.
|
|
100
|
+
- CMS bridges for the popup editor's opt-in destination picker and media
|
|
101
|
+
picker. Only an identifier, a name and a warning cross the boundary — never
|
|
102
|
+
an endpoint or a secret.
|
|
103
|
+
- Vendors popup 0.1.0-dev.2 (image layouts, four presentation formats).
|
|
104
|
+
|
|
105
|
+
## 0.80.2-popup.6
|
|
106
|
+
|
|
107
|
+
- Vendors popup development build 0.0.0-dev.11: two fixes found by using the
|
|
108
|
+
redesigned backoffice in a browser — a false "unsaved changes" state
|
|
109
|
+
immediately after saving, and bottom-anchored presentations rendering at the
|
|
110
|
+
top of the builder's preview frame.
|
|
111
|
+
|
|
112
|
+
## 0.80.2-popup.5
|
|
113
|
+
|
|
114
|
+
- Vendors popup development build 0.0.0-dev.10: the design and usability
|
|
115
|
+
sprint over the popup backoffice and the public popup. No CMS-side code
|
|
116
|
+
changes were needed — the module host, the form and page bridges and the
|
|
117
|
+
lazy admin/runtime split all carry the new screen unchanged, which is the
|
|
118
|
+
layering working as intended.
|
|
119
|
+
|
|
120
|
+
## 0.80.2-popup.4
|
|
121
|
+
|
|
122
|
+
- The popup backoffice screen imports `@bygghammar/lightweight-popup/admin.css`
|
|
123
|
+
itself, from inside the lazily loaded admin module. The screen was rendering
|
|
124
|
+
unstyled because nothing imported the stylesheet; importing it here lets the
|
|
125
|
+
consumer's bundler attach it to the admin chunk, keeping every byte of it out
|
|
126
|
+
of public bundles.
|
|
127
|
+
- Vendored popup development build 0.0.0-dev.9: live step switching in the
|
|
128
|
+
builder, draft-until-valid-save creation, strict schema validation in the
|
|
129
|
+
reference storage adapter.
|
|
130
|
+
|
|
131
|
+
## 0.80.2-popup.3
|
|
132
|
+
|
|
133
|
+
Not yet published. Local development artifact for the Popups module only.
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
|
|
137
|
+
- Rebuilt against the popup package's `0.0.0-dev.8`, which default-exports the
|
|
138
|
+
administration app so the module host's lazy loader resolves to it.
|
|
139
|
+
|
|
140
|
+
## 0.80.2-popup.2
|
|
141
|
+
|
|
142
|
+
Not yet published. Local development artifact for the Popups module only.
|
|
143
|
+
|
|
144
|
+
### Added
|
|
145
|
+
|
|
146
|
+
- The Popups backoffice now mounts the popup package's full administration
|
|
147
|
+
screen — list, builder, rules, appearance, statistics, import and export —
|
|
148
|
+
instead of a read-only summary.
|
|
149
|
+
- `./popups/cms-bridges`: form and page discovery adapted from the platform's
|
|
150
|
+
own host contract, so every site gets working pickers without writing them.
|
|
151
|
+
Only a key and a display name cross into the builder; the full form row
|
|
152
|
+
carries private settings a popup builder has no business seeing. A host with
|
|
153
|
+
no page listing gets no picker rather than a screen that fails to render.
|
|
154
|
+
|
|
155
|
+
## 0.80.2-popup.1
|
|
156
|
+
|
|
157
|
+
Not yet published. Local development artifact for the Popups module only.
|
|
158
|
+
|
|
159
|
+
### Added
|
|
160
|
+
|
|
161
|
+
- **CMS module table** (`config/modules`, exported as `./modules`). Modules were
|
|
162
|
+
described in four places that had to agree by hand — the flag type, the
|
|
163
|
+
defaults, the Modules screen labels and the sidebar definitions. All four now
|
|
164
|
+
derive from one table, so a new first-party module is one entry plus one key
|
|
165
|
+
in `SiteFeatureFlags`. The key space stays a closed typed union and the
|
|
166
|
+
settings parser still drops anything it does not recognise, on read and on
|
|
167
|
+
write. Every pre-existing module keeps its default, label, path and sidebar
|
|
168
|
+
position.
|
|
169
|
+
- **Lazy public module runtimes** (`CmsModuleRuntimes`, `registerModuleRuntime`)
|
|
170
|
+
and **lazy backoffice screens** (`CmsModuleAdmin`, `registerModuleAdmin`). A
|
|
171
|
+
module registers loader functions rather than components, so defining one
|
|
172
|
+
costs nothing: a site with the module off never evaluates its code and the
|
|
173
|
+
browser never requests its chunk. Loading happens in an effect, which is
|
|
174
|
+
itself the hydration gate — the server renders nothing, the first client pass
|
|
175
|
+
renders nothing. Public and admin loaders live in separate registries.
|
|
176
|
+
- **Generic successful-submission seam for forms** (`./forms/success`,
|
|
177
|
+
`./forms/submission-result`). `FormRenderer` reads the submit result instead
|
|
178
|
+
of assuming success, and announces only submissions the host actually stored:
|
|
179
|
+
a honeypot answers `{ ok: true, skipped: true }` after storing nothing, and
|
|
180
|
+
counting that is how a bot inflates every conversion figure a site reports.
|
|
181
|
+
The event is emitted *before* a thank-you redirect assigns
|
|
182
|
+
`window.location.href`, because that tears the page down. Subscribers are
|
|
183
|
+
isolated, and the form system names no module. `ManagedForm` and
|
|
184
|
+
`FormRenderer` also take an optional `onSuccess`.
|
|
185
|
+
- **Popups module** (`popups: false` by default), split across `./popups/host`,
|
|
186
|
+
`./popups/register`, `./popups/runtime` and `./popups/admin`. The public
|
|
187
|
+
bridge imports only the popup package's runtime entry, and the admin screen
|
|
188
|
+
stays out of the eagerly-imported backoffice barrel.
|
|
189
|
+
`@bygghammar/lightweight-popup` is an optional peer dependency.
|
|
190
|
+
|
|
191
|
+
### Changed
|
|
192
|
+
|
|
193
|
+
- `DEFAULT_FEATURE_FLAGS`, the Modules screen and the backoffice sidebar now
|
|
194
|
+
derive from the module table.
|
|
195
|
+
- The lazy module host uses its component-loading effect as the hydration gate;
|
|
196
|
+
an earlier separate `hydrated` flag could be deleted without a single test
|
|
197
|
+
noticing, which meant it was guarding nothing.
|
|
198
|
+
|
|
199
|
+
## 0.80.1
|
|
200
|
+
|
|
201
|
+
- Release pipeline verification: first version published from the canonical
|
|
202
|
+
`byggis79/cms-platform` repository via the tag-triggered GitHub Actions
|
|
203
|
+
release workflow. No source or behaviour changes.
|
|
204
|
+
|
|
205
|
+
### Changed
|
|
206
|
+
|
|
207
|
+
- Module public runtimes and backoffice screens are now **lazily loaded**.
|
|
208
|
+
A module registers loader functions (`() => import(...)`) instead of
|
|
209
|
+
components, through `@bygghammar/cms-platform/popups/register` for popups.
|
|
210
|
+
Defining a module therefore costs nothing: a site with the module switched off
|
|
211
|
+
never evaluates its code and the browser never requests its chunk. Loading
|
|
212
|
+
happens in an effect after hydration, so nothing runs during SSR and the first
|
|
213
|
+
client render matches the server's markup.
|
|
214
|
+
- The public and admin loaders live in separate registries, so a public page
|
|
215
|
+
never pulls the backoffice screen and vice versa. `CmsModuleAdmin` mounts a
|
|
216
|
+
module's screen, loading it on first visit.
|
|
217
|
+
|
|
218
|
+
|
|
11
219
|
## 0.80.0
|
|
12
220
|
|
|
13
221
|
- Search engines status card: the help text now explicitly tells admins which
|
|
@@ -26,6 +234,49 @@ from the published npm tarballs.
|
|
|
26
234
|
screens instead of its own inline card, so the status and help text stay
|
|
27
235
|
identical in both places.
|
|
28
236
|
|
|
237
|
+
### Fixed
|
|
238
|
+
|
|
239
|
+
- The Popups module now registers through an exported `registerPopupModule()`
|
|
240
|
+
instead of a module-scope side effect. A host whose `package.json`
|
|
241
|
+
`sideEffects` field is an allowlist tree-shakes a bare
|
|
242
|
+
`import "@bygghammar/cms-platform/popups/runtime"` away, taking the
|
|
243
|
+
registration and the entire popup runtime with it — a silent failure where the
|
|
244
|
+
build succeeds and no popup ever appears.
|
|
245
|
+
|
|
246
|
+
### Added
|
|
247
|
+
|
|
248
|
+
- **CMS module table** (`config/modules`, exported as `./modules`). Modules were
|
|
249
|
+
previously described in four places that had to agree by hand — the flag type,
|
|
250
|
+
the defaults, the Modules screen labels and the sidebar definitions. All four
|
|
251
|
+
now derive from one table, so a new first-party module is one entry plus one
|
|
252
|
+
key in `SiteFeatureFlags`. The key space stays a closed typed union, and the
|
|
253
|
+
settings parser still drops anything it does not recognise.
|
|
254
|
+
- **Public module runtimes** (`CmsModuleRuntimes`, `registerModuleRuntime`). A
|
|
255
|
+
site renders the host once in its root layout; enabled modules that registered
|
|
256
|
+
a component are mounted. Registration happens by import, so a module a site
|
|
257
|
+
never imports is never bundled, and a module whose flag is off never runs.
|
|
258
|
+
- **Generic successful-submission seam for forms** (`./forms/success`,
|
|
259
|
+
`./forms/submission-result`). `FormRenderer` now reads the submit result
|
|
260
|
+
instead of assuming success, and announces only submissions the host actually
|
|
261
|
+
stored: a honeypot answer of `{ ok: true, skipped: true }` stored nothing and
|
|
262
|
+
must never be counted. The event is emitted *before* a thank-you redirect
|
|
263
|
+
assigns `window.location.href`, because that tears the page down. Subscribers
|
|
264
|
+
are isolated — one that throws cannot break a form — and the form system names
|
|
265
|
+
no module, so it keeps working with none subscribed. `ManagedForm` and
|
|
266
|
+
`FormRenderer` also take an optional `onSuccess`.
|
|
267
|
+
- **Popups module** (`popups: false` by default), with its CMS integration split
|
|
268
|
+
across `./popups/host`, `./popups/runtime` (public) and `./popups/admin`. The
|
|
269
|
+
public bridge imports only the popup package's runtime entry, so a site with
|
|
270
|
+
popups on never pulls the builder, the statistics screen or the validator into
|
|
271
|
+
its public bundle. `@bygghammar/lightweight-popup` is an optional peer
|
|
272
|
+
dependency: sites without the module need not install it.
|
|
273
|
+
|
|
274
|
+
### Changed
|
|
275
|
+
|
|
276
|
+
- `DEFAULT_FEATURE_FLAGS`, the Modules screen and the backoffice sidebar are now
|
|
277
|
+
derived from the module table. Every existing module keeps its previous
|
|
278
|
+
default, label, path and sidebar position.
|
|
279
|
+
|
|
29
280
|
## 0.78.0
|
|
30
281
|
|
|
31
282
|
- Countdown block: the unit tiles now always stay on a single row on mobile.
|
package/docs/POPUPS.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Popups: what this package supplies
|
|
2
|
+
|
|
3
|
+
`@bygghammar/lightweight-popup` is deliberately host-agnostic. It knows nothing
|
|
4
|
+
about Supabase, about this CMS, or about any particular site. Everything it
|
|
5
|
+
needs from the outside arrives through ports.
|
|
6
|
+
|
|
7
|
+
This package is the adapter that fills those ports with the CMS's own systems,
|
|
8
|
+
so a site gets a working popup builder by registering one module instead of
|
|
9
|
+
reimplementing a media library, a form renderer and a page picker.
|
|
10
|
+
|
|
11
|
+
## The bridges
|
|
12
|
+
|
|
13
|
+
| Port | Filled with | File |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `MediaPickerPort` | The CMS media library dialog | `src/popups/media-bridge.tsx` |
|
|
16
|
+
| `FormRenderPort` | The CMS form renderer | `src/popups/cms-bridges.ts` |
|
|
17
|
+
| `PagePickerPort` | The CMS page list | `src/popups/cms-bridges.ts` |
|
|
18
|
+
| `OptInDestinationAdminPort` | The host's webhook server functions | `src/popups/destinations.tsx` |
|
|
19
|
+
|
|
20
|
+
All of them are assembled in `src/popups/admin.tsx`. **An unfilled port is not
|
|
21
|
+
an integration.** `cmsMediaPicker` existed for weeks and was never placed on the
|
|
22
|
+
adapters object, so `adapters.media` was permanently `undefined`, the builder
|
|
23
|
+
took its no-picker fallback branch, and every administrator pasted storage URLs
|
|
24
|
+
by hand. The failure was invisible because the fallback is a legitimate state
|
|
25
|
+
for a host without a media library.
|
|
26
|
+
|
|
27
|
+
`tests/popup-media-bridge.test.ts` now asserts against the source of
|
|
28
|
+
`admin.tsx` that the key is present. Deleting the line fails the suite.
|
|
29
|
+
|
|
30
|
+
## The media bridge
|
|
31
|
+
|
|
32
|
+
`MediaPickerPort` is a render seam:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
renderPicker(props: { onPick(item): void; onClose(): void }): ReactNode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The bridge returns the CMS's own `MediaPickerDialog` with `accept="image"`. Not
|
|
39
|
+
a wrapper around it and not a reimplementation — the same component every other
|
|
40
|
+
inspector uses, so browsing, searching, folders and **upload** all come along
|
|
41
|
+
for free. The popup package never uploads anything and never learns which
|
|
42
|
+
storage provider the site uses.
|
|
43
|
+
|
|
44
|
+
What crosses the boundary is `{ src, name?, alt? }`: the stable public URL the
|
|
45
|
+
library already serves to the site, the filename to show, and the library's own
|
|
46
|
+
description when it has one. `toHostMediaItem` returns `null` for a row with no
|
|
47
|
+
address rather than passing an empty string on.
|
|
48
|
+
|
|
49
|
+
The URL must be the **public** one. A signed or expiring link renders in the
|
|
50
|
+
builder and 404s a month later on the live site, which is why the popup package
|
|
51
|
+
refuses those shapes itself in `checkMediaReference`.
|
|
52
|
+
|
|
53
|
+
## The webhook layers
|
|
54
|
+
|
|
55
|
+
Opt-in delivery is split across all three layers, and the split is the security
|
|
56
|
+
model rather than a tidiness preference.
|
|
57
|
+
|
|
58
|
+
| Layer | Has |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| lightweight-popup | The destinations screen and the delivery log. No fetch, no crypto, no database |
|
|
61
|
+
| **this package** | `webhook-url-guard.ts`, `popup-optin-webhook-core.ts`, `popup-optin-trigger.ts` — validation, signing, the retry schedule, the genuine-opt-in predicate, field selection |
|
|
62
|
+
| The site | Tables, server functions, `process.env` secret resolution, routing |
|
|
63
|
+
|
|
64
|
+
Everything here is pure and host-agnostic: it is handed a row and a secret and
|
|
65
|
+
returns an outcome. It never reads an environment variable and never opens a
|
|
66
|
+
database connection, so the same code is testable without either.
|
|
67
|
+
|
|
68
|
+
`src/server/*` is server-only by convention **and** by reachability: nothing in
|
|
69
|
+
`src/backoffice` or `src/popups` imports it, which is what keeps delivery code
|
|
70
|
+
out of every client bundle. The host's own chunk-boundary test proves that on
|
|
71
|
+
the built assets.
|
|
72
|
+
|
|
73
|
+
### The one rule that matters
|
|
74
|
+
|
|
75
|
+
**Delivery is triggered from the stored submission, on the server.** The
|
|
76
|
+
browser contributes a popup key and nothing else. There is no parameter that
|
|
77
|
+
carries a payload, an endpoint or a recipient, so a crafted request cannot make
|
|
78
|
+
the server send anything anywhere.
|
|
79
|
+
|
|
80
|
+
`shouldDeliverOptIn` is the only predicate. A honeypot's
|
|
81
|
+
`{ ok: true, skipped: true }` — the deliberate lie the form system tells a bot —
|
|
82
|
+
is **not** an opt-in. Neither is a "yes" click, a preview, or a test-mode popup.
|
|
83
|
+
|
|
84
|
+
### Secrets
|
|
85
|
+
|
|
86
|
+
A destination stores the **name** of an environment variable, never a value.
|
|
87
|
+
The host resolves it at delivery time. The admin port's `DestinationRecord`
|
|
88
|
+
carries `secretConfigured: boolean` and an `endpointDisplay` with the query
|
|
89
|
+
string removed; there is no field on it that could hold a credential, and the
|
|
90
|
+
form that creates one has nowhere to type a secret.
|
|
91
|
+
|
|
92
|
+
## Registering it on a site
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { registerPopupModule } from "@bygghammar/cms-platform/popups";
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The host supplies its `fns` object. The webhook slots on it —
|
|
99
|
+
`listWebhookDestinations`, `saveWebhookDestination`, `deleteWebhookDestination`,
|
|
100
|
+
`sendTestWebhook`, `listWebhookDeliveries` — are **optional**. A site with no
|
|
101
|
+
webhook tables simply omits them and the destinations screen is not offered;
|
|
102
|
+
nothing breaks and no dead menu item appears.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bygghammar/cms-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0-m7.1",
|
|
4
4
|
"description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"./trips/TripSectionEditor": "./src/trips/TripSectionEditor.tsx",
|
|
50
50
|
"./navigation": "./src/navigation/index.ts",
|
|
51
51
|
"./forms/FormRenderer": "./src/forms/FormRenderer.tsx",
|
|
52
|
+
"./forms/success": "./src/forms/success-listeners.ts",
|
|
53
|
+
"./forms/submission-result": "./src/forms/submission-result.ts",
|
|
52
54
|
"./forms/conditions": "./src/forms/conditions.ts",
|
|
53
55
|
"./forms/field-sets": "./src/forms/field-sets.ts",
|
|
54
56
|
"./forms/form-audit": "./src/forms/form-audit.ts",
|
|
@@ -179,6 +181,12 @@
|
|
|
179
181
|
"./site/font-bundle": "./src/site/font-bundle.ts",
|
|
180
182
|
"./admin/version-core": "./src/admin/version-core.ts",
|
|
181
183
|
"./config/package-owned-modules": "./src/config/package-owned-modules.ts",
|
|
184
|
+
"./modules": "./src/modules/index.ts",
|
|
185
|
+
"./popups/host": "./src/popups/host.ts",
|
|
186
|
+
"./popups/cms-bridges": "./src/popups/cms-bridges.ts",
|
|
187
|
+
"./popups/register": "./src/popups/register.ts",
|
|
188
|
+
"./popups/runtime": "./src/popups/runtime.tsx",
|
|
189
|
+
"./popups/admin": "./src/popups/admin.tsx",
|
|
182
190
|
"./server/schema-drift": "./src/server/schema-drift.ts",
|
|
183
191
|
"./server/media-focus-core": "./src/server/media-focus-core.ts",
|
|
184
192
|
"./server/media-meta-core": "./src/server/media-meta-core.ts",
|
|
@@ -222,7 +230,11 @@
|
|
|
222
230
|
"./site/routes/section-preview": "./src/site/routes/section-preview.tsx",
|
|
223
231
|
"./media/priority": "./src/media/priority.tsx",
|
|
224
232
|
"./media/preload": "./src/media/preload.ts",
|
|
225
|
-
"./ui/NearViewport": "./src/ui/NearViewport.tsx"
|
|
233
|
+
"./ui/NearViewport": "./src/ui/NearViewport.tsx",
|
|
234
|
+
"./server/webhook-url-guard": "./src/server/webhook-url-guard.ts",
|
|
235
|
+
"./server/popup-optin-webhook-core": "./src/server/popup-optin-webhook-core.ts",
|
|
236
|
+
"./server/popup-optin-trigger": "./src/server/popup-optin-trigger.ts",
|
|
237
|
+
"./popups/destinations": "./src/popups/destinations.tsx"
|
|
226
238
|
},
|
|
227
239
|
"dependencies": {
|
|
228
240
|
"fflate": "^0.8.3"
|
|
@@ -250,7 +262,8 @@
|
|
|
250
262
|
"@dnd-kit/core": ">=6",
|
|
251
263
|
"@dnd-kit/sortable": ">=3",
|
|
252
264
|
"@dnd-kit/utilities": ">=3",
|
|
253
|
-
"zod": ">=3"
|
|
265
|
+
"zod": ">=3",
|
|
266
|
+
"@bygghammar/lightweight-popup": ">=0.2.0-m7.1"
|
|
254
267
|
},
|
|
255
268
|
"peerDependenciesMeta": {
|
|
256
269
|
"@supabase/supabase-js": {
|
|
@@ -294,6 +307,9 @@
|
|
|
294
307
|
},
|
|
295
308
|
"@tiptap/extension-table": {
|
|
296
309
|
"optional": true
|
|
310
|
+
},
|
|
311
|
+
"@bygghammar/lightweight-popup": {
|
|
312
|
+
"optional": true
|
|
297
313
|
}
|
|
298
314
|
},
|
|
299
315
|
"bin": {
|
|
@@ -312,6 +328,7 @@
|
|
|
312
328
|
"check": "bun run typecheck && bun run test && bun run verify:pack"
|
|
313
329
|
},
|
|
314
330
|
"devDependencies": {
|
|
331
|
+
"@bygghammar/lightweight-popup": "file:./vendor/bygghammar-lightweight-popup-0.1.0-dev.9.tgz",
|
|
315
332
|
"@dnd-kit/core": "^6.3.1",
|
|
316
333
|
"@dnd-kit/sortable": "^10.0.0",
|
|
317
334
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -319,6 +336,8 @@
|
|
|
319
336
|
"@tanstack/react-query": "^5.101.1",
|
|
320
337
|
"@tanstack/react-router": "1.170.18",
|
|
321
338
|
"@tanstack/react-start": "1.168.32",
|
|
339
|
+
"@testing-library/dom": "^10.4.0",
|
|
340
|
+
"@testing-library/react": "^16.3.0",
|
|
322
341
|
"@tiptap/extension-highlight": "^3.29.2",
|
|
323
342
|
"@tiptap/extension-image": "^3.29.2",
|
|
324
343
|
"@tiptap/extension-subscript": "^3.29.2",
|
|
@@ -333,6 +352,7 @@
|
|
|
333
352
|
"@types/node": "^22.10.2",
|
|
334
353
|
"@types/react": "19.2.17",
|
|
335
354
|
"@types/react-dom": "19.2.3",
|
|
355
|
+
"jsdom": "^26.1.0",
|
|
336
356
|
"lucide-react": "^0.575.0",
|
|
337
357
|
"react": "^19.2.0",
|
|
338
358
|
"react-dom": "^19.2.0",
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type SiteFeatureFlags,
|
|
12
12
|
type SiteSettingOverrides,
|
|
13
13
|
} from "../config/site-config";
|
|
14
|
+
import { CMS_MODULE_LIST, CMS_MODULE_LABELS } from "../config/modules";
|
|
14
15
|
import type { IndexingPolicy } from "../site/indexing";
|
|
15
16
|
import { IndexingStatusCard } from "./IndexingStatusCard";
|
|
16
17
|
|
|
@@ -48,16 +49,9 @@ export type SiteSettingsDeps = {
|
|
|
48
49
|
MediaField?: (props: any) => ReactNode;
|
|
49
50
|
};
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
reviews: "Reviews",
|
|
55
|
-
email: "Transactional email",
|
|
56
|
-
tracking: "Code & tracking",
|
|
57
|
-
backup: "Backup & restore",
|
|
58
|
-
domains: "Custom domains",
|
|
59
|
-
forms: "Forms",
|
|
60
|
-
};
|
|
52
|
+
// Labels and order come from the module table, so a new module appears here
|
|
53
|
+
// without a second list to keep in step.
|
|
54
|
+
const FEATURE_LABELS = CMS_MODULE_LABELS;
|
|
61
55
|
|
|
62
56
|
const COMPANY_LABELS: Record<keyof typeof EMPTY_COMPANY, string> = {
|
|
63
57
|
legalName: "Legal name",
|
|
@@ -275,14 +269,19 @@ export function SiteSettingsView({ deps }: { deps: SiteSettingsDeps }) {
|
|
|
275
269
|
busy={busy === "features"}
|
|
276
270
|
/>
|
|
277
271
|
<div className="mt-4 grid gap-3 md:grid-cols-2">
|
|
278
|
-
{
|
|
279
|
-
<label key={
|
|
272
|
+
{CMS_MODULE_LIST.map((module) => (
|
|
273
|
+
<label key={module.key} className="flex items-center gap-3 text-sm text-ink">
|
|
280
274
|
<input
|
|
281
275
|
type="checkbox"
|
|
282
|
-
checked={features[
|
|
283
|
-
onChange={(e) => setFeatures({ ...features, [
|
|
276
|
+
checked={features[module.key]}
|
|
277
|
+
onChange={(e) => setFeatures({ ...features, [module.key]: e.target.checked })}
|
|
284
278
|
/>
|
|
285
|
-
|
|
279
|
+
<span>
|
|
280
|
+
{FEATURE_LABELS[module.key]}
|
|
281
|
+
{module.description ? (
|
|
282
|
+
<span className="block text-xs text-ink/60">{module.description}</span>
|
|
283
|
+
) : null}
|
|
284
|
+
</span>
|
|
286
285
|
</label>
|
|
287
286
|
))}
|
|
288
287
|
</div>
|
|
@@ -78,6 +78,18 @@ export type BackofficeHost = {
|
|
|
78
78
|
// locale domains
|
|
79
79
|
saveLocaleDomain: HostServerFn;
|
|
80
80
|
deleteLocaleDomain: HostServerFn;
|
|
81
|
+
/*
|
|
82
|
+
* Popup opt-in destinations, for the popup editor's picker.
|
|
83
|
+
*
|
|
84
|
+
* Optional: a site with no delivery integration simply does not register
|
|
85
|
+
* it, and the popup editor then shows no picker at all rather than an
|
|
86
|
+
* empty control. Returns names and ids — never an endpoint or a secret.
|
|
87
|
+
*/
|
|
88
|
+
listWebhookDestinations?: HostServerFn;
|
|
89
|
+
saveWebhookDestination?: HostServerFn;
|
|
90
|
+
deleteWebhookDestination?: HostServerFn;
|
|
91
|
+
sendTestWebhook?: HostServerFn;
|
|
92
|
+
listWebhookDeliveries?: HostServerFn;
|
|
81
93
|
};
|
|
82
94
|
/** Site-specific example values shown as input placeholders. */
|
|
83
95
|
placeholders?: {
|
|
@@ -105,7 +117,10 @@ export type BackofficeHost = {
|
|
|
105
117
|
/** Editor components a site wires with its own locales, media and forms. */
|
|
106
118
|
components: {
|
|
107
119
|
/** Recursive shared-content editor. */
|
|
108
|
-
SectionForm: (props: {
|
|
120
|
+
SectionForm: (props: {
|
|
121
|
+
value: unknown;
|
|
122
|
+
onChange: (next: unknown) => void;
|
|
123
|
+
}) => ReactNode;
|
|
109
124
|
/** Single media field used by the reviews editor. */
|
|
110
125
|
MediaField: MediaFieldComponent;
|
|
111
126
|
/** Review portrait uploader. */
|
|
@@ -200,7 +215,10 @@ export type BackofficeHost = {
|
|
|
200
215
|
* React hook returning a resolver for a page's public URL. Implemented as a
|
|
201
216
|
* hook so a site can read its slug overrides and locale domains.
|
|
202
217
|
*/
|
|
203
|
-
usePageUrl: () => (
|
|
218
|
+
usePageUrl: () => (
|
|
219
|
+
pageKey: string,
|
|
220
|
+
locale: string,
|
|
221
|
+
) => { path: string; host: string };
|
|
204
222
|
/** Persists unsaved shared-content drafts; returns the keys it saved. */
|
|
205
223
|
flushSharedDrafts?: () => Promise<string[]>;
|
|
206
224
|
/** Renders the block canvas for the builder. */
|
|
@@ -211,7 +229,12 @@ export type BackofficeHost = {
|
|
|
211
229
|
blocks: any[];
|
|
212
230
|
setBlocks: (next: any[]) => void;
|
|
213
231
|
globals: Record<string, any[]>;
|
|
214
|
-
globalSections: {
|
|
232
|
+
globalSections: {
|
|
233
|
+
key: string;
|
|
234
|
+
name: string;
|
|
235
|
+
kind: "block" | "section";
|
|
236
|
+
blocks: any[];
|
|
237
|
+
}[];
|
|
215
238
|
locale: string;
|
|
216
239
|
setLocale: (next: string) => void;
|
|
217
240
|
setDirty: (next: boolean) => void;
|
|
@@ -248,7 +271,6 @@ export type BackofficeHost = {
|
|
|
248
271
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
249
272
|
};
|
|
250
273
|
|
|
251
|
-
|
|
252
274
|
let current: BackofficeHost | null = null;
|
|
253
275
|
|
|
254
276
|
/** Registers the host adapters. Call once, from a module the routes import. */
|
|
@@ -299,9 +321,10 @@ export function backofficePaths(host: BackofficeHost = backofficeHost()) {
|
|
|
299
321
|
}
|
|
300
322
|
|
|
301
323
|
/** Reads a required optional host group, failing loudly when it is missing. */
|
|
302
|
-
export function requireHostGroup<
|
|
303
|
-
|
|
304
|
-
|
|
324
|
+
export function requireHostGroup<
|
|
325
|
+
K extends
|
|
326
|
+
"media" | "forms" | "pages" | "sections" | "dashboard" | "authAdapters",
|
|
327
|
+
>(key: K): NonNullable<BackofficeHost[K]> {
|
|
305
328
|
const group = backofficeHost()[key];
|
|
306
329
|
if (!group) {
|
|
307
330
|
throw new Error(
|