@strapi/admin 4.6.0-alpha.0 → 4.6.0-beta.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 (132) hide show
  1. package/admin/src/content-manager/components/ComponentInitializer/index.js +1 -7
  2. package/admin/src/content-manager/components/{RepeatableComponent/DragPreview.js → DragLayer/ComponentDragPreview.js} +25 -12
  3. package/admin/src/content-manager/components/DragLayer/RelationDragPreview.js +75 -0
  4. package/admin/src/content-manager/components/DragLayer/index.js +23 -7
  5. package/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +130 -84
  6. package/admin/src/content-manager/components/DynamicZone/index.js +99 -24
  7. package/admin/src/content-manager/components/DynamicZone/utils/select.js +9 -5
  8. package/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +76 -14
  9. package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +23 -23
  10. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/cleanData.js +24 -5
  11. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/recursivelyFindPathsBasedOnCondition.js +8 -1
  12. package/admin/src/content-manager/components/Inputs/index.js +5 -19
  13. package/admin/src/content-manager/components/NonRepeatableComponent/index.js +4 -0
  14. package/admin/src/content-manager/components/RelationInput/RelationInput.js +203 -63
  15. package/admin/src/content-manager/components/RelationInput/components/RelationItem.js +134 -21
  16. package/admin/src/content-manager/components/RelationInput/components/RelationList.js +1 -2
  17. package/admin/src/content-manager/components/RelationInput/constants.js +1 -0
  18. package/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js +132 -10
  19. package/admin/src/content-manager/components/RepeatableComponent/components/Accordion.js +77 -0
  20. package/admin/src/content-manager/components/RepeatableComponent/components/Component.js +262 -0
  21. package/admin/src/content-manager/components/RepeatableComponent/{DraggedItem → components}/Preview.js +0 -0
  22. package/admin/src/content-manager/components/RepeatableComponent/index.js +147 -87
  23. package/admin/src/content-manager/components/RepeatableComponent/utils/getComponentErrorKeys.js +1 -1
  24. package/admin/src/content-manager/components/SingleTypeFormWrapper/index.js +1 -1
  25. package/admin/src/content-manager/components/Wysiwyg/Editor.js +1 -1
  26. package/admin/src/content-manager/hooks/index.js +2 -0
  27. package/admin/src/content-manager/hooks/useDragAndDrop.js +120 -0
  28. package/admin/src/content-manager/hooks/useKeyboardDragAndDrop.js +98 -0
  29. package/admin/src/content-manager/hooks/useLazyComponents/index.js +69 -0
  30. package/admin/src/content-manager/pages/CollectionTypeRecursivePath/components/ErrorFallback.js +13 -0
  31. package/admin/src/content-manager/pages/CollectionTypeRecursivePath/index.js +2 -1
  32. package/admin/src/content-manager/pages/EditView/GridRow/index.js +62 -0
  33. package/admin/src/content-manager/pages/EditView/index.js +74 -154
  34. package/admin/src/content-manager/pages/EditView/selectors.js +14 -0
  35. package/admin/src/content-manager/pages/EditView/utils/createAttributesLayout.js +11 -6
  36. package/admin/src/content-manager/pages/EditView/utils/getCustomFieldUidsFromLayout.js +18 -0
  37. package/admin/src/content-manager/pages/EditView/utils/index.js +1 -0
  38. package/admin/src/content-manager/pages/EditViewLayoutManager/index.js +1 -1
  39. package/admin/src/content-manager/sharedReducers/crudReducer/actions.js +5 -0
  40. package/admin/src/content-manager/sharedReducers/crudReducer/constants.js +2 -0
  41. package/admin/src/content-manager/sharedReducers/crudReducer/reducer.js +7 -0
  42. package/admin/src/content-manager/utils/ItemTypes.js +1 -1
  43. package/admin/src/content-manager/utils/composeRefs.js +28 -0
  44. package/admin/src/content-manager/utils/getMaxTempKey.js +1 -1
  45. package/admin/src/content-manager/utils/index.js +7 -0
  46. package/admin/src/core/utils/axiosInstance.js +4 -2
  47. package/admin/src/hooks/index.js +1 -0
  48. package/admin/src/hooks/useFetchClient/index.js +23 -0
  49. package/admin/src/pages/HomePage/SocialLinks.js +4 -4
  50. package/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js +23 -18
  51. package/admin/src/translations/en.json +6 -0
  52. package/admin/src/translations/ru.json +789 -489
  53. package/admin/src/utils/fetchClient.js +45 -0
  54. package/admin/src/utils/getFetchClient.js +10 -0
  55. package/build/4306.df40a798.chunk.js +98 -0
  56. package/build/{4318.daf31770.chunk.js → 4318.80bdf035.chunk.js} +2 -2
  57. package/build/5057.195a59ff.chunk.js +65 -0
  58. package/build/{805.a1894307.chunk.js → 805.e991a370.chunk.js} +6 -6
  59. package/build/{4986.3820d11d.chunk.js → 8176.b19bc128.chunk.js} +32 -32
  60. package/build/{1233.32d6888d.chunk.js → 8186.55910742.chunk.js} +94 -94
  61. package/build/{8633.8da5488a.chunk.js → 8633.59223842.chunk.js} +1 -1
  62. package/build/8881.c693411a.chunk.js +245 -0
  63. package/build/9161.4a0ab137.chunk.js +2119 -0
  64. package/build/9279.6290c87a.chunk.js +117 -0
  65. package/build/9707.a0cc4ad8.chunk.js +70 -0
  66. package/build/Admin-authenticatedApp.f9e74dc0.chunk.js +80 -0
  67. package/build/{Admin_homePage.b4db4df8.chunk.js → Admin_homePage.8945f71a.chunk.js} +5 -5
  68. package/build/{Admin_marketplace.fa51405b.chunk.js → Admin_marketplace.ed754a4a.chunk.js} +1 -1
  69. package/build/{Admin_pluginsPage.14d2840f.chunk.js → Admin_pluginsPage.3c872de7.chunk.js} +1 -1
  70. package/build/{Admin_profilePage.6c2c8398.chunk.js → Admin_profilePage.c07bdf08.chunk.js} +1 -1
  71. package/build/{Admin_settingsPage.5e740514.chunk.js → Admin_settingsPage.50a8765b.chunk.js} +5 -5
  72. package/build/admin-app.2861b6d2.chunk.js +112 -0
  73. package/build/{admin-edit-roles-page.c7c338b3.chunk.js → admin-edit-roles-page.f407538c.chunk.js} +1 -1
  74. package/build/{admin-edit-users.d254c128.chunk.js → admin-edit-users.85231e4c.chunk.js} +1 -1
  75. package/build/{admin-users.7c423e41.chunk.js → admin-users.a2707644.chunk.js} +2 -2
  76. package/build/api-tokens-create-page.dd4ddfcb.chunk.js +1 -0
  77. package/build/api-tokens-edit-page.821c5a6c.chunk.js +1 -0
  78. package/build/{api-tokens-list-page.fe994b6b.chunk.js → api-tokens-list-page.700e575f.chunk.js} +1 -1
  79. package/build/content-manager.ee948f75.chunk.js +1186 -0
  80. package/build/content-type-builder-list-view.4412efc3.chunk.js +201 -0
  81. package/build/content-type-builder.b132b5f4.chunk.js +145 -0
  82. package/build/email-settings-page.db0d98d1.chunk.js +15 -0
  83. package/build/{en-json.7dd57947.chunk.js → en-json.4a56dca7.chunk.js} +1 -1
  84. package/build/index.html +1 -1
  85. package/build/main.faac89ee.js +2025 -0
  86. package/build/ru-json.8830286f.chunk.js +1 -0
  87. package/build/{runtime~main.6e7d95b9.js → runtime~main.75a15b8e.js} +1 -1
  88. package/build/{sso-settings-page.eb30a02e.chunk.js → sso-settings-page.adb12ac3.chunk.js} +1 -1
  89. package/build/upload-settings.450cab1a.chunk.js +18 -0
  90. package/build/upload.e2034370.chunk.js +64 -0
  91. package/build/users-advanced-settings-page.0c0b8230.chunk.js +13 -0
  92. package/build/users-email-settings-page.3126ff8c.chunk.js +28 -0
  93. package/build/users-providers-settings-page.b7b602e2.chunk.js +33 -0
  94. package/build/users-roles-settings-page.ce5b582d.chunk.js +30 -0
  95. package/build/webhook-edit-page.1215a6b7.chunk.js +75 -0
  96. package/build/{webhook-list-page.42533b59.chunk.js → webhook-list-page.b87821f2.chunk.js} +1 -1
  97. package/ee/server/services/passport/provider-registry.js +1 -1
  98. package/package.json +15 -15
  99. package/utils/get-plugins-path.js +17 -3
  100. package/admin/src/content-manager/components/RepeatableComponent/AccordionGroupCustom/index.js +0 -122
  101. package/admin/src/content-manager/components/RepeatableComponent/AddFieldButton.js +0 -58
  102. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/DraggingSibling.js +0 -72
  103. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/IconButtonCustoms.js +0 -32
  104. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js +0 -322
  105. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/utils/connect.js +0 -11
  106. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/utils/index.js +0 -2
  107. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/utils/select.js +0 -30
  108. package/admin/src/content-manager/components/RepeatableComponent/utils/connect.js +0 -11
  109. package/admin/src/content-manager/components/RepeatableComponent/utils/select.js +0 -12
  110. package/admin/src/content-manager/hooks/__test__/usePrev.test.js +0 -26
  111. package/build/2438.afe24949.chunk.js +0 -2525
  112. package/build/2517.5cc235ba.chunk.js +0 -117
  113. package/build/4306.53359960.chunk.js +0 -98
  114. package/build/8881.bfdb6877.chunk.js +0 -245
  115. package/build/9707.932a3c12.chunk.js +0 -70
  116. package/build/Admin-authenticatedApp.cfc3b4c9.chunk.js +0 -80
  117. package/build/admin-app.ee1211cb.chunk.js +0 -112
  118. package/build/api-tokens-create-page.4ca2778d.chunk.js +0 -1
  119. package/build/api-tokens-edit-page.70a791c2.chunk.js +0 -1
  120. package/build/content-manager.794d3373.chunk.js +0 -1200
  121. package/build/content-type-builder-list-view.95012cf0.chunk.js +0 -201
  122. package/build/content-type-builder.95b9d6a2.chunk.js +0 -145
  123. package/build/email-settings-page.4bb3606f.chunk.js +0 -15
  124. package/build/main.a6470578.js +0 -2031
  125. package/build/ru-json.d7cfc2ff.chunk.js +0 -1
  126. package/build/upload-settings.3010911f.chunk.js +0 -18
  127. package/build/upload.9f19f2e8.chunk.js +0 -64
  128. package/build/users-advanced-settings-page.9df41d67.chunk.js +0 -13
  129. package/build/users-email-settings-page.56d82eaf.chunk.js +0 -28
  130. package/build/users-providers-settings-page.96bb7da0.chunk.js +0 -33
  131. package/build/users-roles-settings-page.445e5e16.chunk.js +0 -30
  132. package/build/webhook-edit-page.c5efc08b.chunk.js +0 -75
