@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 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.4",
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.4"
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.4.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",
@@ -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
  );
@@ -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
  }),
@@ -160,14 +160,13 @@ export function cmsPageDirectory(
160
160
  ): PageDirectoryPort {
161
161
  return {
162
162
  listPages: async (): Promise<readonly HostPage[]> => {
163
- // Read through an index rather than a named property: the host contract
164
- // is a closed type that does not currently declare a page listing, and a
165
- // site is free to supply one. A host without it simply has no picker.
166
- const host = backofficeHost() as unknown as {
167
- pages?: { fns?: Record<string, unknown> };
168
- fns: Record<string, unknown>;
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) => {
@@ -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
  );