@aduptive/strapi-blockscene 2.0.0-alpha.6 → 2.0.0-alpha.8

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/dist/server.cjs CHANGED
@@ -106,7 +106,41 @@ var require_groups = __commonJS({
106
106
  const [, end] = groupRange(rows, start, g);
107
107
  return [...rows.slice(0, start), ...rows.slice(end + 1)];
108
108
  }
109
- module2.exports = { safeGroups: safeGroups2, validateGroups: validateGroups2, groupRange, groupRows, topLevelRanges, moveGroup, removeGroup, blockKey, isOpener, isClose };
109
+ var MAX_COLUMNS = 12;
110
+ var parseColumns = (value) => {
111
+ const n = typeof value === "number" ? value : typeof value === "string" && /^\s*\d{1,3}\s*$/.test(value) ? Number(value) : NaN;
112
+ return Number.isInteger(n) && n >= 1 ? n : null;
113
+ };
114
+ var columnsAttribute = (attr) => Boolean(attr) && typeof attr === "object" && !attr.private && (["integer", "biginteger", "float", "decimal", "string"].includes(attr.type) || attr.type === "enumeration" && Array.isArray(attr.enum) && attr.enum.length > 0 && attr.enum.every((value) => parseColumns(value) !== null));
115
+ var layoutFields2 = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => columnsAttribute(attr)).map(([name]) => name);
116
+ function validLayout(value, attributes) {
117
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
118
+ if (!Object.keys(value).every((key) => ["columnsField", "mobileColumnsField", "maxColumns"].includes(key))) return false;
119
+ const field = (name) => typeof name === "string" && /^[A-Za-z_][\w-]{0,63}$/.test(name) && (!attributes || columnsAttribute(attributes[name]));
120
+ return field(value.columnsField) && (value.mobileColumnsField === void 0 || field(value.mobileColumnsField)) && (value.maxColumns === void 0 || Number.isInteger(value.maxColumns) && value.maxColumns >= 1 && value.maxColumns <= MAX_COLUMNS);
121
+ }
122
+ function layoutColumns(row, layout, schema) {
123
+ const max = layout.maxColumns || MAX_COLUMNS;
124
+ const read = (name) => Math.min(max, parseColumns(row?.[name]) ?? parseColumns(schema?.attributes?.[name]?.default) ?? 1);
125
+ return { desktop: read(layout.columnsField), mobile: layout.mobileColumnsField ? read(layout.mobileColumnsField) : null };
126
+ }
127
+ module2.exports = {
128
+ safeGroups: safeGroups2,
129
+ validateGroups: validateGroups2,
130
+ groupRange,
131
+ groupRows,
132
+ topLevelRanges,
133
+ moveGroup,
134
+ removeGroup,
135
+ blockKey,
136
+ isOpener,
137
+ isClose,
138
+ MAX_COLUMNS,
139
+ parseColumns,
140
+ layoutFields: layoutFields2,
141
+ validLayout,
142
+ layoutColumns
143
+ };
110
144
  }
111
145
  });
112
146
 
@@ -146,24 +180,37 @@ var require_settings = __commonJS({
146
180
  duplicate: true,
147
181
  clipboard: true,
148
182
  // Edit view labels that are still the raw attribute name read as "Mobile columns count" (see README "Field labels").
149
- friendlyLabels: true
183
+ friendlyLabels: true,
184
+ // Page preview pane (Strapi 5): which toolbar controls show, in order, and the width menu's entries.
185
+ previewToolbar: ["modes", "history", "devices", "status", "actions"],
186
+ previewDevices: ["fit", "mobile", "tablet", "desktop"]
150
187
  },
151
188
  // Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
152
189
  // previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
153
190
  contentTypes: {},
154
191
  // 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 }
192
+ // or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays; deleted
193
+ // documents stay in the trash trashDays (fixed per document when it is deleted).
194
+ history: { enabled: false, contentTypes: "all", retentionDays: 90, maxSnapshots: 100, eventDays: 365, trashDays: 90 }
157
195
  };
158
196
  var ICONS = ["text", "tag", "seo", "settings", "image", "link", "palette", "list", "globe", "info"];
159
197
  var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
160
198
  var OPENS = ["modal", "drawer"];
161
199
  var LABEL = /^[^<>]{1,40}$/;
200
+ var TOOLBAR = ["modes", "history", "devices", "status", "actions"];
201
+ var DEVICE_NAMES = ["fit", "mobile", "tablet", "desktop"];
202
+ var previewToolbar = (value) => Array.isArray(value) && value.every((id) => TOOLBAR.includes(id)) && new Set(value).size === value.length;
203
+ var DEVICE_LABEL = /^[^<>]{1,24}$/;
204
+ var customDevice = (item) => item && typeof item === "object" && !Array.isArray(item) && Object.keys(item).every((key) => ["label", "width"].includes(key)) && typeof item.label === "string" && DEVICE_LABEL.test(item.label.trim()) && Number.isInteger(item.width) && item.width >= 240 && item.width <= 3840;
205
+ var previewDevices = (value) => Array.isArray(value) && value.length >= 1 && value.length <= 8 && value.every((item) => DEVICE_NAMES.includes(item) || customDevice(item)) && new Set(value.map((item) => typeof item === "string" ? item : item.width)).size === value.length;
206
+ var cleanDevices = (value) => value.map((item) => typeof item === "string" ? item : { label: item.label.trim(), width: item.width });
162
207
  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
208
  var TYPE_KEYS = {
164
209
  enabled: (value) => typeof value === "boolean",
165
210
  previewMode: (value) => PREVIEW_MODES.includes(value),
166
211
  sidebarPosition: (value) => SIDEBAR_POSITIONS.includes(value),
212
+ previewToolbar,
213
+ previewDevices,
167
214
  sidebar: (value, attributes) => Array.isArray(value) && value.length <= 12 && value.every((item) => sidebarItem(item, attributes))
168
215
  };
169
216
  var typeMap = (types) => Array.isArray(types) ? Object.fromEntries(types.map((uid) => [uid, null])) : types || {};
