From 958f5d77e195c87c813966c346e2077e40690574 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 27 Aug 2024 11:10:22 +0200 Subject: [PATCH 1/2] feat: Add addFilter, removeFilter, clearFilters --- .../components/widgets/category-widget.ts | 25 +++--- src/filters.ts | 80 +++++++++++++++++++ src/index.ts | 1 + src/utils.ts | 7 ++ 4 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 src/filters.ts diff --git a/examples/components/widgets/category-widget.ts b/examples/components/widgets/category-widget.ts index 1ad6782..f81e180 100644 --- a/examples/components/widgets/category-widget.ts +++ b/examples/components/widgets/category-widget.ts @@ -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'; @@ -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; + const filters = {...widgetSource.props.filters} as Record; 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}})); diff --git a/src/filters.ts b/src/filters.ts new file mode 100644 index 0000000..f0466d8 --- /dev/null +++ b/src/filters.ts @@ -0,0 +1,80 @@ +import {FilterType} from './constants'; +import {Filter} from './types'; +import {isEmptyObject} from './utils'; + +type FilterTypeOptions = { + type: T; + column: string; +} & Filter[T]; + +export type AddFilterOptions = + | FilterTypeOptions + | FilterTypeOptions + | FilterTypeOptions + | FilterTypeOptions + | FilterTypeOptions; + +/** + * 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, + {column, type, values, owner}: AddFilterOptions +): Record { + if (!filters[column]) { + filters[column] = {}; + } + + const filter = {values, owner} as FilterTypeOptions; + (filters[column][type] as FilterTypeOptions) = 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, + {column, owner}: RemoveFilterOptions +): Record { + 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 +): Record { + for (const column of Object.keys(filters)) { + delete filters[column]; + } + return filters; +} diff --git a/src/index.ts b/src/index.ts index 41ed0c0..04dd5b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/utils.ts b/src/utils.ts index 72ac0b4..4644804 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -83,3 +83,10 @@ export class InvalidColumnError extends Error { ); } } + +export function isEmptyObject(object: object): boolean { + for (const _ in object) { + return true; + } + return false; +} From 451e44de31a89336c44e81578582b93774cbeca2 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Thu, 29 Aug 2024 12:04:31 +0200 Subject: [PATCH 2/2] chore: Add unit tests --- src/utils.ts | 4 +- test/filters.test.ts | 159 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 test/filters.test.ts diff --git a/src/utils.ts b/src/utils.ts index 4644804..fc756c0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -86,7 +86,7 @@ export class InvalidColumnError extends Error { export function isEmptyObject(object: object): boolean { for (const _ in object) { - return true; + return false; } - return false; + return true; } diff --git a/test/filters.test.ts b/test/filters.test.ts new file mode 100644 index 0000000..2d89807 --- /dev/null +++ b/test/filters.test.ts @@ -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({}); +});