@plone/volto 18.0.0-alpha.33 → 18.0.0-alpha.34

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 (74) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/locales/ca/LC_MESSAGES/volto.po +5 -0
  3. package/locales/ca.json +1 -1
  4. package/locales/de/LC_MESSAGES/volto.po +5 -0
  5. package/locales/de.json +1 -1
  6. package/locales/en/LC_MESSAGES/volto.po +5 -0
  7. package/locales/en.json +1 -1
  8. package/locales/es/LC_MESSAGES/volto.po +5 -0
  9. package/locales/es.json +1 -1
  10. package/locales/eu/LC_MESSAGES/volto.po +5 -0
  11. package/locales/eu.json +1 -1
  12. package/locales/fi/LC_MESSAGES/volto.po +5 -0
  13. package/locales/fi.json +1 -1
  14. package/locales/fr/LC_MESSAGES/volto.po +5 -0
  15. package/locales/fr.json +1 -1
  16. package/locales/hi/LC_MESSAGES/volto.po +5 -0
  17. package/locales/hi.json +1 -1
  18. package/locales/it/LC_MESSAGES/volto.po +5 -0
  19. package/locales/it.json +1 -1
  20. package/locales/ja/LC_MESSAGES/volto.po +5 -0
  21. package/locales/ja.json +1 -1
  22. package/locales/nl/LC_MESSAGES/volto.po +5 -0
  23. package/locales/nl.json +1 -1
  24. package/locales/pt/LC_MESSAGES/volto.po +5 -0
  25. package/locales/pt.json +1 -1
  26. package/locales/pt_BR/LC_MESSAGES/volto.po +5 -0
  27. package/locales/pt_BR.json +1 -1
  28. package/locales/ro/LC_MESSAGES/volto.po +5 -0
  29. package/locales/ro.json +1 -1
  30. package/locales/volto.pot +5 -0
  31. package/locales/zh_CN/LC_MESSAGES/volto.po +5 -0
  32. package/locales/zh_CN.json +1 -1
  33. package/package.json +9 -6
  34. package/razzle.config.js +13 -4
  35. package/src/actions/form/form.js +18 -2
  36. package/src/actions/index.js +1 -1
  37. package/src/components/manage/Blocks/Block/BlocksForm.jsx +157 -81
  38. package/src/components/manage/Blocks/Block/BlocksForm.test.jsx +8 -0
  39. package/src/components/manage/Blocks/Block/Edit.jsx +37 -4
  40. package/src/components/manage/Blocks/Block/Order/Item.jsx +122 -0
  41. package/src/components/manage/Blocks/Block/Order/Order.jsx +367 -0
  42. package/src/components/manage/Blocks/Block/Order/SortableItem.jsx +58 -0
  43. package/src/components/manage/Blocks/Block/Order/utilities.js +113 -0
  44. package/src/components/manage/Blocks/Container/Edit.jsx +1 -0
  45. package/src/components/manage/Blocks/Grid/Edit.jsx +6 -4
  46. package/src/components/manage/Blocks/Image/schema.js +2 -0
  47. package/src/components/manage/Controlpanels/Relations/RelationsListing.jsx +1 -1
  48. package/src/components/manage/Controlpanels/Relations/RelationsMatrix.jsx +1 -1
  49. package/src/components/manage/Form/Form.jsx +159 -151
  50. package/src/components/manage/Sidebar/Sidebar.jsx +28 -1
  51. package/src/components/manage/Widgets/InternalUrlWidget.jsx +10 -14
  52. package/src/components/theme/FormattedDate/FormattedDate.jsx +13 -1
  53. package/src/components/theme/Icon/Icon.jsx +4 -4
  54. package/src/config/Loadables.jsx +18 -0
  55. package/src/constants/ActionTypes.js +1 -0
  56. package/src/helpers/Blocks/Blocks.js +182 -1
  57. package/src/helpers/Blocks/Blocks.test.js +136 -0
  58. package/src/helpers/index.js +4 -0
  59. package/src/reducers/form/form.js +18 -1
  60. package/src/reducers/form/form.test.js +15 -1
  61. package/theme/themes/pastanaga/extras/blocks.less +7 -6
  62. package/theme/themes/pastanaga/extras/sidebar.less +145 -0
  63. package/types/actions/form/form.d.ts +8 -1
  64. package/types/actions/index.d.ts +1 -1
  65. package/types/components/manage/Blocks/Block/Order/Item.d.ts +2 -0
  66. package/types/components/manage/Blocks/Block/Order/Order.d.ts +13 -0
  67. package/types/components/manage/Blocks/Block/Order/SortableItem.d.ts +9 -0
  68. package/types/components/manage/Blocks/Block/Order/utilities.d.ts +9 -0
  69. package/types/components/manage/Blocks/Image/schema.d.ts +2 -0
  70. package/types/components/theme/Icon/Icon.d.ts +8 -8
  71. package/types/config/Loadables.d.ts +15 -162
  72. package/types/constants/ActionTypes.d.ts +1 -0
  73. package/types/helpers/Blocks/Blocks.d.ts +13 -0
  74. package/types/helpers/index.d.ts +2 -2
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "18.0.0-alpha.33",
12
+ "version": "18.0.0-alpha.34",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -236,9 +236,9 @@
236
236
  "url": "^0.11.3",
