@eeacms/volto-eea-website-theme 3.19.1 → 4.0.1

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 (112) hide show
  1. package/.eslintrc.js +7 -6
  2. package/CHANGELOG.md +26 -0
  3. package/DEVELOP.md +19 -17
  4. package/README.md +19 -7
  5. package/docker-compose.yml +1 -1
  6. package/jest-addon.config.js +8 -4
  7. package/package.json +1 -1
  8. package/src/actions/navigation.js +1 -1
  9. package/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx +4 -2
  10. package/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.test.jsx +25 -19
  11. package/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx +2 -1
  12. package/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.test.jsx +6 -4
  13. package/src/components/manage/Blocks/ContextNavigation/variations/Accordion.jsx +2 -2
  14. package/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.jsx +4 -2
  15. package/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.test.jsx +1 -1
  16. package/src/components/manage/Blocks/GroupBlockTemplate/FlexGroup/FlexGroup.jsx +12 -44
  17. package/src/components/manage/Blocks/GroupBlockTemplate/FlexGroup/RenderBlocks.jsx +5 -4
  18. package/src/components/manage/Blocks/GroupBlockTemplate/FlexGroup/editor-flex.less +45 -4
  19. package/src/components/manage/Blocks/LayoutSettings/LayoutSettingsEdit.jsx +2 -1
  20. package/src/components/manage/Blocks/LayoutSettings/LayoutSettingsEdit.test.jsx +12 -4
  21. package/src/components/manage/Blocks/LayoutSettings/LayoutSettingsView.jsx +1 -1
  22. package/src/components/manage/Blocks/Title/Edit.jsx +3 -3
  23. package/src/components/manage/Blocks/Title/View.jsx +2 -1
  24. package/src/components/manage/Blocks/Title/variations/WebReport.jsx +2 -2
  25. package/src/components/manage/Blocks/Title/variations/WebReport.test.jsx +6 -4
  26. package/src/components/manage/Blocks/Title/variations/WebReportPage.jsx +2 -2
  27. package/src/components/manage/Blocks/Title/variations/WebReportPage.test.jsx +6 -4
  28. package/src/components/theme/Banner/View.jsx +1 -1
  29. package/src/components/theme/BaseTag.jsx +2 -1
  30. package/src/components/theme/BaseTag.test.jsx +7 -2
  31. package/src/components/theme/DraftBackground/DraftBackground.jsx +2 -1
  32. package/src/components/theme/Homepage/HomePageInverseView.jsx +3 -3
  33. package/src/components/theme/Homepage/HomePageInverseView.test.jsx +1 -1
  34. package/src/components/theme/Homepage/HomePageView.jsx +3 -3
  35. package/src/components/theme/Homepage/HomePageView.test.jsx +1 -1
  36. package/src/components/theme/Logo.jsx +3 -3
  37. package/src/components/theme/NotFound/GoneView.jsx +3 -2
  38. package/src/components/theme/NotFound/GoneView.test.jsx +5 -4
  39. package/src/components/theme/NotFound/NotFound.jsx +1 -1
  40. package/src/components/theme/NotFound/NotFound.test.jsx +3 -2
  41. package/src/components/theme/PrintLoader/PrintLoader.test.jsx +1 -1
  42. package/src/components/theme/SubsiteClass.jsx +6 -4
  43. package/src/components/theme/SubsiteClass.test.jsx +3 -2
  44. package/src/components/theme/WebReport/WebReportSectionView.jsx +2 -2
  45. package/src/components/theme/WebReport/WebReportSectionView.test.jsx +10 -5
  46. package/src/components/theme/Widgets/ADUserGroupSelectWidget.jsx +2 -2
  47. package/src/components/theme/Widgets/ContributorsViewWidget.jsx +1 -1
  48. package/src/components/theme/Widgets/CreatableSelectWidget.jsx +7 -4
  49. package/src/components/theme/Widgets/CreatorsViewWidget.jsx +1 -1
  50. package/src/components/theme/Widgets/DateWidget.jsx +1 -1
  51. package/src/components/theme/Widgets/DateWidget.test.js +1 -1
  52. package/src/components/theme/Widgets/DatetimeWidget.jsx +1 -1
  53. package/src/components/theme/Widgets/DatetimeWidget.test.js +1 -1
  54. package/src/components/theme/Widgets/ImageViewWidget.jsx +1 -0
  55. package/src/components/theme/Widgets/NavigationBehaviorWidget.jsx +7 -3
  56. package/src/components/theme/Widgets/NavigationBehaviorWidget.test.jsx +51 -46
  57. package/src/components/theme/Widgets/UserSelectWidget.jsx +13 -10
  58. package/src/customizations/@plone/volto-slate/blocks/Table/TableBlockView.jsx +3 -3
  59. package/src/customizations/@plone/volto-slate/blocks/Text/TextBlockView.jsx +2 -2
  60. package/src/customizations/@plone/volto-slate/editor/SlateEditor.jsx +23 -10
  61. package/src/customizations/@plone/volto-slate/editor/render.jsx +7 -3
  62. package/src/customizations/@plone/volto-slate/utils/blocks.js +11 -8
  63. package/src/customizations/volto/components/manage/Blocks/Grid/View.jsx +2 -2
  64. package/src/customizations/volto/components/manage/Blocks/Image/Edit.jsx +30 -27
  65. package/src/customizations/volto/components/manage/Blocks/Image/Edit.test.jsx +244 -246
  66. package/src/customizations/volto/components/manage/Blocks/Image/View.jsx +23 -25
  67. package/src/customizations/volto/components/manage/Blocks/LeadImage/Edit.jsx +6 -4
  68. package/src/customizations/volto/components/manage/Blocks/LeadImage/LeadImageSidebar.jsx +4 -2
  69. package/src/customizations/volto/components/manage/Blocks/LeadImage/View.jsx +2 -2
  70. package/src/customizations/volto/components/manage/Controlpanels/Groups/RenderGroups.jsx +1 -1
  71. package/src/customizations/volto/components/manage/Controlpanels/Groups/RenderGroups.test.jsx +108 -42
  72. package/src/customizations/volto/components/manage/Diff/DiffField.jsx +4 -3
  73. package/src/customizations/volto/components/manage/Display/Display.jsx +8 -7
  74. package/src/customizations/volto/components/manage/Sidebar/ObjectBrowserBody.jsx +42 -21
  75. package/src/customizations/volto/components/manage/Sidebar/ObjectBrowserNav.jsx +2 -1
  76. package/src/customizations/volto/components/manage/Sidebar/SidebarPopup.jsx +46 -24
  77. package/src/customizations/volto/components/manage/Sidebar/objectBrowserSelection.js +58 -0
  78. package/src/customizations/volto/components/manage/Toolbar/More.jsx +8 -10
  79. package/src/customizations/volto/components/manage/Widgets/NumberWidget.jsx +1 -1
  80. package/src/customizations/volto/components/manage/Widgets/NumberWidget.test.jsx +6 -1
  81. package/src/customizations/volto/components/manage/Widgets/ObjectBrowserWidget.jsx +66 -12
  82. package/src/customizations/volto/components/manage/Workflow/Workflow.jsx +10 -9
  83. package/src/customizations/volto/components/theme/Breadcrumbs/Breadcrumbs.jsx +3 -2
  84. package/src/customizations/volto/components/theme/Comments/Comments.jsx +9 -8
  85. package/src/customizations/volto/components/theme/Comments/Comments.test.jsx +29 -7
  86. package/src/customizations/volto/components/theme/ContactForm/ContactForm.jsx +1 -1
  87. package/src/customizations/volto/components/theme/ContactForm/ContactForm.test.js +5 -0
  88. package/src/customizations/volto/components/theme/ContentMetadataTags/ContentMetadataTags.jsx +5 -7
  89. package/src/customizations/volto/components/theme/EventDetails/EventDetails.jsx +2 -2
  90. package/src/customizations/volto/components/theme/Footer/Footer.jsx +1 -1
  91. package/src/customizations/volto/components/theme/Header/Header.jsx +10 -8
  92. package/src/customizations/volto/components/theme/Header/Header.test.jsx +1 -1
  93. package/src/customizations/volto/components/theme/Header/LanguageSwitcher.jsx +3 -3
  94. package/src/customizations/volto/components/theme/Image/Image.jsx +4 -3
  95. package/src/customizations/volto/components/theme/Unauthorized/Unauthorized.jsx +1 -1
  96. package/src/customizations/volto/components/theme/View/DefaultView.jsx +4 -3
  97. package/src/customizations/volto/components/theme/View/EventView.jsx +3 -2
  98. package/src/customizations/volto/helpers/Html/Html.jsx +16 -6
  99. package/src/customizations/volto/helpers/Html/Readme.md +7 -1
  100. package/src/customizations/volto/reducers/breadcrumbs/breadcrumbs.js +3 -6
  101. package/src/customizations/volto/server.jsx +13 -15
  102. package/src/helpers/schema-utils.js +1 -1
  103. package/src/helpers/schema-utils.test.js +1 -1
  104. package/src/hocs/withErrorBoundary.jsx +1 -1
  105. package/src/hocs/withErrorBoundary.test.jsx +4 -11
  106. package/src/hocs/withRootNavigation.jsx +3 -2
  107. package/src/hocs/withRootNavigation.test.jsx +18 -14
  108. package/src/index.js +3 -3
  109. package/src/slate.js +1 -1
  110. package/src/customizations/volto/components/manage/Blocks/LeadImage/AlignChooser.jsx +0 -76
  111. package/src/customizations/volto/components/manage/Blocks/LeadImage/AlignChooser.test.js +0 -50
  112. package/src/customizations/volto/components/manage/Sidebar/SidebarPopup copy.jsx +0 -82
