From 77a6a80307a58ebef42c0c7bc222bd12aa221216 Mon Sep 17 00:00:00 2001 From: Tristan Chuine Date: Tue, 17 Sep 2024 19:25:29 +0200 Subject: [PATCH 1/2] Fix contingency `CustomAgGridTable` types --- .../agGridTable/BottomRightButtons.tsx | 10 +- .../agGridTable/CustomAgGridTable.tsx | 153 ++++++++++-------- .../agGridTable/csvUploader/CsvUploader.tsx | 50 +++--- 3 files changed, 112 insertions(+), 101 deletions(-) diff --git a/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx b/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx index b514b956..1f68c472 100644 --- a/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx +++ b/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx @@ -9,13 +9,13 @@ import IconButton from '@mui/material/IconButton'; import { ArrowCircleDown, ArrowCircleUp, Upload } from '@mui/icons-material'; import AddIcon from '@mui/icons-material/ControlPoint'; import DeleteIcon from '@mui/icons-material/Delete'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useIntl } from 'react-intl'; import { styled } from '@mui/material/styles'; import { FieldValues, UseFieldArrayReturn } from 'react-hook-form'; import ErrorInput from '../errorManagement/ErrorInput'; import FieldErrorAlert from '../errorManagement/FieldErrorAlert'; -import CsvUploader from './csvUploader/CsvUploader'; +import CsvUploader, { CsvUploaderProps } from './csvUploader/CsvUploader'; const InnerColoredButton = styled(IconButton)(({ theme }) => { return { @@ -33,7 +33,7 @@ export interface BottomRightButtonsProps { handleMoveRowUp: () => void; handleMoveRowDown: () => void; useFieldArrayOutput: UseFieldArrayReturn; - csvProps: any; + csvProps: Omit; } function BottomRightButtons({ @@ -47,7 +47,7 @@ function BottomRightButtons({ handleMoveRowDown, useFieldArrayOutput, csvProps, -}: BottomRightButtonsProps) { +}: Readonly) { const [uploaderOpen, setUploaderOpen] = useState(false); const intl = useIntl(); @@ -88,7 +88,7 @@ function BottomRightButtons({ setUploaderOpen(false)} + onClose={useCallback(() => setUploaderOpen(false), [])} name={name} useFieldArrayOutput={useFieldArrayOutput} {...csvProps} diff --git a/src/components/inputs/reactHookForm/agGridTable/CustomAgGridTable.tsx b/src/components/inputs/reactHookForm/agGridTable/CustomAgGridTable.tsx index 5b598f90..491268fc 100644 --- a/src/components/inputs/reactHookForm/agGridTable/CustomAgGridTable.tsx +++ b/src/components/inputs/reactHookForm/agGridTable/CustomAgGridTable.tsx @@ -12,10 +12,12 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import { Grid, useTheme } from '@mui/material'; import { useIntl } from 'react-intl'; -import { CellEditingStoppedEvent, ColumnState, SortChangedEvent } from 'ag-grid-community'; -import BottomRightButtons from './BottomRightButtons'; +import { ColDef, GridApi, GridOptions } from 'ag-grid-community'; +import BottomRightButtons, { BottomRightButtonsProps } from './BottomRightButtons'; import FieldConstants from '../../../../utils/constants/fieldConstants'; +type AgGridFn = NonNullable[TFn]>; + export const ROW_DRAGGING_SELECTION_COLUMN_DEF = [ { rowDrag: true, @@ -23,9 +25,9 @@ export const ROW_DRAGGING_SELECTION_COLUMN_DEF = [ checkboxSelection: true, maxWidth: 50, }, -]; +] as const satisfies Readonly; -const style = (customProps: any) => ({ +const style = (customProps: object = {}) => ({ grid: (theme: any) => ({ width: 'auto', height: '100%', @@ -83,13 +85,13 @@ const style = (customProps: any) => ({ }), }); -export interface CustomAgGridTableProps { +export interface CustomAgGridTableProps { name: string; - columnDefs: any; - makeDefaultRowData: any; - csvProps: unknown; - cssProps: unknown; - defaultColDef: unknown; + columnDefs: ColDef[]; + makeDefaultRowData: () => unknown; + csvProps: BottomRightButtonsProps['csvProps']; + cssProps?: object; + defaultColDef: GridOptions['defaultColDef']; pagination: boolean; paginationPageSize: number; suppressRowClickSelection: boolean; @@ -97,7 +99,9 @@ export interface CustomAgGridTableProps { stopEditingWhenCellsLoseFocus: boolean; } -function CustomAgGridTable({ +// TODO: rename ContingencyAgGridTable +// TODO: used only once in gridexplore, move to gridexplore? +function CustomAgGridTable({ name, columnDefs, makeDefaultRowData, @@ -109,11 +113,10 @@ function CustomAgGridTable({ suppressRowClickSelection, alwaysShowVerticalScroll, stopEditingWhenCellsLoseFocus, - ...props -}: CustomAgGridTableProps) { +}: Readonly>) { const theme: any = useTheme(); - const [gridApi, setGridApi] = useState(null); - const [selectedRows, setSelectedRows] = useState([]); + const [gridApi, setGridApi] = useState>(); + const [selectedRows, setSelectedRows] = useState([]); const [newRowAdded, setNewRowAdded] = useState(false); const [isSortApplied, setIsSortApplied] = useState(false); @@ -124,15 +127,15 @@ function CustomAgGridTable({ }); const { append, remove, update, swap, move } = useFieldArrayOutput; - const rowData = watch(name); + const rowData = watch(name); // TODO: use correct types for useFormContext<...>() const isFirstSelected = Boolean( - rowData?.length && gridApi?.api.getRowNode(rowData[0][FieldConstants.AG_GRID_ROW_UUID])?.isSelected() + rowData?.length && gridApi?.getRowNode(rowData[0][FieldConstants.AG_GRID_ROW_UUID])?.isSelected() ); const isLastSelected = Boolean( rowData?.length && - gridApi?.api.getRowNode(rowData[rowData.length - 1][FieldConstants.AG_GRID_ROW_UUID])?.isSelected() + gridApi?.getRowNode(rowData[rowData.length - 1][FieldConstants.AG_GRID_ROW_UUID])?.isSelected() ); const noRowSelected = selectedRows.length === 0; @@ -146,26 +149,26 @@ function CustomAgGridTable({ [getValues, name] ); - const handleMoveRowUp = () => { + const handleMoveRowUp = useCallback(() => { selectedRows - .map((row) => getIndex(row)) + .map(getIndex) .sort() .forEach((idx) => { swap(idx, idx - 1); }); - }; + }, [getIndex, selectedRows, swap]); - const handleMoveRowDown = () => { + const handleMoveRowDown = useCallback(() => { selectedRows - .map((row) => getIndex(row)) + .map(getIndex) .sort() .reverse() .forEach((idx) => { swap(idx, idx + 1); }); - }; + }, [getIndex, selectedRows, swap]); - const handleDeleteRows = () => { + const handleDeleteRows = useCallback(() => { if (selectedRows.length === rowData.length) { remove(); } else { @@ -174,52 +177,59 @@ function CustomAgGridTable({ remove(idx); }); } - }; - - useEffect(() => { - if (gridApi) { - gridApi.api.refreshCells({ - force: true, - }); - } - }, [gridApi, rowData]); + }, [getIndex, remove, rowData.length, selectedRows]); - const handleAddRow = () => { + const handleAddRow = useCallback(() => { append(makeDefaultRowData()); setNewRowAdded(true); - }; + }, [append, makeDefaultRowData]); useEffect(() => { - if (gridApi) { - gridApi.api.sizeColumnsToFit(); - } + gridApi?.refreshCells({ + force: true, + }); + }, [gridApi, rowData]); + + useEffect(() => { + gridApi?.sizeColumnsToFit(); }, [columnDefs, gridApi]); const intl = useIntl(); - const getLocaleText = useCallback( - (params: any) => { - const key = `agGrid.${params.key}`; - return intl.messages[key] || params.defaultValue; - }, + const getLocaleText = useCallback>( + (params) => intl.formatMessage({ id: `agGrid.${params.key}`, defaultMessage: params.defaultValue }), [intl] ); - const onGridReady = (params: any) => { - setGridApi(params); - }; + const onGridReady = useCallback>((event) => { + setGridApi(event.api); + }, []); + + const onRowDragEnd = useCallback>( + (e) => move(getIndex(e.node.data), e.overIndex), + [getIndex, move] + ); + + const onSelectionChanged = useCallback>( + // @ts-expect-error TODO manage null api case (not possible at runtime?) + () => setSelectedRows(gridApi.getSelectedRows()), + [gridApi] + ); - const onRowDataUpdated = () => { - setNewRowAdded(false); - if (gridApi?.api) { - // update due to new appended row, let's scroll - const lastIndex = rowData.length - 1; - gridApi.api.paginationGoToLastPage(); - gridApi.api.ensureIndexVisible(lastIndex, 'bottom'); - } - }; + const onRowDataUpdated = useCallback>( + (/* event */) => { + setNewRowAdded(false); + if (gridApi) { + // update due to new appended row, let's scroll + const lastIndex = rowData.length - 1; + gridApi.paginationGoToLastPage(); + gridApi.ensureIndexVisible(lastIndex, 'bottom'); + } + }, + [gridApi, rowData.length] + ); - const onCellEditingStopped = useCallback( - (event: CellEditingStoppedEvent) => { + const onCellEditingStopped = useCallback>( + (event) => { const rowIndex = getIndex(event.data); if (rowIndex === -1) { return; @@ -229,15 +239,22 @@ function CustomAgGridTable({ [getIndex, update] ); - const onSortChanged = useCallback((event: SortChangedEvent) => { - const isAnycolumnhasSort = event.api.getColumnState().some((col: ColumnState) => col.sort); - setIsSortApplied(isAnycolumnhasSort); - }, []); + const onSortChanged = useCallback>( + (event) => setIsSortApplied(event.api.getColumnState().some((col) => col.sort)), + [] + ); + + const getRowId = useCallback>( + // @ts-expect-error: we don't know at compile time if TData has a "FieldConstants.AG_GRID_ROW_UUID" field + // TODO maybe force TData type to have this field? + (row) => row.data[FieldConstants.AG_GRID_ROW_UUID], + [] + ); return ( - rowData={rowData} onGridReady={onGridReady} getLocaleText={getLocaleText} @@ -246,23 +263,21 @@ function CustomAgGridTable({ domLayout="autoHeight" rowDragEntireRow rowDragManaged - onRowDragEnd={(e) => move(getIndex(e.node.data), e.overIndex)} + onRowDragEnd={onRowDragEnd} suppressBrowserResizeObserver + defaultColDef={defaultColDef} columnDefs={columnDefs} detailRowAutoHeight - onSelectionChanged={() => { - setSelectedRows(gridApi.api.getSelectedRows()); - }} + onSelectionChanged={onSelectionChanged} onRowDataUpdated={newRowAdded ? onRowDataUpdated : undefined} onCellEditingStopped={onCellEditingStopped} onSortChanged={onSortChanged} - getRowId={(row) => row.data[FieldConstants.AG_GRID_ROW_UUID]} + getRowId={getRowId} pagination={pagination} paginationPageSize={paginationPageSize} suppressRowClickSelection={suppressRowClickSelection} alwaysShowVerticalScroll={alwaysShowVerticalScroll} stopEditingWhenCellsLoseFocus={stopEditingWhenCellsLoseFocus} - {...props} /> void; - open: true; - title: string[]; + open: boolean; + title?: ReactNode; fileHeaders: string[]; fileName: string; - csvData: unknown; - validateData: (rows: string[][]) => boolean; + csvData?: Array>; + validateData?: (rows: string[][]) => boolean; getDataFromCsv: any; - useFieldArrayOutput: any; + useFieldArrayOutput: UseFieldArrayReturn; } function CsvUploader({ @@ -42,26 +44,20 @@ function CsvUploader({ title, fileHeaders, fileName, - csvData, + csvData = [], validateData = () => true, getDataFromCsv, useFieldArrayOutput, }: Readonly) { const watchTableValues = useWatch({ name }); const { append, replace } = useFieldArrayOutput; - const [createError, setCreateError] = React.useState(''); + const [createError, setCreateError] = useState(''); const intl = useIntl(); const { CSVReader } = useCSVReader(); - const [importedData, setImportedData] = useState([]); + const [importedData, setImportedData] = useState([]); const [isConfirmationPopupOpen, setIsConfirmationPopupOpen] = useState(false); - const data = useMemo(() => { - const newData = [...[fileHeaders]]; - if (Array.isArray(csvData)) { - csvData.forEach((row) => newData.push([row])); - } - return newData; - }, [csvData, fileHeaders]); + const data = useMemo(() => [[...fileHeaders], ...csvData], [csvData, fileHeaders]); const handleClose = () => { onClose(); setCreateError(''); @@ -85,9 +81,9 @@ function CsvUploader({ }; const getResultsFromImportedData = () => { - return importedData.filter((row: string[]) => { + return importedData.filter((row) => { // We do not keep the comment rows - if (row[0].startsWith('#')) { + if (row[0]?.startsWith('#')) { return false; } // We keep the row if at least one of its column has a value From 23b633aca027b13a9e4801db8253a9b5aefdf074 Mon Sep 17 00:00:00 2001 From: Tristan Chuine Date: Wed, 18 Sep 2024 14:56:15 +0200 Subject: [PATCH 2/2] review --- .../filter/explicitNaming/ExplicitNamingFilterForm.tsx | 8 +++++--- .../reactHookForm/agGridTable/BottomRightButtons.tsx | 3 ++- .../reactHookForm/agGridTable/csvUploader/CsvUploader.tsx | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/filter/explicitNaming/ExplicitNamingFilterForm.tsx b/src/components/filter/explicitNaming/ExplicitNamingFilterForm.tsx index ae8e2fa6..90c045a1 100644 --- a/src/components/filter/explicitNaming/ExplicitNamingFilterForm.tsx +++ b/src/components/filter/explicitNaming/ExplicitNamingFilterForm.tsx @@ -99,7 +99,9 @@ interface ExplicitNamingFilterFormProps { sourceFilterForExplicitNamingConversion?: FilterForExplicitConversionProps; } -function ExplicitNamingFilterForm({ sourceFilterForExplicitNamingConversion }: ExplicitNamingFilterFormProps) { +function ExplicitNamingFilterForm({ + sourceFilterForExplicitNamingConversion, +}: Readonly) { const intl = useIntl(); const { snackError } = useSnackMessage(); @@ -166,9 +168,9 @@ function ExplicitNamingFilterForm({ sourceFilterForExplicitNamingConversion }: E return newCsvFileHeaders; }, [intl, forGeneratorOrLoad]); - const getDataFromCsvFile = useCallback((csvData: any) => { + const getDataFromCsvFile = useCallback((csvData: string[][]) => { if (csvData) { - return csvData.map((value: any) => { + return csvData.map((value) => { return { [FieldConstants.AG_GRID_ROW_UUID]: uuid4(), [FieldConstants.EQUIPMENT_ID]: value[0]?.trim(), diff --git a/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx b/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx index 1f68c472..f14e8109 100644 --- a/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx +++ b/src/components/inputs/reactHookForm/agGridTable/BottomRightButtons.tsx @@ -50,6 +50,7 @@ function BottomRightButtons({ }: Readonly) { const [uploaderOpen, setUploaderOpen] = useState(false); const intl = useIntl(); + const onClose = useCallback(() => setUploaderOpen(false), []); return ( <> @@ -88,7 +89,7 @@ function BottomRightButtons({ setUploaderOpen(false), [])} + onClose={onClose} name={name} useFieldArrayOutput={useFieldArrayOutput} {...csvProps} diff --git a/src/components/inputs/reactHookForm/agGridTable/csvUploader/CsvUploader.tsx b/src/components/inputs/reactHookForm/agGridTable/csvUploader/CsvUploader.tsx index 8b3d6bde..d87996fb 100644 --- a/src/components/inputs/reactHookForm/agGridTable/csvUploader/CsvUploader.tsx +++ b/src/components/inputs/reactHookForm/agGridTable/csvUploader/CsvUploader.tsx @@ -19,7 +19,7 @@ import { useCSVReader } from 'react-papaparse'; import { ReactNode, useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import CsvDownloader from 'react-csv-downloader'; -import { FieldValues, UseFieldArrayReturn, useWatch } from 'react-hook-form'; +import { FieldValues, UseFieldArrayAppend, UseFieldArrayReturn, useWatch } from 'react-hook-form'; import { RECORD_SEP, UNIT_SEP } from 'papaparse'; import FieldConstants from '../../../../../utils/constants/fieldConstants'; import CancelButton from '../../utils/CancelButton'; @@ -33,7 +33,7 @@ export interface CsvUploaderProps { fileName: string; csvData?: Array>; validateData?: (rows: string[][]) => boolean; - getDataFromCsv: any; + getDataFromCsv: (csvData: string[][]) => Parameters>[0]; // keep generics in sync with useFieldArrayOutput field useFieldArrayOutput: UseFieldArrayReturn; }