UNPKG

@plone/volto

Version:
433 lines (411 loc) 13.2 kB
import React, { useState, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import map from 'lodash/map'; import find from 'lodash/find'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { linkIntegrityCheck } from '@plone/volto/actions/content/content'; import { flattenToAppURL } from '@plone/volto/helpers/Url/Url'; import { Confirm, Dimmer, Loader, Table } from 'semantic-ui-react'; const MAX_LINK_INTEGRITY_BREACHES_TO_SHOW = 5; const messages = defineMessages({ deleteConfirmSingleItem: { id: 'Delete this item?', defaultMessage: 'Delete this item?', }, deleteConfirmMultipleItems: { id: 'Delete selected items?', defaultMessage: 'Delete selected items?', }, navigate_to_this_item: { id: 'Navigate to this item', defaultMessage: 'Navigate to this item', }, loading: { id: 'link-integrity: loading references', defaultMessage: 'Checking references...', }, delete: { id: 'Delete', defaultMessage: 'Delete', }, delete_and_broken_links: { id: 'link-integrity: Delete item and break links', defaultMessage: 'Delete item and break links', }, cancel: { id: 'Cancel', defaultMessage: 'Cancel', }, item: { id: 'item', defaultMessage: 'item', }, items: { id: 'items', defaultMessage: 'items', }, reference: { id: 'reference', defaultMessage: 'reference', }, references: { id: 'references', defaultMessage: 'references', }, folderDeletionSingle: { id: 'This item contains subitems. Deleting it will also delete its {containedItemsToDelete} {variation} inside.', defaultMessage: 'This item contains subitems. Deleting it will also delete its {containedItemsToDelete} {variation} inside.', }, folderDeletionMultiple: { id: 'Some items contain subitems. Deleting them will also delete their {containedItemsToDelete} {variation} inside.', defaultMessage: 'Some items contain subitems. Deleting them will also delete their {containedItemsToDelete} {variation} inside.', }, deleteAllItemsPaginated: { id: 'You are about to delete all items in the current pagination of this folder.', defaultMessage: 'You are about to delete all items in the current pagination of this folder.', }, deleteAllItems: { id: 'You are about to delete all items and all its subitems.', defaultMessage: 'You are about to delete all items and all its subitems.', }, brokenReferencesMultiple: { id: 'Some items are referenced by other contents. Deleting them will break {brokenReferences} {variation}.', defaultMessage: 'Some items are referenced by other contents. Deleting them will break {brokenReferences} {variation}.', }, brokenReferencesSingle: { id: 'Deleting this item will break {brokenReferences} {variation}.', defaultMessage: 'Deleting this item will break {brokenReferences} {variation}.', }, }); const safeUrl = (url) => { if (!url) return '#'; return typeof url === 'string' ? flattenToAppURL(url) : '#'; }; const DeleteItemsList = ({ itemsToDelete, titlesToDelete }) => ( <ul> {itemsToDelete.map((id) => ( <li key={id}> <Link to={safeUrl(id)} target="_blank"> {titlesToDelete[id] || id} </Link> </li> ))} </ul> ); const VariationMessage = ({ count, singularId, pluralId }) => ( <span> {count === 1 ? ( <FormattedMessage {...messages[singularId]} /> ) : ( <FormattedMessage {...messages[pluralId]} /> )} </span> ); const DeleteAllMessage = ({ hasMultiplePages }) => ( <p> <FormattedMessage {...messages[ hasMultiplePages ? 'deleteAllItemsPaginated' : 'deleteAllItems' ]} /> </p> ); const DeleteMessage = ({ isMultiple, containedItemsToDelete, brokenReferences, breaches, itemsToDelete, linksAndReferencesViewLink, }) => { const intl = useIntl(); const showFolderMessage = containedItemsToDelete > 0; const showBreachesMessage = brokenReferences > 0; return ( <> {showFolderMessage && ( <p> <FormattedMessage {...messages[ isMultiple ? 'folderDeletionMultiple' : 'folderDeletionSingle' ]} values={{ containedItemsToDelete: <span>{containedItemsToDelete}</span>, variation: ( <VariationMessage count={containedItemsToDelete} singularId="item" pluralId="items" /> ), }} /> </p> )} {showBreachesMessage && ( <BrokenLinksList intl={intl} breaches={breaches} brokenReferences={brokenReferences} isMultiple={isMultiple} itemsToDelete={itemsToDelete} linksAndReferencesViewLink={!isMultiple && linksAndReferencesViewLink} /> )} </> ); }; const ContentsDeleteModal = (props) => { const { itemsToDelete = [], open, onCancel, onOk, items, hasMultiplePages, } = props; const intl = useIntl(); const dispatch = useDispatch(); const linkintegrityInfo = useSelector((state) => state.linkIntegrity?.result); const loading = useSelector((state) => state.linkIntegrity?.loading); const [brokenReferences, setBrokenReferences] = useState(0); const [containedItemsToDelete, setContainedItemsToDelete] = useState(0); const [breaches, setBreaches] = useState([]); const [linksAndReferencesViewLink, setLinkAndReferencesViewLink] = useState(null); const titlesToDelete = useMemo( () => itemsToDelete.reduce((acc, id) => { const item = items.find((item) => item['@id'] === id); acc[id] = item ? item.Title : null; return acc; }, {}), [itemsToDelete, items], ); useEffect(() => { const getFieldById = (id, field) => { const item = find(items, { '@id': id }); return item ? item[field] : ''; }; if (itemsToDelete.length > 0 && open) { dispatch( linkIntegrityCheck( map(itemsToDelete, (item) => getFieldById(item, 'UID')), ), ); } }, [itemsToDelete, items, open, dispatch]); useEffect(() => { if (linkintegrityInfo) { // Always set the total number of contained items, regardless of breaches const totalContainedItems = linkintegrityInfo .map((result) => result.items_total ?? 0) .reduce((acc, value) => acc + value, 0); setContainedItemsToDelete(totalContainedItems); // <-- always set const breaches = linkintegrityInfo.flatMap((result) => result.breaches.map((source) => ({ source: source, target: result, })), ); // Filter out breaches where breach.source is to be deleted const filteredBreaches = breaches.filter( (breach) => !itemsToDelete.some((item) => breach.source['@id'].endsWith(item)), ); // If no breaches are found, return early if (filteredBreaches.length === 0) { setBrokenReferences(0); setLinkAndReferencesViewLink(null); setBreaches([]); return; } const source_by_uid = filteredBreaches.reduce( (acc, value) => acc.set(value.source.uid, value.source), new Map(), ); const by_source = filteredBreaches.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()); setBrokenReferences(by_source.size); setLinkAndReferencesViewLink( linkintegrityInfo.length ? linkintegrityInfo[0]['@id'] + '/links-to-item' : null, ); setBreaches( Array.from(by_source, (entry) => ({ source: source_by_uid.get(entry[0]), targets: Array.from(entry[1]), })), ); } else { setContainedItemsToDelete(0); setBrokenReferences(0); setLinkAndReferencesViewLink(null); setBreaches([]); } }, [itemsToDelete, linkintegrityInfo]); return ( open && ( <Confirm open={open} confirmButton={ brokenReferences === 0 ? intl.formatMessage(messages.delete) : intl.formatMessage(messages.delete_and_broken_links) } cancelButton={intl.formatMessage(messages.cancel)} header={ itemsToDelete.length === 1 ? intl.formatMessage(messages.deleteConfirmSingleItem) : intl.formatMessage(messages.deleteConfirmMultipleItems) } content={ <div className="content"> <Dimmer active={loading} inverted> <Loader indeterminate size="massive"> {intl.formatMessage(messages.loading)} </Loader> </Dimmer> {itemsToDelete.length > 1 && items.length === itemsToDelete.length ? ( <DeleteAllMessage hasMultiplePages={hasMultiplePages} /> ) : ( <DeleteItemsList itemsToDelete={itemsToDelete} titlesToDelete={titlesToDelete} /> )} <DeleteMessage isMultiple={itemsToDelete.length > 1} containedItemsToDelete={containedItemsToDelete} brokenReferences={brokenReferences} breaches={breaches} itemsToDelete={itemsToDelete} linksAndReferencesViewLink={linksAndReferencesViewLink} /> </div> } onCancel={onCancel} onConfirm={onOk} size="medium" /> ) ); }; const BrokenLinksList = ({ intl, breaches, brokenReferences, isMultiple, itemsToDelete, linksAndReferencesViewLink, }) => { return ( <div className="broken-links-list"> <FormattedMessage {...messages[ isMultiple ? 'brokenReferencesMultiple' : 'brokenReferencesSingle' ]} values={{ brokenReferences: <span>{brokenReferences}</span>, variation: ( <VariationMessage count={brokenReferences} singularId="reference" pluralId="references" /> ), }} /> <br /> <FormattedMessage id="These items will have broken links" defaultMessage="These items will have broken links" /> <Table compact> <Table.Body> {breaches .slice(0, MAX_LINK_INTEGRITY_BREACHES_TO_SHOW) .map((breach) => ( <Table.Row key={breach.source['@id']} verticalAlign="top"> <Table.Cell> <Link to={flattenToAppURL(breach.source['@id'])} title={intl.formatMessage(messages.navigate_to_this_item)} > {breach.source.title} </Link> </Table.Cell> <Table.Cell style={{ minWidth: '140px' }}> <FormattedMessage id="refers to" defaultMessage="refers to" /> : </Table.Cell> <Table.Cell> <ul style={{ margin: 0 }}> {breach.targets.map((target) => ( <li key={target['@id']}> <Link to={flattenToAppURL(target['@id'])} title={intl.formatMessage( messages.navigate_to_this_item, )} > {target.title} </Link> </li> ))} </ul> </Table.Cell> </Table.Row> ))} {breaches.length > MAX_LINK_INTEGRITY_BREACHES_TO_SHOW && ( <Table.Row> <Table.Cell colSpan="3"> <FormattedMessage id="and {count} more…" defaultMessage="and {count} more…" values={{ count: breaches.length - MAX_LINK_INTEGRITY_BREACHES_TO_SHOW, }} /> </Table.Cell> </Table.Row> )} </Table.Body> </Table> {linksAndReferencesViewLink && ( <Link to={flattenToAppURL(linksAndReferencesViewLink)}> <FormattedMessage id="View links and references to this item" defaultMessage="View links and references to this item" /> </Link> )} </div> ); }; ContentsDeleteModal.propTypes = { itemsToDelete: PropTypes.arrayOf( PropTypes.shape({ UID: PropTypes.string, }), ).isRequired, open: PropTypes.bool.isRequired, onOk: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, }; export default ContentsDeleteModal;