Skip to content

[WIP] Prepare global filter migration#4059

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

[WIP] Prepare global filter migration#4059
klesaulnier wants to merge 7 commits into
mainfrom
prepare-filter-migration

Conversation

@klesaulnier

Copy link
Copy Markdown
Contributor

PR Summary

… 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

📝 Walkthrough

Walkthrough

The global filter feature is refactored from a Redux-context hybrid into a typed, nullable React context with explicit callbacks. A new adapter provider fetches/validates substation-property filters. State mutations move to pure utilities, backend sync moves to a debounced helper, and consuming files are repointed to relocated modules.

Changes

Global Filter Context-to-Props Refactor

Layer / File(s) Summary
Context types, hook, and provider component
.../global-filter-context.ts, .../context/global-filter-context.ts, .../context/global-filter-context.type.ts, .../context/global-filter-context-provider.tsx
GlobalFilterContextValue type is introduced; GlobalFilterContext becomes nullable with a useGlobalFilterContext hook that throws outside a provider; a new GlobalFilterContextProvider memoizes the value.
Pure state-mutation utilities and sync helper
.../utils/global-filter-state.utils.ts, .../utils/global-filter-sync.utils.ts
New helpers for add/remove/clear/markNotFound table state operations, plus a debounced syncGlobalFilters function replacing inline editing/timer logic.
Reducer and middleware wiring
src/redux/reducer.ts, src/redux/globalFiltersMiddleware.ts
Reducer cases delegate to the new state utilities; middleware calls syncGlobalFilters directly instead of managing debounce timers.
fetchSubstationPropertiesGlobalFilters relocation
.../adapter/global-filter-app-data.ts, .../utils/global-filter-utils.ts, .../hooks/use-global-filter-options.ts
The fetch helper moves to a new adapter module; consumer import updated.
GlobalFilterProvider Redux callbacks
.../global-filter-provider.tsx
Local UI state removed; selection/clear/remove/add callbacks backed by Redux dispatch; renders GlobalFilterContextProvider.
Adapter GlobalFilterProvider
.../adapter/global-filter-provider.tsx
New provider fetches substation-property filters on mount, validates selected generic filters against directory data (marking 404s as deleted), and wires selection/add callbacks.
GlobalFilterDropdownPanel and SelectedGlobalFilters context migration
.../ui/global-filter-dropdown-panel.tsx, .../ui/selected-global-filters.tsx, .../ui/label-with-info-tooltip.tsx
Renamed GlobalFilterPaperGlobalFilterDropdownPanel and TextWithInfoIconLabelWithInfoTooltip; components now read state/actions from useGlobalFilterContext instead of Redux.
GlobalFilter autocomplete context migration
.../ui/global-filter.tsx, .../global-filter-selector.tsx, .../global-filter.style.ts
Renamed GlobalFilterAutocompleteGlobalFilter; onChange routes to context callbacks; country labels use translateCountryCode.
Consumer import-path updates
multiple results/*, spreadsheet-view/*, redux/*, services/study/* files
Imports for GlobalFilter(s) type and related hooks/utils repointed to relocated modules with no logic changes.

Suggested reviewers: Meklo, souissimai, etiennehomer

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Description check ❓ Inconclusive The description is just a template placeholder and does not describe any change in the pull request. Replace the template text with a brief summary of the migration changes and any important reviewer notes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: preparing a global filter migration.
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.

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>
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant