Skip to content

Prepare global filter migration#4059

Open
klesaulnier wants to merge 11 commits into
mainfrom
prepare-filter-migration
Open

Prepare global filter migration#4059
klesaulnier wants to merge 11 commits into
mainfrom
prepare-filter-migration

Conversation

@klesaulnier

@klesaulnier klesaulnier commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR Summary

This PR prepares the future migration of the global-filter directory to commons-ui.

The following subdirectories will be excluded from this migration and will remain in the current project:

  • hooks
  • adapter

As part of this preparation, the GlobalFilter UI component has been refactored to remove any direct dependency on Redux or data fetching. It is now responsible only for rendering and user interactions, making it reusable across projects.

This PR also reorganizes and isolates the code that will be migrated alongside the GlobalFilter UI component in the upcoming commons-ui migration PR.

… into the component

Signed-off-by: LE SAULNIER Kevin <kevin.lesaulnier.pro@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The global filter feature now uses a nullable typed context, extracted state/sync utilities, and an adapter provider that loads and validates substation-property filters. Consumers switch to context-driven selection and updated module paths.

Changes

Global Filter Context Refactor

Layer / File(s) Summary
Context types, hook, and provider component
src/components/results/common/global-filter/context/global-filter-context.type.ts, src/components/results/common/global-filter/global-filter-context.ts, src/components/results/common/global-filter/context/global-filter-context-provider.tsx, src/components/results/common/global-filter/global-filter-provider.tsx
GlobalFilterContextValue and provider props are introduced, GlobalFilterContext becomes nullable, useGlobalFilterContext() throws outside a provider, and the provider now memoizes and supplies the typed context value.
Pure state-mutation utilities and sync helper
src/components/results/common/global-filter/utils/global-filter-state.utils.ts, src/components/results/common/global-filter/utils/global-filter-sync.utils.ts
Shared helpers manage selected filters, recents, deletion marking, and debounced backend sync.
Reducer and middleware wiring
src/redux/reducer.ts, src/redux/globalFiltersMiddleware.ts
Reducer cases call the shared state helpers, and middleware delegates save debouncing to syncGlobalFilters.
Substation filter fetch helper relocation
src/components/results/common/global-filter/adapter/global-filter-app-data.ts, src/components/results/common/global-filter/utils/global-filter-utils.ts, src/components/results/common/global-filter/hooks/use-global-filter-options.ts
fetchSubstationPropertiesGlobalFilters() moves into a new adapter module and the consumer import is updated.
GlobalFilterProvider Redux callbacks
src/components/results/common/global-filter/global-filter-provider.tsx
The provider removes local UI state, derives selected filters from Redux, and exposes Redux-backed context callbacks.
Adapter provider fetch and validation flow
src/components/results/common/global-filter/adapter/global-filter-provider.tsx
The adapter loads substation-property filters, validates selected generic filters against directory data, marks missing ones as deleted, and wires context callbacks.
Dropdown panel and selected chips
src/components/results/common/global-filter/ui/global-filter-dropdown-panel.tsx, src/components/results/common/global-filter/ui/selected-global-filters.tsx, src/components/results/common/global-filter/ui/label-with-info-tooltip.tsx
The dropdown panel and selected chips read context values and callbacks, use substation-property categories, and switch to LabelWithInfoTooltip.
Autocomplete and selector migration
src/components/results/common/global-filter/ui/global-filter.tsx, src/components/results/common/global-filter/global-filter-selector.tsx, src/components/results/common/global-filter/global-filter.style.ts
GlobalFilterAutocomplete becomes GlobalFilter, selection flows through context callbacks, country labels use translateCountryCode, and the selector/style imports are updated.
Consumer import-path updates
src/components/results/*, src/components/spreadsheet-view/*, src/redux/*, src/services/study/*
Global filter types, hooks, and utilities are repointed to their new module paths without runtime logic changes.

Suggested reviewers: flomillot, ghazwarhili

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: preparing the global filter migration.
Description check ✅ Passed The description matches the changeset and explains the migration prep and UI refactor.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Signed-off-by: LE SAULNIER Kevin <kevin.lesaulnier.pro@gmail.com>
Signed-off-by: LE SAULNIER Kevin <kevin.lesaulnier.pro@gmail.com>
…ider props creation

Signed-off-by: LE SAULNIER Kevin <kevin.lesaulnier.pro@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/components/results/common/global-filter/global-filter-context-types.ts (1)

11-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Optional: preserve FilterType[] typing for filterCategories.

GlobalFilterProvider supplies filterCategories: FilterType[], but the context value widens it to string[]. Since FilterType is a string enum it compiles, yet downstream consumers reading from the context lose the enum typing and any exhaustiveness/IDE help it provides. Consider typing this field as FilterType[] (or a shared alias) to keep the contract precise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/results/common/global-filter/global-filter-context-types.ts`
around lines 11 - 23, The `GlobalFilterContextValue` contract widens
`filterCategories` to `string[]`, which loses the intended `FilterType` enum
typing used by `GlobalFilterProvider`. Update the `GlobalFilterContextValue`
definition to use `FilterType[]` (or a shared alias) for `filterCategories`, and
make sure any related context/provider typings stay aligned so consumers of
`useGlobalFilterContext` keep the precise enum-based type information.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/results/common/global-filter/global-filter-context-types.ts`:
- Around line 11-23: The `GlobalFilterContextValue` contract widens
`filterCategories` to `string[]`, which loses the intended `FilterType` enum
typing used by `GlobalFilterProvider`. Update the `GlobalFilterContextValue`
definition to use `FilterType[]` (or a shared alias) for `filterCategories`, and
make sure any related context/provider typings stay aligned so consumers of
`useGlobalFilterContext` keep the precise enum-based type information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d92b0b8c-f995-47cb-916f-d0c5ff5f5eab

📥 Commits

Reviewing files that changed from the base of the PR and between 859774c and 22f0a6d.

📒 Files selected for processing (8)
  • src/components/results/common/global-filter/global-filter-autocomplete.tsx
  • src/components/results/common/global-filter/global-filter-context-provider.tsx
  • src/components/results/common/global-filter/global-filter-context-types.ts
  • src/components/results/common/global-filter/global-filter-context.ts
  • src/components/results/common/global-filter/global-filter-paper.tsx
  • src/components/results/common/global-filter/global-filter-provider.tsx
  • src/components/results/common/global-filter/global-filter-selector.tsx
  • src/components/results/common/global-filter/selected-global-filters.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/results/common/global-filter/global-filter-selector.tsx

Signed-off-by: LE SAULNIER Kevin <kevin.lesaulnier.pro@gmail.com>
Signed-off-by: LE SAULNIER Kevin <kevin.lesaulnier.pro@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/components/results/common/global-filter/global-filter-context-types.ts (1)

8-10: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Use type-only imports here. PropsWithChildren, GlobalFilter, and RecentGlobalFilter are only used in type positions, so import type makes the intent explicit and keeps the file safe if module-preserving transpilation is introduced.

♻️ Proposed import cleanup
-import { PropsWithChildren } from 'react';
-import { GlobalFilter, RecentGlobalFilter } from './global-filter-types';
+import type { PropsWithChildren } from 'react';
+import type { GlobalFilter, RecentGlobalFilter } from './global-filter-types';
 import type { UUID } from 'node:crypto';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/results/common/global-filter/global-filter-context-types.ts`
around lines 8 - 10, Update the imports in global-filter-context-types so they
are type-only: PropsWithChildren, GlobalFilter, and RecentGlobalFilter are only
used in type positions, so switch their imports to import type while leaving
UUID as-is if needed. Use the existing symbols PropsWithChildren, GlobalFilter,
and RecentGlobalFilter in this file to verify the cleanup and keep the module
safe for module-preserving transpilation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/results/common/global-filter/global-filter-paper.tsx`:
- Around line 123-133: The add-selected-filters flow in
addSelectedFiltersToGlobalFiltersOptions leaves the directory selector open when
values is undefined and can also skip cleanup if
addFiltersToGlobalFiltersOptions rejects. Update this callback so the
directoryItemSelectorOpen state is cleared on every exit path, including the
no-values cancel case and a finally block around the async add operation, while
keeping the existing setOpenedDropdown behavior intact.

In `@src/components/results/common/global-filter/global-filter-provider.tsx`:
- Around line 82-86: The initial metadata load in the useEffect inside
GlobalFilterProvider does not handle rejections, so
fetchSubstationPropertiesGlobalFilters() can leave an unhandled promise
rejection and no fallback state. Update the effect to catch errors from
fetchSubstationPropertiesGlobalFilters(), and ensure
setSubstationPropertiesGlobalFilters is still driven to a defined fallback value
when the request fails so the provider remains in a recoverable state.
- Around line 166-194: The duplicate-check in addFiltersToGlobalFiltersOptions
is using selectedGlobalFilters, which can miss IDs that are already selected but
not currently resolvable from globalFilterOptions. Update the membership test to
use the source-of-truth selectedFilterIds for this table instead, so
fetchElementsInfos results are only added when their id is not already present.
Keep the rest of the flow in GlobalFilterProvider the same, including
dispatch(addToGlobalFilterOptions) and the subsequent addToSelectedGlobalFilters
calls.

In `@src/components/results/common/global-filter/global-filter-state.utils.ts`:
- Around line 91-116: The helper markNotFoundGlobalFiltersAsDeletedInState
currently marks matching GlobalFilter entries as deleted and prunes them from
tableState.recents, but it leaves tableState.selected untouched, so deleted
filters remain selected and can still flow into backend sync. Update
markNotFoundGlobalFiltersAsDeletedInState to also filter the
notFoundGlobalFilters IDs out of tableState.selected (and keep the existing
recents cleanup), or ensure the same exclusion happens wherever
GlobalFilterTableState is consumed.

---

Nitpick comments:
In `@src/components/results/common/global-filter/global-filter-context-types.ts`:
- Around line 8-10: Update the imports in global-filter-context-types so they
are type-only: PropsWithChildren, GlobalFilter, and RecentGlobalFilter are only
used in type positions, so switch their imports to import type while leaving
UUID as-is if needed. Use the existing symbols PropsWithChildren, GlobalFilter,
and RecentGlobalFilter in this file to verify the cleanup and keep the module
safe for module-preserving transpilation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a19a0054-4772-4091-8f53-df0b9a67c758

📥 Commits

Reviewing files that changed from the base of the PR and between 859774c and cac5d82.

📒 Files selected for processing (15)
  • src/components/results/common/global-filter/global-filter-app-data.ts
  • src/components/results/common/global-filter/global-filter-autocomplete.tsx
  • src/components/results/common/global-filter/global-filter-context-provider.tsx
  • src/components/results/common/global-filter/global-filter-context-types.ts
  • src/components/results/common/global-filter/global-filter-context.ts
  • src/components/results/common/global-filter/global-filter-paper.tsx
  • src/components/results/common/global-filter/global-filter-provider.tsx
  • src/components/results/common/global-filter/global-filter-selector.tsx
  • src/components/results/common/global-filter/global-filter-state.utils.ts
  • src/components/results/common/global-filter/global-filter-sync.utils.ts
  • src/components/results/common/global-filter/global-filter-utils.ts
  • src/components/results/common/global-filter/selected-global-filters.tsx
  • src/components/results/common/global-filter/use-global-filter-options.ts
  • src/redux/globalFiltersMiddleware.ts
  • src/redux/reducer.ts
💤 Files with no reviewable changes (1)
  • src/components/results/common/global-filter/global-filter-utils.ts
✅ Files skipped from review due to trivial changes (1)
  • src/components/results/common/global-filter/global-filter-selector.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/results/common/global-filter/global-filter-context.ts
  • src/components/results/common/global-filter/selected-global-filters.tsx
  • src/components/results/common/global-filter/global-filter-context-provider.tsx
  • src/components/results/common/global-filter/global-filter-autocomplete.tsx

Comment on lines +123 to +133
const addSelectedFiltersToGlobalFiltersOptions = useCallback(
async (values: TreeViewFinderNodeProps[] | undefined) => {
if (!values) {
return;
}

setOpenedDropdown(true);

const elements: ElementAttributes[] = await fetchElementsInfos(values.map((value) => value.id));
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)) {
// 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));
dispatch(
addToSelectedGlobalFilters(
tableType,
tableUuid,
newlySelectedFilters.map((f) => f.id)
)
);
await addFiltersToGlobalFiltersOptions(values.map((value) => value.id));
setDirectoryItemSelectorOpen(false);
},
[selectedGlobalFilters, setDirectoryItemSelectorOpen, setOpenedDropdown, dispatch, tableType, tableUuid]
[addFiltersToGlobalFiltersOptions, setDirectoryItemSelectorOpen, setOpenedDropdown]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Always close the directory selector on cancel or failure.

When values is undefined, Line 126 returns before clearing directoryItemSelectorOpen; a cancel/close path can leave the controlled selector stuck open. Also close in finally so a rejected add operation does not skip cleanup.

🐛 Proposed close-state fix
 const addSelectedFiltersToGlobalFiltersOptions = useCallback(
     async (values: TreeViewFinderNodeProps[] | undefined) => {
-        if (!values) {
-            return;
-        }
-
-        setOpenedDropdown(true);
-        await addFiltersToGlobalFiltersOptions(values.map((value) => value.id));
-        setDirectoryItemSelectorOpen(false);
+        try {
+            if (values?.length) {
+                setOpenedDropdown(true);
+                await addFiltersToGlobalFiltersOptions(values.map((value) => value.id));
+            }
+        } finally {
+            setDirectoryItemSelectorOpen(false);
+        }
     },
     [addFiltersToGlobalFiltersOptions, setDirectoryItemSelectorOpen, setOpenedDropdown]
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const addSelectedFiltersToGlobalFiltersOptions = useCallback(
async (values: TreeViewFinderNodeProps[] | undefined) => {
if (!values) {
return;
}
setOpenedDropdown(true);
const elements: ElementAttributes[] = await fetchElementsInfos(values.map((value) => value.id));
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)) {
// 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));
dispatch(
addToSelectedGlobalFilters(
tableType,
tableUuid,
newlySelectedFilters.map((f) => f.id)
)
);
await addFiltersToGlobalFiltersOptions(values.map((value) => value.id));
setDirectoryItemSelectorOpen(false);
},
[selectedGlobalFilters, setDirectoryItemSelectorOpen, setOpenedDropdown, dispatch, tableType, tableUuid]
[addFiltersToGlobalFiltersOptions, setDirectoryItemSelectorOpen, setOpenedDropdown]
const addSelectedFiltersToGlobalFiltersOptions = useCallback(
async (values: TreeViewFinderNodeProps[] | undefined) => {
try {
if (values?.length) {
setOpenedDropdown(true);
await addFiltersToGlobalFiltersOptions(values.map((value) => value.id));
}
} finally {
setDirectoryItemSelectorOpen(false);
}
},
[addFiltersToGlobalFiltersOptions, setDirectoryItemSelectorOpen, setOpenedDropdown]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/results/common/global-filter/global-filter-paper.tsx` around
lines 123 - 133, The add-selected-filters flow in
addSelectedFiltersToGlobalFiltersOptions leaves the directory selector open when
values is undefined and can also skip cleanup if
addFiltersToGlobalFiltersOptions rejects. Update this callback so the
directoryItemSelectorOpen state is cleared on every exit path, including the
no-values cancel case and a finally block around the async add operation, while
keeping the existing setOpenedDropdown behavior intact.

Comment on lines +82 to +86
useEffect(() => {
fetchSubstationPropertiesGlobalFilters().then(({ substationPropertiesGlobalFilters }) => {
setSubstationPropertiesGlobalFilters(substationPropertiesGlobalFilters);
});
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle the initial metadata fetch failure path.

If fetchSubstationPropertiesGlobalFilters() rejects here, the effect leaves an unhandled promise rejection and the provider never recovers to a defined fallback state.

Suggested fix
     useEffect(() => {
-        fetchSubstationPropertiesGlobalFilters().then(({ substationPropertiesGlobalFilters }) => {
-            setSubstationPropertiesGlobalFilters(substationPropertiesGlobalFilters);
-        });
-    }, []);
+        let cancelled = false;
+
+        fetchSubstationPropertiesGlobalFilters()
+            .then(({ substationPropertiesGlobalFilters }) => {
+                if (!cancelled) {
+                    setSubstationPropertiesGlobalFilters(substationPropertiesGlobalFilters ?? new Map());
+                }
+            })
+            .catch((error) => {
+                snackWithFallback(snackError, error as Error, {
+                    headerId: 'ComputationFilterResultsError',
+                });
+                if (!cancelled) {
+                    setSubstationPropertiesGlobalFilters(new Map());
+                }
+            });
+
+        return () => {
+            cancelled = true;
+        };
+    }, [snackError]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
fetchSubstationPropertiesGlobalFilters().then(({ substationPropertiesGlobalFilters }) => {
setSubstationPropertiesGlobalFilters(substationPropertiesGlobalFilters);
});
}, []);
useEffect(() => {
let cancelled = false;
fetchSubstationPropertiesGlobalFilters()
.then(({ substationPropertiesGlobalFilters }) => {
if (!cancelled) {
setSubstationPropertiesGlobalFilters(substationPropertiesGlobalFilters ?? new Map());
}
})
.catch((error) => {
snackWithFallback(snackError, error as Error, {
headerId: 'ComputationFilterResultsError',
});
if (!cancelled) {
setSubstationPropertiesGlobalFilters(new Map());
}
});
return () => {
cancelled = true;
};
}, [snackError]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/results/common/global-filter/global-filter-provider.tsx`
around lines 82 - 86, The initial metadata load in the useEffect inside
GlobalFilterProvider does not handle rejections, so
fetchSubstationPropertiesGlobalFilters() can leave an unhandled promise
rejection and no fallback state. Update the effect to catch errors from
fetchSubstationPropertiesGlobalFilters(), and ensure
setSubstationPropertiesGlobalFilters is still driven to a defined fallback value
when the request fails so the provider remains in a recoverable state.

Comment on lines +166 to +194
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)) {
// 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]
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Deduplicate against selectedFilterIds, not selectedGlobalFilters.

selectedGlobalFilters is built by resolving selectedFilterIds through globalFilterOptions and dropping misses on Lines 74-78. If Redux still has a selected ID whose option is temporarily absent, Line 172 treats it as “not selected” and re-dispatches addToSelectedGlobalFilters for the same filter. Use the ID list itself for membership checks.

Suggested fix
     const addFiltersToGlobalFiltersOptions = useCallback(
         async (elementIds: UUID[]) => {
             const elements: ElementAttributes[] = await fetchElementsInfos(elementIds);
+            const selectedIds = new Set(selectedFilterIds ?? []);
             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)) {
+                if (!selectedIds.has(element.elementUuid)) {
                     // add the others
                     const substationOrVoltageLevel =
                         element.specificMetadata?.equipmentType === EquipmentType.SUBSTATION ||
                         element.specificMetadata?.equipmentType === EquipmentType.VOLTAGE_LEVEL;
                     newlySelectedFilters.push({
@@
                 dispatch(addToSelectedGlobalFilters(tableType, tableUuid, [filter.id]))
             );
         },
-        [dispatch, selectedGlobalFilters, tableType, tableUuid]
+        [dispatch, selectedFilterIds, tableType, tableUuid]
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)) {
// 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]
);
const addFiltersToGlobalFiltersOptions = useCallback(
async (elementIds: UUID[]) => {
const elements: ElementAttributes[] = await fetchElementsInfos(elementIds);
const selectedIds = new Set(selectedFilterIds ?? []);
const newlySelectedFilters: GlobalFilter[] = [];
elements.forEach((element: ElementAttributes) => {
// ignore already selected filters and non-generic filters :
if (!selectedIds.has(element.elementUuid)) {
// 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, selectedFilterIds, tableType, tableUuid]
);
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 172-172: Prefer .some(…) over .find(…).

See more on https://sonarcloud.io/project/issues?id=gridsuite_gridstudy-app&issues=AZ8YoXrMLu1dXKlfnL-B&open=AZ8YoXrMLu1dXKlfnL-B&pullRequest=4059

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/results/common/global-filter/global-filter-provider.tsx`
around lines 166 - 194, The duplicate-check in addFiltersToGlobalFiltersOptions
is using selectedGlobalFilters, which can miss IDs that are already selected but
not currently resolvable from globalFilterOptions. Update the membership test to
use the source-of-truth selectedFilterIds for this table instead, so
fetchElementsInfos results are only added when their id is not already present.
Keep the rest of the flow in GlobalFilterProvider the same, including
dispatch(addToGlobalFilterOptions) and the subsequent addToSelectedGlobalFilters
calls.

Comment on lines +91 to +116
export function markNotFoundGlobalFiltersAsDeletedInState(
globalFilterOptions: GlobalFilter[],
tableState: GlobalFilterTableState | undefined,
notFoundGlobalFilters: GlobalFilter[]
): MarkNotFoundGlobalFiltersAsDeletedResult {
const ids = new Set(notFoundGlobalFilters.map((filter) => filter.id));
const updatedGlobalFilterOptions = globalFilterOptions.map((globalFilter) =>
ids.has(globalFilter.id) ? { ...globalFilter, deleted: true } : globalFilter
);

if (!tableState) {
return {
globalFilterOptions: updatedGlobalFilterOptions,
};
}

return {
globalFilterOptions: updatedGlobalFilterOptions,
tableState: {
selected: tableState.selected,
recents: tableState.recents?.length
? tableState.recents.filter((recentFilter) => !ids.has(recentFilter.id))
: tableState.recents,
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect prior reducer behavior for MARK_NOT_FOUND_GLOBAL_FILTERS_AS_DELETED to confirm whether `selected` was pruned.
rg -nP -C5 'MARK_NOT_FOUND_GLOBAL_FILTERS_AS_DELETED' src/redux/reducer.ts
git log -p --follow -S 'deleted = true' -- src/redux/reducer.ts | sed -n '1,120p'

Repository: gridsuite/gridstudy-app

Length of output: 1169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== outline helper ==\n'
ast-grep outline src/components/results/common/global-filter/global-filter-state.utils.ts --view expanded || true

printf '\n== outline reducer ==\n'
ast-grep outline src/redux/reducer.ts --view expanded || true

printf '\n== reducer slice around MARK_NOT_FOUND_GLOBAL_FILTERS_AS_DELETED ==\n'
sed -n '1658,1698p' src/redux/reducer.ts

printf '\n== helper slice around markNotFoundGlobalFiltersAsDeletedInState ==\n'
sed -n '1,180p' src/components/results/common/global-filter/global-filter-state.utils.ts

Repository: gridsuite/gridstudy-app

Length of output: 7934


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== search global filter table selected usage ==\n'
rg -n "tableState\.selected|selected: .*tableState\.selected|selected\)" src/components src/redux src -g '*global-filter*' -g '!**/node_modules/**' || true

printf '\n== search deleted handling in global filter code ==\n'
rg -n "deleted" src/components/results/common/global-filter src/redux src -g '!**/node_modules/**' || true

printf '\n== search selectors/helpers for tableFilters.globalFilters ==\n'
rg -n "tableFilters\.globalFilters|globalFilterOptions" src/components src/redux -g '!**/node_modules/**' || true

Repository: gridsuite/gridstudy-app

Length of output: 21273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== provider slice ==\n'
sed -n '100,140p' src/components/results/common/global-filter/global-filter-provider.tsx

printf '\n== hook slice ==\n'
sed -n '1,80p' src/components/results/common/global-filter/use-selected-global-filters.ts

printf '\n== middleware slice ==\n'
sed -n '1,120p' src/redux/globalFiltersMiddleware.ts

Repository: gridsuite/gridstudy-app

Length of output: 7818


Prune deleted filters from selected The helper only removes not-found IDs from recents; selected is left unchanged, so deleted filters still count as selected and are picked up by the backend sync path. Remove those IDs from selected here too, or explicitly filter out deleted options at the consumer boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/results/common/global-filter/global-filter-state.utils.ts`
around lines 91 - 116, The helper markNotFoundGlobalFiltersAsDeletedInState
currently marks matching GlobalFilter entries as deleted and prunes them from
tableState.recents, but it leaves tableState.selected untouched, so deleted
filters remain selected and can still flow into backend sync. Update
markNotFoundGlobalFiltersAsDeletedInState to also filter the
notFoundGlobalFilters IDs out of tableState.selected (and keep the existing
recents cleanup), or ensure the same exclusion happens wherever
GlobalFilterTableState is consumed.

Signed-off-by: LE SAULNIER Kevin <kevin.lesaulnier.pro@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/results/common/global-filter/utils/global-filter-sync.utils.ts (1)

23-48: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Overlapping in-flight saves for the same index are not guarded.

The !debouncedSyncTimers[index] check in .finally() only detects a newer pending timer, not a previous save still in flight. If a new debounced call fires while an earlier save for the same index is still awaiting its network response, both saves proceed concurrently with no ordering guarantee — the earlier (stale) save can resolve after the later one and overwrite it on the backend. It also causes unmarkEditingGlobalFilter to run while an older save is still pending.

This logic is carried over from the previous inline implementation in globalFiltersMiddleware.ts, but as it's now a shared, reusable utility it's worth hardening here.

🔧 Suggested fix: serialize saves per index
-const debouncedSyncTimers: Record<string, ReturnType<typeof setTimeout>> = {};
+const debouncedSyncTimers: Record<string, ReturnType<typeof setTimeout>> = {};
+const pendingSyncs: Record<string, Promise<void>> = {};

 export function syncGlobalFilters({
     index,
     globalFilters,
     save,
     onError,
     debounceMs = DEFAULT_GLOBAL_FILTER_SYNC_DEBOUNCE_MS,
 }: SyncGlobalFiltersParameters): void {
     markEditingGlobalFilter(index);

     if (debouncedSyncTimers[index]) {
         clearTimeout(debouncedSyncTimers[index]);
     }

     debouncedSyncTimers[index] = setTimeout(() => {
-        save(globalFilters)
-            .catch(onError)
-            .finally(() => {
-                // Only end the sync if no new debounce timer was started while the save was in flight.
-                if (!debouncedSyncTimers[index]) {
-                    unmarkEditingGlobalFilter(index);
-                }
-            });
-
-        delete debouncedSyncTimers[index];
+        delete debouncedSyncTimers[index];
+        const previous = pendingSyncs[index] ?? Promise.resolve();
+        const current: Promise<void> = previous
+            .then(() => save(globalFilters))
+            .catch(onError)
+            .finally(() => {
+                // Only end the sync if this is still the latest chained save and no new timer was scheduled.
+                if (pendingSyncs[index] === current && !debouncedSyncTimers[index]) {
+                    unmarkEditingGlobalFilter(index);
+                }
+            });
+        pendingSyncs[index] = current;
     }, debounceMs);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/results/common/global-filter/utils/global-filter-sync.utils.ts`
around lines 23 - 48, `syncGlobalFilters` only tracks pending debounce timers,
so overlapping saves for the same `index` can still run concurrently and finish
out of order. Harden `syncGlobalFilters` in `global-filter-sync.utils.ts` by
serializing saves per `index` (for example, track an in-flight save/promise or a
version token and skip/chain newer calls until the prior `save(globalFilters)`
settles), and only call `unmarkEditingGlobalFilter` when both the debounce timer
and any in-flight save for that `index` are done.
♻️ Duplicate comments (1)
src/components/results/common/global-filter/ui/global-filter-dropdown-panel.tsx (1)

123-134: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Directory selector can get stuck open on cancel/failure — unresolved from prior review.

When values is undefined (cancel), the function returns at Line 126 before setDirectoryItemSelectorOpen(false) runs at Line 131, leaving the selector controlled-open. There's also no try/finally, so a rejected addFiltersToGlobalFiltersOptions call skips cleanup too. This is the same code flagged in a previous review round and still isn't fixed.

🐛 Proposed fix
 const addSelectedFiltersToGlobalFiltersOptions = useCallback(
     async (values: TreeViewFinderNodeProps[] | undefined) => {
-        if (!values) {
-            return;
-        }
-
-        setOpenedDropdown(true);
-        await addFiltersToGlobalFiltersOptions(values.map((value) => value.id));
-        setDirectoryItemSelectorOpen(false);
+        try {
+            if (values?.length) {
+                setOpenedDropdown(true);
+                await addFiltersToGlobalFiltersOptions(values.map((value) => value.id));
+            }
+        } finally {
+            setDirectoryItemSelectorOpen(false);
+        }
     },
     [addFiltersToGlobalFiltersOptions, setDirectoryItemSelectorOpen, setOpenedDropdown]
 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/results/common/global-filter/ui/global-filter-dropdown-panel.tsx`
around lines 123 - 134, In addSelectedFiltersToGlobalFiltersOptions, the
directory selector can stay open on cancel or when
addFiltersToGlobalFiltersOptions fails because the early return skips cleanup
and there is no guaranteed close path. Update this callback in
global-filter-dropdown-panel so setDirectoryItemSelectorOpen(false) always runs,
using a try/finally around the async addFiltersToGlobalFiltersOptions call and
handling the undefined values case before or within that flow; keep
setOpenedDropdown and the callback dependencies intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@src/components/results/common/global-filter/utils/global-filter-sync.utils.ts`:
- Around line 23-48: `syncGlobalFilters` only tracks pending debounce timers, so
overlapping saves for the same `index` can still run concurrently and finish out
of order. Harden `syncGlobalFilters` in `global-filter-sync.utils.ts` by
serializing saves per `index` (for example, track an in-flight save/promise or a
version token and skip/chain newer calls until the prior `save(globalFilters)`
settles), and only call `unmarkEditingGlobalFilter` when both the debounce timer
and any in-flight save for that `index` are done.

---

Duplicate comments:
In
`@src/components/results/common/global-filter/ui/global-filter-dropdown-panel.tsx`:
- Around line 123-134: In addSelectedFiltersToGlobalFiltersOptions, the
directory selector can stay open on cancel or when
addFiltersToGlobalFiltersOptions fails because the early return skips cleanup
and there is no guaranteed close path. Update this callback in
global-filter-dropdown-panel so setDirectoryItemSelectorOpen(false) always runs,
using a try/finally around the async addFiltersToGlobalFiltersOptions call and
handling the undefined values case before or within that flow; keep
setOpenedDropdown and the callback dependencies intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4bb9bb48-d81f-4e2d-902d-92c88bf343c8

📥 Commits

Reviewing files that changed from the base of the PR and between cac5d82 and 0006abc.

📒 Files selected for processing (53)
  • src/components/app.jsx
  • src/components/results/common/global-filter/adapter/global-filter-app-data.ts
  • src/components/results/common/global-filter/adapter/global-filter-provider.tsx
  • src/components/results/common/global-filter/context/global-filter-context-provider.tsx
  • src/components/results/common/global-filter/context/global-filter-context.ts
  • src/components/results/common/global-filter/context/global-filter-context.type.ts
  • src/components/results/common/global-filter/global-filter-selector.tsx
  • src/components/results/common/global-filter/global-filter.style.ts
  • src/components/results/common/global-filter/global-filter.type.ts
  • src/components/results/common/global-filter/hooks/use-computation-global-filters.ts
  • src/components/results/common/global-filter/hooks/use-global-filter-options.ts
  • src/components/results/common/global-filter/hooks/use-global-filter-results.ts
  • src/components/results/common/global-filter/hooks/use-selected-global-filters.ts
  • src/components/results/common/global-filter/ui/global-filter-dropdown-panel.tsx
  • src/components/results/common/global-filter/ui/global-filter.tsx
  • src/components/results/common/global-filter/ui/label-with-info-tooltip.tsx
  • src/components/results/common/global-filter/ui/selected-global-filters.tsx
  • src/components/results/common/global-filter/utils/build-valid-global-filters.ts
  • src/components/results/common/global-filter/utils/global-filter-state.utils.ts
  • src/components/results/common/global-filter/utils/global-filter-sync.utils.ts
  • src/components/results/common/global-filter/utils/global-filter-utils.ts
  • src/components/results/common/utils.ts
  • src/components/results/loadflow/load-flow-result-tab.tsx
  • src/components/results/pccmin/pcc-min-export-button.tsx
  • src/components/results/pccmin/pcc-min-result-tab.tsx
  • src/components/results/pccmin/pcc-min-result.tsx
  • src/components/results/pccmin/pcc-min-result.type.ts
  • src/components/results/securityanalysis/security-analysis-result-tab.tsx
  • src/components/results/securityanalysis/security-analysis.type.ts
  • src/components/results/sensitivity-analysis/paged-sensitivity-analysis-result.tsx
  • src/components/results/sensitivity-analysis/sensitivity-analysis-export-button.tsx
  • src/components/results/sensitivity-analysis/sensitivity-analysis-result-tab.tsx
  • src/components/results/shortcircuit/shortcircuit-analysis-export-button.tsx
  • src/components/results/shortcircuit/shortcircuit-analysis-result-tab.tsx
  • src/components/results/shortcircuit/shortcircuit-analysis-result.tsx
  • src/components/spreadsheet-view/add-spreadsheet/dialogs/add-spreadsheet-utils.ts
  • src/components/spreadsheet-view/spreadsheet/spreadsheet-content/hooks/use-spreadsheet-gs-filter.ts
  • src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/row-counter/use-filtered-row-counter.tsx
  • src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/save/save-spreadsheet-collection-dialog.tsx
  • src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/save/save-spreadsheet-dialog.tsx
  • src/components/spreadsheet-view/types/spreadsheet.type.ts
  • src/components/voltage-init-result-tab.tsx
  • src/redux/actions.ts
  • src/redux/globalFiltersMiddleware.ts
  • src/redux/reducer.ts
  • src/redux/reducer.type.ts
  • src/services/study/filter.ts
  • src/services/study/loadflow.ts
  • src/services/study/pcc-min.ts
  • src/services/study/sensitivity-analysis.ts
  • src/services/study/short-circuit-analysis.ts
  • src/services/study/study-config.ts
  • src/services/study/voltage-init.ts
💤 Files with no reviewable changes (1)
  • src/components/results/common/global-filter/adapter/global-filter-app-data.ts
✅ Files skipped from review due to trivial changes (25)
  • src/components/results/securityanalysis/security-analysis.type.ts
  • src/components/results/shortcircuit/shortcircuit-analysis-result-tab.tsx
  • src/redux/reducer.type.ts
  • src/components/results/sensitivity-analysis/sensitivity-analysis-result-tab.tsx
  • src/components/results/common/global-filter/hooks/use-selected-global-filters.ts
  • src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/save/save-spreadsheet-dialog.tsx
  • src/components/results/shortcircuit/shortcircuit-analysis-result.tsx
  • src/services/study/pcc-min.ts
  • src/services/study/short-circuit-analysis.ts
  • src/services/study/study-config.ts
  • src/redux/actions.ts
  • src/components/results/securityanalysis/security-analysis-result-tab.tsx
  • src/components/results/common/global-filter/context/global-filter-context.type.ts
  • src/components/results/common/global-filter/hooks/use-computation-global-filters.ts
  • src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/save/save-spreadsheet-collection-dialog.tsx
  • src/components/results/common/global-filter/hooks/use-global-filter-options.ts
  • src/components/results/common/global-filter/hooks/use-global-filter-results.ts
  • src/components/spreadsheet-view/add-spreadsheet/dialogs/add-spreadsheet-utils.ts
  • src/components/results/shortcircuit/shortcircuit-analysis-export-button.tsx
  • src/components/results/pccmin/pcc-min-export-button.tsx
  • src/services/study/sensitivity-analysis.ts
  • src/components/results/pccmin/pcc-min-result.type.ts
  • src/components/voltage-init-result-tab.tsx
  • src/services/study/filter.ts
  • src/components/results/sensitivity-analysis/sensitivity-analysis-export-button.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/redux/reducer.ts

@klesaulnier klesaulnier changed the title [WIP] Prepare global filter migration Prepare global filter migration Jul 9, 2026
@klesaulnier klesaulnier changed the title Prepare global filter migration [WIP] Prepare global filter migration Jul 10, 2026
@klesaulnier klesaulnier changed the title [WIP] Prepare global filter migration Prepare global filter migration Jul 10, 2026
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant