diff --git a/CHANGELOG.md b/CHANGELOG.md index 8465e81b..da7315cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ 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.31.0](https://github.com/eea/volto-eea-website-theme/compare/1.30.0...1.31.0) - 14 March 2024 + +#### :rocket: New Features + +- feat: Put default alt as the rights field than image title - refs #159551 [dobri1408 - [`4f0e60b`](https://github.com/eea/volto-eea-website-theme/commit/4f0e60b02ce1167d92de3b294e75d2577c892c85)] + +#### :hammer_and_wrench: Others + +- Release 1.31.0 [alin - [`36690f7`](https://github.com/eea/volto-eea-website-theme/commit/36690f75e4773e40f5ab4e4ce4b956b650df62b5)] ### [1.30.0](https://github.com/eea/volto-eea-website-theme/compare/1.29.0...1.30.0) - 13 March 2024 #### :rocket: New Features diff --git a/package.json b/package.json index 7f4b1999..d0a52238 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-eea-website-theme", - "version": "1.30.0", + "version": "1.31.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/Sidebar/ObjectBrowserBody.jsx b/src/customizations/volto/components/manage/Sidebar/ObjectBrowserBody.jsx new file mode 100644 index 00000000..7fb93990 --- /dev/null +++ b/src/customizations/volto/components/manage/Sidebar/ObjectBrowserBody.jsx @@ -0,0 +1,511 @@ +/* this customization is used to put default alt as the rights field + */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { Input, Segment, Breadcrumb } from 'semantic-ui-react'; + +import { join } from 'lodash'; + +// These absolute imports (without using the corresponding centralized index.js) are required +// to cut circular import problems, this file should never use them. This is because of +// the very nature of the functionality of the component and its relationship with others +import { searchContent } from '@plone/volto/actions/search/search'; +import Icon from '@plone/volto/components/theme/Icon/Icon'; +import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url'; +import config from '@plone/volto/registry'; + +import backSVG from '@plone/volto/icons/back.svg'; +import folderSVG from '@plone/volto/icons/folder.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; +import searchSVG from '@plone/volto/icons/zoom.svg'; +import linkSVG from '@plone/volto/icons/link.svg'; +import homeSVG from '@plone/volto/icons/home.svg'; + +import ObjectBrowserNav from '@plone/volto/components/manage/Sidebar/ObjectBrowserNav'; + +const messages = defineMessages({ + SearchInputPlaceholder: { + id: 'Search content', + defaultMessage: 'Search content', + }, + SelectedItems: { + id: 'Selected items', + defaultMessage: 'Selected items', + }, + back: { + id: 'Back', + defaultMessage: 'Back', + }, + search: { + id: 'Search SVG', + defaultMessage: 'Search SVG', + }, + of: { id: 'Selected items - x of y', defaultMessage: 'of' }, +}); + +function getParentURL(url) { + return flattenToAppURL(`${join(url.split('/').slice(0, -1), '/')}`) || '/'; +} + +/** + * ObjectBrowserBody container class. + * @class ObjectBrowserBody + * @extends Component + */ +class ObjectBrowserBody extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + block: PropTypes.string.isRequired, + mode: PropTypes.string.isRequired, + data: PropTypes.any.isRequired, + searchSubrequests: PropTypes.objectOf(PropTypes.any).isRequired, + searchContent: PropTypes.func.isRequired, + closeObjectBrowser: PropTypes.func.isRequired, + onChangeBlock: PropTypes.func.isRequired, + onSelectItem: PropTypes.func, + dataName: PropTypes.string, + maximumSelectionSize: PropTypes.number, + contextURL: PropTypes.string, + searchableTypes: PropTypes.arrayOf(PropTypes.string), + }; + + /** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + image: '', + href: '', + onSelectItem: null, + dataName: null, + selectableTypes: [], + searchableTypes: null, + maximumSelectionSize: null, + }; + + /** + * Constructor + * @method constructor + * @param {Object} props Component properties + * @constructs WysiwygEditor + */ + constructor(props) { + super(props); + this.state = { + currentFolder: + this.props.mode === 'multiple' ? '/' : this.props.contextURL || '/', + currentImageFolder: + this.props.mode === 'multiple' + ? '/' + : this.props.mode === 'image' && this.props.data?.url + ? getParentURL(this.props.data.url) + : '/', + currentLinkFolder: + this.props.mode === 'multiple' + ? '/' + : this.props.mode === 'link' && this.props.data?.href + ? getParentURL(this.props.data.href) + : '/', + parentFolder: '', + selectedImage: + this.props.mode === 'multiple' + ? '' + : this.props.mode === 'image' && this.props.data?.url + ? flattenToAppURL(this.props.data.url) + : '', + selectedHref: + this.props.mode === 'multiple' + ? '' + : this.props.mode === 'link' && this.props.data?.href + ? flattenToAppURL(this.props.data.href) + : '', + showSearchInput: false, + // In image mode, the searchable types default to the image types which + // can be overridden with the property if specified. + searchableTypes: + this.props.mode === 'image' + ? this.props.searchableTypes || config.settings.imageObjects + : this.props.searchableTypes, + }; + this.searchInputRef = React.createRef(); + } + + /** + * Component did mount + * @method componentDidMount + * @returns {undefined} + */ + componentDidMount() { + this.initialSearch(this.props.mode); + } + + initialSearch = (mode) => { + const currentSelected = + mode === 'multiple' + ? '' + : mode === 'image' + ? this.state.selectedImage + : this.state.selectedHref; + if (currentSelected && isInternalURL(currentSelected)) { + this.props.searchContent( + getParentURL(currentSelected), + { + 'path.depth': 1, + sort_on: 'getObjPositionInParent', + metadata_fields: '_all', + b_size: 1000, + }, + `${this.props.block}-${mode}`, + ); + } else { + this.props.searchContent( + this.state.currentFolder, + { + 'path.depth': 1, + sort_on: 'getObjPositionInParent', + metadata_fields: '_all', + b_size: 1000, + }, + `${this.props.block}-${mode}`, + ); + } + }; + + navigateTo = (id) => { + this.props.searchContent( + id, + { + 'path.depth': 1, + sort_on: 'getObjPositionInParent', + metadata_fields: '_all', + b_size: 1000, + }, + `${this.props.block}-${this.props.mode}`, + ); + const parent = `${join(id.split('/').slice(0, -1), '/')}` || '/'; + this.setState(() => ({ + parentFolder: parent, + currentFolder: id || '/', + })); + }; + + toggleSearchInput = () => + this.setState( + (prevState) => ({ + showSearchInput: !prevState.showSearchInput, + }), + () => { + if (this.searchInputRef?.current) this.searchInputRef.current.focus(); + }, + ); + + onSearch = (e) => { + const text = flattenToAppURL(e.target.value); + if (text.startsWith('/')) { + this.setState({ currentFolder: text }); + this.props.searchContent( + text, + { + 'path.depth': 1, + sort_on: 'getObjPositionInParent', + metadata_fields: '_all', + portal_type: this.state.searchableTypes, + }, + `${this.props.block}-${this.props.mode}`, + ); + } else { + text.length > 2 + ? this.props.searchContent( + '/', + { + SearchableText: `${text}*`, + metadata_fields: '_all', + portal_type: this.state.searchableTypes, + }, + `${this.props.block}-${this.props.mode}`, + ) + : this.props.searchContent( + '/', + { + 'path.depth': 1, + sort_on: 'getObjPositionInParent', + metadata_fields: '_all', + portal_type: this.state.searchableTypes, + }, + `${this.props.block}-${this.props.mode}`, + ); + } + }; + + onSelectItem = (item) => { + const url = item['@id']; + const { block, data, mode, dataName, onChangeBlock } = this.props; + + const updateState = (mode) => { + switch (mode) { + case 'image': + this.setState({ + selectedImage: url, + currentImageFolder: getParentURL(url), + }); + break; + case 'link': + this.setState({ + selectedHref: url, + currentLinkFolder: getParentURL(url), + }); + break; + default: + break; + } + }; + + if (dataName) { + onChangeBlock(block, { + ...data, + [dataName]: url, + }); + } else if (this.props.onSelectItem) { + this.props.onSelectItem(url, item); + } else if (mode === 'image') { + onChangeBlock(block, { + ...data, + url: flattenToAppURL(item.getURL), + alt: item.title || item.description || '', + copyright: item.rights || '', + }); + } else if (mode === 'link') { + onChangeBlock(block, { + ...data, + href: flattenToAppURL(url), + }); + } + updateState(mode); + }; + + onChangeBlockData = (key, value) => { + this.props.onChangeBlock(this.props.block, { + ...this.props.data, + [key]: value, + }); + }; + + isSelectable = (item) => { + return this.props.selectableTypes.length > 0 + ? this.props.selectableTypes.indexOf(item['@type']) >= 0 + : true; + }; + + handleClickOnItem = (item) => { + if (this.props.mode === 'image') { + if (item.is_folderish) { + this.navigateTo(item['@id']); + } + if (config.settings.imageObjects.includes(item['@type'])) { + this.onSelectItem(item); + } + } else { + if (this.isSelectable(item)) { + if ( + !this.props.maximumSelectionSize || + this.props.mode === 'multiple' || + !this.props.data || + this.props.data.length < this.props.maximumSelectionSize + ) { + this.onSelectItem(item); + let length = this.props.data ? this.props.data.length : 0; + + let stopSelecting = + this.props.mode !== 'multiple' || + (this.props.maximumSelectionSize > 0 && + length + 1 >= this.props.maximumSelectionSize); + + if (stopSelecting) { + this.props.closeObjectBrowser(); + } + } else { + this.props.closeObjectBrowser(); + } + } else { + this.navigateTo(item['@id']); + } + } + }; + + handleDoubleClickOnItem = (item) => { + if (this.props.mode === 'image') { + if (item.is_folderish) { + this.navigateTo(item['@id']); + } + if (config.settings.imageObjects.includes(item['@type'])) { + this.onSelectItem(item); + this.props.closeObjectBrowser(); + } + } else { + if (this.isSelectable(item)) { + if (this.props.data.length < this.props.maximumSelectionSize) { + this.onSelectItem(item); + } + this.props.closeObjectBrowser(); + } else { + this.navigateTo(item['@id']); + } + } + }; + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + return ( + +
+
+ {this.state.currentFolder === '/' ? ( + <> + {this.props.mode === 'image' ? ( + + ) : ( + + )} + + ) : ( + + )} + {this.state.showSearchInput ? ( + + ) : this.props.mode === 'image' ? ( +

+ +

+ ) : ( +

+ +

+ )} + + + +
+ + + {this.state.currentFolder !== '/' ? ( + this.state.currentFolder.split('/').map((item, index, items) => { + return ( + + {index === 0 ? ( + this.navigateTo('/')}> + + + ) : ( + <> + + + this.navigateTo(items.slice(0, index + 1).join('/')) + } + > + {item} + + + )} + + ); + }) + ) : ( + this.navigateTo('/')}> + + + )} + + + {this.props.mode === 'multiple' && ( + + {this.props.intl.formatMessage(messages.SelectedItems)}:{' '} + {this.props.data?.length} + {this.props.maximumSelectionSize > 0 && ( + <> + {' '} + {this.props.intl.formatMessage(messages.of)}{' '} + {this.props.maximumSelectionSize} + + )} + + )} + +
+ ); + } +} + +export default compose( + injectIntl, + connect( + (state) => ({ + searchSubrequests: state.search.subrequests, + }), + { searchContent }, + ), +)(ObjectBrowserBody);