@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
@@ -1,22 +1,104 @@
1
+ /**
2
+ * Image block Edit — unit tests
3
+ *
4
+ * Volto 17 / 18 dual-support notes
5
+ * ---------------------------------
6
+ * Importing `@plone/volto/components` (the full barrel) triggers the chain:
7
+ * TranslationObject.jsx → store.js → `@root/reducers`
8
+ * which Jest cannot resolve in this project's test environment.
9
+ * We therefore stub the entire barrel and provide minimal fakes for the
10
+ * components that Edit.jsx actually uses. `@eeacms/volto-eea-design-system/ui`
11
+ * is mocked for the same reason (external design-system package not present in
12
+ * the test runner).
13
+ *
14
+ * Both mocks are version-agnostic — they expose the same API on V17 and V18.
15
+ *
16
+ * IMPORTANT: jest.mock() factories cannot reference out-of-scope variables
17
+ * (including React imported via ESM). Use require() inside the factory instead.
18
+ */
19
+
20
+ // jest.mock() calls are hoisted above all imports by babel-jest.
21
+ /* eslint-disable no-undef */
22
+
23
+ /* eslint-enable no-undef */
24
+
1
25
  import config from '@plone/volto/registry';
2
- import '@testing-library/jest-dom/extend-expect';
26
+ import '@testing-library/jest-dom';
3
27
  import { render } from '@testing-library/react';
4
28
  import React from 'react';
5
29
  import { Provider } from 'react-intl-redux';
6
30
  import configureMockStore from 'redux-mock-store';
7
- import Edit from './Edit';
8
- import { Image } from '@plone/volto/components';
9
- import { getImageBlockSizes } from './Edit';
31
+ import Edit, { getImageBlockSizes } from './Edit';
32
+
33
+ jest.mock('@plone/volto/components', () => {
34
+ const React = require('react');
35
+
36
+ const MockImage = (props) =>
37
+ React.createElement('img', {
38
+ src: props.src,
39
+ alt: props.alt,
40
+ className: props.className,
41
+ loading: props.loading,
42
+ 'data-testid': 'volto-image',
43
+ });
44
+
45
+ return {
46
+ // Volto SVG icon wrapper — renders nothing in tests
47
+ Icon: () => null,
48
+ // Image sidebar form rendered inside SidebarPortal — not under test here
49
+ ImageSidebar: () => null,
50
+ // Portal that makes sidebar visible only when `selected` is true
51
+ SidebarPortal: (props) =>
52
+ props.selected
53
+ ? React.createElement(
54
+ 'div',
55
+ { 'data-testid': 'sidebar-portal' },
56
+ props.children,
57
+ )
58
+ : null,
59
+ // Volto responsive image component
60
+ Image: MockImage,
61
+ };
62
+ });
10
63
 
64
+ jest.mock('@eeacms/volto-eea-design-system/ui/Copyright/Copyright', () => {
65
+ const React = require('react');
66
+
67
+ const Copyright = (props) =>
68
+ React.createElement(
69
+ 'div',
70
+ { 'data-testid': 'eea-copyright' },
71
+ props.children,
72
+ );
73
+ Copyright.Icon = (props) =>
74
+ React.createElement(
75
+ 'span',
76
+ { 'data-testid': 'eea-copyright-icon' },
77
+ props.children,
78
+ );
79
+ Copyright.Text = (props) =>
80
+ React.createElement(
81
+ 'span',
82
+ { 'data-testid': 'eea-copyright-text' },
83
+ props.children,
84
+ );
85
+
86
+ return {
87
+ __esModule: true,
88
+ default: Copyright,
89
+ };
90
+ });
91
+
92
+ // Register our mock Image component so config.getComponent({ name: 'Image' })
93
+ // returns something renderable during tests.
94
+ const MockImage = jest.requireMock('@plone/volto/components').Image;
11
95
  config.set('components', {
12
- Image: { component: Image },
96
+ Image: { component: MockImage },
13
97
  });
14
98
 
15
99
  const mockStore = configureMockStore();
16
100
  const { settings } = config;
17
101
 
