Skip to content

Commit

Permalink
Merge pull request #273 from eea/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
alecghica authored Nov 28, 2024
2 parents c4fc5f9 + 7e1c407 commit db75942
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 9 deletions.
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@ 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.2.0](https://github.com/eea/volto-eea-website-theme/compare/3.1.0...3.2.0) - 11 November 2024
### [3.3.0](https://github.com/eea/volto-eea-website-theme/compare/3.2.0...3.3.0) - 28 November 2024

#### :bug: Bug Fixes

- fix(tests): add unit tests for ReportNavigation [ana-oprea - [`55ac4c2`](https://github.com/eea/volto-eea-website-theme/commit/55ac4c2a1edf0c8abdb83a2c7e3c7d578464708a)]

#### :house: Internal changes

- chore: package.json [alin - [`4a8a4cb`](https://github.com/eea/volto-eea-website-theme/commit/4a8a4cb014db839b90eceed935c97b85785ddf71)]

#### :hammer_and_wrench: Others

- Update package.json [Ichim David - [`53be025`](https://github.com/eea/volto-eea-website-theme/commit/53be025c116dfc71a2de708075e4e77262eeecf8)]
### [3.2.0](https://github.com/eea/volto-eea-website-theme/compare/3.1.0...3.2.0) - 14 November 2024

#### :hammer_and_wrench: Others

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eeacms/volto-eea-website-theme",
"version": "3.2.0",
"version": "3.3.0",
"description": "@eeacms/volto-eea-website-theme: Volto add-on",
"main": "src/index.js",
"author": "European Environment Agency: IDM2 A-Team",
Expand All @@ -24,8 +24,8 @@
"url": "git@github.com:eea/volto-eea-website-theme.git"
},
"dependencies": {
"@eeacms/volto-block-toc": "*",
"@eeacms/volto-block-style": "*",
"@eeacms/volto-block-toc": "*",
"@eeacms/volto-eea-design-system": "*",
"@eeacms/volto-group-block": "*",
"volto-subsites": "*"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import PropTypes from 'prop-types';
import React from 'react';
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
* @param {boolean} wrapWithDetails - Whether the element is wrapped in details
*/
function handleSummaryClick(e, wrapWithDetails) {
if (wrapWithDetails) {
e.preventDefault();

const currentDetails = e.target.closest('details');
// toggle the current details
if (currentDetails) {
currentDetails.open = !currentDetails.open;
}
}
}

/**
* Renders a navigation node as a list item with proper styling and links
* @param {Object} node - Navigation node object containing title, href, type etc
* @param {number} parentLevel - Parent level in navigation hierarchy
* @returns {React.Component} UL component with navigation node content
*/
function renderNode(node, parentLevel) {
const level = parentLevel + 1;
const hasChildItems = node.items?.length;
const nodeType = node.type;
const isDocument = nodeType === 'document';
let wrapWithDetails = isDocument && level > 2;
return (
<li
key={node['@id']}
active={node.is_current}
className={`list-item level-${level}`}
>
<MaybeWrap
condition={wrapWithDetails}
as="details"
className="context-navigation-detail"
>
{nodeType !== 'link' ? (
<MaybeWrap
condition={wrapWithDetails}
as="summary"
className="context-navigation-summary"
>
<RouterLink
to={flattenToAppURL(node.href)}
tabIndex={wrapWithDetails ? '-1' : 0}
title={node.description}
className={cx(`list-link contenttype-${nodeType}`, {
in_path: node.is_in_path,
})}
onClick={(e) =>
wrapWithDetails && handleSummaryClick(e, wrapWithDetails)
}
>
{node.title}
{nodeType === 'file' && node.getObjSize
? ' [' + node.getObjSize + ']'
: ''}
</RouterLink>
</MaybeWrap>
) : (
<UniversalLink href={flattenToAppURL(node.href)}>
{node.title}
</UniversalLink>
)}
{(hasChildItems && (
<ul className="list">
{node.items.map((node) => renderNode(node, level))}
</ul>
)) ||
''}
</MaybeWrap>
</li>
);
}
/**
* A navigation slot implementation, similar to the classic Plone navigation
* portlet. It uses the same API, so the options are similar to
* INavigationPortlet
*/
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 ? (
<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>
) : (
''
);
}

ReportNavigation.propTypes = {
/**
* Navigation tree returned from @contextnavigation restapi endpoint
*/
navigation: PropTypes.shape({
items: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
}),
),
has_custom_name: PropTypes.bool,
title: PropTypes.string,
}),
};

export default compose(withRouter, withContentNavigation)(ReportNavigation);
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { render, fireEvent } from '@testing-library/react';
import { Provider } from 'react-intl-redux';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import ReportNavigation from './ReportNavigation';
import configureStore from 'redux-mock-store';
import '@testing-library/jest-dom/extend-expect';

const mockStore = configureStore();
const store = mockStore({
intl: {
locale: 'en',
messages: {},
},
});

jest.mock(
'@plone/volto/components/theme/Navigation/withContentNavigation',
() => ({
withContentNavigation: (Component) => (props) => (
<Component {...props} navigation={mockNavigation} />
),
}),
);

// Mock navigation data
const mockNavigation = {
items: [
{
'@id': '/item1',
title: 'Item 1',
href: '/item1',
type: 'document',
description: 'Item 1 description',
is_current: false,
is_in_path: false,
items: [
{
'@id': '/item1/subitem1',
title: 'Subitem 1',
href: '/item1/subitem1',
type: 'document',
is_current: false,
is_in_path: false,
items: [],
},
],
},
{
'@id': '/item2',
title: 'Item 2',
href: '/item2',
type: 'document',
description: 'Item 2 description',
is_current: true,
is_in_path: true,
items: [],
},
],
has_custom_name: true,
title: 'Custom Navigation',
url: '/custom-navigation',
};

describe('ReportNavigation', () => {
it('renders navigation items correctly', () => {
const history = createMemoryHistory();
const { getByText } = render(
<Provider store={store}>
<Router history={history}>
<ReportNavigation />
</Router>
</Provider>,
);

// Check if the navigation header is rendered
expect(getByText('Custom Navigation')).toBeInTheDocument();

// Check if the navigation items are rendered
expect(getByText('Item 1')).toBeInTheDocument();
expect(getByText('Item 2')).toBeInTheDocument();
expect(getByText('Subitem 1')).toBeInTheDocument();
});

it('toggles details on summary click', () => {
const history = createMemoryHistory();
const { container } = render(
<Provider store={store}>
<Router history={history}>
<ReportNavigation />
</Router>
</Provider>,
);

const detailsElement = container.querySelector('a[href="/item1"]');

// Simulate click on summary
fireEvent.click(detailsElement);
});

it('renders links with correct href attributes', () => {
const history = createMemoryHistory();
const { getByText } = render(
<Provider store={store}>
<Router history={history}>
<ReportNavigation />
</Router>
</Provider>,
);

expect(getByText('Item 1').closest('a')).toHaveAttribute('href', '/item1');
expect(getByText('Subitem 1').closest('a')).toHaveAttribute(
'href',
'/item1/subitem1',
);
});

it('applies active class to the current navigation item', () => {
const history = createMemoryHistory();
const { getByText } = render(
<Provider store={store}>
<Router history={history}>
<ReportNavigation />
</Router>
</Provider>,
);

const activeItem = getByText('Item 2');
expect(activeItem).toHaveClass('in_path');
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Accordion from './Accordion';
import Default from './Default';
import ReportNavigation from './ReportNavigation';

const contextBlockVariations = [
{
Expand All @@ -13,6 +14,11 @@ const contextBlockVariations = [
title: 'Accordion',
view: Accordion,
},
{
id: 'report_navigation',
title: 'Additional files',
view: ReportNavigation,
},
];

export default contextBlockVariations;
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Array [
</div>
<a
className="section"
href=""
href="/blog"
onClick={[Function]}
>
Blog
Expand Down Expand Up @@ -118,7 +118,7 @@ Array [
</div>
<a
className="section"
href=""
href="/"
onClick={[Function]}
>
Home
Expand All @@ -134,13 +134,12 @@ Array [
onClick={[Function]}
/>
</div>
<a
<div
className="section"
href=""
onClick={[Function]}
>
News
</a>
</div>
</li>
<li>
<div
Expand Down
Loading

0 comments on commit db75942

Please sign in to comment.