237
237
  "use-deep-compare-effect": "1.8.1",
238
238
  "uuid": "^8.3.2",
239
- "@plone/registry": "1.5.7",
240
- "@plone/volto-slate": "18.0.0-alpha.12",
241
- "@plone/scripts": "3.6.2"
239
+ "@plone/volto-slate": "18.0.0-alpha.13",
240
+ "@plone/scripts": "3.6.2",
241
+ "@plone/registry": "1.6.0"
242
242
  },
243
243
  "devDependencies": {
244
244
  "@babel/core": "^7.0.0",
@@ -253,6 +253,9 @@
253
253
  "@babel/types": "7.20.5",
254
254
  "@fiverr/afterbuild-webpack-plugin": "^1.0.0",
255
255
  "@jest/globals": "^29.7.0",
256
+ "@dnd-kit/core": "6.0.8",
257
+ "@dnd-kit/sortable": "7.0.2",
258
+ "@dnd-kit/utilities": "3.2.2",
256
259
  "@loadable/babel-plugin": "5.13.2",
257
260
  "@loadable/webpack-plugin": "5.15.2",
258
261
  "@sinonjs/fake-timers": "^6.0.1",
@@ -355,8 +358,8 @@
355
358
  "webpack-dev-server": "4.11.1",
356
359
  "webpack-node-externals": "3.0.0",
357
360
  "why": "0.6.2",
358
- "@plone/types": "1.0.0-alpha.14",
359
- "@plone/volto-coresandbox": "1.0.0"
361
+ "@plone/volto-coresandbox": "1.0.0",
362
+ "@plone/types": "1.0.0-alpha.15"
360
363
  },
361
364
  "volta": {
362
365
  "node": "20.9.0"
package/razzle.config.js CHANGED
@@ -172,6 +172,19 @@ const defaultModify = ({
172
172
  });
173
173
  };
174
174
 
