Introduce column filters
Summary: Beyond a search across all columns, it is now possible to specific columns for specific values: * for a row to be visible, all active column filters need to be matched (e.g. both a filter on time and app has to be satisfied) * if multiple values within a column are filtered for, these are -or-ed. * if no value at all within a column is checked, even when they are defined, the column won't take part in filtering * if there is a general search and column filters, a row has to satisfy both Filters can be preconfigured, pre-configured filters cannot be removed. Reseting will reset the filters back to their original Move `useMemoize` to flipper-plugin Merged the `ui/utils` and `utils` folder inside `flipper-plugin` Reviewed By: nikoant Differential Revision: D26450260 fbshipit-source-id: 11693d5d140cea03cad91c1e0f3438d7b129cf29
This commit is contained in:
committed by
Facebook GitHub Bot
parent
8aabce477b
commit
11eb19da4c
@@ -225,6 +225,11 @@ const demos: PreviewProps[] = [
|
|||||||
'boolean (false)',
|
'boolean (false)',
|
||||||
'If set, all children will use their own height, and they will be centered vertically in the layout. If not set, all children will be stretched to the height of the layout.',
|
'If set, all children will use their own height, and they will be centered vertically in the layout. If not set, all children will be stretched to the height of the layout.',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'gap',
|
||||||
|
'true / number (0)',
|
||||||
|
'Set the spacing between children. If just set, theme.space.small will be used.',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
demos: {
|
demos: {
|
||||||
'Layout.Top': (
|
'Layout.Top': (
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import {getInstance} from '../fb-stubs/Logger';
|
|||||||
import {getUser} from '../fb-stubs/user';
|
import {getUser} from '../fb-stubs/user';
|
||||||
import {SandyRatingButton} from '../chrome/RatingButton';
|
import {SandyRatingButton} from '../chrome/RatingButton';
|
||||||
import {filterNotifications} from './notification/notificationUtils';
|
import {filterNotifications} from './notification/notificationUtils';
|
||||||
import {useMemoize} from '../utils/useMemoize';
|
import {useMemoize} from 'flipper-plugin';
|
||||||
import isProduction from '../utils/isProduction';
|
import isProduction from '../utils/isProduction';
|
||||||
import NetworkGraph from '../chrome/NetworkGraph';
|
import NetworkGraph from '../chrome/NetworkGraph';
|
||||||
import FpsGraph from '../chrome/FpsGraph';
|
import FpsGraph from '../chrome/FpsGraph';
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ import React from 'react';
|
|||||||
import {Typography} from 'antd';
|
import {Typography} from 'antd';
|
||||||
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
|
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
|
||||||
import {Layout, Link, styled} from '../../ui';
|
import {Layout, Link, styled} from '../../ui';
|
||||||
import {theme, useValue} from 'flipper-plugin';
|
import {theme, useValue, useMemoize} from 'flipper-plugin';
|
||||||
import {AppSelector} from './AppSelector';
|
import {AppSelector} from './AppSelector';
|
||||||
import {useStore} from '../../utils/useStore';
|
import {useStore} from '../../utils/useStore';
|
||||||
import {PluginList} from './PluginList';
|
import {PluginList} from './PluginList';
|
||||||
import ScreenCaptureButtons from '../../chrome/ScreenCaptureButtons';
|
import ScreenCaptureButtons from '../../chrome/ScreenCaptureButtons';
|
||||||
import MetroButton from '../../chrome/MetroButton';
|
import MetroButton from '../../chrome/MetroButton';
|
||||||
import {BookmarkSection} from './BookmarkSection';
|
import {BookmarkSection} from './BookmarkSection';
|
||||||
import {useMemoize} from '../../utils/useMemoize';
|
|
||||||
import Client from '../../Client';
|
import Client from '../../Client';
|
||||||
import {State} from '../../reducers';
|
import {State} from '../../reducers';
|
||||||
import BaseDevice from '../../devices/BaseDevice';
|
import BaseDevice from '../../devices/BaseDevice';
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {State} from '../../reducers';
|
|||||||
|
|
||||||
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
||||||
import type {NavigationPlugin} from '../../../../plugins/navigation/index';
|
import type {NavigationPlugin} from '../../../../plugins/navigation/index';
|
||||||
import {useMemoize} from '../../utils/useMemoize';
|
import {useMemoize} from 'flipper-plugin';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {Glyph, Layout, styled} from '../../ui';
|
import {Glyph, Layout, styled} from '../../ui';
|
||||||
import {theme, NUX, Tracked, useValue} from 'flipper-plugin';
|
import {theme, NUX, Tracked, useValue, useMemoize} from 'flipper-plugin';
|
||||||
import {useDispatch, useStore} from '../../utils/useStore';
|
import {useDispatch, useStore} from '../../utils/useStore';
|
||||||
import {
|
import {
|
||||||
computePluginLists,
|
computePluginLists,
|
||||||
@@ -29,7 +29,6 @@ import {selectPlugin} from '../../reducers/connections';
|
|||||||
import Client from '../../Client';
|
import Client from '../../Client';
|
||||||
import BaseDevice from '../../devices/BaseDevice';
|
import BaseDevice from '../../devices/BaseDevice';
|
||||||
import {DownloadablePluginDetails} from 'flipper-plugin-lib';
|
import {DownloadablePluginDetails} from 'flipper-plugin-lib';
|
||||||
import {useMemoize} from '../../utils/useMemoize';
|
|
||||||
import MetroDevice from '../../devices/MetroDevice';
|
import MetroDevice from '../../devices/MetroDevice';
|
||||||
import {
|
import {
|
||||||
DownloadablePluginState,
|
DownloadablePluginState,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
updatePluginBlocklist,
|
updatePluginBlocklist,
|
||||||
} from '../../reducers/notifications';
|
} from '../../reducers/notifications';
|
||||||
import {filterNotifications} from './notificationUtils';
|
import {filterNotifications} from './notificationUtils';
|
||||||
import {useMemoize} from '../../utils/useMemoize';
|
import {useMemoize} from 'flipper-plugin';
|
||||||
import BlocklistSettingButton from './BlocklistSettingButton';
|
import BlocklistSettingButton from './BlocklistSettingButton';
|
||||||
|
|
||||||
type NotificationExtra = {
|
type NotificationExtra = {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ test('Correct top level API exposed', () => {
|
|||||||
"styled",
|
"styled",
|
||||||
"theme",
|
"theme",
|
||||||
"useLogger",
|
"useLogger",
|
||||||
|
"useMemoize",
|
||||||
"usePlugin",
|
"usePlugin",
|
||||||
"useTrackedCallback",
|
"useTrackedCallback",
|
||||||
"useValue",
|
"useValue",
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export {
|
|||||||
InteractiveProps as _InteractiveProps,
|
InteractiveProps as _InteractiveProps,
|
||||||
} from './ui/Interactive';
|
} from './ui/Interactive';
|
||||||
|
|
||||||
|
export {useMemoize} from './utils/useMemoize';
|
||||||
|
|
||||||
// It's not ideal that this exists in flipper-plugin sources directly,
|
// It's not ideal that this exists in flipper-plugin sources directly,
|
||||||
// but is the least pain for plugin authors.
|
// but is the least pain for plugin authors.
|
||||||
// Probably we should make sure that testing-library doesn't end up in our final Flipper bundle (which packages flipper-plugin)
|
// Probably we should make sure that testing-library doesn't end up in our final Flipper bundle (which packages flipper-plugin)
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import LowPassFilter from './utils/LowPassFilter';
|
import LowPassFilter from '../utils/LowPassFilter';
|
||||||
import {
|
import {
|
||||||
getDistanceTo,
|
getDistanceTo,
|
||||||
maybeSnapLeft,
|
maybeSnapLeft,
|
||||||
maybeSnapTop,
|
maybeSnapTop,
|
||||||
SNAP_SIZE,
|
SNAP_SIZE,
|
||||||
} from './utils/snap';
|
} from '../utils/snap';
|
||||||
import type {Rect} from './utils/Rect';
|
import type {Rect} from '../utils/Rect';
|
||||||
|
|
||||||
const WINDOW_CURSOR_BOUNDARY = 5;
|
const WINDOW_CURSOR_BOUNDARY = 5;
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ type SplitLayoutProps = {
|
|||||||
* If set, items will be centered over the orthogonal direction, if false (the default) items will be stretched.
|
* If set, items will be centered over the orthogonal direction, if false (the default) items will be stretched.
|
||||||
*/
|
*/
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
|
gap?: Spacing;
|
||||||
children: [React.ReactNode, React.ReactNode];
|
children: [React.ReactNode, React.ReactNode];
|
||||||
style?: React.HTMLAttributes<HTMLDivElement>['style'];
|
style?: React.HTMLAttributes<HTMLDivElement>['style'];
|
||||||
};
|
};
|
||||||
@@ -191,6 +192,7 @@ Object.keys(Layout).forEach((key) => {
|
|||||||
|
|
||||||
const SandySplitContainer = styled.div<{
|
const SandySplitContainer = styled.div<{
|
||||||
grow: 1 | 2;
|
grow: 1 | 2;
|
||||||
|
gap?: Spacing;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
flexDirection: CSSProperties['flexDirection'];
|
flexDirection: CSSProperties['flexDirection'];
|
||||||
}>((props) => ({
|
}>((props) => ({
|
||||||
@@ -199,6 +201,7 @@ const SandySplitContainer = styled.div<{
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: props.flexDirection,
|
flexDirection: props.flexDirection,
|
||||||
alignItems: props.center ? 'center' : 'stretch',
|
alignItems: props.center ? 'center' : 'stretch',
|
||||||
|
gap: normalizeSpace(props.gap, theme.space.small),
|
||||||
overflow: props.center ? undefined : 'hidden', // only use overflow hidden in container mode, to avoid weird resizing issues
|
overflow: props.center ? undefined : 'hidden', // only use overflow hidden in container mode, to avoid weird resizing issues
|
||||||
'> :nth-child(1)': {
|
'> :nth-child(1)': {
|
||||||
flex: props.grow === 1 ? splitGrowStyle : splitFixedStyle,
|
flex: props.grow === 1 ? splitGrowStyle : splitFixedStyle,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import LowPassFilter from '../utils/LowPassFilter';
|
import LowPassFilter from '../../utils/LowPassFilter';
|
||||||
|
|
||||||
test('hasFullBuffer', () => {
|
test('hasFullBuffer', () => {
|
||||||
const lpf = new LowPassFilter();
|
const lpf = new LowPassFilter();
|
||||||
|
|||||||
116
desktop/flipper-plugin/src/ui/datatable/ColumnFilter.tsx
Normal file
116
desktop/flipper-plugin/src/ui/datatable/ColumnFilter.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useMemo, useState} from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import React from 'react';
|
||||||
|
import {theme} from '../theme';
|
||||||
|
import type {DataTableColumn} from './DataTable';
|
||||||
|
|
||||||
|
import {Button, Checkbox, Dropdown, Menu, Typography, Input} from 'antd';
|
||||||
|
import {FilterFilled, MinusCircleOutlined} from '@ant-design/icons';
|
||||||
|
import {Layout} from '../Layout';
|
||||||
|
|
||||||
|
const {Text} = Typography;
|
||||||
|
|
||||||
|
export const HeaderButton = styled(Button)({
|
||||||
|
padding: 4,
|
||||||
|
backgroundColor: theme.backgroundWash,
|
||||||
|
borderRadius: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ColumnFilterHandlers = {
|
||||||
|
onAddColumnFilter(columnId: string, value: string): void;
|
||||||
|
onRemoveColumnFilter(columnId: string, index: number): void;
|
||||||
|
onToggleColumnFilter(columnId: string, index: number): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FilterIcon({
|
||||||
|
column,
|
||||||
|
...props
|
||||||
|
}: {column: DataTableColumn<any>} & ColumnFilterHandlers) {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const {filters} = column;
|
||||||
|
const isActive = useMemo(() => filters?.some((f) => f.enabled), [filters]);
|
||||||
|
|
||||||
|
const onAddFilter = (e: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onAddColumnFilter(column.key, input);
|
||||||
|
setInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation(); // prevents interaction accidentally with the Interactive component organizing resizng
|
||||||
|
}}>
|
||||||
|
<Menu.Item>
|
||||||
|
<Layout.Right gap>
|
||||||
|
<Input
|
||||||
|
placeholder="Filter by value"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setInput(e.target.value);
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onPressEnter={onAddFilter}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
<Button onClick={onAddFilter}>Add</Button>
|
||||||
|
</Layout.Right>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Divider />
|
||||||
|
{filters?.length ? (
|
||||||
|
filters?.map((filter, index) => (
|
||||||
|
<Menu.Item key={index}>
|
||||||
|
<Layout.Right center>
|
||||||
|
<Checkbox
|
||||||
|
checked={filter.enabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
props.onToggleColumnFilter(column.key, index);
|
||||||
|
}}>
|
||||||
|
{filter.label}
|
||||||
|
</Checkbox>
|
||||||
|
{!filter.predefined && (
|
||||||
|
<MinusCircleOutlined
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onRemoveColumnFilter(column.key, index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Layout.Right>
|
||||||
|
</Menu.Item>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text type="secondary" style={{margin: 12}}>
|
||||||
|
No active filters
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={['click']}>
|
||||||
|
<HeaderButton
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
visibility: isActive ? 'visible' : 'hidden',
|
||||||
|
color: isActive ? theme.primaryColor : theme.disabledColor,
|
||||||
|
}}>
|
||||||
|
<FilterFilled />
|
||||||
|
</HeaderButton>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow';
|
|||||||
import {DataSource} from '../../state/datasource/DataSource';
|
import {DataSource} from '../../state/datasource/DataSource';
|
||||||
import {Layout} from '../Layout';
|
import {Layout} from '../Layout';
|
||||||
import {TableHead} from './TableHead';
|
import {TableHead} from './TableHead';
|
||||||
import {Percentage} from '../utils/widthUtils';
|
import {Percentage} from '../../utils/widthUtils';
|
||||||
import {DataSourceRenderer, DataSourceVirtualizer} from './DataSourceRenderer';
|
import {DataSourceRenderer, DataSourceVirtualizer} from './DataSourceRenderer';
|
||||||
import {useDataTableManager, TableManager} from './useDataTableManager';
|
import {useDataTableManager, TableManager} from './useDataTableManager';
|
||||||
import {TableSearch} from './TableSearch';
|
import {TableSearch} from './TableSearch';
|
||||||
@@ -55,6 +55,12 @@ export type DataTableColumn<T = any> = {
|
|||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
align?: 'left' | 'right' | 'center';
|
align?: 'left' | 'right' | 'center';
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
filters?: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
enabled: boolean;
|
||||||
|
predefined?: boolean;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RenderContext<T = any> {
|
export interface RenderContext<T = any> {
|
||||||
@@ -129,6 +135,7 @@ export function DataTable<T extends object>(props: DataTableProps<T>) {
|
|||||||
case 'End':
|
case 'End':
|
||||||
selectItem(() => dataSource.output.length - 1);
|
selectItem(() => dataSource.output.length - 1);
|
||||||
break;
|
break;
|
||||||
|
case ' ': // yes, that is a space
|
||||||
case 'PageDown':
|
case 'PageDown':
|
||||||
selectItem((idx) =>
|
selectItem((idx) =>
|
||||||
Math.min(
|
Math.min(
|
||||||
@@ -196,6 +203,9 @@ export function DataTable<T extends object>(props: DataTableProps<T>) {
|
|||||||
onColumnToggleVisibility={tableManager.toggleColumnVisibility}
|
onColumnToggleVisibility={tableManager.toggleColumnVisibility}
|
||||||
sorting={tableManager.sorting}
|
sorting={tableManager.sorting}
|
||||||
onColumnSort={tableManager.sortColumn}
|
onColumnSort={tableManager.sortColumn}
|
||||||
|
onAddColumnFilter={tableManager.addColumnFilter}
|
||||||
|
onRemoveColumnFilter={tableManager.removeColumnFilter}
|
||||||
|
onToggleColumnFilter={tableManager.toggleColumnFilter}
|
||||||
/>
|
/>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
<DataSourceRenderer<T, RenderContext<T>>
|
<DataSourceRenderer<T, RenderContext<T>>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
isPercentage,
|
isPercentage,
|
||||||
Percentage,
|
Percentage,
|
||||||
Width,
|
Width,
|
||||||
} from '../utils/widthUtils';
|
} from '../../utils/widthUtils';
|
||||||
import {memo, useRef} from 'react';
|
import {memo, useRef} from 'react';
|
||||||
import {Interactive, InteractiveProps} from '../Interactive';
|
import {Interactive, InteractiveProps} from '../Interactive';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@@ -20,18 +20,14 @@ import React from 'react';
|
|||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
import type {DataTableColumn} from './DataTable';
|
import type {DataTableColumn} from './DataTable';
|
||||||
|
|
||||||
import {Button, Checkbox, Dropdown, Menu, Typography} from 'antd';
|
import {Checkbox, Dropdown, Menu, Typography} from 'antd';
|
||||||
import {CaretDownFilled, CaretUpFilled, DownOutlined} from '@ant-design/icons';
|
import {CaretDownFilled, CaretUpFilled, DownOutlined} from '@ant-design/icons';
|
||||||
import {Layout} from '../Layout';
|
import {Layout} from '../Layout';
|
||||||
import {Sorting, OnColumnResize} from './useDataTableManager';
|
import {Sorting, OnColumnResize} from './useDataTableManager';
|
||||||
|
import {ColumnFilterHandlers, FilterIcon, HeaderButton} from './ColumnFilter';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
const TableHeaderArrow = styled.span({
|
|
||||||
float: 'right',
|
|
||||||
});
|
|
||||||
TableHeaderArrow.displayName = 'TableHead:TableHeaderArrow';
|
|
||||||
|
|
||||||
function SortIcons({direction}: {direction?: 'up' | 'down'}) {
|
function SortIcons({direction}: {direction?: 'up' | 'down'}) {
|
||||||
return (
|
return (
|
||||||
<SortIconsContainer direction={direction}>
|
<SortIconsContainer direction={direction}>
|
||||||
@@ -56,11 +52,19 @@ const SortIconsContainer = styled.span<{direction?: 'up' | 'down'}>(
|
|||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginLeft: 4,
|
position: 'relative',
|
||||||
|
left: 4,
|
||||||
|
top: -3,
|
||||||
color: theme.disabledColor,
|
color: theme.disabledColor,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SettingsButton = styled(HeaderButton)({
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const TableHeaderColumnInteractive = styled(Interactive)<InteractiveProps>({
|
const TableHeaderColumnInteractive = styled(Interactive)<InteractiveProps>({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@@ -69,17 +73,6 @@ const TableHeaderColumnInteractive = styled(Interactive)<InteractiveProps>({
|
|||||||
TableHeaderColumnInteractive.displayName =
|
TableHeaderColumnInteractive.displayName =
|
||||||
'TableHead:TableHeaderColumnInteractive';
|
'TableHead:TableHeaderColumnInteractive';
|
||||||
|
|
||||||
const TableHeaderColumnContainer = styled(Layout.Horizontal)({
|
|
||||||
padding: '4px 8px',
|
|
||||||
':hover': {
|
|
||||||
backgroundColor: theme.buttonDefaultBackground,
|
|
||||||
},
|
|
||||||
[`:hover ${SortIconsContainer}`]: {
|
|
||||||
visibility: 'visible',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
TableHeaderColumnContainer.displayName = 'TableHead:TableHeaderColumnContainer';
|
|
||||||
|
|
||||||
const TableHeadContainer = styled.div<{horizontallyScrollable?: boolean}>({
|
const TableHeadContainer = styled.div<{horizontallyScrollable?: boolean}>({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -96,32 +89,36 @@ const TableHeadColumnContainer = styled.div<{
|
|||||||
flexShrink: props.width === undefined ? 1 : 0,
|
flexShrink: props.width === undefined ? 1 : 0,
|
||||||
flexGrow: props.width === undefined ? 1 : 0,
|
flexGrow: props.width === undefined ? 1 : 0,
|
||||||
width: props.width === undefined ? '100%' : props.width,
|
width: props.width === undefined ? '100%' : props.width,
|
||||||
|
'&:last-of-type': {
|
||||||
|
marginRight: 20, // space for settings button
|
||||||
|
},
|
||||||
|
[`:hover ${SortIconsContainer}`]: {
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
|
[`&:hover ${HeaderButton}`]: {
|
||||||
|
visibility: 'visible !important' as any,
|
||||||
|
},
|
||||||
|
padding: '0 4px',
|
||||||
}));
|
}));
|
||||||
TableHeadColumnContainer.displayName = 'TableHead:TableHeadColumnContainer';
|
TableHeadColumnContainer.displayName = 'TableHead:TableHeadColumnContainer';
|
||||||
|
|
||||||
const RIGHT_RESIZABLE = {right: true};
|
const RIGHT_RESIZABLE = {right: true};
|
||||||
|
|
||||||
function TableHeadColumn({
|
function TableHeadColumn({
|
||||||
id,
|
column,
|
||||||
title,
|
|
||||||
width,
|
|
||||||
isResizable,
|
isResizable,
|
||||||
isSortable,
|
|
||||||
onColumnResize,
|
onColumnResize,
|
||||||
isSortable: sortable,
|
|
||||||
onSort,
|
onSort,
|
||||||
sorted,
|
sorted,
|
||||||
|
...filterHandlers
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
column: DataTableColumn<any>;
|
||||||
width: Width;
|
|
||||||
isSortable?: boolean;
|
|
||||||
sorted: 'up' | 'down' | undefined;
|
sorted: 'up' | 'down' | undefined;
|
||||||
isResizable: boolean;
|
isResizable: boolean;
|
||||||
onSort: (id: string) => void;
|
onSort: (id: string) => void;
|
||||||
sortOrder: undefined | Sorting;
|
sortOrder: undefined | Sorting;
|
||||||
onColumnResize: OnColumnResize;
|
onColumnResize: OnColumnResize;
|
||||||
title?: string;
|
} & ColumnFilterHandlers) {
|
||||||
}) {
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const onResize = (newWidth: number) => {
|
const onResize = (newWidth: number) => {
|
||||||
@@ -132,7 +129,7 @@ function TableHeadColumn({
|
|||||||
let normalizedWidth: number | Percentage = newWidth;
|
let normalizedWidth: number | Percentage = newWidth;
|
||||||
|
|
||||||
// normalise number to a percentage if we were originally passed a percentage
|
// normalise number to a percentage if we were originally passed a percentage
|
||||||
if (isPercentage(width) && ref.current) {
|
if (isPercentage(column.width) && ref.current) {
|
||||||
const {parentElement} = ref.current;
|
const {parentElement} = ref.current;
|
||||||
const parentWidth = parentElement!.clientWidth;
|
const parentWidth = parentElement!.clientWidth;
|
||||||
const {childNodes} = parentElement!;
|
const {childNodes} = parentElement!;
|
||||||
@@ -148,14 +145,19 @@ function TableHeadColumn({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onColumnResize(id, normalizedWidth);
|
onColumnResize(column.key, normalizedWidth);
|
||||||
};
|
};
|
||||||
|
|
||||||
let children = (
|
let children = (
|
||||||
<TableHeaderColumnContainer center>
|
<Layout.Right center style={{padding: '0 4px'}}>
|
||||||
<Text strong>{title}</Text>
|
<div onClick={() => onSort(column.key)} role="button" tabIndex={0}>
|
||||||
{isSortable && <SortIcons direction={sorted} />}
|
<Text strong>
|
||||||
</TableHeaderColumnContainer>
|
{column.title ?? <> </>}
|
||||||
|
<SortIcons direction={sorted} />
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<FilterIcon column={column} {...filterHandlers} />
|
||||||
|
</Layout.Right>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isResizable) {
|
if (isResizable) {
|
||||||
@@ -171,11 +173,7 @@ function TableHeadColumn({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHeadColumnContainer
|
<TableHeadColumnContainer width={column.width} ref={ref}>
|
||||||
width={width}
|
|
||||||
title={title}
|
|
||||||
onClick={sortable ? () => onSort(id) : undefined}
|
|
||||||
ref={ref}>
|
|
||||||
{children}
|
{children}
|
||||||
</TableHeadColumnContainer>
|
</TableHeadColumnContainer>
|
||||||
);
|
);
|
||||||
@@ -193,18 +191,18 @@ export const TableHead = memo(function TableHead({
|
|||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
sorting: Sorting | undefined;
|
sorting: Sorting | undefined;
|
||||||
onColumnSort: (key: string) => void;
|
onColumnSort: (key: string) => void;
|
||||||
}) {
|
} & ColumnFilterHandlers) {
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu style={{minWidth: 200}}>
|
<Menu style={{minWidth: 200}}>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<Menu.Item
|
<Menu.Item key={column.key}>
|
||||||
key={column.key}
|
<Checkbox
|
||||||
|
checked={column.visible}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.domEvent.stopPropagation();
|
e.stopPropagation();
|
||||||
e.domEvent.preventDefault();
|
e.preventDefault();
|
||||||
props.onColumnToggleVisibility(column.key);
|
props.onColumnToggleVisibility(column.key);
|
||||||
}}>
|
}}>
|
||||||
<Checkbox checked={column.visible}>
|
|
||||||
{column.title || column.key}
|
{column.title || column.key}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -221,19 +219,19 @@ export const TableHead = memo(function TableHead({
|
|||||||
{visibleColumns.map((column, i) => (
|
{visibleColumns.map((column, i) => (
|
||||||
<TableHeadColumn
|
<TableHeadColumn
|
||||||
key={column.key}
|
key={column.key}
|
||||||
id={column.key}
|
column={column}
|
||||||
isResizable={i < visibleColumns.length - 1}
|
isResizable={i < visibleColumns.length - 1}
|
||||||
width={column.width}
|
|
||||||
isSortable={true} // might depend in the future on for example .getValue()
|
|
||||||
sortOrder={props.sorting}
|
sortOrder={props.sorting}
|
||||||
onSort={props.onColumnSort}
|
onSort={props.onColumnSort}
|
||||||
onColumnResize={props.onColumnResize}
|
onColumnResize={props.onColumnResize}
|
||||||
|
onAddColumnFilter={props.onAddColumnFilter}
|
||||||
|
onRemoveColumnFilter={props.onRemoveColumnFilter}
|
||||||
|
onToggleColumnFilter={props.onToggleColumnFilter}
|
||||||
sorted={
|
sorted={
|
||||||
props.sorting?.key === column.key
|
props.sorting?.key === column.key
|
||||||
? props.sorting!.direction
|
? props.sorting!.direction
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
title={column.title}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Dropdown overlay={menu} trigger={['click']}>
|
<Dropdown overlay={menu} trigger={['click']}>
|
||||||
@@ -244,12 +242,3 @@ export const TableHead = memo(function TableHead({
|
|||||||
</TableHeadContainer>
|
</TableHeadContainer>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const SettingsButton = styled(Button)({
|
|
||||||
padding: 4,
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
backgroundColor: theme.backgroundWash,
|
|
||||||
borderRadius: 0,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import React, {memo} from 'react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {theme} from 'flipper-plugin';
|
import {theme} from 'flipper-plugin';
|
||||||
import type {RenderContext} from './DataTable';
|
import type {RenderContext} from './DataTable';
|
||||||
import {Width} from '../utils/widthUtils';
|
import {Width} from '../../utils/widthUtils';
|
||||||
import {pad} from 'lodash';
|
import {pad} from 'lodash';
|
||||||
|
|
||||||
// heuristic for row estimation, should match any future styling updates
|
// heuristic for row estimation, should match any future styling updates
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import React, {createRef} from 'react';
|
|||||||
import {DataTable, DataTableColumn} from '../DataTable';
|
import {DataTable, DataTableColumn} from '../DataTable';
|
||||||
import {render, act} from '@testing-library/react';
|
import {render, act} from '@testing-library/react';
|
||||||
import {createDataSource} from '../../../state/datasource/DataSource';
|
import {createDataSource} from '../../../state/datasource/DataSource';
|
||||||
import {TableManager} from '../useDataTableManager';
|
import {computeDataTableFilter, TableManager} from '../useDataTableManager';
|
||||||
import {Button} from 'antd';
|
import {Button} from 'antd';
|
||||||
|
|
||||||
type Todo = {
|
type Todo = {
|
||||||
@@ -277,3 +277,226 @@ test('search', async () => {
|
|||||||
expect(elem.length).toBe(3);
|
expect(elem.length).toBe(3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('compute filters', () => {
|
||||||
|
const coffee = {
|
||||||
|
level: 'info',
|
||||||
|
title: 'Drink coffee',
|
||||||
|
done: true,
|
||||||
|
};
|
||||||
|
const espresso = {
|
||||||
|
level: 'info',
|
||||||
|
title: 'Make espresso',
|
||||||
|
done: false,
|
||||||
|
};
|
||||||
|
const meet = {
|
||||||
|
level: 'error',
|
||||||
|
title: 'Meet me',
|
||||||
|
done: false,
|
||||||
|
};
|
||||||
|
const data = [coffee, espresso, meet];
|
||||||
|
|
||||||
|
// results in empty filter
|
||||||
|
expect(computeDataTableFilter('', [])).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
computeDataTableFilter('', [
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
value: 'coffee',
|
||||||
|
label: 'coffee',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toBeUndefined();
|
||||||
|
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('tEsT', [])!;
|
||||||
|
expect(data.filter(filter)).toEqual([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('EE', [])!;
|
||||||
|
expect(data.filter(filter)).toEqual([coffee, meet]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('D', [])!;
|
||||||
|
expect(data.filter(filter)).toEqual([coffee]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('true', [])!;
|
||||||
|
expect(data.filter(filter)).toEqual([coffee]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('false', [])!;
|
||||||
|
expect(data.filter(filter)).toEqual([espresso, meet]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('EE', [
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'error',
|
||||||
|
label: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(data.filter(filter)).toEqual([meet]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('EE', [
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'info',
|
||||||
|
label: 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'error',
|
||||||
|
label: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(data.filter(filter)).toEqual([coffee, meet]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('', [
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'info',
|
||||||
|
label: 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
value: 'error',
|
||||||
|
label: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(data.filter(filter)).toEqual([coffee, espresso]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('', [
|
||||||
|
{
|
||||||
|
key: 'done',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'false',
|
||||||
|
label: 'Not done',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(data.filter(filter)).toEqual([espresso, meet]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// nothing selected anything will not filter anything out for that column
|
||||||
|
const filter = computeDataTableFilter('', [
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
value: 'info',
|
||||||
|
label: 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
value: 'error',
|
||||||
|
label: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(filter).toBeUndefined();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('', [
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'info',
|
||||||
|
label: 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'error',
|
||||||
|
label: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(data.filter(filter)).toEqual([coffee, espresso, meet]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('', [
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'info',
|
||||||
|
label: 'info',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'done',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'false',
|
||||||
|
label: 'false,',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(data.filter(filter)).toEqual([espresso]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const filter = computeDataTableFilter('nonsense', [
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'info',
|
||||||
|
label: 'info',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'done',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
value: 'false',
|
||||||
|
label: 'false,',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])!;
|
||||||
|
expect(data.filter(filter)).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {DataTableColumn} from 'flipper-plugin/src/ui/datatable/DataTable';
|
import {DataTableColumn} from 'flipper-plugin/src/ui/datatable/DataTable';
|
||||||
import {Percentage} from '../utils/widthUtils';
|
import {Percentage} from '../../utils/widthUtils';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
import {DataSource} from '../../state/datasource/DataSource';
|
import {DataSource} from '../../state/datasource/DataSource';
|
||||||
|
import {useMemoize} from '../../utils/useMemoize';
|
||||||
|
|
||||||
export type OnColumnResize = (id: string, size: number | Percentage) => void;
|
export type OnColumnResize = (id: string, size: number | Percentage) => void;
|
||||||
export type Sorting = {
|
export type Sorting = {
|
||||||
@@ -42,29 +43,50 @@ export function useDataTableManager<T extends object>(
|
|||||||
[columns],
|
[columns],
|
||||||
);
|
);
|
||||||
|
|
||||||
// filter is computed by useMemo to support adding column filters etc here in the future
|
const addColumnFilter = useCallback((columnId: string, value: string) => {
|
||||||
const currentFilter = useMemo(
|
// TODO: fix typings
|
||||||
function computeFilter() {
|
setEffectiveColumns(
|
||||||
if (searchValue === '') {
|
produce((draft: DataTableColumn<any>[]) => {
|
||||||
// unset
|
const column = draft.find((c) => c.key === columnId)!;
|
||||||
return undefined;
|
column.filters!.push({
|
||||||
}
|
label: value,
|
||||||
|
value: value.toLowerCase(),
|
||||||
const searchString = searchValue.toLowerCase();
|
enabled: true,
|
||||||
return function dataTableFilter(item: object) {
|
});
|
||||||
return Object.values(item).some((v) =>
|
}),
|
||||||
String(v).toLowerCase().includes(searchString),
|
|
||||||
);
|
);
|
||||||
};
|
}, []);
|
||||||
},
|
|
||||||
[searchValue],
|
const removeColumnFilter = useCallback((columnId: string, index: number) => {
|
||||||
|
// TODO: fix typings
|
||||||
|
setEffectiveColumns(
|
||||||
|
produce((draft: DataTableColumn<any>[]) => {
|
||||||
|
draft.find((c) => c.key === columnId)!.filters?.splice(index, 1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleColumnFilter = useCallback((columnId: string, index: number) => {
|
||||||
|
// TODO: fix typings
|
||||||
|
setEffectiveColumns(
|
||||||
|
produce((draft: DataTableColumn<any>[]) => {
|
||||||
|
const f = draft.find((c) => c.key === columnId)!.filters![index];
|
||||||
|
f.enabled = !f.enabled;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// filter is computed by useMemo to support adding column filters etc here in the future
|
||||||
|
const currentFilter = useMemoize(
|
||||||
|
computeDataTableFilter,
|
||||||
|
[searchValue, columns], // possible optimization: we only need the column filters
|
||||||
);
|
);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setEffectiveColumns(computeInitialColumns(defaultColumns));
|
setEffectiveColumns(computeInitialColumns(defaultColumns));
|
||||||
setSorting(undefined);
|
setSorting(undefined);
|
||||||
|
setSearchValue('');
|
||||||
dataSource.reset();
|
dataSource.reset();
|
||||||
// TODO: local storage
|
|
||||||
}, [dataSource, defaultColumns]);
|
}, [dataSource, defaultColumns]);
|
||||||
|
|
||||||
const resizeColumn = useCallback((id: string, width: number | Percentage) => {
|
const resizeColumn = useCallback((id: string, width: number | Percentage) => {
|
||||||
@@ -152,6 +174,10 @@ export function useDataTableManager<T extends object>(
|
|||||||
/** current selection, describes the index index in the datasources's current output (not window) */
|
/** current selection, describes the index index in the datasources's current output (not window) */
|
||||||
selection,
|
selection,
|
||||||
selectItem,
|
selectItem,
|
||||||
|
/** Changing column filters */
|
||||||
|
addColumnFilter,
|
||||||
|
removeColumnFilter,
|
||||||
|
toggleColumnFilter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +186,45 @@ function computeInitialColumns(
|
|||||||
): DataTableColumn<any>[] {
|
): DataTableColumn<any>[] {
|
||||||
return columns.map((c) => ({
|
return columns.map((c) => ({
|
||||||
...c,
|
...c,
|
||||||
|
filters:
|
||||||
|
c.filters?.map((f) => ({
|
||||||
|
...f,
|
||||||
|
predefined: true,
|
||||||
|
})) ?? [],
|
||||||
visible: c.visible !== false,
|
visible: c.visible !== false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function computeDataTableFilter(
|
||||||
|
searchValue: string,
|
||||||
|
columns: DataTableColumn[],
|
||||||
|
) {
|
||||||
|
const searchString = searchValue.toLowerCase();
|
||||||
|
// the columns with an active filter are those that have filters defined,
|
||||||
|
// with at least one enabled
|
||||||
|
const filteringColumns = columns.filter((c) =>
|
||||||
|
c.filters?.some((f) => f.enabled),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchValue === '' && !filteringColumns.length) {
|
||||||
|
// unset
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return function dataTableFilter(item: any) {
|
||||||
|
for (const column of filteringColumns) {
|
||||||
|
if (
|
||||||
|
!column.filters!.some(
|
||||||
|
(f) =>
|
||||||
|
f.enabled &&
|
||||||
|
String(item[column.key]).toLowerCase().includes(f.value),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false; // there are filters, but none matches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.values(item).some((v) =>
|
||||||
|
String(v).toLowerCase().includes(searchString),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -494,6 +494,30 @@ Utility that wraps React's `useCallback` with tracking capabilities.
|
|||||||
The API is similar, except that the first argument describes the interaction handled by the given event handler.
|
The API is similar, except that the first argument describes the interaction handled by the given event handler.
|
||||||
See [Tracked](#tracked) for more info.
|
See [Tracked](#tracked) for more info.
|
||||||
|
|
||||||
|
### useMemoize
|
||||||
|
|
||||||
|
Slight variation on useMemo that encourages to create hoistable memoization functions,
|
||||||
|
which encourages reuse and testability by no longer closing over variables that are used by the memoized function, but rather receiving them as arguments so that these functions beome pure.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function MyComponent() {
|
||||||
|
const {findMetroDevice} = props;
|
||||||
|
const connections = useSomeHook();
|
||||||
|
|
||||||
|
const metroDevice = useMemoize(
|
||||||
|
findMetroDevice,
|
||||||
|
[connections.devices],
|
||||||
|
);
|
||||||
|
|
||||||
|
// etc
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMetroDevice(findMetroDevice, deviceList) {
|
||||||
|
return deviceList.find(findMetroDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## UI components
|
## UI components
|
||||||
|
|
||||||
### Layout.*
|
### Layout.*
|
||||||
|
|||||||
Reference in New Issue
Block a user