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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ import useStudyNavigationSync from 'hooks/use-study-navigation-sync';
import { useOptionalLoadingParameters } from '../hooks/use-optional-loading-parameters';
import { SortWay } from '../types/custom-aggrid-types.ts';
import { useBaseVoltages } from '../hooks/use-base-voltages.ts';
import { useGlobalFilterOptions } from './results/common/global-filter/use-global-filter-options.ts';
import { useGlobalFilterOptions } from './results/common/global-filter/hooks/use-global-filter-options.ts';
import { updateComputationColumnFilters, updateComputationGlobalFilters } from './results/common/utils.ts';
import { isEditingGlobalFilter } from '../utils/editing-global-filter-sync.ts';
import { isEditingGlobalFilter } from './results/common/global-filter/editing-global-filter-sync.ts';
import { cleanupStaleStudyData } from '../redux/session-storage/local-storage';

const noUserManager = { instance: null, error: null };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* 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 { fetchStudyMetadata } from '@gridsuite/commons-ui';

export async function fetchSubstationPropertiesGlobalFilters(): Promise<{
substationPropertiesGlobalFilters?: Map<string, string[]>;
}> {
const { substationPropertiesGlobalFilters } = await fetchStudyMetadata();
const definedSubstationPropertiesGlobalFilters: Map<string, string[]> = substationPropertiesGlobalFilters
? new Map(Object.entries(substationPropertiesGlobalFilters))
: new Map<string, string[]>();
return {
substationPropertiesGlobalFilters: definedSubstationPropertiesGlobalFilters,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Copyright (c) 2025, 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 { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react';
import { GlobalFilter, RecentGlobalFilter } from '../global-filter.type';
import {
ElementAttributes,
ElementType,
EquipmentType,
fetchDirectoryElementPath,
fetchElementsInfos,
snackWithFallback,
useSnackMessage,
} from '@gridsuite/commons-ui';
import { computeFullPath } from '../../../../../utils/compute-title';
import {
addToGlobalFilterOptions,
addToSelectedGlobalFilters,
clearSelectedGlobalFilters as clearSelectedGlobalFiltersAction,
markNotFoundGlobalFiltersAsDeleted,
removeFromGlobalFilterOptions,
removeFromSelectedGlobalFilters,
} from '../../../../../redux/actions';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch } from '../../../../../redux/store';
import { HttpStatusCode } from '../../../../../utils/http-status-code';
import { TableType } from '../../../../../types/custom-aggrid-types';
import { AppState } from '../../../../../redux/reducer.type';
import GlobalFilterContextProvider from '../context/global-filter-context-provider';
import { fetchSubstationPropertiesGlobalFilters } from './global-filter-app-data';
import { useLocalizedCountries } from '../../../../utils/localized-countries-hook';
import type { UUID } from 'node:crypto';

import { FilterType, isCriteriaFilter } from '../filter.type';

const EMPTY_ARRAY: RecentGlobalFilter[] = [];

type FilterUpdateResult = { kind: 'updated' | 'notFound'; filter: GlobalFilter } | { kind: 'unchanged' };

type GlobalFilterProviderProps = PropsWithChildren<{
filterCategories: FilterType[];
genericFiltersStrictMode: boolean;
filterableEquipmentTypes: string[];
tableType: TableType;
tableUuid: string;
}>;

export default function GlobalFilterProvider({
children,
filterCategories,
genericFiltersStrictMode = false,
filterableEquipmentTypes = [],
tableType,
tableUuid,
}: Readonly<GlobalFilterProviderProps>) {
const dispatch = useDispatch<AppDispatch>();
const { snackError } = useSnackMessage();
const { translate: translateCountryCode } = useLocalizedCountries();
const [substationPropertiesGlobalFilters, setSubstationPropertiesGlobalFilters] = useState<Map<string, string[]>>();

const globalFilterOptions = useSelector((state: AppState) => state.globalFilterOptions);

const selectedFilterIds = useSelector((state: AppState) => state.tableFilters.globalFilters[tableUuid]?.selected);

const recentGlobalFilters = useSelector(
(state: AppState) => state.tableFilters.globalFilters[tableUuid]?.recents ?? EMPTY_ARRAY
);

const selectedGlobalFilters = useMemo(
() =>
selectedFilterIds
? selectedFilterIds
.map((id) => globalFilterOptions.find((opt) => opt.id === id))
.filter((filter): filter is GlobalFilter => filter !== undefined)
: [],
[selectedFilterIds, globalFilterOptions]
);

useEffect(() => {
fetchSubstationPropertiesGlobalFilters().then(({ substationPropertiesGlobalFilters }) => {
setSubstationPropertiesGlobalFilters(substationPropertiesGlobalFilters);
});
}, []);

const updateGenericFilter = useCallback(
async (genericFilter: GlobalFilter): Promise<FilterUpdateResult> => {
try {
if (!genericFilter.uuid) {
return { kind: 'unchanged' };
}
const response: ElementAttributes[] = await fetchDirectoryElementPath(genericFilter.uuid);
const parentDirectoriesNames = response.map((parent) => parent.elementName);
const label =
response.find((parent) => parent.type === ElementType.FILTER)?.elementName ?? genericFilter.label;
const path = computeFullPath(parentDirectoriesNames);
if (path !== genericFilter.path || label !== genericFilter.label) {
return { kind: 'updated', filter: { ...genericFilter, path, label } };
}
return { kind: 'unchanged' };
} catch (responseError) {
const error = responseError as Error & { status: number };
if (error.status === HttpStatusCode.NOT_FOUND) {
return { kind: 'notFound', filter: genericFilter };
}
snackWithFallback(snackError, error, {
headerId: 'ComputationFilterResultsError',
});
return { kind: 'unchanged' };
}
},
[snackError]
);

// Check the selected global filters and mark them as deleted if they no longer exist.
useEffect(() => {
const checkSelectedFilters = async () => {
const genericFilters: GlobalFilter[] = selectedGlobalFilters.filter(
(globalFilter) => isCriteriaFilter(globalFilter) && !globalFilter.deleted
);
const results = await Promise.all(genericFilters.map(updateGenericFilter));
const updatedFilters: GlobalFilter[] = [];
const notFoundFilters: GlobalFilter[] = [];
for (const result of results) {
if (result.kind === 'updated') updatedFilters.push(result.filter);
else if (result.kind === 'notFound') notFoundFilters.push(result.filter);
}
if (updatedFilters.length) {
dispatch(addToGlobalFilterOptions(updatedFilters));
}
if (notFoundFilters.length) {
dispatch(markNotFoundGlobalFiltersAsDeleted(notFoundFilters, tableUuid, tableType));
}
};

checkSelectedFilters().catch((error) => console.error(error));
}, [dispatch, selectedGlobalFilters, tableType, tableUuid, updateGenericFilter]);

const selectGlobalFilter = useCallback(
(id: string) => {
dispatch(addToSelectedGlobalFilters(tableType, tableUuid, [id]));
},
[dispatch, tableType, tableUuid]
);

const unselectGlobalFilters = useCallback(
(ids: string[]) => {
dispatch(removeFromSelectedGlobalFilters(tableType, tableUuid, ids));
},
[dispatch, tableType, tableUuid]
);

const clearSelectedGlobalFilters = useCallback(() => {
dispatch(clearSelectedGlobalFiltersAction(tableType, tableUuid));
}, [dispatch, tableType, tableUuid]);

const removeGlobalFilterOption = useCallback(
(id: string) => {
dispatch(removeFromGlobalFilterOptions(id));
},
[dispatch]
);

const addFiltersToGlobalFiltersOptions = useCallback(
async (elementIds: UUID[]) => {
const elements: ElementAttributes[] = await fetchElementsInfos(elementIds);
const newlySelectedFilters: GlobalFilter[] = [];
elements.forEach((element: ElementAttributes) => {
// ignore already selected filters and non-generic filters :
if (!selectedGlobalFilters.find((filter) => filter.uuid && filter.uuid === element.elementUuid)) {

Check warning on line 173 in src/components/results/common/global-filter/adapter/global-filter-provider.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.some(…)` over `.find(…)`.

See more on https://sonarcloud.io/project/issues?id=gridsuite_gridstudy-app&issues=AZ8YoXrMLu1dXKlfnL-B&open=AZ8YoXrMLu1dXKlfnL-B&pullRequest=4059
// add the others
const substationOrVoltageLevel =
element.specificMetadata?.equipmentType === EquipmentType.SUBSTATION ||
element.specificMetadata?.equipmentType === EquipmentType.VOLTAGE_LEVEL;
newlySelectedFilters.push({
id: element.elementUuid,
uuid: element.elementUuid,
equipmentType: element.specificMetadata?.equipmentType,
label: element.elementName,
filterType: substationOrVoltageLevel ? FilterType.SUBSTATION_OR_VL : FilterType.GENERIC_FILTER,
filterTypeFromMetadata: element.specificMetadata?.type,
});
}
});

dispatch(addToGlobalFilterOptions(newlySelectedFilters));
newlySelectedFilters.forEach((filter) =>
dispatch(addToSelectedGlobalFilters(tableType, tableUuid, [filter.id]))
);
},
[dispatch, selectedGlobalFilters, tableType, tableUuid]
);

return (
<GlobalFilterContextProvider
globalFilterOptions={globalFilterOptions}
selectedGlobalFilters={selectedGlobalFilters}
recentGlobalFilters={recentGlobalFilters}
substationPropertiesGlobalFilters={substationPropertiesGlobalFilters}
filterCategories={filterCategories}
genericFiltersStrictMode={genericFiltersStrictMode}
filterableEquipmentTypes={filterableEquipmentTypes}
translateCountryCode={translateCountryCode}
selectGlobalFilter={selectGlobalFilter}
unselectGlobalFilters={unselectGlobalFilters}
clearSelectedGlobalFilters={clearSelectedGlobalFilters}
removeGlobalFilterOption={removeGlobalFilterOption}
addFiltersToGlobalFiltersOptions={addFiltersToGlobalFiltersOptions}
>
{children}
</GlobalFilterContextProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* 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 { useMemo } from 'react';
import { GlobalFilterContext } from './global-filter-context';
import { GlobalFilterContextProviderProps, GlobalFilterContextValue } from './global-filter-context.type';

export default function GlobalFilterContextProvider({
children,
globalFilterOptions,
selectedGlobalFilters,
recentGlobalFilters,
substationPropertiesGlobalFilters,
filterCategories,
genericFiltersStrictMode,
filterableEquipmentTypes,
translateCountryCode,
selectGlobalFilter,
unselectGlobalFilters,
clearSelectedGlobalFilters,
removeGlobalFilterOption,
addFiltersToGlobalFiltersOptions,
}: Readonly<GlobalFilterContextProviderProps>) {
const value = useMemo<GlobalFilterContextValue>(
() => ({
globalFilterOptions,
selectedGlobalFilters,
recentGlobalFilters,
substationPropertiesGlobalFilters,
filterCategories,
genericFiltersStrictMode,
filterableEquipmentTypes,
translateCountryCode,
selectGlobalFilter,
unselectGlobalFilters,
clearSelectedGlobalFilters,
removeGlobalFilterOption,
addFiltersToGlobalFiltersOptions,
}),
[
globalFilterOptions,
selectedGlobalFilters,
recentGlobalFilters,
substationPropertiesGlobalFilters,
filterCategories,
genericFiltersStrictMode,
filterableEquipmentTypes,
translateCountryCode,
selectGlobalFilter,
unselectGlobalFilters,
clearSelectedGlobalFilters,
removeGlobalFilterOption,
addFiltersToGlobalFiltersOptions,
]
);

return <GlobalFilterContext.Provider value={value}>{children}</GlobalFilterContext.Provider>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* 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 { createContext, useContext } from 'react';
import { GlobalFilterContextValue } from './global-filter-context.type';

export const GlobalFilterContext = createContext<GlobalFilterContextValue | null>(null);

export function useGlobalFilterContext() {
const context = useContext(GlobalFilterContext);
if (!context) {
throw new Error('useGlobalFilterContext must be used inside GlobalFilterProvider');
}
return context;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 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 { PropsWithChildren } from 'react';
import { GlobalFilter, RecentGlobalFilter } from '../global-filter.type';
import type { UUID } from 'node:crypto';

export type GlobalFilterContextValue = {
globalFilterOptions: GlobalFilter[];
selectedGlobalFilters: GlobalFilter[];
recentGlobalFilters: RecentGlobalFilter[];
substationPropertiesGlobalFilters?: Map<string, string[]>;
filterCategories: string[];
genericFiltersStrictMode: boolean;
filterableEquipmentTypes: string[];
translateCountryCode: (countryCode: string) => string;
selectGlobalFilter: (id: string) => void;
unselectGlobalFilters: (ids: string[]) => void;
clearSelectedGlobalFilters: () => void;
removeGlobalFilterOption: (id: string) => void;
addFiltersToGlobalFiltersOptions: (elementIds: UUID[]) => Promise<void>;
};

export type GlobalFilterContextProviderProps = PropsWithChildren<GlobalFilterContextValue>;
31 changes: 31 additions & 0 deletions src/components/results/common/global-filter/filter.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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 { GlobalFilter } from './global-filter.type';

/**
* Global filters types
* the order of those enum values is the default order for global filter displays : do not move them around
*/
export enum FilterType {
VOLTAGE_LEVEL = 'voltageLevel',
COUNTRY = 'country',
SUBSTATION_PROPERTY = 'substationProperty',
SUBSTATION_OR_VL = 'substationOrVoltageLevelFilter', // voltage levels and substation generic filters which uses the filter library
GENERIC_FILTER = 'genericFilter', // generic filters which uses the filter library (except voltage level and substation filters)
}

export function isCriteriaFilter(filter: GlobalFilter): boolean {
return isCriteriaFilterType(filter.filterType);
}

export function isCriteriaFilterType(filterType: string | undefined): boolean {
return (
filterType !== undefined &&
(filterType === FilterType.GENERIC_FILTER || filterType === FilterType.SUBSTATION_OR_VL)
);
}
Loading
Loading