Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Table): add filter to colum settings #1627

Merged
merged 7 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/components/Table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,11 @@ const MyTable1 = withTableSettings({sortable: false})(Table);

### Options

| Name | Description | Type | Default |
| :------- | :------------------------------------------------ | :------------: | :-----: |
| width | Settings' popup width | `number` `fit` | |
| sortable | Whether or not add ability to sort settings items | `boolean` | `true` |
| Name | Description | Type | Default |
| :--------- | :-------------------------------------------------- | :--------------: | :-----: |
| width | Settings' popup width | `number` `"fit"` | |
| sortable | Whether or not add ability to sort settings items | `boolean` | `true` |
| filterable | Whether or not add ability to filter settings items | `boolean` | `false` |

### ColumnMeta

Expand All @@ -251,12 +252,15 @@ const MyTable1 = withTableSettings({sortable: false})(Table);

### Properties

| Name | Description | Type |
| :----------------- | :------------------------------ | :------------------------------------------: |
| settingsPopupWidth | TableColumnSetup pop-up width | `number` `fit` |
| settings | Current settings | `TableSettingsData` |
| updateSettings | Settings update handle | `(data: TableSettingsData) => Promise<void>` |
| renderControls | Allows to render custom actions | `RenderControls` |
| Name | Description | Type |
| :------------------------- | :----------------------------------------------------------- | :------------------------------------------------------: |
| settingsPopupWidth | TableColumnSetup pop-up width | `number` `"fit"` |
| settings | Current settings | `TableSettingsData` |
| updateSettings | Settings update handle | `(data: TableSettingsData) => Promise<void>` |
| renderControls | Allows to render custom actions | `RenderControls` |
| settingsFilterPlaceholder | Text that appears in the control when no search value is set | `string` |
| settingsFilterEmptyMessage | Text that appears when no one item is found | `string` |
| filterSettings | Function for filtering items | `(value: string, item: TableColumnSetupItem) => boolean` |

### TableSettingsData

Expand Down
19 changes: 19 additions & 0 deletions src/components/Table/__stories__/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {WithTableSettingsCustomActionsShowcase} from './WithTableSettingsCustomA
import {
TableWithAction,
TableWithCopy,
TableWithFilterableSettings,
TableWithSelection,
TableWithSettings,
TableWithSettingsFactory,
Expand Down Expand Up @@ -233,6 +234,24 @@ HOCWithTableSettings.args = {
columns: columnsWithSettings,
};

const WithFilterableSettingsTemplate: StoryFn<TableProps<DataItem>> = (args) => {
const [settings, setSettings] = React.useState<TableSettingsData>(DEFAULT_SETTINGS);
return (
<TableWithFilterableSettings
{...args}
settings={settings}
updateSettings={setSettings}
settingsFilterPlaceholder="Filter list"
settingsFilterEmptyMessage="No results"
/>
);
};

export const HOCWithFilterableTableSettings = WithFilterableSettingsTemplate.bind({});
HOCWithFilterableTableSettings.parameters = {
disableStrictMode: true,
};

export const HOCWithTableSettingsFactory = WithTableSettingsTemplate.bind({});
HOCWithTableSettingsFactory.parameters = {
isFactory: true,
Expand Down
4 changes: 4 additions & 0 deletions src/components/Table/__stories__/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,9 @@ export const TableWithAction = withTableActions<DataItem>(Table);
export const TableWithCopy = withTableCopy<DataItem>(Table);
export const TableWithSelection = withTableSelection<DataItem>(Table);
export const TableWithSettings = withTableSettings<DataItem>(Table);
export const TableWithFilterableSettings = withTableSettings<DataItem>({
filterable: true,
width: 200,
})(Table);
export const TableWithSettingsFactory = withTableSettings<DataItem>({sortable: false})(Table);
export const TableWithSorting = withTableSorting<DataItem>(Table);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import userEvent from '@testing-library/user-event';

import {render, screen} from '../../../../test-utils/utils';
import {fireEvent, render, screen, waitFor} from '../../../../test-utils/utils';
import {Button} from '../../Button';
import {Table} from '../Table';
import type {TableColumnConfig, TableProps} from '../Table';
Expand Down Expand Up @@ -331,4 +331,48 @@ describe('withTableSettings', () => {
expect(customControl).toBeVisible();
});
});

