From 109899dd5dddd3e5007220d4fd424d596c042df1 Mon Sep 17 00:00:00 2001 From: Radouane Khouadri Date: Fri, 21 Aug 2026 15:06:34 +0200 Subject: [PATCH 1/6] use universal usetabs hook --- .../modification/BatteryModificationForm.tsx | 19 ++- .../GeneratorModificationForm.tsx | 18 ++- .../network-modifications/hooks/index.ts | 7 - .../hooks/useTabsWithError.ts | 54 -------- src/features/network-modifications/index.ts | 1 - .../line/common/LineForm.tsx | 17 ++- .../load/common/LoadForm.tsx | 17 ++- .../static-var-compensator-creation-form.tsx | 17 ++- .../common/TwoWindingsTransformerForm.tsx | 17 ++- .../creation/VoltageLevelCreationForm.tsx | 18 ++- src/features/parameters/common/hook/index.ts | 1 - .../parameters/common/hook/use-tabs.ts | 94 ------------- .../dynamic-margin-calculation-form.tsx | 4 +- .../dynamic-margin-calculation-inline.tsx | 7 +- .../dynamic-security-analysis-inline.tsx | 4 +- ...amic-security-analysis-parameters-form.tsx | 3 +- .../dynamic-simulation-inline.tsx | 5 +- .../dynamic-simulation-parameters-form.tsx | 4 +- .../loadflow/use-load-flow-parameters-form.ts | 5 +- ...-network-visualizations-parameters-form.ts | 5 +- .../use-security-analysis-parameters-form.ts | 5 +- .../sensi/sensitivity-parameters-selector.tsx | 86 ++++++------ .../use-short-circuit-parameters-form.ts | 6 +- .../use-voltage-init-parameters-form.ts | 5 +- src/hooks/index.ts | 1 + src/hooks/use-tabs.ts | 123 ++++++++++++++++++ 26 files changed, 283 insertions(+), 260 deletions(-) delete mode 100644 src/features/network-modifications/hooks/index.ts delete mode 100644 src/features/network-modifications/hooks/useTabsWithError.ts delete mode 100644 src/features/parameters/common/hook/use-tabs.ts create mode 100644 src/hooks/use-tabs.ts diff --git a/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx b/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx index 575fed6cc..5217aaf9b 100644 --- a/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx +++ b/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx @@ -6,13 +6,13 @@ */ import { Grid, Stack } from '@mui/material'; -import { useWatch } from 'react-hook-form'; +import { useFormState, useWatch } from 'react-hook-form'; import { BatteryDialogHeader, BatteryDialogHeaderProps } from './BatteryDialogHeader'; import { BatteryDialogTabs } from './BatteryDialogTabs'; import { BatteryDialogTabsContent, BatteryDialogTabsContentProps } from './BatteryDialogTabsContent'; import { BATTERY_TAB_FIELDS, BatteryDialogTab } from './batteryTabs.utils'; import { EquipmentType, FieldConstants, Identifiable } from '../../../../utils'; -import { useTabsWithError } from '../../hooks'; +import { useTabs } from '../../../../hooks'; interface BatteryModificationFormProps extends BatteryDialogHeaderProps, Omit { @@ -27,10 +27,17 @@ export function BatteryModificationForm({ PositionDiagramPane, fetchVoltageLevelEquipments, }: Readonly) { - const { tabIndex, setTabIndex, tabIndexesWithError } = useTabsWithError( - BATTERY_TAB_FIELDS, - BatteryDialogTab.CONNECTIVITY_TAB - ); + const { errors } = useFormState(); + const { + selectedTab: tabIndex, + setSelectedTab: setTabIndex, + tabsWithError: tabIndexesWithError, + } = useTabs({ + defaultTab: BatteryDialogTab.CONNECTIVITY_TAB, + errors, + tabFields: BATTERY_TAB_FIELDS, + }); + const equipmentId = useWatch({ name: FieldConstants.EQUIPMENT_ID }); return ( diff --git a/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx b/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx index def92899c..4245c97b4 100644 --- a/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx +++ b/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx @@ -6,13 +6,13 @@ */ import { Grid, Stack } from '@mui/material'; -import { useWatch } from 'react-hook-form'; +import { useFormState, useWatch } from 'react-hook-form'; import { GENERATOR_TAB_FIELDS, GeneratorDialogTab } from './generatorTabs.utils'; import { GeneratorDialogHeader, GeneratorDialogHeaderProps } from './GeneratorDialogHeader'; import { GeneratorDialogTabs } from './GeneratorDialogTabs'; import { GeneratorDialogTabsContent, GeneratorDialogTabsContentProps } from './GeneratorDialogTabsContent'; import { EquipmentType, FieldConstants, Identifiable } from '../../../../utils'; -import { useTabsWithError } from '../../hooks'; +import { useTabs } from '../../../../hooks'; interface GeneratorModificationFormProps extends GeneratorDialogHeaderProps, Omit { @@ -27,10 +27,16 @@ export function GeneratorModificationForm({ PositionDiagramPane, fetchVoltageLevelEquipments, }: Readonly) { - const { tabIndex, setTabIndex, tabIndexesWithError } = useTabsWithError( - GENERATOR_TAB_FIELDS, - GeneratorDialogTab.CONNECTIVITY_TAB - ); + const { errors } = useFormState(); + const { + selectedTab: tabIndex, + setSelectedTab: setTabIndex, + tabsWithError: tabIndexesWithError, + } = useTabs({ + defaultTab: GeneratorDialogTab.CONNECTIVITY_TAB, + errors, + tabFields: GENERATOR_TAB_FIELDS, + }); const equipmentId = useWatch({ name: FieldConstants.EQUIPMENT_ID }); return ( diff --git a/src/features/network-modifications/hooks/index.ts b/src/features/network-modifications/hooks/index.ts deleted file mode 100644 index 9a964f366..000000000 --- a/src/features/network-modifications/hooks/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2026, 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 * from './useTabsWithError'; diff --git a/src/features/network-modifications/hooks/useTabsWithError.ts b/src/features/network-modifications/hooks/useTabsWithError.ts deleted file mode 100644 index 919fb93fb..000000000 --- a/src/features/network-modifications/hooks/useTabsWithError.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useFormState } from 'react-hook-form'; -import { FieldConstants } from '../../../utils'; - -type TabFieldsMap = Readonly>>; - -/** - * Manages tab navigation with automatic error highlighting for react-hook-form-based tabbed forms. - * - * @param tabFields - Maps each tab index to the field names it owns. Must be a module-level - * constant to remain stable across renders. - * @param initialTab - The tab to show on first render. - */ -export function useTabsWithError(tabFields: TabFieldsMap, initialTab: T) { - const { errors, submitCount } = useFormState(); - const [tabIndex, setTabIndex] = useState(initialTab); - const tabIndexRef = useRef(tabIndex); - - // Computed during render (not stored in state) so tab error styles update immediately when - // an error is cleared. RHF mutates the errors object rather than replacing it, so a useEffect - // dependency on `errors` would never fire on error removal. - const tabIndexesWithError = (Object.keys(tabFields).map(Number) as T[]).filter((tab) => - tabFields[tab]!.some((field) => errors[field] !== undefined) - ); - - // Auto-navigate to the first errored tab on submit. Using submitCount as the trigger is the - // only reliable signal that the user explicitly requested validation — reacting to errors - // directly would cause mid-input tab jumps during on-change revalidation. - const tabIndexesWithErrorRef = useRef(tabIndexesWithError); - tabIndexesWithErrorRef.current = tabIndexesWithError; - useEffect(() => { - const tabs = tabIndexesWithErrorRef.current; - if (tabs.length > 0 && !tabs.includes(tabIndexRef.current)) { - const tab = tabs[0]; - tabIndexRef.current = tab; - setTabIndex(tab); - } - // submitCount is the intentional trigger; refs never change identity. - }, [submitCount]); - - const handleSetTabIndex = useCallback((tab: T) => { - tabIndexRef.current = tab; - setTabIndex(tab); - }, []); - - return { tabIndex, setTabIndex: handleSetTabIndex, tabIndexesWithError }; -} diff --git a/src/features/network-modifications/index.ts b/src/features/network-modifications/index.ts index 0d4e8d050..0c8b64a7d 100644 --- a/src/features/network-modifications/index.ts +++ b/src/features/network-modifications/index.ts @@ -7,7 +7,6 @@ export * from './common'; export * from './equipmentDeletion'; -export * from './hooks'; export * from './substation'; export * from './voltageLevel'; export * from './load'; diff --git a/src/features/network-modifications/line/common/LineForm.tsx b/src/features/network-modifications/line/common/LineForm.tsx index 81ac50838..3b52cd3ab 100644 --- a/src/features/network-modifications/line/common/LineForm.tsx +++ b/src/features/network-modifications/line/common/LineForm.tsx @@ -6,11 +6,12 @@ */ import { Box, Stack } from '@mui/material'; +import { useFormState } from 'react-hook-form'; import { LINE_TAB_FIELDS, LineDialogTab } from './line.utils'; import { LineDialogHeader, LineDialogHeaderProps } from './LineDialogHeader'; import { LineDialogTabs } from './LineDialogTabs'; import { LineDialogTabsContent, LineDialogTabsContentProps } from './LineDialogTabsContent'; -import { useTabsWithError } from '../../hooks'; +import { useTabs } from '../../../../hooks'; interface LineFormProps extends LineDialogHeaderProps, Omit {} @@ -23,10 +24,16 @@ export function LineForm({ isModification = false, withConnectivity = true, }: Readonly) { - const { tabIndex, setTabIndex, tabIndexesWithError } = useTabsWithError( - LINE_TAB_FIELDS, - withConnectivity ? LineDialogTab.CONNECTIVITY_TAB : LineDialogTab.CHARACTERISTICS_TAB - ); + const { errors } = useFormState(); + const { + selectedTab: tabIndex, + setSelectedTab: setTabIndex, + tabsWithError: tabIndexesWithError, + } = useTabs({ + defaultTab: withConnectivity ? LineDialogTab.CONNECTIVITY_TAB : LineDialogTab.CHARACTERISTICS_TAB, + errors, + tabFields: LINE_TAB_FIELDS, + }); return ( diff --git a/src/features/network-modifications/load/common/LoadForm.tsx b/src/features/network-modifications/load/common/LoadForm.tsx index 28551ca8f..78ae5cd6a 100644 --- a/src/features/network-modifications/load/common/LoadForm.tsx +++ b/src/features/network-modifications/load/common/LoadForm.tsx @@ -6,11 +6,12 @@ */ import { Grid, Stack } from '@mui/material'; +import { useFormState } from 'react-hook-form'; import { LOAD_TAB_FIELDS, LoadDialogTab } from './load.utils'; import { LoadDialogHeader, LoadDialogHeaderProps } from './LoadDialogHeader'; import { LoadDialogTabs } from './LoadDialogTabs'; import { LoadDialogTabsContent, LoadDialogTabsContentProps } from './LoadDialogTabsContent'; -import { useTabsWithError } from '../../hooks'; +import { useTabs } from '../../../../hooks'; interface LoadFormProps extends LoadDialogHeaderProps, Omit {} @@ -22,10 +23,16 @@ export function LoadForm({ fetchBusesOrBusbarSections, PositionDiagramPane, }: Readonly) { - const { tabIndex, setTabIndex, tabIndexesWithError } = useTabsWithError( - LOAD_TAB_FIELDS, - LoadDialogTab.CONNECTIVITY_TAB - ); + const { errors } = useFormState(); + const { + selectedTab: tabIndex, + setSelectedTab: setTabIndex, + tabsWithError: tabIndexesWithError, + } = useTabs({ + defaultTab: LoadDialogTab.CONNECTIVITY_TAB, + errors, + tabFields: LOAD_TAB_FIELDS, + }); return ( diff --git a/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx b/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx index 8dd1e139e..923234107 100644 --- a/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx +++ b/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx @@ -6,6 +6,7 @@ */ import { Box, Stack } from '@mui/material'; +import { useFormState } from 'react-hook-form'; import { ConnectivityNetworkProps } from '../../common'; import { STATIC_VAR_COMPENSATOR_TAB_FIELDS, @@ -14,7 +15,7 @@ import { import { StaticVarCompensatorDialogHeader, StaticVarCompensatorDialogTabs } from '../common'; import { StaticVarCompensatorTabsContent } from '../common/static-var-compensator-tabs-content'; import { EquipmentType, Identifiable } from '../../../../utils'; -import { useTabsWithError } from '../../hooks'; +import { useTabs } from '../../../../hooks'; export interface StaticVarCompensatorCreationFormProps extends ConnectivityNetworkProps { fetchVoltageLevelEquipments: (voltageLevelId: string) => Promise<(Identifiable & { type: EquipmentType })[]>; @@ -26,10 +27,16 @@ export function StaticVarCompensatorCreationForm({ fetchBusesOrBusbarSections, fetchVoltageLevelEquipments, }: StaticVarCompensatorCreationFormProps) { - const { tabIndex, setTabIndex, tabIndexesWithError } = useTabsWithError( - STATIC_VAR_COMPENSATOR_TAB_FIELDS, - StaticVarCompensatorDialogTab.CONNECTIVITY_TAB - ); + const { errors } = useFormState(); + const { + selectedTab: tabIndex, + setSelectedTab: setTabIndex, + tabsWithError: tabIndexesWithError, + } = useTabs({ + defaultTab: StaticVarCompensatorDialogTab.CONNECTIVITY_TAB, + errors, + tabFields: STATIC_VAR_COMPENSATOR_TAB_FIELDS, + }); return ( diff --git a/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx b/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx index 3b5cec74c..080a058f3 100644 --- a/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx +++ b/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx @@ -6,17 +6,18 @@ */ import { Box, Stack } from '@mui/material'; +import { useFormState } from 'react-hook-form'; import { TwoWindingsTransformerDialogHeader, TwoWindingsTransformerDialogHeaderProps, } from './TwoWindingsTransformerDialogHeader'; -import { useTabsWithError } from '../../hooks'; import { TwoWindingsTransformerDialogTab, TWT_TAB_FIELDS } from './twoWindingsTransformer.utils'; import { TwoWindingsTransformerDialogTabs } from './TwoWindingsTransformerDialogTabs'; import { TwoWindingsTransformerDialogTabsContent, TwoWindingsTransformerDialogTabsContentProps, } from './TwoWindingsTransformerDialogTabsContent'; +import { useTabs } from '../../../../hooks'; interface TwoWindingsTransformerFormProps extends @@ -31,10 +32,16 @@ export function TwoWindingsTransformerForm({ fetchVoltageLevelEquipments, isModification = false, }: Readonly) { - const { tabIndex, setTabIndex, tabIndexesWithError } = useTabsWithError( - TWT_TAB_FIELDS, - TwoWindingsTransformerDialogTab.CONNECTIVITY_TAB - ); + const { errors } = useFormState(); + const { + selectedTab: tabIndex, + setSelectedTab: setTabIndex, + tabsWithError: tabIndexesWithError, + } = useTabs({ + defaultTab: TwoWindingsTransformerDialogTab.CONNECTIVITY_TAB, + errors, + tabFields: TWT_TAB_FIELDS, + }); return ( diff --git a/src/features/network-modifications/voltageLevel/creation/VoltageLevelCreationForm.tsx b/src/features/network-modifications/voltageLevel/creation/VoltageLevelCreationForm.tsx index 64b58948b..8a5aaf6bb 100644 --- a/src/features/network-modifications/voltageLevel/creation/VoltageLevelCreationForm.tsx +++ b/src/features/network-modifications/voltageLevel/creation/VoltageLevelCreationForm.tsx @@ -7,14 +7,14 @@ import { Box, Grid, Tab, Tabs, Stack } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import { useWatch } from 'react-hook-form'; +import { useFormState, useWatch } from 'react-hook-form'; import { TextInput } from '../../../../components/ui'; import { FieldConstants } from '../../../../utils'; import { VOLTAGE_LEVEL_TAB_FIELDS, VoltageLevelTab } from './voltageLevel.constants'; import { CharacteristicsTab, StructureTab, SubstationTab } from './tabs'; import { filledTextField, PropertiesForm } from '../../common'; import { getTabIndicatorStyle, getTabStyle } from '../../../parameters/parameters-style'; -import { useTabsWithError } from '../../hooks'; +import { useTabs } from '../../../../hooks'; export interface VoltageLevelCreationFormProps { substationOptions?: string[]; @@ -25,10 +25,16 @@ export function VoltageLevelCreationForm({ substationOptions, showDeleteSubstationButton = true, }: VoltageLevelCreationFormProps = {}) { - const { tabIndex, setTabIndex, tabIndexesWithError } = useTabsWithError( - VOLTAGE_LEVEL_TAB_FIELDS, - VoltageLevelTab.SUBSTATION_TAB - ); + const { errors } = useFormState(); + const { + selectedTab: tabIndex, + setSelectedTab: setTabIndex, + tabsWithError: tabIndexesWithError, + } = useTabs({ + defaultTab: VoltageLevelTab.SUBSTATION_TAB, + errors, + tabFields: VOLTAGE_LEVEL_TAB_FIELDS, + }); const watchHideBusBarSection = useWatch({ name: FieldConstants.HIDE_BUS_BAR_SECTION }); return ( diff --git a/src/features/parameters/common/hook/index.ts b/src/features/parameters/common/hook/index.ts index 724a7dde8..d144520a3 100644 --- a/src/features/parameters/common/hook/index.ts +++ b/src/features/parameters/common/hook/index.ts @@ -4,5 +4,4 @@ * 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 * from './use-tabs'; export * from './use-parameters-form'; diff --git a/src/features/parameters/common/hook/use-tabs.ts b/src/features/parameters/common/hook/use-tabs.ts deleted file mode 100644 index e28a0251f..000000000 --- a/src/features/parameters/common/hook/use-tabs.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2026, 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 { SyntheticEvent, useCallback, useState } from 'react'; -import { FieldErrors, get } from 'react-hook-form'; -import { isObjectEmpty } from '../../../../utils/functions'; - -export type UseTabsReturn = { - selectedTab: TTabValue; - setSelectedTab: (selectedTab: TTabValue) => void; - tabsWithError: TTabValue[]; - onTabChange: (event: SyntheticEvent, newValue: TTabValue) => void; - onError: (errors: FieldErrors) => void; -}; - -export type UseTabsProps = { - defaultTab: TTabValue; - tabEnum: Record; - errors: FieldErrors; - // Optional: if omitted, each tab uses its value as the field path when checking for errors - tabFields?: Record; -}; - -export function useTabs({ - defaultTab, - tabEnum, - errors, - tabFields, -}: Readonly>): UseTabsReturn { - const [selectedTab, setSelectedTab] = useState(defaultTab); - const [tabsWithError, setTabsWithError] = useState([]); - - const getTabsWithError = useCallback( - (_errors: FieldErrors) => { - const tabsHasError: TTabValue[] = []; - Object.values(tabEnum).forEach((tabValue) => { - const fields = tabFields?.[tabValue] ?? [tabValue]; - if (fields.some((field) => get(_errors, field))) { - tabsHasError.push(tabValue); - } - }); - return tabsHasError; - }, - [tabEnum, tabFields] - ); - - const onTabChange = useCallback( - (event: SyntheticEvent, newSelectedTab: TTabValue) => { - setSelectedTab(newSelectedTab); - - if (!errors || isObjectEmpty(errors)) { - return; - } - const tabsHasError: TTabValue[] = getTabsWithError(errors); - if (tabsHasError.includes(newSelectedTab)) { - // error in current tab => remove current tab in error list - setTabsWithError(tabsHasError.filter((errorTab) => errorTab !== newSelectedTab)); - } else { - setTabsWithError(tabsHasError); - } - }, - [errors, getTabsWithError] - ); - - const onError = useCallback( - (_errors: FieldErrors) => { - if (!_errors || isObjectEmpty(_errors)) { - return; - } - - const tabsHasError: TTabValue[] = getTabsWithError(_errors); - if (tabsHasError.includes(selectedTab)) { - // error in current tab => do not change tab systematically but remove current tab in error list - setTabsWithError(tabsHasError.filter((errorTab) => errorTab !== selectedTab)); - } else if (tabsHasError.length > 0) { - // switch to the first tab in the list then remove the tab in the error list - setSelectedTab(tabsHasError[0]); - setTabsWithError(tabsHasError.filter((errorTab, index, arr) => errorTab !== arr[0])); - } - }, - [getTabsWithError, selectedTab] - ); - - return { - selectedTab, - setSelectedTab, - tabsWithError, - onTabChange, - onError, - }; -} diff --git a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-form.tsx b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-form.tsx index e5df72018..286767a9e 100644 --- a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-form.tsx +++ b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-form.tsx @@ -7,7 +7,7 @@ import { Grid, Stack, Tab, Tabs } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import { ProviderParam, UseTabsReturn } from '../common'; +import { ProviderParam } from '../common'; import { getTabStyle, parametersStyles } from '../parameters-style'; import { TabPanel } from '../common/parameters'; import TimeDelayParameters from './time-delay-parameters'; @@ -16,6 +16,8 @@ import LoadsVariationsParameters from './loads-variations-parameters'; import { TabValues } from './dynamic-margin-calculation.type'; import { UseComputationParametersFormReturn } from '../common/utils'; +import { UseTabsReturn } from '../../../hooks'; + type DynamicMarginCalculationFormProps = { dynamicMarginCalculationMethods: UseComputationParametersFormReturn; useTabsReturn: UseTabsReturn; diff --git a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx index 1cf3d945e..03b19f803 100644 --- a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx +++ b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx @@ -20,8 +20,7 @@ import { ParameterLayout } from '../common'; import { CustomFormProvider } from '../../../components/ui'; import { TreeViewFinderNodeProps } from '../../../components/ui/treeViewFinder'; import { fetchDynamicMarginCalculationParameters } from '../../../services/dynamic-margin-calculation'; -import { useSnackMessage } from '../../../hooks'; -import { useTabs } from '../common/hook/use-tabs'; +import { useSnackMessage, useTabs } from '../../../hooks'; import { TabValues } from './dynamic-margin-calculation.type'; type DynamicMarginCalculationInlineProps = { @@ -50,8 +49,8 @@ export function DynamicMarginCalculationInline({ const { reset, getValues, formState, handleSubmit } = formMethods; const useTabsReturn = useTabs({ - defaultTab: Object.values(TabValues)[0], - tabEnum: TabValues, + defaultTab: TabValues.TAB_TIME_DELAY, + tabValues: Object.values(TabValues), errors: formState.errors, }); diff --git a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx index 807777855..048b37aa0 100644 --- a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx +++ b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx @@ -17,7 +17,7 @@ import { import { ParameterLayout } from '../common'; import { CustomFormProvider } from '../../../components/ui'; import { DynamicSecurityAnalysisParametersForm } from './dynamic-security-analysis-parameters-form'; -import { useTabs } from '../common/hook/use-tabs'; +import { useTabs } from '../../../hooks'; import { TabValues } from './dynamic-security-analysis.type'; type DynamicSecurityAnalysisInlineProps = { @@ -44,7 +44,7 @@ export function DynamicSecurityAnalysisInline({ const useTabsReturn = useTabs({ defaultTab: TabValues.SCENARIO, - tabEnum: TabValues, + tabValues: Object.values(TabValues), errors: formState.errors, }); diff --git a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-parameters-form.tsx b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-parameters-form.tsx index 9c6d167ef..58d3b7e3b 100644 --- a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-parameters-form.tsx +++ b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-parameters-form.tsx @@ -10,10 +10,11 @@ import { FormattedMessage } from 'react-intl'; import ScenarioParameters from './scenario-parameters'; import ContingencyParameters from './contingency-parameters'; -import { ProviderParam, TabPanel, UseTabsReturn } from '../common'; +import { ProviderParam, TabPanel } from '../common'; import { getTabStyle, parametersStyles } from '../parameters-style'; import { TabValues } from './dynamic-security-analysis.type'; import { UseComputationParametersFormReturn } from '../common/utils'; +import { UseTabsReturn } from '../../../hooks'; type DynamicSecurityAnalysisParametersFormProps = { dynamicSecurityAnalysisMethods: UseComputationParametersFormReturn; diff --git a/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx b/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx index e7a2d8f4c..fc285d1fe 100644 --- a/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx +++ b/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx @@ -17,8 +17,7 @@ import { import { UseParametersBackendReturnProps } from '../../../utils/types/parameters.type'; import { ParameterLayout } from '../common'; import { CustomFormProvider } from '../../../components/ui'; -import { useTabs } from '../common/hook/use-tabs'; -import { useSnackMessage } from '../../../hooks'; +import { useTabs, useSnackMessage } from '../../../hooks'; import { TreeViewFinderNodeProps } from '../../../components/ui/treeViewFinder'; import { toFormValues, @@ -67,7 +66,7 @@ export function DynamicSimulationInline({ const useTabsReturn = useTabs({ defaultTab: TabValues.TAB_TIME_DELAY, - tabEnum: TabValues, + tabValues: Object.values(TabValues), errors: formState.errors, }); diff --git a/src/features/parameters/dynamic-simulation/dynamic-simulation-parameters-form.tsx b/src/features/parameters/dynamic-simulation/dynamic-simulation-parameters-form.tsx index 1799488b9..ce8a4d565 100644 --- a/src/features/parameters/dynamic-simulation/dynamic-simulation-parameters-form.tsx +++ b/src/features/parameters/dynamic-simulation/dynamic-simulation-parameters-form.tsx @@ -6,7 +6,7 @@ */ import { Grid, Stack, Tab, Tabs } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import { ProviderParam, TabPanel, UseTabsReturn } from '../common'; +import { ProviderParam, TabPanel } from '../common'; import { getTabStyle, parametersStyles } from '../parameters-style'; import { VoltageLevelInfos } from '../../../utils/types/equipmentType'; @@ -19,6 +19,8 @@ import CurveParameters from './curve/curve-parameters'; import { ExpertFilter, IdentifiableAttributes } from '../../../components/composite/filter'; import { UseComputationParametersFormReturn } from '../common/utils'; +import { UseTabsReturn } from '../../../hooks'; + type DynamicSimulationFormProps = { dynamicSimulationMethods: UseComputationParametersFormReturn; useTabsReturn: UseTabsReturn; diff --git a/src/features/parameters/loadflow/use-load-flow-parameters-form.ts b/src/features/parameters/loadflow/use-load-flow-parameters-form.ts index d8ca40845..f294d29d0 100644 --- a/src/features/parameters/loadflow/use-load-flow-parameters-form.ts +++ b/src/features/parameters/loadflow/use-load-flow-parameters-form.ts @@ -26,7 +26,6 @@ import { PROVIDER, SPECIFIC_PARAMETERS, toFormValuesLimitReductions, - useTabs, VERSION_PARAMETER, } from '../common'; import { @@ -42,7 +41,7 @@ import { getNameElementEditorEmptyFormData, getNameElementEditorSchema, } from '../../../components/ui/dialogs/name-element-editor'; -import { useSnackMessage } from '../../../hooks'; +import { useTabs, useSnackMessage } from '../../../hooks'; import { formatSpecificParameters, getAllSpecificParametersValues, @@ -236,7 +235,7 @@ export const useLoadFlowParametersForm = ( onError: onValidationError, } = useTabs({ defaultTab: TabValues.GENERAL, - tabEnum: TabValues, + tabValues: Object.values(TabValues), errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); diff --git a/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts b/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts index 6cf47ee10..4a7c44a50 100644 --- a/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts +++ b/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts @@ -30,7 +30,7 @@ import { setStudyNetworkVisualizationParameters, updateParameter, } from '../../../services'; -import { useSnackMessage } from '../../../hooks'; +import { useTabs, useSnackMessage } from '../../../hooks'; import { ElementType } from '../../../utils'; import { NetworkVisualizationParameters } from './network-visualizations.types'; import { @@ -38,7 +38,6 @@ import { getNameElementEditorSchema, } from '../../../components/ui/dialogs/name-element-editor'; import { snackWithFallback } from '../../../utils/error'; -import { useTabs } from '../common'; export interface UseNetworkVisualizationParametersFormReturn { formMethods: UseFormReturn; @@ -136,7 +135,7 @@ export const useNetworkVisualizationParametersForm = ({ onError: onValidationError, } = useTabs({ defaultTab: TabValues.MAP, - tabEnum: TabValues, + tabValues: Object.values(TabValues), errors: formMethods.formState.errors, }); diff --git a/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts b/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts index 034bd1a4e..3196c72de 100644 --- a/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts +++ b/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts @@ -24,11 +24,10 @@ import { LIMIT_REDUCTIONS_FORM, PROVIDER, toFormValuesLimitReductions, - useTabs, } from '../common'; import { getNameElementEditorEmptyFormData } from '../../../components/ui/dialogs/name-element-editor'; import { updateParameter } from '../../../services'; -import { useSnackMessage } from '../../../hooks'; +import { useTabs, useSnackMessage } from '../../../hooks'; import { snackWithFallback } from '../../../utils/error'; import { mapSecurityAnalysisParameters, SAParametersEnriched } from '../../../utils/types'; import { getSAParametersFormSchema, toFormValueSaParameters } from './columns-definitions'; @@ -171,7 +170,7 @@ export const useSecurityAnalysisParametersForm = ( onError: onValidationError, } = useTabs({ defaultTab: TabValues.Contingencies, - tabEnum: TabValues, + tabValues: Object.values(TabValues), errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); diff --git a/src/features/parameters/sensi/sensitivity-parameters-selector.tsx b/src/features/parameters/sensi/sensitivity-parameters-selector.tsx index 6df3029be..10d6d5636 100644 --- a/src/features/parameters/sensi/sensitivity-parameters-selector.tsx +++ b/src/features/parameters/sensi/sensitivity-parameters-selector.tsx @@ -5,10 +5,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { Box, Card, CardContent, Grid, Tab, Tabs } from '@mui/material'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, FieldErrors } from 'react-hook-form'; import { COLUMNS_DEFINITIONS_HVDCS, @@ -37,6 +37,8 @@ import { ParameterTableField } from '../common/parameter-table-field'; import { DndColumn } from '../../../components/composite/dnd-table'; import { parametersStyles } from '../parameters-style'; +import { useTabs } from '../../../hooks'; + const styles = { circularProgress: (theme) => ({ display: 'flex', @@ -99,41 +101,48 @@ function SensitivityParametersSelector({ formState: { errors, dirtyFields }, } = useFormContext(); - const [tabValue, setTabValue] = useState(SensiTabValues.SensitivityBranches); - const [subTabValue, setSubTabValue] = useState(SensiBranchesTabValues.SensiInjectionsSet); - const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => { - setTabValue(newValue); - }, []); - const handleSubTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => { - setSubTabValue(newValue); - }, []); - - const hasDirtyFields = Object.keys(dirtyFields).length > 0; + const dirtyErrors = useMemo((): FieldErrors => { + if (!Object.keys(dirtyFields).length) { + return {}; + } + return Object.fromEntries( + Object.entries(errors).filter(([key]) => !!dirtyFields[key as keyof typeof dirtyFields]) + ) as FieldErrors; + }, [errors, dirtyFields]); - const subTabHasError = useMemo>( - () => ({ - [SensiBranchesTabValues.SensiInjectionsSet]: - hasDirtyFields && - !!dirtyFields[PARAMETER_SENSI_INJECTIONS_SET] && - !!errors[PARAMETER_SENSI_INJECTIONS_SET], - [SensiBranchesTabValues.SensiInjection]: - hasDirtyFields && !!dirtyFields[PARAMETER_SENSI_INJECTION] && !!errors[PARAMETER_SENSI_INJECTION], - [SensiBranchesTabValues.SensiHVDC]: - hasDirtyFields && !!dirtyFields[PARAMETER_SENSI_HVDC] && !!errors[PARAMETER_SENSI_HVDC], - [SensiBranchesTabValues.SensiPST]: - hasDirtyFields && !!dirtyFields[PARAMETER_SENSI_PST] && !!errors[PARAMETER_SENSI_PST], - }), - [errors, dirtyFields, hasDirtyFields] - ); + const { + selectedTab: tabValue, + setSelectedTab: setTabValue, + tabsWithError, + onTabChange: handleTabChange, + } = useTabs({ + defaultTab: SensiTabValues.SensitivityBranches, + errors: dirtyErrors, + tabFields: { + [SensiTabValues.SensitivityBranches]: [ + PARAMETER_SENSI_INJECTIONS_SET, + PARAMETER_SENSI_INJECTION, + PARAMETER_SENSI_HVDC, + PARAMETER_SENSI_PST, + ], + [SensiTabValues.SensitivityNodes]: [PARAMETER_SENSI_NODES], + }, + }); - const tabHasError = useMemo>( - () => ({ - [SensiTabValues.SensitivityBranches]: Object.values(subTabHasError).some(Boolean), - [SensiTabValues.SensitivityNodes]: - hasDirtyFields && !!dirtyFields[PARAMETER_SENSI_NODES] && !!errors[PARAMETER_SENSI_NODES], - }), - [subTabHasError, errors, dirtyFields, hasDirtyFields] - ); + const { + selectedTab: subTabValue, + tabsWithError: subTabsWithError, + onTabChange: handleSubTabChange, + } = useTabs({ + defaultTab: SensiBranchesTabValues.SensiInjectionsSet, + errors: dirtyErrors, + tabFields: { + [SensiBranchesTabValues.SensiInjectionsSet]: [PARAMETER_SENSI_INJECTIONS_SET], + [SensiBranchesTabValues.SensiInjection]: [PARAMETER_SENSI_INJECTION], + [SensiBranchesTabValues.SensiHVDC]: [PARAMETER_SENSI_HVDC], + [SensiBranchesTabValues.SensiPST]: [PARAMETER_SENSI_PST], + }, + }); const tabInfo: TabInfo[] = [ { @@ -190,7 +199,7 @@ function SensitivityParametersSelector({ if (!isDeveloperMode) { setTabValue(SensiTabValues.SensitivityBranches); } - }, [isDeveloperMode]); + }, [isDeveloperMode, setTabValue]); return ( @@ -205,7 +214,7 @@ function SensitivityParametersSelector({ fontSize: 17, fontWeight: 'bold', textTransform: 'capitalize', - ...(tabHasError[index] && + ...(tabsWithError.includes(index as SensiTabValues) && index !== tabValue && { color: 'error.main', '&.Mui-selected': { color: 'error.main' }, @@ -275,7 +284,8 @@ function SensitivityParametersSelector({ fontWeight: 'bold', textTransform: 'capitalize', }, - subTabHasError[subIndex] && subIndex !== subTabValue + subTabsWithError.includes(subIndex as SensiBranchesTabValues) && + subIndex !== subTabValue ? parametersStyles.tabWithError : undefined )} diff --git a/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts b/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts index 0b54272ec..19ef30d5c 100644 --- a/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts +++ b/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts @@ -25,14 +25,14 @@ import { SHORT_CIRCUIT_WITH_VSC_CONVERTER_STATIONS, } from './constants'; import { updateParameter } from '../../../services'; -import { useSnackMessage } from '../../../hooks'; +import { useTabs, useSnackMessage } from '../../../hooks'; import { ComputingType, ElementType, SpecificParameterInfos, UseParametersBackendReturnProps } from '../../../utils'; import { getNameElementEditorEmptyFormData, getNameElementEditorSchema, } from '../../../components/ui/dialogs/name-element-editor'; import { ShortCircuitParametersInfos } from './short-circuit-parameters.type'; -import { COMMON_PARAMETERS, PROVIDER, SPECIFIC_PARAMETERS, useTabs, VERSION_PARAMETER } from '../common'; +import { COMMON_PARAMETERS, PROVIDER, SPECIFIC_PARAMETERS, VERSION_PARAMETER } from '../common'; import { formatShortCircuitSpecificParameters, getCommonShortCircuitParametersFormSchema, @@ -241,7 +241,7 @@ export const useShortCircuitParametersForm = ({ onError: onValidationError, } = useTabs({ defaultTab: ShortCircuitParametersTabValues.GENERAL, - tabEnum: ShortCircuitParametersTabValues, + tabValues: Object.values(ShortCircuitParametersTabValues), errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); diff --git a/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts b/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts index cd5e1f9ed..39dca1241 100644 --- a/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts +++ b/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts @@ -33,7 +33,7 @@ import { VoltageInitTabValues as TabValues, } from './constants'; import { getVoltageInitParameters, updateParameter, updateVoltageInitParameters } from '../../../services'; -import { useSnackMessage } from '../../../hooks'; +import { useTabs, useSnackMessage } from '../../../hooks'; import { ElementType, isBlankOrEmpty, MUST_BE_GREATER_OR_EQUAL_TO_ZERO, YUP_REQUIRED } from '../../../utils'; import { getNameElementEditorEmptyFormData, @@ -49,7 +49,6 @@ import { import { SELECTED } from '../../../components/composite/dnd-table'; import { FILTERS, ID } from '../../../utils/constants/filterConstant'; import { snackWithFallback } from '../../../utils/error'; -import { useTabs } from '../common'; export interface UseVoltageInitParametersFormReturn { formMethods: UseFormReturn; @@ -204,7 +203,7 @@ export const useVoltageInitParametersForm = ({ onError: onValidationError, } = useTabs({ defaultTab: TabValues.GENERAL, - tabEnum: TabValues, + tabValues: Object.values(TabValues), errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d0cb1772d..ab0938f20 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -19,3 +19,4 @@ export * from './useSelectAppearance'; export * from './use-parameters-backend'; export * from './useGetLabelEquipmentTypes'; export * from './useOpenLoaderShortWait'; +export * from './use-tabs'; diff --git a/src/hooks/use-tabs.ts b/src/hooks/use-tabs.ts new file mode 100644 index 000000000..8b789efbb --- /dev/null +++ b/src/hooks/use-tabs.ts @@ -0,0 +1,123 @@ +import { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FieldErrors, get, useFormContext } from 'react-hook-form'; +import { isObjectEmpty } from '../utils/functions'; + +export type UseTabsReturn = { + selectedTab: TTabValue; + setSelectedTab: (selectedTab: TTabValue) => void; + tabsWithError: TTabValue[]; + onTabChange: (event: SyntheticEvent, newValue: TTabValue) => void; + onError: (errors: FieldErrors) => void; +}; + +export type UseTabsProps = { + defaultTab: TTabValue; + /** + * All possible tab values. Use a string enum for best compatibility. + * For numeric enums, prefer passing tabFields explicitly to avoid reverse-mapping issues. + */ + tabValues?: TTabValue[]; + errors: FieldErrors; + /** + * Maps each tab value to the field paths it owns. + * If omitted, each tab uses its own value (cast to string) as the field path. + * Required when TTabValue is a number. + */ + tabFields?: Partial>; +}; + +export function useTabs({ + defaultTab, + tabValues, + errors, + tabFields, +}: Readonly>): UseTabsReturn { + const [selectedTab, setSelectedTab] = useState(defaultTab); + const [tabsWithError, setTabsWithError] = useState([]); + + // useFormContext returns null when there is no FormProvider ancestor. + // This happens when useTabs is called from a plain hook (e.g. use-load-flow-parameters-form) + // rather than from inside a component tree already wrapped by FormProvider. + const formContext = useFormContext(); + const submitCount = formContext?.formState.submitCount ?? 0; + + const resolvedTabValues = useMemo( + () => tabValues ?? (Object.keys(tabFields ?? {}).map(Number) as TTabValue[]), + [tabValues, tabFields] + ); + + const getTabsWithError = useCallback( + (_errors: FieldErrors): TTabValue[] => { + return resolvedTabValues.filter((tabValue) => { + const fields = tabFields?.[tabValue] ?? [String(tabValue)]; + return fields.some((field) => get(_errors, field)); + }); + }, + [resolvedTabValues, tabFields] + ); + + // Auto-navigate to the first errored tab on submit, without reacting to mid-input + // revalidation. No-ops when submitCount stays 0 (no FormProvider context). + const selectedTabRef = useRef(selectedTab); + selectedTabRef.current = selectedTab; + const errorsRef = useRef(errors); + errorsRef.current = errors; + const getTabsWithErrorRef = useRef(getTabsWithError); + getTabsWithErrorRef.current = getTabsWithError; + + useEffect(() => { + const currentErrors = errorsRef.current; + if (!currentErrors || isObjectEmpty(currentErrors)) { + return; + } + const tabsHasError = getTabsWithErrorRef.current(currentErrors); + if (tabsHasError.length > 0 && !tabsHasError.includes(selectedTabRef.current)) { + setSelectedTab(tabsHasError[0]); + setTabsWithError(tabsHasError.slice(1)); + } else if (tabsHasError.includes(selectedTabRef.current)) { + setTabsWithError(tabsHasError.filter((t) => t !== selectedTabRef.current)); + } + // submitCount is the intentional trigger; refs never change identity. + }, [submitCount]); + + const onTabChange = useCallback( + (_event: SyntheticEvent, newSelectedTab: TTabValue) => { + setSelectedTab(newSelectedTab); + + if (!errors || isObjectEmpty(errors)) { + return; + } + const tabsHasError = getTabsWithError(errors); + if (tabsHasError.includes(newSelectedTab)) { + setTabsWithError(tabsHasError.filter((errorTab) => errorTab !== newSelectedTab)); + } else { + setTabsWithError(tabsHasError); + } + }, + [errors, getTabsWithError] + ); + + const onError = useCallback( + (_errors: FieldErrors) => { + if (!_errors || isObjectEmpty(_errors)) { + return; + } + const tabsHasError = getTabsWithError(_errors); + if (tabsHasError.includes(selectedTab)) { + setTabsWithError(tabsHasError.filter((errorTab) => errorTab !== selectedTab)); + } else if (tabsHasError.length > 0) { + setSelectedTab(tabsHasError[0]); + setTabsWithError(tabsHasError.slice(1)); + } + }, + [getTabsWithError, selectedTab] + ); + + return { + selectedTab, + setSelectedTab, + tabsWithError, + onTabChange, + onError, + }; +} From 093aedd569f553b5516b7d51cf154e18259cd027 Mon Sep 17 00:00:00 2001 From: Radouane Khouadri Date: Wed, 26 Aug 2026 14:46:02 +0200 Subject: [PATCH 2/6] refactor useTabs for modifications --- .../modification/BatteryDialogTabs.tsx | 8 +- .../modification/BatteryModificationForm.tsx | 27 ++-- .../modification/GeneratorDialogTabs.tsx | 8 +- .../GeneratorModificationForm.tsx | 22 ++- .../line/common/LineDialogTabs.tsx | 8 +- .../line/common/LineForm.tsx | 29 ++-- .../load/common/LoadDialogTabs.tsx | 8 +- .../load/common/LoadForm.tsx | 29 ++-- .../static-var-compensator-dialog-tabs.tsx | 7 +- .../static-var-compensator-creation-form.tsx | 30 ++-- .../TwoWindingsTransformerDialogTabs.tsx | 8 +- .../common/TwoWindingsTransformerForm.tsx | 29 ++-- .../creation/VoltageLevelCreationForm.tsx | 44 +++--- .../voltageLevel/creation/index.ts | 2 +- src/hooks/use-tabs.ts | 133 ++++++++---------- 15 files changed, 161 insertions(+), 231 deletions(-) diff --git a/src/features/network-modifications/battery/modification/BatteryDialogTabs.tsx b/src/features/network-modifications/battery/modification/BatteryDialogTabs.tsx index db9149d9a..cef342c0d 100644 --- a/src/features/network-modifications/battery/modification/BatteryDialogTabs.tsx +++ b/src/features/network-modifications/battery/modification/BatteryDialogTabs.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from 'react'; +import { SyntheticEvent } from 'react'; import { Tab, Tabs } from '@mui/material'; import { FormattedMessage } from 'react-intl'; import { BatteryDialogTab } from './batteryTabs.utils'; @@ -14,15 +14,15 @@ import { getTabIndicatorStyle, getTabStyle } from '../../../parameters/parameter interface BatteryDialogTabsProps { tabIndex: number; tabIndexesWithError: number[]; - setTabIndex: (newTabIndex: number) => void; + onTabChange: (event: SyntheticEvent, newValue: number) => void; } -export function BatteryDialogTabs({ tabIndex, tabIndexesWithError, setTabIndex }: Readonly) { +export function BatteryDialogTabs({ tabIndex, tabIndexesWithError, onTabChange }: Readonly) { return ( setTabIndex(newValue)} + onChange={onTabChange} slotProps={{ indicator: { sx: getTabIndicatorStyle(tabIndexesWithError, tabIndex), diff --git a/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx b/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx index 5217aaf9b..a5dd669b7 100644 --- a/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx +++ b/src/features/network-modifications/battery/modification/BatteryModificationForm.tsx @@ -6,17 +6,18 @@ */ import { Grid, Stack } from '@mui/material'; -import { useFormState, useWatch } from 'react-hook-form'; +import { useWatch } from 'react-hook-form'; import { BatteryDialogHeader, BatteryDialogHeaderProps } from './BatteryDialogHeader'; import { BatteryDialogTabs } from './BatteryDialogTabs'; import { BatteryDialogTabsContent, BatteryDialogTabsContentProps } from './BatteryDialogTabsContent'; -import { BATTERY_TAB_FIELDS, BatteryDialogTab } from './batteryTabs.utils'; +import { BatteryDialogTab } from './batteryTabs.utils'; import { EquipmentType, FieldConstants, Identifiable } from '../../../../utils'; -import { useTabs } from '../../../../hooks'; +import { UseTabsReturn } from '../../../../hooks'; interface BatteryModificationFormProps extends BatteryDialogHeaderProps, Omit { fetchVoltageLevelEquipments: (voltageLevelId: string) => Promise<(Identifiable & { type: EquipmentType })[]>; + useTabsReturn: UseTabsReturn; } export function BatteryModificationForm({ @@ -26,17 +27,9 @@ export function BatteryModificationForm({ fetchBusesOrBusbarSections, PositionDiagramPane, fetchVoltageLevelEquipments, + useTabsReturn, }: Readonly) { - const { errors } = useFormState(); - const { - selectedTab: tabIndex, - setSelectedTab: setTabIndex, - tabsWithError: tabIndexesWithError, - } = useTabs({ - defaultTab: BatteryDialogTab.CONNECTIVITY_TAB, - errors, - tabFields: BATTERY_TAB_FIELDS, - }); + const { selectedTab, tabsWithError, onTabChange } = useTabsReturn; const equipmentId = useWatch({ name: FieldConstants.EQUIPMENT_ID }); @@ -47,14 +40,14 @@ export function BatteryModificationForm({ void; + onTabChange: (event: SyntheticEvent, newValue: number) => void; } export function GeneratorDialogTabs({ tabIndex, tabIndexesWithError, - setTabIndex, + onTabChange, }: Readonly) { return ( setTabIndex(newValue)} + onChange={onTabChange} slotProps={{ indicator: { sx: getTabIndicatorStyle(tabIndexesWithError, tabIndex), diff --git a/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx b/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx index 4245c97b4..286700a62 100644 --- a/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx +++ b/src/features/network-modifications/generator/modification/GeneratorModificationForm.tsx @@ -6,17 +6,18 @@ */ import { Grid, Stack } from '@mui/material'; -import { useFormState, useWatch } from 'react-hook-form'; -import { GENERATOR_TAB_FIELDS, GeneratorDialogTab } from './generatorTabs.utils'; +import { useWatch } from 'react-hook-form'; +import { GeneratorDialogTab } from './generatorTabs.utils'; import { GeneratorDialogHeader, GeneratorDialogHeaderProps } from './GeneratorDialogHeader'; import { GeneratorDialogTabs } from './GeneratorDialogTabs'; import { GeneratorDialogTabsContent, GeneratorDialogTabsContentProps } from './GeneratorDialogTabsContent'; import { EquipmentType, FieldConstants, Identifiable } from '../../../../utils'; -import { useTabs } from '../../../../hooks'; +import { UseTabsReturn } from '../../../../hooks'; interface GeneratorModificationFormProps extends GeneratorDialogHeaderProps, Omit { fetchVoltageLevelEquipments: (voltageLevelId: string) => Promise<(Identifiable & { type: EquipmentType })[]>; + useTabsReturn: UseTabsReturn; } export function GeneratorModificationForm({ @@ -26,17 +27,10 @@ export function GeneratorModificationForm({ fetchBusesOrBusbarSections, PositionDiagramPane, fetchVoltageLevelEquipments, + useTabsReturn, }: Readonly) { - const { errors } = useFormState(); - const { - selectedTab: tabIndex, - setSelectedTab: setTabIndex, - tabsWithError: tabIndexesWithError, - } = useTabs({ - defaultTab: GeneratorDialogTab.CONNECTIVITY_TAB, - errors, - tabFields: GENERATOR_TAB_FIELDS, - }); + const { selectedTab: tabIndex, tabsWithError: tabIndexesWithError, onTabChange } = useTabsReturn; + const equipmentId = useWatch({ name: FieldConstants.EQUIPMENT_ID }); return ( @@ -48,7 +42,7 @@ export function GeneratorModificationForm({ diff --git a/src/features/network-modifications/line/common/LineDialogTabs.tsx b/src/features/network-modifications/line/common/LineDialogTabs.tsx index 0232e3c76..509f5b706 100644 --- a/src/features/network-modifications/line/common/LineDialogTabs.tsx +++ b/src/features/network-modifications/line/common/LineDialogTabs.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from 'react'; +import { SyntheticEvent } from 'react'; import { Tab, Tabs } from '@mui/material'; import { FormattedMessage } from 'react-intl'; import { LineDialogOptions, LineDialogTab } from './line.utils'; @@ -14,13 +14,13 @@ import { getTabIndicatorStyle, getTabStyle } from '../../../parameters/parameter interface LineDialogTabsProps extends LineDialogOptions { tabIndex: number; tabIndexesWithError: number[]; - setTabIndex: (newTabIndex: number) => void; + onTabChange: (event: SyntheticEvent, newValue: number) => void; } export function LineDialogTabs({ tabIndex, tabIndexesWithError, - setTabIndex, + onTabChange, isModification = false, withConnectivity = true, }: Readonly) { @@ -28,7 +28,7 @@ export function LineDialogTabs({ setTabIndex(newValue)} + onChange={onTabChange} slotProps={{ indicator: { sx: getTabIndicatorStyle(tabIndexesWithError, tabIndex) }, }} diff --git a/src/features/network-modifications/line/common/LineForm.tsx b/src/features/network-modifications/line/common/LineForm.tsx index 3b52cd3ab..ba6398c4c 100644 --- a/src/features/network-modifications/line/common/LineForm.tsx +++ b/src/features/network-modifications/line/common/LineForm.tsx @@ -6,15 +6,16 @@ */ import { Box, Stack } from '@mui/material'; -import { useFormState } from 'react-hook-form'; -import { LINE_TAB_FIELDS, LineDialogTab } from './line.utils'; +import { LineDialogTab } from './line.utils'; import { LineDialogHeader, LineDialogHeaderProps } from './LineDialogHeader'; import { LineDialogTabs } from './LineDialogTabs'; import { LineDialogTabsContent, LineDialogTabsContentProps } from './LineDialogTabsContent'; -import { useTabs } from '../../../../hooks'; +import { UseTabsReturn } from '../../../../hooks'; interface LineFormProps - extends LineDialogHeaderProps, Omit {} + extends LineDialogHeaderProps, Omit { + useTabsReturn: UseTabsReturn; +} export function LineForm({ lineToModify, @@ -23,31 +24,23 @@ export function LineForm({ PositionDiagramPane, isModification = false, withConnectivity = true, + useTabsReturn, }: Readonly) { - const { errors } = useFormState(); - const { - selectedTab: tabIndex, - setSelectedTab: setTabIndex, - tabsWithError: tabIndexesWithError, - } = useTabs({ - defaultTab: withConnectivity ? LineDialogTab.CONNECTIVITY_TAB : LineDialogTab.CHARACTERISTICS_TAB, - errors, - tabFields: LINE_TAB_FIELDS, - }); + const { selectedTab, tabsWithError, onTabChange } = useTabsReturn; return ( void; + onTabChange: (event: SyntheticEvent, newValue: number) => void; isModification?: boolean; } export function LoadDialogTabs({ tabIndex, tabIndexesWithError, - setTabIndex, + onTabChange, isModification = false, }: Readonly) { return ( setTabIndex(newValue)} + onChange={onTabChange} slotProps={{ indicator: { sx: getTabIndicatorStyle(tabIndexesWithError, tabIndex), diff --git a/src/features/network-modifications/load/common/LoadForm.tsx b/src/features/network-modifications/load/common/LoadForm.tsx index 78ae5cd6a..95dcb8202 100644 --- a/src/features/network-modifications/load/common/LoadForm.tsx +++ b/src/features/network-modifications/load/common/LoadForm.tsx @@ -6,15 +6,16 @@ */ import { Grid, Stack } from '@mui/material'; -import { useFormState } from 'react-hook-form'; -import { LOAD_TAB_FIELDS, LoadDialogTab } from './load.utils'; +import { LoadDialogTab } from './load.utils'; import { LoadDialogHeader, LoadDialogHeaderProps } from './LoadDialogHeader'; import { LoadDialogTabs } from './LoadDialogTabs'; import { LoadDialogTabsContent, LoadDialogTabsContentProps } from './LoadDialogTabsContent'; -import { useTabs } from '../../../../hooks'; +import { UseTabsReturn } from '../../../../hooks'; interface LoadFormProps - extends LoadDialogHeaderProps, Omit {} + extends LoadDialogHeaderProps, Omit { + useTabsReturn: UseTabsReturn; +} export function LoadForm({ loadToModify, @@ -22,17 +23,9 @@ export function LoadForm({ voltageLevelOptions, fetchBusesOrBusbarSections, PositionDiagramPane, + useTabsReturn, }: Readonly) { - const { errors } = useFormState(); - const { - selectedTab: tabIndex, - setSelectedTab: setTabIndex, - tabsWithError: tabIndexesWithError, - } = useTabs({ - defaultTab: LoadDialogTab.CONNECTIVITY_TAB, - errors, - tabFields: LOAD_TAB_FIELDS, - }); + const { selectedTab, tabsWithError, onTabChange } = useTabsReturn; return ( @@ -41,15 +34,15 @@ export function LoadForm({ void; + onTabChange: (event: SyntheticEvent, newValue: number) => void; } export function StaticVarCompensatorDialogTabs({ tabIndex, tabIndexesWithError, - setTabIndex, + onTabChange, }: StaticVarCompensatorCreationDialogTabsProps) { return ( - setTabIndex(newValue)}> + } sx={getTabStyle(tabIndexesWithError, StaticVarCompensatorDialogTab.CONNECTIVITY_TAB)} diff --git a/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx b/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx index 923234107..ab08bd0de 100644 --- a/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx +++ b/src/features/network-modifications/static-var-compensator/creation/static-var-compensator-creation-form.tsx @@ -6,19 +6,16 @@ */ import { Box, Stack } from '@mui/material'; -import { useFormState } from 'react-hook-form'; import { ConnectivityNetworkProps } from '../../common'; -import { - STATIC_VAR_COMPENSATOR_TAB_FIELDS, - StaticVarCompensatorDialogTab, -} from '../common/static-var-compensator-tab-utils'; +import { StaticVarCompensatorDialogTab } from '../common/static-var-compensator-tab-utils'; import { StaticVarCompensatorDialogHeader, StaticVarCompensatorDialogTabs } from '../common'; import { StaticVarCompensatorTabsContent } from '../common/static-var-compensator-tabs-content'; import { EquipmentType, Identifiable } from '../../../../utils'; -import { useTabs } from '../../../../hooks'; +import { UseTabsReturn } from '../../../../hooks'; export interface StaticVarCompensatorCreationFormProps extends ConnectivityNetworkProps { fetchVoltageLevelEquipments: (voltageLevelId: string) => Promise<(Identifiable & { type: EquipmentType })[]>; + useTabsReturn: UseTabsReturn; } export function StaticVarCompensatorCreationForm({ @@ -26,24 +23,17 @@ export function StaticVarCompensatorCreationForm({ PositionDiagramPane, fetchBusesOrBusbarSections, fetchVoltageLevelEquipments, + useTabsReturn, }: StaticVarCompensatorCreationFormProps) { - const { errors } = useFormState(); - const { - selectedTab: tabIndex, - setSelectedTab: setTabIndex, - tabsWithError: tabIndexesWithError, - } = useTabs({ - defaultTab: StaticVarCompensatorDialogTab.CONNECTIVITY_TAB, - errors, - tabFields: STATIC_VAR_COMPENSATOR_TAB_FIELDS, - }); + const { selectedTab, tabsWithError, onTabChange } = useTabsReturn; + return ( diff --git a/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerDialogTabs.tsx b/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerDialogTabs.tsx index aa3e4def8..178c15185 100644 --- a/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerDialogTabs.tsx +++ b/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerDialogTabs.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from 'react'; +import { SyntheticEvent } from 'react'; import { Tab, Tabs } from '@mui/material'; import { FormattedMessage } from 'react-intl'; import { useWatch } from 'react-hook-form'; @@ -16,14 +16,14 @@ import { FieldConstants } from '../../../../utils'; interface TwoWindingsTransformerDialogProps { tabIndex: number; tabIndexesWithError: number[]; - setTabIndex: (newTabIndex: number) => void; + onTabChange: (event: SyntheticEvent, newValue: number) => void; isModification?: boolean; } export function TwoWindingsTransformerDialogTabs({ tabIndex, tabIndexesWithError, - setTabIndex, + onTabChange, isModification = false, }: Readonly) { const phaseTapChangerEnabledWatch = useWatch({ @@ -36,7 +36,7 @@ export function TwoWindingsTransformerDialogTabs({ setTabIndex(newValue)} + onChange={onTabChange} slotProps={{ indicator: { sx: getTabIndicatorStyle(tabIndexesWithError, tabIndex) }, }} diff --git a/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx b/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx index 6abea0fda..06116d091 100644 --- a/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx +++ b/src/features/network-modifications/twoWindingsTransformer/common/TwoWindingsTransformerForm.tsx @@ -6,23 +6,24 @@ */ import { Box, Stack } from '@mui/material'; -import { useFormState } from 'react-hook-form'; import { TwoWindingsTransformerDialogHeader, TwoWindingsTransformerDialogHeaderProps, } from './TwoWindingsTransformerDialogHeader'; -import { TwoWindingsTransformerDialogTab, TWT_TAB_FIELDS } from './twoWindingsTransformer.utils'; +import { TwoWindingsTransformerDialogTab } from './twoWindingsTransformer.utils'; import { TwoWindingsTransformerDialogTabs } from './TwoWindingsTransformerDialogTabs'; import { TwoWindingsTransformerDialogTabsContent, TwoWindingsTransformerDialogTabsContentProps, } from './TwoWindingsTransformerDialogTabsContent'; -import { useTabs } from '../../../../hooks'; +import { UseTabsReturn } from '../../../../hooks'; interface TwoWindingsTransformerFormProps extends TwoWindingsTransformerDialogHeaderProps, - Omit {} + Omit { + useTabsReturn: UseTabsReturn; +} export function TwoWindingsTransformerForm({ twtToModify, @@ -32,30 +33,22 @@ export function TwoWindingsTransformerForm({ fetchVoltageLevelEquipments, editData, isModification = false, + useTabsReturn, }: Readonly) { - const { errors } = useFormState(); - const { - selectedTab: tabIndex, - setSelectedTab: setTabIndex, - tabsWithError: tabIndexesWithError, - } = useTabs({ - defaultTab: TwoWindingsTransformerDialogTab.CONNECTIVITY_TAB, - errors, - tabFields: TWT_TAB_FIELDS, - }); + const { selectedTab, tabsWithError, onTabChange } = useTabsReturn; return ( ; } export function VoltageLevelCreationForm({ substationOptions, showDeleteSubstationButton = true, -}: VoltageLevelCreationFormProps = {}) { - const { errors } = useFormState(); - const { - selectedTab: tabIndex, - setSelectedTab: setTabIndex, - tabsWithError: tabIndexesWithError, - } = useTabs({ - defaultTab: VoltageLevelTab.SUBSTATION_TAB, - errors, - tabFields: VOLTAGE_LEVEL_TAB_FIELDS, - }); + useTabsReturn, +}: VoltageLevelCreationFormProps) { + const { selectedTab, tabsWithError, onTabChange } = useTabsReturn; + const watchHideBusBarSection = useWatch({ name: FieldConstants.HIDE_BUS_BAR_SECTION }); return ( @@ -55,48 +49,48 @@ export function VoltageLevelCreationForm({ setTabIndex(newValue)} + onChange={onTabChange} slotProps={{ indicator: { - sx: getTabIndicatorStyle(tabIndexesWithError, tabIndex), + sx: getTabIndicatorStyle(tabsWithError, selectedTab), }, }} > } - sx={getTabStyle(tabIndexesWithError, VoltageLevelTab.SUBSTATION_TAB)} + sx={getTabStyle(tabsWithError, VoltageLevelTab.SUBSTATION_TAB)} /> } - sx={getTabStyle(tabIndexesWithError, VoltageLevelTab.CHARACTERISTICS_TAB)} + sx={getTabStyle(tabsWithError, VoltageLevelTab.CHARACTERISTICS_TAB)} /> } - sx={getTabStyle(tabIndexesWithError, VoltageLevelTab.STRUCTURE_TAB)} + sx={getTabStyle(tabsWithError, VoltageLevelTab.STRUCTURE_TAB)} disabled={watchHideBusBarSection} /> } - sx={getTabStyle(tabIndexesWithError, VoltageLevelTab.ADDITIONAL_INFORMATION_TAB)} + sx={getTabStyle(tabsWithError, VoltageLevelTab.ADDITIONAL_INFORMATION_TAB)} /> - diff --git a/src/features/network-modifications/voltageLevel/creation/index.ts b/src/features/network-modifications/voltageLevel/creation/index.ts index 823f5ff53..4dde4494a 100644 --- a/src/features/network-modifications/voltageLevel/creation/index.ts +++ b/src/features/network-modifications/voltageLevel/creation/index.ts @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ export { SwitchesBetweenSections } from './tabs'; -export { MAX_SECTIONS_COUNT } from './voltageLevel.constants'; +export { MAX_SECTIONS_COUNT, VOLTAGE_LEVEL_TAB_FIELDS, VoltageLevelTab } from './voltageLevel.constants'; export * from './VoltageLevelCreationForm'; export * from './voltageLevelCreation.utils'; export * from './voltageLevelCreation.types'; diff --git a/src/hooks/use-tabs.ts b/src/hooks/use-tabs.ts index 8b789efbb..164b6933b 100644 --- a/src/hooks/use-tabs.ts +++ b/src/hooks/use-tabs.ts @@ -1,98 +1,81 @@ -import { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { FieldErrors, get, useFormContext } from 'react-hook-form'; +/** + * Copyright (c) 2026, 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 { SyntheticEvent, useCallback, useMemo, useState } from 'react'; +import { FieldErrors, get } from 'react-hook-form'; import { isObjectEmpty } from '../utils/functions'; -export type UseTabsReturn = { - selectedTab: TTabValue; - setSelectedTab: (selectedTab: TTabValue) => void; - tabsWithError: TTabValue[]; - onTabChange: (event: SyntheticEvent, newValue: TTabValue) => void; - onError: (errors: FieldErrors) => void; -}; - export type UseTabsProps = { defaultTab: TTabValue; /** - * All possible tab values. Use a string enum for best compatibility. - * For numeric enums, prefer passing tabFields explicitly to avoid reverse-mapping issues. + * Live form errors (usually `formState.errors`). Used to refresh the + * highlight when the user switches tabs. Navigation NEVER reacts to it: + * the only navigation trigger is `onError`. */ + errors?: FieldErrors; + /** All possible tab values. Required if `tabFields` is omitted. */ tabValues?: TTabValue[]; - errors: FieldErrors; /** - * Maps each tab value to the field paths it owns. - * If omitted, each tab uses its own value (cast to string) as the field path. - * Required when TTabValue is a number. + * Maps each tab value to the react-hook-form field path(s) whose validity + * decides whether the tab is highlighted. Falls back to `[String(tabValue)]`. */ tabFields?: Partial>; }; +export type UseTabsReturn = { + selectedTab: TTabValue; + setSelectedTab: (tab: TTabValue) => void; + /** Errored tabs, excluding the currently selected one. */ + tabsWithError: TTabValue[]; + /** Handler for MUI ``. Refreshes the highlight. */ + onTabChange: (event: SyntheticEvent, newValue: TTabValue) => void; + /** + * Invalid-submit handler — pass it as the 2nd argument of + * `handleSubmit(onSubmit, onError)`. This is the ONLY place navigation happens: + * stays on the current tab if it has errors, otherwise jumps to the first errored tab. + */ + onError: (errors: FieldErrors) => void; +}; + export function useTabs({ defaultTab, - tabValues, errors, + tabValues, tabFields, }: Readonly>): UseTabsReturn { const [selectedTab, setSelectedTab] = useState(defaultTab); const [tabsWithError, setTabsWithError] = useState([]); - // useFormContext returns null when there is no FormProvider ancestor. - // This happens when useTabs is called from a plain hook (e.g. use-load-flow-parameters-form) - // rather than from inside a component tree already wrapped by FormProvider. - const formContext = useFormContext(); - const submitCount = formContext?.formState.submitCount ?? 0; - - const resolvedTabValues = useMemo( - () => tabValues ?? (Object.keys(tabFields ?? {}).map(Number) as TTabValue[]), - [tabValues, tabFields] - ); + const resolvedTabValues = useMemo(() => { + if (tabValues?.length) { + return tabValues; + } + // Derive from tabFields keys: numeric strings -> numbers, otherwise keep as-is. + return Object.keys(tabFields ?? {}).map((key) => { + const asNumber = Number(key); + return (Number.isNaN(asNumber) ? key : asNumber) as TTabValue; + }); + }, [tabValues, tabFields]); const getTabsWithError = useCallback( - (_errors: FieldErrors): TTabValue[] => { - return resolvedTabValues.filter((tabValue) => { + (_errors: FieldErrors): TTabValue[] => + resolvedTabValues.filter((tabValue) => { const fields = tabFields?.[tabValue] ?? [String(tabValue)]; return fields.some((field) => get(_errors, field)); - }); - }, + }), [resolvedTabValues, tabFields] ); - // Auto-navigate to the first errored tab on submit, without reacting to mid-input - // revalidation. No-ops when submitCount stays 0 (no FormProvider context). - const selectedTabRef = useRef(selectedTab); - selectedTabRef.current = selectedTab; - const errorsRef = useRef(errors); - errorsRef.current = errors; - const getTabsWithErrorRef = useRef(getTabsWithError); - getTabsWithErrorRef.current = getTabsWithError; - - useEffect(() => { - const currentErrors = errorsRef.current; - if (!currentErrors || isObjectEmpty(currentErrors)) { - return; - } - const tabsHasError = getTabsWithErrorRef.current(currentErrors); - if (tabsHasError.length > 0 && !tabsHasError.includes(selectedTabRef.current)) { - setSelectedTab(tabsHasError[0]); - setTabsWithError(tabsHasError.slice(1)); - } else if (tabsHasError.includes(selectedTabRef.current)) { - setTabsWithError(tabsHasError.filter((t) => t !== selectedTabRef.current)); - } - // submitCount is the intentional trigger; refs never change identity. - }, [submitCount]); - const onTabChange = useCallback( (_event: SyntheticEvent, newSelectedTab: TTabValue) => { setSelectedTab(newSelectedTab); - if (!errors || isObjectEmpty(errors)) { - return; - } - const tabsHasError = getTabsWithError(errors); - if (tabsHasError.includes(newSelectedTab)) { - setTabsWithError(tabsHasError.filter((errorTab) => errorTab !== newSelectedTab)); - } else { - setTabsWithError(tabsHasError); - } + // Refresh the highlight against live errors; never navigate here. + const erroredTabs = errors && !isObjectEmpty(errors) ? getTabsWithError(errors) : []; + setTabsWithError(erroredTabs.filter((errorTab) => errorTab !== newSelectedTab)); }, [errors, getTabsWithError] ); @@ -102,22 +85,18 @@ export function useTabs({ if (!_errors || isObjectEmpty(_errors)) { return; } - const tabsHasError = getTabsWithError(_errors); - if (tabsHasError.includes(selectedTab)) { - setTabsWithError(tabsHasError.filter((errorTab) => errorTab !== selectedTab)); - } else if (tabsHasError.length > 0) { - setSelectedTab(tabsHasError[0]); - setTabsWithError(tabsHasError.slice(1)); + const erroredTabs = getTabsWithError(_errors); + if (erroredTabs.includes(selectedTab)) { + // Errors in the current tab: stay here, highlight only the others. + setTabsWithError(erroredTabs.filter((errorTab) => errorTab !== selectedTab)); + } else if (erroredTabs.length > 0) { + // Errors only elsewhere: jump to the first errored tab, highlight the rest. + setSelectedTab(erroredTabs[0]); + setTabsWithError(erroredTabs.slice(1)); } }, [getTabsWithError, selectedTab] ); - return { - selectedTab, - setSelectedTab, - tabsWithError, - onTabChange, - onError, - }; + return { selectedTab, setSelectedTab, tabsWithError, onTabChange, onError }; } From 2a892203986f476cd566dc32228b6131735073bc Mon Sep 17 00:00:00 2001 From: Radouane Khouadri Date: Thu, 27 Aug 2026 15:26:46 +0200 Subject: [PATCH 3/6] address reviews and migrate sensi to use on validationError --- .../dynamic-margin-calculation-inline.tsx | 4 +- .../dynamic-margin-calculation.type.ts | 2 + .../dynamic-security-analysis-inline.tsx | 4 +- .../dynamic-security-analysis.type.ts | 2 + .../dynamic-simulation-inline.tsx | 4 +- .../dynamic-simulation.type.ts | 2 + .../loadflow/load-flow-parameters-utils.ts | 2 + .../loadflow/use-load-flow-parameters-form.ts | 3 +- .../network-visualizations/constants.ts | 3 ++ ...-network-visualizations-parameters-form.ts | 3 +- .../parameters/security-analysis/constants.ts | 2 + .../use-security-analysis-parameters-form.ts | 3 +- .../parameters/sensi/columns-definitions.ts | 22 ++++++++++ .../sensitivity-analysis-parameters-form.tsx | 1 + ...sensitivity-analysis-parameters-inline.tsx | 5 ++- .../sensi/sensitivity-parameters-selector.tsx | 42 +++---------------- .../use-sensitivity-analysis-parameters.ts | 37 +++++++++++++++- .../short-circuit-parameters-utils.ts | 2 + .../use-short-circuit-parameters-form.ts | 3 +- .../parameters/voltage-init/constants.ts | 2 + .../use-voltage-init-parameters-form.ts | 3 +- 21 files changed, 100 insertions(+), 51 deletions(-) diff --git a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx index 03b19f803..36bab64e8 100644 --- a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx +++ b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx @@ -21,7 +21,7 @@ import { CustomFormProvider } from '../../../components/ui'; import { TreeViewFinderNodeProps } from '../../../components/ui/treeViewFinder'; import { fetchDynamicMarginCalculationParameters } from '../../../services/dynamic-margin-calculation'; import { useSnackMessage, useTabs } from '../../../hooks'; -import { TabValues } from './dynamic-margin-calculation.type'; +import { TAB_VALUES, TabValues } from './dynamic-margin-calculation.type'; type DynamicMarginCalculationInlineProps = { studyUuid: UUID | null; @@ -50,7 +50,7 @@ export function DynamicMarginCalculationInline({ const useTabsReturn = useTabs({ defaultTab: TabValues.TAB_TIME_DELAY, - tabValues: Object.values(TabValues), + tabValues: TAB_VALUES, errors: formState.errors, }); diff --git a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation.type.ts b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation.type.ts index ca8af931f..2250ef40b 100644 --- a/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation.type.ts +++ b/src/features/parameters/dynamic-margin-calculation/dynamic-margin-calculation.type.ts @@ -8,3 +8,5 @@ export enum TabValues { TAB_TIME_DELAY = 'TAB_TIME_DELAY', TAB_LOADS_VARIATIONS = 'TAB_LOADS_VARIATIONS', } + +export const TAB_VALUES = Object.values(TabValues); diff --git a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx index 048b37aa0..3a86d6503 100644 --- a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx +++ b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis-inline.tsx @@ -18,7 +18,7 @@ import { ParameterLayout } from '../common'; import { CustomFormProvider } from '../../../components/ui'; import { DynamicSecurityAnalysisParametersForm } from './dynamic-security-analysis-parameters-form'; import { useTabs } from '../../../hooks'; -import { TabValues } from './dynamic-security-analysis.type'; +import { TAB_VALUES, TabValues } from './dynamic-security-analysis.type'; type DynamicSecurityAnalysisInlineProps = { studyUuid: UUID | null; @@ -44,7 +44,7 @@ export function DynamicSecurityAnalysisInline({ const useTabsReturn = useTabs({ defaultTab: TabValues.SCENARIO, - tabValues: Object.values(TabValues), + tabValues: TAB_VALUES, errors: formState.errors, }); diff --git a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis.type.ts b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis.type.ts index 4d0d57130..f3ee1ec83 100644 --- a/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis.type.ts +++ b/src/features/parameters/dynamic-security-analysis/dynamic-security-analysis.type.ts @@ -8,3 +8,5 @@ export enum TabValues { SCENARIO = 'scenario', CONTINGENCY = 'contingency', } + +export const TAB_VALUES = Object.values(TabValues); diff --git a/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx b/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx index fc285d1fe..3f213c2e3 100644 --- a/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx +++ b/src/features/parameters/dynamic-simulation/dynamic-simulation-inline.tsx @@ -24,7 +24,7 @@ import { toParamsEnriched, useDynamicSimulationParametersForm, } from './use-dynamic-simulation-parameters-form'; -import { TabValues } from './dynamic-simulation.type'; +import { TAB_VALUES, TabValues } from './dynamic-simulation.type'; import { DynamicSimulationForm } from './dynamic-simulation-parameters-form'; import { fetchDynamicSimulationParameters } from '../../../services/dynamic-simulation'; import { ExpertFilter, IdentifiableAttributes } from '../../../components/composite/filter'; @@ -66,7 +66,7 @@ export function DynamicSimulationInline({ const useTabsReturn = useTabs({ defaultTab: TabValues.TAB_TIME_DELAY, - tabValues: Object.values(TabValues), + tabValues: TAB_VALUES, errors: formState.errors, }); diff --git a/src/features/parameters/dynamic-simulation/dynamic-simulation.type.ts b/src/features/parameters/dynamic-simulation/dynamic-simulation.type.ts index b939ca4e1..4c922f6d0 100644 --- a/src/features/parameters/dynamic-simulation/dynamic-simulation.type.ts +++ b/src/features/parameters/dynamic-simulation/dynamic-simulation.type.ts @@ -11,3 +11,5 @@ export enum TabValues { TAB_NETWORK = 'TAB_NETWORK', TAB_CURVE = 'TAB_CURVE', } + +export const TAB_VALUES = Object.values(TabValues); diff --git a/src/features/parameters/loadflow/load-flow-parameters-utils.ts b/src/features/parameters/loadflow/load-flow-parameters-utils.ts index b4236a916..10b95643d 100644 --- a/src/features/parameters/loadflow/load-flow-parameters-utils.ts +++ b/src/features/parameters/loadflow/load-flow-parameters-utils.ts @@ -47,6 +47,8 @@ export enum TabValues { PROVIDER_SPECIFIC = 'ProviderSpecific', } +export const TAB_VALUES = Object.values(TabValues); + export const TAB_FIELDS: Record = { [TabValues.GENERAL]: [COMMON_PARAMETERS], [TabValues.PROVIDER_SPECIFIC]: [SPECIFIC_PARAMETERS], diff --git a/src/features/parameters/loadflow/use-load-flow-parameters-form.ts b/src/features/parameters/loadflow/use-load-flow-parameters-form.ts index f294d29d0..b0e7740ae 100644 --- a/src/features/parameters/loadflow/use-load-flow-parameters-form.ts +++ b/src/features/parameters/loadflow/use-load-flow-parameters-form.ts @@ -17,6 +17,7 @@ import { setLimitReductions, splitCommonParameters, TAB_FIELDS, + TAB_VALUES, TabValues, } from './load-flow-parameters-utils'; import { LoadFlowParametersInfos } from './load-flow-parameters-type'; @@ -235,7 +236,7 @@ export const useLoadFlowParametersForm = ( onError: onValidationError, } = useTabs({ defaultTab: TabValues.GENERAL, - tabValues: Object.values(TabValues), + tabValues: TAB_VALUES, errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); diff --git a/src/features/parameters/network-visualizations/constants.ts b/src/features/parameters/network-visualizations/constants.ts index e160a6587..4561fed21 100644 --- a/src/features/parameters/network-visualizations/constants.ts +++ b/src/features/parameters/network-visualizations/constants.ts @@ -30,6 +30,9 @@ export enum NetworkVisualizationTabValues { SINGLE_LINE_DIAGRAM = 'singleLineDiagramParameters', NETWORK_AREA_DIAGRAM = 'networkAreaDiagramParameters', } + +export const NV_TAB_VALUES = Object.values(NetworkVisualizationTabValues); + export const MAP_MANUAL_REFRESH = 'MapManualRefresh'; export const LINE_FLOW_MODE = 'LineFlowMode'; export const MAP_BASE_MAP = 'MapBaseMap'; diff --git a/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts b/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts index 4a7c44a50..4d214ddcb 100644 --- a/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts +++ b/src/features/parameters/network-visualizations/use-network-visualizations-parameters-form.ts @@ -24,6 +24,7 @@ import { NetworkVisualizationTabValues as TabValues, PARAM_NAD_POSITIONS_GENERATION_MODE, PARAM_STATE_ESTIMATION, + NV_TAB_VALUES, } from './constants'; import { getNetworkVisualizationsParameters, @@ -135,7 +136,7 @@ export const useNetworkVisualizationParametersForm = ({ onError: onValidationError, } = useTabs({ defaultTab: TabValues.MAP, - tabValues: Object.values(TabValues), + tabValues: NV_TAB_VALUES, errors: formMethods.formState.errors, }); diff --git a/src/features/parameters/security-analysis/constants.ts b/src/features/parameters/security-analysis/constants.ts index 4efc82255..84a225def 100644 --- a/src/features/parameters/security-analysis/constants.ts +++ b/src/features/parameters/security-analysis/constants.ts @@ -16,3 +16,5 @@ export enum TabValues { Aggravation = 'Aggravation', LimitReductions = 'LimitReductions', } + +export const TAB_VALUES = Object.values(TabValues); diff --git a/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts b/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts index 3196c72de..24e5cf476 100644 --- a/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts +++ b/src/features/parameters/security-analysis/use-security-analysis-parameters-form.ts @@ -40,6 +40,7 @@ import { LOW_VOLTAGE_ABSOLUTE_THRESHOLD, LOW_VOLTAGE_PROPORTIONAL_THRESHOLD, TabValues, + TAB_VALUES, } from './constants'; export interface UseSecurityAnalysisParametersFormReturn { @@ -170,7 +171,7 @@ export const useSecurityAnalysisParametersForm = ( onError: onValidationError, } = useTabs({ defaultTab: TabValues.Contingencies, - tabValues: Object.values(TabValues), + tabValues: TAB_VALUES, errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); diff --git a/src/features/parameters/sensi/columns-definitions.ts b/src/features/parameters/sensi/columns-definitions.ts index 37385bf2f..e3f8a536a 100644 --- a/src/features/parameters/sensi/columns-definitions.ts +++ b/src/features/parameters/sensi/columns-definitions.ts @@ -11,6 +11,11 @@ import { HVDC_LINES, INJECTIONS, MONITORED_BRANCHES, + PARAMETER_SENSI_HVDC, + PARAMETER_SENSI_INJECTION, + PARAMETER_SENSI_INJECTIONS_SET, + PARAMETER_SENSI_NODES, + PARAMETER_SENSI_PST, PSTS, SENSITIVITY_TYPE, SUPERVISED_VOLTAGE_LEVELS, @@ -305,9 +310,26 @@ export enum SensiTabValues { SensitivityNodes = 1, } +export const SENSI_TAB_FIELDS: Partial> = { + [SensiTabValues.SensitivityBranches]: [ + PARAMETER_SENSI_INJECTIONS_SET, + PARAMETER_SENSI_INJECTION, + PARAMETER_SENSI_HVDC, + PARAMETER_SENSI_PST, + ], + [SensiTabValues.SensitivityNodes]: [PARAMETER_SENSI_NODES], +}; + export enum SensiBranchesTabValues { SensiInjectionsSet = 0, SensiInjection = 1, SensiHVDC = 2, SensiPST = 3, } + +export const SENSI_BRANCHES_TAB_FIELDS: Partial> = { + [SensiBranchesTabValues.SensiInjectionsSet]: [PARAMETER_SENSI_INJECTIONS_SET], + [SensiBranchesTabValues.SensiInjection]: [PARAMETER_SENSI_INJECTION], + [SensiBranchesTabValues.SensiHVDC]: [PARAMETER_SENSI_HVDC], + [SensiBranchesTabValues.SensiPST]: [PARAMETER_SENSI_PST], +}; diff --git a/src/features/parameters/sensi/sensitivity-analysis-parameters-form.tsx b/src/features/parameters/sensi/sensitivity-analysis-parameters-form.tsx index 9d571f194..f52e26b7a 100644 --- a/src/features/parameters/sensi/sensitivity-analysis-parameters-form.tsx +++ b/src/features/parameters/sensi/sensitivity-analysis-parameters-form.tsx @@ -44,6 +44,7 @@ export function SensitivityAnalysisParametersForm({ isStudyLinked={sensitivityAnalysisMethods.isStudyLinked} isRootNode={isRootNode} globalBuildStatus={globalBuildStatus} + useSensiTabsReturn={sensitivityAnalysisMethods.useSensiTabsReturn} /> diff --git a/src/features/parameters/sensi/sensitivity-analysis-parameters-inline.tsx b/src/features/parameters/sensi/sensitivity-analysis-parameters-inline.tsx index 1d0729646..b2bfa649d 100644 --- a/src/features/parameters/sensi/sensitivity-analysis-parameters-inline.tsx +++ b/src/features/parameters/sensi/sensitivity-analysis-parameters-inline.tsx @@ -116,7 +116,10 @@ export function SensitivityAnalysisParametersInline({ }} selectParameterHandler={handleSensibilityParameter} resetHandler={clear} - validateHandler={handleSubmit(sensitivityAnalysisMethods.onSaveInline)} + validateHandler={handleSubmit( + sensitivityAnalysisMethods.onSaveInline, + sensitivityAnalysisMethods.onValidationError + )} validateDisabled={ sensitivityAnalysisMethods.isLoading || sensitivityAnalysisMethods.isMaxResultsReached || diff --git a/src/features/parameters/sensi/sensitivity-parameters-selector.tsx b/src/features/parameters/sensi/sensitivity-parameters-selector.tsx index 10d6d5636..20679d08d 100644 --- a/src/features/parameters/sensi/sensitivity-parameters-selector.tsx +++ b/src/features/parameters/sensi/sensitivity-parameters-selector.tsx @@ -8,7 +8,6 @@ import { useCallback, useEffect, useMemo } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { Box, Card, CardContent, Grid, Tab, Tabs } from '@mui/material'; -import { useFormContext, FieldErrors } from 'react-hook-form'; import { COLUMNS_DEFINITIONS_HVDCS, @@ -37,7 +36,7 @@ import { ParameterTableField } from '../common/parameter-table-field'; import { DndColumn } from '../../../components/composite/dnd-table'; import { parametersStyles } from '../parameters-style'; -import { useTabs } from '../../../hooks'; +import { UseSensiTabsReturn } from './use-sensitivity-analysis-parameters'; const styles = { circularProgress: (theme) => ({ @@ -80,6 +79,7 @@ interface SensitivityParametersSelectorProps { isStudyLinked: boolean; isRootNode: boolean; globalBuildStatus?: BuildStatus; + useSensiTabsReturn: UseSensiTabsReturn; } interface TabInfo { @@ -95,54 +95,22 @@ function SensitivityParametersSelector({ isStudyLinked, isRootNode, globalBuildStatus, + useSensiTabsReturn, }: Readonly) { const intl = useIntl(); - const { - formState: { errors, dirtyFields }, - } = useFormContext(); - - const dirtyErrors = useMemo((): FieldErrors => { - if (!Object.keys(dirtyFields).length) { - return {}; - } - return Object.fromEntries( - Object.entries(errors).filter(([key]) => !!dirtyFields[key as keyof typeof dirtyFields]) - ) as FieldErrors; - }, [errors, dirtyFields]); const { selectedTab: tabValue, setSelectedTab: setTabValue, tabsWithError, onTabChange: handleTabChange, - } = useTabs({ - defaultTab: SensiTabValues.SensitivityBranches, - errors: dirtyErrors, - tabFields: { - [SensiTabValues.SensitivityBranches]: [ - PARAMETER_SENSI_INJECTIONS_SET, - PARAMETER_SENSI_INJECTION, - PARAMETER_SENSI_HVDC, - PARAMETER_SENSI_PST, - ], - [SensiTabValues.SensitivityNodes]: [PARAMETER_SENSI_NODES], - }, - }); + } = useSensiTabsReturn.useTabsReturn; const { selectedTab: subTabValue, tabsWithError: subTabsWithError, onTabChange: handleSubTabChange, - } = useTabs({ - defaultTab: SensiBranchesTabValues.SensiInjectionsSet, - errors: dirtyErrors, - tabFields: { - [SensiBranchesTabValues.SensiInjectionsSet]: [PARAMETER_SENSI_INJECTIONS_SET], - [SensiBranchesTabValues.SensiInjection]: [PARAMETER_SENSI_INJECTION], - [SensiBranchesTabValues.SensiHVDC]: [PARAMETER_SENSI_HVDC], - [SensiBranchesTabValues.SensiPST]: [PARAMETER_SENSI_PST], - }, - }); + } = useSensiTabsReturn.useSubTabsReturn; const tabInfo: TabInfo[] = [ { diff --git a/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts b/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts index 3ca0eba29..a14d74315 100644 --- a/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts +++ b/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts @@ -4,7 +4,7 @@ * 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 { useForm, UseFormReturn } from 'react-hook-form'; +import { FieldErrors, useForm, UseFormReturn } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { type ObjectSchema } from 'yup'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -55,11 +55,12 @@ import { setSensitivityAnalysisParameters, } from '../../../services/sensitivity-analysis'; import { DEFAULT_TIMEOUT_MS, IGNORE_SIGNAL, updateParameter } from '../../../services'; -import { useSnackMessage } from '../../../hooks'; +import { useSnackMessage, useTabs, UseTabsReturn } from '../../../hooks'; import { getNameElementEditorEmptyFormData } from '../../../components/ui/dialogs/name-element-editor'; import { BuildStatus } from '../../node'; import { CONTINGENCIES, PROVIDER } from '../common/constants'; import { ACTIVATED } from '../common/parameter-table-field'; +import { SensiTabValues, SensiBranchesTabValues, SENSI_TAB_FIELDS, SENSI_BRANCHES_TAB_FIELDS } from './columns-definitions'; export interface UseSensitivityAnalysisParametersReturn { formMethods: UseFormReturn; @@ -79,6 +80,13 @@ export interface UseSensitivityAnalysisParametersReturn { emptyFormData: Record; factorsCount: FactorsCount; resetFactorsCount: () => void; + onValidationError: (errors: FieldErrors) => void; + useSensiTabsReturn: UseSensiTabsReturn; +} + +export interface UseSensiTabsReturn { + useTabsReturn: UseTabsReturn; + useSubTabsReturn: UseTabsReturn; } type UseSensitivityAnalysisParametersFormProps = @@ -153,6 +161,26 @@ export const useSensitivityAnalysisParametersForm = ({ const { reset, getValues } = formMethods; + const useTabsReturn = useTabs({ + defaultTab: SensiTabValues.SensitivityBranches, + errors: formMethods.formState.errors, + tabFields: SENSI_TAB_FIELDS, + }); + + const useSubTabsReturn = useTabs({ + defaultTab: SensiBranchesTabValues.SensiInjectionsSet, + errors: formMethods.formState.errors, + tabFields: SENSI_BRANCHES_TAB_FIELDS, + }); + + const onError = useCallback( + (errors: FieldErrors) => { + useTabsReturn.onError(errors); + useSubTabsReturn.onError(errors); + }, + [useTabsReturn, useSubTabsReturn] + ); + const formattedProviders = Object.keys(providers).map((key) => ({ id: key, label: providers[key], @@ -494,5 +522,10 @@ export const useSensitivityAnalysisParametersForm = ({ emptyFormData, factorsCount, resetFactorsCount, + onValidationError: onError, + useSensiTabsReturn: { + useTabsReturn, + useSubTabsReturn, + }, }; }; diff --git a/src/features/parameters/short-circuit/short-circuit-parameters-utils.ts b/src/features/parameters/short-circuit/short-circuit-parameters-utils.ts index 213e53e21..600515c30 100644 --- a/src/features/parameters/short-circuit/short-circuit-parameters-utils.ts +++ b/src/features/parameters/short-circuit/short-circuit-parameters-utils.ts @@ -55,6 +55,8 @@ export enum ShortCircuitParametersTabValues { POWER_ELECTRONICS = 'PowerElectronics', } +export const TAB_VALUES = Object.values(ShortCircuitParametersTabValues); + export const TAB_FIELDS: Record = { [ShortCircuitParametersTabValues.GENERAL]: [PROVIDER, SHORT_CIRCUIT_PREDEFINED_PARAMS, COMMON_PARAMETERS], [ShortCircuitParametersTabValues.STUDY_AREA]: [ diff --git a/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts b/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts index 19ef30d5c..f3c41d2b9 100644 --- a/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts +++ b/src/features/parameters/short-circuit/use-short-circuit-parameters-form.ts @@ -41,6 +41,7 @@ import { getSpecificShortCircuitParametersFormSchema, ShortCircuitParametersTabValues, TAB_FIELDS, + TAB_VALUES, } from './short-circuit-parameters-utils'; import { snackWithFallback } from '../../../utils/error'; @@ -241,7 +242,7 @@ export const useShortCircuitParametersForm = ({ onError: onValidationError, } = useTabs({ defaultTab: ShortCircuitParametersTabValues.GENERAL, - tabValues: Object.values(ShortCircuitParametersTabValues), + tabValues: TAB_VALUES, errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); diff --git a/src/features/parameters/voltage-init/constants.ts b/src/features/parameters/voltage-init/constants.ts index 35b76c769..21afab2cb 100644 --- a/src/features/parameters/voltage-init/constants.ts +++ b/src/features/parameters/voltage-init/constants.ts @@ -11,6 +11,8 @@ export enum VoltageInitTabValues { EQUIPMENTS_SELECTION = 'equipmentSelection', } +export const VOLTAGE_INIT_TAB_VALUES = Object.values(VoltageInitTabValues); + export const GENERAL = 'GENERAL'; export const GENERAL_APPLY_MODIFICATIONS = 'GENERAL_APPLY_MODIFICATIONS'; export const DEFAULT_GENERAL_APPLY_MODIFICATIONS = true; diff --git a/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts b/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts index 39dca1241..3be169aaa 100644 --- a/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts +++ b/src/features/parameters/voltage-init/use-voltage-init-parameters-form.ts @@ -31,6 +31,7 @@ import { VOLTAGE_LIMITS_DEFAULT, VOLTAGE_LIMITS_MODIFICATION, VoltageInitTabValues as TabValues, + VOLTAGE_INIT_TAB_VALUES as TAB_VALUES, } from './constants'; import { getVoltageInitParameters, updateParameter, updateVoltageInitParameters } from '../../../services'; import { useTabs, useSnackMessage } from '../../../hooks'; @@ -203,7 +204,7 @@ export const useVoltageInitParametersForm = ({ onError: onValidationError, } = useTabs({ defaultTab: TabValues.GENERAL, - tabValues: Object.values(TabValues), + tabValues: TAB_VALUES, errors: formMethods.formState.errors, tabFields: TAB_FIELDS, }); From cf2fbd42ea161736e665089a024fd1801ea858d1 Mon Sep 17 00:00:00 2001 From: Radouane Khouadri Date: Thu, 27 Aug 2026 15:48:08 +0200 Subject: [PATCH 4/6] fix format --- .../sensi/use-sensitivity-analysis-parameters.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts b/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts index a14d74315..3adf855aa 100644 --- a/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts +++ b/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts @@ -60,7 +60,12 @@ import { getNameElementEditorEmptyFormData } from '../../../components/ui/dialog import { BuildStatus } from '../../node'; import { CONTINGENCIES, PROVIDER } from '../common/constants'; import { ACTIVATED } from '../common/parameter-table-field'; -import { SensiTabValues, SensiBranchesTabValues, SENSI_TAB_FIELDS, SENSI_BRANCHES_TAB_FIELDS } from './columns-definitions'; +import { + SensiTabValues, + SensiBranchesTabValues, + SENSI_TAB_FIELDS, + SENSI_BRANCHES_TAB_FIELDS, +} from './columns-definitions'; export interface UseSensitivityAnalysisParametersReturn { formMethods: UseFormReturn; From 3867d3a069e01aeb3c0f97ff309bc4e8498437a0 Mon Sep 17 00:00:00 2001 From: Radouane Khouadri Date: Mon, 31 Aug 2026 20:09:16 +0200 Subject: [PATCH 5/6] fix access ambient const enums --- src/features/network-modifications/line/common/line.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/network-modifications/line/common/line.utils.ts b/src/features/network-modifications/line/common/line.utils.ts index 64a504d41..c757bb3c1 100644 --- a/src/features/network-modifications/line/common/line.utils.ts +++ b/src/features/network-modifications/line/common/line.utils.ts @@ -12,7 +12,7 @@ export interface LineDialogOptions { withConnectivity?: boolean; } -export const enum LineDialogTab { +export enum LineDialogTab { CONNECTIVITY_TAB = 0, CHARACTERISTICS_TAB = 1, LIMITS_TAB = 2, From e727e98fba0b9e6061e7424a3fe9e1fcb293b879 Mon Sep 17 00:00:00 2001 From: Radouane Khouadri Date: Tue, 1 Sep 2026 15:14:08 +0200 Subject: [PATCH 6/6] refactor --- .../sensi/use-sensitivity-analysis-parameters.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts b/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts index 3adf855aa..22844bc35 100644 --- a/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts +++ b/src/features/parameters/sensi/use-sensitivity-analysis-parameters.ts @@ -178,6 +178,14 @@ export const useSensitivityAnalysisParametersForm = ({ tabFields: SENSI_BRANCHES_TAB_FIELDS, }); + const useSensiTabsReturn = useMemo( + () => ({ + useTabsReturn, + useSubTabsReturn, + }), + [useTabsReturn, useSubTabsReturn] + ); + const onError = useCallback( (errors: FieldErrors) => { useTabsReturn.onError(errors); @@ -528,9 +536,6 @@ export const useSensitivityAnalysisParametersForm = ({ factorsCount, resetFactorsCount, onValidationError: onError, - useSensiTabsReturn: { - useTabsReturn, - useSubTabsReturn, - }, + useSensiTabsReturn, }; };