@eeacms/volto-eea-website-theme 3.2.0 → 3.4.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 +1 -1
- package/CHANGELOG.md +33 -1
- package/package.json +2 -2
- package/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx +15 -2
- package/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx +19 -5
- package/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.jsx +132 -0
- package/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.test.jsx +131 -0
- package/src/components/manage/Blocks/ContextNavigation/variations/index.js +6 -0
- package/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx +2 -1
- package/src/customizations/volto/components/manage/Widgets/NumberWidget.jsx +102 -0
- package/src/customizations/volto/components/manage/Widgets/NumberWidget.test.jsx +120 -0
- package/src/customizations/volto/components/manage/Widgets/README.md +6 -1
- package/src/index.js +12 -1
package/.eslintrc.js
CHANGED
package/CHANGELOG.md
CHANGED
@@ -4,7 +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
|
-
### [3.
|
7
|
+
### [3.4.0](https://github.com/eea/volto-eea-website-theme/compare/3.3.0...3.4.0) - 11 December 2024
|
8
|
+
|
9
|
+
#### :bug: Bug Fixes
|
10
|
+
|
11
|
+
- 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)]
|
12
|
+
- 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)]
|
13
|
+
- fix(context-navigation): contentTypes choice list when creating a new object [David Ichim - [`d1ccc75`](https://github.com/eea/volto-eea-website-theme/commit/d1ccc7523b681da6aef04f91447cc5190d1c8bbf)]
|
14
|
+
- 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)]
|
15
|
+
- fix(context-navigation): missing content types on layout or inside tabs [David Ichim - [`0ab2a04`](https://github.com/eea/volto-eea-website-theme/commit/0ab2a049f2c94aaf6256611309b1bd6b7ff8a610)]
|
16
|
+
- fix(report-navigation): use report-navigation class instead of smart-toc [David Ichim - [`f4d7f56`](https://github.com/eea/volto-eea-website-theme/commit/f4d7f56ae4e0dbdc04425cc86cfbafb9d527dd85)]
|
17
|
+
- fix(report-navigation): remove unnecessary context navigation header fallback [David Ichim - [`877e520`](https://github.com/eea/volto-eea-website-theme/commit/877e520f78b8624b5f887b421979ef753a4d3c9e)]
|
18
|
+
|
19
|
+
#### :house: Internal changes
|
20
|
+
|
21
|
+
- 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)]
|
22
|
+
|
23
|
+
#### :hammer_and_wrench: Others
|
24
|
+
|
25
|
+
- Update package.json [Ichim David - [`b14f4c4`](https://github.com/eea/volto-eea-website-theme/commit/b14f4c46c6fc6c99ec70072f260616136c85f095)]
|
26
|
+
### [3.3.0](https://github.com/eea/volto-eea-website-theme/compare/3.2.0...3.3.0) - 28 November 2024
|
27
|
+
|
28
|
+
#### :bug: Bug Fixes
|
29
|
+
|
30
|
+
- fix(tests): add unit tests for ReportNavigation [ana-oprea - [`55ac4c2`](https://github.com/eea/volto-eea-website-theme/commit/55ac4c2a1edf0c8abdb83a2c7e3c7d578464708a)]
|
31
|
+
|
32
|
+
#### :house: Internal changes
|
33
|
+
|
34
|
+
- chore: package.json [alin - [`4a8a4cb`](https://github.com/eea/volto-eea-website-theme/commit/4a8a4cb014db839b90eceed935c97b85785ddf71)]
|
35
|
+
|
36
|
+
#### :hammer_and_wrench: Others
|
37
|
+
|
38
|
+
- Update package.json [Ichim David - [`53be025`](https://github.com/eea/volto-eea-website-theme/commit/53be025c116dfc71a2de708075e4e77262eeecf8)]
|
39
|
+
### [3.2.0](https://github.com/eea/volto-eea-website-theme/compare/3.1.0...3.2.0) - 14 November 2024
|
8
40
|
|
9
41
|
#### :hammer_and_wrench: Others
|
10
42
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@eeacms/volto-eea-website-theme",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.4.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",
|
@@ -24,8 +24,8 @@
|
|
24
24
|
"url": "git@github.com:eea/volto-eea-website-theme.git"
|
25
25
|
},
|
26
26
|
"dependencies": {
|
27
|
-
"@eeacms/volto-block-toc": "*",
|
28
27
|
"@eeacms/volto-block-style": "*",
|
28
|
+
"@eeacms/volto-block-toc": "*",
|
29
29
|
"@eeacms/volto-eea-design-system": "*",
|
30
30
|
"@eeacms/volto-group-block": "*",
|
31
31
|
"volto-subsites": "*"
|
@@ -5,8 +5,21 @@ 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 =
|
19
|
+
const contentTypes = useSelector(
|
20
|
+
(state) => state.types?.types || [],
|
21
|
+
shallowEqual,
|
22
|
+
);
|
10
23
|
const availableTypes = React.useMemo(
|
11
24
|
() => contentTypes?.map((type) => [type.id, type.title || type.name]),
|
12
25
|
[contentTypes],
|
@@ -42,4 +55,4 @@ const ContextNavigationFillEdit = (props) => {
|
|
42
55
|
);
|
43
56
|
};
|
44
57
|
|
45
|
-
export default ContextNavigationFillEdit;
|
58
|
+
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
|
-
|
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 =
|
8
|
-
|
9
|
-
|
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);
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import PropTypes from 'prop-types';
|
2
|
+
import React from 'react';
|
3
|
+
import { Link as RouterLink } from 'react-router-dom';
|
4
|
+
import cx from 'classnames';
|
5
|
+
import { compose } from 'redux';
|
6
|
+
import { withRouter } from 'react-router';
|
7
|
+
|
8
|
+
import { flattenToAppURL } from '@plone/volto/helpers';
|
9
|
+
import { UniversalLink, MaybeWrap } from '@plone/volto/components';
|
10
|
+
import { withContentNavigation } from '@plone/volto/components/theme/Navigation/withContentNavigation';
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Handles click on summary links and closes parent details elements
|
14
|
+
* @param {Event} e - Click event
|
15
|
+
* @param {boolean} wrapWithDetails - Whether the element is wrapped in details
|
16
|
+
*/
|
17
|
+
function handleSummaryClick(e, wrapWithDetails) {
|
18
|
+
if (wrapWithDetails) {
|
19
|
+
e.preventDefault();
|
20
|
+
|
21
|
+
const currentDetails = e.target.closest('details');
|
22
|
+
// toggle the current details
|
23
|
+
if (currentDetails) {
|
24
|
+
currentDetails.open = !currentDetails.open;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Renders a navigation node as a list item with proper styling and links
|
31
|
+
* @param {Object} node - Navigation node object containing title, href, type etc
|
32
|
+
* @param {number} parentLevel - Parent level in navigation hierarchy
|
33
|
+
* @returns {React.Component} UL component with navigation node content
|
34
|
+
*/
|
35
|
+
function renderNode(node, parentLevel) {
|
36
|
+
const level = parentLevel + 1;
|
37
|
+
const hasChildItems = node.items?.length;
|
38
|
+
const nodeType = node.type;
|
39
|
+
const isDocument = nodeType === 'document';
|
40
|
+
let wrapWithDetails = isDocument && level > 2;
|
41
|
+
return (
|
42
|
+
<li
|
43
|
+
key={node['@id']}
|
44
|
+
className={`list-item level-${level} ${node.is_current ? 'active' : ''}`}
|
45
|
+
>
|
46
|
+
<MaybeWrap
|
47
|
+
condition={wrapWithDetails}
|
48
|
+
as="details"
|
49
|
+
className="context-navigation-detail"
|
50
|
+
>
|
51
|
+
{nodeType !== 'link' ? (
|
52
|
+
<MaybeWrap
|
53
|
+
condition={wrapWithDetails}
|
54
|
+
as="summary"
|
55
|
+
className="context-navigation-summary"
|
56
|
+
>
|
57
|
+
<RouterLink
|
58
|
+
to={flattenToAppURL(node.href)}
|
59
|
+
tabIndex={wrapWithDetails ? '-1' : 0}
|
60
|
+
title={node.description}
|
61
|
+
className={cx(`list-link contenttype-${nodeType}`, {
|
62
|
+
in_path: node.is_in_path,
|
63
|
+
})}
|
64
|
+
onClick={(e) =>
|
65
|
+
wrapWithDetails && handleSummaryClick(e, wrapWithDetails)
|
66
|
+
}
|
67
|
+
>
|
68
|
+
{node.title}
|
69
|
+
{nodeType === 'file' && node.getObjSize
|
70
|
+
? ' [' + node.getObjSize + ']'
|
71
|
+
: ''}
|
72
|
+
</RouterLink>
|
73
|
+
</MaybeWrap>
|
74
|
+
) : (
|
75
|
+
<UniversalLink href={flattenToAppURL(node.href)}>
|
76
|
+
{node.title}
|
77
|
+
</UniversalLink>
|
78
|
+
)}
|
79
|
+
{(hasChildItems && (
|
80
|
+
<ul className="list">
|
81
|
+
{node.items.map((node) => renderNode(node, level))}
|
82
|
+
</ul>
|
83
|
+
)) ||
|
84
|
+
''}
|
85
|
+
</MaybeWrap>
|
86
|
+
</li>
|
87
|
+
);
|
88
|
+
}
|
89
|
+
/**
|
90
|
+
* A navigation slot implementation, similar to the classic Plone navigation
|
91
|
+
* portlet. It uses the same API, so the options are similar to
|
92
|
+
* INavigationPortlet
|
93
|
+
*/
|
94
|
+
export function ReportNavigation(props) {
|
95
|
+
const { navigation = {} } = props;
|
96
|
+
const { items = [] } = navigation;
|
97
|
+
|
98
|
+
return items.length ? (
|
99
|
+
<nav className="context-navigation report-navigation">
|
100
|
+
{navigation.title ? (
|
101
|
+
<div className="context-navigation-header">
|
102
|
+
<RouterLink to={flattenToAppURL(navigation.url || '')}>
|
103
|
+
{navigation.title}
|
104
|
+
</RouterLink>
|
105
|
+
</div>
|
106
|
+
) : (
|
107
|
+
''
|
108
|
+
)}
|
109
|
+
<ul className="list">{items.map((node) => renderNode(node, 0))}</ul>
|
110
|
+
</nav>
|
111
|
+
) : (
|
112
|
+
''
|
113
|
+
);
|
114
|
+
}
|
115
|
+
|
116
|
+
ReportNavigation.propTypes = {
|
117
|
+
/**
|
118
|
+
* Navigation tree returned from @contextnavigation restapi endpoint
|
119
|
+
*/
|
120
|
+
navigation: PropTypes.shape({
|
121
|
+
items: PropTypes.arrayOf(
|
122
|
+
PropTypes.shape({
|
123
|
+
title: PropTypes.string,
|
124
|
+
url: PropTypes.string,
|
125
|
+
}),
|
126
|
+
),
|
127
|
+
has_custom_name: PropTypes.bool,
|
128
|
+
title: PropTypes.string,
|
129
|
+
}),
|
130
|
+
};
|
131
|
+
|
132
|
+
export default compose(withRouter, withContentNavigation)(ReportNavigation);
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react';
|
2
|
+
import { Provider } from 'react-intl-redux';
|
3
|
+
import { Router } from 'react-router-dom';
|
4
|
+
import { createMemoryHistory } from 'history';
|
5
|
+
import ReportNavigation from './ReportNavigation';
|
6
|
+
import configureStore from 'redux-mock-store';
|
7
|
+
import '@testing-library/jest-dom/extend-expect';
|
8
|
+
|
9
|
+
const mockStore = configureStore();
|
10
|
+
const store = mockStore({
|
11
|
+
intl: {
|
12
|
+
locale: 'en',
|
13
|
+
messages: {},
|
14
|
+
},
|
15
|
+
});
|
16
|
+
|
17
|
+
jest.mock(
|
18
|
+
'@plone/volto/components/theme/Navigation/withContentNavigation',
|
19
|
+
() => ({
|
20
|
+
withContentNavigation: (Component) => (props) => (
|
21
|
+
<Component {...props} navigation={mockNavigation} />
|
22
|
+
),
|
23
|
+
}),
|
24
|
+
);
|
25
|
+
|
26
|
+
// Mock navigation data
|
27
|
+
const mockNavigation = {
|
28
|
+
items: [
|
29
|
+
{
|
30
|
+
'@id': '/item1',
|
31
|
+
title: 'Item 1',
|
32
|
+
href: '/item1',
|
33
|
+
type: 'document',
|
34
|
+
description: 'Item 1 description',
|
35
|
+
is_current: false,
|
36
|
+
is_in_path: false,
|
37
|
+
items: [
|
38
|
+
{
|
39
|
+
'@id': '/item1/subitem1',
|
40
|
+
title: 'Subitem 1',
|
41
|
+
href: '/item1/subitem1',
|
42
|
+
type: 'document',
|
43
|
+
is_current: false,
|
44
|
+
is_in_path: false,
|
45
|
+
items: [],
|
46
|
+
},
|
47
|
+
],
|
48
|
+
},
|
49
|
+
{
|
50
|
+
'@id': '/item2',
|
51
|
+
title: 'Item 2',
|
52
|
+
href: '/item2',
|
53
|
+
type: 'document',
|
54
|
+
description: 'Item 2 description',
|
55
|
+
is_current: true,
|
56
|
+
is_in_path: true,
|
57
|
+
items: [],
|
58
|
+
},
|
59
|
+
],
|
60
|
+
has_custom_name: true,
|
61
|
+
title: 'Custom Navigation',
|
62
|
+
url: '/custom-navigation',
|
63
|
+
};
|
64
|
+
|
65
|
+
describe('ReportNavigation', () => {
|
66
|
+
it('renders navigation items correctly', () => {
|
67
|
+
const history = createMemoryHistory();
|
68
|
+
const { getByText } = render(
|
69
|
+
<Provider store={store}>
|
70
|
+
<Router history={history}>
|
71
|
+
<ReportNavigation />
|
72
|
+
</Router>
|
73
|
+
</Provider>,
|
74
|
+
);
|
75
|
+
|
76
|
+
// Check if the navigation header is rendered
|
77
|
+
expect(getByText('Custom Navigation')).toBeInTheDocument();
|
78
|
+
|
79
|
+
// Check if the navigation items are rendered
|
80
|
+
expect(getByText('Item 1')).toBeInTheDocument();
|
81
|
+
expect(getByText('Item 2')).toBeInTheDocument();
|
82
|
+
expect(getByText('Subitem 1')).toBeInTheDocument();
|
83
|
+
});
|
84
|
+
|
85
|
+
it('toggles details on summary click', () => {
|
86
|
+
const history = createMemoryHistory();
|
87
|
+
const { container } = render(
|
88
|
+
<Provider store={store}>
|
89
|
+
<Router history={history}>
|
90
|
+
<ReportNavigation />
|
91
|
+
</Router>
|
92
|
+
</Provider>,
|
93
|
+
);
|
94
|
+
|
95
|
+
const detailsElement = container.querySelector('a[href="/item1"]');
|
96
|
+
|
97
|
+
// Simulate click on summary
|
98
|
+
fireEvent.click(detailsElement);
|
99
|
+
});
|
100
|
+
|
101
|
+
it('renders links with correct href attributes', () => {
|
102
|
+
const history = createMemoryHistory();
|
103
|
+
const { getByText } = render(
|
104
|
+
<Provider store={store}>
|
105
|
+
<Router history={history}>
|
106
|
+
<ReportNavigation />
|
107
|
+
</Router>
|
108
|
+
</Provider>,
|
109
|
+
);
|
110
|
+
|
111
|
+
expect(getByText('Item 1').closest('a')).toHaveAttribute('href', '/item1');
|
112
|
+
expect(getByText('Subitem 1').closest('a')).toHaveAttribute(
|
113
|
+
'href',
|
114
|
+
'/item1/subitem1',
|
115
|
+
);
|
116
|
+
});
|
117
|
+
|
118
|
+
it('applies active class to the current navigation item', () => {
|
119
|
+
const history = createMemoryHistory();
|
120
|
+
const { getByText } = render(
|
121
|
+
<Provider store={store}>
|
122
|
+
<Router history={history}>
|
123
|
+
<ReportNavigation />
|
124
|
+
</Router>
|
125
|
+
</Provider>,
|
126
|
+
);
|
127
|
+
|
128
|
+
const activeItem = getByText('Item 2');
|
129
|
+
expect(activeItem).toHaveClass('in_path');
|
130
|
+
});
|
131
|
+
});
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import Accordion from './Accordion';
|
2
2
|
import Default from './Default';
|
3
|
+
import ReportNavigation from './ReportNavigation';
|
3
4
|
|
4
5
|
const contextBlockVariations = [
|
5
6
|
{
|
@@ -13,6 +14,11 @@ const contextBlockVariations = [
|
|
13
14
|
title: 'Accordion',
|
14
15
|
view: Accordion,
|
15
16
|
},
|
17
|
+
{
|
18
|
+
id: 'report_navigation',
|
19
|
+
title: 'Additional files',
|
20
|
+
view: ReportNavigation,
|
21
|
+
},
|
16
22
|
];
|
17
23
|
|
18
24
|
export default contextBlockVariations;
|
@@ -68,7 +68,8 @@ const UniversalLink = ({
|
|
68
68
|
}
|
69
69
|
|
70
70
|
const isExternal = !isInternalURL(url);
|
71
|
-
const isDownload =
|
71
|
+
const isDownload =
|
72
|
+
(!isExternal && url && url.includes('@@download')) || download;
|
72
73
|
|
73
74
|
const isDisplayFile =
|
74
75
|
(!isExternal && url.includes('@@display-file')) || false;
|
@@ -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
|
-
|
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]
|
package/src/index.js
CHANGED
@@ -4,6 +4,8 @@ import { Icon } from '@plone/volto/components';
|
|
4
4
|
import { default as TokenWidgetEdit } from '@plone/volto/components/manage/Widgets/TokenWidget';
|
5
5
|
import SelectAutoCompleteWidget from '@plone/volto/components/manage/Widgets/SelectAutoComplete';
|
6
6
|
import { serializeNodesToText } from '@plone/volto-slate/editor/render';
|
7
|
+
import TableBlockEdit from '@plone/volto-slate/blocks/Table/TableBlockEdit';
|
8
|
+
import TableBlockView from '@plone/volto-slate/blocks/Table/TableBlockView';
|
7
9
|
import { nanoid } from '@plone/volto-slate/utils';
|
8
10
|
|
9
11
|
import InpageNavigation from '@eeacms/volto-eea-design-system/ui/InpageNavigation/InpageNavigation';
|
@@ -230,6 +232,15 @@ const applyConfig = (config) => {
|
|
230
232
|
config.blocks.blocksConfig.description.restricted = false;
|
231
233
|
config.blocks.requiredBlocks = [];
|
232
234
|
|
235
|
+
// 281166 fix paste of tables in edit mode where paste action deemed the type
|
236
|
+
// of slate type to be table which in Volto 17 is mapped to the Table block which is draftjs based
|
237
|
+
// with this fix we load the edit and view of the slateTable avoiding any draftjs loading and error
|
238
|
+
config.blocks.blocksConfig.table = {
|
239
|
+
...config.blocks.blocksConfig.table,
|
240
|
+
view: TableBlockView,
|
241
|
+
edit: TableBlockEdit,
|
242
|
+
};
|
243
|
+
|
233
244
|
// Date format for EU
|
234
245
|
config.settings.dateLocale = 'en-gb';
|
235
246
|
|
@@ -566,7 +577,7 @@ const applyConfig = (config) => {
|
|
566
577
|
GET_CONTENT: ['breadcrumbs'], // 'navigation', 'actions', 'types'],
|
567
578
|
});
|
568
579
|
|
569
|
-
// Custom blocks: Title,Layout settings, Context navigation
|
580
|
+
// Custom blocks: Title, Layout settings, Context navigation
|
570
581
|
return [
|
571
582
|
installCustomTitle,
|
572
583
|
installLayoutSettingsBlock,
|