describe('filterableSettings', () => {
const TableWithSettings = withTableSettings<SomeItem>({sortable: true, filterable: true})(
Table,
);
const settings = columns.map<TableSetting>((column) => ({id: column.id, isSelected: true}));
const updateSettings = jest.fn();
const placeholder = 'Filter list';

it('should filter columns', async () => {
render(
<TableWithSettings
columns={columns}
data={data}
settings={settings}
settingsFilterPlaceholder={placeholder}
updateSettings={updateSettings}
/>,
);

await userEvent.click(screen.getByRole('button', {name: 'Table settings'}));
const textInput = screen.getByRole('textbox') as HTMLInputElement;
expect(textInput).toBeVisible();
expect(textInput.placeholder).toBe(placeholder);

const column = screen.getByRole('button', {name: 'description'});
expect(column.hasAttribute('draggable')).toBeTruthy();

fireEvent.change(textInput, {target: {value: 'na'}});
const filteredOption = screen.getByRole('option', {name: 'name'});
expect(filteredOption).toBeInTheDocument();
expect(filteredOption.hasAttribute('draggable')).toBeFalsy();
await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(1));

fireEvent.change(textInput, {target: {value: ''}});
expect(screen.getByRole('button', {name: 'id'}).hasAttribute('draggable')).toBeTruthy();
expect(
screen.getByRole('button', {name: 'name'}).hasAttribute('draggable'),
).toBeTruthy();
expect(
screen.getByRole('button', {name: 'description'}).hasAttribute('draggable'),
).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,15 @@ $block: '.#{variables.$ns}inner-table-column-setup';
&__controls {
margin: var(--g-spacing-1) var(--g-spacing-1) 0;
}

&__filter-input {
box-sizing: border-box;
padding: 0 var(--g-spacing-2) var(--g-spacing-1);

border-block-end: 1px solid var(--g-color-line-generic);
}

&__empty-placeholder {
padding: var(--g-spacing-2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ import type {PopperPlacement} from '../../../../../hooks/private';
import {createOnKeyDownHandler} from '../../../../../hooks/useActionHandlers/useActionHandlers';
import {Button} from '../../../../Button';
import {Icon} from '../../../../Icon';
import {Text} from '../../../../Text';
import {TreeSelect} from '../../../../TreeSelect/TreeSelect';
import type {
TreeSelectProps,
TreeSelectRenderContainer,
TreeSelectRenderItem,
} from '../../../../TreeSelect/types';
import {TextInput} from '../../../../controls/TextInput';
import {Flex} from '../../../../layout/Flex/Flex';
import type {ListItemCommonProps, ListItemViewProps} from '../../../../useList';
import {ListContainerView, ListItemView} from '../../../../useList';
import {ListContainerView, ListItemView, useListFilter} from '../../../../useList';
import {block} from '../../../../utils/cn';
import type {TableColumnConfig} from '../../../Table';
import type {TableSetting} from '../withTableSettings';
Expand All @@ -35,6 +37,8 @@ import './TableColumnSetup.scss';

const b = block('inner-table-column-setup');
const controlsCn = b('controls');
const filterInputCn = b('filter-input');
const emptyPlaceholderCn = b('empty-placeholder');

const reorderArray = <T extends unknown>(list: T[], startIndex: number, endIndex: number): T[] => {
const result = [...list];
Expand Down Expand Up @@ -244,6 +248,17 @@ const mapItemDataToProps = (item: TableColumnSetupItem): ListItemCommonProps =>
};
};

const defaultFilterSettingsFn = (value: string, item: TableColumnSetupItem) => {
return typeof item.title === 'string'
? item.title.toLowerCase().includes(value.trim().toLowerCase())
: true;
};

const useEmptyRenderContainer = (placeholder?: string): TreeSelectRenderContainer<{}> => {
const emptyRenderContainer = () => <Text className={emptyPlaceholderCn}>{placeholder}</Text>;
return emptyRenderContainer;
};

export type RenderControls = (params: {
DefaultApplyButton: React.ComponentType;
/**
Expand Down Expand Up @@ -271,6 +286,11 @@ export interface TableColumnSetupProps {

defaultItems?: TableColumnSetupItem[];
showResetButton?: boolean | ((currentItems: TableColumnSetupItem[]) => boolean);

filterable?: boolean;
filterPlaceholder?: string;
filterEmptyMessage?: string;
filterSettings?: (value: string, item: TableColumnSetupItem) => boolean;
}

export const TableColumnSetup = (props: TableColumnSetupProps) => {
Expand All @@ -285,9 +305,19 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
className,
defaultItems = propsItems,
showResetButton: propsShowResetButton,
filterable,
filterPlaceholder,
filterEmptyMessage,
filterSettings = defaultFilterSettingsFn,
} = props;

const [open, setOpen] = React.useState(false);
const [sortingEnabled, setSortingEnabled] = React.useState(sortable);
const [prevSortingEnabled, setPrevSortingEnabled] = React.useState(sortable);
if (sortable !== prevSortingEnabled) {
setPrevSortingEnabled(sortable);
setSortingEnabled(sortable);
}
amje marked this conversation as resolved.
Show resolved Hide resolved

const [items, setItems] = React.useState(propsItems);
const [prevPropsItems, setPrevPropsItems] = React.useState(propsItems);
Expand All @@ -297,10 +327,12 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
setItems(propsItems);
}

const filterState = useListFilter({items, filterItem: filterSettings, debounceTimeout: 0});

const onApply = () => {
const newSettings = items.map<TableSetting>(({id, isSelected}) => ({id, isSelected}));
propsOnUpdate(newSettings);
setOpen(false);
onOpenChange(false);
};

const DefaultApplyButton = () => (
Expand Down Expand Up @@ -344,7 +376,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
),
});

const dndRenderItem = useDndRenderItem(sortable);
const dndRenderItem = useDndRenderItem(sortingEnabled);

const renderControl: TreeSelectProps<unknown>['renderControl'] = ({toggleOpen}) => {
const onKeyDown = createOnKeyDownHandler(toggleOpen);
Expand All @@ -361,9 +393,10 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {

const onOpenChange = (open: boolean) => {
setOpen(open);

if (open === false) {
setItems(propsItems);
setSortingEnabled(sortable);
filterState.reset();
}
};

Expand All @@ -378,6 +411,28 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {

const value = React.useMemo(() => prepareValue(items), [items]);

const emptyRenderContainer = useEmptyRenderContainer(filterEmptyMessage);

const onFilterValueUpdate = (value: string) => {
filterState.onFilterUpdate(value);
setSortingEnabled(!value.length);
};

const slotBeforeListBody = filterable ? (
<TextInput
size="m"
view="clear"
placeholder={filterPlaceholder}
value={filterState.filter}
className={filterInputCn}
onUpdate={onFilterValueUpdate}
hasClear
/>
) : null;

const renderContainer =
filterState.filter && !filterState.items.length ? emptyRenderContainer : dndRenderContainer;

return (
<TreeSelect
className={b(null, className)}
Expand All @@ -386,12 +441,13 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => {
size="l"
open={open}
value={value}
items={items}
items={filterState.filter ? filterState.items : items}
onUpdate={onUpdate}
popupWidth={popupWidth}
onOpenChange={onOpenChange}
placement={popupPlacement}
renderContainer={dndRenderContainer}
slotBeforeListBody={slotBeforeListBody}
renderContainer={renderContainer}
renderControl={renderControl}
renderItem={dndRenderItem}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function getActualItems<I>(
export interface WithTableSettingsOptions {
width?: TreeSelectProps<any>['popupWidth'];
sortable?: boolean;
filterable?: boolean;
}

interface WithTableSettingsBaseProps {
Expand Down Expand Up @@ -145,8 +146,15 @@ interface WithoutDefaultSettings {
showResetButton?: boolean;
}

interface WithFilter {
settingsFilterPlaceholder?: string;
settingsFilterEmptyMessage?: string;
filterSettings?: (value: string, item: TableColumnSetupItem) => boolean;
}

export type WithTableSettingsProps = WithTableSettingsBaseProps &
(WithDefaultSettings | WithoutDefaultSettings);
(WithDefaultSettings | WithoutDefaultSettings) &
WithFilter;

const b = block('table');

Expand All @@ -169,7 +177,7 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>(
) => React.ComponentType<TableProps<I> & WithTableSettingsProps & E>) {
function tableWithSettingsFactory(
TableComponent: React.ComponentType<TableProps<I> & E>,
{width, sortable}: WithTableSettingsOptions = {},
{width, sortable, filterable}: WithTableSettingsOptions = {},
) {
const componentName = getComponentName(TableComponent);

Expand All @@ -181,6 +189,9 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>(
renderControls,
defaultSettings,
showResetButton,
settingsFilterPlaceholder,
settingsFilterEmptyMessage,
filterSettings,
...restTableProps
}: TableProps<I> & WithTableSettingsProps & E) {
const defaultActualItems = React.useMemo(() => {
Expand All @@ -193,14 +204,17 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>(

const enhancedColumns = React.useMemo(() => {
const actualItems = getActualItems(columns, settings || []);

return enhanceSystemColumn(filterColumns(columns, actualItems), (systemColumn) => {
systemColumn.name = () => (
<div className={b('settings')}>
<TableColumnSetup
popupWidth={settingsPopupWidth || width}
popupPlacement={POPUP_PLACEMENT}
sortable={sortable}
filterable={filterable}
filterPlaceholder={settingsFilterPlaceholder}
filterEmptyMessage={settingsFilterEmptyMessage}
filterSettings={filterSettings}
onUpdate={updateSettings}
items={actualItems}
renderSwitcher={({onClick}) => (
Expand Down
Loading