diff --git a/.eslintrc.json b/.eslintrc.json index ea1edb9167..d3de1d9d0c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,6 @@ { "root": true, - "extends": ["react-app", "plugin:prettier/recommended"], + "extends": ["plugin:jest-extended/all", "react-app", "plugin:prettier/recommended"], "ignorePatterns": [ // node_modules is implicitly always ignored "build", diff --git a/jest.config.ts b/jest.config.ts index 848bd5302e..5518a9f358 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -16,6 +16,7 @@ const config: Config = { transformIgnorePatterns: ['node_modules/(?!@gridsuite/commons-ui|react-dnd|dnd-core|@react-dnd)'], // transform from ESM moduleDirectories: ['node_modules', 'src'], // to allow absolute path from ./src setupFiles: ['/jest.setup.ts'], + setupFilesAfterEnv: ['jest-extended/all'], }; export default config; diff --git a/package-lock.json b/package-lock.json index 23a619c7d5..20961b6de4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "eslint-config-react-app": "^7.0.1", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest-extended": "^2.4.0", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.34.1", @@ -102,8 +103,10 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-extended": "^4.0.2", "license-checker": "^25.0.1", "prettier": "^2.8.8", + "ts-arithmetic": "^0.1.1", "ts-node": "^10.9.2", "type-fest": "^4.22.1", "typescript": "5.1.6", @@ -10215,6 +10218,155 @@ "node": "*" } }, + "node_modules/eslint-plugin-jest-extended": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-extended/-/eslint-plugin-jest-extended-2.4.0.tgz", + "integrity": "sha512-olotsH4SczpKgH/EPQZVp3jJ+YpMCsO0wNEMIx09C2/5HC0CtPrxSZ4BzPd/iZsD3bIOgO7H9e3KXVojvoMYsw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-jest-extended/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", @@ -13450,6 +13602,27 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-extended": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", + "dev": true, + "dependencies": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -18375,6 +18548,12 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-arithmetic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ts-arithmetic/-/ts-arithmetic-0.1.1.tgz", + "integrity": "sha512-3VqgsRgzaYfj+zKWn+7O66ifHwbOOnT2BoOrHwdEUBz7az0DetoZOS20+juNJh1klgzvWEi2Qxden41pomOUAQ==", + "dev": true + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index e6a116562e..5d4e75a409 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@reduxjs/toolkit": "^2.2.3", "@svgdotjs/svg.js": "^3.2.0", "@svgdotjs/svg.panzoom.js": "^2.1.2", + "@xyflow/react": "^12.3.2", "ag-grid-community": "^31.0.0", "ag-grid-react": "^31.2.0", "cheap-ruler": "^3.0.2", @@ -36,7 +37,6 @@ "react-beautiful-dnd": "^13.1.1", "react-csv-downloader": "^3.1.0", "react-dom": "^18.2.0", - "@xyflow/react": "^12.3.2", "react-grid-layout": "^1.4.4", "react-hook-form": "^7.51.2", "react-intl": "^6.6.4", @@ -101,6 +101,7 @@ "eslint-config-react-app": "^7.0.1", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest-extended": "^2.4.0", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.34.1", @@ -108,8 +109,10 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-extended": "^4.0.2", "license-checker": "^25.0.1", "prettier": "^2.8.8", + "ts-arithmetic": "^0.1.1", "ts-node": "^10.9.2", "type-fest": "^4.22.1", "typescript": "5.1.6", diff --git a/src/components/diagrams/diagram-pane.tsx b/src/components/diagrams/diagram-pane.tsx index 0d26d4372e..61d3f73577 100644 --- a/src/components/diagrams/diagram-pane.tsx +++ b/src/components/diagrams/diagram-pane.tsx @@ -40,7 +40,7 @@ import { isNodeBuilt, isNodeInNotificationList } from '../graph/util/model-funct import { AutoSizer } from 'react-virtualized'; import Diagram from './diagram'; import { SLD_DISPLAY_MODE } from '../network/constants'; -import { useNameOrId } from '../utils/equipmentInfosHandler'; +import useNameOrId from '../utils/use-name-or-id'; import { syncDiagramStateWithSessionStorage } from '../../redux/session-storage/diagram-state'; import SingleLineDiagramContent from './singleLineDiagram/single-line-diagram-content'; import NetworkAreaDiagramContent from './networkAreaDiagram/network-area-diagram-content'; @@ -210,17 +210,15 @@ const useDisplayView = (studyUuid: UUID, currentNode: CurrentTreeNode) => { let nadTitle = ''; let substationsIds: UUID[] = []; svg.additionalMetadata?.voltageLevels - .map((vl: { name: string; substationId: UUID }) => ({ + .map((vl) => ({ name: getNameOrId(vl), substationId: vl.substationId, })) .sort( - ( - vlA: { name: string; substationId: UUID }, - vlB: { name: string; substationId: UUID } - ) => vlA.name.toLowerCase().localeCompare(vlB.name.toLowerCase()) + (vlA, vlB) => + vlA.name?.toLowerCase()?.localeCompare(vlB.name?.toLowerCase() ?? '') ?? -1 ) - .forEach((voltageLevel: { name: string; substationId: UUID }) => { + .forEach((voltageLevel) => { const name = voltageLevel.name; if (name !== null) { nadTitle += (nadTitle !== '' ? ', ' : '') + name; diff --git a/src/components/diagrams/diagram-utils.test.ts b/src/components/diagrams/diagram-utils.test.ts index d02e9b723d..d5966ee5b7 100644 --- a/src/components/diagrams/diagram-utils.test.ts +++ b/src/components/diagrams/diagram-utils.test.ts @@ -7,59 +7,61 @@ import { makeDiagramSorter } from './diagram-utils'; -test('diagram-common.sortDiagrams', () => { - const diagramStates = [{ id: 4 }, { id: 3 }, { id: 2 }, { id: 1 }, { id: 0 }]; +describe('diagram-common.sortDiagrams', () => { + test('sortDiagrams', () => { + const diagramStates = [{ id: 4 }, { id: 3 }, { id: 2 }, { id: 1 }, { id: 0 }]; - const table = [ - {}, - null, - { id: 1, align: 'right' }, - { id: 2, align: 'left' }, - { id: 3, align: 'left' }, - { id: 4, align: 'right' }, - { id: 0, align: 'hello world' }, - ].sort(makeDiagramSorter(diagramStates)); + const table = [ + {}, + null, + { id: 1, align: 'right' }, + { id: 2, align: 'left' }, + { id: 3, align: 'left' }, + { id: 4, align: 'right' }, + { id: 0, align: 'hello world' }, + ].sort(makeDiagramSorter(diagramStates)); - expect(table.length).toBe(7); - expect(table[0]!.align).toBe('left'); - expect(table[0]!.id).toBe(3); - expect(table[1]!.align).toBe('left'); - expect(table[1]!.id).toBe(2); - expect(table[2]!.align).toBe('right'); - expect(table[2]!.id).toBe(4); - expect(table[3]!.align).toBe('right'); - expect(table[3]!.id).toBe(1); - expect(table[4]?.align).not.toBe('left'); - expect(table[4]?.align).not.toBe('right'); - expect(table[5]?.align).not.toBe('left'); - expect(table[5]?.align).not.toBe('right'); - expect(table[6]?.align).not.toBe('left'); - expect(table[6]?.align).not.toBe('right'); -}); + expect(table.length).toBe(7); + expect(table[0]!.align).toBe('left'); + expect(table[0]!.id).toBe(3); + expect(table[1]!.align).toBe('left'); + expect(table[1]!.id).toBe(2); + expect(table[2]!.align).toBe('right'); + expect(table[2]!.id).toBe(4); + expect(table[3]!.align).toBe('right'); + expect(table[3]!.id).toBe(1); + expect(table[4]?.align).not.toBe('left'); + expect(table[4]?.align).not.toBe('right'); + expect(table[5]?.align).not.toBe('left'); + expect(table[5]?.align).not.toBe('right'); + expect(table[6]?.align).not.toBe('left'); + expect(table[6]?.align).not.toBe('right'); + }); -test('diagram-common.sortDiagrams.missingAligns', () => { - const diagramStates = [ - { id: 4 }, // align: undefined => "first undefined" - { id: 3 }, // align: LEFT => "first left" - { id: 2 }, // align: LEFT => "second left" - { id: 1 }, // align: RIGHT => "right" - { id: 0 }, // align: LEFT => "third left" - { id: 5 }, // align: undefined => "second undefined" - ]; + test('sortDiagrams.missingAligns', () => { + const diagramStates = [ + { id: 4 }, // align: undefined => "first undefined" + { id: 3 }, // align: LEFT => "first left" + { id: 2 }, // align: LEFT => "second left" + { id: 1 }, // align: RIGHT => "right" + { id: 0 }, // align: LEFT => "third left" + { id: 5 }, // align: undefined => "second undefined" + ]; - const table = [ - { id: 1, align: 'right', name: 'right' }, - { id: 2, align: 'left', name: 'second left' }, - { id: 3, align: 'left', name: 'first left' }, - { id: 5, name: 'second undefined' }, - { id: 4, name: 'first undefined' }, - { id: 0, align: 'left', name: 'third left' }, - ].sort(makeDiagramSorter(diagramStates)); + const table = [ + { id: 1, align: 'right', name: 'right' }, + { id: 2, align: 'left', name: 'second left' }, + { id: 3, align: 'left', name: 'first left' }, + { id: 5, name: 'second undefined' }, + { id: 4, name: 'first undefined' }, + { id: 0, align: 'left', name: 'third left' }, + ].sort(makeDiagramSorter(diagramStates)); - expect(table[0].name).toBe('first left'); - expect(table[1].name).toBe('second left'); - expect(table[2].name).toBe('third left'); - expect(table[3].name).toBe('right'); - expect(table[4].name).toBe('first undefined'); - expect(table[5].name).toBe('second undefined'); + expect(table[0].name).toBe('first left'); + expect(table[1].name).toBe('second left'); + expect(table[2].name).toBe('third left'); + expect(table[3].name).toBe('right'); + expect(table[4].name).toBe('first undefined'); + expect(table[5].name).toBe('second undefined'); + }); }); diff --git a/src/components/diagrams/singleLineDiagram/single-line-diagram-content.tsx b/src/components/diagrams/singleLineDiagram/single-line-diagram-content.tsx index 93246a99da..302a2c64b6 100644 --- a/src/components/diagrams/singleLineDiagram/single-line-diagram-content.tsx +++ b/src/components/diagrams/singleLineDiagram/single-line-diagram-content.tsx @@ -25,7 +25,7 @@ import BaseEquipmentMenu, { MapEquipment } from '../../menus/base-equipment-menu import withOperatingStatusMenu from '../../menus/operating-status-menu'; import { OnBreakerCallbackType, SingleLineDiagramViewer, SLDMetadata } from '@powsybl/diagram-viewer'; import { isNodeReadOnly } from '../../graph/util/model-functions'; -import { useIsAnyNodeBuilding } from '../../utils/is-any-node-building-hook'; +import useIsAnyNodeBuilding from '../../utils/is-any-node-building-hook'; import Alert from '@mui/material/Alert'; import { useTheme } from '@mui/material/styles'; import { EquipmentType, useSnackMessage } from '@gridsuite/commons-ui'; diff --git a/src/components/dialogs/export-dialog.jsx b/src/components/dialogs/export-dialog.jsx index 14fd9803d2..009ff6a928 100644 --- a/src/components/dialogs/export-dialog.jsx +++ b/src/components/dialogs/export-dialog.jsx @@ -26,8 +26,8 @@ import { getExportUrl } from '../../services/study/network'; import { isBlankOrEmpty } from 'components/utils/validation-functions'; import TextField from '@mui/material/TextField'; import { useSelector } from 'react-redux'; -import { useParameterState } from './parameters/parameters.jsx'; -import { PARAM_DEVELOPER_MODE } from '../../utils/config-params.js'; +import { useParameterState } from './parameters/parameters'; +import { PARAM_DEVELOPER_MODE } from '../../utils/config-params'; const STRING_LIST = 'STRING_LIST'; diff --git a/src/components/dialogs/network-modifications/by-filter/by-assignment/assignment/assignment-form.tsx b/src/components/dialogs/network-modifications/by-filter/by-assignment/assignment/assignment-form.tsx index fd59e945e1..ecf9e5798b 100644 --- a/src/components/dialogs/network-modifications/by-filter/by-assignment/assignment/assignment-form.tsx +++ b/src/components/dialogs/network-modifications/by-filter/by-assignment/assignment/assignment-form.tsx @@ -102,9 +102,9 @@ const AssignmentForm: FC = ({ options={equipmentFields} label={'EditedField'} size={'small'} - inputTransform={(value: any) => equipmentFields.find((option) => option?.id === value) || value} - outputTransform={(option: any) => getIdOrValue(option) ?? null} - getOptionLabel={(option: any) => formatLabelWithUnit(option)} + inputTransform={(value) => equipmentFields.find((option) => option?.id === value) || value} + outputTransform={(option) => getIdOrValue(option) ?? null} + getOptionLabel={(option) => formatLabelWithUnit(option)} isOptionEqualToValue={areIdsEqual} /> ); diff --git a/src/components/dialogs/restore-modification-dialog.tsx b/src/components/dialogs/restore-modification-dialog.tsx index 6c4e9fddab..a7e20d329b 100644 --- a/src/components/dialogs/restore-modification-dialog.tsx +++ b/src/components/dialogs/restore-modification-dialog.tsx @@ -130,11 +130,7 @@ const RestoreModificationDialog = ({ open, onClose, modifToRestore }: RestoreMod getItemLabel={getLabel} onItemClick={(stashedModification) => setSelectedItems((oldCheckedElements) => - toggleElementFromList( - stashedModification, - oldCheckedElements, - (v: NetworkModificationMetadata) => v.uuid - ) + toggleElementFromList(stashedModification, oldCheckedElements, (v) => v.uuid) ) } addSelectAllCheckbox diff --git a/src/components/graph/menus/create-node-menu.tsx b/src/components/graph/menus/create-node-menu.tsx index 242a749262..65cf84312c 100644 --- a/src/components/graph/menus/create-node-menu.tsx +++ b/src/components/graph/menus/create-node-menu.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useState } from 'react'; import Menu from '@mui/material/Menu'; import { useIntl } from 'react-intl'; -import { useIsAnyNodeBuilding } from '../../utils/is-any-node-building-hook'; +import useIsAnyNodeBuilding from '../../utils/is-any-node-building-hook'; import { useSelector } from 'react-redux'; import ChildMenuItem from './create-child-menu-item'; import { NodeInsertModes } from '../nodes/node-insert-modes'; diff --git a/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx b/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx index 4862c9e43c..a45c24ced0 100644 --- a/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx +++ b/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx @@ -13,7 +13,7 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { useParams } from 'react-router-dom'; import DeleteIcon from '@mui/icons-material/Delete'; import IconButton from '@mui/material/IconButton'; -import { useIsAnyNodeBuilding } from '../../../utils/is-any-node-building-hook'; +import useIsAnyNodeBuilding from '../../../utils/is-any-node-building-hook'; import { addNotification, removeNotificationByNode, setModificationsInProgress } from '../../../../redux/actions'; import { EVENT_CRUD_FINISHED, EventCrudType } from 'components/network/constants.type'; import { UUID } from 'crypto'; diff --git a/src/components/graph/menus/network-modification-node-editor.tsx b/src/components/graph/menus/network-modification-node-editor.tsx index df47ea389e..14da5ef208 100644 --- a/src/components/graph/menus/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modification-node-editor.tsx @@ -57,7 +57,7 @@ import { setModificationsInProgress, } from '../../../redux/actions'; import TwoWindingsTransformerModificationDialog from '../../dialogs/network-modifications/two-windings-transformer/modification/two-windings-transformer-modification-dialog'; -import { useIsAnyNodeBuilding } from '../../utils/is-any-node-building-hook'; +import useIsAnyNodeBuilding from '../../utils/is-any-node-building-hook'; import { RestoreFromTrash } from '@mui/icons-material'; import ImportModificationDialog from 'components/dialogs/import-modification-dialog'; diff --git a/src/components/menus/base-equipment-menu.tsx b/src/components/menus/base-equipment-menu.tsx index 58d9ce8167..dad342729b 100644 --- a/src/components/menus/base-equipment-menu.tsx +++ b/src/components/menus/base-equipment-menu.tsx @@ -15,7 +15,7 @@ import DeleteIcon from '@mui/icons-material/Delete'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; -import { useNameOrId } from '../utils/equipmentInfosHandler'; +import useNameOrId from '../utils/use-name-or-id'; import { getCommonEquipmentType } from 'components/diagrams/diagram-common'; import { isNodeReadOnly } from '../graph/util/model-functions'; import { CustomMenuItem, CustomNestedMenuItem } from '../utils/custom-nested-menu'; @@ -237,6 +237,7 @@ const BaseEquipmentMenu = ({ key={equipment.id} equipmentType={equipmentType} equipmentId={equipment.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(equipment)} handleViewInSpreadsheet={handleViewInSpreadsheet} /> @@ -247,6 +248,7 @@ const BaseEquipmentMenu = ({ key={voltageLevel.id} equipmentType={EquipmentType.VOLTAGE_LEVEL} equipmentId={voltageLevel.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(voltageLevel)} handleViewInSpreadsheet={handleViewInSpreadsheet} /> @@ -257,6 +259,7 @@ const BaseEquipmentMenu = ({ key={equipment.id} equipmentType={equipmentType} equipmentId={equipment.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(equipment)} handleDeleteEquipment={handleDeleteEquipment} /> @@ -267,6 +270,7 @@ const BaseEquipmentMenu = ({ key={voltageLevel.id} equipmentType={EquipmentType.VOLTAGE_LEVEL} equipmentId={voltageLevel.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(voltageLevel)} handleDeleteEquipment={handleDeleteEquipment} /> @@ -278,6 +282,7 @@ const BaseEquipmentMenu = ({ key={equipment.id} equipmentType={equipmentType} equipmentId={equipment.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(equipment)} handleOpenModificationDialog={handleOpenModificationDialog} /> @@ -288,6 +293,7 @@ const BaseEquipmentMenu = ({ key={voltageLevel.id} equipmentType={EquipmentType.VOLTAGE_LEVEL} equipmentId={voltageLevel.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(voltageLevel)} handleOpenModificationDialog={handleOpenModificationDialog} /> @@ -308,6 +314,7 @@ const BaseEquipmentMenu = ({ key={equipment.substationId} equipmentType={EquipmentType.SUBSTATION} equipmentId={equipment.substationId} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId({ name: equipment.substationName, id: equipment.substationId, @@ -319,6 +326,7 @@ const BaseEquipmentMenu = ({ key={equipment.id} equipmentType={EquipmentType.VOLTAGE_LEVEL} equipmentId={equipment.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(equipment)} handleViewInSpreadsheet={handleViewInSpreadsheet} /> @@ -329,6 +337,7 @@ const BaseEquipmentMenu = ({ key={equipment.substationId} equipmentType={EquipmentType.SUBSTATION} equipmentId={equipment.substationId} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId({ name: equipment.substationName, id: equipment.substationId, @@ -340,6 +349,7 @@ const BaseEquipmentMenu = ({ key={equipment.id} equipmentType={EquipmentType.VOLTAGE_LEVEL} equipmentId={equipment.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(equipment)} handleDeleteEquipment={handleDeleteEquipment} /> @@ -350,6 +360,7 @@ const BaseEquipmentMenu = ({ key={equipment.substationId} equipmentType={EquipmentType.SUBSTATION} equipmentId={equipment.substationId} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId({ name: equipment.substationName, id: equipment.substationId, @@ -361,6 +372,7 @@ const BaseEquipmentMenu = ({ key={equipment.id} equipmentType={EquipmentType.VOLTAGE_LEVEL} equipmentId={equipment.id} + // @ts-expect-error TS2322: Type string | null is not assignable to type string itemText={getNameOrId(equipment)} handleOpenModificationDialog={handleOpenModificationDialog} /> diff --git a/src/components/menus/bus-menu.tsx b/src/components/menus/bus-menu.tsx index c3c2bdd646..7c46117b80 100644 --- a/src/components/menus/bus-menu.tsx +++ b/src/components/menus/bus-menu.tsx @@ -12,7 +12,7 @@ import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } f import { isNodeBuilt, isNodeReadOnly } from 'components/graph/util/model-functions'; import { useSelector } from 'react-redux'; import { AppState } from 'redux/reducer'; -import { useIsAnyNodeBuilding } from 'components/utils/is-any-node-building-hook'; +import useIsAnyNodeBuilding from '../utils/is-any-node-building-hook'; import { ComputingType } from 'components/computing-status/computing-type'; import { RunningStatus } from 'components/utils/running-status'; import { useParameterState } from '../dialogs/parameters/parameters'; diff --git a/src/components/menus/equipment-menu.tsx b/src/components/menus/equipment-menu.tsx index e4dd24ee67..e57d690a90 100644 --- a/src/components/menus/equipment-menu.tsx +++ b/src/components/menus/equipment-menu.tsx @@ -11,7 +11,7 @@ import { useParameterState } from '../dialogs/parameters/parameters'; import { PARAM_DEVELOPER_MODE } from '../../utils/config-params'; import { getEventType } from '../dialogs/dynamicsimulation/event/model/event.model'; import { useSelector } from 'react-redux'; -import { useIsAnyNodeBuilding } from '../utils/is-any-node-building-hook'; +import useIsAnyNodeBuilding from '../utils/is-any-node-building-hook'; import { isNodeBuilt, isNodeReadOnly } from '../graph/util/model-functions'; import DynamicSimulationEventMenuItem from './dynamic-simulation/dynamic-simulation-event-menu-item'; import { AppState } from 'redux/reducer'; diff --git a/src/components/menus/operating-status-menu.tsx b/src/components/menus/operating-status-menu.tsx index 96cd709bea..5363cddfc4 100644 --- a/src/components/menus/operating-status-menu.tsx +++ b/src/components/menus/operating-status-menu.tsx @@ -19,11 +19,11 @@ import EnergiseOtherSideIcon from '@mui/icons-material/FirstPage'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import { useIntl } from 'react-intl'; -import { useNameOrId } from '../utils/equipmentInfosHandler'; +import useNameOrId from '../utils/use-name-or-id'; import PropTypes from 'prop-types'; import { EquipmentInfos, EquipmentType, OperatingStatus, useSnackMessage } from '@gridsuite/commons-ui'; import { isNodeBuilt, isNodeReadOnly } from '../graph/util/model-functions'; -import { useIsAnyNodeBuilding } from '../utils/is-any-node-building-hook'; +import useIsAnyNodeBuilding from '../utils/is-any-node-building-hook'; import { BRANCH_SIDE } from '../network/constants'; import { EQUIPMENT_INFOS_TYPES } from '../utils/equipment-types'; import { diff --git a/src/components/results/loadflow/load-flow-result-utils.ts b/src/components/results/loadflow/load-flow-result-utils.ts index 7ad2455798..8addf47ba5 100644 --- a/src/components/results/loadflow/load-flow-result-utils.ts +++ b/src/components/results/loadflow/load-flow-result-utils.ts @@ -147,6 +147,7 @@ export const makeData = ( subjectId: overloadedEquipment.subjectId, value: overloadedEquipment.value, actualOverloadDuration: + // @ts-expect-error TS2367: This comparison appears to be unintentional because the types 300 and 2147483647 have no overlap. => never true overloadedEquipment.actualOverloadDuration === UNDEFINED_ACCEPTABLE_DURATION ? null : overloadedEquipment.actualOverloadDuration, diff --git a/src/components/spreadsheet/renderers/enum-cell-renderer.tsx b/src/components/spreadsheet/renderers/enum-cell-renderer.tsx index cd0a9ffd58..f6bc0ff39d 100644 --- a/src/components/spreadsheet/renderers/enum-cell-renderer.tsx +++ b/src/components/spreadsheet/renderers/enum-cell-renderer.tsx @@ -7,7 +7,7 @@ import { getEnumLabelById } from 'components/utils/utils'; import React from 'react'; import { useIntl } from 'react-intl'; -import { EnumOption } from '../../utils/utils-type'; +import EnumOption from '../../utils/EnumOption'; interface EnumCellRendererProps { value: string; diff --git a/src/components/spreadsheet/utils/config-tables.ts b/src/components/spreadsheet/utils/config-tables.ts index e51d2f4626..29f58de88c 100644 --- a/src/components/spreadsheet/utils/config-tables.ts +++ b/src/components/spreadsheet/utils/config-tables.ts @@ -38,7 +38,7 @@ import { NOMINAL_V } from '../../utils/field-constants'; import CountryCellRenderer from '../renderers/country-cell-render'; import EnumCellRenderer from '../renderers/enum-cell-renderer'; import { BooleanFilterValue } from '../../custom-aggrid/custom-aggrid-filters/custom-aggrid-boolean-filter'; -import { EnumOption } from '../../utils/utils-type'; +import EnumOption from '../../utils/EnumOption'; import { CellClassParams, EditableCallbackParams, ValueGetterParams, ValueSetterParams } from 'ag-grid-community'; import { LARGE_COLUMN_WIDTH, MEDIUM_COLUMN_WIDTH, MIN_COLUMN_WIDTH } from './constants'; diff --git a/src/components/spreadsheet/utils/equipment-table-editors.tsx b/src/components/spreadsheet/utils/equipment-table-editors.tsx index 5e5d548be3..5b735bd8d8 100644 --- a/src/components/spreadsheet/utils/equipment-table-editors.tsx +++ b/src/components/spreadsheet/utils/equipment-table-editors.tsx @@ -15,7 +15,7 @@ import { useLocalizedCountries } from 'components/utils/localized-countries-hook import RegulatingTerminalModificationDialog from 'components/dialogs/network-modifications/generator/modification/regulating-terminal-modification-dialog'; import { getTapChangerRegulationTerminalValue } from 'components/utils/utils'; import { ColDef, GridApi } from 'ag-grid-community'; -import { EnumOption } from '../../utils/utils-type'; +import EnumOption from '../../utils/EnumOption'; interface EquipmentTableEditorProps { gridContext: any; diff --git a/src/components/top-bar-equipment-seach-dialog/use-search-matching-equipments.tsx b/src/components/top-bar-equipment-seach-dialog/use-search-matching-equipments.tsx index d18d33221f..c3d9c44079 100644 --- a/src/components/top-bar-equipment-seach-dialog/use-search-matching-equipments.tsx +++ b/src/components/top-bar-equipment-seach-dialog/use-search-matching-equipments.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo } from 'react'; import { Equipment, EquipmentType, getEquipmentsInfosForSearchBar, useElementSearch } from '@gridsuite/commons-ui'; -import { useNameOrId } from '../utils/equipmentInfosHandler'; +import useNameOrId from '../utils/use-name-or-id'; import { searchEquipmentsInfos } from '../../services/study'; import { UUID } from 'crypto'; @@ -41,6 +41,7 @@ export const useSearchMatchingEquipments = (props: UseSearchMatchingEquipmentsPr }); const equipmentsFound = useMemo( + // @ts-expect-error TS2345: manage null string with getNameOrId () => getEquipmentsInfosForSearchBar(elementsFound, getNameOrId), [elementsFound, getNameOrId] ); diff --git a/src/components/utils/utils-type.ts b/src/components/utils/EnumOption.d.ts similarity index 87% rename from src/components/utils/utils-type.ts rename to src/components/utils/EnumOption.d.ts index 6d9ea21549..fdd50b2424 100644 --- a/src/components/utils/utils-type.ts +++ b/src/components/utils/EnumOption.d.ts @@ -1,10 +1,11 @@ -/** +/* * 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/. */ -export interface EnumOption { + +export default interface EnumOption { id: string; label: string; } diff --git a/src/components/utils/feederType.ts b/src/components/utils/feederType.ts index 6aaa297799..18abf2b57a 100644 --- a/src/components/utils/feederType.ts +++ b/src/components/utils/feederType.ts @@ -6,7 +6,7 @@ */ import { EQUIPMENT_TYPES } from './equipment-types'; -import { ValueOf } from 'type-fest'; +import type { ValueOf } from 'type-fest'; export const FEEDER_TYPES = { ...EQUIPMENT_TYPES, diff --git a/src/components/utils/field-constants.js b/src/components/utils/field-constants.ts similarity index 99% rename from src/components/utils/field-constants.js rename to src/components/utils/field-constants.ts index c1373e6239..1bff3f6895 100644 --- a/src/components/utils/field-constants.js +++ b/src/components/utils/field-constants.ts @@ -125,13 +125,13 @@ export const SHUNT_COMPENSATOR_TYPE = 'shuntCompensatorType'; export const CHARACTERISTICS_CHOICES = { Q_AT_NOMINAL_V: { id: 'Q_AT_NOMINAL_V', label: 'QatNominalVLabel' }, SUSCEPTANCE: { id: 'SUSCEPTANCE', label: 'SusceptanceLabel' }, -}; +} as const; export const VOLTAGE_REGULATION_MODE = 'voltageRegulationMode'; export const VOLTAGE_REGULATION_MODES = { VOLTAGE: { id: 'VOLTAGE', label: 'VoltageRegulationText' }, REACTIVE_POWER: { id: 'REACTIVE_POWER', label: 'ReactivePowerRegulationText' }, OFF: { id: 'OFF', label: 'Off' }, -}; +} as const; export const MAXIMUM_SECTION_COUNT = 'maximumSectionCount'; export const SWITCHED_ON_Q_AT_NOMINAL_V = 'switchedOnQAtNominalV'; export const SWITCHED_ON_SUSCEPTANCE = 'switchedOnSusceptance'; @@ -155,7 +155,7 @@ export const TEMPORARY_LIMIT_MODIFICATION_TYPE = { MODIFIED: 'MODIFIED', ADDED: 'ADDED', DELETED: 'DELETED', -}; +} as const; export const SEGMENT_DISTANCE_VALUE = 'segmentDistanceValue'; export const SEGMENT_TYPE_VALUE = 'segmentTypeValue'; export const SEGMENT_TYPE_ID = 'segmentTypeId'; diff --git a/src/components/utils/functions.js b/src/components/utils/functions.js index d3df2a1198..9f1262d3de 100644 --- a/src/components/utils/functions.js +++ b/src/components/utils/functions.js @@ -5,4 +5,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/** @deprecated use {@link import('@gridsuite/commons-ui').mergeSx} */ export const mergeSx = (...allSx) => allSx.flat(); diff --git a/src/components/utils/is-any-node-building-hook.js b/src/components/utils/is-any-node-building-hook.ts similarity index 52% rename from src/components/utils/is-any-node-building-hook.js rename to src/components/utils/is-any-node-building-hook.ts index d56e0f2a53..06903f49c9 100644 --- a/src/components/utils/is-any-node-building-hook.js +++ b/src/components/utils/is-any-node-building-hook.ts @@ -7,15 +7,16 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; +import type { AppState } from '../../redux/reducer'; -export const useIsAnyNodeBuilding = () => { - const [iAnyNodeBuild, setAnyNodeBuilding] = useState(false); +export default function useIsAnyNodeBuilding() { + const [isAnyNodeBuild, setIsAnyNodeBuilding] = useState(false); - const treeModel = useSelector((state) => state.networkModificationTreeModel); + const treeModel = useSelector((state: AppState) => state.networkModificationTreeModel); useEffect(() => { - setAnyNodeBuilding(treeModel.isAnyNodeBuilding); + setIsAnyNodeBuilding(treeModel?.isAnyNodeBuilding ?? false); }, [treeModel]); - return iAnyNodeBuild; -}; + return isAnyNodeBuild; +} diff --git a/src/components/utils/localized-countries-hook.js b/src/components/utils/localized-countries-hook.js index 05b2010fbd..228fae2235 100644 --- a/src/components/utils/localized-countries-hook.js +++ b/src/components/utils/localized-countries-hook.js @@ -13,6 +13,7 @@ import localizedCountries from 'localized-countries'; import countriesFr from 'localized-countries/data/fr'; import countriesEn from 'localized-countries/data/en'; +/** @deprecated use import('@gridsuite/commons-ui').useLocalizedCountries instead */ export const useLocalizedCountries = () => { const [languageLocal] = useParameterState(PARAM_LANGUAGE); const [localizedCountriesModule, setLocalizedCountriesModule] = useState(); diff --git a/src/components/utils/ts-utils.ts b/src/components/utils/ts-utils.d.ts similarity index 100% rename from src/components/utils/ts-utils.ts rename to src/components/utils/ts-utils.d.ts diff --git a/src/components/utils/equipmentInfosHandler.js b/src/components/utils/use-name-or-id.ts similarity index 58% rename from src/components/utils/equipmentInfosHandler.js rename to src/components/utils/use-name-or-id.ts index 3297fd1cbd..627c79b1e7 100644 --- a/src/components/utils/equipmentInfosHandler.js +++ b/src/components/utils/use-name-or-id.ts @@ -8,23 +8,30 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { PARAM_USE_NAME } from '../../utils/config-params'; +import type { AppState } from '../../redux/reducer'; + +type AnyObjectWithOptionalNameAndId = { + [key: string]: any; // Allows any other fields +} & Partial<{ + name: string | null; + id: string; +}>; + +export default function useNameOrId() { + const useName = useSelector((state: AppState) => state[PARAM_USE_NAME]); -export const useNameOrId = () => { - const useName = useSelector((state) => state[PARAM_USE_NAME]); const getNameOrId = useCallback( - (infos) => { + (infos: AnyObjectWithOptionalNameAndId | null) => { if (infos != null) { const name = infos.name; - return useName && name != null && name.trim() !== '' ? name : infos?.id; + return useName && name != null && name.trim() !== '' ? name : infos?.id ?? null; } return null; }, [useName] ); - const getUseNameParameterKey = useCallback(() => { - return useName ? 'name' : 'id'; - }, [useName]); + const getUseNameParameterKey = useCallback(() => (useName ? 'name' : 'id'), [useName]); return { getNameOrId, getUseNameParameterKey }; -}; +} diff --git a/src/components/utils/utils.js b/src/components/utils/utils.js deleted file mode 100644 index 8fd092a90e..0000000000 --- a/src/components/utils/utils.js +++ /dev/null @@ -1,260 +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 { useEffect, useRef } from 'react'; -import { getIn } from 'yup'; -import { toNumber } from './validation-functions'; - -export const UNDEFINED_ACCEPTABLE_DURATION = Math.pow(2, 31) - 1; - -/** - * Get the label of an enum value from its id - * @param {Array} enumValues - The enum values {id: string, label: string} [] - * @param {string} id - The id of the enum value - * @returns {string | undefined} - The label of the enum value - */ -export const getEnumLabelById = (enumValues, id) => { - if (!enumValues || !id) { - return undefined; - } - const enumValue = enumValues.find((enumValue) => enumValue.id === id); - return enumValue?.label; -}; - -export const isFieldRequired = (fieldName, schema, values) => { - const { schema: fieldSchema, parent: parentValues } = getIn(schema, fieldName, values) || {}; - return fieldSchema.describe({ parent: parentValues })?.optional === false; - - //static way, not working when using "when" in schema, but does not need form values - //return yup.reach(schema, fieldName)?.exclusiveTests?.required === true; -}; - -export const areArrayElementsUnique = (array) => { - let uniqueAlphaValues = [...new Set(array)]; - return uniqueAlphaValues.length === array.length; -}; - -/** - * Returns true if every element of this array is a number and they are ordered (ascending or descending) - * @param array or numbers - * @returns {boolean} - */ -export const areNumbersOrdered = (array) => { - if (!Array.isArray(array)) { - return false; - } - if (array.length === 0) { - return true; - } - if (array.length === 1) { - return !isNaN(toNumber(array[0])); - } - - let current = toNumber(array[0]); - if (isNaN(current)) { - return false; - } - let order = null; - - for (let i = 1; i < array.length; i++) { - const nextOne = toNumber(array[i]); - if (isNaN(nextOne)) { - return false; - } - if (current === nextOne) { - continue; - } - if (order === null) { - order = current < nextOne ? 'asc' : 'desc'; - } - if ((order === 'asc' && current > nextOne) || (order === 'desc' && current < nextOne)) { - return false; - } - current = nextOne; - } - return true; -}; - -export const findIndexesOfDuplicateFieldValues = (values, fieldName) => { - const counts = new Map(); - values.forEach((element, index) => { - const value = element[fieldName]; - counts.set(value, (counts.get(value) || []).concat(index)); - }); - return [...counts.values()].filter((indexes) => indexes.length > 1).flat(); -}; - -export const areIdsEqual = (val1, val2) => { - return val1.id === val2.id; -}; - -export const areUuidsEqual = (val1, val2) => { - return val1.uuid === val2.uuid; -}; - -export const getObjectId = (object) => { - return typeof object === 'string' ? object : object?.id ?? null; -}; - -export const buildNewBusbarSections = (equipmentId, sectionCount, busbarCount) => { - const newBusbarSections = []; - for (let i = 0; i < busbarCount; i++) { - for (let j = 0; j < sectionCount; j++) { - newBusbarSections.push({ - id: equipmentId + '_' + (i + 1) + '_' + (j + 1), - name: '', - }); - } - } - return newBusbarSections; -}; - -export function toModificationOperation(value) { - return value === 0 || value === false || value ? { value: value, op: 'SET' } : null; -} - -export function toModificationUnsetOperation(value) { - if (value === null) { - return null; - } - return value === 0 || value === false || value ? { value: value, op: 'SET' } : { op: 'UNSET' }; -} - -export const formatTemporaryLimits = (temporaryLimits) => - temporaryLimits?.map((limit) => { - return { - name: limit?.name ?? '', - value: limit?.value ?? null, - acceptableDuration: limit?.acceptableDuration ?? null, - modificationType: limit?.modificationType ?? null, - }; - }); - -export const richTypeEquals = (a, b) => a === b; - -export const computeHighTapPosition = (steps) => { - const values = steps?.map((step) => step['index']); - return values?.length > 0 ? Math.max(...values) : null; -}; - -export const compareStepsWithPreviousValues = (tapSteps, previousValues) => { - if (previousValues === undefined) { - return false; - } - if (tapSteps.length !== previousValues?.length) { - return false; - } - return tapSteps.every((step, index) => { - const previousStep = previousValues[index]; - return Object.getOwnPropertyNames(previousStep).every((key) => { - return parseFloat(step[key]) === previousStep[key]; - }); - }); -}; - -export const getTapChangerEquipmentSectionTypeValue = (tapChanger) => { - if (!tapChanger?.regulatingTerminalConnectableType) { - return null; - } else { - return tapChanger?.regulatingTerminalConnectableType + ' : ' + tapChanger?.regulatingTerminalConnectableId; - } -}; - -export const getTapChangerRegulationTerminalValue = (tapChanger) => { - let regulatingTerminalGeneratorValue = tapChanger?.regulatingTerminalConnectableId ?? ''; - if (tapChanger?.regulatingTerminalVlId) { - regulatingTerminalGeneratorValue += ' ( ' + tapChanger?.regulatingTerminalVlId + ' )'; - } - return regulatingTerminalGeneratorValue; -}; - -export function calculateResistance(distance, linearResistance) { - if (distance === undefined || isNaN(distance) || linearResistance === undefined || isNaN(linearResistance)) { - return 0; - } - return Number(distance) * Number(linearResistance); -} - -export function calculateReactance(distance, linearReactance) { - if (distance === undefined || isNaN(distance) || linearReactance === undefined || isNaN(linearReactance)) { - return 0; - } - return Number(distance) * Number(linearReactance); -} - -export const computeSwitchedOnValue = (sectionCount, maximumSectionCount, linkedSwitchedOnValue) => { - return (linkedSwitchedOnValue / maximumSectionCount) * sectionCount; -}; - -export const computeQAtNominalV = (susceptance, nominalVoltage) => { - return Math.abs(susceptance * Math.pow(nominalVoltage, 2)); -}; - -export const computeMaxQAtNominalV = (maxSucepctance, nominalVoltage) => { - return Math.abs(maxSucepctance * Math.pow(nominalVoltage, 2)); -}; - -export const computeMaxSusceptance = (maxQAtNominalV, nominalVoltage) => { - return Math.abs(maxQAtNominalV / Math.pow(nominalVoltage, 2)); -}; - -export function calculateSusceptance(distance, linearCapacity) { - if (distance === undefined || isNaN(distance) || linearCapacity === undefined || isNaN(linearCapacity)) { - return 0; - } - return Number(distance) * Number(linearCapacity) * 2 * Math.PI * 50 * Math.pow(10, 6); -} - -export const replaceAllDefaultValues = (arrayParams, oldValue, newValue) => { - return ( - arrayParams && - arrayParams.reduce((accumulator, current) => { - return [ - ...accumulator, - { - ...current, - defaultValue: current.defaultValue === oldValue ? newValue : current.defaultValue, - }, - ]; - }, []) - ); -}; - -export function getNewVoltageLevelOptions(formattedVoltageLevel, oldVoltageLevelId, voltageLevelOptions) { - const newVoltageLevelOptions = - formattedVoltageLevel.id === oldVoltageLevelId - ? voltageLevelOptions.filter((vl) => vl.id !== formattedVoltageLevel.id) - : voltageLevelOptions.filter((vl) => vl.id !== formattedVoltageLevel.id && vl.id !== oldVoltageLevelId); - newVoltageLevelOptions.push(formattedVoltageLevel); - - return newVoltageLevelOptions; -} - -export function usePrevious(value) { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }, [value]); - return ref.current; -} - -// remove elementToToggle from list, or add it if it does not exist yet -// useful when checking/unchecking checkboxex -export function toggleElementFromList(elementToToggle, list, getFieldId) { - const resultList = [...list]; - const elementToToggleIndex = resultList.findIndex((element) => getFieldId(element) === getFieldId(elementToToggle)); - if (elementToToggleIndex >= 0) { - resultList.splice(elementToToggleIndex, 1); - } else { - resultList.push(elementToToggle); - } - return resultList; -} - -export const comparatorStrIgnoreCase = (str1, str2) => { - return str1.toLowerCase().localeCompare(str2.toLowerCase()); -}; diff --git a/src/components/utils/utils.test.js b/src/components/utils/utils.test.js deleted file mode 100644 index 7348775939..0000000000 --- a/src/components/utils/utils.test.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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 { areArrayElementsUnique, areNumbersOrdered } from './utils'; - -test('utils.areArrayElementsUnique', () => { - expect(areArrayElementsUnique([1, 2, 3])).toBeTruthy(); - expect(areArrayElementsUnique([1, 2, 1])).toBeFalsy(); - expect(areArrayElementsUnique(['1', 1, 2])).toBeTruthy(); - expect(areArrayElementsUnique([null, 0, false])).toBeTruthy(); - expect(areArrayElementsUnique([null, 0, null])).toBeFalsy(); - expect(areArrayElementsUnique(['-1', -1])).toBeTruthy(); -}); - -test('utils.areNumbersOrdered', () => { - expect(areNumbersOrdered([0, 0])).toBeTruthy(); - expect(areNumbersOrdered([0, 0, 0])).toBeTruthy(); - expect(areNumbersOrdered([20, 20, 20])).toBeTruthy(); - expect(areNumbersOrdered([-1, 0, 1])).toBeTruthy(); - expect(areNumbersOrdered([-10, 0, 0])).toBeTruthy(); - expect(areNumbersOrdered([0, 0, -10])).toBeTruthy(); - expect(areNumbersOrdered([1, 1, -10])).toBeTruthy(); - expect(areNumbersOrdered(['1', 1, '-10'])).toBeTruthy(); - expect(areNumbersOrdered(['1', 3, '10'])).toBeTruthy(); - expect(areNumbersOrdered(['0', 3, '10', 10])).toBeTruthy(); - expect(areNumbersOrdered(['0', -3, '-10', -10])).toBeTruthy(); - expect(areNumbersOrdered([3, 3, '10', '10'])).toBeTruthy(); - expect(areNumbersOrdered([-3, 3, '10', '10'])).toBeTruthy(); - expect(areNumbersOrdered([-3, '-2', '0', 0, '-0.0', 0.2, 0.2, 1, '5', '54', 400])).toBeTruthy(); - expect(areNumbersOrdered([400, '54', '5', 1, 0.2, 0.2, '-0.0', 0, '0', '-2', -3])).toBeTruthy(); - expect(areNumbersOrdered([1, 0, 1])).toBeFalsy(); - expect(areNumbersOrdered([-1, 0, -1])).toBeFalsy(); - expect(areNumbersOrdered([0, 0.1, 0.05])).toBeFalsy(); - expect(areNumbersOrdered([0, '-0.1', -0.05])).toBeFalsy(); - expect(areNumbersOrdered([0, 1, 2, 3, 4, 4, 5, 6, 5, 4, 3, 2])).toBeFalsy(); - expect(areNumbersOrdered([0, 1, 2, 3, 4, 4, 5, 6, 5, 6, 7, 8])).toBeFalsy(); - expect(areNumbersOrdered([-0, -1, -2, -3, -4, -4, -5, -6, -5, -6, -7, -8])).toBeFalsy(); - expect(areNumbersOrdered([1, 2, NaN, 3])).toBeFalsy(); // Only numbers allowed - expect(areNumbersOrdered([1, 2, 'three', 4])).toBeFalsy(); - expect(areNumbersOrdered([1])).toBeTruthy(); - expect(areNumbersOrdered([NaN])).toBeFalsy(); - expect(areNumbersOrdered([true])).toBeFalsy(); - expect(areNumbersOrdered([null])).toBeFalsy(); - expect(areNumbersOrdered([false])).toBeFalsy(); - expect(areNumbersOrdered([false, 5])).toBeFalsy(); - expect(areNumbersOrdered(['hello'])).toBeFalsy(); - expect(areNumbersOrdered([])).toBeTruthy(); - expect(areNumbersOrdered()).toBeFalsy(); - expect(areNumbersOrdered({})).toBeFalsy(); - expect(areNumbersOrdered({ length: 1 })).toBeFalsy(); -}); diff --git a/src/components/utils/utils.test.ts b/src/components/utils/utils.test.ts new file mode 100644 index 0000000000..d0171c7195 --- /dev/null +++ b/src/components/utils/utils.test.ts @@ -0,0 +1,84 @@ +/** + * 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 { areArrayElementsUnique, areNumbersOrdered, comparatorStrIgnoreCase } from './utils'; + +const expectToBeTrue = (value: boolean) => expect(value).toBeTrue(); +const expectToBeFalse = (value: boolean) => expect(value).toBeFalse(); + +describe('utils.areArrayElementsUnique', () => { + test.each([ + [[1, 2, 3], true], + [[1, 2, 1], false], + [['1', 1, 2], true], + [[null, 0, false], true], + [[null, 0, null], false], + [['-1', -1], true], + ])('areArrayElementsUnique(%j) == %p', (args, expected) => + (expected ? expectToBeTrue : expectToBeFalse)(areArrayElementsUnique(args)) + ); +}); + +describe('utils.areNumbersOrdered', () => { + test.each([ + [[0, 0], true], + [[0, 0, 0], true], + [[20, 20, 20], true], + [[-1, 0, 1], true], + [[-10, 0, 0], true], + [[0, 0, -10], true], + [[1, 1, -10], true], + [['1', 1, '-10'], true], + [['1', 3, '10'], true], + [['0', 3, '10', 10], true], + [['0', -3, '-10', -10], true], + [[3, 3, '10', '10'], true], + [[-3, 3, '10', '10'], true], + [[-3, '-2', '0', 0, '-0.0', 0.2, 0.2, 1, '5', '54', 400], true], + [[400, '54', '5', 1, 0.2, 0.2, '-0.0', 0, '0', '-2', -3], true], + [[1, 0, 1], false], + [[-1, 0, -1], false], + [[0, 0.1, 0.05], false], + [[0, '-0.1', -0.05], false], + [[0, 1, 2, 3, 4, 4, 5, 6, 5, 4, 3, 2], false], + [[0, 1, 2, 3, 4, 4, 5, 6, 5, 6, 7, 8], false], + [[-0, -1, -2, -3, -4, -4, -5, -6, -5, -6, -7, -8], false], + [[1, 2, NaN, 3], false], // Only numbers allowed + [[1, 2, 'three', 4], false], + [[1], true], + [[NaN], false], + [[true], false], + [[null], false], + [[false], false], + [[false, 5], false], + [['hello'], false], + [[], true], + [undefined, false], + [{}, false], + [{ length: 1 }, false], + ])('areNumbersOrdered(%j) == %p', (args, expected) => + (expected ? expectToBeTrue : expectToBeFalse)(areNumbersOrdered(args)) + ); +}); + +describe('utils.comparatorStrIgnoreCase', () => { + test.each([ + { str1: 'abc', str2: 'abc', ignoreAccent: undefined, expectZero: true }, + { str1: 'abc', str2: 'def', ignoreAccent: undefined, expectZero: false }, + { str1: 'abc', str2: 'ABC', ignoreAccent: undefined, expectZero: true }, + { str1: 'abc', str2: 'àbç', ignoreAccent: undefined, expectZero: false }, + { str1: 'abc', str2: 'àbç', ignoreAccent: false, expectZero: false }, + { str1: 'abc', str2: 'àbç', ignoreAccent: true, expectZero: true }, + ])('comparatorStrIgnoreCase($str1, $str2)', ({ str1, str2, ignoreAccent, expectZero }) => { + const exceptResult = expect(comparatorStrIgnoreCase(str1, str2, ignoreAccent)); + if (expectZero) { + exceptResult.toBe(0); + } else { + exceptResult.not.toBe(0); + } + }); +}); diff --git a/src/components/utils/utils.ts b/src/components/utils/utils.ts new file mode 100644 index 0000000000..b35b05a465 --- /dev/null +++ b/src/components/utils/utils.ts @@ -0,0 +1,282 @@ +/** + * 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 { useEffect, useRef } from 'react'; +import { getIn } from 'yup'; +import { toNumber } from './validation-functions'; +import EnumOption from './EnumOption'; +import type { FixedLengthArray, UnknownArray } from 'type-fest'; +import type { Abs, Divide, Max, Multiply, Pow, Subtract } from 'ts-arithmetic'; +import type { Identifiable } from '@gridsuite/commons-ui'; +import type { VoltageLevel } from './equipment-types'; + +export type PI = 3.141592653589793; // Math.PI + +export type MaxArray = TArray extends [ + infer First extends number, + ...infer Rest extends number[] +] + ? Rest['length'] extends 0 + ? First + : Max> + : never; + +export const UNDEFINED_ACCEPTABLE_DURATION = (Math.pow(2, 31) - 1) as Subtract, 1>; + +// TODO: when NaN become a type in TypeScript, update the definitions functions +// https://github.com/Microsoft/TypeScript/issues/28682 + +/** + * Get the label of an enum value from its id + * @param {Array} enumValues - The enum values {id: string, label: string} [] + * @param {string} id - The id of the enum value + * @returns {string | undefined} - The label of the enum value + */ +export function getEnumLabelById(enumValues: EnumOption[], id: string) { + if (!enumValues || !id) { + return undefined; + } + return enumValues.find((enumValue) => enumValue.id === id)?.label; +} + +export function isFieldRequired(fieldName: any, schema: any, values: any) { + const { schema: fieldSchema, parent: parentValues } = getIn(schema, fieldName, values) || {}; + // @ts-expect-error TODO: look with src/components/utils/rhf-inputs/chip-items-input.jsx for type + return fieldSchema.describe({ parent: parentValues })?.optional === false; + + //static way, not working when using "when" in schema, but does not need form values + //return yup.reach(schema, fieldName)?.exclusiveTests?.required === true; +} + +export function areArrayElementsUnique(array: UnknownArray) { + let uniqueAlphaValues = [...new Set(array)]; + return uniqueAlphaValues.length === array.length; +} + +/** + * Returns true if every element of this array is a number and they are ordered (ascending or descending) + */ +export function areNumbersOrdered(array: unknown) { + if (!Array.isArray(array)) { + return false; + } + if (array.length === 0) { + return true; + } + if (array.length === 1) { + return !isNaN(toNumber(array[0])); + } + + let current = toNumber(array[0]); + if (isNaN(current)) { + return false; + } + let order = null; + + for (let i = 1; i < array.length; i++) { + const nextOne = toNumber(array[i]); + if (isNaN(nextOne)) { + return false; + } + if (current === nextOne) { + continue; + } + if (order === null) { + order = current < nextOne ? 'asc' : 'desc'; + } + if ((order === 'asc' && current > nextOne) || (order === 'desc' && current < nextOne)) { + return false; + } + current = nextOne; + } + return true; +} + +export const areIdsEqual = (val1: any, val2: any) => val1.id === val2.id; + +export function getObjectId(object: any) { + return typeof object === 'string' ? object : object?.id ?? null; +} + +export function buildNewBusbarSections( + equipmentId: string, + sectionCount: NSection, + busbarCount: NBusBar +) { + const newBusbarSections = new Array(busbarCount * sectionCount); + for (let b = 0; b < busbarCount; b++) { + for (let s = 0; s < sectionCount; s++) { + newBusbarSections.push({ + id: `${equipmentId}_${b + 1}_${s + 1}`, + name: '', + }); + } + } + return newBusbarSections as unknown as FixedLengthArray>; +} + +export function toModificationOperation(value: T) { + return value === 0 || value === false || value ? ({ value: value, op: 'SET' } as const) : null; +} + +export function toModificationUnsetOperation(value: T) { + if (value === null) { + return null; + } + return value === 0 || value === false || value + ? ({ value: value, op: 'SET' } as const) + : ({ op: 'UNSET' } as const); +} + +export const formatTemporaryLimits = (temporaryLimits: any[]) => + temporaryLimits?.map((limit) => ({ + name: limit?.name ?? '', + value: limit?.value ?? null, + acceptableDuration: limit?.acceptableDuration ?? null, + modificationType: limit?.modificationType ?? null, + })); + +export const richTypeEquals = (a: unknown, b: unknown) => a === b; + +export function computeHighTapPosition(steps: any[]) { + const values = steps?.map((step) => step['index']); + return values?.length > 0 ? Math.max(...values) : null; +} + +export function compareStepsWithPreviousValues(tapSteps: any[], previousValues: any[]) { + if (previousValues === undefined) { + return false; + } + if (tapSteps.length !== previousValues?.length) { + return false; + } + return tapSteps.every((step, index) => { + const previousStep = previousValues[index]; + return Object.getOwnPropertyNames(previousStep).every((key) => parseFloat(step[key]) === previousStep[key]); + }); +} + +export function getTapChangerEquipmentSectionTypeValue(tapChanger: any) { + if (!tapChanger?.regulatingTerminalConnectableType) { + return null; + } else { + return tapChanger?.regulatingTerminalConnectableType + ' : ' + tapChanger?.regulatingTerminalConnectableId; + } +} + +export function getTapChangerRegulationTerminalValue(tapChanger: any) { + let regulatingTerminalGeneratorValue = tapChanger?.regulatingTerminalConnectableId ?? ''; + if (tapChanger?.regulatingTerminalVlId) { + regulatingTerminalGeneratorValue += ' ( ' + tapChanger?.regulatingTerminalVlId + ' )'; + } + return regulatingTerminalGeneratorValue; +} + +export function calculateResistance( + distance: number | undefined, + linearResistance: number | undefined +) { + if (distance === undefined || isNaN(distance) || linearResistance === undefined || isNaN(linearResistance)) { + return 0; + } + return (Number(distance) * Number(linearResistance)) as Multiply; +} + +export function calculateReactance( + distance: TD | undefined, + linearReactance: TLR | undefined +) { + if (distance === undefined || isNaN(distance) || linearReactance === undefined || isNaN(linearReactance)) { + return 0; + } + return (Number(distance) * Number(linearReactance)) as Multiply; +} + +export function computeSwitchedOnValue( + sectionCount: TS, + maximumSectionCount: TMS, + linkedSwitchedOnValue: TLSV +) { + return ((linkedSwitchedOnValue / maximumSectionCount) * sectionCount) as Multiply, TLSV>; +} + +export function computeMaxQAtNominalV(maxSusceptance: TS, nominalVoltage: TV) { + return Math.abs(maxSusceptance * Math.pow(nominalVoltage, 2)) as Abs>>; +} + +export function computeMaxSusceptance(maxQAtNominalV: TQ, nominalVoltage: TV) { + return Math.abs(maxQAtNominalV / Math.pow(nominalVoltage, 2)) as Abs>>; +} + +export function calculateSusceptance( + distance: TD | undefined, + linearCapacity: TLC | undefined +) { + if (distance === undefined || isNaN(distance) || linearCapacity === undefined || isNaN(linearCapacity)) { + return 0; + } + return (Number(distance) * Number(linearCapacity) * 2 * Math.PI * 50 * Math.pow(10, 6)) as Multiply< + TD, + Multiply>>>> + >; +} + +export function replaceAllDefaultValues(arrayParams: any[], oldValue: TValue, newValue: TValue): any[] { + return arrayParams?.reduce( + (accumulator, current) => [ + ...accumulator, + { + ...current, + defaultValue: current.defaultValue === oldValue ? newValue : current.defaultValue, + }, + ], + [] + ); +} + +export function getNewVoltageLevelOptions( + formattedVoltageLevel: VoltageLevel, + oldVoltageLevelId: VoltageLevel['id'], + voltageLevelOptions: VoltageLevel[] +) { + return [ + ...(formattedVoltageLevel.id === oldVoltageLevelId + ? voltageLevelOptions.filter((vl) => vl.id !== formattedVoltageLevel.id) + : voltageLevelOptions.filter((vl) => vl.id !== formattedVoltageLevel.id && vl.id !== oldVoltageLevelId)), + formattedVoltageLevel, + ]; +} + +export function usePrevious(value: T): T { + const ref = useRef(value); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} + +/** + * Remove elementToToggle from list, or add it if it does not exist yet. + *
Useful when checking/unchecking checkboxes. + */ +export function toggleElementFromList(elementToToggle: T, list: T[], getFieldId: (element: T) => ID) { + const resultList = [...list]; + const elementToToggleIndex = resultList.findIndex((element) => getFieldId(element) === getFieldId(elementToToggle)); + if (elementToToggleIndex >= 0) { + resultList.splice(elementToToggleIndex, 1); + } else { + resultList.push(elementToToggle); + } + return resultList; +} + +/** + * Compare two string ignoring the letter cases + * @return number {@link String.localeCompare} output: 0 if equals, else 1 + */ +export const comparatorStrIgnoreCase = (str1: string, str2: string, ignoreAccent: boolean = false) => + str1.localeCompare(str2, undefined, { sensitivity: ignoreAccent ? 'base' : 'accent' }); diff --git a/src/components/utils/validation-functions.test.js b/src/components/utils/validation-functions.test.js deleted file mode 100644 index 480b835298..0000000000 --- a/src/components/utils/validation-functions.test.js +++ /dev/null @@ -1,415 +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 { - validateValueIsANumber, - exportedForTesting, - validateValueIsLessThanOrEqualTo, - validateValueIsLessThan, - validateField, - checkReactiveCapabilityCurve, -} from './validation-functions'; - -const { toNumber, isBlankOrEmpty } = exportedForTesting; - -test('validation-functions.isBlankOrEmpty', () => { - expect(isBlankOrEmpty('hello')).toBeFalsy(); - expect(isBlankOrEmpty('0')).toBeFalsy(); - expect(isBlankOrEmpty(0)).toBeFalsy(); - expect(isBlankOrEmpty(false)).toBeFalsy(); - expect(isBlankOrEmpty(true)).toBeFalsy(); - expect(isBlankOrEmpty(' ')).toBeTruthy(); - expect(isBlankOrEmpty(null)).toBeTruthy(); - expect(isBlankOrEmpty(undefined)).toBeTruthy(); -}); - -test('validation-functions.toNumber', () => { - expect(toNumber('')).toBeNaN(); - expect(toNumber(undefined)).toBeNaN(); - expect(toNumber(null)).toBeNaN(); - expect(toNumber(false)).toBeNaN(); - expect(toNumber(true)).toBeNaN(); - expect(toNumber(NaN)).toBeNaN(); - expect(toNumber('hello')).toBeNaN(); - expect(toNumber({ 0: 1 })).toBeNaN(); - expect(toNumber([10])).toBeNaN(); - expect(toNumber(' 0020,5000 ')).toBe(20.5); - expect(toNumber(0)).toBe(0); - expect(toNumber(',0')).toBe(0); - expect(toNumber(0.99999)).not.toBe(1); - - // Converted values must be equal in these cases - expect(toNumber(-0) === toNumber(0)).toBe(true); - expect(toNumber(0.0) === toNumber(0)).toBe(true); - expect(toNumber(-0.0) === toNumber(0)).toBe(true); - expect(toNumber('-0') === toNumber(0)).toBe(true); - expect(toNumber('0,0') === toNumber(0)).toBe(true); - expect(toNumber(',0') === toNumber(0)).toBe(true); - expect(toNumber('-.000') === toNumber(0)).toBe(true); - expect(toNumber('-,000') === toNumber(0)).toBe(true); -}); - -test('validation-functions.validateValueIsANumber', () => { - expect(validateValueIsANumber(10)).toBe(true); - expect(validateValueIsANumber(0)).toBe(true); - expect(validateValueIsANumber('0')).toBe(true); - expect(validateValueIsANumber('-0')).toBe(true); - expect(validateValueIsANumber('-10')).toBe(true); - expect(validateValueIsANumber('-.0')).toBe(true); - expect(validateValueIsANumber('-,0')).toBe(true); - expect(validateValueIsANumber('.0')).toBe(true); - expect(validateValueIsANumber('0.510')).toBe(true); - expect(validateValueIsANumber(',510')).toBe(true); - expect(validateValueIsANumber('-,510')).toBe(true); - expect(validateValueIsANumber(' -,510 ')).toBe(true); - expect(validateValueIsANumber(55.51)).toBe(true); - expect(validateValueIsANumber(-55.51)).toBe(true); - expect(validateValueIsANumber('-55,51')).toBe(true); - expect(validateValueIsANumber('-55,')).toBe(true); - expect(validateValueIsANumber('-55.')).toBe(true); - expect(validateValueIsANumber(null)).toBe(false); - expect(validateValueIsANumber(NaN)).toBe(false); - expect(validateValueIsANumber(undefined)).toBe(false); - expect(validateValueIsANumber(false)).toBe(false); - expect(validateValueIsANumber('hello')).toBe(false); - expect(validateValueIsANumber('15.564,54')).toBe(false); - expect(validateValueIsANumber(true)).toBe(false); - expect(validateValueIsANumber('')).toBe(false); -}); - -test('validation-functions.validateValueIsLessThanOrEqualTo', () => { - expect(validateValueIsLessThanOrEqualTo(0, 0)).toBe(true); - expect(validateValueIsLessThanOrEqualTo(0, 1)).toBe(true); - expect(validateValueIsLessThanOrEqualTo(1.0, 1)).toBe(true); - expect(validateValueIsLessThanOrEqualTo(0.9, 0.91)).toBe(true); - expect(validateValueIsLessThanOrEqualTo(-0.91, -0.9)).toBe(true); - expect(validateValueIsLessThanOrEqualTo('90,54', 154)).toBe(true); - expect(validateValueIsLessThanOrEqualTo('-90,54', -90.54)).toBe(true); - expect(validateValueIsLessThanOrEqualTo(false, true)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(false, false)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(undefined, undefined)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(true, true)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(1.00001, 1)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(0.00001, -1)).toBe(false); - expect(validateValueIsLessThanOrEqualTo('1.00001', '1')).toBe(false); - expect(validateValueIsLessThanOrEqualTo('.00001', '-1')).toBe(false); - expect(validateValueIsLessThanOrEqualTo(0, false)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(0, true)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(0, NaN)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(NaN, NaN)).toBe(false); - expect(validateValueIsLessThanOrEqualTo(NaN, undefined)).toBe(false); - expect(validateValueIsLessThanOrEqualTo('a', 9999)).toBe(false); -}); - -test('validation-functions.validateValueIsLessThan', () => { - expect(validateValueIsLessThan(0, 0)).toBe(false); - expect(validateValueIsLessThan(0, 1)).toBe(true); - expect(validateValueIsLessThan(1.0, 1)).toBe(false); - expect(validateValueIsLessThan(0.9, 0.91)).toBe(true); - expect(validateValueIsLessThan(-0.91, -0.9)).toBe(true); - expect(validateValueIsLessThan('90,54', 154)).toBe(true); - expect(validateValueIsLessThan('-90,54', -90.54)).toBe(false); - expect(validateValueIsLessThan(false, true)).toBe(false); - expect(validateValueIsLessThan(false, false)).toBe(false); - expect(validateValueIsLessThan(undefined, undefined)).toBe(false); - expect(validateValueIsLessThan(true, true)).toBe(false); - expect(validateValueIsLessThan(1.00001, 1)).toBe(false); - expect(validateValueIsLessThan(0.00001, -1)).toBe(false); - expect(validateValueIsLessThan('1.00001', '1')).toBe(false); - expect(validateValueIsLessThan('.00001', '-1')).toBe(false); - expect(validateValueIsLessThan(0, false)).toBe(false); - expect(validateValueIsLessThan(0, true)).toBe(false); - expect(validateValueIsLessThan(0, NaN)).toBe(false); - expect(validateValueIsLessThan(NaN, NaN)).toBe(false); - expect(validateValueIsLessThan(NaN, undefined)).toBe(false); - expect(validateValueIsLessThan('a', 9999)).toBe(false); - expect(validateValueIsLessThan('-0', '0')).toBe(false); - expect(validateValueIsLessThan('.0', '0')).toBe(false); - expect(validateValueIsLessThan(',0', '0')).toBe(false); - expect(validateValueIsLessThan('-,0', '0')).toBe(false); - expect(validateValueIsLessThan(0.0, 0)).toBe(false); -}); - -test('validation-functions.validateField.isFieldRequired', () => { - expect(validateField(500, { isFieldRequired: true }).error).toBe(false); - expect(validateField(0, { isFieldRequired: true, isFieldNumeric: true }).error).toBe(false); - expect(validateField('hello', { isFieldRequired: true }).error).toBe(false); - expect(validateField('', { isFieldRequired: true }).error).toBe(true); - expect(validateField(' ', { isFieldRequired: true }).error).toBe(true); - expect(validateField(null, { isFieldRequired: true }).error).toBe(true); - expect(validateField(undefined, { isFieldRequired: true }).error).toBe(true); -}); - -test('validation-functions.validateField.isFieldNumeric', () => { - expect(validateField(500, { isFieldNumeric: true }).error).toBe(false); - expect(validateField(0, { isFieldNumeric: true }).error).toBe(false); - expect(validateField(-0.0, { isFieldNumeric: true }).error).toBe(false); - expect(validateField('hello', { isFieldNumeric: true }).error).toBe(true); - expect(validateField('', { isFieldNumeric: true }).error).toBe(false); // If the field is not required, there should be no validation error - expect(validateField('', { isFieldRequired: true, isFieldNumeric: true }).error).toBe(true); - expect(validateField(' ', { isFieldRequired: true, isFieldNumeric: true }).error).toBe(true); - expect(validateField(null, { isFieldRequired: true, isFieldNumeric: true }).error).toBe(true); - expect( - validateField(undefined, { - isFieldRequired: true, - isFieldNumeric: true, - }).error - ).toBe(true); -}); - -test('validation-functions.validateField.valueGreaterThanOrEqualTo', () => { - expect(validateField(500, { valueGreaterThanOrEqualTo: 10 }).error).toBe(false); - expect(validateField(500, { valueGreaterThanOrEqualTo: 0 }).error).toBe(false); - expect(validateField(0, { valueGreaterThanOrEqualTo: 0 }).error).toBe(false); - expect(validateField(0, { valueGreaterThanOrEqualTo: 10 }).error).toBe(true); - expect(validateField(-500, { valueGreaterThanOrEqualTo: 10 }).error).toBe(true); - expect(validateField(-500, { valueGreaterThanOrEqualTo: 0 }).error).toBe(true); - expect(validateField(0, { valueGreaterThanOrEqualTo: -10 }).error).toBe(false); - expect( - validateField('', { - valueGreaterThanOrEqualTo: 2, - isFieldRequired: false, - }).error - ).toBe(false); - expect( - validateField('', { - valueGreaterThanOrEqualTo: 3, - isFieldRequired: true, - }).error - ).toBe(true); - expect( - validateField(' ', { - valueGreaterThanOrEqualTo: 2, - isFieldRequired: false, - }).error - ).toBe(false); - expect( - validateField(' ', { - valueGreaterThanOrEqualTo: 3, - isFieldRequired: true, - }).error - ).toBe(true); - expect( - validateField(null, { - valueGreaterThanOrEqualTo: 2, - isFieldRequired: false, - }).error - ).toBe(false); - expect( - validateField(null, { - valueGreaterThanOrEqualTo: 3, - isFieldRequired: true, - }).error - ).toBe(true); - expect( - validateField(undefined, { - valueGreaterThanOrEqualTo: 2, - isFieldRequired: false, - }).error - ).toBe(false); - expect( - validateField(undefined, { - valueGreaterThanOrEqualTo: 3, - isFieldRequired: true, - }).error - ).toBe(true); -}); - -test('validation-functions.validateField.valueLessThanOrEqualTo', () => { - expect(validateField(-600, { valueLessThanOrEqualTo: 10 }).error).toBe(false); - expect(validateField(-600, { valueLessThanOrEqualTo: 0 }).error).toBe(false); - expect(validateField(0, { valueLessThanOrEqualTo: -10 }).error).toBe(true); - expect(validateField(600, { valueLessThanOrEqualTo: 10 }).error).toBe(true); - expect(validateField(600, { valueLessThanOrEqualTo: 0 }).error).toBe(true); - expect(validateField(0, { valueLessThanOrEqualTo: 0 }).error).toBe(false); - expect(validateField(0, { valueLessThanOrEqualTo: 10 }).error).toBe(false); - expect( - validateField('', { - valueLessThanOrEqualTo: 20, - isFieldRequired: false, - }).error - ).toBe(false); - expect(validateField('', { valueLessThanOrEqualTo: 20, isFieldRequired: true }).error).toBe(true); - expect( - validateField(' ', { - valueLessThanOrEqualTo: 20, - isFieldRequired: false, - }).error - ).toBe(false); - expect( - validateField(' ', { - valueLessThanOrEqualTo: 20, - isFieldRequired: true, - }).error - ).toBe(true); - expect( - validateField(null, { - valueLessThanOrEqualTo: 20, - isFieldRequired: false, - }).error - ).toBe(false); - expect( - validateField(null, { - valueLessThanOrEqualTo: 20, - isFieldRequired: true, - }).error - ).toBe(true); - expect( - validateField(undefined, { - valueLessThanOrEqualTo: 20, - isFieldRequired: false, - }).error - ).toBe(false); - expect( - validateField(undefined, { - valueLessThanOrEqualTo: 20, - isFieldRequired: true, - }).error - ).toBe(true); -}); - -test('validation-functions.validateField.valueLessThan', () => { - expect(validateField(600, { valueLessThan: 10 }).error).toBe(true); - expect(validateField(600, { valueLessThan: 0 }).error).toBe(true); - expect(validateField(0, { valueLessThan: 0 }).error).toBe(true); - expect(validateField(0, { valueLessThan: 10 }).error).toBe(false); - expect(validateField(-600, { valueLessThan: 10 }).error).toBe(false); - expect(validateField(-600, { valueLessThan: 0 }).error).toBe(false); - expect(validateField(0, { valueLessThan: -10 }).error).toBe(true); - expect(validateField('', { valueLessThan: 6, isFieldRequired: false }).error).toBe(false); - expect(validateField('', { valueLessThan: 6, isFieldRequired: true }).error).toBe(true); - expect(validateField(' ', { valueLessThan: 6, isFieldRequired: false }).error).toBe(false); - expect(validateField(' ', { valueLessThan: 6, isFieldRequired: true }).error).toBe(true); - expect(validateField(null, { valueLessThan: 6, isFieldRequired: false }).error).toBe(false); - expect(validateField(null, { valueLessThan: 6, isFieldRequired: true }).error).toBe(true); - expect(validateField(undefined, { valueLessThan: 6, isFieldRequired: false }).error).toBe(false); - expect(validateField(undefined, { valueLessThan: 6, isFieldRequired: true }).error).toBe(true); -}); - -test('validation-functions.validateField.valueGreaterThan', () => { - expect(validateField(600, { valueGreaterThan: 10 }).error).toBe(false); - expect(validateField(600, { valueGreaterThan: 0 }).error).toBe(false); - expect(validateField(0, { valueGreaterThan: 0 }).error).toBe(true); - expect(validateField(0, { valueGreaterThan: 10 }).error).toBe(true); - expect(validateField(-600, { valueGreaterThan: 10 }).error).toBe(true); - expect(validateField(-600, { valueGreaterThan: 0 }).error).toBe(true); - expect(validateField(0, { valueGreaterThan: -10 }).error).toBe(false); - expect(validateField('', { valueGreaterThan: 2, isFieldRequired: false }).error).toBe(false); - expect(validateField('', { valueGreaterThan: 2, isFieldRequired: true }).error).toBe(true); - expect(validateField(' ', { valueGreaterThan: 2, isFieldRequired: false }).error).toBe(false); - expect(validateField(' ', { valueGreaterThan: 2, isFieldRequired: true }).error).toBe(true); - expect(validateField(null, { valueGreaterThan: 2, isFieldRequired: false }).error).toBe(false); - expect(validateField(null, { valueGreaterThan: 2, isFieldRequired: true }).error).toBe(true); - expect( - validateField(undefined, { - valueGreaterThan: 2, - isFieldRequired: false, - }).error - ).toBe(false); - expect(validateField(undefined, { valueGreaterThan: 2, isFieldRequired: true }).error).toBe(true); -}); - -test('validation-functions.checkReactiveCapabilityCurve', () => { - // Reactive capability curve default format : [{ p: '', minQ: '', maxQ: '' }, { p: '', minQ: '', maxQ: '' }] - - // Correct reactive cabability curves - expect( - checkReactiveCapabilityCurve([ - { p: '0', minQ: '0', maxQ: '0' }, - { p: '10', minQ: '0', maxQ: '0' }, - ]).length - ).toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: '-10', minQ: '-5', maxQ: '-2' }, - { p: '10', minQ: '1', maxQ: '56' }, - ]).length - ).toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: '-10', minQ: '-5', maxQ: '-2' }, - { p: '0', minQ: '0', maxQ: '0' }, - { p: '10', minQ: '1', maxQ: '56' }, - ]).length - ).toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: '-10', minQ: '-5', maxQ: '-2' }, - { p: '0', minQ: '0,8', maxQ: '1' }, - { p: '-3', minQ: '-6.5', maxQ: '-2' }, - { p: '10', minQ: '1', maxQ: '56' }, - ]).length - ).toBe(0); - - // Not enough points - expect(checkReactiveCapabilityCurve([]).length).not.toBe(0); - expect(checkReactiveCapabilityCurve([{ p: '0', minQ: '0', maxQ: '0' }]).length).not.toBe(0); - - // Not unique P values - expect( - checkReactiveCapabilityCurve([ - { p: '10', minQ: '-5', maxQ: '-2' }, - { p: '10', minQ: '1', maxQ: '56' }, - ]).length - ).not.toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: '-10', minQ: '-5', maxQ: '-2' }, - { p: '-0', minQ: '0', maxQ: '0' }, - { p: '0', minQ: '1', maxQ: '56' }, - ]).length - ).not.toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: '-0', minQ: '0', maxQ: '0' }, - { p: '0', minQ: '0', maxQ: '0' }, - ]).length - ).not.toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: '0', minQ: '0', maxQ: '0' }, - { p: '0.0', minQ: '0', maxQ: '0' }, - ]).length - ).not.toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: ',0', minQ: '0', maxQ: '0' }, - { p: '0', minQ: '0', maxQ: '0' }, - ]).length - ).not.toBe(0); - - // Pmin and Pmax values are not in the begining and end of the array - expect( - checkReactiveCapabilityCurve([ - { p: '0', minQ: '-5', maxQ: '-2' }, - { p: '-10', minQ: '0', maxQ: '0' }, - { p: '10', minQ: '1', maxQ: '56' }, - ]).length - ).not.toBe(0); - - // P values between Pmin and Pmax are below Pmin or above Pmax - expect( - checkReactiveCapabilityCurve([ - { p: '-10', minQ: '-5', maxQ: '-2' }, - { p: '260', minQ: '0', maxQ: '0' }, - { p: '10', minQ: '1', maxQ: '56' }, - ]).length - ).not.toBe(0); - expect( - checkReactiveCapabilityCurve([ - { p: '-10', minQ: '-5', maxQ: '-2' }, - { p: '-20', minQ: '0', maxQ: '0' }, - { p: '10', minQ: '1', maxQ: '56' }, - ]).length - ).not.toBe(0); -}); - -test('validation-functions.validateField.forceValidation', () => { - expect(validateField(600, { valueLessThan: 10 }).error).toBe(true); - expect(validateField(600, { valueLessThan: 10 }, true).error).toBe(false); - expect(validateField(600, { valueLessThan: 10, forceValidation: true }, true).error).toBe(true); -}); diff --git a/src/components/utils/validation-functions.test.ts b/src/components/utils/validation-functions.test.ts new file mode 100644 index 0000000000..6ca25349e6 --- /dev/null +++ b/src/components/utils/validation-functions.test.ts @@ -0,0 +1,410 @@ +/** + * 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 { + validateValueIsANumber, + exportedForTesting, + validateValueIsLessThanOrEqualTo, + validateValueIsLessThan, + validateField, + checkReactiveCapabilityCurve, +} from './validation-functions'; + +const { toNumber, isBlankOrEmpty } = exportedForTesting; + +describe('validation-functions.isBlankOrEmpty', () => { + test.each(['hello', '0', 0, false, true])('not expect %j to be blank or empty', (value) => + expect(isBlankOrEmpty(value)).toBeFalse() + ); + test.each([' ', null, undefined])('expect %j to be blank or empty', (value) => + expect(isBlankOrEmpty(value)).toBeTrue() + ); +}); + +describe('validation-functions.toNumber', () => { + describe('Invalid values must result in NaN', () => + test.each(['', undefined, null, false, true, NaN, 'hello', { 0: 1 }, [10]])('$(%j) === NaN', (value) => + expect(toNumber(value)).toBeNaN() + )); + test('String must be trimmed', () => expect(toNumber(' 0020,5000 ')).toBe(20.5)); + test('Numbers equals themselves', () => expect(toNumber(0)).toBe(0)); + test('Decimal fraction notation is also valid', () => expect(toNumber(',0')).toBe(0)); + test("Floating number aren't rounded", () => expect(toNumber(0.99999)).not.toBe(1)); + describe('Converted values must be equal in these cases', () => + test.each([-0, 0.0, -0.0, '-0', '0,0', ',0', '-.000', '-,000'])('$(%j) === 0', (value) => + // FIXME toNumber(0) === -0 for jest, so .toBe(...) fail + expect(toNumber(value) === toNumber(0)).toBeTrue() + )); +}); + +describe('validation-functions.validateValueIsANumber', () => { + test.each([ + 10, + 0, + '0', + '-0', + '-10', + '-.0', + '-,0', + '.0', + '0.510', + ',510', + '-,510', + ' -,510 ', + 55.51, + -55.51, + '-55,51', + '-55,', + '-55.', + ])('%j is valid', (value) => expect(validateValueIsANumber(value)).toBeTrue()); + test.each([null, NaN, undefined, false, 'hello', '15.564,54', true, ''])('%j is invalid', (value) => + expect(validateValueIsANumber(value)).toBeFalse() + ); +}); + +describe('validation-functions.validateValueIsLessThanOrEqualTo', () => { + test.each([ + [0, 0], + [0, 1], + [1.0, 1], + [0.9, 0.91], + [-0.91, -0.9], + ['90,54', 154], + ['-90,54', -90.54], + ])('%j ≤ %j', (a, b) => expect(validateValueIsLessThanOrEqualTo(a, b)).toBeTrue()); + test.each([ + [false, true], + [false, false], + [undefined, undefined], + [true, true], + [1.00001, 1], + [0.00001, -1], + ['1.00001', '1'], + ['.00001', '-1'], + [0, false], + [0, true], + [0, NaN], + [NaN, NaN], + [NaN, undefined], + ['a', 9999], + ])('%j different than %j', (a, b) => expect(validateValueIsLessThanOrEqualTo(a, b)).toBeFalse()); +}); + +describe('validation-functions.validateValueIsLessThan', () => { + test.each([ + [0, 1], + [0.9, 0.91], + [-0.91, -0.9], + ['90,54', 154], + ])('%j < %j', (a, b) => expect(validateValueIsLessThan(a, b)).toBeTrue()); + test.each([ + [0, 0], + [1.0, 1], + ['-90,54', -90.54], + [false, true], + [false, false], + [undefined, undefined], + [true, true], + [1.00001, 1], + [0.00001, -1], + ['1.00001', '1'], + ['.00001', '-1'], + [0, false], + [0, true], + [0, NaN], + [NaN, NaN], + [NaN, undefined], + ['a', 9999], + ['-0', '0'], + ['.0', '0'], + [',0', '0'], + ['-,0', '0'], + [0.0, 0], + ])('%j different than %j', (a, b) => expect(validateValueIsLessThan(a, b)).toBeFalse()); +}); + +describe('validation-functions.validateField', () => { + test('isFieldRequired', () => { + expect(validateField(500, { isFieldRequired: true }).error).toBeFalse(); + expect(validateField(0, { isFieldRequired: true, isFieldNumeric: true }).error).toBeFalse(); + expect(validateField('hello', { isFieldRequired: true }).error).toBeFalse(); + expect(validateField('', { isFieldRequired: true }).error).toBeTrue(); + expect(validateField(' ', { isFieldRequired: true }).error).toBeTrue(); + expect(validateField(null, { isFieldRequired: true }).error).toBeTrue(); + expect(validateField(undefined, { isFieldRequired: true }).error).toBeTrue(); + }); + + test('isFieldNumeric', () => { + expect(validateField(500, { isFieldNumeric: true }).error).toBeFalse(); + expect(validateField(0, { isFieldNumeric: true }).error).toBeFalse(); + expect(validateField(-0.0, { isFieldNumeric: true }).error).toBeFalse(); + expect(validateField('hello', { isFieldNumeric: true }).error).toBeTrue(); + expect(validateField('', { isFieldNumeric: true }).error).toBeFalse(); // If the field is not required, there should be no validation error + expect(validateField('', { isFieldRequired: true, isFieldNumeric: true }).error).toBeTrue(); + expect(validateField(' ', { isFieldRequired: true, isFieldNumeric: true }).error).toBeTrue(); + expect(validateField(null, { isFieldRequired: true, isFieldNumeric: true }).error).toBeTrue(); + expect( + validateField(undefined, { + isFieldRequired: true, + isFieldNumeric: true, + }).error + ).toBeTrue(); + }); + + test('valueGreaterThanOrEqualTo', () => { + expect(validateField(500, { valueGreaterThanOrEqualTo: 10 }).error).toBeFalse(); + expect(validateField(500, { valueGreaterThanOrEqualTo: 0 }).error).toBeFalse(); + expect(validateField(0, { valueGreaterThanOrEqualTo: 0 }).error).toBeFalse(); + expect(validateField(0, { valueGreaterThanOrEqualTo: 10 }).error).toBeTrue(); + expect(validateField(-500, { valueGreaterThanOrEqualTo: 10 }).error).toBeTrue(); + expect(validateField(-500, { valueGreaterThanOrEqualTo: 0 }).error).toBeTrue(); + expect(validateField(0, { valueGreaterThanOrEqualTo: -10 }).error).toBeFalse(); + expect( + validateField('', { + valueGreaterThanOrEqualTo: 2, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect( + validateField('', { + valueGreaterThanOrEqualTo: 3, + isFieldRequired: true, + }).error + ).toBeTrue(); + expect( + validateField(' ', { + valueGreaterThanOrEqualTo: 2, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect( + validateField(' ', { + valueGreaterThanOrEqualTo: 3, + isFieldRequired: true, + }).error + ).toBeTrue(); + expect( + validateField(null, { + valueGreaterThanOrEqualTo: 2, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect( + validateField(null, { + valueGreaterThanOrEqualTo: 3, + isFieldRequired: true, + }).error + ).toBeTrue(); + expect( + validateField(undefined, { + valueGreaterThanOrEqualTo: 2, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect( + validateField(undefined, { + valueGreaterThanOrEqualTo: 3, + isFieldRequired: true, + }).error + ).toBeTrue(); + }); + + test('valueLessThanOrEqualTo', () => { + expect(validateField(-600, { valueLessThanOrEqualTo: 10 }).error).toBeFalse(); + expect(validateField(-600, { valueLessThanOrEqualTo: 0 }).error).toBeFalse(); + expect(validateField(0, { valueLessThanOrEqualTo: -10 }).error).toBeTrue(); + expect(validateField(600, { valueLessThanOrEqualTo: 10 }).error).toBeTrue(); + expect(validateField(600, { valueLessThanOrEqualTo: 0 }).error).toBeTrue(); + expect(validateField(0, { valueLessThanOrEqualTo: 0 }).error).toBeFalse(); + expect(validateField(0, { valueLessThanOrEqualTo: 10 }).error).toBeFalse(); + expect( + validateField('', { + valueLessThanOrEqualTo: 20, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect(validateField('', { valueLessThanOrEqualTo: 20, isFieldRequired: true }).error).toBeTrue(); + expect( + validateField(' ', { + valueLessThanOrEqualTo: 20, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect( + validateField(' ', { + valueLessThanOrEqualTo: 20, + isFieldRequired: true, + }).error + ).toBeTrue(); + expect( + validateField(null, { + valueLessThanOrEqualTo: 20, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect( + validateField(null, { + valueLessThanOrEqualTo: 20, + isFieldRequired: true, + }).error + ).toBeTrue(); + expect( + validateField(undefined, { + valueLessThanOrEqualTo: 20, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect( + validateField(undefined, { + valueLessThanOrEqualTo: 20, + isFieldRequired: true, + }).error + ).toBeTrue(); + }); + + test('valueLessThan', () => { + expect(validateField(600, { valueLessThan: 10 }).error).toBeTrue(); + expect(validateField(600, { valueLessThan: 0 }).error).toBeTrue(); + expect(validateField(0, { valueLessThan: 0 }).error).toBeTrue(); + expect(validateField(0, { valueLessThan: 10 }).error).toBeFalse(); + expect(validateField(-600, { valueLessThan: 10 }).error).toBeFalse(); + expect(validateField(-600, { valueLessThan: 0 }).error).toBeFalse(); + expect(validateField(0, { valueLessThan: -10 }).error).toBeTrue(); + expect(validateField('', { valueLessThan: 6, isFieldRequired: false }).error).toBeFalse(); + expect(validateField('', { valueLessThan: 6, isFieldRequired: true }).error).toBeTrue(); + expect(validateField(' ', { valueLessThan: 6, isFieldRequired: false }).error).toBeFalse(); + expect(validateField(' ', { valueLessThan: 6, isFieldRequired: true }).error).toBeTrue(); + expect(validateField(null, { valueLessThan: 6, isFieldRequired: false }).error).toBeFalse(); + expect(validateField(null, { valueLessThan: 6, isFieldRequired: true }).error).toBeTrue(); + expect(validateField(undefined, { valueLessThan: 6, isFieldRequired: false }).error).toBeFalse(); + expect(validateField(undefined, { valueLessThan: 6, isFieldRequired: true }).error).toBeTrue(); + }); + + test('valueGreaterThan', () => { + expect(validateField(600, { valueGreaterThan: 10 }).error).toBeFalse(); + expect(validateField(600, { valueGreaterThan: 0 }).error).toBeFalse(); + expect(validateField(0, { valueGreaterThan: 0 }).error).toBeTrue(); + expect(validateField(0, { valueGreaterThan: 10 }).error).toBeTrue(); + expect(validateField(-600, { valueGreaterThan: 10 }).error).toBeTrue(); + expect(validateField(-600, { valueGreaterThan: 0 }).error).toBeTrue(); + expect(validateField(0, { valueGreaterThan: -10 }).error).toBeFalse(); + expect(validateField('', { valueGreaterThan: 2, isFieldRequired: false }).error).toBeFalse(); + expect(validateField('', { valueGreaterThan: 2, isFieldRequired: true }).error).toBeTrue(); + expect(validateField(' ', { valueGreaterThan: 2, isFieldRequired: false }).error).toBeFalse(); + expect(validateField(' ', { valueGreaterThan: 2, isFieldRequired: true }).error).toBeTrue(); + expect(validateField(null, { valueGreaterThan: 2, isFieldRequired: false }).error).toBeFalse(); + expect(validateField(null, { valueGreaterThan: 2, isFieldRequired: true }).error).toBeTrue(); + expect( + validateField(undefined, { + valueGreaterThan: 2, + isFieldRequired: false, + }).error + ).toBeFalse(); + expect(validateField(undefined, { valueGreaterThan: 2, isFieldRequired: true }).error).toBeTrue(); + }); + + test('validation-functions.checkReactiveCapabilityCurve', () => { + // Reactive capability curve default format : [{ p: '', minQ: '', maxQ: '' }, { p: '', minQ: '', maxQ: '' }] + + // Correct reactive cabability curves + expect( + checkReactiveCapabilityCurve([ + { p: 0, minQ: 0, maxQ: 0 }, + { p: 10, minQ: 0, maxQ: 0 }, + ]).length + ).toBe(0); + expect( + checkReactiveCapabilityCurve([ + { p: -10, minQ: -5, maxQ: -2 }, + { p: 10, minQ: 1, maxQ: 56 }, + ]).length + ).toBe(0); + expect( + checkReactiveCapabilityCurve([ + { p: -10, minQ: -5, maxQ: -2 }, + { p: 0, minQ: 0, maxQ: 0 }, + { p: 10, minQ: 1, maxQ: 56 }, + ]).length + ).toBe(0); + expect( + checkReactiveCapabilityCurve([ + { p: -10, minQ: -5, maxQ: -2 }, + // { p: 0, minQ: '0,8', maxQ: 1 }, + { p: -3, minQ: -6.5, maxQ: -2 }, + { p: 10, minQ: 1, maxQ: 56 }, + ]).length + ).toBe(0); + + // Not enough points + expect(checkReactiveCapabilityCurve([]).length).not.toBe(0); + expect(checkReactiveCapabilityCurve([{ p: 0, minQ: 0, maxQ: 0 }]).length).not.toBe(0); + + // Not unique P values + expect( + checkReactiveCapabilityCurve([ + { p: 10, minQ: -5, maxQ: -2 }, + { p: 10, minQ: 1, maxQ: 56 }, + ]).length + ).not.toBe(0); + expect( + checkReactiveCapabilityCurve([ + { p: -10, minQ: -5, maxQ: -2 }, + { p: -0, minQ: 0, maxQ: 0 }, + { p: 0, minQ: 1, maxQ: 56 }, + ]).length + ).not.toBe(0); + expect( + checkReactiveCapabilityCurve([ + { p: -0, minQ: 0, maxQ: 0 }, + { p: 0, minQ: 0, maxQ: 0 }, + ]).length + ).not.toBe(0); + expect( + checkReactiveCapabilityCurve([ + { p: 0, minQ: 0, maxQ: 0 }, + // { p: '0.0', minQ: 0, maxQ: 0 }, + ]).length + ).not.toBe(0); + expect( + checkReactiveCapabilityCurve([ + // { p: ',0', minQ: 0, maxQ: 0 }, + { p: 0, minQ: 0, maxQ: 0 }, + ]).length + ).not.toBe(0); + + // Pmin and Pmax values are not in the beginning and end of the array + expect( + checkReactiveCapabilityCurve([ + { p: 0, minQ: -5, maxQ: -2 }, + { p: -10, minQ: 0, maxQ: 0 }, + { p: 10, minQ: 1, maxQ: 56 }, + ]).length + ).not.toBe(0); + + // P values between Pmin and Pmax are below Pmin or above Pmax + expect( + checkReactiveCapabilityCurve([ + { p: -10, minQ: -5, maxQ: -2 }, + { p: 260, minQ: 0, maxQ: 0 }, + { p: 10, minQ: 1, maxQ: 56 }, + ]).length + ).not.toBe(0); + expect( + checkReactiveCapabilityCurve([ + { p: -10, minQ: -5, maxQ: -2 }, + { p: -20, minQ: 0, maxQ: 0 }, + { p: 10, minQ: 1, maxQ: 56 }, + ]).length + ).not.toBe(0); + }); + + test('forceValidation', () => { + expect(validateField(600, { valueLessThan: 10 }).error).toBeTrue(); + expect(validateField(600, { valueLessThan: 10 }, true).error).toBeFalse(); + expect(validateField(600, { valueLessThan: 10, forceValidation: true }, true).error).toBeTrue(); + }); +}); diff --git a/src/components/utils/validation-functions.js b/src/components/utils/validation-functions.ts similarity index 85% rename from src/components/utils/validation-functions.js rename to src/components/utils/validation-functions.ts index 9066ba906c..0b6a8747b6 100644 --- a/src/components/utils/validation-functions.js +++ b/src/components/utils/validation-functions.ts @@ -5,16 +5,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import type { MessageDescriptor } from 'react-intl'; +import type { ReactiveCapabilityCurvePointsData } from '../dialogs/network-modifications/vsc/converter-station/converter-station-utils'; + const NO_ERROR = { error: false, errorMsgId: null, -}; +} as const; -/* +/** * Returns a Number corresponding to provided value, or NaN if not a valid number per * Gridsuite's standard (allows either coma or dots for decimal). */ -export function toNumber(value) { +export function toNumber(value: unknown) { if (typeof value === 'number') { return value; } else if (typeof value === 'string') { @@ -27,11 +30,12 @@ export function toNumber(value) { return NaN; } -/* +//type IsEmptyString = T extends null | undefined ? true : T extends string ? Trim extends '' ? true : false : false; +/** * Returns true if value is either undefined, null, empty or only contains whitespaces. * Otherwise, if value is a boolean or a number, returns false. */ -export function isBlankOrEmpty(value) { +export function isBlankOrEmpty(value: T) { if (value === undefined || value === null) { return true; } @@ -41,23 +45,23 @@ export function isBlankOrEmpty(value) { return false; } -/* +/** * Returns true if the value is a valid number, per Gridsuite's standard (allows either coma or dots for decimal). */ -export function validateValueIsANumber(value) { +export function validateValueIsANumber(value: unknown) { if (value == null || value === '') { return false; } return !isNaN(toNumber(value)); } -/* +/** * Returns true IF and ONLY IF : * - the first parameter value is a valid number * - the second parameter valueToCompareTo is a valid number * - the first parameter's value is lower or equal to the second's */ -export function validateValueIsLessThanOrEqualTo(value, valueToCompareTo) { +export function validateValueIsLessThanOrEqualTo(value: unknown, valueToCompareTo: unknown) { return ( validateValueIsANumber(value) && validateValueIsANumber(valueToCompareTo) && @@ -65,13 +69,13 @@ export function validateValueIsLessThanOrEqualTo(value, valueToCompareTo) { ); } -/* +/** * Returns true IF and ONLY IF : * - the first parameter value is a valid number * - the second parameter valueToCompareTo is a valid number * - the first parameter's value is greater or equal to the second's */ -export function validateValueIsGreaterThanOrEqualTo(value, valueToCompareTo) { +export function validateValueIsGreaterThanOrEqualTo(value: unknown, valueToCompareTo: unknown) { return ( validateValueIsANumber(value) && validateValueIsANumber(valueToCompareTo) && @@ -79,13 +83,13 @@ export function validateValueIsGreaterThanOrEqualTo(value, valueToCompareTo) { ); } -/* +/** * Returns true IF and ONLY IF : * - the first parameter value is a valid number * - the second parameter valueToCompareTo is a valid number * - the first parameter's value is lower than the second's */ -export function validateValueIsLessThan(value, valueToCompareTo) { +export function validateValueIsLessThan(value: unknown, valueToCompareTo: unknown) { return ( validateValueIsANumber(value) && validateValueIsANumber(valueToCompareTo) && @@ -93,13 +97,13 @@ export function validateValueIsLessThan(value, valueToCompareTo) { ); } -/* +/** * Returns true IF and ONLY IF : * - the first parameter value is a valid number * - the second parameter valueToCompareTo is a valid number * - the first parameter's value is greater than the second's */ -export function validateValueIsGreaterThan(value, valueToCompareTo) { +export function validateValueIsGreaterThan(value: unknown, valueToCompareTo: unknown) { return ( validateValueIsANumber(value) && validateValueIsANumber(valueToCompareTo) && @@ -107,11 +111,11 @@ export function validateValueIsGreaterThan(value, valueToCompareTo) { ); } -/* +/** * Rule : if the field is NOT required (toValidate.isFieldRequired is either undefined or equals to false), * then any check that applies to the value will pass if the value is empty. */ -export function validateField(value, toValidate, disabled = false) { +export function validateField(value: unknown, toValidate: any, disabled = false) { if (disabled && !toValidate.forceValidation) { return NO_ERROR; } @@ -153,14 +157,13 @@ export function validateField(value, toValidate, disabled = false) { } /** - * Checks if the provided reactive capabilty curve is valid. Returns a list of - * errors if any, or an empty array otherwise. + * Checks if the provided reactive capability curve is valid. * @param reactiveCapabilityCurve an array of reactive capability curve points of * this format : [{ p: '', minQ: '', maxQ: '' }, { p: '', minQ: '', maxQ: '' }] * @returns An array of error messages. If there is no error, returns an empty array. */ -export function checkReactiveCapabilityCurve(reactiveCapabilityCurve) { - let errorMessages = []; +export function checkReactiveCapabilityCurve(reactiveCapabilityCurve: ReactiveCapabilityCurvePointsData[]) { + let errorMessages: NonNullable[] = []; // At least four points must be set if (reactiveCapabilityCurve.length < 2) { @@ -203,7 +206,7 @@ export function checkReactiveCapabilityCurve(reactiveCapabilityCurve) { return errorMessages; } -function makeErrorRecord(msgId) { +function makeErrorRecord(msgId: MessageDescriptor['id']) { if (msgId === undefined) { console.warn('Error message id missing in validation function !'); } @@ -216,4 +219,4 @@ function makeErrorRecord(msgId) { export const exportedForTesting = { toNumber, isBlankOrEmpty, -}; +} as const; diff --git a/src/components/voltage-level-choice.jsx b/src/components/voltage-level-choice.jsx index fc0084c92f..3b969342ff 100644 --- a/src/components/voltage-level-choice.jsx +++ b/src/components/voltage-level-choice.jsx @@ -13,7 +13,7 @@ import ListItemText from '@mui/material/ListItemText'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; import { getNominalVoltageColor } from '../utils/colors'; -import { useNameOrId } from './utils/equipmentInfosHandler'; +import useNameOrId from './utils/use-name-or-id'; import { Box } from '@mui/system'; const styles = { diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 982ac0d063..c9653b091e 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -33,7 +33,7 @@ import { import { Action } from 'redux'; import { GsLang, GsLangUser, GsTheme, Identifiable } from '@gridsuite/commons-ui'; import { UUID } from 'crypto'; -import { UnknownArray } from 'type-fest'; +import type { UnknownArray } from 'type-fest'; import NetworkModificationTreeModel from '../components/graph/network-modification-tree-model'; import { NodeInsertModes } from '../components/graph/nodes/node-insert-modes'; import { LineFlowColorMode, LineFlowMode } from '@powsybl/diagram-viewer'; diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 277748432a..87fbc9a522 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -288,7 +288,7 @@ import { import { UUID } from 'crypto'; import { Filter } from '../components/results/common/results-global-filter'; import { LineFlowColorMode, LineFlowMode } from '@powsybl/diagram-viewer'; -import { UnknownArray, ValueOf } from 'type-fest'; +import type { UnknownArray, ValueOf } from 'type-fest'; import { Node } from '@xyflow/react'; import { SortConfigType, SortWay } from '../hooks/use-aggrid-sort'; import { CopyType, StudyDisplayMode } from '../components/network-modification.type'; diff --git a/src/test.d.ts b/src/test.d.ts new file mode 100644 index 0000000000..40c2a3e500 --- /dev/null +++ b/src/test.d.ts @@ -0,0 +1,8 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +///