@strapi/admin 4.5.0-beta.0 → 4.5.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 (233) hide show
  1. package/admin/src/StrapiApp.js +17 -6
  2. package/admin/src/assets/images/hot-air-balloon.png +0 -0
  3. package/admin/src/assets/images/icon_offline-cloud.svg +3 -3
  4. package/admin/src/assets/images/logo-strapi-2022.svg +7 -0
  5. package/admin/src/assets/images/upgrade-details.png +0 -0
  6. package/admin/src/content-manager/components/DynamicTable/CellContent/CellValue.js +1 -1
  7. package/admin/src/content-manager/components/DynamicTable/CellContent/RelationMultiple/index.js +5 -4
  8. package/admin/src/content-manager/components/DynamicTable/CellContent/index.js +10 -0
  9. package/admin/src/content-manager/components/DynamicTable/index.js +21 -4
  10. package/admin/src/content-manager/components/DynamicZone/index.js +6 -2
  11. package/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +103 -60
  12. package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +169 -162
  13. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/cleanData.js +70 -16
  14. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/findLeafByPathAndReplace.js +52 -0
  15. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/index.js +2 -0
  16. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/recursivelyFindPathsBasedOnCondition.js +72 -0
  17. package/admin/src/content-manager/components/FieldComponent/index.js +9 -2
  18. package/admin/src/content-manager/components/PreviewWysiwyg/index.js +1 -1
  19. package/admin/src/content-manager/components/RelationInput/RelationInput.js +80 -76
  20. package/admin/src/content-manager/components/RelationInputDataManager/RelationInputDataManager.js +95 -63
  21. package/admin/src/content-manager/components/RelationInputDataManager/utils/diffRelations.js +24 -0
  22. package/admin/src/content-manager/components/RelationInputDataManager/utils/index.js +2 -1
  23. package/admin/src/content-manager/components/RelationInputDataManager/utils/normalizeRelations.js +8 -29
  24. package/admin/src/content-manager/components/RelationInputDataManager/utils/normalizeSearchResults.js +8 -4
  25. package/admin/src/content-manager/components/RelationInputDataManager/utils/select.js +1 -0
  26. package/admin/src/content-manager/components/RepeatableComponent/index.js +4 -3
  27. package/admin/src/content-manager/hooks/__test__/usePrev.test.js +26 -0
  28. package/admin/src/content-manager/hooks/index.js +1 -0
  29. package/admin/src/content-manager/hooks/useFetchContentTypeLayout/utils/formatLayouts.js +19 -48
  30. package/admin/src/content-manager/hooks/usePrev.js +14 -0
  31. package/admin/src/content-manager/hooks/useRelation/useRelation.js +100 -7
  32. package/admin/src/content-manager/pages/App/reducer.js +3 -0
  33. package/admin/src/content-manager/pages/ListSettingsView/components/DraggableCard.js +3 -3
  34. package/admin/src/content-manager/pages/ListSettingsView/components/Settings.js +2 -2
  35. package/admin/src/content-manager/pages/ListSettingsView/components/SortDisplayedFields.js +1 -1
  36. package/admin/src/core/apis/CustomFields.js +0 -1
  37. package/admin/src/core/store/configureStore.js +17 -2
  38. package/admin/src/favicon.png +0 -0
  39. package/admin/src/hooks/useFetchMarketplacePlugins/index.js +2 -2
  40. package/admin/src/hooks/useFetchMarketplacePlugins/utils/api.js +4 -2
  41. package/admin/src/hooks/useFetchMarketplaceProviders/index.js +3 -3
  42. package/admin/src/hooks/useFetchMarketplaceProviders/utils/api.js +5 -3
  43. package/admin/src/index.js +1 -0
  44. package/admin/src/pages/App/index.js +1 -1
  45. package/admin/src/pages/HomePage/assets/corner-ornament.svg +48 -0
  46. package/admin/src/pages/HomePage/index.js +3 -2
  47. package/admin/src/pages/MarketplacePage/components/NpmPackageCard/CardButton.js +110 -0
  48. package/admin/src/pages/MarketplacePage/components/NpmPackageCard/InstallPluginButton.js +32 -21
  49. package/admin/src/pages/MarketplacePage/components/NpmPackageCard/PackageStats.js +79 -0
  50. package/admin/src/pages/MarketplacePage/components/NpmPackageCard/index.js +28 -11
  51. package/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FilterSelect.js +41 -0
  52. package/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FiltersPopover.js +96 -0
  53. package/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/index.js +106 -0
  54. package/admin/src/pages/MarketplacePage/components/NpmPackagesGrid/index.js +4 -0
  55. package/admin/src/pages/MarketplacePage/components/SortSelect/index.js +70 -0
  56. package/admin/src/pages/MarketplacePage/index.js +68 -8
  57. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormApiTokenContainer/index.js +5 -4
  58. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormBody/index.js +4 -3
  59. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormHead/index.js +6 -2
  60. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Regenerate/index.js +1 -1
  61. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/index.js +5 -4
  62. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/schema.js +1 -1
  63. package/admin/src/pages/SettingsPage/pages/Roles/EditPage/components/ConditionsModal/ActionRow/index.js +7 -38
  64. package/admin/src/pages/SettingsPage/pages/Roles/EditPage/components/ConditionsModal/ActionRow/utils/options.js +31 -0
  65. package/admin/src/pages/SettingsPage/pages/Roles/EditPage/components/ConditionsModal/index.js +32 -43
  66. package/admin/src/pages/SettingsPage/pages/Roles/EditPage/components/ContentTypeCollapse/Collapse/index.js +1 -1
  67. package/admin/src/pages/SettingsPage/pages/Roles/ListPage/components/RoleRow/index.js +3 -1
  68. package/admin/src/pages/SettingsPage/pages/Roles/ListPage/index.js +2 -1
  69. package/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/index.js +2 -2
  70. package/admin/src/translations/ca.json +1 -1
  71. package/admin/src/translations/de.json +1 -1
  72. package/admin/src/translations/dk.json +1 -1
  73. package/admin/src/translations/en.json +21 -7
  74. package/admin/src/translations/es.json +1 -1
  75. package/admin/src/translations/fr.json +1 -1
  76. package/admin/src/translations/gu.json +1 -1
  77. package/admin/src/translations/he.json +1 -1
  78. package/admin/src/translations/hi.json +1 -1
  79. package/admin/src/translations/hu.json +1 -1
  80. package/admin/src/translations/id.json +1 -1
  81. package/admin/src/translations/it.json +1 -1
  82. package/admin/src/translations/ja.json +1 -1
  83. package/admin/src/translations/ko.json +1 -1
  84. package/admin/src/translations/ml.json +1 -1
  85. package/admin/src/translations/nl.json +1 -1
  86. package/admin/src/translations/no.json +1 -1
  87. package/admin/src/translations/pl.json +1 -1
  88. package/admin/src/translations/pt-BR.json +1 -1
  89. package/admin/src/translations/ru.json +1 -1
  90. package/admin/src/translations/sa.json +1 -1
  91. package/admin/src/translations/sk.json +1 -1
  92. package/admin/src/translations/sv.json +1 -1
  93. package/admin/src/translations/th.json +1 -1
  94. package/admin/src/translations/zh-Hans.json +1 -1
  95. package/admin/src/translations/zh.json +1 -1
  96. package/build/1856.172d5fa0.chunk.js +174 -0
  97. package/build/2077.058590f4.chunk.js +206 -0
  98. package/build/2912.2c42c07b.chunk.js +259 -0
  99. package/build/4318.5e670740.chunk.js +30 -0
  100. package/build/4715.22747b59.chunk.js +387 -0
  101. package/build/{4800.d09f1225.chunk.js → 4800.a6935af6.chunk.js} +1 -1
  102. package/build/4982.1b75ddb1.chunk.js +325 -0
  103. package/build/617f9c948fa79e6d73bd.png +0 -0
  104. package/build/6d21938306785f176538.png +0 -0
  105. package/build/70674f63fc3904c20de0.svg +7 -0
  106. package/build/7692.a36fb2c2.chunk.js +470 -0
  107. package/build/7841.c50e9509.chunk.js +259 -0
  108. package/build/7866.ba215f99.chunk.js +505 -0
  109. package/build/7e9af4fb7e723fcebf1f.svg +48 -0
  110. package/build/8380.e53e7207.chunk.js +299 -0
  111. package/build/8549.832ed79d.chunk.js +159 -0
  112. package/build/8738.0fe8a61e.chunk.js +463 -0
  113. package/build/{9066.26faf397.chunk.js → 9066.eaf76ff3.chunk.js} +4 -4
  114. package/build/{9166.8fcb3019.chunk.js → 9166.90876521.chunk.js} +14 -13
  115. package/build/{9420.0fe11290.chunk.js → 9420.5292d1d2.chunk.js} +38 -37
  116. package/build/9649.468667d9.chunk.js +199 -0
  117. package/build/9d5d788027e86620c234.svg +5 -0
  118. package/build/Admin-authenticatedApp.c4f68103.chunk.js +80 -0
  119. package/build/{Admin_homePage.4b2be829.chunk.js → Admin_homePage.26d32e30.chunk.js} +5 -4
  120. package/build/Admin_marketplace.32375885.chunk.js +22 -0
  121. package/build/Admin_settingsPage.bf2234e1.chunk.js +178 -0
  122. package/build/admin-app.9049056c.chunk.js +112 -0
  123. package/build/{admin-edit-roles-page.4dd6bcb9.chunk.js → admin-edit-roles-page.69d9fcb2.chunk.js} +1 -1
  124. package/build/ca-json.07ae0f2c.chunk.js +1 -0
  125. package/build/content-manager.ff998bed.chunk.js +1204 -0
  126. package/build/content-type-builder-translation-sv-json.6deff030.chunk.js +1 -0
  127. package/build/{content-type-builder.a6e29716.chunk.js → content-type-builder.16af63a6.chunk.js} +13 -13
  128. package/build/de-json.6b3e1894.chunk.js +1 -0
  129. package/build/dk-json.144c6a8e.chunk.js +1 -0
  130. package/build/{email-settings-page.bfe6227f.chunk.js → email-settings-page.c3469093.chunk.js} +5 -5
  131. package/build/en-json.4a269f6b.chunk.js +1 -0
  132. package/build/es-json.6d123a82.chunk.js +1 -0
  133. package/build/fr-json.28ab54cb.chunk.js +1 -0
  134. package/build/gu-json.9a50ea64.chunk.js +1 -0
  135. package/build/he-json.72f18790.chunk.js +1 -0
  136. package/build/hi-json.0301b7ba.chunk.js +1 -0
  137. package/build/hu-json.c4b641bb.chunk.js +1 -0
  138. package/build/{i18n-settings-page.18166125.chunk.js → i18n-settings-page.46d894ff.chunk.js} +4 -4
  139. package/build/id-json.86035797.chunk.js +1 -0
  140. package/build/index.html +1 -1
  141. package/build/it-json.bbdc8993.chunk.js +1 -0
  142. package/build/ja-json.1c9eeeec.chunk.js +1 -0
  143. package/build/ko-json.e1f66398.chunk.js +1 -0
  144. package/build/main.91328e7a.js +9381 -0
  145. package/build/ml-json.963c889f.chunk.js +1 -0
  146. package/build/nl-json.2b8cc3a0.chunk.js +1 -0
  147. package/build/no-json.a58c28bd.chunk.js +1 -0
  148. package/build/pl-json.249626b3.chunk.js +1 -0
  149. package/build/pt-BR-json.7852f808.chunk.js +1 -0
  150. package/build/ru-json.d7cfc2ff.chunk.js +1 -0
  151. package/build/runtime~main.c9c319c0.js +2 -0
  152. package/build/sa-json.44e95991.chunk.js +1 -0
  153. package/build/sk-json.7ba4b330.chunk.js +1 -0
  154. package/build/sv-json.8e5a7911.chunk.js +1 -0
  155. package/build/th-json.a67309b1.chunk.js +1 -0
  156. package/build/{upload-settings.3d613216.chunk.js → upload-settings.53b690f3.chunk.js} +4 -4
  157. package/build/{users-advanced-settings-page.f4051d92.chunk.js → users-advanced-settings-page.3f4ee86e.chunk.js} +4 -4
  158. package/build/{webhook-edit-page.9e46fc3f.chunk.js → webhook-edit-page.dc9442ce.chunk.js} +1 -1
  159. package/build/webhook-list-page.02191138.chunk.js +134 -0
  160. package/build/zh-Hans-json.21617c24.chunk.js +1 -0
  161. package/build/zh-json.608aaf24.chunk.js +1 -0
  162. package/ee/admin/pages/SettingsPage/pages/Roles/ListPage/index.js +3 -2
  163. package/env.js +1 -0
  164. package/package.json +12 -11
  165. package/scripts/build.js +11 -0
  166. package/utils/create-plugins-exclude-path.js +40 -0
  167. package/webpack.alias.js +0 -13
  168. package/webpack.config.js +4 -1
  169. package/admin/src/assets/images/banner_strapi-rocket.png +0 -0
  170. package/admin/src/assets/images/big-logo-home.png +0 -0
  171. package/admin/src/assets/images/homepage-logo.png +0 -0
  172. package/admin/src/assets/images/icon_made-by-strapi.svg +0 -5
  173. package/admin/src/assets/images/logo_strapi_auth.png +0 -0
  174. package/admin/src/assets/images/logo_strapi_auth_v4.png +0 -0
  175. package/admin/src/assets/images/logo_strapi_menu.png +0 -0
  176. package/admin/src/assets/images/oops.png +0 -0
  177. package/admin/src/content-manager/components/State/index.js +0 -37
  178. package/admin/src/favicon.ico +0 -0
  179. package/build/15026a3d58aeb2828134.png +0 -0
  180. package/build/1856.d8f13391.chunk.js +0 -173
  181. package/build/1939.e3c87653.chunk.js +0 -325
  182. package/build/2077.31a2d91e.chunk.js +0 -205
  183. package/build/2912.ab68a736.chunk.js +0 -258
  184. package/build/4318.7d167b58.chunk.js +0 -30
  185. package/build/4715.44b1ef9b.chunk.js +0 -386
  186. package/build/4982.c2a311b7.chunk.js +0 -324
  187. package/build/7841.4b67af3f.chunk.js +0 -258
  188. package/build/7866.5fbeb7e5.chunk.js +0 -504
  189. package/build/8380.9b53a31d.chunk.js +0 -284
  190. package/build/8549.cf10b5d1.chunk.js +0 -158
  191. package/build/8738.a30a2160.chunk.js +0 -461
  192. package/build/90f49a385afb000fb1d4.svg +0 -5
  193. package/build/962.8651ba3f.chunk.js +0 -184
  194. package/build/Admin-authenticatedApp.883449a5.chunk.js +0 -80
  195. package/build/Admin_marketplace.82c0570b.chunk.js +0 -11
  196. package/build/Admin_settingsPage.98e2a62b.chunk.js +0 -178
  197. package/build/a6b842e0b6d2b61135d1.svg +0 -5
  198. package/build/admin-app.a61d5c2e.chunk.js +0 -112
  199. package/build/b997a22a2e0b87ef1fa2.ico +0 -0
  200. package/build/bd81ba6c07827282255d.png +0 -0
  201. package/build/c3de6118ef47086ad05c.png +0 -0
  202. package/build/ca-json.82df6eab.chunk.js +0 -1
  203. package/build/content-manager.933dc286.chunk.js +0 -1201
  204. package/build/de-json.0ad554eb.chunk.js +0 -1
  205. package/build/dk-json.e195ea1a.chunk.js +0 -1
  206. package/build/en-json.1889403c.chunk.js +0 -1
  207. package/build/es-json.09f80f6e.chunk.js +0 -1
  208. package/build/fb376b132d18bf4522ca.png +0 -0
  209. package/build/fde9b1ad0670d29a2516.png +0 -0
  210. package/build/fr-json.606d056b.chunk.js +0 -1
  211. package/build/gu-json.9881264f.chunk.js +0 -1
  212. package/build/he-json.3b825d80.chunk.js +0 -1
  213. package/build/hi-json.83dcf48f.chunk.js +0 -1
  214. package/build/hu-json.6f328bce.chunk.js +0 -1
  215. package/build/id-json.1f3c4303.chunk.js +0 -1
  216. package/build/it-json.494ac432.chunk.js +0 -1
  217. package/build/ja-json.6f262117.chunk.js +0 -1
  218. package/build/ko-json.36dc3b9a.chunk.js +0 -1
  219. package/build/main.63e7ea0a.js +0 -9338
  220. package/build/ml-json.9566bf9a.chunk.js +0 -1
  221. package/build/nl-json.94c3a289.chunk.js +0 -1
  222. package/build/no-json.40386397.chunk.js +0 -1
  223. package/build/pl-json.ccc6ef23.chunk.js +0 -1
  224. package/build/pt-BR-json.744f024d.chunk.js +0 -1
  225. package/build/ru-json.d22ea13c.chunk.js +0 -1
  226. package/build/runtime~main.3a5e1b07.js +0 -2
  227. package/build/sa-json.8fb1c04d.chunk.js +0 -1
  228. package/build/sk-json.6c7335d4.chunk.js +0 -1
  229. package/build/sv-json.2e589a7d.chunk.js +0 -1
  230. package/build/th-json.72e8de3d.chunk.js +0 -1
  231. package/build/webhook-list-page.a712ae40.chunk.js +0 -134
  232. package/build/zh-Hans-json.a4d7dc69.chunk.js +0 -1
  233. package/build/zh-json.66aa2ae1.chunk.js +0 -1
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable import/no-cycle */
2
- import React, { memo } from 'react';
2
+ import React, { memo, useMemo } from 'react';
3
3
  import PropTypes from 'prop-types';
