From 9f793d7b13a01192567d128508ada688b0ef2d7b Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Tue, 21 Jul 2026 11:41:30 +0200 Subject: [PATCH] fix: keep null in severity filter list HMS-10653 --- src/PresentationalComponents/Filters/SeverityFilter.js | 9 +++------ .../Filters/SeverityFilter.test.js | 8 ++++++++ src/Utilities/Helpers.js | 10 +++++++--- src/Utilities/Helpers.test.js | 4 ++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/PresentationalComponents/Filters/SeverityFilter.js b/src/PresentationalComponents/Filters/SeverityFilter.js index 15d855dfa..f69f4c9de 100644 --- a/src/PresentationalComponents/Filters/SeverityFilter.js +++ b/src/PresentationalComponents/Filters/SeverityFilter.js @@ -4,9 +4,8 @@ import { intl } from '../../Utilities/IntlProvider'; import messages from '../../Messages'; import { conditionalFilterType } from '@redhat-cloud-services/frontend-components/ConditionalFilter'; -// Backend note: Patch handles `filter[severity]=null` as an `IS NULL` predicate and ignores `IN (...)` lists -// that contain NULL. We keep the UI state as arrays for PatternFly (ConditionalFilter checkbox) but collapse -// `[null]` to bare `null` before dispatching so the API stays on the supported code path +// Backend: bare `filter[severity]=null` for "None" only; mixed selection uses `in:1,2,null`. +// Keep UI state as string arrays for PatternFly checkboxes, but collapse `[null]` to bare `null`. const severityFilter = (apply, currentFilter = {}) => { const advisorySeverityMap = React.useMemo( @@ -41,13 +40,11 @@ const severityFilter = (apply, currentFilter = {}) => { return; } - // Convert each string into its respective raw value before passing it to the API - // The engine expects a scalar `null` for the "None" case and integer arrays for actual severities + // Scalar `null` for "None" alone; otherwise an array that may include `null` (encoded as in:1,2,null) const mappedSeverities = severityStrings.map((item) => item === 'null' ? null : parseInt(item, 10), ); - // Send a bare `null` so we use the API's `IS NULL` path instead of an unsupported `IN (NULL)` clause if (mappedSeverities.length === 1 && mappedSeverities[0] === null) { apply({ filter: { severity: null } }); return; diff --git a/src/PresentationalComponents/Filters/SeverityFilter.test.js b/src/PresentationalComponents/Filters/SeverityFilter.test.js index 67acfa3e8..9d74babcb 100644 --- a/src/PresentationalComponents/Filters/SeverityFilter.test.js +++ b/src/PresentationalComponents/Filters/SeverityFilter.test.js @@ -44,6 +44,14 @@ describe('SeverityFilter', () => { expect(rehydratedResponse.filterValues.value).toEqual(['null']); }); + it('keeps null inside a multi-severity selection for in:1,2,null encoding', () => { + const response = renderFilter(currentFilterEmpty); + response.filterValues.onChange('event', ['1', '2', 'null']); + expect(apply).toHaveBeenCalledWith({ filter: { severity: [1, 2, null] } }); + const rehydratedResponse = rehydrateFilter([1, 2, null]); + expect(rehydratedResponse.filterValues.value).toEqual(['1', '2', 'null']); + }); + it('dispatches undefined severity when onChange receives no payload', () => { const response = renderFilter(currentFilterEmpty); response.filterValues.onChange(); diff --git a/src/Utilities/Helpers.js b/src/Utilities/Helpers.js index 7cff905b8..6129b2345 100644 --- a/src/Utilities/Helpers.js +++ b/src/Utilities/Helpers.js @@ -245,6 +245,9 @@ export const getFilterValue = (category, key) => { } }; +// Array#toString / String(array) turns null into '', so keep the API literal "null" +const getFilterStringFromApi = (value) => (value === null ? 'null' : String(value)); + export const encodeParams = (parameters, shouldTranslateKeys) => { const calculateWorkloads = (systemProfile) => { let result = ''; @@ -264,9 +267,12 @@ export const encodeParams = (parameters, shouldTranslateKeys) => { value = (shouldTranslateKeys && getFilterValue(key, value).apiValue) || value; const operator = [].concat(value).length > 1 || multiValueFilters.includes(key) ? 'in:' : ''; + const serializedValue = Array.isArray(value) + ? value.map(getFilterStringFromApi).join(',') + : getFilterStringFromApi(value); result = { ...result, - [`filter[${key}]`]: `${operator}${String(value)}`, + [`filter[${key}]`]: `${operator}${serializedValue}`, }; }); return result; @@ -353,8 +359,6 @@ export const decodeQueryparams = (queryString, parsers = {}) => { return res; }; -const getFilterStringFromApi = (value) => (value === null ? 'null' : String(value)); - const compareNormalizedValues = (left, right) => getFilterStringFromApi(left).localeCompare(getFilterStringFromApi(right)); diff --git a/src/Utilities/Helpers.test.js b/src/Utilities/Helpers.test.js index 96c5bee5b..7ee232913 100644 --- a/src/Utilities/Helpers.test.js +++ b/src/Utilities/Helpers.test.js @@ -270,6 +270,8 @@ describe('Helpers tests', () => { ${{ search: '' }} | ${true} | ${'?'} ${{ filter: { advisory_type: 2 } }} | ${false} | ${'?filter%5Badvisory_type%5D=2'} ${{ filter: { advisory_type: [1, 2] } }} | ${true} | ${'?filter%5Badvisory_type%5D=in%3A1%2C2'} + ${{ filter: { severity: null } }} | ${false} | ${'?filter%5Bseverity%5D=null'} + ${{ filter: { severity: [1, 2, null] } }} | ${false} | ${'?filter%5Bseverity%5D=in%3A1%2C2%2Cnull'} ${{ filter: { advisory_type: [1, 2] }, param: 'text', @@ -286,6 +288,8 @@ describe('Helpers tests', () => { ${'search=trolo'} | ${{ search: 'trolo' }} ${'filter%5Badvisory_type%5D=2'} | ${{ filter: { advisory_type: 2 } }} ${'param=text&filter%5Badvisory_type%5D=in%3A1%2C2'} | ${{ filter: { advisory_type: ['1', '2'] }, param: 'text' }} + ${'filter%5Bseverity%5D=null'} | ${{ filter: { severity: null } }} + ${'filter%5Bseverity%5D=in%3A1%2C2%2Cnull'} | ${{ filter: { severity: ['1', '2', null] } }} `('decodeQueryparams: Should decodeQueryParams $parameters', ({ parameters, result }) => { expect(decodeQueryparams(parameters)).toEqual(result); });