@strapi/admin 4.5.0-alpha.0 → 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 (191) hide show
  1. package/admin/src/StrapiApp.js +4 -12
  2. package/admin/src/components/Providers/index.js +14 -10
  3. package/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js +24 -0
  4. package/admin/src/content-manager/components/DynamicTable/TableRows/index.js +20 -15
  5. package/admin/src/content-manager/components/DynamicZone/components/Component/index.js +19 -9
  6. package/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +50 -3
  7. package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +50 -9
  8. package/admin/src/content-manager/components/FieldTypeIcon/index.js +31 -1
  9. package/admin/src/content-manager/components/Inputs/index.js +36 -14
  10. package/admin/src/content-manager/components/NonRepeatableComponent/index.js +2 -0
  11. package/admin/src/content-manager/components/RelationInput/RelationInput.js +95 -32
  12. package/admin/src/content-manager/components/RelationInput/components/RelationItem.js +2 -2
  13. package/admin/src/content-manager/components/RelationInput/constants.js +1 -1
  14. package/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js +33 -22
  15. package/admin/src/content-manager/components/RelationInputDataManager/utils/index.js +1 -0
  16. package/admin/src/content-manager/components/RelationInputDataManager/utils/normalizeRelations.js +10 -3
  17. package/admin/src/content-manager/components/RelationInputDataManager/utils/normalizeSearchResults.js +12 -0
  18. package/admin/src/content-manager/components/RelationInputDataManager/utils/select.js +34 -11
  19. package/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js +5 -0
  20. package/admin/src/content-manager/components/SingleTypeFormWrapper/index.js +23 -0
  21. package/admin/src/content-manager/hooks/useRelation/useRelation.js +17 -9
  22. package/admin/src/content-manager/pages/EditSettingsView/components/FormModal.js +7 -2
  23. package/admin/src/content-manager/pages/EditSettingsView/index.js +2 -1
  24. package/admin/src/content-manager/pages/EditView/Header/index.js +118 -50
  25. package/admin/src/content-manager/pages/EditView/Header/utils/select.js +4 -0
  26. package/admin/src/content-manager/pages/EditView/index.js +102 -93
  27. package/admin/src/content-manager/pages/ListView/index.js +24 -15
  28. package/admin/src/content-manager/pages/ListView/utils/buildQueryString.js +14 -2
  29. package/admin/src/contexts/ApiTokenPermissions/index.js +24 -0
  30. package/admin/src/core/apis/CustomFields.js +80 -0
  31. package/admin/src/core/apis/index.js +1 -0
  32. package/admin/src/hooks/index.js +1 -0
  33. package/admin/src/hooks/useRegenerate/index.js +34 -0
  34. package/admin/src/pages/HomePage/SocialLinks.js +1 -1
  35. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/ActionBoundRoutes/index.js +56 -0
  36. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/BoundRoute/getMethodColor.js +41 -0
  37. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/BoundRoute/index.js +72 -0
  38. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/CollapsableContentType/CheckBoxWrapper.js +30 -0
  39. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/CollapsableContentType/index.js +150 -0
  40. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/ContenTypesSection/index.js +37 -0
  41. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormApiTokenContainer/index.js +254 -0
  42. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormBody/index.js +77 -0
  43. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormHead/index.js +85 -0
  44. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Permissions/index.js +40 -0
  45. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Regenerate/index.js +68 -0
  46. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/index.js +215 -197
  47. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/init.js +13 -0
  48. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/reducer.js +72 -0
  49. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/getDateOfExpiration.js +16 -0
  50. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/index.js +5 -0
  51. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/schema.js +2 -1
  52. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/transformPermissionsData.js +36 -0
  53. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/DefaultButton/index.js +63 -0
  54. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/DeleteButton/index.js +1 -0
  55. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/ReadButton/index.js +19 -0
  56. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/UpdateButton/index.js +3 -36
  57. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/index.js +13 -11
  58. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/index.js +3 -2
  59. package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/utils/tableHeaders.js +8 -8
  60. package/admin/src/pages/SettingsPage/pages/ApiTokens/ProtectedEditView/index.js +1 -1
  61. package/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js +2 -2
  62. package/admin/src/permissions/defaultPermissions.js +2 -6
  63. package/admin/src/translations/ca.json +3 -1
  64. package/admin/src/translations/de.json +4 -1
  65. package/admin/src/translations/dk.json +3 -0
  66. package/admin/src/translations/en.json +22 -1
  67. package/admin/src/translations/es.json +156 -157
  68. package/admin/src/translations/fr.json +3 -0
  69. package/admin/src/translations/gu.json +608 -606
  70. package/admin/src/translations/hi.json +689 -687
  71. package/admin/src/translations/hu.json +2 -0
  72. package/admin/src/translations/id.json +2 -0
  73. package/admin/src/translations/it.json +2 -0
  74. package/admin/src/translations/ja.json +2 -0
  75. package/admin/src/translations/ko.json +2 -0
  76. package/admin/src/translations/ml.json +689 -687
  77. package/admin/src/translations/nl.json +3 -0
  78. package/admin/src/translations/pl.json +2 -0
  79. package/admin/src/translations/pt-BR.json +3 -0
  80. package/admin/src/translations/ru.json +489 -491
  81. package/admin/src/translations/sa.json +85 -82
  82. package/admin/src/translations/sk.json +3 -0
  83. package/admin/src/translations/sv.json +3 -0
  84. package/admin/src/translations/zh-Hans.json +4 -1
  85. package/admin/src/translations/zh.json +3 -0
  86. package/build/1856.d8f13391.chunk.js +173 -0
  87. package/build/{9311.7cc03f29.chunk.js → 1939.e3c87653.chunk.js} +108 -291
  88. package/build/{2077.c935ee42.chunk.js → 2077.31a2d91e.chunk.js} +4 -4
  89. package/build/{2912.a015078a.chunk.js → 2912.ab68a736.chunk.js} +8 -8
  90. package/build/4318.7d167b58.chunk.js +30 -0
  91. package/build/4715.44b1ef9b.chunk.js +386 -0
  92. package/build/{4982.05eda880.chunk.js → 4982.c2a311b7.chunk.js} +9 -9
  93. package/build/7379.d246dd38.chunk.js +1 -0
  94. package/build/{7841.91f793dc.chunk.js → 7841.4b67af3f.chunk.js} +8 -8
  95. package/build/{7866.1201afbd.chunk.js → 7866.5fbeb7e5.chunk.js} +10 -10
  96. package/build/{8380.8789ff76.chunk.js → 8380.9b53a31d.chunk.js} +7 -7
  97. package/build/{8549.133c4473.chunk.js → 8549.cf10b5d1.chunk.js} +4 -4
  98. package/build/8738.a30a2160.chunk.js +461 -0
  99. package/build/{9066.08049eb1.chunk.js → 9066.26faf397.chunk.js} +4 -4
  100. package/build/{9166.037339e0.chunk.js → 9166.8fcb3019.chunk.js} +5 -5
  101. package/build/{9420.43a86e7c.chunk.js → 9420.0fe11290.chunk.js} +6 -6
  102. package/build/962.8651ba3f.chunk.js +184 -0
  103. package/build/Admin-authenticatedApp.883449a5.chunk.js +80 -0
  104. package/build/{Admin_homePage.118926e0.chunk.js → Admin_homePage.4b2be829.chunk.js} +2 -2
  105. package/build/{Admin_profilePage.9d50ac44.chunk.js → Admin_profilePage.da32abbc.chunk.js} +1 -1
  106. package/build/{Admin_settingsPage.98a711e5.chunk.js → Admin_settingsPage.98e2a62b.chunk.js} +16 -16
  107. package/build/admin-app.a61d5c2e.chunk.js +112 -0
  108. package/build/admin-edit-roles-page.4dd6bcb9.chunk.js +1 -0
  109. package/build/{admin-users.97a08630.chunk.js → admin-users.d71f198a.chunk.js} +3 -3
  110. package/build/api-tokens-create-page.93dd0689.chunk.js +1 -0
  111. package/build/api-tokens-edit-page.b0adac81.chunk.js +1 -0
  112. package/build/api-tokens-list-page.bb36535f.chunk.js +16 -0
  113. package/build/{ca-json.a16899ae.chunk.js → ca-json.82df6eab.chunk.js} +1 -1
  114. package/build/content-manager.933dc286.chunk.js +1201 -0
  115. package/build/content-type-builder-list-view.5b3cd768.chunk.js +194 -0
  116. package/build/content-type-builder-translation-en-json.f985c9c4.chunk.js +1 -0
  117. package/build/content-type-builder.a6e29716.chunk.js +145 -0
  118. package/build/{de-json.aa6026b3.chunk.js → de-json.0ad554eb.chunk.js} +1 -1
  119. package/build/{dk-json.fac2bcfb.chunk.js → dk-json.e195ea1a.chunk.js} +1 -1
  120. package/build/{email-settings-page.64037147.chunk.js → email-settings-page.bfe6227f.chunk.js} +4 -4
  121. package/build/en-json.1889403c.chunk.js +1 -0
  122. package/build/{es-json.d672e181.chunk.js → es-json.09f80f6e.chunk.js} +1 -1
  123. package/build/{fr-json.71a16175.chunk.js → fr-json.606d056b.chunk.js} +1 -1
  124. package/build/{gu-json.ca345cd1.chunk.js → gu-json.9881264f.chunk.js} +1 -1
  125. package/build/{hi-json.50c7e6d4.chunk.js → hi-json.83dcf48f.chunk.js} +1 -1
  126. package/build/{hu-json.e0521dcc.chunk.js → hu-json.6f328bce.chunk.js} +1 -1
  127. package/build/{i18n-settings-page.0b73785d.chunk.js → i18n-settings-page.18166125.chunk.js} +4 -4
  128. package/build/{id-json.4b1ff8d6.chunk.js → id-json.1f3c4303.chunk.js} +1 -1
  129. package/build/index.html +1 -1
  130. package/build/{it-json.86bac220.chunk.js → it-json.494ac432.chunk.js} +1 -1
  131. package/build/{ja-json.4e44e36b.chunk.js → ja-json.6f262117.chunk.js} +1 -1
  132. package/build/{ko-json.1003756e.chunk.js → ko-json.36dc3b9a.chunk.js} +1 -1
  133. package/build/main.63e7ea0a.js +9338 -0
  134. package/build/{ml-json.c7774425.chunk.js → ml-json.9566bf9a.chunk.js} +1 -1
  135. package/build/{nl-json.f58ea235.chunk.js → nl-json.94c3a289.chunk.js} +1 -1
  136. package/build/{pl-json.fed96aba.chunk.js → pl-json.ccc6ef23.chunk.js} +1 -1
  137. package/build/{pt-BR-json.073799ab.chunk.js → pt-BR-json.744f024d.chunk.js} +1 -1
  138. package/build/{ru-json.7ad2cbbf.chunk.js → ru-json.d22ea13c.chunk.js} +1 -1
  139. package/build/{runtime~main.feeac6d3.js → runtime~main.3a5e1b07.js} +1 -1
  140. package/build/{sa-json.f0f704f0.chunk.js → sa-json.8fb1c04d.chunk.js} +1 -1
  141. package/build/{sk-json.a848961b.chunk.js → sk-json.6c7335d4.chunk.js} +1 -1
  142. package/build/sso-settings-page.9ceb0140.chunk.js +1 -0
  143. package/build/{sv-json.b038acbe.chunk.js → sv-json.2e589a7d.chunk.js} +1 -1
  144. package/build/{upload-settings.80ff0974.chunk.js → upload-settings.3d613216.chunk.js} +4 -4
  145. package/build/{upload-translation-en-json.004a86c1.chunk.js → upload-translation-en-json.86da7b0a.chunk.js} +1 -1
  146. package/build/{users-advanced-settings-page.a02f4806.chunk.js → users-advanced-settings-page.f4051d92.chunk.js} +4 -4
  147. package/build/{webhook-edit-page.d2ea3351.chunk.js → webhook-edit-page.9e46fc3f.chunk.js} +1 -1
  148. package/build/{webhook-list-page.2775a683.chunk.js → webhook-list-page.a712ae40.chunk.js} +4 -4
  149. package/build/{zh-Hans-json.03d2bda1.chunk.js → zh-Hans-json.a4d7dc69.chunk.js} +1 -1
  150. package/build/{zh-json.3d0cc664.chunk.js → zh-json.66aa2ae1.chunk.js} +1 -1
  151. package/ee/server/controllers/user.js +5 -3
  152. package/package.json +9 -8
  153. package/server/bootstrap.js +19 -1
  154. package/server/config/admin-actions.js +20 -0
  155. package/server/content-types/api-token-permission.js +36 -0
  156. package/server/content-types/api-token.js +25 -1
  157. package/server/content-types/index.js +1 -0
  158. package/server/controllers/admin.js +3 -0
  159. package/server/controllers/api-token.js +24 -1
  160. package/server/controllers/content-api.js +15 -0
  161. package/server/controllers/index.js +1 -0
  162. package/server/controllers/user.js +3 -2
  163. package/server/routes/api-tokens.js +11 -0
  164. package/server/routes/content-api.js +20 -0
  165. package/server/routes/index.js +2 -0
  166. package/server/services/api-token.js +309 -29
  167. package/server/services/constants.js +10 -0
  168. package/server/services/permission/engine.js +36 -226
  169. package/server/services/permission.js +4 -1
  170. package/server/strategies/admin.js +7 -1
  171. package/server/strategies/api-token.js +72 -11
  172. package/server/validation/api-tokens.js +12 -2
  173. package/utils/get-custom-app-config-file.js +5 -0
  174. package/build/1856.47226450.chunk.js +0 -173
  175. package/build/4715.58cd558f.chunk.js +0 -387
  176. package/build/7098.40dcd7bf.chunk.js +0 -1
  177. package/build/8851.e4ac62f2.chunk.js +0 -158
  178. package/build/Admin-authenticatedApp.e39f36c9.chunk.js +0 -80
  179. package/build/admin-app.4f7618a9.chunk.js +0 -112
  180. package/build/admin-edit-roles-page.554ba3fa.chunk.js +0 -1
  181. package/build/api-tokens-create-page.4c262d6e.chunk.js +0 -1
  182. package/build/api-tokens-edit-page.10a9d368.chunk.js +0 -1
  183. package/build/api-tokens-list-page.442c9f3c.chunk.js +0 -15
  184. package/build/content-manager.7d57c9d1.chunk.js +0 -1200
  185. package/build/content-type-builder-list-view.8cc534e0.chunk.js +0 -194
  186. package/build/content-type-builder-translation-en-json.201bfb78.chunk.js +0 -1
  187. package/build/content-type-builder.684df7a4.chunk.js +0 -142
  188. package/build/en-json.0c69c7d7.chunk.js +0 -1
  189. package/build/main.b47db1a3.js +0 -9337
  190. package/build/sso-settings-page.445184e0.chunk.js +0 -1
  191. package/server/services/permission/engine-hooks.js +0 -82
