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