@@ -180,7 +227,8 @@ var require_settings = __commonJS({
180
227
  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
228
  retentionDays: days,
182
229
  maxSnapshots: (value) => Number.isInteger(value) && value >= 1 && value <= 1e3,
183
- eventDays: days
230
+ eventDays: days,
231
+ trashDays: days
184
232
  };
185
233
  var TYPOLOGIES2 = ["hero", "text", "media", "listing", "cards", "cta", "form", "layout"];
186
234
  var TAG = /^[^<>\n]{1,24}$/;
@@ -223,7 +271,7 @@ var require_settings = __commonJS({
223
271
  return [...facets];
224
272
  }
225
273
  var safeUrl2 = (value) => typeof value === "string" && (/^\/(?!\/)/.test(value) || /^https?:\/\//.test(value)) ? value : void 0;
226
- var { safeGroups: safeGroups2 } = require_groups();
274
+ var { safeGroups: safeGroups2, validLayout } = require_groups();
227
275
  var safeVersion = (value) => typeof value === "string" && /^[\w.-]{1,32}$/.test(value) ? value : void 0;
228
276
  var fail2 = (message, details) => {
229
277
  let ValidationError;
@@ -268,7 +316,7 @@ var require_settings = __commonJS({
268
316
  groups: safeGroups2(config.groups, config.componentUids)
269
317
  };
270
318
  }
271
- function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null) {
319
+ function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
272
320
  const types = typeMap(contentTypeUids2);
273
321
  if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
274
322
  if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
@@ -281,9 +329,9 @@ var require_settings = __commonJS({
281
329
  }
282
330
  for (const [key, value] of Object.entries(input.editor || {})) {
283
331
  if (!(key in DEFAULTS2.editor)) fail2(`Unknown editor option "${key}"`);
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";
332
+ 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) : key === "previewToolbar" ? previewToolbar(value) : key === "previewDevices" ? previewDevices(value) : typeof value === "boolean";
285
333
  if (!valid) fail2(`Invalid value for editor option "${key}"`);
286
- out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : value;
334
+ out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : key === "previewDevices" ? cleanDevices(value) : value;
287
335
  }
288
336
  const components = input.components || {};
289
337
  if (typeof components !== "object" || Array.isArray(components)) fail2("components must be an object");
@@ -304,6 +352,9 @@ var require_settings = __commonJS({
304
352
  } else if (key === "mediaId") {
305
353
  if (!Number.isInteger(entry.mediaId) || entry.mediaId <= 0) fail2(`Invalid media for "${uid}"`);
306
354
  clean.mediaId = entry.mediaId;
355
+ } else if (key === "layout") {
356
+ if (!validLayout(entry.layout, schemas && (schemas[uid]?.attributes || {}))) fail2(`Invalid layout for "${uid}": columnsField (and mobileColumnsField) must name a number, string or numeric enumeration attribute of the component; maxColumns 1 to 12`);
357
+ clean.layout = structuredClone(entry.layout);
307
358
  } else fail2(`Unknown component setting "${key}"`);
308
359
  }
309
360
  if (Object.keys(clean).length) out.components[uid] = clean;
@@ -317,7 +368,7 @@ var require_settings = __commonJS({
317
368
  if (!(key in TYPE_KEYS)) fail2(`Unknown content type setting "${key}"`);
318
369
  if (!TYPE_KEYS[key](value, types[uid])) fail2(`Invalid value for "${key}" of "${uid}"`);
319
370
  }
320
- if (Object.keys(entry).length) out.contentTypes[uid] = structuredClone(entry);
371
+ if (Object.keys(entry).length) out.contentTypes[uid] = { ...structuredClone(entry), ...entry.previewDevices && { previewDevices: cleanDevices(entry.previewDevices) } };
321
372
  }
322
373
  const history = input.history || {};
323
374
  if (typeof history !== "object" || Array.isArray(history)) fail2("history must be an object");
@@ -329,7 +380,7 @@ var require_settings = __commonJS({
329
380
  if (out.history.eventDays < out.history.retentionDays) fail2("history.eventDays must be at least history.retentionDays");
330
381
  return out;
331
382
  }
332
- function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null) {
383
+ function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
333
384
  const types = typeMap(contentTypeUids2);
334
385
  const out = structuredClone(DEFAULTS2);
335
386
  if (!saved || typeof saved !== "object") return out;
@@ -342,6 +393,8 @@ var require_settings = __commonJS({
342
393
  if (key === "hiddenBlocks" && !HIDDEN_MODES.includes(value)) continue;
343
394
  if (key === "previewUrl" && value && !PREVIEW_URL.test(value)) continue;
344
395
  if (key === "blockPreviewUrl" && !blockPreviewUrl(value)) continue;
396
+ if (key === "previewToolbar" && !previewToolbar(value)) continue;
397
+ if (key === "previewDevices" && !previewDevices(value)) continue;
345
398
  if (key === "lazyFields") {
346
399
  if (!Array.isArray(value)) continue;
347
400
  out.editor[key] = [...new Set(value.filter((uid) => lazyFields([uid])))].slice(0, 20);
@@ -357,6 +410,7 @@ var require_settings = __commonJS({
357
410
  clean.tags = cleanTags(clean.tags);
358
411
  if (!clean.tags.length) delete clean.tags;
359
412
  }
413
+ if ("layout" in clean && !validLayout(clean.layout, schemas && (schemas[uid]?.attributes || {}))) delete clean.layout;
360
414
  if (Object.keys(clean).length) out.components[uid] = clean;
361
415
  }
362
416
  for (const [uid, entry] of Object.entries(saved.contentTypes || {})) {
@@ -374,25 +428,28 @@ var require_settings = __commonJS({
374
428
  return out;
375
429
  }
376
430
  var ENTRY_KEYS = ["components", "contentTypes"];
431
+ var UNSET_WHEN_EMPTY = ["previewUrl", "blockPreviewUrl"];
432
+ var withoutEmpty = (editor) => Object.fromEntries(Object.entries(editor).filter(([key, value]) => !(UNSET_WHEN_EMPTY.includes(key) && value === "")));
377
433
  function layer2(base, saved) {
378
434
  if (!base) return saved;
379
435
  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] : {} }]));
436
+ const own = (key) => doc[key] && typeof doc[key] === "object" ? key === "editor" ? withoutEmpty(doc[key]) : doc[key] : {};
437
+ return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => [key, { ...base[key], ...own(key) }]));
381
438
  }
382
439
  function overrides2(next, base) {
383
440
  if (!base) return next;
384
441
  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]));
442
+ const entries = Object.entries(next[key] || {}).filter(([name, value]) => JSON.stringify(value) !== JSON.stringify(base[key]?.[name]) && !(key === "editor" && UNSET_WHEN_EMPTY.includes(name) && value === ""));
386
443
  if (ENTRY_KEYS.includes(key)) {
387
444
  for (const uid of Object.keys(base[key] || {})) if (!(uid in (next[key] || {}))) entries.push([uid, {}]);
388
445
  }
389
446
  return [key, Object.fromEntries(entries)];
390
447
  }));
391
448
  }
392
- var FIELD_TEXT = { label: 80, description: 300, placeholder: 120 };
449
+ var FIELD_TEXT = { label: 80, description: 300, placeholder: 120, help: 500 };
393
450
  var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
394
451
  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));
452
+ 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, text2]) => LOCALE.test(locale) && fieldText(text2, max));
396
453
  function validateFields2(input, schemas = {}, stale = []) {
397
454
  if (input === null || input === void 0) return {};
398
455
  if (!plain(input)) fail2("fields must be an object");
@@ -416,6 +473,87 @@ var require_settings = __commonJS({
416
473
  }
417
474
  return out;
418
475
  }
476
+ var VARIANT = { id: /^[a-z0-9][a-z0-9_-]{0,39}$/, perComponent: 12, bytes: 16 * 1024, total: 256 * 1024, items: 100 };
477
+ var text = (value) => typeof value === "string";
478
+ var finite = (value) => typeof value === "number" && Number.isFinite(value);
479
+ var SCALARS = {
480
+ string: text,
481
+ text,
482
+ richtext: text,
483
+ email: text,
484
+ uid: text,
485
+ date: text,
486
+ datetime: text,
487
+ time: text,
488
+ timestamp: text,
489
+ integer: Number.isInteger,
490
+ biginteger: (value) => Number.isInteger(value) || text(value) && /^-?\d{1,19}$/.test(value),
491
+ float: finite,
492
+ decimal: finite,
493
+ boolean: (value) => typeof value === "boolean",
494
+ json: () => true,
495
+ customField: () => true,
496
+ blocks: Array.isArray,
497
+ enumeration: (value, attr) => (attr.enum || []).includes(value)
498
+ };
499
+ function variantValues(values, schema, schemas, path, skipped) {
500
+ if (!plain(values)) throw new Error(`${path} must be an object`);
501
+ const out = {};
502
+ for (const [name, value] of Object.entries(values)) {
503
+ if (name === "id" || name === "__component") continue;
504
+ const attr = schema.attributes?.[name], at = `${path}.${name}`;
505
+ if (!attr) throw new Error(`${at} is not an attribute of the component`);
506
+ if (attr.type === "media" || attr.type === "relation") {
507
+ skipped.push(at);
508
+ continue;
509
+ }
510
+ if (attr.type === "component") {
511
+ const nested = schemas[attr.component], max = Math.min(attr.max ?? Infinity, VARIANT.items);
512
+ if (!nested) throw new Error(`${at}: unknown component "${attr.component}"`);
513
+ if (!attr.repeatable) out[name] = value === null ? null : variantValues(value, nested, schemas, at, skipped);
514
+ else if (!Array.isArray(value) || value.length > max) throw new Error(`${at} must be a list of at most ${max} items`);
515
+ else out[name] = value.map((item, index) => variantValues(item, nested, schemas, `${at}[${index}]`, skipped));
516
+ } else if (value === null || SCALARS[attr.type]?.(value, attr)) out[name] = structuredClone(value);
517
+ else throw new Error(`${at}: ${SCALARS[attr.type] ? `not a valid ${attr.type}` : `${attr.type} values are not supported`}`);
518
+ }
519
+ return out;
520
+ }
521
+ function validateVariants2(config, schemas = {}, notes = []) {
522
+ const out = {};
523
+ let total = 0;
524
+ for (const [uid, entry] of Object.entries(plain(config) ? config : {})) {
525
+ if (!plain(entry) || entry.variants === void 0) continue;
526
+ if (!schemas[uid]) {
527
+ notes.push(`${uid}: unknown component`);
528
+ continue;
529
+ }
530
+ if (!Array.isArray(entry.variants) || entry.variants.length > VARIANT.perComponent) {
531
+ notes.push(`${uid}: variants must be a list of at most ${VARIANT.perComponent}`);
532
+ continue;
533
+ }
534
+ const ids = /* @__PURE__ */ new Set();
535
+ entry.variants.forEach((variant, index) => {
536
+ const name = `${uid} variant "${variant?.id ?? index}"`;
537
+ try {
538
+ if (!plain(variant) || Object.keys(variant).some((key) => !["id", "label", "values"].includes(key))) throw new Error("expected { id, label, values }");
539
+ if (!text(variant.id) || !VARIANT.id.test(variant.id) || ids.has(variant.id)) throw new Error('id must be 1 to 40 lowercase letters, digits, "-" or "_", unique per component');
540
+ if (!fieldText(variant.label, 60)) throw new Error('label must be a string of 1 to 60 characters, or { "<locale>": string }');
541
+ const skipped = [];
542
+ const values = variantValues(variant.values ?? {}, schemas[uid], schemas, "values", skipped);
543
+ const size = JSON.stringify(values).length;
544
+ if (size > VARIANT.bytes) throw new Error(`values over ${VARIANT.bytes / 1024} KB`);
545
+ if (total + size > VARIANT.total) throw new Error(`all variants together over ${VARIANT.total / 1024} KB`);
546
+ if (skipped.length) notes.push(`${name}: media and relation values are not inserted (${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ", ..." : ""})`);
547
+ ids.add(variant.id);
548
+ total += size;
549
+ (out[uid] ||= []).push({ id: variant.id, label: structuredClone(variant.label), values });
550
+ } catch (error) {
551
+ notes.push(`${name} ignored: ${error.message}`);
552
+ }
553
+ });
554
+ }
555
+ return out;
556
+ }
419
557
  var PREFS = { starred: 200, recent: 20 };
420
558
  function validatePrefs2(input, componentUids2) {
421
559
  if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
@@ -428,7 +566,7 @@ var require_settings = __commonJS({
428
566
  return out;
429
567
  }
430
568
  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 };
569
+ 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, validateVariants: validateVariants2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
432
570
  }
433
571
  });
