strapi-dz-component-duplicator 0.1.2 → 0.1.4
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,424 @@ 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
|
+
locale,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (relation.status !== undefined) {
|
|
381
|
+
next.status = relation.status;
|
|
382
|
+
} else if (typeof relation.publishedAt === 'string') {
|
|
383
|
+
next.status = 'published';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return next;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const sanitizeRelationValue = async ({
|
|
390
|
+
attribute,
|
|
391
|
+
value,
|
|
392
|
+
sourceContext,
|
|
393
|
+
fieldName,
|
|
394
|
+
contentTypes,
|
|
395
|
+
createTempKey,
|
|
396
|
+
fetchRelationItems,
|
|
397
|
+
}) => {
|
|
398
|
+
const currentConnect = Array.isArray(value?.connect) ? value.connect : [];
|
|
399
|
+
const currentDisconnect = Array.isArray(value?.disconnect) ? value.disconnect : [];
|
|
400
|
+
const fetchedRelations =
|
|
401
|
+
sourceContext?.id && sourceContext?.model
|
|
402
|
+
? await fetchRelationItems(sourceContext.model, sourceContext.id, fieldName)
|
|
403
|
+
: [];
|
|
404
|
+
|
|
405
|
+
const connectedByIdentity = new Map();
|
|
406
|
+
|
|
407
|
+
for (const relation of currentConnect) {
|
|
408
|
+
const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey);
|
|
409
|
+
const identity = getRelationIdentity(entry);
|
|
410
|
+
|
|
411
|
+
if (entry && identity) {
|
|
412
|
+
connectedByIdentity.set(identity, entry);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const disconnectedIdentities = new Set(
|
|
417
|
+
currentDisconnect.map(getRelationIdentity).filter(Boolean)
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
const effectiveRelations = [];
|
|
421
|
+
|
|
422
|
+
for (const relation of fetchedRelations) {
|
|
423
|
+
const entry = toRelationConnectEntry(relation, attribute, contentTypes, createTempKey);
|
|
424
|
+
const identity = getRelationIdentity(entry);
|
|
425
|
+
|
|
426
|
+
if (!entry || !identity || disconnectedIdentities.has(identity)) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (connectedByIdentity.has(identity)) {
|
|
431
|
+
effectiveRelations.push(connectedByIdentity.get(identity));
|
|
432
|
+
connectedByIdentity.delete(identity);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
effectiveRelations.push(entry);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const relation of connectedByIdentity.values()) {
|
|
440
|
+
effectiveRelations.push(relation);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const connect = ONE_WAY_RELATIONS.has(attribute.relation)
|
|
444
|
+
? effectiveRelations.slice(-1)
|
|
445
|
+
: effectiveRelations;
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
connect,
|
|
449
|
+
disconnect: [],
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const sanitizeComponentValue = async ({
|
|
454
|
+
value,
|
|
455
|
+
componentUid,
|
|
456
|
+
components,
|
|
457
|
+
contentTypes,
|
|
458
|
+
createTempKey,
|
|
459
|
+
fetchRelationItems,
|
|
460
|
+
sourceContext,
|
|
461
|
+
}) => {
|
|
462
|
+
if (!isPlainObject(value)) {
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const attributes = components?.[componentUid]?.attributes ?? {};
|
|
467
|
+
const next = {};
|
|
468
|
+
|
|
469
|
+
for (const [fieldName, attribute] of Object.entries(attributes)) {
|
|
470
|
+
if (fieldName === 'id' || !(fieldName in value)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
next[fieldName] = await sanitizeAttributeValue({
|
|
475
|
+
attribute,
|
|
476
|
+
fieldName,
|
|
477
|
+
value: value[fieldName],
|
|
478
|
+
components,
|
|
479
|
+
contentTypes,
|
|
480
|
+
createTempKey,
|
|
481
|
+
fetchRelationItems,
|
|
482
|
+
sourceContext: {
|
|
483
|
+
model: componentUid,
|
|
484
|
+
id: sourceContext?.id ?? value?.id,
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return next;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const sanitizeDynamicZoneValue = async ({
|
|
493
|
+
value,
|
|
494
|
+
components,
|
|
495
|
+
contentTypes,
|
|
496
|
+
createTempKey,
|
|
497
|
+
fetchRelationItems,
|
|
498
|
+
}) => {
|
|
499
|
+
if (!isDynamicZoneItem(value)) {
|
|
500
|
+
return value;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const componentUid = value.__component;
|
|
504
|
+
const next = await sanitizeComponentValue({
|
|
505
|
+
value,
|
|
506
|
+
componentUid,
|
|
507
|
+
components,
|
|
508
|
+
contentTypes,
|
|
509
|
+
createTempKey,
|
|
510
|
+
fetchRelationItems,
|
|
511
|
+
sourceContext: {
|
|
512
|
+
model: componentUid,
|
|
513
|
+
id: value?.id,
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
__component: componentUid,
|
|
519
|
+
...next,
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const sanitizeAttributeValue = async ({
|
|
524
|
+
attribute,
|
|
525
|
+
fieldName,
|
|
526
|
+
value,
|
|
527
|
+
components,
|
|
528
|
+
contentTypes,
|
|
529
|
+
createTempKey,
|
|
530
|
+
fetchRelationItems,
|
|
531
|
+
sourceContext,
|
|
532
|
+
}) => {
|
|
533
|
+
if (value === null || value === undefined) {
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (attribute.type === 'component') {
|
|
538
|
+
if (attribute.repeatable) {
|
|
539
|
+
if (!Array.isArray(value)) {
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const next = await Promise.all(
|
|
544
|
+
value.map((item) =>
|
|
545
|
+
sanitizeComponentValue({
|
|
546
|
+
value: item,
|
|
547
|
+
componentUid: attribute.component,
|
|
548
|
+
components,
|
|
549
|
+
contentTypes,
|
|
550
|
+
createTempKey,
|
|
551
|
+
fetchRelationItems,
|
|
552
|
+
sourceContext: {
|
|
553
|
+
model: attribute.component,
|
|
554
|
+
id: item?.id,
|
|
555
|
+
},
|
|
556
|
+
})
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
return next.map((item) =>
|
|
561
|
+
isPlainObject(item)
|
|
562
|
+
? {
|
|
563
|
+
...item,
|
|
564
|
+
__temp_key__: createTempKey(),
|
|
565
|
+
}
|
|
566
|
+
: item
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return sanitizeComponentValue({
|
|
571
|
+
value,
|
|
572
|
+
componentUid: attribute.component,
|
|
573
|
+
components,
|
|
574
|
+
contentTypes,
|
|
575
|
+
createTempKey,
|
|
576
|
+
fetchRelationItems,
|
|
577
|
+
sourceContext: {
|
|
578
|
+
model: attribute.component,
|
|
579
|
+
id: value?.id,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (attribute.type === 'dynamiczone') {
|
|
585
|
+
if (!Array.isArray(value)) {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const next = await Promise.all(
|
|
590
|
+
value.map((item) =>
|
|
591
|
+
sanitizeDynamicZoneValue({
|
|
592
|
+
value: item,
|
|
593
|
+
components,
|
|
594
|
+
contentTypes,
|
|
595
|
+
createTempKey,
|
|
596
|
+
fetchRelationItems,
|
|
597
|
+
})
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
return next.map((item) =>
|
|
602
|
+
isPlainObject(item)
|
|
603
|
+
? {
|
|
604
|
+
...item,
|
|
605
|
+
__temp_key__: createTempKey(),
|
|
606
|
+
}
|
|
607
|
+
: item
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (attribute.type === 'relation') {
|
|
612
|
+
return sanitizeRelationValue({
|
|
613
|
+
attribute,
|
|
614
|
+
value,
|
|
615
|
+
sourceContext,
|
|
616
|
+
fieldName,
|
|
617
|
+
contentTypes,
|
|
618
|
+
createTempKey,
|
|
619
|
+
fetchRelationItems,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (attribute.type === 'media') {
|
|
624
|
+
return cloneValue(value);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (Array.isArray(value)) {
|
|
628
|
+
return value.map((item) => cloneValue(item));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (isPlainObject(value)) {
|
|
632
|
+
if (isMediaObject(value)) {
|
|
633
|
+
return cloneValue(value);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return cloneValue(value);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return value;
|
|
640
|
+
};
|
|
641
|
+
|
|
276
642
|
const DynamicZoneActionInjector = () => {
|
|
277
643
|
const { formatMessage } = useIntl();
|
|
278
644
|
const { toggleNotification } = useNotification();
|
|
279
|
-
const {
|
|
645
|
+
const { get } = useFetchClient();
|
|
646
|
+
const [{ query }] = useQueryParams();
|
|
647
|
+
const { form, isLoading, components, contentTypes, model, collectionType, id } =
|
|
648
|
+
useContentManagerContext();
|
|
280
649
|
|
|
281
650
|
const isBrowser = typeof document !== 'undefined';
|
|
282
|
-
|
|
283
651
|
const values = form?.values;
|
|
284
652
|
const valuesRef = React.useRef(values);
|
|
285
653
|
const observerRef = React.useRef(null);
|
|
286
654
|
const frameRef = React.useRef(0);
|
|
287
655
|
const abortRef = React.useRef(null);
|
|
656
|
+
const relationCacheRef = React.useRef(new Map());
|
|
288
657
|
|
|
289
658
|
valuesRef.current = values;
|
|
290
659
|
|
|
660
|
+
const relationQueryParams = React.useMemo(() => {
|
|
661
|
+
const params = buildValidParams(query ?? {});
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
locale: params?.locale,
|
|
665
|
+
status: params?.status,
|
|
666
|
+
};
|
|
667
|
+
}, [query]);
|
|
668
|
+
|
|
291
669
|
const duplicateLabel = formatMessage({
|
|
292
670
|
id: 'strapi-dz-component-duplicator.action.duplicate',
|
|
293
671
|
defaultMessage: 'Duplicate component',
|
|
@@ -298,6 +676,71 @@ const DynamicZoneActionInjector = () => {
|
|
|
298
676
|
defaultMessage: 'Could not duplicate this component.',
|
|
299
677
|
});
|
|
300
678
|
|
|
679
|
+
const fetchRelationItems = React.useCallback(
|
|
680
|
+
async (relationModel, relationId, fieldName) => {
|
|
681
|
+
if (!relationModel || !relationId || !fieldName) {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const cacheKey = JSON.stringify({
|
|
686
|
+
relationModel,
|
|
687
|
+
relationId,
|
|
688
|
+
fieldName,
|
|
689
|
+
relationQueryParams,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const cachedPromise = relationCacheRef.current.get(cacheKey);
|
|
693
|
+
|
|
694
|
+
if (cachedPromise) {
|
|
695
|
+
return cachedPromise;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const request = (async () => {
|
|
699
|
+
let page = 1;
|
|
700
|
+
let totalPages = 1;
|
|
701
|
+
const relations = [];
|
|
702
|
+
|
|
703
|
+
while (page <= totalPages) {
|
|
704
|
+
const response = await get(
|
|
705
|
+
`/content-manager/relations/${relationModel}/${relationId}/${fieldName}`,
|
|
706
|
+
{
|
|
707
|
+
params: {
|
|
708
|
+
...relationQueryParams,
|
|
709
|
+
page,
|
|
710
|
+
pageSize: 100,
|
|
711
|
+
},
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
const payload = response?.data ?? {};
|
|
715
|
+
const pageResults = normalizeFetchedRelations(payload.results).reverse();
|
|
716
|
+
|
|
717
|
+
relations.push(...pageResults);
|
|
718
|
+
|
|
719
|
+
const pagination = payload?.pagination;
|
|
720
|
+
const pageCount =
|
|
721
|
+
typeof pagination?.pageCount === 'number'
|
|
722
|
+
? pagination.pageCount
|
|
723
|
+
: Math.max(1, Math.ceil((pagination?.total ?? pageResults.length) / 100));
|
|
724
|
+
|
|
725
|
+
totalPages = pageCount;
|
|
726
|
+
page += 1;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return relations;
|
|
730
|
+
})();
|
|
731
|
+
|
|
732
|
+
relationCacheRef.current.set(cacheKey, request);
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
return await request;
|
|
736
|
+
} catch (error) {
|
|
737
|
+
relationCacheRef.current.delete(cacheKey);
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
[get, relationQueryParams]
|
|
742
|
+
);
|
|
743
|
+
|
|
301
744
|
const cleanupInjectedButtons = React.useCallback(() => {
|
|
302
745
|
if (abortRef.current) {
|
|
303
746
|
abortRef.current.abort();
|
|
@@ -311,7 +754,7 @@ const DynamicZoneActionInjector = () => {
|
|
|
311
754
|
}, []);
|
|
312
755
|
|
|
313
756
|
const handleDuplicate = React.useCallback(
|
|
314
|
-
(dynamicZonePath, index) => {
|
|
757
|
+
async (dynamicZonePath, index) => {
|
|
315
758
|
if (!form || typeof form.addFieldRow !== 'function') {
|
|
316
759
|
return;
|
|
317
760
|
}
|
|
@@ -323,7 +766,20 @@ const DynamicZoneActionInjector = () => {
|
|
|
323
766
|
}
|
|
324
767
|
|
|
325
768
|
try {
|
|
326
|
-
|
|
769
|
+
relationCacheRef.current.clear();
|
|
770
|
+
const createTempKey = createTempKeyFactory();
|
|
771
|
+
const cloned = await sanitizeDynamicZoneValue({
|
|
772
|
+
value: item,
|
|
773
|
+
components,
|
|
774
|
+
contentTypes,
|
|
775
|
+
createTempKey,
|
|
776
|
+
fetchRelationItems,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
if (!isDynamicZoneItem(cloned)) {
|
|
780
|
+
throw new Error('Invalid dynamic zone clone');
|
|
781
|
+
}
|
|
782
|
+
|
|
327
783
|
form.addFieldRow(dynamicZonePath, cloned, index + 1);
|
|
328
784
|
} catch {
|
|
329
785
|
toggleNotification({
|
|
@@ -332,7 +788,14 @@ const DynamicZoneActionInjector = () => {
|
|
|
332
788
|
});
|
|
333
789
|
}
|
|
334
790
|
},
|
|
335
|
-
[
|
|
791
|
+
[
|
|
792
|
+
components,
|
|
793
|
+
contentTypes,
|
|
794
|
+
duplicateErrorLabel,
|
|
795
|
+
fetchRelationItems,
|
|
796
|
+
form,
|
|
797
|
+
toggleNotification,
|
|
798
|
+
]
|
|
336
799
|
);
|
|
337
800
|
|
|
338
801
|
const injectButtons = React.useCallback(() => {
|
|
@@ -379,13 +842,13 @@ const DynamicZoneActionInjector = () => {
|
|
|
379
842
|
continue;
|
|
380
843
|
}
|
|
381
844
|
|
|
382
|
-
const duplicateButton = createDuplicateButton(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
anchor
|
|
845
|
+
const duplicateButton = createDuplicateButton(
|
|
846
|
+
anchor,
|
|
847
|
+
duplicateLabel,
|
|
848
|
+
() => handleDuplicate(location.dynamicZonePath, location.index),
|
|
849
|
+
controller.signal
|
|
388
850
|
);
|
|
851
|
+
anchor.parentElement.insertBefore(duplicateButton, anchor);
|
|
389
852
|
}
|
|
390
853
|
|
|
391
854
|
if (observer) {
|
|
@@ -396,6 +859,10 @@ const DynamicZoneActionInjector = () => {
|
|
|
396
859
|
}
|
|
397
860
|
}, [cleanupInjectedButtons, components, duplicateLabel, handleDuplicate, isBrowser, isLoading]);
|
|
398
861
|
|
|
862
|
+
React.useEffect(() => {
|
|
863
|
+
relationCacheRef.current.clear();
|
|
864
|
+
}, [id, model, collectionType, relationQueryParams]);
|
|
865
|
+
|
|
399
866
|
React.useEffect(() => {
|
|
400
867
|
if (!isBrowser) {
|
|
401
868
|
return undefined;
|