From 406b7f74ced8c80ada8278b5c67e12cd64f4fa24 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Wed, 18 Sep 2024 19:12:11 +0200 Subject: [PATCH 01/10] feat: custom and dynamic columns in spreadsheet Signed-off-by: TOURI ANIS --- src/components/app-wrapper.jsx | 4 + .../custom-columns/columns-config-custom.tsx | 70 +++++ .../custom-columns/custom-column-dialog.tsx | 203 ++++++++++++ .../custom-columns/custom-column-table.tsx | 61 ++++ .../custom-columns/custom-columns-dialog.tsx | 294 ++++++++++++++++++ .../custom-columns/custom-columns-form.tsx | 35 +++ .../custom-columns/custom-columns.types.tsx | 21 ++ src/components/spreadsheet/table-wrapper.jsx | 4 + src/components/use-states.ts | 42 +++ src/hooks/use-states.ts | 42 +++ src/redux/actions.ts | 21 ++ src/redux/reducer.ts | 17 +- src/translations/spreadsheet-en.ts | 34 ++ src/translations/spreadsheet-fr.ts | 34 ++ 14 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 src/components/spreadsheet/custom-columns/columns-config-custom.tsx create mode 100644 src/components/spreadsheet/custom-columns/custom-column-dialog.tsx create mode 100644 src/components/spreadsheet/custom-columns/custom-column-table.tsx create mode 100644 src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx create mode 100644 src/components/spreadsheet/custom-columns/custom-columns-form.tsx create mode 100644 src/components/spreadsheet/custom-columns/custom-columns.types.tsx create mode 100644 src/components/use-states.ts create mode 100644 src/hooks/use-states.ts create mode 100644 src/translations/spreadsheet-en.ts create mode 100644 src/translations/spreadsheet-fr.ts diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index e1d532ce9e..c5d1bc85d3 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -67,6 +67,8 @@ import errors_locale_en from '../translations/dynamic/errors-locale-en'; import errors_locale_fr from '../translations/dynamic/errors-locale-fr'; import events_locale_fr from '../translations/dynamic/events-locale-fr'; import events_locale_en from '../translations/dynamic/events-locale-en'; +import spreadsheet_locale_fr from '../translations/spreadsheet-fr'; +import spreadsheet_locale_en from '../translations/spreadsheet-en'; import { store } from '../redux/store'; import CssBaseline from '@mui/material/CssBaseline'; import { @@ -267,6 +269,7 @@ const messages = { ...table_locale_en, ...errors_locale_en, ...events_locale_en, + ...spreadsheet_locale_en, ...messages_plugins.en, // keep it at the end to allow translation overwriting }, fr: { @@ -295,6 +298,7 @@ const messages = { ...table_locale_fr, ...errors_locale_fr, ...events_locale_fr, + ...spreadsheet_locale_fr, ...messages_plugins.fr, // keep it at the end to allow translation overwriting }, }; diff --git a/src/components/spreadsheet/custom-columns/columns-config-custom.tsx b/src/components/spreadsheet/custom-columns/columns-config-custom.tsx new file mode 100644 index 0000000000..19e5d75cd4 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/columns-config-custom.tsx @@ -0,0 +1,70 @@ +/* + * Copyright © 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 { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Badge, Box } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +import { Calculate as CalculateIcon } from '@mui/icons-material'; +import { useSelector } from 'react-redux'; +//import CustomColumnsDialog from './custom-columns-dialog'; +import { TABLES_NAMES } from '../utils/config-tables'; +import { AppState } from '../../../redux/reducer'; +import { useStateBoolean, useStateNumber } from '../../../hooks/use-states'; +import CustomColumnsDialog from './custom-columns-dialog'; + +export type CustomColumnsConfigProps = { + indexTab: number; +}; + +export default function CustomColumnsConfig({ indexTab }: Readonly) { + const formulaCalculating = useStateBoolean(false); //TODO + const formulaError = useStateBoolean(false); //TODO + const numberColumns = useStateNumber(0); + const dialogOpen = useStateBoolean(false); + const allDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]]); + const uEffectNumberColumnsSetValue = numberColumns.setValue; // eslint detection + useEffect(() => { + uEffectNumberColumnsSetValue(allDefinitions.columns.length); + }, [allDefinitions.columns.length, uEffectNumberColumnsSetValue]); + + /* eslint-enable react-hooks/rules-of-hooks */ + return ( + <> + + + + } + loadingPosition="start" + loading={formulaCalculating.value} + onClick={dialogOpen.setTrue} + > + + {(txt) => ( + + {txt} + + )} + + + + + ); +} diff --git a/src/components/spreadsheet/custom-columns/custom-column-dialog.tsx b/src/components/spreadsheet/custom-columns/custom-column-dialog.tsx new file mode 100644 index 0000000000..233f884ad4 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/custom-column-dialog.tsx @@ -0,0 +1,203 @@ +/* + * Copyright © 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 { useCallback, useEffect, useMemo, useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + IconButton, + InputProps, + Stack, + SxProps, + TextField, + Theme, + Tooltip, +} from '@mui/material'; +import { Functions as FunctionsIcon } from '@mui/icons-material'; +import { CustomFormProvider, useSnackMessage } from '@gridsuite/commons-ui'; +import { UseStateBooleanReturn } from '../../../hooks/use-states'; +import { ColumnWithFormula } from './custom-columns.types'; +import { useForm } from 'react-hook-form'; +import { customColumnFormSchema, initialCustomColumnForm } from './custom-columns-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { FOLDER_NAME } from 'components/utils/field-constants'; +import CustomColumnTable from './custom-column-table'; +import SaveIcon from '@mui/icons-material/Save'; +import UploadIcon from '@mui/icons-material/Upload'; +import DownloadIcon from '@mui/icons-material/Download'; +import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; + +export type CustomColumnDialogProps = { + open: UseStateBooleanReturn; + baseData?: ColumnWithFormula; + onSubmit: (data: ColumnWithFormula) => void; +}; + +const styles = { + rightButtons: { + textAlign: 'end', + '& > *': { + marginRight: 1, + }, + }, +} as const satisfies Record>; + +// TODO: redo with react-hook-form +const nameValidationRegExp = /^[^\s$]+$/; + +export default function CustomColumnDialog({ open, baseData, onSubmit }: Readonly) { + const formMethods = useForm({ + defaultValues: initialCustomColumnForm, + resolver: yupResolver(customColumnFormSchema), + }); + + const intl = useIntl(); + const { snackError } = useSnackMessage(); + + const [name, setName] = useState(baseData?.name ?? ''); + const [nameValid, setNameValid] = useState(false); + const handleNameChange = useCallback>( + (event) => { + setName(event.target.value ?? baseData?.name ?? ''); + }, + [baseData?.name] + ); + useEffect(() => { + setNameValid(nameValidationRegExp.test(name)); + }, [name]); + + const [formula, setFormula] = useState(baseData?.formula ?? ''); + const [formulaValid, setFormulaValid] = useState(false); + const handleFormulaChange = useCallback( + (value: string, event?: any) => { + setFormula(value ?? baseData?.formula ?? ''); + }, + [baseData?.formula] + ); + + useEffect(() => { + setFormulaValid(!!formula); + }, [formula]); + + const onSubmitClick = useCallback(() => { + try { + onSubmit({ name, formula }); + open.setFalse(); + } catch (error: unknown) { + console.error(error); + snackError({ + messageTxt: (error as Error).message, + headerId: 'spreadsheet/custom_column/dialog_edit/submit_error', + }); + } + }, [formula, name, onSubmit, open, snackError]); + + const [contentModified, setContentModified] = useState(false); + useEffect(() => { + // eslint-disable-next-line eqeqeq + setContentModified(baseData?.name != name || baseData?.formula != formula); + }, [baseData?.formula, baseData?.name, formula, name]); + + useEffect(() => { + if (open.value) { + setName(baseData?.name ?? ''); + setFormula(baseData?.formula ?? ''); + } + }, [baseData?.formula, baseData?.name, open.value]); + + return ( + + + + + + {/* Bouton Download */} + + + + + + + {/* Bouton Upload */} + + + + + + + {/* Bouton Save to GridExplore */} + + + + + + + {/* Bouton Insert from GridExplore */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/spreadsheet/custom-columns/custom-column-table.tsx b/src/components/spreadsheet/custom-columns/custom-column-table.tsx new file mode 100644 index 0000000000..de3f47cc81 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/custom-column-table.tsx @@ -0,0 +1,61 @@ +import DndTable from 'components/utils/dnd-table/dnd-table'; +import { COLUMN_NAME, FORMULA, TAB_CUSTOM_COLUMN } from './custom-columns-form'; +import { SELECTED } from 'components/utils/field-constants'; +import { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { useFieldArray } from 'react-hook-form'; + +export default function CustomColumnTable() { + const DndTableTyped = DndTable as React.ComponentType; + const intl = useIntl(); + const CUSTOM_COLUMNS_DEFINITIONS = useMemo(() => { + return [ + { + label: 'spreadsheet/custom_column/column_name', + dataKey: COLUMN_NAME, + initialValue: null, + editable: true, + titleId: 'FiltersListsSelection', + }, + { + label: 'spreadsheet/custom_column/column_content', + dataKey: FORMULA, + initialValue: null, + editable: true, + textAlign: 'right', + }, + ].map((column) => ({ + ...column, + label: intl + .formatMessage({ id: column.label }) + .toLowerCase() + .replace(/^\w/, (c) => c.toUpperCase()), + })); + }, [intl]); + + const useTabCustomColumnFieldArrayOutput = useFieldArray({ + name: `${TAB_CUSTOM_COLUMN}`, + }); + + const newCustomColumnRowData = useMemo(() => { + const newRowData: any = {}; + newRowData[SELECTED] = false; + CUSTOM_COLUMNS_DEFINITIONS.forEach((column: any) => (newRowData[column.dataKey] = column.initialValue)); + return newRowData; + }, [CUSTOM_COLUMNS_DEFINITIONS]); + + const createCustomColumnRows = () => [newCustomColumnRowData]; + return ( + + ); +} diff --git a/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx b/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx new file mode 100644 index 0000000000..0783ad8165 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx @@ -0,0 +1,294 @@ +/* + * Copyright © 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 { + AppBar, + Badge, + Button, + Dialog, + Divider, + IconButton, + List, + ListItem, + ListItemText, + SxProps, + Theme, + Toolbar, + Tooltip, + Typography, +} from '@mui/material'; +import { + AddCircle as AddCircleIcon, + Close as CloseIcon, + DeleteForever as DeleteForeverIcon, + Edit as EditIcon, + ImportExport as ImportExportIcon, + Save as SaveIcon, + Warning as WarningIcon, +} from '@mui/icons-material'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { useStateBoolean, UseStateBooleanReturn } from '../../../hooks/use-states'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from '../../../redux/reducer'; +import { TABLES_NAMES } from '../utils/config-tables'; +import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { ColumnWithFormula, CustomEntry } from './custom-columns.types'; +//import CustomColumnsImExPort from './custom-columns-port'; +import { AppDispatch } from '../../../redux/store'; +import { setCustomColumDefinitions } from '../../../redux/actions'; +import CustomColumnDialog from './custom-column-dialog'; +//import CustomColumnDialog from './custom-column-dialog'; + +type CustomColumnItemProps = { + data: ColumnWithFormula; + onDelete: () => void; + onEdit: () => void; +}; + +const styles: Record> = { + toolbarBtn: { + marginRight: 1, + }, +}; + +function CustomColumnItem({ data, onDelete, onEdit }: Readonly) { + return ( + + + + + + + + + } + > + + + ); +} + +function someCheckOnDefs(columns: ColumnWithFormula[]) { + //help some checks with yup? + console.error(JSON.stringify(columns)); + if (!(columns instanceof Array)) { + throw new Error("Column definitions isn't a list of columns"); + } + const names = new Set(); + for (const column of columns) { + if (typeof column !== 'object') { + throw new Error('Column definition must be an object', { cause: column }); + } + const objKeys = Object.keys(column); + if ( + objKeys.length !== 2 || + (objKeys[0] !== 'name' && objKeys[1] !== 'name') || + (objKeys[0] !== 'formula' && objKeys[1] !== 'formula') + ) { + throw new Error('Invalid column definition', { cause: column }); + } + if (!column.name) { + throw new Error(`Invalid column name "${column.name}"`); + } + column.name = column.name.trim(); + if (!column.formula) { + throw new Error(`Invalid formula "${column.formula}"`); + } + column.formula = column.formula.trim(); + if (names.has(column.name)) { + throw new Error('Formula names not unique'); + } else { + names.add(column.name); + } + //TODO found how to validate formula + } + return columns; +} + +export type CustomColumnsDialogProps = { + open: UseStateBooleanReturn; + indexTab: number; +}; + +//TODO idea: we can eval formulas with first line to detect common errors in advance and letting the user correcting it +export default function CustomColumnsDialog({ indexTab, open }: Readonly) { + const intl = useIntl(); + const dispatch = useDispatch(); + + const allDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]]); + const [columnsDefinitions, setColumnsDefinitions] = useState({ filter: { formula: '' }, columns: [] }); + const contentModified = useStateBoolean(false); + const resetContentModified = contentModified.setFalse; + const contentIsModified = contentModified.setTrue; + useEffect(() => { + if (open.value) { + setColumnsDefinitions({ + columns: allDefinitions.columns.map((def) => ({ ...def })), + filter: allDefinitions.filter, + }); + resetContentModified(); + setDialogColumnWorkingOn(undefined); + } + }, [open.value, allDefinitions, resetContentModified]); + + const dialogImportOpen = useStateBoolean(false); + + const dialogColumnOpen = useStateBoolean(false); + const [dialogColumnWorkingOn, setDialogColumnWorkingOn] = useState(undefined); + const onListItemDelete = useCallback( + (itemIdx: number) => { + setColumnsDefinitions((prevState) => { + let tmp = [...prevState.columns]; + tmp.splice(itemIdx, 1); + return { columns: tmp, filter: prevState.filter }; + }); + contentIsModified(); + }, + [contentIsModified] + ); + const onListItemEdit = useCallback( + (itemIdx: number) => { + setDialogColumnWorkingOn(columnsDefinitions.columns[itemIdx]); + dialogColumnOpen.setTrue(); + }, + [columnsDefinitions, dialogColumnOpen] + ); + const onAddColumnClick = useCallback(() => { + setDialogColumnWorkingOn(undefined); + dialogColumnOpen.setTrue(); + }, [dialogColumnOpen]); + const onImportColumn = useCallback( + (columnDef: ColumnWithFormula) => { + let newDefs: ColumnWithFormula[]; + if (dialogColumnWorkingOn === undefined) { + newDefs = [...columnsDefinitions.columns, columnDef]; + } else { + newDefs = [...columnsDefinitions.columns]; + newDefs.splice( + columnsDefinitions.columns.findIndex( + (value, index, arr) => value.name === dialogColumnWorkingOn.name + ), + 1, + columnDef + ); + setDialogColumnWorkingOn(undefined); //clean memory + } + setColumnsDefinitions({ columns: someCheckOnDefs(newDefs), filter: { formula: '' } }); + contentIsModified(); + }, + [columnsDefinitions, contentIsModified, dialogColumnWorkingOn] + ); + + const onSaveClick = useCallback(() => { + dispatch( + setCustomColumDefinitions(TABLES_NAMES[indexTab], columnsDefinitions.columns, columnsDefinitions.filter) + ); + open.setFalse(); + }, [columnsDefinitions, dispatch, indexTab, open]); + + return ( + + + + + theme.palette.grey[500], + }} + > + : null} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + > + + + + + + + + + + + + + + {useMemo( + () => + columnsDefinitions.columns.map((data, idx, arr) => ( + + onListItemDelete(idx)} + onEdit={() => onListItemEdit(idx)} + /> + {idx >= arr.length - 1 ? undefined : } + + )), + [columnsDefinitions, indexTab, onListItemDelete, onListItemEdit] + )} + + + + ); +} diff --git a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx new file mode 100644 index 0000000000..d7425cab28 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx @@ -0,0 +1,35 @@ +/** + * 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 yup from '../../../components/utils/yup-config'; + +export const TAB_CUSTOM_COLUMN = 'TAB_CUSTOM_COLUMN'; +export const FORMULA_NAME = 'FORMULA_NAME'; +export const COLUMN_NAME = 'COLUMN_NAME'; +export const FORMULA = 'FORMULA'; + +export const initialCustomColumnForm: CustomColumnForm = { + [FORMULA_NAME]: '', + [TAB_CUSTOM_COLUMN]: [ + { + [COLUMN_NAME]: '', + [FORMULA]: '', + }, + ], +}; + +export const customColumnFormSchema = yup.object().shape({ + [FORMULA_NAME]: yup.string().required(), + [TAB_CUSTOM_COLUMN]: yup.array().of( + yup.object().shape({ + [COLUMN_NAME]: yup.string().required(), + [FORMULA]: yup.string().required(), + }) + ), +}); + +export type CustomColumnForm = yup.InferType; diff --git a/src/components/spreadsheet/custom-columns/custom-columns.types.tsx b/src/components/spreadsheet/custom-columns/custom-columns.types.tsx new file mode 100644 index 0000000000..07aeaddbe3 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/custom-columns.types.tsx @@ -0,0 +1,21 @@ +/* + * Copyright © 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/. + */ + +// only moved here to resolve cyclic dependency problem +export type ColumnWithFormula = { + name: string; + formula: string; +}; + +export type FormulaFilter = { + formula: string; +}; + +export type CustomEntry = { + columns: ColumnWithFormula[]; + filter: FormulaFilter; +}; diff --git a/src/components/spreadsheet/table-wrapper.jsx b/src/components/spreadsheet/table-wrapper.jsx index fed3370ab1..32f7cfe49e 100644 --- a/src/components/spreadsheet/table-wrapper.jsx +++ b/src/components/spreadsheet/table-wrapper.jsx @@ -78,6 +78,7 @@ import { useAgGridSort } from 'hooks/use-aggrid-sort'; import { setSpreadsheetFilter } from 'redux/actions'; import { useLocalizedCountries } from 'components/utils/localized-countries-hook'; import { SPREADSHEET_SORT_STORE, SPREADSHEET_STORE_FIELD } from 'utils/store-sort-filter-fields'; +import CustomColumnsConfig from './custom-columns/columns-config-custom'; const useEditBuffer = () => { //the data is feeded and read during the edition validation process so we don't need to rerender after a call to one of available methods thus useRef is more suited @@ -1116,6 +1117,9 @@ const TableWrapper = (props) => { setLockedColumnsNames={setLockedColumnsNames} /> + + + >; + setTrue: () => void; + setFalse: () => void; + invert: () => void; +}; + +//TODO move in commons-ui +export function useStateBoolean(initialState: boolean | (() => boolean)): UseStateBooleanReturn { + const [value, setValue] = useState(initialState); + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + const invert = useCallback(() => setValue((prevState) => !prevState), []); + return { value, setTrue, setFalse, invert, setValue }; +} + +export type UseStateNumberReturn = { + value: number; + setValue: Dispatch>; + increment: () => void; + decrement: () => void; + reset: () => void; +}; + +//TODO move in commons-ui +export function useStateNumber(initialState: number | (() => number) = 0): UseStateNumberReturn { + const [value, setValue] = useState(initialState); + const increment = useCallback((n: number = 1) => setValue((prevState) => prevState + n), []); + const decrement = useCallback((n: number = 1) => setValue((prevState) => prevState - n), []); + const reset = useCallback(() => setValue(initialState), [initialState]); + return { value, increment, decrement, reset, setValue }; +} diff --git a/src/hooks/use-states.ts b/src/hooks/use-states.ts new file mode 100644 index 0000000000..fe783109e5 --- /dev/null +++ b/src/hooks/use-states.ts @@ -0,0 +1,42 @@ +/* + * Copyright © 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 { Dispatch, SetStateAction, useCallback, useState } from 'react'; + +export type UseStateBooleanReturn = { + value: boolean; + setValue: Dispatch>; + setTrue: () => void; + setFalse: () => void; + invert: () => void; +}; + +//TODO move in commons-ui +export function useStateBoolean(initialState: boolean | (() => boolean)): UseStateBooleanReturn { + const [value, setValue] = useState(initialState); + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + const invert = useCallback(() => setValue((prevState) => !prevState), []); + return { value, setTrue, setFalse, invert, setValue }; +} + +export type UseStateNumberReturn = { + value: number; + setValue: Dispatch>; + increment: () => void; + decrement: () => void; + reset: () => void; +}; + +//TODO move in commons-ui +export function useStateNumber(initialState: number | (() => number) = 0): UseStateNumberReturn { + const [value, setValue] = useState(initialState); + const increment = useCallback((n: number = 1) => setValue((prevState) => prevState + n), []); + const decrement = useCallback((n: number = 1) => setValue((prevState) => prevState - n), []); + const reset = useCallback(() => setValue(initialState), [initialState]); + return { value, increment, decrement, reset, setValue }; +} diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 3e867e2c16..7216e861db 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -46,6 +46,7 @@ import { SpreadsheetEquipmentType, StudyIndexationStatus, StudyUpdatedEventData, + TablesDefinitionsNames, TableSortKeysType, } from './reducer'; import { ComputingType } from '../components/computing-status/computing-type'; @@ -64,6 +65,7 @@ import { } from '../utils/store-sort-filter-fields'; import { SortConfigType } from '../hooks/use-aggrid-sort'; import { StudyDisplayMode } from '../components/network-modification.type'; +import { ColumnWithFormula, FormulaFilter } from 'components/spreadsheet/custom-columns/custom-columns.types'; type MutableUnknownArray = unknown[]; @@ -1124,3 +1126,22 @@ export function setTableSort(table: TableSortKeysType, tab: string, sort: SortCo sort, }; } + +export const CUSTOM_COLUMNS_DEFINITIONS = 'CUSTOM_COLUMNS_DEFINITIONS'; +export type CustomColumnsDefinitionsAction = Readonly> & { + table: TablesDefinitionsNames; + definitions: ColumnWithFormula[]; + filter: FormulaFilter; +}; +export function setCustomColumDefinitions( + table: TablesDefinitionsNames, + customColumNS: ColumnWithFormula[], + filter: FormulaFilter +): CustomColumnsDefinitionsAction { + return { + type: CUSTOM_COLUMNS_DEFINITIONS, + table, + definitions: customColumNS, + filter: filter, + }; +} diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index e8011b52b9..b66495cc3b 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -196,7 +196,11 @@ import { saveLocalStorageLanguage, saveLocalStorageTheme, } from './session-storage/local-storage'; -import { TABLES_COLUMNS_NAMES_JSON, TABLES_DEFINITIONS } from '../components/spreadsheet/utils/config-tables'; +import { + TABLES_COLUMNS_NAMES_JSON, + TABLES_DEFINITIONS, + TABLES_NAMES, +} from '../components/spreadsheet/utils/config-tables'; import { MAP_BASEMAP_CARTO, MAP_BASEMAP_CARTO_NOLABEL, @@ -271,6 +275,7 @@ import { Node } from 'react-flow-renderer'; import { BUILD_STATUS } from '../components/network/constants'; import { SortConfigType, SortWay } from '../hooks/use-aggrid-sort'; import { StudyDisplayMode } from '../components/network-modification.type'; +import { CustomEntry } from 'components/spreadsheet/custom-columns/custom-columns.types'; export enum NotificationType { STUDY = 'study', @@ -389,6 +394,10 @@ export type SelectionForCopy = { export type Actions = AppActions | AuthenticationActions; +export type TablesDefinitionsType = typeof TABLES_DEFINITIONS; +export type TablesDefinitionsKeys = keyof TablesDefinitionsType; +export type TablesDefinitionsNames = TablesDefinitionsType[TablesDefinitionsKeys]['name']; + export interface AppState extends CommonStoreState { signInCallbackError: Error | null; authenticationRouterError: AuthenticationRouterErrorState | null; @@ -435,6 +444,7 @@ export interface AppState extends CommonStoreState { reloadMap: boolean; isMapEquipmentsInitialized: boolean; spreadsheetNetwork: SpreadsheetNetworkState; + allCustomColumnsDefinitions: Record; [PARAM_THEME]: GsTheme; [PARAM_LANGUAGE]: GsLang; @@ -695,6 +705,11 @@ const initialState: AppState = { }, }, + allCustomColumnsDefinitions: TABLES_NAMES.reduce( + (acc, columnName, idx, arr) => ({ ...acc, [columnName]: { columns: [], filter: { formula: '' } } }), + {} as AppState['allCustomColumnsDefinitions'] + ), + // Hack to avoid reload Geo Data when switching display mode to TREE then back to MAP or HYBRID // defaulted to true to init load geo data with HYBRID defaulted display Mode // TODO REMOVE LATER diff --git a/src/translations/spreadsheet-en.ts b/src/translations/spreadsheet-en.ts new file mode 100644 index 0000000000..a419b4a2be --- /dev/null +++ b/src/translations/spreadsheet-en.ts @@ -0,0 +1,34 @@ +/* + * Copyright © 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/. + */ + +const spreadsheetFr = { + 'spreadsheet/custom_column/main_button': 'Add customized columns', + 'spreadsheet/custom_column/save': 'Enregistrer', + 'spreadsheet/custom_column/dialog/title': 'Manage formulas', + 'spreadsheet/custom_column/dialog/close_tooltip': + 'Close{isContentModified, select, true { without saving change(s)} other {}}', + 'spreadsheet/custom_column/dialog/add_column': 'Add column', + 'spreadsheet/custom_column/dialog/import_export': 'Import/Export formulas', + 'spreadsheet/custom_column/dialog/import_err': 'Error while importing', + 'spreadsheet/custom_column/import': 'Import', + 'spreadsheet/custom_column/reset': 'Reset', + 'spreadsheet/custom_column/copy': 'Copy to clipboard', + 'spreadsheet/custom_column/paste': 'Paste from clipboard', + 'spreadsheet/custom_column/ok': 'OK', + 'spreadsheet/custom_column/dialog_edit/title_add': 'New Formula', + 'spreadsheet/custom_column/dialog_edit/title_edit': 'Edit formula', + 'spreadsheet/custom_column/dialog_edit/placeholder': 'Enter the formula here', + 'spreadsheet/custom_column/dialog_edit/name': 'Name', + 'spreadsheet/custom_column/dialog_edit/name_description': 'Name of the formula', + 'spreadsheet/custom_column/dialog_edit/name_invalid': 'Invalid formula name', + 'spreadsheet/custom_column/dialog_edit/functions_tooltip': 'List of functions', + 'spreadsheet/custom_column/dialog_edit/submit_error': 'Invalid definition', + 'spreadsheet/custom_column/column_name': 'Column name', + 'spreadsheet/custom_column/column_content': 'Column content', +}; + +export default spreadsheetFr; diff --git a/src/translations/spreadsheet-fr.ts b/src/translations/spreadsheet-fr.ts new file mode 100644 index 0000000000..ce1deb890e --- /dev/null +++ b/src/translations/spreadsheet-fr.ts @@ -0,0 +1,34 @@ +/* + * Copyright © 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/. + */ + +const spreadsheetFr = { + 'spreadsheet/custom_column/main_button': 'Ajouter colonnes personnalisées', + 'spreadsheet/custom_column/save': 'Enregistrer', + 'spreadsheet/custom_column/dialog/title': 'Manage formulas', + 'spreadsheet/custom_column/dialog/close_tooltip': + 'Close{isContentModified, select, true { without saving change(s)} other {}}', + 'spreadsheet/custom_column/dialog/add_column': 'Add column', + 'spreadsheet/custom_column/dialog/import_export': 'Import/Export formulas', + 'spreadsheet/custom_column/dialog/import_err': 'Error while importing', + 'spreadsheet/custom_column/import': 'Import', + 'spreadsheet/custom_column/reset': 'Reset', + 'spreadsheet/custom_column/copy': 'Copy to clipboard', + 'spreadsheet/custom_column/paste': 'Paste from clipboard', + 'spreadsheet/custom_column/ok': 'OK', + 'spreadsheet/custom_column/dialog_edit/title_add': 'New Formula', + 'spreadsheet/custom_column/dialog_edit/title_edit': 'Edit formula', + 'spreadsheet/custom_column/dialog_edit/placeholder': 'Enter the formula here', + 'spreadsheet/custom_column/dialog_edit/name': 'Name', + 'spreadsheet/custom_column/dialog_edit/name_description': 'Name of the formula', + 'spreadsheet/custom_column/dialog_edit/name_invalid': 'Invalid formula name', + 'spreadsheet/custom_column/dialog_edit/functions_tooltip': 'List of functions', + 'spreadsheet/custom_column/dialog_edit/submit_error': 'Invalid definition', + 'spreadsheet/custom_column/column_name': 'Nom colonne', + 'spreadsheet/custom_column/column_content': 'Contenu colonne', +}; + +export default spreadsheetFr; From 75c04983c76c821c9e92afde2aba27373ee21be2 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Tue, 24 Sep 2024 21:43:25 +0200 Subject: [PATCH 02/10] update speadsheet from formula Signed-off-by: TOURI ANIS --- package-lock.json | 1 + package.json | 1 + .../custom-columns/columns-config-custom.tsx | 9 +- .../custom-columns/custom-column-dialog.tsx | 262 ++++++---------- .../custom-columns/custom-column-table.tsx | 25 +- .../custom-columns/custom-columns-dialog.tsx | 294 ------------------ .../custom-columns/custom-columns-form.tsx | 25 +- .../custom-columns/use-custom-column.ts | 145 +++++++++ src/components/spreadsheet/table-wrapper.jsx | 39 ++- src/components/utils/dnd-table/dnd-table.jsx | 5 +- src/redux/actions.ts | 11 +- src/redux/reducer.ts | 7 + src/redux/store.ts | 3 +- src/translations/en.json | 2 +- src/translations/fr.json | 2 +- src/translations/spreadsheet-en.ts | 1 + src/translations/spreadsheet-fr.ts | 1 + 17 files changed, 341 insertions(+), 492 deletions(-) delete mode 100644 src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx create mode 100644 src/components/spreadsheet/custom-columns/use-custom-column.ts diff --git a/package-lock.json b/package-lock.json index e05d6ee28d..3f151eaf35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "eventemitter3": "^5.0.1", "localized-countries": "^2.0.0", "lucene-escape-query": "^1.0.1", + "mathjs": "^13.0.3", "mjolnir.js": "^2.7.1", "mui-nested-menu": "^3.3.0", "notistack": "^3.0.1", diff --git a/package.json b/package.json index e9e309effa..71ab3058b7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "eventemitter3": "^5.0.1", "localized-countries": "^2.0.0", "lucene-escape-query": "^1.0.1", + "mathjs": "^13.0.3", "mjolnir.js": "^2.7.1", "mui-nested-menu": "^3.3.0", "notistack": "^3.0.1", diff --git a/src/components/spreadsheet/custom-columns/columns-config-custom.tsx b/src/components/spreadsheet/custom-columns/columns-config-custom.tsx index 19e5d75cd4..ac57990ac3 100644 --- a/src/components/spreadsheet/custom-columns/columns-config-custom.tsx +++ b/src/components/spreadsheet/custom-columns/columns-config-custom.tsx @@ -11,19 +11,18 @@ import { Badge, Box } from '@mui/material'; import { LoadingButton } from '@mui/lab'; import { Calculate as CalculateIcon } from '@mui/icons-material'; import { useSelector } from 'react-redux'; -//import CustomColumnsDialog from './custom-columns-dialog'; import { TABLES_NAMES } from '../utils/config-tables'; import { AppState } from '../../../redux/reducer'; import { useStateBoolean, useStateNumber } from '../../../hooks/use-states'; -import CustomColumnsDialog from './custom-columns-dialog'; +import CustomColumnDialog from './custom-column-dialog'; export type CustomColumnsConfigProps = { indexTab: number; }; export default function CustomColumnsConfig({ indexTab }: Readonly) { - const formulaCalculating = useStateBoolean(false); //TODO - const formulaError = useStateBoolean(false); //TODO + const formulaCalculating = useStateBoolean(false); + const formulaError = useStateBoolean(false); const numberColumns = useStateNumber(0); const dialogOpen = useStateBoolean(false); const allDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]]); @@ -64,7 +63,7 @@ export default function CustomColumnsConfig({ indexTab }: Readonly - + ); } diff --git a/src/components/spreadsheet/custom-columns/custom-column-dialog.tsx b/src/components/spreadsheet/custom-columns/custom-column-dialog.tsx index 233f884ad4..87404b4071 100644 --- a/src/components/spreadsheet/custom-columns/custom-column-dialog.tsx +++ b/src/components/spreadsheet/custom-columns/custom-column-dialog.tsx @@ -5,199 +5,141 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; -import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, - IconButton, - InputProps, - Stack, - SxProps, - TextField, - Theme, - Tooltip, -} from '@mui/material'; -import { Functions as FunctionsIcon } from '@mui/icons-material'; -import { CustomFormProvider, useSnackMessage } from '@gridsuite/commons-ui'; +import { useCallback, useEffect } from 'react'; +import { useIntl } from 'react-intl'; +import { Box, Dialog, DialogActions, DialogContent, DialogTitle, Grid, SxProps, Theme } from '@mui/material'; +import { CancelButton, CustomFormProvider, SubmitButton } from '@gridsuite/commons-ui'; import { UseStateBooleanReturn } from '../../../hooks/use-states'; import { ColumnWithFormula } from './custom-columns.types'; import { useForm } from 'react-hook-form'; -import { customColumnFormSchema, initialCustomColumnForm } from './custom-columns-form'; +import { + COLUMN_NAME, + CustomColumnForm, + customColumnFormSchema, + FORMULA, + initialCustomColumnForm, + TAB_CUSTOM_COLUMN, +} from './custom-columns-form'; + import { yupResolver } from '@hookform/resolvers/yup'; -import { FOLDER_NAME } from 'components/utils/field-constants'; import CustomColumnTable from './custom-column-table'; -import SaveIcon from '@mui/icons-material/Save'; +//TODO +/* import SaveIcon from '@mui/icons-material/Save'; import UploadIcon from '@mui/icons-material/Upload'; import DownloadIcon from '@mui/icons-material/Download'; -import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; +import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; */ +import { setCustomColumDefinitions } from 'redux/actions'; +import { TABLES_NAMES } from '../utils/config-tables'; +import { useDispatch } from 'react-redux'; +import { AppDispatch } from 'redux/store'; +import { useSelector } from 'react-redux'; +import { AppState } from 'redux/reducer'; export type CustomColumnDialogProps = { open: UseStateBooleanReturn; - baseData?: ColumnWithFormula; - onSubmit: (data: ColumnWithFormula) => void; + indexTab: number; }; const styles = { - rightButtons: { - textAlign: 'end', - '& > *': { - marginRight: 1, - }, + dialogContent: { + width: '35%', + maxWidth: 'none', + margin: 'auto', }, + actionButtons: { display: 'flex', gap: 2, justifyContent: 'end' }, } as const satisfies Record>; -// TODO: redo with react-hook-form -const nameValidationRegExp = /^[^\s$]+$/; - -export default function CustomColumnDialog({ open, baseData, onSubmit }: Readonly) { +export default function CustomColumnDialog({ open, indexTab }: Readonly) { const formMethods = useForm({ defaultValues: initialCustomColumnForm, resolver: yupResolver(customColumnFormSchema), }); + const { handleSubmit, reset } = formMethods; + const dispatch = useDispatch(); + const columnsDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions); + const intl = useIntl(); - const { snackError } = useSnackMessage(); - const [name, setName] = useState(baseData?.name ?? ''); - const [nameValid, setNameValid] = useState(false); - const handleNameChange = useCallback>( - (event) => { - setName(event.target.value ?? baseData?.name ?? ''); + const onSubmit = useCallback( + (newParams: CustomColumnForm) => { + dispatch( + setCustomColumDefinitions(TABLES_NAMES[indexTab], newParams.TAB_CUSTOM_COLUMN as ColumnWithFormula[]) + ); + reset({ + [TAB_CUSTOM_COLUMN]: [{ [COLUMN_NAME]: '', [FORMULA]: '' }], + }); + open.setFalse(); }, - [baseData?.name] - ); - useEffect(() => { - setNameValid(nameValidationRegExp.test(name)); - }, [name]); - const [formula, setFormula] = useState(baseData?.formula ?? ''); - const [formulaValid, setFormulaValid] = useState(false); - const handleFormulaChange = useCallback( - (value: string, event?: any) => { - setFormula(value ?? baseData?.formula ?? ''); - }, - [baseData?.formula] + [dispatch, indexTab, open, reset] ); useEffect(() => { - setFormulaValid(!!formula); - }, [formula]); - - const onSubmitClick = useCallback(() => { - try { - onSubmit({ name, formula }); - open.setFalse(); - } catch (error: unknown) { - console.error(error); - snackError({ - messageTxt: (error as Error).message, - headerId: 'spreadsheet/custom_column/dialog_edit/submit_error', + if (open.value && columnsDefinitions[TABLES_NAMES[indexTab]]?.columns.length !== 0) { + reset({ + [TAB_CUSTOM_COLUMN]: [...columnsDefinitions[TABLES_NAMES[indexTab]].columns], + }); + } else { + reset({ + [TAB_CUSTOM_COLUMN]: [{ [COLUMN_NAME]: '', [FORMULA]: '' }], }); } - }, [formula, name, onSubmit, open, snackError]); - - const [contentModified, setContentModified] = useState(false); - useEffect(() => { - // eslint-disable-next-line eqeqeq - setContentModified(baseData?.name != name || baseData?.formula != formula); - }, [baseData?.formula, baseData?.name, formula, name]); - - useEffect(() => { - if (open.value) { - setName(baseData?.name ?? ''); - setFormula(baseData?.formula ?? ''); - } - }, [baseData?.formula, baseData?.name, open.value]); + }, [columnsDefinitions, indexTab, open.value, reset]); return ( - - - - - - {/* Bouton Download */} - - - - - - - {/* Bouton Upload */} - - - - - - - {/* Bouton Save to GridExplore */} - - - - - - - {/* Bouton Insert from GridExplore */} - - - - - - - - + + + + {intl.formatMessage({ id: 'spreadsheet/custom_column/main_button' })} + + {/* TODO import/export json, save to GridExplore, select from GridExplore */} + + {/* + + + + + + + + + + + + + + + + + + + + + + + + */} + - - - - - - - - - - - - - - + + + + + + + + + - - - + + + ); } diff --git a/src/components/spreadsheet/custom-columns/custom-column-table.tsx b/src/components/spreadsheet/custom-columns/custom-column-table.tsx index de3f47cc81..153bdd7c7f 100644 --- a/src/components/spreadsheet/custom-columns/custom-column-table.tsx +++ b/src/components/spreadsheet/custom-columns/custom-column-table.tsx @@ -1,13 +1,28 @@ import DndTable from 'components/utils/dnd-table/dnd-table'; import { COLUMN_NAME, FORMULA, TAB_CUSTOM_COLUMN } from './custom-columns-form'; -import { SELECTED } from 'components/utils/field-constants'; import { useMemo } from 'react'; import { useIntl } from 'react-intl'; import { useFieldArray } from 'react-hook-form'; +import { IconButton, Tooltip } from '@mui/material'; +import InfoIcon from '@mui/icons-material/Info'; export default function CustomColumnTable() { const DndTableTyped = DndTable as React.ComponentType; const intl = useIntl(); + const CustomColumnTooltip = useMemo(() => { + return ( + + + + + + ); + }, [intl]); + const CUSTOM_COLUMNS_DEFINITIONS = useMemo(() => { return [ { @@ -16,13 +31,14 @@ export default function CustomColumnTable() { initialValue: null, editable: true, titleId: 'FiltersListsSelection', + width: '250px', }, { label: 'spreadsheet/custom_column/column_content', dataKey: FORMULA, initialValue: null, editable: true, - textAlign: 'right', + extra: CustomColumnTooltip, }, ].map((column) => ({ ...column, @@ -31,7 +47,7 @@ export default function CustomColumnTable() { .toLowerCase() .replace(/^\w/, (c) => c.toUpperCase()), })); - }, [intl]); + }, [CustomColumnTooltip, intl]); const useTabCustomColumnFieldArrayOutput = useFieldArray({ name: `${TAB_CUSTOM_COLUMN}`, @@ -39,7 +55,6 @@ export default function CustomColumnTable() { const newCustomColumnRowData = useMemo(() => { const newRowData: any = {}; - newRowData[SELECTED] = false; CUSTOM_COLUMNS_DEFINITIONS.forEach((column: any) => (newRowData[column.dataKey] = column.initialValue)); return newRowData; }, [CUSTOM_COLUMNS_DEFINITIONS]); @@ -54,8 +69,6 @@ export default function CustomColumnTable() { tableHeight={270} withAddRowsDialog={false} withLeftButtons={false} - handleUploadButton={undefined /*TODO*/} - uploadButtonMessageId="spreadsheet/custom_column/dialog_edit/upload" /> ); } diff --git a/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx b/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx deleted file mode 100644 index 0783ad8165..0000000000 --- a/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright © 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 { - AppBar, - Badge, - Button, - Dialog, - Divider, - IconButton, - List, - ListItem, - ListItemText, - SxProps, - Theme, - Toolbar, - Tooltip, - Typography, -} from '@mui/material'; -import { - AddCircle as AddCircleIcon, - Close as CloseIcon, - DeleteForever as DeleteForeverIcon, - Edit as EditIcon, - ImportExport as ImportExportIcon, - Save as SaveIcon, - Warning as WarningIcon, -} from '@mui/icons-material'; -import { FormattedMessage, useIntl } from 'react-intl'; -import { useStateBoolean, UseStateBooleanReturn } from '../../../hooks/use-states'; -import { useDispatch, useSelector } from 'react-redux'; -import { AppState } from '../../../redux/reducer'; -import { TABLES_NAMES } from '../utils/config-tables'; -import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; -import { ColumnWithFormula, CustomEntry } from './custom-columns.types'; -//import CustomColumnsImExPort from './custom-columns-port'; -import { AppDispatch } from '../../../redux/store'; -import { setCustomColumDefinitions } from '../../../redux/actions'; -import CustomColumnDialog from './custom-column-dialog'; -//import CustomColumnDialog from './custom-column-dialog'; - -type CustomColumnItemProps = { - data: ColumnWithFormula; - onDelete: () => void; - onEdit: () => void; -}; - -const styles: Record> = { - toolbarBtn: { - marginRight: 1, - }, -}; - -function CustomColumnItem({ data, onDelete, onEdit }: Readonly) { - return ( - - - - - - - - - } - > - - - ); -} - -function someCheckOnDefs(columns: ColumnWithFormula[]) { - //help some checks with yup? - console.error(JSON.stringify(columns)); - if (!(columns instanceof Array)) { - throw new Error("Column definitions isn't a list of columns"); - } - const names = new Set(); - for (const column of columns) { - if (typeof column !== 'object') { - throw new Error('Column definition must be an object', { cause: column }); - } - const objKeys = Object.keys(column); - if ( - objKeys.length !== 2 || - (objKeys[0] !== 'name' && objKeys[1] !== 'name') || - (objKeys[0] !== 'formula' && objKeys[1] !== 'formula') - ) { - throw new Error('Invalid column definition', { cause: column }); - } - if (!column.name) { - throw new Error(`Invalid column name "${column.name}"`); - } - column.name = column.name.trim(); - if (!column.formula) { - throw new Error(`Invalid formula "${column.formula}"`); - } - column.formula = column.formula.trim(); - if (names.has(column.name)) { - throw new Error('Formula names not unique'); - } else { - names.add(column.name); - } - //TODO found how to validate formula - } - return columns; -} - -export type CustomColumnsDialogProps = { - open: UseStateBooleanReturn; - indexTab: number; -}; - -//TODO idea: we can eval formulas with first line to detect common errors in advance and letting the user correcting it -export default function CustomColumnsDialog({ indexTab, open }: Readonly) { - const intl = useIntl(); - const dispatch = useDispatch(); - - const allDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]]); - const [columnsDefinitions, setColumnsDefinitions] = useState({ filter: { formula: '' }, columns: [] }); - const contentModified = useStateBoolean(false); - const resetContentModified = contentModified.setFalse; - const contentIsModified = contentModified.setTrue; - useEffect(() => { - if (open.value) { - setColumnsDefinitions({ - columns: allDefinitions.columns.map((def) => ({ ...def })), - filter: allDefinitions.filter, - }); - resetContentModified(); - setDialogColumnWorkingOn(undefined); - } - }, [open.value, allDefinitions, resetContentModified]); - - const dialogImportOpen = useStateBoolean(false); - - const dialogColumnOpen = useStateBoolean(false); - const [dialogColumnWorkingOn, setDialogColumnWorkingOn] = useState(undefined); - const onListItemDelete = useCallback( - (itemIdx: number) => { - setColumnsDefinitions((prevState) => { - let tmp = [...prevState.columns]; - tmp.splice(itemIdx, 1); - return { columns: tmp, filter: prevState.filter }; - }); - contentIsModified(); - }, - [contentIsModified] - ); - const onListItemEdit = useCallback( - (itemIdx: number) => { - setDialogColumnWorkingOn(columnsDefinitions.columns[itemIdx]); - dialogColumnOpen.setTrue(); - }, - [columnsDefinitions, dialogColumnOpen] - ); - const onAddColumnClick = useCallback(() => { - setDialogColumnWorkingOn(undefined); - dialogColumnOpen.setTrue(); - }, [dialogColumnOpen]); - const onImportColumn = useCallback( - (columnDef: ColumnWithFormula) => { - let newDefs: ColumnWithFormula[]; - if (dialogColumnWorkingOn === undefined) { - newDefs = [...columnsDefinitions.columns, columnDef]; - } else { - newDefs = [...columnsDefinitions.columns]; - newDefs.splice( - columnsDefinitions.columns.findIndex( - (value, index, arr) => value.name === dialogColumnWorkingOn.name - ), - 1, - columnDef - ); - setDialogColumnWorkingOn(undefined); //clean memory - } - setColumnsDefinitions({ columns: someCheckOnDefs(newDefs), filter: { formula: '' } }); - contentIsModified(); - }, - [columnsDefinitions, contentIsModified, dialogColumnWorkingOn] - ); - - const onSaveClick = useCallback(() => { - dispatch( - setCustomColumDefinitions(TABLES_NAMES[indexTab], columnsDefinitions.columns, columnsDefinitions.filter) - ); - open.setFalse(); - }, [columnsDefinitions, dispatch, indexTab, open]); - - return ( - - - - - theme.palette.grey[500], - }} - > - : null} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'right', - }} - > - - - - - - - - - - - - - - {useMemo( - () => - columnsDefinitions.columns.map((data, idx, arr) => ( - - onListItemDelete(idx)} - onEdit={() => onListItemEdit(idx)} - /> - {idx >= arr.length - 1 ? undefined : } - - )), - [columnsDefinitions, indexTab, onListItemDelete, onListItemEdit] - )} - - - - ); -} diff --git a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx index d7425cab28..fa4df822a2 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx @@ -8,12 +8,10 @@ import yup from '../../../components/utils/yup-config'; export const TAB_CUSTOM_COLUMN = 'TAB_CUSTOM_COLUMN'; -export const FORMULA_NAME = 'FORMULA_NAME'; -export const COLUMN_NAME = 'COLUMN_NAME'; -export const FORMULA = 'FORMULA'; +export const COLUMN_NAME = 'name'; +export const FORMULA = 'formula'; export const initialCustomColumnForm: CustomColumnForm = { - [FORMULA_NAME]: '', [TAB_CUSTOM_COLUMN]: [ { [COLUMN_NAME]: '', @@ -23,13 +21,18 @@ export const initialCustomColumnForm: CustomColumnForm = { }; export const customColumnFormSchema = yup.object().shape({ - [FORMULA_NAME]: yup.string().required(), - [TAB_CUSTOM_COLUMN]: yup.array().of( - yup.object().shape({ - [COLUMN_NAME]: yup.string().required(), - [FORMULA]: yup.string().required(), - }) - ), + [TAB_CUSTOM_COLUMN]: yup + .array() + .of( + yup.object().shape({ + [COLUMN_NAME]: yup + .string() + .required() + .matches(/^[^\s$]+$/, 'Column name must not contain spaces or $ symbols'), + [FORMULA]: yup.string().required(), + }) + ) + .min(1, 'The array must have at least one item'), }); export type CustomColumnForm = yup.InferType; diff --git a/src/components/spreadsheet/custom-columns/use-custom-column.ts b/src/components/spreadsheet/custom-columns/use-custom-column.ts new file mode 100644 index 0000000000..40034e0944 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/use-custom-column.ts @@ -0,0 +1,145 @@ +import { useMemo, useCallback } from 'react'; +import { AppState } from 'redux/reducer'; +import { create, all, bignumber } from 'mathjs'; +import { useSelector } from 'react-redux'; +import { TABLES_NAMES } from '../utils/config-tables'; +import { ColumnWithFormula } from './custom-columns.types'; + +export function useCustomColumn(tabIndex: number) { + const customColumnDefinitions = useSelector( + (state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[tabIndex]].columns + ); + + const math = useMemo(() => { + const instance = create(all, { + precision: 10, + number: 'BigNumber', + }); + + const limitedEvaluate = instance.evaluate.bind(instance); + // Disable potentially dangerous functions + instance.import( + { + import: function () { + throw new Error('Function import is disabled'); + }, + createUnit: function () { + throw new Error('Function createUnit is disabled'); + }, + evaluate: function () { + throw new Error('Function evaluate is disabled'); + }, + parse: function () { + throw new Error('Function parse is disabled'); + }, + simplify: function () { + throw new Error('Function simplify is disabled'); + }, + derivative: function () { + throw new Error('Function derivative is disabled'); + }, + equal: function (a: any, b: any) { + // == instead of === to be able to compare strings to numbers + return a === b; + }, + }, + { override: true } + ); + return { limitedEvaluate }; + }, []); + + const createDependencyGraph = (customColumnDefinitions: ColumnWithFormula[]): Map> => { + const graph = new Map>(); + + // Initialize the graph with all columns + customColumnDefinitions.forEach((col) => { + graph.set(col.name, new Set()); + }); + + // Build the graph by adding dependencies between columns + customColumnDefinitions.forEach((col) => { + customColumnDefinitions.forEach((depCol) => { + if (col.formula.includes(depCol.name) && col.name !== depCol.name) { + graph.get(col.name)!.add(depCol.name); + } + }); + }); + + return graph; + }; + + const topologicalSort = (graph: Map>): string[] => { + const sorted: string[] = []; + const visited = new Set(); + const visiting = new Set(); // Temporary set for detecting cycles + + const dfs = (node: string) => { + if (visiting.has(node)) { + throw new Error(`Circular dependency detected at column: ${node}`); + } + if (!visited.has(node)) { + visiting.add(node); // Mark the node as currently being visited + const dependencies = graph.get(node) || new Set(); + dependencies.forEach(dfs); + visiting.delete(node); + visited.add(node); + sorted.push(node); + } + }; + + // Start DFS from each node + for (const node of graph.keys()) { + if (!visited.has(node)) { + dfs(node); + } + } + + return sorted; + }; + + // Main function to sort columns by dependencies and calculate values + const sortedColumnDefinitions = useMemo(() => { + const graph = createDependencyGraph(customColumnDefinitions); + const sortedColumns = topologicalSort(graph); + + return sortedColumns.map((name) => + customColumnDefinitions.find((col) => col.name === name) + ) as ColumnWithFormula[]; + }, [customColumnDefinitions]); + + const calcAllColumnValues = useCallback( + (lineData: Record): Map => { + const customColumnsValues = new Map(); + const scope: Record = {}; + + // Add line data to the scope + Object.entries(lineData).forEach(([key, value]) => { + scope[`${key}`] = typeof value === 'number' ? bignumber(value) : value; + }); + + sortedColumnDefinitions.forEach((column) => { + if (!column) { + return; + } + try { + // Evaluate the formula and update the Map and scope + const result = math.limitedEvaluate(column.formula, { + ...scope, + ...Object.fromEntries(customColumnsValues), + }); + + customColumnsValues.set(column.name, result); + scope[column.name] = result; // Add calculated result to scope for future columns + } catch (error: any) { + console.error(`Error evaluating formula for column ${column.name}: ${error.message}`); + customColumnsValues.set(column.name, '#ERR'); + } + }); + + return customColumnsValues; + }, + [math, sortedColumnDefinitions] + ); + + return { calcAllColumnValues }; +} diff --git a/src/components/spreadsheet/table-wrapper.jsx b/src/components/spreadsheet/table-wrapper.jsx index 32f7cfe49e..223d207155 100644 --- a/src/components/spreadsheet/table-wrapper.jsx +++ b/src/components/spreadsheet/table-wrapper.jsx @@ -21,7 +21,7 @@ import { } from './utils/config-tables'; import { EquipmentTable } from './equipment-table'; import { useSnackMessage } from '@gridsuite/commons-ui'; -import { PARAM_FLUX_CONVENTION } from '../../utils/config-params'; +import { PARAM_DEVELOPER_MODE, PARAM_FLUX_CONVENTION } from '../../utils/config-params'; import { RunningStatus } from '../utils/running-status'; import { DefaultCellRenderer, @@ -79,6 +79,7 @@ import { setSpreadsheetFilter } from 'redux/actions'; import { useLocalizedCountries } from 'components/utils/localized-countries-hook'; import { SPREADSHEET_SORT_STORE, SPREADSHEET_STORE_FIELD } from 'utils/store-sort-filter-fields'; import CustomColumnsConfig from './custom-columns/columns-config-custom'; +import { useCustomColumn } from './custom-columns/use-custom-column'; const useEditBuffer = () => { //the data is feeded and read during the edition validation process so we don't need to rerender after a call to one of available methods thus useRef is more suited @@ -138,12 +139,15 @@ const TableWrapper = (props) => { const { translate } = useLocalizedCountries(); const { snackError } = useSnackMessage(); + const [tabIndex, setTabIndex] = useState(0); const loadFlowStatus = useSelector((state) => state.computingStatus[ComputingType.LOAD_FLOW]); const allDisplayedColumnsNames = useSelector((state) => state.allDisplayedColumnsNames); const allLockedColumnsNames = useSelector((state) => state.allLockedColumnsNames); const allReorderedTableDefinitionIndexes = useSelector((state) => state.allReorderedTableDefinitionIndexes); + const customColumnsDefinitions = useSelector((state) => state.allCustomColumnsDefinitions[TABLES_NAMES[tabIndex]]); + const developerMode = useSelector((state) => state[PARAM_DEVELOPER_MODE]); const [selectedColumnsNames, setSelectedColumnsNames] = useState(new Set()); const [lockedColumnsNames, setLockedColumnsNames] = useState(new Set()); @@ -154,7 +158,6 @@ const TableWrapper = (props) => { const [lastModifiedEquipment, setLastModifiedEquipment] = useState(); - const [tabIndex, setTabIndex] = useState(0); const [manualTabSwitch, setManualTabSwitch] = useState(true); const [priorValuesBuffer, addDataToBuffer, resetBuffer] = useEditBuffer(); @@ -162,11 +165,33 @@ const TableWrapper = (props) => { const editingDataRef = useRef(editingData); const isLockedColumnNamesEmpty = useMemo(() => lockedColumnsNames.size === 0, [lockedColumnsNames.size]); + const [customColumnData, setCustomColumnData] = useState([]); + const [mergedColumnData, setMergedColumnData] = useState([]); + const { calcAllColumnValues } = useCustomColumn(tabIndex); + + useEffect(() => { + setCustomColumnData( + customColumnsDefinitions.columns.map((colWithFormula, idx, arr) => ({ + coldId: `custom-${tabIndex}-${idx}`, + headerName: colWithFormula.name, + valueGetter: (params) => { + const allValues = calcAllColumnValues(params.data); + return allValues.get(colWithFormula.name); + }, + editable: false, + cellDataType: true, // true<=>auto, infer the data type from the row data ('text', 'number', 'boolean', 'date', 'dateString' or 'object') + })) + ); + }, [tabIndex, customColumnsDefinitions, calcAllColumnValues]); const globalFilterRef = useRef(); const [columnData, setColumnData] = useState([]); + useEffect(() => { + setMergedColumnData([...columnData, ...customColumnData]); + }, [columnData, customColumnData]); + const rollbackEdit = useCallback(() => { resetBuffer(); setEditingData(); @@ -1117,9 +1142,11 @@ const TableWrapper = (props) => { setLockedColumnsNames={setLockedColumnsNames} /> - - - + {developerMode && ( + + + + )} { studyUuid={props.studyUuid} currentNode={props.currentNode} rowData={rowData} - columnData={columnData} + columnData={mergedColumnData} topPinnedData={topPinnedData} fetched={equipments || errorMessage} visible={props.visible} diff --git a/src/components/utils/dnd-table/dnd-table.jsx b/src/components/utils/dnd-table/dnd-table.jsx index 29c3b9cbc9..83ec1a74e5 100644 --- a/src/components/utils/dnd-table/dnd-table.jsx +++ b/src/components/utils/dnd-table/dnd-table.jsx @@ -38,7 +38,7 @@ import ChipItemsInput from '../rhf-inputs/chip-items-input'; export const MAX_ROWS_NUMBER = 100; const styles = { columnsStyle: { - display: 'flex', + display: 'inline-flex', justifyContent: 'space-between', alignItems: 'center', margin: 1, @@ -164,6 +164,7 @@ const DndTable = ({ getPreviousValue ? getPreviousValue(rowIndex, column, arrayFormName, previousValues) : undefined } valueModified={isValueModified ? isValueModified(rowIndex, arrayFormName) : false} + sx={{ width: column?.width }} /> ); } @@ -285,7 +286,7 @@ const DndTable = ({ /> {columnsDefinition.map((column) => ( - + {column.label} {column.extra} diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 7216e861db..a45857aba7 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -152,7 +152,8 @@ export type AppActions = | SensitivityAnalysisResultFilterAction | ShortcircuitAnalysisResultFilterAction | DynamicSimulationResultFilterAction - | SpreadsheetFilterAction; + | SpreadsheetFilterAction + | CustomColumnsDefinitionsAction; export const LOAD_EQUIPMENTS = 'LOAD_EQUIPMENTS'; export type LoadEquipmentsAction = Readonly> & { @@ -1131,17 +1132,17 @@ export const CUSTOM_COLUMNS_DEFINITIONS = 'CUSTOM_COLUMNS_DEFINITIONS'; export type CustomColumnsDefinitionsAction = Readonly> & { table: TablesDefinitionsNames; definitions: ColumnWithFormula[]; - filter: FormulaFilter; + filter?: FormulaFilter; }; export function setCustomColumDefinitions( table: TablesDefinitionsNames, - customColumNS: ColumnWithFormula[], - filter: FormulaFilter + customColumns: ColumnWithFormula[], + filter?: FormulaFilter ): CustomColumnsDefinitionsAction { return { type: CUSTOM_COLUMNS_DEFINITIONS, table, - definitions: customColumNS, + definitions: customColumns, filter: filter, }; } diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index b66495cc3b..9ccf50f79e 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -54,6 +54,8 @@ import { ComponentLibraryAction, CURRENT_TREE_NODE, CurrentTreeNodeAction, + CUSTOM_COLUMNS_DEFINITIONS, + CustomColumnsDefinitionsAction, DECREMENT_NETWORK_AREA_DIAGRAM_DEPTH, DecrementNetworkAreaDiagramDepthAction, DELETE_EQUIPMENTS, @@ -1558,6 +1560,11 @@ export const reducer = createReducer(initialState, (builder) => { builder.addCase(TABLE_SORT, (state, action: TableSortAction) => { state.tableSort[action.table][action.tab] = action.sort; }); + + builder.addCase(CUSTOM_COLUMNS_DEFINITIONS, (state, action: CustomColumnsDefinitionsAction) => { + state.allCustomColumnsDefinitions[action.table].columns = action.definitions; + //state.allCustomColumnsDefinitions[action.table].filter = action.filter; + }); }); function updateSubstationAfterVLDeletion(currentSubstations: Substation[], VLToDeleteId: string): Substation[] { diff --git a/src/redux/store.ts b/src/redux/store.ts index 9a819239cc..9904b5c481 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -9,8 +9,9 @@ import { legacy_createStore as createStore, Store } from 'redux'; import { Actions, AppState, reducer } from './reducer'; import { setCommonStore } from '@gridsuite/commons-ui'; import { setUserStore } from './user-store'; +import { composeWithDevTools } from '@redux-devtools/extension'; -export const store = createStore(reducer); +export const store = createStore(reducer, composeWithDevTools()); setCommonStore(store); setUserStore(store); export type AppDispatch = Store['dispatch']; diff --git a/src/translations/en.json b/src/translations/en.json index 54251b5ce0..b91975f571 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -530,7 +530,7 @@ "LossFactor": "Loss Factor", "StopComputation": "Stop computation", "ColumnsList": "Column list", - "LabelSelectList": "Edit columns", + "LabelSelectList": "Show / hide columns", "CheckAll": "Select all / none", "genericConfirmQuestion": "You are about to leave without saving your current modifications.", diff --git a/src/translations/fr.json b/src/translations/fr.json index 66edc28265..ce6c8c3b42 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -532,7 +532,7 @@ "LossFactor": "Facteur de perte", "StopComputation": "Arrêter le calcul", "ColumnsList": "Liste des colonnes", - "LabelSelectList": "Configurer colonnes", + "LabelSelectList": "Afficher / masquer colonnes", "CheckAll": "Sélectionner tout / aucun", "genericConfirmQuestion": "Vous êtes sur le point de quitter sans sauvegarder vos modifications.", diff --git a/src/translations/spreadsheet-en.ts b/src/translations/spreadsheet-en.ts index a419b4a2be..1f5b9116e2 100644 --- a/src/translations/spreadsheet-en.ts +++ b/src/translations/spreadsheet-en.ts @@ -29,6 +29,7 @@ const spreadsheetFr = { 'spreadsheet/custom_column/dialog_edit/submit_error': 'Invalid definition', 'spreadsheet/custom_column/column_name': 'Column name', 'spreadsheet/custom_column/column_content': 'Column content', + 'spreadsheet/custom_column/column_content_tooltip': `Column content is described with variable names (in order to reference grid data) and operators provided by MathJS library (in order to transform grid data). Example: maxP - p in order to display active power reserve within the generator spreadsheet`, }; export default spreadsheetFr; diff --git a/src/translations/spreadsheet-fr.ts b/src/translations/spreadsheet-fr.ts index ce1deb890e..cbeea669c3 100644 --- a/src/translations/spreadsheet-fr.ts +++ b/src/translations/spreadsheet-fr.ts @@ -29,6 +29,7 @@ const spreadsheetFr = { 'spreadsheet/custom_column/dialog_edit/submit_error': 'Invalid definition', 'spreadsheet/custom_column/column_name': 'Nom colonne', 'spreadsheet/custom_column/column_content': 'Contenu colonne', + 'spreadsheet/custom_column/column_content_tooltip': `Le contenu d'une colonne est décrit avec des noms de variables (pour faire référence aux données du réseau) et des opérateurs proposés par la librairie MathJS (pour transformer les données du réseau). Exemple : maxP - p pour afficher la réserve de puissance active dans le tableur des groupes`, }; export default spreadsheetFr; From 2e0cad5b637a271eedb2ece74adf55d82e45ef90 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Tue, 24 Sep 2024 21:47:21 +0200 Subject: [PATCH 03/10] add Copyright Signed-off-by: TOURI ANIS --- .../spreadsheet/custom-columns/custom-column-table.tsx | 6 ++++++ .../spreadsheet/custom-columns/use-custom-column.ts | 6 ++++++ src/redux/store.ts | 3 +-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/spreadsheet/custom-columns/custom-column-table.tsx b/src/components/spreadsheet/custom-columns/custom-column-table.tsx index 153bdd7c7f..cb24bdbca2 100644 --- a/src/components/spreadsheet/custom-columns/custom-column-table.tsx +++ b/src/components/spreadsheet/custom-columns/custom-column-table.tsx @@ -1,3 +1,9 @@ +/* + * Copyright © 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 DndTable from 'components/utils/dnd-table/dnd-table'; import { COLUMN_NAME, FORMULA, TAB_CUSTOM_COLUMN } from './custom-columns-form'; import { useMemo } from 'react'; diff --git a/src/components/spreadsheet/custom-columns/use-custom-column.ts b/src/components/spreadsheet/custom-columns/use-custom-column.ts index 40034e0944..3cb9feb4c9 100644 --- a/src/components/spreadsheet/custom-columns/use-custom-column.ts +++ b/src/components/spreadsheet/custom-columns/use-custom-column.ts @@ -1,3 +1,9 @@ +/* + * Copyright © 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 { useMemo, useCallback } from 'react'; import { AppState } from 'redux/reducer'; import { create, all, bignumber } from 'mathjs'; diff --git a/src/redux/store.ts b/src/redux/store.ts index 9904b5c481..9a819239cc 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -9,9 +9,8 @@ import { legacy_createStore as createStore, Store } from 'redux'; import { Actions, AppState, reducer } from './reducer'; import { setCommonStore } from '@gridsuite/commons-ui'; import { setUserStore } from './user-store'; -import { composeWithDevTools } from '@redux-devtools/extension'; -export const store = createStore(reducer, composeWithDevTools()); +export const store = createStore(reducer); setCommonStore(store); setUserStore(store); export type AppDispatch = Store['dispatch']; From dd16e04633e977e0fa1b1c0f6b56da50aa939327 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Wed, 25 Sep 2024 12:36:04 +0200 Subject: [PATCH 04/10] move useState to commons-ui --- .../custom-columns/columns-config-custom.tsx | 2 +- src/components/use-states.ts | 42 ------------------- src/hooks/use-states.ts | 42 ------------------- 3 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 src/components/use-states.ts delete mode 100644 src/hooks/use-states.ts diff --git a/src/components/spreadsheet/custom-columns/columns-config-custom.tsx b/src/components/spreadsheet/custom-columns/columns-config-custom.tsx index ac57990ac3..a10a7c8739 100644 --- a/src/components/spreadsheet/custom-columns/columns-config-custom.tsx +++ b/src/components/spreadsheet/custom-columns/columns-config-custom.tsx @@ -13,8 +13,8 @@ import { Calculate as CalculateIcon } from '@mui/icons-material'; import { useSelector } from 'react-redux'; import { TABLES_NAMES } from '../utils/config-tables'; import { AppState } from '../../../redux/reducer'; -import { useStateBoolean, useStateNumber } from '../../../hooks/use-states'; import CustomColumnDialog from './custom-column-dialog'; +import { useStateBoolean, useStateNumber } from '@gridsuite/commons-ui'; export type CustomColumnsConfigProps = { indexTab: number; diff --git a/src/components/use-states.ts b/src/components/use-states.ts deleted file mode 100644 index fe783109e5..0000000000 --- a/src/components/use-states.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright © 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 { Dispatch, SetStateAction, useCallback, useState } from 'react'; - -export type UseStateBooleanReturn = { - value: boolean; - setValue: Dispatch>; - setTrue: () => void; - setFalse: () => void; - invert: () => void; -}; - -//TODO move in commons-ui -export function useStateBoolean(initialState: boolean | (() => boolean)): UseStateBooleanReturn { - const [value, setValue] = useState(initialState); - const setTrue = useCallback(() => setValue(true), []); - const setFalse = useCallback(() => setValue(false), []); - const invert = useCallback(() => setValue((prevState) => !prevState), []); - return { value, setTrue, setFalse, invert, setValue }; -} - -export type UseStateNumberReturn = { - value: number; - setValue: Dispatch>; - increment: () => void; - decrement: () => void; - reset: () => void; -}; - -//TODO move in commons-ui -export function useStateNumber(initialState: number | (() => number) = 0): UseStateNumberReturn { - const [value, setValue] = useState(initialState); - const increment = useCallback((n: number = 1) => setValue((prevState) => prevState + n), []); - const decrement = useCallback((n: number = 1) => setValue((prevState) => prevState - n), []); - const reset = useCallback(() => setValue(initialState), [initialState]); - return { value, increment, decrement, reset, setValue }; -} diff --git a/src/hooks/use-states.ts b/src/hooks/use-states.ts deleted file mode 100644 index fe783109e5..0000000000 --- a/src/hooks/use-states.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright © 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 { Dispatch, SetStateAction, useCallback, useState } from 'react'; - -export type UseStateBooleanReturn = { - value: boolean; - setValue: Dispatch>; - setTrue: () => void; - setFalse: () => void; - invert: () => void; -}; - -//TODO move in commons-ui -export function useStateBoolean(initialState: boolean | (() => boolean)): UseStateBooleanReturn { - const [value, setValue] = useState(initialState); - const setTrue = useCallback(() => setValue(true), []); - const setFalse = useCallback(() => setValue(false), []); - const invert = useCallback(() => setValue((prevState) => !prevState), []); - return { value, setTrue, setFalse, invert, setValue }; -} - -export type UseStateNumberReturn = { - value: number; - setValue: Dispatch>; - increment: () => void; - decrement: () => void; - reset: () => void; -}; - -//TODO move in commons-ui -export function useStateNumber(initialState: number | (() => number) = 0): UseStateNumberReturn { - const [value, setValue] = useState(initialState); - const increment = useCallback((n: number = 1) => setValue((prevState) => prevState + n), []); - const decrement = useCallback((n: number = 1) => setValue((prevState) => prevState - n), []); - const reset = useCallback(() => setValue(initialState), [initialState]); - return { value, increment, decrement, reset, setValue }; -} From c15d8b972cdabb3b2069e5f3de09e6319dc27fec Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Mon, 30 Sep 2024 15:05:09 +0200 Subject: [PATCH 05/10] Addressed PR feedback Signed-off-by: TOURI ANIS --- package.json | 2 +- .../custom-aggrid-header.type.ts | 2 +- ...g-custom.tsx => custom-columns-config.tsx} | 9 +- ...n-dialog.tsx => custom-columns-dialog.tsx} | 55 ++------- .../custom-columns/custom-columns-form.tsx | 1 + ...umn-table.tsx => custom-columns-table.tsx} | 5 +- .../custom-columns/custom-columns-utils.tsx | 57 +++++++++ .../custom-columns/use-custom-column.ts | 113 +++++++++--------- src/components/spreadsheet/table-wrapper.jsx | 19 +-- .../spreadsheet/utils/config-tables.js | 2 +- src/components/utils/dnd-table/dnd-table.jsx | 3 +- src/redux/reducer.ts | 2 +- src/translations/spreadsheet-en.ts | 25 +--- src/translations/spreadsheet-fr.ts | 20 ---- .../custom-columns.types.tsx | 0 15 files changed, 138 insertions(+), 177 deletions(-) rename src/components/spreadsheet/custom-columns/{columns-config-custom.tsx => custom-columns-config.tsx} (86%) rename src/components/spreadsheet/custom-columns/{custom-column-dialog.tsx => custom-columns-dialog.tsx} (63%) rename src/components/spreadsheet/custom-columns/{custom-column-table.tsx => custom-columns-table.tsx} (96%) create mode 100644 src/components/spreadsheet/custom-columns/custom-columns-utils.tsx rename src/{components/spreadsheet/custom-columns => types}/custom-columns.types.tsx (100%) diff --git a/package.json b/package.json index 03fe48eb77..111ade38ea 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "eventemitter3": "^5.0.1", "localized-countries": "^2.0.0", "lucene-escape-query": "^1.0.1", - "mathjs": "^13.0.3", + "mathjs": "^13.1.1", "mjolnir.js": "^2.7.1", "mui-nested-menu": "^3.3.0", "notistack": "^3.0.1", diff --git a/src/components/custom-aggrid/custom-aggrid-header.type.ts b/src/components/custom-aggrid/custom-aggrid-header.type.ts index 7178ed1371..8f5644918d 100644 --- a/src/components/custom-aggrid/custom-aggrid-header.type.ts +++ b/src/components/custom-aggrid/custom-aggrid-header.type.ts @@ -28,7 +28,7 @@ export enum FILTER_NUMBER_COMPARATORS { GREATER_THAN = 'greaterThan', } -type FilterParams = { +export type FilterParams = { filterDataType?: string; isDuration?: boolean; filterComparators?: string[]; diff --git a/src/components/spreadsheet/custom-columns/columns-config-custom.tsx b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx similarity index 86% rename from src/components/spreadsheet/custom-columns/columns-config-custom.tsx rename to src/components/spreadsheet/custom-columns/custom-columns-config.tsx index a10a7c8739..25ef01516e 100644 --- a/src/components/spreadsheet/custom-columns/columns-config-custom.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx @@ -13,7 +13,7 @@ import { Calculate as CalculateIcon } from '@mui/icons-material'; import { useSelector } from 'react-redux'; import { TABLES_NAMES } from '../utils/config-tables'; import { AppState } from '../../../redux/reducer'; -import CustomColumnDialog from './custom-column-dialog'; +import CustomColumnDialog from './custom-columns-dialog'; import { useStateBoolean, useStateNumber } from '@gridsuite/commons-ui'; export type CustomColumnsConfigProps = { @@ -26,10 +26,10 @@ export default function CustomColumnsConfig({ indexTab }: Readonly state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]]); - const uEffectNumberColumnsSetValue = numberColumns.setValue; // eslint detection + useEffect(() => { - uEffectNumberColumnsSetValue(allDefinitions.columns.length); - }, [allDefinitions.columns.length, uEffectNumberColumnsSetValue]); + numberColumns.setValue(allDefinitions.columns.length); + }, [allDefinitions.columns.length, numberColumns]); /* eslint-enable react-hooks/rules-of-hooks */ return ( @@ -37,7 +37,6 @@ export default function CustomColumnsConfig({ indexTab }: Readonly { - dispatch( - setCustomColumDefinitions(TABLES_NAMES[indexTab], newParams.TAB_CUSTOM_COLUMN as ColumnWithFormula[]) - ); - reset({ - [TAB_CUSTOM_COLUMN]: [{ [COLUMN_NAME]: '', [FORMULA]: '' }], - }); + dispatch(setCustomColumDefinitions(TABLES_NAMES[indexTab], newParams[TAB_CUSTOM_COLUMN])); + reset(initialCustomColumnForm); open.setFalse(); }, @@ -81,9 +69,7 @@ export default function CustomColumnDialog({ open, indexTab }: Readonly {intl.formatMessage({ id: 'spreadsheet/custom_column/main_button' })} - {/* TODO import/export json, save to GridExplore, select from GridExplore */} - - {/* - - - - - - - - - - - - - - - - - - - - - - - - */} diff --git a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx index fa4df822a2..b1f3b5e5a4 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx @@ -32,6 +32,7 @@ export const customColumnFormSchema = yup.object().shape({ [FORMULA]: yup.string().required(), }) ) + .required() .min(1, 'The array must have at least one item'), }); diff --git a/src/components/spreadsheet/custom-columns/custom-column-table.tsx b/src/components/spreadsheet/custom-columns/custom-columns-table.tsx similarity index 96% rename from src/components/spreadsheet/custom-columns/custom-column-table.tsx rename to src/components/spreadsheet/custom-columns/custom-columns-table.tsx index cb24bdbca2..633270bca9 100644 --- a/src/components/spreadsheet/custom-columns/custom-column-table.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-table.tsx @@ -37,7 +37,7 @@ export default function CustomColumnTable() { initialValue: null, editable: true, titleId: 'FiltersListsSelection', - width: '250px', + width: '30%', }, { label: 'spreadsheet/custom_column/column_content', @@ -45,6 +45,7 @@ export default function CustomColumnTable() { initialValue: null, editable: true, extra: CustomColumnTooltip, + width: '70%', }, ].map((column) => ({ ...column, @@ -72,7 +73,7 @@ export default function CustomColumnTable() { columnsDefinition={CUSTOM_COLUMNS_DEFINITIONS} useFieldArrayOutput={useTabCustomColumnFieldArrayOutput} createRows={createCustomColumnRows} - tableHeight={270} + tableHeight={380} withAddRowsDialog={false} withLeftButtons={false} /> diff --git a/src/components/spreadsheet/custom-columns/custom-columns-utils.tsx b/src/components/spreadsheet/custom-columns/custom-columns-utils.tsx new file mode 100644 index 0000000000..dc305d9150 --- /dev/null +++ b/src/components/spreadsheet/custom-columns/custom-columns-utils.tsx @@ -0,0 +1,57 @@ +/* + * Copyright © 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 { ColumnWithFormula } from 'types/custom-columns.types'; + +export const createDependencyGraph = (customColumnDefinitions: ColumnWithFormula[]): Map> => { + const graph = new Map>(); + + // Initialize the graph with all columns + customColumnDefinitions.forEach((col) => { + graph.set(col.name, new Set()); + }); + + // Build the graph by adding dependencies between columns + customColumnDefinitions.forEach((col) => { + customColumnDefinitions.forEach((depCol) => { + if (col.formula.includes(depCol.name) && col.name !== depCol.name) { + graph.get(col.name)!.add(depCol.name); + } + }); + }); + + return graph; +}; + +export const topologicalSort = (graph: Map>): string[] => { + const sorted: string[] = []; + const visited = new Set(); + const visiting = new Set(); // Temporary set for detecting cycles + + const dfs = (node: string) => { + if (visiting.has(node)) { + throw new Error(`Circular dependency detected at column: ${node}`); + } + if (!visited.has(node)) { + visiting.add(node); // Mark the node as currently being visited + const dependencies = graph.get(node) || new Set(); + dependencies.forEach(dfs); + visiting.delete(node); + visited.add(node); + sorted.push(node); + } + }; + + // Start DFS from each node + for (const node of graph.keys()) { + if (!visited.has(node)) { + dfs(node); + } + } + + return sorted; +}; diff --git a/src/components/spreadsheet/custom-columns/use-custom-column.ts b/src/components/spreadsheet/custom-columns/use-custom-column.ts index 3cb9feb4c9..ac30e7acd2 100644 --- a/src/components/spreadsheet/custom-columns/use-custom-column.ts +++ b/src/components/spreadsheet/custom-columns/use-custom-column.ts @@ -8,14 +8,33 @@ import { useMemo, useCallback } from 'react'; import { AppState } from 'redux/reducer'; import { create, all, bignumber } from 'mathjs'; import { useSelector } from 'react-redux'; -import { TABLES_NAMES } from '../utils/config-tables'; -import { ColumnWithFormula } from './custom-columns.types'; - -export function useCustomColumn(tabIndex: number) { - const customColumnDefinitions = useSelector( +import { defaultNumericFilterConfig, TABLES_DEFINITION_INDEXES, TABLES_NAMES } from '../utils/config-tables'; +import { makeAgGridCustomHeaderColumn } from 'components/custom-aggrid/custom-aggrid-header-utils'; +import { useAgGridSort } from 'hooks/use-aggrid-sort'; +import { SPREADSHEET_SORT_STORE, SPREADSHEET_STORE_FIELD } from 'utils/store-sort-filter-fields'; +import { useAggridLocalRowFilter } from 'hooks/use-aggrid-local-row-filter'; +import { setSpreadsheetFilter } from 'redux/actions'; +import { FilterParams } from 'components/custom-aggrid/custom-aggrid-header.type'; +import { ColumnWithFormula } from 'types/custom-columns.types'; +import { createDependencyGraph, topologicalSort } from './custom-columns-utils'; + +export function useCustomColumn(tabIndex: number, gridRef: any) { + const customColumnsDefinitions = useSelector( (state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[tabIndex]].columns ); + const { onSortChanged, sortConfig } = useAgGridSort( + SPREADSHEET_SORT_STORE, + TABLES_DEFINITION_INDEXES.get(tabIndex)!.type as string + ); + + const { updateFilter, filterSelector } = useAggridLocalRowFilter(gridRef, { + filterType: SPREADSHEET_STORE_FIELD, + filterTab: TABLES_DEFINITION_INDEXES.get(tabIndex)!.type as string, + // @ts-expect-error TODO + filterStoreAction: setSpreadsheetFilter, + }); + const math = useMemo(() => { const instance = create(all, { precision: 10, @@ -54,64 +73,15 @@ export function useCustomColumn(tabIndex: number) { return { limitedEvaluate }; }, []); - const createDependencyGraph = (customColumnDefinitions: ColumnWithFormula[]): Map> => { - const graph = new Map>(); - - // Initialize the graph with all columns - customColumnDefinitions.forEach((col) => { - graph.set(col.name, new Set()); - }); - - // Build the graph by adding dependencies between columns - customColumnDefinitions.forEach((col) => { - customColumnDefinitions.forEach((depCol) => { - if (col.formula.includes(depCol.name) && col.name !== depCol.name) { - graph.get(col.name)!.add(depCol.name); - } - }); - }); - - return graph; - }; - - const topologicalSort = (graph: Map>): string[] => { - const sorted: string[] = []; - const visited = new Set(); - const visiting = new Set(); // Temporary set for detecting cycles - - const dfs = (node: string) => { - if (visiting.has(node)) { - throw new Error(`Circular dependency detected at column: ${node}`); - } - if (!visited.has(node)) { - visiting.add(node); // Mark the node as currently being visited - const dependencies = graph.get(node) || new Set(); - dependencies.forEach(dfs); - visiting.delete(node); - visited.add(node); - sorted.push(node); - } - }; - - // Start DFS from each node - for (const node of graph.keys()) { - if (!visited.has(node)) { - dfs(node); - } - } - - return sorted; - }; - // Main function to sort columns by dependencies and calculate values const sortedColumnDefinitions = useMemo(() => { - const graph = createDependencyGraph(customColumnDefinitions); + const graph = createDependencyGraph(customColumnsDefinitions); const sortedColumns = topologicalSort(graph); return sortedColumns.map((name) => - customColumnDefinitions.find((col) => col.name === name) + customColumnsDefinitions.find((col) => col.name === name) ) as ColumnWithFormula[]; - }, [customColumnDefinitions]); + }, [customColumnsDefinitions]); const calcAllColumnValues = useCallback( (lineData: Record): Map => { @@ -138,7 +108,6 @@ export function useCustomColumn(tabIndex: number) { scope[column.name] = result; // Add calculated result to scope for future columns } catch (error: any) { console.error(`Error evaluating formula for column ${column.name}: ${error.message}`); - customColumnsValues.set(column.name, '#ERR'); } }); @@ -147,5 +116,31 @@ export function useCustomColumn(tabIndex: number) { [math, sortedColumnDefinitions] ); - return { calcAllColumnValues }; + const createCustomColumn = useCallback(() => { + return customColumnsDefinitions.map((colWithFormula: ColumnWithFormula) => { + return makeAgGridCustomHeaderColumn({ + headerName: colWithFormula.name, + field: colWithFormula.name, + numeric: true, + /* sortProps: { + onSortChanged, + sortConfig, + }, */ + filterProps: { + updateFilter, + filterSelector, + }, + filterParams: { ...defaultNumericFilterConfig() } as FilterParams, + valueGetter: (params) => { + const allValues = calcAllColumnValues(params.data); + return allValues.get(colWithFormula.name); + }, + editable: false, + cellDataType: true, + suppressMovable: true, + }); + }); + }, [customColumnsDefinitions, onSortChanged, sortConfig, updateFilter, filterSelector, calcAllColumnValues]); + + return { createCustomColumn }; } diff --git a/src/components/spreadsheet/table-wrapper.jsx b/src/components/spreadsheet/table-wrapper.jsx index 223d207155..aa63c65d51 100644 --- a/src/components/spreadsheet/table-wrapper.jsx +++ b/src/components/spreadsheet/table-wrapper.jsx @@ -78,8 +78,8 @@ import { useAgGridSort } from 'hooks/use-aggrid-sort'; import { setSpreadsheetFilter } from 'redux/actions'; import { useLocalizedCountries } from 'components/utils/localized-countries-hook'; import { SPREADSHEET_SORT_STORE, SPREADSHEET_STORE_FIELD } from 'utils/store-sort-filter-fields'; -import CustomColumnsConfig from './custom-columns/columns-config-custom'; import { useCustomColumn } from './custom-columns/use-custom-column'; +import CustomColumnsConfig from './custom-columns/custom-columns-config'; const useEditBuffer = () => { //the data is feeded and read during the edition validation process so we don't need to rerender after a call to one of available methods thus useRef is more suited @@ -167,22 +167,11 @@ const TableWrapper = (props) => { const isLockedColumnNamesEmpty = useMemo(() => lockedColumnsNames.size === 0, [lockedColumnsNames.size]); const [customColumnData, setCustomColumnData] = useState([]); const [mergedColumnData, setMergedColumnData] = useState([]); - const { calcAllColumnValues } = useCustomColumn(tabIndex); + const { createCustomColumn } = useCustomColumn(tabIndex, gridRef); useEffect(() => { - setCustomColumnData( - customColumnsDefinitions.columns.map((colWithFormula, idx, arr) => ({ - coldId: `custom-${tabIndex}-${idx}`, - headerName: colWithFormula.name, - valueGetter: (params) => { - const allValues = calcAllColumnValues(params.data); - return allValues.get(colWithFormula.name); - }, - editable: false, - cellDataType: true, // true<=>auto, infer the data type from the row data ('text', 'number', 'boolean', 'date', 'dateString' or 'object') - })) - ); - }, [tabIndex, customColumnsDefinitions, calcAllColumnValues]); + setCustomColumnData(createCustomColumn()); + }, [tabIndex, customColumnsDefinitions, createCustomColumn]); const globalFilterRef = useRef(); diff --git a/src/components/spreadsheet/utils/config-tables.js b/src/components/spreadsheet/utils/config-tables.js index 633b83e68e..3ea87e9477 100644 --- a/src/components/spreadsheet/utils/config-tables.js +++ b/src/components/spreadsheet/utils/config-tables.js @@ -167,7 +167,7 @@ const countryEnumFilterConfig = { isCountry: true, }; -const defaultNumericFilterConfig = (applyFluxConvention, getFluxConvention) => { +export const defaultNumericFilterConfig = (applyFluxConvention, getFluxConvention) => { return { filter: 'agNumberColumnFilter', agGridFilterParams: { diff --git a/src/components/utils/dnd-table/dnd-table.jsx b/src/components/utils/dnd-table/dnd-table.jsx index 83ec1a74e5..a986a407f2 100644 --- a/src/components/utils/dnd-table/dnd-table.jsx +++ b/src/components/utils/dnd-table/dnd-table.jsx @@ -164,7 +164,6 @@ const DndTable = ({ getPreviousValue ? getPreviousValue(rowIndex, column, arrayFormName, previousValues) : undefined } valueModified={isValueModified ? isValueModified(rowIndex, arrayFormName) : false} - sx={{ width: column?.width }} /> ); } @@ -286,7 +285,7 @@ const DndTable = ({ /> {columnsDefinition.map((column) => ( - + {column.label} {column.extra} diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 9ccf50f79e..79a4facad4 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -277,7 +277,7 @@ import { Node } from 'react-flow-renderer'; import { BUILD_STATUS } from '../components/network/constants'; import { SortConfigType, SortWay } from '../hooks/use-aggrid-sort'; import { StudyDisplayMode } from '../components/network-modification.type'; -import { CustomEntry } from 'components/spreadsheet/custom-columns/custom-columns.types'; +import { CustomEntry } from 'types/custom-columns.types'; export enum NotificationType { STUDY = 'study', diff --git a/src/translations/spreadsheet-en.ts b/src/translations/spreadsheet-en.ts index 1f5b9116e2..f45fbd577a 100644 --- a/src/translations/spreadsheet-en.ts +++ b/src/translations/spreadsheet-en.ts @@ -5,31 +5,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -const spreadsheetFr = { - 'spreadsheet/custom_column/main_button': 'Add customized columns', - 'spreadsheet/custom_column/save': 'Enregistrer', - 'spreadsheet/custom_column/dialog/title': 'Manage formulas', - 'spreadsheet/custom_column/dialog/close_tooltip': - 'Close{isContentModified, select, true { without saving change(s)} other {}}', - 'spreadsheet/custom_column/dialog/add_column': 'Add column', - 'spreadsheet/custom_column/dialog/import_export': 'Import/Export formulas', - 'spreadsheet/custom_column/dialog/import_err': 'Error while importing', - 'spreadsheet/custom_column/import': 'Import', - 'spreadsheet/custom_column/reset': 'Reset', - 'spreadsheet/custom_column/copy': 'Copy to clipboard', +const spreadsheetEn = { + 'spreadsheet/custom_column/main_button': 'Add custom columns', 'spreadsheet/custom_column/paste': 'Paste from clipboard', - 'spreadsheet/custom_column/ok': 'OK', - 'spreadsheet/custom_column/dialog_edit/title_add': 'New Formula', - 'spreadsheet/custom_column/dialog_edit/title_edit': 'Edit formula', - 'spreadsheet/custom_column/dialog_edit/placeholder': 'Enter the formula here', - 'spreadsheet/custom_column/dialog_edit/name': 'Name', - 'spreadsheet/custom_column/dialog_edit/name_description': 'Name of the formula', - 'spreadsheet/custom_column/dialog_edit/name_invalid': 'Invalid formula name', - 'spreadsheet/custom_column/dialog_edit/functions_tooltip': 'List of functions', - 'spreadsheet/custom_column/dialog_edit/submit_error': 'Invalid definition', 'spreadsheet/custom_column/column_name': 'Column name', 'spreadsheet/custom_column/column_content': 'Column content', 'spreadsheet/custom_column/column_content_tooltip': `Column content is described with variable names (in order to reference grid data) and operators provided by MathJS library (in order to transform grid data). Example: maxP - p in order to display active power reserve within the generator spreadsheet`, }; -export default spreadsheetFr; +export default spreadsheetEn; diff --git a/src/translations/spreadsheet-fr.ts b/src/translations/spreadsheet-fr.ts index cbeea669c3..72931e675e 100644 --- a/src/translations/spreadsheet-fr.ts +++ b/src/translations/spreadsheet-fr.ts @@ -7,26 +7,6 @@ const spreadsheetFr = { 'spreadsheet/custom_column/main_button': 'Ajouter colonnes personnalisées', - 'spreadsheet/custom_column/save': 'Enregistrer', - 'spreadsheet/custom_column/dialog/title': 'Manage formulas', - 'spreadsheet/custom_column/dialog/close_tooltip': - 'Close{isContentModified, select, true { without saving change(s)} other {}}', - 'spreadsheet/custom_column/dialog/add_column': 'Add column', - 'spreadsheet/custom_column/dialog/import_export': 'Import/Export formulas', - 'spreadsheet/custom_column/dialog/import_err': 'Error while importing', - 'spreadsheet/custom_column/import': 'Import', - 'spreadsheet/custom_column/reset': 'Reset', - 'spreadsheet/custom_column/copy': 'Copy to clipboard', - 'spreadsheet/custom_column/paste': 'Paste from clipboard', - 'spreadsheet/custom_column/ok': 'OK', - 'spreadsheet/custom_column/dialog_edit/title_add': 'New Formula', - 'spreadsheet/custom_column/dialog_edit/title_edit': 'Edit formula', - 'spreadsheet/custom_column/dialog_edit/placeholder': 'Enter the formula here', - 'spreadsheet/custom_column/dialog_edit/name': 'Name', - 'spreadsheet/custom_column/dialog_edit/name_description': 'Name of the formula', - 'spreadsheet/custom_column/dialog_edit/name_invalid': 'Invalid formula name', - 'spreadsheet/custom_column/dialog_edit/functions_tooltip': 'List of functions', - 'spreadsheet/custom_column/dialog_edit/submit_error': 'Invalid definition', 'spreadsheet/custom_column/column_name': 'Nom colonne', 'spreadsheet/custom_column/column_content': 'Contenu colonne', 'spreadsheet/custom_column/column_content_tooltip': `Le contenu d'une colonne est décrit avec des noms de variables (pour faire référence aux données du réseau) et des opérateurs proposés par la librairie MathJS (pour transformer les données du réseau). Exemple : maxP - p pour afficher la réserve de puissance active dans le tableur des groupes`, diff --git a/src/components/spreadsheet/custom-columns/custom-columns.types.tsx b/src/types/custom-columns.types.tsx similarity index 100% rename from src/components/spreadsheet/custom-columns/custom-columns.types.tsx rename to src/types/custom-columns.types.tsx From d40fca373c03901291d1da62e66851a049f35dc4 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Mon, 30 Sep 2024 18:15:07 +0200 Subject: [PATCH 06/10] Adresssed PR feedback Signed-off-by: TOURI ANIS --- package-lock.json | 85 +++++++++++++++++-- .../custom-columns/custom-columns-config.tsx | 49 ++++------- 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86b3a64c2a..479ffc7313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "eventemitter3": "^5.0.1", "localized-countries": "^2.0.0", "lucene-escape-query": "^1.0.1", - "mathjs": "^13.0.3", + "mathjs": "^13.1.1", "mjolnir.js": "^2.7.1", "mui-nested-menu": "^3.3.0", "notistack": "^3.0.1", @@ -2152,9 +2152,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -8505,6 +8505,18 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/complex.js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", + "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -9205,8 +9217,7 @@ "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "node_modules/deck.gl": { "version": "8.9.35", @@ -9815,6 +9826,11 @@ "node": ">=6" } }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -11363,6 +11379,18 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -12945,6 +12973,11 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==" + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -15195,6 +15228,28 @@ "@math.gl/core": "3.6.3" } }, + "node_modules/mathjs": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.1.1.tgz", + "integrity": "sha512-duaSAy7m4F+QtP1Dyv8MX2XuxcqpNDDlGly0SdVTCqpAmwdOFWilDdQKbLdo9RfD6IDNMOdo9tIsEaTXkconlQ==", + "dependencies": { + "@babel/runtime": "^7.25.4", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^4.3.7", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -17514,6 +17569,11 @@ "loose-envify": "^1.1.0" } }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -18349,6 +18409,11 @@ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -18739,6 +18804,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "engines": { + "node": ">= 18" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx index 25ef01516e..37e0ae0e55 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx @@ -7,22 +7,19 @@ import { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Badge, Box } from '@mui/material'; -import { LoadingButton } from '@mui/lab'; +import { Badge, Button } from '@mui/material'; import { Calculate as CalculateIcon } from '@mui/icons-material'; import { useSelector } from 'react-redux'; import { TABLES_NAMES } from '../utils/config-tables'; import { AppState } from '../../../redux/reducer'; -import CustomColumnDialog from './custom-columns-dialog'; import { useStateBoolean, useStateNumber } from '@gridsuite/commons-ui'; +import CustomColumnDialog from './custom-columns-dialog'; export type CustomColumnsConfigProps = { indexTab: number; }; export default function CustomColumnsConfig({ indexTab }: Readonly) { - const formulaCalculating = useStateBoolean(false); - const formulaError = useStateBoolean(false); const numberColumns = useStateNumber(0); const dialogOpen = useStateBoolean(false); const allDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]]); @@ -31,37 +28,21 @@ export default function CustomColumnsConfig({ indexTab }: Readonly - - - - } - loadingPosition="start" - loading={formulaCalculating.value} - onClick={dialogOpen.setTrue} - > - - {(txt) => ( - - {txt} - - )} - - + ); From 0035c252909c09430d86e5657367dc682909e4c0 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Tue, 1 Oct 2024 09:26:18 +0200 Subject: [PATCH 07/10] fix text format --- .../custom-columns/custom-columns-form.tsx | 3 +-- .../custom-columns/use-custom-column.ts | 22 +++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx index b1f3b5e5a4..df2886b001 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx @@ -32,8 +32,7 @@ export const customColumnFormSchema = yup.object().shape({ [FORMULA]: yup.string().required(), }) ) - .required() - .min(1, 'The array must have at least one item'), + .required(), }); export type CustomColumnForm = yup.InferType; diff --git a/src/components/spreadsheet/custom-columns/use-custom-column.ts b/src/components/spreadsheet/custom-columns/use-custom-column.ts index cbad74231d..fab08dcd1a 100644 --- a/src/components/spreadsheet/custom-columns/use-custom-column.ts +++ b/src/components/spreadsheet/custom-columns/use-custom-column.ts @@ -11,12 +11,11 @@ import { useSelector } from 'react-redux'; import { defaultNumericFilterConfig, TABLES_DEFINITION_INDEXES, TABLES_NAMES } from '../utils/config-tables'; import { makeAgGridCustomHeaderColumn } from 'components/custom-aggrid/custom-aggrid-header-utils'; import { useAgGridSort } from 'hooks/use-aggrid-sort'; -import { SPREADSHEET_SORT_STORE, SPREADSHEET_STORE_FIELD } from 'utils/store-sort-filter-fields'; -import { useAggridLocalRowFilter } from 'hooks/use-aggrid-local-row-filter'; -import { setSpreadsheetFilter } from 'redux/actions'; +import { SPREADSHEET_SORT_STORE } from 'utils/store-sort-filter-fields'; import { FilterParams } from 'components/custom-aggrid/custom-aggrid-header.type'; import { ColumnWithFormula } from 'types/custom-columns.types'; import { createDependencyGraph, topologicalSort } from './custom-columns-utils'; +import { PropertiesCellRenderer } from '../utils/cell-renderers'; export function useCustomColumn(tabIndex: number, gridRef: any) { const customColumnsDefinitions = useSelector( @@ -28,13 +27,6 @@ export function useCustomColumn(tabIndex: number, gridRef: any) { TABLES_DEFINITION_INDEXES.get(tabIndex)!.type as string ); - const { updateFilter, filterSelector } = useAggridLocalRowFilter(gridRef, { - filterType: SPREADSHEET_STORE_FIELD, - filterTab: TABLES_DEFINITION_INDEXES.get(tabIndex)!.type as string, - // @ts-expect-error TODO - filterStoreAction: setSpreadsheetFilter, - }); - const math = useMemo(() => { const instance = create(all, { precision: 10, @@ -122,15 +114,13 @@ export function useCustomColumn(tabIndex: number, gridRef: any) { headerName: colWithFormula.name, field: colWithFormula.name, numeric: true, + cellRenderer: PropertiesCellRenderer, sortProps: { onSortChanged, sortConfig, }, - filterProps: { - updateFilter, - filterSelector, - }, - filterParams: { ...defaultNumericFilterConfig() } as FilterParams, + + filterParams: { ...defaultNumericFilterConfig().customFilterParams } as FilterParams, valueGetter: (params) => { const allValues = calcAllColumnValues(params.data); return allValues.get(colWithFormula.name); @@ -140,7 +130,7 @@ export function useCustomColumn(tabIndex: number, gridRef: any) { suppressMovable: true, }); }); - }, [customColumnsDefinitions, onSortChanged, sortConfig, updateFilter, filterSelector, calcAllColumnValues]); + }, [customColumnsDefinitions, onSortChanged, sortConfig, calcAllColumnValues]); return { createCustomColumn }; } From 0a83c198e0b068aa6a75a414edf0a7494c050813 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Tue, 1 Oct 2024 18:54:59 +0200 Subject: [PATCH 08/10] Adressed PR feedback Signed-off-by: TOURI ANIS --- .../custom-columns/custom-columns-config.tsx | 14 ++++++++++---- .../custom-columns/custom-columns-dialog.tsx | 17 ++++++++++------- .../custom-columns/custom-columns-table.tsx | 4 ++-- ...olumns-utils.tsx => custom-columns-utils.ts} | 0 src/redux/reducer.ts | 3 +-- 5 files changed, 23 insertions(+), 15 deletions(-) rename src/components/spreadsheet/custom-columns/{custom-columns-utils.tsx => custom-columns-utils.ts} (100%) diff --git a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx index 37e0ae0e55..72a0e33e1a 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx @@ -22,11 +22,13 @@ export type CustomColumnsConfigProps = { export default function CustomColumnsConfig({ indexTab }: Readonly) { const numberColumns = useStateNumber(0); const dialogOpen = useStateBoolean(false); - const allDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]]); + const customColumnsDefinitions = useSelector( + (state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]] + ); useEffect(() => { - numberColumns.setValue(allDefinitions.columns.length); - }, [allDefinitions.columns.length, numberColumns]); + numberColumns.setValue(customColumnsDefinitions.columns.length); + }, [customColumnsDefinitions.columns.length, numberColumns]); return ( <> @@ -43,7 +45,11 @@ export default function CustomColumnsConfig({ indexTab }: Readonly - + ); } diff --git a/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx b/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx index 0e635f3aa9..9541d25fff 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-dialog.tsx @@ -23,12 +23,12 @@ import { setCustomColumDefinitions } from 'redux/actions'; import { TABLES_NAMES } from '../utils/config-tables'; import { useDispatch } from 'react-redux'; import { AppDispatch } from 'redux/store'; -import { useSelector } from 'react-redux'; -import { AppState } from 'redux/reducer'; +import { ColumnWithFormula } from 'types/custom-columns.types'; export type CustomColumnDialogProps = { open: UseStateBooleanReturn; indexTab: number; + customColumnsDefinitions: ColumnWithFormula[]; }; const styles = { @@ -41,7 +41,11 @@ const styles = { actionButtons: { display: 'flex', gap: 2, justifyContent: 'end' }, } as const satisfies Record>; -export default function CustomColumnDialog({ open, indexTab }: Readonly) { +export default function CustomColumnDialog({ + open, + indexTab, + customColumnsDefinitions, +}: Readonly) { const formMethods = useForm({ defaultValues: initialCustomColumnForm, resolver: yupResolver(customColumnFormSchema), @@ -49,7 +53,6 @@ export default function CustomColumnDialog({ open, indexTab }: Readonly(); - const columnsDefinitions = useSelector((state: AppState) => state.allCustomColumnsDefinitions); const intl = useIntl(); @@ -64,14 +67,14 @@ export default function CustomColumnDialog({ open, indexTab }: Readonly { - if (open.value && columnsDefinitions[TABLES_NAMES[indexTab]]?.columns.length !== 0) { + if (open.value && customColumnsDefinitions.length !== 0) { reset({ - [TAB_CUSTOM_COLUMN]: [...columnsDefinitions[TABLES_NAMES[indexTab]].columns], + [TAB_CUSTOM_COLUMN]: customColumnsDefinitions, }); } else { reset(initialCustomColumnForm); } - }, [columnsDefinitions, indexTab, open.value, reset]); + }, [customColumnsDefinitions, indexTab, open.value, reset]); return ( diff --git a/src/components/spreadsheet/custom-columns/custom-columns-table.tsx b/src/components/spreadsheet/custom-columns/custom-columns-table.tsx index 633270bca9..15446e173a 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-table.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-table.tsx @@ -57,7 +57,7 @@ export default function CustomColumnTable() { }, [CustomColumnTooltip, intl]); const useTabCustomColumnFieldArrayOutput = useFieldArray({ - name: `${TAB_CUSTOM_COLUMN}`, + name: TAB_CUSTOM_COLUMN, }); const newCustomColumnRowData = useMemo(() => { @@ -69,7 +69,7 @@ export default function CustomColumnTable() { const createCustomColumnRows = () => [newCustomColumnRowData]; return ( ({ ...acc, [columnName]: { columns: [], filter: { formula: '' } } }), + (acc, columnName) => ({ ...acc, [columnName]: { columns: [], filter: { formula: '' } } }), {} as AppState['allCustomColumnsDefinitions'] ), @@ -1605,7 +1605,6 @@ export const reducer = createReducer(initialState, (builder) => { builder.addCase(CUSTOM_COLUMNS_DEFINITIONS, (state, action: CustomColumnsDefinitionsAction) => { state.allCustomColumnsDefinitions[action.table].columns = action.definitions; - //state.allCustomColumnsDefinitions[action.table].filter = action.filter; }); builder.addCase(REPORT_FILTER, (state, action: ReportFilterAction) => { if (action.messageFilter !== undefined) { From d856fc267622b0ad9f3798c47838845159b8eb83 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Wed, 2 Oct 2024 12:44:28 +0200 Subject: [PATCH 09/10] remove cell format and show all column types --- .../spreadsheet/custom-columns/custom-columns-config.tsx | 8 ++++---- .../spreadsheet/custom-columns/custom-columns-form.tsx | 7 ++++++- .../spreadsheet/custom-columns/use-custom-column.ts | 9 +-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx index 72a0e33e1a..d8259c8419 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx @@ -23,12 +23,12 @@ export default function CustomColumnsConfig({ indexTab }: Readonly state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]] + (state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]].columns ); useEffect(() => { - numberColumns.setValue(customColumnsDefinitions.columns.length); - }, [customColumnsDefinitions.columns.length, numberColumns]); + numberColumns.setValue(customColumnsDefinitions.length); + }, [customColumnsDefinitions.length, numberColumns]); return ( <> @@ -48,7 +48,7 @@ export default function CustomColumnsConfig({ indexTab }: Readonly ); diff --git a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx index df2886b001..299312285a 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-form.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-form.tsx @@ -32,7 +32,12 @@ export const customColumnFormSchema = yup.object().shape({ [FORMULA]: yup.string().required(), }) ) - .required(), + .required() + .test('unique-column-names', 'Column names must be unique', function (columns) { + const columnNames = columns.map((col) => col[COLUMN_NAME]); + const uniqueNames = new Set(columnNames); + return uniqueNames.size === columnNames.length; // Checks that each name is unique + }), }); export type CustomColumnForm = yup.InferType; diff --git a/src/components/spreadsheet/custom-columns/use-custom-column.ts b/src/components/spreadsheet/custom-columns/use-custom-column.ts index fab08dcd1a..da09b3ada6 100644 --- a/src/components/spreadsheet/custom-columns/use-custom-column.ts +++ b/src/components/spreadsheet/custom-columns/use-custom-column.ts @@ -8,14 +8,12 @@ import { useMemo, useCallback } from 'react'; import { AppState } from 'redux/reducer'; import { create, all, bignumber } from 'mathjs'; import { useSelector } from 'react-redux'; -import { defaultNumericFilterConfig, TABLES_DEFINITION_INDEXES, TABLES_NAMES } from '../utils/config-tables'; +import { TABLES_DEFINITION_INDEXES, TABLES_NAMES } from '../utils/config-tables'; import { makeAgGridCustomHeaderColumn } from 'components/custom-aggrid/custom-aggrid-header-utils'; import { useAgGridSort } from 'hooks/use-aggrid-sort'; import { SPREADSHEET_SORT_STORE } from 'utils/store-sort-filter-fields'; -import { FilterParams } from 'components/custom-aggrid/custom-aggrid-header.type'; import { ColumnWithFormula } from 'types/custom-columns.types'; import { createDependencyGraph, topologicalSort } from './custom-columns-utils'; -import { PropertiesCellRenderer } from '../utils/cell-renderers'; export function useCustomColumn(tabIndex: number, gridRef: any) { const customColumnsDefinitions = useSelector( @@ -113,20 +111,15 @@ export function useCustomColumn(tabIndex: number, gridRef: any) { return makeAgGridCustomHeaderColumn({ headerName: colWithFormula.name, field: colWithFormula.name, - numeric: true, - cellRenderer: PropertiesCellRenderer, sortProps: { onSortChanged, sortConfig, }, - - filterParams: { ...defaultNumericFilterConfig().customFilterParams } as FilterParams, valueGetter: (params) => { const allValues = calcAllColumnValues(params.data); return allValues.get(colWithFormula.name); }, editable: false, - cellDataType: true, suppressMovable: true, }); }); From f2d1993480325e4812db26e3927867b374838e14 Mon Sep 17 00:00:00 2001 From: TOURI ANIS Date: Wed, 2 Oct 2024 14:35:08 +0200 Subject: [PATCH 10/10] remove useEffect --- .../custom-columns/custom-columns-config.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx index d8259c8419..0a2d5a0611 100644 --- a/src/components/spreadsheet/custom-columns/custom-columns-config.tsx +++ b/src/components/spreadsheet/custom-columns/custom-columns-config.tsx @@ -5,14 +5,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { Badge, Button } from '@mui/material'; import { Calculate as CalculateIcon } from '@mui/icons-material'; import { useSelector } from 'react-redux'; import { TABLES_NAMES } from '../utils/config-tables'; import { AppState } from '../../../redux/reducer'; -import { useStateBoolean, useStateNumber } from '@gridsuite/commons-ui'; +import { useStateBoolean } from '@gridsuite/commons-ui'; import CustomColumnDialog from './custom-columns-dialog'; export type CustomColumnsConfigProps = { @@ -20,16 +19,11 @@ export type CustomColumnsConfigProps = { }; export default function CustomColumnsConfig({ indexTab }: Readonly) { - const numberColumns = useStateNumber(0); const dialogOpen = useStateBoolean(false); const customColumnsDefinitions = useSelector( (state: AppState) => state.allCustomColumnsDefinitions[TABLES_NAMES[indexTab]].columns ); - useEffect(() => { - numberColumns.setValue(customColumnsDefinitions.length); - }, [customColumnsDefinitions.length, numberColumns]); - return ( <>