@strapi/admin 4.4.3 → 4.5.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/CollectionTypeFormWrapper/index.js +35 -1
  2. package/admin/src/content-manager/components/DynamicTable/CellContent/RelationMultiple/index.js +11 -7
  3. package/admin/src/content-manager/components/DynamicTable/CellContent/index.js +6 -5
  4. package/admin/src/content-manager/components/DynamicTable/TableRows/index.js +5 -0
  5. package/admin/src/content-manager/components/DynamicTable/index.js +1 -1
  6. package/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +73 -20
  7. package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +166 -26
  8. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/cleanData.js +17 -1
  9. package/admin/src/content-manager/components/Inputs/index.js +12 -4
  10. package/admin/src/content-manager/components/NonRepeatableComponent/index.js +2 -0
  11. package/admin/src/content-manager/components/RelationInput/RelationInput.js +427 -0
  12. package/admin/src/content-manager/components/{SelectWrapper → RelationInput/components}/Option.js +15 -25
  13. package/admin/src/content-manager/components/RelationInput/components/Relation.js +48 -0
  14. package/admin/src/content-manager/components/RelationInput/components/RelationItem.js +52 -0
  15. package/admin/src/content-manager/components/RelationInput/components/RelationList.js +52 -0
  16. package/admin/src/content-manager/components/RelationInput/constants.js +1 -0
  17. package/admin/src/content-manager/components/RelationInput/index.js +1 -0
  18. package/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js +261 -0
  19. package/admin/src/content-manager/components/RelationInputDataManager/constants.js +8 -0
  20. package/admin/src/content-manager/components/RelationInputDataManager/index.js +1 -0
  21. package/admin/src/content-manager/components/{SelectWrapper → RelationInputDataManager}/utils/connect.js +0 -1
  22. package/admin/src/content-manager/components/RelationInputDataManager/utils/getRelationLink.js +5 -0
  23. package/admin/src/content-manager/components/RelationInputDataManager/utils/index.js +4 -0
  24. package/admin/src/content-manager/components/RelationInputDataManager/utils/normalizeRelations.js +65 -0
  25. package/admin/src/content-manager/components/RelationInputDataManager/utils/normalizeSearchResults.js +12 -0
  26. package/admin/src/content-manager/components/RelationInputDataManager/utils/select.js +98 -0
  27. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js +5 -0
  28. package/admin/src/content-manager/components/SingleTypeFormWrapper/index.js +42 -2
  29. package/admin/src/content-manager/hooks/useFetchContentTypeLayout/utils/formatLayouts.js +7 -69
  30. package/admin/src/content-manager/hooks/useRelation/index.js +1 -0
  31. package/admin/src/content-manager/hooks/useRelation/useRelation.js +81 -0
  32. package/admin/src/content-manager/pages/EditSettingsView/components/DisplayedFields.js +4 -4
  33. package/admin/src/content-manager/pages/EditSettingsView/index.js +21 -49
  34. package/admin/src/content-manager/pages/EditSettingsView/reducer.js +0 -25
  35. package/admin/src/content-manager/pages/EditView/Header/index.js +121 -140
  36. package/admin/src/content-manager/pages/EditView/Header/utils/index.js +0 -1
  37. package/admin/src/content-manager/pages/EditView/Header/utils/select.js +4 -2
  38. package/admin/src/content-manager/pages/EditView/index.js +12 -65
  39. package/admin/src/content-manager/pages/ListView/FieldPicker/index.js +0 -1
  40. package/admin/src/content-manager/pages/ListView/index.js +0 -1
  41. package/admin/src/content-manager/pages/ListViewLayoutManager/index.js +0 -1
  42. package/admin/src/content-manager/utils/formatLayoutToApi.js +1 -3
  43. package/admin/src/pages/HomePage/SocialLinks.js +1 -1
  44. package/admin/src/translations/ar.json +0 -1
  45. package/admin/src/translations/ca.json +1 -8
  46. package/admin/src/translations/cs.json +0 -2
  47. package/admin/src/translations/de.json +4 -12
  48. package/admin/src/translations/dk.json +3 -11
  49. package/admin/src/translations/en.json +4 -11
  50. package/admin/src/translations/es.json +156 -168
  51. package/admin/src/translations/fr.json +3 -11
  52. package/admin/src/translations/gu.json +608 -617
  53. package/admin/src/translations/hi.json +689 -698
  54. package/admin/src/translations/hu.json +2 -10
  55. package/admin/src/translations/id.json +2 -10
  56. package/admin/src/translations/it.json +2 -10
  57. package/admin/src/translations/ja.json +2 -11
  58. package/admin/src/translations/ko.json +2 -11
  59. package/admin/src/translations/ml.json +689 -698
  60. package/admin/src/translations/ms.json +0 -2
  61. package/admin/src/translations/nl.json +3 -11
  62. package/admin/src/translations/pl.json +2 -11
  63. package/admin/src/translations/pt-BR.json +3 -11
  64. package/admin/src/translations/pt.json +0 -1
  65. package/admin/src/translations/ru.json +489 -501
  66. package/admin/src/translations/sa.json +85 -93
  67. package/admin/src/translations/sk.json +3 -10
  68. package/admin/src/translations/sv.json +3 -9
  69. package/admin/src/translations/th.json +0 -2
  70. package/admin/src/translations/tr.json +0 -1
  71. package/admin/src/translations/uk.json +0 -2
  72. package/admin/src/translations/vi.json +0 -1
  73. package/admin/src/translations/zh-Hans.json +4 -13
  74. package/admin/src/translations/zh.json +3 -11
  75. package/build/{8773.51992277.chunk.js → 1939.e3c87653.chunk.js} +50 -50
  76. package/build/8738.a30a2160.chunk.js +461 -0
  77. package/build/962.8651ba3f.chunk.js +184 -0
  78. package/build/Admin-authenticatedApp.883449a5.chunk.js +80 -0
  79. package/build/{Admin_homePage.6d5e3236.chunk.js → Admin_homePage.4b2be829.chunk.js} +1 -1
  80. package/build/{ar-json.d4cb26d9.chunk.js → ar-json.3489463d.chunk.js} +1 -1
  81. package/build/{ca-json.d16c1d28.chunk.js → ca-json.82df6eab.chunk.js} +1 -1
  82. package/build/content-manager.933dc286.chunk.js +1201 -0
  83. package/build/{cs-json.c8f28ba8.chunk.js → cs-json.ce49da5c.chunk.js} +1 -1
  84. package/build/{de-json.a9b514dc.chunk.js → de-json.0ad554eb.chunk.js} +1 -1
  85. package/build/{dk-json.09e8d145.chunk.js → dk-json.e195ea1a.chunk.js} +1 -1
  86. package/build/{en-json.e936d40e.chunk.js → en-json.1889403c.chunk.js} +1 -1
  87. package/build/{es-json.3a9c7c09.chunk.js → es-json.09f80f6e.chunk.js} +1 -1
  88. package/build/{fr-json.4ed1fc2c.chunk.js → fr-json.606d056b.chunk.js} +1 -1
  89. package/build/{gu-json.d8311297.chunk.js → gu-json.9881264f.chunk.js} +1 -1
  90. package/build/{hi-json.0edb8d29.chunk.js → hi-json.83dcf48f.chunk.js} +1 -1
  91. package/build/{hu-json.7855529a.chunk.js → hu-json.6f328bce.chunk.js} +1 -1
  92. package/build/{id-json.df9618f2.chunk.js → id-json.1f3c4303.chunk.js} +1 -1
  93. package/build/index.html +1 -1
  94. package/build/{it-json.a21bf078.chunk.js → it-json.494ac432.chunk.js} +1 -1
  95. package/build/{ja-json.7b0d9067.chunk.js → ja-json.6f262117.chunk.js} +1 -1
  96. package/build/{ko-json.983c1f8f.chunk.js → ko-json.36dc3b9a.chunk.js} +1 -1
  97. package/build/main.63e7ea0a.js +9338 -0
  98. package/build/{ml-json.8dd021c8.chunk.js → ml-json.9566bf9a.chunk.js} +1 -1
  99. package/build/{ms-json.836ed013.chunk.js → ms-json.ed51e902.chunk.js} +1 -1
  100. package/build/{nl-json.29d2eb37.chunk.js → nl-json.94c3a289.chunk.js} +1 -1
  101. package/build/{pl-json.1f04f00c.chunk.js → pl-json.ccc6ef23.chunk.js} +1 -1
  102. package/build/{pt-BR-json.b4bc8efe.chunk.js → pt-BR-json.744f024d.chunk.js} +1 -1
  103. package/build/{pt-json.c23020ab.chunk.js → pt-json.3161ca22.chunk.js} +1 -1
  104. package/build/{ru-json.7ab40ccf.chunk.js → ru-json.d22ea13c.chunk.js} +1 -1
  105. package/build/{runtime~main.7faf633a.js → runtime~main.3a5e1b07.js} +1 -1
  106. package/build/{sa-json.c5a9f4ea.chunk.js → sa-json.8fb1c04d.chunk.js} +1 -1
  107. package/build/{sk-json.e4c24c4e.chunk.js → sk-json.6c7335d4.chunk.js} +1 -1
  108. package/build/{sv-json.c3f471ae.chunk.js → sv-json.2e589a7d.chunk.js} +1 -1
  109. package/build/{th-json.a59ffb32.chunk.js → th-json.72e8de3d.chunk.js} +1 -1
  110. package/build/{tr-json.276e59fe.chunk.js → tr-json.9c44ea0c.chunk.js} +1 -1
  111. package/build/{uk-json.5b5b9c27.chunk.js → uk-json.c4cd2e24.chunk.js} +1 -1
  112. package/build/{upload-translation-en-json.004a86c1.chunk.js → upload-translation-en-json.86da7b0a.chunk.js} +1 -1
  113. package/build/{vi-json.bf3424be.chunk.js → vi-json.f7890025.chunk.js} +1 -1
  114. package/build/{zh-Hans-json.9c99f8d4.chunk.js → zh-Hans-json.a4d7dc69.chunk.js} +1 -1
  115. package/build/{zh-json.451a0271.chunk.js → zh-json.66aa2ae1.chunk.js} +1 -1
  116. package/package.json +7 -7
  117. package/admin/src/content-manager/components/SelectMany/ListItem.js +0 -102
  118. package/admin/src/content-manager/components/SelectMany/index.js +0 -148
  119. package/admin/src/content-manager/components/SelectOne/SingleValue.js +0 -67
  120. package/admin/src/content-manager/components/SelectOne/index.js +0 -97
  121. package/admin/src/content-manager/components/SelectWrapper/Label.js +0 -60
  122. package/admin/src/content-manager/components/SelectWrapper/index.js +0 -356
  123. package/admin/src/content-manager/components/SelectWrapper/utils/index.js +0 -2
  124. package/admin/src/content-manager/components/SelectWrapper/utils/select.js +0 -45
  125. package/admin/src/content-manager/pages/EditSettingsView/components/RelationalFieldButton.js +0 -135
  126. package/admin/src/content-manager/pages/EditSettingsView/components/RelationalFields.js +0 -103
  127. package/admin/src/content-manager/pages/EditView/Header/utils/getDraftRelations.js +0 -62
  128. package/build/1669.d1b29c28.chunk.js +0 -1
  129. package/build/524.8a540ac1.chunk.js +0 -644
  130. package/build/Admin-authenticatedApp.88fa40ac.chunk.js +0 -80
  131. package/build/content-manager.8bddf2e6.chunk.js +0 -1178
  132. package/build/main.6650d2e7.js +0 -9338
