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:
Daniel Büchele
2018-08-23 04:43:51 -07:00
committed by Facebook Github Bot
parent 33f34650df
commit 1891e2c869
13 changed files with 611 additions and 1253 deletions

View File

@@ -43,7 +43,7 @@ type Electron$MenuItemOptions = {
menuItem: Electron$MenuItem,
browserWindow: Object,
event: Object,
) => void,
) => mixed,
role?: Electron$MenuRoles,
type?: Electron$MenuType,
label?: string,

View 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
View 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'>;
}

View File

@@ -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",

View File

@@ -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}
/>

View File

@@ -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);
}
this.setState({highlightedRows});
if (this.props.onRowHighlighted) {
this.props.onRowHighlighted(highlightedRows);
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();
}
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 {tableRef} = this;
if (tableRef) {
tableRef.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});
};
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}
onColumnOrder={this.onColumnOrder}
columnSizes={state.columnSizes}
onColumnResize={this.onColumnResize}
stickyBottom={props.stickyBottom}
onAddFilter={props.onAddFilter}
zebra={props.zebra}
hideHeader={props.hideHeader}
/>
<FlexColumn style={{flexGrow: 1}}>
<TableHead
columnOrder={columnOrder}
onColumnOrder={this.onColumnOrder}
columns={columns}
onColumnResize={this.onColumnResize}
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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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>;
}
}

View File

@@ -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: () => {

View 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>
);
}
}

View File

@@ -16,8 +16,6 @@ type TableColumnOrderVal = {
visible: boolean,
};
export type TableColumnRawOrder = Array<string | TableColumnOrderVal>;
export type TableColumnOrder = Array<TableColumnOrderVal>;
export type TableColumnSizes = {

View File

@@ -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';

View File

@@ -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"