@bygghammar/cms-platform 0.90.0-m7.5 → 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 CHANGED
@@ -8,6 +8,38 @@ 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
+
11
43
  ## 0.90.0-m7.5
12
44
 
13
45
  - `BackofficeHost.pages.fns.listPages` is declared. It was what
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.90.0-m7.5",
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.5"
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": "file:./vendor/bygghammar-lightweight-popup-0.2.0-m7.5.tgz",
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",
@@ -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
  );
@@ -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
 
@@ -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
  }),
@@ -17,7 +17,10 @@ import {
17
17
  PopupRuntime,
18
18
  withExcludedBase,
19
19
  } from "@bygghammar/lightweight-popup/runtime";
20
- import type { RuntimeAdapters } from "@bygghammar/lightweight-popup/adapters";
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
- return <ManagedForm formKey={formKey} />;
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
- const renderForm = useCallback(
86
- ({ formKey, onSuccess }: { formKey: string; onSuccess: () => void }) => (
87
- <PopupForm formKey={formKey} onSuccess={onSuccess} />
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
  );