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
4 changes: 2 additions & 2 deletions src/components/menus/content-contextual-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ import {
ElementAttributes,
ElementType,
FilterCreationDialog,
PARAM_DEVELOPER_MODE,
PARAM_LANGUAGE,
TreeViewFinderNodeProps,
useSnackMessage,
PARAM_LANGUAGE,
PARAM_DEVELOPER_MODE,
} from '@gridsuite/commons-ui';
import RenameDialog from '../dialogs/rename-dialog';
import DeleteDialog from '../dialogs/delete-dialog';
Expand Down
5 changes: 2 additions & 3 deletions src/components/toolbars/content-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { useSelector } from 'react-redux';
import { useIntl } from 'react-intl';
import {
Delete as DeleteIcon,
DownloadForOffline,
DriveFileMove as DriveFileMoveIcon,
FileDownload,
TableView as TableViewIcon,
DownloadForOffline,
} from '@mui/icons-material';
import { ElementAttributes, ElementType, useSnackMessage, PARAM_DEVELOPER_MODE } from '@gridsuite/commons-ui';
import { ElementAttributes, ElementType, PARAM_DEVELOPER_MODE, useSnackMessage } from '@gridsuite/commons-ui';
import { deleteElements, moveElementsToDirectory, PermissionType } from '../../utils/rest-api';
import DeleteDialog from '../dialogs/delete-dialog';
import CommonToolbar, { CommonToolbarProps } from './common-toolbar';
Expand Down Expand Up @@ -45,7 +45,6 @@ export default function ContentToolbar(props: Readonly<ContentToolbarProps>) {
const [openDialog, setOpenDialog] = useState(constants.DialogsId.NONE);
const [directoryWritable, setDirectoryWritable] = useState(false);
const [enableDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE);

useEffect(() => {
if (selectedDirectory !== null) {
checkPermissionOnDirectory(selectedDirectory, PermissionType.WRITE).then((b) => {
Expand Down
3 changes: 3 additions & 0 deletions src/components/tree-views-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import * as constants from '../utils/UIconstants';
import DirectoryTreeContextualMenu from './menus/directory-tree-contextual-menu';
import { AppState, IDirectory, ITreeData, UploadingElement } from '../redux/types';
import { buildPathToFromMap, updatedTree } from './treeview-utils';
import { useExportNotification } from '../hooks/use-export-notification';

const initialMousePosition = {
mouseX: null,
Expand Down Expand Up @@ -115,6 +116,8 @@ export default function TreeViewsContainer() {
const treeDataRef = useRef<ITreeData>();
treeDataRef.current = treeData;

useExportNotification();

const handleOpenDirectoryMenu = useCallback(
(event: ReactMouseEvent<HTMLDivElement | HTMLButtonElement, MouseEvent>) => {
setOpenDirectoryMenu(true);
Expand Down
27 changes: 13 additions & 14 deletions src/components/utils/downloadUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import {
downloadSpreadsheetConfigCollection,
fetchConvertedCase,
} from '../../utils/rest-api';
import { buildExportIdentifier, setExportSubscription } from '../../utils/case-export-utils';

interface DownloadData {
blob: Blob;
filename: string;
}

const triggerDownload = ({ blob, filename }: DownloadData): void => {
export const triggerDownload = ({ blob, filename }: DownloadData): void => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
Expand Down Expand Up @@ -106,32 +107,30 @@ export function useDownloadUtils() {
fileName?: string
): Promise<void> => {
try {
const result = await fetchConvertedCase(
const exportUuid = await fetchConvertedCase(
caseElement.elementUuid,
fileName || caseElement.elementName, // if no fileName is provided or empty, the case name will be used
format,
formatParameters,
abortController2
);

let downloadFileName =
result.headers.get('Content-Disposition')?.split('filename=')[1] ??
fileName ??
caseElement.elementName;
// We remove quotes
downloadFileName = downloadFileName.substring(1, downloadFileName.length - 1);

const blob = await result.blob();

triggerDownload({ blob, filename: downloadFileName });
const identifier = buildExportIdentifier({
caseUuid: caseElement.elementUuid,
exportUuid,
});
setExportSubscription(identifier);
snackInfo({
messageTxt: intl.formatMessage({ id: 'export.message.started' }),
});
} catch (error: any) {
if (error.name === 'AbortError') {
throw error;
}
handleDownloadError(caseElement, error);
handleDownloadError(caseElement, error.message || 'Export failed');
}
},
[handleDownloadError]
[handleDownloadError, intl, snackInfo]
);

const buildPartialDownloadMessage = useCallback(
Expand Down
44 changes: 44 additions & 0 deletions src/hooks/use-export-download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2025, 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 { useSnackMessage } from '@gridsuite/commons-ui';
import { useCallback } from 'react';
import { UUID } from 'node:crypto';
import { triggerDownload } from '../components/utils/downloadUtils';
import { fetchExportNetworkFile } from '../utils/rest-api';

export function useExportDownload() {
const { snackError } = useSnackMessage();

const downloadExportFile = useCallback(
(exportUuid: UUID) => {
fetchExportNetworkFile(exportUuid)
.then(async (response) => {
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'export.zip';
if (contentDisposition?.includes('filename=')) {
const regex = /filename="?([^"]+)"?/;
const [, extractedFilename] = regex.exec(contentDisposition) ?? [];
if (extractedFilename) {
filename = extractedFilename;
}
}

const blob = await response.blob();
triggerDownload({ blob, filename });
})
.catch((error: Error) => {
snackError({
messageTxt: error.message,
headerId: 'export.header.failed',
});
});
},
[snackError]
);
return { downloadExportFile };
}
50 changes: 50 additions & 0 deletions src/hooks/use-export-notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2025, 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 { useIntl } from 'react-intl';
import { NotificationsUrlKeys, useNotificationsListener, useSnackMessage } from '@gridsuite/commons-ui';
import { useSelector } from 'react-redux';
import { useCallback } from 'react';
import { AppState } from '../redux/types';
import { buildExportIdentifier, isExportSubscribed, unsetExportSubscription } from '../utils/case-export-utils';
import { useExportDownload } from './use-export-download';

export function useExportNotification() {
const intl = useIntl();
const { snackError, snackInfo } = useSnackMessage();
const { downloadExportFile } = useExportDownload();
const userIdProfile = useSelector((state: AppState) => state.user?.profile.sub);
const handleExportNotification = useCallback(
(event: MessageEvent<string>) => {
const eventData = JSON.parse(event.data);
if (eventData?.headers?.notificationType === 'caseExportSucceeded') {
const { caseUuid, userId, exportUuid, error } = eventData.headers;
Comment on lines +22 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can not typing the notification as in study?

const exportIdentifierNotif = buildExportIdentifier({
caseUuid,
exportUuid,
});
const isSubscribed = isExportSubscribed(exportIdentifierNotif);
if (isSubscribed && userIdProfile === userId) {
unsetExportSubscription(exportIdentifierNotif);

if (error) {
snackError({
messageTxt: error,
});
} else {
downloadExportFile(exportUuid);
snackInfo({
messageTxt: intl.formatMessage({ id: 'export.message.succeeded' }),
});
}
}
}
},
[userIdProfile, snackError, downloadExportFile, snackInfo, intl]
);

useNotificationsListener(NotificationsUrlKeys.DIRECTORY, { listenerCallbackMessage: handleExportNotification });
}
23 changes: 23 additions & 0 deletions src/redux/export-network-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2025, 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 { APP_NAME } from '../utils/config-params';

const SESSION_STORAGE_EXPORT_STATE_KEY_PREFIX = `${APP_NAME.toUpperCase()}_EXPORT_STATE_`;

export function getExportState() {
const objJson = sessionStorage.getItem(SESSION_STORAGE_EXPORT_STATE_KEY_PREFIX);
if (objJson) {
const array = JSON.parse(objJson) as string[];
return new Set(array);
}
return undefined;
}

export function saveExportState(newExportState: Set<string>): void {
const array = Array.from(newExportState);
sessionStorage.setItem(SESSION_STORAGE_EXPORT_STATE_KEY_PREFIX, JSON.stringify(array));
}
5 changes: 4 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,5 +201,8 @@
"visualization": "Visualization",
"missingEquipmentsFromStudy": "Equipments missing from study",
"equipmentTypesByFilters": "Contingencies per equipment types in voltage levels & substations",
"reload": "Reload"
"reload": "Reload",
"export.message.started": "Export file started",
"export.message.succeeded": "Export file succeeded",
Comment on lines +205 to +206
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"export.message.started": "Export file started",
"export.message.succeeded": "Export file succeeded",
"export.message.started": "Export case started",
"export.message.succeeded": "Export case succeeded",

"export.header.failed": "export file failed"
Copy link
Contributor

@thangqp thangqp Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"export.header.failed": "export file failed"
"export.header.failed": "Export case failed"

}
5 changes: 4 additions & 1 deletion src/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,8 @@
"visualization": "Visualisation",
"missingEquipmentsFromStudy": "Ouvrages absents de l'étude",
"equipmentTypesByFilters": "Aléas par types d'ouvrage dans les postes et sites",
"reload": "Rafraîchir"
"reload": "Rafraîchir",
"export.message.started": "Vous serez notifié lorsque l'export sera prêt.",
"export.message.succeeded": "Téléchargement en cours",
"export.header.failed": "Échec du téléchargement du fichier"
}
31 changes: 31 additions & 0 deletions src/utils/case-export-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025, 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 { UUID } from 'node:crypto';
import { getExportState, saveExportState } from '../redux/export-network-state';

export function buildExportIdentifier({ caseUuid, exportUuid }: { caseUuid: UUID; exportUuid: String }): string {
return `${caseUuid}|${exportUuid}`;
}

export function setExportSubscription(identifier: string): void {
const currentState = getExportState() || new Set<string>();
currentState.add(identifier);
saveExportState(currentState);
}

export function unsetExportSubscription(identifier: string): void {
const currentState = getExportState();
if (currentState) {
currentState.delete(identifier);
saveExportState(currentState);
}
}

export function isExportSubscribed(identifier: string): boolean {
const currentState = getExportState();
return currentState ? currentState.has(identifier) : false;
}
9 changes: 7 additions & 2 deletions src/utils/rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,16 +736,21 @@ export const fetchConvertedCase = (
formatParameters: unknown,
abortController: AbortController
) =>
backendFetch(
backendFetchText(
Copy link
Contributor

@thangqp thangqp Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exportUuid return as string but when download you send UUID.. it is better to use all as UUID.. (also typing Notification as in study)

`${PREFIX_NETWORK_CONVERSION_SERVER_QUERIES}/v1/cases/${caseUuid}/convert/${format}?fileName=${fileName}`,
{
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formatParameters),
signal: abortController.signal,
}
);

export const fetchExportNetworkFile = (exportUuid: UUID) =>
backendFetch(`${PREFIX_NETWORK_CONVERSION_SERVER_QUERIES}/v1/download-file/${exportUuid}`, {
method: 'get',
headers: { 'Content-Type': 'application/json' },
});

export const downloadCase = (caseUuid: string) =>
backendFetch(`${PREFIX_CASE_QUERIES}/v1/cases/${caseUuid}`, {
method: 'get',
Expand Down
Loading