diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8825bfed..88c375fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,17 @@ 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).
-### [2.3.0](https://github.com/eea/volto-eea-website-theme/compare/2.2.2...2.3.0) - 9 September 2024
+### [2.4.0](https://github.com/eea/volto-eea-website-theme/compare/2.3.0...2.4.0) - 11 October 2024
+
+#### :bug: Bug Fixes
+
+- fix: add subsite_css class when not undefined [nileshgulia1 - [`db3f80f`](https://github.com/eea/volto-eea-website-theme/commit/db3f80f9dac34f528030d1d2a2f858555059879e)]
+
+#### :hammer_and_wrench: Others
+
+- Increase test coverage [dobri1408 - [`e2d46a9`](https://github.com/eea/volto-eea-website-theme/commit/e2d46a981c6f50980f0b0bf1f35b2d03121f3c88)]
+- Update package.json [Ichim David - [`24ea8f0`](https://github.com/eea/volto-eea-website-theme/commit/24ea8f0ef7c474bcf171f4720465e12a0d600b46)]
+### [2.3.0](https://github.com/eea/volto-eea-website-theme/compare/2.2.2...2.3.0) - 13 September 2024
#### :house: Internal changes
diff --git a/cypress/e2e/01-block-basics.cy.js b/cypress/e2e/01-block-basics.cy.js
index 6cf3240f..4a9ce0a7 100644
--- a/cypress/e2e/01-block-basics.cy.js
+++ b/cypress/e2e/01-block-basics.cy.js
@@ -25,7 +25,9 @@ describe('Blocks Tests', () => {
cy.get('.ui.basic.icon.button.block-add-button').first().click();
cy.get('.blocks-chooser .title').contains('Media').click();
cy.get('.content.active.media .button.image').contains('Image').click();
- cy.get('.block.image .ui.input input[type="text"]').type("https://eea.github.io/volto-eea-design-system/img/eea_icon.png{enter}");
+ cy.get('.block.image .ui.input input[type="text"]').type(
+ 'https://eea.github.io/volto-eea-design-system/img/eea_icon.png{enter}',
+ );
cy.get('.align-buttons .ui.basic.icon.button').first().click();
cy.get('#blockform-fieldset-styling').click();
@@ -36,11 +38,73 @@ describe('Blocks Tests', () => {
cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page');
// check banner rss link
- cy.get('.button.rssfeed').should('have.attr', 'href', '/cypress/my-page/rss');
+ cy.get('.button.rssfeed').should(
+ 'have.attr',
+ 'href',
+ '/cypress/my-page/rss',
+ );
cy.get('.button.rssfeed').contains('RSS');
// then the page view should contain our changes
cy.contains('My Add-on Page');
- cy.get('.block.image.align.left img.top').should('have.attr', 'src', 'https://eea.github.io/volto-eea-design-system/img/eea_icon.png');
+ cy.get('.block.image.align.left img.top').should(
+ 'have.attr',
+ 'src',
+ 'https://eea.github.io/volto-eea-design-system/img/eea_icon.png',
+ );
+ });
+
+ //Fails because we don't add navigation block by default
+
+ it('Add Navigation Block', () => {
+ // Change page title
+ cy.clearSlateTitle();
+ cy.getSlateTitle().type('My Add-on Page');
+
+ cy.get('.documentFirstHeading').contains('My Add-on Page');
+
+ cy.getSlate().click();
+
+ // Add Navigation block
+ cy.get('.ui.basic.icon.button.block-add-button').first().click();
+ cy.get('.blocks-chooser input').type('Navigation');
+ cy.get('.blocks-chooser .contextNavigation').click();
+ cy.get('#field-name').type('Nav title');
+ cy.get(
+ '.field-wrapper-includeTop > .grid > :nth-child(1) > .twelve > .wrapper > .ui > label',
+ ).click();
+ // cy.get(
+ // '.field-wrapper-currentFolderOnly > .grid > :nth-child(1) > .twelve > .wrapper > .ui > label',
+ // ).click();
+ cy.get(
+ '.field-wrapper-no_icons > .grid > :nth-child(1) > .twelve > .wrapper > .ui > label',
+ ).click();
+ cy.get(
+ '.field-wrapper-no_thumbs > .grid > :nth-child(1) > .twelve > .wrapper > .ui > label',
+ ).click();
+
+ // Save
+ cy.get('#toolbar-save').click();
+ cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page');
+
+ // // then the page view should contain our changes
+ cy.get('.context-navigation-header').contains('Nav title');
+
+ // Edit to select Accordion variation
+ cy.get('.toolbar-actions .edit').click();
+ cy.get('.block-editor-contextNavigation').click();
+ cy.get(
+ '#sidebar-properties .field-wrapper-variation .react-select__value-container',
+ ).click();
+ cy.get('.field-wrapper-variation .react-select__option')
+ .contains('Accordion')
+ .click();
+
+ cy.get('#toolbar-save').click();
+ cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page');
+
+ // then the page view should contain our changes
+ cy.get('.accordion-header').contains('Nav title');
+ cy.get('.accordion-header').click();
});
});
diff --git a/package.json b/package.json
index 7fb66570..5ebd1220 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@eeacms/volto-eea-website-theme",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "@eeacms/volto-eea-website-theme: Volto add-on",
"main": "src/index.js",
"author": "European Environment Agency: IDM2 A-Team",
@@ -14,6 +14,7 @@
"react"
],
"addons": [
+ "@eeacms/volto-block-toc",
"@eeacms/volto-group-block",
"@eeacms/volto-eea-design-system",
"volto-subsites"
@@ -23,6 +24,7 @@
"url": "git@github.com:eea/volto-eea-website-theme.git"
},
"dependencies": {
+ "@eeacms/volto-block-toc": "*",
"@eeacms/volto-block-style": "*",
"@eeacms/volto-eea-design-system": "*",
"@eeacms/volto-group-block": "*",
diff --git a/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx b/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx
new file mode 100644
index 00000000..95795d95
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { EditSchema } from './schema';
+import { SidebarPortal } from '@plone/volto/components';
+import BlockDataForm from '@plone/volto/components/manage/Form/BlockDataForm';
+
+import ContextNavigationView from './ContextNavigationView';
+
+const ContextNavigationFillEdit = (props) => {
+ const contentTypes = props.properties?.['@components']?.types;
+ const availableTypes = React.useMemo(
+ () => contentTypes?.map((type) => [type.id, type.title || type.name]),
+ [contentTypes],
+ );
+
+ const schema = React.useMemo(
+ () => EditSchema({ availableTypes }),
+ [availableTypes],
+ );
+
+ return (
+ <>
+
Context navigation
+ {' '}
+
+ {
+ props.onChangeBlock(props.block, {
+ ...props.data,
+ [id]: value,
+ });
+ }}
+ onChangeBlock={props.onChangeBlock}
+ formData={props.data}
+ block={props.block}
+ navRoot={props.navRoot}
+ contentType={props.contentType}
+ />
+
+ >
+ );
+};
+
+export default ContextNavigationFillEdit;
diff --git a/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.test.jsx b/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.test.jsx
new file mode 100644
index 00000000..b184f87e
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/ContextNavigationEdit.test.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import ContextNavigationEdit from './ContextNavigationEdit';
+import { Router } from 'react-router-dom';
+import { Provider } from 'react-intl-redux';
+import configureStore from 'redux-mock-store';
+import { createMemoryHistory } from 'history';
+import '@testing-library/jest-dom/extend-expect';
+
+jest.mock('@plone/volto/components', () => ({
+ InlineForm: ({ onChangeField }) => (
+
+ ),
+ SidebarPortal: ({ children, selected }) =>
+ selected ? (
+
+
SidebarPortal
+ {children}
+
+ ) : null,
+}));
+
+jest.mock('@plone/volto/components/theme/Navigation/ContextNavigation', () => {
+ return {
+ __esModule: true,
+ default: ({ params }) => {
+ return ConnectedContextNavigation {params.root_path}
;
+ },
+ };
+});
+
+jest.mock('@plone/volto/helpers', () => ({
+ withBlockExtensions: jest.fn((Component) => Component),
+ emptyBlocksForm: jest.fn(),
+ getBlocksLayoutFieldname: () => 'blocks_layout',
+ withVariationSchemaEnhancer: jest.fn((Component) => Component),
+}));
+
+const mockStore = configureStore();
+const store = mockStore({
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+});
+
+describe('ContextNavigationEdit', () => {
+ it('renders corectly', () => {
+ const history = createMemoryHistory();
+ const { getByText, queryByText } = render(
+
+
+
+
+ ,
+ ,
+ );
+
+ expect(getByText('Context navigation')).toBeInTheDocument();
+ expect(getByText('ConnectedContextNavigation')).toBeInTheDocument();
+ expect(queryByText('InlineForm')).toBeNull();
+ expect(queryByText('SidebarPortal')).toBeNull();
+ });
+
+ it('renders corectly', () => {
+ const history = createMemoryHistory();
+ const { container, getByText } = render(
+
+
+ {}} />
+
+ ,
+ ,
+ );
+
+ expect(getByText('Context navigation')).toBeInTheDocument();
+ expect(getByText('ConnectedContextNavigation')).toBeInTheDocument();
+ expect(getByText('InlineForm')).toBeInTheDocument();
+ expect(getByText('SidebarPortal')).toBeInTheDocument();
+
+ fireEvent.change(container.querySelector('#test'), {
+ target: { value: 'test' },
+ });
+ });
+});
diff --git a/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx b/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx
new file mode 100644
index 00000000..ebaea7ca
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { flattenToAppURL, withBlockExtensions } from '@plone/volto/helpers';
+import DefaultTemplate from './variations/Default';
+
+const ContextNavigationView = (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 Renderer = variation?.view ?? DefaultTemplate;
+ return ;
+};
+
+export default withBlockExtensions(ContextNavigationView);
diff --git a/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.test.jsx b/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.test.jsx
new file mode 100644
index 00000000..ed0e8b80
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/ContextNavigationView.test.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import ContextNavigationView from './ContextNavigationView';
+import { Router } from 'react-router-dom';
+import { Provider } from 'react-intl-redux';
+import configureStore from 'redux-mock-store';
+import { createMemoryHistory } from 'history';
+import '@testing-library/jest-dom/extend-expect';
+
+jest.mock('@plone/volto/components/theme/Navigation/ContextNavigation', () => {
+ return {
+ __esModule: true,
+ default: ({ params }) => {
+ return ConnectedContextNavigation {params.root_path}
;
+ },
+ };
+});
+
+jest.mock('@plone/volto/helpers', () => ({
+ withBlockExtensions: jest.fn((Component) => Component),
+ emptyBlocksForm: jest.fn(),
+ getBlocksLayoutFieldname: () => 'blocks_layout',
+ flattenToAppURL: () => '',
+}));
+
+const mockStore = configureStore();
+const store = mockStore({
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+});
+
+describe('ContextNavigationView', () => {
+ let history;
+ beforeEach(() => {
+ history = createMemoryHistory();
+ });
+
+ it('renders corectly', () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+
+ expect(container.firstChild).toHaveTextContent(
+ 'ConnectedContextNavigation',
+ );
+ });
+
+ it('renders corectly', () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+ expect(container.firstChild).toHaveTextContent(
+ 'ConnectedContextNavigation',
+ );
+ });
+});
diff --git a/src/components/manage/Blocks/ContextNavigation/index.js b/src/components/manage/Blocks/ContextNavigation/index.js
new file mode 100644
index 00000000..b2634603
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/index.js
@@ -0,0 +1,30 @@
+import codeSVG from '@plone/volto/icons/code.svg';
+import ContextNavigationEdit from './ContextNavigationEdit';
+import ContextNavigationView from './ContextNavigationView';
+import BlockSettingsSchema from '@plone/volto/components/manage/Blocks/Block/Schema';
+import variations from './variations';
+
+const applyConfig = (config) => {
+ config.blocks.blocksConfig.contextNavigation = {
+ id: 'contextNavigation',
+ title: 'Navigation',
+ icon: codeSVG,
+ group: 'common',
+ view: ContextNavigationView,
+ edit: ContextNavigationEdit,
+ schema: BlockSettingsSchema,
+ restricted: false,
+ variations,
+ mostUsed: false,
+ blockHasOwnFocusManagement: true,
+ sidebarTab: 1,
+ security: {
+ addPermission: [],
+ view: [],
+ },
+ };
+
+ return config;
+};
+
+export default applyConfig;
diff --git a/src/components/manage/Blocks/ContextNavigation/schema.js b/src/components/manage/Blocks/ContextNavigation/schema.js
new file mode 100644
index 00000000..6e67b930
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/schema.js
@@ -0,0 +1,88 @@
+export const EditSchema = ({ availableTypes }) => {
+ return {
+ title: 'Navigation',
+ fieldsets: [
+ {
+ id: 'default',
+ title: 'Default',
+ fields: [
+ 'name',
+ 'root_node',
+ 'portal_type',
+ 'includeTop',
+ 'currentFolderOnly',
+ 'topLevel',
+ 'bottomLevel',
+ 'no_icons',
+ 'thumb_scale',
+ 'no_thumbs',
+ ],
+ },
+ ],
+ required: [],
+ properties: {
+ name: {
+ title: 'Title',
+ description: 'The title of the navigation tree',
+ },
+ root_node: {
+ title: 'Root node',
+ description:
+ 'You may search for and choose a folder to act as the root of the navigation tree. Leave blank to use the Plone site root.',
+ widget: 'object_browser',
+ // TODO: these don't work. Why?
+ mode: 'link',
+ selectedItemAttrs: ['Title', 'Description'],
+ },
+ portal_type: {
+ title: 'Filter children',
+ description: 'Only show child items of this content type',
+ choices: availableTypes,
+ isMulti: true,
+ },
+ includeTop: {
+ title: 'Include top node',
+ description:
+ "Whether or not to show the top, or 'root', node in the navigation tree. This is affected by the 'Start level' setting.",
+ type: 'boolean',
+ },
+ currentFolderOnly: {
+ title: 'Only show the contents of the current folder',
+ description:
+ 'If selected, the navigation tree will only show the current folder and its children at all times.',
+ type: 'boolean',
+ },
+
+ topLevel: {
+ title: 'Start level',
+ description:
+ 'An integer value that specifies the number of folder levels below the site root that must be exceeded before the navigation tree will display. 0 means that the navigation tree should be displayed everywhere including pages in the root of the site. 1 means the tree only shows up inside folders located in the root and downwards, never showing at the top level.',
+ type: 'number',
+ default: 1,
+ },
+ bottomLevel: {
+ title: 'Navigation tree depth',
+ description:
+ 'How many folders should be included before the navigation tree stops. 0 means no limit. 1 only includes the root folder.',
+ type: 'number',
+ default: 0,
+ },
+ no_icons: {
+ title: 'Suppress icons',
+ description:
+ 'If enabled, the portlet will not show document type icons.',
+ type: 'boolean',
+ },
+ thumb_scale: {
+ title: 'Override thumb scale',
+ description:
+ "Enter a valid scale name (see 'Image Handling' control panel) to override (e.g. icon, tile, thumb, mini, preview, ... ). Leave empty to use default (see 'Site' control panel).",
+ },
+ no_thumbs: {
+ title: 'Suppress thumbs',
+ type: 'boolean',
+ description: 'If enabled, the portlet will not show thumbs.',
+ },
+ },
+ };
+};
diff --git a/src/components/manage/Blocks/ContextNavigation/variations/Accordion.jsx b/src/components/manage/Blocks/ContextNavigation/variations/Accordion.jsx
new file mode 100644
index 00000000..82eaeac0
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/variations/Accordion.jsx
@@ -0,0 +1,179 @@
+import cx from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { withRouter } from 'react-router';
+import { compose } from 'redux';
+import { Accordion } from 'semantic-ui-react';
+
+import Slugger from 'github-slugger';
+
+import { Icon, UniversalLink } from '@plone/volto/components';
+import { withContentNavigation } from '@plone/volto/components/theme/Navigation/withContentNavigation';
+import withEEASideMenu from '@eeacms/volto-block-toc/hocs/withEEASideMenu';
+import { flattenToAppURL } from '@plone/volto/helpers';
+
+import downIcon from '@plone/volto/icons/down-key.svg';
+import upIcon from '@plone/volto/icons/up-key.svg';
+
+const messages = defineMessages({
+ navigation: {
+ id: 'Navigation',
+ defaultMessage: 'Navigation',
+ },
+});
+
+const AccordionNavigation = ({
+ navigation = {},
+ device,
+ isMenuOpenOnOutsideClick,
+}) => {
+ const { items = [], title, has_custom_name } = navigation;
+ const intl = useIntl();
+ const navOpen = ['mobile', 'tablet'].includes(device) ? false : true;
+ const [isNavOpen, setIsNavOpen] = React.useState(navOpen);
+ const [activeItems, setActiveItems] = React.useState({});
+
+ const onClickSummary = React.useCallback((e) => {
+ e.preventDefault();
+ setIsNavOpen((prev) => !prev);
+ }, []);
+
+ React.useEffect(() => {
+ if (isMenuOpenOnOutsideClick === false) setIsNavOpen(false);
+ }, [isMenuOpenOnOutsideClick]);
+
+ const onKeyDownSummary = React.useCallback(
+ (e) => {
+ if (e.keyCode === 13 || e.keyCode === 32) {
+ e.preventDefault();
+ onClickSummary(e);
+ }
+ },
+ [onClickSummary],
+ );
+
+ const renderItems = ({ item, level = 0 }) => {
+ const {
+ title,
+ href,
+ is_current,
+ is_in_path,
+ items: childItems,
+ type,
+ } = item;
+ const hasChildItems = childItems && childItems.length > 0;
+ const normalizedTitle = Slugger.slug(title);
+
+ const checkIfActive = () => {
+ return activeItems[href] !== undefined ? activeItems[href] : is_in_path;
+ };
+
+ const isActive = checkIfActive();
+
+ const handleTitleClick = () => {
+ setActiveItems((prev) => ({ ...prev, [href]: !isActive }));
+ };
+
+ return (
+
+ {hasChildItems ? (
+
+
+ {title}
+
+
+
+
+ {childItems.map((child) =>
+ renderItems({ item: child, level: level + 1 }),
+ )}
+
+
+
+ ) : (
+
+ {title}
+
+ )}
+
+ );
+ };
+
+ return items.length ? (
+ <>
+
+ >
+ ) : null;
+};
+
+AccordionNavigation.propTypes = {
+ /**
+ * Navigation tree returned from @contextnavigation restapi endpoint
+ */
+ navigation: PropTypes.shape({
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ href: PropTypes.string,
+ is_current: PropTypes.bool,
+ is_in_path: PropTypes.bool,
+ items: PropTypes.array,
+ type: PropTypes.string,
+ }),
+ ),
+ has_custom_name: PropTypes.bool,
+ title: PropTypes.string,
+ }),
+};
+
+export default compose(
+ withRouter,
+ withContentNavigation,
+ (WrappedComponent) => (props) =>
+ withEEASideMenu(WrappedComponent)({
+ ...props,
+ shouldRender: props.navigation?.items?.length > 0,
+ }),
+)(AccordionNavigation);
diff --git a/src/components/manage/Blocks/ContextNavigation/variations/Default.jsx b/src/components/manage/Blocks/ContextNavigation/variations/Default.jsx
new file mode 100644
index 00000000..c5420bf6
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/variations/Default.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ConnectedContextNavigation from '@plone/volto/components/theme/Navigation/ContextNavigation';
+
+const Default = (props) => {
+ const { params } = props;
+ return ;
+};
+
+export default Default;
diff --git a/src/components/manage/Blocks/ContextNavigation/variations/index.js b/src/components/manage/Blocks/ContextNavigation/variations/index.js
new file mode 100644
index 00000000..f85f69f6
--- /dev/null
+++ b/src/components/manage/Blocks/ContextNavigation/variations/index.js
@@ -0,0 +1,18 @@
+import Accordion from './Accordion';
+import Default from './Default';
+
+const contextBlockVariations = [
+ {
+ id: 'default',
+ title: 'Listing (default)',
+ view: Default,
+ isDefault: true,
+ },
+ {
+ id: 'accordion',
+ title: 'Accordion',
+ view: Accordion,
+ },
+];
+
+export default contextBlockVariations;
diff --git a/src/components/manage/Blocks/Title/Edit.jsx b/src/components/manage/Blocks/Title/Edit.jsx
index ba31ca2f..1c49fc1f 100644
--- a/src/components/manage/Blocks/Title/Edit.jsx
+++ b/src/components/manage/Blocks/Title/Edit.jsx
@@ -11,8 +11,8 @@ import { ReactEditor, Editable, Slate, withReact } from 'slate-react';
import config from '@plone/volto/registry';
import { SidebarPortal } from '@plone/volto/components';
import { BodyClass } from '@plone/volto/helpers';
-import InlineForm from '@plone/volto/components/manage/Form/InlineForm';
-import BannerView from '@eeacms/volto-eea-website-theme/components/theme/Banner/View';
+import View from '@eeacms/volto-eea-website-theme/components/manage/Blocks/Title/View';
+import BlockDataForm from '@plone/volto/components/manage/Form/BlockDataForm';
import schema from './schema';
const messages = defineMessages({
@@ -165,8 +165,9 @@ export const TitleBlockEdit = (props) => {
return (
- {
fluid
/>
- {
@@ -194,7 +195,9 @@ export const TitleBlockEdit = (props) => {
[id]: value,
});
}}
+ onChangeBlock={props.onChangeBlock}
formData={props.data}
+ block={props.block}
/>
diff --git a/src/components/manage/Blocks/Title/View.jsx b/src/components/manage/Blocks/Title/View.jsx
index ca89833f..89a10706 100644
--- a/src/components/manage/Blocks/Title/View.jsx
+++ b/src/components/manage/Blocks/Title/View.jsx
@@ -4,36 +4,26 @@
*/
import React from 'react';
-import { Portal } from 'react-portal';
-import PropTypes from 'prop-types';
-import { BodyClass } from '@plone/volto/helpers';
-
-import BannerView from '@eeacms/volto-eea-website-theme/components/theme/Banner/View';
-function IsomorphicPortal({ children }) {
- const [isClient, setIsClient] = React.useState();
- React.useEffect(() => setIsClient(true), []);
-
- return isClient ? (
- {children}
- ) : (
- children
- );
-}
+import PropTypes from 'prop-types';
+import { withBlockExtensions, BodyClass } from '@plone/volto/helpers';
+import DefaultTemplate from './variations/Default';
/**
* View title block class.
* @class View
* @extends Component
*/
-const View = (props) => (
-
-
-
-
-
-
-);
+const View = (props = {}) => {
+ const { variation } = props;
+ const Renderer = variation?.view ?? DefaultTemplate;
+ return (
+ <>
+
+
+ >
+ );
+};
/**
* Property types.
@@ -44,4 +34,4 @@ View.propTypes = {
properties: PropTypes.objectOf(PropTypes.any).isRequired,
};
-export default View;
+export default withBlockExtensions(View);
diff --git a/src/components/manage/Blocks/Title/index.js b/src/components/manage/Blocks/Title/index.js
index 614172d6..5af73d27 100644
--- a/src/components/manage/Blocks/Title/index.js
+++ b/src/components/manage/Blocks/Title/index.js
@@ -1,11 +1,63 @@
import Edit from './Edit';
import View from './View';
+import DefaultTemplate from './variations/Default';
+import WebReport from './variations/WebReport';
+import WebReportPage from './variations/WebReportPage';
+import './variations/styles.less';
const applyConfig = (config) => {
config.blocks.blocksConfig.title = {
...config.blocks.blocksConfig.title,
edit: Edit,
view: View,
+ variations: [
+ {
+ id: 'default',
+ title: 'Default',
+ view: DefaultTemplate,
+ isDefault: true,
+ },
+ {
+ id: 'web_report',
+ title: 'Web Report',
+ view: WebReport,
+ schemaEnhancer: ({ schema }) => {
+ const fields = schema.fieldsets[0].fields;
+ schema.fieldsets[0].fields = [
+ ...fields,
+ 'content_type',
+ 'hero_header',
+ ];
+
+ schema.properties.content_type = {
+ title: 'Content type name',
+ description:
+ 'Add a custom content-type name, leave empty for default',
+ };
+ schema.properties.hero_header = {
+ title: 'Hero header size',
+ type: 'boolean',
+ };
+ return schema;
+ },
+ },
+ {
+ id: 'web_report_page',
+ title: 'Web Report Page',
+ view: WebReportPage,
+ schemaEnhancer: ({ schema }) => {
+ const fields = schema.fieldsets[0].fields;
+ schema.fieldsets[0].fields = [...fields, 'content_type'];
+
+ schema.properties.content_type = {
+ title: 'Content type name',
+ description:
+ 'Add a custom content-type name, leave empty for default',
+ };
+ return schema;
+ },
+ },
+ ],
copyrightPrefix: 'Image',
sidebarTab: 1,
};
diff --git a/src/components/manage/Blocks/Title/variations/Default.jsx b/src/components/manage/Blocks/Title/variations/Default.jsx
new file mode 100644
index 00000000..038201ee
--- /dev/null
+++ b/src/components/manage/Blocks/Title/variations/Default.jsx
@@ -0,0 +1,43 @@
+/**
+ * View title block.
+ * @module components/manage/Blocks/Title/View
+ */
+
+import React from 'react';
+import { Portal } from 'react-portal';
+import PropTypes from 'prop-types';
+
+import BannerView from '@eeacms/volto-eea-website-theme/components/theme/Banner/View';
+
+function IsomorphicPortal({ children }) {
+ const [isClient, setIsClient] = React.useState();
+ React.useEffect(() => setIsClient(true), []);
+
+ return isClient ? (
+ {children}
+ ) : (
+ children
+ );
+}
+
+const DefaultTemplate = (props) =>
+ props.isEditMode ? (
+
+ ) : (
+
+
+
+
+
+ );
+
+/**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+DefaultTemplate.propTypes = {
+ properties: PropTypes.objectOf(PropTypes.any).isRequired,
+};
+
+export default DefaultTemplate;
diff --git a/src/components/manage/Blocks/Title/variations/WebReport.jsx b/src/components/manage/Blocks/Title/variations/WebReport.jsx
new file mode 100644
index 00000000..fc408f22
--- /dev/null
+++ b/src/components/manage/Blocks/Title/variations/WebReport.jsx
@@ -0,0 +1,69 @@
+/**
+ * Web Report title block variation.
+ * @module components/manage/Blocks/Title/variations/WebReport
+ */
+
+import React from 'react';
+import { Portal } from 'react-portal';
+import PropTypes from 'prop-types';
+
+import { MaybeWrap } from '@plone/volto/components';
+import BannerView from '@eeacms/volto-eea-website-theme/components/theme/Banner/View';
+import Banner from '@eeacms/volto-eea-design-system/ui/Banner/Banner';
+import clsx from 'clsx';
+
+import { BodyClass } from '@plone/volto/helpers';
+
+function IsomorphicPortal({ children }) {
+ const [isClient, setIsClient] = React.useState();
+ React.useEffect(() => setIsClient(true), []);
+
+ return isClient ? (
+ {children}
+ ) : (
+ children
+ );
+}
+
+const WebReport = (props) => {
+ return (
+
+
+
+ {props.data.content_type || props.properties.type_title}
+
+ ),
+ belowTitle: (
+ <>
+
+ {props.data.subtitle}
+
+ >
+ ),
+ }}
+ />
+
+ );
+};
+
+/**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+WebReport.propTypes = {
+ properties: PropTypes.objectOf(PropTypes.any).isRequired,
+};
+
+export default WebReport;
diff --git a/src/components/manage/Blocks/Title/variations/WebReportPage.jsx b/src/components/manage/Blocks/Title/variations/WebReportPage.jsx
new file mode 100644
index 00000000..da539e78
--- /dev/null
+++ b/src/components/manage/Blocks/Title/variations/WebReportPage.jsx
@@ -0,0 +1,59 @@
+/**
+ * Web Report Page title block variation.
+ * @module components/manage/Title/variations/WebReport
+ */
+
+import React from 'react';
+import { Portal } from 'react-portal';
+import PropTypes from 'prop-types';
+
+import { MaybeWrap } from '@plone/volto/components';
+import BannerView from '@eeacms/volto-eea-website-theme/components/theme/Banner/View';
+import clsx from 'clsx';
+
+import { BodyClass } from '@plone/volto/helpers';
+
+function IsomorphicPortal({ children }) {
+ const [isClient, setIsClient] = React.useState();
+ React.useEffect(() => setIsClient(true), []);
+
+ return isClient ? (
+ {children}
+ ) : (
+ children
+ );
+}
+
+const WebReportPage = (props) => {
+ return (
+
+
+
+
+ {props.data.content_type || props.properties.type_title}
+
+ {props.data.subtitle}
+ >
+ ),
+ }}
+ />
+
+ );
+};
+
+/**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+WebReportPage.propTypes = {
+ properties: PropTypes.objectOf(PropTypes.any).isRequired,
+};
+
+export default WebReportPage;
diff --git a/src/components/manage/Blocks/Title/variations/styles.less b/src/components/manage/Blocks/Title/variations/styles.less
new file mode 100644
index 00000000..7fd9ad1f
--- /dev/null
+++ b/src/components/manage/Blocks/Title/variations/styles.less
@@ -0,0 +1,28 @@
+.view-viewview.light-header .main.bar {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ margin-bottom: -160px;
+}
+//Gradient styles for web report
+.light-header .gradient {
+ background: linear-gradient(
+ 0deg,
+ #ffffff,
+ rgba(255, 255, 255, 0.9) 30%,
+ rgba(46, 82, 114, 0.7) 70%,
+ rgba(14, 21, 26, 0.8) 100%
+ ) !important;
+}
+
+.ui.block.title .eea.banner .content {
+ padding-right: 1rem;
+ padding-left: 1rem;
+}
+
+.share-popup {
+ .actions {
+ display: flex;
+ flex-flow: row;
+ }
+}
diff --git a/src/components/theme/Banner/View.jsx b/src/components/theme/Banner/View.jsx
index e721fc93..78423afe 100644
--- a/src/components/theme/Banner/View.jsx
+++ b/src/components/theme/Banner/View.jsx
@@ -291,8 +291,12 @@ const View = (props) => {
>
}
>
- {subtitle && {subtitle}}
+ {!props.data.aboveTitle && subtitle && (
+ {subtitle}
+ )}
+ {props.data.aboveTitle}
+ {props.data.belowTitle}
{
return (
);
diff --git a/src/components/theme/WebReport/WebReportSectionView.jsx b/src/components/theme/WebReport/WebReportSectionView.jsx
new file mode 100644
index 00000000..523e772c
--- /dev/null
+++ b/src/components/theme/WebReport/WebReportSectionView.jsx
@@ -0,0 +1,49 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { useHistory } from 'react-router-dom';
+import { isInternalURL, flattenToAppURL } from '@plone/volto/helpers';
+import { DefaultView } from '@plone/volto/components/';
+import { Redirect } from 'react-router-dom';
+
+const WebReportSectionView = (props) => {
+ const { content, token } = props;
+ const history = useHistory();
+ const redirectUrl = React.useMemo(() => {
+ if (content) {
+ const items = content.items;
+ const firstItem = items?.[0];
+ return firstItem?.['@id'];
+ }
+ }, [content]);
+
+ useEffect(() => {
+ if (!token) {
+ if (isInternalURL(redirectUrl)) {
+ history.replace(flattenToAppURL(redirectUrl));
+ } else if (!__SERVER__ && redirectUrl) {
+ window.location.href = flattenToAppURL(redirectUrl);
+ }
+ }
+ }, [history, content, redirectUrl, token]);
+
+ if (__SERVER__ && redirectUrl && !token) {
+ return ;
+ }
+ return ;
+};
+
+WebReportSectionView.propTypes = {
+ content: PropTypes.shape({
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ '@id': PropTypes.string,
+ }),
+ ),
+ }),
+};
+
+WebReportSectionView.defaultProps = {
+ content: null,
+};
+
+export default WebReportSectionView;
diff --git a/src/customizations/volto/components/theme/Breadcrumbs/Breadcrumbs.jsx b/src/customizations/volto/components/theme/Breadcrumbs/Breadcrumbs.jsx
index 1fbb1f42..97e7e41a 100644
--- a/src/customizations/volto/components/theme/Breadcrumbs/Breadcrumbs.jsx
+++ b/src/customizations/volto/components/theme/Breadcrumbs/Breadcrumbs.jsx
@@ -3,7 +3,7 @@
* @module components/theme/Breadcrumbs/Breadcrumbs
*/
-import React, { useEffect } from 'react';
+import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router';
@@ -32,10 +32,23 @@ const isContentRoute = (pathname) => {
const Breadcrumbs = (props) => {
const dispatch = useDispatch();
const { items = [], root = '/' } = useSelector((state) => state?.breadcrumbs);
+ const content = useSelector((state) => state?.content?.data);
+
// const pathname = useSelector((state) => state.location.pathname);
const location = useLocation();
const { pathname } = location;
+ const linkLevels = useMemo(() => {
+ if (content) {
+ const type = content['@type'];
+ const isContentTypesToAvoid =
+ config.settings.contentTypeToAvoidAsLinks || {};
+ if (isContentTypesToAvoid[type]) {
+ return isContentTypesToAvoid[type];
+ }
+ }
+ }, [content]);
+
const sections = items.map((item) => ({
title: item.title,
href: item.url,
@@ -54,7 +67,12 @@ const Breadcrumbs = (props) => {
return (
-
+
);
};
diff --git a/src/customizations/volto/components/theme/Header/Header.jsx b/src/customizations/volto/components/theme/Header/Header.jsx
index 3b9d4c44..1c224d9a 100644
--- a/src/customizations/volto/components/theme/Header/Header.jsx
+++ b/src/customizations/volto/components/theme/Header/Header.jsx
@@ -17,7 +17,6 @@ import eeaFlag from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/
import config from '@plone/volto/registry';
import { compose } from 'recompose';
-import { BodyClass } from '@plone/volto/helpers';
import cx from 'classnames';
import loadable from '@loadable/component';
@@ -43,10 +42,12 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
const has_home_layout =
layout === 'homepage_inverse_view' ||
(__CLIENT__ && document.body.classList.contains('homepage-inverse'));
+
return (
has_home_layout &&
(removeTrailingSlash(pathname) === router_pathname ||
- router_pathname.endsWith('/edit'))
+ router_pathname.endsWith('/edit') ||
+ router_pathname.endsWith('/add'))
);
});
@@ -75,7 +76,6 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
return (
- {isHomePageInverse && }
diff --git a/src/customizations/volto/components/theme/View/DefaultView.jsx b/src/customizations/volto/components/theme/View/DefaultView.jsx
new file mode 100644
index 00000000..be84e1b3
--- /dev/null
+++ b/src/customizations/volto/components/theme/View/DefaultView.jsx
@@ -0,0 +1,190 @@
+/**
+ * Document view component.
+ * @module components/theme/View/DefaultView
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ Container as SemanticContainer,
+ Segment,
+ Grid,
+ Label,
+} from 'semantic-ui-react';
+import config from '@plone/volto/registry';
+import { getSchema } from '@plone/volto/actions';
+import { getWidget } from '@plone/volto/helpers/Widget/utils';
+import { RenderBlocks } from '@plone/volto/components';
+
+import { hasBlocksData, getBaseUrl } from '@plone/volto/helpers';
+import { useDispatch, shallowEqual, useSelector } from 'react-redux';
+
+import isEqual from 'lodash/isEqual';
+import AccordionContextNavigation from '@eeacms/volto-eea-website-theme/components/manage/Blocks/ContextNavigation/variations/Accordion';
+
+/**
+ * Component to display the default view.
+ * @function DefaultView
+ * @param {Object} content Content object.
+ * @returns {string} Markup of the component.
+ */
+const DefaultView = (props) => {
+ const { content, location } = props;
+ const [hasLightLayout, setHasLightLayout] = React.useState(false);
+
+ React.useEffect(() => {
+ const updateLightLayout = () => {
+ if (__CLIENT__) {
+ setHasLightLayout(document.body.classList.contains('light-header'));
+ }
+ };
+
+ updateLightLayout();
+
+ if (__CLIENT__) {
+ const observer = new MutationObserver(updateLightLayout);
+ observer.observe(document.body, {
+ attributes: true,
+ attributeFilter: ['class'],
+ });
+
+ return () => observer.disconnect();
+ }
+ }, []);
+
+ const { contextNavigationActions } = useSelector(
+ (state) => ({
+ contextNavigationActions: state.actions?.actions?.context_navigation,
+ }),
+ shallowEqual,
+ );
+
+ const navigation_paths = contextNavigationActions || [];
+ const path = getBaseUrl(location?.pathname || '');
+ const dispatch = useDispatch();
+ const { views } = config.widgets;
+ const contentSchema = useSelector((state) => state.schema?.schema);
+ const fieldsetsToExclude = [
+ 'categorization',
+ 'dates',
+ 'ownership',
+ 'settings',
+ ];
+ const fieldsets = contentSchema?.fieldsets.filter(
+ (fs) => !fieldsetsToExclude.includes(fs.id),
+ );
+
+ // TL;DR: There is a flash of the non block-based view because of the reset
+ // of the content on route change. Subscribing to the content change at this
+ // level has nasty implications, so we can't watch the Redux state for loaded
+ // content flag here (because it forces an additional component update)
+ // Instead, we can watch if the content is "empty", but this has a drawback
+ // since the locking mechanism inserts a `lock` key before the content is there.
+ // So "empty" means `content` is present, but only with a `lock` key, thus the next
+ // ugly condition comes to life
+ const contentLoaded = content && !isEqual(Object.keys(content), ['lock']);
+
+ React.useEffect(() => {
+ content?.['@type'] &&
+ !hasBlocksData(content) &&
+ dispatch(getSchema(content['@type'], location.pathname));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const Container =
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
+ const matchingNavigationPath = navigation_paths.find((navPath) =>
+ path.includes(navPath.url),
+ );
+
+ // If the content is not yet loaded, then do not show anything
+ return contentLoaded ? (
+ hasBlocksData(content) ? (
+ <>
+
+
+
+ {hasLightLayout && matchingNavigationPath && (
+
+ )}
+ >
+ ) : (
+
+ {fieldsets?.map((fs) => {
+ return (
+
+ {fs.id !== 'default' &&
{fs.title}
}
+ {fs.fields?.map((f, key) => {
+ let field = {
+ ...contentSchema?.properties[f],
+ id: f,
+ widget: getWidget(f, contentSchema?.properties[f]),
+ };
+ let Widget = views?.getWidget(field);
+ return f !== 'title' ? (
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ );
+ })}
+
+ );
+ })}
+
+ )
+ ) : null;
+};
+
+/**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+DefaultView.propTypes = {
+ /**
+ * Content of the object
+ */
+ content: PropTypes.shape({
+ /**
+ * Title of the object
+ */
+ title: PropTypes.string,
+ /**
+ * Description of the object
+ */
+ description: PropTypes.string,
+ /**
+ * Text of the object
+ */
+ text: PropTypes.shape({
+ /**
+ * Data of the text of the object
+ */
+ data: PropTypes.string,
+ }),
+ }).isRequired,
+};
+
+export default DefaultView;
diff --git a/src/hocs/withDeviceSize.test.jsx b/src/hocs/withDeviceSize.test.jsx
new file mode 100644
index 00000000..39df8832
--- /dev/null
+++ b/src/hocs/withDeviceSize.test.jsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { render, act } from '@testing-library/react';
+import withDeviceSize from './withDeviceSize.jsx';
+
+describe('withDeviceSize HOC', () => {
+ // Mock the WrappedComponent
+ const WrappedComponent = ({ device }) => (
+ {device}
+ );
+
+ const mockResize = (width) => {
+ Object.defineProperty(document.documentElement, 'clientWidth', {
+ writable: true,
+ configurable: true,
+ value: width,
+ });
+ window.dispatchEvent(new Event('resize'));
+ };
+
+ it('should return mobile for screen width less than 768px', () => {
+ const ComponentWithDeviceSize = withDeviceSize(WrappedComponent);
+
+ const { getByTestId } = render();
+
+ act(() => {
+ mockResize(500); // Simulating a mobile screen
+ });
+
+ expect(getByTestId('device').textContent).toBe('mobile');
+ });
+
+ it('should return tablet for screen width between 768px and 992px', () => {
+ const ComponentWithDeviceSize = withDeviceSize(WrappedComponent);
+
+ const { getByTestId } = render();
+
+ act(() => {
+ mockResize(800); // Simulating a tablet screen
+ });
+
+ expect(getByTestId('device').textContent).toBe('tablet');
+ });
+
+ it('should return computer for screen width between 992px and 1200px', () => {
+ const ComponentWithDeviceSize = withDeviceSize(WrappedComponent);
+
+ const { getByTestId } = render();
+
+ act(() => {
+ mockResize(1000); // Simulating a computer screen
+ });
+
+ expect(getByTestId('device').textContent).toBe('computer');
+ });
+
+ it('should return large for screen width between 1200px and 1920px', () => {
+ const ComponentWithDeviceSize = withDeviceSize(WrappedComponent);
+
+ const { getByTestId } = render();
+
+ act(() => {
+ mockResize(1500); // Simulating a large screen
+ });
+
+ expect(getByTestId('device').textContent).toBe('large');
+ });
+
+ it('should return widescreen for screen width above 1920px', () => {
+ const ComponentWithDeviceSize = withDeviceSize(WrappedComponent);
+
+ const { getByTestId } = render();
+
+ act(() => {
+ mockResize(2000); // Simulating a widescreen display
+ });
+
+ expect(getByTestId('device').textContent).toBe('widescreen');
+ });
+});
diff --git a/src/index.js b/src/index.js
index 0fc2c879..76de3028 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,6 +11,7 @@ import CustomCSS from '@eeacms/volto-eea-website-theme/components/theme/CustomCS
import DraftBackground from '@eeacms/volto-eea-website-theme/components/theme/DraftBackground/DraftBackground';
import HomePageInverseView from '@eeacms/volto-eea-website-theme/components/theme/Homepage/HomePageInverseView';
import HomePageView from '@eeacms/volto-eea-website-theme/components/theme/Homepage/HomePageView';
+import WebReportSectionView from '@eeacms/volto-eea-website-theme/components/theme/WebReport/WebReportSectionView';
import NotFound from '@eeacms/volto-eea-website-theme/components/theme/NotFound/NotFound';
import { TokenWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TokenWidget';
import { TopicsWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TopicsWidget';
@@ -26,6 +27,7 @@ import {
} from '@eeacms/volto-eea-website-theme/helpers/schema-utils';
import installLayoutSettingsBlock from '@eeacms/volto-eea-website-theme/components/manage/Blocks/LayoutSettings';
+import installContextNavigationBlock from '@eeacms/volto-eea-website-theme/components/manage/Blocks/ContextNavigation';
import installCustomTitle from '@eeacms/volto-eea-website-theme/components/manage/Blocks/Title';
import FlexGroup from '@eeacms/volto-eea-website-theme/components/manage/Blocks/GroupBlockTemplate/FlexGroup/FlexGroup';
@@ -241,7 +243,14 @@ const applyConfig = (config) => {
...(config.views.layoutViewsNamesMapping || {}),
homepage_view: 'Homepage view',
homepage_inverse_view: 'Homepage white view',
+ web_report_section: 'Web report section',
};
+
+ config.views.contentTypesViews = {
+ ...(config.views.contentTypesViews || {}),
+ web_report_section: WebReportSectionView,
+ };
+
config.views.errorViews = {
...config.views.errorViews,
404: NotFound,
@@ -486,11 +495,11 @@ const applyConfig = (config) => {
// },
};
- // layout settings
- config = [installLayoutSettingsBlock].reduce(
- (acc, apply) => apply(acc),
- config,
- );
+ //If you don't want to show the content type as a link in the breadcrumbs, you can set it to a number
+ // where 1 is the last item in the breadcrumbs, 2 is the second last, etc.
+ config.settings.contentTypeToAvoidAsLinks = {
+ web_report_section: 2,
+ };
// Group
if (config.blocks.blocksConfig.group) {
@@ -559,8 +568,12 @@ const applyConfig = (config) => {
GET_CONTENT: ['breadcrumbs'], // 'navigation', 'actions', 'types'],
});
- // Custom blocks: Title
- return [installCustomTitle].reduce((acc, apply) => apply(acc), config);
+ // Custom blocks: Title,Layout settings, Context navigation
+ return [
+ installCustomTitle,
+ installLayoutSettingsBlock,
+ installContextNavigationBlock,
+ ].reduce((acc, apply) => apply(acc), config);
};
export default applyConfig;