diff --git a/src/components/manage/Contents/Contents.jsx b/src/components/manage/Contents/Contents.jsx index caa61529ad..b190fcdaf9 100644 --- a/src/components/manage/Contents/Contents.jsx +++ b/src/components/manage/Contents/Contents.jsx @@ -1,2305 +1,2309 @@ -/** - * Contents component. - * @module components/manage/Contents/Contents - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { Portal } from 'react-portal'; -import { Link } from 'react-router-dom'; -import { - Button, - Confirm, - Container as SemanticContainer, - Divider, - Dropdown, - Menu, - Input, - Segment, - Table, - Loader, - Dimmer, -} from 'semantic-ui-react'; -import { - concat, - filter, - find, - indexOf, - keys, - map, - mapValues, - pull, -} from 'lodash'; -import move from 'lodash-move'; -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; -import { asyncConnect } from '@plone/volto/helpers'; -import { flattenToAppURL } from '@plone/volto/helpers'; - -import { - searchContent, - cut, - copy, - copyContent, - deleteContent, - listActions, - moveContent, - orderContent, - sortContent, - updateColumnsContent, - linkIntegrityCheck, - getContent, -} from '@plone/volto/actions'; -import Indexes, { defaultIndexes } from '@plone/volto/constants/Indexes'; -import { - ContentsBreadcrumbs, - ContentsIndexHeader, - ContentsItem, - ContentsRenameModal, - ContentsUploadModal, - ContentsWorkflowModal, - ContentsTagsModal, - ContentsPropertiesModal, - Pagination, - Popup, - Toolbar, - Toast, - Icon, - Unauthorized, -} from '@plone/volto/components'; - -import { Helmet, getBaseUrl } from '@plone/volto/helpers'; -import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; -import config from '@plone/volto/registry'; - -import backSVG from '@plone/volto/icons/back.svg'; -import cutSVG from '@plone/volto/icons/cut.svg'; -import deleteSVG from '@plone/volto/icons/delete.svg'; -import copySVG from '@plone/volto/icons/copy.svg'; -import tagSVG from '@plone/volto/icons/tag.svg'; -import renameSVG from '@plone/volto/icons/rename.svg'; -import semaphoreSVG from '@plone/volto/icons/semaphore.svg'; -import uploadSVG from '@plone/volto/icons/upload.svg'; -import propertiesSVG from '@plone/volto/icons/properties.svg'; -import pasteSVG from '@plone/volto/icons/paste.svg'; -import zoomSVG from '@plone/volto/icons/zoom.svg'; -import checkboxUncheckedSVG from '@plone/volto/icons/checkbox-unchecked.svg'; -import checkboxCheckedSVG from '@plone/volto/icons/checkbox-checked.svg'; -import checkboxIndeterminateSVG from '@plone/volto/icons/checkbox-indeterminate.svg'; -import configurationSVG from '@plone/volto/icons/configuration-app.svg'; -import sortDownSVG from '@plone/volto/icons/sort-down.svg'; -import sortUpSVG from '@plone/volto/icons/sort-up.svg'; -import downKeySVG from '@plone/volto/icons/down-key.svg'; -import moreSVG from '@plone/volto/icons/more.svg'; -import clearSVG from '@plone/volto/icons/clear.svg'; - -const messages = defineMessages({ - back: { - id: 'Back', - defaultMessage: 'Back', - }, - contents: { - id: 'Contents', - defaultMessage: 'Contents', - }, - copy: { - id: 'Copy', - defaultMessage: 'Copy', - }, - cut: { - id: 'Cut', - defaultMessage: 'Cut', - }, - error: { - id: "You can't paste this content here", - defaultMessage: "You can't paste this content here", - }, - delete: { - id: 'Delete', - defaultMessage: 'Delete', - }, - deleteConfirmSingleItem: { - id: 'Delete this item?', - defaultMessage: 'Delete this item?', - }, - deleteConfirmMultipleItems: { - id: 'Delete selected items?', - defaultMessage: 'Delete selected items?', - }, - deleteError: { - id: 'The item could not be deleted.', - defaultMessage: 'The item could not be deleted.', - }, - loading: { - id: 'loading', - defaultMessage: 'Loading', - }, - home: { - id: 'Home', - defaultMessage: 'Home', - }, - filter: { - id: 'Filter…', - defaultMessage: 'Filter…', - }, - messageCopied: { - id: 'Item(s) copied.', - defaultMessage: 'Item(s) copied.', - }, - messageCut: { - id: 'Item(s) cut.', - defaultMessage: 'Item(s) cut.', - }, - messageUpdate: { - id: 'Item(s) has been updated.', - defaultMessage: 'Item(s) has been updated.', - }, - messageReorder: { - id: 'Item succesfully moved.', - defaultMessage: 'Item successfully moved.', - }, - messagePasted: { - id: 'Item(s) pasted.', - defaultMessage: 'Item(s) pasted.', - }, - messageWorkflowUpdate: { - id: 'Item(s) state has been updated.', - defaultMessage: 'Item(s) state has been updated.', - }, - paste: { - id: 'Paste', - defaultMessage: 'Paste', - }, - properties: { - id: 'Properties', - defaultMessage: 'Properties', - }, - rearrangeBy: { - id: 'Rearrange items by…', - defaultMessage: 'Rearrange items by…', - }, - rename: { - id: 'Rename', - defaultMessage: 'Rename', - }, - select: { - id: 'Select…', - defaultMessage: 'Select…', - }, - selected: { - id: '{count} selected', - defaultMessage: '{count} selected', - }, - selectColumns: { - id: 'Select columns to show', - defaultMessage: 'Select columns to show', - }, - sort: { - id: 'sort', - defaultMessage: 'sort', - }, - state: { - id: 'State', - defaultMessage: 'State', - }, - tags: { - id: 'Tags', - defaultMessage: 'Tags', - }, - upload: { - id: 'Upload', - defaultMessage: 'Upload', - }, - success: { - id: 'Success', - defaultMessage: 'Success', - }, - publicationDate: { - id: 'Publication date', - defaultMessage: 'Publication date', - }, - createdOn: { - id: 'Created on', - defaultMessage: 'Created on', - }, - expirationDate: { - id: 'Expiration date', - defaultMessage: 'Expiration date', - }, - id: { - id: 'ID', - defaultMessage: 'ID', - }, - uid: { - id: 'UID', - defaultMessage: 'UID', - }, - reviewState: { - id: 'Review state', - defaultMessage: 'Review state', - }, - folder: { - id: 'Folder', - defaultMessage: 'Folder', - }, - excludedFromNavigation: { - id: 'Excluded from navigation', - defaultMessage: 'Excluded from navigation', - }, - objectSize: { - id: 'Object Size', - defaultMessage: 'Object Size', - }, - lastCommentedDate: { - id: 'Last comment date', - defaultMessage: 'Last comment date', - }, - totalComments: { - id: 'Total comments', - defaultMessage: 'Total comments', - }, - creator: { - id: 'Creator', - defaultMessage: 'Creator', - }, - endDate: { - id: 'End Date', - defaultMessage: 'End Date', - }, - startDate: { - id: 'Start Date', - defaultMessage: 'Start Date', - }, - all: { - id: 'All', - defaultMessage: 'All', - }, -}); - -/** - * Contents class. - * @class Contents - * @extends Component - */ -class Contents extends Component { - /** - * Property types. - * @property {Object} propTypes Property types. - * @static - */ - static propTypes = { - action: PropTypes.string, - source: PropTypes.arrayOf(PropTypes.string), - searchContent: PropTypes.func.isRequired, - cut: PropTypes.func.isRequired, - copy: PropTypes.func.isRequired, - copyContent: PropTypes.func.isRequired, - deleteContent: PropTypes.func.isRequired, - moveContent: PropTypes.func.isRequired, - orderContent: PropTypes.func.isRequired, - sortContent: PropTypes.func.isRequired, - updateColumnsContent: PropTypes.func.isRequired, - linkIntegrityCheck: PropTypes.func.isRequired, - clipboardRequest: PropTypes.shape({ - loading: PropTypes.bool, - loaded: PropTypes.bool, - }).isRequired, - deleteRequest: PropTypes.shape({ - loading: PropTypes.bool, - loaded: PropTypes.bool, - }).isRequired, - updateRequest: PropTypes.shape({ - loading: PropTypes.bool, - loaded: PropTypes.bool, - }).isRequired, - searchRequest: PropTypes.shape({ - loading: PropTypes.bool, - loaded: PropTypes.bool, - }).isRequired, - items: PropTypes.arrayOf( - PropTypes.shape({ - '@id': PropTypes.string, - '@type': PropTypes.string, - title: PropTypes.string, - description: PropTypes.string, - }), - ), - breadcrumbs: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string, - url: PropTypes.string, - }), - ).isRequired, - total: PropTypes.number.isRequired, - pathname: PropTypes.string.isRequired, - }; - - /** - * Default properties. - * @property {Object} defaultProps Default properties. - * @static - */ - static defaultProps = { - items: [], - action: null, - source: null, - index: { - order: keys(Indexes), - values: mapValues(Indexes, (value, key) => ({ - ...value, - selected: indexOf(defaultIndexes, key) !== -1, - })), - selectedCount: defaultIndexes.length + 1, - }, - }; - - /** - * Constructor - * @method constructor - * @param {Object} props Component properties - * @constructs ContentsComponent - */ - constructor(props) { - super(props); - this.onDeselect = this.onDeselect.bind(this); - this.onSelect = this.onSelect.bind(this); - this.onSelectAll = this.onSelectAll.bind(this); - this.onSelectIndex = this.onSelectIndex.bind(this); - this.onSelectNone = this.onSelectNone.bind(this); - this.onDeleteOk = this.onDeleteOk.bind(this); - this.onDeleteCancel = this.onDeleteCancel.bind(this); - this.onUploadOk = this.onUploadOk.bind(this); - this.onUploadCancel = this.onUploadCancel.bind(this); - this.onRenameOk = this.onRenameOk.bind(this); - this.onRenameCancel = this.onRenameCancel.bind(this); - this.onTagsOk = this.onTagsOk.bind(this); - this.onTagsCancel = this.onTagsCancel.bind(this); - this.onPropertiesOk = this.onPropertiesOk.bind(this); - this.onPropertiesCancel = this.onPropertiesCancel.bind(this); - this.onWorkflowOk = this.onWorkflowOk.bind(this); - this.onWorkflowCancel = this.onWorkflowCancel.bind(this); - this.onChangeFilter = this.onChangeFilter.bind(this); - this.onChangePage = this.onChangePage.bind(this); - this.onChangePageSize = this.onChangePageSize.bind(this); - this.onOrderIndex = this.onOrderIndex.bind(this); - this.onOrderItem = this.onOrderItem.bind(this); - this.onSortItems = this.onSortItems.bind(this); - this.onMoveToTop = this.onMoveToTop.bind(this); - this.onChangeSelected = this.onChangeSelected.bind(this); - this.onMoveToBottom = this.onMoveToBottom.bind(this); - this.cut = this.cut.bind(this); - this.copy = this.copy.bind(this); - this.delete = this.delete.bind(this); - this.upload = this.upload.bind(this); - this.rename = this.rename.bind(this); - this.tags = this.tags.bind(this); - this.properties = this.properties.bind(this); - this.workflow = this.workflow.bind(this); - this.paste = this.paste.bind(this); - this.fetchContents = this.fetchContents.bind(this); - this.orderTimeout = null; - this.deleteItemsToShowThreshold = 10; - - this.state = { - selected: [], - showDelete: false, - showUpload: false, - showRename: false, - showTags: false, - showProperties: false, - showWorkflow: false, - itemsToDelete: [], - containedItemsToDelete: [], - brokenReferences: 0, - breaches: [], - showAllItemsToDelete: true, - items: this.props.items, - filter: '', - currentPage: 0, - pageSize: 50, - index: this.props.index || { - order: keys(Indexes), - values: mapValues(Indexes, (value, key) => ({ - ...value, - selected: indexOf(defaultIndexes, key) !== -1, - })), - selectedCount: defaultIndexes.length + 1, - }, - sort_on: this.props.sort?.on || 'getObjPositionInParent', - sort_order: this.props.sort?.order || 'ascending', - isClient: false, - linkIntegrityBreakages: [], - }; - this.filterTimeout = null; - } - - /** - * Component did mount - * @method componentDidMount - * @returns {undefined} - */ - componentDidMount() { - this.fetchContents(); - this.setState({ isClient: true }); - } - async componentDidUpdate(_, prevState) { - if ( - this.state.itemsToDelete !== prevState.itemsToDelete && - this.state.itemsToDelete.length > 0 - ) { - const linkintegrityInfo = await this.props.linkIntegrityCheck( - map(this.state.itemsToDelete, (item) => this.getFieldById(item, 'UID')), - ); - const containedItems = linkintegrityInfo - .map((result) => result.items_total ?? 0) - .reduce((acc, value) => acc + value, 0); - const breaches = linkintegrityInfo.flatMap((result) => - result.breaches.map((source) => ({ - source: source, - target: result, - })), - ); - const source_by_uid = breaches.reduce( - (acc, value) => acc.set(value.source.uid, value.source), - new Map(), - ); - const by_source = breaches.reduce((acc, value) => { - if (acc.get(value.source.uid) === undefined) { - acc.set(value.source.uid, new Set()); - } - acc.get(value.source.uid).add(value.target); - return acc; - }, new Map()); - - this.setState({ - containedItemsToDelete: containedItems, - brokenReferences: by_source.size, - linksAndReferencesViewLink: linkintegrityInfo.length - ? linkintegrityInfo[0]['@id'] + '/links-to-item' - : null, - breaches: Array.from(by_source, (entry) => ({ - source: source_by_uid.get(entry[0]), - targets: Array.from(entry[1]), - })), - showAllItemsToDelete: - this.state.itemsToDelete.length < this.deleteItemsToShowThreshold, - }); - } - } - - /** - * Component will receive props - * @method componentWillReceiveProps - * @param {Object} nextProps Next properties - * @returns {undefined} - */ - UNSAFE_componentWillReceiveProps(nextProps) { - if ( - (this.props.clipboardRequest.loading && - nextProps.clipboardRequest.loaded) || - (this.props.deleteRequest.loading && nextProps.deleteRequest.loaded) || - (this.props.updateRequest.loading && nextProps.updateRequest.loaded) - ) { - this.fetchContents(nextProps.pathname); - } - if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) { - this.props.toastify.toast.success( - , - ); - } - if (this.props.pathname !== nextProps.pathname) { - // Refetching content to sync the current object in the toolbar - this.props.getContent(getBaseUrl(nextProps.pathname)); - this.setState( - { - currentPage: 0, - }, - () => - this.setState({ filter: '' }, () => - this.fetchContents(nextProps.pathname), - ), - ); - } - if (this.props.searchRequest.loading && nextProps.searchRequest.loaded) { - this.setState({ - items: nextProps.items, - }); - } - if ( - this.props.clipboardRequest.loading && - nextProps.clipboardRequest.error - ) { - this.props.toastify.toast.error( - , - ); - } - - if ( - this.props.clipboardRequest.loading && - nextProps.clipboardRequest.loaded - ) { - this.props.toastify.toast.success( - , - ); - } - - if (this.props.deleteRequest.loading && nextProps.deleteRequest.error) { - this.props.toastify.toast.error( - , - ); - } - - if (this.props.orderRequest.loading && nextProps.orderRequest.loaded) { - this.props.toastify.toast.success( - , - ); - } - } - - /** - * On deselect handler - * @method onDeselect - * @param {object} event Event object - * @param {string} value Value - * @returns {undefined} - */ - onDeselect(event, { value }) { - this.setState({ - selected: pull(this.state.selected, value), - }); - } - - /** - * On select handler - * @method onSelect - * @param {object} event Event object - * @returns {undefined} - */ - onSelect(event, id) { - if (indexOf(this.state.selected, id) === -1) { - this.setState({ - selected: concat(this.state.selected, id), - }); - } else { - this.setState({ - selected: pull(this.state.selected, id), - }); - } - } - - /** - * On select all handler - * @method onSelectAll - * @returns {undefined} - */ - onSelectAll() { - this.setState({ - selected: map(this.state.items, (item) => item['@id']), - }); - } - - /** - * On select none handler - * @method onSelectNone - * @returns {undefined} - */ - onSelectNone() { - this.setState({ - selected: [], - }); - } - - /** - * On select index - * @method onSelectIndex - * @param {object} event Event object. - * @param {string} value Index value. - * @returns {undefined} - */ - onSelectIndex(event, { value }) { - let newIndex = { - ...this.state.index, - selectedCount: - this.state.index.selectedCount + - (this.state.index.values[value].selected ? -1 : 1), - values: mapValues(this.state.index.values, (indexValue, indexKey) => ({ - ...indexValue, - selected: - indexKey === value ? !indexValue.selected : indexValue.selected, - })), - }; - this.setState({ - index: newIndex, - }); - this.props.updateColumnsContent(getBaseUrl(this.props.pathname), newIndex); - } - - /** - * On change filter - * @method onChangeFilter - * @param {object} event Event object. - * @param {string} value Filter value. - * @returns {undefined} - */ - onChangeFilter(event, { value }) { - const self = this; - clearTimeout(self.filterTimeout); - this.setState( - { - filter: value, - }, - () => { - self.filterTimeout = setTimeout(() => { - self.fetchContents(); - }, 200); - }, - ); - } - - /** - * On change selected values (Filter) - * @method onChangeSelected - * @param {object} event Event object. - * @param {string} value Filter value. - * @returns {undefined} - */ - onChangeSelected(event, { value }) { - event.stopPropagation(); - const { items, selected } = this.state; - - const filteredItems = filter(selected, (selectedItem) => - find(items, (item) => item['@id'] === selectedItem) - .title.toLowerCase() - .includes(value.toLowerCase()), - ); - - this.setState({ - filteredItems, - selectedMenuFilter: value, - }); - } - - /** - * On change page - * @method onChangePage - * @param {object} event Event object. - * @param {string} value Page value. - * @returns {undefined} - */ - onChangePage(event, { value }) { - this.setState( - { - currentPage: value, - }, - () => this.fetchContents(), - ); - } - - /** - * On change page size - * @method onChangePageSize - * @param {object} event Event object. - * @param {string} value Page size value. - * @returns {undefined} - */ - onChangePageSize(event, { value }) { - this.setState( - { - pageSize: value, - currentPage: 0, - }, - () => this.fetchContents(), - ); - } - - /** - * On order index - * @method onOrderIndex - * @param {number} index Index - * @param {number} delta Delta - * @returns {undefined} - */ - onOrderIndex(index, delta) { - this.setState({ - index: { - ...this.state.index, - order: move(this.state.index.order, index, index + delta), - }, - }); - this.props.updateColumnsContent( - getBaseUrl(this.props.pathname), - this.state.index, - ); - } - - /** - * On order item - * @method onOrderItem - * @param {string} id Item id - * @param {number} itemIndex Item index - * @param {number} delta Delta - * @returns {undefined} - */ - onOrderItem(id, itemIndex, delta, backend) { - if (backend) { - this.props.orderContent( - getBaseUrl(this.props.pathname), - id.replace(/^.*\//, ''), - delta, - ); - } else { - this.setState({ - items: move(this.state.items, itemIndex, itemIndex + delta), - }); - } - } - - /** - * On sort items - * @method onSortItems - * @param {object} event Event object - * @param {string} value Item index - * @returns {undefined} - */ - onSortItems(event, { value }) { - const values = value.split('|'); - this.setState({ - sort_on: values[0], - sort_order: values[1], - }); - this.props.sortContent( - getBaseUrl(this.props.pathname), - values[0], - values[1], - ); - } - - /** - * On move to top - * @method onMoveToTop - * @param {object} event Event object - * @param {string} value Item index - * @returns {undefined} - */ - onMoveToTop(event, { value }) { - const id = this.state.items[value]['@id']; - this.props - .orderContent( - getBaseUrl(this.props.pathname), - id.replace(/^.*\//, ''), - 'top', - ) - .then(() => { - this.setState( - { - currentPage: 0, - }, - () => this.fetchContents(), - ); - }); - } - - /** - * On move to bottom - * @method onMoveToBottom - * @param {object} event Event object - * @param {string} value Item index - * @returns {undefined} - */ - onMoveToBottom(event, { value }) { - const id = this.state.items[value]['@id']; - this.props - .orderContent( - getBaseUrl(this.props.pathname), - id.replace(/^.*\//, ''), - 'bottom', - ) - .then(() => { - this.setState( - { - currentPage: 0, - }, - () => this.fetchContents(), - ); - }); - } - - /** - * On delete ok - * @method onDeleteOk - * @returns {undefined} - */ - onDeleteOk() { - this.props.deleteContent(this.state.itemsToDelete); - this.setState({ - showDelete: false, - itemsToDelete: [], - selected: [], - }); - } - - /** - * On delete cancel - * @method onDeleteCancel - * @returns {undefined} - */ - onDeleteCancel() { - this.setState({ - showDelete: false, - itemsToDelete: [], - }); - } - - /** - * On upload ok - * @method onUploadOk - * @returns {undefined} - */ - onUploadOk() { - this.fetchContents(); - this.setState({ - showUpload: false, - }); - } - - /** - * On upload cancel - * @method onUploadCancel - * @returns {undefined} - */ - onUploadCancel() { - this.setState({ - showUpload: false, - }); - } - - /** - * On rename ok - * @method onRenameOk - * @returns {undefined} - */ - onRenameOk() { - this.setState({ - showRename: false, - selected: [], - }); - } - - /** - * On rename cancel - * @method onRenameCancel - * @returns {undefined} - */ - onRenameCancel() { - this.setState({ - showRename: false, - }); - } - - /** - * On tags ok - * @method onTagsOk - * @returns {undefined} - */ - onTagsOk() { - this.setState({ - showTags: false, - selected: [], - }); - } - - /** - * On tags cancel - * @method onTagsCancel - * @returns {undefined} - */ - onTagsCancel() { - this.setState({ - showTags: false, - }); - } - - /** - * On properties ok - * @method onPropertiesOk - * @returns {undefined} - */ - onPropertiesOk() { - this.setState({ - showProperties: false, - selected: [], - }); - } - - /** - * On properties cancel - * @method onPropertiesCancel - * @returns {undefined} - */ - onPropertiesCancel() { - this.setState({ - showProperties: false, - }); - } - - /** - * On workflow ok - * @method onWorkflowOk - * @returns {undefined} - */ - onWorkflowOk() { - this.fetchContents(); - this.setState({ - showWorkflow: false, - selected: [], - }); - this.props.toastify.toast.success( - , - ); - } - - /** - * On workflow cancel - * @method onWorkflowCancel - * @returns {undefined} - */ - onWorkflowCancel() { - this.setState({ - showWorkflow: false, - }); - } - - /** - * Get field by id - * @method getFieldById - * @param {string} id Id of object - * @param {string} field Field of object - * @returns {string} Field of object - */ - getFieldById(id, field) { - const item = find(this.state.items, { '@id': id }); - return item ? item[field] : ''; - } - - /** - * Fetch contents handler - * @method fetchContents - * @param {string} pathname Pathname to fetch contents. - * @returns {undefined} - */ - fetchContents(pathname) { - if (this.state.pageSize === this.props.intl.formatMessage(messages.all)) { - //'All' - this.props.searchContent(getBaseUrl(pathname || this.props.pathname), { - 'path.depth': 1, - sort_on: this.state.sort_on, - sort_order: this.state.sort_order, - metadata_fields: '_all', - b_size: 100000000, - show_inactive: true, - ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), - }); - } else { - this.props.searchContent(getBaseUrl(pathname || this.props.pathname), { - 'path.depth': 1, - sort_on: this.state.sort_on, - sort_order: this.state.sort_order, - metadata_fields: '_all', - ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), - b_size: this.state.pageSize, - b_start: this.state.currentPage * this.state.pageSize, - show_inactive: true, - }); - } - } - - /** - * Cut handler - * @method cut - * @param {Object} event Event object. - * @param {string} value Value of the event. - * @returns {undefined} - */ - cut(event, { value }) { - this.props.cut(value ? [value] : this.state.selected); - this.onSelectNone(); - this.props.toastify.toast.success( - , - ); - } - - /** - * Copy handler - * @method copy - * @param {Object} event Event object. - * @param {string} value Value of the event. - * @returns {undefined} - */ - copy(event, { value }) { - this.props.copy(value ? [value] : this.state.selected); - this.onSelectNone(); - this.props.toastify.toast.success( - , - ); - } - - /** - * Delete handler - * @method delete - * @param {Object} event Event object. - * @param {string} value Value of the event. - * @returns {undefined} - */ - delete(event, { value }) { - this.setState({ - showDelete: true, - itemsToDelete: value ? [value] : this.state.selected, - }); - } - - /** - * Upload handler - * @method upload - * @returns {undefined} - */ - upload() { - this.setState({ - showUpload: true, - }); - } - - /** - * Rename handler - * @method rename - * @returns {undefined} - */ - rename() { - this.setState({ - showRename: true, - }); - } - - /** - * Tags handler - * @method tags - * @returns {undefined} - */ - tags() { - this.setState({ - showTags: true, - }); - } - - /** - * Properties handler - * @method properties - * @returns {undefined} - */ - properties() { - this.setState({ - showProperties: true, - }); - } - - /** - * Workflow handler - * @method workflow - * @returns {undefined} - */ - workflow() { - this.setState({ - showWorkflow: true, - }); - } - - /** - * Paste handler - * @method paste - * @returns {undefined} - */ - paste() { - if (this.props.action === 'copy') { - this.props.copyContent( - this.props.source, - getBaseUrl(this.props.pathname), - ); - } - if (this.props.action === 'cut') { - this.props.moveContent( - this.props.source, - getBaseUrl(this.props.pathname), - ); - } - } - - /** - * Render method. - * @method render - * @returns {string} Markup for the component. - */ - render() { - const selected = this.state.selected.length > 0; - const filteredItems = this.state.filteredItems || this.state.selected; - const path = getBaseUrl(this.props.pathname); - const folderContentsAction = find(this.props.objectActions, { - id: 'folderContents', - }); - const loading = - (this.props.clipboardRequest?.loading && - !this.props.clipboardRequest?.error) || - (this.props.deleteRequest?.loading && !this.props.deleteRequest?.error) || - (this.props.updateRequest?.loading && !this.props.updateRequest?.error) || - (this.props.orderRequest?.loading && !this.props.orderRequest?.error) || - (this.props.searchRequest?.loading && !this.props.searchRequest?.error); - - const Container = - config.getComponent({ name: 'Container' }).component || SemanticContainer; - - return this.props.token && this.props.objectActions?.length > 0 ? ( - <> - {folderContentsAction ? ( - - - - - {this.props.intl.formatMessage(messages.loading)} - - - - -
-
- - {this.state.itemsToDelete.length > 1 ? ( - this.state.containedItemsToDelete > 0 ? ( - <> - - {this.state.containedItemsToDelete} - - ), - variation: ( - - {this.state.containedItemsToDelete === - 1 ? ( - - ) : ( - - )} - - ), - }} - /> - {this.state.brokenReferences > 0 && ( - <> -
- - {this.state.brokenReferences} - - ), - variation: ( - - {this.state.brokenReferences === 1 ? ( - - ) : ( - - )} - - ), - }} - /> - - )} - - ) : ( - <> - {this.state.brokenReferences > 0 && ( - <> - - {this.state.brokenReferences} - - ), - variation: ( - - {this.state.brokenReferences === 1 ? ( - - ) : ( - - )} - - ), - }} - /> - - )} - - ) - ) : this.state.containedItemsToDelete > 0 ? ( - <> - - {this.state.containedItemsToDelete} - - ), - variation: ( - - {this.state.containedItemsToDelete === 1 ? ( - - ) : ( - - )} - - ), - }} - /> - {this.state.brokenReferences > 0 && ( - <> -
- {this.state.brokenReferences} - ), - variation: ( - - {this.state.brokenReferences === 1 ? ( - - ) : ( - - )} - - ), - }} - /> -
- -
    - {this.state.breaches.map((breach) => ( -
  • - - {breach.source.title} - {' '} - refers to{' '} - {breach.targets - .map((target) => ( - - {target.title} - - )) - .reduce((result, item) => ( - <> - {result}, {item} - - ))} -
  • - ))} -
- {this.state.linksAndReferencesViewLink && ( - - - - )} -
- - )} - - ) : this.state.brokenReferences > 0 ? ( - <> - {this.state.brokenReferences} - ), - variation: ( - - {this.state.brokenReferences === 1 ? ( - - ) : ( - - )} - - ), - }} - /> -
- -
    - {this.state.breaches.map((breach) => ( -
  • - - {breach.source.title} - {' '} - refers to{' '} - {breach.targets - .map((target) => ( - - {target.title} - - )) - .reduce((result, item) => ( - <> - {result}, {item} - - ))} -
  • - ))} -
- {this.state.linksAndReferencesViewLink && ( - - - - )} -
- - ) : null} -
- } - onCancel={this.onDeleteCancel} - onConfirm={this.onDeleteOk} - size="medium" - /> - - ({ - url: item, - title: this.getFieldById(item, 'title'), - id: this.getFieldById(item, 'id'), - }))} - /> - ({ - url: item, - subjects: this.getFieldById(item, 'Subject'), - }))} - /> - - {this.state.showWorkflow && ( - - )} -
- - - - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.upload, - )} - size="mini" - /> - - - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.rename, - )} - size="mini" - /> - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.state, - )} - size="mini" - /> - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.tags, - )} - size="mini" - /> - - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.properties, - )} - size="mini" - /> - - - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.cut, - )} - size="mini" - /> - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.copy, - )} - size="mini" - /> - - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.paste, - )} - size="mini" - /> - - - - - } - position="top center" - content={this.props.intl.formatMessage( - messages.delete, - )} - size="mini" - /> - - -
- - {this.state.filter && ( - - )} - -
-
- -
- - - - } - className="right floating selectIndex" - > - - - - {map( - filter( - this.state.index.order, - (index) => index !== 'sortable_title', - ), - (index) => ( - - {this.state.index.values[index].selected ? ( - - ) : ( - - )} - - {' '} - {this.props.intl.formatMessage({ - id: this.state.index.values[index] - .label, - defaultMessage: - this.state.index.values[index].label, - })} - - - ), - )} - - - - -
- - - - - - } - > - - - {map( - [ - 'id', - 'sortable_title', - 'EffectiveDate', - 'CreationDate', - 'ModificationDate', - 'portal_type', - ], - (index) => ( - - } - text={this.props.intl.formatMessage({ - id: Indexes[index].label, - })} - > - - - {' '} - - - - {' '} - - - - - ), - )} - - - - - 0 - ? '#007eb1' - : '#826a6a' - } - className="dropdown-popup-trigger" - size="24px" - /> - } - > - - - - {' '} - - - - {' '} - - - - - } - iconPosition="left" - className="item search" - placeholder={this.props.intl.formatMessage( - messages.filter, - )} - value={ - this.state.selectedMenuFilter || '' - } - onChange={this.onChangeSelected} - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - }} - /> - - {map(filteredItems, (item) => ( - - {' '} - {this.getFieldById(item, 'title')} - - ))} - - - - - - - - {map( - this.state.index.order, - (index, order) => - this.state.index.values[index].selected && ( - - ), - )} - - - - - - - {this.state.items.map((item, order) => ( - ({ - id: index, - type: this.state.index.values[index].type, - })), - (index) => - this.state.index.values[index.id].selected, - )} - onCut={this.cut} - onCopy={this.copy} - onDelete={this.delete} - onOrderItem={this.onOrderItem} - onMoveToTop={this.onMoveToTop} - onMoveToBottom={this.onMoveToBottom} - /> - ))} - -
-
- -
- -
-
-
- - - {this.state.isClient && ( - - - - - } - /> - - )} -
-
- ) : ( - - )} - - ) : ( - - ); - } -} - -let dndContext; - -const DragDropConnector = (props) => { - const { DragDropContext } = props.reactDnd; - const HTML5Backend = props.reactDndHtml5Backend.default; - - const DndConnectedContents = React.useMemo(() => { - if (!dndContext) { - dndContext = DragDropContext(HTML5Backend); - } - return dndContext(Contents); - }, [DragDropContext, HTML5Backend]); - - return ; -}; - -export const __test__ = compose( - injectIntl, - injectLazyLibs(['toastify', 'reactDnd']), - connect( - (store, props) => { - return { - token: store.userSession.token, - items: store.search.items, - sort: store.content.update.sort, - index: store.content.updatecolumns.idx, - breadcrumbs: store.breadcrumbs.items, - total: store.search.total, - searchRequest: { - loading: store.search.loading, - loaded: store.search.loaded, - }, - pathname: props.location.pathname, - action: store.clipboard.action, - source: store.clipboard.source, - clipboardRequest: store.clipboard.request, - deleteRequest: store.content.delete, - updateRequest: store.content.update, - objectActions: store.actions.actions.object, - orderRequest: store.content.order, - }; - }, - { - searchContent, - cut, - copy, - copyContent, - deleteContent, - listActions, - moveContent, - orderContent, - sortContent, - updateColumnsContent, - linkIntegrityCheck, - getContent, - }, - ), -)(Contents); - -export default compose( - injectIntl, - connect( - (store, props) => { - return { - token: store.userSession.token, - items: store.search.items, - sort: store.content.update.sort, - index: store.content.updatecolumns.idx, - breadcrumbs: store.breadcrumbs.items, - total: store.search.total, - searchRequest: { - loading: store.search.loading, - loaded: store.search.loaded, - }, - pathname: props.location.pathname, - action: store.clipboard.action, - source: store.clipboard.source, - clipboardRequest: store.clipboard.request, - deleteRequest: store.content.delete, - updateRequest: store.content.update, - objectActions: store.actions.actions.object, - orderRequest: store.content.order, - }; - }, - { - searchContent, - cut, - copy, - copyContent, - deleteContent, - listActions, - moveContent, - orderContent, - sortContent, - updateColumnsContent, - linkIntegrityCheck, - getContent, - }, - ), - asyncConnect([ - { - key: 'actions', - // Dispatch async/await to make the operation synchronous, otherwise it returns - // before the promise is resolved - promise: async ({ location, store: { dispatch } }) => - await dispatch(listActions(getBaseUrl(location.pathname))), - }, - ]), - injectLazyLibs(['toastify', 'reactDnd', 'reactDndHtml5Backend']), -)(DragDropConnector); +/** + * Contents component. + * @module components/manage/Contents/Contents + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { Portal } from 'react-portal'; +import { Link } from 'react-router-dom'; +import { + Button, + Confirm, + Container as SemanticContainer, + Divider, + Dropdown, + Menu, + Input, + Segment, + Table, + Loader, + Dimmer, +} from 'semantic-ui-react'; +import { + concat, + filter, + find, + indexOf, + keys, + map, + mapValues, + pull, +} from 'lodash'; +import move from 'lodash-move'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { asyncConnect } from '@plone/volto/helpers'; +import { flattenToAppURL } from '@plone/volto/helpers'; + +import { + searchContent, + cut, + copy, + copyContent, + deleteContent, + listActions, + moveContent, + orderContent, + sortContent, + updateColumnsContent, + linkIntegrityCheck, + getContent, +} from '@plone/volto/actions'; +import Indexes, { defaultIndexes } from '@plone/volto/constants/Indexes'; +import { + ContentsBreadcrumbs, + ContentsIndexHeader, + ContentsItem, + ContentsRenameModal, + ContentsUploadModal, + ContentsWorkflowModal, + ContentsTagsModal, + ContentsPropertiesModal, + Pagination, + Popup, + Toolbar, + Toast, + Icon, + Unauthorized, +} from '@plone/volto/components'; + +import { Helmet, getBaseUrl } from '@plone/volto/helpers'; +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; +import config from '@plone/volto/registry'; + +import backSVG from '@plone/volto/icons/back.svg'; +import cutSVG from '@plone/volto/icons/cut.svg'; +import deleteSVG from '@plone/volto/icons/delete.svg'; +import copySVG from '@plone/volto/icons/copy.svg'; +import tagSVG from '@plone/volto/icons/tag.svg'; +import renameSVG from '@plone/volto/icons/rename.svg'; +import semaphoreSVG from '@plone/volto/icons/semaphore.svg'; +import uploadSVG from '@plone/volto/icons/upload.svg'; +import propertiesSVG from '@plone/volto/icons/properties.svg'; +import pasteSVG from '@plone/volto/icons/paste.svg'; +import zoomSVG from '@plone/volto/icons/zoom.svg'; +import checkboxUncheckedSVG from '@plone/volto/icons/checkbox-unchecked.svg'; +import checkboxCheckedSVG from '@plone/volto/icons/checkbox-checked.svg'; +import checkboxIndeterminateSVG from '@plone/volto/icons/checkbox-indeterminate.svg'; +import configurationSVG from '@plone/volto/icons/configuration-app.svg'; +import sortDownSVG from '@plone/volto/icons/sort-down.svg'; +import sortUpSVG from '@plone/volto/icons/sort-up.svg'; +import downKeySVG from '@plone/volto/icons/down-key.svg'; +import moreSVG from '@plone/volto/icons/more.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; + +const messages = defineMessages({ + back: { + id: 'Back', + defaultMessage: 'Back', + }, + contents: { + id: 'Contents', + defaultMessage: 'Contents', + }, + copy: { + id: 'Copy', + defaultMessage: 'Copy', + }, + cut: { + id: 'Cut', + defaultMessage: 'Cut', + }, + error: { + id: "You can't paste this content here", + defaultMessage: "You can't paste this content here", + }, + delete: { + id: 'Delete', + defaultMessage: 'Delete', + }, + deleteConfirmSingleItem: { + id: 'Delete this item?', + defaultMessage: 'Delete this item?', + }, + deleteConfirmMultipleItems: { + id: 'Delete selected items?', + defaultMessage: 'Delete selected items?', + }, + deleteError: { + id: 'The item could not be deleted.', + defaultMessage: 'The item could not be deleted.', + }, + loading: { + id: 'loading', + defaultMessage: 'Loading', + }, + home: { + id: 'Home', + defaultMessage: 'Home', + }, + filter: { + id: 'Filter…', + defaultMessage: 'Filter…', + }, + messageCopied: { + id: 'Item(s) copied.', + defaultMessage: 'Item(s) copied.', + }, + messageCut: { + id: 'Item(s) cut.', + defaultMessage: 'Item(s) cut.', + }, + messageUpdate: { + id: 'Item(s) has been updated.', + defaultMessage: 'Item(s) has been updated.', + }, + messageReorder: { + id: 'Item succesfully moved.', + defaultMessage: 'Item successfully moved.', + }, + messagePasted: { + id: 'Item(s) pasted.', + defaultMessage: 'Item(s) pasted.', + }, + messageWorkflowUpdate: { + id: 'Item(s) state has been updated.', + defaultMessage: 'Item(s) state has been updated.', + }, + paste: { + id: 'Paste', + defaultMessage: 'Paste', + }, + properties: { + id: 'Properties', + defaultMessage: 'Properties', + }, + rearrangeBy: { + id: 'Rearrange items by…', + defaultMessage: 'Rearrange items by…', + }, + rename: { + id: 'Rename', + defaultMessage: 'Rename', + }, + select: { + id: 'Select…', + defaultMessage: 'Select…', + }, + selected: { + id: '{count} selected', + defaultMessage: '{count} selected', + }, + selectColumns: { + id: 'Select columns to show', + defaultMessage: 'Select columns to show', + }, + sort: { + id: 'sort', + defaultMessage: 'sort', + }, + state: { + id: 'State', + defaultMessage: 'State', + }, + tags: { + id: 'Tags', + defaultMessage: 'Tags', + }, + upload: { + id: 'Upload', + defaultMessage: 'Upload', + }, + success: { + id: 'Success', + defaultMessage: 'Success', + }, + publicationDate: { + id: 'Publication date', + defaultMessage: 'Publication date', + }, + createdOn: { + id: 'Created on', + defaultMessage: 'Created on', + }, + expirationDate: { + id: 'Expiration date', + defaultMessage: 'Expiration date', + }, + id: { + id: 'ID', + defaultMessage: 'ID', + }, + uid: { + id: 'UID', + defaultMessage: 'UID', + }, + reviewState: { + id: 'Review state', + defaultMessage: 'Review state', + }, + folder: { + id: 'Folder', + defaultMessage: 'Folder', + }, + excludedFromNavigation: { + id: 'Excluded from navigation', + defaultMessage: 'Excluded from navigation', + }, + objectSize: { + id: 'Object Size', + defaultMessage: 'Object Size', + }, + lastCommentedDate: { + id: 'Last comment date', + defaultMessage: 'Last comment date', + }, + totalComments: { + id: 'Total comments', + defaultMessage: 'Total comments', + }, + creator: { + id: 'Creator', + defaultMessage: 'Creator', + }, + endDate: { + id: 'End Date', + defaultMessage: 'End Date', + }, + startDate: { + id: 'Start Date', + defaultMessage: 'Start Date', + }, + all: { + id: 'All', + defaultMessage: 'All', + }, +}); + +/** + * Contents class. + * @class Contents + * @extends Component + */ +class Contents extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + action: PropTypes.string, + source: PropTypes.arrayOf(PropTypes.string), + searchContent: PropTypes.func.isRequired, + cut: PropTypes.func.isRequired, + copy: PropTypes.func.isRequired, + copyContent: PropTypes.func.isRequired, + deleteContent: PropTypes.func.isRequired, + moveContent: PropTypes.func.isRequired, + orderContent: PropTypes.func.isRequired, + sortContent: PropTypes.func.isRequired, + updateColumnsContent: PropTypes.func.isRequired, + linkIntegrityCheck: PropTypes.func.isRequired, + clipboardRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + deleteRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + updateRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + searchRequest: PropTypes.shape({ + loading: PropTypes.bool, + loaded: PropTypes.bool, + }).isRequired, + items: PropTypes.arrayOf( + PropTypes.shape({ + '@id': PropTypes.string, + '@type': PropTypes.string, + title: PropTypes.string, + description: PropTypes.string, + }), + ), + breadcrumbs: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string, + url: PropTypes.string, + }), + ).isRequired, + total: PropTypes.number.isRequired, + pathname: PropTypes.string.isRequired, + }; + + /** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + items: [], + action: null, + source: null, + index: { + order: keys(Indexes), + values: mapValues(Indexes, (value, key) => ({ + ...value, + selected: indexOf(defaultIndexes, key) !== -1, + })), + selectedCount: defaultIndexes.length + 1, + }, + }; + + /** + * Constructor + * @method constructor + * @param {Object} props Component properties + * @constructs ContentsComponent + */ + constructor(props) { + super(props); + this.onDeselect = this.onDeselect.bind(this); + this.onSelect = this.onSelect.bind(this); + this.onSelectAll = this.onSelectAll.bind(this); + this.onSelectIndex = this.onSelectIndex.bind(this); + this.onSelectNone = this.onSelectNone.bind(this); + this.onDeleteOk = this.onDeleteOk.bind(this); + this.onDeleteCancel = this.onDeleteCancel.bind(this); + this.onUploadOk = this.onUploadOk.bind(this); + this.onUploadCancel = this.onUploadCancel.bind(this); + this.onRenameOk = this.onRenameOk.bind(this); + this.onRenameCancel = this.onRenameCancel.bind(this); + this.onTagsOk = this.onTagsOk.bind(this); + this.onTagsCancel = this.onTagsCancel.bind(this); + this.onPropertiesOk = this.onPropertiesOk.bind(this); + this.onPropertiesCancel = this.onPropertiesCancel.bind(this); + this.onWorkflowOk = this.onWorkflowOk.bind(this); + this.onWorkflowCancel = this.onWorkflowCancel.bind(this); + this.onChangeFilter = this.onChangeFilter.bind(this); + this.onChangePage = this.onChangePage.bind(this); + this.onChangePageSize = this.onChangePageSize.bind(this); + this.onOrderIndex = this.onOrderIndex.bind(this); + this.onOrderItem = this.onOrderItem.bind(this); + this.onSortItems = this.onSortItems.bind(this); + this.onMoveToTop = this.onMoveToTop.bind(this); + this.onChangeSelected = this.onChangeSelected.bind(this); + this.onMoveToBottom = this.onMoveToBottom.bind(this); + this.cut = this.cut.bind(this); + this.copy = this.copy.bind(this); + this.delete = this.delete.bind(this); + this.upload = this.upload.bind(this); + this.rename = this.rename.bind(this); + this.tags = this.tags.bind(this); + this.properties = this.properties.bind(this); + this.workflow = this.workflow.bind(this); + this.paste = this.paste.bind(this); + this.fetchContents = this.fetchContents.bind(this); + this.orderTimeout = null; + this.deleteItemsToShowThreshold = 10; + + this.state = { + selected: [], + showDelete: false, + showUpload: false, + showRename: false, + showTags: false, + showProperties: false, + showWorkflow: false, + itemsToDelete: [], + containedItemsToDelete: [], + brokenReferences: 0, + breaches: [], + showAllItemsToDelete: true, + items: this.props.items, + filter: '', + currentPage: 0, + pageSize: 50, + index: this.props.index || { + order: keys(Indexes), + values: mapValues(Indexes, (value, key) => ({ + ...value, + selected: indexOf(defaultIndexes, key) !== -1, + })), + selectedCount: defaultIndexes.length + 1, + }, + sort_on: this.props.sort?.on || 'getObjPositionInParent', + sort_order: this.props.sort?.order || 'ascending', + isClient: false, + linkIntegrityBreakages: [], + }; + this.filterTimeout = null; + } + + /** + * Component did mount + * @method componentDidMount + * @returns {undefined} + */ + componentDidMount() { + this.fetchContents(); + this.setState({ isClient: true }); + } + async componentDidUpdate(_, prevState) { + if ( + this.state.itemsToDelete !== prevState.itemsToDelete && + this.state.itemsToDelete.length > 0 + ) { + const linkintegrityInfo = await this.props.linkIntegrityCheck( + map(this.state.itemsToDelete, (item) => this.getFieldById(item, 'UID')), + ); + const containedItems = linkintegrityInfo + .map((result) => result.items_total ?? 0) + .reduce((acc, value) => acc + value, 0); + const breaches = linkintegrityInfo.flatMap((result) => + result.breaches.map((source) => ({ + source: source, + target: result, + })), + ); + const source_by_uid = breaches.reduce( + (acc, value) => acc.set(value.source.uid, value.source), + new Map(), + ); + const by_source = breaches.reduce((acc, value) => { + if (acc.get(value.source.uid) === undefined) { + acc.set(value.source.uid, new Set()); + } + acc.get(value.source.uid).add(value.target); + return acc; + }, new Map()); + + this.setState({ + containedItemsToDelete: containedItems, + brokenReferences: by_source.size, + linksAndReferencesViewLink: linkintegrityInfo.length + ? linkintegrityInfo[0]['@id'] + '/links-to-item' + : null, + breaches: Array.from(by_source, (entry) => ({ + source: source_by_uid.get(entry[0]), + targets: Array.from(entry[1]), + })), + showAllItemsToDelete: + this.state.itemsToDelete.length < this.deleteItemsToShowThreshold, + }); + } + } + + /** + * Component will receive props + * @method componentWillReceiveProps + * @param {Object} nextProps Next properties + * @returns {undefined} + */ + UNSAFE_componentWillReceiveProps(nextProps) { + if ( + (this.props.clipboardRequest.loading && + nextProps.clipboardRequest.loaded) || + (this.props.deleteRequest.loading && nextProps.deleteRequest.loaded) || + (this.props.updateRequest.loading && nextProps.updateRequest.loaded) + ) { + this.fetchContents(nextProps.pathname); + } + if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) { + this.props.toastify.toast.success( + , + ); + } + if (this.props.pathname !== nextProps.pathname) { + // Refetching content to sync the current object in the toolbar + this.props.getContent(getBaseUrl(nextProps.pathname)); + this.setState( + { + currentPage: 0, + }, + () => + this.setState({ filter: '' }, () => + this.fetchContents(nextProps.pathname), + ), + ); + } + if (this.props.searchRequest.loading && nextProps.searchRequest.loaded) { + this.setState({ + items: nextProps.items, + }); + } + if ( + this.props.clipboardRequest.loading && + nextProps.clipboardRequest.error + ) { + this.props.toastify.toast.error( + , + ); + } + + if ( + this.props.clipboardRequest.loading && + nextProps.clipboardRequest.loaded + ) { + this.props.toastify.toast.success( + , + ); + } + + if (this.props.deleteRequest.loading && nextProps.deleteRequest.error) { + this.props.toastify.toast.error( + , + ); + } + + if (this.props.orderRequest.loading && nextProps.orderRequest.loaded) { + this.props.toastify.toast.success( + , + ); + } + } + + /** + * On deselect handler + * @method onDeselect + * @param {object} event Event object + * @param {string} value Value + * @returns {undefined} + */ + onDeselect(event, { value }) { + this.setState({ + selected: pull(this.state.selected, value), + }); + } + + /** + * On select handler + * @method onSelect + * @param {object} event Event object + * @returns {undefined} + */ + onSelect(event, id) { + if (indexOf(this.state.selected, id) === -1) { + this.setState({ + selected: concat(this.state.selected, id), + }); + } else { + this.setState({ + selected: pull(this.state.selected, id), + }); + } + } + + /** + * On select all handler + * @method onSelectAll + * @returns {undefined} + */ + onSelectAll() { + this.setState({ + selected: map(this.state.items, (item) => item['@id']), + }); + } + + /** + * On select none handler + * @method onSelectNone + * @returns {undefined} + */ + onSelectNone() { + this.setState({ + selected: [], + }); + } + + /** + * On select index + * @method onSelectIndex + * @param {object} event Event object. + * @param {string} value Index value. + * @returns {undefined} + */ + onSelectIndex(event, { value }) { + let newIndex = { + ...this.state.index, + selectedCount: + this.state.index.selectedCount + + (this.state.index.values[value].selected ? -1 : 1), + values: mapValues(this.state.index.values, (indexValue, indexKey) => ({ + ...indexValue, + selected: + indexKey === value ? !indexValue.selected : indexValue.selected, + })), + }; + this.setState({ + index: newIndex, + }); + this.props.updateColumnsContent(getBaseUrl(this.props.pathname), newIndex); + } + + /** + * On change filter + * @method onChangeFilter + * @param {object} event Event object. + * @param {string} value Filter value. + * @returns {undefined} + */ + onChangeFilter(event, { value }) { + const self = this; + clearTimeout(self.filterTimeout); + this.setState( + { + filter: value, + }, + () => { + self.filterTimeout = setTimeout(() => { + self.fetchContents(); + }, 200); + }, + ); + } + + /** + * On change selected values (Filter) + * @method onChangeSelected + * @param {object} event Event object. + * @param {string} value Filter value. + * @returns {undefined} + */ + onChangeSelected(event, { value }) { + event.stopPropagation(); + const { items, selected } = this.state; + + const filteredItems = filter(selected, (selectedItem) => + find(items, (item) => item['@id'] === selectedItem) + .title.toLowerCase() + .includes(value.toLowerCase()), + ); + + this.setState({ + filteredItems, + selectedMenuFilter: value, + }); + } + + /** + * On change page + * @method onChangePage + * @param {object} event Event object. + * @param {string} value Page value. + * @returns {undefined} + */ + onChangePage(event, { value }) { + this.setState( + { + currentPage: value, + }, + () => this.fetchContents(), + ); + } + + /** + * On change page size + * @method onChangePageSize + * @param {object} event Event object. + * @param {string} value Page size value. + * @returns {undefined} + */ + onChangePageSize(event, { value }) { + this.setState( + { + pageSize: value, + currentPage: 0, + }, + () => this.fetchContents(), + ); + } + + /** + * On order index + * @method onOrderIndex + * @param {number} index Index + * @param {number} delta Delta + * @returns {undefined} + */ + onOrderIndex(index, delta) { + this.setState({ + index: { + ...this.state.index, + order: move(this.state.index.order, index, index + delta), + }, + }); + this.props.updateColumnsContent( + getBaseUrl(this.props.pathname), + this.state.index, + ); + } + + /** + * On order item + * @method onOrderItem + * @param {string} id Item id + * @param {number} itemIndex Item index + * @param {number} delta Delta + * @returns {undefined} + */ + onOrderItem(id, itemIndex, delta, backend) { + if (backend) { + this.props.orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + delta, + ); + } else { + this.setState({ + items: move(this.state.items, itemIndex, itemIndex + delta), + }); + } + } + + /** + * On sort items + * @method onSortItems + * @param {object} event Event object + * @param {string} value Item index + * @returns {undefined} + */ + onSortItems(event, { value }) { + const values = value.split('|'); + this.setState({ + sort_on: values[0], + sort_order: values[1], + }); + this.props.sortContent( + getBaseUrl(this.props.pathname), + values[0], + values[1], + ); + } + + /** + * On move to top + * @method onMoveToTop + * @param {object} event Event object + * @param {string} value Item index + * @returns {undefined} + */ + onMoveToTop(event, { value }) { + const id = this.state.items[value]['@id']; + this.props + .orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + 'top', + ) + .then(() => { + this.setState( + { + currentPage: 0, + }, + () => this.fetchContents(), + ); + }); + } + + /** + * On move to bottom + * @method onMoveToBottom + * @param {object} event Event object + * @param {string} value Item index + * @returns {undefined} + */ + onMoveToBottom(event, { value }) { + const id = this.state.items[value]['@id']; + this.props + .orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + 'bottom', + ) + .then(() => { + this.setState( + { + currentPage: 0, + }, + () => this.fetchContents(), + ); + }); + } + + /** + * On delete ok + * @method onDeleteOk + * @returns {undefined} + */ + onDeleteOk() { + this.props.deleteContent(this.state.itemsToDelete); + this.setState({ + showDelete: false, + itemsToDelete: [], + selected: [], + }); + } + + /** + * On delete cancel + * @method onDeleteCancel + * @returns {undefined} + */ + onDeleteCancel() { + this.setState({ + showDelete: false, + itemsToDelete: [], + }); + } + + /** + * On upload ok + * @method onUploadOk + * @returns {undefined} + */ + onUploadOk() { + this.fetchContents(); + this.setState({ + showUpload: false, + }); + } + + /** + * On upload cancel + * @method onUploadCancel + * @returns {undefined} + */ + onUploadCancel() { + this.setState({ + showUpload: false, + }); + } + + /** + * On rename ok + * @method onRenameOk + * @returns {undefined} + */ + onRenameOk() { + this.setState({ + showRename: false, + selected: [], + }); + } + + /** + * On rename cancel + * @method onRenameCancel + * @returns {undefined} + */ + onRenameCancel() { + this.setState({ + showRename: false, + }); + } + + /** + * On tags ok + * @method onTagsOk + * @returns {undefined} + */ + onTagsOk() { + this.setState({ + showTags: false, + selected: [], + }); + } + + /** + * On tags cancel + * @method onTagsCancel + * @returns {undefined} + */ + onTagsCancel() { + this.setState({ + showTags: false, + }); + } + + /** + * On properties ok + * @method onPropertiesOk + * @returns {undefined} + */ + onPropertiesOk() { + this.setState({ + showProperties: false, + selected: [], + }); + } + + /** + * On properties cancel + * @method onPropertiesCancel + * @returns {undefined} + */ + onPropertiesCancel() { + this.setState({ + showProperties: false, + }); + } + + /** + * On workflow ok + * @method onWorkflowOk + * @returns {undefined} + */ + onWorkflowOk() { + this.fetchContents(); + this.setState({ + showWorkflow: false, + selected: [], + }); + this.props.toastify.toast.success( + , + ); + } + + /** + * On workflow cancel + * @method onWorkflowCancel + * @returns {undefined} + */ + onWorkflowCancel() { + this.setState({ + showWorkflow: false, + }); + } + + /** + * Get field by id + * @method getFieldById + * @param {string} id Id of object + * @param {string} field Field of object + * @returns {string} Field of object + */ + getFieldById(id, field) { + const item = find(this.state.items, { '@id': id }); + return item ? item[field] : ''; + } + + /** + * Fetch contents handler + * @method fetchContents + * @param {string} pathname Pathname to fetch contents. + * @returns {undefined} + */ + fetchContents(pathname) { + if (this.state.pageSize === this.props.intl.formatMessage(messages.all)) { + //'All' + this.props.searchContent(getBaseUrl(pathname || this.props.pathname), { + 'path.depth': 1, + sort_on: this.state.sort_on, + sort_order: this.state.sort_order, + metadata_fields: '_all', + b_size: 100000000, + show_inactive: true, + ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), + }); + } else { + this.props.searchContent(getBaseUrl(pathname || this.props.pathname), { + 'path.depth': 1, + sort_on: this.state.sort_on, + sort_order: this.state.sort_order, + metadata_fields: '_all', + ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), + b_size: this.state.pageSize, + b_start: this.state.currentPage * this.state.pageSize, + show_inactive: true, + }); + } + } + + /** + * Cut handler + * @method cut + * @param {Object} event Event object. + * @param {string} value Value of the event. + * @returns {undefined} + */ + cut(event, { value }) { + this.props.cut(value ? [value] : this.state.selected); + this.onSelectNone(); + this.props.toastify.toast.success( + , + ); + } + + /** + * Copy handler + * @method copy + * @param {Object} event Event object. + * @param {string} value Value of the event. + * @returns {undefined} + */ + copy(event, { value }) { + this.props.copy(value ? [value] : this.state.selected); + this.onSelectNone(); + this.props.toastify.toast.success( + , + ); + } + + /** + * Delete handler + * @method delete + * @param {Object} event Event object. + * @param {string} value Value of the event. + * @returns {undefined} + */ + delete(event, { value }) { + this.setState({ + showDelete: true, + itemsToDelete: value ? [value] : this.state.selected, + }); + } + + /** + * Upload handler + * @method upload + * @returns {undefined} + */ + upload() { + this.setState({ + showUpload: true, + }); + } + + /** + * Rename handler + * @method rename + * @returns {undefined} + */ + rename() { + this.setState({ + showRename: true, + }); + } + + /** + * Tags handler + * @method tags + * @returns {undefined} + */ + tags() { + this.setState({ + showTags: true, + }); + } + + /** + * Properties handler + * @method properties + * @returns {undefined} + */ + properties() { + this.setState({ + showProperties: true, + }); + } + + /** + * Workflow handler + * @method workflow + * @returns {undefined} + */ + workflow() { + this.setState({ + showWorkflow: true, + }); + } + + /** + * Paste handler + * @method paste + * @returns {undefined} + */ + paste() { + if (this.props.action === 'copy') { + this.props.copyContent( + this.props.source, + getBaseUrl(this.props.pathname), + ); + } + if (this.props.action === 'cut') { + this.props.moveContent( + this.props.source, + getBaseUrl(this.props.pathname), + ); + } + } + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + const selected = this.state.selected.length > 0; + const filteredItems = this.state.filteredItems || this.state.selected; + const path = getBaseUrl(this.props.pathname); + const folderContentsAction = find(this.props.objectActions, { + id: 'folderContents', + }); + const loading = + (this.props.clipboardRequest?.loading && + !this.props.clipboardRequest?.error) || + (this.props.deleteRequest?.loading && !this.props.deleteRequest?.error) || + (this.props.updateRequest?.loading && !this.props.updateRequest?.error) || + (this.props.orderRequest?.loading && !this.props.orderRequest?.error) || + (this.props.searchRequest?.loading && !this.props.searchRequest?.error); + + const Container = + config.getComponent({ name: 'Container' }).component || SemanticContainer; + + return this.props.token && this.props.objectActions?.length > 0 ? ( + <> + {folderContentsAction ? ( + + + + + {this.props.intl.formatMessage(messages.loading)} + + + + +
+
+ + {this.state.itemsToDelete.length > 1 ? ( + this.state.containedItemsToDelete > 0 ? ( + <> + + {this.state.containedItemsToDelete} + + ), + variation: ( + + {this.state.containedItemsToDelete === + 1 ? ( + + ) : ( + + )} + + ), + }} + /> + {this.state.brokenReferences > 0 && ( + <> +
+ + {this.state.brokenReferences} + + ), + variation: ( + + {this.state.brokenReferences === 1 ? ( + + ) : ( + + )} + + ), + }} + /> + + )} + + ) : ( + <> + {this.state.brokenReferences > 0 && ( + <> + + {this.state.brokenReferences} + + ), + variation: ( + + {this.state.brokenReferences === 1 ? ( + + ) : ( + + )} + + ), + }} + /> + + )} + + ) + ) : this.state.containedItemsToDelete > 0 ? ( + <> + + {this.state.containedItemsToDelete} + + ), + variation: ( + + {this.state.containedItemsToDelete === 1 ? ( + + ) : ( + + )} + + ), + }} + /> + {this.state.brokenReferences > 0 && ( + <> +
+ {this.state.brokenReferences} + ), + variation: ( + + {this.state.brokenReferences === 1 ? ( + + ) : ( + + )} + + ), + }} + /> +
+ +
    + {this.state.breaches.map((breach) => ( +
  • + + {breach.source.title} + {' '} + refers to{' '} + {breach.targets + .map((target) => ( + + {target.title} + + )) + .reduce((result, item) => ( + <> + {result}, {item} + + ))} +
  • + ))} +
+ {this.state.linksAndReferencesViewLink && ( + + + + )} +
+ + )} + + ) : this.state.brokenReferences > 0 ? ( + <> + {this.state.brokenReferences} + ), + variation: ( + + {this.state.brokenReferences === 1 ? ( + + ) : ( + + )} + + ), + }} + /> +
+ +
    + {this.state.breaches.map((breach) => ( +
  • + + {breach.source.title} + {' '} + refers to{' '} + {breach.targets + .map((target) => ( + + {target.title} + + )) + .reduce((result, item) => ( + <> + {result}, {item} + + ))} +
  • + ))} +
+ {this.state.linksAndReferencesViewLink && ( + + + + )} +
+ + ) : null} +
+ } + onCancel={this.onDeleteCancel} + onConfirm={this.onDeleteOk} + size="medium" + /> + + ({ + url: item, + title: this.getFieldById(item, 'title'), + id: this.getFieldById(item, 'id'), + }))} + /> + ({ + url: item, + subjects: this.getFieldById(item, 'Subject'), + }))} + /> + + {this.state.showWorkflow && ( + + )} +
+ + + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.upload, + )} + size="mini" + /> + + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.rename, + )} + size="mini" + /> + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.state, + )} + size="mini" + /> + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.tags, + )} + size="mini" + /> + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.properties, + )} + size="mini" + /> + + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.cut, + )} + size="mini" + /> + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.copy, + )} + size="mini" + /> + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.paste, + )} + size="mini" + /> + + + + + } + position="top center" + content={this.props.intl.formatMessage( + messages.delete, + )} + size="mini" + /> + + +
+ + {this.state.filter && ( + + )} + +
+
+ +
+ + + + } + className="right floating selectIndex" + > + + + + {map( + filter( + this.state.index.order, + (index) => index !== 'sortable_title', + ), + (index) => ( + + {this.state.index.values[index].selected ? ( + + ) : ( + + )} + + {' '} + {this.props.intl.formatMessage({ + id: this.state.index.values[index] + .label, + defaultMessage: + this.state.index.values[index].label, + })} + + + ), + )} + + + + +
+ + + + + + } + > + + + {map( + [ + 'id', + 'sortable_title', + 'EffectiveDate', + 'CreationDate', + 'ModificationDate', + 'portal_type', + ], + (index) => ( + + } + text={this.props.intl.formatMessage({ + id: Indexes[index].label, + })} + > + + + {' '} + + + + {' '} + + + + + ), + )} + + + + + 0 + ? '#007eb1' + : '#826a6a' + } + className="dropdown-popup-trigger" + size="24px" + /> + } + > + + + + {' '} + + + + {' '} + + + + + } + iconPosition="left" + className="item search" + placeholder={this.props.intl.formatMessage( + messages.filter, + )} + value={ + this.state.selectedMenuFilter || '' + } + onChange={this.onChangeSelected} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + /> + + {map(filteredItems, (item) => ( + + {' '} + {this.getFieldById(item, 'title')} + + ))} + + + + + + + + {map( + this.state.index.order, + (index, order) => + this.state.index.values[index].selected && ( + + ), + )} + + + + + + + {this.state.items.map((item, order) => ( + ({ + id: index, + type: this.state.index.values[index].type, + })), + (index) => + this.state.index.values[index.id].selected, + )} + onCut={this.cut} + onCopy={this.copy} + onDelete={this.delete} + onOrderItem={this.onOrderItem} + onMoveToTop={this.onMoveToTop} + onMoveToBottom={this.onMoveToBottom} + /> + ))} + +
+
+ +
+ +
+
+
+ + + {this.state.isClient && ( + + + + + } + /> + + )} +
+
+ ) : ( + + )} + + ) : ( + + ); + } +} + +let dndContext; + +const DragDropConnector = (props) => { + const { DragDropContext } = props.reactDnd; + const HTML5Backend = props.reactDndHtml5Backend.default; + + const DndConnectedContents = React.useMemo(() => { + if (!dndContext) { + dndContext = DragDropContext(HTML5Backend); + } + return dndContext(Contents); + }, [DragDropContext, HTML5Backend]); + + return ; +}; + +export const __test__ = compose( + injectIntl, + injectLazyLibs(['toastify', 'reactDnd']), + connect( + (store, props) => { + return { + token: store.userSession.token, + items: store.search.items, + sort: store.content.update.sort, + index: store.content.updatecolumns.idx, + breadcrumbs: store.breadcrumbs.items, + total: store.search.total, + searchRequest: { + loading: store.search.loading, + loaded: store.search.loaded, + }, + pathname: props.location.pathname, + action: store.clipboard.action, + source: store.clipboard.source, + clipboardRequest: store.clipboard.request, + deleteRequest: store.content.delete, + updateRequest: store.content.update, + objectActions: store.actions.actions.object, + orderRequest: store.content.order, + }; + }, + { + searchContent, + cut, + copy, + copyContent, + deleteContent, + listActions, + moveContent, + orderContent, + sortContent, + updateColumnsContent, + linkIntegrityCheck, + getContent, + }, + ), +)(Contents); + +export default compose( + injectIntl, + connect( + (store, props) => { + return { + token: store.userSession.token, + items: store.search.items, + sort: store.content.update.sort, + index: store.content.updatecolumns.idx, + breadcrumbs: store.breadcrumbs.items, + total: store.search.total, + searchRequest: { + loading: store.search.loading, + loaded: store.search.loaded, + }, + pathname: props.location.pathname, + action: store.clipboard.action, + source: store.clipboard.source, + clipboardRequest: store.clipboard.request, + deleteRequest: store.content.delete, + updateRequest: store.content.update, + objectActions: store.actions.actions.object, + orderRequest: store.content.order, + }; + }, + { + searchContent, + cut, + copy, + copyContent, + deleteContent, + listActions, + moveContent, + orderContent, + sortContent, + updateColumnsContent, + linkIntegrityCheck, + getContent, + }, + ), + asyncConnect([ + { + key: 'actions', + // Dispatch async/await to make the operation synchronous, otherwise it returns + // before the promise is resolved + promise: async ({ location, store: { dispatch } }) => + await dispatch(listActions(getBaseUrl(location.pathname))), + }, + ]), + injectLazyLibs(['toastify', 'reactDnd', 'reactDndHtml5Backend']), +)(DragDropConnector); diff --git a/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap b/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap index 968280ce12..a348d4c3db 100644 --- a/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap +++ b/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap @@ -2,6 +2,7 @@ exports[`Contents renders a folder contents view component 1`] = `