Skip to content

Commit 70644d1

Browse files
authored
feat!: implement filter range in category filters (#440) (#458)
1 parent fcd05ec commit 70644d1

62 files changed

Lines changed: 2110 additions & 522 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
CategoryKey,
3+
DataDictionaryAnnotation,
4+
SelectCategoryValueView,
5+
} from "../../entities";
6+
import { RangeViewKind } from "../views/range/types";
7+
import { SelectViewKind } from "../views/select/types";
8+
9+
/**
10+
* Category config.
11+
*/
12+
export type CategoryConfig = RangeCategoryConfig | SelectCategoryConfig;
13+
14+
/**
15+
* Common category config.
16+
*/
17+
export interface CommonCategoryConfig {
18+
annotation?: DataDictionaryAnnotation;
19+
key: CategoryKey;
20+
label: string; // human readable label
21+
}
22+
23+
/**
24+
* Range category config.
25+
*/
26+
export interface RangeCategoryConfig
27+
extends CommonCategoryConfig,
28+
RangeViewKind {
29+
unit?: string; // e.g. "kg"
30+
}
31+
32+
/**
33+
* Select category config.
34+
*/
35+
export interface SelectCategoryConfig
36+
extends CommonCategoryConfig,
37+
SelectViewKind {
38+
enableChartView?: boolean;
39+
mapSelectCategoryValue?: (
40+
selectCategoryValue: SelectCategoryValueView
41+
) => SelectCategoryValueView;
42+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Category } from "../models/types";
2+
import { VIEW_KIND } from "../views/types";
3+
import { CategoryConfig } from "./types";
4+
5+
/**
6+
* Returns the category config for the given category config key and view kind.
7+
* @param viewKind - View kind.
8+
* @param key - Category config key.
9+
* @param configs - Category configs.
10+
* @returns category config.
11+
*/
12+
export function findCategoryConfig<V extends VIEW_KIND>(
13+
viewKind: V,
14+
key: Category["key"],
15+
configs: CategoryConfig[]
16+
): Extract<CategoryConfig, { viewKind: V }> | undefined {
17+
return configs.find(
18+
(c): c is Extract<CategoryConfig, { viewKind: V }> =>
19+
c.viewKind === viewKind && c.key === key
20+
);
21+
}
22+
23+
/**
24+
* Returns the range category config for the given category config key.
25+
* @param key - Category config key.
26+
* @param configs - Category configs.
27+
* @returns category config.
28+
*/
29+
export function findRangeCategoryConfig(
30+
key: Category["key"],
31+
configs: CategoryConfig[]
32+
): Extract<CategoryConfig, { viewKind: VIEW_KIND.RANGE }> | undefined {
33+
return findCategoryConfig(VIEW_KIND.RANGE, key, configs);
34+
}
35+
36+
/**
37+
* Returns the select category config for the given category config key.
38+
* @param key - Category config key.
39+
* @param configs - Category configs.
40+
* @returns category config.
41+
*/
42+
export function findSelectCategoryConfig(
43+
key: Category["key"],
44+
configs: CategoryConfig[]
45+
): Extract<CategoryConfig, { viewKind?: VIEW_KIND.SELECT }> | undefined {
46+
return findCategoryConfig(VIEW_KIND.SELECT, key, configs);
47+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Category } from "../types";
2+
import { RangeCategory, SelectedRange } from "./types";
3+
4+
/**
5+
* Determine if category is a range category.
6+
* @param category - Category to check if range category.
7+
* @returns true if category is a range category.
8+
*/
9+
export function isRangeCategory(category: Category): category is RangeCategory {
10+
return "max" in category && "min" in category;
11+
}
12+
13+
/**
14+
* Determine if value is a selected range.
15+
* @param value - Value to check if selected range.
16+
* @returns true if value is a selected range.
17+
*/
18+
export function isSelectedRange(value: unknown): value is SelectedRange {
19+
return (
20+
Array.isArray(value) &&
21+
value.length === 2 &&
22+
value.every((v) => v === null || typeof v === "number")
23+
);
24+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CategoryKey } from "../../../entities";
2+
3+
/**
4+
* Internal filter model of a range category.
5+
*/
6+
export interface RangeCategory {
7+
key: CategoryKey;
8+
max: number;
9+
min: number;
10+
selectedMax: SelectedRange[1];
11+
selectedMin: SelectedRange[0];
12+
}
13+
14+
/**
15+
* Min and max values selected in range category.
16+
*/
17+
export type SelectedRange = [number | null, number | null];
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { CategoryValueKey, SelectedFilter } from "../../../entities";
2+
import { isSelectedRange } from "./typeGuards";
3+
import { SelectedRange } from "./types";
4+
5+
/**
6+
* Asserts that the given value is a selected range.
7+
* Throws an error if the value is not a selected range.
8+
* @param value - Value to assert is a selected range.
9+
*/
10+
export function assertIsRange(value: unknown): asserts value is SelectedRange {
11+
if (!isSelectedRange(value)) {
12+
throw new Error("Value is not SelectedRange");
13+
}
14+
}
15+
16+
/**
17+
* Build the next filter state for the given range category.
18+
* @param nextCategorySelectedFilter - Next filter state for the range category.
19+
* @param selectedValue - Selected value for the range category.
20+
* @param selected - Whether the category value is selected.
21+
*/
22+
export function buildNextRangeFilterState(
23+
nextCategorySelectedFilter: SelectedFilter,
24+
selectedValue: CategoryValueKey,
25+
selected: boolean
26+
): void {
27+
if (selected) {
28+
// Assert that the selected value is a range.
29+
assertIsRange(selectedValue);
30+
// Set the selected range.
31+
nextCategorySelectedFilter.value = selectedValue;
32+
} else {
33+
// Remove the selected range.
34+
nextCategorySelectedFilter.value = [];
35+
}
36+
}
37+
38+
/**
39+
* Get the selected values for the given category, if any.
40+
* Handles type checking for selected range.
41+
* Falls back to empty array if no selected values or invalid type (should never happen).
42+
* @param categorySelectedFilter - Current filter state for a category.
43+
* @returns The selected filter (i.e. the set of selected values) for the given category.
44+
*/
45+
export function getRangeSelectedValue(
46+
categorySelectedFilter?: SelectedFilter
47+
): SelectedRange {
48+
return isSelectedRange(categorySelectedFilter?.value)
49+
? categorySelectedFilter?.value
50+
: [null, null];
51+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { CategoryValueKey, SelectedFilter } from "../../../entities";
2+
3+
/**
4+
* Build the next filter state for the given select category.
5+
* @param nextCategorySelectedFilter - Next filter state for the select category.
6+
* @param selectedValue - Selected value for the select category.
7+
* @param selected - Whether the category value is selected.
8+
*/
9+
export function buildNextSelectFilterState(
10+
nextCategorySelectedFilter: SelectedFilter,
11+
selectedValue: CategoryValueKey,
12+
selected: boolean
13+
): void {
14+
if (selected) {
15+
// Set the selected value.
16+
nextCategorySelectedFilter.value.push(selectedValue);
17+
} else {
18+
// Remove the selected value from the selected set of values.
19+
nextCategorySelectedFilter.value = nextCategorySelectedFilter.value.filter(
20+
(value: CategoryValueKey) => value !== selectedValue
21+
);
22+
}
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { SelectCategory } from "../../entities";
2+
import { RangeCategory } from "./range/types";
3+
4+
/**
5+
* Internal filter model of a category.
6+
*/
7+
export type Category = SelectCategory | RangeCategory;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { CategoryKey, DataDictionaryAnnotation } from "../../../entities";
2+
3+
/**
4+
* Common properties for category views.
5+
*/
6+
export interface BaseCategoryView {
7+
annotation?: DataDictionaryAnnotation;
8+
isDisabled?: boolean;
9+
key: CategoryKey;
10+
label: string;
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { CategoryView } from "../types";
2+
import { RangeCategoryView } from "./types";
3+
4+
/**
5+
* Returns true if the category view is a range category view.
6+
* @param view - Category view.
7+
* @returns true if the category view is a range category view.
8+
*/
9+
export function isRangeCategoryView(
10+
view: CategoryView
11+
): view is RangeCategoryView {
12+
return "max" in view && "min" in view;
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SelectedRange } from "../../models/range/types";
2+
import { BaseCategoryView } from "../common/types";
3+
import { VIEW_KIND } from "../types";
4+
5+
/**
6+
* View model of range category.
7+
*/
8+
export interface RangeCategoryView extends BaseCategoryView {
9+
max: number;
10+
min: number;
11+
selectedMax: SelectedRange[1];
12+
selectedMin: SelectedRange[0];
13+
unit?: string;
14+
}
15+
16+
/**
17+
* Model of range category view kind.
18+
*/
19+
export interface RangeViewKind {
20+
viewKind: VIEW_KIND.RANGE;
21+
}

0 commit comments

Comments
 (0)