@strapi/admin 4.14.2 → 4.14.4

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 (173) hide show
  1. package/.eslintrc.js +4 -1
  2. package/admin/.eslintrc.js +16 -0
  3. package/admin/custom.d.ts +8 -0
  4. package/admin/src/components/AuthenticatedApp/index.js +3 -7
  5. package/admin/src/components/AuthenticatedApp/utils/api.js +1 -39
  6. package/admin/src/components/AuthenticatedApp/utils/checkLatestStrapiVersion.ts +13 -0
  7. package/admin/src/{hooks/useReleaseNotification/utils/api.js → components/AuthenticatedApp/utils/fetchStrapiLatestRelease.ts} +2 -3
  8. package/admin/src/components/{DragLayer/DragLayer.js → DragLayer.tsx} +18 -10
  9. package/admin/src/components/PrivateRoute.tsx +42 -0
  10. package/admin/src/components/Providers/index.js +2 -2
  11. package/admin/src/components/Theme.tsx +39 -0
  12. package/admin/src/components/ThemeToggleProvider.tsx +50 -0
  13. package/admin/src/components/{UnauthenticatedLogo/index.js → UnauthenticatedLogo.tsx} +2 -4
  14. package/admin/src/content-manager/components/BlocksEditor/BlocksInput/index.js +22 -3
  15. package/admin/src/content-manager/components/BlocksEditor/Toolbar/index.js +263 -134
  16. package/admin/src/content-manager/components/BlocksEditor/hooks/useBlocksStore.js +362 -95
  17. package/admin/src/content-manager/components/BlocksEditor/hooks/useModifiersStore.js +15 -0
  18. package/admin/src/content-manager/components/BlocksEditor/index.js +99 -9
  19. package/admin/src/content-manager/components/BlocksEditor/plugins/index.js +4 -0
  20. package/admin/src/content-manager/components/BlocksEditor/plugins/withLinks.js +61 -0
  21. package/admin/src/content-manager/components/BlocksEditor/plugins/withStrapiSchema.js +33 -0
  22. package/admin/src/content-manager/components/BlocksEditor/utils/links.js +90 -0
  23. package/admin/src/content-manager/components/InputUID/index.js +1 -1
  24. package/admin/src/content-manager/hooks/useAllowedAttributes.js +9 -1
  25. package/admin/src/content-manager/hooks/useRelation/useRelation.js +1 -0
  26. package/admin/src/content-manager/pages/EditSettingsView/index.js +1 -0
  27. package/admin/src/content-manager/pages/EditSettingsView/utils/createPossibleMainFieldsForModelsAndComponents.js +1 -0
  28. package/admin/src/content-manager/pages/ListSettingsView/constants.js +1 -0
  29. package/admin/src/content-manager/pages/ListView/index.js +2 -1
  30. package/admin/src/content-manager/utils/checkIfAttributeIsDisplayable.js +1 -1
  31. package/admin/src/content-manager/utils/schema.js +2 -2
  32. package/admin/src/contexts/configuration.ts +15 -0
  33. package/admin/src/contexts/index.js +1 -2
  34. package/admin/src/contexts/themeToggle.ts +16 -0
  35. package/admin/src/hooks/{useConfigurations/__mocks__/index.js → __mocks__/useConfigurations.ts} +4 -2
  36. package/admin/src/hooks/index.js +1 -5
  37. package/admin/src/hooks/useConfigurations.ts +5 -0
  38. package/admin/src/hooks/useDebounce.ts +17 -0
  39. package/admin/src/hooks/useLicenseLimitNotification.ts +3 -0
  40. package/admin/src/hooks/useThemeToggle.ts +9 -0
  41. package/admin/src/pages/App/index.js +1 -1
  42. package/admin/src/pages/AuthPage/components/ForgotPassword/index.js +1 -1
  43. package/admin/src/pages/AuthPage/components/ForgotPasswordSuccess/index.js +1 -1
  44. package/admin/src/pages/AuthPage/components/Login/BaseLogin.js +1 -1
  45. package/admin/src/pages/AuthPage/components/Oops/index.js +1 -1
  46. package/admin/src/pages/AuthPage/components/Register/index.js +1 -1
  47. package/admin/src/pages/AuthPage/components/ResetPassword/index.js +1 -1
  48. package/admin/src/pages/MarketplacePage/components/NpmPackageCard/index.js +0 -2
  49. package/admin/src/pages/MarketplacePage/hooks/__mocks__/useNavigatorOnline.ts +1 -0
  50. package/admin/src/{hooks/useNavigatorOnLine/index.js → pages/MarketplacePage/hooks/useNavigatorOnline.ts} +4 -6
  51. package/admin/src/pages/MarketplacePage/index.js +3 -3
  52. package/admin/src/pages/ProfilePage/index.js +1 -1
  53. package/admin/src/pages/SettingsPage/components/Tokens/Regenerate/index.js +1 -1
  54. package/admin/src/{hooks/useRegenerate/index.js → pages/SettingsPage/hooks/useRegenerate.ts} +13 -7
  55. package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Regenerate/index.js +1 -1
  56. package/admin/src/pages/UseCasePage/index.js +1 -1
  57. package/admin/src/translations/en.json +8 -0
  58. package/admin/tsconfig.json +5 -0
  59. package/build/1049.f7aed23d.chunk.js +1 -0
  60. package/build/{1227.969e24e6.chunk.js → 1227.f9c74718.chunk.js} +1 -1
  61. package/build/{1386.db9a2795.chunk.js → 1386.6b8819c6.chunk.js} +2 -2
  62. package/build/2224.8af54440.chunk.js +138 -0
  63. package/build/2225.d1bcf7e3.chunk.js +79 -0
  64. package/build/2379.f0baf826.chunk.js +1 -0
  65. package/build/{2395.f6ac2863.chunk.js → 2395.aca6ce66.chunk.js} +1 -1
  66. package/build/2421.a478ba24.chunk.js +105 -0
  67. package/build/2801.c49f88a1.chunk.js +1 -0
  68. package/build/{3483.f6b2439f.chunk.js → 3483.5df8e010.chunk.js} +1 -1
  69. package/build/3911.d4fada48.chunk.js +95 -0
  70. package/build/412.72afdf0c.chunk.js +689 -0
  71. package/build/{4174.3e13fb26.chunk.js → 4174.df9aa09a.chunk.js} +1 -1
  72. package/build/502.8666bbef.chunk.js +25 -0
  73. package/build/570.2f3b4c56.chunk.js +1 -0
  74. package/build/5702.5b433d50.chunk.js +1 -0
  75. package/build/6186.c33ce082.chunk.js +116 -0
  76. package/build/7464.43a4527c.chunk.js +1 -0
  77. package/build/7818.d2196a53.chunk.js +29 -0
  78. package/build/7897.5c03247b.chunk.js +25 -0
  79. package/build/{8276.951e198e.chunk.js → 8276.d4426fd8.chunk.js} +3 -3
  80. package/build/8690.33243bba.chunk.js +38 -0
  81. package/build/{9832.65ed5a44.chunk.js → 8743.31c921b1.chunk.js} +139 -123
  82. package/build/9218.8bc01ab9.chunk.js +1 -0
  83. package/build/Admin-authenticatedApp.27545a1b.chunk.js +112 -0
  84. package/build/{Admin_InternalErrorPage.b3163562.chunk.js → Admin_InternalErrorPage.b66ee9c1.chunk.js} +1 -1
  85. package/build/Admin_homePage.a6281dd6.chunk.js +124 -0
  86. package/build/Admin_marketplace.31b962b8.chunk.js +44 -0
  87. package/build/{Admin_pluginsPage.b9fa2947.chunk.js → Admin_pluginsPage.9217101d.chunk.js} +1 -1
  88. package/build/{Admin_profilePage.a4d41380.chunk.js → Admin_profilePage.680123d9.chunk.js} +2 -2
  89. package/build/{Admin_settingsPage.6dc2af9f.chunk.js → Admin_settingsPage.33378310.chunk.js} +1 -1
  90. package/build/{Upload_ConfigureTheView.cc7ca628.chunk.js → Upload_ConfigureTheView.b40eea4d.chunk.js} +1 -1
  91. package/build/admin-app.e8c52c37.chunk.js +36 -0
  92. package/build/admin-edit-roles-page.fcf056bf.chunk.js +275 -0
  93. package/build/{admin-edit-users.9b42cc9e.chunk.js → admin-edit-users.89efe3c4.chunk.js} +2 -2
  94. package/build/{admin-roles-list.cf964578.chunk.js → admin-roles-list.8b77704a.chunk.js} +3 -3
  95. package/build/admin-users.e3f1be14.chunk.js +19 -0
  96. package/build/{api-tokens-create-page.2f25ddf6.chunk.js → api-tokens-create-page.0dd63e91.chunk.js} +1 -1
  97. package/build/{api-tokens-edit-page.45faac16.chunk.js → api-tokens-edit-page.78d877f8.chunk.js} +1 -1
  98. package/build/{api-tokens-list-page.5baabf1a.chunk.js → api-tokens-list-page.ae13346c.chunk.js} +2 -2
  99. package/build/audit-logs-settings-page.e9c92a75.chunk.js +9 -0
  100. package/build/content-manager.5849dbe3.chunk.js +1226 -0
  101. package/build/{content-type-builder-list-view.aa8a5d1a.chunk.js → content-type-builder-list-view.3fffae65.chunk.js} +1 -1
  102. package/build/{content-type-builder-translation-en-json.b9e5cacd.chunk.js → content-type-builder-translation-en-json.43f9d7bc.chunk.js} +1 -1
  103. package/build/{content-type-builder.885f2cad.chunk.js → content-type-builder.98c71164.chunk.js} +14 -14
  104. package/build/{email-settings-page.6bd7b280.chunk.js → email-settings-page.ecfec9b3.chunk.js} +1 -1
  105. package/build/{en-json.a3973ff5.chunk.js → en-json.bd611a8e.chunk.js} +1 -1
  106. package/build/{i18n-settings-page.6c0157e7.chunk.js → i18n-settings-page.a9708926.chunk.js} +1 -1
  107. package/build/index.html +1 -1
  108. package/build/main.3abb6f34.js +3278 -0
  109. package/build/{review-workflows-settings-create-view.ae369a88.chunk.js → review-workflows-settings-create-view.b7b0c6c5.chunk.js} +1 -1
  110. package/build/{review-workflows-settings-edit-view.9a61c69f.chunk.js → review-workflows-settings-edit-view.c331b3fe.chunk.js} +1 -1
  111. package/build/review-workflows-settings-list-view.70218dc1.chunk.js +75 -0
  112. package/build/{runtime~main.cec66cd9.js → runtime~main.450561b1.js} +1 -1
  113. package/build/{sso-settings-page.a29e6c38.chunk.js → sso-settings-page.1a9e7f8f.chunk.js} +1 -1
  114. package/build/{transfer-tokens-create-page.6e1b8cee.chunk.js → transfer-tokens-create-page.e7f541d3.chunk.js} +1 -1
  115. package/build/{transfer-tokens-edit-page.10bb22e2.chunk.js → transfer-tokens-edit-page.bd1276c2.chunk.js} +1 -1
  116. package/build/{transfer-tokens-list-page.0306652c.chunk.js → transfer-tokens-list-page.5de6bb9f.chunk.js} +2 -2
  117. package/build/upload-settings.97ef4c92.chunk.js +14 -0
  118. package/build/{upload.19e14c8e.chunk.js → upload.f08715a1.chunk.js} +1 -1
  119. package/build/{users-advanced-settings-page.ed69812f.chunk.js → users-advanced-settings-page.36a3c363.chunk.js} +1 -1
  120. package/build/users-email-settings-page.47b47962.chunk.js +149 -0
  121. package/build/users-providers-settings-page.1e0c8376.chunk.js +154 -0
  122. package/build/{users-roles-settings-page.afab5a0d.chunk.js → users-roles-settings-page.d5a8e8a1.chunk.js} +4 -4
  123. package/build/{webhook-edit-page.4c037da4.chunk.js → webhook-edit-page.87456194.chunk.js} +3 -3
  124. package/build/{webhook-list-page.56c82f4a.chunk.js → webhook-list-page.c88a382b.chunk.js} +3 -3
  125. package/ee/admin/hooks/{useLicenseLimitNotification.js → useLicenseLimitNotification.ts} +4 -4
  126. package/ee/admin/pages/AuthPage/components/Providers/index.js +1 -1
  127. package/ee/admin/pages/SettingsPage/pages/Users/ListPage/index.js +1 -3
  128. package/package.json +13 -12
  129. package/scripts/build.js +6 -2
  130. package/webpack.config.js +1 -0
  131. package/admin/src/components/AuthenticatedApp/utils/checkLatestStrapiVersion.js +0 -11
  132. package/admin/src/components/DragLayer/index.js +0 -1
  133. package/admin/src/components/GlobalStyle/index.js +0 -9
  134. package/admin/src/components/PrivateRoute/index.js +0 -46
  135. package/admin/src/components/Theme/index.js +0 -26
  136. package/admin/src/components/ThemeToggleProvider/index.js +0 -79
  137. package/admin/src/contexts/Configurations/index.js +0 -5
  138. package/admin/src/contexts/ThemeToggle/index.js +0 -5
  139. package/admin/src/hooks/useConfigurations/index.js +0 -11
  140. package/admin/src/hooks/useDebounce/index.js +0 -19
  141. package/admin/src/hooks/useLicenseLimitNotification/index.js +0 -5
  142. package/admin/src/hooks/useReleaseNotification/index.js +0 -31
  143. package/admin/src/hooks/useReleaseNotification/utils/checkLatestStrapiVersion.js +0 -11
  144. package/admin/src/hooks/useThemeToggle/index.js +0 -11
  145. package/admin/src/tsconfig.json +0 -10
  146. package/build/1049.acb0e730.chunk.js +0 -1
  147. package/build/2225.78fb9b89.chunk.js +0 -79
  148. package/build/2379.906334f0.chunk.js +0 -1
  149. package/build/2614.3e088d3e.chunk.js +0 -35
  150. package/build/2659.cb94f1e7.chunk.js +0 -105
  151. package/build/2801.2afb4757.chunk.js +0 -1
  152. package/build/2950.216f2e89.chunk.js +0 -1
  153. package/build/3021.33ad47fb.chunk.js +0 -103
  154. package/build/3911.488fbde3.chunk.js +0 -95
  155. package/build/4546.1203ac95.chunk.js +0 -1
  156. package/build/502.9918bff7.chunk.js +0 -1
  157. package/build/5158.c85f841a.chunk.js +0 -1
  158. package/build/6266.e8990811.chunk.js +0 -146
  159. package/build/7464.0280cf59.chunk.js +0 -1
  160. package/build/7897.4a39de37.chunk.js +0 -6
  161. package/build/Admin-authenticatedApp.08f32723.chunk.js +0 -112
  162. package/build/Admin_homePage.6cb51f18.chunk.js +0 -81
  163. package/build/Admin_marketplace.3eb5e132.chunk.js +0 -55
  164. package/build/admin-app.98cdf43a.chunk.js +0 -36
  165. package/build/admin-edit-roles-page.418bb1c5.chunk.js +0 -267
  166. package/build/admin-users.8385dd73.chunk.js +0 -11
  167. package/build/audit-logs-settings-page.91489670.chunk.js +0 -1
  168. package/build/content-manager.0d2b4a60.chunk.js +0 -1199
  169. package/build/main.105dcf23.js +0 -2665
  170. package/build/review-workflows-settings-list-view.067e0c35.chunk.js +0 -56
  171. package/build/upload-settings.0af6edc5.chunk.js +0 -14
  172. package/build/users-email-settings-page.131a00fb.chunk.js +0 -9
  173. package/build/users-providers-settings-page.b3dca41d.chunk.js +0 -14
