Skip to content

Commit 7073586

Browse files
committed
FE: Fix table filter module name
1 parent 10dab80 commit 7073586

File tree

19 files changed

+432
-0
lines changed

19 files changed

+432
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import { Column } from '@tanstack/react-table';
3+
4+
import * as FilterVariant from './variants';
5+
6+
interface FilterProps<T> {
7+
column: Column<T, unknown>;
8+
}
9+
export const Filter = <T,>(props: FilterProps<T>) => {
10+
const { column } = props;
11+
12+
switch (column.columnDef.meta?.filterVariant) {
13+
case 'multi-select': {
14+
return <FilterVariant.MultiSelect column={column} />;
15+
}
16+
default: {
17+
throw Error('Not implemented filter');
18+
}
19+
}
20+
};
21+
22+
export default Filter;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Filter from './Filter';
2+
3+
export { type FilterableColumnDef, type KafbatFilterVariant } from './types';
4+
export { type Persister, useQueryPersister } from './lib';
5+
export { getFilterableColumns } from './lib/getFilterableColumns';
6+
export default Filter;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ColumnDef } from '@tanstack/react-table';
2+
import { FilterableColumnDef } from 'components/common/NewTable/Filter/types';
3+
4+
export function isFilterableColumn<TData, TValue>(
5+
column: ColumnDef<TData, TValue>
6+
): column is FilterableColumnDef<TData, TValue> {
7+
return (
8+
'accessorKey' in column &&
9+
typeof column.accessorKey === 'string' &&
10+
!!column.meta &&
11+
'filterVariant' in column.meta
12+
);
13+
}
14+
15+
export function getFilterableColumns<TData, TValue>(
16+
columns: ColumnDef<TData, TValue>[]
17+
): FilterableColumnDef<TData, TValue>[] {
18+
return columns.filter(isFilterableColumn);
19+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export {
2+
getFilterableColumns,
3+
isFilterableColumn,
4+
} from './getFilterableColumns';
5+
6+
export { type Persister, useQueryPersister } from './persisters/index';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { type Persister } from './types';
2+
3+
export { useQueryPersister } from './queryPersister';
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {
2+
ColumnDef,
3+
ColumnFilter,
4+
ColumnFiltersState,
5+
} from '@tanstack/react-table';
6+
import { useSearchParams } from 'react-router-dom';
7+
import {
8+
FilterableColumnDef,
9+
KafbatFilterVariant,
10+
getFilterableColumns,
11+
} from 'components/common/NewTable/Filter';
12+
import { useCallback, useMemo } from 'react';
13+
14+
import { Persister } from './types';
15+
16+
function getParamsByKeys(
17+
params: URLSearchParams,
18+
keyToFilterVariant: {
19+
[k: string]: KafbatFilterVariant;
20+
}
21+
) {
22+
return Object.entries(keyToFilterVariant).reduce(
23+
(acc, [key, variant]) => {
24+
const foundValue = params.get(key);
25+
if (foundValue) {
26+
if (variant === 'multi-select') {
27+
// Array stored to query params as string, we should recover it back to array of values
28+
acc[key] = foundValue.split(',');
29+
} else {
30+
acc[key] = foundValue;
31+
}
32+
}
33+
return acc;
34+
},
35+
{} as Record<string, string | string[]>
36+
);
37+
}
38+
// By default tanstack table replace all . in accessrorKey by _
39+
// We should normalize out filterable columns key accordingly
40+
const normalizeAccessorKey = (accessorKey: string) =>
41+
accessorKey.replace(/\./g, '_');
42+
43+
function mapColumnKeyToFilterVariant<TData, TValue>(
44+
columns: FilterableColumnDef<TData, TValue>[]
45+
): {
46+
[k: string]: KafbatFilterVariant;
47+
} {
48+
return columns.reduce(
49+
(acc, cur) => {
50+
if (cur.meta?.filterVariant) {
51+
const key = normalizeAccessorKey(cur.accessorKey);
52+
acc[key] = cur.meta?.filterVariant;
53+
}
54+
55+
return acc;
56+
},
57+
{} as Record<string, KafbatFilterVariant>
58+
);
59+
}
60+
61+
function isEmptyFilterValue(columnFilter: ColumnFilter): boolean {
62+
if (Array.isArray(columnFilter.value) && columnFilter.value.length === 0) {
63+
return true;
64+
}
65+
66+
return columnFilter.value === undefined;
67+
}
68+
69+
export function useQueryPersister<TData, TValue>(
70+
columns: ColumnDef<TData, TValue>[]
71+
): Persister {
72+
const [searchParams, setSearchParams] = useSearchParams();
73+
74+
const keyToFilterVariant = useMemo(() => {
75+
const filterableColumns = getFilterableColumns(columns);
76+
77+
return mapColumnKeyToFilterVariant(filterableColumns);
78+
}, [columns]);
79+
80+
const getPrevState = useCallback(() => {
81+
const filterParams = getParamsByKeys(searchParams, keyToFilterVariant);
82+
const prevState: ColumnFiltersState = Object.entries(filterParams).map(
83+
([id, value]) => {
84+
return { id, value };
85+
}
86+
);
87+
return prevState;
88+
}, [searchParams, keyToFilterVariant]);
89+
90+
const update: Persister['update'] = useCallback(
91+
(nextState: ColumnFiltersState, resetPage: boolean = true) => {
92+
const prevState: ColumnFiltersState = getPrevState();
93+
94+
const nextKeys = new Set();
95+
nextState.forEach((columnFilter) => {
96+
if (!isEmptyFilterValue(columnFilter)) {
97+
nextKeys.add(columnFilter.id);
98+
searchParams.set(columnFilter.id, String(columnFilter.value));
99+
}
100+
});
101+
102+
prevState
103+
.map(({ id }) => id)
104+
.forEach((key) => {
105+
if (!nextKeys.has(key)) {
106+
searchParams.delete(key);
107+
}
108+
});
109+
110+
if (resetPage) {
111+
searchParams.delete('page');
112+
}
113+
114+
setSearchParams(searchParams);
115+
},
116+
[getPrevState, searchParams]
117+
);
118+
119+
return useMemo(
120+
() => ({
121+
getPrevState,
122+
update,
123+
}),
124+
[getPrevState, update]
125+
);
126+
}
127+
export default useQueryPersister;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ColumnFiltersState } from '@tanstack/react-table';
2+
3+
export interface Persister {
4+
getPrevState: () => ColumnFiltersState;
5+
update: (nextState: ColumnFiltersState, resetPagination?: boolean) => void;
6+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {
2+
ColumnDef,
3+
ColumnMeta,
4+
FilterFnOption,
5+
RowData,
6+
} from '@tanstack/react-table';
7+
import { FullConnectorInfo } from 'generated-sources';
8+
9+
export type KafbatFilterVariant<T = RowData> = ColumnMeta<
10+
T,
11+
unknown
12+
>['filterVariant'];
13+
14+
export type FilterableColumnDef<TData = RowData, TValue = unknown> = ColumnDef<
15+
TData,
16+
TValue
17+
> &
18+
Required<{
19+
filterFn: FilterFnOption<FullConnectorInfo>;
20+
accessorKey: string;
21+
meta: ColumnMeta<TData, TValue> & {
22+
filterVariant: KafbatFilterVariant;
23+
};
24+
}>;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import styled from 'styled-components';
2+
import { MultiSelect as ReactMultiSelect } from 'react-multi-select-component';
3+
4+
export const MultiSelect = styled(ReactMultiSelect)<{
5+
minWidth?: string;
6+
height?: string;
7+
}>`
8+
font-size: 14px;
9+
padding-right: 12px;
10+
.search input {
11+
color: ${({ theme }) => theme.input.color.normal};
12+
background-color: ${(props) =>
13+
props.theme.input.backgroundColor.normal} !important;
14+
}
15+
.select-item {
16+
color: ${({ theme }) => theme.select.color.normal};
17+
background-color: ${({ theme }) =>
18+
theme.select.backgroundColor.normal} !important;
19+
20+
&:active {
21+
background-color: ${({ theme }) =>
22+
theme.select.backgroundColor.active} !important;
23+
}
24+
}
25+
.select-item.selected {
26+
background-color: ${({ theme }) =>
27+
theme.select.backgroundColor.active} !important;
28+
}
29+
.options li {
30+
background-color: ${({ theme }) =>
31+
theme.select.backgroundColor.normal} !important;
32+
}
33+
34+
& > .dropdown-container {
35+
border: none !important;
36+
background-color: ${({ theme }) =>
37+
theme.input.backgroundColor.normal} !important;
38+
border-color: ${({ theme }) => theme.select.borderColor.normal} !important;
39+
&:hover {
40+
border-color: ${({ theme }) => theme.select.borderColor.hover} !important;
41+
}
42+
43+
height: ${({ height }) => height ?? '32px'};
44+
* {
45+
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
46+
}
47+
48+
& > .dropdown-content {
49+
width: fit-content;
50+
min-width: 120px;
51+
right: 0px;
52+
}
53+
54+
& > .dropdown-heading {
55+
height: ${({ height }) => height ?? '32px'};
56+
color: ${({ disabled, theme }) =>
57+
disabled
58+
? theme.select.color.disabled
59+
: theme.select.color.active} !important;
60+
& > .dropdown-heading-value {
61+
color: ${({ theme }) => theme.table.filter.multiSelect.value.color};
62+
}
63+
}
64+
}
65+
66+
& .clear-selected-button + div {
67+
color: ${({ theme }) =>
68+
theme.table.filter.multiSelect.filterIcon.fill.active};
69+
}
70+
`;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { useCallback, useMemo, useState } from 'react';
2+
import { Column } from '@tanstack/react-table';
3+
4+
import * as S from './MultiSelect.styled';
5+
import { type Option } from './types';
6+
import {
7+
toOption,
8+
getOptionValue,
9+
customValueRenderer,
10+
sortOptionSelectedFirst,
11+
} from './lib';
12+
import FilterIcon from './ui/FilterIcon';
13+
import ClearIcon from './ui/ClearIcon';
14+
15+
interface Props<T, K = string> {
16+
column: Column<T, K>;
17+
}
18+
19+
function MultiSelect<T, K = string>(props: Props<T, K>) {
20+
const { column } = props;
21+
22+
const [selectedOptions, setValues] = useState<Option[]>(() => {
23+
const value = column.getFilterValue() as string[] | undefined;
24+
25+
if (value) {
26+
return value.map(toOption);
27+
}
28+
29+
return [];
30+
});
31+
32+
const allValues = column.getFacetedUniqueValues();
33+
const sortedOptions = useMemo(() => {
34+
const allColumnValues = [...new Set([...allValues.keys()].flat())];
35+
const allOptions = allColumnValues.map(toOption);
36+
return sortOptionSelectedFirst(selectedOptions, allOptions);
37+
}, [allValues]);
38+
39+
const onSelect = useCallback((options: Option[]) => {
40+
column.setFilterValue(options.map(getOptionValue));
41+
setValues(options);
42+
}, []);
43+
44+
return (
45+
<S.MultiSelect
46+
options={sortedOptions}
47+
value={selectedOptions}
48+
onChange={onSelect}
49+
labelledBy=""
50+
valueRenderer={customValueRenderer}
51+
ArrowRenderer={FilterIcon}
52+
hasSelectAll
53+
ClearSelectedIcon={<ClearIcon />}
54+
/>
55+
);
56+
}
57+
58+
export default MultiSelect;

0 commit comments

Comments
 (0)