Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 126 additions & 4 deletions src/components/dialogs/delete-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +44,14 @@
tooltip: {
maxWidth: '1000px',
},
sharedItemsList: {
listStyleType: 'disc',
marginTop: 0.5,
paddingLeft: 3,
},
sharedItem: {
display: 'list-item',
},
} as const satisfies MuiStyles;

/**
Expand All @@ -44,12 +73,28 @@
simpleDeleteFormatMessageId,
error,
}: Readonly<DeleteDialogProps>) {
const { snackError, snackWarning } = useSnackMessage();

const [itemsState, setItemsState] = useState<ElementAttributes[]>([]);

const [loadingState, setLoadingState] = useState(false);

// shared elements held by the directory to delete
const [sharedDescendants, setSharedDescendants] = useState<ElementAttributes[]>([]);

const [loadingSharedDescendants, setLoadingSharedDescendants] = useState(false);

const openRef = useRef<boolean | null>(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);
Expand All @@ -58,6 +103,33 @@
openRef.current = open;
}, [open, items, error]);

useEffect(() => {
if (!open || !directoryToDeleteUuid) {
setSharedDescendants([]);
return undefined;
}
let cancelled = false;
setLoadingSharedDescendants(true);
fetchDirectoryContent(directoryToDeleteUuid, undefined, true)

Check failure on line 113 in src/components/dialogs/delete-dialog.tsx

View workflow job for this annotation

GitHub Actions / build / build

Expected 1-2 arguments, but got 3.
.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;
Expand All @@ -66,6 +138,11 @@
};

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();
Expand Down Expand Up @@ -104,18 +181,63 @@
/>
));

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 (
<Box marginTop={2}>
<FormattedMessage
id="deleteDialogSharedItemMessage"
values={{ count: getSharingLinksCount(sharedItems[0]) }}
/>
</Box>
);
}
return (
<Box marginTop={2}>
<FormattedMessage id="deleteDialogSharedItemsMessage" />
<List dense disablePadding sx={styles.sharedItemsList}>
{sharedItems.map((item) => (
<ListItem key={item.elementUuid} disableGutters disablePadding sx={styles.sharedItem}>
<Box display="grid" gridTemplateColumns="minmax(0, 1fr) auto" columnGap={2} width="100%">
<OverflowableText text={item.elementName} tooltipSx={styles.tooltip} />
<FormattedMessage
id="sharingLinksCount"
values={{ count: getSharingLinksCount(item) }}
/>
</Box>
</ListItem>
))}
</List>
</Box>
);
};

return (
<Dialog open={open} onClose={handleClose} aria-labelledby="dialog-title-delete">
<DialogTitle style={{ display: 'flex' }} data-testid="DialogTitle">
<FormattedMessage id="deleteDialogTitle" />
</DialogTitle>
<DialogContent>
{buildItemsToDeleteGrid(itemsState, multipleDeleteFormatMessageId, simpleDeleteFormatMessageId)}
{buildSharedItems()}
{error !== '' && <Alert severity="error">{error}</Alert>}
</DialogContent>
<DialogActions>
<CancelButton onClick={handleClose} disabled={loadingState} data-testid="CancelButton" />
<Button onClick={handleClick} variant="outlined" disabled={loadingState} data-testid="DeleteButton">
<Button
onClick={handleClick}
variant="outlined"
disabled={loadingState || loadingSharedDescendants}
data-testid="DeleteButton"
>
{(loadingState && <CircularProgress size={24} />) || <FormattedMessage id="delete" />}
</Button>
</DialogActions>
Expand Down
3 changes: 2 additions & 1 deletion src/components/menus/content-contextual-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -409,7 +410,7 @@ export default function ContentContextualMenu(props: Readonly<ContentContextualM
return (
isSingleElement &&
selectedElements[0].type === ElementType.MODIFICATION &&
(selectedElements[0].references?.length ?? 0) > 0
isElementShared(selectedElements[0])
);
}, [isSingleElement, selectedElements]);

Expand Down
5 changes: 5 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions src/utils/element-utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading