@aduptive/strapi-blockscene 2.0.0-alpha.11 → 2.0.0-alpha.12

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
@@ -187,7 +187,7 @@ var require_settings = __commonJS({
187
187
  // Edit view labels that are still the raw attribute name read as "Mobile columns count" (see README "Field labels").
188
188
  friendlyLabels: true,
189
189
  // Page preview pane (Strapi 5): which toolbar controls show, in order, and the width menu's entries.
190
- previewToolbar: ["modes", "history", "devices", "status", "actions"],
190
+ previewToolbar: ["modes", "history", "versions", "devices", "status", "actions"],
191
191
  previewDevices: ["fit", "mobile", "tablet", "desktop"]
192
192
  },
193
193
  // Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
@@ -202,7 +202,7 @@ var require_settings = __commonJS({
202
202
  var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
203
203
  var OPENS = ["modal", "drawer"];
204
204
  var LABEL = /^[^<>]{1,40}$/;
205
- var TOOLBAR = ["modes", "history", "devices", "status", "actions"];
205
+ var TOOLBAR = ["modes", "history", "versions", "devices", "status", "actions"];
206
206
  var DEVICE_NAMES = ["fit", "mobile", "tablet", "desktop"];
207
207
  var previewToolbar = (value) => Array.isArray(value) && value.every((id) => TOOLBAR.includes(id)) && new Set(value).size === value.length;
208
208
  var DEVICE_LABEL = /^[^<>]{1,24}$/;
@@ -624,6 +624,102 @@ var require_settings = __commonJS({
624
624
  }
625
625
  return out;
626
626
  }
627
+ function requiredProblems(schema, values, schemas, path, media, problems) {
628
+ for (const [field, attr] of Object.entries(schema?.attributes || {})) {
629
+ const at = `${path}.${field}`;
630
+ const supplied = Object.hasOwn(values, field);
631
+ const value = supplied ? values[field] : attr.default;
632
+ if (attr.type === "media" || attr.type === "relation") {
633
+ if (attr.required && !supplied) media.push(at);
634
+ continue;
635
+ }
636
+ if (attr.type === "component") {
637
+ const nested = schemas[attr.component];
638
+ if (!nested) {
639
+ problems.push(`${at}: unknown component "${attr.component}"`);
640
+ continue;
641
+ }
642
+ if (attr.repeatable) {
643
+ const minimum = Math.max(attr.min ?? 0, attr.required ? 1 : 0);
644
+ const items = supplied ? value : Array.from({ length: minimum }, () => ({}));
645
+ if (!Array.isArray(items) || items.length < minimum) problems.push(`${at} requires at least ${minimum} item${minimum === 1 ? "" : "s"}`);
646
+ else items.forEach((item, index) => requiredProblems(nested, item, schemas, `${at}[${index}]`, media, problems));
647
+ } else if (supplied && value === null) {
648
+ if (attr.required) problems.push(`${at} is required`);
649
+ } else if (supplied || attr.required) requiredProblems(nested, supplied ? value : {}, schemas, at, media, problems);
650
+ continue;
651
+ }
652
+ if (attr.required && (value === void 0 || value === null || value === "")) problems.push(`${at} is required`);
653
+ }
654
+ }
655
+ function validateKits2(input, contentTypes = {}, schemas = {}, groups = null, notes = []) {
656
+ if (input === null || input === void 0) return {};
657
+ if (!plain(input) || JSON.stringify(input).length > 256 * 1024) {
658
+ notes.push("expected an object up to 256 KB");
659
+ return {};
660
+ }
661
+ const out = {};
662
+ for (const [uid, kits] of Object.entries(input)) {
663
+ const type = contentTypes[uid];
664
+ if (!type || !uid.startsWith("api::")) {
665
+ notes.push(`${uid}: unknown content type`);
666
+ continue;
667
+ }
668
+ if (!Array.isArray(kits) || kits.length > 12) {
669
+ notes.push(`${uid}: kits must be a list of at most 12`);
670
+ continue;
671
+ }
672
+ const ids = /* @__PURE__ */ new Set();
673
+ for (const kit of kits) {
674
+ const name = `${uid} kit "${kit?.id ?? "?"}"`;
675
+ try {
676
+ if (!plain(kit) || Object.keys(kit).some((key) => !["id", "label", "locales", "zones"].includes(key))) throw new Error("expected { id, label, locales?, zones }");
677
+ if (!text(kit.id) || !VARIANT.id.test(kit.id) || ids.has(kit.id)) throw new Error('id must be 1 to 40 lowercase letters, digits, "-" or "_", unique per content type');
678
+ if (!fieldText(kit.label, 60)) throw new Error('label must be a string of 1 to 60 characters, or { "<locale>": string }');
679
+ if (kit.locales !== void 0 && (!Array.isArray(kit.locales) || !kit.locales.length || kit.locales.length > 20 || kit.locales.some((locale) => !text(locale) || !LOCALE.test(locale)))) throw new Error("locales must be 1 to 20 locale codes");
680
+ if (!plain(kit.zones) || !Object.keys(kit.zones).length) throw new Error("zones must be a non-empty object");
681
+ const zones = {};
682
+ for (const [zoneName, rows] of Object.entries(kit.zones)) {
683
+ const zone = type.attributes?.[zoneName];
684
+ if (zone?.type !== "dynamiczone") throw new Error(`unknown Dynamic Zone "${zoneName}"`);
685
+ if (!Array.isArray(rows) || !rows.length || rows.length > 50) throw new Error(`${zoneName} must contain 1 to 50 rows`);
686
+ const clean = [];
687
+ for (let index = 0; index < rows.length; index++) {
688
+ const row = rows[index];
689
+ if (!plain(row) || !text(row.__component)) throw new Error(`${zoneName}[${index}] must have __component`);
690
+ const component = row.__component;
691
+ if (!zone.components?.includes(component) || !schemas[component]) throw new Error(`${zoneName}[${index}]: component "${component}" is not allowed`);
692
+ const skipped = [];
693
+ const values = variantValues(row, schemas[component], schemas, `${zoneName}[${index}]`, skipped);
694
+ if (skipped.length) notes.push(`${name}: media and relation values are not inserted (${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ", ..." : ""})`);
695
+ const media = [], required = [];
696
+ requiredProblems(schemas[component], values, schemas, `${zoneName}[${index}]`, media, required);
697
+ if (required.length) throw new Error(required.slice(0, 5).join("; "));
698
+ if (media.length) notes.push(`${name}: required media or relations must be completed before publishing (${media.slice(0, 5).join(", ")}${media.length > 5 ? ", ..." : ""})`);
699
+ clean.push({ __component: component, values });
700
+ const close = groups?.[component];
701
+ if (close) {
702
+ if (!zone.components?.includes(close) || !schemas[close]) throw new Error(`${zoneName}[${index}]: close component "${close}" is not allowed`);
703
+ clean.push({ __component: close, values: {} });
704
+ }
705
+ }
706
+ if (clean.length > (zone.max ?? Infinity)) throw new Error(`${zoneName} exceeds its maximum of ${zone.max}`);
707
+ zones[zoneName] = clean;
708
+ }
709
+ for (const [zoneName, zone] of Object.entries(type.attributes || {})) {
710
+ if (zone?.type !== "dynamiczone") continue;
711
+ const minimum = Math.max(zone.min ?? 0, zone.required ? 1 : 0);
712
+ if ((zones[zoneName]?.length || 0) < minimum) throw new Error(`${zoneName} requires at least ${minimum} row${minimum === 1 ? "" : "s"}`);
713
+ }
714
+ ids.add(kit.id);
715
+ (out[uid] ||= []).push({ id: kit.id, label: structuredClone(kit.label), ...kit.locales && { locales: [...new Set(kit.locales)] }, zones });
716
+ } catch (error) {
717
+ notes.push(`${name} ignored: ${error.message}`);
718
+ }
719
+ }
720
+ }
721
+ return out;
722
+ }
627
723
  var PREFS = { starred: 200, recent: 20 };
628
724
  function validatePrefs2(input, componentUids2) {
629
725
  if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
@@ -636,7 +732,7 @@ var require_settings = __commonJS({
636
732
  return out;
637
733
  }
638
734
  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) : []]));
639
- 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, schemaMetadata: schemaMetadata2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
735
+ 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, validateKits: validateKits2, schemaMetadata: schemaMetadata2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
640
736
  }
