Skip to content

Commit 05ee5b3

Browse files
authored
fix: Saved legacy filter with classname in query cannot be deleted (#2948)
1 parent 0fad66b commit 05ee5b3

File tree

2 files changed

+207
-10
lines changed

2 files changed

+207
-10
lines changed

src/components/BrowserFilter/BrowserFilter.react.js

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,28 @@ export default class BrowserFilter extends React.Component {
8484
);
8585

8686
if (preferences.filters) {
87-
// Try to find a saved filter that matches the current filter content
88-
const currentFiltersString = JSON.stringify(this.props.filters.toJS());
87+
// Normalize current filters for comparison (remove class property if it matches current className)
88+
const currentFilters = this.props.filters.toJS().map(filter => {
89+
const normalizedFilter = { ...filter };
90+
if (normalizedFilter.class === this.props.className) {
91+
delete normalizedFilter.class;
92+
}
93+
return normalizedFilter;
94+
});
95+
const currentFiltersString = JSON.stringify(currentFilters);
8996

9097
const matchingFilter = preferences.filters.find(savedFilter => {
9198
try {
92-
const savedFiltersString = JSON.stringify(JSON.parse(savedFilter.filter));
99+
const savedFilters = JSON.parse(savedFilter.filter);
100+
// Normalize saved filters for comparison (remove class property if it matches current className)
101+
const normalizedSavedFilters = savedFilters.map(filter => {
102+
const normalizedFilter = { ...filter };
103+
if (normalizedFilter.class === this.props.className) {
104+
delete normalizedFilter.class;
105+
}
106+
return normalizedFilter;
107+
});
108+
const savedFiltersString = JSON.stringify(normalizedSavedFilters);
93109
return savedFiltersString === currentFiltersString;
94110
} catch {
95111
return false;
@@ -147,14 +163,29 @@ export default class BrowserFilter extends React.Component {
147163
);
148164

149165
if (preferences.filters) {
150-
// Try to find a saved filter that matches the current filter content
151-
const currentFiltersString = JSON.stringify(this.props.filters.toJS());
166+
// Normalize current filters for comparison (remove class property if it matches current className)
167+
const currentFilters = this.props.filters.toJS().map(filter => {
168+
const normalizedFilter = { ...filter };
169+
if (normalizedFilter.class === this.props.className) {
170+
delete normalizedFilter.class;
171+
}
172+
return normalizedFilter;
173+
});
174+
const currentFiltersString = JSON.stringify(currentFilters);
152175

153176
const matchingFilter = preferences.filters.find(savedFilter => {
154177
try {
155-
const savedFiltersString = JSON.stringify(JSON.parse(savedFilter.filter));
156-
const matches = savedFiltersString === currentFiltersString;
157-
return matches;
178+
const savedFilters = JSON.parse(savedFilter.filter);
179+
// Normalize saved filters for comparison (remove class property if it matches current className)
180+
const normalizedSavedFilters = savedFilters.map(filter => {
181+
const normalizedFilter = { ...filter };
182+
if (normalizedFilter.class === this.props.className) {
183+
delete normalizedFilter.class;
184+
}
185+
return normalizedFilter;
186+
});
187+
const savedFiltersString = JSON.stringify(normalizedSavedFilters);
188+
return savedFiltersString === currentFiltersString;
158189
} catch {
159190
return false;
160191
}
@@ -377,11 +408,29 @@ export default class BrowserFilter extends React.Component {
377408
if (currentFilterInfo.isLegacy) {
378409
const preferences = ClassPreferences.getPreferences(this.context.applicationId, this.props.className);
379410
if (preferences.filters) {
380-
const currentFiltersString = JSON.stringify(this.props.filters.toJS());
411+
// Normalize current filters for comparison
412+
const currentFilters = this.props.filters.toJS().map(filter => {
413+
const normalizedFilter = { ...filter };
414+
if (normalizedFilter.class === this.props.className) {
415+
delete normalizedFilter.class;
416+
}
417+
return normalizedFilter;
418+
});
419+
const currentFiltersString = JSON.stringify(currentFilters);
420+
381421
const matchingFilter = preferences.filters.find(filter => {
382422
if (!filter.id && filter.name === currentFilterInfo.name) {
383423
try {
384-
const savedFiltersString = JSON.stringify(JSON.parse(filter.filter));
424+
const savedFilters = JSON.parse(filter.filter);
425+
// Normalize saved filters for comparison
426+
const normalizedSavedFilters = savedFilters.map(savedFilter => {
427+
const normalizedFilter = { ...savedFilter };
428+
if (normalizedFilter.class === this.props.className) {
429+
delete normalizedFilter.class;
430+
}
431+
return normalizedFilter;
432+
});
433+
const savedFiltersString = JSON.stringify(normalizedSavedFilters);
385434
return savedFiltersString === currentFiltersString;
386435
} catch {
387436
return false;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
/*
5+
* Copyright (c) 2016-present, Parse, LLC
6+
* All rights reserved.
7+
*
8+
* This source code is licensed under the license found in the LICENSE file in
9+
* the root directory of this source tree.
10+
*/
11+
12+
describe('BrowserFilter - Legacy Filter Normalization', () => {
13+
// Function to normalize filters (same logic as in BrowserFilter)
14+
function normalizeFilters(filters, className) {
15+
return filters.map(filter => {
16+
const normalizedFilter = { ...filter };
17+
if (normalizedFilter.class === className) {
18+
delete normalizedFilter.class;
19+
}
20+
return normalizedFilter;
21+
});
22+
}
23+
24+
describe('filter normalization for legacy support', () => {
25+
it('should normalize filters by removing class property that matches current className', () => {
26+
const filtersWithClass = [
27+
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA', class: 'MyClass' },
28+
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB', class: 'MyClass' }
29+
];
30+
31+
const filtersWithoutClass = [
32+
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' },
33+
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB' }
34+
];
35+
36+
const normalizedWithClass = normalizeFilters(filtersWithClass, 'MyClass');
37+
const normalizedWithoutClass = normalizeFilters(filtersWithoutClass, 'MyClass');
38+
39+
expect(JSON.stringify(normalizedWithClass)).toBe(JSON.stringify(normalizedWithoutClass));
40+
expect(JSON.stringify(normalizedWithClass)).toBe(JSON.stringify(filtersWithoutClass));
41+
});
42+
43+
it('should not remove class property that differs from current className', () => {
44+
const filtersWithDifferentClass = [
45+
{ field: 'name', constraint: 'eq', compareTo: 'test', class: '_User' }
46+
];
47+
48+
const normalized = normalizeFilters(filtersWithDifferentClass, 'MyClass');
49+
50+
expect(normalized[0].class).toBe('_User');
51+
expect(JSON.stringify(normalized)).toBe(JSON.stringify(filtersWithDifferentClass));
52+
});
53+
54+
it('should handle filters without class property', () => {
55+
const filtersWithoutClass = [
56+
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' },
57+
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB' }
58+
];
59+
60+
const normalized = normalizeFilters(filtersWithoutClass, 'MyClass');
61+
62+
expect(JSON.stringify(normalized)).toBe(JSON.stringify(filtersWithoutClass));
63+
});
64+
65+
it('should handle complex filters with dates', () => {
66+
const filtersWithDates = [
67+
{
68+
field: 'createdAt',
69+
constraint: 'after',
70+
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' },
71+
class: 'MyClass'
72+
}
73+
];
74+
75+
const expectedNormalized = [
76+
{
77+
field: 'createdAt',
78+
constraint: 'after',
79+
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' }
80+
}
81+
];
82+
83+
const normalized = normalizeFilters(filtersWithDates, 'MyClass');
84+
85+
expect(JSON.stringify(normalized)).toBe(JSON.stringify(expectedNormalized));
86+
});
87+
88+
it('should match normalized filters', () => {
89+
// This is the exact filter from the broken URL
90+
const originalLegacyFilter = [
91+
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' },
92+
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB' },
93+
{
94+
field: 'createdAt',
95+
constraint: 'after',
96+
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' }
97+
}
98+
];
99+
100+
// This is what extractFiltersFromQuery creates (with class property added)
101+
const processedFilter = [
102+
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA', class: 'MyClass' },
103+
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB', class: 'MyClass' },
104+
{
105+
field: 'createdAt',
106+
constraint: 'after',
107+
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' },
108+
class: 'MyClass'
109+
}
110+
];
111+
112+
// Without normalization, these don't match
113+
expect(JSON.stringify(originalLegacyFilter)).not.toBe(JSON.stringify(processedFilter));
114+
115+
// With normalization, they should match
116+
const normalizedOriginal = normalizeFilters(originalLegacyFilter, 'MyClass');
117+
const normalizedProcessed = normalizeFilters(processedFilter, 'MyClass');
118+
119+
expect(JSON.stringify(normalizedOriginal)).toBe(JSON.stringify(normalizedProcessed));
120+
expect(JSON.stringify(normalizedOriginal)).toBe(JSON.stringify(originalLegacyFilter));
121+
});
122+
123+
it('should handle the working filter', () => {
124+
// This is the filter from the working URL (already has class property)
125+
const workingFilter = [
126+
{ class: 'MyClass', field: 'fieldA', constraint: 'eq', compareTo: 'valueA' }
127+
];
128+
129+
// When processed, it should remain the same since it already has the correct class
130+
const processedWorkingFilter = [
131+
{ class: 'MyClass', field: 'fieldA', constraint: 'eq', compareTo: 'valueA' }
132+
];
133+
134+
// With normalization, both should normalize to the same thing
135+
const normalizedWorking = normalizeFilters(workingFilter, 'MyClass');
136+
const normalizedProcessed = normalizeFilters(processedWorkingFilter, 'MyClass');
137+
138+
expect(JSON.stringify(normalizedWorking)).toBe(JSON.stringify(normalizedProcessed));
139+
140+
// Both should normalize to the version without class property
141+
const expectedNormalized = [
142+
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' }
143+
];
144+
145+
expect(JSON.stringify(normalizedWorking)).toBe(JSON.stringify(expectedNormalized));
146+
});
147+
});
148+
});

0 commit comments

Comments
 (0)