@aduptive/strapi-blockscene 2.0.0-alpha.4 → 2.0.0-alpha.6
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/README.md +410 -24
- package/dist/admin.cjs +5091 -987
- package/dist/admin.mjs +5098 -994
- package/dist/server.cjs +859 -36
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -118,6 +118,7 @@ var require_settings = __commonJS({
|
|
|
118
118
|
var TEMPLATES2 = ["generic", "banner", "cards", "imageText", "faq", "none"];
|
|
119
119
|
var INITIAL_STATES = ["closed", "open", "remember"];
|
|
120
120
|
var PREVIEW_MODES = ["form", "split", "preview"];
|
|
121
|
+
var HIDDEN_MODES = ["strip", "flag", "off"];
|
|
121
122
|
var DEFAULTS2 = {
|
|
122
123
|
palette: { background: "#F6F6F9", surface: "#DCDCE4", text: "#32324D", accent: "#4945FF" },
|
|
123
124
|
components: {},
|
|
@@ -125,11 +126,102 @@ var require_settings = __commonJS({
|
|
|
125
126
|
// blockPreviewInForm: compact read-only preview above a block's fields in form
|
|
126
127
|
// mode. Only installations that ship the accordion integration (server config
|
|
127
128
|
// `blockPreview: true`) show and honour it; the page preview is the editor.
|
|
128
|
-
editor: {
|
|
129
|
+
editor: {
|
|
130
|
+
enabled: true,
|
|
131
|
+
showOpenAll: true,
|
|
132
|
+
showCloseAll: true,
|
|
133
|
+
showRowThumbnails: true,
|
|
134
|
+
initialState: "closed",
|
|
135
|
+
previewMode: "form",
|
|
136
|
+
previewUrl: "",
|
|
137
|
+
blockPreviewUrl: "",
|
|
138
|
+
blockPreviewInForm: false,
|
|
139
|
+
// Strapi 5: rich-text custom fields render a read-only preview until clicked (see admin/LazyInput.tsx).
|
|
140
|
+
lazyEditors: true,
|
|
141
|
+
lazyFields: ["plugin::ckeditor5.CKEditor"],
|
|
142
|
+
// Row actions in each block header (see README "Row actions"). hiddenBlocks: what the content API does with rows
|
|
143
|
+
// hidden on the site: 'strip' removes them, 'flag' sends them with the attribute, 'off' hides the eye icon.
|
|
144
|
+
confirmDelete: true,
|
|
145
|
+
hiddenBlocks: "strip",
|
|
146
|
+
duplicate: true,
|
|
147
|
+
clipboard: true,
|
|
148
|
+
// Edit view labels that are still the raw attribute name read as "Mobile columns count" (see README "Field labels").
|
|
149
|
+
friendlyLabels: true
|
|
150
|
+
},
|
|
151
|
+
// Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
|
|
152
|
+
// previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
|
|
153
|
+
contentTypes: {},
|
|
154
|
+
// Version history (Strapi 5, see README "Version history"): off until turned on. contentTypes: 'all' (every api:: type)
|
|
155
|
+
// or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays.
|
|
156
|
+
history: { enabled: false, contentTypes: "all", retentionDays: 90, maxSnapshots: 100, eventDays: 365 }
|
|
157
|
+
};
|
|
158
|
+
var ICONS = ["text", "tag", "seo", "settings", "image", "link", "palette", "list", "globe", "info"];
|
|
159
|
+
var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
|
|
160
|
+
var OPENS = ["modal", "drawer"];
|
|
161
|
+
var LABEL = /^[^<>]{1,40}$/;
|
|
162
|
+
var sidebarItem = (item, attributes) => item && typeof item === "object" && !Array.isArray(item) && Object.keys(item).every((key) => ["label", "icon", "open", "fields"].includes(key)) && typeof item.label === "string" && LABEL.test(item.label.trim()) && (item.icon === void 0 || item.icon === "" || ICONS.includes(item.icon)) && OPENS.includes(item.open) && Array.isArray(item.fields) && item.fields.length > 0 && item.fields.length <= 30 && item.fields.every((field) => typeof field === "string" && (!attributes || attributes.includes(field)));
|
|
163
|
+
var TYPE_KEYS = {
|
|
164
|
+
enabled: (value) => typeof value === "boolean",
|
|
165
|
+
previewMode: (value) => PREVIEW_MODES.includes(value),
|
|
166
|
+
sidebarPosition: (value) => SIDEBAR_POSITIONS.includes(value),
|
|
167
|
+
sidebar: (value, attributes) => Array.isArray(value) && value.length <= 12 && value.every((item) => sidebarItem(item, attributes))
|
|
129
168
|
};
|
|
169
|
+
var typeMap = (types) => Array.isArray(types) ? Object.fromEntries(types.map((uid) => [uid, null])) : types || {};
|
|
130
170
|
var PREVIEW_URL = /^https?:\/\/[^\s"'<>]{1,500}$/;
|
|
171
|
+
var blockPreviewUrl = (value) => typeof value === "string" && (value === "" || PREVIEW_URL.test(value) && !/[{}]/.test(value.replace(/\{(uid|name|category|variant|locale)\}/g, "")) && URL.canParse?.(value.replace(/[{}]/g, "")) !== false);
|
|
131
172
|
var COLOR = /^#[0-9A-Fa-f]{6}$/;
|
|
173
|
+
var FIELD_UID = /^(plugin|global)::[\w.-]+$/;
|
|
174
|
+
var lazyFields = (value) => Array.isArray(value) && value.length <= 20 && value.every((uid) => typeof uid === "string" && FIELD_UID.test(uid));
|
|
132
175
|
var UID = /^[a-z0-9-]+\.[a-z0-9-]+$/;
|
|
176
|
+
var API_UID = /^api::[\w-]+\.[\w-]+$/;
|
|
177
|
+
var days = (value) => Number.isInteger(value) && value >= 1 && value <= 3650;
|
|
178
|
+
var HISTORY_KEYS = {
|
|
179
|
+
enabled: (value) => typeof value === "boolean",
|
|
180
|
+
contentTypes: (value, uids) => value === "all" || Array.isArray(value) && value.length <= 500 && value.every((uid) => typeof uid === "string" && API_UID.test(uid) && (!uids || uids.includes(uid))),
|
|
181
|
+
retentionDays: days,
|
|
182
|
+
maxSnapshots: (value) => Number.isInteger(value) && value >= 1 && value <= 1e3,
|
|
183
|
+
eventDays: days
|
|
184
|
+
};
|
|
185
|
+
var TYPOLOGIES2 = ["hero", "text", "media", "listing", "cards", "cta", "form", "layout"];
|
|
186
|
+
var TAG = /^[^<>\n]{1,24}$/;
|
|
187
|
+
var cleanTags = (value) => Array.isArray(value) ? [...new Set(value.filter((tag) => typeof tag === "string" && TAG.test(tag.trim())).map((tag) => tag.trim()))].slice(0, 10) : [];
|
|
188
|
+
var validTags = (value) => Array.isArray(value) && value.length <= 10 && value.every((tag) => typeof tag === "string" && TAG.test(tag.trim()));
|
|
189
|
+
var GUESSES = [
|
|
190
|
+
["hero", /hero|banner|cover/],
|
|
191
|
+
["text", /text|rich|quote|title/],
|
|
192
|
+
["media", /image|media|video|gallery|carousel/],
|
|
193
|
+
["listing", /list|query|archive|related|posts|projects/],
|
|
194
|
+
["cards", /card/],
|
|
195
|
+
["cta", /cta|button|link/],
|
|
196
|
+
["form", /form|contact/],
|
|
197
|
+
["layout", /wrapper|column|grid|divider|divisor|spacer|section/]
|
|
198
|
+
];
|
|
199
|
+
function guessTypology(uid, displayName = "") {
|
|
200
|
+
const name = `${uid.split(".").pop()} ${displayName}`.toLowerCase();
|
|
201
|
+
return GUESSES.find(([, pattern]) => pattern.test(name))?.[0] || "text";
|
|
202
|
+
}
|
|
203
|
+
function facetsOf(uid, schema, schemas = {}) {
|
|
204
|
+
const facets = /* @__PURE__ */ new Set();
|
|
205
|
+
const read = (attributes, nested) => {
|
|
206
|
+
for (const attr of Object.values(attributes || {})) {
|
|
207
|
+
if (attr?.type === "media") {
|
|
208
|
+
const allowed = attr.allowedTypes;
|
|
209
|
+
if (!allowed || allowed.includes("images")) facets.add("image");
|
|
210
|
+
if (!allowed || allowed.includes("videos")) facets.add("video");
|
|
211
|
+
if (attr.multiple) facets.add("gallery");
|
|
212
|
+
} else if (["richtext", "blocks"].includes(attr?.type) || attr?.type === "customField" && /ckeditor/i.test(String(attr.customField))) facets.add("richtext");
|
|
213
|
+
else if (attr?.type === "relation") facets.add("dynamic");
|
|
214
|
+
else if (attr?.type === "component") {
|
|
215
|
+
if (attr.repeatable) facets.add("list");
|
|
216
|
+
if (!nested) read(schemas[attr.component]?.attributes, true);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
read(schema?.attributes, false);
|
|
221
|
+
if (/query|archive|related/i.test(uid)) facets.add("dynamic");
|
|
222
|
+
if (/form|contact/i.test(`${uid} ${schema?.info?.displayName || ""}`)) facets.add("form");
|
|
223
|
+
return [...facets];
|
|
224
|
+
}
|
|
133
225
|
var safeUrl2 = (value) => typeof value === "string" && (/^\/(?!\/)/.test(value) || /^https?:\/\//.test(value)) ? value : void 0;
|
|
134
226
|
var { safeGroups: safeGroups2 } = require_groups();
|
|
135
227
|
var safeVersion = (value) => typeof value === "string" && /^[\w.-]{1,32}$/.test(value) ? value : void 0;
|
|
@@ -156,6 +248,15 @@ var require_settings = __commonJS({
|
|
|
156
248
|
["label", "description", "category", "keywords"].filter((key) => typeof entry[key] === "string").map((key) => [key, entry[key]])
|
|
157
249
|
);
|
|
158
250
|
entries[uid].image = safeUrl2(entry.image);
|
|
251
|
+
if (TYPOLOGIES2.includes(entry.typology)) entries[uid].typology = entry.typology;
|
|
252
|
+
const tags = cleanTags(entry.tags);
|
|
253
|
+
if (tags.length || entries[uid].category) entries[uid].tags = tags.length ? tags : cleanTags([entries[uid].category]);
|
|
254
|
+
}
|
|
255
|
+
for (const [uid, schema] of Object.entries(config.schemas || {})) {
|
|
256
|
+
const entry = entries[uid] ||= {};
|
|
257
|
+
entry.facets = facetsOf(uid, schema, config.schemas);
|
|
258
|
+
entry.typology ||= guessTypology(uid, schema?.info?.displayName);
|
|
259
|
+
entry.tags ||= [];
|
|
159
260
|
}
|
|
160
261
|
return {
|
|
161
262
|
components: entries,
|
|
@@ -167,7 +268,8 @@ var require_settings = __commonJS({
|
|
|
167
268
|
groups: safeGroups2(config.groups, config.componentUids)
|
|
168
269
|
};
|
|
169
270
|
}
|
|
170
|
-
function validateSettings2(input, componentUids2) {
|
|
271
|
+
function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null) {
|
|
272
|
+
const types = typeMap(contentTypeUids2);
|
|
171
273
|
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
|
|
172
274
|
if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
|
|
173
275
|
const out = structuredClone(DEFAULTS2);
|
|
@@ -179,9 +281,9 @@ var require_settings = __commonJS({
|
|
|
179
281
|
}
|
|
180
282
|
for (const [key, value] of Object.entries(input.editor || {})) {
|
|
181
283
|
if (!(key in DEFAULTS2.editor)) fail2(`Unknown editor option "${key}"`);
|
|
182
|
-
const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : typeof value === "boolean";
|
|
284
|
+
const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "hiddenBlocks" ? HIDDEN_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : key === "blockPreviewUrl" ? blockPreviewUrl(value) : key === "lazyFields" ? lazyFields(value) : typeof value === "boolean";
|
|
183
285
|
if (!valid) fail2(`Invalid value for editor option "${key}"`);
|
|
184
|
-
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : value;
|
|
286
|
+
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : value;
|
|
185
287
|
}
|
|
186
288
|
const components = input.components || {};
|
|
187
289
|
if (typeof components !== "object" || Array.isArray(components)) fail2("components must be an object");
|
|
@@ -193,6 +295,12 @@ var require_settings = __commonJS({
|
|
|
193
295
|
if (key === "template") {
|
|
194
296
|
if (!TEMPLATES2.includes(entry.template)) fail2(`Unknown template for "${uid}"`);
|
|
195
297
|
clean.template = entry.template;
|
|
298
|
+
} else if (key === "typology") {
|
|
299
|
+
if (!TYPOLOGIES2.includes(entry.typology)) fail2(`Unknown typology for "${uid}"`);
|
|
300
|
+
clean.typology = entry.typology;
|
|
301
|
+
} else if (key === "tags") {
|
|
302
|
+
if (!validTags(entry.tags)) fail2(`Invalid tags for "${uid}": up to 10, each 1 to 24 characters`);
|
|
303
|
+
clean.tags = cleanTags(entry.tags);
|
|
196
304
|
} else if (key === "mediaId") {
|
|
197
305
|
if (!Number.isInteger(entry.mediaId) || entry.mediaId <= 0) fail2(`Invalid media for "${uid}"`);
|
|
198
306
|
clean.mediaId = entry.mediaId;
|
|
@@ -200,9 +308,29 @@ var require_settings = __commonJS({
|
|
|
200
308
|
}
|
|
201
309
|
if (Object.keys(clean).length) out.components[uid] = clean;
|
|
202
310
|
}
|
|
311
|
+
const perType = input.contentTypes || {};
|
|
312
|
+
if (typeof perType !== "object" || Array.isArray(perType)) fail2("contentTypes must be an object");
|
|
313
|
+
for (const [uid, entry] of Object.entries(perType)) {
|
|
314
|
+
if (!(uid in types)) fail2(`Unknown content type "${uid}"`);
|
|
315
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) fail2(`Invalid entry for "${uid}"`);
|
|
316
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
317
|
+
if (!(key in TYPE_KEYS)) fail2(`Unknown content type setting "${key}"`);
|
|
318
|
+
if (!TYPE_KEYS[key](value, types[uid])) fail2(`Invalid value for "${key}" of "${uid}"`);
|
|
319
|
+
}
|
|
320
|
+
if (Object.keys(entry).length) out.contentTypes[uid] = structuredClone(entry);
|
|
321
|
+
}
|
|
322
|
+
const history = input.history || {};
|
|
323
|
+
if (typeof history !== "object" || Array.isArray(history)) fail2("history must be an object");
|
|
324
|
+
for (const [key, value] of Object.entries(history)) {
|
|
325
|
+
if (!(key in HISTORY_KEYS)) fail2(`Unknown history option "${key}"`);
|
|
326
|
+
if (!HISTORY_KEYS[key](value, historyTypes)) fail2(`Invalid value for history option "${key}"`);
|
|
327
|
+
out.history[key] = Array.isArray(value) ? [...new Set(value)] : value;
|
|
328
|
+
}
|
|
329
|
+
if (out.history.eventDays < out.history.retentionDays) fail2("history.eventDays must be at least history.retentionDays");
|
|
203
330
|
return out;
|
|
204
331
|
}
|
|
205
|
-
function mergeSaved2(saved, componentUids2) {
|
|
332
|
+
function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null) {
|
|
333
|
+
const types = typeMap(contentTypeUids2);
|
|
206
334
|
const out = structuredClone(DEFAULTS2);
|
|
207
335
|
if (!saved || typeof saved !== "object") return out;
|
|
208
336
|
for (const key of Object.keys(DEFAULTS2.palette)) if (COLOR.test(String(saved.palette?.[key]))) out.palette[key] = saved.palette[key];
|
|
@@ -211,19 +339,590 @@ var require_settings = __commonJS({
|
|
|
211
339
|
if (value === void 0 || typeof value !== typeof DEFAULTS2.editor[key]) continue;
|
|
212
340
|
if (key === "initialState" && !INITIAL_STATES.includes(value)) continue;
|
|
213
341
|
if (key === "previewMode" && !PREVIEW_MODES.includes(value)) continue;
|
|
342
|
+
if (key === "hiddenBlocks" && !HIDDEN_MODES.includes(value)) continue;
|
|
214
343
|
if (key === "previewUrl" && value && !PREVIEW_URL.test(value)) continue;
|
|
344
|
+
if (key === "blockPreviewUrl" && !blockPreviewUrl(value)) continue;
|
|
345
|
+
if (key === "lazyFields") {
|
|
346
|
+
if (!Array.isArray(value)) continue;
|
|
347
|
+
out.editor[key] = [...new Set(value.filter((uid) => lazyFields([uid])))].slice(0, 20);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
215
350
|
out.editor[key] = value;
|
|
216
351
|
}
|
|
217
|
-
for (const [uid, entry] of Object.entries(saved.components || {}))
|
|
352
|
+
for (const [uid, entry] of Object.entries(saved.components || {})) {
|
|
353
|
+
if (!componentUids2.includes(uid) || !entry || typeof entry !== "object") continue;
|
|
354
|
+
const clean = { ...entry };
|
|
355
|
+
if ("typology" in clean && !TYPOLOGIES2.includes(clean.typology)) delete clean.typology;
|
|
356
|
+
if ("tags" in clean) {
|
|
357
|
+
clean.tags = cleanTags(clean.tags);
|
|
358
|
+
if (!clean.tags.length) delete clean.tags;
|
|
359
|
+
}
|
|
360
|
+
if (Object.keys(clean).length) out.components[uid] = clean;
|
|
361
|
+
}
|
|
362
|
+
for (const [uid, entry] of Object.entries(saved.contentTypes || {})) {
|
|
363
|
+
if (!(uid in types) || !entry || typeof entry !== "object") continue;
|
|
364
|
+
const fix = (key, value) => key === "sidebar" && Array.isArray(value) ? value.filter((item) => sidebarItem(item, types[uid])) : value;
|
|
365
|
+
const clean = Object.fromEntries(Object.entries(entry).map(([key, value]) => [key, fix(key, value)]).filter(([key, value]) => TYPE_KEYS[key]?.(value, types[uid])));
|
|
366
|
+
if (Object.keys(clean).length) out.contentTypes[uid] = clean;
|
|
367
|
+
}
|
|
368
|
+
for (const key of Object.keys(HISTORY_KEYS)) {
|
|
369
|
+
let value = saved.history?.[key];
|
|
370
|
+
if (key === "contentTypes" && Array.isArray(value)) value = value.filter((uid) => HISTORY_KEYS.contentTypes([uid], historyTypes));
|
|
371
|
+
if (value !== void 0 && HISTORY_KEYS[key](value, historyTypes)) out.history[key] = value;
|
|
372
|
+
}
|
|
373
|
+
out.history.eventDays = Math.max(out.history.eventDays, out.history.retentionDays);
|
|
218
374
|
return out;
|
|
219
375
|
}
|
|
220
|
-
|
|
376
|
+
var ENTRY_KEYS = ["components", "contentTypes"];
|
|
377
|
+
function layer2(base, saved) {
|
|
378
|
+
if (!base) return saved;
|
|
379
|
+
const doc = saved && typeof saved === "object" ? saved : {};
|
|
380
|
+
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => [key, { ...base[key], ...doc[key] && typeof doc[key] === "object" ? doc[key] : {} }]));
|
|
381
|
+
}
|
|
382
|
+
function overrides2(next, base) {
|
|
383
|
+
if (!base) return next;
|
|
384
|
+
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => {
|
|
385
|
+
const entries = Object.entries(next[key] || {}).filter(([name, value]) => JSON.stringify(value) !== JSON.stringify(base[key]?.[name]));
|
|
386
|
+
if (ENTRY_KEYS.includes(key)) {
|
|
387
|
+
for (const uid of Object.keys(base[key] || {})) if (!(uid in (next[key] || {}))) entries.push([uid, {}]);
|
|
388
|
+
}
|
|
389
|
+
return [key, Object.fromEntries(entries)];
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
var FIELD_TEXT = { label: 80, description: 300, placeholder: 120 };
|
|
393
|
+
var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
394
|
+
var plain = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
395
|
+
var fieldText = (value, max) => typeof value === "string" ? value.trim() !== "" && value.length <= max : plain(value) && Object.keys(value).length > 0 && Object.keys(value).length <= 30 && Object.entries(value).every(([locale, text]) => LOCALE.test(locale) && fieldText(text, max));
|
|
396
|
+
function validateFields2(input, schemas = {}, stale = []) {
|
|
397
|
+
if (input === null || input === void 0) return {};
|
|
398
|
+
if (!plain(input)) fail2("fields must be an object");
|
|
399
|
+
if (JSON.stringify(input).length > 256 * 1024) fail2("fields config too large");
|
|
400
|
+
const out = {};
|
|
401
|
+
for (const [uid, entries] of Object.entries(input)) {
|
|
402
|
+
if (!plain(entries)) fail2(`Invalid entry for "${uid}"`);
|
|
403
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
404
|
+
if (!plain(entry)) fail2(`Invalid entry for "${uid}.${name}"`);
|
|
405
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
406
|
+
if (!(key in FIELD_TEXT)) fail2(`Unknown field setting "${key}" of "${uid}.${name}"`);
|
|
407
|
+
if (!fieldText(value, FIELD_TEXT[key])) fail2(`Invalid ${key} of "${uid}.${name}": a string of 1 to ${FIELD_TEXT[key]} characters, or { "<locale>": string }`);
|
|
408
|
+
}
|
|
409
|
+
if (!schemas[uid]?.includes(name)) {
|
|
410
|
+
stale.push(`${uid}.${name}`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
;
|
|
414
|
+
(out[uid] ||= {})[name] = structuredClone(entry);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
var PREFS = { starred: 200, recent: 20 };
|
|
420
|
+
function validatePrefs2(input, componentUids2) {
|
|
421
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
|
|
422
|
+
const out = { starred: [], recent: [] };
|
|
423
|
+
for (const [key, value] of Object.entries(input)) {
|
|
424
|
+
if (!(key in PREFS)) fail2(`Unknown preference "${key}"`);
|
|
425
|
+
if (!Array.isArray(value) || value.length > PREFS[key] || !value.every((uid) => typeof uid === "string" && componentUids2.includes(uid))) fail2(`Invalid value for "${key}"`);
|
|
426
|
+
out[key] = [...new Set(value)];
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
var mergePrefs2 = (saved, componentUids2) => Object.fromEntries(Object.entries(PREFS).map(([key, max]) => [key, Array.isArray(saved?.[key]) ? [...new Set(saved[key].filter((uid) => componentUids2.includes(uid)))].slice(0, max) : []]));
|
|
431
|
+
module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, validateFields: validateFields2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// server/hidden.js
|
|
436
|
+
var require_hidden = __commonJS({
|
|
437
|
+
"server/hidden.js"(exports2, module2) {
|
|
438
|
+
"use strict";
|
|
439
|
+
var NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,39}$/;
|
|
440
|
+
var DEFAULT_NAME = "bsHidden";
|
|
441
|
+
var hiddenName2 = (value) => value === void 0 ? DEFAULT_NAME : value === false ? false : typeof value === "string" && NAME.test(value) ? value : null;
|
|
442
|
+
var zoneComponents = (contentTypes) => [...new Set(Object.values(contentTypes || {}).flatMap((schema) => Object.values(schema?.attributes || {}).filter((attr) => attr?.type === "dynamiczone").flatMap((attr) => attr.components || [])))];
|
|
443
|
+
function injectHidden2(components, contentTypes, name) {
|
|
444
|
+
const added = [], skipped = [];
|
|
445
|
+
for (const uid of zoneComponents(contentTypes)) {
|
|
446
|
+
const schema = components?.[uid];
|
|
447
|
+
if (!schema) continue;
|
|
448
|
+
schema.attributes ||= {};
|
|
449
|
+
const current = schema.attributes[name];
|
|
450
|
+
if (current && current.type !== "boolean") {
|
|
451
|
+
skipped.push(uid);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (!current) schema.attributes[name] = { type: "boolean", default: false, configurable: false };
|
|
455
|
+
added.push(uid);
|
|
456
|
+
}
|
|
457
|
+
return { added, skipped };
|
|
458
|
+
}
|
|
459
|
+
function stripHidden(data, schema, schemaOf, name, depth = 0) {
|
|
460
|
+
if (!data || typeof data !== "object" || !schema || depth > 20) return data;
|
|
461
|
+
if (Array.isArray(data)) {
|
|
462
|
+
data.forEach((item) => stripHidden(item, schema, schemaOf, name, depth));
|
|
463
|
+
return data;
|
|
464
|
+
}
|
|
465
|
+
const walk = (value, target) => Array.isArray(value) ? value.forEach((item) => stripHidden(item, target, schemaOf, name, depth + 1)) : stripHidden(value, target, schemaOf, name, depth + 1);
|
|
466
|
+
for (const [key, attr] of Object.entries(schema.attributes || {})) {
|
|
467
|
+
const value = data[key];
|
|
468
|
+
if (value == null || typeof value !== "object") continue;
|
|
469
|
+
if (attr?.type === "dynamiczone" && Array.isArray(value)) {
|
|
470
|
+
data[key] = value.filter((row) => !(row && row[name] === true));
|
|
471
|
+
for (const row of data[key]) stripHidden(row, schemaOf(row?.__component), schemaOf, name, depth + 1);
|
|
472
|
+
} else if (attr?.type === "component") walk(value, schemaOf(attr.component));
|
|
473
|
+
else if (attr?.type === "relation" && attr.target) walk(value, schemaOf(attr.target));
|
|
474
|
+
}
|
|
475
|
+
return data;
|
|
476
|
+
}
|
|
477
|
+
var contentApi = (strapi) => {
|
|
478
|
+
try {
|
|
479
|
+
return strapi.requestContext?.get?.()?.state?.route?.info?.type === "content-api";
|
|
480
|
+
} catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var READS = ["findMany", "findOne", "findFirst"];
|
|
485
|
+
function registerHiddenStrip2(strapi, name, plugin) {
|
|
486
|
+
if (!name) return;
|
|
487
|
+
const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
|
|
488
|
+
const stripping = async () => {
|
|
489
|
+
try {
|
|
490
|
+
return (await plugin.service("settings").get()).editor.hiddenBlocks === "strip";
|
|
491
|
+
} catch {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const apply = async (uid, result) => result && contentApi(strapi) && await stripping() ? stripHidden(result, schemaOf(uid), schemaOf, name) : result;
|
|
496
|
+
if (strapi.documents?.use) {
|
|
497
|
+
strapi.documents.use(async (ctx, next) => {
|
|
498
|
+
const result = await next();
|
|
499
|
+
return READS.includes(ctx.action) ? apply(ctx.uid, result) : result;
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
strapi.entityService?.decorate?.((service) => ({
|
|
504
|
+
async findMany(uid, params) {
|
|
505
|
+
return apply(uid, await service.findMany(uid, params));
|
|
506
|
+
},
|
|
507
|
+
async findOne(uid, id, params) {
|
|
508
|
+
return apply(uid, await service.findOne(uid, id, params));
|
|
509
|
+
},
|
|
510
|
+
async findPage(uid, params) {
|
|
511
|
+
const page = await service.findPage(uid, params);
|
|
512
|
+
if (page?.results) await apply(uid, page.results);
|
|
513
|
+
return page;
|
|
514
|
+
}
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
517
|
+
module2.exports = { DEFAULT_NAME, hiddenName: hiddenName2, zoneComponents, injectHidden: injectHidden2, stripHidden, registerHiddenStrip: registerHiddenStrip2 };
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// server/diff.js
|
|
522
|
+
var require_diff = __commonJS({
|
|
523
|
+
"server/diff.js"(exports2, module2) {
|
|
524
|
+
"use strict";
|
|
525
|
+
var IGNORED = ["id", "documentId", "locale", "localizations", "createdAt", "updatedAt", "publishedAt", "firstPublishedAt", "createdBy", "updatedBy", "strapi_stage", "strapi_assignee", "__temp_key__", "__pivot"];
|
|
526
|
+
var morph = (attr) => String(attr?.relation || "").toLowerCase().includes("morph");
|
|
527
|
+
var kept = (name, attr) => attr && !IGNORED.includes(name) && attr.type !== "password" && !(attr.type === "relation" && (morph(attr) || attr.visible === false));
|
|
528
|
+
var empty = (value) => value === null || value === void 0 || value === "";
|
|
529
|
+
function snapshotPopulate(uid, schemaOf, stack = []) {
|
|
530
|
+
const schema = schemaOf(uid);
|
|
531
|
+
if (!schema || stack.includes(uid)) return {};
|
|
532
|
+
const scalars = (component) => Object.entries(schemaOf(component)?.attributes || {}).filter(([name, attr]) => kept(name, attr) && !["relation", "media", "component", "dynamiczone"].includes(attr.type)).map(([name]) => name);
|
|
533
|
+
const nested = (component) => ({ select: scalars(component).length ? scalars(component) : ["id"], populate: snapshotPopulate(component, schemaOf, [...stack, uid]) });
|
|
534
|
+
const out = {};
|
|
535
|
+
for (const [name, attr] of Object.entries(schema.attributes || {})) {
|
|
536
|
+
if (!kept(name, attr)) continue;
|
|
537
|
+
if (attr.type === "relation") out[name] = { select: ["documentId", "locale"] };
|
|
538
|
+
else if (attr.type === "media") out[name] = { select: ["id"] };
|
|
539
|
+
else if (attr.type === "component") out[name] = nested(attr.component);
|
|
540
|
+
else if (attr.type === "dynamiczone") out[name] = { on: Object.fromEntries((attr.components || []).map((component) => [component, nested(component)])) };
|
|
541
|
+
}
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function canonical(data, schema, schemaOf, { relations = true } = {}, depth = 0) {
|
|
545
|
+
const out = {};
|
|
546
|
+
if (!data || typeof data !== "object" || depth > 20) return out;
|
|
547
|
+
const idOf = (file) => file && typeof file === "object" ? file.id : file;
|
|
548
|
+
const ref = (item) => item && typeof item === "object" && item.documentId ? { documentId: item.documentId, ...item.locale && { locale: item.locale } } : null;
|
|
549
|
+
for (const [name, attr] of Object.entries(schema?.attributes || {})) {
|
|
550
|
+
const value = data[name];
|
|
551
|
+
if (!kept(name, attr) || empty(value)) continue;
|
|
552
|
+
if (attr.type === "relation") {
|
|
553
|
+
if (!relations) continue;
|
|
554
|
+
const refs = (Array.isArray(value) ? value : [value]).map(ref).filter(Boolean);
|
|
555
|
+
if (refs.length) out[name] = Array.isArray(value) ? refs : refs[0];
|
|
556
|
+
} else if (attr.type === "media") {
|
|
557
|
+
const ids = (Array.isArray(value) ? value : [value]).map(idOf).filter((id) => !empty(id));
|
|
558
|
+
if (ids.length) out[name] = attr.multiple ? ids : ids[0];
|
|
559
|
+
} else if (attr.type === "component") {
|
|
560
|
+
const child = schemaOf(attr.component);
|
|
561
|
+
if (attr.repeatable) {
|
|
562
|
+
if (Array.isArray(value) && value.length) out[name] = value.map((item) => canonical(item, child, schemaOf, { relations }, depth + 1));
|
|
563
|
+
} else out[name] = canonical(value, child, schemaOf, { relations }, depth + 1);
|
|
564
|
+
} else if (attr.type === "dynamiczone") {
|
|
565
|
+
if (Array.isArray(value) && value.length) out[name] = value.filter((row) => row && typeof row.__component === "string").map((row) => ({ __component: row.__component, ...canonical(row, schemaOf(row.__component), schemaOf, { relations }, depth + 1) }));
|
|
566
|
+
} else out[name] = value;
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
var stable = (value) => JSON.stringify(value, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]])) : v);
|
|
571
|
+
var same = (a, b) => stable(a ?? null) === stable(b ?? null);
|
|
572
|
+
function diffRows(before = [], after = []) {
|
|
573
|
+
const a = before.map(stable), b = after.map(stable);
|
|
574
|
+
const n = a.length, m = b.length;
|
|
575
|
+
if (n * m > 25e4) return after.map((row, to) => ({ op: a[to] === b[to] ? "same" : "changed", from: to < n ? to : void 0, to, uid: row.__component }));
|
|
576
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
577
|
+
for (let i2 = n - 1; i2 >= 0; i2--) for (let j2 = m - 1; j2 >= 0; j2--) lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
578
|
+
const out = [];
|
|
579
|
+
const gap = (fromRows2, toRows2) => {
|
|
580
|
+
const removed = [...fromRows2];
|
|
581
|
+
for (const to of toRows2) {
|
|
582
|
+
const k = removed.findIndex((from) => before[from].__component === after[to].__component);
|
|
583
|
+
if (k === -1) out.push({ op: "added", to, uid: after[to].__component });
|
|
584
|
+
else out.push({ op: "changed", from: removed.splice(k, 1)[0], to, uid: after[to].__component });
|
|
585
|
+
}
|
|
586
|
+
for (const from of removed) out.push({ op: "removed", from, uid: before[from].__component });
|
|
587
|
+
};
|
|
588
|
+
let i = 0, j = 0, fromRows = [], toRows = [];
|
|
589
|
+
while (i < n || j < m) {
|
|
590
|
+
if (i < n && j < m && a[i] === b[j]) {
|
|
591
|
+
gap(fromRows, toRows);
|
|
592
|
+
fromRows = [];
|
|
593
|
+
toRows = [];
|
|
594
|
+
out.push({ op: "same", from: i, to: j, uid: after[j].__component });
|
|
595
|
+
i++;
|
|
596
|
+
j++;
|
|
597
|
+
} else if (j < m && (i >= n || lcs[i][j + 1] >= lcs[i + 1][j])) toRows.push(j++);
|
|
598
|
+
else fromRows.push(i++);
|
|
599
|
+
}
|
|
600
|
+
gap(fromRows, toRows);
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
function summarize(before, after, schema) {
|
|
604
|
+
const zones = Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
|
|
605
|
+
const blocks = { added: 0, removed: 0, changed: 0 };
|
|
606
|
+
if (!before) return { initial: true, fields: [], blocks };
|
|
607
|
+
const names = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after || {})])];
|
|
608
|
+
const fields = names.filter((name) => !zones.includes(name) && !same(before[name], after?.[name])).sort();
|
|
609
|
+
for (const name of zones) for (const { op } of diffRows(before[name], after?.[name])) if (op !== "same") blocks[op]++;
|
|
610
|
+
return { fields, blocks };
|
|
611
|
+
}
|
|
612
|
+
module2.exports = { IGNORED, kept, snapshotPopulate, canonical, stable, same, diffRows, summarize };
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// server/history.js
|
|
617
|
+
var require_history = __commonJS({
|
|
618
|
+
"server/history.js"(exports2, module2) {
|
|
619
|
+
"use strict";
|
|
620
|
+
var { createHash } = require("node:crypto");
|
|
621
|
+
var { PLUGIN: PLUGIN2 } = require_settings();
|
|
622
|
+
var { snapshotPopulate, canonical, stable, summarize } = require_diff();
|
|
623
|
+
var EVENT_UID = `plugin::${PLUGIN2}.event`;
|
|
624
|
+
var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore"];
|
|
625
|
+
var CAPTURED = { create: "create", clone: "create", update: "update", publish: "publish", unpublish: "unpublish", discardDraft: "discard", delete: "delete" };
|
|
626
|
+
var CRON2 = "blocksceneHistoryPurge";
|
|
627
|
+
var BATCH = 500;
|
|
628
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
629
|
+
var LIST_LIMIT = 100;
|
|
630
|
+
var eventContentType2 = { schema: {
|
|
631
|
+
kind: "collectionType",
|
|
632
|
+
collectionName: "blockscene_events",
|
|
633
|
+
info: { singularName: "event", pluralName: "events", displayName: "Blockscene event" },
|
|
634
|
+
options: { draftAndPublish: false },
|
|
635
|
+
pluginOptions: { "content-manager": { visible: false }, "content-type-builder": { visible: false }, i18n: { localized: false } },
|
|
636
|
+
attributes: {
|
|
637
|
+
contentType: { type: "string", required: true },
|
|
638
|
+
relatedDocumentId: { type: "string", required: true },
|
|
639
|
+
locale: { type: "string" },
|
|
640
|
+
action: { type: "enumeration", enum: ACTIONS, required: true },
|
|
641
|
+
// 'admin:<id>', 'token:<id>', 'user:<id>' (Users & Permissions) or 'system'; the name as it was at that moment.
|
|
642
|
+
actor: { type: "string", required: true },
|
|
643
|
+
actorName: { type: "string" },
|
|
644
|
+
at: { type: "datetime", required: true },
|
|
645
|
+
// { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed.
|
|
646
|
+
summary: { type: "json" },
|
|
647
|
+
// Null when identical to the previous snapshot of the same locale (same hash) or purged.
|
|
648
|
+
snapshot: { type: "json" },
|
|
649
|
+
hash: { type: "string" },
|
|
650
|
+
size: { type: "integer" }
|
|
651
|
+
},
|
|
652
|
+
indexes: [{ name: "blockscene_events_lookup", columns: ["content_type", "related_document_id", "locale", "at"] }]
|
|
653
|
+
} };
|
|
654
|
+
var fail2 = (message, Kind = "ApplicationError") => {
|
|
655
|
+
let E;
|
|
656
|
+
try {
|
|
657
|
+
E = require("@strapi/utils").errors[Kind];
|
|
658
|
+
} catch {
|
|
659
|
+
E = class extends Error {
|
|
660
|
+
constructor(m) {
|
|
661
|
+
super(m);
|
|
662
|
+
this.name = Kind;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
throw new E(message);
|
|
667
|
+
};
|
|
668
|
+
var hashOf = (text) => createHash("sha256").update(text).digest("hex");
|
|
669
|
+
var covers2 = (history, uid) => Boolean(history?.enabled) && uid.startsWith("api::") && (history.contentTypes === "all" || Array.isArray(history.contentTypes) && history.contentTypes.includes(uid));
|
|
670
|
+
var retention = ({ retentionDays, eventDays }, now = Date.now()) => ({ snapshotsBefore: new Date(now - retentionDays * DAY), eventsBefore: new Date(now - eventDays * DAY) });
|
|
671
|
+
function actorOf(strapi) {
|
|
672
|
+
let ctx;
|
|
673
|
+
try {
|
|
674
|
+
ctx = strapi.requestContext?.get?.();
|
|
675
|
+
} catch {
|
|
676
|
+
ctx = null;
|
|
677
|
+
}
|
|
678
|
+
const strategy = ctx?.state?.auth?.strategy?.name, user = ctx?.state?.user, token = ctx?.state?.auth?.credentials;
|
|
679
|
+
const name = (value) => String(value || "").slice(0, 255);
|
|
680
|
+
if (strategy === "admin" && user?.id) return { actor: `admin:${user.id}`, actorName: name([user.firstname, user.lastname].filter(Boolean).join(" ") || user.username || user.email) };
|
|
681
|
+
if (strategy === "api-token" && token?.id) return { actor: `token:${token.id}`, actorName: name(token.name) };
|
|
682
|
+
if (user?.id) return { actor: `user:${user.id}`, actorName: name(user.username || user.email) };
|
|
683
|
+
return { actor: "system", actorName: "" };
|
|
684
|
+
}
|
|
685
|
+
function refsOf(snapshot, schema, schemaOf) {
|
|
686
|
+
const media = /* @__PURE__ */ new Set(), relations = [];
|
|
687
|
+
const walk = (data, schema2, depth) => {
|
|
688
|
+
if (!data || typeof data !== "object" || depth > 20) return;
|
|
689
|
+
for (const [name, attr] of Object.entries(schema2?.attributes || {})) {
|
|
690
|
+
const value = data[name];
|
|
691
|
+
if (value == null) continue;
|
|
692
|
+
const list = Array.isArray(value) ? value : [value];
|
|
693
|
+
if (attr.type === "media") list.forEach((id) => Number.isInteger(id) && media.add(id));
|
|
694
|
+
else if (attr.type === "relation" && attr.target) list.forEach((ref) => ref?.documentId && relations.push({ target: attr.target, ...ref }));
|
|
695
|
+
else if (attr.type === "component") list.forEach((item) => walk(item, schemaOf(attr.component), depth + 1));
|
|
696
|
+
else if (attr.type === "dynamiczone") list.forEach((row) => walk(row, schemaOf(row?.__component), depth + 1));
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
walk(snapshot, schema, 0);
|
|
700
|
+
return { media: [...media], relations };
|
|
701
|
+
}
|
|
702
|
+
function historyService2({ strapi }) {
|
|
703
|
+
const plugin = () => strapi.plugin(PLUGIN2);
|
|
704
|
+
const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
|
|
705
|
+
const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
|
|
706
|
+
const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
|
|
707
|
+
const events = () => strapi.db.query(EVENT_UID);
|
|
708
|
+
const scope = async (uid, locale) => {
|
|
709
|
+
if (!localized(uid)) return [null];
|
|
710
|
+
if (locale === "*") return null;
|
|
711
|
+
if (Array.isArray(locale) && locale.length) return locale;
|
|
712
|
+
if (typeof locale === "string" && locale) return [locale];
|
|
713
|
+
return [await strapi.plugin("i18n").service("locales").getDefaultLocale()];
|
|
714
|
+
};
|
|
715
|
+
const rows = async (uid, documentId, locales, published = false) => !documentId ? [] : strapi.db.query(uid).findMany({
|
|
716
|
+
where: {
|
|
717
|
+
documentId,
|
|
718
|
+
...locales && localized(uid) && { locale: { $in: locales } },
|
|
719
|
+
...draftAndPublish(uid) && { publishedAt: published ? { $notNull: true } : { $null: true } }
|
|
720
|
+
},
|
|
721
|
+
populate: snapshotPopulate(uid, schemaOf)
|
|
722
|
+
});
|
|
723
|
+
const self = {
|
|
724
|
+
EVENT_UID,
|
|
725
|
+
async config() {
|
|
726
|
+
return (await plugin().service("settings").get()).history;
|
|
727
|
+
},
|
|
728
|
+
covers: covers2,
|
|
729
|
+
// One event. The snapshot is stored unless identical to the latest stored one of the same document and locale.
|
|
730
|
+
async record({ uid, documentId, locale = null, action, entry, actor, missing = false }) {
|
|
731
|
+
let summary = missing ? { missing: true } : null, snapshot = null, hash = null, size = 0;
|
|
732
|
+
if (entry) {
|
|
733
|
+
const content = canonical(entry, schemaOf(uid), schemaOf);
|
|
734
|
+
const text = stable(content);
|
|
735
|
+
hash = hashOf(text);
|
|
736
|
+
const previous = await events().findOne({ select: ["hash", "snapshot"], where: { contentType: uid, relatedDocumentId: documentId, locale, size: { $gt: 0 } }, orderBy: [{ at: "desc" }, { id: "desc" }] });
|
|
737
|
+
summary = summarize(previous?.snapshot || null, content, schemaOf(uid));
|
|
738
|
+
if (previous?.hash !== hash) {
|
|
739
|
+
snapshot = content;
|
|
740
|
+
size = Buffer.byteLength(text);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return events().create({ data: { contentType: uid, relatedDocumentId: documentId, locale, action, ...actor, at: /* @__PURE__ */ new Date(), summary, snapshot, hash, size } });
|
|
744
|
+
},
|
|
745
|
+
// After a save or publish: one event per affected entry (drafts, or the published versions after a publish).
|
|
746
|
+
async captureSave({ uid, action, documentId, locales, published, actor }) {
|
|
747
|
+
if (!documentId) return;
|
|
748
|
+
for (const entry of await rows(uid, documentId, locales, published)) await self.record({ uid, documentId, locale: entry.locale ?? null, action, entry, actor });
|
|
749
|
+
},
|
|
750
|
+
scope,
|
|
751
|
+
rows,
|
|
752
|
+
// Latest events of one document and locale, newest first; `stored`: its snapshot (or an identical one) can be loaded.
|
|
753
|
+
async list({ uid, documentId, locale }) {
|
|
754
|
+
const where = { contentType: uid, relatedDocumentId: documentId, locale: locale || null };
|
|
755
|
+
const [list, kept] = await Promise.all([
|
|
756
|
+
events().findMany({ select: ["id", "action", "actor", "actorName", "at", "summary", "hash", "size"], where, orderBy: [{ at: "desc" }, { id: "desc" }], limit: LIST_LIMIT }),
|
|
757
|
+
events().findMany({ select: ["hash"], where: { ...where, size: { $gt: 0 } } })
|
|
758
|
+
]);
|
|
759
|
+
const hashes = new Set(kept.map((row) => row.hash));
|
|
760
|
+
return list.map(({ hash, size, ...event }) => ({ ...event, stored: Boolean(hash && hashes.has(hash)) }));
|
|
761
|
+
},
|
|
762
|
+
// One event with its snapshot (from an identical event when this one was deduplicated) and what loading it needs:
|
|
763
|
+
// the media files and relation targets that still exist (relation labels from the Content Manager's main field).
|
|
764
|
+
async find(id) {
|
|
765
|
+
const event = await events().findOne({ where: { id } });
|
|
766
|
+
if (!event) return null;
|
|
767
|
+
const { snapshot: own, ...meta } = event;
|
|
768
|
+
const snapshot = own || event.hash && (await events().findOne({ select: ["snapshot"], where: { contentType: event.contentType, relatedDocumentId: event.relatedDocumentId, locale: event.locale, hash: event.hash, size: { $gt: 0 } } }))?.snapshot || null;
|
|
769
|
+
if (!snapshot) return { event: meta, snapshot: null, media: {}, relations: {} };
|
|
770
|
+
const refs = refsOf(snapshot, schemaOf(event.contentType), schemaOf);
|
|
771
|
+
const files = refs.media.length ? await strapi.db.query("plugin::upload.file").findMany({ where: { id: { $in: refs.media } } }) : [];
|
|
772
|
+
const relations = {};
|
|
773
|
+
for (const target of [...new Set(refs.relations.map((ref) => ref.target))]) {
|
|
774
|
+
if (!strapi.contentTypes?.[target]) continue;
|
|
775
|
+
let mainField = null;
|
|
776
|
+
try {
|
|
777
|
+
mainField = (await strapi.plugin("content-manager").service("content-types").findConfiguration(strapi.contentTypes[target]))?.settings?.mainField;
|
|
778
|
+
} catch {
|
|
779
|
+
mainField = null;
|
|
780
|
+
}
|
|
781
|
+
const scalar = mainField && !["relation", "media", "component", "dynamiczone", "password"].includes(schemaOf(target)?.attributes?.[mainField]?.type) && mainField !== "id";
|
|
782
|
+
const found = await strapi.db.query(target).findMany({
|
|
783
|
+
select: ["id", "documentId", "locale", ...scalar ? [mainField] : []],
|
|
784
|
+
where: { documentId: { $in: [...new Set(refs.relations.filter((ref) => ref.target === target).map((ref) => ref.documentId))] }, ...draftAndPublish(target) && { publishedAt: { $null: true } } }
|
|
785
|
+
});
|
|
786
|
+
relations[target] = found.map((row) => ({ id: row.id, documentId: row.documentId, locale: row.locale || null, label: scalar && row[mainField] != null ? String(row[mainField]) : void 0, mainField: scalar ? mainField : void 0 }));
|
|
787
|
+
}
|
|
788
|
+
return { event: meta, snapshot, media: Object.fromEntries(files.map((file) => [file.id, file])), relations };
|
|
789
|
+
},
|
|
790
|
+
// Nightly, in batches, idempotent (two instances running it is harmless): events past eventDays are deleted;
|
|
791
|
+
// snapshots past retentionDays, or beyond maxSnapshots per document (newest kept), are emptied. Never touches media.
|
|
792
|
+
async purge(now = Date.now()) {
|
|
793
|
+
const history = await self.config();
|
|
794
|
+
const { snapshotsBefore, eventsBefore } = retention(history, now);
|
|
795
|
+
const counts = { events: 0, expired: 0, excess: 0 };
|
|
796
|
+
const batches = async (key, query, apply) => {
|
|
797
|
+
for (; ; ) {
|
|
798
|
+
const ids = (await events().findMany({ select: ["id"], limit: BATCH, ...query })).map((row) => row.id);
|
|
799
|
+
if (ids.length) await apply({ id: { $in: ids } });
|
|
800
|
+
counts[key] += ids.length;
|
|
801
|
+
if (ids.length < BATCH) return;
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
const empty = (where) => events().updateMany({ where, data: { snapshot: null, size: 0 } });
|
|
805
|
+
await batches("events", { where: { at: { $lt: eventsBefore } }, orderBy: { id: "asc" } }, (where) => events().deleteMany({ where }));
|
|
806
|
+
await batches("expired", { where: { at: { $lt: snapshotsBefore }, size: { $gt: 0 } }, orderBy: { id: "asc" } }, empty);
|
|
807
|
+
const meta = strapi.db.metadata.get(EVENT_UID);
|
|
808
|
+
const column = (name) => meta.attributes[name].columnName || name;
|
|
809
|
+
const crowded = await strapi.db.connection(meta.tableName).select({ uid: column("contentType"), documentId: column("relatedDocumentId") }).where(column("size"), ">", 0).groupBy(column("contentType"), column("relatedDocumentId")).havingRaw("count(*) > ?", [history.maxSnapshots]);
|
|
810
|
+
for (const { uid, documentId } of crowded) await batches("excess", {
|
|
811
|
+
where: { contentType: uid, relatedDocumentId: documentId, size: { $gt: 0 } },
|
|
812
|
+
orderBy: [{ at: "desc" }, { id: "desc" }],
|
|
813
|
+
offset: history.maxSnapshots
|
|
814
|
+
}, empty);
|
|
815
|
+
return counts;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
return self;
|
|
819
|
+
}
|
|
820
|
+
function registerHistory2(strapi) {
|
|
821
|
+
if (!strapi.documents?.use) return;
|
|
822
|
+
const service = () => strapi.plugin(PLUGIN2).service("history");
|
|
823
|
+
const warn = (message, error) => strapi.log.error(`[${PLUGIN2}] ${message}: ${error?.message || error}`);
|
|
824
|
+
strapi.documents.use(async (ctx, next) => {
|
|
825
|
+
const action = CAPTURED[ctx.action];
|
|
826
|
+
if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true) return next();
|
|
827
|
+
const history = service();
|
|
828
|
+
const params = ctx.params || {};
|
|
829
|
+
if (action === "delete") {
|
|
830
|
+
if (!params.documentId || !history.covers(await history.config(), ctx.uid)) return next();
|
|
831
|
+
return strapi.db.transaction(async () => {
|
|
832
|
+
const actor2 = actorOf(strapi);
|
|
833
|
+
let entries;
|
|
834
|
+
try {
|
|
835
|
+
entries = await history.rows(ctx.uid, params.documentId, await history.scope(ctx.uid, params.locale));
|
|
836
|
+
} catch (error) {
|
|
837
|
+
warn("reading the document before a delete failed", error);
|
|
838
|
+
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
839
|
+
}
|
|
840
|
+
const result2 = await next();
|
|
841
|
+
try {
|
|
842
|
+
for (const entry of entries) await history.record({ uid: ctx.uid, documentId: params.documentId, locale: entry.locale ?? null, action, entry, actor: actor2 });
|
|
843
|
+
} catch (error) {
|
|
844
|
+
warn("recording a delete failed, the delete was rolled back", error);
|
|
845
|
+
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
846
|
+
}
|
|
847
|
+
return result2;
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
let covered = false;
|
|
851
|
+
try {
|
|
852
|
+
covered = history.covers(await history.config(), ctx.uid);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
warn("reading the history settings failed, this change is not recorded", error);
|
|
855
|
+
}
|
|
856
|
+
if (!covered) return next();
|
|
857
|
+
const actor = actorOf(strapi);
|
|
858
|
+
const result = await next();
|
|
859
|
+
const documentId = ["create", "update"].includes(action) ? result?.documentId || params.documentId : params.documentId;
|
|
860
|
+
const locales = ["publish", "unpublish", "discard"].includes(action) ? [...new Set((result?.entries || []).map((entry) => entry?.locale ?? null))] : ctx.action === "clone" ? null : await history.scope(ctx.uid, params.locale).catch(() => [result?.locale ?? null]);
|
|
861
|
+
const steps = [[action, action === "publish"]];
|
|
862
|
+
if (["create", "update"].includes(ctx.action) && params.status === "published" && strapi.getModel(ctx.uid)?.options?.draftAndPublish) steps.push(["publish", true]);
|
|
863
|
+
for (const [step, published] of steps) {
|
|
864
|
+
if (locales && !locales.length) continue;
|
|
865
|
+
try {
|
|
866
|
+
await history.captureSave({ uid: ctx.uid, action: step, documentId, locales, published, actor });
|
|
867
|
+
} catch (error) {
|
|
868
|
+
warn(`recording "${step}" of ${ctx.uid} ${documentId} failed, the change was saved without a version`, error);
|
|
869
|
+
try {
|
|
870
|
+
if (documentId) for (const locale of locales || [null]) await history.record({ uid: ctx.uid, documentId, locale, action: step, actor, missing: true });
|
|
871
|
+
} catch (again) {
|
|
872
|
+
warn("marking the missing version failed too", again);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
function registerPurge2(strapi) {
|
|
880
|
+
if (!strapi.documents?.use || !strapi.cron?.add) return;
|
|
881
|
+
strapi.cron.add({ [CRON2]: { options: "0 3 * * *", task: async () => {
|
|
882
|
+
try {
|
|
883
|
+
const counts = await strapi.plugin(PLUGIN2).service("history").purge();
|
|
884
|
+
if (counts.events || counts.expired || counts.excess) strapi.log.info(`[${PLUGIN2}] history purge: ${counts.events} events deleted, ${counts.expired + counts.excess} snapshots emptied`);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
strapi.log.error(`[${PLUGIN2}] history purge failed: ${error?.message || error}`);
|
|
887
|
+
}
|
|
888
|
+
} } });
|
|
889
|
+
}
|
|
890
|
+
var DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
891
|
+
var LOCALE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
892
|
+
var canRead = (strapi, ctx, uid) => {
|
|
893
|
+
try {
|
|
894
|
+
return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot.read();
|
|
895
|
+
} catch {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var historyController2 = ({ strapi }) => ({
|
|
900
|
+
async list(ctx) {
|
|
901
|
+
const { uid, documentId } = ctx.params || {};
|
|
902
|
+
const locale = ctx.query?.locale || "";
|
|
903
|
+
if (!String(uid).startsWith("api::") || !strapi.contentTypes?.[uid] || !DOCUMENT_ID.test(documentId || "") || locale && !LOCALE.test(locale)) return ctx.badRequest("Invalid content type, document or locale");
|
|
904
|
+
if (!canRead(strapi, ctx, uid)) return ctx.forbidden();
|
|
905
|
+
const history = strapi.plugin(PLUGIN2).service("history");
|
|
906
|
+
ctx.body = { results: await history.list({ uid, documentId, locale: (await history.scope(uid, locale || void 0))[0] }) };
|
|
907
|
+
},
|
|
908
|
+
async find(ctx) {
|
|
909
|
+
const id = Number(ctx.params?.id);
|
|
910
|
+
if (!Number.isInteger(id) || id <= 0) return ctx.badRequest("Invalid event");
|
|
911
|
+
const found = await strapi.plugin(PLUGIN2).service("history").find(id);
|
|
912
|
+
if (!found || !canRead(strapi, ctx, found.event.contentType)) return ctx.notFound();
|
|
913
|
+
ctx.body = found;
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
module2.exports = { EVENT_UID, ACTIONS, CAPTURED, CRON: CRON2, BATCH, eventContentType: eventContentType2, covers: covers2, retention, actorOf, refsOf, historyService: historyService2, registerHistory: registerHistory2, registerPurge: registerPurge2, historyController: historyController2 };
|
|
221
917
|
}
|
|
222
918
|
});
|
|
223
919
|
|
|
224
920
|
// server/index.js
|
|
225
|
-
var { PLUGIN, TEMPLATES, DEFAULTS, catalog, validateSettings, mergeSaved, safeUrl, fail } = require_settings();
|
|
921
|
+
var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
|
|
226
922
|
var { safeGroups, validateGroups } = require_groups();
|
|
923
|
+
var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
|
|
924
|
+
var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
|
|
925
|
+
var STRAPI5 = true;
|
|
227
926
|
var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
|
|
228
927
|
var LEGACY_PLUGIN = "block-picker";
|
|
229
928
|
async function readSettings(strapi) {
|
|
@@ -247,6 +946,8 @@ async function resolveMedia(strapi, settings) {
|
|
|
247
946
|
}
|
|
248
947
|
return resolved;
|
|
249
948
|
}
|
|
949
|
+
var contentTypeUids = (strapi) => Object.fromEntries(Object.entries(strapi.contentTypes || {}).filter(([uid, schema]) => uid.startsWith("api::") && zonesOf(schema).length).map(([uid, schema]) => [uid, Object.keys(schema.attributes || {})]));
|
|
950
|
+
var apiUids = (strapi) => Object.keys(strapi.contentTypes || {}).filter((uid) => uid.startsWith("api::"));
|
|
250
951
|
var zonesOf = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
|
|
251
952
|
var label = (error) => `row ${error.index + 1}: ${error.code === "closeBeforeOpen" ? `close marker ${error.uid} has no open marker before it` : error.code === "mismatch" ? `close marker ${error.uid} does not match the open group ${error.open} (expected ${error.expected})` : `group ${error.uid} is not closed (expected ${error.expected})`}`;
|
|
252
953
|
function checkZones(strapi, uid, entry, groups) {
|
|
@@ -263,6 +964,11 @@ function registerPublishGuard(strapi) {
|
|
|
263
964
|
const groups = safeGroups(plugin.config("groups"), componentUids(strapi));
|
|
264
965
|
if (plugin.config("groups") && !groups) strapi.log.warn(`[${PLUGIN}] "groups" config ignored: expected { "<open component uid>": "<close component uid>" } with existing components; every block is treated as ordinary.`);
|
|
265
966
|
if (!groups) return;
|
|
967
|
+
const active = async (uid) => {
|
|
968
|
+
if (plugin.config("disabled") === true) return false;
|
|
969
|
+
const settings = await plugin.service("settings").get();
|
|
970
|
+
return settings.editor.enabled && settings.contentTypes?.[uid]?.enabled !== false;
|
|
971
|
+
};
|
|
266
972
|
if (strapi.documents?.use) {
|
|
267
973
|
const defaultLocale = async () => {
|
|
268
974
|
try {
|
|
@@ -277,7 +983,7 @@ function registerPublishGuard(strapi) {
|
|
|
277
983
|
if (!publishing || !ctx.uid) return next();
|
|
278
984
|
const model = strapi.getModel(ctx.uid);
|
|
279
985
|
const zones = zonesOf(model);
|
|
280
|
-
if (!zones.length) return next();
|
|
986
|
+
if (!zones.length || !await active(ctx.uid)) return next();
|
|
281
987
|
if (action === "create") {
|
|
282
988
|
checkZones(strapi, ctx.uid, params.data || {}, groups);
|
|
283
989
|
return next();
|
|
@@ -303,19 +1009,19 @@ function registerPublishGuard(strapi) {
|
|
|
303
1009
|
const published = (data) => Boolean(data && data.publishedAt);
|
|
304
1010
|
strapi.db.lifecycles.subscribe({
|
|
305
1011
|
async beforeCreate(event) {
|
|
306
|
-
if (published(event.params?.data)) checkZones(strapi, event.model.uid, event.params.data, groups);
|
|
1012
|
+
if (published(event.params?.data) && await active(event.model.uid)) checkZones(strapi, event.model.uid, event.params.data, groups);
|
|
307
1013
|
},
|
|
308
1014
|
async beforeUpdate(event) {
|
|
309
1015
|
const { data, where } = event.params || {};
|
|
310
1016
|
const zones = zonesOf(strapi.getModel(event.model.uid));
|
|
311
|
-
if (!published(data) || !zones.length) return;
|
|
1017
|
+
if (!published(data) || !zones.length || !await active(event.model.uid)) return;
|
|
312
1018
|
const entry = zones.every((name) => Array.isArray(data[name])) ? data : { ...await strapi.entityService.findOne(event.model.uid, where?.id, { populate: zones }), ...data };
|
|
313
1019
|
checkZones(strapi, event.model.uid, entry, groups);
|
|
314
1020
|
},
|
|
315
1021
|
async beforeUpdateMany(event) {
|
|
316
1022
|
const { data, where } = event.params || {};
|
|
317
1023
|
const zones = zonesOf(strapi.getModel(event.model.uid));
|
|
318
|
-
if (!published(data) || !zones.length) return;
|
|
1024
|
+
if (!published(data) || !zones.length || !await active(event.model.uid)) return;
|
|
319
1025
|
for (const entry of await strapi.entityService.findMany(event.model.uid, { filters: where, populate: zones })) checkZones(strapi, event.model.uid, entry, groups);
|
|
320
1026
|
}
|
|
321
1027
|
});
|
|
@@ -332,30 +1038,93 @@ module.exports = {
|
|
|
332
1038
|
blockPreview: false,
|
|
333
1039
|
// Optional layout groups: OPEN component uid -> its CLOSE uid, e.g. { 'wrappers.join': 'wrappers.close' }.
|
|
334
1040
|
// Empty/absent: every Dynamic Zone component is an ordinary block. See README "Layout groups".
|
|
335
|
-
groups: null
|
|
1041
|
+
groups: null,
|
|
1042
|
+
// Project defaults from code: same shape as the stored settings, e.g. require('./blockscene.json'). See README "Settings page".
|
|
1043
|
+
settings: null,
|
|
1044
|
+
// Edit view field texts from code (never stored): { uid: { attribute: { label, description, placeholder } } }. See README "Field labels".
|
|
1045
|
+
fields: null,
|
|
1046
|
+
// "Hide on the site": boolean attribute added to every Dynamic Zone component (a DB column); false adds nothing.
|
|
1047
|
+
hiddenAttribute: "bsHidden"
|
|
336
1048
|
},
|
|
337
1049
|
validator: catalog
|
|
338
1050
|
},
|
|
1051
|
+
// Before the database schema sync, so the column exists. A name the host already uses with another type is skipped.
|
|
1052
|
+
register({ strapi }) {
|
|
1053
|
+
const plugin = strapi.plugin(PLUGIN);
|
|
1054
|
+
const name = hiddenName(plugin.config("hiddenAttribute"));
|
|
1055
|
+
if (name === null) strapi.log.warn(`[${PLUGIN}] "hiddenAttribute" config ignored: expected a name like "bsHidden" or false. Hiding blocks is off.`);
|
|
1056
|
+
if (!name) return;
|
|
1057
|
+
const { skipped } = injectHidden(strapi.components, strapi.contentTypes, name);
|
|
1058
|
+
if (skipped.length) strapi.log.warn(`[${PLUGIN}] "${name}" already exists with another type on ${skipped.join(", ")}; those blocks cannot be hidden.`);
|
|
1059
|
+
},
|
|
339
1060
|
async bootstrap({ strapi }) {
|
|
340
1061
|
await strapi.admin.services.permission.actionProvider.registerMany([
|
|
341
1062
|
{ section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
|
|
342
|
-
{ section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN }
|
|
1063
|
+
{ section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN },
|
|
1064
|
+
...STRAPI5 ? [{ section: "plugins", displayName: "Read version history", uid: "history.read", pluginName: PLUGIN }] : []
|
|
343
1065
|
]);
|
|
1066
|
+
strapi.plugin(PLUGIN).service("settings").projectDefaults();
|
|
1067
|
+
strapi.plugin(PLUGIN).service("settings").fields();
|
|
344
1068
|
registerPublishGuard(strapi);
|
|
1069
|
+
registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
|
|
1070
|
+
if (STRAPI5) {
|
|
1071
|
+
registerHistory(strapi);
|
|
1072
|
+
registerPurge(strapi);
|
|
1073
|
+
}
|
|
345
1074
|
},
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1075
|
+
destroy({ strapi }) {
|
|
1076
|
+
if (STRAPI5) strapi.cron?.remove?.(CRON);
|
|
1077
|
+
},
|
|
1078
|
+
// Version history events (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
|
|
1079
|
+
contentTypes: STRAPI5 ? { event: eventContentType } : {},
|
|
1080
|
+
services: { settings: ({ strapi }) => {
|
|
1081
|
+
let project;
|
|
1082
|
+
const projectDefaults = () => {
|
|
1083
|
+
if (project !== void 0) return project;
|
|
1084
|
+
const code = strapi.plugin(PLUGIN).config("settings");
|
|
1085
|
+
project = null;
|
|
1086
|
+
if (code) try {
|
|
1087
|
+
project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
|
|
354
1090
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1091
|
+
return project;
|
|
1092
|
+
};
|
|
1093
|
+
let fields;
|
|
1094
|
+
const fieldTexts = () => {
|
|
1095
|
+
if (fields !== void 0) return fields;
|
|
1096
|
+
const schemas = Object.fromEntries(Object.entries({ ...strapi.contentTypes, ...strapi.components }).map(([uid, schema]) => [uid, Object.keys(schema?.attributes || {})]));
|
|
1097
|
+
const stale = [];
|
|
1098
|
+
try {
|
|
1099
|
+
fields = validateFields(strapi.plugin(PLUGIN).config("fields"), schemas, stale);
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
fields = {};
|
|
1102
|
+
strapi.log?.warn(`[${PLUGIN}] "fields" config ignored: ${error.message}. Field labels fall back to the Content Manager's own.`);
|
|
1103
|
+
}
|
|
1104
|
+
if (stale.length) strapi.log?.warn(`[${PLUGIN}] "fields" entries for fields the schema no longer has were skipped: ${stale.slice(0, 20).join(", ")}${stale.length > 20 ? ` and ${stale.length - 20} more` : ""}.`);
|
|
1105
|
+
return fields;
|
|
1106
|
+
};
|
|
1107
|
+
const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
|
|
1108
|
+
return {
|
|
1109
|
+
projectDefaults,
|
|
1110
|
+
get,
|
|
1111
|
+
fields: fieldTexts,
|
|
1112
|
+
async set(value) {
|
|
1113
|
+
const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
|
|
1114
|
+
for (const [uid, entry] of Object.entries(next.components)) {
|
|
1115
|
+
if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
|
|
1116
|
+
}
|
|
1117
|
+
await store(strapi).set({ key: "settings", value: overrides(next, projectDefaults()) });
|
|
1118
|
+
return next;
|
|
1119
|
+
},
|
|
1120
|
+
// Back to the project defaults (or the built-in ones); the legacy copy goes too, or it would be migrated again.
|
|
1121
|
+
async reset() {
|
|
1122
|
+
await store(strapi).delete({ key: "settings" });
|
|
1123
|
+
await strapi.store({ type: "plugin", name: LEGACY_PLUGIN }).delete({ key: "settings" });
|
|
1124
|
+
return get();
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}, ...STRAPI5 && { history: historyService } },
|
|
359
1128
|
controllers: {
|
|
360
1129
|
catalog: ({ strapi }) => ({
|
|
361
1130
|
async find(ctx) {
|
|
@@ -367,53 +1136,107 @@ module.exports = {
|
|
|
367
1136
|
disabled: plugin.config("disabled"),
|
|
368
1137
|
blockPreview: plugin.config("blockPreview"),
|
|
369
1138
|
groups: plugin.config("groups"),
|
|
370
|
-
componentUids: componentUids(strapi)
|
|
1139
|
+
componentUids: componentUids(strapi),
|
|
1140
|
+
schemas: strapi.components
|
|
371
1141
|
});
|
|
372
1142
|
const settings = await plugin.service("settings").get();
|
|
373
1143
|
const media = await resolveMedia(strapi, settings);
|
|
374
1144
|
for (const [uid, entry] of Object.entries(settings.components)) {
|
|
375
|
-
base.components[uid] = {
|
|
1145
|
+
base.components[uid] = {
|
|
1146
|
+
...base.components[uid],
|
|
1147
|
+
...media[uid],
|
|
1148
|
+
template: entry.template,
|
|
1149
|
+
...entry.typology && { typology: entry.typology },
|
|
1150
|
+
...entry.tags && { tags: entry.tags }
|
|
1151
|
+
};
|
|
376
1152
|
}
|
|
1153
|
+
const hidden = hiddenName(plugin.config("hiddenAttribute")) || null;
|
|
1154
|
+
const types = Object.fromEntries(Object.entries(strapi.contentTypes || {}).filter(([uid]) => !uid.startsWith("admin::")).map(([uid, schema]) => [uid, [schema.info?.displayName || "", ...Object.keys(schema.attributes || {})]]));
|
|
1155
|
+
const history = STRAPI5 && strapi.documents?.use && !base.disabled && settings.history.enabled ? { contentTypes: apiUids(strapi).filter((uid) => covers(settings.history, uid)) } : null;
|
|
377
1156
|
ctx.body = {
|
|
378
1157
|
...base,
|
|
379
1158
|
palette: settings.palette,
|
|
380
|
-
|
|
1159
|
+
contentTypes: settings.contentTypes,
|
|
1160
|
+
hiddenAttribute: hidden,
|
|
1161
|
+
fields: plugin.service("settings").fields(),
|
|
1162
|
+
types,
|
|
1163
|
+
history,
|
|
1164
|
+
editor: { ...settings.editor, enabled: settings.editor.enabled && !base.disabled, ...!hidden && { hiddenBlocks: "off" } }
|
|
381
1165
|
};
|
|
382
1166
|
}
|
|
383
1167
|
}),
|
|
384
1168
|
settings: ({ strapi }) => ({
|
|
385
1169
|
async find(ctx) {
|
|
386
1170
|
const settings = await strapi.plugin(PLUGIN).service("settings").get();
|
|
1171
|
+
const auto = catalog({ components: strapi.plugin(PLUGIN).config("components"), schemas: strapi.components }).components;
|
|
387
1172
|
const components = Object.entries(strapi.components || {}).map(([uid, schema]) => ({
|
|
388
1173
|
uid,
|
|
389
1174
|
displayName: schema.info?.displayName || uid,
|
|
390
|
-
category: schema.category || uid.split(".")[0]
|
|
1175
|
+
category: schema.category || uid.split(".")[0],
|
|
1176
|
+
typology: auto[uid]?.typology
|
|
1177
|
+
}));
|
|
1178
|
+
const contentTypes = Object.keys(contentTypeUids(strapi)).map((uid) => ({
|
|
1179
|
+
uid,
|
|
1180
|
+
displayName: strapi.contentTypes[uid].info?.displayName || uid,
|
|
1181
|
+
kind: strapi.contentTypes[uid].kind,
|
|
1182
|
+
attributes: Object.entries(strapi.contentTypes[uid].attributes || {}).filter(([, attr]) => attr?.type !== "dynamiczone" && !attr?.private).map(([name, attr]) => ({ name, type: attr.type }))
|
|
391
1183
|
}));
|
|
392
1184
|
ctx.body = {
|
|
393
1185
|
settings,
|
|
394
1186
|
components,
|
|
1187
|
+
contentTypes,
|
|
395
1188
|
media: await resolveMedia(strapi, settings),
|
|
396
1189
|
templates: TEMPLATES,
|
|
1190
|
+
typologies: TYPOLOGIES,
|
|
397
1191
|
disabled: strapi.plugin(PLUGIN).config("disabled") === true,
|
|
1192
|
+
hiddenAttribute: hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")) || null,
|
|
398
1193
|
blockPreviewAvailable: strapi.plugin(PLUGIN).config("blockPreview") === true,
|
|
399
|
-
defaults: DEFAULTS
|
|
1194
|
+
defaults: DEFAULTS,
|
|
1195
|
+
projectDefaults: strapi.plugin(PLUGIN).service("settings").projectDefaults(),
|
|
1196
|
+
// Version history (Strapi 5 only): the content types it can cover.
|
|
1197
|
+
historyTypes: STRAPI5 && strapi.documents?.use ? apiUids(strapi).map((uid) => ({ uid, displayName: strapi.contentTypes[uid].info?.displayName || uid })) : null
|
|
400
1198
|
};
|
|
401
1199
|
},
|
|
402
1200
|
async update(ctx) {
|
|
403
1201
|
ctx.body = await strapi.plugin(PLUGIN).service("settings").set(ctx.request?.body);
|
|
1202
|
+
},
|
|
1203
|
+
async reset(ctx) {
|
|
1204
|
+
ctx.body = await strapi.plugin(PLUGIN).service("settings").reset();
|
|
1205
|
+
}
|
|
1206
|
+
}),
|
|
1207
|
+
prefs: ({ strapi }) => ({
|
|
1208
|
+
async find(ctx) {
|
|
1209
|
+
const id = ctx.state?.user?.id;
|
|
1210
|
+
if (!id) return ctx.unauthorized();
|
|
1211
|
+
ctx.body = mergePrefs(await store(strapi).get({ key: `prefs:${id}` }), componentUids(strapi));
|
|
1212
|
+
},
|
|
1213
|
+
async update(ctx) {
|
|
1214
|
+
const id = ctx.state?.user?.id;
|
|
1215
|
+
if (!id) return ctx.unauthorized();
|
|
1216
|
+
const value = validatePrefs(ctx.request?.body, componentUids(strapi));
|
|
1217
|
+
await store(strapi).set({ key: `prefs:${id}`, value });
|
|
1218
|
+
ctx.body = value;
|
|
404
1219
|
}
|
|
405
|
-
})
|
|
1220
|
+
}),
|
|
1221
|
+
...STRAPI5 && { history: historyController }
|
|
406
1222
|
},
|
|
407
1223
|
routes: { admin: { type: "admin", routes: [
|
|
408
1224
|
{ method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
|
|
409
|
-
...["
|
|
410
|
-
|
|
1225
|
+
...["GET", "PUT"].map((method) => ({ method, path: "/me/prefs", handler: `prefs.${method === "GET" ? "find" : "update"}`, config: { policies: ["admin::isAuthenticatedAdmin"] } })),
|
|
1226
|
+
...[["GET", "find", "read"], ["PUT", "update", "update"], ["DELETE", "reset", "update"]].map(([method, handler, action]) => ({
|
|
1227
|
+
method,
|
|
411
1228
|
path: "/settings",
|
|
412
1229
|
handler: `settings.${handler}`,
|
|
413
1230
|
config: { policies: ["admin::isAuthenticatedAdmin", {
|
|
414
1231
|
name: "admin::hasPermissions",
|
|
415
|
-
config: { actions: [`plugin::${PLUGIN}.settings.${
|
|
1232
|
+
config: { actions: [`plugin::${PLUGIN}.settings.${action}`] }
|
|
416
1233
|
}] }
|
|
417
|
-
}))
|
|
1234
|
+
})),
|
|
1235
|
+
...STRAPI5 ? [["/history/:uid/:documentId", "list"], ["/history-events/:id", "find"]].map(([path, handler]) => ({
|
|
1236
|
+
method: "GET",
|
|
1237
|
+
path,
|
|
1238
|
+
handler: `history.${handler}`,
|
|
1239
|
+
config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}.history.read`] } }] }
|
|
1240
|
+
})) : []
|
|
418
1241
|
] } }
|
|
419
1242
|
};
|