@eeacms/volto-embed 10.0.2 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,12 +4,39 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [10.1.0](https://github.com/eea/volto-embed/compare/10.0.3...10.1.0) - 11 September 2024
8
+
9
+ #### :hammer_and_wrench: Others
10
+
11
+ - Update version [dobri1408 - [`21e7852`](https://github.com/eea/volto-embed/commit/21e7852cfa27bedd7e3670402bf89d7f3b6fdc06)]
12
+ - cleanup preview image loaded [dobri1408 - [`b3e60ff`](https://github.com/eea/volto-embed/commit/b3e60ffedd7e7f7790afbc76e99df4e001e57550)]
13
+ - double check preview [dobri1408 - [`35c6210`](https://github.com/eea/volto-embed/commit/35c621031b4cd25777ac19a2789eac08ffd48176)]
14
+ - tests [dobri1408 - [`70c3620`](https://github.com/eea/volto-embed/commit/70c3620057ffd203105b80ff1c9121a39947c212)]
15
+ - tests [dobri1408 - [`8aa1bfe`](https://github.com/eea/volto-embed/commit/8aa1bfe536968b42cd76abc91b685b28ce485e28)]
16
+ - tests [dobri1408 - [`879f6a7`](https://github.com/eea/volto-embed/commit/879f6a70d329228d974f347cea983621194433a3)]
17
+ - Create preview_image.test.js [dobri1408 - [`fbb7cdb`](https://github.com/eea/volto-embed/commit/fbb7cdb69c1d43a928024d2c83a0dee87faae7af)]
18
+ - Update MapsWidget.jsx [dobri1408 - [`cd46123`](https://github.com/eea/volto-embed/commit/cd46123d39c71d034a2aa9919ad04e7364a08e8d)]
19
+ - Update MapsWidget.jsx [dobri1408 - [`e36c6a9`](https://github.com/eea/volto-embed/commit/e36c6a9c0269622cabf2069d6b8cd082cfbe879b)]
20
+ - preview image middlware [dobri1408 - [`5a0f8e9`](https://github.com/eea/volto-embed/commit/5a0f8e9231d8f5f760dcbd82bbab4a84778d1202)]
21
+ ### [10.0.3](https://github.com/eea/volto-embed/compare/10.0.2...10.0.3) - 24 July 2024
22
+
23
+ #### :bug: Bug Fixes
24
+
25
+ - fix: package.json [alin - [`6c29d0d`](https://github.com/eea/volto-embed/commit/6c29d0d3875a95d24b9197db05c187f7c37083a0)]
26
+
27
+ #### :hammer_and_wrench: Others
28
+
29
+ - update [Miu Razvan - [`371e6fc`](https://github.com/eea/volto-embed/commit/371e6fc4a344a995f8be1e2b8fd80325777515c5)]
30
+ - update [Miu Razvan - [`c0536f0`](https://github.com/eea/volto-embed/commit/c0536f00732e3761a71bf5103ee064b5b3c1cc22)]
31
+ - update [Miu Razvan - [`e6c8526`](https://github.com/eea/volto-embed/commit/e6c852685654986be6c8695b4f8888e78adcc0a9)]
32
+ - add useScreenHeight prop [Miu Razvan - [`a261a59`](https://github.com/eea/volto-embed/commit/a261a593892f44845579b2c0aa764e22bbf48e73)]
33
+ - fix tests [Miu Razvan - [`0798660`](https://github.com/eea/volto-embed/commit/0798660ce054a7a60e4c3888cb63ba9c131a42f8)]
34
+ - Set interactiv map height to window innerheight, ref #272831 [Miu Razvan - [`5ac9eb5`](https://github.com/eea/volto-embed/commit/5ac9eb5e66cdc626b33d0bdad5b80994af2d7d79)]
7
35
  ### [10.0.2](https://github.com/eea/volto-embed/compare/10.0.1...10.0.2) - 7 June 2024
8
36
 
9
37
  #### :bug: Bug Fixes
10
38
 
11
39
  - fix: re-add parseInt with a check for NaN [nileshgulia1 - [`f7dd30c`](https://github.com/eea/volto-embed/commit/f7dd30c47aa2cc31726e49b8eb4cb40aae624941)]
12
- - fix: remove parseInt [nileshgulia1 - [`0b981b4`](https://github.com/eea/volto-embed/commit/0b981b4035b247ce8bd086ab90fc5dd4a23865c8)]
13
40
 
14
41
  ### [10.0.1](https://github.com/eea/volto-embed/compare/10.0.0...10.0.1) - 4 June 2024
15
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-embed",
3
- "version": "10.0.2",
3
+ "version": "10.1.0",
4
4
  "description": "Embed external content",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -23,10 +23,10 @@
23
23
  "@eeacms/volto-datablocks": "*"
24
24
  },
25
25
  "devDependencies": {
26
- "cypress": "13.1.0",
27
26
  "@cypress/code-coverage": "^3.10.0",
28
27
  "@plone/scripts": "*",
29
28
  "babel-plugin-transform-class-properties": "^6.24.1",
29
+ "cypress": "13.1.0",
30
30
  "cypress-fail-fast": "^5.0.1",
31
31
  "dotenv": "^16.3.2",
32
32
  "husky": "^8.0.3",
@@ -35,7 +35,7 @@ export const MapsSchema = (props) => {
35
35
  {
36
36
  id: 'default',
37
37
  title: 'Default',
38
- fields: ['url', 'title', 'align', 'height'],
38
+ fields: ['url', 'title', 'align', 'height', 'useScreenHeight'],
39
39
  },
40
40
  ],
41
41
 
@@ -63,6 +63,11 @@ export const MapsSchema = (props) => {
63
63
  ),
64
64
  description: props.intl.formatMessage(messages.CSSMapHeightDescription),
65
65
  },
66
+ useScreenHeight: {
67
+ title: 'Use screen height',
68
+ type: 'boolean',
69
+ default: false,
70
+ },
66
71
  },
67
72
  required: [],
68
73
  };
@@ -22,12 +22,16 @@ const messages = defineMessages({
22
22
  },
23
23
  });
24
24
 
25
- function getHeight(height) {
25
+ function getHeight(data, screen) {
26
+ const { height, useScreenHeight } = data;
26
27
  const asNumber = isNumber(Number(height)) && !isNaN(Number(height));
27
28
  if (asNumber) {
28
29
  return `${height}px`;
29
30
  }
30
- return height;
31
+ return (
32
+ height ||
33
+ (useScreenHeight && screen?.page?.height ? screen.page.height - 50 : 400)
34
+ );
31
35
  }
32
36
 
33
37
  function EmbedMap({ data, intl, id, screen }) {
@@ -78,7 +82,7 @@ function EmbedMap({ data, intl, id, screen }) {
78
82
  <PrivacyProtection
79
83
  data={data}
80
84
  id={id}
81
- height={getHeight(data.height)}
85
+ height={getHeight(data, screen)}
82
86
  useVisibilitySensor={data.useVisibilitySensor ?? true}
83
87
  >
84
88
  <iframe
@@ -87,7 +91,7 @@ function EmbedMap({ data, intl, id, screen }) {
87
91
  className="google-map"
88
92
  frameBorder="0"
89
93
  allowFullScreen
90
- style={data.height ? { height: getHeight(data.height) } : {}}
94
+ style={{ height: getHeight(data, screen) }}
91
95
  />
92
96
  </PrivacyProtection>
93
97
  </div>
@@ -0,0 +1,34 @@
1
+ import { createImageUrl } from './helpers';
2
+
3
+ describe('createImageUrl', () => {
4
+ it('should create a valid image URL from the base64 encoded data', () => {
5
+ // Mock data for the test
6
+ const mockResult = {
7
+ data: 'aGVsbG8gd29ybGQ=', // "hello world" in base64
8
+ 'content-type': 'image/png',
9
+ };
10
+
11
+ // Mock the atob function
12
+ jest.spyOn(window, 'atob').mockImplementation(() => 'hello world');
13
+
14
+ // Mock the URL.createObjectURL function
15
+ const mockUrl = 'blob:http://localhost:3000/some-url';
16
+ global.URL.createObjectURL = jest.fn().mockReturnValue(mockUrl);
17
+
18
+ // Call the function
19
+ const imageUrl = createImageUrl(mockResult);
20
+
21
+ // Assertions
22
+ expect(window.atob).toHaveBeenCalledWith(mockResult.data);
23
+ expect(URL.createObjectURL).toHaveBeenCalledWith(expect.any(Blob));
24
+ expect(imageUrl).toBe(mockUrl);
25
+
26
+ // Verify the Blob creation
27
+ const blobArgs = URL.createObjectURL.mock.calls[0][0];
28
+ expect(blobArgs.type).toBe(mockResult['content-type']);
29
+ expect(blobArgs.size).toBe(11); // "hello world" is 11 bytes
30
+
31
+ // Clean up mocks
32
+ jest.restoreAllMocks();
33
+ });
34
+ });
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen } from '@testing-library/react';
3
+ import Sources from './Sources';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+
6
+ const mockSources = [
7
+ {
8
+ chart_source: 'Source 1',
9
+ chart_source_link: 'http://example.com/source1',
10
+ organisation: 'Organisation 1',
11
+ title: 'Title 1',
12
+ link: 'http://example.com/link1',
13
+ },
14
+ {
15
+ chart_source: 'Source 2',
16
+ chart_source_link: 'http://example.com/source2',
17
+ organisation: 'Organisation 2',
18
+ title: 'Title 2',
19
+ link: 'http://example.com/link2',
20
+ },
21
+ ];
22
+
23
+ describe('Sources', () => {
24
+ it('renders the Sources button', () => {
25
+ render(<Sources sources={mockSources} />);
26
+ expect(screen.getByText('Sources')).toBeInTheDocument();
27
+ });
28
+
29
+ it('opens and closes the popup on click', () => {
30
+ render(<Sources sources={mockSources} />);
31
+
32
+ const triggerButton = screen.getByText('Sources');
33
+
34
+ expect(screen.queryByText('Source 1')).toBeInTheDocument();
35
+
36
+ fireEvent.click(triggerButton);
37
+ expect(screen.getByText('Source 1')).toBeInTheDocument();
38
+ expect(screen.getByText('Source 2')).toBeInTheDocument();
39
+
40
+ fireEvent.click(triggerButton);
41
+ });
42
+
43
+ it('renders sources correctly', () => {
44
+ render(<Sources sources={mockSources} />);
45
+
46
+ const triggerButton = screen.getByText('Sources');
47
+ fireEvent.click(triggerButton);
48
+
49
+ expect(screen.getByText('Source 1')).toBeInTheDocument();
50
+ expect(screen.getByText('Source 2')).toBeInTheDocument();
51
+
52
+ expect(screen.getByText('Source 1').closest('a')).toHaveAttribute(
53
+ 'href',
54
+ 'http://example.com/source1',
55
+ );
56
+ expect(screen.getByText('Source 2').closest('a')).toHaveAttribute(
57
+ 'href',
58
+ 'http://example.com/source2',
59
+ );
60
+ });
61
+
62
+ it('renders a message when there are no sources', () => {
63
+ render(<Sources sources={[]} />);
64
+
65
+ const triggerButton = screen.getByText('Sources');
66
+ fireEvent.click(triggerButton);
67
+
68
+ expect(
69
+ screen.getByText('Data provenance is not set for this visualization.'),
70
+ ).toBeInTheDocument();
71
+ });
72
+ });
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { Provider } from 'react-redux';
4
+ import configureStore from 'redux-mock-store';
5
+ import MapsViewWidget from './MapsViewWidget';
6
+ import EmbedMap from '@eeacms/volto-embed/EmbedMap/EmbedMap';
7
+ import { pickMetadata } from '@eeacms/volto-embed/helpers';
8
+ import '@testing-library/jest-dom/extend-expect'; // Importă jest-dom
9
+
10
+ jest.mock('@eeacms/volto-embed/EmbedMap/EmbedMap', () =>
11
+ jest.fn(() => <div>Mocked EmbedMap</div>),
12
+ );
13
+ jest.mock('@eeacms/volto-embed/helpers', () => ({
14
+ pickMetadata: jest.fn(),
15
+ }));
16
+
17
+ const mockStore = configureStore([]);
18
+
19
+ describe('MapsViewWidget', () => {
20
+ let store;
21
+
22
+ beforeEach(() => {
23
+ store = mockStore({
24
+ content: {
25
+ data: {
26
+ metadata: {
27
+ title: 'Test Title',
28
+ description: 'Test Description',
29
+ },
30
+ },
31
+ },
32
+ });
33
+ });
34
+
35
+ it('should render EmbedMap with the correct data props', () => {
36
+ const mockValue = {
37
+ map_url: 'http://example.com/map',
38
+ };
39
+ const mockId = 'map-widget-1';
40
+
41
+ pickMetadata.mockReturnValue({
42
+ title: 'Test Title',
43
+ description: 'Test Description',
44
+ });
45
+
46
+ const { getByText } = render(
47
+ <Provider store={store}>
48
+ <MapsViewWidget id={mockId} value={mockValue} />
49
+ </Provider>,
50
+ );
51
+
52
+ expect(EmbedMap).toHaveBeenCalledWith(
53
+ {
54
+ data: {
55
+ map_url: 'http://example.com/map',
56
+ title: 'Test Title',
57
+ description: 'Test Description',
58
+ with_share: true,
59
+ },
60
+ id: mockId,
61
+ },
62
+ {},
63
+ );
64
+
65
+ expect(getByText('Mocked EmbedMap')).toBeInTheDocument(); // Verifică dacă elementul este în document
66
+ });
67
+
68
+ it('should handle empty value prop correctly', () => {
69
+ const mockId = 'map-widget-2';
70
+
71
+ pickMetadata.mockReturnValue({
72
+ title: 'Test Title',
73
+ description: 'Test Description',
74
+ });
75
+
76
+ const { getByText } = render(
77
+ <Provider store={store}>
78
+ <MapsViewWidget id={mockId} value={null} />
79
+ </Provider>,
80
+ );
81
+
82
+ expect(EmbedMap).toHaveBeenCalledWith(
83
+ {
84
+ data: {
85
+ title: 'Test Title',
86
+ description: 'Test Description',
87
+ with_share: true,
88
+ },
89
+ id: mockId,
90
+ },
91
+ {},
92
+ );
93
+
94
+ expect(getByText('Mocked EmbedMap')).toBeInTheDocument(); // Verifică dacă elementul este în document
95
+ });
96
+ });
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useMemo, useState, useEffect } from 'react';
2
2
  import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
3
3
  import { Icon } from '@plone/volto/components';
4
4
  import { Button, Modal, Grid, Label, Input, Message } from 'semantic-ui-react';
@@ -7,6 +7,7 @@ import { FormFieldWrapper, InlineForm } from '@plone/volto/components';
7
7
  import { addPrivacyProtectionToSchema } from '@eeacms/volto-embed';
8
8
  import EmbedMap from '@eeacms/volto-embed/EmbedMap/EmbedMap';
9
9
  import { MapsSchema } from '@eeacms/volto-embed/Blocks/Maps/schema';
10
+ import { getBaseUrl } from '@plone/volto/helpers';
10
11
 
11
12
  import clearSVG from '@plone/volto/icons/clear.svg';
12
13
  import aheadSVG from '@plone/volto/icons/ahead.svg';
@@ -19,6 +20,17 @@ const messages = defineMessages({
19
20
  },
20
21
  });
21
22
 
23
+ function blobToBase64(blob) {
24
+ return new Promise((resolve, reject) => {
25
+ const reader = new FileReader();
26
+ reader.onloadend = () => {
27
+ resolve(reader.result);
28
+ };
29
+ reader.onerror = reject;
30
+ reader.readAsDataURL(blob);
31
+ });
32
+ }
33
+
22
34
  function MapEditorModal({ id, onClose, onChange, ...rest }) {
23
35
  const intl = useIntl();
24
36
  const [value, setValue] = useState(rest.value || {});
@@ -189,14 +201,37 @@ function MapEditorModal({ id, onClose, onChange, ...rest }) {
189
201
  }
190
202
 
191
203
  export default function MapsWidget(props) {
192
- const { id, title, description, error, value } = props;
204
+ const { id, title, description, error, value, onChange } = props;
193
205
  const [mapEditor, setMapEditor] = useState(false);
194
206
 
195
- function onChange(value) {
196
- props.onChange(props.id, value);
207
+ function onChangeMap(value) {
208
+ onChange(id, value);
197
209
  setMapEditor(false);
198
210
  }
199
211
 
212
+ useEffect(() => {
213
+ if (value && value.url && value.preview_url_loaded !== value.url) {
214
+ fetch(
215
+ `${getBaseUrl(
216
+ '',
217
+ )}/cors-proxy/https://screenshot.eea.europa.eu/api/v1/retrieve_image_for_url?url=${encodeURIComponent(
218
+ value.url,
219
+ )}&w=1920&h=1000&waitfor=4000`,
220
+ )
221
+ .then((e) => e.blob())
222
+ .then((myBlob) => {
223
+ blobToBase64(myBlob).then((base64String) => {
224
+ onChange(id, {
225
+ ...value,
226
+ preview: base64String,
227
+ preview_url_loaded: value.url,
228
+ });
229
+ });
230
+ })
231
+ .catch(() => {});
232
+ }
233
+ }, [value, onChange, id]);
234
+
200
235
  return (
201
236
  <FormFieldWrapper {...props} columns={1}>
202
237
  <div className="wrapper">
@@ -218,7 +253,7 @@ export default function MapsWidget(props) {
218
253
  <MapEditorModal
219
254
  id={id}
220
255
  value={value || {}}
221
- onChange={onChange}
256
+ onChange={onChangeMap}
222
257
  onClose={() => setMapEditor(false)}
223
258
  />
224
259
  )}
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ import installBlocks from './Blocks';
2
2
  import MapView from './Views/MapView';
3
3
  import MapsViewWidget from './Widgets/MapsViewWidget';
4
4
  import MapsWidget from './Widgets/MapsWidget';
5
+ import { preview_image } from './middlewares/preview_image';
5
6
  export {
6
7
  PrivacyProtection,
7
8
  addPrivacyProtectionToSchema,
@@ -16,5 +17,9 @@ export default function applyConfig(config) {
16
17
  ...config.settings.allowed_cors_destinations,
17
18
  'screenshot.eea.europa.eu',
18
19
  ];
20
+ config.settings.storeExtenders = [
21
+ ...(config.settings.storeExtenders || []),
22
+ preview_image,
23
+ ];
19
24
  return [installBlocks].reduce((acc, apply) => apply(acc), config);
20
25
  }
@@ -0,0 +1,93 @@
1
+ import {
2
+ CREATE_CONTENT,
3
+ UPDATE_CONTENT,
4
+ } from '@plone/volto/constants/ActionTypes';
5
+
6
+ const cleanAction = (action) => {
7
+ if (action?.request?.data?.maps) {
8
+ const mapVisualizationData = {
9
+ ...action.request.data.maps,
10
+ };
11
+ if (mapVisualizationData.preview && mapVisualizationData.preview_url_loaded)
12
+ delete mapVisualizationData.preview;
13
+ delete mapVisualizationData.preview_url_loaded;
14
+
15
+ return {
16
+ ...action,
17
+ request: {
18
+ ...action.request,
19
+ data: {
20
+ ...action.request.data,
21
+
22
+ maps: mapVisualizationData,
23
+ },
24
+ },
25
+ };
26
+ } else return action;
27
+ };
28
+
29
+ export const preview_image = (middlewares) => [
30
+ (store) => (next) => async (action) => {
31
+ if (![CREATE_CONTENT, UPDATE_CONTENT].includes(action.type)) {
32
+ return next(action);
33
+ }
34
+ const state = store.getState();
35
+ const contentData = state.content.data;
36
+ const lastPreviewImage = Object.keys(action?.request?.data).includes(
37
+ 'preview_image',
38
+ )
39
+ ? action?.request?.data.preview_image
40
+ : contentData?.preview_image;
41
+ const type = action?.request?.data?.['@type'] || contentData['@type'];
42
+
43
+ if (
44
+ !contentData ||
45
+ type !== 'map_interactive' ||
46
+ contentData.preview_image_saved ||
47
+ !action?.request?.data?.maps?.preview
48
+ ) {
49
+ return next(cleanAction(action));
50
+ }
51
+
52
+ if (
53
+ lastPreviewImage &&
54
+ lastPreviewImage.filename !==
55
+ 'preview_image_generated_map_interactive.png'
56
+ ) {
57
+ return next(cleanAction(action));
58
+ }
59
+
60
+ try {
61
+ const previewImage = {
62
+ preview_image: {
63
+ data: action.request.data.maps.preview.split(',')[1],
64
+ encoding: 'base64',
65
+ 'content-type': 'image/png',
66
+ filename: 'preview_image_generated_map_interactive.png',
67
+ },
68
+ preview_image_saved: true,
69
+ };
70
+
71
+ const mapVisualizationData = {
72
+ ...action.request.data.maps,
73
+ };
74
+ delete mapVisualizationData.preview;
75
+ delete mapVisualizationData.preview_url_loaded;
76
+
77
+ return next({
78
+ ...action,
79
+ request: {
80
+ ...action.request,
81
+ data: {
82
+ ...action.request.data,
83
+ ...previewImage,
84
+ maps: mapVisualizationData,
85
+ },
86
+ },
87
+ });
88
+ } catch (error) {
89
+ return next(action);
90
+ }
91
+ },
92
+ ...middlewares,
93
+ ];
@@ -0,0 +1,49 @@
1
+ import { preview_image } from './preview_image';
2
+ describe('preview_image middleware', () => {
3
+ let store;
4
+ let next;
5
+ let action;
6
+ let middlewares;
7
+
8
+ beforeEach(() => {
9
+ store = {
10
+ getState: jest.fn(() => ({
11
+ content: {
12
+ data: {
13
+ '@type': 'map_interactive',
14
+ preview_image: 'existing_image.png',
15
+ preview_image_saved: false,
16
+ },
17
+ },
18
+ })),
19
+ };
20
+ next = jest.fn();
21
+ middlewares = [];
22
+ });
23
+
24
+ it('existing image', () => {
25
+ action = { type: 'UPDATE_CONTENT', request: { data: {} } };
26
+ const middleware = preview_image(middlewares)[0]; // Accesăm prima funcție din array
27
+ middleware(store)(next)(action); // Executăm funcția de middleware
28
+ expect(next).toHaveBeenCalledWith(action); // Verificăm că funcția next a fost apelată cu acțiunea originală
29
+ });
30
+ it('redo the image', () => {
31
+ store = {
32
+ getState: jest.fn(() => ({
33
+ content: {
34
+ data: {
35
+ '@type': 'map_interactive',
36
+ preview_image: 'preview_image_generated_map_interactive.png',
37
+ preview_image_saved: false,
38
+ },
39
+ },
40
+ })),
41
+ };
42
+ next = jest.fn();
43
+ middlewares = [];
44
+ action = { type: 'UPDATE_CONTENT', request: { data: {} } };
45
+ const middleware = preview_image(middlewares)[0]; // Accesăm prima funcție din array
46
+ middleware(store)(next)(action); // Executăm funcția de middleware
47
+ expect(next).toHaveBeenCalledWith(action); // Verificăm că funcția next a fost apelată cu acțiunea originală
48
+ });
49
+ });