@@ -1,6 +1,6 @@
1
1
  import React, { useRef, useState, useMemo, useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import styled, { keyframes } from 'styled-components';
3
+ import styled from 'styled-components';
4
4
  import { FixedSizeList as List } from 'react-window';
5
5
 
6
6
  import { ReactSelect } from '@strapi/helper-plugin';
@@ -15,7 +15,6 @@ import { Tooltip } from '@strapi/design-system/Tooltip';
15
15
 
16
16
  import Cross from '@strapi/icons/Cross';
17
17
  import Refresh from '@strapi/icons/Refresh';
18
- import Loader from '@strapi/icons/Loader';
19
18
 
20
19
  import { Relation } from './components/Relation';
21
20
  import { RelationItem } from './components/RelationItem';
@@ -39,19 +38,15 @@ const BoxEllipsis = styled(Box)`
39
38
  }
40
39
  `;
41
40
 
42
- const rotation = keyframes`
43
- from {
44
- transform: rotate(0deg);
41
+ const DisconnectButton = styled.button`
42
+ svg path {
43
+ fill: ${({ theme }) => theme.colors.neutral500};
45
44
  }
46
- to {
47
- transform: rotate(359deg);
48
- }
49
- `;
50
45
 
51
- // TODO - to replace with loading prop on TextButton after DS release
52
- const LoaderWrapper = styled(Box)`
53
- animation: ${rotation} 2s infinite linear;
54
- will-change: transform;
46
+ &:hover svg path,
47
+ &:focus svg path {
48
+ fill: ${({ theme }) => theme.colors.neutral600};
49
+ }
55
50
  `;
56
51
 
57
52
  const RelationInput = ({
@@ -64,6 +59,7 @@ const RelationInput = ({
64
59
  label,
65
60
  labelAction,
66
61
  labelLoadMore,
62
+ labelDisconnectRelation,
67
63
  loadingMessage,
68
64
  onRelationAdd,
69
65
  onRelationLoadMore,
@@ -84,6 +80,10 @@ const RelationInput = ({
84
80
  const outerListRef = useRef();
85
81
  const [overflow, setOverflow] = useState('');
86
82
 
83
+ const {
84
+ data: { pages },
85
+ } = searchResults;
86
+
87
87
  const relations = useMemo(() => paginatedRelations.data.pages.flat(), [paginatedRelations]);
88
88
  const totalNumberOfRelations = relations.length ?? 0;
89
89
 
@@ -102,12 +102,12 @@ const RelationInput = ({
102
102
 
103
103
  const options = useMemo(
104
104
  () =>
105
- searchResults.data.pages.flat().map((result) => ({
105
+ pages.flat().map((result) => ({
106
106
  ...result,
107
107
  value: result.id,
108
108
  label: result.mainField,
109
109
  })),
110
- [searchResults]
110
+ [pages]
111
111
  );
112
112
 
113
113
  useEffect(() => {
@@ -143,6 +143,71 @@ const RelationInput = ({
143
143
  };
144
144
  }, [paginatedRelations, relations, numberOfRelationsToDisplay, totalNumberOfRelations]);
145
145
 
146
+ /**
147
+ * This code is being isolated because it's a hack to fix a placement bug in
148
+ * `react-select` where when the options prop is updated the position of the
149
+ * menu is not recalculated.
150
+ */
151
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
152
+
153
+ const timeoutRef = useRef();
154
+ const previousOptions = useRef([]);
155
+
156
+ useEffect(() => {
157
+ /**
158
+ * We only really want this effect to fire once when the options
159
+ * change from an empty array to an array with values.
160
+ * Otherwise, it'll fire when the infinite scrolling happens causing
161
+ * the menu to jump to the top all the time when loading more.
162
+ */
163
+ if (options.length > 0 && previousOptions.current.length === 0) {
164
+ setIsMenuOpen((isCurrentlyOpened) => {
165
+ /**
166
+ * If we're currently open and the options changed
167
+ * we want to close and open to ensure the menu's
168
+ * position is correctly calculated
169
+ */
170
+ if (isCurrentlyOpened) {
171
+ timeoutRef.current = setTimeout(() => {
172
+ setIsMenuOpen(true);
173
+ }, 10);
174
+
175
+ return false;
176
+ }
177
+
178
+ return false;
179
+ });
180
+ }
181
+
182
+ return () => {
183
+ previousOptions.current = options || [];
184
+ };
185
+ }, [options]);
186
+
187
+ useEffect(() => {
188
+ return () => {
189
+ /**
190
+ * If the component unmounts and a timer is set we should clear that timer
191
+ */
192
+ if (timeoutRef.current) {
193
+ clearTimeout(timeoutRef.current);
194
+ }
195
+ };
196
+ }, []);
197
+
198
+ const handleMenuClose = () => {
199
+ setIsMenuOpen(false);
200
+
201
+ if (onSearchClose) {
202
+ onSearchClose();
203
+ }
204
+ };
205
+
206
+ const handleMenuOpen = () => {
207
+ setIsMenuOpen(true);
208
+ onSearchOpen();
209
+ };
210
+
146
211
  return (
147
212
  <Field error={error} name={name} hint={description} id={id}>
148
213
  <Relation
@@ -157,6 +222,7 @@ const RelationInput = ({
157
222
  // position fixed doesn't update position on scroll
158
223
  // react select doesn't update menu position on options change
159
224
  menuPosition="absolute"
225
+ menuPlacement="auto"
160
226
  components={{ Option }}
161
227
  options={options}
162
228
  isDisabled={disabled}
@@ -181,8 +247,9 @@ const RelationInput = ({
181
247
  setValue(value);
182
248
  onSearch(value);
183
249
  }}
184
- onMenuClose={onSearchClose}
185
- onMenuOpen={onSearchOpen}
250
+ onMenuClose={handleMenuClose}
251
+ onMenuOpen={handleMenuOpen}
252
+ menuIsOpen={isMenuOpen}
186
253
  onMenuScrollToBottom={() => {
187
254
  if (searchResults.hasNextPage) {
188
255
  onSearchNextPage();
@@ -197,18 +264,10 @@ const RelationInput = ({
197
264
  loadMore={
198
265
  shouldDisplayLoadMoreButton && (
199
266
  <TextButton
200
- disabled={paginatedRelations.isLoading}
267
+ disabled={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
201
268
  onClick={() => onRelationLoadMore()}
202
- startIcon={
203
- paginatedRelations.isLoading ? (
204
- // TODO: To replace with loading prop on TextButton after DS release
205
- <LoaderWrapper>
206
- <Loader />
207
- </LoaderWrapper>
208
- ) : (
209
- <Refresh />
210
- )
211
- }
269
+ loading={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
270
+ startIcon={<Refresh />}
212
271
  >
213
272
  {labelLoadMore}
214
273
  </TextButton>
@@ -234,19 +293,20 @@ const RelationInput = ({
234
293
  disabled={disabled}
235
294
  key={`relation-${name}-${id}`}
236
295
  endAction={
237
- <button
296
+ <DisconnectButton
238
297
  data-testid={`remove-relation-${id}`}
239
298
  disabled={disabled}
240
299
  type="button"
241
300
  onClick={() => onRelationRemove(data[index])}
301
+ aria-label={labelDisconnectRelation}
242
302
  >
243
303
  <Icon width="12px" as={Cross} />
244
- </button>
304
+ </DisconnectButton>
245
305
  }
246
306
  style={style}
247
307
  >
248
308
  <BoxEllipsis minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
249
- <Tooltip description={mainField ?? id}>
309
+ <Tooltip description={mainField ?? `${id}`}>
250
310
  {href ? (
251
311
  <LinkEllipsis to={href} disabled={disabled}>
252
312
  {mainField ?? id}
@@ -300,6 +360,7 @@ const ReactQueryRelationResult = PropTypes.shape({
300
360
  ),
301
361
  }),
302
362
  hasNextPage: PropTypes.bool,
363
+ isFetchingNextPage: PropTypes.bool.isRequired,
303
364
  isLoading: PropTypes.bool.isRequired,
304
365
  isSuccess: PropTypes.bool.isRequired,
305
366
  });
@@ -327,6 +388,7 @@ RelationInput.defaultProps = {
327
388
  error: undefined,
328
389
  labelAction: null,
329
390
  labelLoadMore: null,
391
+ onSearchClose: undefined,
330
392
  required: false,
331
393
  relations: [],
332
394
  searchResults: [],
@@ -340,6 +402,7 @@ RelationInput.propTypes = {
340
402
  label: PropTypes.string.isRequired,
341
403
  labelAction: PropTypes.element,
342
404
  labelLoadMore: PropTypes.string,
405
+ labelDisconnectRelation: PropTypes.string.isRequired,
343
406
  loadingMessage: PropTypes.func.isRequired,
344
407
  name: PropTypes.string.isRequired,
345
408
  numberOfRelationsToDisplay: PropTypes.number.isRequired,
@@ -348,7 +411,7 @@ RelationInput.propTypes = {
348
411
  onRelationLoadMore: PropTypes.func.isRequired,
349
412
  onSearch: PropTypes.func.isRequired,
350
413
  onSearchNextPage: PropTypes.func.isRequired,
351
- onSearchClose: PropTypes.func.isRequired,
414
+ onSearchClose: PropTypes.func,
352
415
  onSearchOpen: PropTypes.func.isRequired,
353
416
  placeholder: PropTypes.string.isRequired,
354
417
  publicationStateTranslations: PropTypes.shape({
@@ -14,8 +14,8 @@ export const RelationItem = ({ children, disabled, endAction, style, ...props })
14
14
  return (
15
15
  <Box style={style} as="li">
16
16
  <Flex
17
- paddingTop={3}
18
- paddingBottom={3}
17
+ paddingTop={2}
18
+ paddingBottom={2}
19
19
  paddingLeft={4}
20
20
  paddingRight={4}
21
21
  hasRadius
@@ -1 +1 @@
1
- export const RELATION_ITEM_HEIGHT = 58;
1
+ export const RELATION_ITEM_HEIGHT = 50;
@@ -1,16 +1,18 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React, { memo, useEffect, useMemo } from 'react';
3
3
  import { useIntl } from 'react-intl';
4
+ import get from 'lodash/get';
4
5
 
5
- import { useCMEditViewDataManager, NotAllowedInput, useQueryParams } from '@strapi/helper-plugin';
6
+ import { useCMEditViewDataManager, NotAllowedInput } from '@strapi/helper-plugin';
6
7
 
7
8
  import { RelationInput } from '../RelationInput';
8
9
  import { useRelation } from '../../hooks/useRelation';
9
- import { connect, select, normalizeRelations } from './utils';
10
+ import { connect, select, normalizeRelations, normalizeSearchResults } from './utils';
10
11
  import { PUBLICATION_STATES, RELATIONS_TO_DISPLAY, SEARCH_RESULTS_TO_DISPLAY } from './constants';
11
12
  import { getTrad } from '../../utils';
12
13
 
13
- export const RelationInputDataManger = ({
14
+ export const RelationInputDataManager = ({
15
+ componentId,
14
16
  editable,
15
17
  description,
16
18
  intlLabel,
@@ -30,15 +32,13 @@ export const RelationInputDataManger = ({
30
32
  const { formatMessage } = useIntl();
31
33
  const { connectRelation, disconnectRelation, loadRelation, modifiedData, slug, initialData } =
32
34
  useCMEditViewDataManager();
33
- const [{ query }] = useQueryParams();
34
35
 
35
36
  const { relations, search, searchFor } = useRelation(`${slug}-${name}-${initialData?.id ?? ''}`, {
36
37
  relation: {
37
- enabled: initialData[name]?.count !== 0 && !!endpoints.relation,
38
+ enabled: get(initialData, name)?.count !== 0 && !!endpoints.relation,
38
39
  endpoint: endpoints.relation,
39
40
  pageParams: {
40
41
  ...defaultParams,
41
- locale: query?.plugins?.i18n?.locale,
42
42
  pageSize: RELATIONS_TO_DISPLAY,
43
43
  },
44
44
  },
@@ -47,18 +47,18 @@ export const RelationInputDataManger = ({
47
47
  endpoint: endpoints.search,
48
48
  pageParams: {
49
49
  ...defaultParams,
50
- entityId: isCreatingEntry ? undefined : initialData.id,
51
- locale: query?.plugins?.i18n?.locale,
50
+ entityId: isCreatingEntry ? undefined : componentId ?? initialData.id,
52
51
  pageSize: SEARCH_RESULTS_TO_DISPLAY,
53
52
  },
54
53
  },
55
54
  });
56
55
 
56
+ const relationsFromModifiedData = get(modifiedData, name);
57
57
  const stringifiedRelations = JSON.stringify(relations);
58
58
  const normalizedRelations = useMemo(
59
59
  () =>
60
60
  normalizeRelations(relations, {
61
- modifiedData: modifiedData?.[name],
61
+ modifiedData: relationsFromModifiedData,
62
62
  mainFieldName: mainField.name,
63
63
  shouldAddLink: shouldDisplayRelationLink,
64
64
  targetModel,
@@ -117,11 +117,17 @@ export const RelationInputDataManger = ({
117
117
  };
118
118
 
119
119
  const handleSearch = (term) => {
120
- searchFor(term, { idsToOmit: modifiedData?.[name]?.connect?.map((relation) => relation.id) });
120
+ searchFor(term, {
121
+ idsToInclude: relationsFromModifiedData?.disconnect?.map((relation) => relation.id),
122
+ idsToOmit: relationsFromModifiedData?.connect?.map((relation) => relation.id),
123
+ });
121
124
  };
122
125
 
123
126
  const handleOpenSearch = () => {
124
- searchFor('', { idsToOmit: modifiedData?.[name]?.connect?.map((relation) => relation.id) });
127
+ searchFor('', {
128
+ idsToInclude: relationsFromModifiedData?.disconnect?.map((relation) => relation.id),
129
+ idsToOmit: relationsFromModifiedData?.connect?.map((relation) => relation.id),
130
+ });
125
131
  };
126
132
 
127
133
  const handleSearchMore = () => {
@@ -146,13 +152,17 @@ export const RelationInputDataManger = ({
146
152
  })} ${initialData[name]?.count !== undefined ? `(${initialData[name].count})` : ''}`}
