-
Notifications
You must be signed in to change notification settings - Fork 5
feat(tabular-modification): inline cell edit and row manipulation #3948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5a095a1
29ef4d1
e28bfec
051c9ca
a18c162
8a0af7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,27 +5,27 @@ | |
| * 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, type UseFieldArrayReturn, useFormContext, 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 +53,7 @@ | |
| transformIfFrenchNumber, | ||
| } from './tabular-common'; | ||
| import { ColDef } from 'ag-grid-community'; | ||
| import { BOOLEAN } 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'; | ||
|
|
@@ -75,6 +75,7 @@ | |
| const { snackWarning } = useSnackMessage(); | ||
| const [isFetching, setIsFetching] = useState<boolean>(dataFetching); | ||
| const { setValue, clearErrors, setError } = useFormContext(); | ||
| const tableRef = useRef<UseFieldArrayReturn<FieldValues, string>>(null); | ||
| const propertiesDialogOpen = useStateBoolean(false); | ||
| const generateFromFilterOpen = useStateBoolean(false); | ||
| const prefilledModelDialogOpen = useStateBoolean(false); | ||
|
|
@@ -91,9 +92,6 @@ | |
| const tabularProperties = useWatch({ | ||
| name: TABULAR_PROPERTIES, | ||
| }); | ||
| const watchTable = useWatch({ | ||
| name: MODIFICATIONS_TABLE, | ||
| }); | ||
| const watchFileName = useWatch({ | ||
| name: CSV_FILENAME, | ||
| }); | ||
|
|
@@ -301,11 +299,15 @@ | |
| } else { | ||
| handleTabularModificationParsingError(results); | ||
| } | ||
| setValue(MODIFICATIONS_TABLE, results.data, { shouldDirty: true }); | ||
| const rowsWithUuid = results.data.map((row) => ({ | ||
| ...row, | ||
| [FieldConstants.AG_GRID_ROW_UUID]: uuid4(), | ||
| })); | ||
| tableRef.current?.replace(rowsWithUuid); | ||
|
Comment on lines
+302
to
+306
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline edits/additions bypass the only real validation path. The grid is editable and can create new rows now, but the required/type checks still only run during CSV parsing. That means users can add blank rows or change a cell to an invalid value and still save because Also applies to: 411-440, 444-453, 545-559 |
||
| 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 +338,7 @@ | |
|
|
||
| useEffect(() => { | ||
| if (selectedFileError) { | ||
| setValue(MODIFICATIONS_TABLE, []); | ||
| tableRef.current?.replace([]); | ||
| setValue(CSV_FILENAME, undefined); | ||
| clearErrors(MODIFICATIONS_TABLE); | ||
| setIsFetching(false); | ||
|
|
@@ -373,7 +375,7 @@ | |
| const handleTypeChange = useCallback(() => { | ||
| setTypeChangedTrigger(!typeChangedTrigger); | ||
| clearErrors(MODIFICATIONS_TABLE); | ||
| setValue(MODIFICATIONS_TABLE, []); | ||
| tableRef.current?.replace([]); | ||
| setValue(CSV_FILENAME, undefined); | ||
| setValue(TABULAR_PROPERTIES, []); | ||
| resetFile(); | ||
|
|
@@ -406,26 +408,50 @@ | |
| const columnDefs = useMemo(() => { | ||
| 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 NUMBER: | ||
| columnDef.cellEditor = NumericEditor; | ||
| break; | ||
| case ENUM: | ||
| columnDef.cellEditor = 'agSelectCellEditor'; | ||
| columnDef.cellEditorParams = { values: [null, ...(field.options ?? [])] }; | ||
| break; | ||
| default: | ||
| 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, | ||
| editable: true, | ||
| singleClickEdit: true, | ||
| })) | ||
| ); | ||
| }, [csvFields, selectedProperties, intl]); | ||
|
|
||
| const makeDefaultRowData = useCallback(() => { | ||
| const row: Record<string, any> = { [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 +460,7 @@ | |
| if (newSelectedProperties.toString() !== selectedProperties.toString()) { | ||
| // new columns => 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 +535,27 @@ | |
| </Button> | ||
| </Grid> | ||
| )} | ||
| <Grid item> | ||
| <ErrorInput name={MODIFICATIONS_TABLE} InputField={FieldErrorAlert} /> | ||
| {selectedFileError && <Alert severity="error">{selectedFileError}</Alert>} | ||
| </Grid> | ||
| {selectedFileError && ( | ||
| <Grid item> | ||
| <Alert severity="error">{selectedFileError}</Alert> | ||
| </Grid> | ||
| )} | ||
| </Grid> | ||
| <Grid item xs={12} sx={dialogStyles.grid}> | ||
| <CustomAGGrid | ||
| rowData={watchTable} | ||
| loading={isFetching} | ||
| defaultColDef={defaultColDef} | ||
| <CustomAgGridTable | ||
| ref={tableRef} | ||
|
Check failure on line 546 in src/components/dialogs/network-modifications/tabular/tabular-form.tsx
|
||
| name={MODIFICATIONS_TABLE} | ||
| columnDefs={columnDefs} | ||
| defaultColDef={defaultColDef} | ||
| makeDefaultRowData={makeDefaultRowData} | ||
| loading={isFetching} | ||
| pagination | ||
| paginationPageSize={100} | ||
| suppressDragLeaveHidesColumns | ||
| rowSelection={{ | ||
| mode: 'multiRow', | ||
| }} | ||
| overrideLocales={AGGRID_LOCALES} | ||
| csvProps={undefined} | ||
| cssProps={{ height: 535 }} | ||
| /> | ||
| </Grid> | ||
| <DefinePropertiesDialog | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: gridsuite/gridstudy-app
Length of output: 10301
🏁 Script executed:
Repository: gridsuite/gridstudy-app
Length of output: 583
🏁 Script executed:
Repository: gridsuite/gridstudy-app
Length of output: 463
🏁 Script executed:
Repository: gridsuite/gridstudy-app
Length of output: 1455
🏁 Script executed:
rg -n 'AG_GRID_ROW_UUID' src/components/dialogs/network-modifications/tabular/tabular-modification-utils.tsRepository: gridsuite/gridstudy-app
Length of output: 49
🏁 Script executed:
rg -n 'AG_GRID_ROW_UUID' src/components/dialogs/network-modifications/tabular/tabular-creation-utils.tsRepository: gridsuite/gridstudy-app
Length of output: 49
Remove
AG_GRID_ROW_UUIDbefore submitting to backend.Lines 121 and 139–140 add
FieldConstants.AG_GRID_ROW_UUIDto row objects for UI state tracking. However, this field propagates to the API payload:convertCreationFieldFromFrontToBack, which has no guard for the UUID field.Add an explicit check in
convertCreationFieldFromFrontToBackor in the modification transformation strategy to excludeFieldConstants.AG_GRID_ROW_UUIDbefore the backend payload is assembled.Also applies to: 137–140
🤖 Prompt for AI Agents