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,
|
menuItem: Electron$MenuItem,
|
||||||
browserWindow: Object,
|
browserWindow: Object,
|
||||||
event: Object,
|
event: Object,
|
||||||
) => void,
|
) => mixed,
|
||||||
role?: Electron$MenuRoles,
|
role?: Electron$MenuRoles,
|
||||||
type?: Electron$MenuType,
|
type?: Electron$MenuType,
|
||||||
label?: string,
|
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-dom": "16",
|
||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-test-renderer": "^16",
|
"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": "^4.0.0",
|
||||||
"redux-persist": "^5.10.0",
|
"redux-persist": "^5.10.0",
|
||||||
"redux-persist-transform-filter": "^0.0.18",
|
"redux-persist-transform-filter": "^0.0.18",
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ class SearchableManagedTable extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: Props) {
|
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 (
|
if (
|
||||||
nextProps.searchTerm !== this.props.searchTerm ||
|
nextProps.searchTerm !== this.props.searchTerm ||
|
||||||
!deepEqual(this.props.filters, nextProps.filters)
|
!deepEqual(this.props.filters, nextProps.filters)
|
||||||
@@ -90,13 +87,16 @@ class SearchableManagedTable extends PureComponent<Props, State> {
|
|||||||
searchTerm: _searchTerm,
|
searchTerm: _searchTerm,
|
||||||
filters: _filters,
|
filters: _filters,
|
||||||
innerRef,
|
innerRef,
|
||||||
|
rows,
|
||||||
...props
|
...props
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
<ManagedTable
|
<ManagedTable
|
||||||
{...props}
|
{...props}
|
||||||
filter={this.state.filterRows}
|
filter={this.state.filterRows}
|
||||||
|
rows={rows.filter(this.state.filterRows)}
|
||||||
onAddFilter={addFilter}
|
onAddFilter={addFilter}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TableColumnRawOrder,
|
|
||||||
TableColumnOrder,
|
TableColumnOrder,
|
||||||
TableColumnSizes,
|
TableColumnSizes,
|
||||||
TableColumns,
|
TableColumns,
|
||||||
@@ -16,8 +15,18 @@ import type {
|
|||||||
TableBodyRow,
|
TableBodyRow,
|
||||||
TableOnAddFilter,
|
TableOnAddFilter,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import styled from '../../styled/index.js';
|
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 = {|
|
export type ManagedTableProps = {|
|
||||||
/**
|
/**
|
||||||
@@ -50,7 +59,7 @@ export type ManagedTableProps = {|
|
|||||||
/**
|
/**
|
||||||
* Order of columns.
|
* Order of columns.
|
||||||
*/
|
*/
|
||||||
columnOrder?: ?TableColumnRawOrder,
|
columnOrder?: TableColumnOrder,
|
||||||
/**
|
/**
|
||||||
* Size of the columns.
|
* Size of the columns.
|
||||||
*/
|
*/
|
||||||
@@ -99,10 +108,11 @@ export type ManagedTableProps = {|
|
|||||||
|};
|
|};
|
||||||
|
|
||||||
type ManagedTableState = {|
|
type ManagedTableState = {|
|
||||||
highlightedRows: TableHighlightedRows,
|
highlightedRows: Set<string>,
|
||||||
sortOrder: ?TableRowSortOrder,
|
sortOrder: ?TableRowSortOrder,
|
||||||
columnOrder: ?TableColumnRawOrder,
|
columnOrder: TableColumnOrder,
|
||||||
columnSizes: ?TableColumnSizes,
|
columnSizes: TableColumnSizes,
|
||||||
|
shouldScrollToBottom: boolean,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,7 +121,7 @@ type ManagedTableState = {|
|
|||||||
* If you require lower level access to the state then use [`<Table>`]()
|
* If you require lower level access to the state then use [`<Table>`]()
|
||||||
* directly.
|
* directly.
|
||||||
*/
|
*/
|
||||||
export default class ManagedTable extends styled.StylablePureComponent<
|
export default class ManagedTable extends styled.StylableComponent<
|
||||||
ManagedTableProps,
|
ManagedTableProps,
|
||||||
ManagedTableState,
|
ManagedTableState,
|
||||||
> {
|
> {
|
||||||
@@ -124,16 +134,32 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state: ManagedTableState = {
|
||||||
columnOrder:
|
columnOrder:
|
||||||
JSON.parse(window.localStorage.getItem(this.getTableKey()) || 'null') ||
|
JSON.parse(window.localStorage.getItem(this.getTableKey()) || 'null') ||
|
||||||
this.props.columnOrder,
|
this.props.columnOrder ||
|
||||||
columnSizes: this.props.columnSizes,
|
Object.keys(this.props.columns).map(key => ({key, visible: true})),
|
||||||
highlightedRows: [],
|
columnSizes: this.props.columnSizes || {},
|
||||||
|
highlightedRows: new Set(),
|
||||||
sortOrder: null,
|
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) {
|
componentWillReceiveProps(nextProps: ManagedTableProps) {
|
||||||
// if columnSizes has changed
|
// if columnSizes has changed
|
||||||
@@ -152,20 +178,64 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
|||||||
columnOrder: nextProps.columnOrder,
|
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) => {
|
componentDidUpdate(prevProps: ManagedTableProps) {
|
||||||
if (this.props.highlightableRows === false) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (this.props.multiHighlight !== true) {
|
if (
|
||||||
highlightedRows = highlightedRows.slice(0, 1);
|
((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});
|
this.setState({highlightedRows}, () => {
|
||||||
|
const {current} = this.tableRef;
|
||||||
if (this.props.onRowHighlighted) {
|
if (current) {
|
||||||
this.props.onRowHighlighted(highlightedRows);
|
current.scrollToItem(newIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,7 +244,6 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
|||||||
};
|
};
|
||||||
|
|
||||||
onColumnOrder = (columnOrder: TableColumnOrder) => {
|
onColumnOrder = (columnOrder: TableColumnOrder) => {
|
||||||
// $FlowFixMe
|
|
||||||
this.setState({columnOrder});
|
this.setState({columnOrder});
|
||||||
// persist column order
|
// persist column order
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@@ -187,45 +256,246 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
|||||||
this.setState({columnSizes});
|
this.setState({columnSizes});
|
||||||
};
|
};
|
||||||
|
|
||||||
setRef = (table: ?Table) => {
|
scrollToBottom() {
|
||||||
this.tableRef = table;
|
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() {
|
onStopDragSelecting = () => {
|
||||||
const {tableRef} = this;
|
this.dragStartIndex = null;
|
||||||
if (tableRef) {
|
document.removeEventListener('mouseup', this.onStopDragSelecting);
|
||||||
tableRef.scrollToBottom();
|
};
|
||||||
|
|
||||||
|
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() {
|
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 (
|
return (
|
||||||
<Table
|
<FlexColumn style={{flexGrow: 1}}>
|
||||||
ref={this.setRef}
|
<TableHead
|
||||||
virtual={props.virtual}
|
columnOrder={columnOrder}
|
||||||
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}
|
onColumnOrder={this.onColumnOrder}
|
||||||
columnSizes={state.columnSizes}
|
columns={columns}
|
||||||
onColumnResize={this.onColumnResize}
|
onColumnResize={this.onColumnResize}
|
||||||
stickyBottom={props.stickyBottom}
|
sortOrder={this.state.sortOrder}
|
||||||
onAddFilter={props.onAddFilter}
|
columnSizes={columnSizes}
|
||||||
zebra={props.zebra}
|
onSort={this.onSort}
|
||||||
hideHeader={props.hideHeader}
|
|
||||||
/>
|
/>
|
||||||
|
<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 {
|
import type {
|
||||||
TableColumnKeys,
|
|
||||||
TableColumnOrder,
|
TableColumnOrder,
|
||||||
TableColumnSizes,
|
TableColumnSizes,
|
||||||
TableColumns,
|
TableColumns,
|
||||||
@@ -193,7 +192,6 @@ class TableHeadColumn extends PureComponent<{
|
|||||||
export default class TableHead extends PureComponent<{
|
export default class TableHead extends PureComponent<{
|
||||||
columnOrder: TableColumnOrder,
|
columnOrder: TableColumnOrder,
|
||||||
onColumnOrder: ?(order: TableColumnOrder) => void,
|
onColumnOrder: ?(order: TableColumnOrder) => void,
|
||||||
columnKeys: TableColumnKeys,
|
|
||||||
columns: TableColumns,
|
columns: TableColumns,
|
||||||
sortOrder: ?TableRowSortOrder,
|
sortOrder: ?TableRowSortOrder,
|
||||||
onSort: ?TableOnSort,
|
onSort: ?TableOnSort,
|
||||||
@@ -201,8 +199,15 @@ export default class TableHead extends PureComponent<{
|
|||||||
onColumnResize: ?TableOnColumnResize,
|
onColumnResize: ?TableOnColumnResize,
|
||||||
}> {
|
}> {
|
||||||
buildContextMenu = (): MenuTemplate => {
|
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 => {
|
return Object.keys(this.props.columns).map(key => {
|
||||||
const visible = this.props.columnKeys.includes(key);
|
const visible = visibles.has(key);
|
||||||
return {
|
return {
|
||||||
label: this.props.columns[key].value,
|
label: this.props.columns[key].value,
|
||||||
click: () => {
|
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,
|
visible: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TableColumnRawOrder = Array<string | TableColumnOrderVal>;
|
|
||||||
|
|
||||||
export type TableColumnOrder = Array<TableColumnOrderVal>;
|
export type TableColumnOrder = Array<TableColumnOrderVal>;
|
||||||
|
|
||||||
export type TableColumnSizes = {
|
export type TableColumnSizes = {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export type {
|
|||||||
TableColumnOrder,
|
TableColumnOrder,
|
||||||
TableColumnSizes,
|
TableColumnSizes,
|
||||||
} from './components/table/types.js';
|
} from './components/table/types.js';
|
||||||
export {default as Table} from './components/table/Table.js';
|
|
||||||
export {default as ManagedTable} from './components/table/ManagedTable.js';
|
export {default as ManagedTable} from './components/table/ManagedTable.js';
|
||||||
export type {ManagedTableProps} 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"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
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:
|
cli-boxes@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
|
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:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
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:
|
domexception@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
||||||
@@ -3258,7 +3250,7 @@ longest@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
|
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"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3326,6 +3318,10 @@ mem@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn "^1.0.0"
|
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:
|
meow@^3.1.0:
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
|
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"
|
version "16.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf"
|
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:
|
react-redux@^5.0.7:
|
||||||
version "5.0.7"
|
version "5.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
|
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"
|
prop-types "^15.6.0"
|
||||||
react-is "^16.4.0"
|
react-is "^16.4.0"
|
||||||
|
|
||||||
react-virtualized@^9.13.0:
|
react-virtualized-auto-sizer@^1.0.2:
|
||||||
version "9.19.1"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.19.1.tgz#84b53253df2d9df61c85ce037141edccc70a73fd"
|
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:
|
dependencies:
|
||||||
babel-runtime "^6.26.0"
|
memoize-one "^3.1.1"
|
||||||
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"
|
|
||||||
|
|
||||||
react@16:
|
react@16:
|
||||||
version "16.4.0"
|
version "16.4.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user