@bygghammar/cms-platform 0.21.0 → 0.23.0
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 +87 -0
- package/README.md +18 -0
- package/docs/NEW-SITE.md +103 -0
- package/package.json +12 -3
- package/scripts/sync-migrations.mjs +49 -0
- package/src/backoffice/SiteSettingsView.tsx +258 -0
- package/src/config/site-config.ts +236 -0
- package/src/platform/contract.ts +0 -5
- package/src/server/site-settings-core.ts +66 -0
- package/migrations/20260811112455_217c4028-f587-4c98-9e4f-9e6c452ab761.sql +0 -20
- package/migrations/20260811114438_6687d67d-bad3-4803-9638-8b7670d2e5d4.sql +0 -86
- package/migrations/20260811220548_80d99ec2-bd4b-475d-a627-9408bb60ce56.sql +0 -1
- package/migrations/20260811222519_6b040f56-4de3-49e0-92e4-d9deec5ead6a.sql +0 -3
- package/migrations/20260812000000_seed_voyara_content.sql +0 -390
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site configuration contract (Phase 1 of the stand-alone CMS work).
|
|
3
|
+
*
|
|
4
|
+
* A site describes itself once, in code, with `defineSiteConfig`. A subset of
|
|
5
|
+
* those values may be overridden at runtime from the `site_settings` table so
|
|
6
|
+
* non-developers can change them in the backoffice. Shared platform code reads
|
|
7
|
+
* the resolved config instead of hardcoding a site name, locale set, backoffice
|
|
8
|
+
* path or feature set.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Feature modules a site can switch on. Unknown flags are ignored. */
|
|
12
|
+
export type SiteFeatureFlags = {
|
|
13
|
+
/** Trips / departures module (`/kontoret/trips`). */
|
|
14
|
+
trips: boolean;
|
|
15
|
+
/** Courses module (`/kontoret/kurser`). */
|
|
16
|
+
courses: boolean;
|
|
17
|
+
/** Reviews module (`/kontoret/recensioner`). */
|
|
18
|
+
reviews: boolean;
|
|
19
|
+
/** Transactional email + provider settings screens. */
|
|
20
|
+
email: boolean;
|
|
21
|
+
/** Head/body code injection and analytics pixels. */
|
|
22
|
+
tracking: boolean;
|
|
23
|
+
/** Backup & restore tooling (superadmin only). */
|
|
24
|
+
backup: boolean;
|
|
25
|
+
/** Custom domain management screen. */
|
|
26
|
+
domains: boolean;
|
|
27
|
+
/** Public forms module. */
|
|
28
|
+
forms: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_FEATURE_FLAGS: SiteFeatureFlags = {
|
|
32
|
+
trips: false,
|
|
33
|
+
courses: false,
|
|
34
|
+
reviews: false,
|
|
35
|
+
email: true,
|
|
36
|
+
tracking: true,
|
|
37
|
+
backup: true,
|
|
38
|
+
domains: false,
|
|
39
|
+
forms: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Official company / contact details. Locale-neutral by design. */
|
|
43
|
+
export type SiteCompany = {
|
|
44
|
+
legalName: string;
|
|
45
|
+
locationName: string;
|
|
46
|
+
street: string;
|
|
47
|
+
postCode: string;
|
|
48
|
+
city: string;
|
|
49
|
+
country: string;
|
|
50
|
+
orgNumber: string;
|
|
51
|
+
email: string;
|
|
52
|
+
phone: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const EMPTY_COMPANY: SiteCompany = {
|
|
56
|
+
legalName: "",
|
|
57
|
+
locationName: "",
|
|
58
|
+
street: "",
|
|
59
|
+
postCode: "",
|
|
60
|
+
city: "",
|
|
61
|
+
country: "",
|
|
62
|
+
orgNumber: "",
|
|
63
|
+
email: "",
|
|
64
|
+
phone: "",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Brand design tokens. Values are CSS colors written into the theme contract. */
|
|
68
|
+
export type SiteBrandTokens = Record<string, string>;
|
|
69
|
+
|
|
70
|
+
export type SiteConfigInput = {
|
|
71
|
+
/** Machine key, e.g. "voyara". Lowercase, no spaces. */
|
|
72
|
+
key: string;
|
|
73
|
+
/** Display name used in the backoffice and default SEO titles. */
|
|
74
|
+
name: string;
|
|
75
|
+
/** Canonical public origin, without a trailing slash. */
|
|
76
|
+
origin: string;
|
|
77
|
+
/** Locales the public site serves, most important first. */
|
|
78
|
+
locales: readonly string[];
|
|
79
|
+
/** Locale used when nothing else matches. Must be in `locales`. */
|
|
80
|
+
defaultLocale: string;
|
|
81
|
+
/** Path the backoffice is mounted at, e.g. "/kontoret". */
|
|
82
|
+
backofficePath?: string;
|
|
83
|
+
/** UI language of the backoffice itself. */
|
|
84
|
+
backofficeLocale?: string;
|
|
85
|
+
company?: Partial<SiteCompany>;
|
|
86
|
+
brand?: SiteBrandTokens;
|
|
87
|
+
features?: Partial<SiteFeatureFlags>;
|
|
88
|
+
/** Optional free-form per-site extras shared code never reads. */
|
|
89
|
+
extra?: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type SiteConfig = {
|
|
93
|
+
key: string;
|
|
94
|
+
name: string;
|
|
95
|
+
origin: string;
|
|
96
|
+
locales: readonly string[];
|
|
97
|
+
defaultLocale: string;
|
|
98
|
+
backofficePath: string;
|
|
99
|
+
backofficeLocale: string;
|
|
100
|
+
company: SiteCompany;
|
|
101
|
+
brand: SiteBrandTokens;
|
|
102
|
+
features: SiteFeatureFlags;
|
|
103
|
+
extra: Record<string, unknown>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function fail(message: string): never {
|
|
107
|
+
throw new Error(`@bygghammar/cms-platform: invalid site config — ${message}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Validate and normalize a site config. Throws on anything unusable. */
|
|
111
|
+
export function defineSiteConfig(input: SiteConfigInput): SiteConfig {
|
|
112
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(input.key)) fail(`key "${input.key}" must be kebab-case`);
|
|
113
|
+
if (!input.name.trim()) fail("name is required");
|
|
114
|
+
if (!/^https?:\/\/[^/]+$/.test(input.origin)) {
|
|
115
|
+
fail(`origin "${input.origin}" must be an absolute URL without a path`);
|
|
116
|
+
}
|
|
117
|
+
if (!input.locales.length) fail("at least one locale is required");
|
|
118
|
+
if (!input.locales.includes(input.defaultLocale)) {
|
|
119
|
+
fail(`defaultLocale "${input.defaultLocale}" is not listed in locales`);
|
|
120
|
+
}
|
|
121
|
+
const backofficePath = input.backofficePath ?? "/kontoret";
|
|
122
|
+
if (!backofficePath.startsWith("/") || backofficePath.endsWith("/")) {
|
|
123
|
+
fail(`backofficePath "${backofficePath}" must start with "/" and not end with one`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
key: input.key,
|
|
127
|
+
name: input.name.trim(),
|
|
128
|
+
origin: input.origin.replace(/\/$/, ""),
|
|
129
|
+
locales: [...input.locales],
|
|
130
|
+
defaultLocale: input.defaultLocale,
|
|
131
|
+
backofficePath,
|
|
132
|
+
backofficeLocale: input.backofficeLocale ?? "en",
|
|
133
|
+
company: { ...EMPTY_COMPANY, ...(input.company ?? {}) },
|
|
134
|
+
brand: { ...(input.brand ?? {}) },
|
|
135
|
+
features: { ...DEFAULT_FEATURE_FLAGS, ...(input.features ?? {}) },
|
|
136
|
+
extra: { ...(input.extra ?? {}) },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Keys the backoffice may override from the database. Everything else is
|
|
142
|
+
* code-only on purpose: routing, locale set and origin must be deployable and
|
|
143
|
+
* reviewable, not editable at runtime.
|
|
144
|
+
*/
|
|
145
|
+
export const OVERRIDABLE_SETTING_KEYS = [
|
|
146
|
+
"name",
|
|
147
|
+
"company",
|
|
148
|
+
"brand",
|
|
149
|
+
"features",
|
|
150
|
+
] as const satisfies readonly (keyof SiteConfig)[];
|
|
151
|
+
|
|
152
|
+
export type OverridableSettingKey = (typeof OVERRIDABLE_SETTING_KEYS)[number];
|
|
153
|
+
|
|
154
|
+
/** Stored override rows, keyed by `OverridableSettingKey`. */
|
|
155
|
+
export type SiteSettingOverrides = Partial<{
|
|
156
|
+
name: string;
|
|
157
|
+
company: Partial<SiteCompany>;
|
|
158
|
+
brand: SiteBrandTokens;
|
|
159
|
+
features: Partial<SiteFeatureFlags>;
|
|
160
|
+
}>;
|
|
161
|
+
|
|
162
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
163
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Narrow an untrusted settings payload to the overridable subset. */
|
|
167
|
+
export function parseSiteSettingOverrides(raw: unknown): SiteSettingOverrides {
|
|
168
|
+
if (!isRecord(raw)) return {};
|
|
169
|
+
const out: SiteSettingOverrides = {};
|
|
170
|
+
if (typeof raw["name"] === "string" && raw["name"].trim()) out.name = raw["name"].trim();
|
|
171
|
+
if (isRecord(raw["company"])) {
|
|
172
|
+
const company: Partial<SiteCompany> = {};
|
|
173
|
+
for (const key of Object.keys(EMPTY_COMPANY) as (keyof SiteCompany)[]) {
|
|
174
|
+
const value = (raw["company"] as Record<string, unknown>)[key];
|
|
175
|
+
if (typeof value === "string") company[key] = value;
|
|
176
|
+
}
|
|
177
|
+
out.company = company;
|
|
178
|
+
}
|
|
179
|
+
if (isRecord(raw["brand"])) {
|
|
180
|
+
const brand: SiteBrandTokens = {};
|
|
181
|
+
for (const [key, value] of Object.entries(raw["brand"])) {
|
|
182
|
+
if (typeof value === "string") brand[key] = value;
|
|
183
|
+
}
|
|
184
|
+
out.brand = brand;
|
|
185
|
+
}
|
|
186
|
+
if (isRecord(raw["features"])) {
|
|
187
|
+
const features: Partial<SiteFeatureFlags> = {};
|
|
188
|
+
for (const key of Object.keys(DEFAULT_FEATURE_FLAGS) as (keyof SiteFeatureFlags)[]) {
|
|
189
|
+
const value = (raw["features"] as Record<string, unknown>)[key];
|
|
190
|
+
if (typeof value === "boolean") features[key] = value;
|
|
191
|
+
}
|
|
192
|
+
out.features = features;
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Layer database overrides over the code config. Code stays the fallback. */
|
|
198
|
+
export function resolveSiteConfig(base: SiteConfig, overrides: unknown): SiteConfig {
|
|
199
|
+
const parsed = parseSiteSettingOverrides(overrides);
|
|
200
|
+
return {
|
|
201
|
+
...base,
|
|
202
|
+
name: parsed.name ?? base.name,
|
|
203
|
+
company: { ...base.company, ...(parsed.company ?? {}) },
|
|
204
|
+
brand: { ...base.brand, ...(parsed.brand ?? {}) },
|
|
205
|
+
features: { ...base.features, ...(parsed.features ?? {}) },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** True when a feature module is enabled for the resolved config. */
|
|
210
|
+
export function isFeatureEnabled(config: SiteConfig, flag: keyof SiteFeatureFlags): boolean {
|
|
211
|
+
return config.features[flag] === true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Build a backoffice URL from the configured mount path. */
|
|
215
|
+
export function backofficeUrl(config: SiteConfig, subpath = ""): string {
|
|
216
|
+
if (!subpath) return config.backofficePath;
|
|
217
|
+
return `${config.backofficePath}/${subpath.replace(/^\//, "")}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let active: SiteConfig | null = null;
|
|
221
|
+
|
|
222
|
+
/** Register the resolved site config once at startup. */
|
|
223
|
+
export function registerSiteConfig(config: SiteConfig): SiteConfig {
|
|
224
|
+
active = config;
|
|
225
|
+
return config;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Read the registered site config. Throws when a site forgot to register. */
|
|
229
|
+
export function siteConfig(): SiteConfig {
|
|
230
|
+
if (!active) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
"@bygghammar/cms-platform: no site config registered. Call registerSiteConfig(defineSiteConfig({...})) during startup.",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return active;
|
|
236
|
+
}
|
package/src/platform/contract.ts
CHANGED
|
@@ -67,12 +67,7 @@ export const SCHEMA_MIGRATIONS: readonly string[] = [
|
|
|
67
67
|
"20260803201518_6e8a6028-b70f-4a7d-8bc9-95c86304e7ae.sql",
|
|
68
68
|
"20260803201535_32a9a213-cb92-4035-b8cd-8211ca34af68.sql",
|
|
69
69
|
"20260811103505_29df753d-14fa-4a05-8fea-f4207785fba3.sql",
|
|
70
|
-
"20260811112455_217c4028-f587-4c98-9e4f-9e6c452ab761.sql",
|
|
71
|
-
"20260811114438_6687d67d-bad3-4803-9638-8b7670d2e5d4.sql",
|
|
72
70
|
"20260811130318_7cb95e95-9baa-40af-8f3f-de1d4e09cec4.sql",
|
|
73
|
-
"20260811220548_80d99ec2-bd4b-475d-a627-9408bb60ce56.sql",
|
|
74
|
-
"20260811222519_6b040f56-4de3-49e0-92e4-d9deec5ead6a.sql",
|
|
75
|
-
"20260812000000_seed_voyara_content.sql",
|
|
76
71
|
"20260818040722_1bdf1b8f-7fb0-4ae8-8ecf-73f1fadff3c2.sql",
|
|
77
72
|
"20260818040808_69a5e832-57e8-4aa7-aff8-7fd7777faa7c.sql",
|
|
78
73
|
"20260818040839_e6523163-bbf5-4a39-9233-91759059569a.sql",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic core for the `site_settings` key/value store.
|
|
3
|
+
*
|
|
4
|
+
* Rows are `{ key, value jsonb }` where `key` is one of
|
|
5
|
+
* `OVERRIDABLE_SETTING_KEYS`. Reads use whatever client the host passes
|
|
6
|
+
* (publishable client for the public site, authenticated client in the
|
|
7
|
+
* backoffice); writes always run as the signed-in staff user so RLS applies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
OVERRIDABLE_SETTING_KEYS,
|
|
12
|
+
parseSiteSettingOverrides,
|
|
13
|
+
type OverridableSettingKey,
|
|
14
|
+
type SiteSettingOverrides,
|
|
15
|
+
} from "../config/site-config";
|
|
16
|
+
|
|
17
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
18
|
+
type Client = {
|
|
19
|
+
from: (table: string) => any;
|
|
20
|
+
};
|
|
21
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
22
|
+
|
|
23
|
+
export const SITE_SETTINGS_TABLE = "site_settings";
|
|
24
|
+
|
|
25
|
+
/** Load every stored override, narrowed to the overridable subset. */
|
|
26
|
+
export async function loadSiteSettingsCore(client: Client): Promise<SiteSettingOverrides> {
|
|
27
|
+
const { data, error } = await client.from(SITE_SETTINGS_TABLE).select("key, value");
|
|
28
|
+
if (error) throw new Error(error.message);
|
|
29
|
+
const raw: Record<string, unknown> = {};
|
|
30
|
+
for (const row of (data ?? []) as Array<{ key: string; value: unknown }>) {
|
|
31
|
+
if ((OVERRIDABLE_SETTING_KEYS as readonly string[]).includes(row.key)) raw[row.key] = row.value;
|
|
32
|
+
}
|
|
33
|
+
return parseSiteSettingOverrides(raw);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Upsert the provided override keys. Keys absent from `patch` are untouched, so
|
|
38
|
+
* a screen can save one section without clobbering another.
|
|
39
|
+
*/
|
|
40
|
+
export async function saveSiteSettingsCore(
|
|
41
|
+
client: Client,
|
|
42
|
+
patch: SiteSettingOverrides,
|
|
43
|
+
actorId?: string | null,
|
|
44
|
+
): Promise<SiteSettingOverrides> {
|
|
45
|
+
const clean = parseSiteSettingOverrides(patch);
|
|
46
|
+
const rows = (Object.keys(clean) as OverridableSettingKey[]).map((key) => ({
|
|
47
|
+
key,
|
|
48
|
+
value: clean[key] as unknown,
|
|
49
|
+
updated_by: actorId ?? null,
|
|
50
|
+
updated_at: new Date().toISOString(),
|
|
51
|
+
}));
|
|
52
|
+
if (rows.length) {
|
|
53
|
+
const { error } = await client.from(SITE_SETTINGS_TABLE).upsert(rows, { onConflict: "key" });
|
|
54
|
+
if (error) throw new Error(error.message);
|
|
55
|
+
}
|
|
56
|
+
return loadSiteSettingsCore(client);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Remove one override so the code default takes over again. */
|
|
60
|
+
export async function clearSiteSettingCore(
|
|
61
|
+
client: Client,
|
|
62
|
+
key: OverridableSettingKey,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
const { error } = await client.from(SITE_SETTINGS_TABLE).delete().eq("key", key);
|
|
65
|
+
if (error) throw new Error(error.message);
|
|
66
|
+
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
UPDATE public.page_seo SET seo = jsonb_set(seo, '{title,sv}', to_jsonb(v.title)) FROM (VALUES
|
|
2
|
-
('home','Nordens största partykryssning och festkryssning | Voyara'),
|
|
3
|
-
('artister','Artister på vinterns hetaste partykryssning · Voyara'),
|
|
4
|
-
('hytter','Hytter ombord · Voyara partykryssning'),
|
|
5
|
-
('mat-och-dryck','Mat och dryck på Voyara · Partykryssning'),
|
|
6
|
-
('tallinn','Tallinn · Voyara partykryssning'),
|
|
7
|
-
('sakerhet','Säkerhet och en trygg miljö · Voyara'),
|
|
8
|
-
('galleri','Galleri · Voyara partykryssning'),
|
|
9
|
-
('faq','FAQ · Voyara partykryssning'),
|
|
10
|
-
('boka','Boka nu · Voyara partykryssning'),
|
|
11
|
-
('samarbete','Samarbete · Voyara partykryssning'),
|
|
12
|
-
('ambassador','Bli ambassadör · Voyara partykryssning'),
|
|
13
|
-
('kontakt','Kontakta oss · Voyara'),
|
|
14
|
-
('intresse','Intresseanmälan · Voyara partykryssning')
|
|
15
|
-
) AS v(page_key, title) WHERE public.page_seo.page_key = v.page_key;
|
|
16
|
-
|
|
17
|
-
UPDATE public.page_seo SET seo = jsonb_set(seo, '{description,sv}', to_jsonb(v.descr)) FROM (VALUES
|
|
18
|
-
('home','Välkommen till en festkryssning och partykryssning utöver det vanliga. Artister, DJ:s och shower varje kväll — studenter och alla är välkomna.'),
|
|
19
|
-
('artister','Välkommen till partykryssningen med de hetaste artisterna. Ett unikt nöjeskoncept i två dagar med uppträdanden båda kvällarna.')
|
|
20
|
-
) AS v(page_key, descr) WHERE public.page_seo.page_key = v.page_key;
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
begin;
|
|
2
|
-
|
|
3
|
-
with p as (select id from public.pages where key = 'home'),
|
|
4
|
-
cur as (
|
|
5
|
-
select v.blocks from public.page_versions v
|
|
6
|
-
join public.pages pg on pg.current_version_id = v.id
|
|
7
|
-
where pg.key = 'home'
|
|
8
|
-
),
|
|
9
|
-
tiles as (
|
|
10
|
-
select jsonb_build_object(
|
|
11
|
-
'id', 'blk-link-tiles-home',
|
|
12
|
-
'type', 'linkTiles',
|
|
13
|
-
'name', 'Boka / Elevkårer / Ambassadör',
|
|
14
|
-
'props', jsonb_build_object(
|
|
15
|
-
'tone', 'white',
|
|
16
|
-
'columns', 3,
|
|
17
|
-
'gap', 'md',
|
|
18
|
-
'aspect', 'portrait',
|
|
19
|
-
'overlay', 'brand',
|
|
20
|
-
'labelAlign', 'left',
|
|
21
|
-
'rounded', false,
|
|
22
|
-
'items', jsonb_build_array(
|
|
23
|
-
jsonb_build_object(
|
|
24
|
-
'id', 'tile-boka',
|
|
25
|
-
'image', 'https://ybnqylupuisyhuldqrgt.supabase.co/storage/v1/object/public/media/boka-nu.jpg',
|
|
26
|
-
'label', jsonb_build_object('sv', 'Boka nu'),
|
|
27
|
-
'alt', jsonb_build_object('sv', 'Publik framför scenen på Voyara partykryssning'),
|
|
28
|
-
'href', '/boka'
|
|
29
|
-
),
|
|
30
|
-
jsonb_build_object(
|
|
31
|
-
'id', 'tile-elevkarer',
|
|
32
|
-
'image', 'https://ybnqylupuisyhuldqrgt.supabase.co/storage/v1/object/public/media/artister-scen.jpg',
|
|
33
|
-
'label', jsonb_build_object('sv', 'För elevkårer'),
|
|
34
|
-
'alt', jsonb_build_object('sv', 'Artist på scen ombord'),
|
|
35
|
-
'href', '/samarbete'
|
|
36
|
-
),
|
|
37
|
-
jsonb_build_object(
|
|
38
|
-
'id', 'tile-ambassador',
|
|
39
|
-
'image', 'https://ybnqylupuisyhuldqrgt.supabase.co/storage/v1/object/public/media/voyara-publik.jpg',
|
|
40
|
-
'label', jsonb_build_object('sv', 'Bli ambassadör'),
|
|
41
|
-
'alt', jsonb_build_object('sv', 'Gäster som festar ombord'),
|
|
42
|
-
'href', '/ambassador'
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
)
|
|
46
|
-
) as blk
|
|
47
|
-
),
|
|
48
|
-
newblocks as (
|
|
49
|
-
select (
|
|
50
|
-
select coalesce(jsonb_agg(x.b order by x.idx), '[]'::jsonb)
|
|
51
|
-
from (
|
|
52
|
-
select b, idx::numeric as idx from cur, lateral jsonb_array_elements(cur.blocks) with ordinality t(b, idx)
|
|
53
|
-
where b->>'type' <> 'linkTiles'
|
|
54
|
-
union all
|
|
55
|
-
select (select blk from tiles),
|
|
56
|
-
coalesce((
|
|
57
|
-
select max(idx) + 0.5 from cur, lateral jsonb_array_elements(cur.blocks) with ordinality t(b, idx)
|
|
58
|
-
where b->>'type' = 'courseList'
|
|
59
|
-
), 999)
|
|
60
|
-
) x
|
|
61
|
-
) as blocks
|
|
62
|
-
),
|
|
63
|
-
ins as (
|
|
64
|
-
insert into public.page_versions (page_id, version_no, blocks, note, created_by_email)
|
|
65
|
-
select p.id,
|
|
66
|
-
coalesce((select max(version_no) from public.page_versions pv where pv.page_id = p.id), 0) + 1,
|
|
67
|
-
(select blocks from newblocks),
|
|
68
|
-
'Add link tiles row (Boka / Elevkårer / Ambassadör)',
|
|
69
|
-
'system'
|
|
70
|
-
from p
|
|
71
|
-
returning id, page_id
|
|
72
|
-
),
|
|
73
|
-
deact as (
|
|
74
|
-
update public.page_publications pp set is_active = false
|
|
75
|
-
where pp.page_id = (select id from p) and pp.is_active
|
|
76
|
-
returning 1
|
|
77
|
-
),
|
|
78
|
-
pub as (
|
|
79
|
-
insert into public.page_publications (page_id, version_id, note)
|
|
80
|
-
select page_id, id, 'Add link tiles row' from ins
|
|
81
|
-
returning page_id, version_id
|
|
82
|
-
)
|
|
83
|
-
update public.pages pg set current_version_id = pub.version_id, updated_at = now()
|
|
84
|
-
from pub where pg.id = pub.page_id;
|
|
85
|
-
|
|
86
|
-
commit;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
UPDATE public.locale_domains SET domain = 'byggis.voyara.se', updated_at = now() WHERE domain = 'www.dev.barskola.nu';
|