@@ -3,15 +3,22 @@ import * as React from 'react';
3
3
  import * as Toolbar from '@radix-ui/react-toolbar';
4
4
  import { Flex, Icon, Tooltip, Select, Option, Box, Typography } from '@strapi/design-system';
5
5
  import { pxToRem, prefixFileUrlWithBackendUrl, useLibrary } from '@strapi/helper-plugin';
6
- import { BulletList, NumberList } from '@strapi/icons';
6
+ import { Link } from '@strapi/icons';
7
7
  import PropTypes from 'prop-types';
8
8
  import { useIntl } from 'react-intl';
9
9
  import { Editor, Transforms, Element as SlateElement } from 'slate';
10
- import { useSlate } from 'slate-react';
10
+ import { ReactEditor, useSlate } from 'slate-react';
11
11
  import styled from 'styled-components';
12
12
 
13
13
  import { useBlocksStore } from '../hooks/useBlocksStore';
14
14
  import { useModifiersStore } from '../hooks/useModifiersStore';
15
+ import { insertLink } from '../utils/links';
16
+
17
+ const ToolbarWrapper = styled(Flex)`
18
+ &[aria-disabled='true'] {
19
+ cursor: not-allowed;
20
+ }
21
+ `;
15
22
 
16
23
  const Separator = styled(Toolbar.Separator)`
17
24
  background: ${({ theme }) => theme.colors.neutral150};
@@ -20,32 +27,58 @@ const Separator = styled(Toolbar.Separator)`
20
27
  `;
21
28
 
22
29
  const FlexButton = styled(Flex).attrs({ as: 'button' })`
