Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bea993d
Add network modification row expansion
Mar 20, 2026
91f6853
Implement composite lazy loading
Mar 20, 2026
2ef2e70
Fix refresh system
Mar 20, 2026
0cda0d2
Fix name overflow ellipsis
Mar 23, 2026
f075833
Merge remote-tracking branch 'origin/main' into marcellinh/add_networ…
Mar 23, 2026
ca62d21
Move utils code
Mar 23, 2026
f5bf572
Change naming
Mar 23, 2026
8b501bf
Add drag indicators
Mar 23, 2026
a2dddef
Refine border style
Mar 23, 2026
559ccbd
Move styles to dedicated file
Mar 23, 2026
3d6d4e6
Simplify
Mar 24, 2026
d8ab20f
Merge remote-tracking branch 'origin/main' into marcellinh/add_networ…
Mar 24, 2026
99f9eb6
Unused variable
Mar 24, 2026
fd71481
Add composite insertion
Mar 24, 2026
8ec4d6f
Refine import dialog
Mar 24, 2026
b35eccf
Lint
Mar 24, 2026
b614074
Lint
Mar 24, 2026
b98a588
Filter delete sub modifications
Mar 24, 2026
f196777
Fix drag row clone style
Mar 24, 2026
c5e0708
Rationalize depth 0 dnd
Mar 24, 2026
4960cdb
Fix range selection
Mar 24, 2026
68a7902
Style rework and dnd handling
Mar 25, 2026
faaeecd
Merge remote-tracking branch 'origin/main' into marcellinh/add_networ…
Mar 25, 2026
8843c03
Fix transient tree state where rows gets duplicated after inter depth…
Mar 25, 2026
0b7bc1c
Fix typing issues
Mar 26, 2026
2a014d9
Merge main
Mar 26, 2026
c84d91f
Simplify drag end
Mar 26, 2026
9773f13
Factorize code
Mar 26, 2026
fa3bfe1
Sonar issue
Mar 26, 2026
a36bdef
Lint
Mar 26, 2026
469d3cd
Fix switch cell for subrows
Mar 26, 2026
d4d9198
Clean up
Mar 26, 2026
662ed87
Merge remote-tracking branch 'origin/main' into marcellinh/add_networ…
Mar 26, 2026
868b7c3
Fix refresh mechanism to avoid not expanded data stale state
Mar 26, 2026
6609e07
Merge branch 'main' into marcellinh/add_network_modification_row_expa…
Meklo Mar 26, 2026
a14ab3d
Make single call to refresh composite submodifications
Mar 27, 2026
47cf07b
Merge main
Mar 27, 2026
898d34b
Fix submodification refresh after dragging nested composite
Mar 27, 2026
157b398
Clean up name style structure
Mar 27, 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
259 changes: 226 additions & 33 deletions src/components/dialogs/import-modification-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,66 +5,259 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { useIntl } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import {
CustomFormProvider,
DirectoryItemSelector,
ElementType,
snackWithFallback,
TreeViewFinderNodeProps,
useSnackMessage,
} from '@gridsuite/commons-ui';
import { copyOrMoveModifications } from '../../services/study';
import { FunctionComponent } from 'react';
import { insertCompositeModifications } from '../../services/study';
import { FunctionComponent, useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'redux/reducer.type';
import { NetworkModificationCopyType } from 'components/graph/menus/network-modifications/network-modification-menu.type';
import { CompositeModificationsActionType } from 'components/graph/menus/network-modifications/network-modification-menu.type';
import {
Box,
Button,
FormControl,
FormControlLabel,
Grid,
Radio,
RadioGroup,
TextField,
Typography,
} from '@mui/material';
import { NoteAlt as NoteAltIcon } from '@mui/icons-material';
import { Controller, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import yup from 'components/utils/yup-config';
import { ModificationDialog } from './commons/modificationDialog';
import { ACTION, COMPOSITE_NAMES, SELECTED_MODIFICATIONS } from 'components/utils/field-constants';

/**
* Dialog to select some network modifications and append them in the current node
* @param {Boolean} open Is the dialog open ?
* @param {EventListener} onClose Event to close the dialog
* @param currentNode the current node
* @param studyUuid Id of the current study
* Dialog to select composite network modifications and append them to the current node.
* - SPLIT mode: extracts and inserts individual modifications contained in the composites.
* - INSERT mode: inserts the composite modifications as-is; a name per composite is required.
*
* The composite selection is presented inline (like RootNetworkCaseSelection):
* a list icon + selected item names + a button to open the DirectoryItemSelector.
* Name fields (one per selected item) appear only when INSERT mode is active,
* and are validated via react-hook-form + yup.
*
* @param open Whether the dialog is open
* @param onClose Callback to close the dialog
*/

interface ImportModificationDialogProps {
open: boolean;
onClose: () => void;
}

interface SelectedModification {
id: string;
name: string;
}

interface FormData {
[ACTION]: CompositeModificationsActionType;
[SELECTED_MODIFICATIONS]: SelectedModification[];
[COMPOSITE_NAMES]: Record<string, string>;
}

const emptyFormData: FormData = {
[ACTION]: CompositeModificationsActionType.SPLIT,
[SELECTED_MODIFICATIONS]: [],
[COMPOSITE_NAMES]: {},
};

const formSchema = yup
.object()
.shape({
[ACTION]: yup
.mixed<CompositeModificationsActionType>()
.oneOf(Object.values(CompositeModificationsActionType))
.required(),
[SELECTED_MODIFICATIONS]: yup.array().min(1).required(),
[COMPOSITE_NAMES]: yup.mixed<Record<string, string>>().when([ACTION], {
is: CompositeModificationsActionType.INSERT,
then: (schema) =>

Check failure on line 86 in src/components/dialogs/import-modification-dialog.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not add `then` to an object.

See more on https://sonarcloud.io/project/issues?id=gridsuite_gridstudy-app&issues=AZ0pZjpOnsJ7xn3-QLFU&open=AZ0pZjpOnsJ7xn3-QLFU&pullRequest=3824
schema.test('all-names-filled', 'FieldIsRequired', function (value) {
const selections: SelectedModification[] = this.parent[SELECTED_MODIFICATIONS] ?? [];
for (const m of selections) {
const name = value?.[m.id] ?? '';
if (!name.trim()) {
return this.createError({
path: `${COMPOSITE_NAMES}.${m.id}`,
message: 'FieldIsRequired',
});
}
}

return true;
}),
otherwise: (schema) => schema.optional(),
}),
})
.required();

type FormSchemaType = yup.InferType<typeof formSchema>;

const ImportModificationDialog: FunctionComponent<ImportModificationDialogProps> = ({ open, onClose }) => {
const intl = useIntl();
const { snackError } = useSnackMessage();
const studyUuid = useSelector((state: AppState) => state.studyUuid);
const currentNode = useSelector((state: AppState) => state.currentTreeNode);

const processSelectedElements = (selectedElements: TreeViewFinderNodeProps[]) => {
const modificationUuidList = selectedElements.map((e) => e.id);
// import selected modifications
if (modificationUuidList.length > 0 && studyUuid && currentNode) {
const copyInfos = {
copyType: NetworkModificationCopyType.SPLIT_COMPOSITE,
originStudyUuid: studyUuid,
originNodeUuid: currentNode.id,
};
copyOrMoveModifications(studyUuid, currentNode.id, modificationUuidList, copyInfos).catch((error) => {
snackWithFallback(snackError, error, { headerId: 'errDuplicateModificationMsg' });
});
}
// close the file selector
onClose();
const [isSelectorOpen, setIsSelectorOpen] = useState(false);

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

const {
control,
reset,
watch,
setValue,
formState: { isValid },
} = formMethods;

const action = watch(ACTION);
const selectedModifications = watch(SELECTED_MODIFICATIONS);
const compositeNames = watch(COMPOSITE_NAMES);
const isInsertMode = action === CompositeModificationsActionType.INSERT;

const handleSelectModification = (selectedElements: TreeViewFinderNodeProps[]) => {
setIsSelectorOpen(false);
if (!selectedElements.length) return;

const newSelections = selectedElements.map((e) => ({ id: e.id, name: e.name }));
setValue(SELECTED_MODIFICATIONS, newSelections, { shouldValidate: true, shouldDirty: true });

const currentNames = compositeNames ?? {};
setValue(COMPOSITE_NAMES, Object.fromEntries(newSelections.map((e) => [e.id, currentNames[e.id] ?? e.name])), {
shouldValidate: true,
});
};

const handleClear = useCallback(() => {
reset(emptyFormData);
}, [reset]);

const handleSave = useCallback(
(values: FormData) => {
if (!studyUuid || !currentNode) return;
const modificationsToInsert: { first: string; second: string }[] = values[SELECTED_MODIFICATIONS].map(
(m) => ({ first: m.id, second: values[COMPOSITE_NAMES][m.id] ?? m.name })
);
insertCompositeModifications(studyUuid, currentNode.id, modificationsToInsert, values[ACTION]).catch(
(error) => {
snackWithFallback(snackError, error, { headerId: 'errInsertCompositeModificationMsg' });
}
);
onClose();
},
[studyUuid, currentNode, snackError, onClose]
);

return (
<DirectoryItemSelector
open={open}
onClose={processSelectedElements}
types={[ElementType.MODIFICATION]}
multiSelect={true}
title={intl.formatMessage({
id: 'ModificationsSelection',
})}
/>
<CustomFormProvider validationSchema={formSchema} {...formMethods}>
<ModificationDialog
fullWidth
maxWidth="sm"
open={open}
onClose={onClose}
onClear={handleClear}
onSave={handleSave}
titleId="ModificationsImport"
disabledSave={!isValid}
>
<Grid container spacing={2} direction="column" marginTop="auto">
<Grid item>
<FormControl>
<Controller
name={ACTION}
control={control}
render={({ field }) => (
<RadioGroup row {...field}>
<FormControlLabel
value={CompositeModificationsActionType.SPLIT}
control={<Radio size="small" />}
label={intl.formatMessage({ id: 'CompositeModificationsActionSplit' })}
/>
<FormControlLabel
value={CompositeModificationsActionType.INSERT}
control={<Radio size="small" />}
label={intl.formatMessage({ id: 'CompositeModificationsActionInsert' })}
/>
</RadioGroup>
)}
/>
</FormControl>
</Grid>
<Grid container alignItems="center" item>
<Grid item display="flex" marginLeft={1}>
<NoteAltIcon />
</Grid>
<Typography m={1} component="span">
<Box fontWeight="fontWeightBold">{selectedModifications.map((m) => m.name).join(', ')}</Box>
</Typography>
<Grid item>
<Button
size={selectedModifications.length ? 'small' : 'medium'}
onClick={() => setIsSelectorOpen(true)}
>
<FormattedMessage id={'ChooseModifications'} />
</Button>
</Grid>
</Grid>

{isInsertMode && selectedModifications.length > 0 && (
<Grid item>
<Grid container spacing={1} direction="column">
{selectedModifications.map((m) => (
<Grid item key={m.id}>
<Controller
name={`${COMPOSITE_NAMES}.${m.id}`}
control={control}
render={({ field, fieldState }) => {
return (
<TextField
{...field}
label={m.name}
fullWidth
required
size="small"
error={!!fieldState.error}
helperText={
fieldState.error
? intl.formatMessage({ id: 'FieldIsRequired' })
: undefined
}
/>
);
}}
/>
</Grid>
))}
</Grid>
</Grid>
)}
</Grid>
</ModificationDialog>

<DirectoryItemSelector
open={isSelectorOpen}
onClose={handleSelectModification}
types={[ElementType.MODIFICATION]}
multiSelect={true}
title={intl.formatMessage({ id: 'ModificationsSelection' })}
/>
</CustomFormProvider>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ export interface ExcludedNetworkModifications {
export enum NetworkModificationCopyType {
COPY = 'COPY',
MOVE = 'MOVE',
SPLIT_COMPOSITE = 'SPLIT_COMPOSITE',
INSERT_COMPOSITE = 'INSERT_COMPOSITE',
}

// New enum for the dedicated composite-modifications endpoint
export enum CompositeModificationsActionType {
SPLIT = 'SPLIT', // Extract individual modifications from the composite
INSERT = 'INSERT', // Keep and insert the composite modification as-is
}

export interface NetworkModificationCopyInfos {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ import { AppState } from 'redux/reducer.type';
import { createCompositeModifications, updateCompositeModifications } from '../../../../services/explore';
import { copyOrMoveModifications } from '../../../../services/study';
import {
changeNetworkModificationOrder,
fetchExcludedNetworkModifications,
fetchNetworkModifications,
stashModifications,
Expand Down Expand Up @@ -123,7 +122,6 @@ import { LimitSetsModificationDialog } from '../../../dialogs/network-modificati
import CreateVoltageLevelSectionDialog from '../../../dialogs/network-modifications/voltage-level/section/create-voltage-level-section-dialog';
import MoveVoltageLevelFeederBaysDialog from '../../../dialogs/network-modifications/voltage-level/move-feeder-bays/move-voltage-level-feeder-bays-dialog';
import { useCopiedNetworkModifications } from 'hooks/copy-paste/use-copied-network-modifications';
import { DragStart, DropResult } from '@hello-pangea/dnd';
import { FetchStatus } from '../../../../services/utils.type';

const nonEditableModificationTypes = new Set([
Expand Down Expand Up @@ -163,7 +161,6 @@ const NetworkModificationNodeEditor = () => {
const [selectedNetworkModifications, setSelectedNetworkModifications] = useState<NetworkModificationMetadata[]>([]);

const [isDragging, setIsDragging] = useState(false);
const [initialPosition, setInitialPosition] = useState<number | undefined>(undefined);

const [editDialogOpen, setEditDialogOpen] = useState<string | undefined>(undefined);
const [editData, setEditData] = useState<NetworkModificationData | undefined>(undefined);
Expand Down Expand Up @@ -1141,47 +1138,13 @@ const NetworkModificationNodeEditor = () => {
[doEditModification, isModificationClickable]
);

const onRowDragStart = (event: DragStart) => {
const onRowDragStart = useCallback(() => {
setIsDragging(true);
setInitialPosition(event.source.index);
};

const onRowDragEnd = (event: DropResult) => {
if (!event.destination) {
setIsDragging(false);
return;
}

let newPosition = event.destination.index;
const oldPosition = initialPosition;

if (!currentNode?.id || newPosition === undefined || oldPosition === undefined || newPosition === oldPosition) {
setIsDragging(false);
return;
}
if (newPosition === -1) {
newPosition = modifications.length;
}

const previousModifications = [...modifications];
const updatedModifications = [...modifications];

const [movedItem] = updatedModifications.splice(oldPosition, 1);
updatedModifications.splice(newPosition, 0, movedItem);

setModifications(updatedModifications);

const before = updatedModifications[newPosition + 1]?.uuid || null;
}, []);

changeNetworkModificationOrder(studyUuid, currentNode?.id, movedItem.uuid, before)
.catch((error) => {
snackWithFallback(snackError, error, { headerId: 'errReorderModificationMsg' });
setModifications(previousModifications);
})
.finally(() => {
setIsDragging(false);
});
};
const onRowDragEnd = useCallback(() => {
setIsDragging(false);
}, []);

const isPasteButtonDisabled = useMemo(() => {
return networkModificationsToCopy.length <= 0 || isAnyNodeBuilding || mapDataLoading || !currentNode;
Expand Down
Loading
Loading