@bygghammar/cms-platform 0.90.0-m7.3 → 0.90.0-m7.5

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,32 @@ 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.5
12
+
13
+ - `BackofficeHost.pages.fns.listPages` is declared. It was what
14
+ `cmsPageDirectory` had always looked for, read through an index cast because
15
+ the contract did not name it — so a site could not register it without a type
16
+ error, and every site that had not silently got a picker with nothing in it.
17
+ The top-level lookup stays for anyone who registered it there anyway.
18
+
19
+ - Requires `@bygghammar/lightweight-popup@>=0.2.0-m7.5`. In `m7.4` an advanced
20
+ path rule could not be typed: the row was reclassified after the first
21
+ keystroke and disappeared. No change here beyond the floor — a site pairing
22
+ this with `m7.4` would get a picker whose rules field does not work.
23
+
24
+ ## 0.90.0-m7.4
25
+
26
+ - `cmsPageDirectory` takes the site's routing engine and asks it for each
27
+ page's public path, instead of assuming the page key is the path. The home
28
+ page is why: its key is `home` and its address is `/`, so the popup page
29
+ picker would have offered an address that matches nothing.
30
+ - The Popups screen supplies that resolver from the `usePageUrl` hook the
31
+ backoffice already registers — no new host field, no second pages query. The
32
+ hook is called from its own component so it stays unconditional while the
33
+ page group remains optional.
34
+ - Requires `@bygghammar/lightweight-popup@>=0.2.0-m7.4`, which is where the
35
+ page picker itself lives.
36
+
11
37
  ## 0.90.0-m7.3
12
38
 
13
39
  - Popup exclusions are derived from the configured `backofficePath`, so a site
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.90.0-m7.3",
3
+ "version": "0.90.0-m7.5",
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.3"
266
+ "@bygghammar/lightweight-popup": ">=0.2.0-m7.5"
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.3.tgz",
331
+ "@bygghammar/lightweight-popup": "file:./vendor/bygghammar-lightweight-popup-0.2.0-m7.5.tgz",
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>;
@@ -11,7 +11,7 @@
11
11
  * CMS half: the site's forms and pages, discovered through the platform's own
12
12
  * host contract rather than reimplemented per site.
13
13
  */
14
- import { useMemo } from "react";
14
+ import { useCallback, useMemo } from "react";
15
15
  import type { ReactNode } from "react";
16
16
  import { PopupAdminApp } from "@bygghammar/lightweight-popup/admin";
17
17
  // The backoffice styling ships with the popup package but must be imported by
@@ -21,6 +21,9 @@ import { PopupAdminApp } from "@bygghammar/lightweight-popup/admin";
21
21
  import "@bygghammar/lightweight-popup/admin.css";
22
22
  import type { AdminAdapters } from "@bygghammar/lightweight-popup/adapters";
23
23
 
24
+ import { backofficeHost } from "../backoffice/routes/host";
25
+ import type { BackofficeHost } from "../backoffice/routes/host";
26
+ import { siteConfig } from "../config/site-config";
24
27
  import { useFeatureGate } from "../backoffice/shell/use-resolved-config";
25
28
  import { cmsPopupNavigation } from "./cms-bridges";
26
29
  import { cmsMediaPicker } from "./media-bridge";
@@ -32,7 +35,21 @@ import {
32
35
  } from "./cms-bridges";
33
36
  import { popupAdminHost } from "./host";
34
37
 
35
- function PopupsRouteInner(): ReactNode {
38
+ /** The page group, or nothing — a site can run popups without the page builder. */
39
+ type PagesHost = NonNullable<BackofficeHost["pages"]>;
40
+
41
+ /**
42
+ * The screen, once it knows how to turn a page key into an address.
43
+ *
44
+ * Split out for one reason: `usePageUrl` is a hook and the page group is
45
+ * optional, so it cannot be called behind a condition. A separate component
46
+ * makes the call unconditional in the only place it happens.
47
+ */
48
+ function PopupsScreen({
49
+ resolvePath,
50
+ }: {
51
+ resolvePath?: (pageKey: string) => string;
52
+ }): ReactNode {
36
53
  const host = popupAdminHost();
37
54
 
38
55
  const adapters = useMemo<AdminAdapters>(
@@ -42,7 +59,7 @@ function PopupsRouteInner(): ReactNode {
42
59
  // The CMS knows its own forms and pages; a host only overrides these when
43
60
  // its content model differs from the platform's.
44
61
  formDirectory: host.formDirectory ?? cmsFormDirectory(),
45
- pages: host.pages ?? cmsPageDirectory(),
62
+ pages: host.pages ?? cmsPageDirectory(resolvePath),
46
63
  // Only offered when the site actually has destinations: the popup editor
47
64
  // shows no picker at all otherwise, rather than an empty control.
48
65
  optInDestinations: cmsOptInDestinations(),
@@ -57,12 +74,40 @@ function PopupsRouteInner(): ReactNode {
57
74
  renderForm: ({ formKey }) => <ManagedForm formKey={formKey} />,
58
75
  },
59
76
  }),
60
- [host],
77
+ [host, resolvePath],
61
78
  );
62
79
 
63
80
  return <PopupAdminApp adapters={adapters} />;
64
81
  }
