@plone/volto
Version:
Volto
433 lines (411 loc) • 13.2 kB
JSX
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;