@@ -57,13 +57,7 @@ const ComponentInitializer = ({ error, isReadOnly, onClick }) => {
57
57
  </Box>
58
58
  {error?.id && (
59
59
  <Typography textColor="danger600" variant="pi">
60
- {formatMessage(
61
- {
62
- id: error.id,
63
- defaultMessage: error.id,
64
- },
65
- { ...error.values }
66
- )}
60
+ {formatMessage(error, { ...error.values })}
67
61
  </Typography>
68
62
  )}
69
63
  </>
@@ -2,13 +2,9 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import styled from 'styled-components';
4
4
  import { pxToRem } from '@strapi/helper-plugin';
5
- import { Box } from '@strapi/design-system/Box';
6
- import { Flex } from '@strapi/design-system/Flex';
7
- import { Typography } from '@strapi/design-system/Typography';
8
- import { IconButton } from '@strapi/design-system/IconButton';
9
- import Trash from '@strapi/icons/Trash';
10
- import DragHandle from '@strapi/icons/Drag';
11
- import CarretDown from '@strapi/icons/CarretDown';
5
+ import { Box, Flex, Typography, IconButton } from '@strapi/design-system';
6
+ import { Trash, Drag, CarretDown } from '@strapi/icons';
7
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12
8
 
13
9
  const DragPreviewBox = styled(Box)`
14
10
  border: 1px solid ${({ theme }) => theme.colors.neutral200};
@@ -31,6 +27,13 @@ const DropdownIconWrapper = styled(Box)`
31
27
  }
