strapi-plugin-dynamic-zone-tools 1.0.0
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/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/admin/custom.d.ts +2 -0
- package/admin/src/components/DynamicZoneComponentDuplicateInjector.tsx +604 -0
- package/admin/src/components/DynamicZoneEditViewExtensions.tsx +7 -0
- package/admin/src/components/DynamicZoneToolsPanel.tsx +1027 -0
- package/admin/src/components/FillFromRecord.tsx +36 -0
- package/admin/src/components/Initializer.tsx +19 -0
- package/admin/src/components/PluginIcon.tsx +5 -0
- package/admin/src/index.ts +61 -0
- package/admin/src/pages/App.tsx +15 -0
- package/admin/src/pages/HomePage.tsx +16 -0
- package/admin/src/pluginId.ts +1 -0
- package/admin/src/translations/en.json +51 -0
- package/admin/src/utils/createRowActionButton.ts +57 -0
- package/admin/src/utils/createRowActionMenu.ts +276 -0
- package/admin/src/utils/dynamicZoneClipboard.ts +134 -0
- package/admin/src/utils/dynamicZonePaths.ts +236 -0
- package/admin/src/utils/getTranslation.ts +5 -0
- package/admin/src/utils/prepareDynamicZoneData.ts +625 -0
- package/admin/src/utils/relationQueryParams.ts +19 -0
- package/admin/tsconfig.build.json +10 -0
- package/admin/tsconfig.json +12 -0
- package/dist/admin/en-Ce0ZP0MJ.js +54 -0
- package/dist/admin/en-DrSdJbJW.mjs +54 -0
- package/dist/admin/index.js +2161 -0
- package/dist/admin/index.mjs +2159 -0
- package/dist/admin/src/index.d.ts +12 -0
- package/dist/server/index.js +137 -0
- package/dist/server/index.mjs +137 -0
- package/dist/server/src/index.d.ts +55 -0
- package/package.json +112 -0
- package/server/src/bootstrap.ts +18 -0
- package/server/src/config/index.ts +4 -0
- package/server/src/content-types/index.ts +1 -0
- package/server/src/controllers/controller.ts +85 -0
- package/server/src/controllers/index.ts +5 -0
- package/server/src/destroy.ts +7 -0
- package/server/src/index.ts +30 -0
- package/server/src/middlewares/index.ts +1 -0
- package/server/src/policies/index.ts +1 -0
- package/server/src/register.ts +7 -0
- package/server/src/routes/admin-api.ts +18 -0
- package/server/src/routes/content-api.ts +1 -0
- package/server/src/routes/index.ts +15 -0
- package/server/src/services/index.ts +5 -0
- package/server/src/services/service.ts +9 -0
- package/server/tsconfig.build.json +10 -0
- package/server/tsconfig.json +11 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +3 -0
|
@@ -0,0 +1,2159 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { jsx, Fragment, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Flex, Dialog, Box, Typography, Alert, SingleSelect, SingleSelectOption, Button, Checkbox } from "@strapi/design-system";
|
|
4
|
+
import { useFetchClient, useForm, useNotification, useQueryParams, unstable_useContentManagerContext } from "@strapi/strapi/admin";
|
|
5
|
+
import { useIntl } from "react-intl";
|
|
6
|
+
import { Duplicate, WarningCircle } from "@strapi/icons";
|
|
7
|
+
import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing";
|
|
8
|
+
const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
|
|
9
|
+
const v = glob[path];
|
|
10
|
+
if (v) {
|
|
11
|
+
return typeof v === "function" ? v() : Promise.resolve(v);
|
|
12
|
+
}
|
|
13
|
+
return new Promise((_, reject) => {
|
|
14
|
+
(typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
|
|
15
|
+
reject.bind(
|
|
16
|
+
null,
|
|
17
|
+
new Error(
|
|
18
|
+
"Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const PLUGIN_ID = "dynamic-zone-tools";
|
|
25
|
+
const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
|
|
26
|
+
const Initializer = ({ setPlugin }) => {
|
|
27
|
+
const ref = useRef(setPlugin);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
ref.current(PLUGIN_ID);
|
|
30
|
+
}, []);
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
const TRANSIENT_ENTRY_KEYS = /* @__PURE__ */ new Set(["id", "documentId", "__temp_key__"]);
|
|
34
|
+
const ENTRY_LABEL_KEYS = ["title", "heading", "name", "label", "displayName", "question", "slug"];
|
|
35
|
+
function getDynamicZoneEntryLabel(entry) {
|
|
36
|
+
if (!entry || typeof entry !== "object") {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
for (const key of ENTRY_LABEL_KEYS) {
|
|
40
|
+
const value = entry[key];
|
|
41
|
+
if (typeof value === "string" && value.trim()) {
|
|
42
|
+
return value.trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const componentUid = entry.__component;
|
|
46
|
+
if (typeof componentUid === "string") {
|
|
47
|
+
const shortName = componentUid.split(".").pop() || componentUid;
|
|
48
|
+
return shortName;
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
function getComponentDisplayName(componentUid) {
|
|
53
|
+
return componentUid.split(".").pop() || componentUid;
|
|
54
|
+
}
|
|
55
|
+
function filterAllowedDynamicZoneEntries(entries, allowedComponents) {
|
|
56
|
+
if (!Array.isArray(entries)) {
|
|
57
|
+
return { allowed: [], skippedCount: 0 };
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(allowedComponents)) {
|
|
60
|
+
return { allowed: entries, skippedCount: 0 };
|
|
61
|
+
}
|
|
62
|
+
const allowed = entries.filter((entry) => allowedComponents.includes(entry?.__component));
|
|
63
|
+
return { allowed, skippedCount: entries.length - allowed.length };
|
|
64
|
+
}
|
|
65
|
+
async function loadComponentSchemas(get) {
|
|
66
|
+
const response = await get("/content-manager/init");
|
|
67
|
+
const payload = response?.data?.data ?? response?.data ?? {};
|
|
68
|
+
const map = {};
|
|
69
|
+
(payload.components ?? []).forEach((component) => {
|
|
70
|
+
map[component.uid] = component.attributes ?? component.schema?.attributes ?? {};
|
|
71
|
+
});
|
|
72
|
+
return map;
|
|
73
|
+
}
|
|
74
|
+
function prepareZoneData(entries, schemas, insertOptions) {
|
|
75
|
+
const buildConnectItems = (value, attribute) => {
|
|
76
|
+
const items = (Array.isArray(value) ? value : [value]).filter(
|
|
77
|
+
(rel) => rel && typeof rel === "object" && rel.documentId
|
|
78
|
+
);
|
|
79
|
+
const keys2 = generateNKeysBetween(void 0, void 0, items.length);
|
|
80
|
+
return items.map((rel, index2) => ({
|
|
81
|
+
id: rel.id,
|
|
82
|
+
apiData: {
|
|
83
|
+
id: rel.id,
|
|
84
|
+
documentId: rel.documentId,
|
|
85
|
+
locale: rel.locale ?? void 0,
|
|
86
|
+
isTemporary: true
|
|
87
|
+
},
|
|
88
|
+
status: rel.status ?? (rel.publishedAt ? "published" : "draft"),
|
|
89
|
+
label: rel.title ?? rel.name ?? rel.label ?? rel.documentId,
|
|
90
|
+
href: `../collection-types/${attribute.target}/${rel.documentId}${rel.locale ? `?plugins[i18n][locale]=${rel.locale}` : ""}`,
|
|
91
|
+
__temp_key__: keys2[index2]
|
|
92
|
+
}));
|
|
93
|
+
};
|
|
94
|
+
const transformComponent = (data, attributes) => {
|
|
95
|
+
const out = {};
|
|
96
|
+
Object.entries(data ?? {}).forEach(([key, value]) => {
|
|
97
|
+
if (key === "id") return;
|
|
98
|
+
if (key === "__component") {
|
|
99
|
+
out[key] = value;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const attribute = attributes?.[key];
|
|
103
|
+
if (!attribute) return;
|
|
104
|
+
if (value === null || value === void 0) {
|
|
105
|
+
if (attribute.type === "boolean") out[key] = value;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
switch (attribute.type) {
|
|
109
|
+
case "component": {
|
|
110
|
+
const nestedAttributes = schemas[attribute.component] ?? {};
|
|
111
|
+
if (attribute.repeatable) {
|
|
112
|
+
const items = Array.isArray(value) ? value : [];
|
|
113
|
+
const keys2 = generateNKeysBetween(void 0, void 0, items.length);
|
|
114
|
+
out[key] = items.map((item, index2) => ({
|
|
115
|
+
...transformComponent(item, nestedAttributes),
|
|
116
|
+
__temp_key__: keys2[index2]
|
|
117
|
+
}));
|
|
118
|
+
} else {
|
|
119
|
+
out[key] = transformComponent(value, nestedAttributes);
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "relation": {
|
|
124
|
+
if (attribute.target) {
|
|
125
|
+
out[key] = { connect: buildConnectItems(value, attribute), disconnect: [] };
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
out[key] = value;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
const { previousKey = null, nextKey = null } = insertOptions ?? {};
|
|
136
|
+
const keys = insertOptions !== void 0 ? generateNKeysBetween(previousKey ?? null, nextKey ?? null, entries.length) : generateNKeysBetween(void 0, void 0, entries.length);
|
|
137
|
+
return entries.map((entry, index2) => ({
|
|
138
|
+
...transformComponent(entry, schemas[entry.__component] ?? {}),
|
|
139
|
+
__component: entry.__component,
|
|
140
|
+
__temp_key__: keys[index2]
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
function prepareZoneDataForInsert(entries, schemas, insertOptions) {
|
|
144
|
+
return prepareZoneData(entries, schemas, insertOptions);
|
|
145
|
+
}
|
|
146
|
+
function clonePlainValue(value) {
|
|
147
|
+
if (typeof structuredClone === "function") {
|
|
148
|
+
return structuredClone(value);
|
|
149
|
+
}
|
|
150
|
+
return JSON.parse(JSON.stringify(value));
|
|
151
|
+
}
|
|
152
|
+
const COLLECTION_TYPES = "collection-types";
|
|
153
|
+
const SINGLE_TYPES = "single-types";
|
|
154
|
+
const ONE_WAY_RELATIONS = /* @__PURE__ */ new Set([
|
|
155
|
+
"oneWay",
|
|
156
|
+
"oneToOne",
|
|
157
|
+
"manyToOne",
|
|
158
|
+
"oneToManyMorph",
|
|
159
|
+
"oneToOneMorph"
|
|
160
|
+
]);
|
|
161
|
+
function isPlainObject$1(value) {
|
|
162
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
163
|
+
}
|
|
164
|
+
function getRelationIdentity(relation) {
|
|
165
|
+
if (!isPlainObject$1(relation)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const documentId = relation.documentId ?? relation.apiData?.documentId;
|
|
169
|
+
const locale = relation.locale ?? relation.apiData?.locale ?? "";
|
|
170
|
+
const id = relation.id ?? relation.apiData?.id;
|
|
171
|
+
if (documentId) {
|
|
172
|
+
return `${documentId}::${locale}`;
|
|
173
|
+
}
|
|
174
|
+
if (id !== null && id !== void 0) {
|
|
175
|
+
return `id:${id}`;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
function getRelationDisplayValue(relation) {
|
|
180
|
+
for (const key of ["label", "title", "name", "displayName", "question", "heading", "slug", "documentId", "id"]) {
|
|
181
|
+
const value = relation[key];
|
|
182
|
+
if (typeof value === "string" && value.trim()) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
if (typeof value === "number") {
|
|
186
|
+
return String(value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
function getRelationCollectionType(targetModel, contentTypes) {
|
|
192
|
+
const targetSchema = contentTypes?.find((schema) => schema?.uid === targetModel);
|
|
193
|
+
return targetSchema?.kind === "singleType" ? SINGLE_TYPES : COLLECTION_TYPES;
|
|
194
|
+
}
|
|
195
|
+
function getRelationHref(targetModel, contentTypes, documentId, locale) {
|
|
196
|
+
if (!targetModel || !documentId) {
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
const collectionType = getRelationCollectionType(targetModel, contentTypes);
|
|
200
|
+
const basePath = collectionType === SINGLE_TYPES ? `../${SINGLE_TYPES}/${targetModel}` : `../${COLLECTION_TYPES}/${targetModel}/${documentId}`;
|
|
201
|
+
return locale ? `${basePath}?plugins[i18n][locale]=${locale}` : basePath;
|
|
202
|
+
}
|
|
203
|
+
function normalizeFetchedRelations(value) {
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return value.filter(isPlainObject$1);
|
|
206
|
+
}
|
|
207
|
+
if (isPlainObject$1(value) && Array.isArray(value.results)) {
|
|
208
|
+
return value.results.filter(isPlainObject$1);
|
|
209
|
+
}
|
|
210
|
+
if (isPlainObject$1(value)) {
|
|
211
|
+
return [value];
|
|
212
|
+
}
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
function toRelationConnectEntry(relation, attribute, contentTypes, tempKey) {
|
|
216
|
+
const id = relation.id ?? relation.apiData?.id;
|
|
217
|
+
const documentId = relation.documentId ?? relation.apiData?.documentId;
|
|
218
|
+
const locale = relation.locale ?? relation.apiData?.locale ?? null;
|
|
219
|
+
const label = relation.label ?? getRelationDisplayValue(relation);
|
|
220
|
+
const href = relation.href ?? getRelationHref(attribute.target, contentTypes, documentId, locale);
|
|
221
|
+
if (id === void 0 && !documentId) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const entry = {
|
|
225
|
+
id,
|
|
226
|
+
documentId,
|
|
227
|
+
locale,
|
|
228
|
+
href,
|
|
229
|
+
label: label || documentId || (id !== void 0 && id !== null ? String(id) : ""),
|
|
230
|
+
__temp_key__: tempKey,
|
|
231
|
+
apiData: {
|
|
232
|
+
id,
|
|
233
|
+
documentId,
|
|
234
|
+
locale,
|
|
235
|
+
isTemporary: true
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
if (relation.status !== void 0) {
|
|
239
|
+
entry.status = relation.status;
|
|
240
|
+
} else if (typeof relation.publishedAt === "string") {
|
|
241
|
+
entry.status = "published";
|
|
242
|
+
}
|
|
243
|
+
return entry;
|
|
244
|
+
}
|
|
245
|
+
async function sanitizeRelationValue(attribute, value, fieldName, sourceContext, contentTypes, fetchRelationItems, createTempKey) {
|
|
246
|
+
const currentConnect = Array.isArray(value?.connect) ? value.connect : [];
|
|
247
|
+
const currentDisconnect = Array.isArray(value?.disconnect) ? value.disconnect : [];
|
|
248
|
+
const fetchedRelations = fetchRelationItems && sourceContext?.model && sourceContext?.id ? await fetchRelationItems(sourceContext.model, sourceContext.id, fieldName) : [];
|
|
249
|
+
const connectedByIdentity = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const relation of currentConnect) {
|
|
251
|
+
const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey());
|
|
252
|
+
const identity = getRelationIdentity(entry);
|
|
253
|
+
if (entry && identity) {
|
|
254
|
+
connectedByIdentity.set(identity, entry);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const disconnectedIdentities = new Set(
|
|
258
|
+
currentDisconnect.map(getRelationIdentity).filter(Boolean)
|
|
259
|
+
);
|
|
260
|
+
const effectiveRelations = [];
|
|
261
|
+
for (const relation of fetchedRelations) {
|
|
262
|
+
const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey());
|
|
263
|
+
const identity = getRelationIdentity(entry);
|
|
264
|
+
if (!entry || !identity || disconnectedIdentities.has(identity)) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (connectedByIdentity.has(identity)) {
|
|
268
|
+
effectiveRelations.push(connectedByIdentity.get(identity));
|
|
269
|
+
connectedByIdentity.delete(identity);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
effectiveRelations.push(entry);
|
|
273
|
+
}
|
|
274
|
+
for (const relation of connectedByIdentity.values()) {
|
|
275
|
+
effectiveRelations.push(relation);
|
|
276
|
+
}
|
|
277
|
+
const connect = ONE_WAY_RELATIONS.has(attribute.relation) ? effectiveRelations.slice(-1) : effectiveRelations;
|
|
278
|
+
return {
|
|
279
|
+
connect,
|
|
280
|
+
disconnect: []
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function cloneComponentFields(data, componentUid, options, sourceContext) {
|
|
284
|
+
const { schemas, contentTypes, fetchRelationItems } = options;
|
|
285
|
+
const attributes = schemas[componentUid] ?? {};
|
|
286
|
+
const out = { __component: data.__component };
|
|
287
|
+
const tempKeys = generateNKeysBetween(void 0, void 0, Object.keys(data).length + 10);
|
|
288
|
+
let tempKeyIndex = 0;
|
|
289
|
+
const createTempKey = () => tempKeys[tempKeyIndex++] ?? generateKeyBetween(null, null);
|
|
290
|
+
const context = {
|
|
291
|
+
model: componentUid,
|
|
292
|
+
id: sourceContext?.id ?? data.id
|
|
293
|
+
};
|
|
294
|
+
for (const [key, rawValue] of Object.entries(data)) {
|
|
295
|
+
const value = rawValue;
|
|
296
|
+
if (TRANSIENT_ENTRY_KEYS.has(key) || key === "__component") {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const attribute = attributes[key];
|
|
300
|
+
if (!attribute) {
|
|
301
|
+
if (value !== void 0) {
|
|
302
|
+
out[key] = clonePlainValue(value);
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (value === null || value === void 0) {
|
|
307
|
+
if (attribute.type === "boolean") {
|
|
308
|
+
out[key] = value;
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
switch (attribute.type) {
|
|
313
|
+
case "component": {
|
|
314
|
+
if (attribute.repeatable) {
|
|
315
|
+
const items = Array.isArray(value) ? value : [];
|
|
316
|
+
const keys = generateNKeysBetween(void 0, void 0, items.length);
|
|
317
|
+
out[key] = await Promise.all(
|
|
318
|
+
items.map(async (item, index2) => ({
|
|
319
|
+
...await cloneComponentFields(item, attribute.component, options, {
|
|
320
|
+
model: attribute.component,
|
|
321
|
+
id: item?.id
|
|
322
|
+
}),
|
|
323
|
+
__temp_key__: keys[index2]
|
|
324
|
+
}))
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
out[key] = await cloneComponentFields(value, attribute.component, options, {
|
|
328
|
+
model: attribute.component,
|
|
329
|
+
id: value?.id
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case "dynamiczone": {
|
|
335
|
+
if (!Array.isArray(value)) {
|
|
336
|
+
out[key] = [];
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
const keys = generateNKeysBetween(void 0, void 0, value.length);
|
|
340
|
+
out[key] = await Promise.all(
|
|
341
|
+
value.map(async (item, index2) => ({
|
|
342
|
+
...await cloneFormDynamicZoneEntry(item, options),
|
|
343
|
+
__temp_key__: keys[index2]
|
|
344
|
+
}))
|
|
345
|
+
);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case "relation":
|
|
349
|
+
if (!attribute.target) {
|
|
350
|
+
out[key] = { connect: [], disconnect: [] };
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
out[key] = await sanitizeRelationValue(
|
|
354
|
+
attribute,
|
|
355
|
+
value,
|
|
356
|
+
key,
|
|
357
|
+
context,
|
|
358
|
+
contentTypes,
|
|
359
|
+
fetchRelationItems,
|
|
360
|
+
createTempKey
|
|
361
|
+
);
|
|
362
|
+
break;
|
|
363
|
+
default:
|
|
364
|
+
out[key] = clonePlainValue(value);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
async function cloneFormDynamicZoneEntry(entry, options) {
|
|
370
|
+
if (!entry || typeof entry.__component !== "string") {
|
|
371
|
+
return entry;
|
|
372
|
+
}
|
|
373
|
+
const cloned = await cloneComponentFields(entry, entry.__component, options, {
|
|
374
|
+
model: entry.__component,
|
|
375
|
+
id: entry.id
|
|
376
|
+
});
|
|
377
|
+
return cloned;
|
|
378
|
+
}
|
|
379
|
+
function createFetchRelationItems(get, relationQueryParams = {}) {
|
|
380
|
+
const cache = /* @__PURE__ */ new Map();
|
|
381
|
+
return async (relationModel, relationId, fieldName) => {
|
|
382
|
+
if (!relationModel || relationId === void 0 || relationId === null || !fieldName) {
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
const cacheKey = JSON.stringify({ relationModel, relationId, fieldName, relationQueryParams });
|
|
386
|
+
const cached = cache.get(cacheKey);
|
|
387
|
+
if (cached) {
|
|
388
|
+
return cached;
|
|
389
|
+
}
|
|
390
|
+
const request = (async () => {
|
|
391
|
+
let page = 1;
|
|
392
|
+
let totalPages = 1;
|
|
393
|
+
const relations = [];
|
|
394
|
+
while (page <= totalPages) {
|
|
395
|
+
const response = await get(
|
|
396
|
+
`/content-manager/relations/${relationModel}/${relationId}/${fieldName}`,
|
|
397
|
+
{
|
|
398
|
+
params: {
|
|
399
|
+
...relationQueryParams,
|
|
400
|
+
page,
|
|
401
|
+
pageSize: 100
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
const payload = response?.data ?? {};
|
|
406
|
+
const pageResults = normalizeFetchedRelations(payload.results).reverse();
|
|
407
|
+
relations.push(...pageResults);
|
|
408
|
+
const pagination = payload?.pagination;
|
|
409
|
+
const pageCount = typeof pagination?.pageCount === "number" ? pagination.pageCount : Math.max(1, Math.ceil((pagination?.total ?? pageResults.length) / 100));
|
|
410
|
+
totalPages = pageCount;
|
|
411
|
+
page += 1;
|
|
412
|
+
}
|
|
413
|
+
return relations;
|
|
414
|
+
})();
|
|
415
|
+
cache.set(cacheKey, request);
|
|
416
|
+
try {
|
|
417
|
+
return await request;
|
|
418
|
+
} catch {
|
|
419
|
+
cache.delete(cacheKey);
|
|
420
|
+
throw new Error("Failed to fetch relations");
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function prepareEntryForZoneInsert(zone, targetIndex, entry) {
|
|
425
|
+
const prevKey = targetIndex > 0 ? zone[targetIndex - 1]?.__temp_key__ ?? null : null;
|
|
426
|
+
const nextKey = targetIndex < zone.length ? zone[targetIndex]?.__temp_key__ ?? null : null;
|
|
427
|
+
const { id, documentId, __temp_key__, ...rest } = entry;
|
|
428
|
+
return {
|
|
429
|
+
...rest,
|
|
430
|
+
__temp_key__: generateKeyBetween(prevKey, nextKey)
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const INDEX_SEGMENT = /^\d+$/;
|
|
434
|
+
function getDynamicZoneFields(attributes) {
|
|
435
|
+
if (!attributes) {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
return Object.entries(attributes).filter(([, attribute]) => attribute?.type === "dynamiczone").map(([fieldName, attribute]) => ({
|
|
439
|
+
fieldName,
|
|
440
|
+
displayName: attribute?.displayName || fieldName
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
function getDynamicZonePathsFromValues(values) {
|
|
444
|
+
return Object.entries(values).filter(([, zone]) => Array.isArray(zone) && zone.some(isDynamicZoneEntry)).map(([fieldName]) => fieldName);
|
|
445
|
+
}
|
|
446
|
+
function isDynamicZoneEntry(value) {
|
|
447
|
+
return Boolean(value && typeof value === "object" && typeof value.__component === "string");
|
|
448
|
+
}
|
|
449
|
+
function getValueAtPath(source, path) {
|
|
450
|
+
if (!path) {
|
|
451
|
+
return source;
|
|
452
|
+
}
|
|
453
|
+
let current = source;
|
|
454
|
+
for (const segment of path.split(".")) {
|
|
455
|
+
if (current === null || current === void 0) {
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
458
|
+
if (INDEX_SEGMENT.test(segment)) {
|
|
459
|
+
if (!Array.isArray(current)) {
|
|
460
|
+
return void 0;
|
|
461
|
+
}
|
|
462
|
+
current = current[Number(segment)];
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
current = current[segment];
|
|
466
|
+
}
|
|
467
|
+
return current;
|
|
468
|
+
}
|
|
469
|
+
function findDynamicZoneLists(root = document) {
|
|
470
|
+
return Array.from(root.querySelectorAll("ol[aria-describedby]"));
|
|
471
|
+
}
|
|
472
|
+
function getDynamicZoneListItems(list) {
|
|
473
|
+
return Array.from(list.children).filter(
|
|
474
|
+
(child) => child instanceof HTMLLIElement
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
function findRowActionContainer(rowElement) {
|
|
478
|
+
const header = rowElement.querySelector("h3");
|
|
479
|
+
if (!header) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
for (const span of header.querySelectorAll("span")) {
|
|
483
|
+
const buttons = span.querySelectorAll(":scope > button");
|
|
484
|
+
if (buttons.length >= 2) {
|
|
485
|
+
return span;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
function getRowActionInsertPoint(actionContainer) {
|
|
491
|
+
const buttons = Array.from(actionContainer.querySelectorAll(":scope > button"));
|
|
492
|
+
if (buttons.length === 0) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
const reorderButton = buttons.find((button) => {
|
|
496
|
+
if (!(button instanceof HTMLButtonElement)) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
const label = button.querySelector("span")?.textContent?.trim().toLowerCase() ?? "";
|
|
500
|
+
return label === "drag" || label === "move up" || label === "move down" || button.hasAttribute("data-handler-id");
|
|
501
|
+
});
|
|
502
|
+
return reorderButton ?? buttons[1] ?? null;
|
|
503
|
+
}
|
|
504
|
+
function disambiguateZonePathByRowLabel(rowElement, candidates, values, index2) {
|
|
505
|
+
const rowText = (rowElement.textContent || "").toLowerCase();
|
|
506
|
+
const matches = candidates.filter((fieldName) => {
|
|
507
|
+
const entry = getValueAtPath(values, `${fieldName}.${index2}`);
|
|
508
|
+
if (!isDynamicZoneEntry(entry)) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const componentUid = entry.__component.split(".").pop() || entry.__component;
|
|
512
|
+
return rowText.includes(componentUid.toLowerCase()) || rowText.includes(fieldName.toLowerCase());
|
|
513
|
+
});
|
|
514
|
+
return matches.length === 1 ? matches[0] : null;
|
|
515
|
+
}
|
|
516
|
+
function resolveBlockFromListItemIndex(listElement, index2, values, zonePaths) {
|
|
517
|
+
const rowCount = getDynamicZoneListItems(listElement).length;
|
|
518
|
+
const candidates = zonePaths.filter((fieldName) => {
|
|
519
|
+
const zone = values[fieldName];
|
|
520
|
+
return Array.isArray(zone) && zone.length === rowCount;
|
|
521
|
+
});
|
|
522
|
+
if (candidates.length === 1) {
|
|
523
|
+
return { zonePath: candidates[0], index: index2 };
|
|
524
|
+
}
|
|
525
|
+
if (candidates.length > 1) {
|
|
526
|
+
const rowElement = getDynamicZoneListItems(listElement)[index2];
|
|
527
|
+
const zonePath = rowElement ? disambiguateZonePathByRowLabel(rowElement, candidates, values, index2) : null;
|
|
528
|
+
if (zonePath) {
|
|
529
|
+
return { zonePath, index: index2 };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
function resolveBlockFromListItem(listItem, values) {
|
|
535
|
+
const fieldNames = Array.from(listItem.querySelectorAll("[name]")).map((element) => element.getAttribute("name") || "").filter(Boolean).sort((a, b) => a.length - b.length);
|
|
536
|
+
for (const fieldName of fieldNames) {
|
|
537
|
+
const segments = fieldName.split(".");
|
|
538
|
+
for (let indexPosition = segments.length - 2; indexPosition >= 0; indexPosition -= 1) {
|
|
539
|
+
const segment = segments[indexPosition];
|
|
540
|
+
if (!INDEX_SEGMENT.test(segment)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const itemPath = segments.slice(0, indexPosition + 1).join(".");
|
|
544
|
+
const zonePath = segments.slice(0, indexPosition).join(".");
|
|
545
|
+
const index2 = Number(segment);
|
|
546
|
+
const itemValue = getValueAtPath(values, itemPath);
|
|
547
|
+
if (isDynamicZoneEntry(itemValue) && Array.isArray(getValueAtPath(values, zonePath))) {
|
|
548
|
+
return { zonePath, index: index2 };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const list = listItem.closest("ol[aria-describedby]");
|
|
553
|
+
const zonePaths = getDynamicZonePathsFromValues(values);
|
|
554
|
+
if (list && zonePaths.length > 0) {
|
|
555
|
+
const index2 = getDynamicZoneListItems(list).indexOf(listItem);
|
|
556
|
+
if (index2 >= 0) {
|
|
557
|
+
return resolveBlockFromListItemIndex(list, index2, values, zonePaths);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
function getButtonLabel(button) {
|
|
563
|
+
return button.querySelector("span")?.textContent?.trim().toLowerCase() ?? "";
|
|
564
|
+
}
|
|
565
|
+
function findDynamicZoneDragHandles(root = document) {
|
|
566
|
+
return Array.from(root.querySelectorAll("button[data-handler-id], button")).filter(
|
|
567
|
+
(button) => {
|
|
568
|
+
const label = getButtonLabel(button);
|
|
569
|
+
return label === "drag" || button.hasAttribute("data-handler-id");
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
function findRowElementForDragHandle(dragHandle) {
|
|
574
|
+
return dragHandle.closest("li");
|
|
575
|
+
}
|
|
576
|
+
const TOOLS_DIALOG_STYLE_ID = `${PLUGIN_ID}-dialog-layout-styles`;
|
|
577
|
+
function ensureToolsDialogStyles() {
|
|
578
|
+
const dom = globalThis.document;
|
|
579
|
+
if (!dom || dom.getElementById(TOOLS_DIALOG_STYLE_ID)) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const style = dom.createElement("style");
|
|
583
|
+
style.id = TOOLS_DIALOG_STYLE_ID;
|
|
584
|
+
style.textContent = `
|
|
585
|
+
[role='alertdialog'][data-dz-tools-dialog='true'] {
|
|
586
|
+
max-height: calc(100dvh - 2rem) !important;
|
|
587
|
+
height: auto !important;
|
|
588
|
+
overflow: hidden !important;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
[data-dz-tools-dialog='true'] [data-dz-tools-shell='true'] {
|
|
592
|
+
display: flex;
|
|
593
|
+
flex: 1 1 auto;
|
|
594
|
+
flex-direction: column;
|
|
595
|
+
min-height: 0;
|
|
596
|
+
overflow: hidden;
|
|
597
|
+
width: 100%;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
[data-dz-tools-dialog='true'] [data-dz-tools-body='true'] {
|
|
601
|
+
flex: 1 1 auto !important;
|
|
602
|
+
min-height: 0 !important;
|
|
603
|
+
overflow-y: auto !important;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
[data-dz-tools-dialog='true'] [data-dz-tools-footer='true'] {
|
|
607
|
+
flex: 0 0 auto !important;
|
|
608
|
+
}
|
|
609
|
+
`;
|
|
610
|
+
dom.head.appendChild(style);
|
|
611
|
+
}
|
|
612
|
+
const DynamicZoneToolsHeaderAction = ({
|
|
613
|
+
document: document2,
|
|
614
|
+
documentId,
|
|
615
|
+
meta,
|
|
616
|
+
model,
|
|
617
|
+
collectionType,
|
|
618
|
+
activeTab
|
|
619
|
+
}) => {
|
|
620
|
+
const schemaAttributes = meta?.schema?.attributes || meta?.attributes || null;
|
|
621
|
+
if (schemaAttributes) {
|
|
622
|
+
const hasDynamicZone = Object.values(schemaAttributes).some(
|
|
623
|
+
(attr) => attr?.type === "dynamiczone"
|
|
624
|
+
);
|
|
625
|
+
if (!hasDynamicZone) return null;
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
type: "icon",
|
|
629
|
+
icon: /* @__PURE__ */ jsx(Duplicate, {}),
|
|
630
|
+
label: "Copy dynamic zone data",
|
|
631
|
+
dialog: {
|
|
632
|
+
type: "dialog",
|
|
633
|
+
title: "Dynamic Zone Tools",
|
|
634
|
+
content: ({ onClose }) => /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
|
|
635
|
+
DynamicZoneToolsDialog,
|
|
636
|
+
{
|
|
637
|
+
document: document2,
|
|
638
|
+
documentId,
|
|
639
|
+
model,
|
|
640
|
+
collectionType,
|
|
641
|
+
activeTab,
|
|
642
|
+
schemaAttributes,
|
|
643
|
+
onClose
|
|
644
|
+
}
|
|
645
|
+
) })
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
};
|
|
649
|
+
const DynamicZoneToolsDialog = ({
|
|
650
|
+
document: document2,
|
|
651
|
+
documentId,
|
|
652
|
+
model,
|
|
653
|
+
collectionType,
|
|
654
|
+
activeTab,
|
|
655
|
+
schemaAttributes,
|
|
656
|
+
onClose
|
|
657
|
+
}) => {
|
|
658
|
+
const { formatMessage } = useIntl();
|
|
659
|
+
const { get } = useFetchClient();
|
|
660
|
+
const { setValues, values } = useForm("DynamicZoneTools", (state) => ({
|
|
661
|
+
setValues: state.setValues,
|
|
662
|
+
values: state.values
|
|
663
|
+
}));
|
|
664
|
+
const { toggleNotification } = useNotification();
|
|
665
|
+
const parsePayloadData = (payload) => {
|
|
666
|
+
if (payload?.data && typeof payload.data === "object") return payload.data;
|
|
667
|
+
if (payload && typeof payload === "object") return payload;
|
|
668
|
+
return null;
|
|
669
|
+
};
|
|
670
|
+
const getDynamicZonesFromAttributes = (attributes) => getDynamicZoneFields(attributes);
|
|
671
|
+
const [targetDynamicZones, setTargetDynamicZones] = useState([]);
|
|
672
|
+
const [selectedTargetZone, setSelectedTargetZone] = useState("");
|
|
673
|
+
const [contentTypes, setContentTypes] = useState([]);
|
|
674
|
+
const [selectedContentType, setSelectedContentType] = useState("");
|
|
675
|
+
const [sourceDynamicZones, setSourceDynamicZones] = useState([]);
|
|
676
|
+
const [selectedSourceZone, setSelectedSourceZone] = useState("");
|
|
677
|
+
const [records, setRecords] = useState([]);
|
|
678
|
+
const [selectedRecord, setSelectedRecord] = useState("");
|
|
679
|
+
const [configuredLocales, setConfiguredLocales] = useState([]);
|
|
680
|
+
const [selectedLocale, setSelectedLocale] = useState("");
|
|
681
|
+
const [selectedVersion, setSelectedVersion] = useState("draft");
|
|
682
|
+
const [recordsLoading, setRecordsLoading] = useState(false);
|
|
683
|
+
const [loading, setLoading] = useState(false);
|
|
684
|
+
const [error, setError] = useState("");
|
|
685
|
+
const [fillMode, setFillMode] = useState("replace");
|
|
686
|
+
const [sourcePreviewLoading, setSourcePreviewLoading] = useState(false);
|
|
687
|
+
const [sourceZoneEntries, setSourceZoneEntries] = useState([]);
|
|
688
|
+
const [selectedBlockIndexes, setSelectedBlockIndexes] = useState(/* @__PURE__ */ new Set());
|
|
689
|
+
const sourceContentType = contentTypes.find((ct) => ct.uid === selectedContentType);
|
|
690
|
+
const sourceHasDraftAndPublish = sourceContentType?.draftAndPublish ?? false;
|
|
691
|
+
const sourceIsLocalized = (sourceContentType?.localized ?? false) && configuredLocales.length > 0;
|
|
692
|
+
const selectedRecordEntry = records.find(
|
|
693
|
+
(record) => (record.documentId || record.id.toString()) === selectedRecord
|
|
694
|
+
);
|
|
695
|
+
const selectedRecordHasPublished = selectedRecordEntry?.status === "published" || selectedRecordEntry?.status === "modified";
|
|
696
|
+
useEffect(() => {
|
|
697
|
+
if (schemaAttributes) {
|
|
698
|
+
setTargetDynamicZones(getDynamicZonesFromAttributes(schemaAttributes));
|
|
699
|
+
} else {
|
|
700
|
+
const currentType = contentTypes.find((ct) => ct.uid === model);
|
|
701
|
+
setTargetDynamicZones(currentType?.dynamicZones || []);
|
|
702
|
+
}
|
|
703
|
+
}, [schemaAttributes, contentTypes, model]);
|
|
704
|
+
useEffect(() => {
|
|
705
|
+
const fetchContentTypes = async () => {
|
|
706
|
+
try {
|
|
707
|
+
const response = await get("/content-manager/content-types");
|
|
708
|
+
let contentTypesData = response.data || response;
|
|
709
|
+
if (!Array.isArray(contentTypesData)) {
|
|
710
|
+
if (contentTypesData.data && Array.isArray(contentTypesData.data)) {
|
|
711
|
+
contentTypesData = contentTypesData.data;
|
|
712
|
+
} else if (contentTypesData.results && Array.isArray(contentTypesData.results)) {
|
|
713
|
+
contentTypesData = contentTypesData.results;
|
|
714
|
+
} else {
|
|
715
|
+
contentTypesData = [];
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const typesWithDynamicZones = contentTypesData.map((ct) => {
|
|
719
|
+
const attributes = ct.attributes || ct.schema?.attributes;
|
|
720
|
+
const dynamicZones = getDynamicZonesFromAttributes(attributes);
|
|
721
|
+
return {
|
|
722
|
+
uid: ct.uid,
|
|
723
|
+
displayName: ct.info?.displayName || ct.schema?.displayName || ct.uid,
|
|
724
|
+
draftAndPublish: Boolean(
|
|
725
|
+
ct.options?.draftAndPublish ?? ct.schema?.options?.draftAndPublish
|
|
726
|
+
),
|
|
727
|
+
localized: Boolean(
|
|
728
|
+
ct.pluginOptions?.i18n?.localized ?? ct.schema?.pluginOptions?.i18n?.localized
|
|
729
|
+
),
|
|
730
|
+
dynamicZones
|
|
731
|
+
};
|
|
732
|
+
}).filter((ct) => ct.dynamicZones.length > 0);
|
|
733
|
+
setContentTypes(typesWithDynamicZones);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
setError("Failed to load content types");
|
|
736
|
+
console.error("Error loading content types:", err);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
fetchContentTypes();
|
|
740
|
+
}, [get]);
|
|
741
|
+
React.useLayoutEffect(() => {
|
|
742
|
+
ensureToolsDialogStyles();
|
|
743
|
+
}, []);
|
|
744
|
+
const dialogLayoutRef = React.useCallback((node) => {
|
|
745
|
+
const markDialog = () => {
|
|
746
|
+
const dialogContent = node?.closest('[role="alertdialog"]');
|
|
747
|
+
if (dialogContent instanceof HTMLElement) {
|
|
748
|
+
dialogContent.setAttribute("data-dz-tools-dialog", "true");
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
if (!node) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
markDialog();
|
|
755
|
+
requestAnimationFrame(markDialog);
|
|
756
|
+
}, []);
|
|
757
|
+
React.useLayoutEffect(() => {
|
|
758
|
+
return () => {
|
|
759
|
+
const dom = globalThis.document;
|
|
760
|
+
if (!dom) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
dom.querySelectorAll('[data-dz-tools-dialog="true"]').forEach((element) => {
|
|
764
|
+
element.removeAttribute("data-dz-tools-dialog");
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
}, []);
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
if (!selectedContentType) {
|
|
770
|
+
setSourceDynamicZones([]);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const selectedType = contentTypes.find((ct) => ct.uid === selectedContentType);
|
|
774
|
+
if (!selectedType) return;
|
|
775
|
+
setSourceDynamicZones(selectedType.dynamicZones);
|
|
776
|
+
}, [selectedContentType, contentTypes]);
|
|
777
|
+
useEffect(() => {
|
|
778
|
+
const fetchRecords = async () => {
|
|
779
|
+
if (!selectedContentType || sourceIsLocalized && !selectedLocale) {
|
|
780
|
+
setRecords([]);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
setRecordsLoading(true);
|
|
785
|
+
const requestParams = new URLSearchParams({
|
|
786
|
+
page: "1",
|
|
787
|
+
pageSize: "500"
|
|
788
|
+
});
|
|
789
|
+
if (sourceIsLocalized) requestParams.set("locale", selectedLocale);
|
|
790
|
+
const response = await get(
|
|
791
|
+
`/content-manager/collection-types/${encodeURIComponent(
|
|
792
|
+
selectedContentType
|
|
793
|
+
)}?${requestParams.toString()}`
|
|
794
|
+
);
|
|
795
|
+
let recordsData = [];
|
|
796
|
+
if (response.data) {
|
|
797
|
+
if (Array.isArray(response.data)) {
|
|
798
|
+
recordsData = response.data;
|
|
799
|
+
} else if (response.data.results && Array.isArray(response.data.results)) {
|
|
800
|
+
recordsData = response.data.results;
|
|
801
|
+
} else if (response.data.data && Array.isArray(response.data.data)) {
|
|
802
|
+
recordsData = response.data.data;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
setRecords(recordsData);
|
|
806
|
+
} catch (err) {
|
|
807
|
+
setError("Failed to load records");
|
|
808
|
+
console.error("Error loading records:", err);
|
|
809
|
+
} finally {
|
|
810
|
+
setRecordsLoading(false);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
fetchRecords();
|
|
814
|
+
}, [selectedContentType, sourceIsLocalized, selectedLocale, get]);
|
|
815
|
+
useEffect(() => {
|
|
816
|
+
const fetchLocales = async () => {
|
|
817
|
+
try {
|
|
818
|
+
const response = await get("/i18n/locales");
|
|
819
|
+
const locales = Array.isArray(response.data) ? response.data : response.data?.data ?? [];
|
|
820
|
+
setConfiguredLocales(
|
|
821
|
+
locales.filter((locale) => locale?.code).map((locale) => ({
|
|
822
|
+
code: locale.code,
|
|
823
|
+
name: locale.name || locale.code,
|
|
824
|
+
isDefault: Boolean(locale.isDefault)
|
|
825
|
+
}))
|
|
826
|
+
);
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
fetchLocales();
|
|
831
|
+
}, [get]);
|
|
832
|
+
useEffect(() => {
|
|
833
|
+
if (!selectedContentType || !sourceIsLocalized) {
|
|
834
|
+
setSelectedLocale("");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const currentLocale = document2?.locale ?? null;
|
|
838
|
+
const defaultLocale = configuredLocales.find((locale) => locale.code === currentLocale)?.code ?? configuredLocales.find((locale) => locale.isDefault)?.code ?? configuredLocales[0]?.code ?? "";
|
|
839
|
+
setSelectedLocale(defaultLocale);
|
|
840
|
+
}, [selectedContentType, sourceIsLocalized, configuredLocales, document2?.locale]);
|
|
841
|
+
const [componentSchemas, setComponentSchemas] = useState(null);
|
|
842
|
+
const fetchComponentSchemas = async () => {
|
|
843
|
+
if (componentSchemas) return componentSchemas;
|
|
844
|
+
const map = await loadComponentSchemas(get);
|
|
845
|
+
setComponentSchemas(map);
|
|
846
|
+
return map;
|
|
847
|
+
};
|
|
848
|
+
const getRecordLabel = (record) => {
|
|
849
|
+
const rawLabel = record.title || record.name || `Document ${record.documentId || record.id}`;
|
|
850
|
+
const maxLength = 60;
|
|
851
|
+
return rawLabel.length > maxLength ? `${rawLabel.substring(0, maxLength)}...` : rawLabel;
|
|
852
|
+
};
|
|
853
|
+
const getLocaleLabel = (code) => {
|
|
854
|
+
const name = configuredLocales.find((locale) => locale.code === code)?.name;
|
|
855
|
+
if (!name) return code;
|
|
856
|
+
return name.includes(`(${code})`) ? name : `${name} (${code})`;
|
|
857
|
+
};
|
|
858
|
+
const getSelectString = (value) => String(value);
|
|
859
|
+
const canFetchSourcePreview = Boolean(selectedContentType && selectedSourceZone && selectedRecord) && (!sourceIsLocalized || Boolean(selectedLocale));
|
|
860
|
+
const buildSourceDocumentQuery = () => {
|
|
861
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
862
|
+
const urlLocale = urlParams.get("plugins[i18n][locale]");
|
|
863
|
+
const locale = selectedLocale || urlLocale;
|
|
864
|
+
const requestParams = new URLSearchParams();
|
|
865
|
+
if (locale) requestParams.set("locale", locale);
|
|
866
|
+
if (sourceHasDraftAndPublish) requestParams.set("status", selectedVersion);
|
|
867
|
+
return requestParams.toString() ? `?${requestParams.toString()}` : "";
|
|
868
|
+
};
|
|
869
|
+
useEffect(() => {
|
|
870
|
+
if (!canFetchSourcePreview) {
|
|
871
|
+
setSourceZoneEntries([]);
|
|
872
|
+
setSelectedBlockIndexes(/* @__PURE__ */ new Set());
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
let active = true;
|
|
876
|
+
const fetchSourcePreview = async () => {
|
|
877
|
+
try {
|
|
878
|
+
setSourcePreviewLoading(true);
|
|
879
|
+
setError("");
|
|
880
|
+
const localeQuery = buildSourceDocumentQuery();
|
|
881
|
+
const response = await get(
|
|
882
|
+
`/${PLUGIN_ID}/source-document/${encodeURIComponent(
|
|
883
|
+
selectedContentType
|
|
884
|
+
)}/${selectedRecord}${localeQuery}`
|
|
885
|
+
);
|
|
886
|
+
const sourceDocument = parsePayloadData(response.data ?? response);
|
|
887
|
+
const zoneData = sourceDocument?.[selectedSourceZone];
|
|
888
|
+
if (!active) return;
|
|
889
|
+
if (!Array.isArray(zoneData) || zoneData.length === 0) {
|
|
890
|
+
setSourceZoneEntries([]);
|
|
891
|
+
setSelectedBlockIndexes(/* @__PURE__ */ new Set());
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
setSourceZoneEntries(zoneData);
|
|
895
|
+
setSelectedBlockIndexes(new Set(zoneData.map((_, index2) => index2)));
|
|
896
|
+
} catch {
|
|
897
|
+
if (active) {
|
|
898
|
+
setSourceZoneEntries([]);
|
|
899
|
+
setSelectedBlockIndexes(/* @__PURE__ */ new Set());
|
|
900
|
+
}
|
|
901
|
+
} finally {
|
|
902
|
+
if (active) {
|
|
903
|
+
setSourcePreviewLoading(false);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
void fetchSourcePreview();
|
|
908
|
+
return () => {
|
|
909
|
+
active = false;
|
|
910
|
+
};
|
|
911
|
+
}, [
|
|
912
|
+
canFetchSourcePreview,
|
|
913
|
+
get,
|
|
914
|
+
selectedContentType,
|
|
915
|
+
selectedRecord,
|
|
916
|
+
selectedSourceZone,
|
|
917
|
+
selectedLocale,
|
|
918
|
+
selectedVersion,
|
|
919
|
+
sourceHasDraftAndPublish
|
|
920
|
+
]);
|
|
921
|
+
const selectAllBlocks = () => {
|
|
922
|
+
setSelectedBlockIndexes(new Set(sourceZoneEntries.map((_, index2) => index2)));
|
|
923
|
+
};
|
|
924
|
+
const clearAllBlocks = () => {
|
|
925
|
+
setSelectedBlockIndexes(/* @__PURE__ */ new Set());
|
|
926
|
+
};
|
|
927
|
+
const handleConfirmFill = async () => {
|
|
928
|
+
if (!selectedTargetZone || !selectedContentType || !selectedSourceZone || !selectedRecord)
|
|
929
|
+
return;
|
|
930
|
+
if (selectedBlockIndexes.size === 0) {
|
|
931
|
+
setError("Select at least one block to copy");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
setLoading(true);
|
|
936
|
+
setError("");
|
|
937
|
+
const schemas = await fetchComponentSchemas();
|
|
938
|
+
const sourceData = sourceZoneEntries.length > 0 ? sourceZoneEntries : (() => {
|
|
939
|
+
throw new Error("Source blocks are not loaded yet");
|
|
940
|
+
})();
|
|
941
|
+
const selectedEntries = sourceData.filter((_, index2) => selectedBlockIndexes.has(index2));
|
|
942
|
+
const allowedComponents = schemaAttributes?.[selectedTargetZone]?.components;
|
|
943
|
+
const { allowed: allowedData, skippedCount } = filterAllowedDynamicZoneEntries(
|
|
944
|
+
selectedEntries,
|
|
945
|
+
allowedComponents
|
|
946
|
+
);
|
|
947
|
+
if (allowedData.length === 0) {
|
|
948
|
+
setError("None of the selected components are allowed in the target dynamic zone");
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const currentZone = Array.isArray(values?.[selectedTargetZone]) ? values[selectedTargetZone] : [];
|
|
952
|
+
const cleanedData = fillMode === "append" ? prepareZoneDataForInsert(allowedData, schemas, {
|
|
953
|
+
previousKey: currentZone.at(-1)?.__temp_key__ ?? null,
|
|
954
|
+
nextKey: null
|
|
955
|
+
}) : prepareZoneData(allowedData, schemas);
|
|
956
|
+
setValues({
|
|
957
|
+
...values,
|
|
958
|
+
[selectedTargetZone]: fillMode === "append" ? [...currentZone, ...cleanedData] : cleanedData
|
|
959
|
+
});
|
|
960
|
+
toggleNotification({
|
|
961
|
+
type: skippedCount > 0 ? "warning" : "success",
|
|
962
|
+
message: skippedCount > 0 ? formatMessage(
|
|
963
|
+
{
|
|
964
|
+
id: `${PLUGIN_ID}.notification.partial`,
|
|
965
|
+
defaultMessage: "Content copied, but {count} component(s) were skipped because the target zone does not allow them."
|
|
966
|
+
},
|
|
967
|
+
{ count: skippedCount }
|
|
968
|
+
) : formatMessage({
|
|
969
|
+
id: fillMode === "append" ? `${PLUGIN_ID}.notification.append-success` : `${PLUGIN_ID}.notification.success`,
|
|
970
|
+
defaultMessage: fillMode === "append" ? "Selected blocks appended successfully!" : "Dynamic zone content copied successfully!"
|
|
971
|
+
})
|
|
972
|
+
});
|
|
973
|
+
setSelectedTargetZone("");
|
|
974
|
+
setSelectedContentType("");
|
|
975
|
+
setSelectedSourceZone("");
|
|
976
|
+
setSelectedRecord("");
|
|
977
|
+
setSelectedVersion("draft");
|
|
978
|
+
setFillMode("replace");
|
|
979
|
+
setSourceZoneEntries([]);
|
|
980
|
+
setSelectedBlockIndexes(/* @__PURE__ */ new Set());
|
|
981
|
+
onClose();
|
|
982
|
+
} catch (err) {
|
|
983
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to fill in data";
|
|
984
|
+
setError(errorMessage);
|
|
985
|
+
toggleNotification({
|
|
986
|
+
type: "warning",
|
|
987
|
+
message: errorMessage
|
|
988
|
+
});
|
|
989
|
+
} finally {
|
|
990
|
+
setLoading(false);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
const isFormValid = selectedTargetZone && selectedContentType && selectedSourceZone && selectedRecord && (!sourceIsLocalized || selectedLocale) && selectedBlockIndexes.size > 0 && !sourcePreviewLoading;
|
|
994
|
+
return /* @__PURE__ */ jsxs(
|
|
995
|
+
Flex,
|
|
996
|
+
{
|
|
997
|
+
ref: dialogLayoutRef,
|
|
998
|
+
"data-dz-tools-shell": "true",
|
|
999
|
+
direction: "column",
|
|
1000
|
+
alignItems: "stretch",
|
|
1001
|
+
width: "100%",
|
|
1002
|
+
children: [
|
|
1003
|
+
/* @__PURE__ */ jsx(Dialog.Body, { "data-dz-tools-body": "true", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, width: "100%", alignItems: "stretch", children: [
|
|
1004
|
+
/* @__PURE__ */ jsxs(
|
|
1005
|
+
Flex,
|
|
1006
|
+
{
|
|
1007
|
+
gap: 3,
|
|
1008
|
+
padding: 4,
|
|
1009
|
+
background: fillMode === "replace" ? "danger100" : "primary100",
|
|
1010
|
+
hasRadius: true,
|
|
1011
|
+
alignItems: "flex-start",
|
|
1012
|
+
width: "100%",
|
|
1013
|
+
children: [
|
|
1014
|
+
/* @__PURE__ */ jsx(Box, { style: { flexShrink: 0, display: "flex" }, paddingTop: 1, children: /* @__PURE__ */ jsx(WarningCircle, { fill: fillMode === "replace" ? "danger600" : "primary600" }) }),
|
|
1015
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
|
|
1016
|
+
/* @__PURE__ */ jsx(
|
|
1017
|
+
Typography,
|
|
1018
|
+
{
|
|
1019
|
+
fontWeight: "semiBold",
|
|
1020
|
+
textColor: fillMode === "replace" ? "danger700" : "primary700",
|
|
1021
|
+
children: fillMode === "replace" ? formatMessage({
|
|
1022
|
+
id: `${PLUGIN_ID}.modal.warning`,
|
|
1023
|
+
defaultMessage: "This will replace the current content of the target dynamic zone."
|
|
1024
|
+
}) : formatMessage({
|
|
1025
|
+
id: `${PLUGIN_ID}.modal.append-info`,
|
|
1026
|
+
defaultMessage: "Selected blocks will be appended to the end of the target dynamic zone."
|
|
1027
|
+
})
|
|
1028
|
+
}
|
|
1029
|
+
),
|
|
1030
|
+
/* @__PURE__ */ jsx(
|
|
1031
|
+
Typography,
|
|
1032
|
+
{
|
|
1033
|
+
variant: "pi",
|
|
1034
|
+
textColor: fillMode === "replace" ? "danger600" : "primary600",
|
|
1035
|
+
children: formatMessage({
|
|
1036
|
+
id: `${PLUGIN_ID}.modal.warning.helper`,
|
|
1037
|
+
defaultMessage: "Nothing is saved yet — review the result and save the document to keep it."
|
|
1038
|
+
})
|
|
1039
|
+
}
|
|
1040
|
+
)
|
|
1041
|
+
] })
|
|
1042
|
+
]
|
|
1043
|
+
}
|
|
1044
|
+
),
|
|
1045
|
+
error && /* @__PURE__ */ jsx(Alert, { closeLabel: "Close", title: "Error", variant: "danger", onClose: () => setError(""), children: error }),
|
|
1046
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, width: "100%", children: [
|
|
1047
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1048
|
+
id: `${PLUGIN_ID}.fill-mode.label`,
|
|
1049
|
+
defaultMessage: "Copy mode"
|
|
1050
|
+
}) }),
|
|
1051
|
+
/* @__PURE__ */ jsxs(
|
|
1052
|
+
SingleSelect,
|
|
1053
|
+
{
|
|
1054
|
+
value: fillMode,
|
|
1055
|
+
onChange: (value) => setFillMode(value === "append" ? "append" : "replace"),
|
|
1056
|
+
disabled: loading,
|
|
1057
|
+
size: "S",
|
|
1058
|
+
children: [
|
|
1059
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "replace", children: formatMessage({
|
|
1060
|
+
id: `${PLUGIN_ID}.fill-mode.replace`,
|
|
1061
|
+
defaultMessage: "Replace current zone"
|
|
1062
|
+
}) }),
|
|
1063
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "append", children: formatMessage({
|
|
1064
|
+
id: `${PLUGIN_ID}.fill-mode.append`,
|
|
1065
|
+
defaultMessage: "Append to current zone"
|
|
1066
|
+
}) })
|
|
1067
|
+
]
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
] }),
|
|
1071
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, width: "100%", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1073
|
+
id: `${PLUGIN_ID}.target-zone.label`,
|
|
1074
|
+
defaultMessage: "Target Dynamic Zone"
|
|
1075
|
+
}) }),
|
|
1076
|
+
/* @__PURE__ */ jsx(
|
|
1077
|
+
SingleSelect,
|
|
1078
|
+
{
|
|
1079
|
+
placeholder: formatMessage({
|
|
1080
|
+
id: `${PLUGIN_ID}.target-zone.placeholder`,
|
|
1081
|
+
defaultMessage: "Select zone to fill"
|
|
1082
|
+
}),
|
|
1083
|
+
value: selectedTargetZone,
|
|
1084
|
+
onChange: (value) => {
|
|
1085
|
+
setSelectedTargetZone(getSelectString(value));
|
|
1086
|
+
setSelectedContentType("");
|
|
1087
|
+
setSelectedSourceZone("");
|
|
1088
|
+
setSelectedRecord("");
|
|
1089
|
+
setSelectedVersion("draft");
|
|
1090
|
+
},
|
|
1091
|
+
disabled: loading,
|
|
1092
|
+
size: "S",
|
|
1093
|
+
children: targetDynamicZones.map((zone) => /* @__PURE__ */ jsx(SingleSelectOption, { value: zone.fieldName, children: zone.displayName }, zone.fieldName))
|
|
1094
|
+
}
|
|
1095
|
+
)
|
|
1096
|
+
] }),
|
|
1097
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, width: "100%", children: [
|
|
1098
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1099
|
+
id: `${PLUGIN_ID}.source-collection.label`,
|
|
1100
|
+
defaultMessage: "Source Collection"
|
|
1101
|
+
}) }),
|
|
1102
|
+
/* @__PURE__ */ jsx(
|
|
1103
|
+
SingleSelect,
|
|
1104
|
+
{
|
|
1105
|
+
placeholder: formatMessage({
|
|
1106
|
+
id: `${PLUGIN_ID}.source-collection.placeholder`,
|
|
1107
|
+
defaultMessage: "Select source collection"
|
|
1108
|
+
}),
|
|
1109
|
+
value: selectedContentType,
|
|
1110
|
+
onChange: (value) => {
|
|
1111
|
+
setSelectedContentType(getSelectString(value));
|
|
1112
|
+
setSelectedSourceZone("");
|
|
1113
|
+
setSelectedRecord("");
|
|
1114
|
+
setSelectedVersion("draft");
|
|
1115
|
+
},
|
|
1116
|
+
disabled: !selectedTargetZone || loading,
|
|
1117
|
+
size: "S",
|
|
1118
|
+
children: contentTypes.map((ct) => /* @__PURE__ */ jsx(SingleSelectOption, { value: ct.uid, children: ct.displayName }, ct.uid))
|
|
1119
|
+
}
|
|
1120
|
+
)
|
|
1121
|
+
] }),
|
|
1122
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, width: "100%", children: [
|
|
1123
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1124
|
+
id: `${PLUGIN_ID}.source-zone.label`,
|
|
1125
|
+
defaultMessage: "Source Dynamic Zone"
|
|
1126
|
+
}) }),
|
|
1127
|
+
/* @__PURE__ */ jsx(
|
|
1128
|
+
SingleSelect,
|
|
1129
|
+
{
|
|
1130
|
+
placeholder: formatMessage({
|
|
1131
|
+
id: `${PLUGIN_ID}.source-zone.placeholder`,
|
|
1132
|
+
defaultMessage: "Select zone to copy from"
|
|
1133
|
+
}),
|
|
1134
|
+
value: selectedSourceZone,
|
|
1135
|
+
onChange: (value) => {
|
|
1136
|
+
setSelectedSourceZone(getSelectString(value));
|
|
1137
|
+
setSelectedRecord("");
|
|
1138
|
+
setSelectedVersion("draft");
|
|
1139
|
+
},
|
|
1140
|
+
disabled: !selectedContentType || loading,
|
|
1141
|
+
size: "S",
|
|
1142
|
+
children: sourceDynamicZones.map((zone) => /* @__PURE__ */ jsx(SingleSelectOption, { value: zone.fieldName, children: zone.displayName }, zone.fieldName))
|
|
1143
|
+
}
|
|
1144
|
+
)
|
|
1145
|
+
] }),
|
|
1146
|
+
sourceIsLocalized && /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, width: "100%", children: [
|
|
1147
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1148
|
+
id: `${PLUGIN_ID}.source-locale.label`,
|
|
1149
|
+
defaultMessage: "Source Locale"
|
|
1150
|
+
}) }),
|
|
1151
|
+
/* @__PURE__ */ jsx(
|
|
1152
|
+
SingleSelect,
|
|
1153
|
+
{
|
|
1154
|
+
value: selectedLocale,
|
|
1155
|
+
onChange: (value) => {
|
|
1156
|
+
setSelectedLocale(getSelectString(value));
|
|
1157
|
+
setSelectedRecord("");
|
|
1158
|
+
setSelectedVersion("draft");
|
|
1159
|
+
},
|
|
1160
|
+
disabled: !selectedSourceZone || loading,
|
|
1161
|
+
size: "S",
|
|
1162
|
+
children: configuredLocales.map((locale) => /* @__PURE__ */ jsx(SingleSelectOption, { value: locale.code, children: getLocaleLabel(locale.code) }, locale.code))
|
|
1163
|
+
}
|
|
1164
|
+
)
|
|
1165
|
+
] }),
|
|
1166
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, width: "100%", children: [
|
|
1167
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1168
|
+
id: `${PLUGIN_ID}.source-record.label`,
|
|
1169
|
+
defaultMessage: "Source Record"
|
|
1170
|
+
}) }),
|
|
1171
|
+
/* @__PURE__ */ jsx(
|
|
1172
|
+
SingleSelect,
|
|
1173
|
+
{
|
|
1174
|
+
placeholder: recordsLoading ? formatMessage({
|
|
1175
|
+
id: `${PLUGIN_ID}.source-record.loading`,
|
|
1176
|
+
defaultMessage: "Loading records..."
|
|
1177
|
+
}) : formatMessage({
|
|
1178
|
+
id: `${PLUGIN_ID}.source-record.placeholder`,
|
|
1179
|
+
defaultMessage: "Select record to copy from"
|
|
1180
|
+
}),
|
|
1181
|
+
value: selectedRecord,
|
|
1182
|
+
onChange: (value) => {
|
|
1183
|
+
setSelectedRecord(getSelectString(value));
|
|
1184
|
+
setSelectedVersion("draft");
|
|
1185
|
+
},
|
|
1186
|
+
disabled: !selectedSourceZone || recordsLoading || loading || sourceIsLocalized && !selectedLocale,
|
|
1187
|
+
size: "S",
|
|
1188
|
+
children: records.map((record) => /* @__PURE__ */ jsx(
|
|
1189
|
+
SingleSelectOption,
|
|
1190
|
+
{
|
|
1191
|
+
value: record.documentId || record.id.toString(),
|
|
1192
|
+
children: getRecordLabel(record)
|
|
1193
|
+
},
|
|
1194
|
+
record.documentId || record.id
|
|
1195
|
+
))
|
|
1196
|
+
}
|
|
1197
|
+
)
|
|
1198
|
+
] }),
|
|
1199
|
+
sourceHasDraftAndPublish && /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, width: "100%", children: [
|
|
1200
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1201
|
+
id: `${PLUGIN_ID}.source-version.label`,
|
|
1202
|
+
defaultMessage: "Source Version"
|
|
1203
|
+
}) }),
|
|
1204
|
+
/* @__PURE__ */ jsxs(
|
|
1205
|
+
SingleSelect,
|
|
1206
|
+
{
|
|
1207
|
+
value: selectedVersion,
|
|
1208
|
+
onChange: (value) => setSelectedVersion(value === "published" ? "published" : "draft"),
|
|
1209
|
+
disabled: !selectedRecord || loading,
|
|
1210
|
+
size: "S",
|
|
1211
|
+
children: [
|
|
1212
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "draft", children: formatMessage({
|
|
1213
|
+
id: `${PLUGIN_ID}.source-version.draft`,
|
|
1214
|
+
defaultMessage: "Draft"
|
|
1215
|
+
}) }),
|
|
1216
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "published", disabled: !selectedRecordHasPublished, children: selectedRecord && !selectedRecordHasPublished ? formatMessage({
|
|
1217
|
+
id: `${PLUGIN_ID}.source-version.published-unavailable`,
|
|
1218
|
+
defaultMessage: "Published (not available)"
|
|
1219
|
+
}) : formatMessage({
|
|
1220
|
+
id: `${PLUGIN_ID}.source-version.published`,
|
|
1221
|
+
defaultMessage: "Published"
|
|
1222
|
+
}) })
|
|
1223
|
+
]
|
|
1224
|
+
}
|
|
1225
|
+
)
|
|
1226
|
+
] }),
|
|
1227
|
+
canFetchSourcePreview && /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, width: "100%", children: [
|
|
1228
|
+
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", width: "100%", children: [
|
|
1229
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: formatMessage({
|
|
1230
|
+
id: `${PLUGIN_ID}.source-blocks.label`,
|
|
1231
|
+
defaultMessage: "Blocks to copy"
|
|
1232
|
+
}) }),
|
|
1233
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
1234
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", onClick: selectAllBlocks, disabled: loading, children: formatMessage({
|
|
1235
|
+
id: `${PLUGIN_ID}.source-blocks.select-all`,
|
|
1236
|
+
defaultMessage: "Select all"
|
|
1237
|
+
}) }),
|
|
1238
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", onClick: clearAllBlocks, disabled: loading, children: formatMessage({
|
|
1239
|
+
id: `${PLUGIN_ID}.source-blocks.clear-all`,
|
|
1240
|
+
defaultMessage: "Clear all"
|
|
1241
|
+
}) })
|
|
1242
|
+
] })
|
|
1243
|
+
] }),
|
|
1244
|
+
sourcePreviewLoading ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
|
|
1245
|
+
id: `${PLUGIN_ID}.source-blocks.loading`,
|
|
1246
|
+
defaultMessage: "Loading source blocks..."
|
|
1247
|
+
}) }) : sourceZoneEntries.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
|
|
1248
|
+
id: `${PLUGIN_ID}.source-blocks.empty`,
|
|
1249
|
+
defaultMessage: "No blocks found in the selected source zone."
|
|
1250
|
+
}) }) : /* @__PURE__ */ jsx(
|
|
1251
|
+
Flex,
|
|
1252
|
+
{
|
|
1253
|
+
direction: "column",
|
|
1254
|
+
gap: 2,
|
|
1255
|
+
width: "100%",
|
|
1256
|
+
padding: 3,
|
|
1257
|
+
background: "neutral100",
|
|
1258
|
+
hasRadius: true,
|
|
1259
|
+
children: sourceZoneEntries.map((entry, index2) => {
|
|
1260
|
+
const componentUid = entry?.__component ?? "";
|
|
1261
|
+
const label = getDynamicZoneEntryLabel(entry);
|
|
1262
|
+
const componentName = getComponentDisplayName(componentUid);
|
|
1263
|
+
const allowedComponents = schemaAttributes?.[selectedTargetZone]?.components;
|
|
1264
|
+
const isAllowed = !Array.isArray(allowedComponents) || allowedComponents.includes(componentUid);
|
|
1265
|
+
return /* @__PURE__ */ jsxs(
|
|
1266
|
+
Checkbox,
|
|
1267
|
+
{
|
|
1268
|
+
checked: selectedBlockIndexes.has(index2),
|
|
1269
|
+
onCheckedChange: (checked) => {
|
|
1270
|
+
setSelectedBlockIndexes((current) => {
|
|
1271
|
+
const next = new Set(current);
|
|
1272
|
+
if (checked === true) {
|
|
1273
|
+
next.add(index2);
|
|
1274
|
+
} else {
|
|
1275
|
+
next.delete(index2);
|
|
1276
|
+
}
|
|
1277
|
+
return next;
|
|
1278
|
+
});
|
|
1279
|
+
},
|
|
1280
|
+
disabled: loading || !isAllowed,
|
|
1281
|
+
children: [
|
|
1282
|
+
componentName,
|
|
1283
|
+
label ? ` · ${label}` : "",
|
|
1284
|
+
!isAllowed ? ` (${formatMessage({
|
|
1285
|
+
id: `${PLUGIN_ID}.source-blocks.not-allowed`,
|
|
1286
|
+
defaultMessage: "not allowed in target zone"
|
|
1287
|
+
})})` : ""
|
|
1288
|
+
]
|
|
1289
|
+
},
|
|
1290
|
+
`${componentUid}-${index2}`
|
|
1291
|
+
);
|
|
1292
|
+
})
|
|
1293
|
+
}
|
|
1294
|
+
)
|
|
1295
|
+
] })
|
|
1296
|
+
] }) }),
|
|
1297
|
+
/* @__PURE__ */ jsxs(Dialog.Footer, { "data-dz-tools-footer": "true", children: [
|
|
1298
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", fullWidth: true, onClick: onClose, disabled: loading, children: formatMessage({
|
|
1299
|
+
id: `${PLUGIN_ID}.modal.cancel`,
|
|
1300
|
+
defaultMessage: "No, cancel"
|
|
1301
|
+
}) }),
|
|
1302
|
+
/* @__PURE__ */ jsx(
|
|
1303
|
+
Button,
|
|
1304
|
+
{
|
|
1305
|
+
variant: "default",
|
|
1306
|
+
fullWidth: true,
|
|
1307
|
+
onClick: handleConfirmFill,
|
|
1308
|
+
loading,
|
|
1309
|
+
disabled: !isFormValid,
|
|
1310
|
+
children: formatMessage({
|
|
1311
|
+
id: `${PLUGIN_ID}.modal.confirm`,
|
|
1312
|
+
defaultMessage: "Yes, fill in"
|
|
1313
|
+
})
|
|
1314
|
+
}
|
|
1315
|
+
)
|
|
1316
|
+
] })
|
|
1317
|
+
]
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
};
|
|
1321
|
+
const DUPLICATE_ICON_PATH = "M27 4H11a1 1 0 0 0-1 1v5H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-5h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1m-1 16h-4v-9a1 1 0 0 0-1-1h-9V6h14z";
|
|
1322
|
+
const COPY_BLOCK_ICON_PATH = "M27.5 21.136 16 27.843 4.5 21.136a1 1 0 0 0-1 1.728l12 7a1 1 0 0 0 1.008 0l12-7a1 1 0 1 0-1.008-1.728M27.5 15.136 16 21.843 4.5 15.136a1 1 0 0 0-1 1.728l12 7a1 1 0 0 0 1.008 0l12-7a1 1 0 1 0-1.008-1.728m-24-4.272 12 7a1 1 0 0 0 1.008 0l12-7a1 1 0 0 0 0-1.728l-12-7a1 1 0 0 0-1.008 0l-12 7a1 1 0 0 0 0 1.728";
|
|
1323
|
+
const CARET_DOWN_ICON_PATH = "m26.708 12.708-10 10a1 1 0 0 1-1.415 0l-10-10A1 1 0 0 1 6 11h20a1 1 0 0 1 .707 1.707";
|
|
1324
|
+
function createRowActionButton(templateButton, label, iconPath, onClick, signal) {
|
|
1325
|
+
const button = document.createElement("button");
|
|
1326
|
+
button.type = "button";
|
|
1327
|
+
button.className = templateButton.className;
|
|
1328
|
+
button.setAttribute("aria-label", label);
|
|
1329
|
+
button.setAttribute("title", label);
|
|
1330
|
+
button.setAttribute("data-state", "closed");
|
|
1331
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1332
|
+
icon.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
1333
|
+
icon.setAttribute("viewBox", "0 0 32 32");
|
|
1334
|
+
icon.setAttribute("width", "16");
|
|
1335
|
+
icon.setAttribute("height", "16");
|
|
1336
|
+
icon.setAttribute("fill", "currentColor");
|
|
1337
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1338
|
+
icon.setAttribute("focusable", "false");
|
|
1339
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1340
|
+
path.setAttribute("d", iconPath);
|
|
1341
|
+
icon.appendChild(path);
|
|
1342
|
+
const text = document.createElement("span");
|
|
1343
|
+
const templateLabel = templateButton.querySelector("span");
|
|
1344
|
+
text.className = templateLabel?.className ?? "sc-kkmxCq hTCBro";
|
|
1345
|
+
text.textContent = label;
|
|
1346
|
+
button.append(icon, text);
|
|
1347
|
+
button.addEventListener(
|
|
1348
|
+
"click",
|
|
1349
|
+
(event) => {
|
|
1350
|
+
event.preventDefault();
|
|
1351
|
+
event.stopPropagation();
|
|
1352
|
+
onClick();
|
|
1353
|
+
},
|
|
1354
|
+
{ signal }
|
|
1355
|
+
);
|
|
1356
|
+
return button;
|
|
1357
|
+
}
|
|
1358
|
+
function createSvgIcon(iconPath, viewBox = "0 0 32 32") {
|
|
1359
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1360
|
+
icon.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
1361
|
+
icon.setAttribute("viewBox", viewBox);
|
|
1362
|
+
icon.setAttribute("width", "16");
|
|
1363
|
+
icon.setAttribute("height", "16");
|
|
1364
|
+
icon.setAttribute("fill", "currentColor");
|
|
1365
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1366
|
+
icon.setAttribute("focusable", "false");
|
|
1367
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1368
|
+
path.setAttribute("d", iconPath);
|
|
1369
|
+
icon.appendChild(path);
|
|
1370
|
+
return icon;
|
|
1371
|
+
}
|
|
1372
|
+
function positionMenu(trigger, menu) {
|
|
1373
|
+
const rect = trigger.getBoundingClientRect();
|
|
1374
|
+
menu.style.position = "fixed";
|
|
1375
|
+
menu.style.top = `${rect.bottom + 4}px`;
|
|
1376
|
+
menu.style.left = `${Math.max(8, rect.right - menu.offsetWidth)}px`;
|
|
1377
|
+
}
|
|
1378
|
+
function closeOpenMenus(exceptMenuId) {
|
|
1379
|
+
for (const menu of document.querySelectorAll('[data-dz-tools-row-menu="true"]')) {
|
|
1380
|
+
if (!(menu instanceof HTMLElement)) {
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
const menuId = menu.getAttribute("data-dz-tools-row-menu-id");
|
|
1384
|
+
if (menuId && menuId === exceptMenuId) {
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
menu.hidden = true;
|
|
1388
|
+
const triggerId = menu.getAttribute("data-dz-tools-row-menu-trigger-id");
|
|
1389
|
+
const trigger = triggerId ? document.querySelector(`[data-dz-tools-row-menu-trigger-id="${triggerId}"]`) : null;
|
|
1390
|
+
if (trigger instanceof HTMLButtonElement) {
|
|
1391
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
let openInsertMenuCount = 0;
|
|
1396
|
+
function getOpenInsertMenuCount() {
|
|
1397
|
+
return openInsertMenuCount;
|
|
1398
|
+
}
|
|
1399
|
+
function createRowActionMenu(templateButton, label, items, signal, options = {}) {
|
|
1400
|
+
const menuId = `dz-tools-menu-${Math.random().toString(36).slice(2)}`;
|
|
1401
|
+
const triggerId = `dz-tools-trigger-${Math.random().toString(36).slice(2)}`;
|
|
1402
|
+
const wrapper = document.createElement("span");
|
|
1403
|
+
wrapper.style.display = "inline-flex";
|
|
1404
|
+
const trigger = document.createElement("button");
|
|
1405
|
+
trigger.type = "button";
|
|
1406
|
+
trigger.className = templateButton.className;
|
|
1407
|
+
trigger.setAttribute("aria-label", label);
|
|
1408
|
+
trigger.setAttribute("title", label);
|
|
1409
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
1410
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1411
|
+
trigger.setAttribute("data-state", "closed");
|
|
1412
|
+
trigger.setAttribute("data-dz-tools-row-menu-trigger-id", triggerId);
|
|
1413
|
+
const text = document.createElement("span");
|
|
1414
|
+
const templateLabel = templateButton.querySelector("span");
|
|
1415
|
+
text.className = templateLabel?.className ?? "sc-kkmxCq hTCBro";
|
|
1416
|
+
text.textContent = label;
|
|
1417
|
+
trigger.append(createSvgIcon(CARET_DOWN_ICON_PATH), text);
|
|
1418
|
+
const menu = document.createElement("div");
|
|
1419
|
+
menu.setAttribute("data-dz-tools-row-menu", "true");
|
|
1420
|
+
menu.setAttribute("data-dz-tools-row-menu-id", menuId);
|
|
1421
|
+
menu.setAttribute("data-dz-tools-row-menu-trigger-id", triggerId);
|
|
1422
|
+
menu.setAttribute("role", "menu");
|
|
1423
|
+
menu.hidden = true;
|
|
1424
|
+
menu.style.zIndex = "10000";
|
|
1425
|
+
menu.style.minWidth = "180px";
|
|
1426
|
+
menu.style.padding = "4px";
|
|
1427
|
+
menu.style.borderRadius = "4px";
|
|
1428
|
+
menu.style.background = "var(--strapi-neutral-0, #ffffff)";
|
|
1429
|
+
menu.style.border = "1px solid var(--strapi-neutral-200, #dcdce4)";
|
|
1430
|
+
menu.style.boxShadow = "0 2px 15px rgba(33, 33, 52, 0.15)";
|
|
1431
|
+
for (const item of items) {
|
|
1432
|
+
const menuItem = document.createElement("button");
|
|
1433
|
+
menuItem.type = "button";
|
|
1434
|
+
menuItem.setAttribute("role", "menuitem");
|
|
1435
|
+
menuItem.textContent = item.label;
|
|
1436
|
+
menuItem.disabled = Boolean(item.disabled);
|
|
1437
|
+
menuItem.style.display = "block";
|
|
1438
|
+
menuItem.style.width = "100%";
|
|
1439
|
+
menuItem.style.padding = "8px 12px";
|
|
1440
|
+
menuItem.style.border = "none";
|
|
1441
|
+
menuItem.style.background = "transparent";
|
|
1442
|
+
menuItem.style.textAlign = "left";
|
|
1443
|
+
menuItem.style.font = "inherit";
|
|
1444
|
+
menuItem.style.fontSize = "1.2rem";
|
|
1445
|
+
menuItem.style.lineHeight = "1.33";
|
|
1446
|
+
menuItem.style.color = item.disabled ? "var(--strapi-neutral-400, #a5a5ba)" : "var(--strapi-neutral-800, #32324d)";
|
|
1447
|
+
menuItem.style.cursor = item.disabled ? "not-allowed" : "pointer";
|
|
1448
|
+
menuItem.style.borderRadius = "4px";
|
|
1449
|
+
if (!item.disabled) {
|
|
1450
|
+
menuItem.addEventListener("mouseenter", () => {
|
|
1451
|
+
menuItem.style.background = "var(--strapi-neutral-100, #f6f6f9)";
|
|
1452
|
+
});
|
|
1453
|
+
menuItem.addEventListener("mouseleave", () => {
|
|
1454
|
+
menuItem.style.background = "transparent";
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
menuItem.addEventListener(
|
|
1458
|
+
"click",
|
|
1459
|
+
(event) => {
|
|
1460
|
+
event.preventDefault();
|
|
1461
|
+
event.stopPropagation();
|
|
1462
|
+
if (item.disabled) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
menu.hidden = true;
|
|
1466
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1467
|
+
item.onSelect();
|
|
1468
|
+
},
|
|
1469
|
+
{ signal }
|
|
1470
|
+
);
|
|
1471
|
+
menu.appendChild(menuItem);
|
|
1472
|
+
}
|
|
1473
|
+
const openMenu = () => {
|
|
1474
|
+
if (!document.body.contains(menu)) {
|
|
1475
|
+
document.body.appendChild(menu);
|
|
1476
|
+
}
|
|
1477
|
+
menu.hidden = false;
|
|
1478
|
+
openInsertMenuCount += 1;
|
|
1479
|
+
requestAnimationFrame(() => {
|
|
1480
|
+
positionMenu(trigger, menu);
|
|
1481
|
+
});
|
|
1482
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
1483
|
+
};
|
|
1484
|
+
const closeMenu = () => {
|
|
1485
|
+
if (menu.hidden) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
menu.hidden = true;
|
|
1489
|
+
openInsertMenuCount = Math.max(0, openInsertMenuCount - 1);
|
|
1490
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1491
|
+
};
|
|
1492
|
+
trigger.addEventListener(
|
|
1493
|
+
"click",
|
|
1494
|
+
(event) => {
|
|
1495
|
+
void (async () => {
|
|
1496
|
+
event.preventDefault();
|
|
1497
|
+
event.stopPropagation();
|
|
1498
|
+
const willOpen = menu.hidden;
|
|
1499
|
+
closeOpenMenus(willOpen ? menuId : void 0);
|
|
1500
|
+
if (!willOpen) {
|
|
1501
|
+
closeMenu();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (options.onBeforeOpen) {
|
|
1505
|
+
const canOpen = await options.onBeforeOpen();
|
|
1506
|
+
if (!canOpen) {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
ignoreNextDocumentClick = true;
|
|
1511
|
+
openMenu();
|
|
1512
|
+
window.setTimeout(() => {
|
|
1513
|
+
ignoreNextDocumentClick = false;
|
|
1514
|
+
}, 0);
|
|
1515
|
+
})();
|
|
1516
|
+
},
|
|
1517
|
+
{ signal }
|
|
1518
|
+
);
|
|
1519
|
+
let ignoreNextDocumentClick = false;
|
|
1520
|
+
const onDocumentClick = (event) => {
|
|
1521
|
+
if (ignoreNextDocumentClick) {
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (!wrapper.contains(event.target) && !menu.contains(event.target)) {
|
|
1525
|
+
closeMenu();
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
const onReposition = () => {
|
|
1529
|
+
if (!menu.hidden) {
|
|
1530
|
+
positionMenu(trigger, menu);
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
document.addEventListener(
|
|
1534
|
+
"click",
|
|
1535
|
+
(event) => {
|
|
1536
|
+
if (menu.hidden) {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
onDocumentClick(event);
|
|
1540
|
+
},
|
|
1541
|
+
{ signal }
|
|
1542
|
+
);
|
|
1543
|
+
document.addEventListener("scroll", onReposition, { capture: true, signal });
|
|
1544
|
+
window.addEventListener("resize", onReposition, { signal });
|
|
1545
|
+
document.addEventListener(
|
|
1546
|
+
"keydown",
|
|
1547
|
+
(event) => {
|
|
1548
|
+
if (event.key === "Escape" && !menu.hidden) {
|
|
1549
|
+
closeMenu();
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
{ signal }
|
|
1553
|
+
);
|
|
1554
|
+
signal.addEventListener("abort", () => {
|
|
1555
|
+
if (!menu.hidden) {
|
|
1556
|
+
openInsertMenuCount = Math.max(0, openInsertMenuCount - 1);
|
|
1557
|
+
}
|
|
1558
|
+
menu.remove();
|
|
1559
|
+
});
|
|
1560
|
+
wrapper.append(trigger);
|
|
1561
|
+
return { wrapper, menu };
|
|
1562
|
+
}
|
|
1563
|
+
const CLIPBOARD_PLUGIN_ID = PLUGIN_ID;
|
|
1564
|
+
const CLIPBOARD_VERSION = 1;
|
|
1565
|
+
let memoryFallback = null;
|
|
1566
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1567
|
+
function notifyListeners() {
|
|
1568
|
+
listeners.forEach((listener) => listener());
|
|
1569
|
+
}
|
|
1570
|
+
function subscribeClipboard(listener) {
|
|
1571
|
+
listeners.add(listener);
|
|
1572
|
+
return () => listeners.delete(listener);
|
|
1573
|
+
}
|
|
1574
|
+
function getMemoryClipboardPayload() {
|
|
1575
|
+
return memoryFallback;
|
|
1576
|
+
}
|
|
1577
|
+
function isPlainObject(value) {
|
|
1578
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1579
|
+
}
|
|
1580
|
+
function parseClipboardPayload(text) {
|
|
1581
|
+
try {
|
|
1582
|
+
const parsed = JSON.parse(text);
|
|
1583
|
+
if (!isPlainObject(parsed)) {
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
if (parsed.plugin !== CLIPBOARD_PLUGIN_ID || parsed.version !== CLIPBOARD_VERSION) {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
if (!Array.isArray(parsed.entries) || parsed.entries.length === 0) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
const validEntries = parsed.entries.every(
|
|
1593
|
+
(entry) => isPlainObject(entry) && typeof entry.__component === "string"
|
|
1594
|
+
);
|
|
1595
|
+
if (!validEntries) {
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
return parsed;
|
|
1599
|
+
} catch {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function buildClipboardPayload(entries) {
|
|
1604
|
+
return {
|
|
1605
|
+
plugin: CLIPBOARD_PLUGIN_ID,
|
|
1606
|
+
version: CLIPBOARD_VERSION,
|
|
1607
|
+
entries,
|
|
1608
|
+
source: {
|
|
1609
|
+
copiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1610
|
+
components: entries.map((entry) => entry.__component).filter(Boolean)
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
async function writeClipboardPayload(payload) {
|
|
1615
|
+
memoryFallback = payload;
|
|
1616
|
+
notifyListeners();
|
|
1617
|
+
const serialized = JSON.stringify(payload);
|
|
1618
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
1619
|
+
try {
|
|
1620
|
+
await navigator.clipboard.writeText(serialized);
|
|
1621
|
+
} catch {
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
async function resolveClipboardPayload() {
|
|
1626
|
+
return getMemoryClipboardPayload() ?? await readClipboardPayload();
|
|
1627
|
+
}
|
|
1628
|
+
async function readClipboardPayload() {
|
|
1629
|
+
if (memoryFallback) {
|
|
1630
|
+
return memoryFallback;
|
|
1631
|
+
}
|
|
1632
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.readText) {
|
|
1633
|
+
try {
|
|
1634
|
+
const text = await navigator.clipboard.readText();
|
|
1635
|
+
const parsed = parseClipboardPayload(text);
|
|
1636
|
+
if (parsed) {
|
|
1637
|
+
memoryFallback = parsed;
|
|
1638
|
+
notifyListeners();
|
|
1639
|
+
}
|
|
1640
|
+
return parsed;
|
|
1641
|
+
} catch {
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
function isClipboardCompatibleWithZone(payload, allowedComponents) {
|
|
1648
|
+
if (!payload || payload.entries.length === 0) {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
if (!Array.isArray(allowedComponents)) {
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
return payload.entries.every((entry) => allowedComponents.includes(entry.__component));
|
|
1655
|
+
}
|
|
1656
|
+
function getRelationQueryParams(query) {
|
|
1657
|
+
if (!query) {
|
|
1658
|
+
return {};
|
|
1659
|
+
}
|
|
1660
|
+
const { plugins, ...rest } = query;
|
|
1661
|
+
const pluginParams = Object.values(plugins ?? {}).reduce(
|
|
1662
|
+
(acc, current) => Object.assign(acc, current),
|
|
1663
|
+
{}
|
|
1664
|
+
);
|
|
1665
|
+
const params = { ...rest, ...pluginParams };
|
|
1666
|
+
return {
|
|
1667
|
+
locale: params.locale,
|
|
1668
|
+
status: params.status
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
const ROW_ACTIONS_ATTR = "data-dz-tools-row-actions";
|
|
1672
|
+
const DynamicZoneComponentDuplicateInjector = () => {
|
|
1673
|
+
const { formatMessage } = useIntl();
|
|
1674
|
+
const { get } = useFetchClient();
|
|
1675
|
+
const { toggleNotification } = useNotification();
|
|
1676
|
+
const [{ query }] = useQueryParams();
|
|
1677
|
+
const { form, contentTypes, contentType } = unstable_useContentManagerContext();
|
|
1678
|
+
const contentManagerForm = form;
|
|
1679
|
+
const addFieldRow = contentManagerForm?.addFieldRow;
|
|
1680
|
+
const moveFieldRow = contentManagerForm?.moveFieldRow;
|
|
1681
|
+
const formValues = contentManagerForm?.values ?? {};
|
|
1682
|
+
const valuesRef = React.useRef(formValues);
|
|
1683
|
+
const schemasRef = React.useRef(null);
|
|
1684
|
+
const fetchRelationItemsRef = React.useRef(null);
|
|
1685
|
+
const mountedButtonsRef = React.useRef([]);
|
|
1686
|
+
const frameRef = React.useRef(0);
|
|
1687
|
+
const observerRef = React.useRef(null);
|
|
1688
|
+
const clipboardReadyRef = React.useRef(Boolean(getMemoryClipboardPayload()));
|
|
1689
|
+
const [schemasReady, setSchemasReady] = React.useState(false);
|
|
1690
|
+
const [clipboardTick, setClipboardTick] = React.useState(0);
|
|
1691
|
+
valuesRef.current = formValues;
|
|
1692
|
+
const relationQueryParams = React.useMemo(
|
|
1693
|
+
() => getRelationQueryParams(query),
|
|
1694
|
+
[query]
|
|
1695
|
+
);
|
|
1696
|
+
React.useEffect(() => {
|
|
1697
|
+
fetchRelationItemsRef.current = createFetchRelationItems(get, relationQueryParams);
|
|
1698
|
+
}, [get, relationQueryParams]);
|
|
1699
|
+
React.useEffect(() => {
|
|
1700
|
+
return subscribeClipboard(() => {
|
|
1701
|
+
clipboardReadyRef.current = Boolean(getMemoryClipboardPayload());
|
|
1702
|
+
setClipboardTick((value) => value + 1);
|
|
1703
|
+
});
|
|
1704
|
+
}, []);
|
|
1705
|
+
React.useEffect(() => {
|
|
1706
|
+
const syncClipboardFromSystem = () => {
|
|
1707
|
+
void resolveClipboardPayload().then((payload) => {
|
|
1708
|
+
if (payload) {
|
|
1709
|
+
setClipboardTick((value) => value + 1);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
};
|
|
1713
|
+
window.addEventListener("focus", syncClipboardFromSystem);
|
|
1714
|
+
return () => {
|
|
1715
|
+
window.removeEventListener("focus", syncClipboardFromSystem);
|
|
1716
|
+
};
|
|
1717
|
+
}, []);
|
|
1718
|
+
const duplicateLabel = formatMessage({
|
|
1719
|
+
id: `${PLUGIN_ID}.component-duplicate.action`,
|
|
1720
|
+
defaultMessage: "Duplicate component"
|
|
1721
|
+
});
|
|
1722
|
+
const copyLabel = formatMessage({
|
|
1723
|
+
id: `${PLUGIN_ID}.component-copy.action`,
|
|
1724
|
+
defaultMessage: "Copy block"
|
|
1725
|
+
});
|
|
1726
|
+
const insertLabel = formatMessage({
|
|
1727
|
+
id: `${PLUGIN_ID}.component-insert.menu`,
|
|
1728
|
+
defaultMessage: "Insert"
|
|
1729
|
+
});
|
|
1730
|
+
const pasteAboveLabel = formatMessage({
|
|
1731
|
+
id: `${PLUGIN_ID}.component-paste.above`,
|
|
1732
|
+
defaultMessage: "Paste above"
|
|
1733
|
+
});
|
|
1734
|
+
const pasteBelowLabel = formatMessage({
|
|
1735
|
+
id: `${PLUGIN_ID}.component-paste.below`,
|
|
1736
|
+
defaultMessage: "Paste below"
|
|
1737
|
+
});
|
|
1738
|
+
const pasteUnavailableLabel = formatMessage({
|
|
1739
|
+
id: `${PLUGIN_ID}.component-paste.unavailable`,
|
|
1740
|
+
defaultMessage: "No compatible copied block found. Copy a block first."
|
|
1741
|
+
});
|
|
1742
|
+
const duplicateSuccessLabel = formatMessage({
|
|
1743
|
+
id: `${PLUGIN_ID}.component-duplicate.success`,
|
|
1744
|
+
defaultMessage: "Component duplicated. Save the document to keep it."
|
|
1745
|
+
});
|
|
1746
|
+
const copySuccessLabel = formatMessage({
|
|
1747
|
+
id: `${PLUGIN_ID}.component-copy.success`,
|
|
1748
|
+
defaultMessage: "Block copied."
|
|
1749
|
+
});
|
|
1750
|
+
const pasteSuccessLabels = React.useMemo(
|
|
1751
|
+
() => ({
|
|
1752
|
+
above: formatMessage({
|
|
1753
|
+
id: `${PLUGIN_ID}.component-paste.success.above`,
|
|
1754
|
+
defaultMessage: "Block pasted above. Save the document to keep it."
|
|
1755
|
+
}),
|
|
1756
|
+
below: formatMessage({
|
|
1757
|
+
id: `${PLUGIN_ID}.component-paste.success.below`,
|
|
1758
|
+
defaultMessage: "Block pasted below. Save the document to keep it."
|
|
1759
|
+
})
|
|
1760
|
+
}),
|
|
1761
|
+
[formatMessage]
|
|
1762
|
+
);
|
|
1763
|
+
const pasteErrorLabel = formatMessage({
|
|
1764
|
+
id: `${PLUGIN_ID}.component-paste.error`,
|
|
1765
|
+
defaultMessage: "Could not paste the copied block."
|
|
1766
|
+
});
|
|
1767
|
+
const duplicateErrorLabel = formatMessage({
|
|
1768
|
+
id: `${PLUGIN_ID}.component-duplicate.error`,
|
|
1769
|
+
defaultMessage: "Could not duplicate this component."
|
|
1770
|
+
});
|
|
1771
|
+
const copyErrorLabel = formatMessage({
|
|
1772
|
+
id: `${PLUGIN_ID}.component-copy.error`,
|
|
1773
|
+
defaultMessage: "Could not copy this block."
|
|
1774
|
+
});
|
|
1775
|
+
React.useEffect(() => {
|
|
1776
|
+
let active = true;
|
|
1777
|
+
const fetchSchemas = async () => {
|
|
1778
|
+
try {
|
|
1779
|
+
schemasRef.current = await loadComponentSchemas(get);
|
|
1780
|
+
if (active) {
|
|
1781
|
+
setSchemasReady(true);
|
|
1782
|
+
}
|
|
1783
|
+
} catch {
|
|
1784
|
+
if (active) {
|
|
1785
|
+
setSchemasReady(false);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
void fetchSchemas();
|
|
1790
|
+
return () => {
|
|
1791
|
+
active = false;
|
|
1792
|
+
};
|
|
1793
|
+
}, [get]);
|
|
1794
|
+
const cleanupMountedButtons = React.useCallback(() => {
|
|
1795
|
+
for (const mounted of mountedButtonsRef.current) {
|
|
1796
|
+
mounted.abort.abort();
|
|
1797
|
+
mounted.menu?.remove();
|
|
1798
|
+
mounted.host.remove();
|
|
1799
|
+
}
|
|
1800
|
+
mountedButtonsRef.current = [];
|
|
1801
|
+
}, []);
|
|
1802
|
+
const getAllowedComponents = React.useCallback(
|
|
1803
|
+
(zonePath) => {
|
|
1804
|
+
const schema = contentType;
|
|
1805
|
+
return schema?.attributes?.[zonePath]?.components;
|
|
1806
|
+
},
|
|
1807
|
+
[contentType]
|
|
1808
|
+
);
|
|
1809
|
+
const insertEntryAt = React.useCallback(
|
|
1810
|
+
(zonePath, targetIndex, entry) => {
|
|
1811
|
+
const zone = valuesRef.current[zonePath];
|
|
1812
|
+
if (!Array.isArray(zone) || typeof addFieldRow !== "function") {
|
|
1813
|
+
return false;
|
|
1814
|
+
}
|
|
1815
|
+
const appendedIndex = zone.length;
|
|
1816
|
+
addFieldRow(zonePath, entry);
|
|
1817
|
+
if (appendedIndex !== targetIndex && typeof moveFieldRow === "function") {
|
|
1818
|
+
moveFieldRow(zonePath, appendedIndex, targetIndex);
|
|
1819
|
+
}
|
|
1820
|
+
return true;
|
|
1821
|
+
},
|
|
1822
|
+
[addFieldRow, moveFieldRow]
|
|
1823
|
+
);
|
|
1824
|
+
const insertEntryBelow = React.useCallback(
|
|
1825
|
+
(zonePath, sourceIndex, entry) => insertEntryAt(zonePath, sourceIndex + 1, entry),
|
|
1826
|
+
[insertEntryAt]
|
|
1827
|
+
);
|
|
1828
|
+
const ensurePasteAvailable = React.useCallback(
|
|
1829
|
+
async (zonePath) => {
|
|
1830
|
+
const payload = await resolveClipboardPayload();
|
|
1831
|
+
const allowedComponents = getAllowedComponents(zonePath);
|
|
1832
|
+
const compatible = isClipboardCompatibleWithZone(payload, allowedComponents);
|
|
1833
|
+
if (compatible) {
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
toggleNotification({ type: "warning", message: pasteUnavailableLabel });
|
|
1837
|
+
return false;
|
|
1838
|
+
},
|
|
1839
|
+
[getAllowedComponents, pasteUnavailableLabel, toggleNotification]
|
|
1840
|
+
);
|
|
1841
|
+
const handleDuplicate = React.useCallback(
|
|
1842
|
+
async (zonePath, sourceIndex) => {
|
|
1843
|
+
const schemas = schemasRef.current;
|
|
1844
|
+
if (!schemas || typeof addFieldRow !== "function") {
|
|
1845
|
+
toggleNotification({ type: "danger", message: duplicateErrorLabel });
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const zone = valuesRef.current[zonePath];
|
|
1849
|
+
if (!Array.isArray(zone) || !zone[sourceIndex]) {
|
|
1850
|
+
toggleNotification({ type: "danger", message: duplicateErrorLabel });
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
try {
|
|
1854
|
+
const clonedEntry = await cloneFormDynamicZoneEntry(zone[sourceIndex], {
|
|
1855
|
+
schemas,
|
|
1856
|
+
contentTypes,
|
|
1857
|
+
fetchRelationItems: fetchRelationItemsRef.current ?? void 0
|
|
1858
|
+
});
|
|
1859
|
+
if (!insertEntryBelow(zonePath, sourceIndex, clonedEntry)) {
|
|
1860
|
+
throw new Error("Insert failed");
|
|
1861
|
+
}
|
|
1862
|
+
toggleNotification({
|
|
1863
|
+
type: "success",
|
|
1864
|
+
message: duplicateSuccessLabel
|
|
1865
|
+
});
|
|
1866
|
+
} catch {
|
|
1867
|
+
toggleNotification({ type: "danger", message: duplicateErrorLabel });
|
|
1868
|
+
}
|
|
1869
|
+
},
|
|
1870
|
+
[
|
|
1871
|
+
contentTypes,
|
|
1872
|
+
duplicateErrorLabel,
|
|
1873
|
+
duplicateSuccessLabel,
|
|
1874
|
+
insertEntryBelow,
|
|
1875
|
+
toggleNotification
|
|
1876
|
+
]
|
|
1877
|
+
);
|
|
1878
|
+
const handleCopy = React.useCallback(
|
|
1879
|
+
async (zonePath, sourceIndex) => {
|
|
1880
|
+
const schemas = schemasRef.current;
|
|
1881
|
+
const zone = valuesRef.current[zonePath];
|
|
1882
|
+
if (!schemas || !Array.isArray(zone) || !zone[sourceIndex]) {
|
|
1883
|
+
toggleNotification({ type: "danger", message: copyErrorLabel });
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
const clonedEntry = await cloneFormDynamicZoneEntry(zone[sourceIndex], {
|
|
1888
|
+
schemas,
|
|
1889
|
+
contentTypes,
|
|
1890
|
+
fetchRelationItems: fetchRelationItemsRef.current ?? void 0
|
|
1891
|
+
});
|
|
1892
|
+
const payload = buildClipboardPayload([clonedEntry]);
|
|
1893
|
+
await writeClipboardPayload(payload);
|
|
1894
|
+
clipboardReadyRef.current = true;
|
|
1895
|
+
setClipboardTick((value) => value + 1);
|
|
1896
|
+
toggleNotification({
|
|
1897
|
+
type: "success",
|
|
1898
|
+
message: copySuccessLabel
|
|
1899
|
+
});
|
|
1900
|
+
} catch {
|
|
1901
|
+
toggleNotification({ type: "danger", message: copyErrorLabel });
|
|
1902
|
+
}
|
|
1903
|
+
},
|
|
1904
|
+
[contentTypes, copyErrorLabel, copySuccessLabel, toggleNotification]
|
|
1905
|
+
);
|
|
1906
|
+
const handlePaste = React.useCallback(
|
|
1907
|
+
async (zonePath, sourceIndex, position) => {
|
|
1908
|
+
if (typeof addFieldRow !== "function") {
|
|
1909
|
+
toggleNotification({ type: "danger", message: pasteErrorLabel });
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
try {
|
|
1913
|
+
const payload = await resolveClipboardPayload();
|
|
1914
|
+
const allowedComponents = getAllowedComponents(zonePath);
|
|
1915
|
+
if (!isClipboardCompatibleWithZone(payload, allowedComponents)) {
|
|
1916
|
+
toggleNotification({ type: "danger", message: pasteErrorLabel });
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
let insertIndex = sourceIndex;
|
|
1920
|
+
const zone = valuesRef.current[zonePath];
|
|
1921
|
+
if (!Array.isArray(zone)) {
|
|
1922
|
+
throw new Error("Zone not found");
|
|
1923
|
+
}
|
|
1924
|
+
for (const entry of payload.entries) {
|
|
1925
|
+
let inserted = false;
|
|
1926
|
+
let preparedEntry = entry;
|
|
1927
|
+
if (position === "above") {
|
|
1928
|
+
preparedEntry = prepareEntryForZoneInsert(zone, insertIndex, entry);
|
|
1929
|
+
inserted = insertEntryAt(zonePath, insertIndex, preparedEntry);
|
|
1930
|
+
insertIndex += 1;
|
|
1931
|
+
} else {
|
|
1932
|
+
preparedEntry = prepareEntryForZoneInsert(zone, insertIndex + 1, entry);
|
|
1933
|
+
inserted = insertEntryBelow(zonePath, insertIndex, preparedEntry);
|
|
1934
|
+
insertIndex += 1;
|
|
1935
|
+
}
|
|
1936
|
+
if (!inserted) {
|
|
1937
|
+
throw new Error("Insert failed");
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
toggleNotification({
|
|
1941
|
+
type: "success",
|
|
1942
|
+
message: pasteSuccessLabels[position]
|
|
1943
|
+
});
|
|
1944
|
+
} catch {
|
|
1945
|
+
toggleNotification({ type: "danger", message: pasteErrorLabel });
|
|
1946
|
+
}
|
|
1947
|
+
},
|
|
1948
|
+
[
|
|
1949
|
+
addFieldRow,
|
|
1950
|
+
getAllowedComponents,
|
|
1951
|
+
insertEntryAt,
|
|
1952
|
+
insertEntryBelow,
|
|
1953
|
+
pasteErrorLabel,
|
|
1954
|
+
pasteSuccessLabels,
|
|
1955
|
+
toggleNotification
|
|
1956
|
+
]
|
|
1957
|
+
);
|
|
1958
|
+
const mountRowActions = React.useCallback(
|
|
1959
|
+
(actionContainer, zonePath, sourceIndex, insertBefore) => {
|
|
1960
|
+
const templateButton = actionContainer.querySelector("button");
|
|
1961
|
+
if (!(templateButton instanceof HTMLButtonElement)) {
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const host = document.createElement("span");
|
|
1965
|
+
host.setAttribute(ROW_ACTIONS_ATTR, "true");
|
|
1966
|
+
host.style.display = "inline-flex";
|
|
1967
|
+
host.style.gap = "0";
|
|
1968
|
+
const abort = new AbortController();
|
|
1969
|
+
host.appendChild(
|
|
1970
|
+
createRowActionButton(
|
|
1971
|
+
templateButton,
|
|
1972
|
+
duplicateLabel,
|
|
1973
|
+
DUPLICATE_ICON_PATH,
|
|
1974
|
+
() => {
|
|
1975
|
+
void handleDuplicate(zonePath, sourceIndex);
|
|
1976
|
+
},
|
|
1977
|
+
abort.signal
|
|
1978
|
+
)
|
|
1979
|
+
);
|
|
1980
|
+
host.appendChild(
|
|
1981
|
+
createRowActionButton(
|
|
1982
|
+
templateButton,
|
|
1983
|
+
copyLabel,
|
|
1984
|
+
COPY_BLOCK_ICON_PATH,
|
|
1985
|
+
() => {
|
|
1986
|
+
void handleCopy(zonePath, sourceIndex);
|
|
1987
|
+
},
|
|
1988
|
+
abort.signal
|
|
1989
|
+
)
|
|
1990
|
+
);
|
|
1991
|
+
const { wrapper: insertMenu, menu } = createRowActionMenu(
|
|
1992
|
+
templateButton,
|
|
1993
|
+
insertLabel,
|
|
1994
|
+
[
|
|
1995
|
+
{
|
|
1996
|
+
label: pasteAboveLabel,
|
|
1997
|
+
onSelect: () => {
|
|
1998
|
+
void handlePaste(zonePath, sourceIndex, "above");
|
|
1999
|
+
}
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
label: pasteBelowLabel,
|
|
2003
|
+
onSelect: () => {
|
|
2004
|
+
void handlePaste(zonePath, sourceIndex, "below");
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
],
|
|
2008
|
+
abort.signal,
|
|
2009
|
+
{
|
|
2010
|
+
onBeforeOpen: () => ensurePasteAvailable(zonePath)
|
|
2011
|
+
}
|
|
2012
|
+
);
|
|
2013
|
+
host.appendChild(insertMenu);
|
|
2014
|
+
mountedButtonsRef.current.push({ host, menu, abort });
|
|
2015
|
+
if (insertBefore) {
|
|
2016
|
+
actionContainer.insertBefore(host, insertBefore);
|
|
2017
|
+
} else {
|
|
2018
|
+
actionContainer.appendChild(host);
|
|
2019
|
+
}
|
|
2020
|
+
return;
|
|
2021
|
+
},
|
|
2022
|
+
[
|
|
2023
|
+
copyLabel,
|
|
2024
|
+
duplicateLabel,
|
|
2025
|
+
ensurePasteAvailable,
|
|
2026
|
+
handleCopy,
|
|
2027
|
+
handleDuplicate,
|
|
2028
|
+
handlePaste,
|
|
2029
|
+
insertLabel,
|
|
2030
|
+
pasteAboveLabel,
|
|
2031
|
+
pasteBelowLabel
|
|
2032
|
+
]
|
|
2033
|
+
);
|
|
2034
|
+
const mountForRow = React.useCallback(
|
|
2035
|
+
(rowElement, blockRef) => {
|
|
2036
|
+
const actionContainer = findRowActionContainer(rowElement);
|
|
2037
|
+
if (!(actionContainer instanceof HTMLElement) || actionContainer.querySelector(`[${ROW_ACTIONS_ATTR}]`)) {
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
mountRowActions(
|
|
2041
|
+
actionContainer,
|
|
2042
|
+
blockRef.zonePath,
|
|
2043
|
+
blockRef.index,
|
|
2044
|
+
getRowActionInsertPoint(actionContainer)
|
|
2045
|
+
);
|
|
2046
|
+
},
|
|
2047
|
+
[mountRowActions]
|
|
2048
|
+
);
|
|
2049
|
+
const syncDuplicateButtons = React.useCallback(() => {
|
|
2050
|
+
const observer = observerRef.current;
|
|
2051
|
+
if (observer) {
|
|
2052
|
+
observer.disconnect();
|
|
2053
|
+
}
|
|
2054
|
+
cleanupMountedButtons();
|
|
2055
|
+
const currentValues = valuesRef.current;
|
|
2056
|
+
const zonePaths = getDynamicZonePathsFromValues(currentValues);
|
|
2057
|
+
if (!schemasReady || zonePaths.length === 0 || typeof addFieldRow !== "function") {
|
|
2058
|
+
observer?.observe(document.body, { childList: true, subtree: true });
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
for (const list of findDynamicZoneLists()) {
|
|
2062
|
+
const rows = getDynamicZoneListItems(list);
|
|
2063
|
+
rows.forEach((row, index2) => {
|
|
2064
|
+
const blockRef = resolveBlockFromListItem(row, currentValues) ?? resolveBlockFromListItemIndex(list, index2, currentValues, zonePaths);
|
|
2065
|
+
if (!blockRef) {
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
mountForRow(row, blockRef);
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
for (const dragHandle of findDynamicZoneDragHandles()) {
|
|
2072
|
+
const rowElement = findRowElementForDragHandle(dragHandle);
|
|
2073
|
+
if (!rowElement || rowElement.querySelector(`[${ROW_ACTIONS_ATTR}]`)) {
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
const blockRef = resolveBlockFromListItem(rowElement, currentValues);
|
|
2077
|
+
if (blockRef) {
|
|
2078
|
+
mountForRow(rowElement, blockRef);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
observer?.observe(document.body, { childList: true, subtree: true });
|
|
2082
|
+
}, [addFieldRow, cleanupMountedButtons, clipboardTick, mountForRow, schemasReady]);
|
|
2083
|
+
React.useEffect(() => {
|
|
2084
|
+
if (typeof document === "undefined") {
|
|
2085
|
+
return void 0;
|
|
2086
|
+
}
|
|
2087
|
+
const observer = new MutationObserver(() => {
|
|
2088
|
+
if (getOpenInsertMenuCount() > 0) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
cancelAnimationFrame(frameRef.current);
|
|
2092
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
2093
|
+
if (getOpenInsertMenuCount() > 0) {
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
syncDuplicateButtons();
|
|
2097
|
+
});
|
|
2098
|
+
});
|
|
2099
|
+
observerRef.current = observer;
|
|
2100
|
+
syncDuplicateButtons();
|
|
2101
|
+
return () => {
|
|
2102
|
+
cancelAnimationFrame(frameRef.current);
|
|
2103
|
+
observer.disconnect();
|
|
2104
|
+
observerRef.current = null;
|
|
2105
|
+
cleanupMountedButtons();
|
|
2106
|
+
};
|
|
2107
|
+
}, [cleanupMountedButtons, syncDuplicateButtons, clipboardTick]);
|
|
2108
|
+
return null;
|
|
2109
|
+
};
|
|
2110
|
+
const DynamicZoneEditViewExtensions = () => /* @__PURE__ */ jsx(DynamicZoneComponentDuplicateInjector, {});
|
|
2111
|
+
const index = {
|
|
2112
|
+
register(app) {
|
|
2113
|
+
app.registerPlugin({
|
|
2114
|
+
id: PLUGIN_ID,
|
|
2115
|
+
initializer: Initializer,
|
|
2116
|
+
isReady: false,
|
|
2117
|
+
name: PLUGIN_ID
|
|
2118
|
+
});
|
|
2119
|
+
},
|
|
2120
|
+
async bootstrap(app) {
|
|
2121
|
+
try {
|
|
2122
|
+
const contentManager = app.getPlugin("content-manager");
|
|
2123
|
+
contentManager.apis.addDocumentHeaderAction([DynamicZoneToolsHeaderAction]);
|
|
2124
|
+
const injector = {
|
|
2125
|
+
name: `${PLUGIN_ID}-edit-view-extensions`,
|
|
2126
|
+
Component: DynamicZoneEditViewExtensions
|
|
2127
|
+
};
|
|
2128
|
+
if (typeof contentManager?.injectComponent === "function") {
|
|
2129
|
+
contentManager.injectComponent("editView", "right-links", injector);
|
|
2130
|
+
} else if (typeof app?.injectComponent === "function") {
|
|
2131
|
+
app.injectComponent("editView", "right-links", injector);
|
|
2132
|
+
}
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
console.warn("Failed to register dynamic zone tools admin extensions:", error);
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2137
|
+
async registerTrads(app) {
|
|
2138
|
+
const { locales } = app;
|
|
2139
|
+
const importedTranslations = await Promise.all(
|
|
2140
|
+
locales.map((locale) => {
|
|
2141
|
+
return __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-DrSdJbJW.mjs") }), `./translations/${locale}.json`, 3).then(({ default: data }) => {
|
|
2142
|
+
return {
|
|
2143
|
+
data: getTranslation(data),
|
|
2144
|
+
locale
|
|
2145
|
+
};
|
|
2146
|
+
}).catch(() => {
|
|
2147
|
+
return {
|
|
2148
|
+
data: {},
|
|
2149
|
+
locale
|
|
2150
|
+
};
|
|
2151
|
+
});
|
|
2152
|
+
})
|
|
2153
|
+
);
|
|
2154
|
+
return importedTranslations;
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
export {
|
|
2158
|
+
index as default
|
|
2159
|
+
};
|