4
4
  import size from 'lodash/size';
5
5
  import isEqual from 'react-fast-compare';
@@ -17,6 +17,7 @@ import RepeatableComponent from '../RepeatableComponent';
17
17
  import connect from './utils/connect';
18
18
  import select from './utils/select';
19
19
  import Label from './Label';
20
+ import { useContentTypeLayout } from '../../hooks';
20
21
 
21
22
  const FieldComponent = ({
22
23
  addNonRepeatableComponentToField,
@@ -46,6 +47,12 @@ const FieldComponent = ({
46
47
  const showResetComponent =
47
48
  !isRepeatable && isInitialized && !isFromDynamicZone && hasChildrenAllowedFields;
48
49
 
50
+ const { getComponentLayout, components } = useContentTypeLayout();
51
+ const componentLayoutData = useMemo(
52
+ () => getComponentLayout(componentUid),
53
+ [componentUid, getComponentLayout]
54
+ );
55
+
49
56
  if (!hasChildrenAllowedFields && isCreatingEntry) {
50
57
  return <NotAllowedInput labelAction={labelAction} intlLabel={intlLabel} name={name} />;
51
58
  }
@@ -55,7 +62,7 @@ const FieldComponent = ({
55
62
  }
56
63
 
57
64
  const handleClickAddNonRepeatableComponentToField = () => {
58
- addNonRepeatableComponentToField(name, componentUid);
65
+ addNonRepeatableComponentToField(name, componentLayoutData, components);
59
66
  };
60
67
 
61
68
  return (
@@ -13,7 +13,7 @@ import sanitizeHtml from './utils/satinizeHtml';
13
13
  import Wrapper from './Wrapper';
14
14
 
15
15
  const PreviewWysiwyg = ({ data }) => {
16
- const html = useMemo(() => sanitizeHtml(md.render(data || '')), [data]);
16
+ const html = useMemo(() => sanitizeHtml(md.render(data.replaceAll('\\n', '\n') || '')), [data]);
17
17
 
18
18
  return (
19
19
  <Wrapper>
@@ -4,7 +4,7 @@ import styled from 'styled-components';
4
4
  import { FixedSizeList as List } from 'react-window';
5
5
 
6
6
  import { ReactSelect } from '@strapi/helper-plugin';
7
- import { Badge } from '@strapi/design-system/Badge';
7
+ import { Status } from '@strapi/design-system/Status';
8
8
  import { Box } from '@strapi/design-system/Box';
9
9
  import { Link } from '@strapi/design-system/Link';
10
10
  import { Icon } from '@strapi/design-system/Icon';
@@ -21,6 +21,7 @@ import { RelationItem } from './components/RelationItem';
21
21
  import { RelationList } from './components/RelationList';
22
22
  import { Option } from './components/Option';
23
23
  import { RELATION_ITEM_HEIGHT } from './constants';
24
+ import { usePrev } from '../../hooks';
24
25
 
25
26
  const LinkEllipsis = styled(Link)`
26
27
  white-space: nowrap;
@@ -61,12 +62,11 @@ const RelationInput = ({
61
62
  labelLoadMore,
62
63
  labelDisconnectRelation,
63
64
  loadingMessage,
64
- onRelationAdd,
65
+ noRelationsMessage,
66
+ onRelationConnect,
65
67
  onRelationLoadMore,
66
- onRelationRemove,
67
- onSearchClose,
68
+ onRelationDisconnect,
68
69
  onSearchNextPage,
69
- onSearchOpen,
70
70
  onSearch,
71
71
  placeholder,
72
72
  publicationStateTranslations,
@@ -80,11 +80,9 @@ const RelationInput = ({
80
80
  const outerListRef = useRef();
81
81
  const [overflow, setOverflow] = useState('');
82
82
 
83
- const {
84
- data: { pages },
85
- } = searchResults;
83
+ const { data } = searchResults;
86
84
 
87
- const relations = useMemo(() => paginatedRelations.data.pages.flat(), [paginatedRelations]);
85
+ const relations = paginatedRelations.data;
88
86
  const totalNumberOfRelations = relations.length ?? 0;
89
87
 
90
88
  const dynamicListHeight = useMemo(
@@ -102,12 +100,15 @@ const RelationInput = ({
102
100
 
103
101
  const options = useMemo(
104
102
  () =>
105
- pages.flat().map((result) => ({
106
- ...result,
107
- value: result.id,
108
- label: result.mainField,
109
- })),
110
- [pages]
103
+ data
104
+ .flat()
105
+ .filter(Boolean)
106
+ .map((result) => ({
107
+ ...result,
108
+ value: result.id,
109
+ label: result.mainField,
110
+ })),
111
+ [data]
111
112
  );
112
113
 
113
114
  useEffect(() => {
@@ -197,17 +198,38 @@ const RelationInput = ({
197
198
 
198
199
  const handleMenuClose = () => {
199
200
  setIsMenuOpen(false);
200
-
201
- if (onSearchClose) {
202
- onSearchClose();
203
- }
204
201
  };
205
202
 
206
203
  const handleMenuOpen = () => {
207
204
  setIsMenuOpen(true);
208
- onSearchOpen();
205
+ onSearch();
206
+ };
207
+
208
+ const previewRelationsLength = usePrev(relations.length);
209
+ /**
210
+ * @type {React.MutableRefObject<'onChange' | 'loadMore'>}
211
+ */
212
+ const updatedRelationsWith = useRef();
213
+
214
+ const handleLoadMore = () => {
215
+ updatedRelationsWith.current = 'loadMore';
216
+ onRelationLoadMore();
209
217
  };
210
218
 
219
+ useEffect(() => {
220
+ if (
221
+ updatedRelationsWith.current === 'onChange' &&
222
+ relations.length !== previewRelationsLength
223
+ ) {
224
+ listRef.current.scrollToItem(relations.length, 'end');
225
+ } else if (
226
+ updatedRelationsWith.current === 'loadMore' &&
227
+ relations.length !== previewRelationsLength
228
+ ) {
229
+ listRef.current.scrollToItem(0, 'start');
230
+ }
231
+ }, [previewRelationsLength, relations]);
232
+
211
233
  return (
212
234
  <Field error={error} name={name} hint={description} id={id}>
213
235
  <Relation
@@ -231,17 +253,11 @@ const RelationInput = ({
231
253
  inputId={id}
232
254
  isSearchable
233
255
  isClear
234
- loadingMessage={loadingMessage}
256
+ loadingMessage={() => loadingMessage}
235
257
  onChange={(relation) => {
236
258
  setValue(null);
237
- onRelationAdd(relation);
238
-
239
- // scroll to the end of the list
240
- if (relations.length > 0) {
241
- setTimeout(() => {
242
- listRef.current.scrollToItem(relations.length, 'end');
243
- });
244
- }
259
+ onRelationConnect(relation);
260
+ updatedRelationsWith.current = 'onChange';
245
261
  }}
246
262
  onInputChange={(value) => {
247
263
  setValue(value);
@@ -250,6 +266,7 @@ const RelationInput = ({
250
266
  onMenuClose={handleMenuClose}
251
267
  onMenuOpen={handleMenuOpen}
252
268
  menuIsOpen={isMenuOpen}
269
+ noOptionsMessage={() => noRelationsMessage}
253
270
  onMenuScrollToBottom={() => {
254
271
  if (searchResults.hasNextPage) {
255
272
  onSearchNextPage();
@@ -265,7 +282,7 @@ const RelationInput = ({
265
282
  shouldDisplayLoadMoreButton && (
266
283
  <TextButton
267
284
  disabled={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
268
- onClick={() => onRelationLoadMore()}
285
+ onClick={handleLoadMore}
269
286
  loading={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
270
287
  startIcon={<Refresh />}
271
288
  >
@@ -286,7 +303,7 @@ const RelationInput = ({
286
303
  >
287
304
  {({ data, index, style }) => {
288
305
  const { publicationState, href, mainField, id } = data[index];
289
- const badgeColor = publicationState === 'draft' ? 'secondary' : 'success';
306
+ const statusColor = publicationState === 'draft' ? 'secondary' : 'success';
290
307
 
291
308
  return (
292
309
  <RelationItem
@@ -297,7 +314,7 @@ const RelationInput = ({
297
314
  data-testid={`remove-relation-${id}`}
298
315
  disabled={disabled}
299
316
  type="button"
300
- onClick={() => onRelationRemove(data[index])}
317
+ onClick={() => onRelationDisconnect(data[index])}
301
318
  aria-label={labelDisconnectRelation}
302
319
  >
303
320
  <Icon width="12px" as={Cross} />
@@ -320,15 +337,11 @@ const RelationInput = ({
320
337
  </BoxEllipsis>
321
338
 
322
339
  {publicationState && (
323
- <Badge
324
- borderSize={1}
325
- borderColor={`${badgeColor}200`}
326
- backgroundColor={`${badgeColor}100`}
327
- textColor={`${badgeColor}700`}
328
- shrink={0}
329
- >
330
- {publicationStateTranslations[publicationState]}
331
- </Badge>
340
+ <Status variant={statusColor} showBullet={false} size="S">
341
+ <Typography fontWeight="bold" textColor={`${statusColor}700`}>
342
+ {publicationStateTranslations[publicationState]}
343
+ </Typography>
344
+ </Status>
332
345
  )}
333
346
  </RelationItem>
334
347
  );
@@ -346,37 +359,30 @@ const RelationInput = ({
346
359
  );
347
360
  };
348
361
 
349
- const ReactQueryRelationResult = PropTypes.shape({
350
- data: PropTypes.shape({
351
- pages: PropTypes.arrayOf(
352
- PropTypes.arrayOf(
353
- PropTypes.shape({
354
- href: PropTypes.string,
355
- id: PropTypes.number.isRequired,
356
- publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
357
- mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
358
- })
359
- )
360
- ),
361
- }),
362
+ const RelationsResult = PropTypes.shape({
363
+ data: PropTypes.arrayOf(
364
+ PropTypes.shape({
365
+ href: PropTypes.string,
366
+ id: PropTypes.number.isRequired,
367
+ publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
368
+ mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
369
+ })
370
+ ),
362
371
  hasNextPage: PropTypes.bool,
363
372
  isFetchingNextPage: PropTypes.bool.isRequired,
364
373
  isLoading: PropTypes.bool.isRequired,
365
374
  isSuccess: PropTypes.bool.isRequired,
366
375
  });
367
376
 
368
- const ReactQuerySearchResult = PropTypes.shape({
369
- data: PropTypes.shape({
370
- pages: PropTypes.arrayOf(
371
- PropTypes.arrayOf(
372
- PropTypes.shape({
373
- id: PropTypes.number.isRequired,
374
- mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
375
- publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
376
- })
377
- )
378
- ),
379
- }),
377
+ const SearchResults = PropTypes.shape({
378
+ data: PropTypes.arrayOf(
379
+ PropTypes.shape({
380
+ id: PropTypes.number.isRequired,
381
+ href: PropTypes.string,
382
+ mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
383
+ publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
384
+ })
385
+ ),
380
386
  hasNextPage: PropTypes.bool,
381
387
  isLoading: PropTypes.bool.isRequired,
382
388
  isSuccess: PropTypes.bool.isRequired,
@@ -388,10 +394,9 @@ RelationInput.defaultProps = {
388
394
  error: undefined,
389
395
  labelAction: null,
390
396
  labelLoadMore: null,
391
- onSearchClose: undefined,
392
397
  required: false,
393
- relations: [],
394
- searchResults: [],
398
+ relations: { data: [] },
399
+ searchResults: { data: [] },
395
400
  };
396
401
 
397
402
  RelationInput.propTypes = {
@@ -403,25 +408,24 @@ RelationInput.propTypes = {
403
408
  labelAction: PropTypes.element,
404
409
  labelLoadMore: PropTypes.string,
405
410
  labelDisconnectRelation: PropTypes.string.isRequired,
406
- loadingMessage: PropTypes.func.isRequired,
411
+ loadingMessage: PropTypes.string.isRequired,
407
412
  name: PropTypes.string.isRequired,
413
+ noRelationsMessage: PropTypes.string.isRequired,
408
414
  numberOfRelationsToDisplay: PropTypes.number.isRequired,
409
- onRelationAdd: PropTypes.func.isRequired,
410
- onRelationRemove: PropTypes.func.isRequired,
415
+ onRelationConnect: PropTypes.func.isRequired,
416
+ onRelationDisconnect: PropTypes.func.isRequired,
411
417
  onRelationLoadMore: PropTypes.func.isRequired,
412
418
  onSearch: PropTypes.func.isRequired,
413
419
  onSearchNextPage: PropTypes.func.isRequired,
414
- onSearchClose: PropTypes.func,
415
- onSearchOpen: PropTypes.func.isRequired,
416
420
  placeholder: PropTypes.string.isRequired,
417
421
  publicationStateTranslations: PropTypes.shape({
418
422
  draft: PropTypes.string.isRequired,
419
423
  published: PropTypes.string.isRequired,
420
424
  }).isRequired,
421
425
  required: PropTypes.bool,
422
- searchResults: ReactQuerySearchResult,
426
+ searchResults: SearchResults,
423
427
  size: PropTypes.number.isRequired,
424
- relations: ReactQueryRelationResult,
428
+ relations: RelationsResult,
425
429
  };
426
430
 
427
431
  export default RelationInput;
@@ -1,18 +1,25 @@
1
+ /* eslint-disable no-nested-ternary */
1
2
  import PropTypes from 'prop-types';
2
- import React, { memo, useEffect, useMemo } from 'react';
3
+ import React, { memo, useMemo } from 'react';
3
4
  import { useIntl } from 'react-intl';
4
5
  import get from 'lodash/get';
6
+ import pick from 'lodash/pick';
5
7
 
6
8
  import { useCMEditViewDataManager, NotAllowedInput } from '@strapi/helper-plugin';
7
9
 
8
10
  import { RelationInput } from '../RelationInput';
11
+
9
12
  import { useRelation } from '../../hooks/useRelation';
10
- import { connect, select, normalizeRelations, normalizeSearchResults } from './utils';
11
- import { PUBLICATION_STATES, RELATIONS_TO_DISPLAY, SEARCH_RESULTS_TO_DISPLAY } from './constants';
13
+
12
14
  import { getTrad } from '../../utils';
13
15
 
16
+ import { PUBLICATION_STATES, RELATIONS_TO_DISPLAY, SEARCH_RESULTS_TO_DISPLAY } from './constants';
17
+ import { connect, select, normalizeSearchResults, diffRelations, normalizeRelation } from './utils';
18
+
14
19
  export const RelationInputDataManager = ({
20
+ error,
15
21
  componentId,
22
+ isComponentRelation,
16
23
  editable,
17
24
  description,
18
25
  intlLabel,
@@ -33,58 +40,41 @@ export const RelationInputDataManager = ({
33
40
  const { connectRelation, disconnectRelation, loadRelation, modifiedData, slug, initialData } =
34
41
  useCMEditViewDataManager();
35
42
 
43
+ const relationsFromModifiedData = get(modifiedData, name) ?? [];
44
+
45
+ const currentLastPage = Math.ceil(get(initialData, name, []).length / RELATIONS_TO_DISPLAY);
46
+
36
47
  const { relations, search, searchFor } = useRelation(`${slug}-${name}-${initialData?.id ?? ''}`, {
48
+ name,
37
49
  relation: {
38
- enabled: get(initialData, name)?.count !== 0 && !!endpoints.relation,
50
+ enabled: !!endpoints.relation,
39
51
  endpoint: endpoints.relation,
52
+ pageGoal: currentLastPage,
40
53
  pageParams: {
41
54
  ...defaultParams,
42
55
  pageSize: RELATIONS_TO_DISPLAY,
43
56
  },
57
+ onLoad: loadRelation,
58
+ normalizeArguments: {
59
+ mainFieldName: mainField.name,
60
+ shouldAddLink: shouldDisplayRelationLink,
61
+ targetModel,
62
+ },
44
63
  },
45
64
 
46
65
  search: {
47
66
  endpoint: endpoints.search,
48
67
  pageParams: {
49
68
  ...defaultParams,
50
- entityId: isCreatingEntry ? undefined : componentId ?? initialData.id,
69
+ // eslint-disable-next-line no-nested-ternary
70
+ entityId: isCreatingEntry ? undefined : isComponentRelation ? componentId : initialData.id,
51
71
  pageSize: SEARCH_RESULTS_TO_DISPLAY,
52
72
  },
53
73
  },
54
74
  });
55
75
 
56
- const relationsFromModifiedData = get(modifiedData, name);
57
- const stringifiedRelations = JSON.stringify(relations);
58
- const normalizedRelations = useMemo(
59
- () =>
60
- normalizeRelations(relations, {
61
- modifiedData: relationsFromModifiedData,
62
- mainFieldName: mainField.name,
63
- shouldAddLink: shouldDisplayRelationLink,
64
- targetModel,
65
- }),
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
- [
68
- stringifiedRelations,
69
- modifiedData,
70
- name,
71
- mainField.name,
72
- shouldDisplayRelationLink,
73
- targetModel,
74
- ]
75
- );
76
-
77
- useEffect(() => {
78
- if (relations.status === 'success') {
79
- loadRelation({
80
- target: { name, value: normalizedRelations.data.pages.flat() },
81
- });
82
- }
83
- // eslint-disable-next-line react-hooks/exhaustive-deps
84
- }, [loadRelation, relations.status, stringifiedRelations, name]);
85
-
86
76
  const isMorph = useMemo(() => relationType.toLowerCase().includes('morph'), [relationType]);
87
- const isSingleRelation = [
77
+ const toOneRelation = [
88
78
  'oneWay',
89
79
  'oneToOne',
90
80
  'manyToOne',
@@ -104,29 +94,36 @@ export const RelationInputDataManager = ({
104
94
  return !editable;
105
95
  }, [isMorph, isCreatingEntry, editable, isFieldAllowed, isFieldReadable]);
106
96
 
107
- const handleRelationAdd = (relation) => {
108
- connectRelation({ target: { name, value: relation, replace: isSingleRelation } });
97
+ const handleRelationConnect = (relation) => {
98
+ /**
99
+ * Any relation being added to the store should be normalized so it has it's link.
100
+ */
101
+ const normalizedRelation = normalizeRelation(relation, {
102
+ mainFieldName: mainField.name,
103
+ shouldAddLink: shouldDisplayRelationLink,
104
+ targetModel,
105
+ });
106
+
107
+ connectRelation({ name, value: normalizedRelation, toOneRelation });
109
108
  };
110
109
 
111
- const handleRelationRemove = (relation) => {
112
- disconnectRelation({ target: { name, value: relation } });
110
+ const handleRelationDisconnect = (relation) => {
111
+ disconnectRelation({ name, id: relation.id });
113
112
  };
114
113
 
115
114
  const handleRelationLoadMore = () => {
116
115
  relations.fetchNextPage();
117
116
  };
118
117
 
119
- const handleSearch = (term) => {
120
- searchFor(term, {
121
- idsToInclude: relationsFromModifiedData?.disconnect?.map((relation) => relation.id),
122
- idsToOmit: relationsFromModifiedData?.connect?.map((relation) => relation.id),
123
- });
124
- };
118
+ const handleSearch = (term = '') => {
119
+ const [connected, disconnected] = diffRelations(
120
+ relationsFromModifiedData,
121
+ get(initialData, name)
122
+ );
125
123
 
126
- const handleOpenSearch = () => {
127
- searchFor('', {
128
- idsToInclude: relationsFromModifiedData?.disconnect?.map((relation) => relation.id),
129
- idsToOmit: relationsFromModifiedData?.connect?.map((relation) => relation.id),
124
+ searchFor(term, {
125
+ idsToInclude: disconnected,
126
+ idsToOmit: connected,
130
127
  });
131
128
  };
132
129
 
@@ -138,18 +135,41 @@ export const RelationInputDataManager = ({
138
135
  (!isFieldAllowed && isCreatingEntry) ||
139
136
  (!isCreatingEntry && !isFieldAllowed && !isFieldReadable)
140
137
  ) {
141
- return <NotAllowedInput intlLabel={intlLabel} labelAction={labelAction} />;
138
+ return <NotAllowedInput name={name} intlLabel={intlLabel} labelAction={labelAction} />;
142
139
  }
143
140
 
141
+ /**
142
+ * How to calculate the total number of relations even if you don't
143
+ * have them all loaded in the browser.
144
+ *
145
+ * 1. The `infiniteQuery` gives you the total number of relations in the pagination result.
146
+ * 2. You can diff the length of the browserState vs the fetchedServerState to determine if you've
147
+ * either added or removed relations.
148
+ * 3. Add them together, if you've removed relations you'll get a negative number and it'll
149
+ * actually subtract from the total number on the server (regardless of how many you fetched).
150
+ */
151
+ const browserRelationsCount = relationsFromModifiedData.length;
152
+ const serverRelationsCount = (get(initialData, name) ?? []).length;
153
+ const realServerRelationsCount = relations.data?.pages[0]?.pagination?.total ?? 0;
154
+ /**
155
+ * _IF_ theres no relations data and the browserCount is the same as serverCount you can therefore assume
156
+ * that the browser count is correct because we've just _made_ this entry and the in-component hook is now fetching.
157
+ */
158
+ const totalRelations =
159
+ !relations.data && browserRelationsCount === serverRelationsCount
160
+ ? browserRelationsCount
161
+ : browserRelationsCount - serverRelationsCount + realServerRelationsCount;
162
+
144
163
  return (
145
164
  <RelationInput
165
+ error={error}
146
166
  description={description}
147
167
  disabled={isDisabled}
148
168
  id={name}
149
169
  label={`${formatMessage({
150
170
  id: intlLabel.id,
151
171
  defaultMessage: intlLabel.defaultMessage,
152
- })} ${initialData[name]?.count !== undefined ? `(${initialData[name].count})` : ''}`}
172
+ })} ${totalRelations > 0 ? `(${totalRelations})` : ''}`}
153
173
  labelAction={labelAction}
154
174
  labelLoadMore={
155
175
  !isCreatingEntry
@@ -164,20 +184,21 @@ export const RelationInputDataManager = ({
164
184
  defaultMessage: 'Remove',
165
185
  })}
166
186
  listHeight={320}
167
- loadingMessage={() =>
168
- formatMessage({
169
- id: getTrad('relation.isLoading'),
170
- defaultMessage: 'Relations are loading',
171
- })
172
- }
187
+ loadingMessage={formatMessage({
188
+ id: getTrad('relation.isLoading'),
189
+ defaultMessage: 'Relations are loading',
190
+ })}
173
191
  name={name}
192
+ noRelationsMessage={formatMessage({
193
+ id: getTrad('relation.notAvailable'),
194
+ defaultMessage: 'No relations available',
195
+ })}
174
196
  numberOfRelationsToDisplay={RELATIONS_TO_DISPLAY}
175
- onRelationAdd={(relation) => handleRelationAdd(relation)}
176
- onRelationRemove={(relation) => handleRelationRemove(relation)}
197
+ onRelationConnect={(relation) => handleRelationConnect(relation)}
198
+ onRelationDisconnect={(relation) => handleRelationDisconnect(relation)}
177
199
  onRelationLoadMore={() => handleRelationLoadMore()}
178
200
  onSearch={(term) => handleSearch(term)}
179
201
  onSearchNextPage={() => handleSearchMore()}
180
- onSearchOpen={handleOpenSearch}
181
202
  placeholder={formatMessage(
182
203
  placeholder || {
183
204
  id: getTrad('relation.add'),
@@ -195,7 +216,14 @@ export const RelationInputDataManager = ({
195
216
  defaultMessage: 'Published',
196
217
  }),
197
218
  }}
198
- relations={normalizedRelations}
219
+ relations={pick(
220
+ { ...relations, data: relationsFromModifiedData },
221
+ 'data',
222
+ 'hasNextPage',
223
+ 'isFetchingNextPage',
224
+ 'isLoading',
225
+ 'isSuccess'
226
+ )}
199
227
  required={required}
200
228
  searchResults={normalizeSearchResults(search, {
201
229
  mainFieldName: mainField.name,
@@ -208,8 +236,10 @@ export const RelationInputDataManager = ({
208
236
  RelationInputDataManager.defaultProps = {
209
237
  componentId: undefined,
210
238
  editable: true,
239
+ error: undefined,
211
240
  description: '',
212
241
  labelAction: null,
242
+ isComponentRelation: false,
213
243
  isFieldAllowed: true,
214
244
  placeholder: null,
215
245
  required: false,
@@ -218,6 +248,7 @@ RelationInputDataManager.defaultProps = {
218
248
  RelationInputDataManager.propTypes = {
219
249
  componentId: PropTypes.number,
220
250
  editable: PropTypes.bool,
251
+ error: PropTypes.string,
221
252
  description: PropTypes.string,
222
253
  intlLabel: PropTypes.shape({
223
254
  id: PropTypes.string.isRequired,
@@ -226,6 +257,7 @@ RelationInputDataManager.propTypes = {
226
257
  }).isRequired,
227
258
  labelAction: PropTypes.element,
228
259
  isCreatingEntry: PropTypes.bool.isRequired,
260
+ isComponentRelation: PropTypes.bool,
229
261
  isFieldAllowed: PropTypes.bool,
230
262
  isFieldReadable: PropTypes.bool.isRequired,
231
263
  mainField: PropTypes.shape({
@@ -246,7 +278,7 @@ RelationInputDataManager.propTypes = {
246
278
  targetModel: PropTypes.string.isRequired,
247
279
  queryInfos: PropTypes.shape({
248
280
  defaultParams: PropTypes.shape({
249
- _component: PropTypes.string,
281
+ locale: PropTypes.string,
250
282
  }),
251
283
  endpoints: PropTypes.shape({
252
284
  relation: PropTypes.string,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @param {Array<{id: string}>} browserStateRelations
3
+ * @param {Array<{id: string}>} serverStateRelations
4
+ * @returns {[connected: string[], disconnected: string[]]} – the connected and disconnected relations ids
5
+ */
6
+ export const diffRelations = (browserStateRelations = [], serverStateRelations = []) => {
7
+ const connected = browserStateRelations.reduce((acc, relation) => {
8
+ if (!serverStateRelations.find((oldRelation) => oldRelation.id === relation.id)) {
9
+ return [...acc, relation.id];
10
+ }
11
+
12
+ return acc;
13
+ }, []);
14
+
15
+ const disconnected = serverStateRelations.reduce((acc, relation) => {
16
+ if (!browserStateRelations.find((oldRelation) => oldRelation.id === relation.id)) {
17
+ return [...acc, relation.id];
18
+ }
19
+
20
+ return acc;
21
+ }, []);
22
+
23
+ return [connected, disconnected];
24
+ };
@@ -1,4 +1,5 @@
1
1
  export { default as connect } from './connect';
2
2
  export { default as select } from './select';
3
- export { normalizeRelations } from './normalizeRelations';
3
+ export { normalizeRelations, normalizeRelation } from './normalizeRelations';
4
4
  export { normalizeSearchResults } from './normalizeSearchResults';
5
+ export { diffRelations } from './diffRelations';