@bygghammar/cms-platform 0.80.1 → 0.90.0-m7.10
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 +489 -0
- package/README.md +16 -2
- package/docs/EDIT-LOCKS.md +213 -0
- package/docs/POPUPS.md +102 -0
- package/migrations/20260902090000_dc072b25-8467-46ae-aef3-0ccdf40aa7eb.sql +274 -0
- package/package.json +31 -3
- package/scripts/create-cms-site.mjs +5 -4
- package/scripts/sync-migrations.mjs +27 -6
- package/src/backoffice/ContactDetailsView.tsx +19 -5
- package/src/backoffice/DomainsView.tsx +152 -25
- package/src/backoffice/EditLock.tsx +513 -0
- package/src/backoffice/EmailSettingsView.tsx +99 -0
- package/src/backoffice/FormEditorView.tsx +531 -512
- package/src/backoffice/FormSettingsView.tsx +16 -4
- package/src/backoffice/GlobalSectionBuilderView.tsx +42 -11
- package/src/backoffice/NavigationView.tsx +35 -11
- package/src/backoffice/PageBuilderView.tsx +51 -16
- package/src/backoffice/RichTextEditor.tsx +50 -14
- package/src/backoffice/SectionEditorView.tsx +18 -6
- package/src/backoffice/SectionFormView.tsx +184 -31
- package/src/backoffice/SiteSettingsView.tsx +14 -15
- package/src/backoffice/TrackingView.tsx +247 -208
- package/src/backoffice/forms/VersionHistory.tsx +4 -2
- package/src/backoffice/inspectors/blocks.tsx +22 -1
- package/src/backoffice/inspectors/fields.tsx +21 -0
- package/src/backoffice/routes/host.ts +58 -7
- package/src/backoffice/routes/index.tsx +117 -15
- package/src/backoffice/routes/pages.tsx +10 -6
- package/src/backoffice/seo/PageSeoPanelView.tsx +215 -197
- package/src/backoffice/shell/domain-guard.ts +27 -14
- package/src/backoffice/shell/index.ts +5 -0
- package/src/backoffice/shell/nav-model.ts +32 -23
- package/src/backoffice/shell/use-resolved-config.tsx +67 -0
- package/src/backoffice/tracking-drafts.ts +150 -0
- package/src/blocks/components/CtaBannerBlock.tsx +18 -12
- package/src/blocks/components/HeroFullBlock.tsx +25 -12
- package/src/blocks/components/MediaSplitBlock.tsx +91 -41
- package/src/blocks/components/PriceTableBlock.tsx +123 -89
- package/src/blocks/components/RichTextBlock.tsx +26 -4
- package/src/blocks/index.ts +8 -0
- package/src/blocks/optional-content.ts +82 -0
- package/src/blocks/registry.tsx +1 -0
- package/src/blocks/types.ts +6 -0
- package/src/cms/edit-lock.ts +491 -0
- package/src/config/modules.ts +208 -0
- package/src/config/site-config.ts +9 -31
- package/src/forms/FormRenderer.tsx +62 -3
- package/src/forms/submission-result.ts +35 -0
- package/src/forms/success-listeners.ts +46 -0
- package/src/host/index.tsx +27 -1
- package/src/modules/index.ts +28 -0
- package/src/modules/public-runtime.tsx +159 -0
- package/src/navigation/index.ts +29 -0
- package/src/popups/admin.tsx +123 -0
- package/src/popups/cms-bridges.ts +240 -0
- package/src/popups/destinations.tsx +147 -0
- package/src/popups/host.ts +91 -0
- package/src/popups/media-bridge.tsx +84 -0
- package/src/popups/register.ts +23 -0
- package/src/popups/runtime.tsx +128 -0
- package/src/runtime/server-entry.ts +8 -2
- package/src/server/content-sections-core.ts +22 -5
- package/src/server/domains-core.ts +124 -107
- package/src/server/edit-lock-adapter.ts +91 -0
- package/src/server/edit-lock-supabase-adapter.ts +135 -0
- package/src/server/edit-locks-core.ts +286 -0
- package/src/server/forms-core.ts +37 -2
- package/src/server/injections-core.ts +230 -49
- package/src/server/page-seo-save-core.ts +27 -0
- package/src/server/pages-core.ts +108 -11
- package/src/server/popup-optin-trigger.ts +111 -0
- package/src/server/popup-optin-webhook-core.ts +363 -0
- package/src/server/provider-secrets.ts +100 -4
- package/src/server/schemas.ts +74 -9
- package/src/server/webhook-url-guard.ts +199 -0
- package/src/site/SiteHeader.tsx +11 -5
- package/src/site/host.ts +172 -0
- package/src/site/locale-domains.ts +500 -0
- package/src/site/routes.ts +206 -38
- package/src/site/site-url.ts +80 -17
- package/src/tracking/injections.ts +52 -9
- package/src/types/css-modules.d.ts +6 -0
- package/src/ui/Reveal.tsx +74 -9
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { ReactNode } from "react";
|
|
10
10
|
|
|
11
11
|
import type { SiteConfig } from "../../config/site-config";
|
|
12
|
+
import type { SecretsLocation } from "../../server/provider-secrets";
|
|
12
13
|
|
|
13
14
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
14
15
|
/** Media field component as the reviews editor consumes it. */
|
|
@@ -34,6 +35,8 @@ export type BackofficeHost = {
|
|
|
34
35
|
company: Record<string, string>;
|
|
35
36
|
/** Locale used to preview form copy in the backoffice. */
|
|
36
37
|
previewLocale?: string;
|
|
38
|
+
/** Where this installation configures provider and system-mail secrets. */
|
|
39
|
+
secretsLocation?: SecretsLocation;
|
|
37
40
|
/** Server functions, grouped per module. */
|
|
38
41
|
fns: {
|
|
39
42
|
// admin
|
|
@@ -78,6 +81,31 @@ export type BackofficeHost = {
|
|
|
78
81
|
// locale domains
|
|
79
82
|
saveLocaleDomain: HostServerFn;
|
|
80
83
|
deleteLocaleDomain: HostServerFn;
|
|
84
|
+
/*
|
|
85
|
+
* Which hostname actually serves the backoffice, and why.
|
|
86
|
+
*
|
|
87
|
+
* `SITE_URL` is server information: it cannot be read from the browser,
|
|
88
|
+
* and a value baked into this registry would be a guess presented to an
|
|
89
|
+
* administrator as fact about their own install. So the answer is fetched
|
|
90
|
+
* from the server instead, and returns the package's `EffectiveAdminHost`.
|
|
91
|
+
*
|
|
92
|
+
* Optional: a host that has not registered it simply gets no statement
|
|
93
|
+
* about the backoffice hostname on the domains screen. The editor works
|
|
94
|
+
* either way — this only ever adds an explanation, never a permission.
|
|
95
|
+
*/
|
|
96
|
+
getEffectiveAdminHost?: HostServerFn;
|
|
97
|
+
/*
|
|
98
|
+
* Popup opt-in destinations, for the popup editor's picker.
|
|
99
|
+
*
|
|
100
|
+
* Optional: a site with no delivery integration simply does not register
|
|
101
|
+
* it, and the popup editor then shows no picker at all rather than an
|
|
102
|
+
* empty control. Returns names and ids — never an endpoint or a secret.
|
|
103
|
+
*/
|
|
104
|
+
listWebhookDestinations?: HostServerFn;
|
|
105
|
+
saveWebhookDestination?: HostServerFn;
|
|
106
|
+
deleteWebhookDestination?: HostServerFn;
|
|
107
|
+
sendTestWebhook?: HostServerFn;
|
|
108
|
+
listWebhookDeliveries?: HostServerFn;
|
|
81
109
|
};
|
|
82
110
|
/** Site-specific example values shown as input placeholders. */
|
|
83
111
|
placeholders?: {
|
|
@@ -105,7 +133,10 @@ export type BackofficeHost = {
|
|
|
105
133
|
/** Editor components a site wires with its own locales, media and forms. */
|
|
106
134
|
components: {
|
|
107
135
|
/** Recursive shared-content editor. */
|
|
108
|
-
SectionForm: (props: {
|
|
136
|
+
SectionForm: (props: {
|
|
137
|
+
value: unknown;
|
|
138
|
+
onChange: (next: unknown) => void;
|
|
139
|
+
}) => ReactNode;
|
|
109
140
|
/** Single media field used by the reviews editor. */
|
|
110
141
|
MediaField: MediaFieldComponent;
|
|
111
142
|
/** Review portrait uploader. */
|
|
@@ -193,6 +224,15 @@ export type BackofficeHost = {
|
|
|
193
224
|
listPagePublications: HostServerFn;
|
|
194
225
|
cancelPagePublication: HostServerFn;
|
|
195
226
|
renamePage: HostServerFn;
|
|
227
|
+
/**
|
|
228
|
+
* The site's pages, listed.
|
|
229
|
+
*
|
|
230
|
+
* Optional because the builder screens do not need it — they open one
|
|
231
|
+
* page by key. What needs it is anything offering a choice of pages,
|
|
232
|
+
* and the popup page picker is the first: without this the picker has
|
|
233
|
+
* nothing to show and falls back to typing addresses, silently.
|
|
234
|
+
*/
|
|
235
|
+
listPages?: HostServerFn;
|
|
196
236
|
};
|
|
197
237
|
/** Human labels for the locale switch, keyed by locale. */
|
|
198
238
|
localeLabels?: Record<string, string>;
|
|
@@ -200,7 +240,10 @@ export type BackofficeHost = {
|
|
|
200
240
|
* React hook returning a resolver for a page's public URL. Implemented as a
|
|
201
241
|
* hook so a site can read its slug overrides and locale domains.
|
|
202
242
|
*/
|
|
203
|
-
usePageUrl: () => (
|
|
243
|
+
usePageUrl: () => (
|
|
244
|
+
pageKey: string,
|
|
245
|
+
locale: string,
|
|
246
|
+
) => { path: string; host: string };
|
|
204
247
|
/** Persists unsaved shared-content drafts; returns the keys it saved. */
|
|
205
248
|
flushSharedDrafts?: () => Promise<string[]>;
|
|
206
249
|
/** Renders the block canvas for the builder. */
|
|
@@ -211,7 +254,12 @@ export type BackofficeHost = {
|
|
|
211
254
|
blocks: any[];
|
|
212
255
|
setBlocks: (next: any[]) => void;
|
|
213
256
|
globals: Record<string, any[]>;
|
|
214
|
-
globalSections: {
|
|
257
|
+
globalSections: {
|
|
258
|
+
key: string;
|
|
259
|
+
name: string;
|
|
260
|
+
kind: "block" | "section";
|
|
261
|
+
blocks: any[];
|
|
262
|
+
}[];
|
|
215
263
|
locale: string;
|
|
216
264
|
setLocale: (next: string) => void;
|
|
217
265
|
setDirty: (next: boolean) => void;
|
|
@@ -248,7 +296,6 @@ export type BackofficeHost = {
|
|
|
248
296
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
249
297
|
};
|
|
250
298
|
|
|
251
|
-
|
|
252
299
|
let current: BackofficeHost | null = null;
|
|
253
300
|
|
|
254
301
|
/** Registers the host adapters. Call once, from a module the routes import. */
|
|
@@ -283,6 +330,7 @@ export function backofficePaths(host: BackofficeHost = backofficeHost()) {
|
|
|
283
330
|
email: `${base}/admin/email`,
|
|
284
331
|
contact: `${base}/admin/contact`,
|
|
285
332
|
tracking: `${base}/admin/tracking`,
|
|
333
|
+
domains: `${base}/admin/domains`,
|
|
286
334
|
redirects: `${base}/admin/redirects`,
|
|
287
335
|
sitemap: `${base}/admin/sitemap`,
|
|
288
336
|
navigation: `${base}/admin/navigation`,
|
|
@@ -295,13 +343,16 @@ export function backofficePaths(host: BackofficeHost = backofficeHost()) {
|
|
|
295
343
|
formDeliveries: `${base}/forms/deliveries`,
|
|
296
344
|
pagesBuilder: `${base}/pages-builder`,
|
|
297
345
|
resetPassword: `${base}/reset-password`,
|
|
346
|
+
popups: `${base}/popups`,
|
|
347
|
+
popupDestinations: `${base}/popups/destinations`,
|
|
298
348
|
};
|
|
299
349
|
}
|
|
300
350
|
|
|
301
351
|
/** Reads a required optional host group, failing loudly when it is missing. */
|
|
302
|
-
export function requireHostGroup<
|
|
303
|
-
|
|
304
|
-
|
|
352
|
+
export function requireHostGroup<
|
|
353
|
+
K extends
|
|
354
|
+
"media" | "forms" | "pages" | "sections" | "dashboard" | "authAdapters",
|
|
355
|
+
>(key: K): NonNullable<BackofficeHost[K]> {
|
|
305
356
|
const group = backofficeHost()[key];
|
|
306
357
|
if (!group) {
|
|
307
358
|
throw new Error(
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Menu,
|
|
19
19
|
Package,
|
|
20
20
|
ScrollText,
|
|
21
|
+
Languages,
|
|
21
22
|
Shield,
|
|
22
23
|
Signpost,
|
|
23
24
|
SlidersHorizontal,
|
|
@@ -25,8 +26,10 @@ import {
|
|
|
25
26
|
Users,
|
|
26
27
|
} from "lucide-react";
|
|
27
28
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
29
|
+
import { useCallback, useRef } from "react";
|
|
28
30
|
import { useServerFn } from "@tanstack/react-start";
|
|
29
31
|
|
|
32
|
+
import { MISSING_CREATED_ID } from "../../tracking/injections";
|
|
30
33
|
import type { SiteSettingOverrides } from "../../config/site-config";
|
|
31
34
|
import { AuditView } from "../AuditView";
|
|
32
35
|
import { BackupView } from "../BackupView";
|
|
@@ -42,10 +45,16 @@ import { RedirectsView } from "../RedirectsView";
|
|
|
42
45
|
import { RolesView } from "../RolesView";
|
|
43
46
|
import { SitemapView, type SitemapCheck } from "../SitemapView";
|
|
44
47
|
import { SiteSettingsView } from "../SiteSettingsView";
|
|
45
|
-
import { TrackingView } from "../TrackingView";
|
|
48
|
+
import { TrackingView, type TrackingDeps } from "../TrackingView";
|
|
46
49
|
import { UsersView } from "../UsersView";
|
|
47
50
|
import { VersionView } from "../VersionView";
|
|
48
|
-
import {
|
|
51
|
+
import { parseEffectiveAdminHost } from "../../site/locale-domains";
|
|
52
|
+
import {
|
|
53
|
+
useFeatureEnabled,
|
|
54
|
+
useFeatureGate,
|
|
55
|
+
useModuleGate,
|
|
56
|
+
useMyRoles,
|
|
57
|
+
} from "../shell/use-resolved-config";
|
|
49
58
|
import { backofficeHost, backofficePaths } from "./host";
|
|
50
59
|
|
|
51
60
|
export * from "./host";
|
|
@@ -200,13 +209,54 @@ function TrackingRouteInner() {
|
|
|
200
209
|
const q = useQuery({ queryKey: ["admin", "injections"], queryFn: () => list() });
|
|
201
210
|
const domainsQ = useQuery({ queryKey: ["admin", "locale-domains"], queryFn: () => domains() });
|
|
202
211
|
|
|
212
|
+
/*
|
|
213
|
+
* A create whose answer carried no id, if one has happened.
|
|
214
|
+
*
|
|
215
|
+
* The insert may well have succeeded — what is missing is the identity of
|
|
216
|
+
* the row it wrote, not evidence that it wrote none. Sending the same draft
|
|
217
|
+
* again would therefore be as likely to create a second profile as to retry
|
|
218
|
+
* the first, and the screen has no way to tell which. So no further create
|
|
219
|
+
* is attempted at all until a reload has reconciled the list with what the
|
|
220
|
+
* server actually holds. Updates carry their own id and are unaffected.
|
|
221
|
+
*/
|
|
222
|
+
const createUnresolved = useRef(false);
|
|
223
|
+
|
|
224
|
+
const saveInjection = useCallback<TrackingDeps["saveInjection"]>(
|
|
225
|
+
async (input) => {
|
|
226
|
+
const isCreate = !input.data.id;
|
|
227
|
+
if (isCreate && createUnresolved.current) throw new Error(MISSING_CREATED_ID);
|
|
228
|
+
let result: { id?: unknown } | null | undefined;
|
|
229
|
+
try {
|
|
230
|
+
result = (await save(input as never)) as { id?: unknown } | null | undefined;
|
|
231
|
+
} catch (err) {
|
|
232
|
+
/*
|
|
233
|
+
* The server refuses an insert it cannot identify, and that refusal is
|
|
234
|
+
* indeterminate in exactly the way the check below is: the row may have
|
|
235
|
+
* landed. Any other failure is a plain no, so only this one closes
|
|
236
|
+
* creates — a rejected create is otherwise still worth another try.
|
|
237
|
+
*/
|
|
238
|
+
if (isCreate && err instanceof Error && err.message === MISSING_CREATED_ID) {
|
|
239
|
+
createUnresolved.current = true;
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
const id = typeof result?.id === "string" ? result.id.trim() : "";
|
|
244
|
+
if (isCreate && !id) {
|
|
245
|
+
createUnresolved.current = true;
|
|
246
|
+
throw new Error(MISSING_CREATED_ID);
|
|
247
|
+
}
|
|
248
|
+
return { id };
|
|
249
|
+
},
|
|
250
|
+
[save],
|
|
251
|
+
);
|
|
252
|
+
|
|
203
253
|
return (
|
|
204
254
|
<TrackingView
|
|
205
255
|
deps={{
|
|
206
256
|
profiles: q.data ?? [],
|
|
207
257
|
domains: domainsQ.data ?? [],
|
|
208
258
|
locales: locales as never,
|
|
209
|
-
saveInjection
|
|
259
|
+
saveInjection,
|
|
210
260
|
deleteInjection: (input) => remove(input),
|
|
211
261
|
refresh: () => q.refetch(),
|
|
212
262
|
}}
|
|
@@ -308,9 +358,20 @@ export function AdminLayoutRoute() {
|
|
|
308
358
|
export function AdminOverviewRoute() {
|
|
309
359
|
const host = backofficeHost();
|
|
310
360
|
const paths = backofficePaths(host);
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
361
|
+
const { roles } = useMyRoles();
|
|
362
|
+
const isSuper = roles.includes("superadmin");
|
|
363
|
+
// Both screens write through server functions that ask `has_role(user,
|
|
364
|
+
// 'admin')`, so a card offered to anyone else is an invitation to a screen
|
|
365
|
+
// whose every save comes back refused. The role is read from the list the
|
|
366
|
+
// host returns; holding `superadmin` is not the same grant.
|
|
367
|
+
const isAdmin = roles.includes("admin");
|
|
368
|
+
// The live flags, not the compiled ones. Both route gates already read the
|
|
369
|
+
// live value, so a card driven by the compiled default disagrees with the
|
|
370
|
+
// screen behind it: an override that switched a module on leaves its card
|
|
371
|
+
// missing, and one that switched a module off leaves a card that opens
|
|
372
|
+
// straight into a no-access screen.
|
|
373
|
+
const domainsEnabled = useFeatureEnabled("domains") === true;
|
|
374
|
+
const trackingEnabled = useFeatureEnabled("tracking") === true;
|
|
314
375
|
|
|
315
376
|
const cards = [
|
|
316
377
|
{
|
|
@@ -360,7 +421,7 @@ export function AdminOverviewRoute() {
|
|
|
360
421
|
body: "Company address, org. number, email and phone.",
|
|
361
422
|
icon: Building2,
|
|
362
423
|
},
|
|
363
|
-
...(
|
|
424
|
+
...(isAdmin && trackingEnabled
|
|
364
425
|
? [
|
|
365
426
|
{
|
|
366
427
|
to: paths.tracking,
|
|
@@ -370,6 +431,16 @@ export function AdminOverviewRoute() {
|
|
|
370
431
|
},
|
|
371
432
|
]
|
|
372
433
|
: []),
|
|
434
|
+
...(isAdmin && domainsEnabled
|
|
435
|
+
? [
|
|
436
|
+
{
|
|
437
|
+
to: paths.domains,
|
|
438
|
+
title: "Languages & domains",
|
|
439
|
+
body: "One domain per language, the default fallback and the hostname the backoffice is served from.",
|
|
440
|
+
icon: Languages,
|
|
441
|
+
},
|
|
442
|
+
]
|
|
443
|
+
: []),
|
|
373
444
|
{
|
|
374
445
|
to: paths.redirects,
|
|
375
446
|
title: "Redirects",
|
|
@@ -532,6 +603,7 @@ function EmailSettingsRouteInner() {
|
|
|
532
603
|
fromEmail: host.placeholders?.fromEmail ?? "",
|
|
533
604
|
notifyTo: host.placeholders?.notifyTo ?? "",
|
|
534
605
|
},
|
|
606
|
+
secretsLocation: host.secretsLocation,
|
|
535
607
|
}}
|
|
536
608
|
/>
|
|
537
609
|
);
|
|
@@ -559,6 +631,15 @@ export function NavigationRoute() {
|
|
|
559
631
|
);
|
|
560
632
|
}
|
|
561
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Stand-in for the optional effective-admin-host function.
|
|
636
|
+
*
|
|
637
|
+
* `useServerFn` is a hook and cannot be called conditionally, so an
|
|
638
|
+
* unregistered function still needs something to hand it. This one is never
|
|
639
|
+
* invoked: the query that would call it stays disabled.
|
|
640
|
+
*/
|
|
641
|
+
const noEffectiveAdminHost = async () => null;
|
|
642
|
+
|
|
562
643
|
/** Per-locale domain mapping. */
|
|
563
644
|
function DomainsRouteInner() {
|
|
564
645
|
const host = backofficeHost();
|
|
@@ -567,19 +648,40 @@ function DomainsRouteInner() {
|
|
|
567
648
|
const list = useServerFn(host.fns.getLocaleDomains);
|
|
568
649
|
const save = useServerFn(host.fns.saveLocaleDomain);
|
|
569
650
|
const del = useServerFn(host.fns.deleteLocaleDomain);
|
|
570
|
-
const q = useQuery({ queryKey: ["locale-domains"], queryFn: () => list() });
|
|
651
|
+
const q = useQuery({ queryKey: ["admin", "locale-domains"], queryFn: () => list() });
|
|
652
|
+
|
|
653
|
+
// Where the backoffice is actually served is server knowledge — `SITE_URL`
|
|
654
|
+
// and the request that arrived. A host that has not registered the function
|
|
655
|
+
// gets no statement about it at all, which is the honest answer: the editor
|
|
656
|
+
// keeps working, it just stops explaining a hostname it cannot know.
|
|
657
|
+
const getAdminHost = useServerFn(host.fns.getEffectiveAdminHost ?? noEffectiveAdminHost);
|
|
658
|
+
const adminHostQuery = useQuery({
|
|
659
|
+
queryKey: ["admin", "effective-admin-host"],
|
|
660
|
+
queryFn: () => getAdminHost(),
|
|
661
|
+
enabled: Boolean(host.fns.getEffectiveAdminHost),
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const domains = q.data ?? [];
|
|
571
665
|
|
|
572
666
|
return (
|
|
573
667
|
<DomainsView
|
|
574
668
|
deps={{
|
|
575
|
-
domains
|
|
669
|
+
domains,
|
|
576
670
|
locales: host.siteConfig.locales,
|
|
577
671
|
defaultLocale: host.siteConfig.defaultLocale,
|
|
578
|
-
backPath: paths.
|
|
672
|
+
backPath: paths.admin,
|
|
579
673
|
domainPlaceholder: host.placeholders?.domain ?? "",
|
|
674
|
+
adminHost: parseEffectiveAdminHost(adminHostQuery.data) ?? undefined,
|
|
675
|
+
backofficePath: paths.base,
|
|
580
676
|
saveDomain: (input) => save(input as never),
|
|
581
677
|
deleteDomain: (input) => del(input as never),
|
|
582
|
-
|
|
678
|
+
// A saved or deleted mapping can move the backoffice itself, and the
|
|
679
|
+
// banner above the table states where that is. Refreshing only the
|
|
680
|
+
// table leaves it stating the hostname from before the change.
|
|
681
|
+
refresh: async () => {
|
|
682
|
+
await qc.invalidateQueries({ queryKey: ["admin", "locale-domains"] });
|
|
683
|
+
await qc.invalidateQueries({ queryKey: ["admin", "effective-admin-host"] });
|
|
684
|
+
},
|
|
583
685
|
}}
|
|
584
686
|
/>
|
|
585
687
|
);
|
|
@@ -658,16 +760,16 @@ export function ReviewsRoute() {
|
|
|
658
760
|
return <ReviewsRouteInner />;
|
|
659
761
|
}
|
|
660
762
|
|
|
661
|
-
/** DomainsRoute guarded by the "domains" module switch. */
|
|
763
|
+
/** DomainsRoute guarded by the "domains" module switch and its admin role. */
|
|
662
764
|
export function DomainsRoute() {
|
|
663
|
-
const gate =
|
|
765
|
+
const gate = useModuleGate("domains", "Languages & domains");
|
|
664
766
|
if (gate) return <>{gate}</>;
|
|
665
767
|
return <DomainsRouteInner />;
|
|
666
768
|
}
|
|
667
769
|
|
|
668
|
-
/** TrackingRoute guarded by the "tracking" module switch. */
|
|
770
|
+
/** TrackingRoute guarded by the "tracking" module switch and its admin role. */
|
|
669
771
|
export function TrackingRoute() {
|
|
670
|
-
const gate =
|
|
772
|
+
const gate = useModuleGate("tracking", "Code & tracking");
|
|
671
773
|
if (gate) return <>{gate}</>;
|
|
672
774
|
return <TrackingRouteInner />;
|
|
673
775
|
}
|
|
@@ -107,8 +107,10 @@ export function PageBuilderRoute() {
|
|
|
107
107
|
publicHost: (locale) => resolveUrl(key, locale).host,
|
|
108
108
|
versions: versionsQ.data as any,
|
|
109
109
|
publications: pubsQ.data as any,
|
|
110
|
-
save: async ({ pageId: id, blocks, publish, note }) => {
|
|
111
|
-
const res = await save({
|
|
110
|
+
save: async ({ pageId: id, blocks, publish, note, editSessionId }) => {
|
|
111
|
+
const res = await save({
|
|
112
|
+
data: { pageId: id, blocks, publish, note, editSessionId },
|
|
113
|
+
});
|
|
112
114
|
return res as { versionId: string };
|
|
113
115
|
},
|
|
114
116
|
flushSharedDrafts: pages.flushSharedDrafts
|
|
@@ -120,8 +122,10 @@ export function PageBuilderRoute() {
|
|
|
120
122
|
}
|
|
121
123
|
: undefined,
|
|
122
124
|
schedule: (input) => schedule({ data: input }),
|
|
123
|
-
cancelPublication: (publicationId) =>
|
|
124
|
-
|
|
125
|
+
cancelPublication: (publicationId, editSessionId) =>
|
|
126
|
+
cancelPub({ data: { publicationId, editSessionId } }),
|
|
127
|
+
restoreVersion: (versionId, editSessionId) =>
|
|
128
|
+
restore({ data: { versionId, editSessionId } }),
|
|
125
129
|
rename: (input) => rename({ data: input }),
|
|
126
130
|
refetchPage: () => void pageQ.refetch(),
|
|
127
131
|
refetchVersions: () => void versionsQ.refetch(),
|
|
@@ -190,8 +194,8 @@ export function SectionEditorRoute() {
|
|
|
190
194
|
meta,
|
|
191
195
|
value,
|
|
192
196
|
codeDefault,
|
|
193
|
-
save: async (next) => {
|
|
194
|
-
await save({ data: { key: sectionKey, data: next } });
|
|
197
|
+
save: async (next, editSessionId) => {
|
|
198
|
+
await save({ data: { key: sectionKey, data: next, editSessionId } });
|
|
195
199
|
await queryClient.invalidateQueries({ queryKey: host.content.overridesQueryKey });
|
|
196
200
|
},
|
|
197
201
|
goToList: () => navigate({ to: paths.sections as any }),
|