From 5927d3e55bfaba541172706d2ab4192ca6a5ba55 Mon Sep 17 00:00:00 2001 From: "SOUISSI Maissa (Externe)" Date: Tue, 26 May 2026 01:00:43 +0200 Subject: [PATCH 01/17] import modification dialog : composite selection Signed-off-by: SOUISSI Maissa (Externe) --- .../dialogs/import-modification-dialog.tsx | 541 ++++++++++++------ src/translations/en.json | 4 +- src/translations/fr.json | 4 +- 3 files changed, 377 insertions(+), 172 deletions(-) diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx index 97d7eed77d..e712bb099c 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-modification-dialog.tsx @@ -9,6 +9,9 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { CustomFormProvider, DirectoryItemSelector, + DndColumn, + DndColumnType, + DndTable, ElementType, PARAM_DEVELOPER_MODE, snackWithFallback, @@ -16,35 +19,48 @@ import { useSnackMessage, } from '@gridsuite/commons-ui'; import { insertCompositeModifications, type ModificationPair } from '../../services/study'; -import { JSX, useCallback, useEffect, useState } from 'react'; +import { JSX, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'redux/reducer.type'; import { CompositeModificationAction } from 'components/graph/menus/network-modifications/network-modification-menu.type'; -import { Button, FormControl, Grid, Radio, RadioGroup, TextField, Typography } from '@mui/material'; -import { NoteAlt as NoteAltIcon } from '@mui/icons-material'; -import { Controller, useController, useForm } from 'react-hook-form'; +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + FormControlLabel, + Radio, + RadioGroup, + Step, + StepLabel, + Stepper, + TextField, + Typography, +} from '@mui/material'; + +import { + Controller, + useController, + useFieldArray, + useForm, + UseFieldArrayReturn, + useFormContext, + useWatch, +} 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'; -import GridItem from './commons/grid-item'; +import { ACTION, SELECTED_MODIFICATIONS } from 'components/utils/field-constants'; import { UUID } from 'node:crypto'; import { useParameterState } from './parameters/use-parameters-state'; -import FormControlLabel from '@mui/material/FormControlLabel'; -/** - * 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 - */ +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + interface ImportModificationDialogProps { open: boolean; onClose: () => void; @@ -53,67 +69,125 @@ interface ImportModificationDialogProps { interface SelectedComposite { id: UUID; name: string; + originalName: string; + isShared: boolean; } -type CompositeNameOverrides = Record; interface FormData { [ACTION]: CompositeModificationAction; [SELECTED_MODIFICATIONS]: SelectedComposite[]; - [COMPOSITE_NAMES]: CompositeNameOverrides; } const emptyFormData: FormData = { [ACTION]: CompositeModificationAction.SPLIT, [SELECTED_MODIFICATIONS]: [], - [COMPOSITE_NAMES]: {}, }; +// --------------------------------------------------------------------------- +// Sub-components — defined outside main component to respect Rules of Hooks +// --------------------------------------------------------------------------- + +interface SharedCellProps { + rowIndex: number; +} + +function SharedCell({ rowIndex }: Readonly) { + const isShared: boolean = useWatch({ + name: `${SELECTED_MODIFICATIONS}.${rowIndex}.isShared`, + }); + + return ( + } + label={ + + + + } + sx={{ mr: 0, alignItems: 'center' }} + /> + ); +} + +interface InsertNameCellProps { + rowIndex: number; +} + +function InsertNameCell({ rowIndex }: Readonly) { + const { control } = useFormContext(); + const originalName: string = useWatch({ + name: `${SELECTED_MODIFICATIONS}.${rowIndex}.originalName`, + }); + + return ( + + ( + + )} + /> + + {originalName} + + + ); +} + +// --------------------------------------------------------------------------- +// Validation schema +// --------------------------------------------------------------------------- + const formSchema = yup .object() .shape({ [ACTION]: yup.mixed().oneOf(Object.values(CompositeModificationAction)).required(), - [SELECTED_MODIFICATIONS]: yup.array().min(1).required(), - [COMPOSITE_NAMES]: yup.mixed().when(ACTION, ([action], schema) => { - if (action === CompositeModificationAction.INSERT) { - return schema.test('all-names-filled', 'FieldIsRequired', function (value) { - const selections: SelectedComposite[] = 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', - }); + [SELECTED_MODIFICATIONS]: yup + .array() + .min(1) + .when(ACTION, ([action], schema) => { + if (action === CompositeModificationAction.INSERT) { + return schema.test('all-names-filled', 'FieldIsRequired', function (rows) { + for (const row of rows ?? []) { + if (!row.name?.trim()) { + return this.createError({ + path: `${SELECTED_MODIFICATIONS}`, + message: 'FieldIsRequired', + }); + } } - } - return true; - }); - } - - return schema.optional(); - }), + return true; + }); + } + return schema; + }) + .required(), }) .required() as yup.ObjectSchema; type FormSchemaType = yup.InferType; -const ImportModificationDialog: ({ open, onClose }: Readonly) => JSX.Element = ({ - open, - onClose, -}) => { +const STEP_SELECTION = 0; +const STEP_ORGANIZATION = 1; + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- + +const ImportModificationDialog = ({ open, onClose }: Readonly): JSX.Element => { const intl = useIntl(); const { snackError } = useSnackMessage(); const studyUuid = useSelector((state: AppState) => state.studyUuid); const currentNode = useSelector((state: AppState) => state.currentTreeNode); + const [isDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE); + const [activeStep, setActiveStep] = useState(STEP_SELECTION); const [isSelectorOpen, setIsSelectorOpen] = useState(false); - const [hasConfirmedFirstSelection, setHasConfirmedFirstSelection] = useState(false); - useEffect(() => { - if (open) { - setIsSelectorOpen(true); - } - }, [open]); + // ----------------------------------------------------------------------- + // Form + // ----------------------------------------------------------------------- const formMethods = useForm({ defaultValues: emptyFormData, @@ -127,13 +201,83 @@ const ImportModificationDialog: ({ open, onClose }: Readonly ({ + label: '', + dataKey: 'isShared', + initialValue: false, + editable: true, + width: 160, + type: DndColumnType.CUSTOM, + component: (rowIndex: number) => , + }), + [] + ); + + // SPLIT mode — name is read-only, drag-and-drop only + const splitColumnsDefinition: DndColumn[] = useMemo( + () => [ + { + label: '', + dataKey: 'name', + initialValue: '', + editable: false, + type: DndColumnType.TEXT, + }, + sharedColumn, + ], + [sharedColumn] + ); + + // INSERT mode — name is editable with original name shown below + const insertColumnsDefinition: DndColumn[] = useMemo( + () => [ + { + label: '', + dataKey: 'name', + initialValue: '', + editable: true, + type: DndColumnType.CUSTOM, + component: (rowIndex: number) => , + }, + sharedColumn, + ], + [sharedColumn] + ); + + const columnsDefinition = isInsertMode ? insertColumnsDefinition : splitColumnsDefinition; + + // ----------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------- + + useEffect(() => { + if (open) { + setActiveStep(STEP_SELECTION); + setIsSelectorOpen(true); + } else { + setIsSelectorOpen(false); + reset(emptyFormData); + } + }, [open, reset]); useEffect(() => { if (!isDeveloperMode && action === CompositeModificationAction.INSERT) { @@ -141,141 +285,198 @@ const ImportModificationDialog: ({ open, onClose }: Readonly { - setIsSelectorOpen(true); - }, []); + // ----------------------------------------------------------------------- + // Selection handler + // ----------------------------------------------------------------------- const handleSelectModification = useCallback( (selectedElements: TreeViewFinderNodeProps[]) => { setIsSelectorOpen(false); if (!selectedElements.length) { - // Cancel before any confirmed selection -> close the whole dialog. - if (!hasConfirmedFirstSelection) { + if (selectedModifications.length === 0) { onClose(); } return; } - setHasConfirmedFirstSelection(true); - const newSelections = selectedElements.map((e) => ({ id: e.id, name: e.name })); - setValue(SELECTED_MODIFICATIONS, newSelections, { shouldValidate: true, shouldDirty: true }); + const newRows: SelectedComposite[] = selectedElements.map((e) => ({ + id: e.id as UUID, + name: e.name, + originalName: e.name, + // isShared comes from the directory element; default false if not provided + isShared: false, + })); - const currentNames = compositeNames ?? {}; - setValue( - COMPOSITE_NAMES, - Object.fromEntries(newSelections.map((e) => [e.id, currentNames[e.id] ?? e.name])), - { shouldValidate: true } - ); + setValue(SELECTED_MODIFICATIONS, newRows, { + shouldValidate: true, + shouldDirty: true, + }); }, - [hasConfirmedFirstSelection, compositeNames, onClose, setValue] + [setValue, selectedModifications.length, onClose] ); - const handleClear = useCallback(() => { + // ----------------------------------------------------------------------- + // Navigation + // ----------------------------------------------------------------------- + + const handleNext = useCallback(() => { + if (!selectedModifications.length) return; + setActiveStep(STEP_ORGANIZATION); + }, [selectedModifications.length]); + + const handlePrevious = useCallback(() => { + setActiveStep(STEP_SELECTION); + setIsSelectorOpen(true); + }, []); + + const handleClose = useCallback(() => { reset(emptyFormData); - }, [reset]); - - const handleSave = useCallback( - (values: FormData) => { - if (!studyUuid || !currentNode) return; - const modificationsToInsert: ModificationPair[] = 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: 'importComposites.error' }); - } - ); - onClose(); - }, - [studyUuid, currentNode, snackError, onClose] - ); + setActiveStep(STEP_SELECTION); + setIsSelectorOpen(false); + onClose(); + }, [reset, onClose]); + + // ----------------------------------------------------------------------- + // Save + // ----------------------------------------------------------------------- + + const handleSave = useCallback(() => { + if (!studyUuid || !currentNode || !isValid) return; + + const rows: SelectedComposite[] = formMethods.getValues(SELECTED_MODIFICATIONS); + + const modificationsToInsert: ModificationPair[] = rows.map((m) => ({ + first: m.id, + second: m.name, + })); + + insertCompositeModifications( + studyUuid, + currentNode.id, + modificationsToInsert, + formMethods.getValues(ACTION) + ).catch((error) => snackWithFallback(snackError, error, { headerId: 'importComposites.error' })); + + handleClose(); + }, [studyUuid, currentNode, isValid, formMethods, snackError, handleClose]); + + // ----------------------------------------------------------------------- + // Render + // ----------------------------------------------------------------------- return ( - - - - - - - - - - - - } - value={CompositeModificationAction.SPLIT} - label={} - /> - } - value={CompositeModificationAction.INSERT} - label={} - disabled={!isDeveloperMode} + + + + + + + {/* ---- Stepper ---- */} + + STEP_SELECTION}> + + + + + + + + + + + + {/* ====================================================== + STEP 1 — SELECTION + ====================================================== */} + {activeStep === STEP_SELECTION && ( + + + + )} + + {/* ====================================================== + STEP 2 — ORGANIZATION & SHARING + ====================================================== */} + {activeStep === STEP_ORGANIZATION && ( + + {/* Radio group */} + + + + + + + } + label={} + /> + } + label={} + disabled={!isDeveloperMode} + /> + + + + + {/* Section label */} + + + + + {/* DnD table — thead hidden, no column headers needed */} + + - - - - - {selectedModifications.length > 0 && ( - - - {selectedModifications.map((m) => ( - - {isInsertMode ? ( - { - return ( - - ); - }} - /> - ) : ( - - {m.name} - - )} - - ))} - - )} - - - + + + )} + + + {/* ---- Actions ---- */} + + + {activeStep === STEP_ORGANIZATION && ( + + )} + + + + {activeStep === STEP_SELECTION ? ( + + ) : ( + + )} + + + ); }; diff --git a/src/translations/en.json b/src/translations/en.json index 99619bfd91..aeafbf9cd0 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -440,12 +440,14 @@ "CreateCompositeModificationLabel": "Create a new composite modification", "UpdateCompositeModificationLabel": "Replace an existing composite modification", "importComposites.title": "Import composite modifications", - "importComposites.label": "Modifications organisation: ", + "importComposites.organizationLabel": "Modifications organisation: ", "importComposites.action.split": "Only unitary", "importComposites.action.insert": "Composites", "importComposites.select": "Select composite modifications", "importComposites.selected": "Selected composite modifications: ", "importComposites.error": "Error during the composite modification import.", + "importComposites.step.selection": "Selection", + "importComposites.step.organization": "Organization & share", "descriptionProperty": "Description", "DeleteModificationText": "{numberToDelete, plural, =0 {No modification to delete.} =1 {One modification will be permanently deleted.} other {# modifications will be permanently deleted.}}", "HybridDisplay": "Hybrid display", diff --git a/src/translations/fr.json b/src/translations/fr.json index a07fdd1d5b..c0f7f5fab5 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -439,12 +439,14 @@ "CreateCompositeModificationLabel": "Créer une nouvelle modification composite", "UpdateCompositeModificationLabel": "Remplacer une modification composite existante", "importComposites.title": "Importer des modifications composites", - "importComposites.label": "Organisation des modifications : ", + "importComposites.organizationLabel": "Organisation des modifications : ", "importComposites.action.split": "Unitaires seulement", "importComposites.action.insert": "Composites", "importComposites.select": "Sélectionner les composites", "importComposites.selected": "Composites sélectionnées : ", "importComposites.error": "Erreur à l'import de la modification composite.", + "importComposites.step.selection": "Sélection", + "importComposites.step.organization": "Organisation & partage", "descriptionProperty": "Description", "DeleteModificationText": "{numberToDelete, plural, =0 {Aucune modification à supprimer.} =1 {Une modification va être définitivement supprimée.} other {# modifications vont être définitivement supprimées.}}", "HybridDisplay": "Affichage hybride", From 96a019c6f98f5ae13e63898c6958bea62842cf9f Mon Sep 17 00:00:00 2001 From: "SOUISSI Maissa (Externe)" Date: Tue, 26 May 2026 11:35:17 +0200 Subject: [PATCH 02/17] fixes Signed-off-by: SOUISSI Maissa (Externe) --- .../dialogs/import-modification-dialog.tsx | 50 ++----------------- src/translations/en.json | 5 +- src/translations/fr.json | 5 +- 3 files changed, 12 insertions(+), 48 deletions(-) diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx index e712bb099c..d9c589d21a 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-modification-dialog.tsx @@ -57,10 +57,6 @@ import { ACTION, SELECTED_MODIFICATIONS } from 'components/utils/field-constants import { UUID } from 'node:crypto'; import { useParameterState } from './parameters/use-parameters-state'; -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - interface ImportModificationDialogProps { open: boolean; onClose: () => void; @@ -83,10 +79,6 @@ const emptyFormData: FormData = { [SELECTED_MODIFICATIONS]: [], }; -// --------------------------------------------------------------------------- -// Sub-components — defined outside main component to respect Rules of Hooks -// --------------------------------------------------------------------------- - interface SharedCellProps { rowIndex: number; } @@ -135,10 +127,6 @@ function InsertNameCell({ rowIndex }: Readonly) { ); } -// --------------------------------------------------------------------------- -// Validation schema -// --------------------------------------------------------------------------- - const formSchema = yup .object() .shape({ @@ -171,10 +159,6 @@ type FormSchemaType = yup.InferType; const STEP_SELECTION = 0; const STEP_ORGANIZATION = 1; -// --------------------------------------------------------------------------- -// Main component -// --------------------------------------------------------------------------- - const ImportModificationDialog = ({ open, onClose }: Readonly): JSX.Element => { const intl = useIntl(); const { snackError } = useSnackMessage(); @@ -185,10 +169,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly({ defaultValues: emptyFormData, resolver: yupResolver(formSchema), @@ -214,10 +194,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly ({ @@ -265,10 +242,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { if (open) { setActiveStep(STEP_SELECTION); @@ -285,10 +258,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { setIsSelectorOpen(false); @@ -304,8 +273,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { if (!selectedModifications.length) return; setActiveStep(STEP_ORGANIZATION); @@ -337,10 +301,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { if (!studyUuid || !currentNode || !isValid) return; @@ -430,7 +390,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly - {/* Section label */} @@ -452,22 +411,21 @@ const ImportModificationDialog = ({ open, onClose }: Readonly - {/* ---- Actions ---- */} {activeStep === STEP_ORGANIZATION && ( )} {activeStep === STEP_SELECTION ? ( ) : ( {activeStep === STEP_SELECTION ? ( - ) : ( diff --git a/src/translations/en.json b/src/translations/en.json index 9f9c024ad3..649d9599fb 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -441,13 +441,14 @@ "UpdateCompositeModificationLabel": "Replace an existing composite modification", "importComposites.title": "Import composite modifications", "importComposites.organizationLabel": "Modifications organisation: ", - "importComposites.action.split": "Only unitary", + "importComposites.action.split": "Same", "importComposites.action.insert": "Composites", "importComposites.select": "Select composite modifications", "importComposites.selected": "Selected composite modifications: ", "importComposites.error": "Error during the composite modification import.", "importComposites.step.selection": "Selection", "importComposites.step.organization": "Organization & share", + "importComposites.shared": "Shared", "descriptionProperty": "Description", "DeleteModificationText": "{numberToDelete, plural, =0 {No modification to delete.} =1 {One modification will be permanently deleted.} other {# modifications will be permanently deleted.}}", "HybridDisplay": "Hybrid display", @@ -1547,7 +1548,6 @@ "deselectModification": "Deselect modification", "selectModification": "Select modification", "moveModification": "Move modification", - "button.cancel": "Cancel", "button.next": "Next", "button.previous": "Previous" } diff --git a/src/translations/fr.json b/src/translations/fr.json index 7c5ed856b0..ec403270a3 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -440,13 +440,14 @@ "UpdateCompositeModificationLabel": "Remplacer une modification composite existante", "importComposites.title": "Importer des modifications composites", "importComposites.organizationLabel": "Organisation des modifications : ", - "importComposites.action.split": "Unitaires seulement", + "importComposites.action.split": "Identiques", "importComposites.action.insert": "Composites", "importComposites.select": "Sélectionner les composites", "importComposites.selected": "Composites sélectionnées : ", "importComposites.error": "Erreur à l'import de la modification composite.", "importComposites.step.selection": "Sélection", "importComposites.step.organization": "Organisation & partage", + "importComposites.shared": "Partagé", "descriptionProperty": "Description", "DeleteModificationText": "{numberToDelete, plural, =0 {Aucune modification à supprimer.} =1 {Une modification va être définitivement supprimée.} other {# modifications vont être définitivement supprimées.}}", "HybridDisplay": "Affichage hybride", @@ -1542,7 +1543,6 @@ "deselectModification": "Désélectionner la modification", "selectModification": "Sélectionner la modification", "moveModification": "Déplacer la modification", - "button.cancel": "Annuler", "button.next": "Suivant", "button.previous": "Précédent" } From 7ce4492c2e43ecebffb3230634880412bce18e51 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Wed, 3 Jun 2026 16:07:51 +0200 Subject: [PATCH 05/17] import shared modification Signed-off-by: Mathieu DEHARBE --- .../dialogs/import-modification-dialog.tsx | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx index fb2fd70be0..9f81f6bd56 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-modification-dialog.tsx @@ -7,6 +7,7 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { + CheckboxInput, CustomFormProvider, DirectoryItemSelector, DndColumn, @@ -26,7 +27,6 @@ import { CompositeModificationAction } from 'components/graph/menus/network-modi import { Box, Button, - Checkbox, Dialog, DialogActions, DialogContent, @@ -97,21 +97,7 @@ interface SharedCellProps { } function SharedCell({ rowIndex }: Readonly) { - const isShared: boolean = useWatch({ - name: `${SELECTED_MODIFICATIONS}.${rowIndex}.isShared`, - }); - - return ( - } - label={ - - - - } - sx={{ mr: 0, alignItems: 'center' }} - /> - ); + return ; } interface InsertNameCellProps { From 08b72681131c0ca44774328daa569dcf84b354a8 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 4 Jun 2026 15:05:26 +0200 Subject: [PATCH 06/17] IS_SHARED as constant + cleaning Signed-off-by: Mathieu DEHARBE --- .../dialogs/import-modification-dialog.tsx | 48 +++++-------------- src/components/utils/field-constants.ts | 2 +- 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx index 9f81f6bd56..4d1e415220 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-modification-dialog.tsx @@ -54,7 +54,7 @@ import { useWatch, } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { ACTION, SELECTED_MODIFICATIONS } from 'components/utils/field-constants'; +import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from 'components/utils/field-constants'; import * as yup from 'yup'; import { UUID } from 'node:crypto'; import { useParameterState } from './parameters/use-parameters-state'; @@ -97,7 +97,9 @@ interface SharedCellProps { } function SharedCell({ rowIndex }: Readonly) { - return ; + return ( + + ); } interface InsertNameCellProps { @@ -184,7 +186,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly ({ label: '', - dataKey: 'isShared', + dataKey: IS_SHARED, initialValue: false, editable: true, width: '25%', @@ -265,7 +267,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { - setIsSelectorOpen(false); - - if (!selectedElements.length) { - if (selectedModifications.length === 0) { - onClose(); - } - return; - } - - const newRows: SelectedComposite[] = selectedElements.map((e) => ({ - id: e.id as UUID, - name: e.name, - originalName: e.name, - isShared: false, // actually is false, to be computed later - })); - - setValue(SELECTED_MODIFICATIONS, newRows, { - shouldValidate: true, - shouldDirty: true, - }); - }, - [setValue, selectedModifications.length, onClose] - ); - const handleNext = useCallback(() => { if (isNextDisabled) return; setActiveStep(STEP_ORGANIZATION); @@ -322,13 +298,13 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { if (!studyUuid || !currentNode || !isValid) return; - const rows: SelectedComposite[] = formMethods.getValues(SELECTED_MODIFICATIONS); - - const modificationsToInsert: ModificationPair[] = rows.map((m) => ({ + const modificationsToInsert: ModificationPair[] = selectedModifications.map((m: SelectedComposite) => ({ first: m.id, second: m.name, })); + // TODO : revoir l'envoi : l'ordre doit être préservé, donc faire un dto ? séparer les deux actions import ? + // [IS_SHARED]: selectedModifications.find((mod) => mod.id === e.id)?.isShared ?? false, insertCompositeModifications( studyUuid, currentNode.id, @@ -337,7 +313,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly snackWithFallback(snackError, error, { headerId: 'importComposites.error' })); handleClose(); - }, [studyUuid, currentNode, isValid, formMethods, snackError, handleClose]); + }, [studyUuid, currentNode, isValid, formMethods, snackError, handleClose, selectedModifications]); // ----------------------------------------------------------------------- // Render @@ -382,7 +358,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly setIsSelectorOpen(false)} types={[ElementType.MODIFICATION]} multiSelect inline diff --git a/src/components/utils/field-constants.ts b/src/components/utils/field-constants.ts index 065de66ae8..cd560c673d 100644 --- a/src/components/utils/field-constants.ts +++ b/src/components/utils/field-constants.ts @@ -461,4 +461,4 @@ export const CONNECTION_SIDE = 'connectionSide'; export const ACTION = 'action'; export const SELECTED_MODIFICATIONS = 'selectedModifications'; -export const COMPOSITE_NAMES = 'compositeNames'; +export const IS_SHARED = 'isShared'; From 43f4be6a6434cbcc283b8b4c10cf0e659ee68b61 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 4 Jun 2026 15:20:13 +0200 Subject: [PATCH 07/17] disable sharing in INSERT mode Signed-off-by: Mathieu DEHARBE --- src/components/dialogs/import-modification-dialog.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx index 4d1e415220..40770415bd 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-modification-dialog.tsx @@ -185,7 +185,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly [ { @@ -220,9 +220,8 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { if (!studyUuid || !currentNode || !isValid) return; + console.log('Mathieu handleSave selectedModifications', selectedModifications); const modificationsToInsert: ModificationPair[] = selectedModifications.map((m: SelectedComposite) => ({ first: m.id, second: m.name, })); + console.log('Mathieu handleSave modificationsToInsert', modificationsToInsert); // TODO : revoir l'envoi : l'ordre doit être préservé, donc faire un dto ? séparer les deux actions import ? + //shared à false pour le mode INSERT // [IS_SHARED]: selectedModifications.find((mod) => mod.id === e.id)?.isShared ?? false, insertCompositeModifications( studyUuid, From 7e291d1bcbe542165e32776fac0dac47d0fccfb3 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 4 Jun 2026 16:11:01 +0200 Subject: [PATCH 08/17] InsertNameCell + non editable name when shared Signed-off-by: Mathieu DEHARBE --- .../import-modification-dialog.tsx | 51 ++++--------------- .../import-composite/insert-name-cell.tsx | 41 +++++++++++++++ .../network-modification-node-editor.tsx | 10 ++-- 3 files changed, 53 insertions(+), 49 deletions(-) rename src/components/dialogs/{ => import-composite}/import-modification-dialog.tsx (91%) create mode 100644 src/components/dialogs/import-composite/insert-name-cell.tsx diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-composite/import-modification-dialog.tsx similarity index 91% rename from src/components/dialogs/import-modification-dialog.tsx rename to src/components/dialogs/import-composite/import-modification-dialog.tsx index 40770415bd..eee64d6882 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-composite/import-modification-dialog.tsx @@ -19,11 +19,11 @@ import { TreeViewFinderNodeProps, useSnackMessage, } from '@gridsuite/commons-ui'; -import { insertCompositeModifications, type ModificationPair } from '../../services/study'; +import { insertCompositeModifications, type ModificationPair } from '../../../services/study'; import { JSX, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { AppState } from 'redux/reducer.type'; -import { CompositeModificationAction } from 'components/graph/menus/network-modifications/network-modification-menu.type'; +import { AppState } from '../../../redux/reducer.type'; +import { CompositeModificationAction } from '../../graph/menus/network-modifications/network-modification-menu.type'; import { Box, Button, @@ -34,30 +34,20 @@ import { Divider, FormControl, FormControlLabel, - FormHelperText, Radio, RadioGroup, Step, StepLabel, Stepper, - TextField, Typography, } from '@mui/material'; - -import { - Controller, - useController, - useFieldArray, - useForm, - UseFieldArrayReturn, - useFormContext, - useWatch, -} from 'react-hook-form'; +import { useController, useFieldArray, useForm, UseFieldArrayReturn } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from 'components/utils/field-constants'; +import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from '../../utils/field-constants'; import * as yup from 'yup'; import { UUID } from 'node:crypto'; -import { useParameterState } from './parameters/use-parameters-state'; +import { useParameterState } from '../parameters/use-parameters-state'; +import InsertNameCell from './insert-name-cell'; /** * Dialog to select composite network modifications and append them to the current node. @@ -102,30 +92,6 @@ function SharedCell({ rowIndex }: Readonly) { ); } -interface InsertNameCellProps { - rowIndex: number; -} - -function InsertNameCell({ rowIndex }: Readonly) { - const { control } = useFormContext(); - const originalName: string = useWatch({ - name: `${SELECTED_MODIFICATIONS}.${rowIndex}.originalName`, - }); - - return ( - - ( - - )} - /> - {originalName} - - ); -} - const formSchema = yup .object() .shape({ @@ -306,6 +272,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly mod.id === e.id)?.isShared ?? false, insertCompositeModifications( studyUuid, @@ -323,7 +290,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly - + diff --git a/src/components/dialogs/import-composite/insert-name-cell.tsx b/src/components/dialogs/import-composite/insert-name-cell.tsx new file mode 100644 index 0000000000..90d5701752 --- /dev/null +++ b/src/components/dialogs/import-composite/insert-name-cell.tsx @@ -0,0 +1,41 @@ +import { IS_SHARED, SELECTED_MODIFICATIONS } from '../../utils/field-constants'; +import { Box, FormHelperText, TextField, Typography } from '@mui/material'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; +import { JSX } from 'react'; + +interface InsertNameCellProps { + rowIndex: number; +} + +const InsertNameCell = ({ rowIndex }: Readonly): JSX.Element => { + const { control } = useFormContext(); + const originalName: string = useWatch({ + name: `${SELECTED_MODIFICATIONS}.${rowIndex}.originalName`, + }); + const isShared: boolean = useWatch({ + name: `${SELECTED_MODIFICATIONS}.${rowIndex}.${IS_SHARED}`, + }); + + return ( + + {isShared ? ( + + {originalName} + + ) : ( + <> + ( + + )} + /> + {originalName} + + )} + + ); +}; + +export default InsertNameCell; diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx index ce5a9b6d49..1a002a6840 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx @@ -76,7 +76,7 @@ import TwoWindingsTransformerModificationDialog from '../../../dialogs/network-m import { useIsAnyNodeBuilding } from '../../../utils/is-any-node-building-hook'; import { FileUpload, RestoreFromTrash } from '@mui/icons-material'; -import ImportModificationDialog from 'components/dialogs/import-modification-dialog'; +import ImportModificationDialog from '../../../dialogs/import-composite/import-modification-dialog'; import RestoreModificationDialog from 'components/dialogs/restore-modification-dialog'; import type { UUID } from 'node:crypto'; import { AppState } from 'redux/reducer.type'; @@ -733,14 +733,10 @@ const NetworkModificationNodeEditor = () => { // Check if during asynchronous request currentNode has already changed // otherwise accept fetch results if (currentNode.id === currentNodeIdRef.current) { - const liveModifications = res.filter( - (networkModification) => networkModification.stashed === false - ); + const liveModifications = res.filter((networkModification) => !networkModification.stashed); updateSelectedItems(liveModifications); setModifications(liveModifications); - setModificationsToRestore( - res.filter((networkModification) => networkModification.stashed === true) - ); + setModificationsToRestore(res.filter((networkModification) => networkModification.stashed)); } }) .catch((error) => { From f94fa7e8fc251c7d885ce62a51dfc5e8ba7a29c6 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 4 Jun 2026 17:41:15 +0200 Subject: [PATCH 09/17] CompositesToBeInserted Signed-off-by: Mathieu DEHARBE --- .../import-modification-dialog.tsx | 30 ++++++++----------- .../import-composite/insert-name-cell.tsx | 6 ++++ src/services/study/index.ts | 9 +++--- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/components/dialogs/import-composite/import-modification-dialog.tsx b/src/components/dialogs/import-composite/import-modification-dialog.tsx index eee64d6882..071c75efc5 100644 --- a/src/components/dialogs/import-composite/import-modification-dialog.tsx +++ b/src/components/dialogs/import-composite/import-modification-dialog.tsx @@ -19,7 +19,7 @@ import { TreeViewFinderNodeProps, useSnackMessage, } from '@gridsuite/commons-ui'; -import { insertCompositeModifications, type ModificationPair } from '../../../services/study'; +import { type CompositesToBeInserted, insertCompositeModifications } from '../../../services/study'; import { JSX, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from '../../../redux/reducer.type'; @@ -41,7 +41,7 @@ import { Stepper, Typography, } from '@mui/material'; -import { useController, useFieldArray, useForm, UseFieldArrayReturn } from 'react-hook-form'; +import { useController, useFieldArray, UseFieldArrayReturn, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from '../../utils/field-constants'; import * as yup from 'yup'; @@ -263,26 +263,20 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { if (!studyUuid || !currentNode || !isValid) return; - console.log('Mathieu handleSave selectedModifications', selectedModifications); - const modificationsToInsert: ModificationPair[] = selectedModifications.map((m: SelectedComposite) => ({ - first: m.id, - second: m.name, + const modificationsToInsert: CompositesToBeInserted[] = selectedModifications.map((m: SelectedComposite) => ({ + id: m.id, + // only INSERTed non shared composites may be renamed + name: action === CompositeModificationAction.SPLIT || m.isShared ? m.originalName : m.name, + // SPLIT modifications are never shared + isShared: action === CompositeModificationAction.INSERT ? m.isShared : false, })); - console.log('Mathieu handleSave modificationsToInsert', modificationsToInsert); - // TODO : revoir l'envoi : l'ordre doit être préservé, donc faire un dto ? séparer les deux actions import ? - //shared à false pour le mode INSERT - // name = originalName pour les partagées - // [IS_SHARED]: selectedModifications.find((mod) => mod.id === e.id)?.isShared ?? false, - insertCompositeModifications( - studyUuid, - currentNode.id, - modificationsToInsert, - formMethods.getValues(ACTION) - ).catch((error) => snackWithFallback(snackError, error, { headerId: 'importComposites.error' })); + insertCompositeModifications(studyUuid, currentNode.id, modificationsToInsert, action).catch((error) => + snackWithFallback(snackError, error, { headerId: 'importComposites.error' }) + ); handleClose(); - }, [studyUuid, currentNode, isValid, formMethods, snackError, handleClose, selectedModifications]); + }, [studyUuid, currentNode, isValid, formMethods, snackError, handleClose, selectedModifications, action]); // ----------------------------------------------------------------------- // Render diff --git a/src/components/dialogs/import-composite/insert-name-cell.tsx b/src/components/dialogs/import-composite/insert-name-cell.tsx index 90d5701752..bf162fbe4e 100644 --- a/src/components/dialogs/import-composite/insert-name-cell.tsx +++ b/src/components/dialogs/import-composite/insert-name-cell.tsx @@ -1,3 +1,9 @@ +/** + * 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 { IS_SHARED, SELECTED_MODIFICATIONS } from '../../utils/field-constants'; import { Box, FormHelperText, TextField, Typography } from '@mui/material'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; diff --git a/src/services/study/index.ts b/src/services/study/index.ts index 2d25709d69..14ee57d30f 100644 --- a/src/services/study/index.ts +++ b/src/services/study/index.ts @@ -286,15 +286,16 @@ export function copyOrMoveModifications( }); } -export interface ModificationPair { - first: UUID; - second: string; +export interface CompositesToBeInserted { + id: UUID; + newName?: string; // may be empty if the composite is not renamed or if it is shared + isShared: boolean; } export const insertCompositeModifications = ( studyUuid: string, nodeUuid: string, - modifications: ModificationPair[], + modifications: CompositesToBeInserted[], action: CompositeModificationAction ): Promise => { const urlSearchParams = new URLSearchParams({ action }); From 6f66892859aea265de90e154a29fc7a63611158f Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Fri, 5 Jun 2026 16:33:06 +0200 Subject: [PATCH 10/17] updates CompositesToBeInserted Signed-off-by: Mathieu DEHARBE --- src/services/study/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/study/index.ts b/src/services/study/index.ts index 14ee57d30f..87a52949c5 100644 --- a/src/services/study/index.ts +++ b/src/services/study/index.ts @@ -288,7 +288,7 @@ export function copyOrMoveModifications( export interface CompositesToBeInserted { id: UUID; - newName?: string; // may be empty if the composite is not renamed or if it is shared + name: string; isShared: boolean; } From 5d77ff613adc257aa2e8b9c79f7ca257fd9a92fc Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Tue, 9 Jun 2026 15:10:51 +0200 Subject: [PATCH 11/17] conflicts Signed-off-by: Mathieu DEHARBE --- .../import-composite/import-modification-dialog.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/dialogs/import-composite/import-modification-dialog.tsx b/src/components/dialogs/import-composite/import-modification-dialog.tsx index 071c75efc5..d501de42df 100644 --- a/src/components/dialogs/import-composite/import-modification-dialog.tsx +++ b/src/components/dialogs/import-composite/import-modification-dialog.tsx @@ -14,7 +14,6 @@ import { DndColumnType, DndTable, ElementType, - PARAM_DEVELOPER_MODE, snackWithFallback, TreeViewFinderNodeProps, useSnackMessage, @@ -46,7 +45,6 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from '../../utils/field-constants'; import * as yup from 'yup'; import { UUID } from 'node:crypto'; -import { useParameterState } from '../parameters/use-parameters-state'; import InsertNameCell from './insert-name-cell'; /** @@ -129,7 +127,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly state.studyUuid); const currentNode = useSelector((state: AppState) => state.currentTreeNode); - const [isDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE); const [activeStep, setActiveStep] = useState(STEP_SELECTION); const [isSelectorOpen, setIsSelectorOpen] = useState(false); @@ -220,12 +217,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly { - if (!isDeveloperMode && action === CompositeModificationAction.INSERT) { - setValue(ACTION, CompositeModificationAction.SPLIT, { shouldValidate: true }); - } - }, [isDeveloperMode, action, setValue]); - const handleInlineSelectionChange = useCallback( (selectedElements: TreeViewFinderNodeProps[]) => { const newRows: SelectedComposite[] = selectedElements.map((e) => ({ @@ -352,7 +343,6 @@ const ImportModificationDialog = ({ open, onClose }: Readonly} label={} - disabled={!isDeveloperMode} /> From b116ba1a34afa58741fae3e06839d47a3fe8b18a Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Tue, 9 Jun 2026 15:58:40 +0200 Subject: [PATCH 12/17] selectionContainsShared Signed-off-by: Mathieu DEHARBE --- .../dialogs/import-modification-dialog.tsx | 483 ------------------ .../network-modification-node-editor.tsx | 18 +- 2 files changed, 13 insertions(+), 488 deletions(-) delete mode 100644 src/components/dialogs/import-modification-dialog.tsx diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx deleted file mode 100644 index f08f42b186..0000000000 --- a/src/components/dialogs/import-modification-dialog.tsx +++ /dev/null @@ -1,483 +0,0 @@ -/** - * Copyright (c) 2024, 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 { FormattedMessage, useIntl } from 'react-intl'; -import { - CustomFormProvider, - DirectoryItemSelector, - DndColumn, - DndColumnType, - DndTable, - ElementType, - PARAM_DEVELOPER_MODE, - snackWithFallback, - TreeViewFinderNodeProps, - useSnackMessage, -} from '@gridsuite/commons-ui'; -import { insertCompositeModifications, type ModificationPair } from '../../services/study'; -import { JSX, useCallback, useEffect, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'redux/reducer.type'; -import { CompositeModificationAction } from 'components/graph/menus/network-modifications/network-modification-menu.type'; -import { - Box, - Button, - Checkbox, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - FormControl, - FormControlLabel, - FormHelperText, - Radio, - RadioGroup, - Step, - StepLabel, - Stepper, - TextField, - Typography, -} from '@mui/material'; - -import { - Controller, - useController, - useFieldArray, - useForm, - UseFieldArrayReturn, - useFormContext, - useWatch, -} from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { ACTION, SELECTED_MODIFICATIONS } from 'components/utils/field-constants'; -import * as yup from 'yup'; -import { UUID } from 'node:crypto'; -import { useParameterState } from './parameters/use-parameters-state'; - -/** - * Dialog to select composite network modifications and append them to the current node. - * Organised as a two-step stepper: - * - * - Step 1 — Selection: inline tree view to browse and pick one or more composites. - * - Step 2 — Organisation: reorder selected items and choose insertion mode - * (SPLIT to expand individual modifications, INSERT to keep composites as-is with a required name). - * - * @param open Whether the dialog is open. - * @param onClose Callback invoked when the dialog is closed or cancelled. - */ -interface ImportModificationDialogProps { - open: boolean; - onClose: () => void; -} - -interface SelectedComposite { - id: UUID; - name: string; - originalName: string; - isShared: boolean; -} - -interface FormData { - [ACTION]: CompositeModificationAction; - [SELECTED_MODIFICATIONS]: SelectedComposite[]; -} - -const emptyFormData: FormData = { - [ACTION]: CompositeModificationAction.SPLIT, - [SELECTED_MODIFICATIONS]: [], -}; - -interface SharedCellProps { - rowIndex: number; -} - -function SharedCell({ rowIndex }: Readonly) { - const isShared: boolean = useWatch({ - name: `${SELECTED_MODIFICATIONS}.${rowIndex}.isShared`, - }); - - return ( - } - label={ - - - - } - sx={{ mr: 0, alignItems: 'center' }} - /> - ); -} - -interface InsertNameCellProps { - rowIndex: number; -} - -function InsertNameCell({ rowIndex }: Readonly) { - const { control } = useFormContext(); - const originalName: string = useWatch({ - name: `${SELECTED_MODIFICATIONS}.${rowIndex}.originalName`, - }); - - return ( - - ( - - )} - /> - {originalName} - - ); -} - -const formSchema = yup - .object() - .shape({ - [ACTION]: yup.mixed().oneOf(Object.values(CompositeModificationAction)).required(), - [SELECTED_MODIFICATIONS]: yup - .array() - .min(1) - .when(ACTION, ([action], schema) => { - if (action === CompositeModificationAction.INSERT) { - return schema.test('all-names-filled', 'FieldIsRequired', function (rows) { - for (const row of rows ?? []) { - if (!row.name?.trim()) { - return this.createError({ - path: `${SELECTED_MODIFICATIONS}`, - message: 'FieldIsRequired', - }); - } - } - return true; - }); - } - return schema; - }) - .required(), - }) - .required() as yup.ObjectSchema; - -type FormSchemaType = yup.InferType; - -const STEP_SELECTION = 0; -const STEP_ORGANIZATION = 1; - -const ImportModificationDialog = ({ open, onClose }: Readonly): JSX.Element => { - const intl = useIntl(); - const { snackError } = useSnackMessage(); - const studyUuid = useSelector((state: AppState) => state.studyUuid); - const currentNode = useSelector((state: AppState) => state.currentTreeNode); - const [isDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE); - - const [activeStep, setActiveStep] = useState(STEP_SELECTION); - const [isSelectorOpen, setIsSelectorOpen] = useState(false); - // true = Next button disabled; starts disabled until TreeViewFinder signals a valid selection - const [isNextDisabled, setIsNextDisabled] = useState(true); - - const formMethods = useForm({ - defaultValues: emptyFormData, - resolver: yupResolver(formSchema), - }); - - const { - control, - reset, - watch, - setValue, - formState: { isValid }, - } = formMethods; - - const { field: actionField } = useController({ name: ACTION, control }); - - const action = watch(ACTION); - const selectedModifications = watch(SELECTED_MODIFICATIONS); - const isInsertMode = action === CompositeModificationAction.INSERT; - - // useFieldArray — consumed by DndTable - const useFieldArrayOutput = useFieldArray({ - control, - name: SELECTED_MODIFICATIONS, - }) as unknown as UseFieldArrayReturn; - - // Column definitions - const sharedColumn: DndColumn = useMemo( - () => ({ - label: '', - dataKey: 'isShared', - initialValue: false, - editable: true, - width: '25%', - type: DndColumnType.CUSTOM, - component: (rowIndex: number) => , - }), - [] - ); - - // SPLIT mode — name is read-only, drag-and-drop only - const splitColumnsDefinition: DndColumn[] = useMemo( - () => [ - { - label: '', - dataKey: 'name', - initialValue: '', - editable: false, - width: '75%', - type: DndColumnType.TEXT, - }, - sharedColumn, - ], - [sharedColumn] - ); - - // INSERT mode — name is editable with original name shown below - const insertColumnsDefinition: DndColumn[] = useMemo( - () => [ - { - label: '', - dataKey: 'name', - initialValue: '', - editable: true, - width: '75%', - type: DndColumnType.CUSTOM, - component: (rowIndex: number) => , - }, - sharedColumn, - ], - [sharedColumn] - ); - - const columnsDefinition = isInsertMode ? insertColumnsDefinition : splitColumnsDefinition; - - useEffect(() => { - if (open) { - setActiveStep(STEP_SELECTION); - setIsSelectorOpen(true); - setIsNextDisabled(true); // reset on each open - } else { - setIsSelectorOpen(false); - reset(emptyFormData); - } - }, [open, reset]); - - useEffect(() => { - if (!isDeveloperMode && action === CompositeModificationAction.INSERT) { - setValue(ACTION, CompositeModificationAction.SPLIT, { shouldValidate: true }); - } - }, [isDeveloperMode, action, setValue]); - - const handleInlineSelectionChange = useCallback( - (selectedElements: TreeViewFinderNodeProps[]) => { - const newRows: SelectedComposite[] = selectedElements.map((e) => ({ - id: e.id as UUID, - name: e.name, - originalName: e.name, - isShared: false, - })); - setValue(SELECTED_MODIFICATIONS, newRows, { - shouldValidate: true, - shouldDirty: true, - }); - setIsNextDisabled(selectedElements.length === 0); - }, - [setValue] - ); - - const handleSelectModification = useCallback( - (selectedElements: TreeViewFinderNodeProps[]) => { - setIsSelectorOpen(false); - - if (!selectedElements.length) { - if (selectedModifications.length === 0) { - onClose(); - } - return; - } - - const newRows: SelectedComposite[] = selectedElements.map((e) => ({ - id: e.id as UUID, - name: e.name, - originalName: e.name, - isShared: false, // actually is false, to be computed later - })); - - setValue(SELECTED_MODIFICATIONS, newRows, { - shouldValidate: true, - shouldDirty: true, - }); - }, - [setValue, selectedModifications.length, onClose] - ); - - const handleNext = useCallback(() => { - if (isNextDisabled) return; - setActiveStep(STEP_ORGANIZATION); - }, [isNextDisabled]); - - const handlePrevious = useCallback(() => { - setActiveStep(STEP_SELECTION); - setIsSelectorOpen(true); - }, []); - - const handleClose = useCallback(() => { - reset(emptyFormData); - setActiveStep(STEP_SELECTION); - setIsSelectorOpen(false); - onClose(); - }, [reset, onClose]); - - const handleSave = useCallback(() => { - if (!studyUuid || !currentNode || !isValid) return; - - const rows: SelectedComposite[] = formMethods.getValues(SELECTED_MODIFICATIONS); - - const modificationsToInsert: ModificationPair[] = rows.map((m) => ({ - first: m.id, - second: m.name, - })); - - insertCompositeModifications( - studyUuid, - currentNode.id, - modificationsToInsert, - formMethods.getValues(ACTION) - ).catch((error) => snackWithFallback(snackError, error, { headerId: 'importComposites.error' })); - - handleClose(); - }, [studyUuid, currentNode, isValid, formMethods, snackError, handleClose]); - - // ----------------------------------------------------------------------- - // Render - // ----------------------------------------------------------------------- - - return ( - - - - - - - - {' '} - {/* ---- Stepper ---- */} - - STEP_SELECTION}> - - - - - - - - - - - - {/* ====================================================== - STEP 1 — SELECTION - ====================================================== */} - {activeStep === STEP_SELECTION && ( - - - - )} - {/* ====================================================== - STEP 2 — ORGANIZATION & SHARING - ====================================================== */} - {activeStep === STEP_ORGANIZATION && ( - - {/* Radio group */} - - - - - - - } - label={} - /> - } - label={} - disabled={!isDeveloperMode} - /> - - - - - - - - - {/* DnD table — thead hidden, no column headers needed */} - - - - - )} - - - - - {activeStep === STEP_ORGANIZATION && ( - - )} - - - - {activeStep === STEP_SELECTION ? ( - - ) : ( - - )} - - - - - ); -}; - -export default ImportModificationDialog; diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx index 2219d44aa1..7843b4c9a3 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx @@ -90,9 +90,9 @@ import { AppState } from 'redux/reducer.type'; import { createCompositeModifications, updateCompositeModifications } from '../../../../services/explore'; import { copyOrMoveModifications } from '../../../../services/study'; import { + assembleModificationsIntoComposite, fetchExcludedNetworkModifications, fetchNetworkModifications, - assembleModificationsIntoComposite, stashModifications, } from '../../../../services/study/network-modifications'; import { @@ -177,6 +177,12 @@ const NetworkModificationNodeEditor = () => { [] ); + const selectionContainsShared: boolean = useMemo(() => { + return selectedNetworkModifications.some( + (modification) => modification.type === ModificationType.MODIFICATION_REFERENCE + ); + }, [selectedNetworkModifications]); + const [isDragging, setIsDragging] = useState(false); const [isAssemblyDepthExceeded, setIsAssemblyDepthExceeded] = useState(false); @@ -1307,7 +1313,7 @@ const NetworkModificationNodeEditor = () => { @@ -1323,7 +1329,8 @@ const NetworkModificationNodeEditor = () => { isAnyNodeBuilding || mapDataLoading || !currentNode || - isRootNode + isRootNode || + selectionContainsShared } > @@ -1339,7 +1346,8 @@ const NetworkModificationNodeEditor = () => { selectedNetworkModifications.length === 0 || isAnyNodeBuilding || mapDataLoading || - isRootNode + isRootNode || + selectionContainsShared } > @@ -1361,7 +1369,7 @@ const NetworkModificationNodeEditor = () => { From 3deb8ac18ba0d5be73596f36607ca7d370b9bb2e Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 11 Jun 2026 15:59:57 +0200 Subject: [PATCH 13/17] comment Signed-off-by: Mathieu DEHARBE --- .../network-modifications/network-modification-node-editor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx index 7843b4c9a3..cf84b4af85 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx @@ -177,9 +177,10 @@ const NetworkModificationNodeEditor = () => { [] ); + // TODO : this is temporary, until copy/paste/save is done for the shared modifications : const selectionContainsShared: boolean = useMemo(() => { return selectedNetworkModifications.some( - (modification) => modification.type === ModificationType.MODIFICATION_REFERENCE + (modification: ComposedModificationMetadata) => modification.type === ModificationType.MODIFICATION_REFERENCE ); }, [selectedNetworkModifications]); From d72d30c278b89d49c1deac23e78df12f0919291f Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Wed, 17 Jun 2026 13:11:39 +0200 Subject: [PATCH 14/17] temp move back import-shared-modification to import-composite Signed-off-by: Mathieu DEHARBE --- .../import-modification-dialog.tsx | 10 +++++----- .../network-modification-node-editor.tsx | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) rename src/components/dialogs/{import-composite => }/import-modification-dialog.tsx (97%) diff --git a/src/components/dialogs/import-composite/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx similarity index 97% rename from src/components/dialogs/import-composite/import-modification-dialog.tsx rename to src/components/dialogs/import-modification-dialog.tsx index d501de42df..1526895148 100644 --- a/src/components/dialogs/import-composite/import-modification-dialog.tsx +++ b/src/components/dialogs/import-modification-dialog.tsx @@ -18,11 +18,11 @@ import { TreeViewFinderNodeProps, useSnackMessage, } from '@gridsuite/commons-ui'; -import { type CompositesToBeInserted, insertCompositeModifications } from '../../../services/study'; +import { type CompositesToBeInserted, insertCompositeModifications } from '../../services/study'; import { JSX, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { AppState } from '../../../redux/reducer.type'; -import { CompositeModificationAction } from '../../graph/menus/network-modifications/network-modification-menu.type'; +import { AppState } from '../../redux/reducer.type'; +import { CompositeModificationAction } from '../graph/menus/network-modifications/network-modification-menu.type'; import { Box, Button, @@ -42,10 +42,10 @@ import { } from '@mui/material'; import { useController, useFieldArray, UseFieldArrayReturn, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from '../../utils/field-constants'; +import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from '../utils/field-constants'; import * as yup from 'yup'; import { UUID } from 'node:crypto'; -import InsertNameCell from './insert-name-cell'; +import InsertNameCell from './import-composite/insert-name-cell'; /** * Dialog to select composite network modifications and append them to the current node. diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx index b6aacca7d9..70b5d879fa 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx @@ -83,7 +83,7 @@ import TwoWindingsTransformerModificationDialog from '../../../dialogs/network-m import { useIsAnyNodeBuilding } from '../../../utils/is-any-node-building-hook'; import { FileUpload, RestoreFromTrash } from '@mui/icons-material'; -import ImportModificationDialog from '../../../dialogs/import-composite/import-modification-dialog'; +import ImportModificationDialog from '../../../dialogs/import-modification-dialog'; import RestoreModificationDialog from 'components/dialogs/restore-modification-dialog'; import type { UUID } from 'node:crypto'; import { AppState } from 'redux/reducer.type'; @@ -180,7 +180,8 @@ const NetworkModificationNodeEditor = () => { // TODO : this is temporary, until copy/paste/save is done for the shared modifications : const selectionContainsShared: boolean = useMemo(() => { return selectedNetworkModifications.some( - (modification: ComposedModificationMetadata) => modification.type === ModificationType.MODIFICATION_REFERENCE + (modification: ComposedModificationMetadata) => + modification.type === ModificationType.MODIFICATION_REFERENCE ); }, [selectedNetworkModifications]); From 59ead2c6f988af0ddad1a85e339ccb280c5bd511 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Wed, 17 Jun 2026 14:34:19 +0200 Subject: [PATCH 15/17] post merge Signed-off-by: Mathieu DEHARBE --- src/components/dialogs/import-modification-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-modification-dialog.tsx index 7b235999ae..37806d3890 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-modification-dialog.tsx @@ -309,7 +309,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly setIsSelectorOpen(false)} types={[ElementType.MODIFICATION]} multiSelect inline From 9df75b61dfd0981174e678ac6cbe2e7533617147 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Fri, 19 Jun 2026 14:32:28 +0200 Subject: [PATCH 16/17] move back component file Signed-off-by: Mathieu DEHARBE --- .../import-modification-dialog.tsx | 10 +++++----- .../network-modification-node-editor.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/components/dialogs/{ => import-composite}/import-modification-dialog.tsx (97%) diff --git a/src/components/dialogs/import-modification-dialog.tsx b/src/components/dialogs/import-composite/import-modification-dialog.tsx similarity index 97% rename from src/components/dialogs/import-modification-dialog.tsx rename to src/components/dialogs/import-composite/import-modification-dialog.tsx index 37806d3890..1de864477d 100644 --- a/src/components/dialogs/import-modification-dialog.tsx +++ b/src/components/dialogs/import-composite/import-modification-dialog.tsx @@ -18,11 +18,11 @@ import { TreeViewFinderNodeProps, useSnackMessage, } from '@gridsuite/commons-ui'; -import { type CompositesToBeInserted, insertCompositeModifications } from '../../services/study'; +import { type CompositesToBeInserted, insertCompositeModifications } from '../../../services/study'; import { JSX, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { AppState } from '../../redux/reducer.type'; -import { CompositeModificationAction } from '../graph/menus/network-modifications/network-modification-menu.type'; +import { AppState } from '../../../redux/reducer.type'; +import { CompositeModificationAction } from '../../graph/menus/network-modifications/network-modification-menu.type'; import { Box, Button, @@ -42,10 +42,10 @@ import { } from '@mui/material'; import { useController, useFieldArray, UseFieldArrayReturn, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from '../utils/field-constants'; +import { ACTION, IS_SHARED, SELECTED_MODIFICATIONS } from '../../utils/field-constants'; import * as yup from 'yup'; import { UUID } from 'node:crypto'; -import InsertNameCell from './import-composite/insert-name-cell'; +import InsertNameCell from './insert-name-cell'; /** * Dialog to select composite network modifications and append them to the current node. diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx index 70b5d879fa..1123487719 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx @@ -83,7 +83,7 @@ import TwoWindingsTransformerModificationDialog from '../../../dialogs/network-m import { useIsAnyNodeBuilding } from '../../../utils/is-any-node-building-hook'; import { FileUpload, RestoreFromTrash } from '@mui/icons-material'; -import ImportModificationDialog from '../../../dialogs/import-modification-dialog'; +import ImportModificationDialog from '../../../dialogs/import-composite/import-modification-dialog'; import RestoreModificationDialog from 'components/dialogs/restore-modification-dialog'; import type { UUID } from 'node:crypto'; import { AppState } from 'redux/reducer.type'; From c91032aa2958345a5981ead36d2c1919a1897463 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Mon, 22 Jun 2026 13:16:16 +0200 Subject: [PATCH 17/17] comments Signed-off-by: Mathieu DEHARBE --- .../dialogs/import-composite/import-modification-dialog.tsx | 4 +--- .../network-modification-node-editor.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/dialogs/import-composite/import-modification-dialog.tsx b/src/components/dialogs/import-composite/import-modification-dialog.tsx index 1de864477d..fa40635dae 100644 --- a/src/components/dialogs/import-composite/import-modification-dialog.tsx +++ b/src/components/dialogs/import-composite/import-modification-dialog.tsx @@ -253,7 +253,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly ({ id: m.id, - // only INSERTed non shared composites may be renamed + // only inserted non shared composites may be renamed name: action === CompositeModificationAction.SPLIT || m.isShared ? m.originalName : m.name, // SPLIT modifications are never shared isShared: action === CompositeModificationAction.INSERT ? m.isShared : false, @@ -293,9 +293,7 @@ const ImportModificationDialog = ({ open, onClose }: Readonly - - {/* ====================================================== STEP 1 — SELECTION ====================================================== */} diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx index 1123487719..422855951b 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx @@ -177,7 +177,7 @@ const NetworkModificationNodeEditor = () => { [] ); - // TODO : this is temporary, until copy/paste/save is done for the shared modifications : + // TODO : this is temporary, until copy/paste/save is done for the shared modifications in GRD-4785 : const selectionContainsShared: boolean = useMemo(() => { return selectedNetworkModifications.some( (modification: ComposedModificationMetadata) =>