Skip to content

Commit

Permalink
Merge pull request #9 from CartoDB/feat/filter-fns
Browse files Browse the repository at this point in the history
feat: Add addFilter, removeFilter, clearFilters
  • Loading branch information
donmccurdy authored Aug 29, 2024
2 parents 145b278 + 451e44d commit 74912f6
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 10 deletions.
25 changes: 15 additions & 10 deletions examples/components/widgets/category-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import {Task, TaskStatus} from '@lit/task';
import {Ref, createRef, ref} from 'lit/directives/ref.js';
import {cache} from 'lit/directives/cache.js';
import * as echarts from 'echarts';
import {AggregationType} from '@carto/api-client';
import {
AggregationType,
Filter,
FilterType,
addFilter,
removeFilter,
} from '@carto/api-client';

import {DEFAULT_PALETTE, DEFAULT_TEXT_STYLE} from './styles.js';
import {DEBOUNCE_TIME_MS} from '../constants.js';
Expand Down Expand Up @@ -129,19 +135,18 @@ export class CategoryWidget extends BaseWidget {
if (!this.data) return;

const {widgetSource} = await this.data;
const filters = {...widgetSource.props.filters} as Record<string, unknown>;
const filters = {...widgetSource.props.filters} as Record<string, Filter>;
const column = this.column as string;

// TODO: Append filters from multiple widgets on the same columns.
if (this._filterValues.length > 0) {
filters[column] = {
in: {
owner: this._widgetId,
values: Array.from(this._filterValues),
},
};
addFilter(filters, {
column,
type: FilterType.IN,
values: Array.from(this._filterValues),
owner: this._widgetId,
});
} else {
delete filters[column];
removeFilter(filters, {column, owner: this._widgetId});
}

this.dispatchEvent(new CustomEvent('filter', {detail: {filters}}));
Expand Down
80 changes: 80 additions & 0 deletions src/filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {FilterType} from './constants';
import {Filter} from './types';
import {isEmptyObject} from './utils';

type FilterTypeOptions<T extends FilterType> = {
type: T;
column: string;
} & Filter[T];

export type AddFilterOptions =
| FilterTypeOptions<FilterType.IN>
| FilterTypeOptions<FilterType.BETWEEN>
| FilterTypeOptions<FilterType.CLOSED_OPEN>
| FilterTypeOptions<FilterType.TIME>
| FilterTypeOptions<FilterType.STRING_SEARCH>;

/**
* Adds a {@link Filter} to the filter set. Any previous filters with the same
* `column` and `type` will be replaced.
*/
export function addFilter(
filters: Record<string, Filter>,
{column, type, values, owner}: AddFilterOptions
): Record<string, Filter> {
if (!filters[column]) {
filters[column] = {};
}

const filter = {values, owner} as FilterTypeOptions<typeof type>;
(filters[column][type] as FilterTypeOptions<typeof type>) = filter;

return filters;
}

export type RemoveFilterOptions = {
column: string;
owner?: string;
};

/**
* Removes one or more {@link Filter filters} from the filter set. If only
* `column` is specified, then all filters on that column are removed. If both
* `column` and `owner` are specified, then only filters for that column
* associated with the owner are removed.
*/
export function removeFilter(
filters: Record<string, Filter>,
{column, owner}: RemoveFilterOptions
): Record<string, Filter> {
const filter = filters[column];
if (!filter) {
return filters;
}

if (owner) {
for (const type in FilterType) {
if (owner === filter[type as FilterType]?.owner) {
delete filter[type as FilterType];
}
}
}

if (!owner || isEmptyObject(filter)) {
delete filters[column];
}

return filters;
}

/**
* Clears all {@link Filter filters} from the filter set.
*/
export function clearFilters(
filters: Record<string, Filter>
): Record<string, Filter> {
for (const column of Object.keys(filters)) {
delete filters[column];
}
return filters;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './client.js';
export * from './constants.js';
export * from './filters.js';
export * from './sources/index.js';
export * from './types.js';
7 changes: 7 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,10 @@ export class InvalidColumnError extends Error {
);
}
}

export function isEmptyObject(object: object): boolean {
for (const _ in object) {
return false;
}
return true;
}
159 changes: 159 additions & 0 deletions test/filters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {expect, test} from 'vitest';
import {
GroupDateType,
FilterType,
clearFilters,
addFilter,
removeFilter,
} from '@carto/api-client';

test('addFilter', () => {
let filters = {};

filters = addFilter(filters, {
column: 'column_a',
type: FilterType.IN,
values: [1, 2],
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
});

filters = addFilter(filters, {
column: 'column_b',
type: FilterType.IN,
values: [3, 4],
owner: 'my-widget',
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
column_b: {
[FilterType.IN]: {values: [3, 4], owner: 'my-widget'},
},
});

filters = addFilter(filters, {
column: 'column_b',
type: FilterType.BETWEEN,
values: [[3, 4]],
owner: 'my-widget-2',
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
column_b: {
[FilterType.IN]: {values: [3, 4], owner: 'my-widget'},
[FilterType.BETWEEN]: {values: [[3, 4]], owner: 'my-widget-2'},
},
});

filters = addFilter(filters, {
column: 'column_b',
type: FilterType.IN,
values: ['a', 'b'],
owner: 'my-widget-3',
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
column_b: {
[FilterType.IN]: {values: ['a', 'b'], owner: 'my-widget-3'},
[FilterType.BETWEEN]: {values: [[3, 4]], owner: 'my-widget-2'},
},
});
});

test('removeFilter', () => {
let filters = {};

filters = removeFilter(filters, {
column: 'no-such-column',
owner: 'my-widget',
});

expect(filters).toMatchObject({});

filters = {
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
column_b: {
[FilterType.IN]: {values: ['a', 'b'], owner: 'my-widget-3'},
[FilterType.BETWEEN]: {values: [[3, 4]], owner: 'my-widget-2'},
},
};

filters = removeFilter(filters, {
column: 'column_b',
owner: 'my-widget-3',
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
column_b: {
[FilterType.BETWEEN]: {values: [[3, 4]], owner: 'my-widget-2'},
},
});

filters = removeFilter(filters, {
column: 'column_b',
owner: 'no-such-owner',
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
column_b: {
[FilterType.BETWEEN]: {values: [[3, 4]], owner: 'my-widget-2'},
},
});

filters = removeFilter(filters, {
column: 'column_b',
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
});

filters = removeFilter(filters, {
column: 'no-such-column',
});

expect(filters).toMatchObject({
column_a: {
[FilterType.IN]: {values: [1, 2]},
},
});
});

test('clearFilters', () => {
let filters = clearFilters({});

expect(filters).toMatchObject({});

filters = clearFilters({
column_a: {[FilterType.IN]: {values: [1, 2]}},
column_b: {
[FilterType.IN]: {values: [3, 4]},
[FilterType.BETWEEN]: {values: [[0, 1]]},
},
});

expect(filters).toMatchObject({});
});

0 comments on commit 74912f6

Please sign in to comment.