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:
committed by
Facebook GitHub Bot
parent
8e0d3cf779
commit
6c7b69803f
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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, [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user