147
153
  labelAction={labelAction}
148
154
  labelLoadMore={
149
- // TODO: only display if there are more; derive from count
150
- !isCreatingEntry &&
151
- formatMessage({
152
- id: getTrad('relation.loadMore'),
153
- defaultMessage: 'Load More',
154
- })
155
+ !isCreatingEntry
156
+ ? formatMessage({
157
+ id: getTrad('relation.loadMore'),
158
+ defaultMessage: 'Load More',
159
+ })
160
+ : null
155
161
  }
162
+ labelDisconnectRelation={formatMessage({
163
+ id: getTrad('relation.disconnect'),
164
+ defaultMessage: 'Remove',
165
+ })}
156
166
  listHeight={320}
157
167
  loadingMessage={() =>
158
168
  formatMessage({
@@ -167,7 +177,6 @@ export const RelationInputDataManger = ({
167
177
  onRelationLoadMore={() => handleRelationLoadMore()}
168
178
  onSearch={(term) => handleSearch(term)}
169
179
  onSearchNextPage={() => handleSearchMore()}
170
- onSearchClose={() => {}}
171
180
  onSearchOpen={handleOpenSearch}
172
181
  placeholder={formatMessage(
173
182
  placeholder || {
@@ -188,7 +197,7 @@ export const RelationInputDataManger = ({
188
197
  }}
189
198
  relations={normalizedRelations}
190
199
  required={required}
191
- searchResults={normalizeRelations(search, {
200
+ searchResults={normalizeSearchResults(search, {
192
201
  mainFieldName: mainField.name,
193
202
  })}
194
203
  size={size}
@@ -196,7 +205,8 @@ export const RelationInputDataManger = ({
196
205
  );
197
206
  };
198
207
 
199
- RelationInputDataManger.defaultProps = {
208
+ RelationInputDataManager.defaultProps = {
209
+ componentId: undefined,
200
210
  editable: true,
201
211
  description: '',
202
212
  labelAction: null,
@@ -205,7 +215,8 @@ RelationInputDataManger.defaultProps = {
205
215
  required: false,
206
216
  };
207
217
 
208
- RelationInputDataManger.propTypes = {
218
+ RelationInputDataManager.propTypes = {
219
+ componentId: PropTypes.number,
209
220
  editable: PropTypes.bool,
210
221
  description: PropTypes.string,
211
222
  intlLabel: PropTypes.shape({
@@ -245,6 +256,6 @@ RelationInputDataManger.propTypes = {
245
256
  }).isRequired,
246
257
  };
247
258
 
248
- const Memoized = memo(RelationInputDataManger);
259
+ const Memoized = memo(RelationInputDataManager);
249
260
 
250
261
  export default connect(Memoized, select);
@@ -1,3 +1,4 @@
1
1
  export { default as connect } from './connect';
2
2
  export { default as select } from './select';
3
3
  export { normalizeRelations } from './normalizeRelations';
4
+ export { normalizeSearchResults } from './normalizeSearchResults';
@@ -2,7 +2,7 @@ import { getRelationLink } from './getRelationLink';
2
2
 
3
3
  import { PUBLICATION_STATES } from '../constants';
4
4
 
5
- const normalizeRelation = (relation, { shouldAddLink, mainFieldName, targetModel }) => {
5
+ export const normalizeRelation = (relation, { shouldAddLink, mainFieldName, targetModel }) => {
6
6
  const nextRelation = { ...relation };
7
7
 
8
8
  if (shouldAddLink) {
@@ -22,6 +22,14 @@ const normalizeRelation = (relation, { shouldAddLink, mainFieldName, targetModel
22
22
  return nextRelation;
23
23
  };
24
24
 
25
+ /*
26
+ * Applies some transformations to existing and new relations in order to display them correctly
27
+ * relations: raw relations data coming from useRelations
28
+ * shouldAddLink: comes from generateRelationQueryInfos, if true we display a link to the relation (TO FIX: explanation)
29
+ * mainFieldName: name of the main field inside the relation (e.g. text field), if no displayable main field exists (e.g. date field) we use the id of the entry
30
+ * targetModel: the model on which the relation is based on, used to create an URL link
31
+ */
32
+
25
33
  export const normalizeRelations = (
26
34
  relations,
27
35
  { modifiedData = {}, shouldAddLink = false, mainFieldName, targetModel }
@@ -31,7 +39,7 @@ export const normalizeRelations = (
31
39
  data: {
32
40
  pages:
33
41
  [
34
- ...(relations?.data?.pages ?? []),
42
+ ...(relations?.data?.pages.reverse() ?? []),
35
43
  ...(modifiedData?.connect ? [{ results: modifiedData.connect }] : []),
36
44
  ]
37
45
  ?.map((page) =>
@@ -44,7 +52,6 @@ export const normalizeRelations = (
44
52
  )
45
53
  .map((relation) =>
46
54
  normalizeRelation(relation, {
47
- modifiedData,
48
55
  shouldAddLink,
49
56
  mainFieldName,
50
57
  targetModel,
@@ -0,0 +1,12 @@
1
+ import { normalizeRelation } from './normalizeRelations';
2
+
3
+ export const normalizeSearchResults = (relations, { mainFieldName }) => {
4
+ return {
5
+ ...relations,
6
+ data: {
7
+ pages: [...(relations?.data?.pages ?? [])]?.map((page) =>
8
+ page?.results.map((relation) => normalizeRelation(relation, { mainFieldName }))
9
+ ),
10
+ },
11
+ };
12
+ };
@@ -1,9 +1,16 @@
1
1
  import { useMemo } from 'react';
2
+ import get from 'lodash/get';
2
3
  import { useCMEditViewDataManager } from '@strapi/helper-plugin';
3
4
 
4
5
  import { getRequestUrl } from '../../../utils';
5
6
 
6
- function useSelect({ isUserAllowedToEditField, isUserAllowedToReadField, name, queryInfos }) {
7
+ function useSelect({
8
+ componentUid,
9
+ isUserAllowedToEditField,
10
+ isUserAllowedToReadField,
11
+ name,
12
+ queryInfos,
13
+ }) {
7
14
  const {
8
15
  isCreatingEntry,
9
16
  createActionAllowedFields,
@@ -11,7 +18,6 @@ function useSelect({ isUserAllowedToEditField, isUserAllowedToReadField, name, q
11
18
  updateActionAllowedFields,
12
19
  slug,
13
20
  initialData,
14
- isSingleType,
15
21
  } = useCMEditViewDataManager();
16
22
 
17
23
  const isFieldAllowed = useMemo(() => {
@@ -40,25 +46,42 @@ function useSelect({ isUserAllowedToEditField, isUserAllowedToReadField, name, q
40
46
  return allowedFields.includes(name);
41
47
  }, [isCreatingEntry, isUserAllowedToReadField, name, readActionAllowedFields]);
42
48
 
43
- // /content-manager/[collection-type]/[content-type]/[id]/[field-name]
44
- const relationFetchEndpoint = useMemo(() => {
45
- const collectionTypePrefix = isSingleType ? 'single-types' : 'collection-types';
49
+ const fieldNameKeys = name.split('.');
50
+ let componentId;
51
+
52
+ if (componentUid) {
53
+ componentId = get(initialData, fieldNameKeys.slice(0, -1))?.id;
54
+ }
46
55
 
56
+ // /content-manager/relations/[model]/[id]/[field-name]
57
+ const relationFetchEndpoint = useMemo(() => {
47
58
  if (isCreatingEntry) {
48
59
  return null;
49
60
  }
50
61
 
51
- return getRequestUrl(
52
- `${collectionTypePrefix}/${slug}/${initialData.id}/${name.split('.').at(-1)}`
53
- );
54
- }, [isCreatingEntry, slug, initialData, name, isSingleType]);
62
+ if (componentUid) {
63
+ // repeatable components and dz are dynamically created
64
+ // if no componentId exists in initialData it means that the user just created it
65
+ // there then are no relations to request
66
+ return componentId
67
+ ? getRequestUrl(`relations/${componentUid}/${componentId}/${fieldNameKeys.at(-1)}`)
68
+ : null;
69
+ }
55
70
 
56
- // /content-manager/relations/[content-type]/[field-name]
71
+ return getRequestUrl(`relations/${slug}/${initialData.id}/${name.split('.').at(-1)}`);
72
+ }, [isCreatingEntry, componentUid, slug, initialData.id, name, componentId, fieldNameKeys]);
73
+
74
+ // /content-manager/relations/[model]/[field-name]
57
75
  const relationSearchEndpoint = useMemo(() => {
76
+ if (componentUid) {
77
+ return getRequestUrl(`relations/${componentUid}/${name.split('.').at(-1)}`);
78
+ }
79
+
58
80
  return getRequestUrl(`relations/${slug}/${name.split('.').at(-1)}`);
59
- }, [slug, name]);
81
+ }, [componentUid, slug, name]);
60
82
 
61
83
  return {
84
+ componentId,
62
85
  queryInfos: {
63
86
  ...queryInfos,
64
87
  endpoints: {
@@ -44,6 +44,7 @@ const DragButton = styled.span`
44
44
 
45
45
  const DraggedItem = ({
46
46
  componentFieldName,
47
+ componentUid,
47
48
  // Errors are retrieved from the AccordionGroupCustom cloneElement
48
49
  hasErrorMessage,
49
50
  hasErrors,
@@ -265,11 +266,13 @@ const DraggedItem = ({
265
266
  return (
266
267
  <GridItem key={keys} col={size} s={12} xs={12}>
267
268
  <Inputs
269
+ componentUid={componentUid}
268
270
  fieldSchema={fieldSchema}
269
271
  keys={keys}
270
272
  metadatas={metadatas}
271
273
  // onBlur={hasErrors ? checkFormErrors : null}
272
274
  queryInfos={queryInfos}
275
+ size={size}
273
276
  />
274
277
  </GridItem>
275
278
  );
@@ -286,6 +289,7 @@ const DraggedItem = ({
286
289
  };
287
290
 
288
291
  DraggedItem.defaultProps = {
292
+ componentUid: undefined,
289
293
  isDraggingSibling: false,
290
294
  isOpen: false,
291
295
  setIsDraggingSibling() {},
@@ -294,6 +298,7 @@ DraggedItem.defaultProps = {
294
298
 
295
299
  DraggedItem.propTypes = {
296
300
  componentFieldName: PropTypes.string.isRequired,
301
+ componentUid: PropTypes.string,
297
302
  hasErrorMessage: PropTypes.bool.isRequired,
298
303
  hasErrors: PropTypes.bool.isRequired,
299
304
  isDraggingSibling: PropTypes.bool,
@@ -234,6 +234,28 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
234
234
  queryClient,
235
235
  ]
236
236
  );
237
+
238
+ const onDraftRelationCheck = useCallback(async () => {
239
+ try {
240
+ trackUsageRef.current('willCheckDraftRelations');
241
+
242
+ const endPoint = getRequestUrl(`${slug}/actions/numberOfDraftRelations`);
243
+ dispatch(setStatus('draft-relation-check-pending'));
244
+
245
+ const numberOfDraftRelations = await axiosInstance.get(endPoint);
246
+ trackUsageRef.current('didCheckDraftRelations');
247
+
248
+ dispatch(setStatus('resolved'));
249
+
250
+ return numberOfDraftRelations.data.data;
251
+ } catch (err) {
252
+ displayErrors(err);
253
+ dispatch(setStatus('resolved'));
254
+
255
+ return Promise.reject(err);
256
+ }
257
+ }, [displayErrors, slug, dispatch]);
258
+
237
259
  const onPublish = useCallback(async () => {
238
260
  try {
239
261
  trackUsageRef.current('willPublishEntry');
@@ -337,6 +359,7 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
337
359
  onDelete,
338
360
  onDeleteSucceeded,
339
361
  onPost,
362
+ onDraftRelationCheck,
340
363
  onPublish,
341
364
  onPut,
342
365
  onUnpublish,
@@ -22,18 +22,23 @@ export const useRelation = (cacheKey, { relation, search }) => {
22
22
  };
23
23
 
24
24
  const fetchSearch = async ({ pageParam = 1 }) => {
25
- const { data } = await axiosInstance.get(search.endpoint, {
26
- params: {
27
- ...(search.pageParams ?? {}),
28
- ...searchParams,
29
- page: pageParam,
30
- },
31
- });
25
+ try {
26
+ const { data } = await axiosInstance.get(search.endpoint, {
27
+ params: {
28
+ ...(search.pageParams ?? {}),
29
+ ...searchParams,
30
+ page: pageParam,
31
+ },
32
+ });
32
33
 
33
- return data;
34
+ return data;
35
+ } catch (err) {
36
+ return null;
37
+ }
34
38
  };
35
39
 
36
40
  const relationsRes = useInfiniteQuery(['relation', cacheKey], fetchRelations, {
41
+ cacheTime: 0,
37
42
  enabled: relation.enabled,
38
43
  getNextPageParam(lastPage) {
39
44
  // the API may send an empty 204 response
@@ -44,6 +49,9 @@ export const useRelation = (cacheKey, { relation, search }) => {
44
49
  // eslint-disable-next-line consistent-return
45
50
  return lastPage.pagination.page + 1;
46
51
  },
52
+ select: (data) => ({
53
+ pages: data.pages.map((page) => ({ ...page, results: [...(page.results ?? [])].reverse() })),
54
+ }),
47
55
  });
48
56
 
49
57
  const searchRes = useInfiniteQuery(
@@ -65,7 +73,7 @@ export const useRelation = (cacheKey, { relation, search }) => {
65
73
  const searchFor = (term, options = {}) => {
66
74
  setSearchParams({
67
75
  ...options,
68
- _q: encodeURIComponent(term),
76
+ _q: term,
69
77
  });
70
78
  };
71
79