65
82
 
83
+ /**
84
+ * The same screen, with the site's routing engine behind the page addresses.
85
+ *
86
+ * The default locale's path, because a popup's targeting is one list of paths
87
+ * for the whole site rather than one per language. On a site that gives each
88
+ * locale its own address, the other locales are a rule away in Advanced.
89
+ */
90
+ function PopupsScreenWithPageUrls({ pages }: { pages: PagesHost }): ReactNode {
91
+ const pageUrl = pages.usePageUrl();
92
+ const locale = siteConfig().defaultLocale;
93
+ const resolvePath = useCallback(
94
+ (pageKey: string) => pageUrl(pageKey, locale).path,
95
+ [pageUrl, locale],
96
+ );
97
+ return <PopupsScreen resolvePath={resolvePath} />;
98
+ }
99
+
100
+ function PopupsRouteInner(): ReactNode {
101
+ const pages = backofficeHost().pages;
102
+ // Branching on which component renders, not on which hooks run: each of the
103
+ // two calls its own hooks unconditionally, which is the rule.
104
+ return pages === undefined ? (
105
+ <PopupsScreen />
106
+ ) : (
107
+ <PopupsScreenWithPageUrls pages={pages} />
108
+ );
109
+ }
110
+
66
111
  /** PopupsRoute guarded by the "popups" module switch. */
67
112
  export function PopupsRoute(): ReactNode {
68
113
  const gate = useFeatureGate("popups", "Popups");
@@ -138,27 +138,47 @@ export function cmsFormDirectory(): FormDirectoryPort {
138
138
  /**
139
139
  * The site's pages, for targeting.
140
140
  *
141
+ * The popup builder shows an editor their pages by name and stores the
142
+ * address, so both halves have to be right. The name is the CMS's own page
143
+ * name. The address is the one a visitor would type — which is not the page's
144
+ * key: this platform's routing turns a key into a path through the site's slug
145
+ * table, and the home page is the case that proves it, since its key is
146
+ * `home` and its address is `/`. Targeting `/home` would match nothing, and
147
+ * nothing would say why.
148
+ *
149
+ * So a caller passes `resolvePath` — the routing engine the backoffice already
150
+ * uses for "view this page" links. Without one, the key is the best guess
151
+ * available, which is what this did before and is right for a host whose keys
152
+ * are paths.
153
+ *
141
154
  * Returns an empty list rather than throwing when the host has no page module
142
155
  * registered: a missing picker is a smaller problem than a backoffice screen
143
156
  * that will not render.
144
157
  */
145
- export function cmsPageDirectory(): PageDirectoryPort {
158
+ export function cmsPageDirectory(
159
+ resolvePath?: (pageKey: string) => string,
160
+ ): PageDirectoryPort {
146
161
  return {
147
162
  listPages: async (): Promise<readonly HostPage[]> => {
148
- // Read through an index rather than a named property: the host contract
149
- // is a closed type that does not currently declare a page listing, and a
150
- // site is free to supply one. A host without it simply has no picker.
151
- const host = backofficeHost() as unknown as {
152
- pages?: { fns?: Record<string, unknown> };
153
- fns: Record<string, unknown>;
154
- };
155
- 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"];
156
170
  if (typeof listPages !== "function") return [];
157
171
  const rows = asRecords(await (listPages as () => Promise<unknown>)());
158
172
  return rows.flatMap((row) => {
159
173
  const key = text(row, "path", "slug", "key");
160
174
  if (key === undefined) return [];
161
- const path = key.startsWith("/") ? key : `/${key}`;
175
+ const resolved = resolvePath === undefined ? undefined : resolvePath(key);
176
+ const path =
177
+ resolved !== undefined && resolved.startsWith("/")
178
+ ? resolved
179
+ : key.startsWith("/")
180
+ ? key
181
+ : `/${key}`;
162
182
  return [{ path, title: text(row, "title", "name") ?? path }];
163
183
  });
164
184
  },