diff --git a/.eslintrc.js b/.eslintrc.js index 0cbd65cc..52469170 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,7 +54,7 @@ const defaultConfig = { allowReferrer: true, }, ], - } + }, }; const config = addonExtenders.reduce( diff --git a/CHANGELOG.md b/CHANGELOG.md index b2dbcdd3..59b9fda8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +### [3.4.0](https://github.com/eea/volto-eea-website-theme/compare/3.3.0...3.4.0) - 11 December 2024 + +#### :bug: Bug Fixes + +- 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)] +- 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)] +- fix(context-navigation): contentTypes choice list when creating a new object [David Ichim - [`d1ccc75`](https://github.com/eea/volto-eea-website-theme/commit/d1ccc7523b681da6aef04f91447cc5190d1c8bbf)] +- 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)] +- fix(context-navigation): missing content types on layout or inside tabs [David Ichim - [`0ab2a04`](https://github.com/eea/volto-eea-website-theme/commit/0ab2a049f2c94aaf6256611309b1bd6b7ff8a610)] +- fix(report-navigation): use report-navigation class instead of smart-toc [David Ichim - [`f4d7f56`](https://github.com/eea/volto-eea-website-theme/commit/f4d7f56ae4e0dbdc04425cc86cfbafb9d527dd85)] +- fix(report-navigation): remove unnecessary context navigation header fallback [David Ichim - [`877e520`](https://github.com/eea/volto-eea-website-theme/commit/877e520f78b8624b5f887b421979ef753a4d3c9e)] + +#### :house: Internal changes + +- 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)] + +#### :hammer_and_wrench: Others + +- Update package.json [Ichim David - [`b14f4c4`](https://github.com/eea/volto-eea-website-theme/commit/b14f4c46c6fc6c99ec70072f260616136c85f095)] ### [3.3.0](https://github.com/eea/volto-eea-website-theme/compare/3.2.0...3.3.0) - 28 November 2024 #### :bug: Bug Fixes diff --git a/package.json b/package.json index feb59872..1a03bbe2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-eea-website-theme", - "version": "3.3.0", + "version": "3.4.0", "description": "@eeacms/volto-eea-website-theme: Volto add-on", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", diff --git a/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx b/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx index 95795d95..06b90648 100644 --- a/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx +++ b/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx @@ -5,8 +5,21 @@ import BlockDataForm from '@plone/volto/components/manage/Form/BlockDataForm'; import ContextNavigationView from './ContextNavigationView'; +import { useSelector, shallowEqual } from 'react-redux'; + +function arePropsEqual(oldProps, newProps) { + return ( + newProps.selected === oldProps.selected && + newProps.data === oldProps.data && + newProps.id === oldProps.id + ); +} + const ContextNavigationFillEdit = (props) => { - const contentTypes = props.properties?.['@components']?.types; + const contentTypes = useSelector( + (state) => state.types?.types || [], + shallowEqual, + ); const availableTypes = React.useMemo( () => contentTypes?.map((type) => [type.id, type.title || type.name]), [contentTypes], @@ -42,4 +55,4 @@ const ContextNavigationFillEdit = (props) => { ); }; -export default ContextNavigationFillEdit; +export default React.memo(ContextNavigationFillEdit, arePropsEqual); diff --git a/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx b/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx index ebaea7ca..92d447e5 100644 --- a/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx +++ b/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx @@ -2,13 +2,27 @@ import React from 'react'; import { flattenToAppURL, withBlockExtensions } from '@plone/volto/helpers'; import DefaultTemplate from './variations/Default'; -const ContextNavigationView = (props = {}) => { +function arePropsEqual(prevProps, nextProps) { + // check if component should be re-rendered + return ( + prevProps.mode === nextProps.mode && + prevProps.id === nextProps.id && + prevProps.path === nextProps.path && + JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) + ); +} + +const ContextNavigationView = React.memo((props = {}) => { const { variation, data = {} } = props; - const navProps = { ...data }; - const root_path = data?.root_node?.[0]?.['@id']; - if (root_path) navProps['root_path'] = flattenToAppURL(root_path); + const navProps = React.useMemo(() => { + const props = { ...data }; + const root_path = data?.root_node?.[0]?.['@id']; + if (root_path) props['root_path'] = flattenToAppURL(root_path); + return props; + }, [data]); + const Renderer = variation?.view ?? DefaultTemplate; return ; -}; +}, arePropsEqual); export default withBlockExtensions(ContextNavigationView); diff --git a/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.jsx b/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.jsx index bbb8bab1..6476f4a4 100644 --- a/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.jsx +++ b/src/components/manage/Blocks/ContextNavigation/variations/ReportNavigation.jsx @@ -4,19 +4,11 @@ import { Link as RouterLink } from 'react-router-dom'; import cx from 'classnames'; import { compose } from 'redux'; import { withRouter } from 'react-router'; -import { defineMessages, useIntl } from 'react-intl'; import { flattenToAppURL } from '@plone/volto/helpers'; import { UniversalLink, MaybeWrap } from '@plone/volto/components'; import { withContentNavigation } from '@plone/volto/components/theme/Navigation/withContentNavigation'; -const messages = defineMessages({ - navigation: { - id: 'Navigation', - defaultMessage: 'Navigation', - }, -}); - /** * Handles click on summary links and closes parent details elements * @param {Event} e - Click event @@ -49,8 +41,7 @@ function renderNode(node, parentLevel) { return (
  • - {navigation.has_custom_name ? ( + diff --git a/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx b/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx index d108be3f..036b11a1 100644 --- a/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx +++ b/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx @@ -68,7 +68,8 @@ const UniversalLink = ({ } const isExternal = !isInternalURL(url); - const isDownload = !isExternal && url && url.includes('@@download'); + const isDownload = + (!isExternal && url && url.includes('@@download')) || download; const isDisplayFile = (!isExternal && url.includes('@@display-file')) || false; diff --git a/src/customizations/volto/components/manage/Widgets/NumberWidget.jsx b/src/customizations/volto/components/manage/Widgets/NumberWidget.jsx new file mode 100644 index 00000000..0e71575e --- /dev/null +++ b/src/customizations/volto/components/manage/Widgets/NumberWidget.jsx @@ -0,0 +1,102 @@ +/** + * NumberWidget component. + * @module components/manage/Widgets/PassswordWidget + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Input } from 'semantic-ui-react'; +import { FormFieldWrapper } from '@plone/volto/components'; +import { injectIntl } from 'react-intl'; + +/** + * NumberWidget component class. + * + * To use it, in schema properties, declare a field like: + * + * ```jsx + * { + * title: "Number", + * type: 'number', + * } + * ``` + */ +const NumberWidget = (props) => { + const { + id, + value, + onChange, + onBlur, + onClick, + isDisabled, + maximum, + minimum, + placeholder, + step, + } = props; + return ( + + + onChange( + id, + target.value === '' ? undefined : window.parseInt(target.value), + ) + } + onBlur={({ target }) => + onBlur( + id, + target.value === '' ? undefined : window.parseInt(target.value), + ) + } + onClick={() => onClick()} + /> + + ); +}; + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +NumberWidget.propTypes = { + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + required: PropTypes.bool, + error: PropTypes.arrayOf(PropTypes.string), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + onChange: PropTypes.func.isRequired, + wrapped: PropTypes.bool, + maximum: PropTypes.number, + minimum: PropTypes.number, + step: PropTypes.number, + placeholder: PropTypes.string, +}; + +/** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ +NumberWidget.defaultProps = { + description: null, + required: false, + error: [], + value: null, + onChange: () => {}, + onBlur: () => {}, + onClick: () => {}, +}; + +export default injectIntl(NumberWidget); diff --git a/src/customizations/volto/components/manage/Widgets/NumberWidget.test.jsx b/src/customizations/volto/components/manage/Widgets/NumberWidget.test.jsx new file mode 100644 index 00000000..d5494010 --- /dev/null +++ b/src/customizations/volto/components/manage/Widgets/NumberWidget.test.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { Provider } from 'react-intl-redux'; +import configureStore from 'redux-mock-store'; +import '@testing-library/jest-dom/extend-expect'; + +import NumberWidget from './NumberWidget'; + +const mockStore = configureStore(); + +const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, +}); + +describe('NumberWidget', () => { + it('renders a number widget component', () => { + const onChange = jest.fn(); + render( + + + , + ); + + expect(screen.getByRole('spinbutton')).toBeInTheDocument(); + }); + + describe('onChange behavior', () => { + it('converts string value to number on change', () => { + const onChange = jest.fn(); + render( + + + , + ); + + const input = screen.getByRole('spinbutton'); + fireEvent.change(input, { target: { value: '42' } }); + + expect(onChange).toHaveBeenCalledWith('my-field', 42); + }); + + it('handles empty value correctly', () => { + const onChange = jest.fn(); + render( + + + , + ); + + const input = screen.getByRole('spinbutton'); + fireEvent.change(input, { target: { value: '' } }); + + expect(onChange).toHaveBeenCalledWith('my-field', undefined); + }); + }); + + describe('onBlur behavior', () => { + it('calls onBlur with the current value', () => { + const onBlur = jest.fn(); + render( + + + , + ); + + const input = screen.getByRole('spinbutton'); + fireEvent.blur(input); + + expect(onBlur).toHaveBeenCalled(); + }); + }); + + describe('validation constraints', () => { + it('respects minimum and maximum values', () => { + render( + + + , + ); + + const input = screen.getByRole('spinbutton'); + expect(input).toHaveAttribute('min', '1'); + expect(input).toHaveAttribute('max', '100'); + }); + + it('handles step attribute', () => { + render( + + + , + ); + + const input = screen.getByRole('spinbutton'); + expect(input).toHaveAttribute('step', '0.5'); + }); + }); +}); diff --git a/src/customizations/volto/components/manage/Widgets/README.md b/src/customizations/volto/components/manage/Widgets/README.md index b52d0898..cdd5331d 100644 --- a/src/customizations/volto/components/manage/Widgets/README.md +++ b/src/customizations/volto/components/manage/Widgets/README.md @@ -1 +1,6 @@ -Customized ObjectBrowserWidget to preserve anchor links in the manually pasted internal URL. +### Customizations + +- Customized ObjectBrowserWidget to preserve anchor links in the manually pasted internal URL. + +- Customized NumberWidget to parse the number input and convert it to a number. + [ichim-david refs #280463]