Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions src/components/customAGGrid/cell-renderers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/**
* Copyright (c) 2023, 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 { Box, Checkbox, Tooltip } from '@mui/material';
import { ReactNode, useEffect, useRef, useState } from 'react';
import { isBlankOrEmpty } from '../../utils/conversionUtils';

Check failure on line 10 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

`../../utils/conversionUtils` import should occur after import of `react-intl`
import { ICellRendererParams } from 'ag-grid-community';
import { CustomCellRendererProps } from 'ag-grid-react';
import { mergeSx, type MuiStyles } from '../../utils/styles';

Check failure on line 13 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

`../../utils/styles` import should occur after import of `react-intl`
import { useIntl } from 'react-intl';

const styles = {
tableCell: (theme) => ({
fontSize: 'small',
cursor: 'inherit',
display: 'flex',
'&:before': {
content: '""',
position: 'absolute',
left: theme.spacing(0.5),
right: theme.spacing(0.5),
bottom: 0,
},
}),
overflow: {
whiteSpace: 'pre',
textOverflow: 'ellipsis',
overflow: 'hidden',
},
numericValue: {
marginLeft: 'inherit',
},
} as const satisfies MuiStyles;

const FORMULA_ERROR_KEY = 'spreadsheet/formula/error';

interface BaseCellRendererProps {
value: string | undefined;
tooltip?: string;
}

export const BooleanCellRenderer = (props: any) => {

Check failure on line 46 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Function component is not a function declaration
const isChecked = props.value;

Check failure on line 47 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Must use destructuring props assignment
return (
<div>
{props.value !== undefined && (

Check failure on line 50 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Must use destructuring props assignment
<Checkbox style={{ padding: 0 }} color="default" checked={isChecked} disableRipple={true} />

Check failure on line 51 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Value must be omitted for boolean attribute `disableRipple`
)}
</div>
);
};

export const BooleanNullableCellRenderer = (props: any) => {

Check failure on line 57 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Function component is not a function declaration
return (
<div>
<Checkbox
style={{ padding: 0 }}
color="default"
checked={props.value === true}

Check failure on line 63 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Must use destructuring props assignment
indeterminate={isBlankOrEmpty(props.value)}

Check failure on line 64 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Must use destructuring props assignment
disableRipple={true}

Check failure on line 65 in src/components/customAGGrid/cell-renderers.tsx

View workflow job for this annotation

GitHub Actions / build / build

Value must be omitted for boolean attribute `disableRipple`
/>
</div>
);
};

const formatNumericCell = (value: number, fractionDigits?: number) => {
if (value === null || isNaN(value)) {
return { value: null };
}
return { value: value.toFixed(fractionDigits ?? 2), tooltip: value?.toString() };
};

const formatCell = (props: any) => {
let value = props?.valueFormatted || props.value;
let tooltipValue = undefined;
// we use valueGetter only if value is not defined
if (!value && props.colDef.valueGetter) {
props.colDef.valueGetter(props);
}
if (value != null && props.colDef.context?.numeric && props.colDef.context?.fractionDigits) {
// only numeric rounded cells have a tooltip (their raw numeric value)
tooltipValue = value;
value = parseFloat(value).toFixed(props.colDef.context.fractionDigits);
}
if (props.colDef.context?.numeric && isNaN(value)) {
value = null;
}
return { value: value, tooltip: tooltipValue };
};

export interface NumericCellRendererProps extends CustomCellRendererProps {
fractionDigits?: number;
}

export const NumericCellRenderer = (props: NumericCellRendererProps) => {
const numericalValue = typeof props.value === 'number' ? props.value : Number.parseFloat(props.value);
const cellValue = formatNumericCell(numericalValue, props.fractionDigits);
return (
<Box sx={mergeSx(styles.tableCell)}>
<Tooltip
disableFocusListener
disableTouchListener
title={cellValue.tooltip ? cellValue.tooltip : cellValue.value?.toString()}
>
<Box sx={styles.overflow}>{cellValue.value}</Box>
</Tooltip>
</Box>
);
};

const BaseCellRenderer = ({ value, tooltip }: BaseCellRendererProps) => (
<Box sx={mergeSx(styles.tableCell)}>
<Tooltip disableFocusListener disableTouchListener title={tooltip || value || ''}>
<Box sx={styles.overflow}>{value}</Box>
</Tooltip>
</Box>
);

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 <BaseCellRenderer value={errorValue} tooltip={errorMessage} />;
};

export const DefaultCellRenderer = (props: CustomCellRendererProps) => {
const cellValue = formatCell(props).value?.toString();
return <BaseCellRenderer value={cellValue} />;
};

export const NetworkModificationNameCellRenderer = (props: CustomCellRendererProps) => {
return (
<Box sx={mergeSx(styles.tableCell)}>
<Tooltip
disableFocusListener
disableTouchListener
title={props.value}
componentsProps={{
tooltip: {
sx: {
maxWidth: 'none',
},
},
}}
>
<Box sx={styles.overflow}>{props.value}</Box>
</Tooltip>
</Box>
);
};

export const MessageLogCellRenderer = ({
param,
highlightColor,
currentHighlightColor,
searchTerm,
currentResultIndex,
searchResults,
}: {
param: ICellRendererParams;
highlightColor?: string;
currentHighlightColor?: string;
searchTerm?: string;
currentResultIndex?: number;
searchResults?: number[];
}) => {
const marginLeft = (param.data?.depth ?? 0) * 2; // add indentation based on depth
const textRef = useRef<HTMLDivElement>(null);
const [isEllipsisActive, setIsEllipsisActive] = useState(false);

const checkEllipsis = () => {
if (textRef.current) {
const zoomLevel = window.devicePixelRatio;
const adjustedScrollWidth = textRef.current.scrollWidth / zoomLevel;
const adjustedClientWidth = textRef.current.clientWidth / zoomLevel;
setIsEllipsisActive(adjustedScrollWidth > adjustedClientWidth);
}
};

useEffect(() => {
checkEllipsis();
const resizeObserver = new ResizeObserver(() => checkEllipsis());
if (textRef.current) {
resizeObserver.observe(textRef.current);
}

return () => {
resizeObserver.disconnect();
};
}, [param.value]);

const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};

const renderHighlightedText = (value: string) => {
if (!searchTerm || searchTerm === '') {
return value;
}

const escapedSearchTerm = escapeRegExp(searchTerm);
const parts = value.split(new RegExp(`(${escapedSearchTerm})`, 'gi'));
return (
<span>
{parts.map((part: string, index: number) =>
part.toLowerCase() === searchTerm.toLowerCase() ? (
<span
key={`${part}-${index}`}
style={{
backgroundColor:
searchResults &&
currentResultIndex !== undefined &&
searchResults[currentResultIndex] === param.node.rowIndex
? currentHighlightColor
: highlightColor,
}}
>
{part}
</span>
) : (
part
)
)}
</span>
);
};

return (
<Box sx={mergeSx(styles.tableCell)}>
<Tooltip disableFocusListener disableTouchListener title={isEllipsisActive ? param.value : ''}>
<Box
ref={textRef}
sx={{
...styles.overflow,
marginLeft,
}}
>
{renderHighlightedText(param.value)}
</Box>
</Tooltip>
</Box>
);
};

export const ContingencyCellRenderer = ({ value }: { value: { cellValue: ReactNode; tooltipValue: ReactNode } }) => {
const { cellValue, tooltipValue } = value ?? {};

if (cellValue == null || tooltipValue == null) {
return null;
}

return (
<Box sx={mergeSx(styles.tableCell)}>
<Tooltip title={<div style={{ whiteSpace: 'pre-line' }}>{tooltipValue}</div>}>
<Box sx={styles.overflow}>{cellValue}</Box>
</Tooltip>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* 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 React, { FunctionComponent, SyntheticEvent, useCallback, useEffect, useState } from 'react';
import { Autocomplete, TextField } from '@mui/material';
import { useIntl } from 'react-intl';
import { useCustomAggridFilter } from './hooks/use-custom-aggrid-filter';
import { isNonEmptyStringOrArray } from '../../../utils/types-utils';
import { CustomAggridFilterParams, FILTER_TEXT_COMPARATORS } from './custom-aggrid-filter.type';

export interface CustomAggridAutocompleteFilterParams extends CustomAggridFilterParams {
getOptionLabel?: (value: string) => string; // Used for translation of enum values in the filter
options?: string[];
}

export const CustomAggridAutocompleteFilter: FunctionComponent<CustomAggridAutocompleteFilterParams> = ({
api,
colId,
filterParams,
getOptionLabel,
options,
}) => {
const intl = useIntl();
const { selectedFilterData, handleChangeFilterValue } = useCustomAggridFilter(api, colId, filterParams);
const [computedFilterOptions, setComputedFilterOptions] = useState<string[]>(options ?? []);

const getUniqueValues = useCallback(() => {
const uniqueValues = new Set<string>();
let allNumbers = true;
api.forEachNode((node) => {
const value = api.getCellValue({
rowNode: node,
colKey: colId,
});
if (value !== undefined && value !== null && value !== '') {
uniqueValues.add(value);
if (allNumbers && isNaN(Number(value))) {
allNumbers = false;
}
}
});
// sort the values if they are all numbers
if (allNumbers) {
return Array.from(uniqueValues).sort((a, b) => Number(a) - Number(b));
}
return Array.from(uniqueValues);
}, [api, colId]);

useEffect(() => {
if (!options) {
setComputedFilterOptions(getUniqueValues());
}
}, [options, getUniqueValues]);

const handleFilterAutoCompleteChange = (_: SyntheticEvent, data: string[]) => {
handleChangeFilterValue({ value: data, type: FILTER_TEXT_COMPARATORS.EQUALS });
};

return (
<Autocomplete
multiple
value={Array.isArray(selectedFilterData) ? selectedFilterData : []}
options={computedFilterOptions}
getOptionLabel={(option) => String(getOptionLabel ? getOptionLabel(option) : option)}
onChange={handleFilterAutoCompleteChange}
size="small"
disableCloseOnSelect
renderInput={(params) => (
<TextField
{...params}
placeholder={
!isNonEmptyStringOrArray(selectedFilterData)
? intl.formatMessage({
id: 'filter.filterOoo',
})
: ''
}
/>
)}
fullWidth
/>
);
};
Loading
Loading