diff --git a/CHANGELOG.md b/CHANGELOG.md
index da7315cf..bd72e35e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@ 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).
+### [1.32.0](https://github.com/eea/volto-eea-website-theme/compare/1.31.0...1.32.0) - 21 March 2024
+
+#### :rocket: New Features
+
+- feat(slate): add new h3 slate element [Miu Razvan - [`20a7e93`](https://github.com/eea/volto-eea-website-theme/commit/20a7e9316d5c1de279712c38bc05eb775f8bd326)]
+- feat(slate): add h4 in slate toolbar [Miu Razvan - [`2c523ac`](https://github.com/eea/volto-eea-website-theme/commit/2c523acdeb78ed224c4a37285c48dbf4555604e0)]
+
+#### :nail_care: Enhancements
+
+- change(slate): Use F as icon and renamed title of new heading Slate toolbar button to Figure title [David Ichim - [`7355bb3`](https://github.com/eea/volto-eea-website-theme/commit/7355bb3f3cf527c24fc41a10f72cc854773ab84b)]
+- change(slate): renamed addFormat to addSlateToolbarButton [ichim-david - [`0809c04`](https://github.com/eea/volto-eea-website-theme/commit/0809c040ea9e096c60cbce36e679f9bab23a52a2)]
+
+#### :house: Internal changes
+
+- style: Automated code fix [eea-jenkins - [`6971349`](https://github.com/eea/volto-eea-website-theme/commit/69713496d601e4e0b2fe3469a4ffcd8d36105040)]
+- style: Automated code fix [eea-jenkins - [`a292700`](https://github.com/eea/volto-eea-website-theme/commit/a292700cd047bad925add1fde4d834f7fd03ec62)]
+
+#### :hammer_and_wrench: Others
+
+- Update package.json [ichim-david - [`9db96ff`](https://github.com/eea/volto-eea-website-theme/commit/9db96ff3f8e8eedce92c53befe6a1d6e965d8699)]
+- Revert "(fix): In search block on edit, the sort on and sort order are not working (#205)" [David Ichim - [`2045d50`](https://github.com/eea/volto-eea-website-theme/commit/2045d50b3d2f18fab0d6c6794d8030975b1a3f21)]
### [1.31.0](https://github.com/eea/volto-eea-website-theme/compare/1.30.0...1.31.0) - 14 March 2024
#### :rocket: New Features
diff --git a/package.json b/package.json
index d0a52238..553a8d61 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@eeacms/volto-eea-website-theme",
- "version": "1.31.0",
+ "version": "1.32.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/customizations/volto/components/manage/Blocks/Search/SearchBlockEdit.jsx b/src/customizations/volto/components/manage/Blocks/Search/SearchBlockEdit.jsx
deleted file mode 100644
index 4ce1f10a..00000000
--- a/src/customizations/volto/components/manage/Blocks/Search/SearchBlockEdit.jsx
+++ /dev/null
@@ -1,106 +0,0 @@
-//this customization is used for fixing: in the search block, on edit sort on and reversed order doesn't work.
-//See here volto pr: https://github.com/plone/volto/pull/5262
-import React, { useEffect } from 'react';
-import { defineMessages } from 'react-intl';
-import { compose } from 'redux';
-
-import { SidebarPortal, BlockDataForm } from '@plone/volto/components';
-import { addExtensionFieldToSchema } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
-import { getBaseUrl } from '@plone/volto/helpers';
-import config from '@plone/volto/registry';
-
-import { SearchBlockViewComponent } from '@plone/volto/components/manage/Blocks/Search/SearchBlockView';
-import Schema from '@plone/volto/components/manage/Blocks/Search/schema';
-import {
- withSearch,
- withQueryString,
-} from '@plone/volto/components/manage/Blocks/Search/hocs';
-import { cloneDeep } from 'lodash';
-
-const messages = defineMessages({
- template: {
- id: 'Results template',
- defaultMessage: 'Results template',
- },
-});
-
-const SearchBlockEdit = (props) => {
- const {
- block,
- onChangeBlock,
- data,
- selected,
- intl,
- navRoot,
- contentType,
- onTriggerSearch,
- querystring = {},
- } = props;
- const { sortable_indexes = {} } = querystring;
-
- let schema = Schema({ data, intl });
-
- schema = addExtensionFieldToSchema({
- schema,
- name: 'listingBodyTemplate',
- items: config.blocks.blocksConfig.listing.variations,
- intl,
- title: { id: intl.formatMessage(messages.template) },
- });
- const listingVariations = config.blocks.blocksConfig?.listing?.variations;
- let activeItem = listingVariations.find(
- (item) => item.id === data.listingBodyTemplate,
- );
- const listingSchemaEnhancer = activeItem?.schemaEnhancer;
- if (listingSchemaEnhancer)
- schema = listingSchemaEnhancer({
- schema: cloneDeep(schema),
- data,
- intl,
- });
- schema.properties.sortOnOptions.items = {
- choices: Object.keys(sortable_indexes).map((k) => [
- k,
- sortable_indexes[k].title,
- ]),
- };
-
- const { query = {} } = data || {};
- // We don't need deep compare here, as this is just json serializable data.
- const deepQuery = JSON.stringify(query);
- useEffect(() => {
- onTriggerSearch(
- '',
- data?.facets,
- data?.query?.sort_on,
- data?.query?.sort_order,
- );
- }, [deepQuery, onTriggerSearch, data]);
-
- return (
- <>
-
-
- {
- onChangeBlock(block, {
- ...data,
- [id]: value,
- });
- }}
- onChangeBlock={onChangeBlock}
- formData={data}
- navRoot={navRoot}
- contentType={contentType}
- />
-
- >
- );
-};
-
-export default compose(withQueryString, withSearch())(SearchBlockEdit);
diff --git a/src/customizations/volto/components/manage/Blocks/Search/hocs/withSearch.jsx b/src/customizations/volto/components/manage/Blocks/Search/hocs/withSearch.jsx
deleted file mode 100644
index 6bf2b191..00000000
--- a/src/customizations/volto/components/manage/Blocks/Search/hocs/withSearch.jsx
+++ /dev/null
@@ -1,479 +0,0 @@
-//this customization is used for fixing: in the search block, on edit sort on and reversed order doesn't work.
-//See here volto pr: https://github.com/plone/volto/pull/5262
-import React from 'react';
-import { useSelector } from 'react-redux';
-import qs from 'query-string';
-import { useLocation, useHistory } from 'react-router-dom';
-
-import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
-import config from '@plone/volto/registry';
-import { usePrevious } from '@plone/volto/helpers';
-import { isEqual } from 'lodash';
-
-function getDisplayName(WrappedComponent) {
- return WrappedComponent.displayName || WrappedComponent.name || 'Component';
-}
-
-const SEARCH_ENDPOINT_FIELDS = [
- 'SearchableText',
- 'b_size',
- 'limit',
- 'sort_on',
- 'sort_order',
-];
-
-const PAQO = 'plone.app.querystring.operation';
-
-/**
- * Based on URL state, gets an initial internal state for the search
- *
- * @function getInitialState
- *
- */
-function getInitialState(
- data,
- facets,
- urlSearchText,
- id,
- sortOnParam,
- sortOrderParam,
-) {
- const {
- types: facetWidgetTypes,
- } = config.blocks.blocksConfig.search.extensions.facetWidgets;
- const facetSettings = data?.facets || [];
-
- return {
- query: [
- ...(data.query?.query || []),
- ...(facetSettings || [])
- .map((facet) => {
- if (!facet?.field) return null;
-
- const { valueToQuery } = resolveExtension(
- 'type',
- facetWidgetTypes,
- facet,
- );
-
- const name = facet.field.value;
- const value = facets[name];
-
- return valueToQuery({ value, facet });
- })
- .filter((f) => !!f),
- ...(urlSearchText
- ? [
- {
- i: 'SearchableText',
- o: 'plone.app.querystring.operation.string.contains',
- v: urlSearchText,
- },
- ]
- : []),
- ],
- sort_on: sortOnParam || data.query?.sort_on,
- sort_order: sortOrderParam || data.query?.sort_order,
- b_size: data.query?.b_size,
- limit: data.query?.limit,
- block: id,
- };
-}
-
-/**
- * "Normalizes" the search state to something that's serializable
- * (for querying) and used to compute data for the ListingBody
- *
- * @function normalizeState
- *
- */
-function normalizeState({
- query, // base query
- facets, // facet values
- id, // block id
- searchText, // SearchableText
- sortOn,
- sortOrder,
- facetSettings, // data.facets extracted from block data
-}) {
- const {
- types: facetWidgetTypes,
- } = config.blocks.blocksConfig.search.extensions.facetWidgets;
-
- // Here, we are removing the QueryString of the Listing ones, which is present in the Facet
- // because we already initialize the facet with those values.
- const configuredFacets = facetSettings
- ? facetSettings.map((facet) => facet?.field?.value)
- : [];
-
- let copyOfQuery = query.query ? [...query.query] : [];
-
- const queryWithoutFacet = copyOfQuery.filter((query) => {
- return !configuredFacets.includes(query.i);
- });
-
- const params = {
- query: [
- ...(queryWithoutFacet || []),
- ...(facetSettings || []).map((facet) => {
- if (!facet?.field) return null;
-
- const { valueToQuery } = resolveExtension(
- 'type',
- facetWidgetTypes,
- facet,
- );
-
- const name = facet.field.value;
- const value = facets[name];
-
- return valueToQuery({ value, facet });
- }),
- ].filter((o) => !!o),
- sort_on: sortOn || query.sort_on,
- sort_order: sortOrder || query.sort_order,
- b_size: query.b_size,
- limit: query.limit,
- block: id,
- };
-
- // Note Ideally the searchtext functionality should be restructured as being just
- // another facet. But right now it's the same. This means that if a searchText
- // is provided, it will override the SearchableText facet.
- // If there is no searchText, the SearchableText in the query remains in effect.
- // TODO eventually the searchText should be a distinct facet from SearchableText, and
- // the two conditions could be combined, in comparison to the current state, when
- // one overrides the other.
- if (searchText) {
- params.query = params.query.reduce(
- // Remove SearchableText from query
- (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
- [],
- );
- params.query.push({
- i: 'SearchableText',
- o: 'plone.app.querystring.operation.string.contains',
- v: searchText,
- });
- }
-
- return params;
-}
-
-const getSearchFields = (searchData) => {
- return Object.assign(
- {},
- ...SEARCH_ENDPOINT_FIELDS.map((k) => {
- return searchData[k] ? { [k]: searchData[k] } : {};
- }),
- searchData.query ? { query: serializeQuery(searchData['query']) } : {},
- );
-};
-
-/**
- * A hook that will mirror the search block state to a hash location
- */
-const useHashState = () => {
- const location = useLocation();
- const history = useHistory();
-
- /**
- * Required to maintain parameter compatibility.
- With this we will maintain support for receiving hash (#) and search (?) type parameters.
- */
- const oldState = React.useMemo(() => {
- return {
- ...qs.parse(location.search),
- ...qs.parse(location.hash),
- };
- }, [location.hash, location.search]);
-
- // This creates a shallow copy. Why is this needed?
- const current = Object.assign(
- {},
- ...Array.from(Object.keys(oldState)).map((k) => ({ [k]: oldState[k] })),
- );
-
- const setSearchData = React.useCallback(
- (searchData) => {
- const newParams = qs.parse(location.search);
-
- let changed = false;
-
- Object.keys(searchData)
- .sort()
- .forEach((k) => {
- if (searchData[k]) {
- newParams[k] = searchData[k];
- if (oldState[k] !== searchData[k]) {
- changed = true;
- }
- }
- });
-
- if (changed) {
- history.push({
- search: qs.stringify(newParams),
- });
- }
- },
- [history, oldState, location.search],
- );
-
- return [current, setSearchData];
-};
-
-/**
- * A hook to make it possible to switch disable mirroring the search block
- * state to the window location. When using the internal state we "start from
- * scratch", as it's intended to be used in the edit page.
- */
-const useSearchBlockState = (uniqueId, isEditMode) => {
- const [hashState, setHashState] = useHashState();
- const [internalState, setInternalState] = React.useState({});
-
- return isEditMode
- ? [internalState, setInternalState]
- : [hashState, setHashState];
-};
-
-// Simple compress/decompress the state in URL by replacing the lengthy string
-const deserializeQuery = (q) => {
- return JSON.parse(q)?.map((kvp) => ({
- ...kvp,
- o: kvp.o.replace(/^paqo/, PAQO),
- }));
-};
-const serializeQuery = (q) => {
- return JSON.stringify(
- q?.map((kvp) => ({ ...kvp, o: kvp.o.replace(PAQO, 'paqo') })),
- );
-};
-
-const withSearch = (options) => (WrappedComponent) => {
- const { inputDelay = 1000 } = options || {};
-
- function WithSearch(props) {
- const { data, id, editable = false } = props;
-
- const [locationSearchData, setLocationSearchData] = useSearchBlockState(
- id,
- editable,
- );
-
- // TODO: Improve the hook dependencies out of the scope of https://github.com/plone/volto/pull/4662
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const urlQuery = locationSearchData.query
- ? deserializeQuery(locationSearchData.query)
- : [];
- const urlSearchText =
- locationSearchData.SearchableText ||
- urlQuery.find(({ i }) => i === 'SearchableText')?.v ||
- '';
-
- // TODO: refactor, should use only useLocationStateManager()!!!
- const [searchText, setSearchText] = React.useState(urlSearchText);
- // TODO: Improve the hook dependencies out of the scope of https://github.com/plone/volto/pull/4662
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const configuredFacets =
- data.facets?.map((facet) => facet?.field?.value) || [];
-
- // Here we are getting the initial value of the facet if Listing Query contains the same criteria as
- // facet.
- const queryData = data?.query?.query
- ? deserializeQuery(JSON.stringify(data?.query?.query))
- : [];
-
- let intializeFacetWithQueryValue = [];
-
- for (let value of configuredFacets) {
- const queryString = queryData.find((item) => item.i === value);
- if (queryString) {
- intializeFacetWithQueryValue = [
- ...intializeFacetWithQueryValue,
- { [queryString.i]: queryString.v },
- ];
- }
- }
-
- const multiFacets = data.facets
- ?.filter((facet) => facet?.multiple)
- .map((facet) => facet?.field?.value);
- const [facets, setFacets] = React.useState(
- Object.assign(
- {},
- ...urlQuery.map(({ i, v }) => ({ [i]: v })),
- // TODO: the 'o' should be kept. This would be a major refactoring of the facets
- ...intializeFacetWithQueryValue,
- // support for simple filters like ?Subject=something
- // TODO: since the move to hash params this is no longer working.
- // We'd have to treat the location.search and manage it just like the
- // hash, to support it. We can read it, but we'd have to reset it as
- // well, so at that point what's the difference to the hash?
- ...configuredFacets.map((f) =>
- locationSearchData[f]
- ? {
- [f]:
- multiFacets.indexOf(f) > -1
- ? [locationSearchData[f]]
- : locationSearchData[f],
- }
- : {},
- ),
- ),
- );
- const previousUrlQuery = usePrevious(urlQuery);
-
- // During first render the previousUrlQuery is undefined and urlQuery
- // is empty so it ressetting the facet when you are navigating but during reload we have urlQuery and we need
- // to set the facet at first render.
- const preventOverrideOfFacetState =
- previousUrlQuery === undefined && urlQuery.length === 0;
-
- React.useEffect(() => {
- if (
- !isEqual(urlQuery, previousUrlQuery) &&
- !preventOverrideOfFacetState
- ) {
- setFacets(
- Object.assign(
- {},
- ...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
-
- // support for simple filters like ?Subject=something
- // TODO: since the move to hash params this is no longer working.
- // We'd have to treat the location.search and manage it just like the
- // hash, to support it. We can read it, but we'd have to reset it as
- // well, so at that point what's the difference to the hash?
- ...configuredFacets.map((f) =>
- locationSearchData[f]
- ? {
- [f]:
- multiFacets.indexOf(f) > -1
- ? [locationSearchData[f]]
- : locationSearchData[f],
- }
- : {},
- ),
- ),
- );
- }
- }, [
- urlQuery,
- configuredFacets,
- locationSearchData,
- multiFacets,
- previousUrlQuery,
- preventOverrideOfFacetState,
- ]);
-
- const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
- const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
-
- const [searchData, setSearchData] = React.useState(
- getInitialState(data, facets, urlSearchText, id),
- );
-
- const deepFacets = JSON.stringify(facets);
- const deepData = JSON.stringify(data);
- React.useEffect(() => {
- setSearchData(
- getInitialState(
- JSON.parse(deepData),
- JSON.parse(deepFacets),
- urlSearchText,
- id,
- sortOn,
- sortOrder,
- ),
- );
- }, [deepData, deepFacets, urlSearchText, id, sortOn, sortOrder]);
-
- const timeoutRef = React.useRef();
- const facetSettings = data?.facets;
-
- const deepQuery = JSON.stringify(data.query);
- const onTriggerSearch = React.useCallback(
- (
- toSearchText = undefined,
- toSearchFacets = undefined,
- toSortOn = undefined,
- toSortOrder = undefined,
- ) => {
- if (timeoutRef.current) clearTimeout(timeoutRef.current);
- timeoutRef.current = setTimeout(
- () => {
- const newSearchData = normalizeState({
- id,
- query: data.query || {},
- facets: toSearchFacets || facets,
- searchText: toSearchText ? toSearchText.trim() : '',
- sortOn: toSortOn || undefined,
- sortOrder: toSortOrder || sortOrder,
- facetSettings,
- });
- if (toSearchFacets) setFacets(toSearchFacets);
- if (toSortOn) setSortOn(toSortOn || undefined);
- if (toSortOrder) setSortOrder(toSortOrder);
- setSearchData(newSearchData);
- setLocationSearchData(getSearchFields(newSearchData));
- },
- toSearchFacets ? inputDelay / 3 : inputDelay,
- );
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- // Use deep comparison of data.query
- deepQuery,
- facets,
- id,
- setLocationSearchData,
- searchText,
- sortOn,
- sortOrder,
- facetSettings,
- ],
- );
-
- const removeSearchQuery = () => {
- let newSearchData = { ...searchData };
- newSearchData.query = searchData.query.reduce(
- // Remove SearchableText from query
- (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
- [],
- );
- setSearchData(newSearchData);
- setLocationSearchData(getSearchFields(newSearchData));
- };
-
- const querystringResults = useSelector(
- (state) => state.querystringsearch.subrequests,
- );
- const totalItems =
- querystringResults[id]?.total || querystringResults[id]?.items?.length;
-
- return (
-
- );
- }
- WithSearch.displayName = `WithSearch(${getDisplayName(WrappedComponent)})`;
-
- return WithSearch;
-};
-
-export default withSearch;
diff --git a/src/icons/font-mono.svg b/src/icons/font-mono.svg
new file mode 100644
index 00000000..86fa362e
--- /dev/null
+++ b/src/icons/font-mono.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index dab7f3ac..a49cbd01 100644
--- a/src/index.js
+++ b/src/index.js
@@ -373,6 +373,7 @@ const applyConfig = (config) => {
},
];
+ // Install slate
config = installSlate(config);
// Custom block-style colors
diff --git a/src/slate.js b/src/slate.js
index c4f0c40d..0a0559f6 100644
--- a/src/slate.js
+++ b/src/slate.js
@@ -1,6 +1,11 @@
import React from 'react';
+import cx from 'classnames';
import { List } from 'semantic-ui-react';
-import { MarkElementButton, ToolbarButton } from '@plone/volto-slate/editor/ui';
+import {
+ MarkElementButton,
+ ToolbarButton,
+ BlockButton,
+} from '@plone/volto-slate/editor/ui';
import installCallout from '@plone/volto-slate/editor/plugins/Callout';
import { Icon } from '@plone/volto/components';
import { Editor, Transforms, Text } from 'slate';
@@ -14,6 +19,39 @@ import alignJustifyIcon from '@plone/volto/icons/align-justify.svg';
import lightIcon from './icons/light.svg';
import smallIcon from './icons/small.svg';
import clearIcon from './icons/eraser.svg';
+import fontMono from './icons/font-mono.svg';
+
+const installSlateToolbarButton = ({
+ config,
+ key,
+ before,
+ button,
+ element,
+}) => {
+ const toolbarButtons = config.settings.slate.toolbarButtons;
+ const index = toolbarButtons.indexOf(key);
+ const beforeIndex = toolbarButtons.indexOf(before);
+ if (index === -1) {
+ if (beforeIndex > -1) {
+ toolbarButtons.splice(beforeIndex + 1, 0, key);
+ } else {
+ toolbarButtons.push(key);
+ }
+ } else if (index > -1 && beforeIndex > -1 && index > beforeIndex + 1) {
+ toolbarButtons.splice(index, 1);
+ toolbarButtons.splice(beforeIndex + 1, 0, key);
+ } else if (index > -1 && index < beforeIndex) {
+ toolbarButtons.splice(index, 1);
+ toolbarButtons.splice(beforeIndex, 0, key);
+ }
+ if (button) {
+ config.settings.slate.buttons[key] = button;
+ }
+ if (element) {
+ config.settings.slate.elements[key] = element;
+ }
+ return config;
+};
const toggleBlockClassFormat = (editor, format) => {
const levels = Array.from(Editor.levels(editor, editor.selection));
@@ -67,20 +105,16 @@ const clearFormatting = (editor) => {
Editor.nodes(editor, {
mode: 'lowest',
match: (n, p) => {
- // console.log('node', n, p);
return Text.isText(n);
},
//at: [0], // uncomment if you want everything to be cleared
}),
);
- // console.log('sn', sn);
-
sn.forEach(([n, at]) => {
const toRemove = Object.keys(n).filter((k) => k.startsWith('style-'));
if (toRemove.length) {
Transforms.unsetNodes(editor, toRemove, { at });
- // console.log('unset', n, at, toRemove);
}
});
@@ -111,9 +145,46 @@ const ClearFormattingButton = ({ icon, ...props }) => {
export default function installSlate(config) {
if (config.settings.slate) {
+ let renderLinkElement;
// Callout slate button
config = installCallout(config);
+ try {
+ renderLinkElement = require('@eeacms/volto-anchors/helpers')
+ .renderLinkElement;
+ } catch {}
+
+ installSlateToolbarButton({
+ config,
+ key: 'h3-light',
+ before: 'heading-three',
+ button: (props) => (
+
+ ),
+ element: renderLinkElement
+ ? (opts) => {
+ return renderLinkElement('h3')({
+ ...opts,
+ className: 'subtitle-light',
+ });
+ }
+ : ({ attributes, children }) => (
+
+ {children}
+
+ ),
+ });
+ config.settings.slate.topLevelTargetElements.push('h3-light');
+
config.settings.slate.buttons.clearformatting = (props) => (
);