@bygghammar/cms-platform 0.90.0-m7.4 → 0.90.0-m7.8
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 +45 -0
- package/package.json +3 -3
- package/src/backoffice/routes/host.ts +9 -0
- package/src/forms/FormRenderer.tsx +22 -0
- package/src/host/index.tsx +11 -0
- package/src/popups/admin.tsx +4 -0
- package/src/popups/cms-bridges.ts +7 -8
- package/src/popups/runtime.tsx +23 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,51 @@ 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.8
|
|
12
|
+
|
|
13
|
+
- Requires `@bygghammar/lightweight-popup@>=0.2.0-m7.8`. `m7.6` and `m7.7` were
|
|
14
|
+
tagged and never published: two defects in the release workflow stopped them
|
|
15
|
+
after every gate had passed. The form-identity work they were meant to carry
|
|
16
|
+
is unchanged and ships here.
|
|
17
|
+
- The release workflow gets both corrections, proven in lightweight-popup:
|
|
18
|
+
the redundant post-checkout `git fetch` is gone (checkout already brings
|
|
19
|
+
every branch, and its credentials are removed on the way out), and
|
|
20
|
+
`npm pack`'s destination is created before it is packed into, with the same
|
|
21
|
+
directory carried to the tarball verification rather than spelled twice.
|
|
22
|
+
- `ops/verify-release-tag.mjs` checks `refs/remotes/origin/main` is present
|
|
23
|
+
before asking `merge-base` about it, so a checkout that fetched too little no
|
|
24
|
+
longer reports itself as a tag on an unmerged branch.
|
|
25
|
+
|
|
26
|
+
## 0.90.0-m7.6
|
|
27
|
+
|
|
28
|
+
- A form submitted from inside a popup now says so. `FormRenderer` and
|
|
29
|
+
`ManagedForm` take an opaque `context` that is merged into the submission,
|
|
30
|
+
and the popup module fills it with `{ popupKey }` — the form system keeps
|
|
31
|
+
knowing nothing about the features that use it. Without this the request was
|
|
32
|
+
indistinguishable from the same form filled on an ordinary page, so the
|
|
33
|
+
server stored an ordinary entry and every delivery configured on the popup
|
|
34
|
+
silently did nothing.
|
|
35
|
+
- `HostSubmitFormInput` gains `popupKey?: string`, the only popup-shaped value
|
|
36
|
+
the browser sends. Destination, endpoint, signing secret and which fields may
|
|
37
|
+
leave are all read on the server from the popup's stored configuration.
|
|
38
|
+
- Drafts and the builder preview carry no context, so a part-filled form and an
|
|
39
|
+
editor checking a layout cannot fire a campaign's webhook.
|
|
40
|
+
- Requires `@bygghammar/lightweight-popup@>=0.2.0-m7.6`, which is where the
|
|
41
|
+
identity comes from.
|
|
42
|
+
|
|
43
|
+
## 0.90.0-m7.5
|
|
44
|
+
|
|
45
|
+
- `BackofficeHost.pages.fns.listPages` is declared. It was what
|
|
46
|
+
`cmsPageDirectory` had always looked for, read through an index cast because
|
|
47
|
+
the contract did not name it — so a site could not register it without a type
|
|
48
|
+
error, and every site that had not silently got a picker with nothing in it.
|
|
49
|
+
The top-level lookup stays for anyone who registered it there anyway.
|
|
50
|
+
|
|
51
|
+
- Requires `@bygghammar/lightweight-popup@>=0.2.0-m7.5`. In `m7.4` an advanced
|
|
52
|
+
path rule could not be typed: the row was reclassified after the first
|
|
53
|
+
keystroke and disappeared. No change here beyond the floor — a site pairing
|
|
54
|
+
this with `m7.4` would get a picker whose rules field does not work.
|
|
55
|
+
|
|
11
56
|
## 0.90.0-m7.4
|
|
12
57
|
|
|
13
58
|
- `cmsPageDirectory` takes the site's routing engine and asks it for each
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bygghammar/cms-platform",
|
|
3
|
-
"version": "0.90.0-m7.
|
|
3
|
+
"version": "0.90.0-m7.8",
|
|
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": [
|
|
@@ -263,7 +263,7 @@
|
|
|
263
263
|
"@dnd-kit/sortable": ">=3",
|
|
264
264
|
"@dnd-kit/utilities": ">=3",
|
|
265
265
|
"zod": ">=3",
|
|
266
|
-
"@bygghammar/lightweight-popup": ">=0.2.0-m7.
|
|
266
|
+
"@bygghammar/lightweight-popup": ">=0.2.0-m7.8"
|
|
267
267
|
},
|
|
268
268
|
"peerDependenciesMeta": {
|
|
269
269
|
"@supabase/supabase-js": {
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
"check": "bun run typecheck && bun run test && bun run verify:pack"
|
|
329
329
|
},
|
|
330
330
|
"devDependencies": {
|
|
331
|
-
"@bygghammar/lightweight-popup": "
|
|
331
|
+
"@bygghammar/lightweight-popup": "0.2.0-m7.8",
|
|
332
332
|
"@dnd-kit/core": "^6.3.1",
|
|
333
333
|
"@dnd-kit/sortable": "^10.0.0",
|
|
334
334
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -208,6 +208,15 @@ export type BackofficeHost = {
|
|
|
208
208
|
listPagePublications: HostServerFn;
|
|
209
209
|
cancelPagePublication: HostServerFn;
|
|
210
210
|
renamePage: HostServerFn;
|
|
211
|
+
/**
|
|
212
|
+
* The site's pages, listed.
|
|
213
|
+
*
|
|
214
|
+
* Optional because the builder screens do not need it — they open one
|
|
215
|
+
* page by key. What needs it is anything offering a choice of pages,
|
|
216
|
+
* and the popup page picker is the first: without this the picker has
|
|
217
|
+
* nothing to show and falls back to typing addresses, silently.
|
|
218
|
+
*/
|
|
219
|
+
listPages?: HostServerFn;
|
|
211
220
|
};
|
|
212
221
|
/** Human labels for the locale switch, keyed by locale. */
|
|
213
222
|
localeLabels?: Record<string, string>;
|
|
@@ -48,12 +48,25 @@ export function FormRenderer({
|
|
|
48
48
|
dark = false,
|
|
49
49
|
className,
|
|
50
50
|
preview = false,
|
|
51
|
+
context,
|
|
51
52
|
onSuccess,
|
|
52
53
|
}: {
|
|
53
54
|
form: PublicForm;
|
|
54
55
|
dark?: boolean;
|
|
55
56
|
className?: string;
|
|
56
57
|
preview?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Where this form is being rendered, for whoever is rendering it.
|
|
60
|
+
*
|
|
61
|
+
* Merged into the submission itself, so the server decides what a submission
|
|
62
|
+
* was from what it stored — not from anything the browser says afterwards.
|
|
63
|
+
* A feature that needs the server to treat its submissions differently sends
|
|
64
|
+
* that here, and names itself: this file deliberately does not know which
|
|
65
|
+
* features exist, and a test holds it to that.
|
|
66
|
+
*
|
|
67
|
+
* Absent for an ordinary page form, and that absence is the signal.
|
|
68
|
+
*/
|
|
69
|
+
context?: Readonly<Record<string, string>>;
|
|
57
70
|
/**
|
|
58
71
|
* Called only for a submission the host actually stored. A honeypot
|
|
59
72
|
* rejection — `{ ok: true, skipped: true }` — never reaches it, and neither
|
|
@@ -235,6 +248,11 @@ export function FormRenderer({
|
|
|
235
248
|
elapsedMs: Date.now() - mountedAt.current,
|
|
236
249
|
hp,
|
|
237
250
|
clientToken: form.savePartials ? draftToken(form.key) : undefined,
|
|
251
|
+
// Spread rather than nested, and omitted entirely when there is none:
|
|
252
|
+
// the host reads a key's presence as the signal, so an always-present
|
|
253
|
+
// field holding an empty value is a different contract wearing the
|
|
254
|
+
// same name.
|
|
255
|
+
...(context ?? {}),
|
|
238
256
|
values: visible,
|
|
239
257
|
});
|
|
240
258
|
|
|
@@ -438,11 +456,14 @@ export function ManagedForm({
|
|
|
438
456
|
formKey,
|
|
439
457
|
dark = false,
|
|
440
458
|
className,
|
|
459
|
+
context,
|
|
441
460
|
onSuccess,
|
|
442
461
|
}: {
|
|
443
462
|
formKey: string;
|
|
444
463
|
dark?: boolean;
|
|
445
464
|
className?: string;
|
|
465
|
+
/** Forwarded to `FormRenderer`; see its `context`. */
|
|
466
|
+
context?: Readonly<Record<string, string>>;
|
|
446
467
|
/** Forwarded to `FormRenderer`; fires only for a stored submission. */
|
|
447
468
|
onSuccess?: (event: FormSubmissionSuccess) => void;
|
|
448
469
|
}) {
|
|
@@ -466,6 +487,7 @@ export function ManagedForm({
|
|
|
466
487
|
form={form}
|
|
467
488
|
dark={dark}
|
|
468
489
|
className={className}
|
|
490
|
+
{...(context === undefined ? {} : { context })}
|
|
469
491
|
{...(onSuccess === undefined ? {} : { onSuccess })}
|
|
470
492
|
/>
|
|
471
493
|
);
|
package/src/host/index.tsx
CHANGED
|
@@ -174,6 +174,17 @@ export type HostSubmitFormInput = {
|
|
|
174
174
|
elapsedMs?: number;
|
|
175
175
|
hp?: string;
|
|
176
176
|
clientToken?: string;
|
|
177
|
+
/**
|
|
178
|
+
* The popup this form was submitted from, when it was submitted from one.
|
|
179
|
+
*
|
|
180
|
+
* Absent for every ordinary page form, and that absence is meaningful: a
|
|
181
|
+
* host decides whether a submission is a campaign opt-in by whether this is
|
|
182
|
+
* here. It is the only popup-shaped value the browser sends — the
|
|
183
|
+
* destination, the endpoint, the signing secret and which fields may leave
|
|
184
|
+
* are all read on the server from the popup's stored configuration, because
|
|
185
|
+
* anything the browser could name it could also change.
|
|
186
|
+
*/
|
|
187
|
+
popupKey?: string;
|
|
177
188
|
values: HostFormValues;
|
|
178
189
|
};
|
|
179
190
|
|
package/src/popups/admin.tsx
CHANGED
|
@@ -71,6 +71,10 @@ function PopupsScreen({
|
|
|
71
71
|
forms: {
|
|
72
72
|
// The builder preview renders the site's real form, so what an editor
|
|
73
73
|
// checks is what a visitor will see.
|
|
74
|
+
//
|
|
75
|
+
// Without `popupKey`, deliberately. An editor filling the preview to
|
|
76
|
+
// see how it looks would otherwise store an opt-in and fire the
|
|
77
|
+
// popup's delivery — a real webhook, from somebody checking a layout.
|
|
74
78
|
renderForm: ({ formKey }) => <ManagedForm formKey={formKey} />,
|
|
75
79
|
},
|
|
76
80
|
}),
|
|
@@ -160,14 +160,13 @@ export function cmsPageDirectory(
|
|
|
160
160
|
): PageDirectoryPort {
|
|
161
161
|
return {
|
|
162
162
|
listPages: async (): Promise<readonly HostPage[]> => {
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
const host = backofficeHost()
|
|
167
|
-
|
|
168
|
-
fns
|
|
169
|
-
|
|
170
|
-
const listPages = host.pages?.fns?.["listPages"] ?? host.fns["listPages"];
|
|
163
|
+
// `pages.fns.listPages` is the declared home for this. The top-level
|
|
164
|
+
// lookup stays for a site that registered it there before the contract
|
|
165
|
+
// named it, and a host with neither simply has no picker.
|
|
166
|
+
const host = backofficeHost();
|
|
167
|
+
const listPages =
|
|
168
|
+
host.pages?.fns.listPages ??
|
|
169
|
+
(host.fns as unknown as Record<string, unknown>)["listPages"];
|
|
171
170
|
if (typeof listPages !== "function") return [];
|
|
172
171
|
const rows = asRecords(await (listPages as () => Promise<unknown>)());
|
|
173
172
|
return rows.flatMap((row) => {
|
package/src/popups/runtime.tsx
CHANGED
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
PopupRuntime,
|
|
18
18
|
withExcludedBase,
|
|
19
19
|
} from "@bygghammar/lightweight-popup/runtime";
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
FormRenderPort,
|
|
22
|
+
RuntimeAdapters,
|
|
23
|
+
} from "@bygghammar/lightweight-popup/adapters";
|
|
21
24
|
|
|
22
25
|
import { ManagedForm } from "../forms/FormRenderer";
|
|
23
26
|
import { onFormSubmissionSuccess } from "../forms/success-listeners";
|
|
@@ -28,14 +31,23 @@ import { popupPublicHost } from "./host";
|
|
|
28
31
|
/**
|
|
29
32
|
* Renders one of the site's own forms inside a popup.
|
|
30
33
|
*
|
|
34
|
+
* `popupKey` goes into the submission itself, and that is the whole point of
|
|
35
|
+
* this component existing: without it the request is indistinguishable from
|
|
36
|
+
* the same form filled on an ordinary page, so the server stores an ordinary
|
|
37
|
+
* entry and every delivery configured on the popup quietly does nothing.
|
|
38
|
+
*
|
|
31
39
|
* Success comes from the CMS form seam rather than from this component, which
|
|
32
40
|
* is what makes a honeypot rejection impossible to miscount: the form runtime
|
|
33
|
-
* only announces submissions the host actually stored.
|
|
41
|
+
* only announces submissions the host actually stored. That listener drives
|
|
42
|
+
* the popup's own flow and analytics — it is deliberately not a second way to
|
|
43
|
+
* deliver, because it would be delivering on the browser's word.
|
|
34
44
|
*/
|
|
35
45
|
function PopupForm({
|
|
46
|
+
popupKey,
|
|
36
47
|
formKey,
|
|
37
48
|
onSuccess,
|
|
38
49
|
}: {
|
|
50
|
+
popupKey: string;
|
|
39
51
|
formKey: string;
|
|
40
52
|
onSuccess: () => void;
|
|
41
53
|
}): ReactNode {
|
|
@@ -50,7 +62,9 @@ function PopupForm({
|
|
|
50
62
|
[formKey],
|
|
51
63
|
);
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
// The popup names itself here, in the popup module. The form renderer takes
|
|
66
|
+
// an opaque context and stays independent of the features that use it.
|
|
67
|
+
return <ManagedForm context={{ popupKey }} formKey={formKey} />;
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
/**
|
|
@@ -82,9 +96,12 @@ export function PopupModuleRuntime(): ReactNode {
|
|
|
82
96
|
const path = useRouterState({ select: (state) => state.location.pathname });
|
|
83
97
|
const host = popupPublicHost();
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
// The argument type comes from the port, not a copy of it: a package that
|
|
100
|
+
// stopped supplying `popupKey` would fail to compile here rather than
|
|
101
|
+
// quietly go back to submitting forms with no identity.
|
|
102
|
+
const renderForm = useCallback<FormRenderPort["renderForm"]>(
|
|
103
|
+
({ popupKey, formKey, onSuccess }) => (
|
|
104
|
+
<PopupForm popupKey={popupKey} formKey={formKey} onSuccess={onSuccess} />
|
|
88
105
|
),
|
|
89
106
|
[],
|
|
90
107
|
);
|