strapi-dz-component-duplicator 0.1.2 → 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,36 +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.map(stripTransientKeys);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!isPlainObject(value)) {
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Media references must keep their id/documentId so Strapi can
|
|
95
|
-
// maintain the relation on save.
|
|
96
|
-
if (isMediaObject(value)) {
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const next = {};
|
|
101
|
-
|
|
102
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
103
|
-
if (key === 'id' || key === 'documentId' || key === '__temp_key__') {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
next[key] = stripTransientKeys(nested);
|
|
108
|
-
}
|
|
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)}`;
|
|
109
103
|
|
|
110
|
-
return
|
|
104
|
+
return () => `${String(counter++).padStart(4, '0')}-${seed}`;
|
|
111
105
|
};
|
|
112
106
|
|
|
113
107
|
const getActionAnchor = (listItem) => {
|
|
@@ -117,10 +111,6 @@ const getActionAnchor = (listItem) => {
|
|
|
117
111
|
return null;
|
|
118
112
|
}
|
|
119
113
|
|
|
120
|
-
// Dynamic zone headers include a drag handle, a delete button, and a
|
|
121
|
-
// more-actions menu. We look for the drag button by its aria-label and
|
|
122
|
-
// fall back to verifying that at least three buttons exist (the minimum
|
|
123
|
-
// for a dynamic zone header as of Strapi 5).
|
|
124
114
|
const dragButton =
|
|
125
115
|
header.querySelector('button[aria-label="Drag"]') ||
|
|
126
116
|
header.querySelector('button[aria-label="drag"]');
|
|
@@ -258,36 +248,426 @@ const createDuplicateButton = (anchor, label, onClick, signal) => {
|
|
|
258
248
|
|
|
259
249
|
const textTemplate = anchor.querySelector('span');
|
|
260
250
|
const text = document.createElement('span');
|
|
251
|
+
|
|
261
252
|
if (textTemplate?.className) {
|
|
262
253
|
text.className = textTemplate.className;
|
|
263
254
|
}
|
|
264
255
|
text.textContent = label;
|
|
265
256
|
|
|
266
257
|
button.append(icon, text);
|
|
267
|
-
button.addEventListener(
|
|
268
|
-
|
|
269
|
-
event
|
|
270
|
-
|
|
271
|
-
|
|
258
|
+
button.addEventListener(
|
|
259
|
+
'click',
|
|
260
|
+
(event) => {
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
event.stopPropagation();
|
|
263
|
+
void onClick();
|
|
264
|
+
},
|
|
265
|
+
{ signal }
|
|
266
|
+
);
|
|
272
267
|
|
|
273
268
|
return button;
|
|
274
269
|
};
|
|
275
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
|
+
|
|
276
644
|
const DynamicZoneActionInjector = () => {
|
|
277
645
|
const { formatMessage } = useIntl();
|
|
278
646
|
const { toggleNotification } = useNotification();
|
|
279
|
-
const {
|
|
647
|
+
const { get } = useFetchClient();
|
|
648
|
+
const [{ query }] = useQueryParams();
|
|
649
|
+
const { form, isLoading, components, contentTypes, model, collectionType, id } =
|
|
650
|
+
useContentManagerContext();
|
|
280
651
|
|
|
281
652
|
const isBrowser = typeof document !== 'undefined';
|
|
282
|
-
|
|
283
653
|
const values = form?.values;
|
|
284
654
|
const valuesRef = React.useRef(values);
|
|
285
655
|
const observerRef = React.useRef(null);
|
|
286
656
|
const frameRef = React.useRef(0);
|
|
287
657
|
const abortRef = React.useRef(null);
|
|
658
|
+
const relationCacheRef = React.useRef(new Map());
|
|
288
659
|
|
|
289
660
|
valuesRef.current = values;
|
|
290
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
|
+
|
|
291
671
|
const duplicateLabel = formatMessage({
|
|
292
672
|
id: 'strapi-dz-component-duplicator.action.duplicate',
|
|
293
673
|
defaultMessage: 'Duplicate component',
|
|
@@ -298,6 +678,71 @@ const DynamicZoneActionInjector = () => {
|
|
|
298
678
|
defaultMessage: 'Could not duplicate this component.',
|
|
299
679
|
});
|
|
300
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
|
+
|
|
301
746
|
const cleanupInjectedButtons = React.useCallback(() => {
|
|
302
747
|
if (abortRef.current) {
|
|
303
748
|
abortRef.current.abort();
|
|
@@ -311,7 +756,7 @@ const DynamicZoneActionInjector = () => {
|
|
|
311
756
|
}, []);
|
|
312
757
|
|
|
313
758
|
const handleDuplicate = React.useCallback(
|
|
314
|
-
(dynamicZonePath, index) => {
|
|
759
|
+
async (dynamicZonePath, index) => {
|
|
315
760
|
if (!form || typeof form.addFieldRow !== 'function') {
|
|
316
761
|
return;
|
|
317
762
|
}
|
|
@@ -323,7 +768,20 @@ const DynamicZoneActionInjector = () => {
|
|
|
323
768
|
}
|
|
324
769
|
|
|
325
770
|
try {
|
|
326
|
-
|
|
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
|
+
|
|
327
785
|
form.addFieldRow(dynamicZonePath, cloned, index + 1);
|
|
328
786
|
} catch {
|
|
329
787
|
toggleNotification({
|
|
@@ -332,7 +790,14 @@ const DynamicZoneActionInjector = () => {
|
|
|
332
790
|
});
|
|
333
791
|
}
|
|
334
792
|
},
|
|
335
|
-
[
|
|
793
|
+
[
|
|
794
|
+
components,
|
|
795
|
+
contentTypes,
|
|
796
|
+
duplicateErrorLabel,
|
|
797
|
+
fetchRelationItems,
|
|
798
|
+
form,
|
|
799
|
+
toggleNotification,
|
|
800
|
+
]
|
|
336
801
|
);
|
|
337
802
|
|
|
338
803
|
const injectButtons = React.useCallback(() => {
|
|
@@ -379,13 +844,13 @@ const DynamicZoneActionInjector = () => {
|
|
|
379
844
|
continue;
|
|
380
845
|
}
|
|
381
846
|
|
|
382
|
-
const duplicateButton = createDuplicateButton(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
anchor
|
|
847
|
+
const duplicateButton = createDuplicateButton(
|
|
848
|
+
anchor,
|
|
849
|
+
duplicateLabel,
|
|
850
|
+
() => handleDuplicate(location.dynamicZonePath, location.index),
|
|
851
|
+
controller.signal
|
|
388
852
|
);
|
|
853
|
+
anchor.parentElement.insertBefore(duplicateButton, anchor);
|
|
389
854
|
}
|
|
390
855
|
|
|
391
856
|
if (observer) {
|
|
@@ -396,6 +861,10 @@ const DynamicZoneActionInjector = () => {
|
|
|
396
861
|
}
|
|
397
862
|
}, [cleanupInjectedButtons, components, duplicateLabel, handleDuplicate, isBrowser, isLoading]);
|
|
398
863
|
|
|
864
|
+
React.useEffect(() => {
|
|
865
|
+
relationCacheRef.current.clear();
|
|
866
|
+
}, [id, model, collectionType, relationQueryParams]);
|
|
867
|
+
|
|
399
868
|
React.useEffect(() => {
|
|
400
869
|
if (!isBrowser) {
|
|
401
870
|
return undefined;
|