Add support for negative filters

Summary:
Changelog: Add support for negative filters in data tables

As requested per somewhere on workplace but couldn't find it back :)

Reviewed By: nikoant

Differential Revision: D29486096

fbshipit-source-id: 467c8598f6d09afc9a5ed85affb6c51840afe00c
This commit is contained in:
Michel Weststrate
2021-06-30 10:40:50 -07:00
committed by Facebook GitHub Bot
parent 8e0d3cf779
commit 6c7b69803f
4 changed files with 84 additions and 11 deletions

View File

@@ -10,7 +10,15 @@
import {useMemo, useState} from 'react'; import {useMemo, useState} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React from 'react'; import React from 'react';
import {Button, Checkbox, Dropdown, Menu, Typography, Input} from 'antd'; import {
Button,
Checkbox,
Dropdown,
Menu,
Typography,
Input,
Switch,
} from 'antd';
import { import {
FilterOutlined, FilterOutlined,
MinusCircleOutlined, MinusCircleOutlined,
@@ -115,6 +123,29 @@ export function FilterIcon({
</Menu.Item> </Menu.Item>
)} )}
<Menu.Divider /> <Menu.Divider />
<Menu.Item>
<Layout.Horizontal
gap
center
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}>
<Switch
checked={!!column.inversed}
size="small"
onChange={(inversed) => {
dispatch({
type: 'setColumnFilterInverse',
column: column.key,
inversed,
});
}}
/>
Exclude items matching filter
</Layout.Horizontal>
</Menu.Item>
<Menu.Divider />
<Menu.Item disabled> <Menu.Item disabled>
<div style={{textAlign: 'right'}}> <div style={{textAlign: 'right'}}>
<Button <Button

View File

@@ -106,6 +106,7 @@ export type DataTableColumn<T = any> = {
enabled: boolean; enabled: boolean;
predefined?: boolean; predefined?: boolean;
}[]; }[];
inversed?: boolean;
}; };
export interface TableRowRenderContext<T = any> { export interface TableRowRenderContext<T = any> {
@@ -341,7 +342,7 @@ export function DataTable<T extends object>(
// Important dep optimization: we don't want to recalc filters if just the width or visibility changes! // Important dep optimization: we don't want to recalc filters if just the width or visibility changes!
// We pass entire state.columns to computeDataTableFilter, but only changes in the filter are a valid cause to compute a new filter function // We pass entire state.columns to computeDataTableFilter, but only changes in the filter are a valid cause to compute a new filter function
// eslint-disable-next-line // eslint-disable-next-line
[tableState.searchValue, tableState.useRegex, ...tableState.columns.map((c) => c.filters)], [tableState.searchValue, tableState.useRegex, ...tableState.columns.map((c) => c.filters), ...tableState.columns.map((c => c.inversed))],
); );
useEffect( useEffect(
@@ -599,7 +600,7 @@ function EmptyTable({dataSource}: {dataSource: DataSource<any>}) {
<Layout.Container <Layout.Container
center center
style={{width: '100%', padding: 40, color: theme.textColorSecondary}}> style={{width: '100%', padding: 40, color: theme.textColorSecondary}}>
{dataSource.records.length === 0 ? ( {dataSource.size === 0 ? (
<> <>
<CoffeeOutlined style={{fontSize: '2em', margin: 8}} /> <CoffeeOutlined style={{fontSize: '2em', margin: 8}} />
<Typography.Text type="secondary">No records yet</Typography.Text> <Typography.Text type="secondary">No records yet</Typography.Text>

View File

@@ -85,6 +85,7 @@ type DataManagerActions<T> =
> >
| Action<'removeColumnFilter', {column: keyof T; index: number}> | Action<'removeColumnFilter', {column: keyof T; index: number}>
| Action<'toggleColumnFilter', {column: keyof T; index: number}> | Action<'toggleColumnFilter', {column: keyof T; index: number}>
| Action<'setColumnFilterInverse', {column: keyof T; inversed: boolean}>
| Action<'setColumnFilterFromSelection', {column: keyof T}> | Action<'setColumnFilterFromSelection', {column: keyof T}>
| Action<'appliedInitialScroll'> | Action<'appliedInitialScroll'>
| Action<'toggleUseRegex'> | Action<'toggleUseRegex'>
@@ -218,6 +219,11 @@ export const dataTableManagerReducer = produce<
f.enabled = !f.enabled; f.enabled = !f.enabled;
break; break;
} }
case 'setColumnFilterInverse': {
draft.columns.find((c) => c.key === action.column)!.inversed =
action.inversed;
break;
}
case 'setColumnFilterFromSelection': { case 'setColumnFilterFromSelection': {
const items = getSelectedItems( const items = getSelectedItems(
config.dataSource as DataSource<any>, config.dataSource as DataSource<any>,
@@ -504,14 +510,15 @@ export function computeDataTableFilter(
return function dataTableFilter(item: any) { return function dataTableFilter(item: any) {
for (const column of filteringColumns) { for (const column of filteringColumns) {
if ( const rowMatchesFilter = column.filters!.some(
!column.filters!.some(
(f) => (f) =>
f.enabled && f.enabled && String(item[column.key]).toLowerCase().includes(f.value),
String(item[column.key]).toLowerCase().includes(f.value), );
) if (column.inversed && rowMatchesFilter) {
) { return false;
return false; // there are filters, but none matches }
if (!column.inversed && !rowMatchesFilter) {
return false;
} }
} }
return Object.values(item).some((v) => return Object.values(item).some((v) =>

View File

@@ -493,6 +493,40 @@ test('compute filters', () => {
])!; ])!;
expect(data.filter(filter)).toEqual([espresso]); expect(data.filter(filter)).toEqual([espresso]);
} }
{
// inverse filter
const filter = computeDataTableFilter('', false, [
{
key: 'level',
filters: [
{
enabled: true,
value: 'error',
label: 'error',
},
],
inversed: true,
},
])!;
expect(data.filter(filter)).toEqual([coffee, espresso]);
}
{
// inverse filter with search
const filter = computeDataTableFilter('coffee', false, [
{
key: 'level',
filters: [
{
enabled: true,
value: 'error',
label: 'error',
},
],
inversed: true,
},
])!;
expect(data.filter(filter)).toEqual([coffee]);
}
{ {
const filter = computeDataTableFilter('nonsense', false, [ const filter = computeDataTableFilter('nonsense', false, [
{ {