Skip to content

Commit

Permalink
✨ feat(filters): implement multi select filters
Browse files Browse the repository at this point in the history
  • Loading branch information
thrownullexception committed Jul 2, 2024
1 parent fadaaf0 commit 2726e33
Show file tree
Hide file tree
Showing 17 changed files with 200 additions and 183 deletions.
4 changes: 3 additions & 1 deletion src/components/app/drop-down.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export interface IProps {
export function Dropdown({ target, children, className }: IProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger>{target}</DropdownMenuTrigger>
<DropdownMenuTrigger className="outline-none">
{target}
</DropdownMenuTrigger>
<DropdownMenuContent className={className} align="end">
{children}
</DropdownMenuContent>
Expand Down
21 changes: 13 additions & 8 deletions src/components/app/form/input/select-async.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useMemo, useState } from "react";
import { useDebounce } from "react-use";
import { ISelectData } from "shared/types/options";
import { ILabelValue } from "shared/types/options";
import { useApi } from "frontend/lib/data/useApi";
import { useLingui } from "@lingui/react";
import { ErrorAlert } from "@/components/app/alert";
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";
import { FormSelect } from "./select";
import { IBaseFormSelect } from "./types";
import { transformLabelValueToSelectData } from "@/translations/fake";

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

const fullData = useApi<ISelectData[]>(url, {
const fullData = useApi<ILabelValue[]>(url, {
defaultData: [],
});

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

const currentLabelFromSelection = useMemo(() => {
const isValueInFirstDataLoad = fullData.data.find(
({ value }: ISelectData) => String(value) === String(input.value)
({ value }: ILabelValue) => String(value) === String(input.value)
);

if (isValueInFirstDataLoad) {
return _(isValueInFirstDataLoad.label);
}

const isValueInSelectionOptions = selectOptions.data.find(
({ value }: ISelectData) => String(value) === String(input.value)
({ value }: ILabelValue) => String(value) === String(input.value)
);

if (isValueInSelectionOptions) {
Expand Down Expand Up @@ -78,7 +79,7 @@ export function AsyncFormSelect(props: IProps) {
return (
<FormSelect
{...props}
selectData={selectOptions.data}
selectData={transformLabelValueToSelectData(selectOptions.data)}
isLoading={isLoading}
onSearch={{
isLoading: selectOptions.isLoading,
Expand All @@ -91,6 +92,10 @@ export function AsyncFormSelect(props: IProps) {
}

return (
<FormSelect {...props} selectData={fullData.data} isLoading={isLoading} />
<FormSelect
{...props}
selectData={transformLabelValueToSelectData(fullData.data)}
isLoading={isLoading}
/>
);
}
2 changes: 1 addition & 1 deletion src/components/app/form/input/select-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { VariantProps } from "class-variance-authority";
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { LabelAndError } from "./label-and-error";
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";
import { IBaseFormSelect } from "./types";

interface IFormSelect extends IBaseFormSelect {
selectData: ISelectData[];
Expand Down
2 changes: 1 addition & 1 deletion src/components/app/form/input/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
generateClassNames,
generateFormArias,
} from "@/components/app/form/input/label-and-error";
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";
import { IBaseFormSelect } from "./types";

interface IFormSelect extends IBaseFormSelect {
selectData: ISelectData[];
Expand Down
4 changes: 4 additions & 0 deletions src/components/app/form/input/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export interface ISharedFormInput extends ILabelAndErrorProps {
placeholder?: MessageDescriptor;
disabled?: boolean;
}

export interface IBaseFormSelect extends ISharedFormInput {
disabledOptions?: string[];
}
4 changes: 3 additions & 1 deletion src/components/app/table/Head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface IProps {
table: Table<Record<string, unknown>>;
}

export function TableHead({ table }: IProps) {
export function _TableHead({ table }: IProps) {
return (
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
Expand Down Expand Up @@ -74,3 +74,5 @@ export function TableHead({ table }: IProps) {
</TableHeader>
);
}

export const TableHead = React.memo(_TableHead);
79 changes: 70 additions & 9 deletions src/components/app/table/filters/List.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,85 @@
import { IColumnFilterBag } from "shared/types/data";
import { msg } from "@lingui/macro";
import { useDebounce, useSessionStorage } from "react-use";
import { useState } from "react";
import { IFilterProps } from "./types";
// import { AsyncFormMultiSelect } from "@/frontend/design-system/components/Form/Select/Async";
import { MultiFilterValues } from "./_MultiFilterValues";
import { Select } from "@/components/ui/select";
import { ILabelValue } from "@/shared/types/options";
import { useApi } from "@/frontend/lib/data/useApi";
import { sluggify } from "@/shared/lib/strings";
import { transformLabelValueToSelectData } from "@/translations/fake";

export function FilterTableByListSelection({
column: { filterValue, setFilter },
bag,
bag: url,
}: IFilterProps<IColumnFilterBag<string[]>, string>) {
const values = filterValue?.value || [];

const [cosmeticValues, setCosmeticValues] = useSessionStorage<ILabelValue[]>(
`cosmetic-multi-select-values-${sluggify(url)}`,
[]
);

const [search, setSearch] = useState("");

const [debounceSearch, setDebounceSearch] = useState("");

const selectOptions = useApi<ILabelValue[]>(
debounceSearch ? `${url}?search=${debounceSearch}` : url,
{
defaultData: [],
}
);

const appendCosmeticValues = (value: string) => {
setCosmeticValues([
...cosmeticValues,
{
value,
label:
selectOptions.data.find(
(option) => String(option.value) === String(value)
)?.label || value,
},
]);
};

useDebounce(
() => {
setDebounceSearch(search);
},
700,
[search]
);

return (
<div className="min-w-64">
<div>TODO</div>
{/* <AsyncFormMultiSelect
url={bag}
values={filterValue?.value || []}
<div>
<MultiFilterValues
filterValue={filterValue}
setFilter={setFilter}
values={values}
options={cosmeticValues}
/>
<Select
onChange={(value) => {
appendCosmeticValues(value);
setFilter({
...filterValue,
value,
value: [...new Set([...values, value])],
});
}}
/> */}
name="select-filter"
value=""
options={transformLabelValueToSelectData(selectOptions.data)}
onSearch={{
isLoading: selectOptions.isLoading,
onChange: setSearch,
value: search,
}}
disabledOptions={values}
placeholder={msg`Select Option`}
/>
</div>
);
}
33 changes: 24 additions & 9 deletions src/components/app/table/filters/Status.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
import { IColumnFilterBag } from "shared/types/data";
import { ISelectData } from "shared/types/options";
import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import { IFilterProps } from "./types";
// import { FormMultiSelect } from "@/frontend/design-system/components/Form/Select";
import { Select } from "@/components/ui/select";
import { MultiFilterValues } from "./_MultiFilterValues";

export function FilterTableByStatus({
column: { filterValue, setFilter },
bag,
}: IFilterProps<IColumnFilterBag<string[]>, ISelectData[]>) {
const values = filterValue?.value || [];
const { _ } = useLingui();
return (
<div className="min-w-64">
<div>TODO</div>
{/* <FormMultiSelect
selectData={bag}
values={filterValue?.value || []}
ariaLabel="Select Status"
<div>
<MultiFilterValues
filterValue={filterValue}
setFilter={setFilter}
values={values}
options={bag.map(({ label, value }) => ({
value: String(value),
label: _(label),
}))}
/>
<Select
onChange={(value) => {
setFilter({
...filterValue,
value,
value: [...new Set([...values, value])],
});
}}
/> */}
name="select-filter"
value=""
options={bag}
disabledOptions={values}
placeholder={msg`Select Option`}
/>
</div>
);
}
18 changes: 7 additions & 11 deletions src/components/app/table/filters/_FilterWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,16 @@ export function FilterWrapper({
<Dropdown
className="w-64"
target={
<div
className="cursor-pointer"
<SystemIcon
icon={systemIcon}
aria-label={`Filter ${columnLabel} By ${filterLabel}${
filterHasValue ? " Is Active" : ""
}`}
>
<SystemIcon
icon={systemIcon}
className={cn("w-4 h-4 text-muted", {
"text-primary": filterHasValue,
"opacity-70": !filterHasValue,
})}
/>
</div>
className={cn("w-4 h-4 text-muted align-text-top", {
"text-primary": filterHasValue,
"opacity-70": !filterHasValue,
})}
/>
}
>
<div className="flex flex-col gap-3 p-2">
Expand Down
53 changes: 53 additions & 0 deletions src/components/app/table/filters/_MultiFilterValues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { X } from "react-feather";
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { IColumnFilterBag } from "@/shared/types/data";

interface IProps {
values: string[];
filterValue: IColumnFilterBag<string[]>;
setFilter: (value: IColumnFilterBag<string[]>) => void;
options: { value: string; label: string }[];
}

export function MultiFilterValues({
values,
filterValue,
setFilter,
options,
}: IProps) {
return (
<>
{values.map((value) => {
const label = options.find(
(option) => String(option.value) === String(value)
)?.label;
return (
<div key={value} className="inline-flex mb-1 mr-1">
<div
className={cn(
buttonVariants({ variant: "soft", size: "sm" }),
"rounded-r-none hover:bg-primary-alpha hover:text-primary-alpha-text"
)}
>
{label || value}
</div>
<Button
className="rounded-l-none px-1"
size="sm"
variant="destructive"
onClick={() => {
setFilter({
...filterValue,
value: values.filter((option) => option !== value),
});
}}
>
<X size={14} />
</Button>
</div>
);
})}
</>
);
}
7 changes: 1 addition & 6 deletions src/components/app/table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,7 @@ export function Table<T extends unknown>({

return (
<div className="w-full">
<ScrollArea
orientation="horizontal"
className={cn("relative bg-base", {
"min-h-[500px]": dataLength > 0 && !lean,
})}
>
<ScrollArea orientation="horizontal" className={cn("relative bg-base")}>
{previousDataRender}
<TableRoot
className={cn("w-full text-main border-collapse", {
Expand Down
Loading

0 comments on commit 2726e33

Please sign in to comment.