Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 59 additions & 10 deletions src/components/BrowserFilter/BrowserFilter.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,28 @@ export default class BrowserFilter extends React.Component {
);

if (preferences.filters) {
// Try to find a saved filter that matches the current filter content
const currentFiltersString = JSON.stringify(this.props.filters.toJS());
// Normalize current filters for comparison (remove class property if it matches current className)
const currentFilters = this.props.filters.toJS().map(filter => {
const normalizedFilter = { ...filter };
if (normalizedFilter.class === this.props.className) {
delete normalizedFilter.class;
}
return normalizedFilter;
});
const currentFiltersString = JSON.stringify(currentFilters);

const matchingFilter = preferences.filters.find(savedFilter => {
try {
const savedFiltersString = JSON.stringify(JSON.parse(savedFilter.filter));
const savedFilters = JSON.parse(savedFilter.filter);
// Normalize saved filters for comparison (remove class property if it matches current className)
const normalizedSavedFilters = savedFilters.map(filter => {
const normalizedFilter = { ...filter };
if (normalizedFilter.class === this.props.className) {
delete normalizedFilter.class;
}
return normalizedFilter;
});
const savedFiltersString = JSON.stringify(normalizedSavedFilters);
return savedFiltersString === currentFiltersString;
} catch {
return false;
Expand Down Expand Up @@ -147,14 +163,29 @@ export default class BrowserFilter extends React.Component {
);

if (preferences.filters) {
// Try to find a saved filter that matches the current filter content
const currentFiltersString = JSON.stringify(this.props.filters.toJS());
// Normalize current filters for comparison (remove class property if it matches current className)
const currentFilters = this.props.filters.toJS().map(filter => {
const normalizedFilter = { ...filter };
if (normalizedFilter.class === this.props.className) {
delete normalizedFilter.class;
}
return normalizedFilter;
});
const currentFiltersString = JSON.stringify(currentFilters);

const matchingFilter = preferences.filters.find(savedFilter => {
try {
const savedFiltersString = JSON.stringify(JSON.parse(savedFilter.filter));
const matches = savedFiltersString === currentFiltersString;
return matches;
const savedFilters = JSON.parse(savedFilter.filter);
// Normalize saved filters for comparison (remove class property if it matches current className)
const normalizedSavedFilters = savedFilters.map(filter => {
const normalizedFilter = { ...filter };
if (normalizedFilter.class === this.props.className) {
delete normalizedFilter.class;
}
return normalizedFilter;
});
const savedFiltersString = JSON.stringify(normalizedSavedFilters);
return savedFiltersString === currentFiltersString;
} catch {
return false;
}
Expand Down Expand Up @@ -377,11 +408,29 @@ export default class BrowserFilter extends React.Component {
if (currentFilterInfo.isLegacy) {
const preferences = ClassPreferences.getPreferences(this.context.applicationId, this.props.className);
if (preferences.filters) {
const currentFiltersString = JSON.stringify(this.props.filters.toJS());
// Normalize current filters for comparison
const currentFilters = this.props.filters.toJS().map(filter => {
const normalizedFilter = { ...filter };
if (normalizedFilter.class === this.props.className) {
delete normalizedFilter.class;
}
return normalizedFilter;
});
const currentFiltersString = JSON.stringify(currentFilters);

const matchingFilter = preferences.filters.find(filter => {
if (!filter.id && filter.name === currentFilterInfo.name) {
try {
const savedFiltersString = JSON.stringify(JSON.parse(filter.filter));
const savedFilters = JSON.parse(filter.filter);
// Normalize saved filters for comparison
const normalizedSavedFilters = savedFilters.map(savedFilter => {
const normalizedFilter = { ...savedFilter };
if (normalizedFilter.class === this.props.className) {
delete normalizedFilter.class;
}
return normalizedFilter;
});
const savedFiltersString = JSON.stringify(normalizedSavedFilters);
return savedFiltersString === currentFiltersString;
} catch {
return false;
Expand Down
148 changes: 148 additions & 0 deletions src/lib/tests/BrowserFilter.legacySupport.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* @jest-environment jsdom
*/
/*
* Copyright (c) 2016-present, Parse, LLC
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/

describe('BrowserFilter - Legacy Filter Normalization', () => {
// Function to normalize filters (same logic as in BrowserFilter)
function normalizeFilters(filters, className) {
return filters.map(filter => {
const normalizedFilter = { ...filter };
if (normalizedFilter.class === className) {
delete normalizedFilter.class;
}
return normalizedFilter;
});
}

describe('filter normalization for legacy support', () => {
it('should normalize filters by removing class property that matches current className', () => {
const filtersWithClass = [
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA', class: 'MyClass' },
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB', class: 'MyClass' }
];

const filtersWithoutClass = [
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' },
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB' }
];

const normalizedWithClass = normalizeFilters(filtersWithClass, 'MyClass');
const normalizedWithoutClass = normalizeFilters(filtersWithoutClass, 'MyClass');

expect(JSON.stringify(normalizedWithClass)).toBe(JSON.stringify(normalizedWithoutClass));
expect(JSON.stringify(normalizedWithClass)).toBe(JSON.stringify(filtersWithoutClass));
});

it('should not remove class property that differs from current className', () => {
const filtersWithDifferentClass = [
{ field: 'name', constraint: 'eq', compareTo: 'test', class: '_User' }
];

const normalized = normalizeFilters(filtersWithDifferentClass, 'MyClass');

expect(normalized[0].class).toBe('_User');
expect(JSON.stringify(normalized)).toBe(JSON.stringify(filtersWithDifferentClass));
});

it('should handle filters without class property', () => {
const filtersWithoutClass = [
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' },
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB' }
];

const normalized = normalizeFilters(filtersWithoutClass, 'MyClass');

expect(JSON.stringify(normalized)).toBe(JSON.stringify(filtersWithoutClass));
});

it('should handle complex filters with dates', () => {
const filtersWithDates = [
{
field: 'createdAt',
constraint: 'after',
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' },
class: 'MyClass'
}
];

const expectedNormalized = [
{
field: 'createdAt',
constraint: 'after',
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' }
}
];

const normalized = normalizeFilters(filtersWithDates, 'MyClass');

expect(JSON.stringify(normalized)).toBe(JSON.stringify(expectedNormalized));
});

it('should match normalized filters', () => {
// This is the exact filter from the broken URL
const originalLegacyFilter = [
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' },
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB' },
{
field: 'createdAt',
constraint: 'after',
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' }
}
];

// This is what extractFiltersFromQuery creates (with class property added)
const processedFilter = [
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA', class: 'MyClass' },
{ field: 'fieldB', constraint: 'eq', compareTo: 'valueB', class: 'MyClass' },
{
field: 'createdAt',
constraint: 'after',
compareTo: { __type: 'Date', iso: '2023-11-18T00:00:00.000Z' },
class: 'MyClass'
}
];

// Without normalization, these don't match
expect(JSON.stringify(originalLegacyFilter)).not.toBe(JSON.stringify(processedFilter));

// With normalization, they should match
const normalizedOriginal = normalizeFilters(originalLegacyFilter, 'MyClass');
const normalizedProcessed = normalizeFilters(processedFilter, 'MyClass');

expect(JSON.stringify(normalizedOriginal)).toBe(JSON.stringify(normalizedProcessed));
expect(JSON.stringify(normalizedOriginal)).toBe(JSON.stringify(originalLegacyFilter));
});

it('should handle the working filter', () => {
// This is the filter from the working URL (already has class property)
const workingFilter = [
{ class: 'MyClass', field: 'fieldA', constraint: 'eq', compareTo: 'valueA' }
];

// When processed, it should remain the same since it already has the correct class
const processedWorkingFilter = [
{ class: 'MyClass', field: 'fieldA', constraint: 'eq', compareTo: 'valueA' }
];

// With normalization, both should normalize to the same thing
const normalizedWorking = normalizeFilters(workingFilter, 'MyClass');
const normalizedProcessed = normalizeFilters(processedWorkingFilter, 'MyClass');

expect(JSON.stringify(normalizedWorking)).toBe(JSON.stringify(normalizedProcessed));

// Both should normalize to the version without class property
const expectedNormalized = [
{ field: 'fieldA', constraint: 'eq', compareTo: 'valueA' }
];

expect(JSON.stringify(normalizedWorking)).toBe(JSON.stringify(expectedNormalized));
});
});
});
Loading