Skip to content

Commit

Permalink
Merge pull request #276 from eea/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
avoinea authored Dec 11, 2024
2 parents db75942 + 650c7c8 commit 5e0c608
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const defaultConfig = {
allowReferrer: true,
},
],
}
},
};

const config = addonExtenders.reduce(
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -42,4 +55,4 @@ const ContextNavigationFillEdit = (props) => {
);
};

export default ContextNavigationFillEdit;
export default React.memo(ContextNavigationFillEdit, arePropsEqual);
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Renderer params={navProps} mode={props.mode} />;
};
}, arePropsEqual);

export default withBlockExtensions(ContextNavigationView);
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,8 +41,7 @@ function renderNode(node, parentLevel) {
return (
<li
key={node['@id']}
active={node.is_current}
className={`list-item level-${level}`}
className={`list-item level-${level} ${node.is_current ? 'active' : ''}`}
>
<MaybeWrap
condition={wrapWithDetails}
Expand Down Expand Up @@ -103,20 +94,17 @@ function renderNode(node, parentLevel) {
export function ReportNavigation(props) {
const { navigation = {} } = props;
const { items = [] } = navigation;
const intl = useIntl();

return items.length ? (
<nav className="context-navigation smart-toc">
{navigation.has_custom_name ? (
<nav className="context-navigation report-navigation">
{navigation.title ? (
<div className="context-navigation-header">
<RouterLink to={flattenToAppURL(navigation.url || '')}>
{navigation.title}
</RouterLink>
</div>
) : (
<div className="context-navigation-header">
{intl.formatMessage(messages.navigation)}
</div>
''
)}
<ul className="list">{items.map((node) => renderNode(node, 0))}</ul>
</nav>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
102 changes: 102 additions & 0 deletions src/customizations/volto/components/manage/Widgets/NumberWidget.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<FormFieldWrapper {...props}>
<Input
id={`field-${id}`}
name={id}
type="number"
disabled={isDisabled}
min={minimum || null}
max={maximum || null}
step={step}
value={value ?? ''}
placeholder={placeholder}
onChange={({ target }) =>
onChange(
id,
target.value === '' ? undefined : window.parseInt(target.value),
)
}
onBlur={({ target }) =>
onBlur(
id,
target.value === '' ? undefined : window.parseInt(target.value),
)
}
onClick={() => onClick()}
/>
</FormFieldWrapper>
);
};

/**
* 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);
Original file line number Diff line number Diff line change
@@ -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(
<Provider store={store}>
<NumberWidget
id="my-field"
title="My field"
fieldSet="default"
onChange={onChange}
/>
</Provider>,
);

expect(screen.getByRole('spinbutton')).toBeInTheDocument();
});

describe('onChange behavior', () => {
it('converts string value to number on change', () => {
const onChange = jest.fn();
render(
<Provider store={store}>
<NumberWidget id="my-field" title="My field" onChange={onChange} />
</Provider>,
);

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(
<Provider store={store}>
<NumberWidget
id="my-field"
value="1"
title="My field"
onChange={onChange}
/>
</Provider>,
);

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(
<Provider store={store}>
<NumberWidget
id="my-field"
title="My field"
onBlur={onBlur}
value="123"
/>
</Provider>,
);

const input = screen.getByRole('spinbutton');
fireEvent.blur(input);

expect(onBlur).toHaveBeenCalled();
});
});

describe('validation constraints', () => {
it('respects minimum and maximum values', () => {
render(
<Provider store={store}>
<NumberWidget
id="my-field"
title="My field"
minimum={1}
maximum={100}
/>
</Provider>,
);

const input = screen.getByRole('spinbutton');
expect(input).toHaveAttribute('min', '1');
expect(input).toHaveAttribute('max', '100');
});

it('handles step attribute', () => {
render(
<Provider store={store}>
<NumberWidget id="my-field" title="My field" step={0.5} />
</Provider>,
);

const input = screen.getByRole('spinbutton');
expect(input).toHaveAttribute('step', '0.5');
});
});
});
Loading

0 comments on commit 5e0c608

Please sign in to comment.