@@ -1,4 +1,5 @@
1
1
  import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { useQueryClient } from 'react-query';
2
3
  import { useHistory } from 'react-router-dom';
3
4
  import axios from 'axios';
4
5
  import get from 'lodash/get';
@@ -34,6 +35,7 @@ import selectCrudReducer from '../../sharedReducers/crudReducer/selectors';
34
35
 
35
36
  // This container is used to handle the CRUD
36
37
  const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }) => {
38
+ const queryClient = useQueryClient();
37
39
  const toggleNotification = useNotification();
38
40
  const { setCurrentStep } = useGuidedTour();
39
41
  const { trackUsage } = useTracking();
@@ -260,7 +262,11 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
260
262
 
261
263
  setCurrentStep('contentManager.success');
262
264
 
265
+ // TODO: need to find a better place, or a better abstraction
266
+ queryClient.invalidateQueries(['relation']);
267
+
263
268
  dispatch(submitSucceeded(cleanReceivedData(data)));
269
+
264
270
  // Enable navigation and remove loaders
265
271
  dispatch(setStatus('resolved'));
266
272
 
@@ -284,9 +290,33 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
284
290
  rawQuery,
285
291
  toggleNotification,
286
292
  setCurrentStep,
293
+ queryClient,
287
294
  ]
