diff --git a/packages/components/src/components/List/List.test.tsx b/packages/components/src/components/List/List.test.tsx
index 34f0d1d1c..8894f5789 100644
--- a/packages/components/src/components/List/List.test.tsx
+++ b/packages/components/src/components/List/List.test.tsx
@@ -1,25 +1,30 @@
import { render, screen } from "@testing-library/react";
-import { List, ListItem, ListStaticData } from "@/components/List";
+import { List, ListFilter, ListItem, ListStaticData } from "@/components/List";
+import type { ReactNode } from "react";
import React from "react";
import { test } from "vitest";
test("renders empty list without errors", async () => {
render(
);
});
+interface Data {
+ num: number;
+}
+
+const renderTest = (items: number[], children: ReactNode = null) => {
+ render(
+
+ {children}
+ data={items.map((num) => ({ num }))} />
+ textValue={(num) => String(num)}>
+ {({ num }) => {num}}
+
+
,
+ );
+};
describe("Static data", () => {
test("Items are updated when data changes", async () => {
- const renderTest = (items: number[]) => {
- render(
-
-
- textValue={(num) => String(num)}>
- {(num) => {num}}
-
-
,
- );
- };
-
renderTest([42]);
await screen.findByText(42);
@@ -28,3 +33,14 @@ describe("Static data", () => {
await screen.findByText(43);
});
});
+
+describe("Filter", () => {
+ test("Items are initially filtered", async () => {
+ renderTest(
+ [42, 43],
+ property="num" defaultSelected={[42]} />,
+ );
+ expect(screen.queryAllByText(42)).toHaveLength(1);
+ expect(screen.queryAllByText(43)).toHaveLength(0);
+ });
+});
diff --git a/packages/components/src/components/List/model/filter/Filter.ts b/packages/components/src/components/List/model/filter/Filter.ts
index 63de7e2fa..a74d8aa8c 100644
--- a/packages/components/src/components/List/model/filter/Filter.ts
+++ b/packages/components/src/components/List/model/filter/Filter.ts
@@ -19,20 +19,19 @@ import { customPropertyPrefix } from "@/components/List/model/types";
import { difference, unique } from "remeda";
import { FilterValue } from "@/components/List/model/filter/FilterValue";
import z from "zod";
+import { toArray } from "@/lib/array/toArray";
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const equalsPropertyMatcher: FilterMatcher = (
+const equalsPropertyMatcher: FilterMatcher = (
filterValue,
propertyValue,
) => filterValue === propertyValue;
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const stringCastRenderMethod: PropertyValueRenderMethod = (value) =>
+const stringCastRenderMethod: PropertyValueRenderMethod = (value) =>
String(value);
export class Filter, TMatchValue> {
public static readonly settingsStorageSchema = z
- .record(z.array(z.unknown()))
+ .record(z.array(z.string()))
.optional();
private _values?: FilterValue[] | undefined;
@@ -44,31 +43,22 @@ export class Filter, TMatchValue> {
public readonly renderItem: PropertyValueRenderMethod;
public readonly name?: string;
private onFilterUpdateCallbacks = new Set<() => unknown>();
- private readonly defaultSelectedValues?: FilterValue[];
+ private readonly defaultSelectedValues?: readonly NonNullable[];
public constructor(list: List, shape: FilterShape) {
this.list = list;
this.property = shape.property;
this.mode = shape.mode ?? "one";
- this._values = shape.values?.map((v) => new FilterValue(this, v));
+ this._values = shape.values?.map((v) => FilterValue.create(this, v));
this.matcher = shape.matcher ?? equalsPropertyMatcher;
this.renderItem = shape.renderItem ?? stringCastRenderMethod;
this.name = shape.name;
- this.defaultSelectedValues = shape.defaultSelected
- ? this.values.filter((v) =>
- shape.defaultSelected?.some((d) => d === v.value),
- )
- : undefined;
+ this.defaultSelectedValues = shape.defaultSelected;
}
- private getStoredDefaultSelectedValues() {
- const storedValues =
- this.list.getStoredFilterDefaultSettings()?.[String(this.property)];
-
- return storedValues
- ? this.values.filter((v) => storedValues.includes(v.id))
- : undefined;
+ private getStoredSelectedIds() {
+ return this.list.getStoredFilterDefaultSettings()?.[String(this.property)];
}
public updateInitialState(initialState: InitialTableState) {
@@ -104,25 +94,27 @@ export class Filter, TMatchValue> {
private checkFilterMatches(
property: unknown,
- filterValue: FilterValue,
+ filterValueInput: unknown,
): boolean {
- if (filterValue === null) {
+ if (filterValueInput === null) {
return true;
}
- const toArray = (val: FilterValue | FilterValue[]): FilterValue[] =>
- Array.isArray(val) ? val : [val];
-
const predicate = (filterValue: FilterValue) =>
this.matcher(filterValue.value as never, property as never);
+ const toFilterValue = (something: unknown) =>
+ FilterValue.create(this, something);
+
if (this.mode === "all") {
- return toArray(filterValue).every(predicate);
+ return toArray(filterValueInput).map(toFilterValue).every(predicate);
} else if (this.mode === "some") {
- const filterArr = toArray(filterValue);
- return filterArr.length === 0 || filterArr.some(predicate);
+ const filterArr = toArray(filterValueInput);
+ return (
+ filterArr.length === 0 || filterArr.map(toFilterValue).some(predicate)
+ );
} else if (this.mode === "one") {
- return predicate(filterValue);
+ return predicate(toFilterValue(filterValueInput));
}
throw new Error(`Unknown filter mode '${this.mode}'`);
@@ -147,7 +139,7 @@ export class Filter, TMatchValue> {
Array.from(this.getTableColumn().getFacetedUniqueValues().keys())
.flatMap((v) => v)
.filter((v) => v !== undefined && v !== null),
- ).map((v) => new FilterValue(this, v));
+ ).map((v) => FilterValue.create(this, v));
}
private checkIfValueIsUnknown(value: FilterValue) {
@@ -179,12 +171,10 @@ export class Filter, TMatchValue> {
}
public getArrayValue(): FilterValue[] {
- const currentValue = this.getValue();
- return Array.isArray(currentValue)
- ? currentValue
- : currentValue === null
- ? []
- : [currentValue];
+ const value = this.getValue();
+ return value === null
+ ? []
+ : toArray(value).map((v) => FilterValue.create(this, v));
}
public isValueActive(value: FilterValue): boolean {
@@ -214,7 +204,8 @@ export class Filter, TMatchValue> {
public hasChanged(): boolean {
const currentValues = this.getArrayValue().map((v) => v.value);
- const initialValues = (this.getInitialValues() ?? []).map((v) => v.value);
+ const initialValues =
+ this.getInitialFilterValues()?.map((v) => v.value) ?? [];
return (
currentValues.length !== initialValues.length ||
@@ -223,7 +214,11 @@ export class Filter, TMatchValue> {
}
private getInitialValues() {
- return this.getStoredDefaultSelectedValues() ?? this.defaultSelectedValues;
+ return this.getStoredSelectedIds() ?? this.defaultSelectedValues;
+ }
+
+ private getInitialFilterValues() {
+ return this.getInitialValues()?.map((v) => FilterValue.create(this, v));
}
public resetValues(): void {
diff --git a/packages/components/src/components/List/model/filter/FilterValue.ts b/packages/components/src/components/List/model/filter/FilterValue.ts
index 3054fac0a..7f88bc23c 100644
--- a/packages/components/src/components/List/model/filter/FilterValue.ts
+++ b/packages/components/src/components/List/model/filter/FilterValue.ts
@@ -5,19 +5,31 @@ import { hash } from "object-code";
export class FilterValue {
public readonly filter: Filter;
public readonly value: unknown;
+ public readonly id: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- public constructor(filter: Filter, value: unknown) {
+ private constructor(filter: Filter, value: unknown) {
this.filter = filter;
- this.value = value;
+
+ if (typeof value === "string" && value.startsWith("FilterValueId@@")) {
+ this.value = filter.values.find((v) => v.id === value)?.value;
+ this.id = value;
+ } else {
+ this.value = value;
+ this.id = `FilterValueId@@${this.filter.property}@@${hash(this.value)}`;
+ }
}
- public equals(otherValue: FilterValue) {
- return isShallowEqual(this.value, otherValue.value);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public static create(filter: Filter, value: unknown) {
+ if (value instanceof FilterValue) {
+ return value;
+ }
+ return new FilterValue(filter, value);
}
- public get id() {
- return `${this.filter.property}@@${hash(this.value)}`;
+ public equals(otherValue: FilterValue) {
+ return isShallowEqual(this.value, otherValue.value);
}
public get isActive() {
diff --git a/packages/components/src/components/List/model/filter/types.ts b/packages/components/src/components/List/model/filter/types.ts
index c47826f25..ee9b5feaa 100644
--- a/packages/components/src/components/List/model/filter/types.ts
+++ b/packages/components/src/components/List/model/filter/types.ts
@@ -19,5 +19,5 @@ export interface FilterShape, TMatcherValue> {
matcher?: FilterMatcher;
values?: readonly TMatcherValue[];
name?: string;
- defaultSelected?: readonly NonNullable>[];
+ defaultSelected?: readonly NonNullable[];
}
diff --git a/packages/components/src/lib/array/toArray.ts b/packages/components/src/lib/array/toArray.ts
new file mode 100644
index 000000000..d07470fac
--- /dev/null
+++ b/packages/components/src/lib/array/toArray.ts
@@ -0,0 +1,2 @@
+export const toArray = (val: T | T[]): T[] =>
+ Array.isArray(val) ? val : [val];