23
- &:hover {
24
- background: ${({ theme }) => theme.colors.primary100};
30
+ // Inherit the not-allowed cursor from ToolbarWrapper when disabled
31
+ &[aria-disabled] {
32
+ cursor: inherit;
33
+ }
34
+
35
+ &[aria-disabled='false'] {
36
+ cursor: pointer;
37
+
38
+ // Only apply hover styles if the button is enabled
39
+ &:hover {
40
+ background: ${({ theme }) => theme.colors.primary100};
41
+ }
25
42
  }
26
43
  `;
27
44
 
28
- const ToolbarButton = ({ icon, name, label, isActive, handleClick }) => {
45
+ const ToolbarButton = ({ icon, name, label, isActive, disabled, handleClick }) => {
46
+ const editor = useSlate();
29
47
  const { formatMessage } = useIntl();
30
48
  const labelMessage = formatMessage(label);
31
49
 
50
+ const enabledColor = isActive ? 'primary600' : 'neutral600';
51
+
32
52
  return (
33
53
  <Tooltip description={labelMessage}>
34
- <Toolbar.ToggleItem value={name} data-state={isActive ? 'on' : 'off'} asChild>
54
+ <Toolbar.ToggleItem
55
+ value={name}
56
+ data-state={isActive ? 'on' : 'off'}
57
+ onMouseDown={(e) => {
58
+ e.preventDefault();
59
+ handleClick();
60
+ }}
61
+ aria-disabled={disabled}
62
+ disabled={disabled}
63
+ aria-label={labelMessage}
64
+ asChild
65
+ >
35
66
  <FlexButton
67
+ disabled={disabled}
36
68
  background={isActive ? 'primary100' : ''}
37
69
  alignItems="center"
38
70
  justifyContent="center"
39
71
  width={7}
40
72
  height={7}
41
73
  hasRadius
42
- onMouseDown={(e) => {
43
- e.preventDefault();
74
+ onMouseDown={() => {
44
75
  handleClick();
76
+ // When a button is clicked it blurs the editor, restore the focus to the editor
77
+ ReactEditor.focus(editor);
45
78
  }}
46
79
  aria-label={labelMessage}
47
80
  >
48
- <Icon width={3} height={3} as={icon} color={isActive ? 'primary600' : 'neutral600'} />
81
+ <Icon width={3} height={3} as={icon} color={disabled ? 'neutral300' : enabledColor} />
49
82
  </FlexButton>
50
83
  </Toolbar.ToggleItem>
51
84
  </Tooltip>
@@ -60,10 +93,11 @@ ToolbarButton.propTypes = {
60
93
  defaultMessage: PropTypes.string.isRequired,
61
94
  }).isRequired,
62
95
  isActive: PropTypes.bool.isRequired,
96
+ disabled: PropTypes.bool.isRequired,
63
97
  handleClick: PropTypes.func.isRequired,
64
98
  };
65
99
 
66
- const ModifierButton = ({ icon, name, label }) => {
100
+ const ModifierButton = ({ icon, name, label, disabled }) => {
67
101
  const editor = useSlate();
68
102
 
69
103
  const isModifierActive = () => {
@@ -90,6 +124,7 @@ const ModifierButton = ({ icon, name, label }) => {
90
124
  name={name}
91
125
  label={label}
92
126
  isActive={isActive}
127
+ disabled={disabled}
93
128
  handleClick={toggleModifier}
94
129
  />
95
130
  );
@@ -102,32 +137,29 @@ ModifierButton.propTypes = {
102
137
  id: PropTypes.string.isRequired,
103
138
  defaultMessage: PropTypes.string.isRequired,
104
139
  }).isRequired,
105
- };
106
-
107
- const isBlockActive = (editor, matchNode) => {
108
- const { selection } = editor;
109
-
110
- if (!selection) return false;
111
-
112
- const match = Array.from(
113
- Editor.nodes(editor, {
114
- at: Editor.unhangRange(editor, selection),
115
- match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && matchNode(n),
116
- })
117
- );
118
-
119
- return match.length > 0;
140
+ disabled: PropTypes.bool.isRequired,
120
141
  };
121
142
 
122
143
  const toggleBlock = (editor, value) => {
123
- const { type, level } = value;
144
+ const { type, level, format } = value;
124
145
 
125
- const newProperties = {
146
+ // Set the selected block properties received from the useBlockStore
147
+ const blockProperties = {
126
148
  type,
127
149
  level: level || null,
150
+ format: format || null,
128
151
  };
129
152
 
130
- Transforms.setNodes(editor, newProperties);
153
+ if (editor.selection) {
154
+ // When there is a selection, update the existing block in the tree
155
+ Transforms.setNodes(editor, blockProperties);
156
+ } else {
157
+ // Otherwise, add a new block to the tree
158
+ Transforms.insertNodes(editor, { ...blockProperties, children: [{ type: 'text', text: '' }] });
159
+ }
160
+
161
+ // When the select is clicked it blurs the editor, restore the focus to the editor
162
+ ReactEditor.focus(editor);
131
163
  };
132
164
 
133
165
  const ALLOWED_MEDIA_TYPE = 'images';
@@ -175,7 +207,7 @@ const ImageDialog = ({ handleClose }) => {
175
207
 
176
208
  const handleSelectAssets = (images) => {
177
209
  const formattedImages = images.map((image) => {
178
- // create an object with imageSchema defined and exclude unnecessary props coming from media library config
210
+ // Create an object with imageSchema defined and exclude unnecessary props coming from media library config
179
211
  const expectedImage = pick(image, IMAGE_SCHEMA_FIELDS);
180
212
 
181
213
  return {
@@ -186,6 +218,12 @@ const ImageDialog = ({ handleClose }) => {
186
218
  });
187
219
 
188
220
  insertImages(formattedImages);
221
+
222
+ if (isLastBlockType(editor, 'image')) {
223
+ // Insert blank line to add new blocks below image block
224
+ insertEmptyBlockAtLast(editor);
225
+ }
226
+
189
227
  handleClose();
190
228
  };
191
229
 
@@ -202,18 +240,18 @@ ImageDialog.propTypes = {
202
240
  handleClose: PropTypes.func.isRequired,
203
241
  };
204
242
 
205
- const isLastBlockImageOrCode = (editor) => {
243
+ const isLastBlockType = (editor, type) => {
206
244
  const { selection } = editor;
207
245
 
208
246
  if (!selection) return false;
209
247
 
210
- const [currentImageORCodeBlock] = Editor.nodes(editor, {
248
+ const [currentBlock] = Editor.nodes(editor, {
211
249
  at: selection,
212
- match: (n) => n.type === 'image' || n.type === 'code',
250
+ match: (n) => n.type === type,
213
251
  });
214
252
 
215
- if (currentImageORCodeBlock) {
216
- const [, currentNodePath] = currentImageORCodeBlock;
253
+ if (currentBlock) {
254
+ const [, currentNodePath] = currentBlock;
217
255
 
218
256
  const isNodeAfter = Boolean(Editor.after(editor, currentNodePath));
219
257
 
@@ -223,7 +261,18 @@ const isLastBlockImageOrCode = (editor) => {
223
261
  return false;
224
262
  };
225
263
 
226
- export const BlocksDropdown = () => {
264
+ const insertEmptyBlockAtLast = (editor) => {
265
+ Transforms.insertNodes(
266
+ editor,
267
+ {
268
+ type: 'paragraph',
269
+ children: [{ type: 'text', text: '' }],
270
+ },
271
+ { at: [editor.children.length] }
272
+ );
273
+ };
274
+
275
+ export const BlocksDropdown = ({ disabled }) => {
227
276
  const editor = useSlate();
228
277
  const { formatMessage } = useIntl();
229
278
  const [isMediaLibraryVisible, setIsMediaLibraryVisible] = React.useState(false);
@@ -241,20 +290,27 @@ export const BlocksDropdown = () => {
241
290
  * @param {string} optionKey - key of the heading selected
242
291
  */
243
292
  const selectOption = (optionKey) => {
244
- toggleBlock(editor, blocks[optionKey].value);
293
+ if (optionKey === 'image') {
294
+ // Image node created using select or existing selection node needs to be deleted before adding new image nodes
295
+ Transforms.removeNodes(editor);
296
+ } else if (['list-ordered', 'list-unordered'].includes(optionKey)) {
297
+ // retrieve the list format
298
+ const listFormat = blocks[optionKey].value.format;
299
+
300
+ // check if the list is already active
301
+ const isActive = isListActive(editor, blocks[optionKey].matchNode);
302
+
303
+ // toggle the list
304
+ toggleList(editor, isActive, listFormat);
305
+ } else {
306
+ toggleBlock(editor, blocks[optionKey].value);
307
+ }
245
308
 
246
309
  setBlockSelected(optionKey);
247
310
 
248
- if (isLastBlockImageOrCode(editor)) {
249
- // insert blank line to add new blocks below code or image blocks
250
- Transforms.insertNodes(
251
- editor,
252
- {
253
- type: 'paragraph',
254
- children: [{ type: 'text', text: '' }],
255
- },
256
- { at: [editor.children.length] }
257
- );
311
+ if (optionKey === 'code' && isLastBlockType(editor, 'code')) {
312
+ // Insert blank line to add new blocks below code block
313
+ insertEmptyBlockAtLast(editor);
258
314
  }
259
315
 
260
316
  if (optionKey === 'image') {
@@ -262,6 +318,37 @@ export const BlocksDropdown = () => {
262
318
  }
263
319
  };
264
320
 
321
+ /**
322
+ * Prevent the select from focusing itself so ReactEditor.focus(editor) can focus the editor instead.
323
+ *
324
+ * The editor first loses focus to a blur event when clicking the select button. However,
325
+ * refocusing the editor is not enough since the select's default behavior is to refocus itself
326
+ * after an option is selected.
327
+ *
328
+ */
329
+ const preventSelectFocus = (e) => e.preventDefault();
330
+
331
+ // Listen to the selection change and update the selected block in the dropdown
332
+ React.useEffect(() => {
333
+ if (editor.selection) {
334
+ // Get the parent node of the anchor
335
+ // with a depth of two to retrieve also the list item parents
336
+ const [anchorNode] = Editor.parent(editor, editor.selection.anchor, {
337
+ edge: 'start',
338
+ depth: 2,
339
+ });
340
+ // Find the block key that matches the anchor node
341
+ const anchorBlockKey = Object.keys(blocks).find((blockKey) =>
342
+ blocks[blockKey].matchNode(anchorNode)
343
+ );
344
+
345
+ // Change the value selected in the dropdown if it doesn't match the anchor block key
346
+ if (anchorBlockKey && anchorBlockKey !== blockSelected) {
347
+ setBlockSelected(anchorBlockKey);
348
+ }
349
+ }
350
+ }, [editor.selection, editor, blocks, blockSelected]);
351
+
265
352
  return (
266
353
  <>
267
354
  <Select
@@ -269,10 +356,12 @@ export const BlocksDropdown = () => {
269
356
  onChange={selectOption}
270
357
  placeholder={blocks[blockSelected].label}
271
358
  value={blockSelected}
359
+ onCloseAutoFocus={preventSelectFocus}
272
360
  aria-label={formatMessage({
273
361
  id: 'components.Blocks.blocks.selectBlock',
274
362
  defaultMessage: 'Select a block',
275
363
  })}
364
+ disabled={disabled}
276
365
  >
277
366
  {blockKeysToInclude.map((key) => (
278
367
  <BlockOption
@@ -280,8 +369,6 @@ export const BlocksDropdown = () => {
280
369
  value={key}
281
370
  label={blocks[key].label}
282
371
  icon={blocks[key].icon}
283
- matchNode={blocks[key].matchNode}
284
- handleSelection={setBlockSelected}
285
372
  blockSelected={blockSelected}
286
373
  />
287
374
  ))}
@@ -291,19 +378,15 @@ export const BlocksDropdown = () => {
291
378
  );
292
379
  };
293
380
 
294
- const BlockOption = ({ value, icon, label, handleSelection, blockSelected, matchNode }) => {
381
+ BlocksDropdown.propTypes = {
382
+ disabled: PropTypes.bool.isRequired,
383
+ };
384
+
385
+ const BlockOption = ({ value, icon, label, blockSelected }) => {
295
386
  const { formatMessage } = useIntl();
296
- const editor = useSlate();
297
387
 
298
- const isActive = isBlockActive(editor, matchNode);
299
388
  const isSelected = value === blockSelected;
300
389
 
301
- React.useEffect(() => {
302
- if (isActive && !isSelected) {
303
- handleSelection(value);
304
- }
305
- }, [handleSelection, isActive, isSelected, value]);
306
-
307
390
  return (
308
391
  <Option
309
392
  startIcon={<Icon as={icon} color={isSelected ? 'primary600' : 'neutral600'} />}
@@ -321,24 +404,95 @@ BlockOption.propTypes = {
321
404
  id: PropTypes.string.isRequired,
322
405
  defaultMessage: PropTypes.string.isRequired,
323
406
  }).isRequired,
324
- matchNode: PropTypes.func.isRequired,
325
- handleSelection: PropTypes.func.isRequired,
326
407
  blockSelected: PropTypes.string.isRequired,
327
408
  };
328
409
 
329
- const ListButton = ({ icon, format, label }) => {
410
+ /**
411
+ *
412
+ * @param {import('slate').Node} node
413
+ * @returns boolean
414
+ */
415
+ const isListNode = (node) => {
416
+ return !Editor.isEditor(node) && SlateElement.isElement(node) && node.type === 'list';
417
+ };
418
+
419
+ const isListActive = (editor, matchNode) => {
420
+ const { selection } = editor;
421
+
422
+ if (!selection) return false;
423
+
424
+ const [match] = Array.from(
425
+ Editor.nodes(editor, {
426
+ at: Editor.unhangRange(editor, selection),
427
+ match: matchNode,
428
+ })
429
+ );
430
+
431
+ return Boolean(match);
432
+ };
433
+
434
+ const toggleList = (editor, isActive, format) => {
435
+ // Delete the parent list so that we're left with only the list items directly
436
+ Transforms.unwrapNodes(editor, {
437
+ match: (node) => isListNode(node) && ['ordered', 'unordered'].includes(node.format),
438
+ split: true,
439
+ });
440
+
441
+ // Change the type of the current selection
442
+ Transforms.setNodes(editor, {
443
+ type: isActive ? 'paragraph' : 'list-item',
444
+ });
445
+
446
+ // If the selection is now a list item, wrap it inside a list
447
+ if (!isActive) {
448
+ const block = { type: 'list', format, children: [] };
449
+ Transforms.wrapNodes(editor, block);
450
+ }
451
+ };
452
+
453
+ const ListButton = ({ block, disabled }) => {
330
454
  const editor = useSlate();
331
455
 
332
- /**
333
- *
334
- * @param {import('slate').Node} node
335
- * @returns boolean
336
- */
337
- const isListNode = (node) => {
338
- return !Editor.isEditor(node) && SlateElement.isElement(node) && node.type === 'list';
339
- };
456
+ const {
457
+ icon,
458
+ matchNode,
459
+ value: { format },
460
+ label,
461
+ } = block;
462
+
463
+ const isActive = isListActive(editor, matchNode);
464
+
465
+ return (
466
+ <ToolbarButton
467
+ icon={icon}
468
+ name={format}
469
+ label={label}
470
+ isActive={isActive}
471
+ disabled={disabled}
472
+ handleClick={() => toggleList(editor, isActive, format)}
473
+ />
474
+ );
475
+ };
476
+
477
+ ListButton.propTypes = {
478
+ block: PropTypes.shape({
479
+ icon: PropTypes.elementType.isRequired,
480
+ matchNode: PropTypes.func.isRequired,
481
+ value: PropTypes.shape({
482
+ format: PropTypes.string.isRequired,
483
+ }).isRequired,
484
+ label: PropTypes.shape({
485
+ id: PropTypes.string.isRequired,
486
+ defaultMessage: PropTypes.string.isRequired,
487
+ }).isRequired,
488
+ }).isRequired,
489
+ disabled: PropTypes.bool.isRequired,
490
+ };
491
+
492
+ const LinkButton = ({ disabled }) => {
493
+ const editor = useSlate();
340
494
 
341
- const isListActive = () => {
495
+ const isLinkActive = () => {
342
496
  const { selection } = editor;
343
497
 
344
498
  if (!selection) return false;
@@ -346,74 +500,57 @@ const ListButton = ({ icon, format, label }) => {
346
500
  const [match] = Array.from(
347
501
  Editor.nodes(editor, {
348
502
  at: Editor.unhangRange(editor, selection),
349
- match: (node) => isListNode(node) && node.format === format,
503
+ match: (node) => SlateElement.isElement(node) && node.type === 'link',
350
504
  })
351
505
  );
352
506
 
353
507
  return Boolean(match);
354
508
  };
355
509
 
356
- const isActive = isListActive();
357
-
358
- const toggleList = () => {
359
- // Delete the parent list so that we're left with only the list items directly
360
- Transforms.unwrapNodes(editor, {
361
- match: (node) => isListNode(node) && ['ordered', 'unordered'].includes(node.format),
362
- split: true,
363
- });
364
-
365
- // Change the type of the current selection
366
- Transforms.setNodes(editor, {
367
- type: isActive ? 'paragraph' : 'list-item',
368
- });
369
-
370
- // If the selection is now a list item, wrap it inside a list
371
- if (!isActive) {
372
- const block = { type: 'list', format, children: [] };
373
- Transforms.wrapNodes(editor, block);
374
- }
510
+ const addLink = () => {
511
+ // We insert an empty anchor, so we split the DOM to have a element we can use as reference for the popover
512
+ insertLink(editor, { url: '' });
375
513
  };
376
514
 
377
515
  return (
378
516
  <ToolbarButton
379
- icon={icon}
380
- name={format}
381
- label={label}
382
- isActive={isActive}
383
- handleClick={toggleList}
517
+ icon={Link}
518
+ name="link"
519
+ label={{
520
+ id: 'components.Blocks.link',
521
+ defaultMessage: 'Link',
522
+ }}
523
+ isActive={isLinkActive()}
524
+ handleClick={addLink}
525
+ disabled={disabled}
384
526
  />
385
527
  );
386
528
  };
387
529
 
388
- ListButton.propTypes = {
389
- icon: PropTypes.elementType.isRequired,
390
- format: PropTypes.string.isRequired,
391
- label: PropTypes.shape({
392
- id: PropTypes.string.isRequired,
393
- defaultMessage: PropTypes.string.isRequired,
394
- }).isRequired,
530
+ LinkButton.propTypes = {
531
+ disabled: PropTypes.bool.isRequired,
395
532
  };
396
533
 
397
- // TODO: Remove after the RTE Blocks Alpha release
398
- const AlphaTag = styled(Box)`
399
- background-color: ${({ theme }) => theme.colors.warning100};
400
- border: ${({ theme }) => `1px solid ${theme.colors.warning200}`};
534
+ // TODO: Remove after the RTE Blocks Beta release
535
+ const BetaTag = styled(Box)`
536
+ background-color: ${({ theme }) => theme.colors.secondary100};
537
+ border: ${({ theme }) => `1px solid ${theme.colors.secondary200}`};
401
538
  border-radius: ${({ theme }) => theme.borderRadius};
402
539
  font-size: ${({ theme }) => theme.fontSizes[0]};
403
540
  padding: ${({ theme }) => `${2 / 16}rem ${theme.spaces[1]}`};
404
541
  `;
405
542
 
406
- const BlocksToolbar = () => {
543
+ const BlocksToolbar = ({ disabled }) => {
407
544
  const modifiers = useModifiersStore();
545
+ const blocks = useBlocksStore();
408
546
 
409
547
  return (
410
- <Toolbar.Root asChild>
411
- {/* Remove after the RTE Blocks Alpha release (paddingRight and width) */}
412
- <Flex gap={1} padding={2} paddingRight={4} width="100%">
413
- <BlocksDropdown />
414
- <Separator />
548
+ <Toolbar.Root aria-disabled={disabled} asChild>
549
+ {/* Remove after the RTE Blocks Beta release (paddingRight and width) */}
550
+ <ToolbarWrapper gap={1} padding={2} paddingRight={4} width="100%">
551
+ <BlocksDropdown disabled={disabled} />
415
552
  <Toolbar.ToggleGroup type="multiple" asChild>
416
- <Flex gap={1}>
553
+ <Flex gap={1} marginLeft={1}>
417
554
  {Object.entries(modifiers).map(([name, modifier]) => (
418
555
  <ToolbarButton
419
556
  key={name}
@@ -422,42 +559,34 @@ const BlocksToolbar = () => {
422
559
  label={modifier.label}
423
560
  isActive={modifier.checkIsActive()}
424
561
  handleClick={modifier.handleToggle}
562
+ disabled={disabled}
425
563
  />
426
564
  ))}
565
+ <LinkButton disabled={disabled} />
427
566
  </Flex>
428
567
  </Toolbar.ToggleGroup>
429
568
  <Separator />
430
569
  <Toolbar.ToggleGroup type="single" asChild>
431
570
  <Flex gap={1}>
432
- <ListButton
433
- label={{
434
- id: 'components.Blocks.blocks.unorderedList',
435
- defaultMessage: 'Bulleted list',
436
- }}
437
- format="unordered"
438
- icon={BulletList}
439
- />
440
- <ListButton
441
- label={{
442
- id: 'components.Blocks.blocks.orderedList',
443
- defaultMessage: 'Numbered list',
444
- }}
445
- format="ordered"
446
- icon={NumberList}
447
- />
571
+ <ListButton block={blocks['list-unordered']} disabled={disabled} />
572
+ <ListButton block={blocks['list-ordered']} disabled={disabled} />
448
573
  </Flex>
449
574
  </Toolbar.ToggleGroup>
450
- {/* TODO: Remove after the RTE Blocks Alpha release */}
575
+ {/* TODO: Remove after the RTE Blocks Beta release */}
451
576
  <Flex grow={1} justifyContent="flex-end">
452
- <AlphaTag>
453
- <Typography textColor="warning600" variant="sigma">
454
- ALPHA
577
+ <BetaTag>
578
+ <Typography textColor="secondary600" variant="sigma">
579
+ BETA
455
580
  </Typography>
456
- </AlphaTag>
581
+ </BetaTag>
457
582
  </Flex>
458
- </Flex>
583
+ </ToolbarWrapper>
459
584
  </Toolbar.Root>
460
585
  );
461
586
  };
462
587
 
588
+ BlocksToolbar.propTypes = {
589
+ disabled: PropTypes.bool.isRequired,
590
+ };
591
+
463
592
  export { BlocksToolbar };