@eeacms/volto-eea-website-theme 3.3.0 → 3.5.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/.eslintrc.js CHANGED
@@ -54,7 +54,7 @@ const defaultConfig = {
54
54
  allowReferrer: true,
55
55
  },
56
56
  ],
57
- }
57
+ },
58
58
  };
59
59
 
60
60
  const config = addonExtenders.reduce(
package/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ 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
+ ### [3.5.0](https://github.com/eea/volto-eea-website-theme/compare/3.4.0...3.5.0) - 16 December 2024
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(report-navigation): add download icon and ensure file is downloaded directly [David Ichim - [`fa653c2`](https://github.com/eea/volto-eea-website-theme/commit/fa653c288988248218877a7ea66a7fe63bb59b09)]
12
+
13
+ #### :bug: Bug Fixes
14
+
15
+ - fix(UniversaLink): added option to open in new tab when isDisplayFile is true refs#281635 [laszlocseh - [`6e65ded`](https://github.com/eea/volto-eea-website-theme/commit/6e65dedef15e395156380318bdd19bc3812aba44)]
16
+ - fix(report-navigation): check if page has children before rendering it as a detail [David Ichim - [`35aabb3`](https://github.com/eea/volto-eea-website-theme/commit/35aabb31ef1619ff60695228373a9b1e08c248d6)]
17
+ - fix(context-navigation): error on layout page when types was an object [David Ichim - [`d06f7ab`](https://github.com/eea/volto-eea-website-theme/commit/d06f7ab641fafad0cfdf1c69381dc1e44696e008)]
18
+
19
+ #### :house: Internal changes
20
+
21
+ - style: Automated code fix [eea-jenkins - [`fabc331`](https://github.com/eea/volto-eea-website-theme/commit/fabc331a5ae2048c695ae7be57d45d47f0744e84)]
22
+
23
+ #### :hammer_and_wrench: Others
24
+
25
+ - bump package version [David Ichim - [`fb11d4c`](https://github.com/eea/volto-eea-website-theme/commit/fb11d4c5a48aadbde7fef65f5e5fa07dd451d58c)]
26
+ ### [3.4.0](https://github.com/eea/volto-eea-website-theme/compare/3.3.0...3.4.0) - 11 December 2024
27
+
28
+ #### :bug: Bug Fixes
29
+
30
+ - fix(UniversaLink): use download prop in calculating anchor for downloadable files refs#281622 [nileshgulia1 - [`239b492`](https://github.com/eea/volto-eea-website-theme/commit/239b4928d39a9584fdc9ce3ef3f012dee23f840e)]
31
+ - fix(ContextNavigation): Add memoization for View that triggered fetch on Edit page modifications [David Ichim - [`48fdf60`](https://github.com/eea/volto-eea-website-theme/commit/48fdf6089e21aeefacf927310b9a41ea9cd0e2be)]
32
+ - fix(context-navigation): contentTypes choice list when creating a new object [David Ichim - [`d1ccc75`](https://github.com/eea/volto-eea-website-theme/commit/d1ccc7523b681da6aef04f91447cc5190d1c8bbf)]
33
+ - fix(number-widget): from Volto core to parse values to int avoiding passing wrong values to restapi code such as context navigation [David Ichim - [`0d67686`](https://github.com/eea/volto-eea-website-theme/commit/0d6768652201d2b1dbf8e478613049e654b7476e)]
34
+ - fix(context-navigation): missing content types on layout or inside tabs [David Ichim - [`0ab2a04`](https://github.com/eea/volto-eea-website-theme/commit/0ab2a049f2c94aaf6256611309b1bd6b7ff8a610)]
35
+ - fix(report-navigation): use report-navigation class instead of smart-toc [David Ichim - [`f4d7f56`](https://github.com/eea/volto-eea-website-theme/commit/f4d7f56ae4e0dbdc04425cc86cfbafb9d527dd85)]
36
+ - fix(report-navigation): remove unnecessary context navigation header fallback [David Ichim - [`877e520`](https://github.com/eea/volto-eea-website-theme/commit/877e520f78b8624b5f887b421979ef753a4d3c9e)]
37
+
38
+ #### :house: Internal changes
39
+
40
+ - chore: fix eslint config lint warning and avoid warning for active property within report navigation block list items [David Ichim - [`9b3b03c`](https://github.com/eea/volto-eea-website-theme/commit/9b3b03c36626ee5d4bb7b64b6413217ee8903873)]
41
+
42
+ #### :hammer_and_wrench: Others
43
+
44
+ - Update package.json [Ichim David - [`b14f4c4`](https://github.com/eea/volto-eea-website-theme/commit/b14f4c46c6fc6c99ec70072f260616136c85f095)]
7
45
  ### [3.3.0](https://github.com/eea/volto-eea-website-theme/compare/3.2.0...3.3.0) - 28 November 2024
8
46
 
9
47
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "@eeacms/volto-eea-website-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -82,4 +82,4 @@
82
82
  "cypress:open": "make cypress-open",
83
83
  "prepare": "husky install"
84
84
  }
85
- }
85
+ }
@@ -5,10 +5,27 @@ import BlockDataForm from '@plone/volto/components/manage/Form/BlockDataForm';
5
5
 
6
6
  import ContextNavigationView from './ContextNavigationView';
7
7
 
8
+ import { useSelector, shallowEqual } from 'react-redux';
9
+
10
+ function arePropsEqual(oldProps, newProps) {
11
+ return (
12
+ newProps.selected === oldProps.selected &&
13
+ newProps.data === oldProps.data &&
14
+ newProps.id === oldProps.id
15
+ );
16
+ }
17
+
8
18
  const ContextNavigationFillEdit = (props) => {
9
- const contentTypes = props.properties?.['@components']?.types;
19
+ const contentTypes = useSelector(
20
+ (state) => state.types?.types || [],
21
+ shallowEqual,
22
+ );
23
+
10
24
  const availableTypes = React.useMemo(
11
- () => contentTypes?.map((type) => [type.id, type.title || type.name]),
25
+ () =>
26
+ Array.isArray(contentTypes)
27
+ ? contentTypes.map((type) => [type.id, type.title || type.name])
28
+ : [],
12
29
  [contentTypes],
13
30
  );
14
31
 
@@ -42,4 +59,4 @@ const ContextNavigationFillEdit = (props) => {
42
59
  );
43
60
  };
44
61
 
45
- export default ContextNavigationFillEdit;
62
+ export default React.memo(ContextNavigationFillEdit, arePropsEqual);
@@ -2,13 +2,27 @@ import React from 'react';
2
2
  import { flattenToAppURL, withBlockExtensions } from '@plone/volto/helpers';
3
3
  import DefaultTemplate from './variations/Default';
4
4
 
5
- const ContextNavigationView = (props = {}) => {
5
+ function arePropsEqual(prevProps, nextProps) {
6
+ // check if component should be re-rendered
7
+ return (
8
+ prevProps.mode === nextProps.mode &&
9
+ prevProps.id === nextProps.id &&
10
+ prevProps.path === nextProps.path &&
11
+ JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data)
12
+ );
13
+ }
14
+
15
+ const ContextNavigationView = React.memo((props = {}) => {
6
16
  const { variation, data = {} } = props;
7
- const navProps = { ...data };
8
- const root_path = data?.root_node?.[0]?.['@id'];
9
- if (root_path) navProps['root_path'] = flattenToAppURL(root_path);
17
+ const navProps = React.useMemo(() => {
18
+ const props = { ...data };
19
+ const root_path = data?.root_node?.[0]?.['@id'];
20
+ if (root_path) props['root_path'] = flattenToAppURL(root_path);
21
+ return props;
22
+ }, [data]);
23
+
10
24
  const Renderer = variation?.view ?? DefaultTemplate;
11
25
  return <Renderer params={navProps} mode={props.mode} />;
12
- };
26
+ }, arePropsEqual);
13
27
 
14
28
  export default withBlockExtensions(ContextNavigationView);
@@ -1,21 +1,14 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { Link as RouterLink } from 'react-router-dom';
4
3
  import cx from 'classnames';
5
4
  import { compose } from 'redux';
6
5
  import { withRouter } from 'react-router';
7
- import { defineMessages, useIntl } from 'react-intl';
8
6
 
9
7
  import { flattenToAppURL } from '@plone/volto/helpers';
10
- import { UniversalLink, MaybeWrap } from '@plone/volto/components';
8
+ import { UniversalLink, MaybeWrap, Icon } from '@plone/volto/components';
11
9
  import { withContentNavigation } from '@plone/volto/components/theme/Navigation/withContentNavigation';
12
10
 
13
- const messages = defineMessages({
14
- navigation: {
15
- id: 'Navigation',
16
- defaultMessage: 'Navigation',
17
- },
18
- });
11
+ import downloadSVG from '@plone/volto/icons/download.svg';
19
12
 
20
13
  /**
21
14
  * Handles click on summary links and closes parent details elements
@@ -45,46 +38,40 @@ function renderNode(node, parentLevel) {
45
38
  const hasChildItems = node.items?.length;
46
39
  const nodeType = node.type;
47
40
  const isDocument = nodeType === 'document';
48
- let wrapWithDetails = isDocument && level > 2;
41
+ const isFile = nodeType === 'file';
42
+ let wrapWithDetails = isDocument && level > 2 && hasChildItems;
49
43
  return (
50
44
  <li
51
45
  key={node['@id']}
52
- active={node.is_current}
53
- className={`list-item level-${level}`}
46
+ className={`list-item level-${level} ${node.is_current ? 'active' : ''}`}
54
47
  >
55
48
  <MaybeWrap
56
49
  condition={wrapWithDetails}
57
50
  as="details"
58
51
  className="context-navigation-detail"
59
52
  >
60
- {nodeType !== 'link' ? (
61
- <MaybeWrap
62
- condition={wrapWithDetails}
63
- as="summary"
64
- className="context-navigation-summary"
53
+ <MaybeWrap
54
+ condition={wrapWithDetails}
55
+ as="summary"
56
+ className="context-navigation-summary"
57
+ >
58
+ <UniversalLink
59
+ href={flattenToAppURL(node.href)}
60
+ download={isFile}
61
+ tabIndex={wrapWithDetails ? '-1' : 0}
62
+ title={node.description}
63
+ className={cx(`list-link contenttype-${nodeType}`, {
64
+ in_path: node.is_in_path,
65
+ })}
66
+ onClick={(e) =>
67
+ wrapWithDetails && handleSummaryClick(e, wrapWithDetails)
68
+ }
65
69
  >
66
- <RouterLink
67
- to={flattenToAppURL(node.href)}
68
- tabIndex={wrapWithDetails ? '-1' : 0}
69
- title={node.description}
70
- className={cx(`list-link contenttype-${nodeType}`, {
71
- in_path: node.is_in_path,
72
- })}
73
- onClick={(e) =>
74
- wrapWithDetails && handleSummaryClick(e, wrapWithDetails)
75
- }
76
- >
77
- {node.title}
78
- {nodeType === 'file' && node.getObjSize
79
- ? ' [' + node.getObjSize + ']'
80
- : ''}
81
- </RouterLink>
82
- </MaybeWrap>
83
- ) : (
84
- <UniversalLink href={flattenToAppURL(node.href)}>
70
+ {isFile && <Icon name={downloadSVG} size="16px" />}
85
71
  {node.title}
72
+ {isFile && node.getObjSize ? ' [' + node.getObjSize + ']' : ''}
86
73
  </UniversalLink>
87
- )}
74
+ </MaybeWrap>
88
75
  {(hasChildItems && (
89
76
  <ul className="list">
90
77
  {node.items.map((node) => renderNode(node, level))}
@@ -103,20 +90,17 @@ function renderNode(node, parentLevel) {
103
90
  export function ReportNavigation(props) {
104
91
  const { navigation = {} } = props;
105
92
  const { items = [] } = navigation;
106
- const intl = useIntl();
107
93
 
108
94
  return items.length ? (
109
- <nav className="context-navigation smart-toc">
110
- {navigation.has_custom_name ? (
95
+ <nav className="context-navigation report-navigation">
96
+ {navigation.title ? (
111
97
  <div className="context-navigation-header">
112
- <RouterLink to={flattenToAppURL(navigation.url || '')}>
98
+ <UniversalLink href={flattenToAppURL(navigation.url || '')}>
113
99
  {navigation.title}
114
- </RouterLink>
100
+ </UniversalLink>
115
101
  </div>
116
102
  ) : (
117
- <div className="context-navigation-header">
118
- {intl.formatMessage(messages.navigation)}
119
- </div>
103
+ ''
120
104
  )}
121
105
  <ul className="list">{items.map((node) => renderNode(node, 0))}</ul>
122
106
  </nav>
@@ -68,7 +68,8 @@ const UniversalLink = ({
68
68
  }
69
69
 
70
70
  const isExternal = !isInternalURL(url);
71
- const isDownload = !isExternal && url && url.includes('@@download');
71
+ const isDownload =
72
+ (!isExternal && url && url.includes('@@download')) || download;
72
73
 
73
74
  const isDisplayFile =
74
75
  (!isExternal && url.includes('@@display-file')) || false;
@@ -126,6 +127,7 @@ const UniversalLink = ({
126
127
  <a
127
128
  href={flattenToAppURL(url)}
128
129
  title={title}
130
+ target={!(openLinkInNewTab === false) ? '_blank' : null}
129
131
  rel="noopener"
130
132
  className={className}
131
133
  {...props}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * NumberWidget component.
3
+ * @module components/manage/Widgets/PassswordWidget
4
+ */
5
+
6
+ import React from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import { Input } from 'semantic-ui-react';
9
+ import { FormFieldWrapper } from '@plone/volto/components';
10
+ import { injectIntl } from 'react-intl';
11
+
12
+ /**
13
+ * NumberWidget component class.
14
+ *
15
+ * To use it, in schema properties, declare a field like:
16
+ *
17
+ * ```jsx
18
+ * {
19
+ * title: "Number",
20
+ * type: 'number',
21
+ * }
22
+ * ```
23
+ */
24
+ const NumberWidget = (props) => {
25
+ const {
26
+ id,
27
+ value,
28
+ onChange,
29
+ onBlur,
30
+ onClick,
31
+ isDisabled,
32
+ maximum,
33
+ minimum,
34
+ placeholder,
35
+ step,
36
+ } = props;
37
+ return (
38
+ <FormFieldWrapper {...props}>
39
+ <Input
40
+ id={`field-${id}`}
41
+ name={id}
42
+ type="number"
43
+ disabled={isDisabled}
44
+ min={minimum || null}
45
+ max={maximum || null}
46
+ step={step}
47
+ value={value ?? ''}
48
+ placeholder={placeholder}
49
+ onChange={({ target }) =>
50
+ onChange(
51
+ id,
52
+ target.value === '' ? undefined : window.parseInt(target.value),
53
+ )
54
+ }
55
+ onBlur={({ target }) =>
56
+ onBlur(
57
+ id,
58
+ target.value === '' ? undefined : window.parseInt(target.value),
59
+ )
60
+ }
61
+ onClick={() => onClick()}
62
+ />
63
+ </FormFieldWrapper>
64
+ );
65
+ };
66
+
67
+ /**
68
+ * Property types.
69
+ * @property {Object} propTypes Property types.
70
+ * @static
71
+ */
72
+ NumberWidget.propTypes = {
73
+ id: PropTypes.string.isRequired,
74
+ title: PropTypes.string.isRequired,
75
+ description: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
76
+ required: PropTypes.bool,
77
+ error: PropTypes.arrayOf(PropTypes.string),
78
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
79
+ onChange: PropTypes.func.isRequired,
80
+ wrapped: PropTypes.bool,
81
+ maximum: PropTypes.number,
82
+ minimum: PropTypes.number,
83
+ step: PropTypes.number,
84
+ placeholder: PropTypes.string,
85
+ };
86
+
87
+ /**
88
+ * Default properties.
89
+ * @property {Object} defaultProps Default properties.
90
+ * @static
91
+ */
92
+ NumberWidget.defaultProps = {
93
+ description: null,
94
+ required: false,
95
+ error: [],
96
+ value: null,
97
+ onChange: () => {},
98
+ onBlur: () => {},
99
+ onClick: () => {},
100
+ };
101
+
102
+ export default injectIntl(NumberWidget);
@@ -0,0 +1,120 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { Provider } from 'react-intl-redux';
4
+ import configureStore from 'redux-mock-store';
5
+ import '@testing-library/jest-dom/extend-expect';
6
+
7
+ import NumberWidget from './NumberWidget';
8
+
9
+ const mockStore = configureStore();
10
+
11
+ const store = mockStore({
12
+ intl: {
13
+ locale: 'en',
14
+ messages: {},
15
+ },
16
+ });
17
+
18
+ describe('NumberWidget', () => {
19
+ it('renders a number widget component', () => {
20
+ const onChange = jest.fn();
21
+ render(
22
+ <Provider store={store}>
23
+ <NumberWidget
24
+ id="my-field"
25
+ title="My field"
26
+ fieldSet="default"
27
+ onChange={onChange}
28
+ />
29
+ </Provider>,
30
+ );
31
+
32
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
33
+ });
34
+
35
+ describe('onChange behavior', () => {
36
+ it('converts string value to number on change', () => {
37
+ const onChange = jest.fn();
38
+ render(
39
+ <Provider store={store}>
40
+ <NumberWidget id="my-field" title="My field" onChange={onChange} />
41
+ </Provider>,
42
+ );
43
+
44
+ const input = screen.getByRole('spinbutton');
45
+ fireEvent.change(input, { target: { value: '42' } });
46
+
47
+ expect(onChange).toHaveBeenCalledWith('my-field', 42);
48
+ });
49
+
50
+ it('handles empty value correctly', () => {
51
+ const onChange = jest.fn();
52
+ render(
53
+ <Provider store={store}>
54
+ <NumberWidget
55
+ id="my-field"
56
+ value="1"
57
+ title="My field"
58
+ onChange={onChange}
59
+ />
60
+ </Provider>,
61
+ );
62
+
63
+ const input = screen.getByRole('spinbutton');
64
+ fireEvent.change(input, { target: { value: '' } });
65
+
66
+ expect(onChange).toHaveBeenCalledWith('my-field', undefined);
67
+ });
68
+ });
69
+
70
+ describe('onBlur behavior', () => {
71
+ it('calls onBlur with the current value', () => {
72
+ const onBlur = jest.fn();
73
+ render(
74
+ <Provider store={store}>
75
+ <NumberWidget
76
+ id="my-field"
77
+ title="My field"
78
+ onBlur={onBlur}
79
+ value="123"
80
+ />
81
+ </Provider>,
82
+ );
83
+
84
+ const input = screen.getByRole('spinbutton');
85
+ fireEvent.blur(input);
86
+
87
+ expect(onBlur).toHaveBeenCalled();
88
+ });
89
+ });
90
+
91
+ describe('validation constraints', () => {
92
+ it('respects minimum and maximum values', () => {
93
+ render(
94
+ <Provider store={store}>
95
+ <NumberWidget
96
+ id="my-field"
97
+ title="My field"
98
+ minimum={1}
99
+ maximum={100}
100
+ />
101
+ </Provider>,
102
+ );
103
+
104
+ const input = screen.getByRole('spinbutton');
105
+ expect(input).toHaveAttribute('min', '1');
106
+ expect(input).toHaveAttribute('max', '100');
107
+ });
108
+
109
+ it('handles step attribute', () => {
110
+ render(
111
+ <Provider store={store}>
112
+ <NumberWidget id="my-field" title="My field" step={0.5} />
113
+ </Provider>,
114
+ );
115
+
116
+ const input = screen.getByRole('spinbutton');
117
+ expect(input).toHaveAttribute('step', '0.5');
118
+ });
119
+ });
120
+ });
@@ -1 +1,6 @@
1
- Customized ObjectBrowserWidget to preserve anchor links in the manually pasted internal URL.
1
+ ### Customizations
2
+
3
+ - Customized ObjectBrowserWidget to preserve anchor links in the manually pasted internal URL.
4
+
5
+ - Customized NumberWidget to parse the number input and convert it to a number.
6
+ [ichim-david refs #280463]