From 5a095a105b6b99566abd6e95989dc492ba97cb21 Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Fri, 15 May 2026 18:50:20 +0200 Subject: [PATCH 1/6] feat(tabular-modification): inline cell edit and row deletion The tabular modification/creation dialog used to be fed exclusively by a CSV import: rows were displayed read-only and any correction required editing the CSV outside the app and re-importing it. Replace the read-only CustomAGGrid by CustomAgGridTable, which brings inline edit, multi-row selection + delete, add row, drag handle and move up/down via its bottom-right buttons. The existing CSV import flow on top of the dialog is preserved untouched; CSV-parsed rows are loaded into the grid imperatively through a forwarded ref to the internal useFieldArray. Details: - Inject AG_GRID_ROW_UUID on each row when loading from CSV (handleComplete) and when restoring an existing modification via editData (initTabularCreationData / initTabularModificationData). CustomAgGridTable uses this UUID for getRowId, so without it row tracking degenerates to "undefined === undefined", which silently collapses the field array. - Make columns editable with the right cell editor per field type: agSelectCellEditor for BOOLEAN and ENUM, NumericEditor for NUMBER, default text editor otherwise. Properties columns are editable as text. - Mark the pinned EQUIPMENT_ID column as the row drag handle (rowDrag: true), now that commons-ui no longer hardcodes rowDragEntireRow. - Replace every setValue(MODIFICATIONS_TABLE, ...) by replace() via the table ref to keep the underlying useFieldArray in sync (a second useFieldArray on the same name is forbidden by RHF). - Drop the top-of-dialog ErrorInput; BottomRightButtons already renders one right under the grid. - Add the agGridBackground token to the gridstudy theme (light and dark) so CustomAgGridTable's styles can read theme.agGridBackground.color. Requires commons-ui change forwarding useFieldArray via ref and making row drag opt-in (gridsuite/commons-ui#1133). Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- src/components/app-wrapper.jsx | 6 + .../tabular/tabular-dialog.tsx | 15 ++- .../tabular/tabular-form.tsx | 115 ++++++++++++------ src/module-mui.d.ts | 3 + 4 files changed, 102 insertions(+), 37 deletions(-) diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index 7a4c31b376..06c3264b8b 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -219,6 +219,9 @@ const lightTheme = createTheme({ }, highlightColor: '#1976D214', }, + agGridBackground: { + color: 'white', + }, networkModificationPanel: { backgroundColor: 'white', border: 'solid 1px #babfc7', @@ -335,6 +338,9 @@ const darkTheme = createTheme({ }, highlightColor: '#90CAF929', }, + agGridBackground: { + color: '#383838', + }, networkModificationPanel: { backgroundColor: '#252525', border: 'solid 1px #68686e', diff --git a/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx b/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx index 4088377b03..9ae8c778ff 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx @@ -6,7 +6,14 @@ */ import { yupResolver } from '@hookform/resolvers/yup'; -import { CustomFormProvider, ModificationType, snackWithFallback, useSnackMessage } from '@gridsuite/commons-ui'; +import { + CustomFormProvider, + FieldConstants, + ModificationType, + snackWithFallback, + useSnackMessage, +} from '@gridsuite/commons-ui'; +import { v4 as uuid4 } from 'uuid'; import { useForm } from 'react-hook-form'; import { useCallback, useEffect, useMemo } from 'react'; import { useOpenShortWaitFetching } from 'components/dialogs/commons/handle-modification-form.js'; @@ -111,6 +118,7 @@ export function TabularDialog({ } } modification = addPropertiesFromBack(modification, modif?.[TABULAR_PROPERTIES]); + modification[FieldConstants.AG_GRID_ROW_UUID] = uuid4(); return modification; }); reset({ @@ -126,7 +134,10 @@ export function TabularDialog({ const initTabularCreationData = useCallback( (editData: TabularModificationEditDataType) => { const equipmentType = getEquipmentTypeFromCreationType(editData?.modificationType); - const creations = convertCreations(editData?.modifications); + const creations = convertCreations(editData?.modifications).map((creation) => ({ + ...creation, + [FieldConstants.AG_GRID_ROW_UUID]: uuid4(), + })); reset({ [TYPE]: equipmentType, [MODIFICATIONS_TABLE]: creations, diff --git a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx index b8b12ac627..8996fb6aa4 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx @@ -5,27 +5,28 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { useFormContext, useWatch } from 'react-hook-form'; +import { type FieldValues, useFormContext, type UseFieldArrayReturn, useWatch } from 'react-hook-form'; import { AutocompleteInput, BooleanNullableCellRenderer, - CustomAGGrid, + CustomAgGridTable, DefaultCellRenderer, DirectoryItemSelector, ElementType, EquipmentType, - ErrorInput, fetchStudyMetadata, - FieldErrorAlert, + FieldConstants, getObjectId, LANG_FRENCH, type MuiStyles, + NumericEditor, type TreeViewFinderNodeProps, useSnackMessage, useStateBoolean, } from '@gridsuite/commons-ui'; +import { v4 as uuid4 } from 'uuid'; import { CSV_FILENAME, EQUIPMENT_ID, @@ -53,7 +54,7 @@ import { transformIfFrenchNumber, } from './tabular-common'; import { ColDef } from 'ag-grid-community'; -import { BOOLEAN } from '../../../network/constants'; +import { BOOLEAN, ENUM, NUMBER } from '../../../network/constants'; import { TABULAR_CREATION_FIELDS } from './tabular-creation-utils'; import { TABULAR_MODIFICATION_FIELDS } from './tabular-modification-utils'; import { useFilterCsvGenerator } from './use-filter-csv-generator'; @@ -75,6 +76,7 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly(dataFetching); const { setValue, clearErrors, setError } = useFormContext(); + const tableRef = useRef>(null); const propertiesDialogOpen = useStateBoolean(false); const generateFromFilterOpen = useStateBoolean(false); const prefilledModelDialogOpen = useStateBoolean(false); @@ -91,9 +93,6 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly ({ + ...row, + [FieldConstants.AG_GRID_ROW_UUID]: uuid4(), + })); + tableRef.current?.replace(rowsWithUuid); setValue(CSV_FILENAME, selectedFile?.name); } else { // If the file is undefined we don't update the values because it's outdated - setValue(MODIFICATIONS_TABLE, []); + tableRef.current?.replace([]); setValue(CSV_FILENAME, undefined); } setIsFetching(false); @@ -336,7 +339,7 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly { if (selectedFileError) { - setValue(MODIFICATIONS_TABLE, []); + tableRef.current?.replace([]); setValue(CSV_FILENAME, undefined); clearErrors(MODIFICATIONS_TABLE); setIsFetching(false); @@ -373,7 +376,7 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly { setTypeChangedTrigger(!typeChangedTrigger); clearErrors(MODIFICATIONS_TABLE); - setValue(MODIFICATIONS_TABLE, []); + tableRef.current?.replace([]); setValue(CSV_FILENAME, undefined); setValue(TABULAR_PROPERTIES, []); resetFile(); @@ -406,26 +409,59 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly { return csvFields .map((field) => { - const columnDef: ColDef = {}; + const columnDef: ColDef = { + field: field.id, + headerName: intl.formatMessage({ id: field.id }) + (field.required ? ' (*)' : ''), + editable: true, + singleClickEdit: true, + }; if (field.id === EQUIPMENT_ID) { columnDef.pinned = true; + columnDef.rowDrag = true; + } + switch (field.type) { + case BOOLEAN: + columnDef.cellRenderer = BooleanNullableCellRenderer; + columnDef.cellEditor = 'agSelectCellEditor'; + columnDef.cellEditorParams = { values: [null, true, false] }; + break; + case NUMBER: + columnDef.cellRenderer = DefaultCellRenderer; + columnDef.cellEditor = NumericEditor; + break; + case ENUM: + columnDef.cellRenderer = DefaultCellRenderer; + columnDef.cellEditor = 'agSelectCellEditor'; + columnDef.cellEditorParams = { values: [null, ...(field.options ?? [])] }; + break; + default: + columnDef.cellRenderer = DefaultCellRenderer; + break; } - columnDef.field = field.id; - columnDef.headerName = intl.formatMessage({ id: field.id }) + (field.required ? ' (*)' : ''); - columnDef.cellRenderer = field.type === BOOLEAN ? BooleanNullableCellRenderer : DefaultCellRenderer; return columnDef; }) .concat( - selectedProperties.map((propertyName: string) => { - const columnDef: ColDef = {}; - columnDef.field = PROPERTY_CSV_COLUMN_PREFIX + propertyName; - columnDef.headerName = propertyName; - columnDef.cellRenderer = DefaultCellRenderer; - return columnDef; - }) + selectedProperties.map((propertyName: string) => ({ + field: PROPERTY_CSV_COLUMN_PREFIX + propertyName, + headerName: propertyName, + cellRenderer: DefaultCellRenderer, + editable: true, + singleClickEdit: true, + })) ); }, [csvFields, selectedProperties, intl]); + const makeDefaultRowData = useCallback(() => { + const row: Record = { [FieldConstants.AG_GRID_ROW_UUID]: uuid4() }; + csvFields.forEach((field) => { + row[field.id] = null; + }); + selectedProperties.forEach((propertyName) => { + row[PROPERTY_CSV_COLUMN_PREFIX + propertyName] = ''; + }); + return row; + }, [csvFields, selectedProperties]); + const onPropertiesChange = (formData: PropertiesFormType) => { const newSelectedProperties = formData[TABULAR_PROPERTIES]?.filter((property: TabularProperty) => property.selected)?.map( @@ -434,7 +470,7 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly reset table clearErrors(MODIFICATIONS_TABLE); - setValue(MODIFICATIONS_TABLE, []); + tableRef.current?.replace([]); setValue(CSV_FILENAME, undefined); } setValue(TABULAR_PROPERTIES, formData[TABULAR_PROPERTIES], { shouldDirty: true }); @@ -509,21 +545,30 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly )} - - - {selectedFileError && {selectedFileError}} - + {selectedFileError && ( + + {selectedFileError} + + )} - Date: Mon, 18 May 2026 15:27:42 +0200 Subject: [PATCH 2/6] refactor(tabular-modification): hoist DefaultCellRenderer to defaultColDef DefaultCellRenderer was repeated for every typed branch (BOOLEAN, NUMBER, ENUM, default). Move it once into defaultColDef and drop the per-case assignments. BOOLEAN now falls through to the default text editor instead of agSelectCellEditor + BooleanNullableCellRenderer; remove the dead cellEditorParams and the now-unused BooleanNullableCellRenderer import. Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../network-modifications/tabular/tabular-form.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx index 8996fb6aa4..d6d651d86b 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx @@ -7,10 +7,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { type FieldValues, useFormContext, type UseFieldArrayReturn, useWatch } from 'react-hook-form'; +import { type FieldValues, type UseFieldArrayReturn, useFormContext, useWatch } from 'react-hook-form'; import { AutocompleteInput, - BooleanNullableCellRenderer, CustomAgGridTable, DefaultCellRenderer, DirectoryItemSelector, @@ -421,21 +420,15 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly Date: Mon, 18 May 2026 15:29:51 +0200 Subject: [PATCH 3/6] refactor(tabular-form): remove redundant DefaultCellRenderer assignment Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../dialogs/network-modifications/tabular/tabular-form.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx index d6d651d86b..120f96b081 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx @@ -437,7 +437,6 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly ({ field: PROPERTY_CSV_COLUMN_PREFIX + propertyName, headerName: propertyName, - cellRenderer: DefaultCellRenderer, editable: true, singleClickEdit: true, })) From 051c9cace36ee72fa61e93ab83e33609b4901d48 Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Mon, 18 May 2026 15:53:12 +0200 Subject: [PATCH 4/6] refactor(tabular-form): clean up unused BOOLEAN case and obsolete row selection props Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../dialogs/network-modifications/tabular/tabular-form.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx index 120f96b081..89be03c95d 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx @@ -419,8 +419,6 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly Date: Mon, 18 May 2026 16:12:06 +0200 Subject: [PATCH 5/6] style(tabular-form): adjust CSS height and dialog paper height for improved layout consistency Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../dialogs/network-modifications/tabular/tabular-dialog.tsx | 1 + .../dialogs/network-modifications/tabular/tabular-form.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx b/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx index 9ae8c778ff..e5164955cd 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx @@ -252,6 +252,7 @@ export function TabularDialog({ titleId={dialogMode === TabularModificationType.CREATION ? 'TabularCreation' : 'TabularModification'} open={open} isDataFetching={dataFetching} + slotProps={{ paper: { sx: { height: '83vh' } } }} {...dialogProps} > diff --git a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx index 89be03c95d..c57fcf730e 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx @@ -555,7 +555,7 @@ export function TabularForm({ dataFetching, dialogMode }: Readonly Date: Mon, 18 May 2026 17:20:23 +0200 Subject: [PATCH 6/6] prettier Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../dialogs/network-modifications/tabular/tabular-dialog.tsx | 2 +- .../dialogs/network-modifications/tabular/tabular-form.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx b/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx index e5164955cd..645fee318c 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-dialog.tsx @@ -18,7 +18,7 @@ import { useForm } from 'react-hook-form'; import { useCallback, useEffect, useMemo } from 'react'; import { useOpenShortWaitFetching } from 'components/dialogs/commons/handle-modification-form.js'; import { FORM_LOADING_DELAY } from 'components/network/constants.js'; -import { TABULAR_PROPERTIES, MODIFICATIONS_TABLE, CSV_FILENAME, TYPE } from 'components/utils/field-constants.js'; +import { CSV_FILENAME, MODIFICATIONS_TABLE, TABULAR_PROPERTIES, TYPE } from 'components/utils/field-constants.js'; import { ModificationDialog } from 'components/dialogs/commons/modificationDialog.js'; import { createTabularModification } from 'services/study/network-modifications.js'; import { FetchStatus } from 'services/utils.type'; diff --git a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx index c57fcf730e..f9ef19ed47 100644 --- a/src/components/dialogs/network-modifications/tabular/tabular-form.tsx +++ b/src/components/dialogs/network-modifications/tabular/tabular-form.tsx @@ -53,7 +53,7 @@ import { transformIfFrenchNumber, } from './tabular-common'; import { ColDef } from 'ag-grid-community'; -import { BOOLEAN, ENUM, NUMBER } from '../../../network/constants'; +import { ENUM, NUMBER } from '../../../network/constants'; import { TABULAR_CREATION_FIELDS } from './tabular-creation-utils'; import { TABULAR_MODIFICATION_FIELDS } from './tabular-modification-utils'; import { useFilterCsvGenerator } from './use-filter-csv-generator';