@strapi/admin 4.5.4 → 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 (90) hide show
  1. package/admin/src/components/AuthenticatedApp/index.js +2 -13
  2. package/admin/src/content-manager/components/ComponentInitializer/index.js +1 -7
  3. package/admin/src/content-manager/components/{RepeatableComponent/DragPreview.js → DragLayer/ComponentDragPreview.js} +25 -12
  4. package/admin/src/content-manager/components/DragLayer/RelationDragPreview.js +75 -0
  5. package/admin/src/content-manager/components/DragLayer/index.js +23 -7
  6. package/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +130 -84
  7. package/admin/src/content-manager/components/DynamicZone/index.js +99 -24
  8. package/admin/src/content-manager/components/DynamicZone/utils/select.js +9 -5
  9. package/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +37 -11
  10. package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +18 -22
  11. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/cleanData.js +24 -5
  12. package/admin/src/content-manager/components/RelationInput/RelationInput.js +209 -64
  13. package/admin/src/content-manager/components/RelationInput/components/RelationItem.js +134 -21
  14. package/admin/src/content-manager/components/RelationInput/components/RelationList.js +1 -2
  15. package/admin/src/content-manager/components/RelationInput/constants.js +1 -0
  16. package/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js +131 -9
  17. package/admin/src/content-manager/components/RepeatableComponent/components/Accordion.js +77 -0
  18. package/admin/src/content-manager/components/RepeatableComponent/components/Component.js +262 -0
  19. package/admin/src/content-manager/components/RepeatableComponent/{DraggedItem → components}/Preview.js +0 -0
  20. package/admin/src/content-manager/components/RepeatableComponent/index.js +147 -87
  21. package/admin/src/content-manager/components/RepeatableComponent/utils/getComponentErrorKeys.js +1 -1
  22. package/admin/src/content-manager/hooks/index.js +2 -0
  23. package/admin/src/content-manager/hooks/useDragAndDrop.js +120 -0
  24. package/admin/src/content-manager/hooks/useKeyboardDragAndDrop.js +98 -0
  25. package/admin/src/content-manager/utils/ItemTypes.js +1 -1
  26. package/admin/src/content-manager/utils/composeRefs.js +28 -0
  27. package/admin/src/content-manager/utils/getMaxTempKey.js +1 -1
  28. package/admin/src/content-manager/utils/index.js +7 -0
  29. package/admin/src/pages/App/index.js +13 -20
  30. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/index.js +3 -2
  31. package/admin/src/translations/en.json +6 -0
  32. package/admin/src/translations/tr.json +5 -485
  33. package/admin/src/utils/index.js +0 -1
  34. package/build/4318.80bdf035.chunk.js +30 -0
  35. package/build/{8176.e929d326.chunk.js → 8176.b19bc128.chunk.js} +32 -32
  36. package/build/{1233.802422fa.chunk.js → 8186.55910742.chunk.js} +96 -96
  37. package/build/{8633.43ec9042.chunk.js → 8633.59223842.chunk.js} +1 -1
  38. package/build/Admin-authenticatedApp.f9e74dc0.chunk.js +80 -0
  39. package/build/{Admin_profilePage.60ab80bb.chunk.js → Admin_profilePage.c07bdf08.chunk.js} +1 -1
  40. package/build/{Admin_settingsPage.6ef8acc9.chunk.js → Admin_settingsPage.50a8765b.chunk.js} +1 -1
  41. package/build/admin-app.2861b6d2.chunk.js +112 -0
  42. package/build/admin-edit-users.85231e4c.chunk.js +10 -0
  43. package/build/{admin-users.e64fb0f1.chunk.js → admin-users.a2707644.chunk.js} +2 -2
  44. package/build/api-tokens-create-page.dd4ddfcb.chunk.js +1 -0
  45. package/build/api-tokens-edit-page.821c5a6c.chunk.js +1 -0
  46. package/build/content-manager.ee948f75.chunk.js +1186 -0
  47. package/build/content-type-builder-translation-tr-json.2e52bc60.chunk.js +1 -0
  48. package/build/email-settings-page.db0d98d1.chunk.js +15 -0
  49. package/build/email-translation-tr-json.87f2feb3.chunk.js +1 -0
  50. package/build/{en-json.7dd57947.chunk.js → en-json.4a56dca7.chunk.js} +1 -1
  51. package/build/index.html +1 -1
  52. package/build/main.faac89ee.js +2025 -0
  53. package/build/runtime~main.75a15b8e.js +2 -0
  54. package/build/sso-settings-page.adb12ac3.chunk.js +1 -0
  55. package/build/tr-json.9c44ea0c.chunk.js +1 -0
  56. package/build/{upload.74540aab.chunk.js → upload.e2034370.chunk.js} +3 -3
  57. package/build/users-permissions-translation-tr-json.cdc49a3c.chunk.js +1 -0
  58. package/package.json +9 -9
  59. package/server/controllers/admin.js +0 -2
  60. package/server/routes/admin.js +1 -1
  61. package/server/services/metrics.js +2 -5
  62. package/admin/src/content-manager/components/RepeatableComponent/AccordionGroupCustom/index.js +0 -122
  63. package/admin/src/content-manager/components/RepeatableComponent/AddFieldButton.js +0 -58
  64. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/DraggingSibling.js +0 -72
  65. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/IconButtonCustoms.js +0 -32
  66. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js +0 -326
  67. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/utils/connect.js +0 -11
  68. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/utils/index.js +0 -2
  69. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/utils/select.js +0 -30
  70. package/admin/src/content-manager/components/RepeatableComponent/utils/connect.js +0 -11
  71. package/admin/src/content-manager/components/RepeatableComponent/utils/select.js +0 -12
  72. package/admin/src/content-manager/hooks/__test__/usePrev.test.js +0 -26
  73. package/admin/src/utils/uniqueAdminHash.js +0 -22
  74. package/build/4318.9283c350.chunk.js +0 -30
  75. package/build/Admin-authenticatedApp.0da578b8.chunk.js +0 -80
  76. package/build/admin-app.a3277e72.chunk.js +0 -112
  77. package/build/admin-edit-users.5547b126.chunk.js +0 -10
  78. package/build/api-tokens-create-page.93dd0689.chunk.js +0 -1
  79. package/build/api-tokens-edit-page.b0adac81.chunk.js +0 -1
  80. package/build/content-manager.f9630c3b.chunk.js +0 -1197
  81. package/build/content-type-builder-translation-tr-json.949e22eb.chunk.js +0 -1
  82. package/build/email-settings-page.c6e62f6b.chunk.js +0 -15
  83. package/build/email-translation-tr-json.8aa034bb.chunk.js +0 -1
  84. package/build/i18n-translation-tr-json.34ca9d61.chunk.js +0 -1
  85. package/build/main.71f24343.js +0 -2034
  86. package/build/runtime~main.1115f82b.js +0 -2
  87. package/build/sso-settings-page.feed2f45.chunk.js +0 -1
  88. package/build/tr-json.eac8bd79.chunk.js +0 -1
  89. package/build/upload-translation-tr-json.b173223a.chunk.js +0 -1
  90. package/build/users-permissions-translation-tr-json.9bebc250.chunk.js +0 -1
