diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx
index d96fa0081beb..1d06e72902a8 100644
--- a/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx
+++ b/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx
@@ -51,7 +51,7 @@ function SearchFiltersBarNarrow({queryJSON}: SearchFiltersBarNarrowProps) {
ref={scrollRef}
showsHorizontalScrollIndicator={false}
data={data}
- keyExtractor={(item) => item.key}
+ keyExtractor={(item) => item.id}
renderItem={renderFilterItem}
onEndReached={adjustScroll}
onEndReachedThreshold={0.75}
diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx
index e7006ded5896..58131b5eb304 100644
--- a/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx
+++ b/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx
@@ -30,7 +30,7 @@ function SearchFiltersBarWideContent({hasErrors, shouldShowFiltersBarLoading, fi
return filters.map((item) => (
));
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 53a376153a8e..f6e1aec0d46e 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -6256,6 +6256,8 @@ function getReportFieldTextKey(key: ReportFieldKey): ReportFieldTextKey {
type SearchFilter = {
key: keyof typeof FILTER_VIEW_MAP;
+ /** Unique per filter, unlike `key` which is shared by a filter and its negation (e.g. `status` and `statusNot`) */
+ id: MappedFilterKey;
label: string;
value: string | string[];
};
@@ -6328,7 +6330,7 @@ function mapFiltersFormToLabelValueList(
if (displayValue && label) {
addedGroups.add(syntax);
const isDefault = defaultSearchQueryFilterKeys.has(syntax);
- (isDefault ? defaultFilters : nonDefaultFilters).push({key: syntax, label: translate(label), value: displayValue, ...mapper?.(syntax, isDefault)});
+ (isDefault ? defaultFilters : nonDefaultFilters).push({key: syntax, id: syntax, label: translate(label), value: displayValue, ...mapper?.(syntax, isDefault)});
}
continue;
}
@@ -6344,7 +6346,13 @@ function mapFiltersFormToLabelValueList(
addedGroups.add(CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX);
const isDefault = defaultSearchQueryFilterKeys.has(getReportFieldTextKey(key));
const extra = mapper?.(CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD, isDefault);
- (isDefault ? defaultFilters : nonDefaultFilters).push({key: CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD, label: translate('workspace.common.reportField'), value, ...extra});
+ (isDefault ? defaultFilters : nonDefaultFilters).push({
+ key: CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD,
+ id: CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD,
+ label: translate('workspace.common.reportField'),
+ value,
+ ...extra,
+ });
}
continue;
}
@@ -6361,7 +6369,7 @@ function mapFiltersFormToLabelValueList(
if (label && value && !(Array.isArray(value) && value.length === 0)) {
const isDefault = defaultSearchQueryFilterKeys.has(baseKey);
- (isDefault ? defaultFilters : nonDefaultFilters).push({key: baseKey, label, value, ...mapper?.(key, isDefault)});
+ (isDefault ? defaultFilters : nonDefaultFilters).push({key: baseKey, id: key, label, value, ...mapper?.(key, isDefault)});
}
}
diff --git a/src/pages/Search/SearchSavePage.tsx b/src/pages/Search/SearchSavePage.tsx
index b32ecd1f5436..afa8cd4e390f 100644
--- a/src/pages/Search/SearchSavePage.tsx
+++ b/src/pages/Search/SearchSavePage.tsx
@@ -240,7 +240,7 @@ function SearchSavePage() {
appliedFilters.map((filter) => (
{CONST.DOT_SEPARATOR}
diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts
index 938e5656a8b9..36fd0a6a1adb 100644
--- a/tests/unit/Search/SearchUIUtilsTest.ts
+++ b/tests/unit/Search/SearchUIUtilsTest.ts
@@ -15364,6 +15364,22 @@ describe('SearchUIUtils', () => {
expect(result.at(0)?.isDefault).toBe(true);
expect(result.at(1)?.isDefault).toBe(false);
});
+
+ it('gives a filter and its negation the same key but different ids', () => {
+ // Given a query that uses the status filter both normally and negated, e.g. `status:approved -status:drafts,paid`
+ const form = {
+ [FILTER_KEYS.TYPE]: CONST.SEARCH.DATA_TYPES.EXPENSE,
+ [FILTER_KEYS.STATUS]: [CONST.SEARCH.STATUS.EXPENSE.APPROVED],
+ [FILTER_KEYS.STATUS_NOT]: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.PAID],
+ };
+
+ // When the form is mapped to filter chips
+ const result = SearchUIUtils.mapFiltersFormToLabelValueList(form, new Set(), new Set(), translateLocal, undefined, localeCompare, convertToDisplayStringWithoutCurrency);
+ const statusFilters = result.filter((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS);
+
+ // Then both chips share the base key (used to pick the chip's popover), while their ids stay unique so they can be used as React keys
+ expect(statusFilters.map((filter) => filter.id)).toEqual([FILTER_KEYS.STATUS, FILTER_KEYS.STATUS_NOT]);
+ });
});
});
diff --git a/tests/unit/pages/Search/SearchSavePageTest.tsx b/tests/unit/pages/Search/SearchSavePageTest.tsx
index 12992acbccf4..ae56ee5f76a3 100644
--- a/tests/unit/pages/Search/SearchSavePageTest.tsx
+++ b/tests/unit/pages/Search/SearchSavePageTest.tsx
@@ -75,8 +75,8 @@ it.each([[['12']], [['12', '23']], [['123']]])('renders canonical card selection
});
it('renders scalar feed and tax values without calling array display hooks', () => {
jest.spyOn(SearchUIUtils, 'mapFiltersFormToLabelValueList').mockReturnValueOnce([
- {key: CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED, label: 'Feed', value: 'feed-scalar'},
- {key: CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE, label: 'Tax', value: 'tax-scalar'},
+ {key: CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED, id: CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED, label: 'Feed', value: 'feed-scalar'},
+ {key: CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE, id: CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE, label: 'Tax', value: 'tax-scalar'},
]);
const output = JSON.stringify(render().toJSON());
expect(['feed-scalar', 'tax-scalar'].every((text) => output.includes(text))).toBe(true);