Skip to content

Commit

Permalink
feat(table): support sorter and filter controll (#1016)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lee Hon authored May 28, 2021
1 parent b4bdfe1 commit bf9c6e2
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 85 deletions.
7 changes: 4 additions & 3 deletions src/components/table/FilterPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,19 @@ interface FilterPopoverProps {
onClick: (newFilterState: string[]) => void;
filters?: filterType[];
values: string[];
placeholder?: string;
}

const FilterPopover = (props: FilterPopoverProps): React.ReactElement => {
const { children, onClick, filters = [], values, prefixCls } = props;
const { children, onClick, filters = [], values, prefixCls, placeholder = '搜索过滤条件' } = props;
const [seachValue, setSearchValue] = useState<string>('');
const [selectFilterKeys, setSelectFilterKeys] = useState<string[]>(values);
const [visible, setVisible] = useState<boolean>(false);
const { tableRef } = useContext(TableContext);

useEffect(() => {
setSelectFilterKeys(values);
}, [values]);
}, [values, visible]);

return (
<Popover
Expand All @@ -45,7 +46,7 @@ const FilterPopover = (props: FilterPopoverProps): React.ReactElement => {
overlayClassName={`${prefixCls}-filter-popover`}
contentArea={
<>
<SearchBar placeholder="搜索过滤条件" size="small" value={seachValue} onChange={setSearchValue} />
<SearchBar placeholder={placeholder} size="small" value={seachValue} onChange={setSearchValue} />
<FilterList
prefixCls={prefixCls}
value={selectFilterKeys}
Expand Down
3 changes: 2 additions & 1 deletion src/components/table/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TableScroll } from './demo/TableScroll.stories';
import { TableEmpty } from './demo/TableEmpty.stories';
import { TableLoading } from './demo/TableLoading.stories';
import { TablePagination } from './demo/TablePagination.stories';
import { ControlledTable } from './demo/TableControlled.stories'

export default {
title: 'Functional Components/Table',
Expand All @@ -20,4 +21,4 @@ export default {
},
} as Meta;

export { Base, TableHeader, MultiLine, TableScroll, TableEmpty, TableLoading, TablePagination };
export { Base, TableHeader, MultiLine, TableScroll, TableEmpty, TableLoading, TablePagination, ControlledTable };
15 changes: 9 additions & 6 deletions src/components/table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import usePagination from './hook/usePagination';
import useSelection, { getRowKey } from './hook/useSelection';
import useEllipsisTooltip from './hook/useEllipsisTooltip';
import Title from './Title';
import { TableProps, ColumnsType } from './interface';
import { TableProps, ColumnsType, OnTriggerStateUpdateProps } from './interface';
import Empty from '../empty';
import { translateInnerColumns } from './utils';
import Loading from '../loading';
Expand Down Expand Up @@ -53,8 +53,8 @@ function Table <RecordType>(
const debounceLoading = useDebounceLoading(loading, 1000);
const onHackRow = useHackOnRow(onRow, hackRowEvent);
const innerColumns = useMemo(() => translateInnerColumns(columns), [columns]);
const [activeSorterStates, updateSorterStates, sortedData] = useSorter(innerColumns, dataSource);
const [activeFilterStates, updateFilterStates, filtedData] = useFilter(innerColumns, sortedData);
const [activeSorterStates, updateSorterStates, sortedData, sorter] = useSorter(innerColumns, dataSource);
const [activeFilterStates, updateFilterStates, filtedData, filters] = useFilter(innerColumns, sortedData);
const [
transformShowIndexPipeline,
activePaginationedState,
Expand All @@ -67,9 +67,12 @@ function Table <RecordType>(
});
const [transformEllipsisTooltipPipeline] = useEllipsisTooltip();

const onTriggerStateUpdate = (paginationState = activePaginationedState): void => {
// 分页状态更新的时候,通过 activePaginationedState 拿不到最新的状态。
onChange?.(paginationState, activeSorterStates, activeFilterStates);
const onTriggerStateUpdate = ({
paginationState = activePaginationedState,
sorterState = sorter,
filterStates = filters
}: OnTriggerStateUpdateProps<RecordType>): void => {
onChange?.(paginationState, filterStates, sorterState);
};

const renderTitle = (_columns: ColumnsType<RecordType>): ColumnsType<RecordType> =>
Expand Down
12 changes: 5 additions & 7 deletions src/components/table/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ const Title = <RecordType,>(props: TitleProps<RecordType>): React.ReactElement =
const { sortOrder: sorterOrder } = sorterState;

const handleSorterChange = (): void => {
updateSorterStates({
...sorterState,
sortOrder: getNextSortDirection(sortDirections, sorterOrder),
});
onTriggerStateUpdate();
const changedSorterState = { ...sorterState, sortOrder: getNextSortDirection(sortDirections, sorterOrder) };
onTriggerStateUpdate({ sorterState: updateSorterStates(changedSorterState) });
};
return (
<span className={classNames(`${prefixCls}-column-sorter`)}>
Expand Down Expand Up @@ -59,13 +56,13 @@ const Title = <RecordType,>(props: TitleProps<RecordType>): React.ReactElement =

const renderFilter = (): React.ReactNode => {
const { filterState, updateFilterStates } = props;
const { filterSearchPlaceHolder } = column;
if (isUndefined(filterState)) {
return null;
}
const { filteredKeys, filters } = filterState;
const handleFilterPopoverClick = (newFilteredKeys: string[]): void => {
updateFilterStates({ ...filterState, filteredKeys: newFilteredKeys });
onTriggerStateUpdate();
onTriggerStateUpdate({ filterStates: updateFilterStates({ ...filterState, filteredKeys: newFilteredKeys }) });
};

return (
Expand All @@ -76,6 +73,7 @@ const Title = <RecordType,>(props: TitleProps<RecordType>): React.ReactElement =
onClick={handleFilterPopoverClick}
filters={filters}
values={filteredKeys}
placeholder={filterSearchPlaceHolder}
>
<Button
type="text"
Expand Down
2 changes: 1 addition & 1 deletion src/components/table/__test__/Table.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('Testing Table', () => {
);
expect(wrapper.exists('.gio-table-pagination')).toBe(true);
wrapper.find('.gio-pagination-item').at(1).simulate('click');
expect(onChange).toBeCalledWith({ current: 2, pageSize: 10 }, [], []);
expect(onChange).toBeCalledWith({ current: 2, pageSize: 10 }, {}, undefined);
});

it('should be render rightly', () => {
Expand Down
76 changes: 48 additions & 28 deletions src/components/table/__test__/TableSorter.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-shadow */
import { renderHook, act } from '@testing-library/react-hooks';
import { isEqual, cloneDeep } from 'lodash';
import { cloneDeep } from 'lodash';
import useSorter, { collectSortStates } from '../hook/useSorter';
import { getNextSortDirection } from '../Title';

const columns = [
const dataColumns = [
{
title: '姓名',
dataIndex: 'name',
Expand Down Expand Up @@ -32,7 +32,7 @@ const columns = [
},
];

const dataSource = [
const dataDataSource = [
{
key: '1',
name: '胡彦斌',
Expand All @@ -59,55 +59,75 @@ const dataSource = [
},
];

const dataControlledColumns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
sorter: true,
sortOrder: 'descend'
}
];

describe('Testing Table Sorter', () => {
test('collectSortStates function', () => {
const sortStates = collectSortStates(columns);
const sortStates = collectSortStates(dataColumns);
expect(sortStates.length).toBe(3);
expect(sortStates[0].key).toBe(columns[0].key);
expect(sortStates[0].key).toBe(dataColumns[0].key);
expect(sortStates[2].sortPriorityOrder).toBe(2);
});

test('useSorter hook', () => {
const { result } = renderHook(({ columns, dataSource }) => useSorter(columns, dataSource), {
initialProps: { columns, dataSource },
initialProps: { columns: dataColumns, dataSource: dataDataSource },
});
const [sortStates, updateSorterStates, sortedData] = result.current;
const sorterState0 = sortStates[0];
act(() => {
updateSorterStates({
...sorterState0,
sortOrder: getNextSortDirection(sorterState0.sortDirections, sorterState0.sorterOrder),
});
updateSorterStates({
...sorterState0,
sortOrder: getNextSortDirection(sorterState0.sortDirections, sorterState0.sorterOrder),
});
expect(isEqual(result.current[2], sortedData)).toBe(false);
expect(result.current[2]).not.toStrictEqual(sortedData);
const sorterState2 = sortStates[2];
act(() => {
updateSorterStates({
...sorterState2,
sortOrder: getNextSortDirection(sorterState2.sortDirections, sorterState2.sorterOrder),
});
updateSorterStates({
...sorterState2,
sortOrder: getNextSortDirection(sorterState2.sortDirections, sorterState2.sorterOrder),
});
expect(isEqual(result.current[2], sortedData)).toBe(false);
expect(result.current[2]).not.toStrictEqual(sortedData);
expect(result.current[0][0].sortOrder).toBe(null);
expect(result.current[0][2].sortOrder).toBe('ascend');
});

it('should re-collect states, after columns update', () => {
const { result, rerender } = renderHook(({ columns, dataSource }) => useSorter(columns, dataSource), {
initialProps: { columns, dataSource },
initialProps: { columns: dataColumns, dataSource: dataDataSource },
});
const [oldSortStates] = result.current;
act(() => {
rerender({
columns: cloneDeep(columns).map((column) => {
column.key = `#${ column.key}`;
return column;
}),
dataSource,
});
rerender({
columns: cloneDeep(dataColumns).map((column) => {
column.key = `#${ column.key}`;
return column;
}),
dataDataSource,
});

const [newSortStates] = result.current;
expect(isEqual(oldSortStates, newSortStates)).toBe(false);
expect(oldSortStates).not.toStrictEqual(newSortStates);
});

test('controlled sorter', () => {
const { result } = renderHook(({ columns, dataSource }) => useSorter(columns, dataSource), {
initialProps: { columns: dataControlledColumns, dataSource: dataDataSource },
});
const [sortStates, updateSorterStates, sortedData] = result.current;
// sorter: true 前端不排序
expect(sortedData).toStrictEqual(dataDataSource);
const sorterState0 = sortStates[0];
updateSorterStates({
...sorterState0,
sortOrder: getNextSortDirection(sorterState0.sortDirections, sorterState0.sorterOrder),
});
// 受控时排序字段不改变
expect(result.current[0][0]).toStrictEqual(sorterState0);
});
});
71 changes: 71 additions & 0 deletions src/components/table/demo/TableControlled.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useState } from 'react';
import { SortOrder } from '../interface';
import Table from '../index';


const dataSource = Array.from({ length: 1000 }, (_, key) => ({ a: key, b: key, c: key, d: key }));


const ControlledTable = () => {
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
const [sortOrder2, setSortOrder2] = useState<SortOrder>(null);
const [filters, setFilters] = useState<string[]>([]);
const columns = [
{
title: 'A',
dataIndex: 'a',
key: 'a',
sorter: (a: any, b: any) => a.a - b.a,
sortOrder
},
{
title: 'B',
dataIndex: 'b',
key: 'b',
sorter: (a: any, b: any) => a.a - b.a,
sortOrder: sortOrder2
},
{
title: 'C',
dataIndex: 'c',
key: 'c',
filteredValue: filters,
filters: ['奇数', '偶数'],
onFilter: (value: any, record: any) => {
if (value === '奇数') {
return record.c % 2 === 1;
}
if (value === '偶数') {
return record.c % 2 === 0;
}
return false;
},
},
{
title: 'D',
dataIndex: 'd',
key: 'd',
},
];

return (
<Table
dataSource={dataSource}
columns={columns}
onChange={(p, s, f) => {
if(s?.key === 'a') {
setSortOrder(s.sortOrder);
}
if(s?.key === 'b') {
setSortOrder2(s.sortOrder);
}
if(f.c) {
setFilters(f.c)
}
}}
/>
)
}

// eslint-disable-next-line import/prefer-default-export
export { ControlledTable };
7 changes: 4 additions & 3 deletions src/components/table/hook/useEllipsisTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { has, get, isNil } from 'lodash';
import { has, get, isNil, isUndefined } from 'lodash';
import useRefs from '../../../utils/hooks/useRefs';
import { ColumnsType, ColumnGroupType, ColumnType } from '../interface';
import ToolTip from '../../tooltip';
Expand All @@ -12,10 +12,11 @@ const useEllipsisTooltip = <RecordType,>(): [
const transformEllipsisTooltipPipeline = useCallback(
(columns: ColumnsType<RecordType>) =>
columns.map((column) => {
if (has(column, 'ellipsis') && has(column, 'width') && !has(column, 'render')) {
if (has(column, 'ellipsis') && has(column, 'width')) {
const originRender = column.render;
// eslint-disable-next-line no-param-reassign
column.render = (...args) => {
const text = args[0];
const text = isUndefined(originRender) ? args[0] : originRender(...args);
const index = args[2];
const textNode = getRef(get(column, 'dataIndex') + index)?.current;
const columnWidth = Number(get(column, 'width')) - 32;
Expand Down
Loading

1 comment on commit bf9c6e2

@vercel
Copy link

@vercel vercel bot commented on bf9c6e2 May 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.