@@ -2,8 +2,9 @@ import React, { useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Segment } from 'semantic-ui-react';
4
4
  import { FormattedMessage, injectIntl } from 'react-intl';
5
- import { Icon, BlockDataForm } from '@plone/volto/components';
6
- import { flattenToAppURL } from '@plone/volto/helpers';
5
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
6
+ import BlockDataForm from '@plone/volto/components/manage/Form/BlockDataForm';
7
+ import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
7
8
  import { LeadImageSchema } from './schema';
8
9
  import imageSVG from '@plone/volto/icons/image.svg';
9
10
 
@@ -48,6 +49,7 @@ const LeadImageSidebar = ({ properties, data, block, onChangeBlock, intl }) => {
48
49
  <>
49
50
  <Segment className="sidebar-metadata-container" secondary>
50
51
  {properties.image.filename}
52
+ {/* eslint-disable-next-line no-restricted-syntax */}
51
53
  <img
52
54
  src={
53
55
  properties.image.data
@@ -5,10 +5,10 @@
5
5
 
6
6
  import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
- import { UniversalLink } from '@plone/volto/components';
8
+ import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
9
9
  import cx from 'classnames';
10
10
  import config from '@plone/volto/registry';
11
- import { Copyright } from '@eeacms/volto-eea-design-system/ui';
11
+ import Copyright from '@eeacms/volto-eea-design-system/ui/Copyright/Copyright';
12
12
  import { Icon } from 'semantic-ui-react';
13
13
 
14
14
  /**
@@ -8,7 +8,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
8
8
  import { Dropdown, Table, Checkbox } from 'semantic-ui-react';
9
9
  import trashSVG from '@plone/volto/icons/delete.svg';
10
10
  import ploneSVG from '@plone/volto/icons/plone.svg';
11
- import { Icon } from '@plone/volto/components';
11
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
12
12
 
13
13
  /**
14
14
  * UsersControlpanelGroups class.
@@ -1,58 +1,124 @@
1
+ /**
2
+ * RenderGroups — unit tests
3
+ *
4
+ * Volto 17 / 18 dual-support notes
5
+ * ---------------------------------
6
+ * The EEA change is a single-line tweak on line 80:
7
+ * `{this.props.group.title || this.props.group.groupname}`
8
+ * instead of showing only `groupname`.
9
+ *
10
+ * V17 and V18 upstream are byte-for-byte identical, so this shadow is
11
+ * compatible with both versions without any rebase.
12
+ *
13
+ * The `@plone/volto/components` barrel is mocked to prevent the chain:
14
+ * TranslationObject.jsx → store.js → @root/reducers
15
+ * that cannot be resolved in this Jest environment. The previous snapshot
16
+ * test used `react-test-renderer` which produced a snapshot that contained
17
+ * the full icon SVG tree; the new tests use @testing-library/react and
18
+ * assert on the actual EEA-specific behavior instead.
19
+ */
20
+
1
21
  import React from 'react';
2
- import renderer from 'react-test-renderer';
22
+ import { render, screen } from '@testing-library/react';
23
+ import '@testing-library/jest-dom';
3
24
  import configureStore from 'redux-mock-store';
4
25
  import { Provider } from 'react-intl-redux';
5
26
 
6
27
  import RenderGroups from './RenderGroups';
7
28
 
29
+ jest.mock('@plone/volto/components', () => ({
30
+ // RenderGroups.jsx uses only Icon from the barrel
31
+ Icon: () => null,
32
+ }));
33
+
8
34
  const mockStore = configureStore();
9
35
 
10
- const testGroups = {
11
- '@id': 'http://localhost:55001/plone/@groups/Administrators',
12
- description: '',
13
- email: '',
14
- groupname: 'Administrators',
15
- id: 'Administrators',
16
- title: 'Administrators',
17
- roles: ['Manager'],
18
- };
36
+ const makeStore = () =>
37
+ mockStore({
38
+ intl: { locale: 'en', messages: {} },
39
+ });
19
40
 
20
41
  const testRoles = [
21
- {
22
- '@id': 'http://localhost:8080/Plone/@roles/Member',
23
- '@type': 'role',
24
- id: 'Member',
25
- },
26
- {
27
- '@id': 'http://localhost:8080/Plone/@roles/Reader',
28
- '@type': 'role',
29
- id: 'Reader',
30
- },
31
- {
32
- '@id': 'http://localhost:8080/Plone/@roles/Manager',
33
- '@type': 'role',
34
- id: 'Manager',
35
- },
42
+ { '@id': 'http://localhost:8080/Plone/@roles/Member', id: 'Member' },
43
+ { '@id': 'http://localhost:8080/Plone/@roles/Reader', id: 'Reader' },
44
+ { '@id': 'http://localhost:8080/Plone/@roles/Manager', id: 'Manager' },
36
45
  ];
37
46
 
38
- describe('UsersControlpanelGroups', () => {
39
- it('renders a UsersControlpanelGroups component', () => {
40
- const store = mockStore({
41
- intl: {
42
- locale: 'en',
43
- messages: {},
44
- },
45
- });
46
- const component = renderer.create(
47
- <Provider store={store}>
48
- <RenderGroups
49
- group={testGroups}
50
- roles={testRoles}
51
- onDelete={() => {}}
52
- />
47
+ describe('RenderGroups — EEA: title || groupname display', () => {
48
+ it('shows the group title when available (EEA change)', () => {
49
+ const group = {
50
+ groupname: 'editors',
51
+ title: 'Site Editors',
52
+ roles: [],
53
+ };
54
+ render(
55
+ <Provider store={makeStore()}>
56
+ <RenderGroups group={group} roles={testRoles} onDelete={() => {}} />
57
+ </Provider>,
58
+ );
59
+ // EEA customization: title takes precedence over groupname
60
+ expect(screen.getByText('Site Editors')).toBeInTheDocument();
61
+ });
62
+
63
+ it('falls back to groupname when title is absent', () => {
64
+ const group = {
65
+ groupname: 'Administrators',
66
+ title: '',
67
+ roles: ['Manager'],
68
+ };
69
+ render(
70
+ <Provider store={makeStore()}>
71
+ <RenderGroups group={group} roles={testRoles} onDelete={() => {}} />
72
+ </Provider>,
73
+ );
74
+ expect(screen.getByText('Administrators')).toBeInTheDocument();
75
+ });
76
+
77
+ it('falls back to groupname when title is undefined', () => {
78
+ const group = {
79
+ groupname: 'Reviewers',
80
+ // no title key at all
81
+ roles: [],
82
+ };
83
+ render(
84
+ <Provider store={makeStore()}>
85
+ <RenderGroups group={group} roles={testRoles} onDelete={() => {}} />
86
+ </Provider>,
87
+ );
88
+ expect(screen.getByText('Reviewers')).toBeInTheDocument();
89
+ });
90
+
91
+ it('renders a checkbox for each role', () => {
92
+ const group = {
93
+ groupname: 'Administrators',
94
+ title: 'Administrators',
95
+ roles: ['Manager'],
96
+ };
97
+ const { container } = render(
98
+ <Provider store={makeStore()}>
99
+ <RenderGroups group={group} roles={testRoles} onDelete={() => {}} />
100
+ </Provider>,
101
+ );
102
+ // One checkbox per role
103
+ const checkboxes = container.querySelectorAll('input[type="checkbox"]');
104
+ expect(checkboxes).toHaveLength(testRoles.length);
105
+ });
106
+
107
+ it('checks the checkbox for roles the group already has', () => {
108
+ const group = {
109
+ groupname: 'managers',
110
+ title: 'Managers',
111
+ roles: ['Manager'],
112
+ };
113
+ const { container } = render(
114
+ <Provider store={makeStore()}>
115
+ <RenderGroups group={group} roles={testRoles} onDelete={() => {}} />
53
116
  </Provider>,
54
117
  );
55
- const json = component.toJSON();
56
- expect(json).toMatchSnapshot();
118
+ const checkboxes = container.querySelectorAll('input[type="checkbox"]');
119
+ // Member → unchecked, Reader → unchecked, Manager → checked
120
+ expect(checkboxes[0]).not.toBeChecked();
121
+ expect(checkboxes[1]).not.toBeChecked();
122
+ expect(checkboxes[2]).toBeChecked();
57
123
  });
58
124
  });
@@ -4,7 +4,8 @@
4
4
  */
5
5
 
6
6
  import React from 'react';
7
- import { join, map } from 'lodash';
7
+ import join from 'lodash/join';
8
+ import map from 'lodash/map';
8
9
  import PropTypes from 'prop-types';
9
10
  import { Grid } from 'semantic-ui-react';
10
11
  import ReactDOMServer from 'react-dom/server';
@@ -13,9 +14,9 @@ import { createBrowserHistory } from 'history';
13
14
  import { ConnectedRouter } from 'connected-react-router';
14
15
  import { useSelector, useStore } from 'react-redux';
15
16
  import config from '@plone/volto/registry';
16
- import { Api } from '@plone/volto/helpers';
17
+ import Api from '@plone/volto/helpers/Api/Api';
17
18
  import configureStore from '@plone/volto/store';
18
- import { RenderBlocks } from '@plone/volto/components';
19
+ import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks';
19
20
  import { serializeNodes } from '@plone/volto-slate/editor/render';
20
21
  import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
21
22
 
@@ -5,14 +5,15 @@ import { compose } from 'redux';
5
5
  import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
6
6
 
7
7
  import jwtDecode from 'jwt-decode';
8
+ import { getSchema } from '@plone/volto/actions/schema/schema';
9
+ import { getUser } from '@plone/volto/actions/users/users';
8
10
  import {
9
- getSchema,
10
- getUser,
11
11
  updateContent,
12
12
  getContent,
13
- } from '@plone/volto/actions';
14
- import { getLayoutFieldname } from '@plone/volto/helpers';
15
- import { FormFieldWrapper, Icon } from '@plone/volto/components';
13
+ } from '@plone/volto/actions/content/content';
14
+ import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content';
15
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
16
+ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
16
17
  import { defineMessages, injectIntl } from 'react-intl';
17
18
  import config from '@plone/volto/registry';
18
19
 
@@ -101,8 +102,8 @@ const customSelectStyles = {
101
102
  color: state.isSelected
102
103
  ? '#007bc1'
103
104
  : state.isFocused
104
- ? '#4a4a4a'
105
- : 'inherit',
105
+ ? '#4a4a4a'
106
+ : 'inherit',
106
107
  ':active': {
107
108
  backgroundColor: null,
108
109
  },
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
7
7
  import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
8
8
  import { Input, Segment, Breadcrumb } from 'semantic-ui-react';
9
9
 
10
- import { join } from 'lodash';
10
+ import join from 'lodash/join';
11
11
 
12
12
  // These absolute imports (without using the corresponding centralized index.js) are required
13
13
  // to cut circular import problems, this file should never use them. This is because of
@@ -25,6 +25,11 @@ import linkSVG from '@plone/volto/icons/link.svg';
25
25
  import homeSVG from '@plone/volto/icons/home.svg';
26
26
 
27
27
  import ObjectBrowserNav from '@plone/volto/components/manage/Sidebar/ObjectBrowserNav';
28
+ import {
29
+ isItemAlreadySelected,
30
+ isSelectableObjectBrowserItem,
31
+ shouldCloseAfterObjectBrowserSelection,
32
+ } from './objectBrowserSelection';
28
33
 
29
34
  const messages = defineMessages({
30
35
  SearchInputPlaceholder: {
@@ -74,6 +79,7 @@ class ObjectBrowserBody extends Component {
74
79
  maximumSelectionSize: PropTypes.number,
75
80
  contextURL: PropTypes.string,
76
81
  searchableTypes: PropTypes.arrayOf(PropTypes.string),
82
+ onlyFolderishSelectable: PropTypes.bool,
77
83
  };
78
84
 
79
85
  /**
@@ -89,6 +95,7 @@ class ObjectBrowserBody extends Component {
89
95
  selectableTypes: [],
90
96
  searchableTypes: null,
91
97
  maximumSelectionSize: null,
98
+ onlyFolderishSelectable: false,
92
99
  };
93
100
 
94
101
  /**
@@ -106,27 +113,27 @@ class ObjectBrowserBody extends Component {
106
113
  this.props.mode === 'multiple'
107
114
  ? '/'
108
115
  : this.props.mode === 'image' && this.props.data?.url
109
- ? getParentURL(this.props.data.url)
110
- : '/',
116
+ ? getParentURL(this.props.data.url)
117
+ : '/',
111
118
  currentLinkFolder:
112
119
  this.props.mode === 'multiple'
113
120
  ? '/'
114
121
  : this.props.mode === 'link' && this.props.data?.href
115
- ? getParentURL(this.props.data.href)
116
- : '/',
122
+ ? getParentURL(this.props.data.href)
123
+ : '/',
117
124
  parentFolder: '',
118
125
  selectedImage:
119
126
  this.props.mode === 'multiple'
120
127
  ? ''
121
128
  : this.props.mode === 'image' && this.props.data?.url
122
- ? flattenToAppURL(this.props.data.url)
123
- : '',
129
+ ? flattenToAppURL(this.props.data.url)
130
+ : '',
124
131
  selectedHref:
125
132
  this.props.mode === 'multiple'
126
133
  ? ''
127
134
  : this.props.mode === 'link' && this.props.data?.href
128
- ? flattenToAppURL(this.props.data.href)
129
- : '',
135
+ ? flattenToAppURL(this.props.data.href)
136
+ : '',
130
137
  showSearchInput: false,
131
138
  // In image mode, the searchable types default to the image types which
132
139
  // can be overridden with the property if specified.
@@ -152,8 +159,8 @@ class ObjectBrowserBody extends Component {
152
159
  mode === 'multiple'
153
160
  ? ''
154
161
  : mode === 'image'
155
- ? this.state.selectedImage
156
- : this.state.selectedHref;
162
+ ? this.state.selectedImage
163
+ : this.state.selectedHref;
157
164
  if (currentSelected && isInternalURL(currentSelected)) {
158
165
  this.props.searchContent(
159
166
  getParentURL(currentSelected),
@@ -299,9 +306,15 @@ class ObjectBrowserBody extends Component {
299
306
  };
300
307
 
301
308
  isSelectable = (item) => {
302
- return this.props.selectableTypes.length > 0
303
- ? this.props.selectableTypes.indexOf(item['@type']) >= 0
304
- : true;
309
+ return isSelectableObjectBrowserItem({
310
+ item,
311
+ selectableTypes: this.props.selectableTypes,
312
+ onlyFolderishSelectable: this.props.onlyFolderishSelectable,
313
+ maximumSelectionSize: this.props.maximumSelectionSize,
314
+ data: this.props.data,
315
+ mode: this.props.mode,
316
+ normalize: flattenToAppURL,
317
+ });
305
318
  };
306
319
 
307
320
  handleClickOnItem = (item) => {
@@ -318,15 +331,23 @@ class ObjectBrowserBody extends Component {
318
331
  !this.props.maximumSelectionSize ||
319
332
  this.props.mode === 'multiple' ||
320
333
  !this.props.data ||
321
- this.props.data.length < this.props.maximumSelectionSize
334
+ this.props.data.length <= this.props.maximumSelectionSize
322
335
  ) {
323
- this.onSelectItem(item);
324
- let length = this.props.data ? this.props.data.length : 0;
336
+ const isDeselecting =
337
+ this.props.mode === 'multiple' &&
338
+ isItemAlreadySelected({
339
+ data: this.props.data,
340
+ item,
341
+ normalize: flattenToAppURL,
342
+ });
325
343
 
326
- let stopSelecting =
327
- this.props.mode !== 'multiple' ||
328
- (this.props.maximumSelectionSize > 0 &&
329
- length + 1 >= this.props.maximumSelectionSize);
344
+ this.onSelectItem(item);
345
+ const stopSelecting = shouldCloseAfterObjectBrowserSelection({
346
+ mode: this.props.mode,
347
+ maximumSelectionSize: this.props.maximumSelectionSize,
348
+ currentLength: this.props.data ? this.props.data.length : 0,
349
+ isDeselecting,
350
+ });
330
351
 
331
352
  if (stopSelecting) {
332
353
  this.props.closeObjectBrowser();
@@ -3,7 +3,8 @@ import { Button, Segment, Popup } from 'semantic-ui-react';
3
3
  import { useIntl, defineMessages } from 'react-intl';
4
4
  import cx from 'classnames';
5
5
  import Icon from '@plone/volto/components/theme/Icon/Icon';
6
- import { flattenToAppURL, getContentIcon } from '@plone/volto/helpers';
6
+ import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
7
+ import { getContentIcon } from '@plone/volto/helpers/Content/Content';
7
8
  import { Image } from 'semantic-ui-react';
8
9
  import config from '@plone/volto/registry';
9
10
 
@@ -1,9 +1,10 @@
1
- // Check this https://github.com/plone/volto/pull/5520
1
+ // Backport of https://github.com/plone/volto/pull/5520 for Volto 17.
2
+ // On Volto 18 this shadow is identical to upstream — keep while V17 is supported.
2
3
  import React from 'react';
3
- import { Portal } from 'react-portal';
4
+ import { createPortal } from 'react-dom';
4
5
  import { CSSTransition } from 'react-transition-group';
5
6
  import PropTypes from 'prop-types';
6
- import { doesNodeContainClick } from 'semantic-ui-react/dist/commonjs/lib';
7
+ import doesNodeContainClick from 'semantic-ui-react/dist/commonjs/lib/doesNodeContainClick';
7
8
 
8
9
  const DEFAULT_TIMEOUT = 500;
9
10
 
@@ -17,10 +18,24 @@ const SidebarPopup = (props) => {
17
18
  onClose();
18
19
  };
19
20
 
21
+ const handleEscapeKey = (e) => {
22
+ if (open && e.key === 'Escape') {
23
+ onClose();
24
+ e.stopPropagation();
25
+ }
26
+ };
27
+
28
+ const [isClient, setIsClient] = React.useState(false);
29
+ React.useEffect(() => {
30
+ setIsClient(true);
31
+ }, []);
32
+
20
33
  React.useEffect(() => {
21
34
  document.addEventListener('mousedown', handleClickOutside, false);
35
+ document.addEventListener('keyup', handleEscapeKey, false);
22
36
  return () => {
23
37
  document.removeEventListener('mousedown', handleClickOutside, false);
38
+ document.removeEventListener('keyup', handleEscapeKey, false);
24
39
  };
25
40
  });
26
41
 
@@ -33,9 +48,13 @@ const SidebarPopup = (props) => {
33
48
  classNames="overlay-container"
34
49
  unmountOnExit
35
50
  >
36
- <Portal node={document?.body}>
37
- <div className="overlay-container"></div>
38
- </Portal>
51
+ <>
52
+ {document?.body &&
53
+ createPortal(
54
+ <div className="overlay-container"></div>,
55
+ document?.body,
56
+ )}
57
+ </>
39
58
  </CSSTransition>
40
59
  )}
41
60
  <CSSTransition
@@ -44,24 +63,27 @@ const SidebarPopup = (props) => {
44
63
  classNames="sidebar-container"
45
64
  unmountOnExit
46
65
  >
47
- <Portal>
48
- <aside
49
- id="test"
50
- role="presentation"
51
- onClick={(e) => {
52
- e.stopPropagation();
53
- }}
54
- onKeyDown={(e) => {
55
- e.stopPropagation();
56
- }}
57
- ref={asideElement}
58
- key="sidebarpopup"
59
- className="sidebar-container"
60
- style={{ overflowY: 'auto' }}
61
- >
62
- {children}
63
- </aside>
64
- </Portal>
66
+ <>
67
+ {isClient &&
68
+ createPortal(
69
+ <aside
70
+ role="presentation"
71
+ onClick={(e) => {
72
+ e.stopPropagation();
73
+ }}
74
+ onKeyDown={(e) => {
75
+ e.stopPropagation();
76
+ }}
77
+ ref={asideElement}
78
+ key="sidebarpopup"
79
+ className="sidebar-container"
80
+ style={{ overflowY: 'auto' }}
81
+ >
82
+ {children}
83
+ </aside>,
84
+ document.body,
85
+ )}
86
+ </>
65
87
  </CSSTransition>
66
88
  </>
67
89
  );
@@ -0,0 +1,58 @@
1
+ export const isItemAlreadySelected = ({
2
+ data,
3
+ item,
4
+ normalize = (value) => value,
5
+ }) => {
6
+ const selectedItems = Array.isArray(data) ? data : [];
7
+
8
+ return selectedItems.some(
9
+ (selectedItem) =>
10
+ normalize(selectedItem?.['@id']) === normalize(item?.['@id']),
11
+ );
12
+ };
13
+
14
+ export const isSelectableObjectBrowserItem = ({
15
+ item,
16
+ selectableTypes = [],
17
+ onlyFolderishSelectable = false,
18
+ maximumSelectionSize,
19
+ data,
20
+ mode,
21
+ normalize = (value) => value,
22
+ }) => {
23
+ if (onlyFolderishSelectable && !item?.is_folderish) {
24
+ return false;
25
+ }
26
+
27
+ if (
28
+ maximumSelectionSize &&
29
+ Array.isArray(data) &&
30
+ mode === 'multiple' &&
31
+ maximumSelectionSize <= data.length
32
+ ) {
33
+ return isItemAlreadySelected({ data, item, normalize });
34
+ }
35
+
36
+ return selectableTypes.length > 0
37
+ ? selectableTypes.indexOf(item?.['@type']) >= 0
38
+ : true;
39
+ };
40
+
41
+ export const shouldCloseAfterObjectBrowserSelection = ({
42
+ mode,
43
+ maximumSelectionSize,
44
+ currentLength = 0,
45
+ isDeselecting = false,
46
+ }) => {
47
+ let stopSelecting = mode !== 'multiple';
48
+
49
+ if (isDeselecting && !stopSelecting) {
50
+ stopSelecting =
51
+ maximumSelectionSize > 0 && currentLength - 1 >= maximumSelectionSize;
52
+ } else {
53
+ stopSelecting =
54
+ maximumSelectionSize > 0 && currentLength + 1 >= maximumSelectionSize;
55
+ }
56
+
57
+ return stopSelecting;
58
+ };
@@ -9,22 +9,20 @@ import PropTypes from 'prop-types';
9
9
  import { connect } from 'react-redux';
10
10
  import { compose } from 'redux';
11
11
  import { Link, withRouter } from 'react-router-dom';
12
- import { find } from 'lodash';
12
+ import find from 'lodash/find';
13
13
  import { toast } from 'react-toastify';
14
- import { Toast } from '@plone/volto/components';
14
+ import Toast from '@plone/volto/components/manage/Toast/Toast';
15
15
  import { Pluggable, Plug } from '@plone/volto/components/manage/Pluggable';
16
- import {
17
- FormattedDate,
18
- Icon,
19
- Display,
20
- Workflow,
21
- } from '@plone/volto/components';
16
+ import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
17
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
18
+ import Display from '@plone/volto/components/manage/Display/Display';
19
+ import Workflow from '@plone/volto/components/manage/Workflow/Workflow';
22
20
  import {
23
21
  applyWorkingCopy,
24
22
  createWorkingCopy,
25
23
  removeWorkingCopy,
26
- } from '@plone/volto/actions';
27
- import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers';
24
+ } from '@plone/volto/actions//workingcopy/workingcopy';
25
+ import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers/Url/Url';
28
26
  import config from '@plone/volto/registry';
29
27
 
30
28
  import rightArrowSVG from '@plone/volto/icons/right-key.svg';
@@ -6,7 +6,7 @@
6
6
  import React from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { Input } from 'semantic-ui-react';
9
- import { FormFieldWrapper } from '@plone/volto/components';
9
+ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
10
10
  import { injectIntl } from 'react-intl';
11
11
 
12
12
  /**
@@ -2,10 +2,15 @@ import React from 'react';
2
2
  import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import { Provider } from 'react-intl-redux';
4
4
  import configureStore from 'redux-mock-store';
5
- import '@testing-library/jest-dom/extend-expect';
5
+ import '@testing-library/jest-dom';
6
6
 
7
7
  import NumberWidget from './NumberWidget';
8
8
 
9
+ jest.mock('@plone/volto/components/manage/Widgets/FormFieldWrapper', () => ({
10
+ __esModule: true,
11
+ default: ({ children }) => <>{children}</>,
12
+ }));
13
+
9
14
  const mockStore = configureStore();
10
15
 
11
16
  const store = mockStore({