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 { unstable_useContentManagerContext as useContentManagerContext } from '@strapi/content-manager/strapi-admin';
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 stripTransientKeys = (value) => {
82
- if (Array.isArray(value)) {
83
- return value.map(stripTransientKeys);
84
- }
85
-
86
- if (!isPlainObject(value)) {
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
- next[key] = stripTransientKeys(nested);
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('click', (event) => {
248
- event.preventDefault();
249
- event.stopPropagation();
250
- onClick();
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 { form, isLoading, components } = useContentManagerContext();
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 (typeof document === 'undefined') {
279
- return;
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
- const cloned = stripTransientKeys(cloneValue(item));
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
- [form, toggleNotification, duplicateErrorLabel]
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 (typeof document === 'undefined') {
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(anchor, duplicateLabel, () => {
355
- handleDuplicate(location.dynamicZonePath, location.index);
356
- });
357
- anchor.parentElement.insertBefore(
358
- duplicateButton,
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
- if (typeof document === 'undefined') {
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
- injectButtons();
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.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": {