Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
883cd44
wip
dbraquart Jan 16, 2026
9b45baf
wip 2 / compile
dbraquart Jan 19, 2026
5e9f433
fixes
dbraquart Jan 19, 2026
a55c854
sonar issues
dbraquart Jan 20, 2026
c2cd606
use Enum rather then 'LCC'
dbraquart Jan 20, 2026
e318cd5
more typing
dbraquart Jan 20, 2026
d8c0623
fix connected mcs display
dbraquart Jan 20, 2026
35482af
fix after rebase
dbraquart Jan 28, 2026
365970f
fix update issue when we switch hvdc-lcc in the ID combobox
dbraquart Jan 28, 2026
7ba9bdf
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
dbraquart Jan 28, 2026
bbb89af
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
dbraquart Jan 29, 2026
0779377
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
dbraquart Feb 3, 2026
7d1debf
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
achour94 Feb 4, 2026
fbf633f
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
dbraquart Feb 10, 2026
afc292c
Merge remote-tracking branch 'origin/main' into dbraquart/ts-migratio…
dbraquart Feb 10, 2026
f74710c
lint after merge
dbraquart Feb 10, 2026
e8fed89
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
dbraquart Feb 12, 2026
12a345e
rev remark: fix double fetch in update mode
dbraquart Feb 12, 2026
c9b0a28
clean
dbraquart Feb 12, 2026
9976633
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
dbraquart Feb 13, 2026
c1ef516
Merge branch 'main' into dbraquart/ts-migration-for-equipment-deletion
dbraquart Feb 13, 2026
4c22dd8
force init eqpt type to null
dbraquart Feb 13, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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 { EquipmentType, ModificationType } from '@gridsuite/commons-ui';
import { UUID } from 'node:crypto';

export interface LccShuntCompensatorConnectionInfos {
id: string;
connectedToHvdc: boolean;
}

export interface EquipmentDeletionSpecificInfos {
specificType: string;
// below is specific to HVDC-LCC deletion (then specificType = HVDC_LINE_LCC_DELETION_SPECIFIC_TYPE)
mcsOnSide1: LccShuntCompensatorConnectionInfos[];
mcsOnSide2: LccShuntCompensatorConnectionInfos[];
}

export type EquipmentDeletionInfos = {
type: ModificationType;
uuid?: UUID;
equipmentId: UUID;
equipmentType: EquipmentType;
equipmentInfos?: EquipmentDeletionSpecificInfos;
};