434
572
 
@@ -613,6 +751,453 @@ var require_diff = __commonJS({
613
751
  }
614
752
  });
615
753
 
754
+ // server/trash.js
755
+ var require_trash = __commonJS({
756
+ "server/trash.js"(exports2, module2) {
757
+ "use strict";
758
+ var { AsyncLocalStorage } = require("node:async_hooks");
759
+ var { PLUGIN: PLUGIN2 } = require_settings();
760
+ var { kept, canonical, stable } = require_diff();
761
+ var history = () => require_history();
762
+ var TRASH_UID = `plugin::${PLUGIN2}.trash`;
763
+ var STATUSES = ["trashed", "restored"];
764
+ var DAY = 24 * 60 * 60 * 1e3;
765
+ var BATCH = 500;
766
+ var restoring = new AsyncLocalStorage();
767
+ var trashContentType2 = { schema: {
768
+ kind: "collectionType",
769
+ collectionName: "blockscene_trash",
770
+ info: { singularName: "trash", pluralName: "trashes", displayName: "Blockscene trash" },
771
+ options: { draftAndPublish: false },
772
+ pluginOptions: { "content-manager": { visible: false }, "content-type-builder": { visible: false }, i18n: { localized: false } },
773
+ attributes: {
774
+ contentType: { type: "string", required: true },
775
+ relatedDocumentId: { type: "string", required: true },
776
+ // The document's main field (Content Manager setting) at delete time, for the list and the search.
777
+ title: { type: "string" },
778
+ // The locales the delete removed ([null] for a type without i18n).
779
+ locales: { type: "json" },
780
+ // [{ locale, status: 'draft' | 'published' | null (no Draft & Publish), snapshot }]: canonical snapshots (server/diff.js).
781
+ entries: { type: "json" },
782
+ // Links other documents' and blocks' unidirectional relations had to the deleted rows (see `incomingRows`).
783
+ incoming: { type: "json" },
784
+ actor: { type: "string", required: true },
785
+ actorName: { type: "string" },
786
+ deletedAt: { type: "datetime", required: true },
787
+ // deletedAt + trash retention at delete time; the nightly purge deletes the row after it.
788
+ expiresAt: { type: "datetime", required: true },
789
+ status: { type: "enumeration", enum: STATUSES, required: true, default: "trashed" },
790
+ restoredAs: { type: "string" },
791
+ restoredAt: { type: "datetime" },
792
+ size: { type: "integer" }
793
+ },
794
+ indexes: [{ name: "blockscene_trash_list", columns: ["status", "content_type", "deleted_at"] }, { name: "blockscene_trash_expiry", columns: ["expires_at"] }]
795
+ } };
796
+ var expiresAt = (deletedAt, days) => new Date(new Date(deletedAt).getTime() + days * DAY);
797
+ var isObject = (value) => value && typeof value === "object" && !Array.isArray(value);
798
+ var blank = (value) => value === null || value === void 0 || value === "" || Array.isArray(value) && !value.length;
799
+ var toMany = (attr) => /ToMany$|^manyWay$/.test(String(attr?.relation || ""));
800
+ var toOne = (attr) => ["oneToOne", "manyToOne"].includes(attr?.relation);
801
+ var uniqueFields = (schema) => Object.entries(schema?.attributes || {}).filter(([name, attr]) => kept(name, attr) && (attr.unique === true || attr.type === "uid")).map(([name]) => name);
802
+ function chooseEntries(entries) {
803
+ const chosen = /* @__PURE__ */ new Map();
804
+ for (const entry of Array.isArray(entries) ? entries : []) {
805
+ const locale = entry?.locale ?? null, current = chosen.get(locale);
806
+ if (!current || current.status === "published" && entry.status !== "published") chosen.set(locale, entry);
807
+ }
808
+ return chosen;
809
+ }
810
+ function restorePayload(snapshot, schema, schemaOf, { media = /* @__PURE__ */ new Set(), relation = () => false } = {}) {
811
+ const report = { removed: [], media: 0, relations: 0, blocks: 0 };
812
+ const ref = (item) => ({ documentId: item.documentId, ...item.locale && { locale: item.locale } });
813
+ const build = (data, schema2, top, depth) => {
814
+ const out = {};
815
+ if (!isObject(data) || depth > 20) return out;
816
+ for (const [name, value] of Object.entries(data)) {
817
+ if (name === "__component") continue;
818
+ const attr = schema2?.attributes?.[name];
819
+ const drop = () => {
820
+ if (top) report.removed.push(name);
821
+ };
822
+ if (!kept(name, attr)) {
823
+ drop();
824
+ continue;
825
+ }
826
+ const list = Array.isArray(value) ? value : value == null ? [] : [value];
827
+ if (attr.type === "relation") {
828
+ const refs = list.filter((item) => isObject(item) && typeof item.documentId === "string");
829
+ const found = refs.filter((item) => relation(attr.target, item));
830
+ report.relations += refs.length - found.length;
831
+ out[name] = toMany(attr) ? found.map(ref) : found[0] ? ref(found[0]) : null;
832
+ } else if (attr.type === "media") {
833
+ const ids = list.filter(Number.isInteger), found = ids.filter((id) => media.has(id));
834
+ report.media += ids.length - found.length;
835
+ out[name] = attr.multiple ? found : found[0] ?? null;
836
+ } else if (attr.type === "component") {
837
+ const child = schemaOf(attr.component);
838
+ if (!child) {
839
+ drop();
840
+ continue;
841
+ }
842
+ out[name] = attr.repeatable ? list.filter(isObject).map((item) => build(item, child, false, depth + 1)) : isObject(value) ? build(value, child, false, depth + 1) : null;
843
+ } else if (attr.type === "dynamiczone") {
844
+ const rows = list.filter((row) => isObject(row) && typeof row.__component === "string");
845
+ const allowed = rows.filter((row) => (attr.components || []).includes(row.__component) && schemaOf(row.__component));
846
+ report.blocks += rows.length - allowed.length;
847
+ out[name] = allowed.map((row) => ({ __component: row.__component, ...build(row, schemaOf(row.__component), false, depth + 1) }));
848
+ } else if (attr.type === "enumeration" && !(attr.enum || []).includes(value)) drop();
849
+ else out[name] = value;
850
+ }
851
+ return out;
852
+ };
853
+ return { data: build(snapshot, schema, true, 0), report };
854
+ }
855
+ function planRestore(entries, { schema, schemaOf, locales = null, defaultLocale = null, existing = null, singleTaken = false, taken = () => false, media, relation }) {
856
+ const blocking = [], warnings = [], steps = [];
857
+ const published = (Array.isArray(entries) ? entries : []).filter((entry) => entry?.status === "published").map((entry) => entry.locale ?? null);
858
+ if (published.length) warnings.push({ code: "published", locales: published });
859
+ if (singleTaken && !existing) blocking.push({ code: "singleExists" });
860
+ for (const [locale, entry] of chooseEntries(entries)) {
861
+ if (locales && !locales.includes(locale)) {
862
+ warnings.push({ code: "localeRemoved", locale });
863
+ continue;
864
+ }
865
+ if (existing?.includes(locale)) {
866
+ blocking.push({ code: "localeExists", locale });
867
+ continue;
868
+ }
869
+ const { data, report } = restorePayload(entry.snapshot, schema, schemaOf, { media, relation });
870
+ for (const field of uniqueFields(schema)) if (!blank(data[field]) && taken(field, locale, data[field])) blocking.push({ code: "unique", locale, field, value: String(data[field]).slice(0, 100) });
871
+ const required = Object.entries(schema?.attributes || {}).filter(([name, attr]) => attr?.required && kept(name, attr) && blank(data[name])).map(([name]) => name);
872
+ if (required.length) warnings.push({ code: "required", locale, fields: required });
873
+ if (report.removed.length) warnings.push({ code: "fieldsRemoved", locale, fields: report.removed });
874
+ for (const code of ["media", "relations", "blocks"]) if (report[code]) warnings.push({ code, locale, count: report[code] });
875
+ steps.push({ locale, data });
876
+ }
877
+ if (!steps.length && !blocking.length) blocking.push({ code: "nothing" });
878
+ steps.sort((a, b) => (b.locale === defaultLocale) - (a.locale === defaultLocale));
879
+ return { blocking, warnings, steps };
880
+ }
881
+ var incomingSources = (uid, models) => models.flatMap((model) => Object.entries(model.attributes || {}).filter(([, attr]) => attr?.type === "relation" && attr.target === uid && !attr.inversedBy && !attr.mappedBy && attr.joinTable).map(([field, attr]) => ({ owner: model.uid, kind: model.kind, field, joinTable: attr.joinTable, single: toOne(attr) })));
882
+ function incomingRows(source, links, deleted, owners = /* @__PURE__ */ new Map()) {
883
+ const { joinColumn, inverseJoinColumn, orderColumnName } = source.joinTable;
884
+ return links.filter((link) => deleted.has(link[inverseJoinColumn.name]) && !(source.kind === "contentType" && source.owner === source.target && deleted.has(link[joinColumn.name]))).map((link) => {
885
+ const target = deleted.get(link[inverseJoinColumn.name]), owner = owners.get(link[joinColumn.name]);
886
+ return {
887
+ owner: source.owner,
888
+ kind: source.kind,
889
+ field: source.field,
890
+ ownerId: link[joinColumn.name],
891
+ ...source.kind === "contentType" && { ownerDocumentId: owner?.documentId ?? null, ownerLocale: owner?.locale ?? null },
892
+ locale: target.locale ?? null,
893
+ published: Boolean(target.published),
894
+ order: orderColumnName ? link[orderColumnName] ?? null : null
895
+ };
896
+ });
897
+ }
898
+ function previewOf(snapshot, schema, schemaOf) {
899
+ const fields = [], blocks = [];
900
+ const text = (value) => {
901
+ const s = typeof value === "string" ? value : stable(value);
902
+ return s.length > 200 ? `${s.slice(0, 199)}\u2026` : s;
903
+ };
904
+ for (const [name, value] of Object.entries(isObject(snapshot) ? snapshot : {})) {
905
+ const attr = schema?.attributes?.[name];
906
+ if (attr?.type === "dynamiczone") {
907
+ for (const row of Array.isArray(value) ? value : []) blocks.push({ zone: name, uid: row?.__component, name: schemaOf(row?.__component)?.info?.displayName || row?.__component });
908
+ continue;
909
+ }
910
+ const count = Array.isArray(value) ? value.length : 1;
911
+ fields.push({ name, type: attr?.type || "removed", ...["relation", "media", "component"].includes(attr?.type) ? { count } : { value: text(value) } });
912
+ }
913
+ return { fields, blocks };
914
+ }
915
+ function trashService2({ strapi }) {
916
+ const events = () => strapi.plugin(PLUGIN2).service("history");
917
+ const rows = () => strapi.db.query(TRASH_UID);
918
+ const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
919
+ const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
920
+ const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
921
+ const models = () => [...Object.values(strapi.contentTypes || {}).map((model) => ({ uid: model.uid, kind: "contentType" })), ...Object.values(strapi.components || {}).map((model) => ({ uid: model.uid, kind: "component" }))].map((model) => ({ ...model, attributes: strapi.db.metadata.get(model.uid)?.attributes })).filter((model) => model.attributes);
922
+ const table = (trx, name) => strapi.db.getConnection(name).transacting(trx);
923
+ const lookups = async (uid, snapshots) => {
924
+ const ids = /* @__PURE__ */ new Set(), refs = [];
925
+ for (const snapshot of snapshots) {
926
+ const found = history().refsOf(snapshot, schemaOf(uid), schemaOf);
927
+ found.media.forEach((id) => ids.add(id));
928
+ refs.push(...found.relations);
929
+ }
930
+ const files = ids.size ? await strapi.db.query("plugin::upload.file").findMany({ select: ["id"], where: { id: { $in: [...ids] } } }) : [];
931
+ const present = /* @__PURE__ */ new Set();
932
+ for (const target of [...new Set(refs.map((ref) => ref.target))]) {
933
+ if (!strapi.contentTypes?.[target]) continue;
934
+ const found = await strapi.db.query(target).findMany({ select: ["documentId", "locale"], where: {
935
+ documentId: { $in: [...new Set(refs.filter((ref) => ref.target === target).map((ref) => ref.documentId))] },
936
+ ...draftAndPublish(target) && { publishedAt: { $null: true } }
937
+ } });
938
+ for (const row of found) {
939
+ present.add(`${target}|${row.documentId}|${row.locale || ""}`);
940
+ present.add(`${target}|${row.documentId}|*`);
941
+ }
942
+ }
943
+ return { media: new Set(files.map((file) => file.id)), relation: (target, ref) => present.has(`${target}|${ref.documentId}|${localized(target) && ref.locale ? ref.locale : "*"}`) };
944
+ };
945
+ const resolveIncoming = async (uid, incoming, locales, trx) => {
946
+ const out = { links: [], gone: 0, published: 0, replaced: 0 };
947
+ for (const link of Array.isArray(incoming) ? incoming : []) {
948
+ if (!locales.includes(link.locale)) continue;
949
+ if (link.published) {
950
+ out.published++;
951
+ continue;
952
+ }
953
+ const attr = strapi.db.metadata.get(link.owner)?.attributes?.[link.field];
954
+ if (!attr?.joinTable || attr.target !== uid) {
955
+ out.gone++;
956
+ continue;
957
+ }
958
+ let ownerId = null;
959
+ if (link.kind === "component") ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: { id: link.ownerId } }))?.id ?? null;
960
+ else if (strapi.contentTypes?.[link.owner] && link.ownerDocumentId) ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: {
961
+ documentId: link.ownerDocumentId,
962
+ ...localized(link.owner) && { locale: link.ownerLocale },
963
+ ...draftAndPublish(link.owner) && { publishedAt: { $null: true } }
964
+ } }))?.id ?? null;
965
+ if (!ownerId) {
966
+ out.gone++;
967
+ continue;
968
+ }
969
+ const { joinTable } = attr;
970
+ if (toOne(attr) && await table(trx, joinTable.name).where(joinTable.joinColumn.name, ownerId).first()) {
971
+ out.replaced++;
972
+ continue;
973
+ }
974
+ out.links.push({ ...link, ownerRowId: ownerId, joinTable });
975
+ }
976
+ return out;
977
+ };
978
+ const self = {
979
+ TRASH_UID,
980
+ // Before the delete, in its transaction: the links other documents and blocks have to the rows about to be deleted.
981
+ async incoming(uid, deleted) {
982
+ if (!deleted.size) return [];
983
+ return strapi.db.transaction(async ({ trx }) => {
984
+ const out = [];
985
+ for (const source of incomingSources(uid, models())) {
986
+ const links = await table(trx, source.joinTable.name).select("*").whereIn(source.joinTable.inverseJoinColumn.name, [...deleted.keys()]);
987
+ if (!links.length) continue;
988
+ const owners = source.kind === "contentType" && strapi.contentTypes?.[source.owner] ? new Map((await strapi.db.query(source.owner).findMany({
989
+ select: ["id", "documentId", "locale"],
990
+ where: { id: { $in: [...new Set(links.map((link) => link[source.joinTable.joinColumn.name]))] } }
991
+ })).map((row) => [row.id, row])) : /* @__PURE__ */ new Map();
992
+ out.push(...incomingRows({ ...source, target: uid }, links, deleted, owners));
993
+ }
994
+ return out;
995
+ });
996
+ },
997
+ // After the delete, in its transaction: the trash row. drafts/published: the stored rows read before the delete.
998
+ async record({ uid, documentId, locales, drafts, published, incoming, actor, days }) {
999
+ const dp = draftAndPublish(uid);
1000
+ const entries = [...drafts.map((row) => [row, dp ? "draft" : null]), ...published.map((row) => [row, "published"])].map(([row, status]) => ({ locale: row.locale ?? null, status, snapshot: canonical(row, schemaOf(uid), schemaOf) }));
1001
+ const main = await events().mainField(uid);
1002
+ const first = chooseEntries(entries);
1003
+ const source = first.get(await events().scope(uid).then((list) => list?.[0] ?? null).catch(() => null)) || [...first.values()][0];
1004
+ const title = main && source?.snapshot?.[main] != null ? String(source.snapshot[main]).slice(0, 255) : null;
1005
+ const deletedAt = /* @__PURE__ */ new Date();
1006
+ return rows().create({ data: {
1007
+ contentType: uid,
1008
+ relatedDocumentId: documentId,
1009
+ title,
1010
+ locales: locales || [...new Set(entries.map((entry) => entry.locale))],
1011
+ entries,
1012
+ incoming,
1013
+ ...actor,
1014
+ deletedAt,
1015
+ expiresAt: expiresAt(deletedAt, days),
1016
+ status: "trashed",
1017
+ size: Buffer.byteLength(stable({ entries, incoming }))
1018
+ } });
1019
+ },
1020
+ // One page of trash rows, without their snapshots. `types`: the content types the user may read.
1021
+ async list({ contentType, status = "trashed", page = 1, pageSize = 20, types }) {
1022
+ const where = { status, contentType: contentType ? contentType : { $in: types } };
1023
+ if (contentType && !types.includes(contentType)) return { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 } };
1024
+ const [results, total] = await Promise.all([
1025
+ rows().findMany({
1026
+ select: ["id", "contentType", "relatedDocumentId", "title", "locales", "actor", "actorName", "deletedAt", "expiresAt", "status", "restoredAs", "restoredAt"],
1027
+ where,
1028
+ orderBy: [{ deletedAt: "desc" }, { id: "desc" }],
1029
+ offset: (page - 1) * pageSize,
1030
+ limit: pageSize
1031
+ }),
1032
+ rows().count({ where })
1033
+ ]);
1034
+ return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) } };
1035
+ },
1036
+ async find(id) {
1037
+ return rows().findOne({ where: { id } });
1038
+ },
1039
+ // One row for the preview: fields and blocks of each entry, and how many links it held.
1040
+ preview(row) {
1041
+ const schema = schemaOf(row.contentType);
1042
+ const { entries, incoming, ...meta } = row;
1043
+ return {
1044
+ ...meta,
1045
+ incoming: (incoming || []).length,
1046
+ entries: (entries || []).map((entry) => ({ locale: entry.locale ?? null, status: entry.status ?? null, ...previewOf(entry.snapshot, schema, schemaOf) }))
1047
+ };
1048
+ },
1049
+ // The pre-check: every lookup a restore needs, then planRestore. Read-only; the restore runs it again in its transaction.
1050
+ async plan(row) {
1051
+ const uid = row.contentType, schema = schemaOf(uid);
1052
+ if (!schema || !strapi.contentTypes?.[uid]) return { blocking: [{ code: "typeGone" }], warnings: [], steps: [], incoming: { links: [], gone: 0, published: 0, replaced: 0 } };
1053
+ const i18n = localized(uid);
1054
+ const locales = i18n ? (await strapi.plugin("i18n").service("locales").find()).map((locale) => locale.code) : null;
1055
+ const defaultLocale = i18n ? await strapi.plugin("i18n").service("locales").getDefaultLocale() : null;
1056
+ const current = await strapi.db.query(uid).findMany({ select: ["locale"], where: { documentId: row.relatedDocumentId } });
1057
+ const existing = current.length ? [...new Set(current.map((entry) => i18n ? entry.locale : null))] : null;
1058
+ const singleTaken = schema.kind === "singleType" && !existing && Boolean(await strapi.db.query(uid).findOne({ select: ["id"] }));
1059
+ const chosen = [...chooseEntries(row.entries).values()];
1060
+ const { media, relation } = await lookups(uid, chosen.map((entry) => entry.snapshot));
1061
+ const taken = /* @__PURE__ */ new Set();
1062
+ for (const entry of chosen) for (const field of uniqueFields(schema)) {
1063
+ const value = entry.snapshot?.[field];
1064
+ if (blank(value) || typeof value === "object") continue;
1065
+ const other = await strapi.db.query(uid).findOne({ select: ["id"], where: { [field]: value, ...i18n && { locale: entry.locale }, ...existing && { documentId: { $ne: row.relatedDocumentId } } } });
1066
+ if (other) taken.add(stable([field, entry.locale ?? null, value]));
1067
+ }
1068
+ const plan = planRestore(row.entries, { schema, schemaOf, locales, defaultLocale, existing, singleTaken, media, relation, taken: (field, locale, value) => taken.has(stable([field, locale ?? null, value])) });
1069
+ const incoming = await strapi.db.transaction(({ trx }) => resolveIncoming(uid, row.incoming, plan.steps.map((step) => step.locale), trx));
1070
+ return { ...plan, documentId: existing ? row.relatedDocumentId : null, incoming };
1071
+ },
1072
+ // What the pre-check endpoint returns: the plan without the data it would write.
1073
+ async check(row) {
1074
+ if (row.status !== "trashed") return { status: row.status, restoredAs: row.restoredAs, blocking: [{ code: "restored" }], warnings: [] };
1075
+ const { steps, incoming: { links, ...incoming }, ...plan } = await self.plan(row);
1076
+ return { ...plan, locales: steps.map((step) => step.locale), incoming: { ...incoming, reconnect: links.length } };
1077
+ },
1078
+ // Restore as a draft, in one transaction: claim the row (a second restore of the same row, concurrent or later, gets
1079
+ // `already` with the first one's document), check again, recreate (default locale first, the others under the same
1080
+ // documentId), put the links back, record `restore` events. A blocking problem throws `conflict` and nothing is written.
1081
+ async restore(id, { actor, userId }) {
1082
+ return restoring.run(true, () => strapi.db.transaction(async ({ trx }) => {
1083
+ const claimed = await rows().updateMany({ where: { id, status: "trashed" }, data: { status: "restored", restoredAt: /* @__PURE__ */ new Date() } });
1084
+ const row = await rows().findOne({ where: { id } });
1085
+ if (!row) return null;
1086
+ if (!claimed?.count) return { already: true, documentId: row.restoredAs || null };
1087
+ const plan = await self.plan(row);
1088
+ if (plan.blocking.length) {
1089
+ const error = new Error("conflict");
1090
+ error.conflict = { blocking: plan.blocking, warnings: plan.warnings };
1091
+ throw error;
1092
+ }
1093
+ const uid = row.contentType, documents = strapi.documents(uid);
1094
+ const creator = userId ? { createdBy: userId, updatedBy: userId } : {};
1095
+ let documentId = plan.documentId;
1096
+ for (const { locale, data } of plan.steps) {
1097
+ const params = { ...locale && { locale }, data: { ...data, ...creator } };
1098
+ if (documentId) await documents.update({ documentId, ...params });
1099
+ else documentId = (await documents.create(params)).documentId;
1100
+ }
1101
+ const restored = await events().rows(uid, documentId, localized(uid) ? plan.steps.map((step) => step.locale) : null);
1102
+ const rowOf = new Map(restored.map((entry) => [entry.locale ?? null, entry]));
1103
+ for (const link of plan.incoming.links) {
1104
+ const target = rowOf.get(link.locale);
1105
+ if (!target) continue;
1106
+ const { joinColumn, inverseJoinColumn, orderColumnName } = link.joinTable;
1107
+ await table(trx, link.joinTable.name).insert({ [joinColumn.name]: link.ownerRowId, [inverseJoinColumn.name]: target.id, ...orderColumnName && link.order != null && { [orderColumnName]: link.order } });
1108
+ }
1109
+ for (const entry of restored) await events().record({ uid, documentId, locale: entry.locale ?? null, action: "restore", entry, actor, extra: { restoredFrom: row.relatedDocumentId, trash: row.id } });
1110
+ await rows().update({ where: { id }, data: { restoredAs: documentId } });
1111
+ const { links, ...incoming } = plan.incoming;
1112
+ return { documentId, locales: plan.steps.map((step) => step.locale), warnings: plan.warnings, incoming: { ...incoming, reconnected: links.length } };
1113
+ }));
1114
+ },
1115
+ // Delete forever (one row), recorded as a `purge` event. Never touches media files.
1116
+ async remove(row, actor) {
1117
+ return strapi.db.transaction(async () => {
1118
+ const { count } = await rows().deleteMany({ where: { id: row.id } });
1119
+ if (count) await events().record({ uid: row.contentType, documentId: row.relatedDocumentId, action: "purge", actor, extra: { trash: row.id, title: row.title } });
1120
+ return count;
1121
+ });
1122
+ },
1123
+ // Nightly, in batches, idempotent: rows past expiresAt are deleted (restored ones too). Never touches media files.
1124
+ async purge(now = Date.now()) {
1125
+ let count = 0;
1126
+ for (; ; ) {
1127
+ const ids = (await rows().findMany({ select: ["id"], where: { expiresAt: { $lt: new Date(now) } }, orderBy: { id: "asc" }, limit: BATCH })).map((row) => row.id);
1128
+ if (ids.length) await rows().deleteMany({ where: { id: { $in: ids } } });
1129
+ count += ids.length;
1130
+ if (ids.length < BATCH) return count;
1131
+ }
1132
+ }
1133
+ };
1134
+ return self;
1135
+ }
1136
+ var ID = /^[1-9]\d{0,15}$/;
1137
+ var API_UID = /^api::[\w-]+\.[\w-]+$/;
1138
+ var int = (value, fallback, max) => {
1139
+ if (value === void 0 || value === "") return fallback;
1140
+ const n = Number(value);
1141
+ return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
1142
+ };
1143
+ var trashController2 = ({ strapi }) => {
1144
+ const service = () => strapi.plugin(PLUGIN2).service("trash");
1145
+ const { can, readableTypes, actorOf } = history();
1146
+ const load = async (ctx, action = "read") => {
1147
+ if (!ID.test(String(ctx.params?.id || ""))) {
1148
+ ctx.badRequest("Invalid trash entry");
1149
+ return null;
1150
+ }
1151
+ const row = await service().find(Number(ctx.params.id));
1152
+ if (!row || !can(strapi, ctx, row.contentType, "read") || action !== "read" && !can(strapi, ctx, row.contentType, action)) {
1153
+ ctx.notFound();
1154
+ return null;
1155
+ }
1156
+ return row;
1157
+ };
1158
+ return {
1159
+ async list(ctx) {
1160
+ const { contentType = "", status = "trashed" } = ctx.query || {};
1161
+ const page = int(ctx.query?.page, 1, 1e4), pageSize = int(ctx.query?.pageSize, 20, 100);
1162
+ if (typeof contentType !== "string" || contentType && !API_UID.test(contentType) || !STATUSES.includes(status) || Number.isNaN(page) || Number.isNaN(pageSize)) return ctx.badRequest("Invalid filters");
1163
+ const types = readableTypes(strapi, ctx);
1164
+ ctx.body = {
1165
+ ...await service().list({ contentType, status, page, pageSize, types }),
1166
+ types: types.map((uid) => ({ uid, displayName: strapi.contentTypes[uid].info?.displayName || uid, kind: strapi.contentTypes[uid].kind, localized: Boolean(strapi.contentTypes[uid].pluginOptions?.i18n?.localized) }))
1167
+ };
1168
+ },
1169
+ async find(ctx) {
1170
+ const row = await load(ctx);
1171
+ if (row) ctx.body = service().preview(row);
1172
+ },
1173
+ async check(ctx) {
1174
+ const row = await load(ctx, "create");
1175
+ if (row) ctx.body = await service().check(row);
1176
+ },
1177
+ async restore(ctx) {
1178
+ const row = await load(ctx, "create");
1179
+ if (!row) return;
1180
+ try {
1181
+ const result = await service().restore(row.id, { actor: actorOf(strapi), userId: ctx.state?.user?.id });
1182
+ if (!result) return ctx.notFound();
1183
+ ctx.body = result;
1184
+ } catch (error) {
1185
+ if (!error?.conflict) throw error;
1186
+ ctx.status = 409;
1187
+ ctx.body = { data: null, error: { status: 409, name: "ConflictError", message: "The document cannot be restored as it is: nothing was written.", details: error.conflict } };
1188
+ }
1189
+ },
1190
+ async remove(ctx) {
1191
+ const row = await load(ctx, "delete");
1192
+ if (!row) return;
1193
+ ctx.body = { deleted: await service().remove(row, actorOf(strapi)) };
1194
+ }
1195
+ };
1196
+ };
1197
+ module2.exports = { TRASH_UID, STATUSES, restoring, trashContentType: trashContentType2, expiresAt, uniqueFields, chooseEntries, restorePayload, planRestore, incomingSources, incomingRows, previewOf, trashService: trashService2, trashController: trashController2 };
1198
+ }
1199
+ });
1200
+
616
1201
  // server/history.js
