Skip to content

Commit 2726e33

Browse files
✨ feat(filters): implement multi select filters
1 parent fadaaf0 commit 2726e33

File tree

17 files changed

+200
-183
lines changed

17 files changed

+200
-183
lines changed

‎src/components/app/drop-down.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export interface IProps {
1414
export function Dropdown({ target, children, className }: IProps) {
1515
return (
1616
<DropdownMenu>
17-
<DropdownMenuTrigger>{target}</DropdownMenuTrigger>
17+
<DropdownMenuTrigger className="outline-none">
18+
{target}
19+
</DropdownMenuTrigger>
1820
<DropdownMenuContent className={className} align="end">
1921
{children}
2022
</DropdownMenuContent>

‎src/components/app/form/input/select-async.tsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useMemo, useState } from "react";
22
import { useDebounce } from "react-use";
3-
import { ISelectData } from "shared/types/options";
3+
import { ILabelValue } from "shared/types/options";
44
import { useApi } from "frontend/lib/data/useApi";
55
import { useLingui } from "@lingui/react";
66
import { ErrorAlert } from "@/components/app/alert";
7-
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";
87
import { FormSelect } from "./select";
8+
import { IBaseFormSelect } from "./types";
9+
import { transformLabelValueToSelectData } from "@/translations/fake";
910

1011
interface IProps extends IBaseFormSelect {
1112
url: string;
@@ -21,11 +22,11 @@ export function AsyncFormSelect(props: IProps) {
2122
const [search, setSearch] = useState("");
2223
const [debounceSearch, setDebounceSearch] = useState("");
2324

24-
const fullData = useApi<ISelectData[]>(url, {
25+
const fullData = useApi<ILabelValue[]>(url, {
2526
defaultData: [],
2627
});
2728

28-
const selectOptions = useApi<ISelectData[]>(
29+
const selectOptions = useApi<ILabelValue[]>(
2930
debounceSearch ? `${url}?search=${debounceSearch}` : url,
3031
{
3132
defaultData: [],
@@ -34,15 +35,15 @@ export function AsyncFormSelect(props: IProps) {
3435

3536
const currentLabelFromSelection = useMemo(() => {
3637
const isValueInFirstDataLoad = fullData.data.find(
37-
({ value }: ISelectData) => String(value) === String(input.value)
38+
({ value }: ILabelValue) => String(value) === String(input.value)
3839
);
3940

4041
if (isValueInFirstDataLoad) {
4142
return _(isValueInFirstDataLoad.label);
4243
}
4344

4445
const isValueInSelectionOptions = selectOptions.data.find(
45-
({ value }: ISelectData) => String(value) === String(input.value)
46+
({ value }: ILabelValue) => String(value) === String(input.value)
4647
);
4748

4849
if (isValueInSelectionOptions) {
@@ -78,7 +79,7 @@ export function AsyncFormSelect(props: IProps) {
7879
return (
7980
<FormSelect
8081
{...props}
81-
selectData={selectOptions.data}
82+
selectData={transformLabelValueToSelectData(selectOptions.data)}
8283
isLoading={isLoading}
8384
onSearch={{
8485
isLoading: selectOptions.isLoading,
@@ -91,6 +92,10 @@ export function AsyncFormSelect(props: IProps) {
9192
}
9293

9394
return (
94-
<FormSelect {...props} selectData={fullData.data} isLoading={isLoading} />
95+
<FormSelect
96+
{...props}
97+
selectData={transformLabelValueToSelectData(fullData.data)}
98+
isLoading={isLoading}
99+
/>
95100
);
96101
}

‎src/components/app/form/input/select-button.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { VariantProps } from "class-variance-authority";
55
import { Button, buttonVariants } from "@/components/ui/button";
66
import { cn } from "@/lib/utils";
77
import { LabelAndError } from "./label-and-error";
8-
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";
8+
import { IBaseFormSelect } from "./types";
99

1010
interface IFormSelect extends IBaseFormSelect {
1111
selectData: ISelectData[];

‎src/components/app/form/input/select.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
generateClassNames,
99
generateFormArias,
1010
} from "@/components/app/form/input/label-and-error";
11-
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";
11+
import { IBaseFormSelect } from "./types";
1212

1313
interface IFormSelect extends IBaseFormSelect {
1414
selectData: ISelectData[];

‎src/components/app/form/input/types.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ export interface ISharedFormInput extends ILabelAndErrorProps {
1515
placeholder?: MessageDescriptor;
1616
disabled?: boolean;
1717
}
18+
19+
export interface IBaseFormSelect extends ISharedFormInput {
20+
disabledOptions?: string[];
21+
}

‎src/components/app/table/Head.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface IProps {
1818
table: Table<Record<string, unknown>>;
1919
}
2020

21-
export function TableHead({ table }: IProps) {
21+
export function _TableHead({ table }: IProps) {
2222
return (
2323
<TableHeader>
2424
{table.getHeaderGroups().map((headerGroup) => (
@@ -74,3 +74,5 @@ export function TableHead({ table }: IProps) {
7474
</TableHeader>
7575
);
7676
}
77+
78+
export const TableHead = React.memo(_TableHead);
+70-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,85 @@
11
import { IColumnFilterBag } from "shared/types/data";
2+
import { msg } from "@lingui/macro";
3+
import { useDebounce, useSessionStorage } from "react-use";
4+
import { useState } from "react";
25
import { IFilterProps } from "./types";
3-
// import { AsyncFormMultiSelect } from "@/frontend/design-system/components/Form/Select/Async";
6+
import { MultiFilterValues } from "./_MultiFilterValues";
7+
import { Select } from "@/components/ui/select";
8+
import { ILabelValue } from "@/shared/types/options";
9+
import { useApi } from "@/frontend/lib/data/useApi";
10+
import { sluggify } from "@/shared/lib/strings";
11+
import { transformLabelValueToSelectData } from "@/translations/fake";
412

513
export function FilterTableByListSelection({
614
column: { filterValue, setFilter },
7-
bag,
15+
bag: url,
816
}: IFilterProps<IColumnFilterBag<string[]>, string>) {
17+
const values = filterValue?.value || [];
18+
19+
const [cosmeticValues, setCosmeticValues] = useSessionStorage<ILabelValue[]>(
20+
`cosmetic-multi-select-values-${sluggify(url)}`,
21+
[]
22+
);
23+
24+
const [search, setSearch] = useState("");
25+
26+
const [debounceSearch, setDebounceSearch] = useState("");
27+
28+
const selectOptions = useApi<ILabelValue[]>(
29+
debounceSearch ? `${url}?search=${debounceSearch}` : url,
30+
{
31+
defaultData: [],
32+
}
33+
);
34+
35+
const appendCosmeticValues = (value: string) => {
36+
setCosmeticValues([
37+
...cosmeticValues,
38+
{
39+
value,
40+
label:
41+
selectOptions.data.find(
42+
(option) => String(option.value) === String(value)
43+
)?.label || value,
44+
},
45+
]);
46+
};
47+
48+
useDebounce(
49+
() => {
50+
setDebounceSearch(search);
51+
},
52+
700,
53+
[search]
54+
);
55+
956
return (
10-
<div className="min-w-64">
11-
<div>TODO</div>
12-
{/* <AsyncFormMultiSelect
13-
url={bag}
14-
values={filterValue?.value || []}
57+
<div>
58+
<MultiFilterValues
59+
filterValue={filterValue}
60+
setFilter={setFilter}
61+
values={values}
62+
options={cosmeticValues}
63+
/>
64+
<Select
1565
onChange={(value) => {
66+
appendCosmeticValues(value);
1667
setFilter({
1768
...filterValue,
18-
value,
69+
value: [...new Set([...values, value])],
1970
});
2071
}}
21-
/> */}
72+
name="select-filter"
73+
value=""
74+
options={transformLabelValueToSelectData(selectOptions.data)}
75+
onSearch={{
76+
isLoading: selectOptions.isLoading,
77+
onChange: setSearch,
78+
value: search,
79+
}}
80+
disabledOptions={values}
81+
placeholder={msg`Select Option`}
82+
/>
2283
</div>
2384
);
2485
}
+24-9
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
import { IColumnFilterBag } from "shared/types/data";
22
import { ISelectData } from "shared/types/options";
3+
import { msg } from "@lingui/macro";
4+
import { useLingui } from "@lingui/react";
35
import { IFilterProps } from "./types";
4-
// import { FormMultiSelect } from "@/frontend/design-system/components/Form/Select";
6+
import { Select } from "@/components/ui/select";
7+
import { MultiFilterValues } from "./_MultiFilterValues";
58

69
export function FilterTableByStatus({
710
column: { filterValue, setFilter },
811
bag,
912
}: IFilterProps<IColumnFilterBag<string[]>, ISelectData[]>) {
13+
const values = filterValue?.value || [];
14+
const { _ } = useLingui();
1015
return (
11-
<div className="min-w-64">
12-
<div>TODO</div>
13-
{/* <FormMultiSelect
14-
selectData={bag}
15-
values={filterValue?.value || []}
16-
ariaLabel="Select Status"
16+
<div>
17+
<MultiFilterValues
18+
filterValue={filterValue}
19+
setFilter={setFilter}
20+
values={values}
21+
options={bag.map(({ label, value }) => ({
22+
value: String(value),
23+
label: _(label),
24+
}))}
25+
/>
26+
<Select
1727
onChange={(value) => {
1828
setFilter({
1929
...filterValue,
20-
value,
30+
value: [...new Set([...values, value])],
2131
});
2232
}}
23-
/> */}
33+
name="select-filter"
34+
value=""
35+
options={bag}
36+
disabledOptions={values}
37+
placeholder={msg`Select Option`}
38+
/>
2439
</div>
2540
);
2641
}

‎src/components/app/table/filters/_FilterWrapper.tsx

+7-11
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,16 @@ export function FilterWrapper({
4444
<Dropdown
4545
className="w-64"
4646
target={
47-
<div
48-
className="cursor-pointer"
47+
<SystemIcon
48+
icon={systemIcon}
4949
aria-label={`Filter ${columnLabel} By ${filterLabel}${
5050
filterHasValue ? " Is Active" : ""
5151
}`}
52-
>
53-
<SystemIcon
54-
icon={systemIcon}
55-
className={cn("w-4 h-4 text-muted", {
56-
"text-primary": filterHasValue,
57-
"opacity-70": !filterHasValue,
58-
})}
59-
/>
60-
</div>
52+
className={cn("w-4 h-4 text-muted align-text-top", {
53+
"text-primary": filterHasValue,
54+
"opacity-70": !filterHasValue,
55+
})}
56+
/>
6157
}
6258
>
6359
<div className="flex flex-col gap-3 p-2">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { X } from "react-feather";
2+
import { Button, buttonVariants } from "@/components/ui/button";
3+
import { cn } from "@/lib/utils";
4+
import { IColumnFilterBag } from "@/shared/types/data";
5+
6+
interface IProps {
7+
values: string[];
8+
filterValue: IColumnFilterBag<string[]>;
9+
setFilter: (value: IColumnFilterBag<string[]>) => void;
10+
options: { value: string; label: string }[];
11+
}
12+
13+
export function MultiFilterValues({
14+
values,
15+
filterValue,
16+
setFilter,
17+
options,
18+
}: IProps) {
19+
return (
20+
<>
21+
{values.map((value) => {
22+
const label = options.find(
23+
(option) => String(option.value) === String(value)
24+
)?.label;
25+
return (
26+
<div key={value} className="inline-flex mb-1 mr-1">
27+
<div
28+
className={cn(
29+
buttonVariants({ variant: "soft", size: "sm" }),
30+
"rounded-r-none hover:bg-primary-alpha hover:text-primary-alpha-text"
31+
)}
32+
>
33+
{label || value}
34+
</div>
35+
<Button
36+
className="rounded-l-none px-1"
37+
size="sm"
38+
variant="destructive"
39+
onClick={() => {
40+
setFilter({
41+
...filterValue,
42+
value: values.filter((option) => option !== value),
43+
});
44+
}}
45+
>
46+
<X size={14} />
47+
</Button>
48+
</div>
49+
);
50+
})}
51+
</>
52+
);
53+
}

‎src/components/app/table/index.tsx

+1-6
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,7 @@ export function Table<T extends unknown>({
9595

9696
return (
9797
<div className="w-full">
98-
<ScrollArea
99-
orientation="horizontal"
100-
className={cn("relative bg-base", {
101-
"min-h-[500px]": dataLength > 0 && !lean,
102-
})}
103-
>
98+
<ScrollArea orientation="horizontal" className={cn("relative bg-base")}>
10499
{previousDataRender}
105100
<TableRoot
106101
className={cn("w-full text-main border-collapse", {

0 commit comments

Comments
 (0)