175
+ // If we are in development mode, we copy the public directory to the
176
+ // public directory of the setup root, so the files are available
177
+ if (dev && !registry.isVoltoProject && registry.addonNames.length > 0) {
178
+ const devPublicPath = `${projectRootPath}/../../../public`;
179
+ if (!fs.existsSync(devPublicPath)) {
180
+ fs.mkdirSync(devPublicPath);
181
+ }
182
+ mergeDirectories(
183
+ path.join(projectRootPath, 'public'),
184
+ `${projectRootPath}/../../../public`,
185
+ );
186
+ }
187
+
175
188
  registry.getAddonDependencies().forEach((addonDep) => {
176
189
  // What comes from getAddonDependencies is in the form of `@package/addon:profile`
177
190
  const addon = addonDep.split(':')[0];
@@ -189,10 +202,6 @@ const defaultModify = ({
189
202
  !registry.isVoltoProject &&
190
203
  registry.addonNames.length > 0
191
204
  ) {
192
- const devPublicPath = `${projectRootPath}/../../../public`;
193
- if (!fs.existsSync(devPublicPath)) {
194
- fs.mkdirSync(devPublicPath);
195
- }
196
205
  mergeDirectories(
197
206
  path.join(p, 'public'),
198
207
  `${projectRootPath}/../../../public`,
@@ -3,13 +3,16 @@
3
3
  * @module actions/form/form
4
4
  */
5
5
 
6
- import { SET_FORM_DATA } from '@plone/volto/constants/ActionTypes';
6
+ import {
7
+ SET_FORM_DATA,
8
+ SET_UI_STATE,
9
+ } from '@plone/volto/constants/ActionTypes';
7
10
 
8
11
  /**
9
12
  * Set form data function.
10
13
  * @function setFormData
11
14
  * @param {Object} data New form data.
12
- * @returns {Object} Set sidebar action.
15
+ * @returns {Object} Set form data action.
13
16
  */
14
17
  export function setFormData(data) {
15
18
  return {
@@ -17,3 +20,16 @@ export function setFormData(data) {
17
20
  data,
18
21
  };
19
22
  }
23
+
24
+ /**
25
+ * Set ui state function.
26
+ * @function setUIState
27
+ * @param {Object} ui New ui state.
28
+ * @returns {Object} Set ui state action.
29
+ */
30
+ export function setUIState(ui) {
31
+ return {
32
+ type: SET_UI_STATE,
33
+ ui,
34
+ };
35
+ }
@@ -157,7 +157,7 @@ export {
157
157
  resetMetadataFocus,
158
158
  setSidebarTab,
159
159
  } from '@plone/volto/actions/sidebar/sidebar';
160
- export { setFormData } from '@plone/volto/actions/form/form';
160
+ export { setFormData, setUIState } from '@plone/volto/actions/form/form';
161
161
  export {
162
162
  deleteLinkTranslation,
163
163
  getTranslationLocator,
@@ -1,11 +1,14 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
+ import { cloneDeep, map } from 'lodash';
3
4
  import EditBlock from './Edit';
4
5
  import { DragDropList } from '@plone/volto/components';
5
6
  import {
6
7
  getBlocks,
7
8
  getBlocksFieldname,
9
+ getBlocksLayoutFieldname,
8
10
  applyBlockDefaults,
11
+ getBlocksHierarchy,
9
12
  } from '@plone/volto/helpers';
10
13
  import {
11
14
  addBlock,
@@ -13,15 +16,19 @@ import {
13
16
  changeBlock,
14
17
  deleteBlock,
15
18
  moveBlock,
19
+ moveBlockEnhanced,
16
20
  mutateBlock,
17
21
  nextBlockId,
18
22
  previousBlockId,
19
23
  } from '@plone/volto/helpers';
20
24
  import EditBlockWrapper from './EditBlockWrapper';
21
- import { setSidebarTab } from '@plone/volto/actions';
25
+ import { setSidebarTab, setUIState } from '@plone/volto/actions';
22
26
  import { useDispatch } from 'react-redux';
23
27
  import { useDetectClickOutside, useEvent } from '@plone/volto/helpers';
24
28
  import config from '@plone/volto/registry';
29
+ import { createPortal } from 'react-dom';
30
+
31
+ import Order from './Order/Order';
25
32
 
26
33
  const BlocksForm = (props) => {
27
34
  const {
@@ -53,6 +60,12 @@ const BlocksForm = (props) => {
53
60
  token,
54
61
  } = props;
55
62
 
63
+ const [isClient, setIsClient] = useState(false);
64
+
65
+ useEffect(() => {
66
+ setIsClient(true);
67
+ }, []);
68
+
56
69
  const blockList = getBlocks(properties);
57
70
 
58
71
  const dispatch = useDispatch();
@@ -183,6 +196,52 @@ const BlocksForm = (props) => {
183
196
  onChangeFormData(newFormData);
184
197
  };
185
198
 
199
+ const onMoveBlockEnhanced = ({ source, destination }) => {
200
+ const newFormData = moveBlockEnhanced(cloneDeep(properties), {
201
+ source,
202
+ destination,
203
+ });
204
+ const blocksFieldname = getBlocksFieldname(newFormData);
205
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(newFormData);
206
+ let error = false;
207
+
208
+ const allowedBlocks = Object.keys(blocksConfig);
209
+
210
+ map(newFormData[blocksLayoutFieldname].items, (id) => {
211
+ const block = newFormData[blocksFieldname][id];
212
+ if (!allowedBlocks.includes(block['@type'])) {
213
+ error = true;
214
+ }
215
+ if (Array.isArray(block[blocksLayoutFieldname]?.items)) {
216
+ const size = block[blocksLayoutFieldname].items.length;
217
+ const allowedSubBlocks = [
218
+ ...(blocksConfig[block['@type']].allowedBlocks || allowedBlocks),
219
+ 'empty',
220
+ ] || ['empty'];
221
+ if (size < 1 || size > (blocksConfig[block['@type']].maxLength || 4)) {
222
+ error = true;
223
+ }
224
+ map(block[blocksLayoutFieldname].items, (subId) => {
225
+ const subBlock = block[blocksFieldname][subId];
226
+ if (!allowedSubBlocks.includes(subBlock['@type'])) {
227
+ error = true;
228
+ }
229
+ });
230
+ }
231
+ });
232
+
233
+ if (!error) {
234
+ onChangeFormData(newFormData);
235
+ dispatch(
236
+ setUIState({
237
+ selected: null,
238
+ multiSelected: [],
239
+ gridSelected: null,
240
+ }),
241
+ );
242
+ }
243
+ };
244
+
186
245
  const defaultBlockWrapper = ({ draginfo }, editBlock, blockProps) => (
187
246
  <EditBlockWrapper draginfo={draginfo} blockProps={blockProps}>
188
247
  {editBlock}
@@ -195,6 +254,7 @@ const BlocksForm = (props) => {
195
254
  // Note they are alreaady filtered by DragDropList, but we also want them
196
255
  // to be removed when the user saves the page next. Otherwise the invalid
197
256
  // blocks would linger for ever.
257
+
198
258
  for (const [n, v] of blockList) {
199
259
  if (!v) {
200
260
  const newFormData = deleteBlock(properties, n);
@@ -210,85 +270,101 @@ const BlocksForm = (props) => {
210
270
  });
211
271
 
212
272
  return (
213
- <div
214
- className="blocks-form"
215
- role="presentation"
216
- ref={ref}
217
- onKeyDown={(e) => {
218
- if (stopPropagation) {
219
- e.stopPropagation();
220
- }
221
- }}
222
- >
223
- <fieldset className="invisible" disabled={!editable}>
224
- <DragDropList
225
- childList={blockList}
226
- onMoveItem={(result) => {
227
- const { source, destination } = result;
228
- if (!destination) {
229
- return;
230
- }
231
- const newFormData = moveBlock(
232
- properties,
233
- source.index,
234
- destination.index,
235
- );
236
- onChangeFormData(newFormData);
237
- return true;
238
- }}
239
- direction={direction}
240
- >
241
- {(dragProps) => {
242
- const { child, childId, index } = dragProps;
243
- const blockProps = {
244
- allowedBlocks,
245
- showRestricted,
246
- block: childId,
247
- data: child,
248
- handleKeyDown,
249
- id: childId,
250
- formTitle: title,
251
- formDescription: description,
252
- index,
253
- manage,
254
- onAddBlock,
255
- onInsertBlock,
256
- onChangeBlock,
257
- onChangeField,
258
- onChangeFormData,
259
- onDeleteBlock,
260
- onFocusNextBlock,
261
- onFocusPreviousBlock,
262
- onMoveBlock,
263
- onMutateBlock,
264
- onSelectBlock,
265
- pathname,
266
- metadata,
267
- properties,
268
- contentType: type,
269
- navRoot,
270
- blocksConfig,
271
- selected: selectedBlock === childId,
272
- multiSelected: multiSelected?.includes(childId),
273
- type: child['@type'],
274
- editable,
275
- showBlockChooser: selectedBlock === childId,
276
- detached: isContainer,
277
- // Properties to pass to the BlocksForm to match the View ones
278
- content: properties,
279
- history,
280
- location,
281
- token,
282
- };
283
- return editBlockWrapper(
284
- dragProps,
285
- <EditBlock key={childId} {...blockProps} />,
286
- blockProps,
287
- );
288
- }}
289
- </DragDropList>
290
- </fieldset>
291
- </div>
273
+ <>
274
+ {isMainForm &&
275
+ isClient &&
276
+ createPortal(
277
+ <div>
278
+ <Order
279
+ items={getBlocksHierarchy(properties)}
280
+ onMoveBlock={onMoveBlockEnhanced}
281
+ onDeleteBlock={onDeleteBlock}
282
+ onSelectBlock={onSelectBlock}
283
+ removable
284
+ />
285
+ </div>,
286
+ document.getElementById('sidebar-order'),
287
+ )}
288
+ <div
289
+ className="blocks-form"
290
+ role="presentation"
291
+ ref={ref}
292
+ onKeyDown={(e) => {
293
+ if (stopPropagation) {
294
+ e.stopPropagation();
295
+ }
296
+ }}
297
+ >
298
+ <fieldset className="invisible" disabled={!editable}>
299
+ <DragDropList
300
+ childList={blockList}
301
+ onMoveItem={(result) => {
302
+ const { source, destination } = result;
303
+ if (!destination) {
304
+ return;
305
+ }
306
+ const newFormData = moveBlock(
307
+ properties,
308
+ source.index,
309
+ destination.index,
310
+ );
311
+ onChangeFormData(newFormData);
312
+ return true;
313
+ }}
314
+ direction={direction}
315
+ >
316
+ {(dragProps) => {
317
+ const { child, childId, index } = dragProps;
318
+ const blockProps = {
319
+ allowedBlocks,
320
+ showRestricted,
321
+ block: childId,
322
+ data: child,
323
+ handleKeyDown,
324
+ id: childId,
325
+ formTitle: title,
326
+ formDescription: description,
327
+ index,
328
+ manage,
329
+ onAddBlock,
330
+ onInsertBlock,
331
+ onChangeBlock,
332
+ onChangeField,
333
+ onChangeFormData,
334
+ onDeleteBlock,
335
+ onFocusNextBlock,
336
+ onFocusPreviousBlock,
337
+ onMoveBlock,
338
+ onMutateBlock,
339
+ onSelectBlock,
340
+ pathname,
341
+ metadata,
342
+ properties,
343
+ contentType: type,
344
+ navRoot,
345
+ blocksConfig,
346
+ selected: selectedBlock === childId,
347
+ multiSelected: multiSelected?.includes(childId),
348
+ type: child['@type'],
349
+ editable,
350
+ showBlockChooser: selectedBlock === childId,
351
+ detached: isContainer,
352
+ // Properties to pass to the BlocksForm to match the View ones
353
+ content: properties,
354
+ history,
355
+ location,
356
+ token,
357
+ };
358
+ return editBlockWrapper(
359
+ dragProps,
360
+ <EditBlock key={childId} {...blockProps} />,
361
+ blockProps,
362
+ );
363
+ }}
364
+ </DragDropList>
365
+ </fieldset>
366
+ </div>
367
+ </>
292
368
  );
293
369
  };
294
370
 
@@ -30,6 +30,9 @@ test('Allow override of blocksConfig', () => {
30
30
  locale: 'en',
31
31
  messages: {},
32
32
  },
33
+ form: {
34
+ ui: {},
35
+ },
33
36
  });
34
37
 
35
38
  const data = {
@@ -68,6 +71,7 @@ test('Allow override of blocksConfig', () => {
68
71
  const { container } = render(
69
72
  <Provider store={store}>
70
73
  <BlocksForm {...data} />
74
+ <div id="sidebar-order"></div>
71
75
  </Provider>,
72
76
  );
73
77
  expect(container).toMatchSnapshot();
@@ -79,6 +83,9 @@ test('Removes invalid blocks on saving', () => {
79
83
  locale: 'en',
80
84
  messages: {},
81
85
  },
86
+ form: {
87
+ ui: {},
88
+ },
82
89
  });
83
90
 
84
91
  const onChangeFormData = jest.fn(() => {});
@@ -120,6 +127,7 @@ test('Removes invalid blocks on saving', () => {
120
127
  render(
121
128
  <Provider store={store}>
122
129
  <BlocksForm {...data} />
130
+ <div id="sidebar-order"></div>
123
131
  </Provider>,
124
132
  );
125
133
  expect(onChangeFormData).toBeCalledWith({
@@ -9,7 +9,7 @@ import { compose } from 'redux';
9
9
  import { connect } from 'react-redux';
10
10
  import { defineMessages, injectIntl } from 'react-intl';
11
11
  import cx from 'classnames';
12
- import { setSidebarTab } from '@plone/volto/actions';
12
+ import { setSidebarTab, setUIState } from '@plone/volto/actions';
13
13
  import config from '@plone/volto/registry';
14
14
  import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
15
15
  import { applyBlockDefaults } from '@plone/volto/helpers';
@@ -79,7 +79,11 @@ export class Edit extends Component {
79
79
  this.blockNode.current.focus();
80
80
  }
81
81
  const tab = this.props.manage ? 1 : blocksConfig?.[type]?.sidebarTab || 0;
82
- if (this.props.selected && this.props.editable) {
82
+ if (
83
+ this.props.selected &&
84
+ this.props.editable &&
85
+ this.props.sidebarTab !== 2
86
+ ) {
83
87
  this.props.setSidebarTab(tab);
84
88
  }
85
89
  }
@@ -105,7 +109,9 @@ export class Edit extends Component {
105
109
  const tab = this.props.manage
106
110
  ? 1
107
111
  : blocksConfig?.[nextProps.type]?.sidebarTab || 0;
108
- this.props.setSidebarTab(tab);
112
+ if (this.props.sidebarTab !== 2) {
113
+ this.props.setSidebarTab(tab);
114
+ }
109
115
  }
110
116
  }
111
117
 
@@ -138,6 +144,21 @@ export class Edit extends Component {
138
144
  {Block !== null ? (
139
145
  <div
140
146
  role="presentation"
147
+ onMouseOver={() => {
148
+ if (this.props.hovered !== this.props.id) {
149
+ this.props.setUIState({ hovered: this.props.id });
150
+ }
151
+ }}
152
+ onFocus={() => {
153
+ // TODO: This `onFocus` steals somehow the focus from the slate block
154
+ // we have to investigate why this is happening
155
+ // Apparently, I can't see any difference in the behavior
156
+ // If any, we can fix it in successive iterations
157
+ // if (this.props.hovered !== this.props.id) {
158
+ // this.props.setUIState({ hovered: this.props.id });
159
+ // }
160
+ }}
161
+ onMouseLeave={() => this.props.setUIState({ hovered: null })}
141
162
  onClick={(e) => {
142
163
  const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey;
143
164
  !this.props.selected &&
@@ -161,6 +182,7 @@ export class Edit extends Component {
161
182
  className={cx('block', type, this.props.data.variation, {
162
183
  selected: this.props.selected || this.props.multiSelected,
163
184
  multiSelected: this.props.multiSelected,
185
+ hovered: this.props.hovered === this.props.id,
164
186
  })}
165
187
  style={{ outline: 'none' }}
166
188
  ref={this.blockNode}
@@ -185,6 +207,11 @@ export class Edit extends Component {
185
207
  ) : (
186
208
  <div
187
209
  role="presentation"
210
+ onMouseOver={() =>
211
+ this.props.setUIState({ hovered: this.props.id })
212
+ }
213
+ onFocus={() => this.props.setUIState({ hovered: this.props.id })}
214
+ onMouseLeave={() => this.props.setUIState({ hovered: null })}
188
215
  onClick={() =>
189
216
  !this.props.selected && this.props.onSelectBlock(this.props.id)
190
217
  }
@@ -218,5 +245,11 @@ export class Edit extends Component {
218
245
  export default compose(
219
246
  injectIntl,
220
247
  withObjectBrowser,
221
- connect(null, { setSidebarTab }),
248
+ connect(
249
+ (state, props) => ({
250
+ hovered: state.form?.ui.hovered || null,
251
+ sidebarTab: state.sidebar?.tab,
252
+ }),
253
+ { setSidebarTab, setUIState },
254
+ ),
222
255
  )(Edit);
@@ -0,0 +1,122 @@
1
+ import React, { forwardRef } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useDispatch, useSelector } from 'react-redux';
4
+ import { includes } from 'lodash';
5
+
6
+ import { Icon } from '@plone/volto/components';
7
+ import { setUIState } from '@plone/volto/actions';
8
+ import config from '@plone/volto/registry';
9
+
10
+ import deleteSVG from '@plone/volto/icons/delete.svg';
11
+ import dragSVG from '@plone/volto/icons/drag.svg';
12
+
13
+ export const Item = forwardRef(
14
+ (
15
+ {
16
+ clone,
17
+ data,
18
+ depth,
19
+ disableSelection,
20
+ disableInteraction,
21
+ ghost,
22
+ id,
23
+ handleProps,
24
+ indentationWidth,
25
+ onRemove,
26
+ onSelectBlock,
27
+ parentId,
28
+ style,
29
+ value,
30
+ wrapperRef,
31
+ ...props
32
+ },
33
+ ref,
34
+ ) => {
35
+ const selected = useSelector((state) => state.form.ui.selected);
36
+ const hovered = useSelector((state) => state.form.ui.hovered);
37
+ const multiSelected = useSelector((state) => state.form.ui.multiSelected);
38
+ const gridSelected = useSelector((state) => state.form.ui.gridSelected);
39
+ const dispatch = useDispatch();
40
+ return (
41
+ <li
42
+ className={classNames(
43
+ 'tree-item-wrapper',
44
+ clone && 'clone',
45
+ ghost && 'ghost',
46
+ disableSelection && 'disable-selection',
47
+ disableInteraction && 'disable-interaction',
48
+ )}
49
+ role="presentation"
50
+ onMouseOver={() => dispatch(setUIState({ hovered: id }))}
51
+ onFocus={() => dispatch(setUIState({ hovered: id }))}
52
+ onMouseLeave={() => dispatch(setUIState({ hovered: null }))}
53
+ onClick={(e) => {
54
+ if (depth === 0) {
55
+ const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey;
56
+ selected !== id &&
57
+ onSelectBlock(
58
+ id,
59
+ selected === id ? false : isMultipleSelection,
60
+ e,
61
+ );
62
+ } else {
63
+ dispatch(
64
+ setUIState({
65
+ selected: parentId,
66
+ multiSelected: [],
67
+ gridSelected: id,
68
+ }),
69
+ );
70
+ }
71
+ }}
72
+ ref={wrapperRef}
73
+ style={{
74
+ '--spacing': `${indentationWidth * depth}px`,
75
+ }}
76
+ {...props}
77
+ >
78
+ <div
79
+ className={classNames(
80
+ 'tree-item',
81
+ (selected === id || gridSelected === id) && 'selected',
82
+ hovered === id && 'hovered',
83
+ includes(multiSelected, id) && 'multiSelected',
84
+ `depth-${depth}`,
85
+ )}
86
+ ref={ref}
87
+ style={style}
88
+ >
89
+ <button
90
+ ref={ref}
91
+ {...handleProps}
92
+ className={classNames('action', 'drag')}
93
+ tabIndex={0}
94
+ data-cypress="draggable-handle"
95
+ >
96
+ <Icon name={dragSVG} size="16px" />
97
+ </button>
98
+ <span className="text">
99
+ {config.blocks.blocksConfig[data?.['@type']]?.icon && (
100
+ <Icon
101
+ name={config.blocks.blocksConfig[data?.['@type']]?.icon}
102
+ size="20px"
103
+ style={{ verticalAlign: 'middle' }}
104
+ />
105
+ )}{' '}
106
+ {data?.plaintext ||
107
+ config.blocks.blocksConfig[data?.['@type']]?.title}
108
+ </span>
109
+ {!clone && onRemove && (
110
+ <button
111
+ onClick={onRemove}
112
+ className={classNames('action', 'delete')}
113
+ tabIndex={0}
114
+ >
115
+ <Icon name={deleteSVG} size="18" />
116
+ </button>
117
+ )}
118
+ </div>
119
+ </li>
120
+ );
121
+ },
122
+ );