From 86b146cd879f256b6405b3e55538456ea129bce6 Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Fri, 24 May 2024 10:13:49 +0200 Subject: [PATCH 1/8] Add support for voltage level properties in expert filters (#449) Signed-off-by: Florent MILLOT --- .../filter/expert/expert-filter-constants.ts | 29 +++++++++++++++++++ .../filter/expert/expert-filter.type.ts | 3 ++ .../react-query-builder/value-editor.tsx | 11 ++++++- .../translations/filter-expert-en.js | 3 ++ .../translations/filter-expert-fr.js | 3 ++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/components/filter/expert/expert-filter-constants.ts b/src/components/filter/expert/expert-filter-constants.ts index ac7ff4d8..b55a16d3 100644 --- a/src/components/filter/expert/expert-filter-constants.ts +++ b/src/components/filter/expert/expert-filter-constants.ts @@ -541,6 +541,27 @@ export const FIELDS_OPTIONS = { valueEditorType: 'select', defaultValue: '', }, + VOLTAGE_LEVEL_PROPERTY: { + name: FieldType.VOLTAGE_LEVEL_PROPERTY, + label: 'voltageLevelProperty', + dataType: DataType.PROPERTY, + valueEditorType: 'select', + defaultValue: '', + }, + VOLTAGE_LEVEL_PROPERTY_1: { + name: FieldType.VOLTAGE_LEVEL_PROPERTY_1, + label: 'voltageLevelProperty1', + dataType: DataType.PROPERTY, + valueEditorType: 'select', + defaultValue: '', + }, + VOLTAGE_LEVEL_PROPERTY_2: { + name: FieldType.VOLTAGE_LEVEL_PROPERTY_2, + label: 'voltageLevelProperty2', + dataType: DataType.PROPERTY, + valueEditorType: 'select', + defaultValue: '', + }, }; export const fields: Record = { @@ -580,6 +601,8 @@ export const fields: Record = { FIELDS_OPTIONS.PROPERTY, FIELDS_OPTIONS.SUBSTATION_PROPERTY_1, FIELDS_OPTIONS.SUBSTATION_PROPERTY_2, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY_1, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY_2, ], TWO_WINDINGS_TRANSFORMER: [ FIELDS_OPTIONS.ID, @@ -610,6 +633,8 @@ export const fields: Record = { FIELDS_OPTIONS.PROPERTY, FIELDS_OPTIONS.SUBSTATION_PROPERTY_1, FIELDS_OPTIONS.SUBSTATION_PROPERTY_2, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY_1, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY_2, ], GENERATOR: [ FIELDS_OPTIONS.ID, @@ -632,6 +657,7 @@ export const fields: Record = { FIELDS_OPTIONS.FORCED_OUTAGE_RATE, FIELDS_OPTIONS.PROPERTY, FIELDS_OPTIONS.SUBSTATION_PROPERTY, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY, ], LOAD: [ FIELDS_OPTIONS.ID, @@ -645,6 +671,7 @@ export const fields: Record = { FIELDS_OPTIONS.Q0, FIELDS_OPTIONS.PROPERTY, FIELDS_OPTIONS.SUBSTATION_PROPERTY, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY, ], BATTERY: [ FIELDS_OPTIONS.ID, @@ -659,6 +686,7 @@ export const fields: Record = { FIELDS_OPTIONS.TARGET_Q, FIELDS_OPTIONS.PROPERTY, FIELDS_OPTIONS.SUBSTATION_PROPERTY, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY, ], SHUNT_COMPENSATOR: [ FIELDS_OPTIONS.ID, @@ -676,5 +704,6 @@ export const fields: Record = { FIELDS_OPTIONS.SWITCHED_ON_SUSCEPTANCE, FIELDS_OPTIONS.PROPERTY, FIELDS_OPTIONS.SUBSTATION_PROPERTY, + FIELDS_OPTIONS.VOLTAGE_LEVEL_PROPERTY, ], }; diff --git a/src/components/filter/expert/expert-filter.type.ts b/src/components/filter/expert/expert-filter.type.ts index 1c1d42e9..db2f21f1 100644 --- a/src/components/filter/expert/expert-filter.type.ts +++ b/src/components/filter/expert/expert-filter.type.ts @@ -91,6 +91,9 @@ export enum FieldType { SUBSTATION_PROPERTY = 'SUBSTATION_PROPERTIES', SUBSTATION_PROPERTY_1 = 'SUBSTATION_PROPERTIES_1', SUBSTATION_PROPERTY_2 = 'SUBSTATION_PROPERTIES_2', + VOLTAGE_LEVEL_PROPERTY = 'VOLTAGE_LEVEL_PROPERTIES', + VOLTAGE_LEVEL_PROPERTY_1 = 'VOLTAGE_LEVEL_PROPERTIES_1', + VOLTAGE_LEVEL_PROPERTY_2 = 'VOLTAGE_LEVEL_PROPERTIES_2', } export enum DataType { diff --git a/src/components/inputs/react-query-builder/value-editor.tsx b/src/components/inputs/react-query-builder/value-editor.tsx index 1b64e186..692cad22 100644 --- a/src/components/inputs/react-query-builder/value-editor.tsx +++ b/src/components/inputs/react-query-builder/value-editor.tsx @@ -129,7 +129,10 @@ const ValueEditor: FunctionComponent = (props) => { props.field === FieldType.PROPERTY || props.field === FieldType.SUBSTATION_PROPERTY || props.field === FieldType.SUBSTATION_PROPERTY_1 || - props.field === FieldType.SUBSTATION_PROPERTY_2 + props.field === FieldType.SUBSTATION_PROPERTY_2 || + props.field === FieldType.VOLTAGE_LEVEL_PROPERTY || + props.field === FieldType.VOLTAGE_LEVEL_PROPERTY_1 || + props.field === FieldType.VOLTAGE_LEVEL_PROPERTY_2 ) { let equipmentType; if ( @@ -138,6 +141,12 @@ const ValueEditor: FunctionComponent = (props) => { props.field === FieldType.SUBSTATION_PROPERTY_2 ) { equipmentType = Substation.type; + } else if ( + props.field === FieldType.VOLTAGE_LEVEL_PROPERTY || + props.field === FieldType.VOLTAGE_LEVEL_PROPERTY_1 || + props.field === FieldType.VOLTAGE_LEVEL_PROPERTY_2 + ) { + equipmentType = VoltageLevel.type; } else { equipmentType = getValues(FieldConstants.EQUIPMENT_TYPE); } diff --git a/src/components/translations/filter-expert-en.js b/src/components/translations/filter-expert-en.js index 19c5ed86..df35c2ed 100644 --- a/src/components/translations/filter-expert-en.js +++ b/src/components/translations/filter-expert-en.js @@ -63,6 +63,9 @@ const filter_expert_en = { substationProperty: 'Substation property', substationProperty1: 'Substation property 1', substationProperty2: 'Substation property 2', + voltageLevelProperty: 'Voltage level property', + voltageLevelProperty1: 'Voltage level property 1', + voltageLevelProperty2: 'Voltage level property 2', ratedVoltage1Or: 'Rated Voltage 1 (kV)', ratedVoltage2Ex: 'Rated Voltage 2 (kV)', }; diff --git a/src/components/translations/filter-expert-fr.js b/src/components/translations/filter-expert-fr.js index ea42893a..0e088cf0 100644 --- a/src/components/translations/filter-expert-fr.js +++ b/src/components/translations/filter-expert-fr.js @@ -63,6 +63,9 @@ const filter_expert_fr = { substationProperty: 'Propriété site', substationProperty1: 'Propriété site Or', substationProperty2: 'Propriété site Ex', + voltageLevelProperty: 'Propriété poste', + voltageLevelProperty1: 'Propriété poste Or', + voltageLevelProperty2: 'Propriété poste Ex', ratedVoltage1Or: "Tension d'enroulement Or (kV)", ratedVoltage2Ex: "Tension d'enroulement Ex (kV)", }; From cf5fafac69bb56b3d4d1ccad24a28b576d97c839 Mon Sep 17 00:00:00 2001 From: Joris Mancini <53527338+TheMaskedTurtle@users.noreply.github.com> Date: Fri, 24 May 2024 12:42:21 +0200 Subject: [PATCH 2/8] refactor(typescript): last chunk of file change (#445) Signed-off-by: Joris Mancini --- package-lock.json | 7 + package.json | 1 + .../AuthenticationRouter.tsx | 4 +- .../ElementSearchDialog/equipment-item.tsx | 9 +- .../ElementSearchDialog/tag-renderer.tsx | 2 +- ...{FlatParameters.jsx => FlatParameters.tsx} | 65 ++-- .../MuiVirtualizedTable/ColumnHeader.jsx | 156 --------- .../MuiVirtualizedTable/ColumnHeader.tsx | 194 ++++++++++++ ...Indexer.jsx => KeyedColumnsRowIndexer.tsx} | 247 +++++++++++---- ...lizedTable.jsx => MuiVirtualizedTable.tsx} | 298 ++++++++++++------ .../OverflowableText/overflowable-text.tsx | 2 +- src/components/ReportViewer/log-table.tsx | 5 +- src/components/TopBar/AboutDialog.tsx | 18 +- .../{TopBar.test.js => TopBar.test.tsx} | 45 ++- .../TopBar/{TopBar.jsx => TopBar.tsx} | 65 +++- src/components/TopBar/{index.js => index.ts} | 0 .../dialogs/modify-element-selection.tsx | 8 +- .../explicit-naming-filter-form.tsx | 6 +- .../select-inputs/countries-input.tsx | 16 +- ...ilter-expert-en.js => filter-expert-en.ts} | 0 ...ilter-expert-fr.js => filter-expert-fr.ts} | 0 ...es-hook.js => localized-countries-hook.ts} | 17 +- src/hooks/predefined-properties-hook.ts | 2 +- src/index.d.ts | 288 ----------------- src/{index.js => index.ts} | 1 + src/redux/actions.ts | 16 +- src/services/study.ts | 2 +- src/services/utils.ts | 2 +- src/utils/{AuthService.js => AuthService.ts} | 134 +++++--- ...{UserManagerMock.js => UserManagerMock.ts} | 18 +- tsconfig.json | 2 - vite.config.mts | 3 +- 32 files changed, 877 insertions(+), 756 deletions(-) rename src/components/FlatParameters/{FlatParameters.jsx => FlatParameters.tsx} (91%) delete mode 100644 src/components/MuiVirtualizedTable/ColumnHeader.jsx create mode 100644 src/components/MuiVirtualizedTable/ColumnHeader.tsx rename src/components/MuiVirtualizedTable/{KeyedColumnsRowIndexer.jsx => KeyedColumnsRowIndexer.tsx} (69%) rename src/components/MuiVirtualizedTable/{MuiVirtualizedTable.jsx => MuiVirtualizedTable.tsx} (78%) rename src/components/TopBar/{TopBar.test.js => TopBar.test.tsx} (60%) rename src/components/TopBar/{TopBar.jsx => TopBar.tsx} (93%) rename src/components/TopBar/{index.js => index.ts} (100%) rename src/components/translations/{filter-expert-en.js => filter-expert-en.ts} (100%) rename src/components/translations/{filter-expert-fr.js => filter-expert-fr.ts} (100%) rename src/hooks/{localized-countries-hook.js => localized-countries-hook.ts} (84%) delete mode 100644 src/index.d.ts rename src/{index.js => index.ts} (99%) rename src/utils/{AuthService.js => AuthService.ts} (84%) rename src/utils/{UserManagerMock.js => UserManagerMock.ts} (92%) diff --git a/package-lock.json b/package-lock.json index f2674650..42a29207 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@types/eslint-config-prettier": "^6.11.3", "@types/json-logic-js": "^2.0.7", "@types/license-checker": "^25.0.6", + "@types/localized-countries": "^2.0.3", "@types/node": "^18.19.31", "@types/prop-types": "^15.7.12", "@types/react": "^18.2.75", @@ -5386,6 +5387,12 @@ "integrity": "sha512-ju/75+YPkNE5vX1iPer+qtI1eI/LqJVYZgOsmSHI1iiEM1bQL5Gh1lEvyjR9T7ZXVE1FwJa2doWJEEmPNwbZkw==", "dev": true }, + "node_modules/@types/localized-countries": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/localized-countries/-/localized-countries-2.0.3.tgz", + "integrity": "sha512-CxGSA2lrVS1bGCcYf+lzZMwB71vRkrVC8xoPBYVm1TkU4abyQLUdpHidgzF9cIQtQzNa7uphOFanSR/tJlgbAQ==", + "dev": true + }, "node_modules/@types/node": { "version": "18.19.31", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", diff --git a/package.json b/package.json index 93cb663f..206c25a4 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@types/eslint-config-prettier": "^6.11.3", "@types/json-logic-js": "^2.0.7", "@types/license-checker": "^25.0.6", + "@types/localized-countries": "^2.0.3", "@types/node": "^18.19.31", "@types/prop-types": "^15.7.12", "@types/react": "^18.2.75", diff --git a/src/components/AuthenticationRouter/AuthenticationRouter.tsx b/src/components/AuthenticationRouter/AuthenticationRouter.tsx index b6cff6a4..4027e819 100644 --- a/src/components/AuthenticationRouter/AuthenticationRouter.tsx +++ b/src/components/AuthenticationRouter/AuthenticationRouter.tsx @@ -38,7 +38,7 @@ export interface AuthenticationRouterProps { showAuthenticationRouterLogin: boolean; dispatch: Dispatch; navigate: () => void; - location: () => void; + location: Location; } const AuthenticationRouter = ({ @@ -119,7 +119,7 @@ const AuthenticationRouter = ({ - logout(location, userManager.instance) + logout(dispatch, userManager.instance) } /> diff --git a/src/components/ElementSearchDialog/equipment-item.tsx b/src/components/ElementSearchDialog/equipment-item.tsx index 5f8200e5..bc6583d3 100644 --- a/src/components/ElementSearchDialog/equipment-item.tsx +++ b/src/components/ElementSearchDialog/equipment-item.tsx @@ -9,7 +9,7 @@ import match from 'autosuggest-highlight/match'; import parse from 'autosuggest-highlight/parse'; import clsx from 'clsx'; import { FormattedMessage } from 'react-intl'; -import { EQUIPMENT_TYPE, EquipmentType } from '../../utils/EquipmentType'; +import { EQUIPMENT_TYPE, EquipmentInfos } from '../../utils/EquipmentType'; import { Box, SxProps } from '@mui/material'; import OverflowableText from '../OverflowableText'; import { mergeSx } from '../../utils/styles'; @@ -17,12 +17,7 @@ import { mergeSx } from '../../utils/styles'; export interface EquipmentItemProps { inputValue: string; suffixRenderer: typeof TagRenderer; - element: { - key: string; - label: string; - type: EquipmentType; - voltageLevelLabel: string; - }; + element: EquipmentInfos; showsJustText: boolean; classes?: { result?: string; diff --git a/src/components/ElementSearchDialog/tag-renderer.tsx b/src/components/ElementSearchDialog/tag-renderer.tsx index 667d96aa..3b703d46 100644 --- a/src/components/ElementSearchDialog/tag-renderer.tsx +++ b/src/components/ElementSearchDialog/tag-renderer.tsx @@ -23,7 +23,7 @@ interface TagRendererProps { }; element: { type: EquipmentType; - voltageLevelLabel: string; + voltageLevelLabel?: string; }; } diff --git a/src/components/FlatParameters/FlatParameters.jsx b/src/components/FlatParameters/FlatParameters.tsx similarity index 91% rename from src/components/FlatParameters/FlatParameters.jsx rename to src/components/FlatParameters/FlatParameters.tsx index 4d7d2041..9d4e8c4e 100644 --- a/src/components/FlatParameters/FlatParameters.jsx +++ b/src/components/FlatParameters/FlatParameters.tsx @@ -17,6 +17,8 @@ import { Select, Switch, TextField, + TextFieldProps, + Theme, Tooltip, Typography, } from '@mui/material'; @@ -29,7 +31,7 @@ const styles = { width: '100%', margin: 0, }, - paramListItem: (theme) => ({ + paramListItem: (theme: Theme) => ({ display: 'flex', justifyContent: 'space-between', gap: theme.spacing(2), @@ -48,7 +50,7 @@ const IntegerRE = /^-?\d*$/; const ListRE = /^\[(.*)]$/; const sepRE = /[, ]/; -export function extractDefault(paramDescription) { +export function extractDefault(paramDescription: any) { const d = paramDescription.defaultValue; if (paramDescription.type === 'BOOLEAN') { return !!d; @@ -64,22 +66,22 @@ export function extractDefault(paramDescription) { return d; } const mo = ListRE.exec(d); - if (mo?.length > 1) { - return mo[1] - .split(sepRE) - .map((s) => s.trim()) - .filter((s) => !!s); + if (mo === null || mo.length <= 1) { + return []; } - return []; + return mo[1] + .split(sepRE) + .map((s) => s.trim()) + .filter((s) => !!s); } return d ?? null; } -function longestCommonPrefix(stringList) { +function longestCommonPrefix(stringList: string[]) { if (!stringList?.length) { return ''; } - let prefix = stringList.reduce((acc, str) => + let prefix = stringList.reduce((acc: string, str: string) => str.length < acc.length ? str : acc ); @@ -91,6 +93,23 @@ function longestCommonPrefix(stringList) { return prefix; } +export interface Parameter { + type: 'BOOLEAN' | 'DOUBLE' | 'INTEGER' | 'STRING_LIST' | 'STRING'; + description?: string; + name: string; + possibleValues: any; + defaultValue: any; +} + +export interface FlatParametersProps { + paramsAsArray: Parameter[]; + initValues: Record; + onChange: (paramName: string, value: unknown, isInEdition: boolean) => void; + variant: TextFieldProps['variant']; + showSeparator: boolean; + selectionWithDialog: (param: Parameter) => boolean; +} + /** * Present a "list" of independently editable parameters according to * their description, as given by paramsAsArray, with current values as in initValues. @@ -108,7 +127,7 @@ export const FlatParameters = ({ variant = 'outlined', showSeparator = false, selectionWithDialog = (param) => false, -}) => { +}: FlatParametersProps) => { const intl = useIntl(); const longestPrefix = longestCommonPrefix(paramsAsArray.map((m) => m.name)); @@ -116,11 +135,11 @@ export const FlatParameters = ({ const prefix = longestPrefix.slice(0, lastDotIndex + 1); const [uncommitted, setUncommitted] = useState(null); - const [inEditionParam, setInEditionParam] = useState(null); + const [inEditionParam, setInEditionParam] = useState(null); const [openSelector, setOpenSelector] = useState(false); const getTranslatedValue = useCallback( - (prefix, value) => { + (prefix: string, value: string) => { return intl.formatMessage({ id: prefix + '.' + value, defaultMessage: value, @@ -130,7 +149,7 @@ export const FlatParameters = ({ ); const getSelectionDialogName = useCallback( - (paramName) => { + (paramName: string) => { const defaultMessage = intl.formatMessage({ id: paramName, defaultMessage: paramName.slice(prefix.length), @@ -143,7 +162,7 @@ export const FlatParameters = ({ [intl, prefix.length] ); const sortPossibleValues = useCallback( - (prefix, values) => { + (prefix: string, values: string[]) => { if (values == null) { return []; } @@ -161,7 +180,7 @@ export const FlatParameters = ({ ); const onFieldChange = useCallback( - (value, param) => { + (value: any, param: Parameter) => { const paramName = param.name; const isInEdition = inEditionParam === paramName; if (isInEdition) { @@ -183,7 +202,7 @@ export const FlatParameters = ({ ); const onUncommitted = useCallback( - (param, inEdit) => { + (param: Parameter, inEdit: boolean) => { if (inEdit) { setInEditionParam(param.name); } else { @@ -204,7 +223,7 @@ export const FlatParameters = ({ [uncommitted, onChange] ); - function mixInitAndDefault(param) { + function mixInitAndDefault(param: Parameter) { if (param.name === inEditionParam && uncommitted !== null) { return uncommitted; } else if (initValues && initValues.hasOwnProperty(param.name)) { @@ -233,11 +252,14 @@ export const FlatParameters = ({ } } - const outputTransformFloatString = (value) => { + const outputTransformFloatString = (value: string | undefined) => { return value?.replace(',', '.') || ''; }; - const getStringListValue = (allValues, selectValues) => { + const getStringListValue = ( + allValues: string[], + selectValues: string[] + ) => { if (!selectValues || !selectValues.length) { return intl.formatMessage({ id: 'flat_parameters/none' }); } @@ -249,7 +271,7 @@ export const FlatParameters = ({ return intl.formatMessage({ id: 'flat_parameters/some' }); }; - const renderField = (param) => { + const renderField = (param: Parameter) => { const fieldValue = mixInitAndDefault(param); switch (param.type) { case 'BOOLEAN': @@ -410,6 +432,7 @@ export const FlatParameters = ({ /> ); } + //@ts-ignore fallthrough is the expected behavior case 'STRING': if (param.possibleValues) { return ( diff --git a/src/components/MuiVirtualizedTable/ColumnHeader.jsx b/src/components/MuiVirtualizedTable/ColumnHeader.jsx deleted file mode 100644 index fff5ebea..00000000 --- a/src/components/MuiVirtualizedTable/ColumnHeader.jsx +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright (c) 2022, 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 { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; - -import { - ArrowDownward as ArrowDownwardIcon, - ArrowUpward as ArrowUpwardIcon, - FilterAltOutlined as FilterAltOutlinedIcon, -} from '@mui/icons-material'; - -import { styled } from '@mui/system'; -import { Box } from '@mui/material'; -import { mergeSx } from '../../utils/styles'; - -const styles = { - label: { - fontWeight: 'bold', - fontSize: '0.875rem', // to mimic TableCellRoot 'binding' - }, - divFlex: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - height: '100%', - }, - divNum: { - flexDirection: 'row-reverse', - textAlign: 'right', - }, - sortDiv: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }, - sortButton: { - fill: 'currentcolor', - }, - filterButton: { - stroke: 'currentcolor', - }, - filterTooLossy: (theme) => ({ - stroke: theme.palette.secondary.main, - }), - transparent: { - opacity: 0, - }, - hovered: { - opacity: 0.5, - }, -}; - -// Shows an arrow pointing to smaller value when sorting is active. -// signedRank of 0 means no sorting, we only show the arrow on hovering of the header, -// in the same direction as it will get if clicked (once). -// signedRank > 0 means sorted by ascending value from lower indices to higher indices -// so lesser values are at top, so the upward arrow -const SortButton = (props) => { - const sortRank = Math.abs(props.signedRank); - const visibilityStyle = - (!props.signedRank || undefined) && - (props.headerHovered ? styles.hovered : styles.transparent); - return ( - - {props.signedRank >= 0 ? ( - - ) : ( - - )} - {sortRank > 1 && !props.hovered && {sortRank}} - - ); -}; - -const FilterButton = (props) => { - const visibilityStyle = - !props.filterLevel && - (props.headerHovered ? styles.hovered : styles.transparent); - return ( - 1 && styles.filterTooLossy, - visibilityStyle - )} - /> - ); -}; - -export const ColumnHeader = forwardRef((props, ref) => { - const { - className, - label, - numeric, - sortSignedRank, - filterLevel, - onSortClick, - onFilterClick, - onContextMenu, - style, - } = props; - - const [hovered, setHovered] = useState(); - const onHover = useCallback((evt) => { - setHovered(evt.type === 'mouseenter'); - }, []); - - const topmostDiv = useRef(); - - const handleFilterClick = useMemo(() => { - if (!onFilterClick) { - return undefined; - } - return (evt) => { - onFilterClick(evt, topmostDiv.current); - }; - }, [onFilterClick]); - - return ( - - {/* we cheat here to get the _variable_ height */} - - {label} - - {onSortClick && ( - - )} - {handleFilterClick && ( - - )} - - ); -}); - -export default styled(ColumnHeader)({}); diff --git a/src/components/MuiVirtualizedTable/ColumnHeader.tsx b/src/components/MuiVirtualizedTable/ColumnHeader.tsx new file mode 100644 index 00000000..488648cc --- /dev/null +++ b/src/components/MuiVirtualizedTable/ColumnHeader.tsx @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2022, 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 { + ComponentProps, + forwardRef, + MouseEvent, + ReactNode, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; + +import { + ArrowDownward as ArrowDownwardIcon, + ArrowUpward as ArrowUpwardIcon, + FilterAltOutlined as FilterAltOutlinedIcon, +} from '@mui/icons-material'; + +import { styled } from '@mui/system'; +import { Box, BoxProps, Theme } from '@mui/material'; +import { mergeSx } from '../../utils/styles'; + +const styles = { + label: { + fontWeight: 'bold', + fontSize: '0.875rem', // to mimic TableCellRoot 'binding' + }, + divFlex: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + height: '100%', + }, + divNum: { + flexDirection: 'row-reverse', + textAlign: 'right', + }, + sortDiv: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + sortButton: { + fill: 'currentcolor', + }, + filterButton: { + stroke: 'currentcolor', + }, + filterTooLossy: (theme: Theme) => ({ + stroke: theme.palette.secondary.main, + }), + transparent: { + opacity: 0, + }, + hovered: { + opacity: 0.5, + }, +}; + +interface SortButtonProps { + signedRank?: number; + headerHovered: boolean; + hovered?: boolean; + onClick: BoxProps['onClick']; +} + +// Shows an arrow pointing to smaller value when sorting is active. +// signedRank of 0 means no sorting, we only show the arrow on hovering of the header, +// in the same direction as it will get if clicked (once). +// signedRank > 0 means sorted by ascending value from lower indices to higher indices +// so lesser values are at top, so the upward arrow +const SortButton = ({ signedRank = 0, ...props }: SortButtonProps) => { + const sortRank = Math.abs(signedRank); + const visibilityStyle = + (!signedRank || undefined) && + (props.headerHovered ? styles.hovered : styles.transparent); + return ( + + {signedRank >= 0 ? ( + + ) : ( + + )} + {sortRank > 1 && !props.hovered && {sortRank}} + + ); +}; + +interface FilterButtonProps { + filterLevel: number; + headerHovered: boolean; + onClick: ComponentProps['onClick']; +} + +const FilterButton = (props: FilterButtonProps) => { + const visibilityStyle = + !props.filterLevel && + (props.headerHovered ? styles.hovered : styles.transparent); + return ( + 1 && styles.filterTooLossy, + visibilityStyle + )} + /> + ); +}; + +export interface ColumnHeaderProps extends BoxProps { + label: ReactNode; + numeric: boolean; + sortSignedRank: SortButtonProps['signedRank']; + filterLevel: FilterButtonProps['filterLevel']; + onSortClick: SortButtonProps['onClick']; + onFilterClick: FilterButtonProps['onClick']; +} + +export const ColumnHeader = forwardRef( + (props, ref) => { + const { + className, + label, + numeric, + sortSignedRank, + filterLevel, + onSortClick, + onFilterClick, + onContextMenu, + style, + } = props; + + const [hovered, setHovered] = useState(false); + const onHover = useCallback((evt: Event) => { + setHovered(evt.type === 'mouseenter'); + }, []); + + const topmostDiv = useRef(); + + const handleFilterClick = useMemo(() => { + if (!onFilterClick) { + return undefined; + } + return (evt: MouseEvent) => { + onFilterClick(evt); + }; + }, [onFilterClick]); + + return ( + //@ts-ignore it does not let us define Box with onMouseEnter/onMouseLeave attributes with 'div' I think, not sure though + + {/* we cheat here to get the _variable_ height */} + + {label} + + {onSortClick && ( + + )} + {handleFilterClick && ( + + )} + + ); + } +); + +export default styled(ColumnHeader)({}); diff --git a/src/components/MuiVirtualizedTable/KeyedColumnsRowIndexer.jsx b/src/components/MuiVirtualizedTable/KeyedColumnsRowIndexer.tsx similarity index 69% rename from src/components/MuiVirtualizedTable/KeyedColumnsRowIndexer.jsx rename to src/components/MuiVirtualizedTable/KeyedColumnsRowIndexer.tsx index 6f89c15c..a0e8ae01 100644 --- a/src/components/MuiVirtualizedTable/KeyedColumnsRowIndexer.jsx +++ b/src/components/MuiVirtualizedTable/KeyedColumnsRowIndexer.tsx @@ -6,13 +6,14 @@ */ import { equalsArray } from '../../utils/algos'; +import { CustomColumnProps, RowProps } from './MuiVirtualizedTable'; -export const CHANGE_WAYS = { - SIMPLE: 'Simple', - TAIL: 'Tail', - AMEND: 'Amend', - REMOVE: 'Remove', -}; +export enum CHANGE_WAYS { + SIMPLE = 'Simple', + TAIL = 'Tail', + AMEND = 'Amend', + REMOVE = 'Remove', +} /* This is not real code commented const someTypicalColumns = [ @@ -31,12 +32,23 @@ const someTypicalColumns = [ ]; */ +export interface ColStat { + imin?: number | null; + imax?: number | null; + seen?: any; + kept?: any; +} + export const noOpHelper = Object.freeze({ debugName: 'noOp', maintainsStats: false, initStat: () => undefined, - updateStat: (colStat, value) => {}, - accepts: (value, userParams, outerParams) => { + updateStat: (colStat: ColStat, value: number) => {}, + accepts: ( + value: any, + userParams: any[] | undefined, + outerParams: any[] | undefined + ) => { return true; }, }); @@ -47,15 +59,27 @@ const numericHelper = Object.freeze({ initStat: () => { return { imin: null, imax: null }; }, - updateStat: (colStat, value) => { - if (colStat.imin === null || colStat.imin > value) { + updateStat: (colStat: ColStat, value: number) => { + if ( + colStat.imin === undefined || + colStat.imin === null || + colStat.imin > value + ) { colStat.imin = value; } - if (colStat.imax === null || colStat.imax < value) { + if ( + colStat.imax === undefined || + colStat.imax === null || + colStat.imax < value + ) { colStat.imax = value; } }, - accepts: (value, userParams, outerParams) => { + accepts: ( + value: number, + userParams: any[] | undefined, + outerParams: any[] | undefined + ) => { return true; }, }); @@ -66,7 +90,7 @@ export const collectibleHelper = Object.freeze({ initStat: () => { return { seen: {}, kept: {} }; }, - updateStat: (colStat, cellValue, isForKeep) => { + updateStat: (colStat: ColStat, cellValue: number, isForKeep: boolean) => { const m = isForKeep ? colStat.kept : colStat.seen; if (!m[cellValue]) { m[cellValue] = 1; @@ -74,12 +98,16 @@ export const collectibleHelper = Object.freeze({ m[cellValue] += 1; } }, - accepts: (value, userParams, outerParams) => { + accepts: ( + value: number, + userParams: any[] | undefined, + outerParams: any[] | undefined + ) => { return !userParams || userParams.some((v) => v === value); }, }); -export const getHelper = (column) => { +export const getHelper = (column?: CustomColumnProps) => { if (column?.numeric) { return numericHelper; } else if (!column?.nostat) { @@ -89,6 +117,17 @@ export const getHelper = (column) => { } }; +export interface Preferences { + isThreeState: boolean; + singleColumnByDefault: boolean; +} + +export interface FilteredRows { + rowAndOrigIndex: [RowProps, number][]; + colsStats: Record; + rowsCount: number; +} + /** * A rows indexer for MuiVirtualizedTable to delegate to an instance of it * for filtering, grouping and multi-column sorting via @@ -99,11 +138,32 @@ export class KeyedColumnsRowIndexer { return CHANGE_WAYS; } + _versionSetter: ((version: number) => void) | null; + byColFilter: Record< + string, + { userParams?: any[]; outerParams?: any[] } + > | null; + byRowFilter: ((row: RowProps) => boolean) | null; + delegatorCallback: + | (( + instance: KeyedColumnsRowIndexer, + callback: (input: any) => void + ) => void) + | null; + filterVersion: number; + groupingCount: number; + indirectionStatus: string | null; + isThreeState: boolean; + lastUsedRank: number; + singleColumnByDefault: boolean; + sortingState: [string, string | undefined][] | null; + version: number; + constructor( isThreeState = true, singleColumnByDefault = false, delegatorCallback = null, - versionSetter = null + versionSetter: ((version: number) => void) | null = null ) { this._versionSetter = versionSetter; this.version = 0; @@ -138,7 +198,7 @@ export class KeyedColumnsRowIndexer { } if (this.delegatorCallback) { this.indirectionStatus = 'to sort'; - this.delegatorCallback(this, (updated_ok) => { + this.delegatorCallback(this, (updated_ok: boolean) => { this.indirectionStatus = updated_ok ? 'done' : 'no_luck'; }); } @@ -147,7 +207,7 @@ export class KeyedColumnsRowIndexer { } }; - updatePreferences = (preferences) => { + updatePreferences = (preferences: Preferences) => { if ( preferences.isThreeState === this.isThreeState && preferences.singleColumnByDefault === this.singleColumnByDefault @@ -179,13 +239,17 @@ export class KeyedColumnsRowIndexer { // }, colKeyN, ... // } // } - preFilterRowMapping = (columns, rows, rowFilter) => { + preFilterRowMapping = ( + columns: CustomColumnProps[], + rows: RowProps[], + rowFilter: (row: RowProps) => boolean + ): FilteredRows | null => { if (!rows?.length || !columns?.length) { return null; } - const ri = []; - const cs = {}; + const ri: [RowProps, number][] = []; + const cs: Record = {}; for (const col of columns) { const helper = getHelper(col); @@ -198,11 +262,12 @@ export class KeyedColumnsRowIndexer { for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) { const row = rows[rowIdx]; let acceptsRow = true; - let acceptedOnRow = {}; + let acceptedOnRow: Record = {}; for (let colIdx = 0; colIdx < columns.length; colIdx++) { const col = columns[colIdx]; const helper = getHelper(col); const colKey = col.dataKey; + // @ts-ignore should not access to value directly const cellValue = row[colKey]; helper.updateStat(cs[colKey], cellValue, false); @@ -219,7 +284,7 @@ export class KeyedColumnsRowIndexer { if (helper.maintainsStats && acceptsCell) { acceptedOnRow[colIdx] = cellValue; } - acceptsRow &= acceptsCell; + acceptsRow &&= acceptsCell; } if (acceptsRow && rowFilter) { @@ -231,7 +296,7 @@ export class KeyedColumnsRowIndexer { if (acceptsRow) { for (let [idx, value] of Object.entries(acceptedOnRow)) { - const col = columns[idx]; + const col = columns[idx as unknown as number]; const helper = getHelper(col); helper.updateStat(cs[col.dataKey], value, true); } @@ -244,7 +309,10 @@ export class KeyedColumnsRowIndexer { // Does not mutate any internal // returns an array of indexes in rows given to preFilter - makeGroupAndSortIndirector = (preFilteredRowPairs, columns) => { + makeGroupAndSortIndirector = ( + preFilteredRowPairs: FilteredRows | null, + columns: CustomColumnProps[] + ) => { if (!preFilteredRowPairs) { return null; } @@ -276,7 +344,7 @@ export class KeyedColumnsRowIndexer { }; // returns true if really changed (and calls versionSetter if needed) - updateSortingFromUser = (colKey, change_way) => { + updateSortingFromUser = (colKey: string, change_way: CHANGE_WAYS) => { const keyAndDirections = this.sortingState; if (change_way === CHANGE_WAYS.REMOVE) { @@ -301,51 +369,60 @@ export class KeyedColumnsRowIndexer { if (change_way === CHANGE_WAYS.SIMPLE) { if (wasSignDir < 0 && this.isThreeState) { - if (this.sortingState.length === 1) { + if (this.sortingState?.length === 1) { this.sortingState = null; } else { - this.sortingState.splice(wasAtIdx, 1); + this.sortingState?.splice(wasAtIdx, 1); } } else { if (this.singleColumnByDefault || wasAtIdx < 0) { this.sortingState = []; } else { - this.sortingState.splice(wasAtIdx, 1); + this.sortingState?.splice(wasAtIdx, 1); } const nextSign = wasSignDir ? -wasSignDir : 1; - const nextKD = [colKey, canonicalForSign(nextSign)]; - this.sortingState.unshift(nextKD); + const nextKD: [string, string | undefined] = [ + colKey, + canonicalForSign(nextSign), + ]; + this.sortingState?.unshift(nextKD); } } else if (change_way === CHANGE_WAYS.TAIL) { if (wasAtIdx < 0) { - this.sortingState.push([colKey, canonicalForSign(1)]); + this.sortingState?.push([colKey, canonicalForSign(1)]); } else if (wasAtIdx !== keyAndDirections.length - 1) { return false; } else if (!(this.isThreeState && wasSignDir === -1)) { + // @ts-ignore could be null but hard to handle with such accesses this.sortingState[wasAtIdx][1] = canonicalForSign( -wasSignDir ); } else { - this.sortingState.splice(wasAtIdx, 1); + this.sortingState?.splice(wasAtIdx, 1); } } else { // AMEND if (wasAtIdx < 0) { - if (this.lastUsedRank - 1 > this.sortingState.length) { + if ( + this.lastUsedRank - 1 > + //@ts-ignore could be undefined, how to handle this case ? + this.sortingState.length + ) { return false; } else { - this.sortingState.splice(this.lastUsedRank - 1, 0, [ + this.sortingState?.splice(this.lastUsedRank - 1, 0, [ colKey, canonicalForSign(1), ]); } } else if (!(this.isThreeState && wasSignDir === -1)) { + // @ts-ignore could be null but hard to handle with such accesses this.sortingState[wasAtIdx][1] = canonicalForSign( -wasSignDir ); } else { this.lastUsedRank = wasAtIdx + 1; - this.sortingState.splice(wasAtIdx, 1); + this.sortingState?.splice(wasAtIdx, 1); } } } @@ -353,11 +430,11 @@ export class KeyedColumnsRowIndexer { return true; }; - codedRankByColumnIndex = (columns) => { + codedRankByColumnIndex = (columns: CustomColumnProps[]) => { return codedColumnsFromKeyAndDirection(this.sortingState, columns); }; - columnSortingSignedRank = (colKey) => { + columnSortingSignedRank = (colKey: string) => { if (!this?.sortingState?.length) { return 0; } @@ -369,7 +446,7 @@ export class KeyedColumnsRowIndexer { return giveDirSignFor(colSorting[1]) * (idx + 1); }; - highestCodedColumn = (columns) => { + highestCodedColumn = (columns: CustomColumnProps[]) => { if (!this?.sortingState?.length) { return 0; } @@ -382,7 +459,7 @@ export class KeyedColumnsRowIndexer { return giveDirSignFor(colSorting[1]) * (idx + 1); }; - _getColFilterParams = (colKey, isForUser) => { + _getColFilterParams = (colKey: string | null, isForUser: boolean) => { if (!colKey || !this.byColFilter) { return undefined; } @@ -394,7 +471,11 @@ export class KeyedColumnsRowIndexer { return colFilter[isForUser ? 'userParams' : 'outerParams']; }; - _setColFilterParams = (colKey, params, isForUser) => { + _setColFilterParams = ( + colKey: string | null, + params: any[] | null, + isForUser: boolean + ) => { if (!colKey) { if (params) { throw new Error('column key has to be defined'); @@ -408,7 +489,8 @@ export class KeyedColumnsRowIndexer { if (!this.byColFilter) { this.byColFilter = {}; } - let colFilter = this.byColFilter[colKey]; + let colFilter: { userParams?: any[]; outerParams?: any[] } = + this.byColFilter[colKey]; if (!colFilter) { colFilter = {}; this.byColFilter[colKey] = colFilter; @@ -435,36 +517,37 @@ export class KeyedColumnsRowIndexer { return true; }; - getColFilterOuterParams = (colKey) => { + getColFilterOuterParams = (colKey: string | null) => { return this._getColFilterParams(colKey, false); }; - setColFilterOuterParams = (colKey, outerParams) => { + setColFilterOuterParams = (colKey: string, outerParams: any[]) => { return this._setColFilterParams(colKey, outerParams, false); }; - getColFilterUserParams = (colKey) => { + getColFilterUserParams = (colKey: string | null) => { return this._getColFilterParams(colKey, true); }; - setColFilterUserParams = (colKey, params) => { + setColFilterUserParams = (colKey: string | null, params: any[] | null) => { return this._setColFilterParams(colKey, params, true); }; getUserFiltering = () => { - const ret = {}; + const ret: Record = {}; if (this.byColFilter) { Object.entries(this.byColFilter).forEach(([k, v]) => { if (!v.userParams) { return; } + // @ts-ignore must be an iterator to use spread iterator ret[k] = [...v.userParams]; }); } return ret; }; - updateRowFiltering = (rowFilterFunc) => { + updateRowFiltering = (rowFilterFunc: (row: RowProps) => boolean) => { if (typeof rowFilterFunc !== 'function') { throw new Error('row filter should be a function'); } @@ -473,10 +556,12 @@ export class KeyedColumnsRowIndexer { }; } -const giveDirSignFor = (fuzzySign) => { +const giveDirSignFor = (fuzzySign: number | string | undefined) => { + //@ts-ignore we should check whether it is a string or a number first if (fuzzySign < 0) { return -1; } + //@ts-ignore we should check whether it is a string or a number first if (fuzzySign > 0) { return 1; } @@ -489,7 +574,7 @@ const giveDirSignFor = (fuzzySign) => { return 0; }; -const canonicalForSign = (dirSign) => { +const canonicalForSign = (dirSign: number): string | undefined => { if (dirSign > 0) { return 'asc'; } @@ -499,13 +584,16 @@ const canonicalForSign = (dirSign) => { return undefined; }; -const codedColumnsFromKeyAndDirection = (keyAndDirections, columns) => { +const codedColumnsFromKeyAndDirection = ( + keyAndDirections: [string, string | undefined][] | null, + columns: CustomColumnProps[] +) => { if (!keyAndDirections) { return null; } const ret = []; - const columIndexByKey = {}; + const columIndexByKey: Record = {}; for (let colIdx = 0; colIdx < columns.length; colIdx++) { const col = columns[colIdx]; const colKey = col.dataKey; @@ -527,7 +615,12 @@ const codedColumnsFromKeyAndDirection = (keyAndDirections, columns) => { return ret; }; -const compareValue = (a, b, isNumeric, undefSign = -1) => { +const compareValue = ( + a: number | string | undefined, + b: number | string | undefined, + isNumeric: boolean, + undefSign: number = -1 +) => { if (a === undefined && b === undefined) { return 0; } else { @@ -540,12 +633,12 @@ const compareValue = (a, b, isNumeric, undefSign = -1) => { } } if (!isNumeric) { - return ('' + a).localeCompare(b); + return ('' + a).localeCompare(b as string); } else { - if (isNaN(a)) { - return isNaN(b) ? 0 : 1; + if (isNaN(a as number)) { + return isNaN(b as number) ? 0 : 1; } - if (isNaN(b)) { + if (isNaN(b as number)) { return -1; } return Math.sign(Number(a) - Number(b)); @@ -553,18 +646,25 @@ const compareValue = (a, b, isNumeric, undefSign = -1) => { }; const makeCompositeComparatorFromCodedColumns = ( - codedColumns, - columns, - rowExtractor + codedColumns: number[] | null, + columns: CustomColumnProps[], + rowExtractor: ( + row: [Record, number][] + ) => Record ) => { - return (row_a_i, row_b_i) => { + return ( + row_a_i: [Record, number][], + row_b_i: [Record, number][] + ) => { const row_a = rowExtractor(row_a_i); const row_b = rowExtractor(row_b_i); + //@ts-ignore codedColumns could be null we should add a check for (const cc of codedColumns) { const i = Math.abs(cc) - 1; const mul = Math.sign(cc); const col = columns[i]; const key = col.dataKey; + //@ts-ignore numeric could be undefined, how to handle this case ? const sgn = compareValue(row_a[key], row_b[key], col.numeric); if (sgn) { return mul * sgn; @@ -574,19 +674,26 @@ const makeCompositeComparatorFromCodedColumns = ( }; }; -const groupRows = (groupingColumnsCount, columns, indexedArray) => { +const groupRows = ( + groupingColumnsCount: number, + columns: CustomColumnProps[], + indexedArray: [Record, number][] +) => { const groupingComparator = makeCompositeComparatorFromCodedColumns( Array(groupingColumnsCount).map((x, i) => i + 1), columns, + //@ts-ignore does not match other pattern (ar) => ar[0] ); + //@ts-ignore does not match other pattern indexedArray.sort(groupingComparator); - const groups = []; + const groups: any = []; let prevSlice = null; - let inBuildGroup = []; + let inBuildGroup: any = []; groups.push(inBuildGroup); for (const p of indexedArray) { + // @ts-ignore could be undefined how to handle this case ? const nextSlice = p[0].slice(0, groupingColumnsCount); if (prevSlice === null || !equalsArray(prevSlice, nextSlice)) { inBuildGroup = []; @@ -599,10 +706,10 @@ const groupRows = (groupingColumnsCount, columns, indexedArray) => { }; const groupAndSort = ( - preFilteredRowPairs, - codedColumns, - groupingColumnsCount, - columns + preFilteredRowPairs: FilteredRows, + codedColumns: number[] | null, + groupingColumnsCount: number, + columns: CustomColumnProps[] ) => { const nothingToDo = !codedColumns && !groupingColumnsCount; @@ -621,10 +728,13 @@ const groupAndSort = ( const sortingComparator = makeCompositeComparatorFromCodedColumns( codedColumns, columns, + //@ts-ignore I don't know how to fix this one (ar) => ar[0] ); + //@ts-ignore I don't know how to fix this one indexedArray.sort(sortingComparator); } else { + //@ts-ignore I don't know how to fix this one const groups = groupRows(groupingColumnsCount, columns, indexedArray); const interGroupSortingComparator = @@ -639,6 +749,7 @@ const groupAndSort = ( makeCompositeComparatorFromCodedColumns( codedColumns, columns, + //@ts-ignore I don't know how to fix this one (ar) => ar[0] ); diff --git a/src/components/MuiVirtualizedTable/MuiVirtualizedTable.jsx b/src/components/MuiVirtualizedTable/MuiVirtualizedTable.tsx similarity index 78% rename from src/components/MuiVirtualizedTable/MuiVirtualizedTable.jsx rename to src/components/MuiVirtualizedTable/MuiVirtualizedTable.tsx index 44babe43..4466c9d0 100644 --- a/src/components/MuiVirtualizedTable/MuiVirtualizedTable.jsx +++ b/src/components/MuiVirtualizedTable/MuiVirtualizedTable.tsx @@ -8,9 +8,16 @@ /** * This class has been taken from 'Virtualized Table' example at https://material-ui.com/components/tables/ */ -import { createRef, PureComponent } from 'react'; +import { + createRef, + PureComponent, + ReactElement, + ReactNode, + MouseEvent, + KeyboardEvent, + MutableRefObject, +} from 'react'; import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; import clsx from 'clsx'; import memoize from 'memoize-one'; import { @@ -18,12 +25,20 @@ import { Chip, IconButton, Popover, + SxProps, TableCell, TextField, } from '@mui/material'; import { styled } from '@mui/system'; import { GetApp as GetAppIcon } from '@mui/icons-material'; -import { AutoSizer, Column, Table } from 'react-virtualized'; +import { + AutoSizer, + Column, + ColumnProps, + RowMouseEventHandlerParams, + Table, + TableCellProps, +} from 'react-virtualized'; import CsvDownloader from 'react-csv-downloader'; import OverflowableText from '../OverflowableText/overflowable-text'; import { @@ -38,10 +53,12 @@ import { } from './KeyedColumnsRowIndexer'; import ColumnHeader from './ColumnHeader'; -function getTextWidth(text) { +function getTextWidth(text: any): number { // re-use canvas object for better performance let canvas = + //@ts-ignore this is questioning getTextWidth.canvas || + //@ts-ignore this is questioning (getTextWidth.canvas = document.createElement('canvas')); let context = canvas.getContext('2d'); // TODO find a better way to find Material UI style @@ -109,18 +126,26 @@ const defaultTooltipSx = { }; //TODO do we need to export this to clients (index.js) ? -export const generateMuiVirtualizedTableClass = (className) => +export const generateMuiVirtualizedTableClass = (className: string) => `MuiVirtualizedTable-${className}`; const composeClasses = makeComposeClasses(generateMuiVirtualizedTableClass); -const AmongChooser = (props) => { +interface AmongChooserProps { + options: T[]; + value?: T[]; + setValue: (values: T[]) => void; + onDropDownVisibility: (visibility: boolean) => void; + id: string; +} + +const AmongChooser = (props: AmongChooserProps) => { const { options, value, setValue, id, onDropDownVisibility } = props; return ( { setValue(newVal); @@ -146,14 +171,20 @@ const AmongChooser = (props) => { ); }; -function makeIndexRecord(viewIndexToModel, rows) { +function makeIndexRecord( + viewIndexToModel: number[] | null, + rows: Record +): { + viewIndexToModel: number[] | null; + rowGetter: (index: number) => RowProps; +} { return { viewIndexToModel, rowGetter: !viewIndexToModel - ? (viewIndex) => rows[viewIndex] - : (viewIndex) => { + ? (viewIndex: number) => rows[viewIndex] + : (viewIndex: number) => { if (viewIndex >= viewIndexToModel.length || viewIndex < 0) { - return {}; + return {} as RowProps; } const modelIndex = viewIndexToModel[viewIndex]; return rows[modelIndex]; @@ -161,7 +192,26 @@ function makeIndexRecord(viewIndexToModel, rows) { }; } -const initIndexer = (props, oldProps, versionSetter) => { +export interface CustomColumnProps extends ColumnProps { + sortable?: boolean; + numeric?: boolean; + indexer?: KeyedColumnsRowIndexer; + label: string; + clickable?: boolean; + fractionDigits?: number; + unit?: number; + extra?: ReactElement; + nostat?: boolean; +} + +export interface RowProps { + notClickable?: boolean; +} + +const initIndexer = ( + props: CustomColumnProps, + versionSetter: (version: number) => void +) => { if (!props.sortable) { return null; } @@ -193,11 +243,14 @@ const reorderIndex = memoize( columns, filterFromProps, sortFromProps - ) => { + ): { + viewIndexToModel: number[] | null; + rowGetter: ((index: number) => RowProps) | ((index: number) => number); + } => { if (!rows) { return { viewIndexToModel: [], - rowGetter: (viewIndex) => viewIndex, + rowGetter: (viewIndex: number) => viewIndex, }; } @@ -241,9 +294,9 @@ const reorderIndex = memoize( } if (filterFromProps) { const viewIndexToModel = rows - .map((r, i) => [r, i]) - .filter(([r, idx]) => filterFromProps(r)) - .map(([r, j]) => j); + .map((r: unknown, i: number) => [r, i]) + .filter(([r, _]: [unknown, number]) => filterFromProps(r)) + .map(([_, j]: [unknown, number]) => j); return makeIndexRecord(viewIndexToModel, rows); } @@ -251,7 +304,39 @@ const reorderIndex = memoize( } ); -class MuiVirtualizedTable extends PureComponent { +export interface MuiVirtualizedTableProps extends CustomColumnProps { + headerHeight: number; + columns: CustomColumnProps[]; + defersFilterChanges: (() => void) | null; + rows: RowProps[]; + filter: unknown; + sort: unknown; + classes: Record; + onRowClick?: (event: RowMouseEventHandlerParams) => void; + rowHeight: number; + onCellClick: (row: RowProps, column: ColumnProps) => void; + tooltipSx: SxProps; + name: string; + exportCSVDataKeys: unknown[]; + enableExportCSV: boolean; +} + +export interface MuiVirtualizedTableState { + headerHeight: number; + indexer: KeyedColumnsRowIndexer | null; + indirectionVersion: number; + popoverAnchorEl: Element | null; + popoverColKey: string | null; + deferredFilterChange: null | { + newVal: unknown[] | null; + colKey: string | null; + }; +} + +class MuiVirtualizedTable extends PureComponent< + MuiVirtualizedTableProps, + MuiVirtualizedTableState +> { static defaultProps = { headerHeight: DEFAULT_HEADER_HEIGHT, rowHeight: DEFAULT_ROW_HEIGHT, @@ -259,12 +344,18 @@ class MuiVirtualizedTable extends PureComponent { classes: {}, }; - constructor(props, context) { + headers: MutableRefObject; + observer: IntersectionObserver; + dropDownVisible: boolean; + + constructor(props: MuiVirtualizedTableProps, context: any) { super(props, context); this._computeHeaderSize = this._computeHeaderSize.bind(this); this._registerHeader = this._registerHeader.bind(this); this._registerObserver = this._registerObserver.bind(this); + // we shouldn't use createRef here, just defining an object would be enough + // We have to type RefObject to MutableRefObject to enable mutability, and TS enables that... this.headers = createRef(); this.headers.current = {}; let options = { @@ -276,9 +367,10 @@ class MuiVirtualizedTable extends PureComponent { this._computeHeaderSize, options ); + this.dropDownVisible = false; this.state = { headerHeight: this.props.headerHeight, - indexer: initIndexer(props, null, this.setVersion), + indexer: initIndexer(props, this.setVersion), indirectionVersion: 0, popoverAnchorEl: null, popoverColKey: null, @@ -286,18 +378,18 @@ class MuiVirtualizedTable extends PureComponent { }; } - setVersion = (v) => { + setVersion = (v: number) => { this.setState({ indirectionVersion: v }); }; - componentDidUpdate(oldProps) { + componentDidUpdate(oldProps: MuiVirtualizedTableProps) { if ( oldProps.indexer !== this.props.indexer || oldProps.sortable !== this.props.sortable ) { this.setState((state) => { return { - indexer: initIndexer(this.props, oldProps, this.setVersion), + indexer: initIndexer(this.props, this.setVersion), indirectionVersion: (state?.indirectionVersion ?? 0) + 1, }; }); @@ -316,25 +408,25 @@ class MuiVirtualizedTable extends PureComponent { this.observer.disconnect(); } - _registerHeader(label, header) { - if (header !== null) { + _registerHeader(label: string, header: unknown) { + if (this.headers.current) { this.headers.current[label] = header; } } - _registerObserver(element) { + _registerObserver(element: Element) { if (element !== null) { this.observer.observe(element); } } - computeDataWidth = (text) => { + computeDataWidth = (text: string) => { return getTextWidth(text || '') + 2 * DEFAULT_CELL_PADDING; }; sizes = memoize((columns, rows, rowGetter) => { - let sizes = {}; - columns.forEach((col) => { + let sizes: Record = {}; + columns.forEach((col: CustomColumnProps) => { if (col.width) { sizes[col.dataKey] = col.width; } else { @@ -360,7 +452,7 @@ class MuiVirtualizedTable extends PureComponent { return sizes; }); - openPopover = (popoverTarget, colKey) => { + openPopover = (popoverTarget: Element, colKey: string) => { const col = this.props.columns.find((c) => c.dataKey === colKey); if (getHelper(col) !== collectibleHelper) { return; @@ -373,18 +465,18 @@ class MuiVirtualizedTable extends PureComponent { }); }; - handleKeyDownOnPopover = (evt) => { + handleKeyDownOnPopover = (evt: KeyboardEvent) => { if (evt.key === 'Enter' && !this.dropDownVisible) { this.closePopover(evt, 'enterKeyDown'); } }; - closePopover = (evt, reason) => { + closePopover = (_: KeyboardEvent, reason: string) => { let bumpsVersion = false; if (reason === 'backdropClick' || reason === 'enterKeyDown') { bumpsVersion = this._commitFilterChange(); } - this.setState((state, props) => { + this.setState((state, _) => { return { popoverAnchorEl: null, popoverColKey: null, @@ -397,11 +489,11 @@ class MuiVirtualizedTable extends PureComponent { makeColumnFilterEditor = () => { const colKey = this.state.popoverColKey; - const outerParams = this.state.indexer.getColFilterOuterParams(colKey); + const outerParams = this.state.indexer?.getColFilterOuterParams(colKey); const userParams = !this.props.defersFilterChanges || !this.state.deferredFilterChange - ? this.state.indexer.getColFilterUserParams(colKey) - : this.state.deferredFilterChange.newVal; + ? this.state.indexer?.getColFilterUserParams(colKey) + : this.state.deferredFilterChange.newVal ?? undefined; const prefiltered = preFilterData( this.props.columns, this.props.rows, @@ -414,6 +506,7 @@ class MuiVirtualizedTable extends PureComponent { if (outerParams) { options.push(...outerParams); } + // @ts-ignore colKey could be null, how to handle this ? const colStat = prefiltered?.colsStats?.[colKey]; if (colStat?.seen) { for (const key of Object.getOwnPropertyNames(colStat.seen)) { @@ -424,14 +517,11 @@ class MuiVirtualizedTable extends PureComponent { } options.sort(); - const col = this.props.columns.find((c) => c.dataKey === colKey); - return ( { this.onFilterParamsChange(newVal, colKey); }} @@ -449,7 +539,7 @@ class MuiVirtualizedTable extends PureComponent { if (newVal?.length === 0) { newVal = null; } - if (this.state.indexer.setColFilterUserParams(colKey, newVal)) { + if (this.state.indexer?.setColFilterUserParams(colKey, newVal)) { return true; } } @@ -457,14 +547,14 @@ class MuiVirtualizedTable extends PureComponent { return false; }; - onFilterParamsChange(newVal, colKey) { - const nonEmpty = newVal.length === 0 ? null : newVal; + onFilterParamsChange(newVal: unknown[] | null, colKey: string | null) { + const nonEmpty = newVal?.length === 0 ? null : newVal; if (this.props.defersFilterChanges) { this.setState({ deferredFilterChange: { newVal: newVal, colKey }, }); } else if ( - this.state.indexer.setColFilterUserParams(colKey, nonEmpty) + this.state.indexer?.setColFilterUserParams(colKey, nonEmpty) ) { this.setState({ indirectionVersion: this.state.indirectionVersion + 1, @@ -472,10 +562,11 @@ class MuiVirtualizedTable extends PureComponent { } } - sortClickHandler = (evt, name, columnIndex) => { + sortClickHandler = (evt: MouseEvent, _: unknown, columnIndex: number) => { const colKey = this.props.columns[columnIndex].dataKey; if (evt.altKey) { + //@ts-ignore should be currentTarget maybe ? this.openPopover(evt.target, colKey); return; } @@ -489,27 +580,38 @@ class MuiVirtualizedTable extends PureComponent { way = CHANGE_WAYS.TAIL; } - if (this.state.indexer.updateSortingFromUser(colKey, way)) { + if (this.state.indexer?.updateSortingFromUser(colKey, way)) { this.setState({ indirectionVersion: this.state.indirectionVersion + 1, }); } }; - filterClickHandler = (evt, target, columnIndex) => { + filterClickHandler = ( + _: MouseEvent, + target: Element | undefined, + columnIndex: number + ) => { // ColumnHeader to (header) TableCell - const retargeted = target.parentNode ?? target; + const retargeted = target?.parentNode ?? target; const colKey = this.props.columns[columnIndex].dataKey; + //@ts-ignore still not the good types this.openPopover(retargeted, colKey); }; - sortableHeader = ({ label, columnIndex }) => { + sortableHeader = ({ + label, + columnIndex, + }: { + label: string; + columnIndex: number; + }) => { const { columns } = this.props; const indexer = this.state.indexer; const colKey = columns[columnIndex].dataKey; - const signedRank = indexer.columnSortingSignedRank(colKey); - const userParams = indexer.getColFilterUserParams(colKey); + const signedRank = indexer?.columnSortingSignedRank(colKey); + const userParams = indexer?.getColFilterUserParams(colKey); const numeric = columns[columnIndex].numeric; const prefiltered = preFilterData( @@ -517,7 +619,7 @@ class MuiVirtualizedTable extends PureComponent { this.props.rows, this.props.filter, indexer, - indexer.filterVersion + indexer?.filterVersion ); const colStat = prefiltered?.colsStats?.[colKey]; let filterLevel = 0; @@ -525,7 +627,9 @@ class MuiVirtualizedTable extends PureComponent { filterLevel += 1; if (!colStat?.seen) { filterLevel += 2; - } else if (userParams.filter((v) => !colStat.seen[v]).length) { + } else if ( + userParams.filter((v: string) => !colStat.seen[v]).length + ) { filterLevel += 2; } } @@ -537,7 +641,7 @@ class MuiVirtualizedTable extends PureComponent { const onFilterClick = numeric || this.props.sort || columns[columnIndex].cellRenderer ? undefined - : (ev, retargeted) => { + : (ev: MouseEvent, retargeted?: Element) => { this.filterClickHandler(ev, retargeted, columnIndex); }; return ( @@ -546,8 +650,11 @@ class MuiVirtualizedTable extends PureComponent { ref={(e) => this._registerHeader(label, e)} sortSignedRank={signedRank} filterLevel={filterLevel} - numeric={numeric} - onSortClick={(ev, name) => { + numeric={numeric ?? false} + onSortClick={( + ev: MouseEvent, + name?: Element + ) => { this.sortClickHandler(ev, name, columnIndex); }} onFilterClick={onFilterClick} @@ -555,7 +662,7 @@ class MuiVirtualizedTable extends PureComponent { ); }; - simpleHeaderRenderer = ({ label }) => { + simpleHeaderRenderer = ({ label }: { label: string }) => { return (
{ @@ -567,7 +674,13 @@ class MuiVirtualizedTable extends PureComponent { ); }; - getRowClassName = ({ index, rowGetter }) => { + getRowClassName = ({ + index, + rowGetter, + }: { + index: number; + rowGetter: any; // Should be ((index: number) => RowProps) | ((index: number) => number) but it's not compatible with the code reorderIndex should be fixed to return only (index: number) => RowProps + }) => { const { classes, onRowClick } = this.props; return clsx( composeClasses(classes, cssTableRow), @@ -583,17 +696,18 @@ class MuiVirtualizedTable extends PureComponent { ); }; - onClickableRowClick = (event) => { + onClickableRowClick = (event: RowMouseEventHandlerParams) => { if ( event.rowData?.notClickable !== true || event.event?.shiftKey || event.event?.ctrlKey ) { + //@ts-ignore onRowClick is possibly undefined this.props.onRowClick(event); } }; - cellRenderer = ({ cellData, columnIndex, rowIndex }) => { + cellRenderer = ({ cellData, columnIndex, rowIndex }: TableCellProps) => { const { columns, classes, rowHeight, onCellClick, rows, tooltipSx } = this.props; @@ -644,15 +758,16 @@ class MuiVirtualizedTable extends PureComponent { > ); }; - - getDisplayValue(column, cellData) { - let displayedValue; + // type check should be increased here + getDisplayValue(column: CustomColumnProps, cellData: any) { + let displayedValue: any; if (!column.numeric) { displayedValue = cellData; } else if (isNaN(cellData)) { @@ -681,7 +796,7 @@ class MuiVirtualizedTable extends PureComponent { // for now keep 'historical' use of scrollHeight, // though it can not make a difference from clientHeight, // as overflow-y as no scroll value - const scrollHeights = headers.map((header) => header.scrollHeight); + const scrollHeights = headers.map((header: any) => header.scrollHeight); let headerHeight = Math.max( Math.max(...scrollHeights) + DEFAULT_CELL_PADDING, this.props.headerHeight @@ -694,9 +809,9 @@ class MuiVirtualizedTable extends PureComponent { } } - makeHeaderRenderer(dataKey, columnIndex) { + makeHeaderRenderer(dataKey: string, columnIndex: number) { const { columns, classes } = this.props; - return (headerProps) => { + return (headerProps: any) => { return ( this._registerObserver(e)} + ref={(e: Element) => this._registerObserver(e)} > {this.props.sortable && this.state.indexer ? this.sortableHeader({ @@ -728,7 +843,13 @@ class MuiVirtualizedTable extends PureComponent { }; } - makeSizedTable = (height, width, sizes, reorderedIndex, rowGetter) => { + makeSizedTable = ( + height: number, + width: number, + sizes: Record, + reorderedIndex: number[] | null, + rowGetter: ((index: number) => RowProps) | ((index: number) => number) + ) => { const { sort, ...otherProps } = this.props; return ( @@ -766,6 +887,7 @@ class MuiVirtualizedTable extends PureComponent { cellRenderer={this.cellRenderer} dataKey={dataKey} flexGrow={1} + //@ts-ignore will be overwritten by ...other width={sizes[dataKey]} {...other} /> @@ -800,11 +922,11 @@ class MuiVirtualizedTable extends PureComponent { const csvData = []; for (let index = 0; index < rowsCount; ++index) { - const myobj = {}; - const sortedRow = reorderedIndex.rowGetter(index); + const myobj: Record = {}; + const sortedRow: any = reorderedIndex.rowGetter(index); const exportedKeys = this.props.exportCSVDataKeys; this.props.columns.forEach((col) => { - if (exportedKeys?.find((el) => el === col.dataKey)) { + if (exportedKeys?.find((el: any) => el === col.dataKey)) { myobj[col.dataKey] = sortedRow[col.dataKey]; } }); @@ -815,11 +937,11 @@ class MuiVirtualizedTable extends PureComponent { }; csvHeaders = memoize((columns, exportCSVDataKeys) => { - let tempHeaders = []; - columns.forEach((col) => { + let tempHeaders: { displayName: string; id: string }[] = []; + columns.forEach((col: CustomColumnProps) => { if ( exportCSVDataKeys !== undefined && - exportCSVDataKeys.find((el) => el === col.dataKey) + exportCSVDataKeys.find((el: string) => el === col.dataKey) ) { tempHeaders.push({ displayName: col.label, @@ -919,36 +1041,6 @@ class MuiVirtualizedTable extends PureComponent { } } -MuiVirtualizedTable.propTypes = { - name: PropTypes.string, - classes: PropTypes.object, - rows: PropTypes.array, - columns: PropTypes.arrayOf( - PropTypes.shape({ - dataKey: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - numeric: PropTypes.bool, - width: PropTypes.number, - minWidth: PropTypes.number, - maxWidth: PropTypes.number, - unit: PropTypes.string, - fractionDigits: PropTypes.number, - extra: PropTypes.element, - }) - ).isRequired, - enableExportCSV: PropTypes.bool, - exportCSVDataKeys: PropTypes.array, - sort: PropTypes.func, - sortable: PropTypes.bool, - indexer: PropTypes.object, - headerHeight: PropTypes.number, - onRowClick: PropTypes.func, - onCellClick: PropTypes.func, - rowHeight: PropTypes.number, - filter: PropTypes.func, - tooltipSx: PropTypes.object, -}; - const nestedGlobalSelectorsStyles = toNestedGlobalSelectors( defaultStyles, generateMuiVirtualizedTableClass diff --git a/src/components/OverflowableText/overflowable-text.tsx b/src/components/OverflowableText/overflowable-text.tsx index cbdcb9e6..3d57c32d 100644 --- a/src/components/OverflowableText/overflowable-text.tsx +++ b/src/components/OverflowableText/overflowable-text.tsx @@ -40,7 +40,7 @@ const multilineOverflowStyle = (numberOfLinesToDisplay?: number): SxProps => ({ }); export interface OverflowableTextProps extends BoxProps { - text: ReactElement | string; + text?: ReactElement | string; maxLineCount?: number; tooltipStyle?: Style; tooltipSx?: SxProps; diff --git a/src/components/ReportViewer/log-table.tsx b/src/components/ReportViewer/log-table.tsx index d66f3493..06eda0c0 100644 --- a/src/components/ReportViewer/log-table.tsx +++ b/src/components/ReportViewer/log-table.tsx @@ -11,6 +11,7 @@ import { styled } from '@mui/system'; import MuiVirtualizedTable from '../MuiVirtualizedTable'; import { FilterButton } from './filter-button'; import LogReportItem from './log-report-item'; +import { RowProps } from '../MuiVirtualizedTable/MuiVirtualizedTable'; const SEVERITY_COLUMN_FIXED_WIDTH = 115; @@ -79,6 +80,7 @@ const LogTable = ({ .toUpperCase(), id: 'severity', dataKey: 'severity', + width: SEVERITY_COLUMN_FIXED_WIDTH, maxWidth: SEVERITY_COLUMN_FIXED_WIDTH, minWidth: SEVERITY_COLUMN_FIXED_WIDTH, cellRenderer: severityCellRender, @@ -95,6 +97,7 @@ const LogTable = ({ .toUpperCase(), id: 'message', dataKey: 'message', + width: SEVERITY_COLUMN_FIXED_WIDTH, }, ]; @@ -111,7 +114,7 @@ const LogTable = ({ message: log.getLog(), backgroundColor: log.getColorName(), reportId: log.getReportId(), - }; + } as RowProps; }); }; diff --git a/src/components/TopBar/AboutDialog.tsx b/src/components/TopBar/AboutDialog.tsx index 8fee419a..06e08323 100644 --- a/src/components/TopBar/AboutDialog.tsx +++ b/src/components/TopBar/AboutDialog.tsx @@ -77,7 +77,7 @@ const styles = { }; function getGlobalVersion( - fnPromise: () => Promise, + fnPromise: (() => Promise) | undefined, type: string, setData: (data: string | null) => void, setLoader?: (loader: boolean) => void @@ -137,20 +137,20 @@ function compareModules(c1: ModuleDefinition, c2: ModuleDefinition) { type GridSuiteModule = { name: string; type: ModuleType; - version: string; - gitTag: string; - license: string; + version?: string; + gitTag?: string; + license?: string; }; export interface AboutDialogProps { open: boolean; onClose: () => void; - globalVersionPromise: () => Promise; + globalVersionPromise?: () => Promise; appName: string; - appVersion: string; - appGitTag: string; - appLicense: string; - additionalModulesPromise: () => Promise; + appVersion?: string; + appGitTag?: string; + appLicense?: string; + additionalModulesPromise?: () => Promise; } const AboutDialog = ({ diff --git a/src/components/TopBar/TopBar.test.js b/src/components/TopBar/TopBar.test.tsx similarity index 60% rename from src/components/TopBar/TopBar.test.js rename to src/components/TopBar/TopBar.test.tsx index 4ac6c981..281ab7aa 100644 --- a/src/components/TopBar/TopBar.test.js +++ b/src/components/TopBar/TopBar.test.tsx @@ -5,7 +5,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import expect from 'expect'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { act } from 'react-dom/test-utils'; @@ -18,8 +17,10 @@ import PowsyblLogo from '../images/powsybl_logo.svg?react'; import { red } from '@mui/material/colors'; import { createTheme, ThemeProvider } from '@mui/material'; +import { beforeEach, afterEach, it, expect } from '@jest/globals'; + +let container: Element; -let container = null; beforeEach(() => { // setup a DOM element as a render target container = document.createElement('div'); @@ -28,13 +29,24 @@ beforeEach(() => { afterEach(() => { // cleanup on exiting - container.remove(); - container = null; + container?.remove(); }); const apps = [ - { name: 'App1', url: '/app1', appColor: 'blue', hiddenInAppsMenu: false }, - { name: 'App2', url: '/app2' }, + { + name: 'App1', + url: '/app1', + appColor: 'blue', + hiddenInAppsMenu: false, + resources: [], + }, + { + name: 'App2', + url: '/app2', + appColor: 'green', + resources: [], + hiddenInAppsMenu: true, + }, ]; const theme = createTheme({ @@ -58,7 +70,26 @@ it('renders', () => { onParametersClick={() => {}} onLogoutClick={() => {}} onLogoClick={() => {}} - user={{ profile: { name: 'John Doe' } }} + user={{ + profile: { + name: 'John Doe', + iss: 'issuer', + sub: 'sub', + aud: 'aud', + exp: 213443, + iat: 3214324, + }, + id_token: 'id_token', + access_token: 'access_token', + token_type: 'code', + scope: 'scope', + expires_at: 123343, + scopes: ['code', 'token'], + expired: false, + state: null, + toStorageString: () => 'stored', + expires_in: 1232, + }} appsAndUrls={apps} language={LANG_ENGLISH} onLanguageClick={() => {}} diff --git a/src/components/TopBar/TopBar.jsx b/src/components/TopBar/TopBar.tsx similarity index 93% rename from src/components/TopBar/TopBar.jsx rename to src/components/TopBar/TopBar.tsx index e47e5e6a..b1cf2e5a 100644 --- a/src/components/TopBar/TopBar.jsx +++ b/src/components/TopBar/TopBar.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useMemo, useState } from 'react'; +import { MouseEvent, PropsWithChildren, useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { @@ -20,8 +20,10 @@ import { Menu, MenuItem, MenuList, + MenuProps, Paper, Popper, + Theme, ToggleButton, ToggleButtonGroup, Toolbar, @@ -42,8 +44,11 @@ import { import { styled } from '@mui/system'; import PropTypes from 'prop-types'; -import GridLogo from './GridLogo'; -import AboutDialog from './AboutDialog'; +import GridLogo, { GridLogoProps } from './GridLogo'; +import AboutDialog, { AboutDialogProps } from './AboutDialog'; +import { LogoutProps } from '../Login/Logout'; +import { User } from 'oidc-client'; +import { Metadata } from '../../hooks/predefined-properties-hook'; const styles = { grow: { @@ -58,7 +63,7 @@ const styles = { textDecoration: 'none', color: 'inherit', }, - name: (theme) => ({ + name: (theme: Theme) => ({ backgroundColor: darken(theme.palette.background.paper, 0.1), paddingTop: '10px', borderRadius: '100%', @@ -107,7 +112,7 @@ const styles = { }, }; -const StyledMenu = styled((props) => ( +const StyledMenu = styled((props: MenuProps) => ( & + Omit & + Omit & { + onParametersClick?: () => void; + onLogoClick: GridLogoProps['onClick']; + user: User; + onAboutClick?: () => void; + appsAndUrls: Metadata[]; + onThemeClick?: (theme: GsTheme) => void; + theme?: GsTheme; + onEquipmentLabellingClick?: (toggle: boolean) => void; + equipmentLabelling?: boolean; + onLanguageClick: (value: GsLang) => void; + language: GsLang; + }; + const TopBar = ({ appName, appColor, @@ -170,11 +197,14 @@ const TopBar = ({ equipmentLabelling, onLanguageClick, language, -}) => { - const [anchorElSettingsMenu, setAnchorElSettingsMenu] = useState(null); - const [anchorElAppsMenu, setAnchorElAppsMenu] = useState(null); +}: PropsWithChildren) => { + const [anchorElSettingsMenu, setAnchorElSettingsMenu] = + useState(null); + const [anchorElAppsMenu, setAnchorElAppsMenu] = useState( + null + ); - const handleToggleSettingsMenu = (event) => { + const handleToggleSettingsMenu = (event: MouseEvent) => { setAnchorElSettingsMenu(event.currentTarget); }; @@ -182,7 +212,7 @@ const TopBar = ({ setAnchorElSettingsMenu(null); }; - const handleClickAppsMenu = (event) => { + const handleClickAppsMenu = (event: MouseEvent) => { setAnchorElAppsMenu(event.currentTarget); }; @@ -197,7 +227,7 @@ const TopBar = ({ } }; - const abbreviationFromUserName = (name) => { + const abbreviationFromUserName = (name: string) => { const tab = name.split(' ').map((x) => x.charAt(0)); if (tab.length === 1) { return tab[0]; @@ -206,19 +236,19 @@ const TopBar = ({ } }; - const changeTheme = (event, value) => { + const changeTheme = (_: MouseEvent, value: GsTheme) => { if (onThemeClick && value !== null) { onThemeClick(value); } }; - const changeEquipmentLabelling = (event, value) => { + const changeEquipmentLabelling = (_: MouseEvent, value: boolean) => { if (onEquipmentLabellingClick && value !== null) { onEquipmentLabellingClick(value); } }; - const changeLanguage = (event, value) => { + const changeLanguage = (_: MouseEvent, value: GsLang) => { if (onLanguageClick && value !== null) { onLanguageClick(value); } @@ -226,6 +256,7 @@ const TopBar = ({ const [isAboutDialogOpen, setAboutDialogOpen] = useState(false); const onAboutClicked = () => { + // @ts-ignore should be an Element, or maybe we should use null ? setAnchorElSettingsMenu(false); if (onAboutClick) { onAboutClick(); @@ -247,6 +278,7 @@ const TopBar = ({ ); return ( + //@ts-ignore appBar style is not defined {logo_clickable} @@ -313,7 +345,7 @@ const TopBar = ({ )} {user && ( - + {/* Button width abbreviation and arrow icon */}