617
1202
  var require_history = __commonJS({
618
1203
  "server/history.js"(exports2, module2) {
@@ -620,13 +1205,16 @@ var require_history = __commonJS({
620
1205
  var { createHash } = require("node:crypto");
621
1206
  var { PLUGIN: PLUGIN2 } = require_settings();
622
1207
  var { snapshotPopulate, canonical, stable, summarize } = require_diff();
1208
+ var { TRASH_UID, restoring } = require_trash();
623
1209
  var EVENT_UID = `plugin::${PLUGIN2}.event`;
624
- var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore"];
1210
+ var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore", "purge"];
625
1211
  var CAPTURED = { create: "create", clone: "create", update: "update", publish: "publish", unpublish: "unpublish", discardDraft: "discard", delete: "delete" };
626
1212
  var CRON2 = "blocksceneHistoryPurge";
627
1213
  var BATCH = 500;
628
1214
  var DAY = 24 * 60 * 60 * 1e3;
629
1215
  var LIST_LIMIT = 100;
1216
+ var ACTORS_LIMIT = 500;
1217
+ var SEARCH_LIMIT = 200;
630
1218
  var eventContentType2 = { schema: {
631
1219
  kind: "collectionType",
632
1220
  collectionName: "blockscene_events",
@@ -642,7 +1230,8 @@ var require_history = __commonJS({
642
1230
  actor: { type: "string", required: true },
643
1231
  actorName: { type: "string" },
644
1232
  at: { type: "datetime", required: true },
645
- // { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed.
1233
+ // { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed;
1234
+ // restore: + { restoredFrom, trash }; purge: { trash, title }.
646
1235
  summary: { type: "json" },
647
1236
  // Null when identical to the previous snapshot of the same locale (same hash) or purged.
648
1237
  snapshot: { type: "json" },
@@ -705,6 +1294,11 @@ var require_history = __commonJS({
705
1294
  const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
706
1295
  const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
707
1296
  const events = () => strapi.db.query(EVENT_UID);
1297
+ const actors = async () => {
1298
+ const meta = strapi.db.metadata.get(EVENT_UID);
1299
+ const column = (name) => meta.attributes[name].columnName || name;
1300
+ return strapi.db.connection(meta.tableName).select({ actor: column("actor") }).max({ actorName: column("actorName") }).groupBy(column("actor")).orderBy(column("actor")).limit(ACTORS_LIMIT);
1301
+ };
708
1302
  const scope = async (uid, locale) => {
709
1303
  if (!localized(uid)) return [null];
710
1304
  if (locale === "*") return null;
@@ -727,7 +1321,7 @@ var require_history = __commonJS({
727
1321
  },
728
1322
  covers: covers2,
729
1323
  // 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 }) {
1324
+ async record({ uid, documentId, locale = null, action, entry, actor, missing = false, extra = null }) {
731
1325
  let summary = missing ? { missing: true } : null, snapshot = null, hash = null, size = 0;
732
1326
  if (entry) {
733
1327
  const content = canonical(entry, schemaOf(uid), schemaOf);
@@ -740,6 +1334,7 @@ var require_history = __commonJS({
740
1334
  size = Buffer.byteLength(text);
741
1335
  }
742
1336
  }
1337
+ if (extra) summary = { ...summary, ...extra };
743
1338
  return events().create({ data: { contentType: uid, relatedDocumentId: documentId, locale, action, ...actor, at: /* @__PURE__ */ new Date(), summary, snapshot, hash, size } });
744
1339
  },
745
1340
  // After a save or publish: one event per affected entry (drafts, or the published versions after a publish).
@@ -749,6 +1344,16 @@ var require_history = __commonJS({
749
1344
  },
750
1345
  scope,
751
1346
  rows,
1347
+ // The Content Manager's main field of a content type when it is a plain value (what list views show), else null.
1348
+ async mainField(uid) {
1349
+ let name = null;
1350
+ try {
1351
+ name = (await strapi.plugin("content-manager").service("content-types").findConfiguration(strapi.contentTypes[uid]))?.settings?.mainField;
1352
+ } catch {
1353
+ name = null;
1354
+ }
1355
+ return name && name !== "id" && schemaOf(uid)?.attributes?.[name] && !["relation", "media", "component", "dynamiczone", "password", "json", "blocks"].includes(schemaOf(uid).attributes[name].type) ? name : null;
1356
+ },
752
1357
  // Latest events of one document and locale, newest first; `stored`: its snapshot (or an identical one) can be loaded.
753
1358
  async list({ uid, documentId, locale }) {
754
1359
  const where = { contentType: uid, relatedDocumentId: documentId, locale: locale || null };
@@ -772,13 +1377,7 @@ var require_history = __commonJS({
772
1377
  const relations = {};
773
1378
  for (const target of [...new Set(refs.relations.map((ref) => ref.target))]) {
774
1379
  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";
1380
+ const mainField = await self.mainField(target), scalar = Boolean(mainField);
782
1381
  const found = await strapi.db.query(target).findMany({
783
1382
  select: ["id", "documentId", "locale", ...scalar ? [mainField] : []],
784
1383
  where: { documentId: { $in: [...new Set(refs.relations.filter((ref) => ref.target === target).map((ref) => ref.documentId))] }, ...draftAndPublish(target) && { publishedAt: { $null: true } } }
@@ -787,6 +1386,56 @@ var require_history = __commonJS({
787
1386
  }
788
1387
  return { event: meta, snapshot, media: Object.fromEntries(files.map((file) => [file.id, file])), relations };
789
1388
  },
1389
+ // Content activity: one page of events (no snapshots), newest first, with each document's title and whether it still
1390
+ // exists (the list links to it). `types`: the content types the user may read. `q`: documents whose main field
1391
+ // contains it now, or whose trash entry's title does (at most SEARCH_LIMIT per content type).
1392
+ async activity({ actor, contentType, action, from, to, q, page, pageSize, types }) {
1393
+ const empty = { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 }, actors: [] };
1394
+ if (contentType && !types.includes(contentType)) return empty;
1395
+ const where = {
1396
+ contentType: contentType || { $in: types },
1397
+ ...actor && { actor },
1398
+ ...action && { action },
1399
+ ...(from || to) && { at: { ...from && { $gte: from }, ...to && { $lte: to } } }
1400
+ };
1401
+ if (q) {
1402
+ const matches = [];
1403
+ for (const uid of contentType ? [contentType] : types) {
1404
+ const main = await self.mainField(uid);
1405
+ const found = new Set(main ? (await strapi.db.query(uid).findMany({ select: ["documentId"], where: { [main]: { $containsi: q } }, limit: SEARCH_LIMIT })).map((row) => row.documentId) : []);
1406
+ for (const row of await strapi.db.query(TRASH_UID).findMany({ select: ["relatedDocumentId"], where: { contentType: uid, title: { $containsi: q } }, limit: SEARCH_LIMIT })) found.add(row.relatedDocumentId);
1407
+ if (found.size) matches.push({ contentType: uid, relatedDocumentId: { $in: [...found] } });
1408
+ }
1409
+ if (!matches.length) return { ...empty, actors: await actors() };
1410
+ where.$or = matches;
1411
+ }
1412
+ const [list, total] = await Promise.all([
1413
+ events().findMany({ select: ["id", "contentType", "relatedDocumentId", "locale", "action", "actor", "actorName", "at", "summary"], where, orderBy: [{ at: "desc" }, { id: "desc" }], offset: (page - 1) * pageSize, limit: pageSize }),
1414
+ events().count({ where })
1415
+ ]);
1416
+ const docs = /* @__PURE__ */ new Map();
1417
+ for (const uid of [...new Set(list.map((event) => event.contentType))]) {
1418
+ const ids = [...new Set(list.filter((event) => event.contentType === uid).map((event) => event.relatedDocumentId))];
1419
+ const main = strapi.contentTypes?.[uid] ? await self.mainField(uid) : null;
1420
+ const found = strapi.contentTypes?.[uid] ? await strapi.db.query(uid).findMany({ select: ["documentId", "locale", ...main ? [main] : []], where: { documentId: { $in: ids } } }) : [];
1421
+ for (const row of found) {
1422
+ const key = `${uid}|${row.documentId}`;
1423
+ docs.set(key, [...docs.get(key) || [], { locale: row.locale ?? null, title: main && row[main] != null ? String(row[main]) : null }]);
1424
+ }
1425
+ const gone = ids.filter((id) => !docs.has(`${uid}|${id}`));
1426
+ if (gone.length) {
1427
+ for (const row of await strapi.db.query(TRASH_UID).findMany({ select: ["relatedDocumentId", "title"], where: { contentType: uid, relatedDocumentId: { $in: gone } }, orderBy: { id: "desc" } }))
1428
+ if (!docs.has(`trash|${uid}|${row.relatedDocumentId}`)) docs.set(`trash|${uid}|${row.relatedDocumentId}`, row.title);
1429
+ }
1430
+ }
1431
+ const results = list.map((event) => {
1432
+ const rows2 = docs.get(`${event.contentType}|${event.relatedDocumentId}`) || [];
1433
+ const own = rows2.find((row) => row.locale === (event.locale ?? null));
1434
+ if (event.action === "purge") return { ...event, exists: false, title: event.summary?.title ?? null };
1435
+ return { ...event, exists: Boolean(own), title: (own || rows2[0])?.title ?? docs.get(`trash|${event.contentType}|${event.relatedDocumentId}`) ?? null };
1436
+ });
1437
+ return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) }, actors: await actors() };
1438
+ },
790
1439
  // Nightly, in batches, idempotent (two instances running it is harmless): events past eventDays are deleted;
791
1440
  // snapshots past retentionDays, or beyond maxSnapshots per document (newest kept), are emptied. Never touches media.
792
1441
  async purge(now = Date.now()) {
@@ -823,16 +1472,21 @@ var require_history = __commonJS({
823
1472
  const warn = (message, error) => strapi.log.error(`[${PLUGIN2}] ${message}: ${error?.message || error}`);
824
1473
  strapi.documents.use(async (ctx, next) => {
825
1474
  const action = CAPTURED[ctx.action];
826
- if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true) return next();
1475
+ if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true || restoring.getStore()) return next();
827
1476
  const history = service();
828
1477
  const params = ctx.params || {};
829
1478
  if (action === "delete") {
830
- if (!params.documentId || !history.covers(await history.config(), ctx.uid)) return next();
1479
+ const config = params.documentId ? await history.config() : null;
1480
+ if (!config || !history.covers(config, ctx.uid)) return next();
831
1481
  return strapi.db.transaction(async () => {
832
- const actor2 = actorOf(strapi);
833
- let entries;
1482
+ const actor2 = actorOf(strapi), trash = strapi.plugin(PLUGIN2).service("trash");
1483
+ const published = strapi.getModel(ctx.uid)?.options?.draftAndPublish === true;
1484
+ let locales2, entries, live, incoming;
834
1485
  try {
835
- entries = await history.rows(ctx.uid, params.documentId, await history.scope(ctx.uid, params.locale));
1486
+ locales2 = await history.scope(ctx.uid, params.locale);
1487
+ entries = await history.rows(ctx.uid, params.documentId, locales2);
1488
+ live = published ? await history.rows(ctx.uid, params.documentId, locales2, true) : [];
1489
+ incoming = await trash.incoming(ctx.uid, new Map([...entries.map((row) => [row.id, { locale: row.locale, published: false }]), ...live.map((row) => [row.id, { locale: row.locale, published: true }])]));
836
1490
  } catch (error) {
837
1491
  warn("reading the document before a delete failed", error);
838
1492
  fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
@@ -840,6 +1494,7 @@ var require_history = __commonJS({
840
1494
  const result2 = await next();
841
1495
  try {
842
1496
  for (const entry of entries) await history.record({ uid: ctx.uid, documentId: params.documentId, locale: entry.locale ?? null, action, entry, actor: actor2 });
1497
+ if (entries.length || live.length) await trash.record({ uid: ctx.uid, documentId: params.documentId, locales: locales2, drafts: entries, published: live, incoming, actor: actor2, days: config.trashDays });
843
1498
  } catch (error) {
844
1499
  warn("recording a delete failed, the delete was rolled back", error);
845
1500
  fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
@@ -885,17 +1540,36 @@ var require_history = __commonJS({
885
1540
  } catch (error) {
886
1541
  strapi.log.error(`[${PLUGIN2}] history purge failed: ${error?.message || error}`);
887
1542
  }
1543
+ try {
1544
+ const count = await strapi.plugin(PLUGIN2).service("trash").purge();
1545
+ if (count) strapi.log.info(`[${PLUGIN2}] trash purge: ${count} expired entries deleted`);
1546
+ } catch (error) {
1547
+ strapi.log.error(`[${PLUGIN2}] trash purge failed: ${error?.message || error}`);
1548
+ }
888
1549
  } } });
889
1550
  }
890
1551
  var DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
891
1552
  var LOCALE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
892
- var canRead = (strapi, ctx, uid) => {
1553
+ var can = (strapi, ctx, uid, action = "read") => {
893
1554
  try {
894
- return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot.read();
1555
+ return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot[action]();
895
1556
  } catch {
896
1557
  return false;
897
1558
  }
898
1559
  };
1560
+ var canRead = (strapi, ctx, uid) => can(strapi, ctx, uid);
1561
+ var readableTypes = (strapi, ctx) => Object.keys(strapi.contentTypes || {}).filter((uid) => uid.startsWith("api::") && canRead(strapi, ctx, uid));
1562
+ var ACTOR = /^(system|(admin|token|user):\d{1,15})$/;
1563
+ var int = (value, fallback, max) => {
1564
+ if (value === void 0 || value === "") return fallback;
1565
+ const n = Number(value);
1566
+ return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
1567
+ };
1568
+ var date = (value) => {
1569
+ if (!value) return null;
1570
+ const time = Date.parse(value);
1571
+ return typeof value === "string" && value.length <= 40 && Number.isFinite(time) ? new Date(time) : NaN;
1572
+ };
899
1573
  var historyController2 = ({ strapi }) => ({
900
1574
  async list(ctx) {
901
1575
  const { uid, documentId } = ctx.params || {};
@@ -911,17 +1585,30 @@ var require_history = __commonJS({
911
1585
  const found = await strapi.plugin(PLUGIN2).service("history").find(id);
912
1586
  if (!found || !canRead(strapi, ctx, found.event.contentType)) return ctx.notFound();
913
1587
  ctx.body = found;
1588
+ },
1589
+ async activity(ctx) {
1590
+ const { actor = "", contentType = "", action = "", q = "" } = ctx.query || {};
1591
+ const page = int(ctx.query?.page, 1, 1e4), pageSize = int(ctx.query?.pageSize, 25, 100), from = date(ctx.query?.from), to = date(ctx.query?.to);
1592
+ if ([actor, contentType, action, q].some((value) => typeof value !== "string") || actor && !ACTOR.test(actor) || contentType && !/^api::[\w-]+\.[\w-]+$/.test(contentType) || action && !ACTIONS.includes(action) || q.length > 100 || [page, pageSize, from, to].some((value) => Number.isNaN(value))) return ctx.badRequest("Invalid filters");
1593
+ const history = strapi.plugin(PLUGIN2).service("history");
1594
+ const types = readableTypes(strapi, ctx);
1595
+ ctx.body = {
1596
+ ...await history.activity({ actor, contentType, action, from, to, q: q.trim(), page, pageSize, types }),
1597
+ enabled: Boolean((await history.config()).enabled) && strapi.plugin(PLUGIN2).config("disabled") !== true,
1598
+ types: types.map((uid) => ({ uid, displayName: strapi.contentTypes[uid].info?.displayName || uid, kind: strapi.contentTypes[uid].kind, localized: Boolean(strapi.contentTypes[uid].pluginOptions?.i18n?.localized) }))
1599
+ };
914
1600
  }
915
1601
  });
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 };
1602
+ module2.exports = { EVENT_UID, ACTIONS, CAPTURED, CRON: CRON2, BATCH, eventContentType: eventContentType2, covers: covers2, retention, actorOf, refsOf, can, readableTypes, historyService: historyService2, registerHistory: registerHistory2, registerPurge: registerPurge2, historyController: historyController2 };
917
1603
  }
918
1604
  });
919
1605
 
920
1606
  // server/index.js
921
- var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
922
- var { safeGroups, validateGroups } = require_groups();
1607
+ var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, validateVariants, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
1608
+ var { safeGroups, validateGroups, layoutFields } = require_groups();
923
1609
  var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
924
1610
  var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
1611
+ var { trashContentType, trashService, trashController } = require_trash();
925
1612
  var STRAPI5 = true;
926
1613
  var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
927
1614
  var LEGACY_PLUGIN = "block-picker";
@@ -1061,10 +1748,17 @@ module.exports = {
1061
1748
  await strapi.admin.services.permission.actionProvider.registerMany([
1062
1749
  { section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
1063
1750
  { section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN },
1064
- ...STRAPI5 ? [{ section: "plugins", displayName: "Read version history", uid: "history.read", pluginName: PLUGIN }] : []
1751
+ ...STRAPI5 ? [
1752
+ ["history.read", "Read version history"],
1753
+ ["activity.read", "Read content activity"],
1754
+ ["trash.read", "Read the trash"],
1755
+ ["trash.restore", "Restore from the trash"],
1756
+ ["trash.purge", "Delete from the trash forever"]
1757
+ ].map(([uid, displayName]) => ({ section: "plugins", displayName, uid, pluginName: PLUGIN })) : []
1065
1758
  ]);
1066
1759
  strapi.plugin(PLUGIN).service("settings").projectDefaults();
1067
1760
  strapi.plugin(PLUGIN).service("settings").fields();
1761
+ strapi.plugin(PLUGIN).service("settings").variants();
1068
1762
  registerPublishGuard(strapi);
1069
1763
  registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
1070
1764
  if (STRAPI5) {
@@ -1075,8 +1769,8 @@ module.exports = {
1075
1769
  destroy({ strapi }) {
1076
1770
  if (STRAPI5) strapi.cron?.remove?.(CRON);
1077
1771
  },
1078
- // Version history events (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
1079
- contentTypes: STRAPI5 ? { event: eventContentType } : {},
1772
+ // Version history events and the trash (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
1773
+ contentTypes: STRAPI5 ? { event: eventContentType, trash: trashContentType } : {},
1080
1774
  services: { settings: ({ strapi }) => {
1081
1775
  let project;
1082
1776
  const projectDefaults = () => {
@@ -1084,7 +1778,7 @@ module.exports = {
1084
1778
  const code = strapi.plugin(PLUGIN).config("settings");
1085
1779
  project = null;
1086
1780
  if (code) try {
1087
- project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
1781
+ project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
1088
1782
  } catch (error) {
1089
1783
  strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
1090
1784
  }
@@ -1104,13 +1798,22 @@ module.exports = {
1104
1798
  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
1799
  return fields;
1106
1800
  };
1107
- const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
1801
+ let variants;
1802
+ const insertVariants = () => {
1803
+ if (variants !== void 0) return variants;
1804
+ const notes = [];
1805
+ variants = validateVariants(strapi.plugin(PLUGIN).config("components"), strapi.components || {}, notes);
1806
+ for (const note of notes) strapi.log?.warn(`[${PLUGIN}] components variants: ${note}.`);
1807
+ return variants;
1808
+ };
1809
+ const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
1108
1810
  return {
1109
1811
  projectDefaults,
1110
1812
  get,
1111
1813
  fields: fieldTexts,
1814
+ variants: insertVariants,
1112
1815
  async set(value) {
1113
- const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
1816
+ const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
1114
1817
  for (const [uid, entry] of Object.entries(next.components)) {
1115
1818
  if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
1116
1819
  }
@@ -1124,7 +1827,7 @@ module.exports = {
1124
1827
  return get();
1125
1828
  }
1126
1829
  };
1127
- }, ...STRAPI5 && { history: historyService } },
1830
+ }, ...STRAPI5 && { history: historyService, trash: trashService } },
1128
1831
  controllers: {
1129
1832
  catalog: ({ strapi }) => ({
1130
1833
  async find(ctx) {
@@ -1141,6 +1844,7 @@ module.exports = {
1141
1844
  });
1142
1845
  const settings = await plugin.service("settings").get();
1143
1846
  const media = await resolveMedia(strapi, settings);
1847
+ for (const [uid, list] of Object.entries(plugin.service("settings").variants())) base.components[uid] = { ...base.components[uid], variants: list };
1144
1848
  for (const [uid, entry] of Object.entries(settings.components)) {
1145
1849
  base.components[uid] = {
1146
1850
  ...base.components[uid],
@@ -1153,8 +1857,10 @@ module.exports = {
1153
1857
  const hidden = hiddenName(plugin.config("hiddenAttribute")) || null;
1154
1858
  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
1859
  const history = STRAPI5 && strapi.documents?.use && !base.disabled && settings.history.enabled ? { contentTypes: apiUids(strapi).filter((uid) => covers(settings.history, uid)) } : null;
1860
+ const layouts = Object.fromEntries(Object.entries(settings.components).filter(([uid, entry]) => entry.layout && base.groups?.[uid]).map(([uid, entry]) => [uid, entry.layout]));
1156
1861
  ctx.body = {
1157
1862
  ...base,
1863
+ layouts,
1158
1864
  palette: settings.palette,
1159
1865
  contentTypes: settings.contentTypes,
1160
1866
  hiddenAttribute: hidden,
@@ -1173,7 +1879,8 @@ module.exports = {
1173
1879
  uid,
1174
1880
  displayName: schema.info?.displayName || uid,
1175
1881
  category: schema.category || uid.split(".")[0],
1176
- typology: auto[uid]?.typology
1882
+ typology: auto[uid]?.typology,
1883
+ layoutFields: layoutFields(schema)
1177
1884
  }));
1178
1885
  const contentTypes = Object.keys(contentTypeUids(strapi)).map((uid) => ({
1179
1886
  uid,
@@ -1218,7 +1925,7 @@ module.exports = {
1218
1925
  ctx.body = value;
1219
1926
  }
1220
1927
  }),
1221
- ...STRAPI5 && { history: historyController }
1928
+ ...STRAPI5 && { history: historyController, trash: trashController }
1222
1929
  },
1223
1930
  routes: { admin: { type: "admin", routes: [
1224
1931
  { method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
@@ -1232,11 +1939,20 @@ module.exports = {
1232
1939
  config: { actions: [`plugin::${PLUGIN}.settings.${action}`] }
1233
1940
  }] }
1234
1941
  })),
1235
- ...STRAPI5 ? [["/history/:uid/:documentId", "list"], ["/history-events/:id", "find"]].map(([path, handler]) => ({
1236
- method: "GET",
1942
+ ...STRAPI5 ? [
1943
+ ["GET", "/history/:uid/:documentId", "history.list", "history.read"],
1944
+ ["GET", "/history-events/:id", "history.find", "history.read"],
1945
+ ["GET", "/activity", "history.activity", "activity.read"],
1946
+ ["GET", "/trash", "trash.list", "trash.read"],
1947
+ ["GET", "/trash/:id", "trash.find", "trash.read"],
1948
+ ["GET", "/trash/:id/check", "trash.check", "trash.restore"],
1949
+ ["POST", "/trash/:id/restore", "trash.restore", "trash.restore"],
1950
+ ["DELETE", "/trash/:id", "trash.remove", "trash.purge"]
1951
+ ].map(([method, path, handler, action]) => ({
1952
+ method,
1237
1953
  path,
1238
- handler: `history.${handler}`,
1239
- config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}.history.read`] } }] }
1954
+ handler,
1955
+ config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}.${action}`] } }] }
1240
1956
  })) : []
1241
1957
  ] } }
1242
1958
  };