@aduptive/strapi-blockscene 2.0.0-alpha.5 → 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 +143 -5
- package/dist/admin.cjs +1196 -320
- package/dist/admin.mjs +1194 -317
- package/dist/server.cjs +514 -15
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -144,11 +144,16 @@ var require_settings = __commonJS({
|
|
|
144
144
|
confirmDelete: true,
|
|
145
145
|
hiddenBlocks: "strip",
|
|
146
146
|
duplicate: true,
|
|
147
|
-
clipboard: 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
|
|
148
150
|
},
|
|
149
151
|
// Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
|
|
150
152
|
// previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
|
|
151
|
-
contentTypes: {}
|
|
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 }
|
|
152
157
|
};
|
|
153
158
|
var ICONS = ["text", "tag", "seo", "settings", "image", "link", "palette", "list", "globe", "info"];
|
|
154
159
|
var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
|
|
@@ -168,6 +173,15 @@ var require_settings = __commonJS({
|
|
|
168
173
|
var FIELD_UID = /^(plugin|global)::[\w.-]+$/;
|
|
169
174
|
var lazyFields = (value) => Array.isArray(value) && value.length <= 20 && value.every((uid) => typeof uid === "string" && FIELD_UID.test(uid));
|
|
170
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
|
+
};
|
|
171
185
|
var TYPOLOGIES2 = ["hero", "text", "media", "listing", "cards", "cta", "form", "layout"];
|
|
172
186
|
var TAG = /^[^<>\n]{1,24}$/;
|
|
173
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) : [];
|
|
@@ -254,7 +268,7 @@ var require_settings = __commonJS({
|
|
|
254
268
|
groups: safeGroups2(config.groups, config.componentUids)
|
|
255
269
|
};
|
|
256
270
|
}
|
|
257
|
-
function validateSettings2(input, componentUids2, contentTypeUids2 = []) {
|
|
271
|
+
function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null) {
|
|
258
272
|
const types = typeMap(contentTypeUids2);
|
|
259
273
|
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
|
|
260
274
|
if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
|
|
@@ -305,9 +319,17 @@ var require_settings = __commonJS({
|
|
|
305
319
|
}
|
|
306
320
|
if (Object.keys(entry).length) out.contentTypes[uid] = structuredClone(entry);
|
|
307
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");
|
|
308
330
|
return out;
|
|
309
331
|
}
|
|
310
|
-
function mergeSaved2(saved, componentUids2, contentTypeUids2 = []) {
|
|
332
|
+
function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null) {
|
|
311
333
|
const types = typeMap(contentTypeUids2);
|
|
312
334
|
const out = structuredClone(DEFAULTS2);
|
|
313
335
|
if (!saved || typeof saved !== "object") return out;
|
|
@@ -343,6 +365,12 @@ var require_settings = __commonJS({
|
|
|
343
365
|
const clean = Object.fromEntries(Object.entries(entry).map(([key, value]) => [key, fix(key, value)]).filter(([key, value]) => TYPE_KEYS[key]?.(value, types[uid])));
|
|
344
366
|
if (Object.keys(clean).length) out.contentTypes[uid] = clean;
|
|
345
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);
|
|
346
374
|
return out;
|
|
347
375
|
}
|
|
348
376
|
var ENTRY_KEYS = ["components", "contentTypes"];
|
|
@@ -361,6 +389,33 @@ var require_settings = __commonJS({
|
|
|
361
389
|
return [key, Object.fromEntries(entries)];
|
|
362
390
|
}));
|
|
363
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
|
+
}
|
|
364
419
|
var PREFS = { starred: 200, recent: 20 };
|
|
365
420
|
function validatePrefs2(input, componentUids2) {
|
|
366
421
|
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
|
|
@@ -373,7 +428,7 @@ var require_settings = __commonJS({
|
|
|
373
428
|
return out;
|
|
374
429
|
}
|
|
375
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) : []]));
|
|
376
|
-
module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
|
|
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 };
|
|
377
432
|
}
|
|
378
433
|
});
|
|
379
434
|
|
|
@@ -396,7 +451,7 @@ var require_hidden = __commonJS({
|
|
|
396
451
|
skipped.push(uid);
|
|
397
452
|
continue;
|
|
398
453
|
}
|
|
399
|
-
if (!current) schema.attributes[name] = { type: "boolean", default: false,
|
|
454
|
+
if (!current) schema.attributes[name] = { type: "boolean", default: false, configurable: false };
|
|
400
455
|
added.push(uid);
|
|
401
456
|
}
|
|
402
457
|
return { added, skipped };
|
|
@@ -463,10 +518,411 @@ var require_hidden = __commonJS({
|
|
|
463
518
|
}
|
|
464
519
|
});
|
|
465
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 };
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
466
920
|
// server/index.js
|
|
467
|
-
var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
|
|
921
|
+
var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
|
|
468
922
|
var { safeGroups, validateGroups } = require_groups();
|
|
469
923
|
var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
|
|
924
|
+
var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
|
|
925
|
+
var STRAPI5 = true;
|
|
470
926
|
var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
|
|
471
927
|
var LEGACY_PLUGIN = "block-picker";
|
|
472
928
|
async function readSettings(strapi) {
|
|
@@ -491,6 +947,7 @@ async function resolveMedia(strapi, settings) {
|
|
|
491
947
|
return resolved;
|
|
492
948
|
}
|
|
493
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::"));
|
|
494
951
|
var zonesOf = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
|
|
495
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})`}`;
|
|
496
953
|
function checkZones(strapi, uid, entry, groups) {
|
|
@@ -584,6 +1041,8 @@ module.exports = {
|
|
|
584
1041
|
groups: null,
|
|
585
1042
|
// Project defaults from code: same shape as the stored settings, e.g. require('./blockscene.json'). See README "Settings page".
|
|
586
1043
|
settings: null,
|
|
1044
|
+
// Edit view field texts from code (never stored): { uid: { attribute: { label, description, placeholder } } }. See README "Field labels".
|
|
1045
|
+
fields: null,
|
|
587
1046
|
// "Hide on the site": boolean attribute added to every Dynamic Zone component (a DB column); false adds nothing.
|
|
588
1047
|
hiddenAttribute: "bsHidden"
|
|
589
1048
|
},
|
|
@@ -601,12 +1060,23 @@ module.exports = {
|
|
|
601
1060
|
async bootstrap({ strapi }) {
|
|
602
1061
|
await strapi.admin.services.permission.actionProvider.registerMany([
|
|
603
1062
|
{ section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
|
|
604
|
-
{ 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 }] : []
|
|
605
1065
|
]);
|
|
606
1066
|
strapi.plugin(PLUGIN).service("settings").projectDefaults();
|
|
1067
|
+
strapi.plugin(PLUGIN).service("settings").fields();
|
|
607
1068
|
registerPublishGuard(strapi);
|
|
608
1069
|
registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
|
|
1070
|
+
if (STRAPI5) {
|
|
1071
|
+
registerHistory(strapi);
|
|
1072
|
+
registerPurge(strapi);
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1075
|
+
destroy({ strapi }) {
|
|
1076
|
+
if (STRAPI5) strapi.cron?.remove?.(CRON);
|
|
609
1077
|
},
|
|
1078
|
+
// Version history events (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
|
|
1079
|
+
contentTypes: STRAPI5 ? { event: eventContentType } : {},
|
|
610
1080
|
services: { settings: ({ strapi }) => {
|
|
611
1081
|
let project;
|
|
612
1082
|
const projectDefaults = () => {
|
|
@@ -614,18 +1084,33 @@ module.exports = {
|
|
|
614
1084
|
const code = strapi.plugin(PLUGIN).config("settings");
|
|
615
1085
|
project = null;
|
|
616
1086
|
if (code) try {
|
|
617
|
-
project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi));
|
|
1087
|
+
project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
|
|
618
1088
|
} catch (error) {
|
|
619
1089
|
strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
|
|
620
1090
|
}
|
|
621
1091
|
return project;
|
|
622
1092
|
};
|
|
623
|
-
|
|
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));
|
|
624
1108
|
return {
|
|
625
1109
|
projectDefaults,
|
|
626
1110
|
get,
|
|
1111
|
+
fields: fieldTexts,
|
|
627
1112
|
async set(value) {
|
|
628
|
-
const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi));
|
|
1113
|
+
const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
|
|
629
1114
|
for (const [uid, entry] of Object.entries(next.components)) {
|
|
630
1115
|
if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
|
|
631
1116
|
}
|
|
@@ -639,7 +1124,7 @@ module.exports = {
|
|
|
639
1124
|
return get();
|
|
640
1125
|
}
|
|
641
1126
|
};
|
|
642
|
-
} },
|
|
1127
|
+
}, ...STRAPI5 && { history: historyService } },
|
|
643
1128
|
controllers: {
|
|
644
1129
|
catalog: ({ strapi }) => ({
|
|
645
1130
|
async find(ctx) {
|
|
@@ -666,11 +1151,16 @@ module.exports = {
|
|
|
666
1151
|
};
|
|
667
1152
|
}
|
|
668
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;
|
|
669
1156
|
ctx.body = {
|
|
670
1157
|
...base,
|
|
671
1158
|
palette: settings.palette,
|
|
672
1159
|
contentTypes: settings.contentTypes,
|
|
673
1160
|
hiddenAttribute: hidden,
|
|
1161
|
+
fields: plugin.service("settings").fields(),
|
|
1162
|
+
types,
|
|
1163
|
+
history,
|
|
674
1164
|
editor: { ...settings.editor, enabled: settings.editor.enabled && !base.disabled, ...!hidden && { hiddenBlocks: "off" } }
|
|
675
1165
|
};
|
|
676
1166
|
}
|
|
@@ -702,7 +1192,9 @@ module.exports = {
|
|
|
702
1192
|
hiddenAttribute: hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")) || null,
|
|
703
1193
|
blockPreviewAvailable: strapi.plugin(PLUGIN).config("blockPreview") === true,
|
|
704
1194
|
defaults: DEFAULTS,
|
|
705
|
-
projectDefaults: strapi.plugin(PLUGIN).service("settings").projectDefaults()
|
|
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
|
|
706
1198
|
};
|
|
707
1199
|
},
|
|
708
1200
|
async update(ctx) {
|
|
@@ -725,7 +1217,8 @@ module.exports = {
|
|
|
725
1217
|
await store(strapi).set({ key: `prefs:${id}`, value });
|
|
726
1218
|
ctx.body = value;
|
|
727
1219
|
}
|
|
728
|
-
})
|
|
1220
|
+
}),
|
|
1221
|
+
...STRAPI5 && { history: historyController }
|
|
729
1222
|
},
|
|
730
1223
|
routes: { admin: { type: "admin", routes: [
|
|
731
1224
|
{ method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
|
|
@@ -738,6 +1231,12 @@ module.exports = {
|
|
|
738
1231
|
name: "admin::hasPermissions",
|
|
739
1232
|
config: { actions: [`plugin::${PLUGIN}.settings.${action}`] }
|
|
740
1233
|
}] }
|
|
741
|
-
}))
|
|
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
|
+
})) : []
|
|
742
1241
|
] } }
|
|
743
1242
|
};
|