@aduptive/strapi-blockscene 1.0.0-alpha.1
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/LICENSE +21 -0
- package/README.md +340 -0
- package/dist/admin.cjs +1838 -0
- package/dist/admin.mjs +1808 -0
- package/dist/server.cjs +419 -0
- package/package.json +67 -0
- package/strapi-admin.js +1 -0
- package/strapi-server.js +1 -0
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
4
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// server/groups.js
|
|
8
|
+
var require_groups = __commonJS({
|
|
9
|
+
"server/groups.js"(exports2, module2) {
|
|
10
|
+
"use strict";
|
|
11
|
+
var UID = /^[a-z0-9-]+\.[a-z0-9-]+$/;
|
|
12
|
+
function safeGroups2(value, knownUids) {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const [open, close] of Object.entries(value)) {
|
|
16
|
+
if (!UID.test(open) || typeof close !== "string" || !UID.test(close) || open === close) continue;
|
|
17
|
+
if (knownUids && (!knownUids.includes(open) || !knownUids.includes(close))) continue;
|
|
18
|
+
out[open] = close;
|
|
19
|
+
}
|
|
20
|
+
for (const close of Object.values(out)) if (out[close]) return null;
|
|
21
|
+
return Object.keys(out).length ? out : null;
|
|
22
|
+
}
|
|
23
|
+
var uidOf = (row) => typeof row?.__component === "string" ? row.__component : "";
|
|
24
|
+
var isOpener = (row, g) => Boolean(g) && Object.prototype.hasOwnProperty.call(g, uidOf(row));
|
|
25
|
+
var isClose = (row, g) => Boolean(g) && Object.values(g).includes(uidOf(row));
|
|
26
|
+
var blockKey = (row) => row && row.id != null ? `${row.__component}#${row.id}` : row && row.__temp_key__ != null ? String(row.__temp_key__) : null;
|
|
27
|
+
function validateGroups2(rows, g) {
|
|
28
|
+
const errors = [];
|
|
29
|
+
if (!g || !Array.isArray(rows)) return errors;
|
|
30
|
+
const stack = [];
|
|
31
|
+
rows.forEach((row, index) => {
|
|
32
|
+
const uid = uidOf(row);
|
|
33
|
+
if (isOpener(row, g)) {
|
|
34
|
+
stack.push({ index, uid, close: g[uid] });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!isClose(row, g)) return;
|
|
38
|
+
const top = stack[stack.length - 1];
|
|
39
|
+
if (!top) errors.push({ code: "closeBeforeOpen", index, uid });
|
|
40
|
+
else if (top.close !== uid) errors.push({ code: "mismatch", index, uid, expected: top.close, open: top.uid, openIndex: top.index });
|
|
41
|
+
else stack.pop();
|
|
42
|
+
});
|
|
43
|
+
for (const open of stack) errors.push({ code: "unclosed", index: open.index, uid: open.uid, expected: open.close });
|
|
44
|
+
return errors;
|
|
45
|
+
}
|
|
46
|
+
function groupRange(rows, start, g) {
|
|
47
|
+
const stack = [g[uidOf(rows[start])]];
|
|
48
|
+
for (let i = start + 1; i < rows.length; i++) {
|
|
49
|
+
const row = rows[i];
|
|
50
|
+
if (isOpener(row, g)) stack.push(g[uidOf(row)]);
|
|
51
|
+
else if (isClose(row, g) && uidOf(row) === stack[stack.length - 1]) {
|
|
52
|
+
stack.pop();
|
|
53
|
+
if (!stack.length) return [start, i];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [start, rows.length - 1];
|
|
57
|
+
}
|
|
58
|
+
function groupRows(rows, g, depth = 0, offset = 0) {
|
|
59
|
+
const out = [];
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (i < rows.length) {
|
|
62
|
+
const row = rows[i];
|
|
63
|
+
if (isOpener(row, g)) {
|
|
64
|
+
const [, end] = groupRange(rows, i, g);
|
|
65
|
+
const closed = uidOf(rows[end]) === g[uidOf(row)] && end > i;
|
|
66
|
+
out.push({
|
|
67
|
+
type: "group",
|
|
68
|
+
key: blockKey(row),
|
|
69
|
+
closeKey: closed ? blockKey(rows[end]) : null,
|
|
70
|
+
row,
|
|
71
|
+
index: offset + i,
|
|
72
|
+
depth,
|
|
73
|
+
children: groupRows(rows.slice(i + 1, closed ? end : end + 1), g, depth + 1, offset + i + 1)
|
|
74
|
+
});
|
|
75
|
+
i = end + 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
out.push({ type: "block", key: blockKey(row), row, index: offset + i, depth, stray: isClose(row, g) });
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function topLevelRanges(rows, g) {
|
|
84
|
+
const ranges = [];
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < rows.length) {
|
|
87
|
+
const end = isOpener(rows[i], g) ? groupRange(rows, i, g)[1] : i;
|
|
88
|
+
ranges.push([i, end]);
|
|
89
|
+
i = end + 1;
|
|
90
|
+
}
|
|
91
|
+
return ranges;
|
|
92
|
+
}
|
|
93
|
+
function moveGroup(rows, key, direction, g) {
|
|
94
|
+
if (!g) return null;
|
|
95
|
+
const ranges = topLevelRanges(rows, g);
|
|
96
|
+
const at = ranges.findIndex(([s]) => blockKey(rows[s]) === String(key));
|
|
97
|
+
const to = at + (direction === "up" ? -1 : 1);
|
|
98
|
+
if (at < 0 || to < 0 || to >= ranges.length) return null;
|
|
99
|
+
const [a, b] = direction === "up" ? [ranges[to], ranges[at]] : [ranges[at], ranges[to]];
|
|
100
|
+
return [...rows.slice(0, a[0]), ...rows.slice(b[0], b[1] + 1), ...rows.slice(a[0], a[1] + 1), ...rows.slice(b[1] + 1)];
|
|
101
|
+
}
|
|
102
|
+
function removeGroup(rows, key, g) {
|
|
103
|
+
if (!g) return null;
|
|
104
|
+
const start = rows.findIndex((row) => blockKey(row) === String(key));
|
|
105
|
+
if (start < 0 || !isOpener(rows[start], g)) return null;
|
|
106
|
+
const [, end] = groupRange(rows, start, g);
|
|
107
|
+
return [...rows.slice(0, start), ...rows.slice(end + 1)];
|
|
108
|
+
}
|
|
109
|
+
module2.exports = { safeGroups: safeGroups2, validateGroups: validateGroups2, groupRange, groupRows, topLevelRanges, moveGroup, removeGroup, blockKey, isOpener, isClose };
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// server/settings.js
|
|
114
|
+
var require_settings = __commonJS({
|
|
115
|
+
"server/settings.js"(exports2, module2) {
|
|
116
|
+
"use strict";
|
|
117
|
+
var PLUGIN2 = "blockscene";
|
|
118
|
+
var TEMPLATES2 = ["generic", "banner", "cards", "imageText", "faq", "none"];
|
|
119
|
+
var INITIAL_STATES = ["closed", "open", "remember"];
|
|
120
|
+
var PREVIEW_MODES = ["form", "split", "preview"];
|
|
121
|
+
var DEFAULTS2 = {
|
|
122
|
+
palette: { background: "#F6F6F9", surface: "#DCDCE4", text: "#32324D", accent: "#4945FF" },
|
|
123
|
+
components: {},
|
|
124
|
+
// previewUrl: '' means "reuse Strapi's native Preview origin when configured"; a URL overrides it.
|
|
125
|
+
// blockPreviewInForm: compact read-only preview above a block's fields in form
|
|
126
|
+
// mode. Only installations that ship the accordion integration (server config
|
|
127
|
+
// `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
|
+
};
|
|
130
|
+
var PREVIEW_URL = /^https?:\/\/[^\s"'<>]{1,500}$/;
|
|
131
|
+
var COLOR = /^#[0-9A-Fa-f]{6}$/;
|
|
132
|
+
var UID = /^[a-z0-9-]+\.[a-z0-9-]+$/;
|
|
133
|
+
var safeUrl2 = (value) => typeof value === "string" && (/^\/(?!\/)/.test(value) || /^https?:\/\//.test(value)) ? value : void 0;
|
|
134
|
+
var { safeGroups: safeGroups2 } = require_groups();
|
|
135
|
+
var safeVersion = (value) => typeof value === "string" && /^[\w.-]{1,32}$/.test(value) ? value : void 0;
|
|
136
|
+
var fail2 = (message, details) => {
|
|
137
|
+
let ValidationError;
|
|
138
|
+
try {
|
|
139
|
+
ValidationError = require("@strapi/utils").errors.ValidationError;
|
|
140
|
+
} catch {
|
|
141
|
+
ValidationError = class extends Error {
|
|
142
|
+
constructor(m, d) {
|
|
143
|
+
super(m);
|
|
144
|
+
this.name = "ValidationError";
|
|
145
|
+
this.details = d;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
throw new ValidationError(message, details);
|
|
150
|
+
};
|
|
151
|
+
function catalog2(config = {}) {
|
|
152
|
+
const entries = {};
|
|
153
|
+
for (const [uid, entry] of Object.entries(config.components || {})) {
|
|
154
|
+
if (!entry || typeof entry !== "object") continue;
|
|
155
|
+
entries[uid] = Object.fromEntries(
|
|
156
|
+
["label", "description", "category", "keywords"].filter((key) => typeof entry[key] === "string").map((key) => [key, entry[key]])
|
|
157
|
+
);
|
|
158
|
+
entries[uid].image = safeUrl2(entry.image);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
components: entries,
|
|
162
|
+
previewBaseUrl: safeUrl2(config.previewBaseUrl) || "/block-previews",
|
|
163
|
+
previewVersion: safeVersion(config.previewVersion),
|
|
164
|
+
disabled: config.disabled === true,
|
|
165
|
+
blockPreviewAvailable: config.blockPreview === true,
|
|
166
|
+
// Optional OPEN -> CLOSE map (see groups.js); null means every component is an ordinary block.
|
|
167
|
+
groups: safeGroups2(config.groups, config.componentUids)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function validateSettings2(input, componentUids2) {
|
|
171
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
|
|
172
|
+
if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
|
|
173
|
+
const out = structuredClone(DEFAULTS2);
|
|
174
|
+
for (const key of Object.keys(input)) if (!(key in DEFAULTS2)) fail2(`Unknown setting "${key}"`);
|
|
175
|
+
for (const [key, value] of Object.entries(input.palette || {})) {
|
|
176
|
+
if (!(key in DEFAULTS2.palette)) fail2(`Unknown palette color "${key}"`);
|
|
177
|
+
if (!COLOR.test(String(value))) fail2(`Color "${key}" must use #RRGGBB`);
|
|
178
|
+
out.palette[key] = String(value).toUpperCase();
|
|
179
|
+
}
|
|
180
|
+
for (const [key, value] of Object.entries(input.editor || {})) {
|
|
181
|
+
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";
|
|
183
|
+
if (!valid) fail2(`Invalid value for editor option "${key}"`);
|
|
184
|
+
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : value;
|
|
185
|
+
}
|
|
186
|
+
const components = input.components || {};
|
|
187
|
+
if (typeof components !== "object" || Array.isArray(components)) fail2("components must be an object");
|
|
188
|
+
for (const [uid, entry] of Object.entries(components)) {
|
|
189
|
+
if (!UID.test(uid) || !componentUids2.includes(uid)) fail2(`Unknown component "${uid}"`);
|
|
190
|
+
if (!entry || typeof entry !== "object") fail2(`Invalid entry for "${uid}"`);
|
|
191
|
+
const clean = {};
|
|
192
|
+
for (const key of Object.keys(entry)) {
|
|
193
|
+
if (key === "template") {
|
|
194
|
+
if (!TEMPLATES2.includes(entry.template)) fail2(`Unknown template for "${uid}"`);
|
|
195
|
+
clean.template = entry.template;
|
|
196
|
+
} else if (key === "mediaId") {
|
|
197
|
+
if (!Number.isInteger(entry.mediaId) || entry.mediaId <= 0) fail2(`Invalid media for "${uid}"`);
|
|
198
|
+
clean.mediaId = entry.mediaId;
|
|
199
|
+
} else fail2(`Unknown component setting "${key}"`);
|
|
200
|
+
}
|
|
201
|
+
if (Object.keys(clean).length) out.components[uid] = clean;
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
function mergeSaved2(saved, componentUids2) {
|
|
206
|
+
const out = structuredClone(DEFAULTS2);
|
|
207
|
+
if (!saved || typeof saved !== "object") return out;
|
|
208
|
+
for (const key of Object.keys(DEFAULTS2.palette)) if (COLOR.test(String(saved.palette?.[key]))) out.palette[key] = saved.palette[key];
|
|
209
|
+
for (const key of Object.keys(DEFAULTS2.editor)) {
|
|
210
|
+
const value = saved.editor?.[key];
|
|
211
|
+
if (value === void 0 || typeof value !== typeof DEFAULTS2.editor[key]) continue;
|
|
212
|
+
if (key === "initialState" && !INITIAL_STATES.includes(value)) continue;
|
|
213
|
+
if (key === "previewMode" && !PREVIEW_MODES.includes(value)) continue;
|
|
214
|
+
if (key === "previewUrl" && value && !PREVIEW_URL.test(value)) continue;
|
|
215
|
+
out.editor[key] = value;
|
|
216
|
+
}
|
|
217
|
+
for (const [uid, entry] of Object.entries(saved.components || {})) if (componentUids2.includes(uid) && entry && typeof entry === "object") out.components[uid] = { ...entry };
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
module2.exports = { PLUGIN: PLUGIN2, TEMPLATES: TEMPLATES2, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, mergeSaved: mergeSaved2, safeUrl: safeUrl2, fail: fail2 };
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// server/index.js
|
|
225
|
+
var { PLUGIN, TEMPLATES, DEFAULTS, catalog, validateSettings, mergeSaved, safeUrl, fail } = require_settings();
|
|
226
|
+
var { safeGroups, validateGroups } = require_groups();
|
|
227
|
+
var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
|
|
228
|
+
var LEGACY_PLUGIN = "block-picker";
|
|
229
|
+
async function readSettings(strapi) {
|
|
230
|
+
const current = await store(strapi).get({ key: "settings" });
|
|
231
|
+
if (current) return current;
|
|
232
|
+
const legacy = await strapi.store({ type: "plugin", name: LEGACY_PLUGIN }).get({ key: "settings" });
|
|
233
|
+
if (legacy) {
|
|
234
|
+
await store(strapi).set({ key: "settings", value: legacy });
|
|
235
|
+
strapi.log.info(`[${PLUGIN}] settings migrated from "${LEGACY_PLUGIN}"`);
|
|
236
|
+
}
|
|
237
|
+
return legacy;
|
|
238
|
+
}
|
|
239
|
+
var componentUids = (strapi) => Object.keys(strapi.components || {});
|
|
240
|
+
var findMedia = (strapi, id) => strapi.db.query("plugin::upload.file").findOne({ where: { id }, select: ["id", "url", "mime", "name"] });
|
|
241
|
+
async function resolveMedia(strapi, settings) {
|
|
242
|
+
const resolved = {};
|
|
243
|
+
for (const [uid, entry] of Object.entries(settings.components)) {
|
|
244
|
+
if (!entry.mediaId) continue;
|
|
245
|
+
const file = await findMedia(strapi, entry.mediaId).catch(() => null);
|
|
246
|
+
resolved[uid] = file && safeUrl(file.url) && String(file.mime || "").startsWith("image/") ? { manualImage: file.url, manualName: file.name } : { manualMissing: true };
|
|
247
|
+
}
|
|
248
|
+
return resolved;
|
|
249
|
+
}
|
|
250
|
+
var zonesOf = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
|
|
251
|
+
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
|
+
function checkZones(strapi, uid, entry, groups) {
|
|
253
|
+
for (const name of zonesOf(strapi.getModel(uid))) {
|
|
254
|
+
const errors = validateGroups(Array.isArray(entry?.[name]) ? entry[name] : [], groups);
|
|
255
|
+
if (errors.length) fail(
|
|
256
|
+
`Block groups in "${name}" are not balanced; fix the markers before publishing (${errors.map(label).join("; ")}).`,
|
|
257
|
+
{ plugin: PLUGIN, field: name, errors: errors.map((error) => ({ path: [name, error.index], message: label(error), name: "ValidationError", code: error.code, uid: error.uid, expected: error.expected })) }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function registerPublishGuard(strapi) {
|
|
262
|
+
const plugin = strapi.plugin(PLUGIN);
|
|
263
|
+
const groups = safeGroups(plugin.config("groups"), componentUids(strapi));
|
|
264
|
+
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
|
+
if (!groups) return;
|
|
266
|
+
if (strapi.documents?.use) {
|
|
267
|
+
const defaultLocale = async () => {
|
|
268
|
+
try {
|
|
269
|
+
return await strapi.plugin("i18n")?.service("locales")?.getDefaultLocale?.();
|
|
270
|
+
} catch {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
strapi.documents.use(async (ctx, next) => {
|
|
275
|
+
const { action, params = {} } = ctx;
|
|
276
|
+
const publishing = action === "publish" || (action === "create" || action === "update") && params.status === "published";
|
|
277
|
+
if (!publishing || !ctx.uid) return next();
|
|
278
|
+
const model = strapi.getModel(ctx.uid);
|
|
279
|
+
const zones = zonesOf(model);
|
|
280
|
+
if (!zones.length) return next();
|
|
281
|
+
if (action === "create") {
|
|
282
|
+
checkZones(strapi, ctx.uid, params.data || {}, groups);
|
|
283
|
+
return next();
|
|
284
|
+
}
|
|
285
|
+
const populate = Object.fromEntries(zones.map((name) => [name, true]));
|
|
286
|
+
const localized = Boolean(model?.pluginOptions?.i18n?.localized);
|
|
287
|
+
const { documentId } = params;
|
|
288
|
+
let drafts;
|
|
289
|
+
if (!localized) drafts = [await strapi.documents(ctx.uid).findOne({ documentId, status: "draft", populate })];
|
|
290
|
+
else if (params.locale === "*") drafts = await strapi.documents(ctx.uid).findMany({ filters: { documentId }, status: "draft", locale: "*", populate });
|
|
291
|
+
else {
|
|
292
|
+
const locale = params.locale || await defaultLocale();
|
|
293
|
+
drafts = [await strapi.documents(ctx.uid).findOne({ documentId, locale, status: "draft", populate })];
|
|
294
|
+
}
|
|
295
|
+
for (const draft of drafts) {
|
|
296
|
+
const entry = action === "update" ? { ...draft || {}, ...Object.fromEntries(zones.filter((name) => Array.isArray(params.data?.[name])).map((name) => [name, params.data[name]])) } : draft;
|
|
297
|
+
checkZones(strapi, ctx.uid, entry, groups);
|
|
298
|
+
}
|
|
299
|
+
return next();
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const published = (data) => Boolean(data && data.publishedAt);
|
|
304
|
+
strapi.db.lifecycles.subscribe({
|
|
305
|
+
async beforeCreate(event) {
|
|
306
|
+
if (published(event.params?.data)) checkZones(strapi, event.model.uid, event.params.data, groups);
|
|
307
|
+
},
|
|
308
|
+
async beforeUpdate(event) {
|
|
309
|
+
const { data, where } = event.params || {};
|
|
310
|
+
const zones = zonesOf(strapi.getModel(event.model.uid));
|
|
311
|
+
if (!published(data) || !zones.length) return;
|
|
312
|
+
const entry = zones.every((name) => Array.isArray(data[name])) ? data : { ...await strapi.entityService.findOne(event.model.uid, where?.id, { populate: zones }), ...data };
|
|
313
|
+
checkZones(strapi, event.model.uid, entry, groups);
|
|
314
|
+
},
|
|
315
|
+
async beforeUpdateMany(event) {
|
|
316
|
+
const { data, where } = event.params || {};
|
|
317
|
+
const zones = zonesOf(strapi.getModel(event.model.uid));
|
|
318
|
+
if (!published(data) || !zones.length) return;
|
|
319
|
+
for (const entry of await strapi.entityService.findMany(event.model.uid, { filters: where, populate: zones })) checkZones(strapi, event.model.uid, entry, groups);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
module.exports = {
|
|
324
|
+
config: {
|
|
325
|
+
default: {
|
|
326
|
+
components: {},
|
|
327
|
+
previewBaseUrl: "/block-previews",
|
|
328
|
+
previewVersion: void 0,
|
|
329
|
+
// Emergency bypass read once at boot: BLOCKSCENE_DISABLED=true forces the native editor. Restart to change.
|
|
330
|
+
disabled: process.env.BLOCKSCENE_DISABLED === "true",
|
|
331
|
+
// true only where the Content Manager ships the per-block preview slot (see docs).
|
|
332
|
+
blockPreview: false,
|
|
333
|
+
// Optional layout groups: OPEN component uid -> its CLOSE uid, e.g. { 'wrappers.join': 'wrappers.close' }.
|
|
334
|
+
// Empty/absent: every Dynamic Zone component is an ordinary block. See README "Layout groups".
|
|
335
|
+
groups: null
|
|
336
|
+
},
|
|
337
|
+
validator: catalog
|
|
338
|
+
},
|
|
339
|
+
async bootstrap({ strapi }) {
|
|
340
|
+
await strapi.admin.services.permission.actionProvider.registerMany([
|
|
341
|
+
{ section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
|
|
342
|
+
{ section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN }
|
|
343
|
+
]);
|
|
344
|
+
registerPublishGuard(strapi);
|
|
345
|
+
},
|
|
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`);
|
|
354
|
+
}
|
|
355
|
+
await store(strapi).set({ key: "settings", value: next });
|
|
356
|
+
return next;
|
|
357
|
+
}
|
|
358
|
+
}) },
|
|
359
|
+
controllers: {
|
|
360
|
+
catalog: ({ strapi }) => ({
|
|
361
|
+
async find(ctx) {
|
|
362
|
+
const plugin = strapi.plugin(PLUGIN);
|
|
363
|
+
const base = catalog({
|
|
364
|
+
components: plugin.config("components"),
|
|
365
|
+
previewBaseUrl: plugin.config("previewBaseUrl"),
|
|
366
|
+
previewVersion: plugin.config("previewVersion"),
|
|
367
|
+
disabled: plugin.config("disabled"),
|
|
368
|
+
blockPreview: plugin.config("blockPreview"),
|
|
369
|
+
groups: plugin.config("groups"),
|
|
370
|
+
componentUids: componentUids(strapi)
|
|
371
|
+
});
|
|
372
|
+
const settings = await plugin.service("settings").get();
|
|
373
|
+
const media = await resolveMedia(strapi, settings);
|
|
374
|
+
for (const [uid, entry] of Object.entries(settings.components)) {
|
|
375
|
+
base.components[uid] = { ...base.components[uid], ...media[uid], template: entry.template };
|
|
376
|
+
}
|
|
377
|
+
ctx.body = {
|
|
378
|
+
...base,
|
|
379
|
+
palette: settings.palette,
|
|
380
|
+
editor: { ...settings.editor, enabled: settings.editor.enabled && !base.disabled }
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}),
|
|
384
|
+
settings: ({ strapi }) => ({
|
|
385
|
+
async find(ctx) {
|
|
386
|
+
const settings = await strapi.plugin(PLUGIN).service("settings").get();
|
|
387
|
+
const components = Object.entries(strapi.components || {}).map(([uid, schema]) => ({
|
|
388
|
+
uid,
|
|
389
|
+
displayName: schema.info?.displayName || uid,
|
|
390
|
+
category: schema.category || uid.split(".")[0]
|
|
391
|
+
}));
|
|
392
|
+
ctx.body = {
|
|
393
|
+
settings,
|
|
394
|
+
components,
|
|
395
|
+
media: await resolveMedia(strapi, settings),
|
|
396
|
+
templates: TEMPLATES,
|
|
397
|
+
disabled: strapi.plugin(PLUGIN).config("disabled") === true,
|
|
398
|
+
blockPreviewAvailable: strapi.plugin(PLUGIN).config("blockPreview") === true,
|
|
399
|
+
defaults: DEFAULTS
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
async update(ctx) {
|
|
403
|
+
ctx.body = await strapi.plugin(PLUGIN).service("settings").set(ctx.request?.body);
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
},
|
|
407
|
+
routes: { admin: { type: "admin", routes: [
|
|
408
|
+
{ method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
|
|
409
|
+
...["find", "update"].map((handler, index) => ({
|
|
410
|
+
method: index ? "PUT" : "GET",
|
|
411
|
+
path: "/settings",
|
|
412
|
+
handler: `settings.${handler}`,
|
|
413
|
+
config: { policies: ["admin::isAuthenticatedAdmin", {
|
|
414
|
+
name: "admin::hasPermissions",
|
|
415
|
+
config: { actions: [`plugin::${PLUGIN}.settings.${index ? "update" : "read"}`] }
|
|
416
|
+
}] }
|
|
417
|
+
}))
|
|
418
|
+
] } }
|
|
419
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aduptive/strapi-blockscene",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "Visual component gallery for native Strapi dynamic zones.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Andrea Scarpello <aduptive@gmail.com>",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"strapi",
|
|
9
|
+
"strapi-plugin",
|
|
10
|
+
"blockscene"
|
|
11
|
+
],
|
|
12
|
+
"main": "strapi-server.js",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"strapi-admin.js",
|
|
16
|
+
"strapi-server.js",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
"./strapi-admin": {
|
|
22
|
+
"import": "./dist/admin.mjs",
|
|
23
|
+
"require": "./dist/admin.cjs",
|
|
24
|
+
"default": "./dist/admin.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./strapi-server": {
|
|
27
|
+
"require": "./strapi-server.js",
|
|
28
|
+
"default": "./strapi-server.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"strapi": {
|
|
33
|
+
"name": "blockscene",
|
|
34
|
+
"displayName": "Blockscene",
|
|
35
|
+
"kind": "plugin",
|
|
36
|
+
"description": "Independent of the frontend framework."
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.17 <21"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@strapi/strapi": ">=4.20.0 <5",
|
|
43
|
+
"@strapi/design-system": "^1.19.0",
|
|
44
|
+
"react": "^18.0.0",
|
|
45
|
+
"react-dom": "^18.0.0",
|
|
46
|
+
"styled-components": "^5.3.0",
|
|
47
|
+
"react-intl": "^6.0.0",
|
|
48
|
+
"@strapi/helper-plugin": ">=4.20.0 <5",
|
|
49
|
+
"@strapi/admin": ">=4.20.0 <5"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"fractional-indexing": "3.2.0"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/aduptive/strapi-plugin-blockscene.git",
|
|
57
|
+
"directory": "packages/strapi4"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/aduptive/strapi-plugin-blockscene#readme",
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/aduptive/strapi-plugin-blockscene/issues"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public",
|
|
65
|
+
"tag": "strapi4"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/strapi-admin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/admin.cjs').default
|
package/strapi-server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/server.cjs')
|