diff --git a/src/components/custom-aggrid/cell-renderers.tsx b/src/components/custom-aggrid/cell-renderers.tsx index 639fc60e6d..12a580e037 100644 --- a/src/components/custom-aggrid/cell-renderers.tsx +++ b/src/components/custom-aggrid/cell-renderers.tsx @@ -11,6 +11,7 @@ import { isBlankOrEmpty } from 'components/utils/validation-functions'; import { ICellRendererParams } from 'ag-grid-community'; import { CustomCellRendererProps } from 'ag-grid-react'; import { mergeSx, type MuiStyles } from '@gridsuite/commons-ui'; +import { useIntl } from 'react-intl'; const styles = { tableCell: (theme) => ({ @@ -35,6 +36,13 @@ const styles = { }, } as const satisfies MuiStyles; +const FORMULA_ERROR_KEY = 'spreadsheet/formula/error'; + +interface BaseCellRendererProps { + value: string | undefined; + tooltip?: string; +} + export const BooleanCellRenderer = (props: any) => { const isChecked = props.value; return ( @@ -105,15 +113,24 @@ export const NumericCellRenderer = (props: NumericCellRendererProps) => { ); }; +const BaseCellRenderer = ({ value, tooltip }: BaseCellRendererProps) => ( + + + {value} + + +); + +export const ErrorCellRenderer = (props: CustomCellRendererProps) => { + const intl = useIntl(); + const errorMessage = intl.formatMessage({ id: props.value?.error }); + const errorValue = intl.formatMessage({ id: FORMULA_ERROR_KEY }); + return ; +}; + export const DefaultCellRenderer = (props: CustomCellRendererProps) => { - const cellValue = formatCell(props); - return ( - - - {cellValue.value?.toString()} - - - ); + const cellValue = formatCell(props).value?.toString(); + return ; }; export const NetworkModificationNameCellRenderer = (props: CustomCellRendererProps) => { diff --git a/src/components/custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type.ts b/src/components/custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type.ts index 0559ee28ef..546a25cb10 100644 --- a/src/components/custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type.ts +++ b/src/components/custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type.ts @@ -12,6 +12,7 @@ import { ColumnMenuProps } from '../../spreadsheet-view/columns/column-menu'; import { SortParams } from '../hooks/use-custom-aggrid-sort'; import { COLUMN_TYPES, CustomCellType } from '../custom-aggrid-header.type'; import type { UUID } from 'node:crypto'; +import { type ValidationError } from '../../spreadsheet-view/columns/utils/formula-validator'; export enum FILTER_DATA_TYPES { TEXT = 'text', @@ -66,8 +67,10 @@ export interface ColumnContext - extends ColDef { + extends ColDef { colId: string; context?: ColumnContext; } diff --git a/src/components/spreadsheet-view/columns/utils/column-mapper.ts b/src/components/spreadsheet-view/columns/utils/column-mapper.ts index 0f38b2f67b..7e1da76d73 100644 --- a/src/components/spreadsheet-view/columns/utils/column-mapper.ts +++ b/src/components/spreadsheet-view/columns/utils/column-mapper.ts @@ -6,7 +6,7 @@ */ import { ColumnMenu } from '../column-menu'; import { COLUMN_TYPES } from '../../../custom-aggrid/custom-aggrid-header.type'; -import { limitedEvaluate } from './math'; +import { limitedEvaluate, MathJsValidationError } from './math'; import { ColDef, ValueGetterParams } from 'ag-grid-community'; import { booleanColumnDefinition, @@ -14,14 +14,18 @@ import { numberColumnDefinition, textColumnDefinition, } from '../common-column-definitions'; -import { validateFormulaResult } from './formula-validator'; +import { isValidationError, validateFormulaResult } from './formula-validator'; import { ColumnDefinition, SpreadsheetTabDefinition } from '../../types/spreadsheet.type'; -import { CustomColDef } from '../../../custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type'; +import { + type CustomAggridValue, + type CustomColDef, +} from '../../../custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type'; import { isCalculationRow } from '../../utils/calculation-utils'; +import { ErrorCellRenderer } from '../../../custom-aggrid/cell-renderers'; const createValueGetter = (colDef: ColumnDefinition) => - (params: ValueGetterParams): boolean | string | number | undefined => { + (params: ValueGetterParams): CustomAggridValue | undefined => { try { // Skip formula processing for pinned rows and use raw value if (isCalculationRow(params.node?.data?.rowType)) { @@ -34,13 +38,11 @@ const createValueGetter = }); const escapedFormula = colDef.formula.replace(/\\/g, '\\\\'); const result = limitedEvaluate(escapedFormula, scope); - const validation = validateFormulaResult(result, colDef.type); - - if (!validation.isValid) { - return undefined; - } - return result; + return validateFormulaResult(result, colDef.type); } catch (e) { + if (e instanceof MathJsValidationError) { + return { error: e.error }; + } return undefined; } }; @@ -82,6 +84,8 @@ export const mapColumns = (tableDefinition: SpreadsheetTabDefinition) => }, }, valueGetter: createValueGetter(colDef), + cellRendererSelector: (params) => + isValidationError(params.value) ? { component: ErrorCellRenderer } : undefined, //Returning undefined make it so the originally defined renderer is used hide: !colDef.visible, editable: false, enableCellChangeFlash: true, diff --git a/src/components/spreadsheet-view/columns/utils/formula-validator.ts b/src/components/spreadsheet-view/columns/utils/formula-validator.ts index 1a4ddc7761..f29c09848d 100644 --- a/src/components/spreadsheet-view/columns/utils/formula-validator.ts +++ b/src/components/spreadsheet-view/columns/utils/formula-validator.ts @@ -6,35 +6,37 @@ */ import { COLUMN_TYPES } from 'components/custom-aggrid/custom-aggrid-header.type'; import { MAX_FORMULA_CHARACTERS } from '../../constants'; +import { type CustomAggridValue } from '../../../custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type'; -interface ValidationResult { - isValid: boolean; - error?: string; +export interface ValidationError { + error: string; } -export const validateFormulaResult = (value: any, type: COLUMN_TYPES): ValidationResult => { +export function isValidationError(value: unknown): value is ValidationError { + return !!(typeof value === 'object' && value?.hasOwnProperty('error')); +} + +export const validateFormulaResult = (value: CustomAggridValue, type: COLUMN_TYPES): CustomAggridValue => { switch (type) { case COLUMN_TYPES.NUMBER: - return { - isValid: - (typeof value === 'number' && !isNaN(value)) || - (typeof value !== 'boolean' && !isNaN(Number(value))), - error: 'Formula must evaluate to a number', - }; + return (typeof value === 'number' && !Number.isNaN(value)) || + (typeof value !== 'boolean' && !Number.isNaN(Number(value))) + ? value + : { error: 'spreadsheet/formula/type/number' }; case COLUMN_TYPES.BOOLEAN: - return { - isValid: typeof value === 'boolean', - error: 'Formula must evaluate to a boolean', - }; + return typeof value === 'boolean' + ? value + : { + error: 'spreadsheet/formula/type/boolean', + }; case COLUMN_TYPES.ENUM: - return { - isValid: typeof value === 'string' || typeof value === 'number', - error: 'Formula must evaluate to a string', - }; + return typeof value === 'string' || typeof value === 'number' + ? value + : { error: 'spreadsheet/formula/type/enum' }; case COLUMN_TYPES.TEXT: - return { isValid: true }; // Text accepts any type + return value; // Text accepts any type default: - return { isValid: false, error: 'Unknown column type' }; + return { error: 'spreadsheet/formula/type/unknown' }; } }; diff --git a/src/components/spreadsheet-view/columns/utils/math.ts b/src/components/spreadsheet-view/columns/utils/math.ts index 1d8af99e9c..c71c3a4bf7 100644 --- a/src/components/spreadsheet-view/columns/utils/math.ts +++ b/src/components/spreadsheet-view/columns/utils/math.ts @@ -12,25 +12,42 @@ const instance = create(all); export const limitedEvaluate = instance.evaluate; +// Custom error class for MathJS validation errors +export class MathJsValidationError extends Error { + constructor(public error: string) { + super(error); + this.name = 'MathJsValidationError'; + } +} + instance.import( { - import: function () { - throw new Error('Function import is disabled'); + import: () => { + throw new MathJsValidationError('spreadsheet/formula/import/disabled'); + }, + createUnit: () => { + throw new MathJsValidationError('spreadsheet/formula/createUnit/disabled'); + }, + evaluate: () => { + throw new MathJsValidationError('spreadsheet/formula/evaluate/disabled'); + }, + parse: () => { + throw new MathJsValidationError('spreadsheet/formula/parse/disabled'); }, - createUnit: function () { - throw new Error('Function createUnit is disabled'); + simplify: () => { + throw new MathJsValidationError('spreadsheet/formula/simplify/disabled'); }, - evaluate: function () { - throw new Error('Function evaluate is disabled'); + derivative: () => { + throw new MathJsValidationError('spreadsheet/formula/derivative/disabled'); }, - parse: function () { - throw new Error('Function parse is disabled'); + compile: () => { + throw new MathJsValidationError('spreadsheet/formula/compile/disabled'); }, - simplify: function () { - throw new Error('Function simplify is disabled'); + help: () => { + throw new MathJsValidationError('spreadsheet/formula/help/disabled'); }, - derivative: function () { - throw new Error('Function derivative is disabled'); + parser: () => { + throw new MathJsValidationError('spreadsheet/formula/parser/disabled'); }, equal: function (a: any, b: any) { // == instead of === to be able to compare strings to numbers @@ -45,7 +62,7 @@ instance.import( } else if (Array.isArray(obj)) { return obj.length; } - throw new Error('length() expects an array or object'); + throw new MathJsValidationError('spreadsheet/formula/length/error'); }, unitToKiloUnit, unitToMicroUnit, diff --git a/src/translations/spreadsheet-en.ts b/src/translations/spreadsheet-en.ts index f782fc51e5..ad9faf23e7 100644 --- a/src/translations/spreadsheet-en.ts +++ b/src/translations/spreadsheet-en.ts @@ -149,6 +149,22 @@ const spreadsheetEn = { 'spreadsheet/calculation/min_abbrev': 'Min', 'spreadsheet/calculation/max_abbrev': 'Max', + //Formula errors + 'spreadsheet/formula/error': '#ERROR', + 'spreadsheet/formula/import/disabled': 'Function import is disabled', + 'spreadsheet/formula/createUnit/disabled': 'Function createUnit is disabled', + 'spreadsheet/formula/evaluate/disabled': 'Function evaluate is disabled', + 'spreadsheet/formula/parse/disabled': 'Function parse is disabled', + 'spreadsheet/formula/simplify/disabled': 'Function simplify is disabled', + 'spreadsheet/formula/derivative/disabled': 'Function derivative is disabled', + 'spreadsheet/formula/compile/disabled': 'Function compile is disabled', + 'spreadsheet/formula/help/disabled': 'Function help is disabled', + 'spreadsheet/formula/length/error': 'Function length expects an array or object', + 'spreadsheet/formula/type/number': 'Formula must evaluate to a number', + 'spreadsheet/formula/type/boolean': 'Formula must evaluate to a boolean', + 'spreadsheet/formula/type/enum': 'Formula must evaluate to a string or a number', + 'spreadsheet/formula/type/unknown': 'Unknown column type', + // Column types TEXT: 'Text', NUMBER: 'Number', diff --git a/src/translations/spreadsheet-fr.ts b/src/translations/spreadsheet-fr.ts index 335c1fa99d..22c374cba2 100644 --- a/src/translations/spreadsheet-fr.ts +++ b/src/translations/spreadsheet-fr.ts @@ -154,6 +154,22 @@ const spreadsheetFr = { 'spreadsheet/calculation/min_abbrev': 'Min', 'spreadsheet/calculation/max_abbrev': 'Max', + //Formula errors + 'spreadsheet/formula/error': '#ERREUR', + 'spreadsheet/formula/import/disabled': 'La fonction import est désactivée', + 'spreadsheet/formula/createUnit/disabled': 'La fonction createUnit est désactivée', + 'spreadsheet/formula/evaluate/disabled': 'La fonction evaluate est désactivée', + 'spreadsheet/formula/parse/disabled': 'La fonction parse est désactivée', + 'spreadsheet/formula/simplify/disabled': 'La fonction simplify est désactivée', + 'spreadsheet/formula/derivative/disabled': 'La fonction derivative est désactivée', + 'spreadsheet/formula/compile/disabled': 'La fonction compile est désactivée', + 'spreadsheet/formula/help/disabled': 'La fonction help est désactivée', + 'spreadsheet/formula/length/error': 'La fonction length attend une donnée de type tableau ou objet', + 'spreadsheet/formula/type/number': 'La formule doit exprimer une donnée numérique', + 'spreadsheet/formula/type/boolean': 'La formule doit exprimer une donnée booléenne', + 'spreadsheet/formula/type/enum': 'La formule doit exprimer une donnée textuelle ou numérique', + 'spreadsheet/formula/type/unknown': 'Type de donnée inconnu', + // Column types TEXT: 'Texte', NUMBER: 'Nombre',