@aduptive/strapi-blockscene 2.0.0-alpha.7 → 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
 
@@ -237,7 +271,7 @@ var require_settings = __commonJS({
237
271
  return [...facets];
238
272
  }
239
273
  var safeUrl2 = (value) => typeof value === "string" && (/^\/(?!\/)/.test(value) || /^https?:\/\//.test(value)) ? value : void 0;
240
- var { safeGroups: safeGroups2 } = require_groups();
274
+ var { safeGroups: safeGroups2, validLayout } = require_groups();
241
275
  var safeVersion = (value) => typeof value === "string" && /^[\w.-]{1,32}$/.test(value) ? value : void 0;
242
276
  var fail2 = (message, details) => {
243
277
  let ValidationError;
@@ -282,7 +316,7 @@ var require_settings = __commonJS({
282
316
  groups: safeGroups2(config.groups, config.componentUids)
283
317
  };
284
318
  }
285
- function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null) {
319
+ function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
286
320
  const types = typeMap(contentTypeUids2);
287
321
  if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
288
322
  if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
@@ -318,6 +352,9 @@ var require_settings = __commonJS({
318
352
  } else if (key === "mediaId") {
319
353
  if (!Number.isInteger(entry.mediaId) || entry.mediaId <= 0) fail2(`Invalid media for "${uid}"`);
320
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);
321
358
  } else fail2(`Unknown component setting "${key}"`);
322
359
  }
323
360
  if (Object.keys(clean).length) out.components[uid] = clean;
@@ -343,7 +380,7 @@ var require_settings = __commonJS({
343
380
  if (out.history.eventDays < out.history.retentionDays) fail2("history.eventDays must be at least history.retentionDays");
344
381
  return out;
345
382
  }
346
- function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null) {
383
+ function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
347
384
  const types = typeMap(contentTypeUids2);
348
385
  const out = structuredClone(DEFAULTS2);
349
386
  if (!saved || typeof saved !== "object") return out;
@@ -373,6 +410,7 @@ var require_settings = __commonJS({
373
410
  clean.tags = cleanTags(clean.tags);
374
411
  if (!clean.tags.length) delete clean.tags;
375
412
  }
413
+ if ("layout" in clean && !validLayout(clean.layout, schemas && (schemas[uid]?.attributes || {}))) delete clean.layout;
376
414
  if (Object.keys(clean).length) out.components[uid] = clean;
377
415
  }
378
416
  for (const [uid, entry] of Object.entries(saved.contentTypes || {})) {
@@ -411,7 +449,7 @@ var require_settings = __commonJS({
411
449
  var FIELD_TEXT = { label: 80, description: 300, placeholder: 120, help: 500 };
412
450
  var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
413
451
  var plain = (value) => value && typeof value === "object" && !Array.isArray(value);
414
- 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));
415
453
  function validateFields2(input, schemas = {}, stale = []) {
416
454
  if (input === null || input === void 0) return {};
417
455
  if (!plain(input)) fail2("fields must be an object");
@@ -435,6 +473,87 @@ var require_settings = __commonJS({
435
473
  }
436
474
  return out;
437
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
+ }
438
557
  var PREFS = { starred: 200, recent: 20 };
439
558
  function validatePrefs2(input, componentUids2) {
440
559
  if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
@@ -447,7 +566,7 @@ var require_settings = __commonJS({
447
566
  return out;
448
567
  }
449
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) : []]));
450
- 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 };
451
570
  }
452
571
  });
453
572
 
@@ -1485,8 +1604,8 @@ var require_history = __commonJS({
1485
1604
  });
1486
1605
 
1487
1606
  // server/index.js
1488
- var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
1489
- 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();
1490
1609
  var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
1491
1610
  var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
1492
1611
  var { trashContentType, trashService, trashController } = require_trash();
@@ -1639,6 +1758,7 @@ module.exports = {
1639
1758
  ]);
1640
1759
  strapi.plugin(PLUGIN).service("settings").projectDefaults();
1641
1760
  strapi.plugin(PLUGIN).service("settings").fields();
1761
+ strapi.plugin(PLUGIN).service("settings").variants();
1642
1762
  registerPublishGuard(strapi);
1643
1763
  registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
1644
1764
  if (STRAPI5) {
@@ -1658,7 +1778,7 @@ module.exports = {
1658
1778
  const code = strapi.plugin(PLUGIN).config("settings");
1659
1779
  project = null;
1660
1780
  if (code) try {
1661
- project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
1781
+ project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
1662
1782
  } catch (error) {
1663
1783
  strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
1664
1784
  }
@@ -1678,13 +1798,22 @@ module.exports = {
1678
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` : ""}.`);
1679
1799
  return fields;
1680
1800
  };
1681
- 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);
1682
1810
  return {
1683
1811
  projectDefaults,
1684
1812
  get,
1685
1813
  fields: fieldTexts,
1814
+ variants: insertVariants,
1686
1815
  async set(value) {
1687
- const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
1816
+ const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
1688
1817
  for (const [uid, entry] of Object.entries(next.components)) {
1689
1818
  if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
1690
1819
  }
@@ -1715,6 +1844,7 @@ module.exports = {
1715
1844
  });
1716
1845
  const settings = await plugin.service("settings").get();
1717
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 };
1718
1848
  for (const [uid, entry] of Object.entries(settings.components)) {
1719
1849
  base.components[uid] = {
1720
1850
  ...base.components[uid],
@@ -1727,8 +1857,10 @@ module.exports = {
1727
1857
  const hidden = hiddenName(plugin.config("hiddenAttribute")) || null;
1728
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 || {})]]));
1729
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]));
1730
1861
  ctx.body = {
1731
1862
  ...base,
1863
+ layouts,
1732
1864
  palette: settings.palette,
1733
1865
  contentTypes: settings.contentTypes,
1734
1866
  hiddenAttribute: hidden,
@@ -1747,7 +1879,8 @@ module.exports = {
1747
1879
  uid,
1748
1880
  displayName: schema.info?.displayName || uid,
1749
1881
  category: schema.category || uid.split(".")[0],
1750
- typology: auto[uid]?.typology
1882
+ typology: auto[uid]?.typology,
1883
+ layoutFields: layoutFields(schema)
1751
1884
  }));
1752
1885
  const contentTypes = Object.keys(contentTypeUids(strapi)).map((uid) => ({
1753
1886
  uid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aduptive/strapi-blockscene",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.8",
4
4
  "description": "Visual component gallery for native Strapi dynamic zones.",
5
5
  "license": "MIT",
6
6
  "author": "Andrea Scarpello <aduptive@gmail.com>",