18
- let store;
19
-
20
102
  config.blocks.blocksConfig = {
21
103
  image: {
22
104
  id: 'image',
@@ -34,286 +116,202 @@ config.blocks.blocksConfig = {
34
116
  getSizes: getImageBlockSizes,
35
117
  },
36
118
  };
119
+
37
120
  const blockId = '1234';
38
121
 
39
- describe('Edit', () => {
40
- beforeEach(() => {
41
- store = mockStore({
42
- content: {
43
- create: {},
44
- data: {},
45
- subrequests: {
46
- [blockId]: {},
47
- },
48
- },
49
- intl: {
50
- locale: 'en',
51
- messages: {},
52
- },
53
- });
122
+ /**
123
+ * Helper: render Edit wrapped in Redux + intl Provider.
124
+ */
125
+ const renderEdit = (data, extraProps = {}) => {
126
+ settings.internalApiPath = 'http://localhost:8080/Plone';
127
+ const store = mockStore({
128
+ content: {
129
+ create: {},
130
+ data: {},
131
+ subrequests: { [blockId]: {} },
132
+ },
133
+ intl: { locale: 'en', messages: {} },
54
134
  });
55
135
 
56
- it('should render without errors', () => {
57
- const { container } = render(
58
- <Provider store={store}>
59
- <Edit
60
- data={{
61
- url: 'http://localhost:8080/Plone/image',
62
- '@type': 'image',
63
- align: 'full',
64
- }}
65
- selected={false}
66
- block={blockId}
67
- content={{}}
68
- request={{
69
- loading: false,
70
- loaded: false,
71
- }}
72
- pathname="/news"
73
- onChangeBlock={() => {}}
74
- onSelectBlock={() => {}}
75
- onDeleteBlock={() => {}}
76
- createContent={() => {}}
77
- onFocusPreviousBlock={() => {}}
78
- onFocusNextBlock={() => {}}
79
- handleKeyDown={() => {}}
80
- index={1}
81
- openObjectBrowser={() => {}}
82
- />
83
- </Provider>,
84
- );
136
+ return render(
137
+ <Provider store={store}>
138
+ <Edit
139
+ data={{ '@type': 'image', ...data }}
140
+ selected={false}
141
+ block={blockId}
142
+ content={{}}
143
+ request={{ loading: false, loaded: false }}
144
+ pathname="/news"
145
+ onChangeBlock={() => {}}
146
+ onSelectBlock={() => {}}
147
+ onDeleteBlock={() => {}}
148
+ createContent={() => {}}
149
+ onFocusPreviousBlock={() => {}}
150
+ onFocusNextBlock={() => {}}
151
+ handleKeyDown={() => {}}
152
+ index={1}
153
+ openObjectBrowser={() => {}}
154
+ {...extraProps}
155
+ />
156
+ </Provider>,
157
+ );
158
+ };
85
159
 
86
- settings.internalApiPath = 'http://localhost:8080/Plone';
160
+ // ---------------------------------------------------------------------------
161
+ // getImageBlockSizes — pure-function export used by blocksConfig.getSizes
162
+ // ---------------------------------------------------------------------------
87
163
 
88
- expect(container.querySelector('.block.image.align')).toBeInTheDocument();
89
- expect(
90
- container.querySelector('.image-block-container'),
91
- ).toBeInTheDocument();
92
- expect(container.querySelector('.copyright-wrapper')).toBeInTheDocument();
164
+ describe('getImageBlockSizes', () => {
165
+ it('returns 100vw for full-width alignment', () => {
166
+ expect(getImageBlockSizes({ align: 'full' })).toBe('100vw');
93
167
  });
94
168
 
95
- it('should render without errors', () => {
96
- const { container } = render(
97
- <Provider store={store}>
98
- <Edit
99
- data={{
100
- url: 'http://localhost:8080/Plone/image',
101
- '@type': 'image',
102
- size: 'l',
103
- copyright: 'Copyright',
104
- }}
105
- selected={false}
106
- block={blockId}
107
- content={{}}
108
- request={{
109
- loading: false,
110
- loaded: false,
111
- }}
112
- pathname="/news"
113
- onChangeBlock={() => {}}
114
- onSelectBlock={() => {}}
115
- onDeleteBlock={() => {}}
116
- createContent={() => {}}
117
- onFocusPreviousBlock={() => {}}
118
- onFocusNextBlock={() => {}}
119
- handleKeyDown={() => {}}
120
- index={1}
121
- openObjectBrowser={() => {}}
122
- />
123
- </Provider>,
124
- );
169
+ it('returns correct sizes for centered alignment', () => {
170
+ expect(getImageBlockSizes({ align: 'center', size: 'l' })).toBe('100vw');
171
+ expect(getImageBlockSizes({ align: 'center', size: 'm' })).toBe('50vw');
172
+ expect(getImageBlockSizes({ align: 'center', size: 's' })).toBe('25vw');
173
+ });
125
174
 
126
- settings.internalApiPath = 'http://localhost:8080/Plone';
175
+ it('returns correct sizes for left / right alignment', () => {
176
+ expect(getImageBlockSizes({ align: 'left', size: 'l' })).toBe('50vw');
177
+ expect(getImageBlockSizes({ align: 'left', size: 'm' })).toBe('25vw');
178
+ expect(getImageBlockSizes({ align: 'right', size: 's' })).toBe('15vw');
179
+ });
127
180
 
128
- expect(container.querySelector('.block.image.align')).toBeInTheDocument();
129
- expect(
130
- container.querySelector('.image-block-container'),
131
- ).toBeInTheDocument();
132
- expect(container.querySelector('.copyright-wrapper')).toBeInTheDocument();
181
+ it('returns undefined for unknown alignment', () => {
182
+ expect(getImageBlockSizes({})).toBeUndefined();
133
183
  });
184
+ });
134
185
 
135
- it('should render without errors', () => {
136
- const { container } = render(
137
- <Provider store={store}>
138
- <Edit
139
- data={{
140
- url: 'http://localhost:8080/Plone/image',
141
- '@type': 'image',
142
- size: 'm',
143
- }}
144
- selected={false}
145
- block={blockId}
146
- content={{}}
147
- request={{
148
- loading: false,
149
- loaded: false,
150
- }}
151
- pathname="/news"
152
- onChangeBlock={() => {}}
153
- onSelectBlock={() => {}}
154
- onDeleteBlock={() => {}}
155
- createContent={() => {}}
156
- onFocusPreviousBlock={() => {}}
157
- onFocusNextBlock={() => {}}
158
- handleKeyDown={() => {}}
159
- index={1}
160
- openObjectBrowser={() => {}}
161
- />
162
- </Provider>,
163
- );
186
+ // ---------------------------------------------------------------------------
187
+ // Edit rendering EEA structural requirements (image-block-container, etc.)
188
+ // ---------------------------------------------------------------------------
164
189
 
165
- settings.internalApiPath = 'http://localhost:8080/Plone';
190
+ describe('Edit', () => {
191
+ it('renders the EEA wrapper structure when a URL is provided (full align)', () => {
192
+ const { container } = renderEdit({
193
+ url: 'http://localhost:8080/Plone/image',
194
+ align: 'full',
195
+ });
166
196
 
167
197
  expect(container.querySelector('.block.image.align')).toBeInTheDocument();
168
198
  expect(
169
199
  container.querySelector('.image-block-container'),
170
200
  ).toBeInTheDocument();
201
+ // copyright-wrapper is always rendered when there is a URL
171
202
  expect(container.querySelector('.copyright-wrapper')).toBeInTheDocument();
172
203
  });
173
204
 
174
- it('should render without errors', () => {
175
- const { container } = render(
176
- <Provider store={store}>
177
- <Edit
178
- data={{
179
- url: 'http://localhost:8080/Plone/image',
180
- '@type': 'image',
181
- size: 's',
182
- }}
183
- selected={false}
184
- block={blockId}
185
- content={{}}
186
- request={{
187
- loading: false,
188
- loaded: false,
189
- }}
190
- pathname="/news"
191
- onChangeBlock={() => {}}
192
- onSelectBlock={() => {}}
193
- onDeleteBlock={() => {}}
194
- createContent={() => {}}
195
- onFocusPreviousBlock={() => {}}
196
- onFocusNextBlock={() => {}}
197
- handleKeyDown={() => {}}
198
- index={1}
199
- openObjectBrowser={() => {}}
200
- />
201
- </Provider>,
205
+ it('shows the copyright overlay when copyright text is set and size is l', () => {
206
+ const { getByTestId } = renderEdit({
207
+ url: 'http://localhost:8080/Plone/image',
208
+ size: 'l',
209
+ copyright: 'EEA Copyright',
210
+ copyrightIcon: 'ri-copyright-line',
211
+ copyrightPosition: 'left',
212
+ });
213
+
214
+ expect(getByTestId('eea-copyright')).toBeInTheDocument();
215
+ expect(getByTestId('eea-copyright-text')).toHaveTextContent(
216
+ 'EEA Copyright',
202
217
  );
218
+ });
219
+
220
+ it('shows the copyright overlay when no size is specified (defaults to showing)', () => {
221
+ const { getByTestId } = renderEdit({
222
+ url: 'http://localhost:8080/Plone/image',
223
+ copyright: 'EEA Copyright',
224
+ });
225
+ // showCopyright is true when size is undefined
226
+ expect(getByTestId('eea-copyright')).toBeInTheDocument();
227
+ });
203
228
 
204
- settings.internalApiPath = 'http://localhost:8080/Plone';
229
+ it('hides the copyright overlay when size is m', () => {
230
+ const { queryByTestId } = renderEdit({
231
+ url: 'http://localhost:8080/Plone/image',
232
+ size: 'm',
233
+ copyright: 'EEA Copyright',
234
+ });
235
+
236
+ // showCopyright is false for size !== 'l' and size !== undefined
237
+ expect(queryByTestId('eea-copyright')).not.toBeInTheDocument();
238
+ });
239
+
240
+ it('hides the copyright overlay when size is s', () => {
241
+ const { queryByTestId } = renderEdit({
242
+ url: 'http://localhost:8080/Plone/image',
243
+ size: 's',
244
+ copyright: 'EEA Copyright',
245
+ });
246
+ expect(queryByTestId('eea-copyright')).not.toBeInTheDocument();
247
+ });
248
+
249
+ it('renders without copyright when no copyright text is provided', () => {
250
+ const { container, queryByTestId } = renderEdit({
251
+ url: 'http://localhost:8080/Plone/image',
252
+ size: 'l',
253
+ });
205
254
 
206
- expect(container.querySelector('.block.image.align')).toBeInTheDocument();
207
255
  expect(
208
256
  container.querySelector('.image-block-container'),
209
257
  ).toBeInTheDocument();
210
- expect(container.querySelector('.copyright-wrapper')).toBeInTheDocument();
258
+ expect(queryByTestId('eea-copyright')).not.toBeInTheDocument();
211
259
  });
212
260
 
213
- it('should render without errors', () => {
214
- const { container } = render(
215
- <Provider store={store}>
216
- <Edit
217
- data={{
218
- url: 'http://localhost:8080/Plone/image',
219
- '@type': 'image',
220
- }}
221
- selected={false}
222
- block={blockId}
223
- content={{}}
224
- request={{
225
- loading: false,
226
- loaded: false,
227
- }}
228
- pathname="/news"
229
- onChangeBlock={() => {}}
230
- onSelectBlock={() => {}}
231
- onDeleteBlock={() => {}}
232
- createContent={() => {}}
233
- onFocusPreviousBlock={() => {}}
234
- onFocusNextBlock={() => {}}
235
- handleKeyDown={() => {}}
236
- index={1}
237
- openObjectBrowser={() => {}}
238
- />
239
- </Provider>,
240
- );
241
-
242
- settings.internalApiPath = 'http://localhost:8080/Plone';
261
+ it('renders without a URL — shows the dropzone upload UI', () => {
262
+ const { container } = renderEdit({ url: undefined }, { editable: true });
243
263
 
244
264
  expect(container.querySelector('.block.image.align')).toBeInTheDocument();
245
265
  expect(
246
266
  container.querySelector('.image-block-container'),
247
267
  ).toBeInTheDocument();
248
- expect(container.querySelector('.copyright-wrapper')).toBeInTheDocument();
268
+ // No copyright-wrapper when there is no image URL
269
+ expect(
270
+ container.querySelector('.copyright-wrapper'),
271
+ ).not.toBeInTheDocument();
249
272
  });
250
273
 
251
- it('should render without errors', () => {
252
- const { container } = render(
253
- <Provider store={store}>
254
- <Edit
255
- data={{
256
- url: 'http://localhost:80801/Plone/image',
257
- '@type': 'image',
258
- }}
259
- selected={false}
260
- block={blockId}
261
- content={{}}
262
- request={{
263
- loading: false,
264
- loaded: false,
265
- }}
266
- pathname="/news"
267
- onChangeBlock={() => {}}
268
- onSelectBlock={() => {}}
269
- onDeleteBlock={() => {}}
270
- createContent={() => {}}
271
- onFocusPreviousBlock={() => {}}
272
- onFocusNextBlock={() => {}}
273
- handleKeyDown={() => {}}
274
- index={1}
275
- openObjectBrowser={() => {}}
276
- />
277
- </Provider>,
278
- );
274
+ it('applies align class to both the outer block and the EEA container', () => {
275
+ const { container } = renderEdit({
276
+ url: 'http://localhost:8080/Plone/image',
277
+ align: 'left',
278
+ });
279
+
280
+ const outer = container.querySelector('.block.image.align');
281
+ expect(outer).toHaveClass('left');
282
+ const inner = container.querySelector('.image-block-container');
283
+ expect(inner).toHaveClass('left');
284
+ });
279
285
 
280
- settings.internalApiPath = 'http://localhost:8080/Plone';
286
+ it('applies center class when no align is provided', () => {
287
+ const { container } = renderEdit({
288
+ url: 'http://localhost:8080/Plone/image',
289
+ });
290
+
291
+ const outer = container.querySelector('.block.image.align');
292
+ expect(outer).toHaveClass('center');
293
+ });
294
+
295
+ it('applies size class to the EEA container and the image', () => {
296
+ const { container } = renderEdit({
297
+ url: 'http://localhost:8080/Plone/image',
298
+ size: 'l',
299
+ });
300
+
301
+ const inner = container.querySelector('.image-block-container');
302
+ expect(inner).toHaveClass('large');
303
+ const img = container.querySelector('[data-testid="volto-image"]');
304
+ expect(img).toHaveClass('large');
305
+ });
306
+
307
+ it('renders with an external URL', () => {
308
+ const { container } = renderEdit({
309
+ url: 'https://example.com/image.png',
310
+ });
281
311
 
282
312
  expect(container.querySelector('.block.image.align')).toBeInTheDocument();
283
313
  expect(
284
314
  container.querySelector('.image-block-container'),
285
315
  ).toBeInTheDocument();
286
- expect(container.querySelector('.copyright-wrapper')).toBeInTheDocument();
287
- });
288
-
289
- it('should render without errors', () => {
290
- render(
291
- <Provider store={store}>
292
- <Edit
293
- data={{
294
- url: undefined,
295
- '@type': 'image',
296
- }}
297
- selected={false}
298
- block={blockId}
299
- content={{}}
300
- request={{
301
- loading: false,
302
- loaded: false,
303
- }}
304
- editable={true}
305
- pathname="/news"
306
- onChangeBlock={() => {}}
307
- onSelectBlock={() => {}}
308
- onDeleteBlock={() => {}}
309
- createContent={() => {}}
310
- onFocusPreviousBlock={() => {}}
311
- onFocusNextBlock={() => {}}
312
- handleKeyDown={() => {}}
313
- index={1}
314
- openObjectBrowser={() => {}}
315
- />
316
- </Provider>,
317
- );
318
316
  });
319
317
  });
@@ -6,16 +6,13 @@
6
6
 
7
7
  import React from 'react';
8
8
  import PropTypes from 'prop-types';
9
- import { UniversalLink } from '@plone/volto/components';
9
+ import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
10
10
  import { Icon } from 'semantic-ui-react';
11
11
  import cx from 'classnames';
12
- import {
13
- flattenToAppURL,
14
- isInternalURL,
15
- withBlockExtensions,
16
- } from '@plone/volto/helpers';
12
+ import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url';
13
+ import { withBlockExtensions } from '@plone/volto/helpers//Extensions';
17
14
  import config from '@plone/volto/registry';
18
- import { Copyright } from '@eeacms/volto-eea-design-system/ui';
15
+ import Copyright from '@eeacms/volto-eea-design-system/ui/Copyright/Copyright';
19
16
 
20
17
  /**
21
18
  * View image block class.
@@ -25,7 +22,8 @@ import { Copyright } from '@eeacms/volto-eea-design-system/ui';
25
22
  export const View = (props) => {
26
23
  const { className, data, detached, style } = props;
27
24
  const { copyright, copyrightIcon, copyrightPosition } = data;
28
- const href = data?.href?.[0]?.['@id'] || '';
25
+ const hrefItem = data?.href?.[0];
26
+ const href = hrefItem?.['@id'] || '';
29
27
  const showCopyright = data?.size === 'l' || !data.size;
30
28
 
31
29
  const Image = config.getComponent({ name: 'Image' }).component;
@@ -84,25 +82,25 @@ export const View = (props) => {
84
82
  data.image_scales
85
83
  ? undefined
86
84
  : isInternalURL(data.url)
87
- ? // Backwards compat in the case that the block is storing the full server URL
88
- (() => {
89
- if (data.size === 'l')
85
+ ? // Backwards compat in the case that the block is storing the full server URL
86
+ (() => {
87
+ if (data.size === 'l')
88
+ return `${flattenToAppURL(
89
+ data.url,
90
+ )}/@@images/image/large`;
91
+ if (data.size === 'm')
92
+ return `${flattenToAppURL(
93
+ data.url,
94
+ )}/@@images/image/preview`;
95
+ if (data.size === 's')
96
+ return `${flattenToAppURL(
97
+ data.url,
98
+ )}/@@images/image/mini`;
90
99
  return `${flattenToAppURL(
91
100
  data.url,
92
101
  )}/@@images/image/large`;
93
- if (data.size === 'm')
94
- return `${flattenToAppURL(
95
- data.url,
96
- )}/@@images/image/preview`;
97
- if (data.size === 's')
98
- return `${flattenToAppURL(
99
- data.url,
100
- )}/@@images/image/mini`;
101
- return `${flattenToAppURL(
102
- data.url,
103
- )}/@@images/image/large`;
104
- })()
105
- : data.url
102
+ })()
103
+ : data.url
106
104
  }
107
105
  sizes={config.blocks.blocksConfig.image.getSizes(data)}
108
106
  alt={data.alt || ''}
@@ -130,7 +128,7 @@ export const View = (props) => {
130
128
  if (href) {
131
129
  return (
132
130
  <UniversalLink
133
- href={href}
131
+ item={hrefItem}
134
132
  openLinkInNewTab={data.openLinkInNewTab}
135
133
  >
136
134
  {image}
@@ -2,18 +2,18 @@
2
2
  * Edit image block.
3
3
  * @module components/manage/Blocks/Image/Edit
4
4
  */
5
-
6
5
  import React, { Component } from 'react';
7
6
  import PropTypes from 'prop-types';
8
7
  import { compose } from 'redux';
9
8
  import { defineMessages, injectIntl } from 'react-intl';
10
9
  import cx from 'classnames';
11
10
  import { Message } from 'semantic-ui-react';
12
- import { isEqual } from 'lodash';
11
+ import isEqual from 'lodash/isEqual';
13
12
 
14
- import { Copyright } from '@eeacms/volto-eea-design-system/ui';
13
+ import Copyright from '@eeacms/volto-eea-design-system/ui/Copyright/Copyright';
15
14
  import { Icon } from 'semantic-ui-react';
16
- import { LeadImageSidebar, SidebarPortal } from '@plone/volto/components';
15
+ import LeadImageSidebar from '@plone/volto/components/manage/Blocks/LeadImage/LeadImageSidebar';
16
+ import SidebarPortal from '@plone/volto/components/manage/Sidebar/SidebarPortal';
17
17
  import config from '@plone/volto/registry';
18
18
 
19
19
  import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
@@ -111,6 +111,7 @@ class Edit extends Component {
111
111
  {!hasImage && (
112
112
  <Message>
113
113
  <center>
114
+ {/* eslint-disable-next-line no-restricted-syntax */}
114
115
  <img src={imageBlockSVG} alt="" />
115
116
  <div className="message-text">{placeholder}</div>
116
117
  </center>
@@ -118,6 +119,7 @@ class Edit extends Component {
118
119
  )}
119
120
  {hasImage && hasImageData && (
120
121
  <div className="image-block">
122
+ {/* eslint-disable-next-line no-restricted-syntax */}
121
123
  <img
122
124
  className={(className, data?.styles?.objectPosition)}
123
125
  src={`data:${properties.image['content-type']};base64,${properties.image.data}`}