diff --git a/package-lock.json b/package-lock.json
index df9128e99..78c688fd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
- "@gridsuite/commons-ui": "0.126.0",
+ "@gridsuite/commons-ui": "0.127.0",
"@hookform/resolvers": "^4.1.3",
"@mui/icons-material": "^5.18.0",
"@mui/lab": "5.0.0-alpha.175",
@@ -3121,9 +3121,9 @@
}
},
"node_modules/@gridsuite/commons-ui": {
- "version": "0.126.0",
- "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.126.0.tgz",
- "integrity": "sha512-PQKNxVOYeygiRMLsKyBdjF+u1HZKG3yZONDc4YHGpaV8rOqmtHC7721qUL6dg5EP7C8b+Wi7nlSxeMd3n/UrBg==",
+ "version": "0.127.0",
+ "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.127.0.tgz",
+ "integrity": "sha512-TZd6Hx/zOE2zxJSdgd4hnIcSN+PXYpwZvwsJI/3s1oic75rO90GzhP43EpgUlM++txYS5Gbxc2fok1SifK0DLg==",
"license": "MPL-2.0",
"dependencies": {
"@ag-grid-community/locale": "^33.3.2",
diff --git a/package.json b/package.json
index 0a509f387..a0d82a293 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
- "@gridsuite/commons-ui": "0.126.0",
+ "@gridsuite/commons-ui": "0.127.0",
"@hookform/resolvers": "^4.1.3",
"@mui/icons-material": "^5.18.0",
"@mui/lab": "5.0.0-alpha.175",
diff --git a/src/components/dialogs/contingency-list/contingency-list-utils.ts b/src/components/dialogs/contingency-list/contingency-list-utils.ts
index 4742e1c70..59b010b88 100644
--- a/src/components/dialogs/contingency-list/contingency-list-utils.ts
+++ b/src/components/dialogs/contingency-list/contingency-list-utils.ts
@@ -16,6 +16,7 @@ import {
import type { SetRequired } from 'type-fest';
import { prepareContingencyListForBackend } from '../contingency-list-helper';
import { ContingencyListType } from '../../../utils/elementType';
+import { FilterAttributes } from '../../../utils/contingency-list.type';
export interface Identifier {
type: 'ID_BASED';
@@ -71,6 +72,15 @@ export const getCriteriaBasedFormDataFromFetchedElement = (response: any, name:
...getCriteriaBasedFormData(response),
});
+export const getFilterBasedFormDataFromFetchedElement = (response: any, name: string, description: string) => ({
+ [FieldConstants.NAME]: name,
+ [FieldConstants.DESCRIPTION]: description,
+ [FieldConstants.CONTINGENCY_LIST_TYPE]: ContingencyListType.FILTERS.id,
+ [FieldConstants.FILTERS]: response.filters.map((filter: FilterAttributes) => {
+ return { id: filter.id, name: filter.name, specificMetadata: { equipmentType: filter.equipmentType } };
+ }),
+});
+
export const getExplicitNamingFormDataFromFetchedElement = (response: any, name: string, description: string) => {
let result;
if (response.identifierContingencyList?.identifiers?.length) {
diff --git a/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.tsx b/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.tsx
index 9351a4cc2..fc0903b8f 100644
--- a/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.tsx
+++ b/src/components/dialogs/contingency-list/creation/contingency-list-creation-form.tsx
@@ -42,7 +42,7 @@ export default function ContingencyListCreationForm() {
const contingencyListTypeField = (
);
diff --git a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx
new file mode 100644
index 000000000..a4ecaac55
--- /dev/null
+++ b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx
@@ -0,0 +1,180 @@
+/**
+ * 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 {
+ CustomMuiDialog,
+ FieldConstants,
+ MAX_CHAR_DESCRIPTION,
+ TreeViewFinderNodeProps,
+ useSnackMessage,
+ yupConfig as yup,
+} from '@gridsuite/commons-ui';
+import { useForm } from 'react-hook-form';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { useSelector } from 'react-redux';
+import { useCallback, useEffect, useState } from 'react';
+import { UUID } from 'crypto';
+import { ObjectSchema } from 'yup';
+import ContingencyListFilterBasedForm from './contingency-list-filter-based-form';
+import { AppState } from '../../../../redux/types';
+import {
+ createFilterBasedContingency,
+ getContingencyList,
+ saveFilterBasedContingencyList,
+} from '../../../../utils/rest-api';
+import { handleNotAllowedError } from '../../../utils/rest-errors';
+import { ContingencyListType } from '../../../../utils/elementType';
+import { getFilterBasedFormDataFromFetchedElement } from '../contingency-list-utils';
+import { FilterBasedContingencyList } from '../../../../utils/contingency-list.type';
+
+const schema: ObjectSchema = yup.object().shape({
+ [FieldConstants.NAME]: yup.string().required(),
+ [FieldConstants.DESCRIPTION]: yup.string().max(MAX_CHAR_DESCRIPTION),
+ [FieldConstants.FILTERS]: yup.array().required(),
+});
+
+export interface ContingencyListFilterBasedFormData {
+ [FieldConstants.NAME]: string;
+ [FieldConstants.DESCRIPTION]?: string;
+ [FieldConstants.FILTERS]: TreeViewFinderNodeProps[];
+}
+
+const getContingencyListEmptyFormData = (name = '') => ({
+ [FieldConstants.NAME]: name,
+ [FieldConstants.DESCRIPTION]: '',
+ [FieldConstants.FILTERS]: [],
+});
+
+const emptyFormData = (name?: string) => getContingencyListEmptyFormData(name);
+
+export interface FilterBasedContingencyListProps {
+ titleId: string;
+ open: boolean;
+ onClose: () => void;
+ name?: string;
+ description?: string;
+ id?: UUID;
+}
+
+export default function FilterBasedContingencyListDialog({
+ titleId,
+ open,
+ onClose,
+ name,
+ description,
+ id,
+}: Readonly) {
+ const activeDirectory = useSelector((state: AppState) => state.activeDirectory);
+ const { snackError } = useSnackMessage();
+ const [isFetching, setIsFetching] = useState(!!id);
+
+ const methods = useForm({
+ defaultValues: emptyFormData(),
+ resolver: yupResolver(schema),
+ });
+ const {
+ reset,
+ formState: { errors },
+ } = methods;
+
+ useEffect(() => {
+ if (id) {
+ setIsFetching(true);
+ getContingencyList(ContingencyListType.FILTERS.id, id?.toString())
+ .then((response) => {
+ const formData: ContingencyListFilterBasedFormData = getFilterBasedFormDataFromFetchedElement(
+ response,
+ name ?? '',
+ description ?? ''
+ );
+ reset({ ...formData });
+ })
+ .catch((error) => {
+ snackError({
+ messageTxt: error.message,
+ headerId: 'cannotRetrieveContingencyList',
+ });
+ })
+ .finally(() => setIsFetching(false));
+ }
+ }, [id, name, reset, snackError, description]);
+
+ const closeAndClear = useCallback(() => {
+ reset(emptyFormData());
+ onClose();
+ }, [onClose, reset]);
+
+ const onSubmit = useCallback(
+ (data: ContingencyListFilterBasedFormData) => {
+ const filterBaseContingencyList: FilterBasedContingencyList = {
+ filters: data[FieldConstants.FILTERS]?.map((item: TreeViewFinderNodeProps) => {
+ return {
+ id: item.id,
+ };
+ }),
+ };
+
+ if (id) {
+ saveFilterBasedContingencyList(
+ id,
+ data[FieldConstants.NAME],
+ data[FieldConstants.DESCRIPTION] ?? '',
+ filterBaseContingencyList
+ )
+ .then(() => closeAndClear())
+ .catch((error) => {
+ if (handleNotAllowedError(error, snackError)) {
+ return;
+ }
+ snackError({
+ messageTxt: error.message,
+ headerId: 'contingencyListEditingError',
+ headerValues: { name: data[FieldConstants.NAME] },
+ });
+ });
+ } else {
+ createFilterBasedContingency(
+ data[FieldConstants.NAME],
+ data[FieldConstants.DESCRIPTION] ?? '',
+ filterBaseContingencyList,
+ activeDirectory
+ )
+ .then(() => closeAndClear())
+ .catch((error) => {
+ if (handleNotAllowedError(error, snackError)) {
+ return;
+ }
+ snackError({
+ messageTxt: error.message,
+ headerId: 'contingencyListCreationError',
+ headerValues: { name: data[FieldConstants.NAME] },
+ });
+ });
+ }
+ },
+ [activeDirectory, closeAndClear, id, snackError]
+ );
+
+ const nameError = errors[FieldConstants.NAME];
+ const isValidating = errors.root?.isValidating;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx
new file mode 100644
index 000000000..3278a462b
--- /dev/null
+++ b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx
@@ -0,0 +1,206 @@
+/**
+ * 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 {
+ CustomAGGrid,
+ DescriptionField,
+ DirectoryItemSelector,
+ DirectoryItemsInput,
+ ElementType,
+ EquipmentType,
+ FieldConstants,
+ getFilterEquipmentTypeLabel,
+ SeparatorCellRenderer,
+ TreeViewFinderNodeProps,
+ UniqueNameInput,
+ unscrollableDialogStyles,
+ useSnackMessage,
+} from '@gridsuite/commons-ui';
+import { Box, Button, Typography } from '@mui/material';
+import { useSelector } from 'react-redux';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { useCallback, useEffect, useState } from 'react';
+import { FolderOutlined } from '@mui/icons-material';
+import { ColDef } from 'ag-grid-community';
+import { blue, brown, green, indigo, lime, red, teal } from '@mui/material/colors';
+import { useWatch } from 'react-hook-form';
+import { UUID } from 'crypto';
+import { AppState } from '../../../../redux/types';
+import { getIdentifiablesFromFitlers } from '../../../../utils/rest-api';
+import {
+ FilteredIdentifiables,
+ FilterAttributes,
+ IdentifiableAttributes,
+} from '../../../../utils/contingency-list.type';
+
+const separator = '/';
+const defaultDef: ColDef = {
+ flex: 1,
+ resizable: false,
+ sortable: false,
+};
+
+const equipmentTypes: string[] = [
+ EquipmentType.TWO_WINDINGS_TRANSFORMER,
+ EquipmentType.LINE,
+ EquipmentType.LOAD,
+ EquipmentType.GENERATOR,
+ EquipmentType.SHUNT_COMPENSATOR,
+ EquipmentType.STATIC_VAR_COMPENSATOR,
+ EquipmentType.HVDC_LINE,
+];
+
+const equipmentColorsMap: Map = new Map([
+ [EquipmentType.TWO_WINDINGS_TRANSFORMER, blue[700]],
+ [EquipmentType.LINE, indigo[700]],
+ [EquipmentType.LOAD, brown[700]],
+ [EquipmentType.GENERATOR, green[700]],
+ [EquipmentType.SHUNT_COMPENSATOR, red[700]],
+ [EquipmentType.STATIC_VAR_COMPENSATOR, lime[700]],
+ [EquipmentType.HVDC_LINE, teal[700]],
+]);
+
+export default function ContingencyListFilterBasedForm() {
+ const activeDirectory = useSelector((state: AppState) => state.activeDirectory);
+ const [selectedStudy, setSelectedStudy] = useState('');
+ const [selectedStudyId, setSelectedStudyId] = useState();
+ const [selectedFolder, setSelectedFolder] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const [isFetching, setIsFetching] = useState(false);
+ const [rowsData, setRowsData] = useState([]);
+
+ const intl = useIntl();
+ const { snackError } = useSnackMessage();
+ const filters = useWatch({ name: FieldConstants.FILTERS });
+
+ const colDef: ColDef[] = [
+ {
+ headerName: intl.formatMessage({
+ id: FieldConstants.EQUIPMENT_ID,
+ }),
+ field: FieldConstants.ID,
+ cellRenderer: ({ data }: { data: IdentifiableAttributes }) => {
+ if (data.id === 'SEPARATOR') {
+ return SeparatorCellRenderer({
+ value: intl.formatMessage({ id: 'missingFromStudy' }),
+ });
+ }
+ return data.id;
+ },
+ },
+ {
+ headerName: intl.formatMessage({
+ id: FieldConstants.TYPE,
+ }),
+ field: FieldConstants.TYPE,
+ },
+ ];
+
+ useEffect(() => {
+ if (filters && selectedStudyId) {
+ setIsFetching(true);
+ getIdentifiablesFromFitlers(
+ selectedStudyId,
+ filters.map((filter: FilterAttributes) => filter.id)
+ )
+ .then((response: FilteredIdentifiables) => {
+ const SEPARATOR_TYPE = 'SEPARATOR';
+ const attributes: IdentifiableAttributes[] = response.equipmentIds.map(
+ (element: IdentifiableAttributes) => {
+ const equipmentType: string = getFilterEquipmentTypeLabel(element?.type);
+ return {
+ id: element.id,
+ type: equipmentType ? intl.formatMessage({ id: equipmentType }) : '',
+ };
+ }
+ );
+ if (response.notFoundIds?.length > 0) {
+ attributes.push({ id: SEPARATOR_TYPE, type: '' });
+ response.notFoundIds.forEach((element: IdentifiableAttributes) => {
+ const equipmentType: string = getFilterEquipmentTypeLabel(element?.type);
+ attributes.push({
+ id: element.id,
+ type: equipmentType ? intl.formatMessage({ id: equipmentType }) : '',
+ });
+ });
+ }
+ setRowsData(attributes);
+ })
+ .catch((error) =>
+ snackError({
+ messageTxt: error.message,
+ headerId: 'cannotComputeContingencyList',
+ })
+ )
+ .finally(() => setIsFetching(false));
+ }
+ }, [filters, intl, selectedStudyId, snackError]);
+
+ const onNodeChanged = useCallback((nodes: TreeViewFinderNodeProps[]) => {
+ if (nodes.length > 0) {
+ if (nodes[0].parents && nodes[0].parents.length > 0) {
+ setSelectedFolder(nodes[0].parents.map((entry) => entry.name).join(separator));
+ }
+ setSelectedStudy(nodes[0].name);
+ setSelectedStudyId(nodes[0].id);
+ }
+ setIsOpen(false);
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedStudy.length > 0 ? (
+
+ {selectedFolder ? selectedFolder + separator + selectedStudy : selectedStudy}
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/directory-content-dialog.tsx b/src/components/directory-content-dialog.tsx
index fa0271838..04919068b 100644
--- a/src/components/directory-content-dialog.tsx
+++ b/src/components/directory-content-dialog.tsx
@@ -44,6 +44,7 @@ import * as constants from '../utils/UIconstants';
import type { AppState } from '../redux/types';
import { useParameterState } from './dialogs/use-parameters-dialog';
import type { useDirectoryContent } from '../hooks/useDirectoryContent';
+import FilterBasedContingencyListDialog from './dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog';
export type DirectoryContentDialogApi = {
handleClick: (event: CellClickedEvent) => void;
@@ -122,6 +123,15 @@ function DirectoryContentDialog(
setElementName('');
}, [setActiveElement, setOpenDialog]);
+ /* Filter based contingency list dialog: window status value for editing a filter based contingency list */
+ const [currentFilterBasedContingencyListId, setcurrentFilterBasedContingencyListId] = useState();
+ const handleCloseFilterBasedContingency = useCallback(() => {
+ setOpenDialog(constants.DialogsId.NONE);
+ setcurrentFilterBasedContingencyListId(undefined);
+ setActiveElement(undefined);
+ setElementName('');
+ }, [setActiveElement, setOpenDialog]);
+
const [currentExplicitNamingFilterId, setCurrentExplicitNamingFilterId] = useState();
/* Filters dialog: window status value to edit ExplicitNaming filters */
const handleCloseExplicitNamingFilterDialog = useCallback(() => {
@@ -194,6 +204,9 @@ function DirectoryContentDialog(
} else if (subtype === ContingencyListType.EXPLICIT_NAMING.id) {
setCurrentExplicitNamingContingencyListId(event.data.elementUuid);
setOpenDialog(subtype);
+ } else if (subtype === ContingencyListType.FILTERS.id) {
+ setcurrentFilterBasedContingencyListId(event.data.elementUuid);
+ setOpenDialog(subtype);
}
break;
case ElementType.FILTER:
@@ -293,6 +306,18 @@ function DirectoryContentDialog(
/>
);
}
+ if (currentFilterBasedContingencyListId !== undefined && activeElement) {
+ return (
+
+ );
+ }
if (currentExplicitNamingFilterId !== undefined && activeElement) {
return (
{
directory: ElementAttributes | null;
@@ -233,8 +234,20 @@ export default function DirectoryTreeContextualMenu(props: Readonly handleOpenDialog(DialogsId.ADD_NEW_CONTINGENCY_LIST),
icon: ,
+ subMenuItems: [
+ {
+ messageDescriptorId: 'contingencyList.criteriaBasedOrExplicitNaming',
+ callback: () =>
+ handleOpenDialog(DialogsId.ADD_NEW_EXPLICIT_NAMING_OR_CRITERIA_BASED_CONTINGENCY_LIST),
+ icon: null,
+ },
+ {
+ messageDescriptorId: 'contingencyList.filterBased',
+ callback: () => handleOpenDialog(DialogsId.ADD_NEW_FILTERS_CONTINGENCY_LIST),
+ icon: null,
+ },
+ ],
},
{
messageDescriptorId: 'createNewFilter',
@@ -358,7 +371,7 @@ export default function DirectoryTreeContextualMenu(props: Readonly;
- case DialogsId.ADD_NEW_CONTINGENCY_LIST:
+ case DialogsId.ADD_NEW_EXPLICIT_NAMING_OR_CRITERIA_BASED_CONTINGENCY_LIST:
return (
);
+ case DialogsId.ADD_NEW_FILTERS_CONTINGENCY_LIST:
+ return (
+
+ );
case DialogsId.ADD_DIRECTORY:
return (
}
@@ -550,6 +575,18 @@ export function getContingencyList(type: string, id: string) {
});
}
+export function getIdentifiablesFromFitlers(studyUuid: UUID, filters: UUID[]): Promise {
+ console.info('get identifiables resulting from application of filters list on study root network');
+
+ const filtersListsQueryParams = getRequestParamFromList('filtersUuid', filters);
+ const urlSearchParams = new URLSearchParams(filtersListsQueryParams);
+
+ return backendFetchJson(`${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/filters/elements?${urlSearchParams}`, {
+ method: 'get',
+ headers: { 'Content-Type': 'application/json' },
+ });
+}
+
export interface CriteriaBasedEditionFormData {
[FieldConstants.NAME]: string;
[FieldConstants.DESCRIPTION]?: string;
@@ -606,6 +643,30 @@ export function saveCriteriaBasedContingencyList(id: string, form: CriteriaBased
});
}
+/**
+ * Saves a Filter contingency list
+ * @returns {Promise}
+ */
+export function saveFilterBasedContingencyList(
+ id: string,
+ name: string,
+ description: string,
+ contingencyList: FilterBasedContingencyList
+) {
+ const urlSearchParams = new URLSearchParams();
+ urlSearchParams.append('name', name);
+ urlSearchParams.append('description', description ?? '');
+ urlSearchParams.append('contingencyListType', ContingencyListType.FILTERS.id);
+
+ const url = `${PREFIX_EXPLORE_SERVER_QUERIES}/v1/explore/contingency-lists/${id}?${urlSearchParams.toString()}`;
+
+ return backendFetch(url, {
+ method: 'put',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(contingencyList),
+ });
+}
+
/**
* Saves an explicit naming contingency list
* @returns {Promise}