diff --git a/src/components/dialogs/delete-dialog.tsx b/src/components/dialogs/delete-dialog.tsx index b58ba4da5..8f1c94c7c 100644 --- a/src/components/dialogs/delete-dialog.tsx +++ b/src/components/dialogs/delete-dialog.tsx @@ -4,10 +4,31 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Alert, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; +import { + Alert, + Box, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + List, + ListItem, +} from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import { type CSSProperties, type SyntheticEvent, useEffect, useRef, useState } from 'react'; -import { CancelButton, type ElementAttributes, type MuiStyles, OverflowableText } from '@gridsuite/commons-ui'; +import { type CSSProperties, type SyntheticEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { + CancelButton, + type ElementAttributes, + ElementType, + fetchDirectoryContent, + type MuiStyles, + OverflowableText, + snackWithFallback, + useSnackMessage, +} from '@gridsuite/commons-ui'; +import { getSharingLinksCount, isElementShared } from '../../utils/element-utils'; export interface DeleteDialogProps { open: boolean; @@ -23,6 +44,14 @@ const styles = { tooltip: { maxWidth: '1000px', }, + sharedItemsList: { + listStyleType: 'disc', + marginTop: 0.5, + paddingLeft: 3, + }, + sharedItem: { + display: 'list-item', + }, } as const satisfies MuiStyles; /** @@ -44,12 +73,28 @@ export default function DeleteDialog({ simpleDeleteFormatMessageId, error, }: Readonly) { + const { snackError, snackWarning } = useSnackMessage(); + const [itemsState, setItemsState] = useState([]); const [loadingState, setLoadingState] = useState(false); + // shared elements held by the directory to delete + const [sharedDescendants, setSharedDescendants] = useState([]); + + const [loadingSharedDescendants, setLoadingSharedDescendants] = useState(false); + const openRef = useRef(null); + // The shared elements that will be included in the deletion + const sharedItems = useMemo( + () => [...itemsState.filter(isElementShared), ...sharedDescendants], + [itemsState, sharedDescendants] + ); + + // directories are only deleted one at a time, from the tree + const directoryToDeleteUuid = itemsState.find((item) => item.type === ElementType.DIRECTORY)?.elementUuid; + useEffect(() => { if ((open && !openRef.current) || error !== '') { setItemsState(items); @@ -58,6 +103,33 @@ export default function DeleteDialog({ openRef.current = open; }, [open, items, error]); + useEffect(() => { + if (!open || !directoryToDeleteUuid) { + setSharedDescendants([]); + return undefined; + } + let cancelled = false; + setLoadingSharedDescendants(true); + fetchDirectoryContent(directoryToDeleteUuid, undefined, true) + .then((directoryContent) => { + if (!cancelled) { + setSharedDescendants(directoryContent.filter(isElementShared)); + } + }) + .catch((fetchError) => { + console.error(fetchError); + snackWithFallback(snackError, fetchError, { headerId: 'sharedDescendantsError' }); + }) + .finally(() => { + if (!cancelled) { + setLoadingSharedDescendants(false); + } + }); + return () => { + cancelled = true; + }; + }, [open, directoryToDeleteUuid, snackError]); + const handleClose = (_: SyntheticEvent, reason?: string) => { if (reason === 'backdropClick') { return; @@ -66,6 +138,11 @@ export default function DeleteDialog({ }; const handleClick = () => { + // TODO remove later when fixed : a shared element cannot be deleted while other elements still reference it + if (sharedItems.length > 0) { + snackWarning({ messageId: 'deleteSharedItemsForbidden' }); + return; + } console.debug('Request for deletion'); setLoadingState(true); onClick(); @@ -104,6 +181,45 @@ export default function DeleteDialog({ /> )); + const buildSharedItems = () => { + if (sharedItems.length === 0) { + return false; + } + // a single shared element needs no list + if ( + itemsState.length === 1 && + sharedItems.length === 1 && + sharedItems[0].elementUuid === itemsState[0].elementUuid + ) { + return ( + + + + ); + } + return ( + + + + {sharedItems.map((item) => ( + + + + + + + ))} + + + ); + }; + return ( @@ -111,11 +227,17 @@ export default function DeleteDialog({ {buildItemsToDeleteGrid(itemsState, multipleDeleteFormatMessageId, simpleDeleteFormatMessageId)} + {buildSharedItems()} {error !== '' && {error}} - diff --git a/src/components/menus/content-contextual-menu.tsx b/src/components/menus/content-contextual-menu.tsx index c7d9577da..02df3491f 100644 --- a/src/components/menus/content-contextual-menu.tsx +++ b/src/components/menus/content-contextual-menu.tsx @@ -47,6 +47,7 @@ import { renameElement, } from '../../utils/rest-api'; import { FilterType } from '../../utils/elementType'; +import { isElementShared } from '../../utils/element-utils'; import CommonContextualMenu, { CommonContextualMenuProps, MenuItemType } from './common-contextual-menu'; import { useDeferredFetch, useMultipleDeferredFetch } from '../../utils/custom-hooks'; import MoveDialog from '../dialogs/move-dialog'; @@ -409,7 +410,7 @@ export default function ContentContextualMenu(props: Readonly 0 + isElementShared(selectedElements[0]) ); }, [isSingleElement, selectedElements]); diff --git a/src/translations/en.json b/src/translations/en.json index a44ddad6a..0e7d30a31 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -51,6 +51,8 @@ "displaySharingLinks": "Display sharing links", "sharingLinksOf": "Sharing links of", "sharingLinksError": "An error occurred while fetching the sharing links", + "sharedDescendantsError": "Could not check whether the folder to delete holds shared elements: deleting it may break sharing links", + "sharingLinksCount": "{count, plural, one {# sharing link} other {# sharing links}}", "path": "Path", "node": "Node", "createFolder": "Create folder", @@ -70,6 +72,9 @@ "deleteItemDialogMessage": "The selected item will be deleted permanently.", "deleteMultipleItemsDialogMessage": "All selected items will be deleted permanently.", "deleteDialogTitle": "Confirmation", + "deleteDialogSharedItemMessage": "This item is shared with {count, plural, one {# sharing link} other {# sharing links}}.", + "deleteDialogSharedItemsMessage": "The following items are shared:", + "deleteSharedItemsForbidden": "Deleting shared elements is not possible yet: nothing has been deleted.", "renameDirectoryDialogTitle": "Rename the folder", "edit": "Edit", "createNewContingencyList": "Create a contingency list", diff --git a/src/translations/fr.json b/src/translations/fr.json index 49a08f308..1a9f2aecb 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -50,6 +50,8 @@ "displaySharingLinks": "Voir les liens de partage", "sharingLinksOf": "Liens de partage de", "sharingLinksError": "Une erreur est survenue lors de la récupération des liens de partage", + "sharedDescendantsError": "Impossible de vérifier si le dossier à supprimer contient des éléments partagés : sa suppression risque de casser des liens de partage", + "sharingLinksCount": "{count, plural, one {# lien de partage} other {# liens de partage}}", "path": "Chemin", "node": "Noeud", "createNewStudyFromImportedCase": "Créer étude", @@ -69,6 +71,9 @@ "deleteItemDialogMessage": "L'élement sélectionné va être supprimé définitivement.", "deleteMultipleItemsDialogMessage": "Tous les éléments sélectionnés vont être supprimés définitivement.", "deleteDialogTitle": "Confirmation", + "deleteDialogSharedItemMessage": "Cet élément est partagé avec {count, plural, one {# lien de partage} other {# liens de partage}}.", + "deleteDialogSharedItemsMessage": "Les éléments suivants sont partagés :", + "deleteSharedItemsForbidden": "La suppression d'éléments partagés n'est pas encore possible : aucun élément n'a été supprimé.", "renameDirectoryDialogTitle": "Renommer le dossier", "edit": "Modifier", "createNewContingencyList": "Créer une liste d'aléas", diff --git a/src/utils/element-utils.ts b/src/utils/element-utils.ts new file mode 100644 index 000000000..185831e7c --- /dev/null +++ b/src/utils/element-utils.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import type { ElementAttributes } from '@gridsuite/commons-ui'; + +/** + * Number of elements using the given element, each reference being a "sharing link". + */ +export function getSharingLinksCount(element: ElementAttributes): number { + return element.references?.length ?? 0; +} + +/** + * An element is shared as soon as another element references it. + */ +export function isElementShared(element: ElementAttributes): boolean { + return getSharingLinksCount(element) > 0; +}