@@ -1,24 +1,22 @@
1
- import React, { memo, useCallback, useMemo, useState } from 'react';
2
1
  /* eslint-disable import/no-cycle */
3
- import { useDrop } from 'react-dnd';
2
+ import React, { memo, useMemo, useState } from 'react';
4
3
  import { useIntl } from 'react-intl';
5
4
  import styled from 'styled-components';
6
5
  import PropTypes from 'prop-types';
7
6
  import get from 'lodash/get';
8
- import { useNotification } from '@strapi/helper-plugin';
9
- import { Box } from '@strapi/design-system/Box';
10
- import { Flex } from '@strapi/design-system/Flex';
11
- import { TextButton } from '@strapi/design-system/TextButton';
12
- import Plus from '@strapi/icons/Plus';
7
+
8
+ import { useNotification, useCMEditViewDataManager } from '@strapi/helper-plugin';
9
+ import { Box, Flex, TextButton, VisuallyHidden } from '@strapi/design-system';
10
+ import { Plus } from '@strapi/icons';
11
+
13
12
  import { getMaxTempKey, getTrad } from '../../utils';
14
13
  import { useContentTypeLayout } from '../../hooks';
15
- import ItemTypes from '../../utils/ItemTypes';
14
+
16
15
  import ComponentInitializer from '../ComponentInitializer';
17
- import connect from './utils/connect';
18
- import select from './utils/select';
16
+ import Component from './components/Component';
17
+ import * as Accordion from './components/Accordion';
18
+
19
19
  import getComponentErrorKeys from './utils/getComponentErrorKeys';