641
737
  });
642
738
 
@@ -826,8 +922,8 @@ var require_diff = __commonJS({
826
922
  }
827
923
  function summarize(before, after, schema) {
828
924
  const zones = Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
829
- const blocks = { added: 0, removed: 0, changed: 0 };
830
- if (!before) return { initial: true, fields: [], blocks };
925
+ const blocks = { total: zones.reduce((n, name) => n + (Array.isArray(after?.[name]) ? after[name].length : 0), 0), added: 0, removed: 0, changed: 0 };
926
+ if (!before) return { initial: true, fields: [], blocks: { ...blocks, added: blocks.total } };
831
927
  const names = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after || {})])];
832
928
  const fields = names.filter((name) => !zones.includes(name) && !same(before[name], after?.[name])).sort();
833
929
  for (const name of zones) for (const { op } of diffRows(before[name], after?.[name])) if (op !== "same") blocks[op]++;
@@ -1298,7 +1394,6 @@ var require_history = __commonJS({
1298
1394
  var CRON2 = "blocksceneHistoryPurge";
1299
1395
  var BATCH = 500;
1300
1396
  var DAY = 24 * 60 * 60 * 1e3;
1301
- var LIST_LIMIT = 100;
1302
1397
  var ACTORS_LIMIT = 500;
1303
1398
  var SEARCH_LIMIT = 200;
1304
1399
  var eventContentType2 = { schema: {
@@ -1441,14 +1536,23 @@ var require_history = __commonJS({
1441
1536
  return name && name !== "id" && schemaOf(uid)?.attributes?.[name] && !["relation", "media", "component", "dynamiczone", "password", "json", "blocks"].includes(schemaOf(uid).attributes[name].type) ? name : null;
1442
1537
  },
1443
1538
  // Latest events of one document and locale, newest first; `stored`: its snapshot (or an identical one) can be loaded.
1444
- async list({ uid, documentId, locale }) {
1445
- const where = { contentType: uid, relatedDocumentId: documentId, locale: locale || null };
1446
- const [list, kept] = await Promise.all([
1447
- events().findMany({ select: ["id", "action", "actor", "actorName", "at", "summary", "hash", "size"], where, orderBy: [{ at: "desc" }, { id: "desc" }], limit: LIST_LIMIT }),
1448
- events().findMany({ select: ["hash"], where: { ...where, size: { $gt: 0 } } })
1539
+ async list({ uid, documentId, locale, actor = "", page = 1, pageSize = 25 }) {
1540
+ const scope2 = { contentType: uid, relatedDocumentId: documentId, locale: locale || null };
1541
+ const where = { ...scope2, ...actor && { actor } };
1542
+ const [list, kept, total, people] = await Promise.all([
1543
+ events().findMany({ select: ["id", "action", "actor", "actorName", "at", "summary", "hash", "size"], where, orderBy: [{ at: "desc" }, { id: "desc" }], offset: (page - 1) * pageSize, limit: pageSize }),
1544
+ events().findMany({ select: ["hash"], where: { ...scope2, size: { $gt: 0 } } }),
1545
+ events().count({ where }),
1546
+ events().findMany({ select: ["actor", "actorName"], where: scope2, orderBy: [{ at: "desc" }, { id: "desc" }] })
1449
1547
  ]);
1450
1548
  const hashes = new Set(kept.map((row) => row.hash));
1451
- return list.map(({ hash, size, ...event }) => ({ ...event, stored: Boolean(hash && hashes.has(hash)) }));
1549
+ const actors2 = [...new Map(people.slice().reverse().map(({ actor: actor2, actorName }) => [actor2, { actor: actor2, actorName }])).values()];
1550
+ return {
1551
+ results: list.map(({ hash, size, ...event }) => ({ ...event, stored: Boolean(hash && hashes.has(hash)) })),
1552
+ pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) },
1553
+ actors: actors2,
1554
+ locale: locale || null
1555
+ };
1452
1556
  },
1453
1557
  // One event with its snapshot (from an identical event when this one was deduplicated) and what loading it needs:
1454
1558
  // the media files and relation targets that still exist (relation labels from the Content Manager's main field).
@@ -1660,10 +1764,13 @@ var require_history = __commonJS({
1660
1764
  async list(ctx) {
1661
1765
  const { uid, documentId } = ctx.params || {};
1662
1766
  const locale = ctx.query?.locale || "";
1663
- 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");
1767
+ const actor = ctx.query?.actor || "";
1768
+ const page = int(ctx.query?.page, 1, 1e6), pageSize = int(ctx.query?.pageSize, 25, 100);
1769
+ if (typeof actor !== "string" || actor && !ACTOR.test(actor) || [page, pageSize].some(Number.isNaN)) return ctx.badRequest("Invalid history filters");
1770
+ if (!String(uid).startsWith("api::") || !strapi.contentTypes?.[uid] || !DOCUMENT_ID.test(documentId || "") || typeof locale !== "string" || locale && !LOCALE.test(locale)) return ctx.badRequest("Invalid content type, document or locale");
1664
1771
  if (!canRead(strapi, ctx, uid)) return ctx.forbidden();
1665
1772
  const history = strapi.plugin(PLUGIN2).service("history");
1666
- ctx.body = { results: await history.list({ uid, documentId, locale: (await history.scope(uid, locale || void 0))[0] }) };
1773
+ ctx.body = await history.list({ uid, documentId, locale: (await history.scope(uid, locale || void 0))[0], actor, page, pageSize });
1667
1774
  },
1668
1775
  async find(ctx) {
1669
1776
  const id = Number(ctx.params?.id);
@@ -1690,7 +1797,7 @@ var require_history = __commonJS({
1690
1797
  });
1691
1798
 
1692
1799
  // server/index.js
1693
- var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, validateVariants, schemaMetadata, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
1800
+ var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, validateVariants, validateKits, schemaMetadata, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
1694
1801
  var { safeGroups, validateGroups, invalidComponents, layoutFields } = require_groups();
1695
1802
  var { hiddenName, injectHidden, hideFromBuilder, registerHiddenStrip } = require_hidden();
1696
1803
  var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
@@ -1842,6 +1949,8 @@ module.exports = {
1842
1949
  settings: null,
1843
1950
  // Edit view field texts from code (never stored): { uid: { attribute: { label, description, placeholder } } }. See README "Field labels".
1844
1951
  fields: null,
1952
+ // Initial block sets per content type, shown only on a new empty entry. See README "Starter kits".
1953
+ kits: null,
1845
1954
  // Other languages for the schema metadata (pluginOptions.blockscene, info.description), one flat map per locale:
1846
1955
  // { 'pt-BR': require('./blockscene/pt-BR.json') }. See README "Describing blocks and fields in the schema".
1847
1956
  translations: null,
@@ -1876,6 +1985,7 @@ module.exports = {
1876
1985
  strapi.plugin(PLUGIN).service("settings").projectDefaults();
1877
1986
  strapi.plugin(PLUGIN).service("settings").fields();
1878
1987
  strapi.plugin(PLUGIN).service("settings").variants();
1988
+ strapi.plugin(PLUGIN).service("settings").kits();
1879
1989
  registerZoneGuards(strapi);
1880
1990
  registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
1881
1991
  if (withHidden.length) hideFromBuilder(typeof strapi.get === "function" ? strapi.get("services") : strapi.container.get("services"), withHidden, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")));
@@ -1934,6 +2044,14 @@ module.exports = {
1934
2044
  for (const note of notes) strapi.log?.warn(`[${PLUGIN}] components variants: ${note}.`);
1935
2045
  return variants;
1936
2046
  };
2047
+ let kits;
2048
+ const starterKits = () => {
2049
+ if (kits !== void 0) return kits;
2050
+ const notes = [];
2051
+ kits = validateKits(strapi.plugin(PLUGIN).config("kits"), strapi.contentTypes || {}, strapi.components || {}, safeGroups(strapi.plugin(PLUGIN).config("groups"), componentUids(strapi)), notes);
2052
+ for (const note of notes) strapi.log?.warn(`[${PLUGIN}] starter kits: ${note}.`);
2053
+ return kits;
2054
+ };
1937
2055
  const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
1938
2056
  return {
1939
2057
  projectDefaults,
@@ -1941,6 +2059,7 @@ module.exports = {
1941
2059
  fields: fieldTexts,
1942
2060
  schema: schemaMeta,
1943
2061
  variants: insertVariants,
2062
+ kits: starterKits,
1944
2063
  async set(value) {
1945
2064
  const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
1946
2065
  for (const [uid, entry] of Object.entries(next.components)) {
@@ -1991,6 +2110,7 @@ module.exports = {
1991
2110
  ctx.body = {
1992
2111
  ...base,
1993
2112
  layouts,
2113
+ kits: plugin.service("settings").kits(),
1994
2114
  palette: settings.palette,
1995
2115
  contentTypes: settings.contentTypes,
1996
2116
  hiddenAttribute: hidden,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aduptive/strapi-blockscene",
3
- "version": "2.0.0-alpha.11",
3
+ "version": "2.0.0-alpha.12",
4
4
  "description": "Visual component gallery for native Strapi dynamic zones.",
5
5
  "license": "MIT",
6
6
  "author": "Andrea Scarpello <aduptive@gmail.com>",