moving tables to react-window
Summary: Tables were using a custom virtualization, which wasn't as performant as other solutions out there. In this diff, the table component is reworked for performance. - removes `Table` component, because it was never used standalone, `ManagedTable` is what all plugins used - uses `react-window` for `ManagedTable` - reworks table highlighting and arrow-navigation to work with the new virtualization - moves actual filtering out of `ManagedTable` into `Searchable` component for a better separation of concerns. Reviewed By: jknoxville Differential Revision: D9447721 fbshipit-source-id: 15eb2eb55eed9f49a0cb1ccfb2d748b3672fa898
This commit is contained in:
committed by
Facebook Github Bot
parent
33f34650df
commit
1891e2c869
2
flow-typed/electron-menu.js
vendored
2
flow-typed/electron-menu.js
vendored
@@ -43,7 +43,7 @@ type Electron$MenuItemOptions = {
|
||||
menuItem: Electron$MenuItem,
|
||||
browserWindow: Object,
|
||||
event: Object,
|
||||
) => void,
|
||||
) => mixed,
|
||||
role?: Electron$MenuRoles,
|
||||
type?: Electron$MenuType,
|
||||
label?: string,
|
||||
|
||||
39
flow-typed/npm/react-virtualized-auto-sizer_vx.x.x.js
vendored
Normal file
39
flow-typed/npm/react-virtualized-auto-sizer_vx.x.x.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// flow-typed signature: 914da65c76e12730f3b4e60d17889885
|
||||
// flow-typed version: <<STUB>>/react-virtualized-auto-sizer_v1.0.2/flow_v0.76.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'react-virtualized-auto-sizer'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'react-virtualized-auto-sizer' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'react-virtualized-auto-sizer/dist/index.cjs' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'react-virtualized-auto-sizer/dist/index.esm' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'react-virtualized-auto-sizer/dist/index.cjs.js' {
|
||||
declare module.exports: $Exports<'react-virtualized-auto-sizer/dist/index.cjs'>;
|
||||
}
|
||||
declare module 'react-virtualized-auto-sizer/dist/index.esm.js' {
|
||||
declare module.exports: $Exports<'react-virtualized-auto-sizer/dist/index.esm'>;
|
||||
}
|
||||
39
flow-typed/npm/react-window_vx.x.x.js
vendored
Normal file
39
flow-typed/npm/react-window_vx.x.x.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// flow-typed signature: 351408bd2a4f82fae81f253ee1947834
|
||||
// flow-typed version: <<STUB>>/react-window_v1.1.1/flow_v0.76.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'react-window'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'react-window' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'react-window/dist/index.cjs' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'react-window/dist/index.esm' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'react-window/dist/index.cjs.js' {
|
||||
declare module.exports: $Exports<'react-window/dist/index.cjs'>;
|
||||
}
|
||||
declare module 'react-window/dist/index.esm.js' {
|
||||
declare module.exports: $Exports<'react-window/dist/index.esm'>;
|
||||
}
|
||||
@@ -63,7 +63,8 @@
|
||||
"react-dom": "16",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-test-renderer": "^16",
|
||||
"react-virtualized": "^9.13.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.1.1",
|
||||
"redux": "^4.0.0",
|
||||
"redux-persist": "^5.10.0",
|
||||
"redux-persist-transform-filter": "^0.0.18",
|
||||
|
||||
@@ -71,9 +71,6 @@ class SearchableManagedTable extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
// ManagedTable is a PureComponent and does not update when this.filterRows
|
||||
// would return a different value. This is why we update the funtion reference
|
||||
// once the results of the function changed.
|
||||
if (
|
||||
nextProps.searchTerm !== this.props.searchTerm ||
|
||||
!deepEqual(this.props.filters, nextProps.filters)
|
||||
@@ -90,13 +87,16 @@ class SearchableManagedTable extends PureComponent<Props, State> {
|
||||
searchTerm: _searchTerm,
|
||||
filters: _filters,
|
||||
innerRef,
|
||||
rows,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<ManagedTable
|
||||
{...props}
|
||||
filter={this.state.filterRows}
|
||||
rows={rows.filter(this.state.filterRows)}
|
||||
onAddFilter={addFilter}
|
||||
ref={innerRef}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
TableColumnRawOrder,
|
||||
TableColumnOrder,
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
@@ -16,8 +15,18 @@ import type {
|
||||
TableBodyRow,
|
||||
TableOnAddFilter,
|
||||
} from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
import styled from '../../styled/index.js';
|
||||
import Table from './Table.js';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {VariableSizeList as List} from 'react-window';
|
||||
import {clipboard} from 'electron';
|
||||
import TableHead from './TableHead.js';
|
||||
import TableRow from './TableRow.js';
|
||||
import ContextMenu from '../ContextMenu.js';
|
||||
import FlexColumn from '../FlexColumn.js';
|
||||
import createPaste from '../../../utils/createPaste.js';
|
||||
import {DEFAULT_ROW_HEIGHT} from './types';
|
||||
|
||||
export type ManagedTableProps = {|
|
||||
/**
|
||||
@@ -50,7 +59,7 @@ export type ManagedTableProps = {|
|
||||
/**
|
||||
* Order of columns.
|
||||
*/
|
||||
columnOrder?: ?TableColumnRawOrder,
|
||||
columnOrder?: TableColumnOrder,
|
||||
/**
|
||||
* Size of the columns.
|
||||
*/
|
||||
@@ -99,10 +108,11 @@ export type ManagedTableProps = {|
|
||||
|};
|
||||
|
||||
type ManagedTableState = {|
|
||||
highlightedRows: TableHighlightedRows,
|
||||
highlightedRows: Set<string>,
|
||||
sortOrder: ?TableRowSortOrder,
|
||||
columnOrder: ?TableColumnRawOrder,
|
||||
columnSizes: ?TableColumnSizes,
|
||||
columnOrder: TableColumnOrder,
|
||||
columnSizes: TableColumnSizes,
|
||||
shouldScrollToBottom: boolean,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -111,7 +121,7 @@ type ManagedTableState = {|
|
||||
* If you require lower level access to the state then use [`<Table>`]()
|
||||
* directly.
|
||||
*/
|
||||
export default class ManagedTable extends styled.StylablePureComponent<
|
||||
export default class ManagedTable extends styled.StylableComponent<
|
||||
ManagedTableProps,
|
||||
ManagedTableState,
|
||||
> {
|
||||
@@ -124,16 +134,32 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
);
|
||||
};
|
||||
|
||||
state = {
|
||||
state: ManagedTableState = {
|
||||
columnOrder:
|
||||
JSON.parse(window.localStorage.getItem(this.getTableKey()) || 'null') ||
|
||||
this.props.columnOrder,
|
||||
columnSizes: this.props.columnSizes,
|
||||
highlightedRows: [],
|
||||
this.props.columnOrder ||
|
||||
Object.keys(this.props.columns).map(key => ({key, visible: true})),
|
||||
columnSizes: this.props.columnSizes || {},
|
||||
highlightedRows: new Set(),
|
||||
sortOrder: null,
|
||||
shouldScrollToBottom: Boolean(this.props.stickyBottom),
|
||||
};
|
||||
|
||||
tableRef: ?Table;
|
||||
tableRef: {
|
||||
current: null | List,
|
||||
} = React.createRef();
|
||||
scrollRef: {
|
||||
current: null | HTMLDivElement,
|
||||
} = React.createRef();
|
||||
dragStartIndex: ?number = null;
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: ManagedTableProps) {
|
||||
// if columnSizes has changed
|
||||
@@ -152,20 +178,64 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
columnOrder: nextProps.columnOrder,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
nextProps.filter !== this.props.filter &&
|
||||
this.tableRef &&
|
||||
this.tableRef.current
|
||||
) {
|
||||
// rows were filtered, we need to recalculate heights
|
||||
this.tableRef.current.resetAfterIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
onHighlight = (highlightedRows: TableHighlightedRows) => {
|
||||
if (this.props.highlightableRows === false) {
|
||||
componentDidUpdate(prevProps: ManagedTableProps) {
|
||||
if (
|
||||
this.props.rows.length !== prevProps.rows.length &&
|
||||
this.state.shouldScrollToBottom &&
|
||||
this.state.highlightedRows.size < 2
|
||||
) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
onCopy = () => {
|
||||
clipboard.writeText(this.getSelectedText());
|
||||
};
|
||||
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
const {highlightedRows} = this.state;
|
||||
if (highlightedRows.size === 0) {
|
||||
return;
|
||||
}
|
||||
if (this.props.multiHighlight !== true) {
|
||||
highlightedRows = highlightedRows.slice(0, 1);
|
||||
if (
|
||||
((e.metaKey && process.platform === 'darwin') ||
|
||||
(e.ctrlKey && process.platform !== 'darwin')) &&
|
||||
e.keyCode === 67
|
||||
) {
|
||||
this.onCopy();
|
||||
} else if (e.keyCode === 38 || e.keyCode === 40) {
|
||||
// arrow navigation
|
||||
const {rows} = this.props;
|
||||
const {highlightedRows} = this.state;
|
||||
const lastItemKey = Array.from(this.state.highlightedRows).pop();
|
||||
const lastItemIndex = this.props.rows.findIndex(
|
||||
row => row.key === lastItemKey,
|
||||
);
|
||||
const newIndex = Math.min(
|
||||
rows.length - 1,
|
||||
Math.max(0, e.keyCode === 38 ? lastItemIndex - 1 : lastItemIndex + 1),
|
||||
);
|
||||
if (!e.shiftKey) {
|
||||
highlightedRows.clear();
|
||||
}
|
||||
|
||||
this.setState({highlightedRows});
|
||||
|
||||
if (this.props.onRowHighlighted) {
|
||||
this.props.onRowHighlighted(highlightedRows);
|
||||
highlightedRows.add(rows[newIndex].key);
|
||||
this.setState({highlightedRows}, () => {
|
||||
const {current} = this.tableRef;
|
||||
if (current) {
|
||||
current.scrollToItem(newIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,7 +244,6 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
};
|
||||
|
||||
onColumnOrder = (columnOrder: TableColumnOrder) => {
|
||||
// $FlowFixMe
|
||||
this.setState({columnOrder});
|
||||
// persist column order
|
||||
window.localStorage.setItem(
|
||||
@@ -187,45 +256,246 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
this.setState({columnSizes});
|
||||
};
|
||||
|
||||
setRef = (table: ?Table) => {
|
||||
this.tableRef = table;
|
||||
scrollToBottom() {
|
||||
const {current} = this.tableRef;
|
||||
if (current && this.props.rows.length > 1) {
|
||||
current.scrollToItem(this.props.rows.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
onHighlight = (
|
||||
e: SyntheticMouseEvent<>,
|
||||
row: TableBodyRow,
|
||||
index: number,
|
||||
) => {
|
||||
if (e.button !== 0) {
|
||||
// Only highlight rows when using primary mouse button,
|
||||
// otherwise do nothing, to not interfere context menus.
|
||||
return;
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
// prevents text selection
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
let {highlightedRows} = this.state;
|
||||
|
||||
this.dragStartIndex = index;
|
||||
document.addEventListener('mouseup', this.onStopDragSelecting);
|
||||
|
||||
if (
|
||||
(e.metaKey && process.platform === 'darwin') ||
|
||||
(e.ctrlKey && process.platform !== 'darwin')
|
||||
) {
|
||||
highlightedRows.add(row.key);
|
||||
} else if (e.shiftKey) {
|
||||
// range select
|
||||
const lastItemKey = Array.from(this.state.highlightedRows).pop();
|
||||
highlightedRows = new Set([
|
||||
...highlightedRows,
|
||||
...this.selectInRange(lastItemKey, row.key),
|
||||
]);
|
||||
} else {
|
||||
// single select
|
||||
this.state.highlightedRows.clear();
|
||||
this.state.highlightedRows.add(row.key);
|
||||
}
|
||||
|
||||
this.setState({highlightedRows});
|
||||
};
|
||||
|
||||
scrollToBottom() {
|
||||
const {tableRef} = this;
|
||||
if (tableRef) {
|
||||
tableRef.scrollToBottom();
|
||||
onStopDragSelecting = () => {
|
||||
this.dragStartIndex = null;
|
||||
document.removeEventListener('mouseup', this.onStopDragSelecting);
|
||||
};
|
||||
|
||||
selectInRange = (fromKey: string, toKey: string): Array<string> => {
|
||||
const selected = [];
|
||||
let startIndex = -1;
|
||||
let endIndex = -1;
|
||||
for (let i = 0; i < this.props.rows.length; i++) {
|
||||
if (this.props.rows[i].key === fromKey) {
|
||||
startIndex = i;
|
||||
}
|
||||
if (this.props.rows[i].key === toKey) {
|
||||
endIndex = i;
|
||||
}
|
||||
if (endIndex > -1 && startIndex > -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
let i = Math.min(startIndex, endIndex);
|
||||
i <= Math.max(startIndex, endIndex);
|
||||
i++
|
||||
) {
|
||||
try {
|
||||
selected.push(this.props.rows[i].key);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return selected;
|
||||
};
|
||||
|
||||
onMouseEnterRow = (
|
||||
e: SyntheticMouseEvent<>,
|
||||
row: TableBodyRow,
|
||||
index: number,
|
||||
) => {
|
||||
const {dragStartIndex} = this;
|
||||
const {current} = this.tableRef;
|
||||
if (dragStartIndex && current) {
|
||||
current.scrollToItem(index + 1);
|
||||
const startKey = this.props.rows[dragStartIndex].key;
|
||||
const highlightedRows = new Set(this.selectInRange(startKey, row.key));
|
||||
this.setState({
|
||||
highlightedRows,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
buildContextMenuItems = () => {
|
||||
const {highlightedRows} = this.state;
|
||||
if (highlightedRows.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label:
|
||||
highlightedRows.size > 1
|
||||
? `Copy ${highlightedRows.size} rows`
|
||||
: 'Copy row',
|
||||
click: this.onCopy,
|
||||
},
|
||||
{
|
||||
label: 'Create Paste',
|
||||
click: () => createPaste(this.getSelectedText()),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
getSelectedText = (): string => {
|
||||
const {highlightedRows} = this.state;
|
||||
|
||||
if (highlightedRows.size === 0) {
|
||||
return '';
|
||||
}
|
||||
return this.props.rows
|
||||
.filter(row => highlightedRows.has(row.key))
|
||||
.map(
|
||||
(row: TableBodyRow) =>
|
||||
row.copyText ||
|
||||
Array.from(
|
||||
document.querySelectorAll(`[data-key='${row.key}'] > *`) || [],
|
||||
)
|
||||
.map(node => node.textContent)
|
||||
.join('\t'),
|
||||
)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
onScroll = ({
|
||||
scrollDirection,
|
||||
scrollOffset,
|
||||
}: {
|
||||
scrollDirection: 'forward' | 'backward',
|
||||
scrollOffset: number,
|
||||
scrollUpdateWasRequested: boolean,
|
||||
}) => {
|
||||
const {current} = this.scrollRef;
|
||||
const parent = current ? current.parentElement : null;
|
||||
if (
|
||||
this.props.stickyBottom &&
|
||||
scrollDirection === 'forward' &&
|
||||
!this.state.shouldScrollToBottom &&
|
||||
current &&
|
||||
parent instanceof HTMLElement &&
|
||||
current.offsetHeight - (scrollOffset + parent.offsetHeight) <
|
||||
parent.offsetHeight
|
||||
) {
|
||||
this.setState({shouldScrollToBottom: true});
|
||||
} else if (
|
||||
this.props.stickyBottom &&
|
||||
scrollDirection === 'backward' &&
|
||||
this.state.shouldScrollToBottom
|
||||
) {
|
||||
this.setState({shouldScrollToBottom: false});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
const {
|
||||
onAddFilter,
|
||||
columns,
|
||||
multiline,
|
||||
zebra,
|
||||
rows,
|
||||
rowLineHeight,
|
||||
} = this.props;
|
||||
|
||||
const {columnOrder, columnSizes, highlightedRows} = this.state;
|
||||
const columnKeys = columnOrder
|
||||
.map(k => (k.visible ? k.key : null))
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<Table
|
||||
ref={this.setRef}
|
||||
virtual={props.virtual}
|
||||
floating={props.floating}
|
||||
multiline={props.multiline}
|
||||
columns={props.columns}
|
||||
rows={props.rows}
|
||||
rowLineHeight={props.rowLineHeight}
|
||||
autoHeight={props.autoHeight}
|
||||
filter={props.filter}
|
||||
filterValue={props.filterValue}
|
||||
highlightedRows={state.highlightedRows}
|
||||
onHighlight={this.onHighlight}
|
||||
sortOrder={state.sortOrder}
|
||||
onSort={this.onSort}
|
||||
columnOrder={state.columnOrder}
|
||||
<FlexColumn style={{flexGrow: 1}}>
|
||||
<TableHead
|
||||
columnOrder={columnOrder}
|
||||
onColumnOrder={this.onColumnOrder}
|
||||
columnSizes={state.columnSizes}
|
||||
columns={columns}
|
||||
onColumnResize={this.onColumnResize}
|
||||
stickyBottom={props.stickyBottom}
|
||||
onAddFilter={props.onAddFilter}
|
||||
zebra={props.zebra}
|
||||
hideHeader={props.hideHeader}
|
||||
sortOrder={this.state.sortOrder}
|
||||
columnSizes={columnSizes}
|
||||
onSort={this.onSort}
|
||||
/>
|
||||
<FlexColumn
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
<AutoSizer>
|
||||
{({width, height}) => (
|
||||
<ContextMenu buildItems={this.buildContextMenuItems}>
|
||||
<List
|
||||
itemCount={this.props.rows.length}
|
||||
itemSize={index =>
|
||||
(rows[index] && rows[index].height) ||
|
||||
rowLineHeight ||
|
||||
DEFAULT_ROW_HEIGHT
|
||||
}
|
||||
ref={this.tableRef}
|
||||
width={width}
|
||||
estimatedItemSize={rowLineHeight || DEFAULT_ROW_HEIGHT}
|
||||
overscanCount={5}
|
||||
innerRef={this.scrollRef}
|
||||
onScroll={this.onScroll}
|
||||
height={height}>
|
||||
{({index, style}) => (
|
||||
<TableRow
|
||||
columnSizes={columnSizes}
|
||||
columnKeys={columnKeys}
|
||||
onMouseDown={e => this.onHighlight(e, rows[index], index)}
|
||||
onMouseEnter={e =>
|
||||
this.onMouseEnterRow(e, rows[index], index)
|
||||
}
|
||||
multiline={multiline}
|
||||
rowLineHeight={24}
|
||||
highlighted={highlightedRows.has(rows[index].key)}
|
||||
row={rows[index]}
|
||||
index={index}
|
||||
style={style}
|
||||
onAddFilter={onAddFilter}
|
||||
zebra={zebra}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</FlexColumn>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {
|
||||
TableColumnRawOrder,
|
||||
TableColumnKeys,
|
||||
TableColumnOrder,
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
TableHighlightedRows,
|
||||
TableOnColumnOrder,
|
||||
TableOnColumnResize,
|
||||
TableOnHighlight,
|
||||
TableOnSort,
|
||||
TableRowSortOrder,
|
||||
TableBodyRow,
|
||||
TableRows,
|
||||
TableOnAddFilter,
|
||||
} from './types.js';
|
||||
import {PureComponent} from 'react';
|
||||
import FlexColumn from '../FlexColumn.js';
|
||||
import TableHead from './TableHead.js';
|
||||
import TableBody from './TableBody.js';
|
||||
import FlexBox from '../FlexBox.js';
|
||||
import createPaste from '../../../utils/createPaste.js';
|
||||
import textContent from '../../../utils/textContent.js';
|
||||
import {clipboard} from 'electron';
|
||||
|
||||
const TableInner = FlexColumn.extends(
|
||||
{
|
||||
minWidth: props => props.minWidth || '0',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
},
|
||||
{
|
||||
ignoreAttributes: ['minWidth'],
|
||||
},
|
||||
);
|
||||
|
||||
const TableOuter = FlexBox.extends(
|
||||
{
|
||||
width: '100%',
|
||||
backgroundColor: '#fff',
|
||||
border: props => (props.floating ? '1px solid #c9ced4' : 'none'),
|
||||
borderRadius: props => (props.floating ? 2 : 'none'),
|
||||
height: props => (props.autoHeight ? 'auto' : '100%'),
|
||||
overflow: props => (props.autoHeight ? 'visible' : 'auto'),
|
||||
},
|
||||
{
|
||||
ignoreAttributes: ['floating', 'autoHeight'],
|
||||
},
|
||||
);
|
||||
|
||||
function getColumnOrder(
|
||||
colOrder: ?TableColumnRawOrder,
|
||||
cols: TableColumns,
|
||||
): TableColumnOrder {
|
||||
// we have a specific column order, let's validate it
|
||||
if (colOrder) {
|
||||
const computedOrder = [];
|
||||
for (const obj of colOrder) {
|
||||
if (typeof obj === 'string') {
|
||||
computedOrder.push({key: obj, visible: true});
|
||||
} else {
|
||||
computedOrder.push(obj);
|
||||
}
|
||||
}
|
||||
return computedOrder;
|
||||
}
|
||||
|
||||
// produce a column order
|
||||
const keys = Object.keys(cols);
|
||||
const computedOrder = [];
|
||||
for (const key of keys) {
|
||||
computedOrder.push({key, visible: true});
|
||||
}
|
||||
return computedOrder;
|
||||
}
|
||||
|
||||
const sortedBodyCache: WeakMap<
|
||||
TableRows,
|
||||
{
|
||||
sortOrder: TableRowSortOrder,
|
||||
rows: TableRows,
|
||||
},
|
||||
> = new WeakMap();
|
||||
function getSortedRows(
|
||||
maybeSortOrder: ?TableRowSortOrder,
|
||||
rows: TableRows,
|
||||
): TableRows {
|
||||
if (!maybeSortOrder) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const sortOrder: TableRowSortOrder = maybeSortOrder;
|
||||
|
||||
const cached = sortedBodyCache.get(rows);
|
||||
if (cached && cached.sortOrder === sortOrder) {
|
||||
return cached.rows;
|
||||
}
|
||||
|
||||
let sortedRows = rows.sort((a, b) => {
|
||||
const aVal = a.columns[sortOrder.key].sortValue;
|
||||
const bVal = b.columns[sortOrder.key].sortValue;
|
||||
|
||||
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
||||
return aVal.localeCompare(bVal);
|
||||
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
return aVal - bVal;
|
||||
} else {
|
||||
throw new Error('Unsure how to sort this');
|
||||
}
|
||||
});
|
||||
|
||||
if (sortOrder.direction === 'up') {
|
||||
sortedRows = sortedRows.reverse();
|
||||
}
|
||||
|
||||
sortedBodyCache.set(rows, {
|
||||
rows: sortedRows,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
return sortedRows;
|
||||
}
|
||||
|
||||
const getRowsInRange = (
|
||||
from: string,
|
||||
to: string,
|
||||
rows: TableRows,
|
||||
): TableHighlightedRows => {
|
||||
let fromFound = false;
|
||||
let toFound = false;
|
||||
const range = [];
|
||||
if (from === to) {
|
||||
return [from];
|
||||
}
|
||||
for (const {key} of rows) {
|
||||
if (key === from) {
|
||||
fromFound = true;
|
||||
} else if (key === to) {
|
||||
toFound = true;
|
||||
}
|
||||
|
||||
if (fromFound && !toFound) {
|
||||
// range going downwards
|
||||
range.push(key);
|
||||
} else if (toFound && !fromFound) {
|
||||
// range going upwards
|
||||
range.unshift(key);
|
||||
} else if (fromFound && toFound) {
|
||||
// add last item
|
||||
if (key === from) {
|
||||
range.unshift(key);
|
||||
} else {
|
||||
range.push(key);
|
||||
}
|
||||
// we're done
|
||||
break;
|
||||
}
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
const filterRows = (
|
||||
rows: TableRows,
|
||||
filterValue: ?string,
|
||||
filter: ?(row: TableBodyRow) => boolean,
|
||||
): TableRows => {
|
||||
// check that we don't have a filter
|
||||
const hasFilterValue = filterValue !== '' && filterValue != null;
|
||||
const hasFilter = hasFilterValue || typeof filter === 'function';
|
||||
if (!hasFilter) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
let filteredRows = [];
|
||||
|
||||
if (hasFilter) {
|
||||
for (const row of rows) {
|
||||
let keep = false;
|
||||
|
||||
// check if this row's filterValue contains the current filter
|
||||
if (filterValue != null && row.filterValue != null) {
|
||||
keep = row.filterValue.includes(filterValue);
|
||||
}
|
||||
|
||||
// call filter() prop
|
||||
if (keep === false && typeof filter === 'function') {
|
||||
keep = filter(row);
|
||||
}
|
||||
|
||||
if (keep) {
|
||||
filteredRows.push(row);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filteredRows = rows;
|
||||
}
|
||||
|
||||
return filteredRows;
|
||||
};
|
||||
|
||||
type TableProps = {|
|
||||
/**
|
||||
* Column definitions.
|
||||
*/
|
||||
columns: TableColumns,
|
||||
/**
|
||||
* Row definitions.
|
||||
*/
|
||||
rows: TableRows,
|
||||
|
||||
/**
|
||||
* Minimum width of the table. If the table is sized smaller than this then
|
||||
* it's scrollable.
|
||||
*/
|
||||
minWidth?: number,
|
||||
|
||||
/**
|
||||
* Whether to use a virtual list. Items visible in the viewport are the only
|
||||
* included in the DOM. This can have a noticable performance improvement.
|
||||
*/
|
||||
virtual?: boolean,
|
||||
/**
|
||||
* Whether the table has a border.
|
||||
*/
|
||||
floating?: boolean,
|
||||
/**
|
||||
* Whether a row can span over multiple lines. Otherwise lines cannot wrap and
|
||||
* are truncated.
|
||||
*/
|
||||
multiline?: boolean,
|
||||
/**
|
||||
* Height of each row.
|
||||
*/
|
||||
rowLineHeight?: number,
|
||||
/**
|
||||
* Whether the body is scrollable. When this is set to `true` then the table
|
||||
* is not scrollable.
|
||||
*/
|
||||
autoHeight?: boolean,
|
||||
/**
|
||||
* Highlighted rows.
|
||||
*/
|
||||
highlightedRows?: ?TableHighlightedRows,
|
||||
/**
|
||||
* Callback when the highlighted rows change.
|
||||
*/
|
||||
onHighlight?: ?TableOnHighlight,
|
||||
/**
|
||||
* Enable or disable zebra striping
|
||||
*/
|
||||
zebra?: boolean,
|
||||
/**
|
||||
* Value to filter rows on. Alternative to the `filter` prop.
|
||||
*/
|
||||
filterValue?: string,
|
||||
/**
|
||||
* Callback to filter rows.
|
||||
*/
|
||||
filter?: (row: TableBodyRow) => boolean,
|
||||
|
||||
/**
|
||||
* Sort order.
|
||||
*/
|
||||
sortOrder?: ?TableRowSortOrder,
|
||||
/**
|
||||
* Callback when the sort order changes.
|
||||
*/
|
||||
onSort?: ?TableOnSort,
|
||||
|
||||
/**
|
||||
* Order of columns.
|
||||
*/
|
||||
columnOrder?: ?TableColumnRawOrder,
|
||||
/**
|
||||
* Callback when a column is reordered or visibility changed.
|
||||
*/
|
||||
onColumnOrder?: ?TableOnColumnOrder,
|
||||
|
||||
/**
|
||||
* Size of the columns.
|
||||
*/
|
||||
columnSizes?: ?TableColumnSizes,
|
||||
/**
|
||||
* Callback for when a column size changes.
|
||||
*/
|
||||
onColumnResize?: ?TableOnColumnResize,
|
||||
/**
|
||||
* This makes it so the scroll position sticks to the bottom of the window.
|
||||
* Useful for streaming data like requests, logs etc.
|
||||
*/
|
||||
stickyBottom?: ?boolean,
|
||||
/**
|
||||
* Used by SearchableTable to add filters for rows.
|
||||
*/
|
||||
onAddFilter?: TableOnAddFilter,
|
||||
/**
|
||||
* Whether to hide the column names at the top of the table.
|
||||
*/
|
||||
hideHeader?: boolean,
|
||||
|};
|
||||
|
||||
type TableState = {
|
||||
columnOrder: TableColumnOrder,
|
||||
columnSizes: TableColumnSizes,
|
||||
columnKeys: TableColumnKeys,
|
||||
sortedRows: TableRows,
|
||||
dragStartingKey?: ?string,
|
||||
};
|
||||
|
||||
const NO_COLUMN_SIZE: TableColumnSizes = {};
|
||||
|
||||
/**
|
||||
* A table component with all the native features you would expect.
|
||||
*
|
||||
* - Row sorting
|
||||
* - Row filtering
|
||||
* - Row highlight
|
||||
* - Row keyboard navigation
|
||||
* - Column reordering
|
||||
* - Column visibility
|
||||
*
|
||||
* This component is fairly low level. It's likely you're looking for
|
||||
* [`<ManagedTable>`]().
|
||||
*/
|
||||
export default class Table extends PureComponent<TableProps, TableState> {
|
||||
constructor(props: TableProps, context: Object) {
|
||||
super(props, context);
|
||||
this.state = this.deriveState(props);
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
floating: true,
|
||||
virtual: true,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// listning to mouseUp event on document to catch events even when
|
||||
// the cursor moved outside the table while dragging
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
}
|
||||
|
||||
deriveState(props: TableProps): TableState {
|
||||
const columnSizes: TableColumnSizes = props.columnSizes || NO_COLUMN_SIZE;
|
||||
const columnOrder: TableColumnOrder = getColumnOrder(
|
||||
props.columnOrder,
|
||||
props.columns,
|
||||
);
|
||||
|
||||
let columnKeys;
|
||||
if (this.state && this.state.columnOrder === columnOrder) {
|
||||
columnKeys = this.state.columnKeys;
|
||||
} else {
|
||||
columnKeys = [];
|
||||
for (const {key, visible} of columnOrder) {
|
||||
if (visible) {
|
||||
columnKeys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sortedRows = [];
|
||||
if (
|
||||
!this.state ||
|
||||
this.props.filter !== props.filter ||
|
||||
this.props.filterValue !== props.filterValue ||
|
||||
this.props.sortOrder !== props.sortOrder ||
|
||||
this.props.rows !== props.rows
|
||||
) {
|
||||
// need to reorder or refilter the rows
|
||||
sortedRows = getSortedRows(
|
||||
props.sortOrder,
|
||||
filterRows(props.rows, props.filterValue, props.filter),
|
||||
);
|
||||
} else {
|
||||
sortedRows = this.state.sortedRows;
|
||||
}
|
||||
|
||||
return {
|
||||
columnKeys,
|
||||
columnOrder,
|
||||
columnSizes,
|
||||
sortedRows,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: TableProps) {
|
||||
this.setState(this.deriveState(nextProps));
|
||||
}
|
||||
|
||||
onMouseUp = () => this.setState({dragStartingKey: null});
|
||||
|
||||
onKeyDown = (e: SyntheticKeyboardEvent<HTMLElement>) => {
|
||||
const {onHighlight, highlightedRows} = this.props;
|
||||
const {sortedRows} = this.state;
|
||||
const currentlyHighlightedRows = highlightedRows || [];
|
||||
let selectedRow: ?string;
|
||||
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (currentlyHighlightedRows.length === 0) {
|
||||
// no selection yet
|
||||
const index = e.key === 'ArrowUp' ? sortedRows.length - 1 : 0;
|
||||
selectedRow = sortedRows[index].key;
|
||||
} else {
|
||||
// determine sibling row to select
|
||||
const prevRowFinder = (row, index) =>
|
||||
index < sortedRows.length - 1
|
||||
? sortedRows[index + 1].key ===
|
||||
currentlyHighlightedRows[currentlyHighlightedRows.length - 1]
|
||||
: false;
|
||||
|
||||
const nextRowFinder = (row, index) =>
|
||||
index > 0
|
||||
? sortedRows[index - 1].key ===
|
||||
currentlyHighlightedRows[currentlyHighlightedRows.length - 1]
|
||||
: false;
|
||||
|
||||
const siblingRow = sortedRows.find(
|
||||
e.key === 'ArrowUp' ? prevRowFinder : nextRowFinder,
|
||||
);
|
||||
if (siblingRow) {
|
||||
selectedRow = siblingRow.key;
|
||||
}
|
||||
}
|
||||
|
||||
if (onHighlight && selectedRow != null) {
|
||||
// scroll into view
|
||||
const index = sortedRows.findIndex(row => row.key === selectedRow);
|
||||
if (this.tableBodyRef && index) {
|
||||
this.tableBodyRef.scrollRowIntoView(index);
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
onHighlight(
|
||||
currentlyHighlightedRows
|
||||
.filter(row => selectedRow !== row)
|
||||
.concat([selectedRow]),
|
||||
e,
|
||||
);
|
||||
} else {
|
||||
onHighlight([selectedRow], e);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
highlightedRows &&
|
||||
e.key === 'c' &&
|
||||
((e.metaKey && process.platform === 'darwin') ||
|
||||
(e.ctrlKey && process.platform !== 'darwin'))
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.onCopyRows();
|
||||
}
|
||||
};
|
||||
|
||||
getRowText = (): string => {
|
||||
const {highlightedRows} = this.props;
|
||||
const {sortedRows} = this.state;
|
||||
const visibleColums = this.state.columnOrder
|
||||
.filter(({visible}) => visible)
|
||||
.map(({key}) => key);
|
||||
|
||||
const rows =
|
||||
!highlightedRows || highlightedRows.length === 0
|
||||
? sortedRows
|
||||
: sortedRows.filter(row => highlightedRows.indexOf(row.key) > -1);
|
||||
|
||||
return rows
|
||||
.map(
|
||||
row =>
|
||||
row.copyText != null
|
||||
? row.copyText
|
||||
: visibleColums
|
||||
.map(col => textContent(row.columns[col].value))
|
||||
.filter(Boolean)
|
||||
.join('\t'),
|
||||
)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
onCopyRows = () => {
|
||||
clipboard.writeText(this.getRowText());
|
||||
};
|
||||
|
||||
onCreatePaste = () => {
|
||||
createPaste(this.getRowText());
|
||||
};
|
||||
|
||||
onHighlight = (
|
||||
newHighlightedRows: TableHighlightedRows,
|
||||
e: SyntheticKeyboardEvent<*>,
|
||||
) => {
|
||||
const {onHighlight, highlightedRows} = this.props;
|
||||
if (!onHighlight) {
|
||||
return;
|
||||
}
|
||||
if (e.shiftKey === true && highlightedRows && highlightedRows.length > 0) {
|
||||
const from = highlightedRows[highlightedRows.length - 1];
|
||||
const to = newHighlightedRows[0];
|
||||
const range = getRowsInRange(from, to, this.state.sortedRows);
|
||||
newHighlightedRows = highlightedRows
|
||||
.filter(key => range.indexOf(key) === -1)
|
||||
.concat(range);
|
||||
} else {
|
||||
this.setState({dragStartingKey: newHighlightedRows[0]});
|
||||
}
|
||||
|
||||
onHighlight(newHighlightedRows, e);
|
||||
};
|
||||
|
||||
onDragSelect = (e: SyntheticMouseEvent<>, key: string, index: number) => {
|
||||
const {dragStartingKey, sortedRows} = this.state;
|
||||
const {onHighlight} = this.props;
|
||||
if (dragStartingKey != null && onHighlight != null) {
|
||||
const range = getRowsInRange(dragStartingKey, key, this.state.sortedRows);
|
||||
if (this.tableBodyRef) {
|
||||
const startIndex = sortedRows.findIndex(
|
||||
row => row.key === dragStartingKey,
|
||||
);
|
||||
const nextIndex = startIndex < index ? index + 1 : index - 1;
|
||||
// only scroll one row every 100ms to not scroll to the end of the table immediatelly
|
||||
setTimeout(
|
||||
() =>
|
||||
this.tableBodyRef && this.tableBodyRef.scrollRowIntoView(nextIndex),
|
||||
100,
|
||||
);
|
||||
}
|
||||
onHighlight(range, e);
|
||||
}
|
||||
};
|
||||
|
||||
scrollToBottom() {
|
||||
const {tableBodyRef} = this;
|
||||
if (tableBodyRef) {
|
||||
tableBodyRef.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
tableBodyRef: ?TableBody;
|
||||
|
||||
setTableBodyRef = (ref: ?TableBody) => {
|
||||
this.tableBodyRef = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
|
||||
const tableHead =
|
||||
props.hideHeader === true ? null : (
|
||||
<TableHead
|
||||
columnOrder={state.columnOrder}
|
||||
onColumnOrder={props.onColumnOrder}
|
||||
columnKeys={state.columnKeys}
|
||||
columns={props.columns}
|
||||
sortOrder={props.sortOrder}
|
||||
onSort={props.onSort}
|
||||
columnSizes={state.columnSizes}
|
||||
onColumnResize={props.onColumnResize}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableOuter
|
||||
floating={props.floating}
|
||||
autoHeight={props.autoHeight}
|
||||
onKeyDown={this.onKeyDown}
|
||||
tabIndex={0}>
|
||||
<TableInner minWidth={props.minWidth}>
|
||||
{tableHead}
|
||||
|
||||
<TableBody
|
||||
ref={this.setTableBodyRef}
|
||||
virtual={props.virtual}
|
||||
filter={props.filter}
|
||||
filterValue={props.filterValue}
|
||||
autoHeight={props.autoHeight}
|
||||
rowLineHeight={props.rowLineHeight}
|
||||
multiline={props.multiline}
|
||||
onHighlight={this.onHighlight}
|
||||
highlightedRows={props.highlightedRows}
|
||||
columnKeys={state.columnKeys}
|
||||
rows={state.sortedRows}
|
||||
columnSizes={state.columnSizes}
|
||||
stickyBottom={props.stickyBottom}
|
||||
isDragging={Boolean(state.dragStartingKey)}
|
||||
zebra={props.zebra}
|
||||
onDragSelect={this.onDragSelect}
|
||||
onCopyRows={this.onCopyRows}
|
||||
onCreatePaste={this.onCreatePaste}
|
||||
onAddFilter={props.onAddFilter}
|
||||
/>
|
||||
</TableInner>
|
||||
</TableOuter>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,559 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {
|
||||
TableBodyRow,
|
||||
TableColumnKeys,
|
||||
TableColumnSizes,
|
||||
TableHighlightedRows,
|
||||
TableOnDragSelect,
|
||||
TableOnHighlight,
|
||||
TableRows,
|
||||
TableOnAddFilter,
|
||||
} from './types.js';
|
||||
import {FixedList, DynamicList} from '../../../ui/virtualized/index.js';
|
||||
import {normaliseColumnWidth} from './utils.js';
|
||||
import {PureComponent} from 'react';
|
||||
|
||||
import FilterRow from '../filter/FilterRow.js';
|
||||
import {DEFAULT_ROW_HEIGHT} from './types.js';
|
||||
import styled from '../../styled/index.js';
|
||||
import FlexColumn from '../FlexColumn.js';
|
||||
import {ContextMenu} from 'sonar';
|
||||
|
||||
import FlexRow from '../FlexRow.js';
|
||||
import {colors} from '../colors.js';
|
||||
|
||||
const TableBodyContainer = FlexColumn.extends(
|
||||
{
|
||||
backgroundColor: colors.white,
|
||||
zIndex: 1,
|
||||
flexGrow: props => (props.autoHeight ? 0 : 1),
|
||||
flexShrink: props => (props.autoHeight ? 0 : 1),
|
||||
flexBasis: props => (props.autoHeight ? 'content' : 0),
|
||||
overflow: props => (props.autoHeight ? 'hidden' : 'auto'),
|
||||
maxWidth: '100%',
|
||||
},
|
||||
{
|
||||
ignoreAttributes: ['autoHeight'],
|
||||
},
|
||||
);
|
||||
|
||||
const TableBodyRowContainer = FlexRow.extends(
|
||||
{
|
||||
backgroundColor: props => {
|
||||
if (props.highlighted) {
|
||||
if (props.highlightedBackgroundColor) {
|
||||
return props.highlightedBackgroundColor;
|
||||
} else {
|
||||
return colors.macOSTitleBarIconSelected;
|
||||
}
|
||||
} else {
|
||||
if (props.backgroundColor) {
|
||||
return props.backgroundColor;
|
||||
} else if (props.even && props.zebra) {
|
||||
return colors.light02;
|
||||
} else {
|
||||
return 'transparent';
|
||||
}
|
||||
}
|
||||
},
|
||||
boxShadow: props => {
|
||||
if (props.backgroundColor || props.zebra === false) {
|
||||
return 'inset 0 -1px #E9EBEE';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
},
|
||||
color: props =>
|
||||
props.highlighted ? colors.white : props.color || 'inherit',
|
||||
'& *': {
|
||||
color: props => (props.highlighted ? `${colors.white} !important` : null),
|
||||
},
|
||||
'& img': {
|
||||
backgroundColor: props =>
|
||||
props.highlighted ? `${colors.white} !important` : 'none',
|
||||
},
|
||||
height: props => (props.multiline ? 'auto' : props.rowLineHeight),
|
||||
lineHeight: props => `${String(props.rowLineHeight)}px`,
|
||||
fontWeight: props => props.fontWeight || 'inherit',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
userSelect: 'none',
|
||||
flexShrink: 0,
|
||||
'&:hover': {
|
||||
backgroundColor: props =>
|
||||
!props.highlighted && props.highlightOnHover ? colors.light02 : 'none',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignoreAttributes: [
|
||||
'highlightedBackgroundColor',
|
||||
'highlightOnHover',
|
||||
'backgroundColor',
|
||||
'rowLineHeight',
|
||||
'highlighted',
|
||||
'multiline',
|
||||
'hasHover',
|
||||
'zebra',
|
||||
'even',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const TableBodyColumnContainer = styled.view(
|
||||
{
|
||||
display: 'flex',
|
||||
flexShrink: props => (props.width === 'flex' ? 1 : 0),
|
||||
overflow: 'hidden',
|
||||
padding: '0 8px',
|
||||
userSelect: 'none',
|
||||
textOverflow: 'ellipsis',
|
||||
verticalAlign: 'top',
|
||||
whiteSpace: props => (props.multiline ? 'normal' : 'nowrap'),
|
||||
wordWrap: props => (props.multiline ? 'break-word' : 'normal'),
|
||||
width: props => (props.width === 'flex' ? '100%' : props.width),
|
||||
maxWidth: '100%',
|
||||
},
|
||||
{
|
||||
ignoreAttributes: ['multiline', 'width'],
|
||||
},
|
||||
);
|
||||
|
||||
type TableBodyRowElementProps = {
|
||||
columnSizes: TableColumnSizes,
|
||||
columnKeys: TableColumnKeys,
|
||||
onHighlight: ?TableOnHighlight,
|
||||
onMouseEnter?: (e: SyntheticMouseEvent<>) => void,
|
||||
multiline: ?boolean,
|
||||
rowLineHeight: number,
|
||||
highlightedRows: ?TableHighlightedRows,
|
||||
row: TableBodyRow,
|
||||
columnNo: number,
|
||||
style: ?Object,
|
||||
onCopyRows: () => void,
|
||||
onCreatePaste: () => void,
|
||||
onAddFilter?: TableOnAddFilter,
|
||||
zebra: ?boolean,
|
||||
};
|
||||
|
||||
type TableBodyRowElementState = {
|
||||
contextMenu: any,
|
||||
};
|
||||
|
||||
class TableBodyRowElement extends PureComponent<
|
||||
TableBodyRowElementProps,
|
||||
TableBodyRowElementState,
|
||||
> {
|
||||
static defaultProps = {
|
||||
zebra: true,
|
||||
};
|
||||
|
||||
onMouseDown = (e: SyntheticMouseEvent<>) => {
|
||||
if (e.button !== 0) {
|
||||
// Only highlight rows when using primary mouse button,
|
||||
// otherwise do nothing, to not interfere context menus.
|
||||
return;
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
// prevents text selection
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const {highlightedRows, onHighlight, row} = this.props;
|
||||
if (!onHighlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newHighlightedRows = highlightedRows ? highlightedRows.slice() : [];
|
||||
const alreadyHighlighted = newHighlightedRows.includes(row.key);
|
||||
if (
|
||||
(e.metaKey && process.platform === 'darwin') ||
|
||||
(e.ctrlKey && process.platform !== 'darwin')
|
||||
) {
|
||||
if (alreadyHighlighted) {
|
||||
newHighlightedRows.splice(newHighlightedRows.indexOf(row.key), 1);
|
||||
} else {
|
||||
newHighlightedRows.push(row.key);
|
||||
}
|
||||
} else {
|
||||
newHighlightedRows = [row.key];
|
||||
}
|
||||
onHighlight(newHighlightedRows, e);
|
||||
};
|
||||
|
||||
getContextMenu = () => {
|
||||
const {highlightedRows, onCopyRows, onCreatePaste} = this.props;
|
||||
return [
|
||||
{
|
||||
label:
|
||||
highlightedRows && highlightedRows.length > 1
|
||||
? `Copy ${highlightedRows.length} items`
|
||||
: 'Copy all',
|
||||
click: onCopyRows,
|
||||
},
|
||||
{
|
||||
label:
|
||||
highlightedRows && highlightedRows.length > 1
|
||||
? `Create paste from selection`
|
||||
: 'Create paste',
|
||||
click: onCreatePaste,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
columnNo,
|
||||
highlightedRows,
|
||||
rowLineHeight,
|
||||
row,
|
||||
style,
|
||||
multiline,
|
||||
columnKeys,
|
||||
columnSizes,
|
||||
onMouseEnter,
|
||||
zebra,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ContextMenu buildItems={this.getContextMenu}>
|
||||
<TableBodyRowContainer
|
||||
rowLineHeight={rowLineHeight}
|
||||
highlightedBackgroundColor={row.highlightedBackgroundColor}
|
||||
backgroundColor={row.backgroundColor}
|
||||
highlighted={highlightedRows && highlightedRows.includes(row.key)}
|
||||
onDoubleClick={row.onDoubleClick}
|
||||
multiline={multiline}
|
||||
even={columnNo % 2 === 0}
|
||||
zebra={zebra}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseEnter={onMouseEnter}
|
||||
style={style}
|
||||
highlightOnHover={row.highlightOnHover}
|
||||
data-key={row.key}
|
||||
{...row.style}>
|
||||
{columnKeys.map(key => {
|
||||
const col = row.columns[key];
|
||||
if (col == null) {
|
||||
throw new Error(
|
||||
`Trying to access column "${key}" which does not exist on row. Make sure buildRow is returning a valid row.`,
|
||||
);
|
||||
}
|
||||
const isFilterable = col.isFilterable || false;
|
||||
const value = col ? col.value : '';
|
||||
const title = col ? col.title : '';
|
||||
return (
|
||||
<TableBodyColumnContainer
|
||||
key={key}
|
||||
title={title}
|
||||
multiline={multiline}
|
||||
width={normaliseColumnWidth(columnSizes[key])}>
|
||||
{isFilterable && this.props.onAddFilter != null ? (
|
||||
<FilterRow addFilter={this.props.onAddFilter} filterKey={key}>
|
||||
{value}
|
||||
</FilterRow>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</TableBodyColumnContainer>
|
||||
);
|
||||
})}
|
||||
</TableBodyRowContainer>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type TableBodyProps = {
|
||||
virtual: ?boolean,
|
||||
autoHeight: ?boolean,
|
||||
multiline: ?boolean,
|
||||
rowLineHeight: number,
|
||||
stickyBottom: ?boolean,
|
||||
zebra?: boolean,
|
||||
|
||||
onHighlight: ?TableOnHighlight,
|
||||
highlightedRows: ?TableHighlightedRows,
|
||||
|
||||
columnKeys: TableColumnKeys,
|
||||
columnSizes: TableColumnSizes,
|
||||
|
||||
rows: TableRows,
|
||||
|
||||
filterValue?: string,
|
||||
filter?: (row: TableBodyRow) => boolean,
|
||||
|
||||
isDragging: boolean,
|
||||
onDragSelect: TableOnDragSelect,
|
||||
onCopyRows: () => void,
|
||||
onCreatePaste: () => void,
|
||||
onAddFilter?: TableOnAddFilter,
|
||||
};
|
||||
|
||||
type TableBodyState = {
|
||||
atScrollBottom: boolean,
|
||||
pureBodyData: Array<any>,
|
||||
};
|
||||
|
||||
export default class TableBody extends PureComponent<
|
||||
TableBodyProps,
|
||||
TableBodyState,
|
||||
> {
|
||||
static defaultProps = {
|
||||
rowLineHeight: DEFAULT_ROW_HEIGHT,
|
||||
};
|
||||
|
||||
state = {
|
||||
atScrollBottom: true,
|
||||
pureBodyData: [
|
||||
this.props.columnSizes,
|
||||
this.props.rows,
|
||||
this.props.highlightedRows,
|
||||
],
|
||||
};
|
||||
|
||||
listRef: ?DynamicList;
|
||||
scrollRef: ?any;
|
||||
keepSelectedRowInView: ?[number, number];
|
||||
|
||||
buildElement = (
|
||||
key: string,
|
||||
row: TableBodyRow,
|
||||
index: number,
|
||||
style?: Object,
|
||||
) => {
|
||||
let onMouseEnter;
|
||||
if (this.props.isDragging) {
|
||||
onMouseEnter = (e: SyntheticMouseEvent<>) =>
|
||||
this.props.onDragSelect(e, key, index);
|
||||
}
|
||||
return (
|
||||
<TableBodyRowElement
|
||||
key={key}
|
||||
columnNo={index}
|
||||
rowLineHeight={this.props.rowLineHeight}
|
||||
row={row}
|
||||
style={style}
|
||||
columnSizes={this.props.columnSizes}
|
||||
multiline={this.props.multiline}
|
||||
columnKeys={this.props.columnKeys}
|
||||
highlightedRows={this.props.highlightedRows}
|
||||
zebra={this.props.zebra}
|
||||
onHighlight={this.props.onHighlight}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onCopyRows={this.props.onCopyRows}
|
||||
onCreatePaste={this.props.onCreatePaste}
|
||||
onAddFilter={this.props.onAddFilter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
buildVirtualElement = ({index, style}: {index: number, style: Object}) => {
|
||||
const row = this.props.rows[index];
|
||||
return this.buildElement(row.key, row, index, style);
|
||||
};
|
||||
|
||||
buildAutoElement = (row: TableBodyRow, index: number) => {
|
||||
return this.buildElement(row.key, row, index);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.maybeScrollToBottom();
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps: TableBodyProps) {
|
||||
if (
|
||||
nextProps.highlightedRows != null &&
|
||||
nextProps.highlightedRows.length === 1 &&
|
||||
nextProps.filter !== this.props.filter &&
|
||||
nextProps.rows.length !== this.props.rows.length &&
|
||||
this.listRef != null
|
||||
) {
|
||||
// We want to keep the selected row in the view once the filter changes.
|
||||
// Here we get the current position, in componentDidUpdate it is scrolled into view
|
||||
const {highlightedRows} = nextProps;
|
||||
const selectedIndex = nextProps.rows.findIndex(
|
||||
row => row.key === highlightedRows[0],
|
||||
);
|
||||
if (
|
||||
nextProps.rows[selectedIndex] != null &&
|
||||
nextProps.rows[selectedIndex].key != null
|
||||
) {
|
||||
const rowDOMNode = document.querySelector(
|
||||
`[data-key="${nextProps.rows[selectedIndex].key}"]`,
|
||||
);
|
||||
let offset = 0;
|
||||
if (
|
||||
rowDOMNode != null &&
|
||||
rowDOMNode.parentElement instanceof HTMLElement
|
||||
) {
|
||||
offset = rowDOMNode.parentElement.offsetTop;
|
||||
}
|
||||
this.keepSelectedRowInView = [selectedIndex, offset];
|
||||
}
|
||||
} else {
|
||||
this.keepSelectedRowInView = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TableBodyProps) {
|
||||
if (this.listRef != null && this.keepSelectedRowInView != null) {
|
||||
this.listRef.scrollToIndex(...this.keepSelectedRowInView);
|
||||
} else {
|
||||
this.maybeScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
maybeScrollToBottom = () => {
|
||||
// we only care if we have the stickyBottom prop
|
||||
if (this.props.stickyBottom !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we only want to scroll to the bottom if we're actually at the bottom
|
||||
if (this.state.atScrollBottom === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
scrollToBottom() {
|
||||
// only handle non-virtualised scrolling, virtualised scrolling is handled
|
||||
// by the getScrollToIndex method
|
||||
if (this.isVirtualisedDisabled()) {
|
||||
const {scrollRef} = this;
|
||||
if (scrollRef != null) {
|
||||
scrollRef.scrollTop = scrollRef.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
const {listRef} = this;
|
||||
if (listRef != null) {
|
||||
listRef.scrollToIndex(this.props.rows.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollRowIntoView(index: number) {
|
||||
if (
|
||||
this.isVirtualisedDisabled() &&
|
||||
this.scrollRef &&
|
||||
index < this.scrollRef.children.length
|
||||
) {
|
||||
this.scrollRef.children[index].scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: TableBodyProps) {
|
||||
if (
|
||||
nextProps.columnSizes !== this.props.columnSizes ||
|
||||
nextProps.rows !== this.props.rows ||
|
||||
nextProps.highlightedRows !== this.props.highlightedRows
|
||||
) {
|
||||
this.setState({
|
||||
pureBodyData: [
|
||||
nextProps.columnSizes,
|
||||
nextProps.rows,
|
||||
nextProps.highlightedRows,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setListRef = (ref: ?DynamicList) => {
|
||||
this.listRef = ref;
|
||||
};
|
||||
|
||||
setNonVirtualScrollRef = (ref: any) => {
|
||||
this.scrollRef = ref;
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
onScroll = ({
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
scrollTop,
|
||||
}: {
|
||||
clientHeight: number,
|
||||
scrollHeight: number,
|
||||
scrollTop: number,
|
||||
}) => {
|
||||
// check if the user has scrolled within 20px of the bottom
|
||||
const bottom = scrollTop + clientHeight;
|
||||
const atScrollBottom = Math.abs(bottom - scrollHeight) < 20;
|
||||
|
||||
if (atScrollBottom !== this.state.atScrollBottom) {
|
||||
this.setState({atScrollBottom});
|
||||
}
|
||||
};
|
||||
|
||||
isVirtualisedDisabled() {
|
||||
return this.props.virtual === false || this.props.autoHeight === true;
|
||||
}
|
||||
|
||||
keyMapper = (index: number): string => {
|
||||
return this.props.rows[index].key;
|
||||
};
|
||||
|
||||
getPrecalculatedDimensions = (index: number) => {
|
||||
const row = this.props.rows[index];
|
||||
if (row != null && row.height != null) {
|
||||
return {
|
||||
height: row.height,
|
||||
width: '100%',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.isVirtualisedDisabled()) {
|
||||
return (
|
||||
<TableBodyContainer
|
||||
innerRef={this.setNonVirtualScrollRef}
|
||||
onScroll={this.onScroll}
|
||||
autoHeight={true}>
|
||||
{this.props.rows.map(this.buildAutoElement)}
|
||||
</TableBodyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
let children;
|
||||
|
||||
if (this.props.multiline === true) {
|
||||
// multiline has a virtual list with dynamic heights
|
||||
children = (
|
||||
<DynamicList
|
||||
ref={this.setListRef}
|
||||
pureData={this.state.pureBodyData}
|
||||
keyMapper={this.keyMapper}
|
||||
rowCount={this.props.rows.length}
|
||||
rowRenderer={this.buildVirtualElement}
|
||||
onScroll={this.onScroll}
|
||||
getPrecalculatedDimensions={this.getPrecalculatedDimensions}
|
||||
onMount={this.maybeScrollToBottom}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// virtual list with a fixed row height
|
||||
children = (
|
||||
<FixedList
|
||||
pureData={this.state.pureBodyData}
|
||||
keyMapper={this.keyMapper}
|
||||
rowCount={this.props.rows.length}
|
||||
rowHeight={this.props.rowLineHeight}
|
||||
rowRenderer={this.buildVirtualElement}
|
||||
onScroll={this.onScroll}
|
||||
innerRef={this.setListRef}
|
||||
onMount={this.maybeScrollToBottom}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <TableBodyContainer>{children}</TableBodyContainer>;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
TableColumnKeys,
|
||||
TableColumnOrder,
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
@@ -193,7 +192,6 @@ class TableHeadColumn extends PureComponent<{
|
||||
export default class TableHead extends PureComponent<{
|
||||
columnOrder: TableColumnOrder,
|
||||
onColumnOrder: ?(order: TableColumnOrder) => void,
|
||||
columnKeys: TableColumnKeys,
|
||||
columns: TableColumns,
|
||||
sortOrder: ?TableRowSortOrder,
|
||||
onSort: ?TableOnSort,
|
||||
@@ -201,8 +199,15 @@ export default class TableHead extends PureComponent<{
|
||||
onColumnResize: ?TableOnColumnResize,
|
||||
}> {
|
||||
buildContextMenu = (): MenuTemplate => {
|
||||
const visibles = this.props.columnOrder
|
||||
.map(c => (c.visible ? c.key : null))
|
||||
.filter(Boolean)
|
||||
.reduce((acc, cv) => {
|
||||
acc.add(cv);
|
||||
return acc;
|
||||
}, new Set());
|
||||
return Object.keys(this.props.columns).map(key => {
|
||||
const visible = this.props.columnKeys.includes(key);
|
||||
const visible = visibles.has(key);
|
||||
return {
|
||||
label: this.props.columns[key].value,
|
||||
click: () => {
|
||||
|
||||
181
src/ui/components/table/TableRow.js
Normal file
181
src/ui/components/table/TableRow.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {
|
||||
TableColumnKeys,
|
||||
TableColumnSizes,
|
||||
TableOnAddFilter,
|
||||
TableBodyRow,
|
||||
} from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
import FilterRow from '../filter/FilterRow.js';
|
||||
import styled from '../../styled/index.js';
|
||||
import FlexRow from '../FlexRow.js';
|
||||
import {colors} from '../colors.js';
|
||||
import {normaliseColumnWidth} from './utils.js';
|
||||
import {DEFAULT_ROW_HEIGHT} from './types';
|
||||
|
||||
const TableBodyRowContainer = FlexRow.extends(
|
||||
{
|
||||
backgroundColor: props => {
|
||||
if (props.highlighted) {
|
||||
if (props.highlightedBackgroundColor) {
|
||||
return props.highlightedBackgroundColor;
|
||||
} else {
|
||||
return colors.macOSTitleBarIconSelected;
|
||||
}
|
||||
} else {
|
||||
if (props.backgroundColor) {
|
||||
return props.backgroundColor;
|
||||
} else if (props.even && props.zebra) {
|
||||
return colors.light02;
|
||||
} else {
|
||||
return 'transparent';
|
||||
}
|
||||
}
|
||||
},
|
||||
boxShadow: props => (props.zebra ? 'none' : 'inset 0 -1px #E9EBEE'),
|
||||
color: props =>
|
||||
props.highlighted ? colors.white : props.color || 'inherit',
|
||||
'& *': {
|
||||
color: props => (props.highlighted ? `${colors.white} !important` : null),
|
||||
},
|
||||
'& img': {
|
||||
backgroundColor: props =>
|
||||
props.highlighted ? `${colors.white} !important` : 'none',
|
||||
},
|
||||
height: props => (props.multiline ? 'auto' : props.rowLineHeight),
|
||||
lineHeight: props =>
|
||||
`${String(props.rowLineHeight || DEFAULT_ROW_HEIGHT)}px`,
|
||||
fontWeight: props => props.fontWeight || 'inherit',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
userSelect: 'none',
|
||||
flexShrink: 0,
|
||||
'&:hover': {
|
||||
backgroundColor: props =>
|
||||
!props.highlighted && props.highlightOnHover ? colors.light02 : 'none',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignoreAttributes: [
|
||||
'highlightedBackgroundColor',
|
||||
'highlightOnHover',
|
||||
'backgroundColor',
|
||||
'rowLineHeight',
|
||||
'highlighted',
|
||||
'multiline',
|
||||
'hasHover',
|
||||
'zebra',
|
||||
'even',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const TableBodyColumnContainer = styled.view(
|
||||
{
|
||||
display: 'flex',
|
||||
flexShrink: props => (props.width === 'flex' ? 1 : 0),
|
||||
overflow: 'hidden',
|
||||
padding: '0 8px',
|
||||
userSelect: 'none',
|
||||
textOverflow: 'ellipsis',
|
||||
verticalAlign: 'top',
|
||||
whiteSpace: props => (props.multiline ? 'normal' : 'nowrap'),
|
||||
wordWrap: props => (props.multiline ? 'break-word' : 'normal'),
|
||||
width: props => (props.width === 'flex' ? '100%' : props.width),
|
||||
maxWidth: '100%',
|
||||
},
|
||||
{
|
||||
ignoreAttributes: ['multiline', 'width'],
|
||||
},
|
||||
);
|
||||
|
||||
type Props = {
|
||||
columnSizes: TableColumnSizes,
|
||||
columnKeys: TableColumnKeys,
|
||||
onMouseDown: (e: SyntheticMouseEvent<>) => mixed,
|
||||
onMouseEnter?: (e: SyntheticMouseEvent<>) => void,
|
||||
multiline: ?boolean,
|
||||
rowLineHeight: number,
|
||||
highlighted: boolean,
|
||||
row: TableBodyRow,
|
||||
index: number,
|
||||
style: ?Object,
|
||||
onAddFilter?: TableOnAddFilter,
|
||||
zebra: ?boolean,
|
||||
};
|
||||
|
||||
export default class TableRow extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
zebra: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
index,
|
||||
highlighted,
|
||||
rowLineHeight,
|
||||
row,
|
||||
style,
|
||||
multiline,
|
||||
columnKeys,
|
||||
columnSizes,
|
||||
onMouseEnter,
|
||||
onMouseDown,
|
||||
zebra,
|
||||
onAddFilter,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableBodyRowContainer
|
||||
rowLineHeight={rowLineHeight}
|
||||
highlightedBackgroundColor={row.highlightedBackgroundColor}
|
||||
backgroundColor={row.backgroundColor}
|
||||
highlighted={highlighted}
|
||||
multiline={multiline}
|
||||
even={index % 2 === 0}
|
||||
zebra={zebra}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseEnter={onMouseEnter}
|
||||
style={style}
|
||||
highlightOnHover={row.highlightOnHover}
|
||||
data-key={row.key}
|
||||
{...row.style}>
|
||||
{columnKeys.map(key => {
|
||||
const col = row.columns[key];
|
||||
|
||||
if (col == null) {
|
||||
throw new Error(
|
||||
`Trying to access column "${key}" which does not exist on row. Make sure buildRow is returning a valid row.`,
|
||||
);
|
||||
}
|
||||
const isFilterable = col.isFilterable || false;
|
||||
const value = col ? col.value : '';
|
||||
const title = col ? col.title : '';
|
||||
|
||||
return (
|
||||
<TableBodyColumnContainer
|
||||
key={key}
|
||||
title={title}
|
||||
multiline={multiline}
|
||||
width={normaliseColumnWidth(columnSizes[key])}>
|
||||
{isFilterable && onAddFilter != null ? (
|
||||
<FilterRow addFilter={onAddFilter} filterKey={key}>
|
||||
{value}
|
||||
</FilterRow>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</TableBodyColumnContainer>
|
||||
);
|
||||
})}
|
||||
</TableBodyRowContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@ type TableColumnOrderVal = {
|
||||
visible: boolean,
|
||||
};
|
||||
|
||||
export type TableColumnRawOrder = Array<string | TableColumnOrderVal>;
|
||||
|
||||
export type TableColumnOrder = Array<TableColumnOrderVal>;
|
||||
|
||||
export type TableColumnSizes = {
|
||||
|
||||
@@ -41,7 +41,6 @@ export type {
|
||||
TableColumnOrder,
|
||||
TableColumnSizes,
|
||||
} from './components/table/types.js';
|
||||
export {default as Table} from './components/table/Table.js';
|
||||
export {default as ManagedTable} from './components/table/ManagedTable.js';
|
||||
export type {ManagedTableProps} from './components/table/ManagedTable.js';
|
||||
|
||||
|
||||
35
yarn.lock
35
yarn.lock
@@ -921,10 +921,6 @@ class-utils@^0.3.5:
|
||||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classnames@^2.2.3:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
|
||||
|
||||
cli-boxes@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
|
||||
@@ -1297,10 +1293,6 @@ doctrine@^2.0.2, doctrine@^2.1.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
"dom-helpers@^2.4.0 || ^3.0.0":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
|
||||
|
||||
domexception@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
||||
@@ -3258,7 +3250,7 @@ longest@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.3.1:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
|
||||
dependencies:
|
||||
@@ -3326,6 +3318,10 @@ mem@^1.1.0:
|
||||
dependencies:
|
||||
mimic-fn "^1.0.0"
|
||||
|
||||
memoize-one@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
|
||||
|
||||
meow@^3.1.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
|
||||
@@ -4055,10 +4051,6 @@ react-is@^16.4.0:
|
||||
version "16.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf"
|
||||
|
||||
react-lifecycles-compat@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
|
||||
react-redux@^5.0.7:
|
||||
version "5.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
|
||||
@@ -4079,16 +4071,15 @@ react-test-renderer@^16:
|
||||
prop-types "^15.6.0"
|
||||
react-is "^16.4.0"
|
||||
|
||||
react-virtualized@^9.13.0:
|
||||
version "9.19.1"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.19.1.tgz#84b53253df2d9df61c85ce037141edccc70a73fd"
|
||||
react-virtualized-auto-sizer@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
|
||||
|
||||
react-window@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.1.1.tgz#8a0cf488c9db19425fb804f118f5aac6227d7fd2"
|
||||
dependencies:
|
||||
babel-runtime "^6.26.0"
|
||||
classnames "^2.2.3"
|
||||
dom-helpers "^2.4.0 || ^3.0.0"
|
||||
loose-envify "^1.3.0"
|
||||
prop-types "^15.6.0"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
memoize-one "^3.1.1"
|
||||
|
||||
react@16:
|
||||
version "16.4.0"
|
||||
|
||||
Reference in New Issue
Block a user