Skip to content
Merged
1 change: 1 addition & 0 deletions src/features/network-modifications/voltageLevel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './creation';
export * from './voltage-level.type';
export * from './modification';
export * from './topology';
export * from './section';
Original file line number Diff line number Diff line change
@@ -0,0 +1,376 @@
/**
* 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 { useCallback, useEffect, useMemo, useState } from 'react';
import { Box, Button, Grid, Slider, TextField, Tooltip, Typography } from '@mui/material';
import { FormattedMessage, useIntl } from 'react-intl';
import { InfoOutlined } from '@mui/icons-material';
import { useFormContext, useWatch } from 'react-hook-form';
import { AutocompleteInput, SelectInput, SwitchInput, useCustomFormContext } from '../../../../components/ui';
import { areIdsEqual, FieldConstants, getObjectId, Option } from '../../../../utils';
import { GridSection } from '../../../../components/composite/grid/grid-section';
import { filledTextField } from '../../common';
import { PositionDiagramPaneType } from '../../common/connectivity/connectivity.type';
import { SWITCH_TYPE } from '../creation';
import { BusBarSections } from './voltageLevelSectionCreation.types';
import { POSITION_NEW_SECTION_SIDE } from './voltageLevelSectionCreation.utils';

const getArrayPosition = (data: BusBarSections, selectedOptionId: string) => {
if (!selectedOptionId || !data) {
return { position: -1, length: 0 };
}

const matchingArray = Object.values(data).find(
(array) => Array.isArray(array) && array.indexOf(selectedOptionId) !== -1
);
if (matchingArray) {
return { position: matchingArray.indexOf(selectedOptionId), length: matchingArray.length };
}
return { position: -1, length: 0 };
};

type OptionWithDisabled = Option & { disabled?: boolean };

export interface VoltageLevelSectionCreationFormProps {
busBarSectionInfos?: BusBarSections;
allBusbarSectionsList?: string[];
isSymmetricalNbBusBarSections?: boolean;
isNotFoundOrNotSupported?: boolean;
PositionDiagramPane?: PositionDiagramPaneType;
}

export function VoltageLevelSectionCreationForm({
busBarSectionInfos,
allBusbarSectionsList,
isSymmetricalNbBusBarSections = false,
isNotFoundOrNotSupported = false,
PositionDiagramPane,
}: Readonly<VoltageLevelSectionCreationFormProps>) {
const intl = useIntl();
const { isNodeBuilt, isUpdate } = useCustomFormContext();
const [isDiagramPaneOpen, setIsDiagramPaneOpen] = useState(false);
const [busBarSectionsIdOptions, setBusBarSectionsIdOptions] = useState<Option[]>([]);
const [isNotRequiredSwitchBefore, setIsNotRequiredSwitchBefore] = useState(false);
const [isNotRequiredSwitchAfter, setIsNotRequiredSwitchAfter] = useState(false);
const { setValue } = useFormContext();
const voltageLevelId = useWatch({ name: FieldConstants.EQUIPMENT_ID });
const busbarIndex = useWatch({ name: FieldConstants.BUS_BAR_INDEX });
const selectedOption = useWatch({ name: FieldConstants.BUSBAR_SECTION_ID });
const selectedPositionOption = useWatch({ name: FieldConstants.IS_AFTER_BUSBAR_SECTION_ID });
// free input mode (no suggestions) is used when no network busbar section data is available,
// e.g. when this form is used outside of a study (GridExplore)
const isFreeInputMode = !busBarSectionInfos;

const voltageLevelIdField = (
<TextField
size="small"
fullWidth
label={intl.formatMessage({ id: 'VoltageLevelId' })}
value={voltageLevelId ?? ''}
slotProps={{
input: {
readOnly: true,
},
}}
disabled
{...filledTextField}
/>
);
const switchValue = useWatch({ name: FieldConstants.NEW_SWITCH_STATES });

useEffect(() => {
if (busBarSectionInfos && busbarIndex) {
const selectedKey = busbarIndex?.id;
setValue(FieldConstants.ALL_BUS_BAR_SECTIONS, false);
if (selectedKey === 'all') {
setValue(FieldConstants.ALL_BUS_BAR_SECTIONS, true);
if (allBusbarSectionsList && Array.isArray(allBusbarSectionsList)) {
const options = allBusbarSectionsList
.filter((id): id is string => Boolean(id))
.map((id) => ({
id,
Comment thread
KoloMenek marked this conversation as resolved.
label: id,
}));
setBusBarSectionsIdOptions(options);
} else {
setBusBarSectionsIdOptions([]);
}
return;
}
const sections = busBarSectionInfos[selectedKey];
if (!sections || !Array.isArray(sections)) {
setBusBarSectionsIdOptions([]);
return;
}
const options = sections
.filter((id): id is string => Boolean(id))
.map((id) => ({
id,
label: id,
}));
setBusBarSectionsIdOptions(options);
} else {
setBusBarSectionsIdOptions([]);
}
}, [allBusbarSectionsList, busBarSectionInfos, intl, busbarIndex, setValue]);

const arrayPosition = useMemo(
() => busBarSectionInfos && getArrayPosition(busBarSectionInfos, selectedOption?.id),
[busBarSectionInfos, selectedOption?.id]
);

useEffect(() => {
if (selectedOption && selectedPositionOption && busBarSectionInfos && arrayPosition) {
const selectedSectionIndex = arrayPosition.position;
const busBarSections = arrayPosition.length - 1;
if (selectedSectionIndex === 0 && selectedPositionOption === POSITION_NEW_SECTION_SIDE.BEFORE.id) {
setValue(FieldConstants.SWITCH_BEFORE_NOT_REQUIRED, true);
setIsNotRequiredSwitchBefore(true);
} else {
setValue(FieldConstants.SWITCH_BEFORE_NOT_REQUIRED, false);
setIsNotRequiredSwitchBefore(false);
}
if (
busBarSections === selectedSectionIndex &&
selectedPositionOption === POSITION_NEW_SECTION_SIDE.AFTER.id
) {
setValue(FieldConstants.SWITCH_AFTER_NOT_REQUIRED, true);
setIsNotRequiredSwitchAfter(true);
} else {
setValue(FieldConstants.SWITCH_AFTER_NOT_REQUIRED, false);
setIsNotRequiredSwitchAfter(false);
Comment thread
benrejebmoh marked this conversation as resolved.
}
}
if (isUpdate && isNodeBuilt) {
setValue(FieldConstants.SWITCH_AFTER_NOT_REQUIRED, true);
setValue(FieldConstants.SWITCH_BEFORE_NOT_REQUIRED, true);
}
}, [selectedOption, setValue, busBarSectionInfos, selectedPositionOption, arrayPosition, isUpdate, isNodeBuilt]);

const busBarIndexOptions = useMemo((): OptionWithDisabled[] => {
if (busBarSectionInfos) {
const sortedOptions = Object.keys(busBarSectionInfos || {})
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
.map((key) => ({
id: key,
label: key,
}));
const allOption = {
id: 'all',
label: intl.formatMessage({ id: 'allBusbarSections' }),
disabled: !isSymmetricalNbBusBarSections,
} as Option & { disabled?: boolean };

return [...sortedOptions, allOption];
}
return [];
}, [busBarSectionInfos, intl, isSymmetricalNbBusBarSections]);

const getOptionLabel = (object: string | { id: string | number; label: string | number }) => {
if (typeof object === 'string') {
return object;
}
if (object?.id === 'all') {
return intl.formatMessage({ id: 'allBusbarSections' }) ?? '';
}
return String(object?.id ?? '');
};

const isOptionEqualToValue = (val1: Option, val2: Option) => {
const getId = (option: Option) => {
return typeof option === 'string' ? option : String(option?.id ?? '');
};

return getId(val1) === getId(val2);
};

const handleChangeBusbarIndex = useCallback(() => {
if (!isFreeInputMode) {
setValue(FieldConstants.BUSBAR_SECTION_ID, null);
}
}, [isFreeInputMode, setValue]);

const freeInputOutputTransform = useCallback(
(value: Option | null) => (typeof value === 'string' ? { id: value, label: value } : value),
[]
);

const busbarCountField = (
<AutocompleteInput
name={FieldConstants.BUS_BAR_INDEX}
label="Busbar"
onChangeCallback={handleChangeBusbarIndex}
options={busBarIndexOptions as Option[]}
getOptionLabel={getOptionLabel}
isOptionEqualToValue={isOptionEqualToValue}
allowNewValue={isFreeInputMode}
outputTransform={freeInputOutputTransform}
renderOption={(props, option) => {
const allOptionsDisabled = (option as any).id === 'all' && (option as any)?.disabled;
const { key, ...otherProps } = props;
return (
<li key={key} {...otherProps}>
<div>
<div>{getOptionLabel(option)}</div>
{allOptionsDisabled && (
<div
style={{
fontSize: '0.85rem',
color: 'red',
marginTop: '2px',
}}
>
{intl.formatMessage({ id: 'allOptionHelperText' })}
</div>
)}
</div>
</li>
);
}}
getOptionDisabled={(option) => (option as any)?.disabled}
size="small"
disabled={isNotFoundOrNotSupported}
/>
);

const busbarSectionsField = (
<AutocompleteInput
name={FieldConstants.BUSBAR_SECTION_ID}
label="BusBarSectionsReference"
options={busBarSectionsIdOptions}
getOptionLabel={getObjectId}
isOptionEqualToValue={areIdsEqual}
allowNewValue={isFreeInputMode}
outputTransform={freeInputOutputTransform}
size="small"
disabled={!busbarIndex || isNotFoundOrNotSupported}
/>
);

const positionSideNewSectionField = (
<SelectInput
name={FieldConstants.IS_AFTER_BUSBAR_SECTION_ID}
label="isAfterBusBarSectionId"
options={Object.values(POSITION_NEW_SECTION_SIDE)}
size="small"
disabled={!busbarIndex || isNotFoundOrNotSupported}
/>
);

const switchBeforeField = (
<SelectInput
name={FieldConstants.SWITCHES_BEFORE_SECTIONS}
label="switchesBeforeSections"
options={Object.values(SWITCH_TYPE)}
size="small"
disabled={!busbarIndex || isNotRequiredSwitchBefore || isNotFoundOrNotSupported}
/>
);
const switchAfterField = (
<SelectInput
name={FieldConstants.SWITCHES_AFTER_SECTIONS}
label="switchesAfterSections"
options={Object.values(SWITCH_TYPE)}
size="small"
disabled={!busbarIndex || isNotRequiredSwitchAfter || isNotFoundOrNotSupported}
/>
);
const newSwitchState = (
<SwitchInput
name={FieldConstants.NEW_SWITCH_STATES}
label={switchValue ? 'areSwitchesClosed' : 'areSwitchesOpen'}
/>
);
const getLabelDescription = useCallback(() => {
return intl.formatMessage({ id: 'newSection' });
}, [intl]);
const newSectionField = (
<Slider
min={0}
max={3}
step={0.1}
value={1.5}
track={false}
valueLabelFormat={getLabelDescription}
valueLabelDisplay="on"
size="medium"
disabled
sx={{
'& .MuiSlider-thumb': {
backgroundColor: '#1976d2',
color: '#1976d2',
'&:hover': {
backgroundColor: '#1976d2',
},
'&.Mui-disabled': {
backgroundColor: '#1976d2',
color: '#1976d2',
},
},
}}
/>
);
const handleCloseDiagramPane = useCallback(() => {
setIsDiagramPaneOpen(false);
}, []);
const handleClickOpenDiagramPane = useCallback(() => {
setIsDiagramPaneOpen(true);
}, []);
const diagramToolTip = (
<Tooltip sx={{ paddingLeft: 1 }} title={intl.formatMessage({ id: 'builtNodeTooltipForDiagram' })}>
<InfoOutlined color="info" fontSize="medium" />
</Tooltip>
);
return (
<Box sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid size={12}>
<Grid container spacing={2} alignItems="center">
<Grid size={{ xs: 12, md: 6 }}>{voltageLevelIdField}</Grid>
{PositionDiagramPane && isNodeBuilt && (
<Grid size={{ xs: 12, md: 3 }}>
<Button onClick={handleClickOpenDiagramPane} variant="outlined" size="small">
<FormattedMessage id="CreateCouplingDeviceDiagramButton" />
</Button>
{diagramToolTip}
</Grid>
)}
</Grid>
</Grid>
{isNotFoundOrNotSupported && (
<Grid size={12}>
<Typography variant="body1" color="red">
<FormattedMessage id="notValidVoltageLevel" />
</Typography>
</Grid>
)}
<Grid size={12}>
<GridSection title="SectionPosition" />
</Grid>

<Grid size={4}>{busbarCountField}</Grid>
<Grid size={4}>{busbarSectionsField}</Grid>
<Grid size={4}>{positionSideNewSectionField}</Grid>

<Grid size={12}>
<GridSection title="Switch" />
</Grid>

<Grid size={{ xs: 12, sm: 4 }}>{switchBeforeField}</Grid>
<Grid size={{ xs: 12, sm: 4 }}>{newSectionField}</Grid>
<Grid size={{ xs: 12, sm: 4 }}>{switchAfterField}</Grid>
<Grid size={12}>{newSwitchState}</Grid>
</Grid>
{PositionDiagramPane && (
<PositionDiagramPane
open={isDiagramPaneOpen}
onClose={handleCloseDiagramPane}
voltageLevelId={voltageLevelId}
/>
)}
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* 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/.
*/

export * from './VoltageLevelSectionCreationForm';
export * from './voltageLevelSectionCreation.types';
export * from './voltageLevelSectionCreation.utils';
Loading
Loading