288
295
  );
289
296
 
297
+ const onDraftRelationCheck = useCallback(async () => {
298
+ try {
299
+ trackUsageRef.current('willCheckDraftRelations');
300
+
301
+ const endPoint = getRequestUrl(
302
+ `collection-types/${slug}/${id}/actions/numberOfDraftRelations`
303
+ );
304
+ dispatch(setStatus('draft-relation-check-pending'));
305
+
306
+ const numberOfDraftRelations = await axiosInstance.get(endPoint);
307
+ trackUsageRef.current('didCheckDraftRelations');
308
+
309
+ dispatch(setStatus('resolved'));
310
+
311
+ return numberOfDraftRelations.data.data;
312
+ } catch (err) {
313
+ displayErrors(err);
314
+ dispatch(setStatus('resolved'));
315
+
316
+ return Promise.reject(err);
317
+ }
318
+ }, [displayErrors, id, slug, dispatch]);
319
+
290
320
  const onPublish = useCallback(async () => {
291
321
  try {
292
322
  trackUsageRef.current('willPublishEntry');
@@ -332,6 +362,9 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
332
362
  message: { id: getTrad('success.record.save') },
333
363
  });
334
364
 
365
+ // TODO: need to find a better place, or a better abstraction
366
+ queryClient.invalidateQueries(['relation']);
367
+
335
368
  dispatch(submitSucceeded(cleanReceivedData(data)));
336
369
 
337
370
  dispatch(setStatus('resolved'));
@@ -346,7 +379,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
346
379
  return Promise.reject(err);
347
380
  }
348
381
  },
349
- [cleanReceivedData, displayErrors, slug, id, dispatch, toggleNotification]
382
+ [cleanReceivedData, displayErrors, slug, id, dispatch, toggleNotification, queryClient]
350
383
  );
351
384
 