20
- import DraggedItem from './DraggedItem';
21
- import AccordionGroupCustom from './AccordionGroupCustom';
22
20
 
23
21
  const TextButtonCustom = styled(TextButton)`
24
22
  height: 100%;
@@ -33,8 +31,6 @@ const TextButtonCustom = styled(TextButton)`
33
31
  `;
34
32
 
35
33
  const RepeatableComponent = ({
36
- addRepeatableComponentToField,
37
- formErrors,
38
34
  componentUid,
39
35
  componentValue,
40
36
  componentValueLength,
@@ -43,11 +39,12 @@ const RepeatableComponent = ({
43
39
  min,
44
40
  name,
45
41
  }) => {
42
+ const { addRepeatableComponentToField, formErrors, moveComponentField } =
43
+ useCMEditViewDataManager();
46
44
  const toggleNotification = useNotification();
47
45
  const { formatMessage } = useIntl();
48
46
  const [collapseToOpen, setCollapseToOpen] = useState('');
49
- const [isDraggingSibling, setIsDraggingSibling] = useState(false);
50
- const [, drop] = useDrop({ accept: ItemTypes.COMPONENT });
47
+ const [liveText, setLiveText] = useState('');
51
48
  const { getComponentLayout, components } = useContentTypeLayout();
52
49
  const componentLayoutData = useMemo(
53
50
  () => getComponentLayout(componentUid),
@@ -60,15 +57,15 @@ const RepeatableComponent = ({
60
57
 
61
58
  const componentErrorKeys = getComponentErrorKeys(name, formErrors);
62
59
 
63
- const toggleCollapses = () => {
64
- setCollapseToOpen('');
65
- };
66
-
67
60
  const missingComponentsValue = min - componentValueLength;
68
61
 
69
62
  const hasMinError = get(formErrors, name, { id: '' }).id.includes('min');
70
63
 
71
- const handleClick = useCallback(() => {
64
+ const toggleCollapses = () => {
65
+ setCollapseToOpen('');
66
+ };
67
+
68
+ const handleClick = () => {
72
69
  if (!isReadOnly) {
73
70
  if (componentValueLength < max) {
74
71
  const shouldCheckErrors = hasMinError;
@@ -83,18 +80,89 @@ const RepeatableComponent = ({
83
80
  });
84
81
  }
85
82
  }
86
- }, [
87
- components,
88
- addRepeatableComponentToField,
89
- componentLayoutData,
90
- componentValueLength,
91
- hasMinError,
92
- isReadOnly,
93
- max,
94
- name,
95
- nextTempKey,
96
- toggleNotification,
97
- ]);
83
+ };
84
+
85
+ const handleMoveComponentField = (newIndex, currentIndex) => {
86
+ setLiveText(
87
+ formatMessage(
88
+ {
89
+ id: getTrad('dnd.reorder'),
90
+ defaultMessage: '{item}, moved. New position in list: {position}.',
91
+ },
92
+ {
93
+ item: `${name}.${currentIndex}`,
94
+ position: getItemPos(newIndex),
95
+ }
96
+ )
97
+ );
98
+
99
+ moveComponentField({
100
+ name,
101
+ newIndex,
102
+ currentIndex,
103
+ });
104
+ };
105
+
106
+ const mainField = get(componentLayoutData, ['settings', 'mainField'], 'id');
107
+
108
+ const handleToggle = (key) => () => {
109
+ if (collapseToOpen === key) {
110
+ setCollapseToOpen('');
111
+ } else {
112
+ setCollapseToOpen(key);
113
+ }
114
+ };
115
+
116
+ /**
117
+ *
118
+ * @param {number} index
119
+ * @returns {string}
120
+ */
121
+ const getItemPos = (index) => `${index + 1} of ${componentValueLength}`;
122
+
123
+ const handleCancel = (index) => {
124
+ setLiveText(
125
+ formatMessage(
126
+ {
127
+ id: getTrad('dnd.cancel-item'),
128
+ defaultMessage: '{item}, dropped. Re-order cancelled.',
129
+ },
130
+ {
131
+ item: `${name}.${index}`,
132
+ }
133
+ )
134
+ );
135
+ };
136
+
137
+ const handleGrabItem = (index) => {
138
+ setLiveText(
139
+ formatMessage(
140
+ {
141
+ id: getTrad('dnd.grab-item'),
142
+ defaultMessage: `{item}, grabbed. Current position in list: {position}. Press up and down arrow to change position, Spacebar to drop, Escape to cancel.`,
143
+ },
144
+ {
145
+ item: `${name}.${index}`,
146
+ position: getItemPos(index),
147
+ }
148
+ )
149
+ );
150
+ };
151
+
152
+ const handleDropItem = (index) => {
153
+ setLiveText(
154
+ formatMessage(
155
+ {
156
+ id: getTrad('dnd.drop-item'),
157
+ defaultMessage: `{item}, dropped. Final position in list: {position}.`,
158
+ },
159
+ {
160
+ item: `${name}.${index}`,
161
+ position: getItemPos(index),
162
+ }
163
+ )
164
+ );
165
+ };
98
166
 
99
167
  let errorMessage = formErrors[name];
100
168
 
@@ -105,6 +173,11 @@ const RepeatableComponent = ({
105
173
  'There {number, plural, =0 {are # missing components} one {is # missing component} other {are # missing components}}',
106
174
  values: { number: missingComponentsValue },
107
175
  };
176
+ } else if (componentErrorKeys.some((error) => error.split('.').length > 1) && !hasMinError) {
177
+ errorMessage = {
178
+ id: getTrad('components.RepeatableComponent.error-message'),
179
+ defaultMessage: 'The component(s) contain error(s)',
180
+ };
108
181
  }
109
182
 
110
183
  if (componentValueLength === 0) {
@@ -113,22 +186,44 @@ const RepeatableComponent = ({
113
186
  );
114
187
  }
115
188
 
116
- const doesRepComponentHasChildError = componentErrorKeys.some(
117
- (error) => error.split('.').length > 1
118
- );
119
-
120
- if (doesRepComponentHasChildError && !hasMinError) {
121
- errorMessage = {
122
- id: getTrad('components.RepeatableComponent.error-message'),
123
- defaultMessage: 'The component(s) contain error(s)',
124
- };
125
- }
189
+ const ariaDescriptionId = `${name}-item-instructions`;
126
190
 
127
191
  return (
128
- <Box hasRadius ref={drop}>
129
- <AccordionGroupCustom
130
- error={errorMessage}
131
- footer={
192
+ <Box hasRadius>
193
+ <VisuallyHidden id={ariaDescriptionId}>
194
+ {formatMessage({
195
+ id: getTrad('dnd.instructions'),
196
+ defaultMessage: `Press spacebar to grab and re-order`,
197
+ })}
198
+ </VisuallyHidden>
199
+ <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>
200
+ <Accordion.Group error={errorMessage} ariaDescribedBy={ariaDescriptionId}>
201
+ <Accordion.Content aria-describedby={ariaDescriptionId}>
202
+ {componentValue.map((data, index) => {
203
+ const key = data.__temp_key__;
204
+ const componentFieldName = `${name}.${index}`;
205
+
206
+ return (
207
+ <Component
208
+ componentFieldName={componentFieldName}
209
+ componentUid={componentUid}
210
+ fields={componentLayoutData.layouts.edit}
211
+ key={key}
212
+ index={index}
213
+ isOpen={collapseToOpen === key}
214
+ isReadOnly={isReadOnly}
215
+ mainField={mainField}
216
+ moveComponentField={handleMoveComponentField}
217
+ onClickToggle={handleToggle(key)}
218
+ toggleCollapses={toggleCollapses}
219
+ onCancel={handleCancel}
220
+ onDropItem={handleDropItem}
221
+ onGrabItem={handleGrabItem}
222
+ />
223
+ );
224
+ })}
225
+ </Accordion.Content>
226
+ <Accordion.Footer>
132
227
  <Flex justifyContent="center" height="48px" background="neutral0">
133
228
  <TextButtonCustom disabled={isReadOnly} onClick={handleClick} startIcon={<Plus />}>
134
229
  {formatMessage({
@@ -137,39 +232,8 @@ const RepeatableComponent = ({
137
232
  })}
138
233
  </TextButtonCustom>
139
234
  </Flex>
140
- }
141
- >
142
- {componentValue.map((data, index) => {
143
- const key = data.__temp_key__;
144
- const isOpen = collapseToOpen === key;
145
- const componentFieldName = `${name}.${index}`;
146
- const hasErrors = componentErrorKeys.includes(componentFieldName);
147
-
148
- return (
149
- <DraggedItem
150
- componentFieldName={componentFieldName}
151
- componentUid={componentUid}
152
- hasErrors={hasErrors}
153
- hasMinError={hasMinError}
154
- isDraggingSibling={isDraggingSibling}
155
- isOpen={isOpen}
156
- isReadOnly={isReadOnly}
157
- key={key}
158
- onClickToggle={() => {
159
- if (isOpen) {
160
- setCollapseToOpen('');
161
- } else {
162
- setCollapseToOpen(key);
163
- }
164
- }}
165
- parentName={name}
166
- schema={componentLayoutData}
167
- setIsDraggingSibling={setIsDraggingSibling}
168
- toggleCollapses={toggleCollapses}
169
- />
170
- );
171
- })}
172
- </AccordionGroupCustom>
235
+ </Accordion.Footer>
236
+ </Accordion.Group>
173
237
  </Box>
174
238
  );
175
239
  };
@@ -177,25 +241,21 @@ const RepeatableComponent = ({
177
241
  RepeatableComponent.defaultProps = {
178
242
  componentValue: null,
179
243
  componentValueLength: 0,
180
- formErrors: {},
244
+ isReadOnly: false,
181
245
  max: Infinity,
182
246
  min: 0,
183
247
  };
184
248
 
185
249
  RepeatableComponent.propTypes = {
186
- addRepeatableComponentToField: PropTypes.func.isRequired,
187
250
  componentUid: PropTypes.string.isRequired,
188
251
  componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
189
252
  componentValueLength: PropTypes.number,
190
- formErrors: PropTypes.object,
191
- isReadOnly: PropTypes.bool.isRequired,
253
+ isReadOnly: PropTypes.bool,
192
254
  max: PropTypes.number,
193
255
  min: PropTypes.number,
194
256
  name: PropTypes.string.isRequired,
195
257
  };
196
258
 
197
- const Memoized = memo(RepeatableComponent);
198
-
199
- export default connect(Memoized, select);
259
+ export default memo(RepeatableComponent);
200
260
 
201
261
  export { RepeatableComponent };
@@ -1,4 +1,4 @@
1
- export default function getComponentErrorKeys(name, formErrors) {
1
+ export default function getComponentErrorKeys(name, formErrors = {}) {
2
2
  return Object.keys(formErrors)
3
3
  .filter((errorKey) => errorKey.startsWith(name))
4
4
  .map((errorKey) =>
@@ -6,3 +6,5 @@ export { default as usePluginsQueryParams } from './usePluginsQueryParams';
6
6
  export { default as useSyncRbac } from './useSyncRbac';
7
7
  export { default as useWysiwyg } from './useWysiwyg';
8
8
  export { usePrev } from './usePrev';
9
+ export { useDragAndDrop } from './useDragAndDrop';
10
+ export { useKeyboardDragAndDrop } from './useKeyboardDragAndDrop';
@@ -0,0 +1,120 @@
1
+ import { useRef } from 'react';
2
+ import { useDrag, useDrop } from 'react-dnd';
3
+
4
+ import { useKeyboardDragAndDrop } from './useKeyboardDragAndDrop';
5
+
6
+ /**
7
+ * @typedef UseDragAndDropOptions
8
+ *
9
+ * @type {{
10
+ * type?: string,
11
+ * index: number,
12
+ * item?: object,
13
+ * onStart?: () => void,
14
+ * onEnd?: () => void,
15
+ * } & import('./useKeyboardDragAndDrop').UseKeyboardDragAndDropCallbacks}
16
+ */
17
+
18
+ /**
19
+ * @typedef UseDragAndDropReturn
20
+ *
21
+ * @type {[props: {handlerId: import('dnd-core').Identifier, isDragging: boolean, handleKeyDown: (event: import('react').KeyboardEvent<HTMLButtonElement>) => void}, objectRef: React.RefObject<HTMLElement>, dropRef: import('react-dnd').ConnectDropTarget, dragRef: import('react-dnd').ConnectDragSource, dragPreviewRef: import('react-dnd').ConnectDragPreview]}
22
+ */
23
+
24
+ /**
25
+ * A utility hook abstracting the general drag and drop hooks from react-dnd.
26
+ * Centralising the same behaviours and by default offering keyboard support.
27
+ *
28
+ * @type {(active: boolean, options: UseDragAndDropOptions) => UseDragAndDropReturn}
29
+ */
30
+ export const useDragAndDrop = (
31
+ active,
32
+ {
33
+ type = 'STRAPI_DND',
34
+ index,
35
+ item = {},
36
+ onStart,
37
+ onEnd,
38
+ onGrabItem,
39
+ onDropItem,
40
+ onCancel,
41
+ onMoveItem,
42
+ }
43
+ ) => {
44
+ const objectRef = useRef(null);
45
+
46
+ const [{ handlerId }, dropRef] = useDrop({
47
+ accept: type,
48
+ collect(monitor) {
49
+ return {
50
+ handlerId: monitor.getHandlerId(),
51
+ };
52
+ },
53
+ hover(item, monitor) {
54
+ if (!objectRef.current) {
55
+ return;
56
+ }
57
+ const dragIndex = item.index;
58
+ const newInd = index;
59
+
60
+ // Don't replace items with themselves
61
+ if (dragIndex === newInd) {
62
+ return;
63
+ }
64
+
65
+ const hoverBoundingRect = objectRef.current.getBoundingClientRect();
66
+ const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
67
+ const clientOffset = monitor.getClientOffset();
68
+ const hoverClientY = clientOffset.y - hoverBoundingRect.top;
69
+
70
+ // Dragging downwards
71
+ if (dragIndex < newInd && hoverClientY < hoverMiddleY) {
72
+ return;
73
+ }
74
+
75
+ // Dragging upwards
76
+ if (dragIndex > newInd && hoverClientY > hoverMiddleY) {
77
+ return;
78
+ }
79
+
80
+ // Time to actually perform the action
81
+ onMoveItem(newInd, dragIndex);
82
+ item.index = newInd;
83
+ },
84
+ });
85
+
86
+ const [{ isDragging }, dragRef, dragPreviewRef] = useDrag({
87
+ type,
88
+ item() {
89
+ if (onStart) {
90
+ onStart();
91
+ }
92
+
93
+ /**
94
+ * This will be attached and it helps define the preview sizes
95
+ * when a component is flexy e.g. Relations
96
+ */
97
+ const { width } = objectRef.current?.getBoundingClientRect() ?? {};
98
+
99
+ return { index, width, ...item };
100
+ },
101
+ end() {
102
+ if (onEnd) {
103
+ onEnd();
104
+ }
105
+ },
106
+ canDrag: active,
107
+ collect: (monitor) => ({
108
+ isDragging: monitor.isDragging(),
109
+ }),
110
+ });
111
+
112
+ const handleKeyDown = useKeyboardDragAndDrop(index, {
113
+ onGrabItem,
114
+ onDropItem,
115
+ onCancel,
116
+ onMoveItem,
117
+ });
118
+
119
+ return [{ handlerId, isDragging, handleKeyDown }, objectRef, dropRef, dragRef, dragPreviewRef];
120
+ };
@@ -0,0 +1,98 @@
1
+ import { useState } from 'react';
2
+
3
+ /**
4
+ * @typedef UseKeyboardDragAndDropCallbacks
5
+ *
6
+ * @type {{
7
+ * onCancel?: (index: number) => void,
8
+ * onDropItem?: (index: number) => void,
9
+ * onGrabItem?: (index: number) => void,
10
+ * onMoveItem: (newIndex: number, currentIndex: number) => void,
11
+ * }}
12
+ */
13
+
14
+ /**
15
+ * Utility hook designed to implement keyboard accessibile drag and drop by
16
+ * returning an onKeyDown handler to be passed to the drag icon button.
17
+ *
18
+ * @internal - You should use `useDragAndDrop` instead.
19
+ *
20
+ * @type {(index: number, callbacks: UseKeyboardDragAndDropCallbacks) => (event: React.KeyboardEvent<HTMLButtonElement>) => void}
21
+ */
22
+ export const useKeyboardDragAndDrop = (index, { onCancel, onDropItem, onGrabItem, onMoveItem }) => {
23
+ const [isSelected, setIsSelected] = useState(false);
24
+ /**
25
+ * @type {(movement: 'UP' | 'DOWN') => void})}
26
+ */
27
+ const handleMove = (movement) => {
28
+ if (!isSelected) {
29
+ return;
30
+ }
31
+
32
+ if (movement === 'UP') {
33
+ onMoveItem(index - 1, index);
34
+ } else if (movement === 'DOWN') {
35
+ onMoveItem(index + 1, index);
36
+ }
37
+ };
38
+
39
+ const handleDragClick = () => {
40
+ if (isSelected) {
41
+ if (onDropItem) {
42
+ onDropItem(index);
43
+ }
44
+ setIsSelected(false);
45
+ } else {
46
+ if (onGrabItem) {
47
+ onGrabItem(index);
48
+ }
49
+ setIsSelected(true);
50
+ }
51
+ };
52
+
53
+ const handleCancel = () => {
54
+ if (isSelected) {
55
+ setIsSelected(false);
56
+
57
+ if (onCancel) {
58
+ onCancel(index);
59
+ }
60
+ }
61
+ };
62
+
63
+ /**
64
+ * @type {React.KeyboardEventHandler<HTMLButtonElement>}
65
+ */
66
+ const handleKeyDown = (e) => {
67
+ if (e.key === 'Tab' && !isSelected) {
68
+ return;
69
+ }
70
+
71
+ e.preventDefault();
72
+
73
+ switch (e.key) {
74
+ case ' ':
75
+ case 'Enter':
76
+ handleDragClick();
77
+ break;
78
+
79
+ case 'Escape':
80
+ handleCancel();
81
+ break;
82
+
83
+ case 'ArrowDown':
84
+ case 'ArrowRight':
85
+ handleMove('DOWN');
86
+ break;
87
+
88
+ case 'ArrowUp':
89
+ case 'ArrowLeft':
90
+ handleMove('UP');
91
+ break;
92
+
93
+ default:
94
+ }
95
+ };
96
+
97
+ return handleKeyDown;
98
+ };
@@ -1,7 +1,7 @@
1
1
  export default {
2
2
  COMPONENT: 'component',
3
3
  EDIT_FIELD: 'editField',
4
- EDIT_RELATION: 'editRelation',
5
4
  FIELD: 'field',
5
+ DYNAMIC_ZONE: 'dynamicZone',
6
6
  RELATION: 'relation',
7
7
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @typedef PossibleRef<T>
3
+ * @type {React.Ref<T> | undefined;}
4
+ *
5
+ * @typedef setRef
6
+ * @type {<T>(ref: PossibleRef<T>, value: T) => React.RefCallback<T>}
7
+ */
8
+
9
+ /**
10
+ * @type {setRef}
11
+ */
12
+ const setRef = (ref, value) => {
13
+ if (typeof ref === 'function') {
14
+ ref(value);
15
+ } else if (ref !== null && ref !== undefined) {
16
+ ref.current = value;
17
+ }
18
+ };
19
+
20
+ /**
21
+ * A utility to compose multiple refs together
22
+ * Accepts callback refs and RefObject(s)
23
+ *
24
+ * @type {<T>(...refs: PossibleRef<T>[]) => (node: T) => void}
25
+ */
26
+ export const composeRefs = (...refs) => {
27
+ return (node) => refs.forEach((ref) => setRef(ref, node));
28
+ };
@@ -5,7 +5,7 @@ const getMaxTempKey = (arr) => {
5
5
 
6
6
  return Math.max.apply(
7
7
  Math,
8
- arr.map((o) => o.__temp_key__)
8
+ arr.map((o) => o.__temp_key__ ?? o.id)
9
9
  );
10
10
  };
11
11
 
@@ -1,13 +1,20 @@
1
1
  export { default as arrayMoveItem } from './arrayMoveItem';
2
+
2
3
  export { default as checkIfAttributeIsDisplayable } from './checkIfAttributeIsDisplayable';
4
+ export { composeRefs } from './composeRefs';
3
5
  export { default as createDefaultForm } from './createDefaultForm';
6
+
4
7
  export { default as formatLayoutToApi } from './formatLayoutToApi';
8
+
5
9
  export { default as generatePermissionsObject } from './generatePermissionsObject';
6
10
  export { default as getFieldName } from './getFieldName';
7
11
  export { default as getMaxTempKey } from './getMaxTempKey';
8
12
  export { default as getRequestUrl } from './getRequestUrl';
9
13
  export { default as getTrad } from './getTrad';
14
+
10
15
  export { default as ItemTypes } from './ItemTypes';
16
+
11
17
  export { default as mergeMetasWithSchema } from './mergeMetasWithSchema';
18
+
12
19
  export { default as removeKeyInObject } from './removeKeyInObject';
13
20
  export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';