32
28
  `;
33
29
 
30
+ const Icon = styled(FontAwesomeIcon)`
31
+ width: ${pxToRem(12)};
32
+ height: ${pxToRem(12)};
33
+
34
+ color: ${({ theme }) => theme.colors.neutral600};
35
+ `;
36
+
34
37
  const ToggleButton = styled.button`
35
38
  border: none;
36
39
  background: transparent;
@@ -40,7 +43,7 @@ const ToggleButton = styled.button`
40
43
  padding: 0;
41
44
  `;
42
45
 
43
- const DragPreview = ({ displayedValue }) => {
46
+ const DragPreview = ({ displayedValue, icon }) => {
44
47
  return (
45
48
  <DragPreviewBox
46
49
  paddingLeft={3}
@@ -57,18 +60,23 @@ const DragPreview = ({ displayedValue }) => {
57
60
  <DropdownIconWrapper background="neutral200">
58
61
  <CarretDown />
59
62
  </DropdownIconWrapper>
60
- <Box paddingLeft={6} maxWidth={pxToRem(150)}>
63
+ <Flex gap={2} paddingLeft={icon ? 3 : 6} maxWidth={pxToRem(150)}>
64
+ {icon ? <Icon icon={icon} /> : null}
61
65
  <Typography textColor="neutral700" ellipsis>
62
66
  {displayedValue}
63
67
  </Typography>
64
- </Box>
68
+ </Flex>
65
69
  </Flex>
66
70
  </ToggleButton>
67
71
  <Box paddingLeft={3}>
68
72
  <Flex>
69
- <IconButton icon={<Trash />} />
73
+ <IconButton noBorder>
74
+ <Trash />
75
+ </IconButton>
70
76
  <Box paddingLeft={2}>
71
- <IconButton icon={<DragHandle />} />
77
+ <IconButton noBorder>
78
+ <Drag />
79
+ </IconButton>
72
80
  </Box>
73
81
  </Flex>
74
82
  </Box>
@@ -77,8 +85,13 @@ const DragPreview = ({ displayedValue }) => {
77
85
  );
78
86
  };
79
87
 
88
+ DragPreview.defaultProps = {
89
+ icon: undefined,
90
+ };
91
+
80
92
  DragPreview.propTypes = {
81
93
  displayedValue: PropTypes.string.isRequired,
94
+ icon: PropTypes.string,
82
95
  };
83
96
 
84
97
  export default DragPreview;
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import PropTypes from 'prop-types';
4
+ import { Box, Flex, IconButton, Typography, Status, Icon } from '@strapi/design-system';
5
+ import { Drag, Cross } from '@strapi/icons';
6
+
7
+ import { getTrad } from '../../utils';
8
+ import { PUBLICATION_STATES } from '../RelationInputDataManager/constants';
9
+ import { ChildrenWrapper, StackWrapper } from '../RelationInput/components/RelationItem';
10
+ import { BoxEllipsis, DisconnectButton } from '../RelationInput/RelationInput';
11
+
12
+ export const RelationDragPreview = ({ status, displayedValue, width }) => {
13
+ const { formatMessage } = useIntl();
14
+
15
+ const stateMessage = {
16
+ [PUBLICATION_STATES.DRAFT]: formatMessage({
17
+ id: getTrad('relation.publicationState.draft'),
18
+ defaultMessage: 'Draft',
19
+ }),
20
+
21
+ [PUBLICATION_STATES.PUBLISHED]: formatMessage({
22
+ id: getTrad('relation.publicationState.published'),
23
+ defaultMessage: 'Published',
24
+ }),
25
+ };
26
+
27
+ const statusColor = status === PUBLICATION_STATES.DRAFT ? 'secondary' : 'success';
28
+
29
+ return (
30
+ <Box style={{ width }}>
31
+ <Flex
32
+ paddingTop={2}
33
+ paddingBottom={2}
34
+ paddingLeft={2}
35
+ paddingRight={4}
36
+ hasRadius
37
+ borderSize={1}
38
+ background="neutral0"
39
+ borderColor="neutral200"
40
+ justifyContent="space-between"
41
+ >
42
+ <StackWrapper spacing={1} horizontal>
43
+ <IconButton noBorder>
44
+ <Drag />
45
+ </IconButton>
46
+ <ChildrenWrapper justifyContent="space-between">
47
+ <BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
48
+ <Typography textColor="primary600" ellipsis>
49
+ {displayedValue}
50
+ </Typography>
51
+ </BoxEllipsis>
52
+ {status && (
53
+ <Status variant={statusColor} showBullet={false} size="S">
54
+ <Typography fontWeight="bold" textColor={`${statusColor}700`}>
55
+ {stateMessage[status]}
56
+ </Typography>
57
+ </Status>
58
+ )}
59
+ </ChildrenWrapper>
60
+ </StackWrapper>
61
+ <Box paddingLeft={4}>
62
+ <DisconnectButton type="button">
63
+ <Icon width="12px" as={Cross} />
64
+ </DisconnectButton>
65
+ </Box>
66
+ </Flex>
67
+ </Box>
68
+ );
69
+ };
70
+
71
+ RelationDragPreview.propTypes = {
72
+ status: PropTypes.string.isRequired,
73
+ displayedValue: PropTypes.string.isRequired,
74
+ width: PropTypes.number.isRequired,
75
+ };
@@ -3,11 +3,10 @@ import { useDragLayer } from 'react-dnd';
3
3
  import LayoutDndProvider from '../LayoutDndProvider';
4
4
 
5
5
  import ItemTypes from '../../utils/ItemTypes';
6
- import RepeatableComponentPreview from '../RepeatableComponent/DragPreview';
7
6
  import CardPreview from '../../pages/ListSettingsView/components/CardPreview';
8
- // import RelationItem from '../SelectMany/Relation';
9
- // import { Li } from '../SelectMany/components';
10
- // import DraggedField from '../DraggedField';
7
+
8
+ import ComponentPreview from './ComponentDragPreview';
9
+ import { RelationDragPreview } from './RelationDragPreview';
11
10
 
12
11
  const layerStyles = {
13
12
  position: 'fixed',
@@ -50,15 +49,32 @@ const CustomDragLayer = () => {
50
49
  return null;
51
50
  }
52
51
 
52
+ /**
53
+ * Because a user may have multiple relations / dynamic zones / repeable fields in the same content type,
54
+ * we append the fieldName for the item type to make them unique, however, we then want to extract that
55
+ * first type to apply the correct preview.
56
+ */
57
+ const [actualType] = itemType.split('_');
58
+
53
59
  return (
54
60
  <LayoutDndProvider>
55
61
  <div style={layerStyles}>
56
62
  <div style={getItemStyles(initialOffset, currentOffset, mouseOffset)} className="col-md-2">
57
- {[ItemTypes.EDIT_RELATION, ItemTypes.EDIT_FIELD, ItemTypes.FIELD].includes(itemType) && (
63
+ {[ItemTypes.EDIT_FIELD, ItemTypes.FIELD].includes(itemType) && (
58
64
  <CardPreview labelField={item.labelField} />
59
65
  )}
60
- {itemType === ItemTypes.COMPONENT && (
61
- <RepeatableComponentPreview displayedValue={item.displayedValue} />
66
+ {actualType === ItemTypes.COMPONENT && (
67
+ <ComponentPreview displayedValue={item.displayedValue} />
68
+ )}
69
+ {actualType === ItemTypes.DYNAMIC_ZONE && (
70
+ <ComponentPreview icon={item.icon} displayedValue={item.displayedValue} />
71
+ )}
72
+ {actualType === ItemTypes.RELATION && (
73
+ <RelationDragPreview
74
+ displayedValue={item.displayedValue}
75
+ status={item.status}
76
+ width={item.width}
77
+ />
62
78
  )}
63
79
  </div>
64
80
  </div>
@@ -1,27 +1,39 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import styled from 'styled-components';
4
4
  import { useIntl } from 'react-intl';
5
5
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6
6
  import get from 'lodash/get';
7
-
8
- import { Accordion, AccordionToggle, AccordionContent } from '@strapi/design-system/Accordion';
9
- import { IconButton } from '@strapi/design-system/IconButton';
10
- import { Box } from '@strapi/design-system/Box';
11
- import { Flex } from '@strapi/design-system/Flex';
12
- import { Stack } from '@strapi/design-system/Stack';
13
-
7
+ import { getEmptyImage } from 'react-dnd-html5-backend';
8
+
9
+ import {
10
+ Accordion,
11
+ AccordionToggle,
12
+ AccordionContent,
13
+ IconButton,
14
+ Box,
15
+ Flex,
16
+ Stack,
17
+ } from '@strapi/design-system';
14
18
  import { useCMEditViewDataManager } from '@strapi/helper-plugin';
19
+ import { Trash, Drag } from '@strapi/icons';
15
20
 
16
- import Trash from '@strapi/icons/Trash';
17
- import ArrowDown from '@strapi/icons/ArrowDown';
18
- import ArrowUp from '@strapi/icons/ArrowUp';
19
-
20
- import { useContentTypeLayout } from '../../../hooks';
21
- import { getTrad } from '../../../utils';
21
+ import { useContentTypeLayout, useDragAndDrop } from '../../../hooks';
22
+ import { composeRefs, getTrad, ItemTypes } from '../../../utils';
22
23
 
23
24
  import FieldComponent from '../../FieldComponent';
24
25
 
26
+ const ActionsStack = styled(Stack)`
27
+ /*
28
+ we need to remove the background from the button but we can't
29
+ wrap the element in styled because it breaks the forwardedAs which
30
+ we need for drag handler to work on firefox
31
+ */
32
+ div[role='button'] {
33
+ background: transparent;
34
+ }
35
+ `;
36
+
25
37
  const IconButtonCustom = styled(IconButton)`
26
38
  background-color: transparent;
27
39
 
@@ -46,17 +58,31 @@ const Rectangle = styled(Box)`
46
58
  height: ${({ theme }) => theme.spaces[4]};
47
59
  `;
48
60
 
61
+ const Preview = styled.span`
62
+ display: block;
63
+ background-color: ${({ theme }) => theme.colors.primary100};
64
+ outline: 1px dashed ${({ theme }) => theme.colors.primary500};
65
+ outline-offset: -1px;
66
+ padding: ${({ theme }) => theme.spaces[6]};
67
+ `;
68
+
69
+ const ComponentContainer = styled(Box)`
70
+ list-style: none;
71
+ padding: 0;
72
+ margin: 0;
73
+ `;
74
+
49
75
  const DynamicZoneComponent = ({
50
76
  componentUid,
51
77
  formErrors,
52
78
  index,
53
79
  isFieldAllowed,
54
- onMoveComponentDownClick,
55
- onMoveComponentUpClick,
56
80
  name,
57
81
  onRemoveComponentClick,
58
- showDownIcon,
59
- showUpIcon,
82
+ onMoveComponent,
83
+ onGrabItem,
84
+ onDropItem,
85
+ onCancel,
60
86
  }) => {
61
87
  const [isOpen, setIsOpen] = useState(true);
62
88
  const { formatMessage } = useIntl();
@@ -103,71 +129,90 @@ const DynamicZoneComponent = ({
103
129
  setIsOpen((s) => !s);
104
130
  };
105
131
 
132
+ const [{ handlerId, isDragging, handleKeyDown }, boxRef, dropRef, dragRef, dragPreviewRef] =
133
+ useDragAndDrop(isFieldAllowed, {
134
+ type: `${ItemTypes.DYNAMIC_ZONE}_${name}`,
135
+ index,
136
+ item: {
137
+ displayedValue: `${friendlyName}${mainValue}`,
138
+ icon,
139
+ },
140
+ onMoveItem: onMoveComponent,
141
+ onGrabItem,
142
+ onDropItem,
143
+ onCancel,
144
+ });
145
+
146
+ useEffect(() => {
147
+ dragPreviewRef(getEmptyImage(), { captureDraggingState: false });
148
+ }, [dragPreviewRef, index]);
149
+
150
+ const composedBoxRefs = composeRefs(boxRef, dropRef);
151
+
152
+ const accordionActions = !isFieldAllowed ? null : (
153
+ <ActionsStack horizontal spacing={0} expanded={isOpen}>
154
+ <IconButtonCustom
155
+ noBorder
156
+ label={formatMessage(
157
+ {
158
+ id: getTrad('components.DynamicZone.delete-label'),
159
+ defaultMessage: 'Delete {name}',
160
+ },
161
+ { name: friendlyName }
162
+ )}
163
+ onClick={onRemoveComponentClick}
164
+ >
165
+ <Trash />
166
+ </IconButtonCustom>
167
+ <IconButton
168
+ forwardedAs="div"
169
+ role="button"
170
+ noBorder
171
+ tabIndex={0}
172
+ onClick={(e) => e.stopPropagation()}
173
+ data-handler-id={handlerId}
174
+ ref={dragRef}
175
+ label={formatMessage({
176
+ id: getTrad('components.DragHandle-label'),
177
+ defaultMessage: 'Drag',
178
+ })}
179
+ onKeyDown={handleKeyDown}
180
+ >
181
+ <Drag />
182
+ </IconButton>
183
+ </ActionsStack>
184
+ );
185
+
106
186
  return (
107
- <Box>
187
+ <ComponentContainer as="li">
108
188
  <Flex justifyContent="center">
109
189
  <Rectangle background="neutral200" />
110
190
  </Flex>
111
- <StyledBox hasRadius>
112
- <Accordion expanded={isOpen} onToggle={handleToggle} size="S" error={errorMessage}>
113
- <AccordionToggle
114
- startIcon={<FontAwesomeIcon icon={icon} />}
115
- action={
116
- <Stack horizontal spacing={0} expanded={isOpen}>
117
- {showDownIcon && (
118
- <IconButtonCustom
119
- noBorder
120
- label={formatMessage({
121
- id: getTrad('components.DynamicZone.move-down-label'),
122
- defaultMessage: 'Move component down',
123
- })}
124
- onClick={onMoveComponentDownClick}
125
- icon={<ArrowDown />}
126
- />
127
- )}
128
- {showUpIcon && (
129
- <IconButtonCustom
130
- noBorder
131
- label={formatMessage({
132
- id: getTrad('components.DynamicZone.move-up-label'),
133
- defaultMessage: 'Move component up',
134
- })}
135
- onClick={onMoveComponentUpClick}
136
- icon={<ArrowUp />}
137
- />
138
- )}
139
- {isFieldAllowed && (
140
- <IconButtonCustom
141
- noBorder
142
- label={formatMessage(
143
- {
144
- id: getTrad('components.DynamicZone.delete-label'),
145
- defaultMessage: 'Delete {name}',
146
- },
147
- { name: friendlyName }
148
- )}
149
- onClick={onRemoveComponentClick}
150
- icon={<Trash />}
151
- />
152
- )}
153
- </Stack>
154
- }
155
- title={`${friendlyName}${mainValue}`}
156
- togglePosition="left"
157
- />
158
- <AccordionContent>
159
- <AccordionContentRadius background="neutral0">
160
- <FieldComponent
161
- componentUid={componentUid}
162
- icon={icon}
163
- name={`${name}.${index}`}
164
- isFromDynamicZone
165
- />
166
- </AccordionContentRadius>
167
- </AccordionContent>
168
- </Accordion>
191
+ <StyledBox ref={composedBoxRefs} hasRadius>
192
+ {isDragging ? (
193
+ <Preview padding={6} background="primary100" />
194
+ ) : (
195
+ <Accordion expanded={isOpen} onToggle={handleToggle} size="S" error={errorMessage}>
196
+ <AccordionToggle
197
+ startIcon={icon && <FontAwesomeIcon icon={icon} />}
198
+ action={accordionActions}
199
+ title={`${friendlyName}${mainValue}`}
200
+ togglePosition="left"
201
+ />
202
+ <AccordionContent>
203
+ <AccordionContentRadius background="neutral0">
204
+ <FieldComponent
205
+ componentUid={componentUid}
206
+ icon={icon}
207
+ name={`${name}.${index}`}
208
+ isFromDynamicZone
209
+ />
210
+ </AccordionContentRadius>
211
+ </AccordionContent>
212
+ </Accordion>
213
+ )}
169
214
  </StyledBox>
170
- </Box>
215
+ </ComponentContainer>
171
216
  );
172
217
  };
173
218
 
@@ -175,8 +220,9 @@ DynamicZoneComponent.defaultProps = {
175
220
  formErrors: {},
176
221
  index: 0,
177
222
  isFieldAllowed: true,
178
- showDownIcon: true,
179
- showUpIcon: true,
223
+ onGrabItem: undefined,
224
+ onDropItem: undefined,
225
+ onCancel: undefined,
180
226
  };
181
227
 
182
228
  DynamicZoneComponent.propTypes = {
@@ -185,11 +231,11 @@ DynamicZoneComponent.propTypes = {
185
231
  index: PropTypes.number,
186
232
  isFieldAllowed: PropTypes.bool,
187
233
  name: PropTypes.string.isRequired,
188
- onMoveComponentDownClick: PropTypes.func.isRequired,
189
- onMoveComponentUpClick: PropTypes.func.isRequired,
234
+ onGrabItem: PropTypes.func,
235
+ onDropItem: PropTypes.func,
236
+ onCancel: PropTypes.func,
237
+ onMoveComponent: PropTypes.func.isRequired,
190
238
  onRemoveComponentClick: PropTypes.func.isRequired,
191
- showDownIcon: PropTypes.bool,
192
- showUpIcon: PropTypes.bool,
193
239
  };
194
240
 
195
241
  export default DynamicZoneComponent;
@@ -2,9 +2,9 @@ import React, { memo, useMemo, useState } from 'react';
2
2
  import get from 'lodash/get';
3
3
  import isEqual from 'react-fast-compare';
4
4
  import PropTypes from 'prop-types';
5
- import { Stack } from '@strapi/design-system/Stack';
6
- import { Box } from '@strapi/design-system/Box';
5
+ import { Box, Stack, VisuallyHidden } from '@strapi/design-system';
7
6
  import { NotAllowedInput, useNotification } from '@strapi/helper-plugin';
7
+ import { useIntl } from 'react-intl';
8
8
 
9
9
  import { getTrad } from '../../utils';
10
10
 
@@ -27,14 +27,16 @@ const DynamicZone = ({
27
27
  isFieldAllowed,
28
28
  isFieldReadable,
29
29
  labelAction,
30
- moveComponentUp,
31
- moveComponentDown,
30
+ moveComponentField,
32
31
  removeComponentFromDynamicZone,
33
32
  dynamicDisplayedComponents,
34
33
  fieldSchema,
35
34
  metadatas,
36
35
  }) => {
37
36
  const [addComponentIsOpen, setAddComponentIsOpen] = useState(false);
37
+ const [liveText, setLiveText] = useState('');
38
+
39
+ const { formatMessage } = useIntl();
38
40
 
39
41
  const toggleNotification = useNotification();
40
42
  const { getComponentLayout, components } = useContentTypeLayout();
@@ -82,12 +84,76 @@ const DynamicZone = ({
82
84
  }
83
85
  };
84
86
 
85
- const handleMoveComponentDown = (name, componentIndex) => () => {
86
- moveComponentDown(name, componentIndex);
87
+ const handleMoveComponent = (newIndex, currentIndex) => {
88
+ setLiveText(
89
+ formatMessage(
90
+ {
91
+ id: getTrad('dnd.reorder'),
92
+ defaultMessage: '{item}, moved. New position in list: {position}.',
93
+ },
94
+ {
95
+ item: `${name}.${currentIndex}`,
96
+ position: getItemPos(newIndex),
97
+ }
98
+ )
99
+ );
100
+
101
+ moveComponentField({
102
+ name,
103
+ newIndex,
104
+ currentIndex,
105
+ });
87
106
  };
88
107
 
89
- const handleMoveComponentUp = (name, componentIndex) => () => {
90
- moveComponentUp(name, componentIndex);
108
+ /**
109
+ *
110
+ * @param {number} index
111
+ * @returns {string}
112
+ */
113
+ const getItemPos = (index) => `${index + 1} of ${dynamicDisplayedComponents.length}`;
114
+
115
+ const handleCancel = (index) => {
116
+ setLiveText(
117
+ formatMessage(
118
+ {
119
+ id: getTrad('dnd.cancel-item'),
120
+ defaultMessage: '{item}, dropped. Re-order cancelled.',
121
+ },
122
+ {
123
+ item: `${name}.${index}`,
124
+ }
125
+ )
126
+ );
127
+ };
128
+
129
+ const handleGrabItem = (index) => {
130
+ setLiveText(
131
+ formatMessage(
132
+ {
133
+ id: getTrad('dnd.grab-item'),
134
+ defaultMessage: `{item}, grabbed. Current position in list: {position}. Press up and down arrow to change position, Spacebar to drop, Escape to cancel.`,
135
+ },
136
+ {
137
+ item: `${name}.${index}`,
138
+ position: getItemPos(index),
139
+ }
140
+ )
141
+ );
142
+ };
143
+
144
+ const handleDropItem = (index) => {
145
+ setLiveText(
146
+ formatMessage(
147
+ {
148
+ id: getTrad('dnd.drop-item'),
149
+ defaultMessage: `{item}, dropped. Final position in list: {position}.`,
150
+ },
151
+ {
152
+ item: `${name}.${index}`,
153
+ position: getItemPos(index),
154
+ }
155
+ )
156
+ );
91
157
  };
92
158
 
93
159
  const handleRemoveComponent = (name, currentIndex) => () => {
@@ -105,6 +171,8 @@ const DynamicZone = ({
105
171
  );
106
172
  }
107
173
 
174
+ const ariaDescriptionId = `${name}-item-instructions`;
175
+
108
176
  return (
109
177
  <Stack spacing={6}>
110
178
  {dynamicDisplayedComponentsLength > 0 && (
@@ -117,27 +185,30 @@ const DynamicZone = ({
117
185
  numberOfComponents={dynamicDisplayedComponentsLength}
118
186
  required={fieldSchema.required || false}
119
187
  />
120
- {dynamicDisplayedComponents.map((componentUid, index) => {
121
- const showDownIcon = isFieldAllowed && index < dynamicDisplayedComponentsLength - 1;
122
- const showUpIcon = isFieldAllowed && index > 0;
123
-
124
- return (
188
+ <VisuallyHidden id={ariaDescriptionId}>
189
+ {formatMessage({
190
+ id: getTrad('dnd.instructions'),
191
+ defaultMessage: `Press spacebar to grab and re-order`,
192
+ })}
193
+ </VisuallyHidden>
194
+ <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>
195
+ <ol aria-describedby={ariaDescriptionId}>
196
+ {dynamicDisplayedComponents.map(({ componentUid, id }, index) => (
125
197
  <DynamicZoneComponent
126
198
  componentUid={componentUid}
127
199
  formErrors={formErrors}
128
- // eslint-disable-next-line react/no-array-index-key
129
- key={index}
200
+ key={`${componentUid}-${id}`}
130
201
  index={index}
131
202
  isFieldAllowed={isFieldAllowed}
132
- onMoveComponentDownClick={handleMoveComponentDown(name, index)}
133
- onMoveComponentUpClick={handleMoveComponentUp(name, index)}
134
203
  name={name}
204
+ onMoveComponent={handleMoveComponent}
135
205
  onRemoveComponentClick={handleRemoveComponent(name, index)}
136
- showDownIcon={showDownIcon}
137
- showUpIcon={showUpIcon}
206
+ onCancel={handleCancel}
207
+ onDropItem={handleDropItem}
208
+ onGrabItem={handleGrabItem}
138
209
  />
139
- );
140
- })}
210
+ ))}
211
+ </ol>
141
212
  </Box>
142
213
  )}
143
214
 
@@ -172,7 +243,12 @@ DynamicZone.defaultProps = {
172
243
 
173
244
  DynamicZone.propTypes = {
174
245
  addComponentToDynamicZone: PropTypes.func.isRequired,
175
- dynamicDisplayedComponents: PropTypes.array,
246
+ dynamicDisplayedComponents: PropTypes.arrayOf(
247
+ PropTypes.shape({
248
+ componentUid: PropTypes.string.isRequired,
249
+ id: PropTypes.number.isRequired,
250
+ })
251
+ ),
176
252
  fieldSchema: PropTypes.shape({
177
253
  components: PropTypes.array.isRequired,
178
254
  max: PropTypes.number,
@@ -188,8 +264,7 @@ DynamicZone.propTypes = {
188
264
  description: PropTypes.string,
189
265
  label: PropTypes.string,
190
266
  }).isRequired,
191
- moveComponentUp: PropTypes.func.isRequired,
192
- moveComponentDown: PropTypes.func.isRequired,
267
+ moveComponentField: PropTypes.func.isRequired,
193
268
  name: PropTypes.string.isRequired,
194
269
  removeComponentFromDynamicZone: PropTypes.func.isRequired,
195
270
  };