352
385
  const onUnpublish = useCallback(async () => {
@@ -387,6 +420,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
387
420
  onDeleteSucceeded,
388
421
  onPost,
389
422
  onPublish,
423
+ onDraftRelationCheck,
390
424
  onPut,
391
425
  onUnpublish,
392
426
  status,
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useMemo } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useQuery } from 'react-query';
4
4
  import { useIntl } from 'react-intl';
@@ -28,10 +28,12 @@ const fetchRelation = async (endPoint, notifyStatus) => {
28
28
  return { results, pagination };
29
29
  };
30
30
 
31
- const RelationMultiple = ({ fieldSchema, metadatas, queryInfos, name, rowId, value }) => {
31
+ const RelationMultiple = ({ fieldSchema, metadatas, name, entityId, value, contentType }) => {
32
32
  const { formatMessage } = useIntl();
33
33
  const { notifyStatus } = useNotifyAT();
34
- const requestURL = getRequestUrl(`${queryInfos.endPoint}/${rowId}/${name.split('.')[0]}`);
34
+ const relationFetchEndpoint = useMemo(() => {
35
+ return getRequestUrl(`collection-types/${contentType.uid}/${entityId}/${name.split('.')[0]}`);
36
+ }, [entityId, name, contentType]);
35
37
  const [isOpen, setIsOpen] = useState(false);
36
38
 
37
39
  const Label = (
@@ -56,8 +58,8 @@ const RelationMultiple = ({ fieldSchema, metadatas, queryInfos, name, rowId, val
56
58
  };
57
59
 
58
60
  const { data, status } = useQuery(
59
- [fieldSchema.targetModel, rowId],
60
- () => fetchRelation(requestURL, notify),
61
+ [fieldSchema.targetModel, entityId],
62
+ () => fetchRelation(relationFetchEndpoint, notify),
61
63
  {
62
64
  enabled: isOpen,
63
65
  staleTime: 0,
@@ -115,6 +117,9 @@ const RelationMultiple = ({ fieldSchema, metadatas, queryInfos, name, rowId, val
115
117
  };
116
118
 
117
119
  RelationMultiple.propTypes = {
120
+ contentType: PropTypes.shape({
121
+ uid: PropTypes.string.isRequired,
122
+ }).isRequired,
118
123
  fieldSchema: PropTypes.shape({
119
124
  relation: PropTypes.string,
120
125
  targetModel: PropTypes.string,
@@ -127,8 +132,7 @@ RelationMultiple.propTypes = {
127
132
  }),
128
133
  }).isRequired,
129
134
  name: PropTypes.string.isRequired,
130
- rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
131
- queryInfos: PropTypes.shape({ endPoint: PropTypes.string.isRequired }).isRequired,
135
+ entityId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
132
136
  value: PropTypes.object.isRequired,
133
137
  };
134
138
 
@@ -16,7 +16,7 @@ const TypographyMaxWidth = styled(Typography)`
16
16
  max-width: 300px;
17
17
  `;
18
18
 
19
- const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId }) => {
19
+ const CellContent = ({ content, fieldSchema, metadatas, name, rowId, contentType }) => {
20
20
  const { type } = fieldSchema;
21
21
 
22
22
  if (!hasContent(type, content, metadatas, fieldSchema)) {
@@ -39,11 +39,11 @@ const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId
39
39
  return (
40
40
  <RelationMultiple
41
41
  fieldSchema={fieldSchema}
42
- queryInfos={queryInfos}
43
42
  metadatas={metadatas}
44
43
  value={content}
45
44
  name={name}
46
- rowId={rowId}
45
+ entityId={rowId}
46
+ contentType={contentType}
47
47
  />
48
48
  );
49
49
  }
@@ -66,11 +66,13 @@ const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId
66
66
 
67
67
  CellContent.defaultProps = {
68
68
  content: undefined,
69
- queryInfos: undefined,
70
69
  };
71
70
 
72
71
  CellContent.propTypes = {
73
72
  content: PropTypes.any,
73
+ contentType: PropTypes.shape({
74
+ uid: PropTypes.string.isRequired,
75
+ }).isRequired,
74
76
  fieldSchema: PropTypes.shape({
75
77
  component: PropTypes.string,
76
78
  multiple: PropTypes.bool,
@@ -81,7 +83,6 @@ CellContent.propTypes = {
81
83
  metadatas: PropTypes.object.isRequired,
82
84
  name: PropTypes.string.isRequired,
83
85
  rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
84
- queryInfos: PropTypes.shape({ endPoint: PropTypes.string.isRequired }),
85
86
  };
86
87
 
87
88
  export default CellContent;
@@ -24,6 +24,7 @@ import CellContent from '../CellContent';
24
24
  const TableRows = ({
25
25
  canCreate,
26
26
  canDelete,
27
+ contentType,
27
28
  headers,
28
29
  entriesToDelete,
29
30
  onClickDelete,
@@ -94,6 +95,7 @@ const TableRows = ({
94
95
  <CellContent
95
96
  content={data[name.split('.')[0]]}
96
97
  name={name}
98
+ contentType={contentType}
97
99
  {...rest}
98
100
  rowId={data.id}
99
101
  />
@@ -185,6 +187,9 @@ TableRows.defaultProps = {
185
187
  TableRows.propTypes = {
186
188
  canCreate: PropTypes.bool,
187
189
  canDelete: PropTypes.bool,
190
+ contentType: PropTypes.shape({
191
+ uid: PropTypes.string.isRequired,
192
+ }).isRequired,
188
193
  entriesToDelete: PropTypes.array,
189
194
  headers: PropTypes.array.isRequired,
190
195
  onClickDelete: PropTypes.func,
@@ -112,6 +112,7 @@ const DynamicTable = ({
112
112
  <TableRows
113
113
  canCreate={canCreate}
114
114
  canDelete={canDelete}
115
+ contentType={layout.contentType}
115
116
  headers={tableHeaders}
116
117
  rows={rows}
117
118
  withBulkActions
@@ -139,7 +140,6 @@ DynamicTable.propTypes = {
139
140
  metadatas: PropTypes.object.isRequired,
140
141
  layouts: PropTypes.shape({
141
142
  list: PropTypes.array.isRequired,
142
- editRelations: PropTypes.array,
143
143
  }).isRequired,
144
144
  options: PropTypes.object.isRequired,
145
145
  settings: PropTypes.object.isRequired,
@@ -36,6 +36,7 @@ const EditViewDataManagerProvider = ({
36
36
  isSingleType,
37
37
  onPost,
38
38
  onPublish,
39
+ onDraftRelationCheck,
39
40
  onPut,
40
41
  onUnpublish,
41
42
  readActionAllowedFields,
@@ -46,7 +47,15 @@ const EditViewDataManagerProvider = ({
46
47
  updateActionAllowedFields,
47
48
  }) => {
48
49
  const [reducerState, dispatch] = useReducer(reducer, initialState);
49
- const { formErrors, initialData, modifiedData, modifiedDZName, shouldCheckErrors } = reducerState;
50
+ const {
51
+ formErrors,
52
+ initialData,
53
+ modifiedData,
54
+ modifiedDZName,
55
+ shouldCheckErrors,
56
+ publishConfirmation,
57
+ } = reducerState;
58
+
50
59
  const toggleNotification = useNotification();
51
60
  const { lockApp, unlockApp } = useOverlayBlocker();
52
61
 
@@ -130,12 +139,17 @@ const EditViewDataManagerProvider = ({
130
139
 
131
140
  useEffect(() => {
132
141
  if (initialValues) {
142
+ const relationalFields = Object.keys(initialValues).filter(
143
+ (key) => currentContentTypeLayout?.attributes[key]?.type === 'relation'
144
+ );
145
+
133
146
  dispatch({
134
147
  type: 'INIT_FORM',
135
148
  initialValues,
149
+ relationalFields,
136
150
  });
137
151
  }
138
- }, [initialValues]);
152
+ }, [initialValues, currentContentTypeLayout]);
139
153
 
140
154
  const addComponentToDynamicZone = useCallback((keys, componentUid, shouldCheckErrors = false) => {
141
155
  trackUsageRef.current('didAddComponentToDynamicZone');
@@ -156,9 +170,18 @@ const EditViewDataManagerProvider = ({
156
170
  });
157
171
  }, []);
158
172
 
159
- const addRelation = useCallback(({ target: { name, value } }) => {
173
+ const connectRelation = useCallback(({ target: { name, value, replace } }) => {
174
+ dispatch({
175
+ type: 'CONNECT_RELATION',
176
+ keys: name.split('.'),
177
+ value,
178
+ replace,
179
+ });
180
+ }, []);
181
+
182
+ const loadRelation = useCallback(({ target: { name, value } }) => {
160
183
  dispatch({
161
- type: 'ADD_RELATION',
184
+ type: 'LOAD_RELATION',
162
185
  keys: name.split('.'),
163
186
  value,
164
187
  });
@@ -287,6 +310,14 @@ const EditViewDataManagerProvider = ({
287
310
  return shouldNotRunValidations ? { status: 'draft' } : {};
288
311
  }, [hasDraftAndPublish, shouldNotRunValidations]);
289
312
 
313
+ const handlePublishPromptDismissal = useCallback(async (e) => {
314
+ e.preventDefault();
315
+
316
+ return dispatch({
317
+ type: 'RESET_PUBLISH_CONFIRMATION',
318
+ });
319
+ }, []);
320
+
290
321
  const handleSubmit = useCallback(
291
322
  async (e) => {
292
323
  e.preventDefault();
@@ -332,8 +363,27 @@ const EditViewDataManagerProvider = ({
332
363
  },
333
364
  { isCreatingEntry, isDraft: false, isFromComponent: false }
334
365
  );
335
- let errors = {};
336
366
 
367
+ const draftCount = await onDraftRelationCheck();
368
+
369
+ if (!publishConfirmation.show && draftCount > 0) {
370
+ // If the warning hasn't already been shown and draft relations are found,
371
+ // abort the publish call and ask for confirmation from the user
372
+ dispatch({
373
+ type: 'SET_PUBLISH_CONFIRMATION',
374
+ publishConfirmation: {
375
+ show: true,
376
+ draftCount,
377
+ },
378
+ });
379
+
380
+ return;
381
+ }
382
+ dispatch({
383
+ type: 'RESET_PUBLISH_CONFIRMATION',
384
+ });
385
+
386
+ let errors = {};
337
387
  try {
338
388
  await schema.validate(modifiedData, { abortEarly: false });
339
389
  } catch (err) {
@@ -355,7 +405,15 @@ const EditViewDataManagerProvider = ({
355
405
  type: 'SET_FORM_ERRORS',
356
406
  errors,
357
407
  });
358
- }, [allLayoutData, currentContentTypeLayout, isCreatingEntry, modifiedData, onPublish]);
408
+ }, [
409
+ allLayoutData,
410
+ currentContentTypeLayout,
411
+ isCreatingEntry,
412
+ modifiedData,
413
+ publishConfirmation.show,
414
+ onPublish,
415
+ onDraftRelationCheck,
416
+ ]);
359
417
 
360
418
  const shouldCheckDZErrors = useCallback(
361
419
  (dzName) => {
@@ -404,19 +462,11 @@ const EditViewDataManagerProvider = ({
404
462
  });
405
463
  }, []);
406
464
 
407
- const moveRelation = useCallback((dragIndex, overIndex, name) => {
465
+ const disconnectRelation = useCallback(({ target: { name, value } }) => {
408
466
  dispatch({
409
- type: 'MOVE_FIELD',
410
- dragIndex,
411
- overIndex,
467
+ type: 'DISCONNECT_RELATION',
412
468
  keys: name.split('.'),
413
- });
414
- }, []);
415
-
416
- const onRemoveRelation = useCallback((keys) => {
417
- dispatch({
418
- type: 'REMOVE_RELATION',
419
- keys,
469
+ value,
420
470
  });
421
471
  }, []);
422
472
 
@@ -470,7 +520,7 @@ const EditViewDataManagerProvider = ({
470
520
  value={{
471
521
  addComponentToDynamicZone,
472
522
  addNonRepeatableComponentToField,
473
- addRelation,
523
+ connectRelation,
474
524
  addRepeatableComponentToField,
475
525
  allLayoutData,
476
526
  checkFormErrors,
@@ -483,15 +533,15 @@ const EditViewDataManagerProvider = ({
483
533
  shouldNotRunValidations,
484
534
  status,
485
535
  layout: currentContentTypeLayout,
536
+ loadRelation,
486
537
  modifiedData,
487
538
  moveComponentDown,
488
539
  moveComponentField,
489
540
  moveComponentUp,
490
- moveRelation,
491
541
  onChange: handleChange,
492
542
  onPublish: handlePublish,
493
543
  onUnpublish,
494
- onRemoveRelation,
544
+ disconnectRelation,
495
545
  readActionAllowedFields,
496
546
  redirectToPreviousPage,
497
547
  removeComponentFromDynamicZone,
@@ -500,6 +550,8 @@ const EditViewDataManagerProvider = ({
500
550
  slug,
501
551
  triggerFormValidation,
502
552
  updateActionAllowedFields,
553
+ onPublishPromptDismissal: handlePublishPromptDismissal,
554
+ publishConfirmation,
503
555
  }}
504
556
  >
505
557
  <>
@@ -543,6 +595,7 @@ EditViewDataManagerProvider.propTypes = {
543
595
  isSingleType: PropTypes.bool.isRequired,
544
596
  onPost: PropTypes.func.isRequired,
545
597
  onPublish: PropTypes.func.isRequired,
598
+ onDraftRelationCheck: PropTypes.func.isRequired,
546
599
  onPut: PropTypes.func.isRequired,
547
600
  onUnpublish: PropTypes.func.isRequired,
548
601
  readActionAllowedFields: PropTypes.array.isRequired,
@@ -3,6 +3,7 @@ import unset from 'lodash/unset';
3
3
  import get from 'lodash/get';
4
4
  import set from 'lodash/set';
5
5
  import take from 'lodash/take';
6
+ import pull from 'lodash/pull';
6
7
  import { moveFields } from './utils';
7
8
  import { getMaxTempKey } from '../../utils';
8
9
 
@@ -14,6 +15,10 @@ const initialState = {
14
15
  modifiedData: null,
15
16
  shouldCheckErrors: false,
16
17
  modifiedDZName: null,
18
+ publishConfirmation: {
19
+ show: false,
20
+ draftCount: 0,
21
+ },
17
22
  };
18
23
 
19
24
  const reducer = (state, action) =>
@@ -72,29 +77,170 @@ const reducer = (state, action) =>
72
77
 
73
78
  break;
74
79
  }
75
- case 'ADD_RELATION': {
76
- if (!Array.isArray(action.value) || !action.value.length) {
77
- break;
78
- }
80
+ case 'LOAD_RELATION': {
81
+ const initialDataPath = ['initialData', ...action.keys, 'results'];
82
+ const modifiedDataPath = ['modifiedData', ...action.keys, 'results'];
83
+ const { value } = action;
79
84
 
80
- const el = action.value[0].value;
85
+ set(draftState, initialDataPath, value);
81
86
 
82
- const currentValue = get(state, ['modifiedData', ...action.keys], null);
87
+ /**
88
+ * We need to set the value also on modifiedData, because initialData
89
+ * and modifiedData need to stay in sync, so that the CM can compare
90
+ * both states, to render the dirty UI state
91
+ */
83
92
 
84
- if (!currentValue) {
85
- set(draftState, ['modifiedData', ...action.keys], [el]);
93
+ set(draftState, modifiedDataPath, value);
86
94
 
87
- break;
95
+ break;
96
+ }
97
+ case 'CONNECT_RELATION': {
98
+ const path = ['modifiedData', ...action.keys];
99
+ const { value, replace = false } = action;
100
+ const connectedRelations = get(state, [...path, 'connect']);
101
+ const disconnectedRelations = get(state, [...path, 'disconnect']) ?? [];
102
+ const savedRelations = get(state, [...path, 'results']) ?? [];
103
+ const existInSavedRelation =
104
+ savedRelations?.findIndex((savedRelations) => savedRelations.id === value.id) !== -1;
105
+
106
+ if (!connectedRelations) {
107
+ set(draftState, [...path, 'connect'], []);
108
+ }
109
+
110
+ // We should add a relation in the connect array only if it is not an already saved relation
111
+ if (!existInSavedRelation) {
112
+ if (replace) {
113
+ set(draftState, [...path, 'connect'], [value]);
114
+ } else {
115
+ const nextValue = get(draftState, [...path, 'connect']);
116
+ nextValue.push(value);
117
+ }
88
118
  }
89
119
 
90
- set(draftState, ['modifiedData', ...action.keys], [...currentValue, el]);
120
+ // Disconnect array handling
121
+ if (replace) {
122
+ // In xToOne relations we should place the saved relation in disconnected array to not display it
123
+ // only needed if there is a saved relation and it is not already stored in disconnected array
124
+ if (savedRelations.length && !disconnectedRelations.length) {
125
+ set(draftState, [...path, 'disconnect'], savedRelations);
126
+ }
127
+
128
+ // If the saved relation is stored in disconnected array
129
+ // We should remove it when an action requires to reconnect this relation
130
+ // We then reset the connect/disconnect state
131
+ if (disconnectedRelations.length) {
132
+ const existsInDisconnectedRelations =
133
+ disconnectedRelations.findIndex(
134
+ (disconnectedRelation) => disconnectedRelation?.id === value.id
135
+ ) > -1;
136
+
137
+ if (existsInDisconnectedRelations) {
138
+ set(draftState, [...path, 'disconnect'], []);
139
+ set(draftState, [...path, 'connect'], []);
140
+ }
141
+ }
142
+ } else if (disconnectedRelations.length) {
143
+ // In xToMany relations, when an action requires to connect a relation
144
+ // We should remove it from the disconnected array if it existed in it
145
+ const existsInDisconnect = disconnectedRelations.find(
146
+ (disconnectValue) => disconnectValue.id === value.id
147
+ );
148
+
149
+ if (existsInDisconnect) {
150
+ const newDisconnectArray = pull([...disconnectedRelations], existsInDisconnect);
151
+ set(draftState, [...path, 'disconnect'], newDisconnectArray);
152
+ }
153
+ }
154
+
155
+ break;
156
+ }
157
+ case 'DISCONNECT_RELATION': {
158
+ const path = ['modifiedData', ...action.keys];
159
+ const { value } = action;
160
+ const connectedRelations = get(state, [...path, 'connect']);
161
+ const disconnectedRelations = get(state, [...path, 'disconnect']);
162
+
163
+ if (!disconnectedRelations) {
164
+ set(draftState, [...path, 'disconnect'], []);
165
+ }
166
+
167
+ const nextValue = get(draftState, [...path, 'disconnect']);
168
+ nextValue.push(value);
169
+
170
+ if (connectedRelations?.length) {
171
+ const existsInConnect = connectedRelations.find(
172
+ (connectValue) => connectValue.id === value.id
173
+ );
174
+
175
+ if (existsInConnect) {
176
+ const newConnectArray = pull([...connectedRelations], existsInConnect);
177
+ set(draftState, [...path, 'connect'], newConnectArray);
178
+ }
179
+ }
91
180
 
92
181
  break;
93
182
  }
94
183
  case 'INIT_FORM': {
184
+ const { initialValues, relationalFields = [] } = action;
185
+
95
186
  draftState.formErrors = {};
96
- draftState.initialData = action.initialValues;
97
- draftState.modifiedData = action.initialValues;
187
+
188
+ draftState.initialData = {
189
+ ...initialValues,
190
+
191
+ /**
192
+ * The state we keep in the client for relations looks like:
193
+ *
194
+ * {
195
+ * count: <int>
196
+ * results: [<Relation>]
197
+ * }
198
+ *
199
+ * The content API only returns { count: <int> }, which is why
200
+ * we need to extend the existing state rather than overwriting it.
201
+ */
202
+
203
+ ...relationalFields.reduce((acc, name) => {
204
+ acc[name] = {
205
+ ...(state.initialData?.[name] ?? {}),
206
+ ...(initialValues?.[name] ?? {}),
207
+ };
208
+
209
+ return acc;
210
+ }, {}),
211
+ };
212
+
213
+ draftState.modifiedData = {
214
+ ...initialValues,
215
+
216
+ /**
217
+ * The client sends the following to the content API:
218
+ *
219
+ * {
220
+ * connect: [<Relation>],
221
+ * disconnect: [<Relation>]
222
+ * }
223
+ *
224
+ * but receives only { count: <int> } in return. After save/ publish
225
+ * we have to:
226
+ *
227
+ * 1) reset the connect/ disconnect arrays
228
+ * 2) extend the existing state with the API response, so that `count`
229
+ * stays in sync
230
+ */
231
+
232
+ ...relationalFields.reduce((acc, name) => {
233
+ const { connect, disconnect, ...currentState } = state.modifiedData?.[name] ?? {};
234
+
235
+ acc[name] = {
236
+ ...(currentState ?? {}),
237
+ ...(initialValues?.[name] ?? {}),
238
+ };
239
+
240
+ return acc;
241
+ }, {}),
242
+ };
243
+
98
244
  draftState.modifiedDZName = null;
99
245
  draftState.shouldCheckErrors = false;
100
246
  break;
@@ -213,19 +359,6 @@ const reducer = (state, action) =>
213
359
 
214
360
  break;
215
361
  }
216
- case 'REMOVE_RELATION': {
217
- const pathArray = action.keys.split('.');
218
- const pathArrayLength = pathArray.length - 1;
219
- const pathToData = ['modifiedData', ...take(pathArray, pathArrayLength)];
220
- const currentValue = get(state, pathToData).slice();
221
- const indexToRemove = parseInt(pathArray[pathArrayLength], 10);
222
-
223
- currentValue.splice(indexToRemove, 1);
224
-
225
- set(draftState, pathToData, currentValue);
226
-
227
- break;
228
- }
229
362
  case 'SET_DEFAULT_DATA_STRUCTURES': {
230
363
  draftState.componentsDataStructure = action.componentsDataStructure;
231
364
  draftState.contentTypeDataStructure = action.contentTypeDataStructure;
@@ -246,7 +379,14 @@ const reducer = (state, action) =>
246
379
 
247
380
  break;
248
381
  }
249
-
382
+ case 'SET_PUBLISH_CONFIRMATION': {
383
+ draftState.publishConfirmation = { ...action.publishConfirmation };
384
+ break;
385
+ }
386
+ case 'RESET_PUBLISH_CONFIRMATION': {
387
+ draftState.publishConfirmation = { ...state.publishConfirmation, show: false };
388
+ break;
389
+ }
250
390
  default:
251
391
  return draftState;
252
392
  }
@@ -51,6 +51,19 @@ const cleanData = (retrievedData, currentSchema, componentsSchema) => {
51
51
  }
52
52
 
53
53
  break;
54
+
55
+ case 'relation':
56
+ // Instead of the full relation object, we only want to send its ID
57
+ // and need to clean-up the connect|disconnect arrays
58
+ cleanedData = Object.entries(value).reduce((acc, [key, value]) => {
59
+ if (['connect', 'disconnect'].includes(key)) {
60
+ acc[key] = value.map((currentValue) => ({ id: currentValue.id }));
61
+ }
62
+
63
+ return acc;
64
+ }, {});
65
+ break;
66
+
54
67
  case 'dynamiczone':
55
68
  cleanedData = value.map((componentData) => {
56
69
  const subCleanedData = recursiveCleanData(
@@ -62,7 +75,6 @@ const cleanData = (retrievedData, currentSchema, componentsSchema) => {
62
75
  });
63
76
  break;
64
77
  default:
65
- // The helper is mainly used for the relations in order to just send the id
66
78
  cleanedData = helperCleanData(value, 'id');
67
79
  }
68
80
 
@@ -75,6 +87,10 @@ const cleanData = (retrievedData, currentSchema, componentsSchema) => {
75
87
  return recursiveCleanData(retrievedData, currentSchema);
76
88
  };
77
89
 
90
+ // TODO: check which parts are still needed: I suspect the
91
+ // isArray part can go away, but I'm not sure what could send
92
+ // an object; in case both can go away we might be able to get
93
+ // rid of the whole helper
78
94
  export const helperCleanData = (value, key) => {
79
95
  if (isArray(value)) {
80
96
  return value.map((obj) => (obj[key] ? obj[key] : obj));