// Maps HvdcLccDeletionInfos from modification-server
export interface HvdcLccDeletionInfos extends EquipmentDeletionSpecificInfos {
id?: UUID;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,62 @@
import { yupResolver } from '@hookform/resolvers/yup';
import yup from 'components/utils/yup-config';
import { DELETION_SPECIFIC_DATA, EQUIPMENT_ID, TYPE } from '../../../utils/field-constants';
import { CustomFormProvider, snackWithFallback, useSnackMessage } from '@gridsuite/commons-ui';
import {
CustomFormProvider,
DeepNullable,
EquipmentType,
snackWithFallback,
useSnackMessage,
} from '@gridsuite/commons-ui';
import { useForm } from 'react-hook-form';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { ModificationDialog } from '../../commons/modificationDialog';
import { EQUIPMENT_TYPES } from 'components/utils/equipment-types';
import DeleteEquipmentForm from './equipment-deletion-form';
import PropTypes from 'prop-types';
import { useOpenShortWaitFetching } from 'components/dialogs/commons/handle-modification-form';
import { FORM_LOADING_DELAY } from 'components/network/constants';
import { deleteEquipment } from '../../../../services/study/network-modifications';
import { FetchStatus } from '../../../../services/utils';
import { UUID } from 'node:crypto';
import { FetchStatus } from 'services/utils.type';
import { EquipmentDeletionInfos } from './equipement-deletion-dialog.type';
import { NetworkModificationDialogProps } from '../../../graph/menus/network-modifications/network-modification-menu.type';
import { getHvdcLccDeletionSchema } from './hvdc-lcc-deletion/hvdc-lcc-deletion-utils';

const formSchema = yup
.object()
.shape({
[TYPE]: yup.string().nullable().required(),
[EQUIPMENT_ID]: yup.string().nullable().required(),
[TYPE]: yup.mixed<EquipmentType>().oneOf(Object.values(EquipmentType)).nullable().required(),
[DELETION_SPECIFIC_DATA]: getHvdcLccDeletionSchema(),
})
.required();

const emptyFormData = {
[TYPE]: null,
[EQUIPMENT_ID]: null,
export type EquipmentDeletionFormInfos = yup.InferType<typeof formSchema>;

const emptyFormData: EquipmentDeletionFormInfos = {
[EQUIPMENT_ID]: '',
[TYPE]: null!,
[DELETION_SPECIFIC_DATA]: null,
};

type EquipmentDeletionDialogProps = NetworkModificationDialogProps & {
editData?: EquipmentDeletionInfos;
defaultIdValue?: UUID;
equipmentType: EquipmentType;
editDataFetchStatus?: FetchStatus;
};

/**
* Dialog to delete an equipment from its type and ID.
* Dialog to delete equipment from its type and ID.
* @param studyUuid the study we are currently working on
* @param currentNode the node we are currently working on
* @param currentRootNetworkUuid
* @param editData the data to edit
* @param isUpdate check if edition form
* @param defaultIdValue the default equipment id
* @param editDataFetchStatus indicates the status of fetching EditData
* @param equipmentType
* @param onClose a callback when dialog has closed
* @param editDataFetchStatus indicates the status of fetching EditData
* @param onClose a callback when dialog has been closed
* @param onValidated a callback when dialog has been validated
* @param dialogProps props that are forwarded to the generic ModificationDialog component
*/
const EquipmentDeletionDialog = ({
Expand All @@ -54,36 +73,38 @@ const EquipmentDeletionDialog = ({
editData,
isUpdate,
defaultIdValue, // Used to pre-select an equipmentId when calling this dialog from the SLD/map
editDataFetchStatus,
equipmentType,
editDataFetchStatus,
onClose,
onValidated,
...dialogProps
}) => {
}: EquipmentDeletionDialogProps) => {
const currentNodeUuid = currentNode?.id;

const { snackError } = useSnackMessage();

const formMethods = useForm({
const formMethods = useForm<DeepNullable<EquipmentDeletionFormInfos>>({
defaultValues: emptyFormData,
resolver: yupResolver(formSchema),
resolver: yupResolver<DeepNullable<EquipmentDeletionFormInfos>>(formSchema),
});

const { reset } = formMethods;

const fromEditDataToFormValues = useCallback(
(editData) => {
(editData: EquipmentDeletionInfos) => {
reset({
[TYPE]: EQUIPMENT_TYPES[editData.equipmentType],
[TYPE]: editData.equipmentType,
[EQUIPMENT_ID]: editData.equipmentId,
[DELETION_SPECIFIC_DATA]: null,
});
},
[reset]
);

const fromMenuDataValues = useCallback(
(menuSelectId) => {
(menuSelectId: UUID) => {
reset({
[TYPE]: EQUIPMENT_TYPES.HVDC_LINE,
[TYPE]: EquipmentType.HVDC_LINE,
[EQUIPMENT_ID]: menuSelectId,
[DELETION_SPECIFIC_DATA]: null,
});
Expand All @@ -92,7 +113,7 @@ const EquipmentDeletionDialog = ({
);

const resetFormWithEquipmentType = useCallback(
(equipmentType) => {
(equipmentType: EquipmentType) => {
reset({
[TYPE]: equipmentType,
});
Expand All @@ -118,15 +139,15 @@ const EquipmentDeletionDialog = ({
]);

const onSubmit = useCallback(
(formData) => {
deleteEquipment(
(formData: EquipmentDeletionFormInfos) => {
deleteEquipment({
studyUuid,
currentNodeUuid,
formData[TYPE],
formData[EQUIPMENT_ID],
editData?.uuid,
formData[DELETION_SPECIFIC_DATA]
).catch((error) => {
nodeUuid: currentNodeUuid,
uuid: editData?.uuid,
equipmentId: formData[EQUIPMENT_ID] as UUID,
equipmentType: formData[TYPE],
equipmentSpecificInfos: formData[DELETION_SPECIFIC_DATA] ?? undefined,
}).catch((error) => {
snackWithFallback(snackError, error, { headerId: 'UnableToDeleteEquipment' });
});
},
Expand All @@ -143,17 +164,23 @@ const EquipmentDeletionDialog = ({
delay: FORM_LOADING_DELAY,
});

const waitingForData = useMemo(
() => isUpdate && editDataFetchStatus === FetchStatus.RUNNING,
[editDataFetchStatus, isUpdate]
);

return (
<CustomFormProvider validationSchema={formSchema} {...formMethods}>
<ModificationDialog
fullWidth
maxWidth="md"
onClear={clear}
onSave={onSubmit}
onClose={onClose}
onValidated={onValidated}
onSave={onSubmit}
titleId="DeleteEquipment"
open={open}
isDataFetching={isUpdate && editDataFetchStatus === FetchStatus.RUNNING}
isDataFetching={waitingForData}
{...dialogProps}
>
<DeleteEquipmentForm
Expand All @@ -167,16 +194,4 @@ const EquipmentDeletionDialog = ({
);
};

EquipmentDeletionDialog.propTypes = {
studyUuid: PropTypes.string,
currentNode: PropTypes.object,
currentRootNetworkUuid: PropTypes.string,
editData: PropTypes.object,
isUpdate: PropTypes.bool,
defaultIdValue: PropTypes.string,
editDataFetchStatus: PropTypes.string,
equipmentType: PropTypes.string,
onClose: PropTypes.func,
};

export default EquipmentDeletionDialog;
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
import { Grid } from '@mui/material';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useSnackMessage, AutocompleteInput, snackWithFallback, filledTextField } from '@gridsuite/commons-ui';
import {
useSnackMessage,
AutocompleteInput,
EquipmentType,
filledTextField,
snackWithFallback,
} from '@gridsuite/commons-ui';
import {
DELETION_SPECIFIC_DATA,
EQUIPMENT_ID,
Expand All @@ -18,16 +24,33 @@ import {
import { areIdsEqual, getObjectId, richTypeEquals } from 'components/utils/utils';
import { EQUIPMENT_TYPES } from 'components/utils/equipment-types';
import HvdcLccDeletionSpecificForm from './hvdc-lcc-deletion/hvdc-lcc-deletion-specific-form';
import useHvdcLccDeletion from './hvdc-lcc-deletion/hvdc-lcc-deletion-utils';

import { fetchEquipmentsIds } from '../../../../services/study/network-map';
import useGetLabelEquipmentTypes from '../../../../hooks/use-get-label-equipment-types';
import GridItem from '../../commons/grid-item';
import type { UUID } from 'node:crypto';
import { CurrentTreeNode } from '../../../graph/tree-node.type';
import { EquipmentDeletionInfos } from './equipement-deletion-dialog.type';
import useHvdcLccDeletion from './hvdc-lcc-deletion/use-hvdc-lcc-deletion';

export interface DeleteEquipmentFormProps {
studyUuid: UUID;
currentNode: CurrentTreeNode;
currentRootNetworkUuid: UUID;
editData?: EquipmentDeletionInfos;
}

const DeleteEquipmentForm = ({ studyUuid, currentNode, currentRootNetworkUuid, editData }) => {
const NULL_UUID: UUID = '00000000-0000-0000-0000-000000000000';

export default function DeleteEquipmentForm({
studyUuid,
currentNode,
currentRootNetworkUuid,
editData,
}: Readonly<DeleteEquipmentFormProps>) {
const { snackError } = useSnackMessage();
const editedIdRef = useRef(null);
const currentTypeRef = useRef(null);
const editedIdRef = useRef<UUID | null>(null);
const currentTypeRef = useRef<EquipmentType>(null);

const watchType = useWatch({
name: TYPE,
Expand Down Expand Up @@ -84,31 +107,32 @@ const DeleteEquipmentForm = ({ studyUuid, currentNode, currentRootNetworkUuid, e
}, [studyUuid, currentNode?.id, currentRootNetworkUuid, watchType, snackError]);

useEffect(() => {
if (studyUuid && currentNode?.id && currentRootNetworkUuid) {
if (editData?.equipmentId) {
if (editedIdRef.current === null) {
// The first time we render an edition, we want to merge the
// dynamic data with the edition data coming from the database
editedIdRef.current = editData.equipmentId;
} else if (watchEquipmentId !== editedIdRef.current && editedIdRef.current !== '') {
// we have changed eqptId, leave the "first edit" mode (then if we circle back
// to editData?.equipmentId, we wont make the merge anymore).
editedIdRef.current = '';
}
if (!studyUuid || !currentNode?.id || !currentRootNetworkUuid) {
return;
}
if (editData?.equipmentId) {
if (editedIdRef.current === null) {
// The first time we render an edition, we want to merge the
// dynamic data with the edition data coming from the database
editedIdRef.current = editData.equipmentId;
} else if (watchEquipmentId !== editedIdRef.current && editedIdRef.current !== NULL_UUID) {
// we have changed eqptId, leave the "first edit" mode (then if we circle back
// to editData?.equipmentId, we won't make the merge anymore).
editedIdRef.current = NULL_UUID;
}
}

if (watchEquipmentId && currentTypeRef.current === EQUIPMENT_TYPES.HVDC_LINE) {
// need specific update related to HVDC LCC deletion (for MCS lists)
hvdcLccSpecificUpdate(
studyUuid,
currentNode?.id,
currentRootNetworkUuid,
watchEquipmentId,
watchEquipmentId === editedIdRef.current ? editData : null
);
} else {
setValue(DELETION_SPECIFIC_DATA, null);
}
if (watchEquipmentId && currentTypeRef.current === EquipmentType.HVDC_LINE) {
// need specific update related to HVDC LCC deletion (for MCS lists)
hvdcLccSpecificUpdate(
studyUuid,
currentNode?.id,
currentRootNetworkUuid,
watchEquipmentId,
watchEquipmentId === editedIdRef.current ? editData : undefined
);
} else {
setValue(DELETION_SPECIFIC_DATA, null);
}
}, [
studyUuid,
Expand Down Expand Up @@ -149,9 +173,9 @@ const DeleteEquipmentForm = ({ studyUuid, currentNode, currentRootNetworkUuid, e
options={equipmentsOptions}
getOptionLabel={getObjectId}
//hack to work with freesolo autocomplete
//setting null programatically when freesolo is enable wont empty the field
//setting null programmatically when freesolo is enable won't empty the field
inputTransform={(value) => value ?? ''}
outputTransform={(value) => (value === '' ? null : getObjectId(value))}
outputTransform={(value: any) => (value === '' ? null : getObjectId(value))}
size={'small'}
formProps={filledTextField}
/>
Expand All @@ -168,6 +192,4 @@ const DeleteEquipmentForm = ({ studyUuid, currentNode, currentRootNetworkUuid, e
)}
</>
);
};

export default DeleteEquipmentForm;
}
Loading
Loading