From 2e16de556fc336916034cefa98041a449cfae2f6 Mon Sep 17 00:00:00 2001 From: Sara Bianchi <43245702+SaraBianchi@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:26:36 +0100 Subject: [PATCH] feat: added new configuration of content folder columns (#439) * feat: added new configuration of content folder columns * chore: updated object type with render component from site configuration * chore: updated ContentsItems indexes props --- src/config/italiaConfig.js | 16 + .../components/manage/Contents/Contents.jsx | 411 +++++++++++++----- .../manage/Contents/ContentsItem.jsx | 410 +++++++++++++++++ 3 files changed, 733 insertions(+), 104 deletions(-) create mode 100644 src/customizations/volto/components/manage/Contents/ContentsItem.jsx diff --git a/src/config/italiaConfig.js b/src/config/italiaConfig.js index a5f61ff0f..d44c15c74 100644 --- a/src/config/italiaConfig.js +++ b/src/config/italiaConfig.js @@ -122,6 +122,22 @@ export default function applyConfig(voltoConfig) { Faq: faQuestionSVG, }, + /* lista delle colonne di default da visualizzare nella Content Folder */ + customDefaultIndexes: [ + // ...config.settings.customDefaultIndexes, + // 'ufficio_responsabile_bando', + ], + /* lista delle colonne custom da poter visualizzare nella Content Folder */ + customIndexes: { + // ...config.settings.customIndexes, + // ufficio_responsabile_bando: { + // label: 'Bandi gara - Uff. Resp.', + // type: 'object', + // sort_on: 'ufficio_responsabile_bando', + // component: (item) => <>{item.ufficio_responsabile_bando.title}, + // }, + }, + imageScales: { listing: 16, icon: 32, diff --git a/src/customizations/volto/components/manage/Contents/Contents.jsx b/src/customizations/volto/components/manage/Contents/Contents.jsx index fb07fb120..af17e3080 100644 --- a/src/customizations/volto/components/manage/Contents/Contents.jsx +++ b/src/customizations/volto/components/manage/Contents/Contents.jsx @@ -1,12 +1,13 @@ /** * Contents component. * @module components/manage/Contents/Contents - */ - -/** !!!IMPORTANTE!!! - * CUSTOMIZATION -> FILE DA RIMUOVERE QUANDO AGGIORNIAMO A VOLTO16 - * - added getContent action from '@plone/volto/actions', - * getContent refetching content to sync the current object in the toolbar + * + * * CUSTOMIZATIONS: + * - Changed Indexes and defaultIndexes with spread between Volto objects and customIndex configured from config.js, + * applied in the constructor and changed props with this.Indexes and this.defaultIndexes + * - index in defaultProps is redefined in the constructor with the new properties + * - Filtered Object.keys(config.settings.customIndexes) in dropdown menu map + * - Updated indexes props passed to ContentsItem component */ import React, { Component } from 'react'; @@ -18,13 +19,13 @@ import { Link } from 'react-router-dom'; import { Button, Confirm, - Container, + Container as SemanticContainer, + Divider, Dropdown, Menu, Input, Segment, Table, - Popup, Loader, Dimmer, } from 'semantic-ui-react'; @@ -53,9 +54,9 @@ import { orderContent, sortContent, updateColumnsContent, + linkIntegrityCheck, getContent, } from '@plone/volto/actions'; -import Indexes, { defaultIndexes } from '@plone/volto/constants/Indexes'; import { ContentsBreadcrumbs, ContentsIndexHeader, @@ -66,6 +67,7 @@ import { ContentsTagsModal, ContentsPropertiesModal, Pagination, + Popup, Toolbar, Toast, Icon, @@ -74,6 +76,7 @@ import { 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'; @@ -94,6 +97,11 @@ 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'; + +import VoltoIndexes, { + defaultIndexes as DefaultVoltoIndexes, +} from '@plone/volto/constants/Indexes'; const messages = defineMessages({ back: { @@ -154,7 +162,7 @@ const messages = defineMessages({ }, messageReorder: { id: 'Item succesfully moved.', - defaultMessage: 'Item succesfully moved.', + defaultMessage: 'Item successfully moved.', }, messagePasted: { id: 'Item(s) pasted.', @@ -272,6 +280,35 @@ const messages = defineMessages({ id: 'All', defaultMessage: 'All', }, + linkIntegrityMessageHeader: { + id: 'Potential link breakage', + defaultMessage: 'Potential link breakage', + }, + linkIntegrityMessageBody: { + id: + 'By deleting this item, you will break ' + + 'links that exist in the items listed below. ' + + 'If this is indeed what you want to do, ' + + 'we recommend that remove these references first.', + defaultMessage: + 'By deleting this item, ' + + 'you will break links that exist in the items ' + + 'listed below. If this is indeed what you ' + + 'want to do, we recommend that remove ' + + 'these references first.', + }, + linkIntegrityMessageExtra: { + id: 'This Page is referenced by the following items:', + defaultMessage: 'This Page is referenced by the following items:', + }, + deleteItemCountMessage: { + id: 'Total items to be deleted:', + defaultMessage: 'Total items to be deleted:', + }, + deleteItemMessage: { + id: 'Items to be deleted:', + defaultMessage: 'Items to be deleted:', + }, }); /** @@ -297,6 +334,7 @@ class Contents extends Component { 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, @@ -340,14 +378,14 @@ class Contents extends Component { items: [], action: null, source: null, - index: { - order: keys(Indexes), - values: mapValues(Indexes, (value, key) => ({ - ...value, - selected: indexOf(defaultIndexes, key) !== -1, - })), - selectedCount: defaultIndexes.length + 1, - }, + // index: { + // order: keys(VoltoIndexes), + // values: mapValues(VoltoIndexes, (value, key) => ({ + // ...value, + // selected: indexOf(DefaultVoltoIndexes, key) !== -1, + // })), + // selectedCount: DefaultVoltoIndexes.length + 1, + // }, }; /** @@ -395,6 +433,26 @@ class Contents extends Component { this.paste = this.paste.bind(this); this.fetchContents = this.fetchContents.bind(this); this.orderTimeout = null; + this.deleteItemsToShowThreshold = 10; + this.filterTimeout = null; + this.defaultIndexes = [ + ...DefaultVoltoIndexes, + ...(config.settings.customDefaultIndexes ?? []), + ]; + this.Indexes = { + ...VoltoIndexes, + ...(config.settings.customIndexes ?? {}), + }; + if (!this.index) { + this.index = { + order: keys(this.Indexes), + values: mapValues(this.Indexes, (value, key) => ({ + ...value, + selected: indexOf(this.defaultIndexes, key) !== -1, + })), + selectedCount: this.defaultIndexes.length + 1, + }; + } this.state = { selected: [], showDelete: false, @@ -404,21 +462,23 @@ class Contents extends Component { showProperties: false, showWorkflow: false, itemsToDelete: [], + showAllItemsToDelete: true, items: this.props.items, filter: '', currentPage: 0, pageSize: 50, index: this.props.index || { - order: keys(Indexes), - values: mapValues(Indexes, (value, key) => ({ + order: keys(this.Indexes), + values: mapValues(this.Indexes, (value, key) => ({ ...value, - selected: indexOf(defaultIndexes, key) !== -1, + selected: indexOf(this.defaultIndexes, key) !== -1, })), - selectedCount: defaultIndexes.length + 1, + selectedCount: this.defaultIndexes.length + 1, }, sort_on: this.props.sort?.on || 'getObjPositionInParent', sort_order: this.props.sort?.order || 'ascending', isClient: false, + linkIntegrityBreakages: '', }; this.filterTimeout = null; } @@ -432,6 +492,22 @@ class Contents extends Component { this.fetchContents(); this.setState({ isClient: true }); } + async componentDidUpdate(_, prevState) { + if ( + this.state.itemsToDelete !== prevState.itemsToDelete && + this.state.itemsToDelete.length > 0 + ) { + this.setState({ + linkIntegrityBreakages: await this.props.linkIntegrityCheck( + map(this.state.itemsToDelete, (item) => + this.getFieldById(item, 'UID'), + ), + ), + showAllItemsToDelete: + this.state.itemsToDelete.length < this.deleteItemsToShowThreshold, + }); + } + } /** * Component will receive props @@ -464,7 +540,10 @@ class Contents extends Component { { currentPage: 0, }, - () => this.fetchContents(nextProps.pathname), + () => + this.setState({ filter: '' }, () => + this.fetchContents(nextProps.pathname), + ), ); } if (this.props.searchRequest.loading && nextProps.searchRequest.loaded) { @@ -638,6 +717,7 @@ class Contents extends Component { this.setState({ filteredItems, + selectedMenuFilter: value, }); } @@ -745,18 +825,20 @@ class Contents extends Component { */ onMoveToTop(event, { value }) { const id = this.state.items[value]['@id']; - value = this.state.currentPage * this.state.pageSize + value; - this.props.orderContent( - getBaseUrl(this.props.pathname), - id.replace(/^.*\//, ''), - -value, - ); - this.setState( - { - currentPage: 0, - }, - () => this.fetchContents(), - ); + this.props + .orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + 'top', + ) + .then(() => { + this.setState( + { + currentPage: 0, + }, + () => this.fetchContents(), + ); + }); } /** @@ -767,18 +849,21 @@ class Contents extends Component { * @returns {undefined} */ onMoveToBottom(event, { value }) { - this.onOrderItem( - this.state.items[value]['@id'], - value, - this.state.items.length - 1 - value, - false, - ); - this.onOrderItem( - this.state.items[value]['@id'], - value, - this.state.items.length - 1 - value, - true, - ); + const id = this.state.items[value]['@id']; + this.props + .orderContent( + getBaseUrl(this.props.pathname), + id.replace(/^.*\//, ''), + 'bottom', + ) + .then(() => { + this.setState( + { + currentPage: 0, + }, + () => this.fetchContents(), + ); + }); } /** @@ -957,6 +1042,7 @@ class Contents extends Component { sort_order: this.state.sort_order, metadata_fields: '_all', b_size: 100000000, + show_inactive: true, ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), }); } else { @@ -968,6 +1054,7 @@ class Contents extends Component { ...(this.state.filter && { SearchableText: `${this.state.filter}*` }), b_size: this.state.pageSize, b_start: this.state.currentPage * this.state.pageSize, + show_inactive: true, }); } } @@ -1111,7 +1198,6 @@ class Contents extends Component { const folderContentsAction = find(this.props.objectActions, { id: 'folderContents', }); - const loading = (this.props.clipboardRequest?.loading && !this.props.clipboardRequest?.error) || @@ -1120,6 +1206,9 @@ class Contents extends Component { (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 ? ( @@ -1138,23 +1227,88 @@ class Contents extends Component {
+

+ {this.props.intl.formatMessage( + messages.deleteItemCountMessage, + ) + ` ${this.state.itemsToDelete.length}`} +

+ {!this.state.showAllItemsToDelete && ( + + )} + {this.state.linkIntegrityBreakages.length > 0 ? ( +
+

+ {this.props.intl.formatMessage( + messages.linkIntegrityMessageHeader, + )} +

+

+ {this.props.intl.formatMessage( + messages.linkIntegrityMessageBody, + )} +

+
    + {map( + this.state.linkIntegrityBreakages, + (item) => ( +
  • + {item.title} +

    + {this.props.intl.formatMessage( + messages.linkIntegrityMessageExtra, + )} +

    + +
  • + ), + )} +
+
+ ) : ( +
+ )} } onCancel={this.onDeleteCancel} onConfirm={this.onDeleteOk} - size="mini" + size="medium" /> + {this.state.filter && ( + + )}
@@ -1486,9 +1655,8 @@ class Contents extends Component { {this.props.intl.formatMessage({ id: this.state.index.values[index] .label, - defaultMessage: this.state.index.values[ - index - ].label, + defaultMessage: + this.state.index.values[index].label, })} @@ -1503,24 +1671,26 @@ class Contents extends Component { - } > - - + + config.settings.customIndexes[i] + .sort_on, + ), ], (index) => ( - + } + text={this.props.intl.formatMessage({ + id: this.Indexes[index].label, + })} > - - - + ), )} - - + + - } - icon={null} > - - + - + - - + + - - - + + } iconPosition="left" - className="search" + 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')} - + ))} - - - + + + ({ id: index, - type: this.state.index.values[index].type, + ...this.state.index.values[index], })), (index) => this.state.index.values[index.id].selected, @@ -1787,14 +1984,18 @@ class Contents extends Component { } } +let dndContext; + const DragDropConnector = (props) => { const { DragDropContext } = props.reactDnd; const HTML5Backend = props.reactDndHtml5Backend.default; - const DndConnectedContents = React.useMemo( - () => DragDropContext(HTML5Backend)(Contents), - [DragDropContext, HTML5Backend], - ); + const DndConnectedContents = React.useMemo(() => { + if (!dndContext) { + dndContext = DragDropContext(HTML5Backend); + } + return dndContext(Contents); + }, [DragDropContext, HTML5Backend]); return ; }; @@ -1836,6 +2037,7 @@ export const __test__ = compose( orderContent, sortContent, updateColumnsContent, + linkIntegrityCheck, getContent, }, ), @@ -1877,6 +2079,7 @@ export default compose( orderContent, sortContent, updateColumnsContent, + linkIntegrityCheck, getContent, }, ), diff --git a/src/customizations/volto/components/manage/Contents/ContentsItem.jsx b/src/customizations/volto/components/manage/Contents/ContentsItem.jsx new file mode 100644 index 000000000..2528b2156 --- /dev/null +++ b/src/customizations/volto/components/manage/Contents/ContentsItem.jsx @@ -0,0 +1,410 @@ +/** + * Contents item component. + * @module components/manage/Contents/ContentsItem + * + * CUSTOMIZATIONS: + * - Managed index type of objects -> index.type === 'object' + */ + +import React from 'react'; +import { Button, Table, Menu, Divider } from 'semantic-ui-react'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { map } from 'lodash'; +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; +import { Circle, FormattedDate, Icon, Popup } from '@plone/volto/components'; +import { getContentIcon } from '@plone/volto/helpers'; +import moreSVG from '@plone/volto/icons/more.svg'; +import checkboxUncheckedSVG from '@plone/volto/icons/checkbox-unchecked.svg'; +import checkboxCheckedSVG from '@plone/volto/icons/checkbox-checked.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 showSVG from '@plone/volto/icons/show.svg'; +import moveUpSVG from '@plone/volto/icons/move-up.svg'; +import moveDownSVG from '@plone/volto/icons/move-down.svg'; +import editingSVG from '@plone/volto/icons/editing.svg'; +import dragSVG from '@plone/volto/icons/drag.svg'; +import cx from 'classnames'; + +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; + +const messages = defineMessages({ + private: { + id: 'private', + defaultMessage: 'Private', + }, + pending: { + id: 'pending', + defaultMessage: 'Pending', + }, + published: { + id: 'published', + defaultMessage: 'Published', + }, + intranet: { + id: 'intranet', + defaultMessage: 'Intranet', + }, + draft: { + id: 'draft', + defaultMessage: 'Draft', + }, + no_workflow_state: { + id: 'no workflow state', + defaultMessage: 'No workflow state', + }, + none: { + id: 'None', + defaultMessage: 'None', + }, +}); + +function getColor(string) { + switch (string) { + case 'private': + return '#ed4033'; + case 'published': + return '#007bc1'; + case 'intranet': + return '#51aa55'; + case 'draft': + return '#f6a808'; + default: + return 'grey'; + } +} + +/** + * Contents item component class. + * @function ContentsItemComponent + * @returns {string} Markup of the component. + */ +export const ContentsItemComponent = ({ + item, + selected, + onClick, + indexes, + onCut, + onCopy, + onDelete, + onMoveToTop, + onMoveToBottom, + connectDragPreview, + connectDragSource, + connectDropTarget, + isDragging, + order, +}) => { + const intl = useIntl(); + + return connectDropTarget( + connectDragPreview( + + + {connectDragSource( +
+ +
, + )} +
+ + {selected ? ( + + ) : ( + + )} + + + +
+ {' '} + {item.title} +
+ {item.ExpirationDate !== 'None' && + new Date(item.ExpirationDate).getTime() < + new Date().getTime() && ( + + )} + {item.EffectiveDate !== 'None' && + new Date(item.EffectiveDate).getTime() > new Date().getTime() && ( + + )} + +
+ {map(indexes, (index) => ( + + {index.type === 'boolean' && + (item[index.id] ? ( + + ) : ( + + ))} + {index.type === 'string' && + index.id !== 'review_state' && + item[index.id]} + {index.id === 'review_state' && ( +
+ + + + {messages[item[index.id]] + ? intl.formatMessage(messages[item[index.id]]) + : item['review_title'] || + item['review_state'] || + intl.formatMessage(messages.no_workflow_state)} +
+ )} + {index.type === 'date' && ( + <> + {item[index?.id] && item[index.id] !== 'None' ? ( + + ) : ( + intl.formatMessage(messages.none) + )} + + )} + {index.type === 'array' && ( + {item[index.id]?.join(', ')} + )} + {index.type === 'object' && + (index?.component + ? index.component(item) + : item[index.id]?.map((obj) => obj.title).join(', '))} +
+ ))} + + + } + > + + + {' '} + + + + {' '} + + + + + {' '} + + + + {' '} + + + + {' '} + + + + + {' '} + + + + {' '} + + + + + + , + ), + ); +}; + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +ContentsItemComponent.propTypes = { + item: PropTypes.shape({ + '@id': PropTypes.string, + title: PropTypes.string, + is_folderish: PropTypes.bool, + '@type': PropTypes.string, + }).isRequired, + selected: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + indexes: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + type: PropTypes.string, + }), + ).isRequired, + onCut: PropTypes.func.isRequired, + onCopy: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + onMoveToTop: PropTypes.func.isRequired, + onMoveToBottom: PropTypes.func.isRequired, + connectDragPreview: PropTypes.func.isRequired, + connectDragSource: PropTypes.func.isRequired, + connectDropTarget: PropTypes.func.isRequired, + isDragging: PropTypes.bool.isRequired, + order: PropTypes.number.isRequired, + onOrderItem: PropTypes.func.isRequired, +}; + +const DragDropConnector = (props) => { + const { DropTarget, DragSource } = props.reactDnd; + + const DndConnectedContentsItem = React.useMemo( + () => + DropTarget( + 'item', + { + hover(props, monitor) { + const id = monitor.getItem().id; + const dragOrder = monitor.getItem().order; + const hoverOrder = props.order; + + if (dragOrder === hoverOrder) { + return; + } + + props.onOrderItem(id, dragOrder, hoverOrder - dragOrder, false); + + monitor.getItem().order = hoverOrder; + }, + drop(props, monitor) { + const id = monitor.getItem().id; + const dragOrder = monitor.getItem().startOrder; + const dropOrder = props.order; + + if (dragOrder === dropOrder) { + return; + } + + props.onOrderItem(id, dragOrder, dropOrder - dragOrder, true); + + monitor.getItem().order = dropOrder; + }, + }, + (connect) => ({ + connectDropTarget: connect.dropTarget(), + }), + )( + DragSource( + 'item', + { + beginDrag(props) { + return { + id: props.item['@id'], + order: props.order, + startOrder: props.order, + }; + }, + }, + (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + connectDragPreview: connect.dragPreview(), + isDragging: monitor.isDragging(), + }), + )(ContentsItemComponent), + ), + [DragSource, DropTarget], + ); + + return ; +}; + +export default injectLazyLibs('reactDnd')(DragDropConnector);