strapi-dz-component-duplicator 0.1.1 → 0.1.3
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.
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { useNotification } from '@strapi/admin/strapi-admin';
|
|
3
|
-
import {
|
|
2
|
+
import { useFetchClient, useNotification, useQueryParams } from '@strapi/admin/strapi-admin';
|
|
3
|
+
import {
|
|
4
|
+
buildValidParams,
|
|
5
|
+
unstable_useContentManagerContext as useContentManagerContext,
|
|
6
|
+
} from '@strapi/content-manager/strapi-admin';
|
|
4
7
|
import { useIntl } from 'react-intl';
|
|
5
8
|
|
|
6
9
|
const DUPLICATE_CONTAINER_ATTR = 'data-dz-component-duplicator-action';
|
|
7
10
|
const INDEX_SEGMENT_REGEX = /^\d+$/;
|
|
11
|
+
const COLLECTION_TYPES = 'collection-types';
|
|
12
|
+
const SINGLE_TYPES = 'single-types';
|
|
13
|
+
const ONE_WAY_RELATIONS = new Set([
|
|
14
|
+
'oneWay',
|
|
15
|
+
'oneToOne',
|
|
16
|
+
'manyToOne',
|
|
17
|
+
'oneToManyMorph',
|
|
18
|
+
'oneToOneMorph',
|
|
19
|
+
]);
|
|
8
20
|
const DUPLICATE_ICON_PATH =
|
|
9
21
|
'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';
|
|
10
22
|
|
|
@@ -44,6 +56,10 @@ const isDynamicZoneItem = (value) => {
|
|
|
44
56
|
return isPlainObject(value) && typeof value.__component === 'string';
|
|
45
57
|
};
|
|
46
58
|
|
|
59
|
+
const isMediaObject = (value) => {
|
|
60
|
+
return isPlainObject(value) && typeof value.mime === 'string';
|
|
61
|
+
};
|
|
62
|
+
|
|
47
63
|
const collectDynamicZonePaths = (value, currentPath = '', acc = []) => {
|
|
48
64
|
if (Array.isArray(value)) {
|
|
49
65
|
if (currentPath && value.length > 0 && value.every(isDynamicZoneItem)) {
|
|
@@ -78,26 +94,14 @@ const cloneValue = (value) => {
|
|
|
78
94
|
return JSON.parse(JSON.stringify(value));
|
|
79
95
|
};
|
|
80
96
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const next = {};
|
|
91
|
-
|
|
92
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
93
|
-
if (key === 'id' || key === 'documentId' || key === '__temp_key__') {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
97
|
+
const createTempKeyFactory = () => {
|
|
98
|
+
let counter = 0;
|
|
99
|
+
const seed =
|
|
100
|
+
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
101
|
+
? crypto.randomUUID()
|
|
102
|
+
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return next;
|
|
104
|
+
return () => `${String(counter++).padStart(4, '0')}-${seed}`;
|
|
101
105
|
};
|
|
102
106
|
|
|
103
107
|
const getActionAnchor = (listItem) => {
|
|
@@ -107,10 +111,16 @@ const getActionAnchor = (listItem) => {
|
|
|
107
111
|
return null;
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
const dragButton =
|
|
115
|
+
header.querySelector('button[aria-label="Drag"]') ||
|
|
116
|
+
header.querySelector('button[aria-label="drag"]');
|
|
117
|
+
|
|
118
|
+
if (dragButton) {
|
|
119
|
+
return dragButton;
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
const buttons = header.querySelectorAll('button');
|
|
111
123
|
|
|
112
|
-
// Dynamic zone headers include delete, drag and more-actions controls.
|
|
113
|
-
// Repeatable components usually don't include the extra menu action.
|
|
114
124
|
if (buttons.length < 3) {
|
|
115
125
|
return null;
|
|
116
126
|
}
|
|
@@ -214,7 +224,7 @@ const findDynamicZoneLocation = (listItem, values, components) => {
|
|
|
214
224
|
return findDynamicZoneLocationFromList(listItem, values, components);
|
|
215
225
|
};
|
|
216
226
|
|
|
217
|
-
const createDuplicateButton = (anchor, label, onClick) => {
|
|
227
|
+
const createDuplicateButton = (anchor, label, onClick, signal) => {
|
|
218
228
|
const button = document.createElement('button');
|
|
219
229
|
button.type = 'button';
|
|
220
230
|
button.className = anchor.className;
|
|
@@ -238,32 +248,426 @@ const createDuplicateButton = (anchor, label, onClick) => {
|
|
|
238
248
|
|
|
239
249
|
const textTemplate = anchor.querySelector('span');
|
|
240
250
|
const text = document.createElement('span');
|
|
251
|
+
|
|
241
252
|
if (textTemplate?.className) {
|
|
242
253
|
text.className = textTemplate.className;
|
|
243
254
|
}
|
|
244
255
|
text.textContent = label;
|
|
245
256
|
|
|
246
257
|
button.append(icon, text);
|
|
247
|
-
button.addEventListener(
|
|
248
|
-
|
|
249
|
-
event
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
button.addEventListener(
|
|
259
|
+
'click',
|
|
260
|
+
(event) => {
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
event.stopPropagation();
|
|
263
|
+
void onClick();
|
|
264
|
+
},
|
|
265
|
+
{ signal }
|
|
266
|
+
);
|
|
252
267
|
|
|
253
268
|
return button;
|
|
254
269
|
};
|
|
255
270
|
|
|
271
|
+
const getRelationIdentity = (relation) => {
|
|
272
|
+
if (!isPlainObject(relation)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const documentId = relation.documentId ?? relation.apiData?.documentId;
|
|
277
|
+
const locale = relation.locale ?? relation.apiData?.locale ?? '';
|
|
278
|
+
const id = relation.id ?? relation.apiData?.id;
|
|
279
|
+
|
|
280
|
+
if (documentId) {
|
|
281
|
+
return `${documentId}::${locale}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (id !== null && id !== undefined) {
|
|
285
|
+
return `id:${id}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return null;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const getRelationDisplayValue = (relation) => {
|
|
292
|
+
const candidateKeys = [
|
|
293
|
+
'label',
|
|
294
|
+
'title',
|
|
295
|
+
'name',
|
|
296
|
+
'displayName',
|
|
297
|
+
'question',
|
|
298
|
+
'heading',
|
|
299
|
+
'slug',
|
|
300
|
+
'documentId',
|
|
301
|
+
'id',
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
for (const key of candidateKeys) {
|
|
305
|
+
const value = relation?.[key];
|
|
306
|
+
|
|
307
|
+
if (typeof value === 'string' && value.trim()) {
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (typeof value === 'number') {
|
|
312
|
+
return String(value);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return '';
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const getRelationCollectionType = (targetModel, contentTypes) => {
|
|
320
|
+
const targetSchema = Array.isArray(contentTypes)
|
|
321
|
+
? contentTypes.find((schema) => schema?.uid === targetModel)
|
|
322
|
+
: null;
|
|
323
|
+
|
|
324
|
+
return targetSchema?.kind === 'singleType' ? SINGLE_TYPES : COLLECTION_TYPES;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const getRelationHref = (targetModel, contentTypes, documentId, locale) => {
|
|
328
|
+
if (!targetModel || !documentId) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const collectionType = getRelationCollectionType(targetModel, contentTypes);
|
|
333
|
+
const basePath =
|
|
334
|
+
collectionType === SINGLE_TYPES
|
|
335
|
+
? `../${SINGLE_TYPES}/${targetModel}`
|
|
336
|
+
: `../${COLLECTION_TYPES}/${targetModel}/${documentId}`;
|
|
337
|
+
|
|
338
|
+
return locale ? `${basePath}?plugins[i18n][locale]=${locale}` : basePath;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const normalizeFetchedRelations = (value) => {
|
|
342
|
+
if (Array.isArray(value)) {
|
|
343
|
+
return value.filter(isPlainObject);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (isPlainObject(value) && Array.isArray(value.results)) {
|
|
347
|
+
return value.results.filter(isPlainObject);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (isPlainObject(value)) {
|
|
351
|
+
return [value];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return [];
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const toRelationConnectEntry = (relation, attribute, contentTypes, createTempKey) => {
|
|
358
|
+
if (!isPlainObject(relation)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const id = relation.id ?? relation.apiData?.id;
|
|
363
|
+
const documentId = relation.documentId ?? relation.apiData?.documentId;
|
|
364
|
+
const locale = relation.locale ?? relation.apiData?.locale ?? null;
|
|
365
|
+
const label = relation.label ?? getRelationDisplayValue(relation);
|
|
366
|
+
const href = relation.href ?? getRelationHref(attribute.target, contentTypes, documentId, locale);
|
|
367
|
+
const next = {
|
|
368
|
+
id,
|
|
369
|
+
documentId,
|
|
370
|
+
locale,
|
|
371
|
+
href,
|
|
372
|
+
label: label || documentId || (id !== undefined && id !== null ? String(id) : ''),
|
|
373
|
+
__temp_key__: createTempKey(),
|
|
374
|
+
apiData: {
|
|
375
|
+
id,
|
|
376
|
+
documentId: documentId ?? '',
|
|
377
|
+
locale,
|
|
378
|
+
isTemporary: relation.apiData?.isTemporary ?? true,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
if (relation.status !== undefined) {
|
|
383
|
+
next.status = relation.status;
|
|
384
|
+
} else if (typeof relation.publishedAt === 'string') {
|
|
385
|
+
next.status = 'published';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return next;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const sanitizeRelationValue = async ({
|
|
392
|
+
attribute,
|
|
393
|
+
value,
|
|
394
|
+
sourceContext,
|
|
395
|
+
fieldName,
|
|
396
|
+
contentTypes,
|
|
397
|
+
createTempKey,
|
|
398
|
+
fetchRelationItems,
|
|
399
|
+
}) => {
|
|
400
|
+
const currentConnect = Array.isArray(value?.connect) ? value.connect : [];
|
|
401
|
+
const currentDisconnect = Array.isArray(value?.disconnect) ? value.disconnect : [];
|
|
402
|
+
const fetchedRelations =
|
|
403
|
+
sourceContext?.id && sourceContext?.model
|
|
404
|
+
? await fetchRelationItems(sourceContext.model, sourceContext.id, fieldName)
|
|
405
|
+
: [];
|
|
406
|
+
|
|
407
|
+
const connectedByIdentity = new Map();
|
|
408
|
+
|
|
409
|
+
for (const relation of currentConnect) {
|
|
410
|
+
const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey);
|
|
411
|
+
const identity = getRelationIdentity(entry);
|
|
412
|
+
|
|
413
|
+
if (entry && identity) {
|
|
414
|
+
connectedByIdentity.set(identity, entry);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const disconnectedIdentities = new Set(
|
|
419
|
+
currentDisconnect.map(getRelationIdentity).filter(Boolean)
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const effectiveRelations = [];
|
|
423
|
+
|
|
424
|
+
for (const relation of fetchedRelations) {
|
|
425
|
+
const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey);
|
|
426
|
+
const identity = getRelationIdentity(entry);
|
|
427
|
+
|
|
428
|
+
if (!entry || !identity || disconnectedIdentities.has(identity)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (connectedByIdentity.has(identity)) {
|
|
433
|
+
effectiveRelations.push(connectedByIdentity.get(identity));
|
|
434
|
+
connectedByIdentity.delete(identity);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
effectiveRelations.push(entry);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const relation of connectedByIdentity.values()) {
|
|
442
|
+
effectiveRelations.push(relation);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const connect = ONE_WAY_RELATIONS.has(attribute.relation)
|
|
446
|
+
? effectiveRelations.slice(-1)
|
|
447
|
+
: effectiveRelations;
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
connect,
|
|
451
|
+
disconnect: [],
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const sanitizeComponentValue = async ({
|
|
456
|
+
value,
|
|
457
|
+
componentUid,
|
|
458
|
+
components,
|
|
459
|
+
contentTypes,
|
|
460
|
+
createTempKey,
|
|
461
|
+
fetchRelationItems,
|
|
462
|
+
sourceContext,
|
|
463
|
+
}) => {
|
|
464
|
+
if (!isPlainObject(value)) {
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const attributes = components?.[componentUid]?.attributes ?? {};
|
|
469
|
+
const next = {};
|
|
470
|
+
|
|
471
|
+
for (const [fieldName, attribute] of Object.entries(attributes)) {
|
|
472
|
+
if (!(fieldName in value)) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
next[fieldName] = await sanitizeAttributeValue({
|
|
477
|
+
attribute,
|
|
478
|
+
fieldName,
|
|
479
|
+
value: value[fieldName],
|
|
480
|
+
components,
|
|
481
|
+
contentTypes,
|
|
482
|
+
createTempKey,
|
|
483
|
+
fetchRelationItems,
|
|
484
|
+
sourceContext: {
|
|
485
|
+
model: componentUid,
|
|
486
|
+
id: sourceContext?.id ?? value?.id,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return next;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const sanitizeDynamicZoneValue = async ({
|
|
495
|
+
value,
|
|
496
|
+
components,
|
|
497
|
+
contentTypes,
|
|
498
|
+
createTempKey,
|
|
499
|
+
fetchRelationItems,
|
|
500
|
+
}) => {
|
|
501
|
+
if (!isDynamicZoneItem(value)) {
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const componentUid = value.__component;
|
|
506
|
+
const next = await sanitizeComponentValue({
|
|
507
|
+
value,
|
|
508
|
+
componentUid,
|
|
509
|
+
components,
|
|
510
|
+
contentTypes,
|
|
511
|
+
createTempKey,
|
|
512
|
+
fetchRelationItems,
|
|
513
|
+
sourceContext: {
|
|
514
|
+
model: componentUid,
|
|
515
|
+
id: value?.id,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
__component: componentUid,
|
|
521
|
+
...next,
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const sanitizeAttributeValue = async ({
|
|
526
|
+
attribute,
|
|
527
|
+
fieldName,
|
|
528
|
+
value,
|
|
529
|
+
components,
|
|
530
|
+
contentTypes,
|
|
531
|
+
createTempKey,
|
|
532
|
+
fetchRelationItems,
|
|
533
|
+
sourceContext,
|
|
534
|
+
}) => {
|
|
535
|
+
if (value === null || value === undefined) {
|
|
536
|
+
return value;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (attribute.type === 'component') {
|
|
540
|
+
if (attribute.repeatable) {
|
|
541
|
+
if (!Array.isArray(value)) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const next = await Promise.all(
|
|
546
|
+
value.map((item) =>
|
|
547
|
+
sanitizeComponentValue({
|
|
548
|
+
value: item,
|
|
549
|
+
componentUid: attribute.component,
|
|
550
|
+
components,
|
|
551
|
+
contentTypes,
|
|
552
|
+
createTempKey,
|
|
553
|
+
fetchRelationItems,
|
|
554
|
+
sourceContext: {
|
|
555
|
+
model: attribute.component,
|
|
556
|
+
id: item?.id,
|
|
557
|
+
},
|
|
558
|
+
})
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
return next.map((item) =>
|
|
563
|
+
isPlainObject(item)
|
|
564
|
+
? {
|
|
565
|
+
...item,
|
|
566
|
+
__temp_key__: createTempKey(),
|
|
567
|
+
}
|
|
568
|
+
: item
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return sanitizeComponentValue({
|
|
573
|
+
value,
|
|
574
|
+
componentUid: attribute.component,
|
|
575
|
+
components,
|
|
576
|
+
contentTypes,
|
|
577
|
+
createTempKey,
|
|
578
|
+
fetchRelationItems,
|
|
579
|
+
sourceContext: {
|
|
580
|
+
model: attribute.component,
|
|
581
|
+
id: value?.id,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (attribute.type === 'dynamiczone') {
|
|
587
|
+
if (!Array.isArray(value)) {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const next = await Promise.all(
|
|
592
|
+
value.map((item) =>
|
|
593
|
+
sanitizeDynamicZoneValue({
|
|
594
|
+
value: item,
|
|
595
|
+
components,
|
|
596
|
+
contentTypes,
|
|
597
|
+
createTempKey,
|
|
598
|
+
fetchRelationItems,
|
|
599
|
+
})
|
|
600
|
+
)
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
return next.map((item) =>
|
|
604
|
+
isPlainObject(item)
|
|
605
|
+
? {
|
|
606
|
+
...item,
|
|
607
|
+
__temp_key__: createTempKey(),
|
|
608
|
+
}
|
|
609
|
+
: item
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (attribute.type === 'relation') {
|
|
614
|
+
return sanitizeRelationValue({
|
|
615
|
+
attribute,
|
|
616
|
+
value,
|
|
617
|
+
sourceContext,
|
|
618
|
+
fieldName,
|
|
619
|
+
contentTypes,
|
|
620
|
+
createTempKey,
|
|
621
|
+
fetchRelationItems,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (attribute.type === 'media') {
|
|
626
|
+
return cloneValue(value);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (Array.isArray(value)) {
|
|
630
|
+
return value.map((item) => cloneValue(item));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (isPlainObject(value)) {
|
|
634
|
+
if (isMediaObject(value)) {
|
|
635
|
+
return cloneValue(value);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return cloneValue(value);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return value;
|
|
642
|
+
};
|
|
643
|
+
|
|
256
644
|
const DynamicZoneActionInjector = () => {
|
|
257
645
|
const { formatMessage } = useIntl();
|
|
258
646
|
const { toggleNotification } = useNotification();
|
|
259
|
-
const {
|
|
647
|
+
const { get } = useFetchClient();
|
|
648
|
+
const [{ query }] = useQueryParams();
|
|
649
|
+
const { form, isLoading, components, contentTypes, model, collectionType, id } =
|
|
650
|
+
useContentManagerContext();
|
|
260
651
|
|
|
652
|
+
const isBrowser = typeof document !== 'undefined';
|
|
261
653
|
const values = form?.values;
|
|
262
654
|
const valuesRef = React.useRef(values);
|
|
263
655
|
const observerRef = React.useRef(null);
|
|
656
|
+
const frameRef = React.useRef(0);
|
|
657
|
+
const abortRef = React.useRef(null);
|
|
658
|
+
const relationCacheRef = React.useRef(new Map());
|
|
264
659
|
|
|
265
660
|
valuesRef.current = values;
|
|
266
661
|
|
|
662
|
+
const relationQueryParams = React.useMemo(() => {
|
|
663
|
+
const params = buildValidParams(query ?? {});
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
locale: params?.locale,
|
|
667
|
+
status: params?.status,
|
|
668
|
+
};
|
|
669
|
+
}, [query]);
|
|
670
|
+
|
|
267
671
|
const duplicateLabel = formatMessage({
|
|
268
672
|
id: 'strapi-dz-component-duplicator.action.duplicate',
|
|
269
673
|
defaultMessage: 'Duplicate component',
|
|
@@ -274,9 +678,75 @@ const DynamicZoneActionInjector = () => {
|
|
|
274
678
|
defaultMessage: 'Could not duplicate this component.',
|
|
275
679
|
});
|
|
276
680
|
|
|
681
|
+
const fetchRelationItems = React.useCallback(
|
|
682
|
+
async (relationModel, relationId, fieldName) => {
|
|
683
|
+
if (!relationModel || !relationId || !fieldName) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const cacheKey = JSON.stringify({
|
|
688
|
+
relationModel,
|
|
689
|
+
relationId,
|
|
690
|
+
fieldName,
|
|
691
|
+
relationQueryParams,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const cachedPromise = relationCacheRef.current.get(cacheKey);
|
|
695
|
+
|
|
696
|
+
if (cachedPromise) {
|
|
697
|
+
return cachedPromise;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const request = (async () => {
|
|
701
|
+
let page = 1;
|
|
702
|
+
let totalPages = 1;
|
|
703
|
+
const relations = [];
|
|
704
|
+
|
|
705
|
+
while (page <= totalPages) {
|
|
706
|
+
const response = await get(
|
|
707
|
+
`/content-manager/relations/${relationModel}/${relationId}/${fieldName}`,
|
|
708
|
+
{
|
|
709
|
+
params: {
|
|
710
|
+
...relationQueryParams,
|
|
711
|
+
page,
|
|
712
|
+
pageSize: 100,
|
|
713
|
+
},
|
|
714
|
+
}
|
|
715
|
+
);
|
|
716
|
+
const payload = response?.data ?? {};
|
|
717
|
+
const pageResults = normalizeFetchedRelations(payload.results).reverse();
|
|
718
|
+
|
|
719
|
+
relations.push(...pageResults);
|
|
720
|
+
|
|
721
|
+
const pagination = payload?.pagination;
|
|
722
|
+
const pageCount =
|
|
723
|
+
typeof pagination?.pageCount === 'number'
|
|
724
|
+
? pagination.pageCount
|
|
725
|
+
: Math.max(1, Math.ceil((pagination?.total ?? pageResults.length) / 100));
|
|
726
|
+
|
|
727
|
+
totalPages = pageCount;
|
|
728
|
+
page += 1;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return relations;
|
|
732
|
+
})();
|
|
733
|
+
|
|
734
|
+
relationCacheRef.current.set(cacheKey, request);
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
return await request;
|
|
738
|
+
} catch (error) {
|
|
739
|
+
relationCacheRef.current.delete(cacheKey);
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
[get, relationQueryParams]
|
|
744
|
+
);
|
|
745
|
+
|
|
277
746
|
const cleanupInjectedButtons = React.useCallback(() => {
|
|
278
|
-
if (
|
|
279
|
-
|
|
747
|
+
if (abortRef.current) {
|
|
748
|
+
abortRef.current.abort();
|
|
749
|
+
abortRef.current = null;
|
|
280
750
|
}
|
|
281
751
|
|
|
282
752
|
const injectedNodes = document.querySelectorAll(`[${DUPLICATE_CONTAINER_ATTR}]`);
|
|
@@ -286,7 +756,7 @@ const DynamicZoneActionInjector = () => {
|
|
|
286
756
|
}, []);
|
|
287
757
|
|
|
288
758
|
const handleDuplicate = React.useCallback(
|
|
289
|
-
(dynamicZonePath, index) => {
|
|
759
|
+
async (dynamicZonePath, index) => {
|
|
290
760
|
if (!form || typeof form.addFieldRow !== 'function') {
|
|
291
761
|
return;
|
|
292
762
|
}
|
|
@@ -298,7 +768,20 @@ const DynamicZoneActionInjector = () => {
|
|
|
298
768
|
}
|
|
299
769
|
|
|
300
770
|
try {
|
|
301
|
-
|
|
771
|
+
relationCacheRef.current.clear();
|
|
772
|
+
const createTempKey = createTempKeyFactory();
|
|
773
|
+
const cloned = await sanitizeDynamicZoneValue({
|
|
774
|
+
value: item,
|
|
775
|
+
components,
|
|
776
|
+
contentTypes,
|
|
777
|
+
createTempKey,
|
|
778
|
+
fetchRelationItems,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
if (!isDynamicZoneItem(cloned)) {
|
|
782
|
+
throw new Error('Invalid dynamic zone clone');
|
|
783
|
+
}
|
|
784
|
+
|
|
302
785
|
form.addFieldRow(dynamicZonePath, cloned, index + 1);
|
|
303
786
|
} catch {
|
|
304
787
|
toggleNotification({
|
|
@@ -307,11 +790,18 @@ const DynamicZoneActionInjector = () => {
|
|
|
307
790
|
});
|
|
308
791
|
}
|
|
309
792
|
},
|
|
310
|
-
[
|
|
793
|
+
[
|
|
794
|
+
components,
|
|
795
|
+
contentTypes,
|
|
796
|
+
duplicateErrorLabel,
|
|
797
|
+
fetchRelationItems,
|
|
798
|
+
form,
|
|
799
|
+
toggleNotification,
|
|
800
|
+
]
|
|
311
801
|
);
|
|
312
802
|
|
|
313
803
|
const injectButtons = React.useCallback(() => {
|
|
314
|
-
if (
|
|
804
|
+
if (!isBrowser) {
|
|
315
805
|
return;
|
|
316
806
|
}
|
|
317
807
|
|
|
@@ -336,6 +826,9 @@ const DynamicZoneActionInjector = () => {
|
|
|
336
826
|
return;
|
|
337
827
|
}
|
|
338
828
|
|
|
829
|
+
const controller = new AbortController();
|
|
830
|
+
abortRef.current = controller;
|
|
831
|
+
|
|
339
832
|
const listItems = document.querySelectorAll('ol > li');
|
|
340
833
|
|
|
341
834
|
for (const listItem of listItems) {
|
|
@@ -351,13 +844,13 @@ const DynamicZoneActionInjector = () => {
|
|
|
351
844
|
continue;
|
|
352
845
|
}
|
|
353
846
|
|
|
354
|
-
const duplicateButton = createDuplicateButton(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
anchor
|
|
847
|
+
const duplicateButton = createDuplicateButton(
|
|
848
|
+
anchor,
|
|
849
|
+
duplicateLabel,
|
|
850
|
+
() => handleDuplicate(location.dynamicZonePath, location.index),
|
|
851
|
+
controller.signal
|
|
360
852
|
);
|
|
853
|
+
anchor.parentElement.insertBefore(duplicateButton, anchor);
|
|
361
854
|
}
|
|
362
855
|
|
|
363
856
|
if (observer) {
|
|
@@ -366,15 +859,20 @@ const DynamicZoneActionInjector = () => {
|
|
|
366
859
|
subtree: true,
|
|
367
860
|
});
|
|
368
861
|
}
|
|
369
|
-
}, [cleanupInjectedButtons, duplicateLabel, handleDuplicate, isLoading]);
|
|
862
|
+
}, [cleanupInjectedButtons, components, duplicateLabel, handleDuplicate, isBrowser, isLoading]);
|
|
370
863
|
|
|
371
864
|
React.useEffect(() => {
|
|
372
|
-
|
|
865
|
+
relationCacheRef.current.clear();
|
|
866
|
+
}, [id, model, collectionType, relationQueryParams]);
|
|
867
|
+
|
|
868
|
+
React.useEffect(() => {
|
|
869
|
+
if (!isBrowser) {
|
|
373
870
|
return undefined;
|
|
374
871
|
}
|
|
375
872
|
|
|
376
873
|
const observer = new MutationObserver(() => {
|
|
377
|
-
|
|
874
|
+
cancelAnimationFrame(frameRef.current);
|
|
875
|
+
frameRef.current = requestAnimationFrame(() => injectButtons());
|
|
378
876
|
});
|
|
379
877
|
|
|
380
878
|
observerRef.current = observer;
|
|
@@ -387,15 +885,12 @@ const DynamicZoneActionInjector = () => {
|
|
|
387
885
|
injectButtons();
|
|
388
886
|
|
|
389
887
|
return () => {
|
|
888
|
+
cancelAnimationFrame(frameRef.current);
|
|
390
889
|
observer.disconnect();
|
|
391
890
|
observerRef.current = null;
|
|
392
891
|
cleanupInjectedButtons();
|
|
393
892
|
};
|
|
394
|
-
}, [cleanupInjectedButtons, injectButtons]);
|
|
395
|
-
|
|
396
|
-
React.useEffect(() => {
|
|
397
|
-
injectButtons();
|
|
398
|
-
}, [injectButtons, values]);
|
|
893
|
+
}, [cleanupInjectedButtons, isBrowser, injectButtons]);
|
|
399
894
|
|
|
400
895
|
return null;
|
|
401
896
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-dz-component-duplicator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Duplicate dynamic zone components directly from Strapi 5 edit view.",
|
|
5
5
|
"author": "Djordje Savanovic",
|
|
6
6
|
"keywords": [
|
|
@@ -47,10 +47,6 @@
|
|
|
47
47
|
"@strapi/strapi": "^5.0.0",
|
|
48
48
|
"react": "^18.0.0",
|
|
49
49
|
"react-dom": "^18.0.0",
|
|
50
|
-
"react-router-dom": "^6.0.0",
|
|
51
|
-
"styled-components": "^6.0.0"
|
|
52
|
-
},
|
|
53
|
-
"dependencies": {
|
|
54
50
|
"react-intl": "^7.0.0"
|
|
55
51
|
},
|
|
56
52
|
"strapi": {
|