@aduptive/strapi-blockscene 2.0.0-alpha.4 → 2.0.0-alpha.5

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
@@ -118,6 +118,7 @@ var require_settings = __commonJS({
118
118
  var TEMPLATES2 = ["generic", "banner", "cards", "imageText", "faq", "none"];
119
119
  var INITIAL_STATES = ["closed", "open", "remember"];
120
120
  var PREVIEW_MODES = ["form", "split", "preview"];
121
+ var HIDDEN_MODES = ["strip", "flag", "off"];
121
122
  var DEFAULTS2 = {
122
123
  palette: { background: "#F6F6F9", surface: "#DCDCE4", text: "#32324D", accent: "#4945FF" },
123
124
  components: {},
@@ -125,11 +126,88 @@ var require_settings = __commonJS({
125
126
  // blockPreviewInForm: compact read-only preview above a block's fields in form
126
127
  // mode. Only installations that ship the accordion integration (server config
127
128
  // `blockPreview: true`) show and honour it; the page preview is the editor.
128
- editor: { enabled: true, showOpenAll: true, showCloseAll: true, initialState: "closed", previewMode: "form", previewUrl: "", blockPreviewInForm: false }
129
+ editor: {
130
+ enabled: true,
131
+ showOpenAll: true,
132
+ showCloseAll: true,
133
+ showRowThumbnails: true,
134
+ initialState: "closed",
135
+ previewMode: "form",
136
+ previewUrl: "",
137
+ blockPreviewUrl: "",
138
+ blockPreviewInForm: false,
139
+ // Strapi 5: rich-text custom fields render a read-only preview until clicked (see admin/LazyInput.tsx).
140
+ lazyEditors: true,
141
+ lazyFields: ["plugin::ckeditor5.CKEditor"],
142
+ // Row actions in each block header (see README "Row actions"). hiddenBlocks: what the content API does with rows
143
+ // hidden on the site: 'strip' removes them, 'flag' sends them with the attribute, 'off' hides the eye icon.
144
+ confirmDelete: true,
145
+ hiddenBlocks: "strip",
146
+ duplicate: true,
147
+ clipboard: true
148
+ },
149
+ // Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
150
+ // previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
151
+ contentTypes: {}
152
+ };
153
+ var ICONS = ["text", "tag", "seo", "settings", "image", "link", "palette", "list", "globe", "info"];
154
+ var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
155
+ var OPENS = ["modal", "drawer"];
156
+ var LABEL = /^[^<>]{1,40}$/;
157
+ 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)));
158
+ var TYPE_KEYS = {
159
+ enabled: (value) => typeof value === "boolean",
160
+ previewMode: (value) => PREVIEW_MODES.includes(value),
161
+ sidebarPosition: (value) => SIDEBAR_POSITIONS.includes(value),
162
+ sidebar: (value, attributes) => Array.isArray(value) && value.length <= 12 && value.every((item) => sidebarItem(item, attributes))
129
163
  };
164
+ var typeMap = (types) => Array.isArray(types) ? Object.fromEntries(types.map((uid) => [uid, null])) : types || {};
130
165
  var PREVIEW_URL = /^https?:\/\/[^\s"'<>]{1,500}$/;
166
+ var blockPreviewUrl = (value) => typeof value === "string" && (value === "" || PREVIEW_URL.test(value) && !/[{}]/.test(value.replace(/\{(uid|name|category|variant|locale)\}/g, "")) && URL.canParse?.(value.replace(/[{}]/g, "")) !== false);
131
167
  var COLOR = /^#[0-9A-Fa-f]{6}$/;
168
+ var FIELD_UID = /^(plugin|global)::[\w.-]+$/;
169
+ var lazyFields = (value) => Array.isArray(value) && value.length <= 20 && value.every((uid) => typeof uid === "string" && FIELD_UID.test(uid));
132
170
  var UID = /^[a-z0-9-]+\.[a-z0-9-]+$/;
171
+ var TYPOLOGIES2 = ["hero", "text", "media", "listing", "cards", "cta", "form", "layout"];
172
+ var TAG = /^[^<>\n]{1,24}$/;
173
+ var cleanTags = (value) => Array.isArray(value) ? [...new Set(value.filter((tag) => typeof tag === "string" && TAG.test(tag.trim())).map((tag) => tag.trim()))].slice(0, 10) : [];
174
+ var validTags = (value) => Array.isArray(value) && value.length <= 10 && value.every((tag) => typeof tag === "string" && TAG.test(tag.trim()));
175
+ var GUESSES = [
176
+ ["hero", /hero|banner|cover/],
177
+ ["text", /text|rich|quote|title/],
178
+ ["media", /image|media|video|gallery|carousel/],
179
+ ["listing", /list|query|archive|related|posts|projects/],
180
+ ["cards", /card/],
181
+ ["cta", /cta|button|link/],
182
+ ["form", /form|contact/],
183
+ ["layout", /wrapper|column|grid|divider|divisor|spacer|section/]
184
+ ];
185
+ function guessTypology(uid, displayName = "") {
186
+ const name = `${uid.split(".").pop()} ${displayName}`.toLowerCase();
187
+ return GUESSES.find(([, pattern]) => pattern.test(name))?.[0] || "text";
188
+ }
189
+ function facetsOf(uid, schema, schemas = {}) {
190
+ const facets = /* @__PURE__ */ new Set();
191
+ const read = (attributes, nested) => {
192
+ for (const attr of Object.values(attributes || {})) {
193
+ if (attr?.type === "media") {
194
+ const allowed = attr.allowedTypes;
195
+ if (!allowed || allowed.includes("images")) facets.add("image");
196
+ if (!allowed || allowed.includes("videos")) facets.add("video");
197
+ if (attr.multiple) facets.add("gallery");
198
+ } else if (["richtext", "blocks"].includes(attr?.type) || attr?.type === "customField" && /ckeditor/i.test(String(attr.customField))) facets.add("richtext");
199
+ else if (attr?.type === "relation") facets.add("dynamic");
200
+ else if (attr?.type === "component") {
201
+ if (attr.repeatable) facets.add("list");
202
+ if (!nested) read(schemas[attr.component]?.attributes, true);
203
+ }
204
+ }
205
+ };
206
+ read(schema?.attributes, false);
207
+ if (/query|archive|related/i.test(uid)) facets.add("dynamic");
208
+ if (/form|contact/i.test(`${uid} ${schema?.info?.displayName || ""}`)) facets.add("form");
209
+ return [...facets];
210
+ }
133
211
  var safeUrl2 = (value) => typeof value === "string" && (/^\/(?!\/)/.test(value) || /^https?:\/\//.test(value)) ? value : void 0;
134
212
  var { safeGroups: safeGroups2 } = require_groups();
135
213
  var safeVersion = (value) => typeof value === "string" && /^[\w.-]{1,32}$/.test(value) ? value : void 0;
@@ -156,6 +234,15 @@ var require_settings = __commonJS({
156
234
  ["label", "description", "category", "keywords"].filter((key) => typeof entry[key] === "string").map((key) => [key, entry[key]])
157
235
  );
158
236
  entries[uid].image = safeUrl2(entry.image);
237
+ if (TYPOLOGIES2.includes(entry.typology)) entries[uid].typology = entry.typology;
238
+ const tags = cleanTags(entry.tags);
239
+ if (tags.length || entries[uid].category) entries[uid].tags = tags.length ? tags : cleanTags([entries[uid].category]);
240
+ }
241
+ for (const [uid, schema] of Object.entries(config.schemas || {})) {
242
+ const entry = entries[uid] ||= {};
243
+ entry.facets = facetsOf(uid, schema, config.schemas);
244
+ entry.typology ||= guessTypology(uid, schema?.info?.displayName);
245
+ entry.tags ||= [];
159
246
  }
160
247
  return {
161
248
  components: entries,
@@ -167,7 +254,8 @@ var require_settings = __commonJS({
167
254
  groups: safeGroups2(config.groups, config.componentUids)
168
255
  };
169
256
  }
170
- function validateSettings2(input, componentUids2) {
257
+ function validateSettings2(input, componentUids2, contentTypeUids2 = []) {
258
+ const types = typeMap(contentTypeUids2);
171
259
  if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
172
260
  if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
173
261
  const out = structuredClone(DEFAULTS2);
@@ -179,9 +267,9 @@ var require_settings = __commonJS({
179
267
  }
180
268
  for (const [key, value] of Object.entries(input.editor || {})) {
181
269
  if (!(key in DEFAULTS2.editor)) fail2(`Unknown editor option "${key}"`);
182
- const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : typeof value === "boolean";
270
+ const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "hiddenBlocks" ? HIDDEN_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : key === "blockPreviewUrl" ? blockPreviewUrl(value) : key === "lazyFields" ? lazyFields(value) : typeof value === "boolean";
183
271
  if (!valid) fail2(`Invalid value for editor option "${key}"`);
184
- out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : value;
272
+ out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : value;
185
273
  }
186
274
  const components = input.components || {};
187
275
  if (typeof components !== "object" || Array.isArray(components)) fail2("components must be an object");
@@ -193,6 +281,12 @@ var require_settings = __commonJS({
193
281
  if (key === "template") {
194
282
  if (!TEMPLATES2.includes(entry.template)) fail2(`Unknown template for "${uid}"`);
195
283
  clean.template = entry.template;
284
+ } else if (key === "typology") {
285
+ if (!TYPOLOGIES2.includes(entry.typology)) fail2(`Unknown typology for "${uid}"`);
286
+ clean.typology = entry.typology;
287
+ } else if (key === "tags") {
288
+ if (!validTags(entry.tags)) fail2(`Invalid tags for "${uid}": up to 10, each 1 to 24 characters`);
289
+ clean.tags = cleanTags(entry.tags);
196
290
  } else if (key === "mediaId") {
197
291
  if (!Number.isInteger(entry.mediaId) || entry.mediaId <= 0) fail2(`Invalid media for "${uid}"`);
198
292
  clean.mediaId = entry.mediaId;
@@ -200,9 +294,21 @@ var require_settings = __commonJS({
200
294
  }
201
295
  if (Object.keys(clean).length) out.components[uid] = clean;
202
296
  }
297
+ const perType = input.contentTypes || {};
298
+ if (typeof perType !== "object" || Array.isArray(perType)) fail2("contentTypes must be an object");
299
+ for (const [uid, entry] of Object.entries(perType)) {
300
+ if (!(uid in types)) fail2(`Unknown content type "${uid}"`);
301
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) fail2(`Invalid entry for "${uid}"`);
302
+ for (const [key, value] of Object.entries(entry)) {
303
+ if (!(key in TYPE_KEYS)) fail2(`Unknown content type setting "${key}"`);
304
+ if (!TYPE_KEYS[key](value, types[uid])) fail2(`Invalid value for "${key}" of "${uid}"`);
305
+ }
306
+ if (Object.keys(entry).length) out.contentTypes[uid] = structuredClone(entry);
307
+ }
203
308
  return out;
204
309
  }
205
- function mergeSaved2(saved, componentUids2) {
310
+ function mergeSaved2(saved, componentUids2, contentTypeUids2 = []) {
311
+ const types = typeMap(contentTypeUids2);
206
312
  const out = structuredClone(DEFAULTS2);
207
313
  if (!saved || typeof saved !== "object") return out;
208
314
  for (const key of Object.keys(DEFAULTS2.palette)) if (COLOR.test(String(saved.palette?.[key]))) out.palette[key] = saved.palette[key];
@@ -211,19 +317,156 @@ var require_settings = __commonJS({
211
317
  if (value === void 0 || typeof value !== typeof DEFAULTS2.editor[key]) continue;
212
318
  if (key === "initialState" && !INITIAL_STATES.includes(value)) continue;
213
319
  if (key === "previewMode" && !PREVIEW_MODES.includes(value)) continue;
320
+ if (key === "hiddenBlocks" && !HIDDEN_MODES.includes(value)) continue;
214
321
  if (key === "previewUrl" && value && !PREVIEW_URL.test(value)) continue;
322
+ if (key === "blockPreviewUrl" && !blockPreviewUrl(value)) continue;
323
+ if (key === "lazyFields") {
324
+ if (!Array.isArray(value)) continue;
325
+ out.editor[key] = [...new Set(value.filter((uid) => lazyFields([uid])))].slice(0, 20);
326
+ continue;
327
+ }
215
328
  out.editor[key] = value;
216
329
  }
217
- for (const [uid, entry] of Object.entries(saved.components || {})) if (componentUids2.includes(uid) && entry && typeof entry === "object") out.components[uid] = { ...entry };
330
+ for (const [uid, entry] of Object.entries(saved.components || {})) {
331
+ if (!componentUids2.includes(uid) || !entry || typeof entry !== "object") continue;
332
+ const clean = { ...entry };
333
+ if ("typology" in clean && !TYPOLOGIES2.includes(clean.typology)) delete clean.typology;
334
+ if ("tags" in clean) {
335
+ clean.tags = cleanTags(clean.tags);
336
+ if (!clean.tags.length) delete clean.tags;
337
+ }
338
+ if (Object.keys(clean).length) out.components[uid] = clean;
339
+ }
340
+ for (const [uid, entry] of Object.entries(saved.contentTypes || {})) {
341
+ if (!(uid in types) || !entry || typeof entry !== "object") continue;
342
+ const fix = (key, value) => key === "sidebar" && Array.isArray(value) ? value.filter((item) => sidebarItem(item, types[uid])) : value;
343
+ const clean = Object.fromEntries(Object.entries(entry).map(([key, value]) => [key, fix(key, value)]).filter(([key, value]) => TYPE_KEYS[key]?.(value, types[uid])));
344
+ if (Object.keys(clean).length) out.contentTypes[uid] = clean;
345
+ }
346
+ return out;
347
+ }
348
+ var ENTRY_KEYS = ["components", "contentTypes"];
349
+ function layer2(base, saved) {
350
+ if (!base) return saved;
351
+ const doc = saved && typeof saved === "object" ? saved : {};
352
+ return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => [key, { ...base[key], ...doc[key] && typeof doc[key] === "object" ? doc[key] : {} }]));
353
+ }
354
+ function overrides2(next, base) {
355
+ if (!base) return next;
356
+ return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => {
357
+ const entries = Object.entries(next[key] || {}).filter(([name, value]) => JSON.stringify(value) !== JSON.stringify(base[key]?.[name]));
358
+ if (ENTRY_KEYS.includes(key)) {
359
+ for (const uid of Object.keys(base[key] || {})) if (!(uid in (next[key] || {}))) entries.push([uid, {}]);
360
+ }
361
+ return [key, Object.fromEntries(entries)];
362
+ }));
363
+ }
364
+ var PREFS = { starred: 200, recent: 20 };
365
+ function validatePrefs2(input, componentUids2) {
366
+ if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
367
+ const out = { starred: [], recent: [] };
368
+ for (const [key, value] of Object.entries(input)) {
369
+ if (!(key in PREFS)) fail2(`Unknown preference "${key}"`);
370
+ if (!Array.isArray(value) || value.length > PREFS[key] || !value.every((uid) => typeof uid === "string" && componentUids2.includes(uid))) fail2(`Invalid value for "${key}"`);
371
+ out[key] = [...new Set(value)];
372
+ }
218
373
  return out;
219
374
  }
220
- module2.exports = { PLUGIN: PLUGIN2, TEMPLATES: TEMPLATES2, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, mergeSaved: mergeSaved2, safeUrl: safeUrl2, fail: fail2 };
375
+ var mergePrefs2 = (saved, componentUids2) => Object.fromEntries(Object.entries(PREFS).map(([key, max]) => [key, Array.isArray(saved?.[key]) ? [...new Set(saved[key].filter((uid) => componentUids2.includes(uid)))].slice(0, max) : []]));
376
+ module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
377
+ }
378
+ });
379
+
380
+ // server/hidden.js
381
+ var require_hidden = __commonJS({
382
+ "server/hidden.js"(exports2, module2) {
383
+ "use strict";
384
+ var NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,39}$/;
385
+ var DEFAULT_NAME = "bsHidden";
386
+ var hiddenName2 = (value) => value === void 0 ? DEFAULT_NAME : value === false ? false : typeof value === "string" && NAME.test(value) ? value : null;
387
+ var zoneComponents = (contentTypes) => [...new Set(Object.values(contentTypes || {}).flatMap((schema) => Object.values(schema?.attributes || {}).filter((attr) => attr?.type === "dynamiczone").flatMap((attr) => attr.components || [])))];
388
+ function injectHidden2(components, contentTypes, name) {
389
+ const added = [], skipped = [];
390
+ for (const uid of zoneComponents(contentTypes)) {
391
+ const schema = components?.[uid];
392
+ if (!schema) continue;
393
+ schema.attributes ||= {};
394
+ const current = schema.attributes[name];
395
+ if (current && current.type !== "boolean") {
396
+ skipped.push(uid);
397
+ continue;
398
+ }
399
+ if (!current) schema.attributes[name] = { type: "boolean", default: false, visible: false, configurable: false };
400
+ added.push(uid);
401
+ }
402
+ return { added, skipped };
403
+ }
404
+ function stripHidden(data, schema, schemaOf, name, depth = 0) {
405
+ if (!data || typeof data !== "object" || !schema || depth > 20) return data;
406
+ if (Array.isArray(data)) {
407
+ data.forEach((item) => stripHidden(item, schema, schemaOf, name, depth));
408
+ return data;
409
+ }
410
+ const walk = (value, target) => Array.isArray(value) ? value.forEach((item) => stripHidden(item, target, schemaOf, name, depth + 1)) : stripHidden(value, target, schemaOf, name, depth + 1);
411
+ for (const [key, attr] of Object.entries(schema.attributes || {})) {
412
+ const value = data[key];
413
+ if (value == null || typeof value !== "object") continue;
414
+ if (attr?.type === "dynamiczone" && Array.isArray(value)) {
415
+ data[key] = value.filter((row) => !(row && row[name] === true));
416
+ for (const row of data[key]) stripHidden(row, schemaOf(row?.__component), schemaOf, name, depth + 1);
417
+ } else if (attr?.type === "component") walk(value, schemaOf(attr.component));
418
+ else if (attr?.type === "relation" && attr.target) walk(value, schemaOf(attr.target));
419
+ }
420
+ return data;
421
+ }
422
+ var contentApi = (strapi) => {
423
+ try {
424
+ return strapi.requestContext?.get?.()?.state?.route?.info?.type === "content-api";
425
+ } catch {
426
+ return false;
427
+ }
428
+ };
429
+ var READS = ["findMany", "findOne", "findFirst"];
430
+ function registerHiddenStrip2(strapi, name, plugin) {
431
+ if (!name) return;
432
+ const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
433
+ const stripping = async () => {
434
+ try {
435
+ return (await plugin.service("settings").get()).editor.hiddenBlocks === "strip";
436
+ } catch {
437
+ return true;
438
+ }
439
+ };
440
+ const apply = async (uid, result) => result && contentApi(strapi) && await stripping() ? stripHidden(result, schemaOf(uid), schemaOf, name) : result;
441
+ if (strapi.documents?.use) {
442
+ strapi.documents.use(async (ctx, next) => {
443
+ const result = await next();
444
+ return READS.includes(ctx.action) ? apply(ctx.uid, result) : result;
445
+ });
446
+ return;
447
+ }
448
+ strapi.entityService?.decorate?.((service) => ({
449
+ async findMany(uid, params) {
450
+ return apply(uid, await service.findMany(uid, params));
451
+ },
452
+ async findOne(uid, id, params) {
453
+ return apply(uid, await service.findOne(uid, id, params));
454
+ },
455
+ async findPage(uid, params) {
456
+ const page = await service.findPage(uid, params);
457
+ if (page?.results) await apply(uid, page.results);
458
+ return page;
459
+ }
460
+ }));
461
+ }
462
+ module2.exports = { DEFAULT_NAME, hiddenName: hiddenName2, zoneComponents, injectHidden: injectHidden2, stripHidden, registerHiddenStrip: registerHiddenStrip2 };
221
463
  }
222
464
  });
223
465
 
224
466
  // server/index.js
225
- var { PLUGIN, TEMPLATES, DEFAULTS, catalog, validateSettings, mergeSaved, safeUrl, fail } = require_settings();
467
+ var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
226
468
  var { safeGroups, validateGroups } = require_groups();
469
+ var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
227
470
  var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
228
471
  var LEGACY_PLUGIN = "block-picker";
229
472
  async function readSettings(strapi) {
@@ -247,6 +490,7 @@ async function resolveMedia(strapi, settings) {
247
490
  }
248
491
  return resolved;
249
492
  }
493
+ var contentTypeUids = (strapi) => Object.fromEntries(Object.entries(strapi.contentTypes || {}).filter(([uid, schema]) => uid.startsWith("api::") && zonesOf(schema).length).map(([uid, schema]) => [uid, Object.keys(schema.attributes || {})]));
250
494
  var zonesOf = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
251
495
  var label = (error) => `row ${error.index + 1}: ${error.code === "closeBeforeOpen" ? `close marker ${error.uid} has no open marker before it` : error.code === "mismatch" ? `close marker ${error.uid} does not match the open group ${error.open} (expected ${error.expected})` : `group ${error.uid} is not closed (expected ${error.expected})`}`;
252
496
  function checkZones(strapi, uid, entry, groups) {
@@ -263,6 +507,11 @@ function registerPublishGuard(strapi) {
263
507
  const groups = safeGroups(plugin.config("groups"), componentUids(strapi));
264
508
  if (plugin.config("groups") && !groups) strapi.log.warn(`[${PLUGIN}] "groups" config ignored: expected { "<open component uid>": "<close component uid>" } with existing components; every block is treated as ordinary.`);
265
509
  if (!groups) return;
510
+ const active = async (uid) => {
511
+ if (plugin.config("disabled") === true) return false;
512
+ const settings = await plugin.service("settings").get();
513
+ return settings.editor.enabled && settings.contentTypes?.[uid]?.enabled !== false;
514
+ };
266
515
  if (strapi.documents?.use) {
267
516
  const defaultLocale = async () => {
268
517
  try {
@@ -277,7 +526,7 @@ function registerPublishGuard(strapi) {
277
526
  if (!publishing || !ctx.uid) return next();
278
527
  const model = strapi.getModel(ctx.uid);
279
528
  const zones = zonesOf(model);
280
- if (!zones.length) return next();
529
+ if (!zones.length || !await active(ctx.uid)) return next();
281
530
  if (action === "create") {
282
531
  checkZones(strapi, ctx.uid, params.data || {}, groups);
283
532
  return next();
@@ -303,19 +552,19 @@ function registerPublishGuard(strapi) {
303
552
  const published = (data) => Boolean(data && data.publishedAt);
304
553
  strapi.db.lifecycles.subscribe({
305
554
  async beforeCreate(event) {
306
- if (published(event.params?.data)) checkZones(strapi, event.model.uid, event.params.data, groups);
555
+ if (published(event.params?.data) && await active(event.model.uid)) checkZones(strapi, event.model.uid, event.params.data, groups);
307
556
  },
308
557
  async beforeUpdate(event) {
309
558
  const { data, where } = event.params || {};
310
559
  const zones = zonesOf(strapi.getModel(event.model.uid));
311
- if (!published(data) || !zones.length) return;
560
+ if (!published(data) || !zones.length || !await active(event.model.uid)) return;
312
561
  const entry = zones.every((name) => Array.isArray(data[name])) ? data : { ...await strapi.entityService.findOne(event.model.uid, where?.id, { populate: zones }), ...data };
313
562
  checkZones(strapi, event.model.uid, entry, groups);
314
563
  },
315
564
  async beforeUpdateMany(event) {
316
565
  const { data, where } = event.params || {};
317
566
  const zones = zonesOf(strapi.getModel(event.model.uid));
318
- if (!published(data) || !zones.length) return;
567
+ if (!published(data) || !zones.length || !await active(event.model.uid)) return;
319
568
  for (const entry of await strapi.entityService.findMany(event.model.uid, { filters: where, populate: zones })) checkZones(strapi, event.model.uid, entry, groups);
320
569
  }
321
570
  });
@@ -332,30 +581,65 @@ module.exports = {
332
581
  blockPreview: false,
333
582
  // Optional layout groups: OPEN component uid -> its CLOSE uid, e.g. { 'wrappers.join': 'wrappers.close' }.
334
583
  // Empty/absent: every Dynamic Zone component is an ordinary block. See README "Layout groups".
335
- groups: null
584
+ groups: null,
585
+ // Project defaults from code: same shape as the stored settings, e.g. require('./blockscene.json'). See README "Settings page".
586
+ settings: null,
587
+ // "Hide on the site": boolean attribute added to every Dynamic Zone component (a DB column); false adds nothing.
588
+ hiddenAttribute: "bsHidden"
336
589
  },
337
590
  validator: catalog
338
591
  },
592
+ // Before the database schema sync, so the column exists. A name the host already uses with another type is skipped.
593
+ register({ strapi }) {
594
+ const plugin = strapi.plugin(PLUGIN);
595
+ const name = hiddenName(plugin.config("hiddenAttribute"));
596
+ if (name === null) strapi.log.warn(`[${PLUGIN}] "hiddenAttribute" config ignored: expected a name like "bsHidden" or false. Hiding blocks is off.`);
597
+ if (!name) return;
598
+ const { skipped } = injectHidden(strapi.components, strapi.contentTypes, name);
599
+ if (skipped.length) strapi.log.warn(`[${PLUGIN}] "${name}" already exists with another type on ${skipped.join(", ")}; those blocks cannot be hidden.`);
600
+ },
339
601
  async bootstrap({ strapi }) {
340
602
  await strapi.admin.services.permission.actionProvider.registerMany([
341
603
  { section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
342
604
  { section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN }
343
605
  ]);
606
+ strapi.plugin(PLUGIN).service("settings").projectDefaults();
344
607
  registerPublishGuard(strapi);
608
+ registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
345
609
  },
346
- services: { settings: ({ strapi }) => ({
347
- async get() {
348
- return mergeSaved(await readSettings(strapi), componentUids(strapi));
349
- },
350
- async set(value) {
351
- const next = validateSettings(value, componentUids(strapi));
352
- for (const [uid, entry] of Object.entries(next.components)) {
353
- if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
610
+ services: { settings: ({ strapi }) => {
611
+ let project;
612
+ const projectDefaults = () => {
613
+ if (project !== void 0) return project;
614
+ const code = strapi.plugin(PLUGIN).config("settings");
615
+ project = null;
616
+ if (code) try {
617
+ project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi));
618
+ } catch (error) {
619
+ strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
354
620
  }
355
- await store(strapi).set({ key: "settings", value: next });
356
- return next;
357
- }
358
- }) },
621
+ return project;
622
+ };
623
+ const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi));
624
+ return {
625
+ projectDefaults,
626
+ get,
627
+ async set(value) {
628
+ const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi));
629
+ for (const [uid, entry] of Object.entries(next.components)) {
630
+ if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
631
+ }
632
+ await store(strapi).set({ key: "settings", value: overrides(next, projectDefaults()) });
633
+ return next;
634
+ },
635
+ // Back to the project defaults (or the built-in ones); the legacy copy goes too, or it would be migrated again.
636
+ async reset() {
637
+ await store(strapi).delete({ key: "settings" });
638
+ await strapi.store({ type: "plugin", name: LEGACY_PLUGIN }).delete({ key: "settings" });
639
+ return get();
640
+ }
641
+ };
642
+ } },
359
643
  controllers: {
360
644
  catalog: ({ strapi }) => ({
361
645
  async find(ctx) {
@@ -367,52 +651,92 @@ module.exports = {
367
651
  disabled: plugin.config("disabled"),
368
652
  blockPreview: plugin.config("blockPreview"),
369
653
  groups: plugin.config("groups"),
370
- componentUids: componentUids(strapi)
654
+ componentUids: componentUids(strapi),
655
+ schemas: strapi.components
371
656
  });
372
657
  const settings = await plugin.service("settings").get();
373
658
  const media = await resolveMedia(strapi, settings);
374
659
  for (const [uid, entry] of Object.entries(settings.components)) {
375
- base.components[uid] = { ...base.components[uid], ...media[uid], template: entry.template };
660
+ base.components[uid] = {
661
+ ...base.components[uid],
662
+ ...media[uid],
663
+ template: entry.template,
664
+ ...entry.typology && { typology: entry.typology },
665
+ ...entry.tags && { tags: entry.tags }
666
+ };
376
667
  }
668
+ const hidden = hiddenName(plugin.config("hiddenAttribute")) || null;
377
669
  ctx.body = {
378
670
  ...base,
379
671
  palette: settings.palette,
380
- editor: { ...settings.editor, enabled: settings.editor.enabled && !base.disabled }
672
+ contentTypes: settings.contentTypes,
673
+ hiddenAttribute: hidden,
674
+ editor: { ...settings.editor, enabled: settings.editor.enabled && !base.disabled, ...!hidden && { hiddenBlocks: "off" } }
381
675
  };
382
676
  }
383
677
  }),
384
678
  settings: ({ strapi }) => ({
385
679
  async find(ctx) {
386
680
  const settings = await strapi.plugin(PLUGIN).service("settings").get();
681
+ const auto = catalog({ components: strapi.plugin(PLUGIN).config("components"), schemas: strapi.components }).components;
387
682
  const components = Object.entries(strapi.components || {}).map(([uid, schema]) => ({
388
683
  uid,
389
684
  displayName: schema.info?.displayName || uid,
390
- category: schema.category || uid.split(".")[0]
685
+ category: schema.category || uid.split(".")[0],
686
+ typology: auto[uid]?.typology
687
+ }));
688
+ const contentTypes = Object.keys(contentTypeUids(strapi)).map((uid) => ({
689
+ uid,
690
+ displayName: strapi.contentTypes[uid].info?.displayName || uid,
691
+ kind: strapi.contentTypes[uid].kind,
692
+ attributes: Object.entries(strapi.contentTypes[uid].attributes || {}).filter(([, attr]) => attr?.type !== "dynamiczone" && !attr?.private).map(([name, attr]) => ({ name, type: attr.type }))
391
693
  }));
392
694
  ctx.body = {
393
695
  settings,
394
696
  components,
697
+ contentTypes,
395
698
  media: await resolveMedia(strapi, settings),
396
699
  templates: TEMPLATES,
700
+ typologies: TYPOLOGIES,
397
701
  disabled: strapi.plugin(PLUGIN).config("disabled") === true,
702
+ hiddenAttribute: hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")) || null,
398
703
  blockPreviewAvailable: strapi.plugin(PLUGIN).config("blockPreview") === true,
399
- defaults: DEFAULTS
704
+ defaults: DEFAULTS,
705
+ projectDefaults: strapi.plugin(PLUGIN).service("settings").projectDefaults()
400
706
  };
401
707
  },
402
708
  async update(ctx) {
403
709
  ctx.body = await strapi.plugin(PLUGIN).service("settings").set(ctx.request?.body);
710
+ },
711
+ async reset(ctx) {
712
+ ctx.body = await strapi.plugin(PLUGIN).service("settings").reset();
713
+ }
714
+ }),
715
+ prefs: ({ strapi }) => ({
716
+ async find(ctx) {
717
+ const id = ctx.state?.user?.id;
718
+ if (!id) return ctx.unauthorized();
719
+ ctx.body = mergePrefs(await store(strapi).get({ key: `prefs:${id}` }), componentUids(strapi));
720
+ },
721
+ async update(ctx) {
722
+ const id = ctx.state?.user?.id;
723
+ if (!id) return ctx.unauthorized();
724
+ const value = validatePrefs(ctx.request?.body, componentUids(strapi));
725
+ await store(strapi).set({ key: `prefs:${id}`, value });
726
+ ctx.body = value;
404
727
  }
405
728
  })
406
729
  },
407
730
  routes: { admin: { type: "admin", routes: [
408
731
  { method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
409
- ...["find", "update"].map((handler, index) => ({
410
- method: index ? "PUT" : "GET",
732
+ ...["GET", "PUT"].map((method) => ({ method, path: "/me/prefs", handler: `prefs.${method === "GET" ? "find" : "update"}`, config: { policies: ["admin::isAuthenticatedAdmin"] } })),
733
+ ...[["GET", "find", "read"], ["PUT", "update", "update"], ["DELETE", "reset", "update"]].map(([method, handler, action]) => ({
734
+ method,
411
735
  path: "/settings",
412
736
  handler: `settings.${handler}`,
413
737
  config: { policies: ["admin::isAuthenticatedAdmin", {
414
738
  name: "admin::hasPermissions",
415
- config: { actions: [`plugin::${PLUGIN}.settings.${index ? "update" : "read"}`] }
739
+ config: { actions: [`plugin::${PLUGIN}.settings.${action}`] }
416
740
  }] }
417
741
  }))
418
742
  ] } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aduptive/strapi-blockscene",
3
- "version": "2.0.0-alpha.4",
3
+ "version": "2.0.0-alpha.5",
4
4
  "description": "Visual component gallery for native Strapi dynamic zones.",
5
5
  "license": "MIT",
6
6
  "author": "Andrea Scarpello <aduptive@gmail.com>",