immutable data structures in tables 1/n: init
Summary: Migrating tables' row collection to Immutable.js List: 1. Сopy ManagedTable code, no changes 2. Add deps to Immutable.js ----- Current implementation of tables forces to copy arrays on new data arrival which causes O(N^2) complexity -> tables can't handle a lot of new rows in short period of time -> tables freeze and become unresponsive for a few seconds. Immutable data structures will bring us to O(N) complexity Reviewed By: jknoxville Differential Revision: D16416870 fbshipit-source-id: d3e1a9571ea08fa7ccaedc5ad3eca863d22a79a4
This commit is contained in:
committed by
Facebook Github Bot
parent
5283bf5ada
commit
af7830d94a
@@ -92,6 +92,7 @@
|
|||||||
"expand-tilde": "^2.0.2",
|
"expand-tilde": "^2.0.2",
|
||||||
"express": "^4.15.2",
|
"express": "^4.15.2",
|
||||||
"fs-extra": "^8.0.1",
|
"fs-extra": "^8.0.1",
|
||||||
|
"immutable": "^4.0.0-rc.12",
|
||||||
"invariant": "^2.2.2",
|
"invariant": "^2.2.2",
|
||||||
"line-replace": "^1.0.2",
|
"line-replace": "^1.0.2",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
|||||||
722
src/ui/components/table/ManagedTable_immutable.js
Normal file
722
src/ui/components/table/ManagedTable_immutable.js
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
TableColumnOrder,
|
||||||
|
TableColumnSizes,
|
||||||
|
TableColumns,
|
||||||
|
TableHighlightedRows,
|
||||||
|
TableRowSortOrder,
|
||||||
|
TableRows,
|
||||||
|
TableBodyRow,
|
||||||
|
TableOnAddFilter,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
import type {MenuTemplate} from '../ContextMenu.js';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import styled from '../../styled/index.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 '../../../fb-stubs/createPaste.js';
|
||||||
|
import debounceRender from 'react-debounce-render';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import {DEFAULT_ROW_HEIGHT} from './types';
|
||||||
|
import textContent from '../../../utils/textContent.js';
|
||||||
|
|
||||||
|
export type ManagedTableProps = {|
|
||||||
|
/**
|
||||||
|
* Column definitions.
|
||||||
|
*/
|
||||||
|
columns: TableColumns,
|
||||||
|
/**
|
||||||
|
* Row definitions.
|
||||||
|
*/
|
||||||
|
rows: TableRows,
|
||||||
|
/*
|
||||||
|
* Globally unique key for persisting data between uses of a table such as column sizes.
|
||||||
|
*/
|
||||||
|
tableKey?: string,
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
/**
|
||||||
|
* Whether the body is scrollable. When this is set to `true` then the table
|
||||||
|
* is not scrollable.
|
||||||
|
*/
|
||||||
|
autoHeight?: boolean,
|
||||||
|
/**
|
||||||
|
* Order of columns.
|
||||||
|
*/
|
||||||
|
columnOrder?: TableColumnOrder,
|
||||||
|
/**
|
||||||
|
* Initial size of the columns.
|
||||||
|
*/
|
||||||
|
columnSizes?: TableColumnSizes,
|
||||||
|
/**
|
||||||
|
* Value to filter rows on. Alternative to the `filter` prop.
|
||||||
|
*/
|
||||||
|
filterValue?: string,
|
||||||
|
/**
|
||||||
|
* Callback to filter rows.
|
||||||
|
*/
|
||||||
|
filter?: (row: TableBodyRow) => boolean,
|
||||||
|
/**
|
||||||
|
* Callback when the highlighted rows change.
|
||||||
|
*/
|
||||||
|
onRowHighlighted?: (keys: TableHighlightedRows) => void,
|
||||||
|
/**
|
||||||
|
* Whether rows can be highlighted or not.
|
||||||
|
*/
|
||||||
|
highlightableRows?: boolean,
|
||||||
|
/**
|
||||||
|
* Whether multiple rows can be highlighted or not.
|
||||||
|
*/
|
||||||
|
multiHighlight?: boolean,
|
||||||
|
/**
|
||||||
|
* Height of each row.
|
||||||
|
*/
|
||||||
|
rowLineHeight?: number,
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
/**
|
||||||
|
* Enable or disable zebra striping.
|
||||||
|
*/
|
||||||
|
zebra?: boolean,
|
||||||
|
/**
|
||||||
|
* Whether to hide the column names at the top of the table.
|
||||||
|
*/
|
||||||
|
hideHeader?: boolean,
|
||||||
|
/**
|
||||||
|
* Rows that are highlighted initially.
|
||||||
|
*/
|
||||||
|
highlightedRows?: Set<string>,
|
||||||
|
/**
|
||||||
|
* Allows to create context menu items for rows.
|
||||||
|
*/
|
||||||
|
buildContextMenuItems?: () => MenuTemplate,
|
||||||
|
initialSortOrder?: ?TableRowSortOrder,
|
||||||
|
/**
|
||||||
|
* Callback when sorting changes.
|
||||||
|
*/
|
||||||
|
onSort?: (order: TableRowSortOrder) => void,
|
||||||
|
/**
|
||||||
|
* Initial sort order of the table.
|
||||||
|
*/
|
||||||
|
initialSortOrder?: ?TableRowSortOrder,
|
||||||
|
/**
|
||||||
|
* Table scroll horizontally, if needed
|
||||||
|
*/
|
||||||
|
horizontallyScrollable?: boolean,
|
||||||
|
|};
|
||||||
|
|
||||||
|
type ManagedTableState = {|
|
||||||
|
highlightedRows: Set<string>,
|
||||||
|
sortOrder: ?TableRowSortOrder,
|
||||||
|
columnOrder: TableColumnOrder,
|
||||||
|
columnSizes: TableColumnSizes,
|
||||||
|
shouldScrollToBottom: boolean,
|
||||||
|
|};
|
||||||
|
|
||||||
|
const Container = styled(FlexColumn)(props => ({
|
||||||
|
overflow: props.canOverflow ? 'scroll' : 'visible',
|
||||||
|
flexGrow: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const globalTableState: {[string]: TableColumnSizes} = {};
|
||||||
|
|
||||||
|
class ManagedTable extends React.Component<
|
||||||
|
ManagedTableProps,
|
||||||
|
ManagedTableState,
|
||||||
|
> {
|
||||||
|
static defaultProps = {
|
||||||
|
highlightableRows: true,
|
||||||
|
multiHighlight: false,
|
||||||
|
autoHeight: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
getTableKey = (): string => {
|
||||||
|
return (
|
||||||
|
'TABLE_COLUMNS_' +
|
||||||
|
Object.keys(this.props.columns)
|
||||||
|
.join('_')
|
||||||
|
.toUpperCase()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
state: ManagedTableState = {
|
||||||
|
columnOrder:
|
||||||
|
JSON.parse(window.localStorage.getItem(this.getTableKey()) || 'null') ||
|
||||||
|
this.props.columnOrder ||
|
||||||
|
Object.keys(this.props.columns).map(key => ({key, visible: true})),
|
||||||
|
columnSizes:
|
||||||
|
this.props.tableKey && globalTableState[this.props.tableKey]
|
||||||
|
? globalTableState[this.props.tableKey]
|
||||||
|
: this.props.columnSizes || {},
|
||||||
|
highlightedRows: this.props.highlightedRows || new Set(),
|
||||||
|
sortOrder: this.props.initialSortOrder || null,
|
||||||
|
shouldScrollToBottom: Boolean(this.props.stickyBottom),
|
||||||
|
};
|
||||||
|
|
||||||
|
tableRef = React.createRef<List>();
|
||||||
|
|
||||||
|
scrollRef: {
|
||||||
|
current: null | HTMLDivElement,
|
||||||
|
} = React.createRef();
|
||||||
|
|
||||||
|
dragStartIndex: ?number = null;
|
||||||
|
|
||||||
|
// We want to call scrollToHighlightedRows on componentDidMount. However, at
|
||||||
|
// this time, tableRef is still null, because AutoSizer needs one render to
|
||||||
|
// measure the size of the table. This is why we are using this flag to
|
||||||
|
// trigger actions on the first update instead.
|
||||||
|
firstUpdate = true;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: ManagedTableProps) {
|
||||||
|
// if columnSizes has changed
|
||||||
|
if (nextProps.columnSizes !== this.props.columnSizes) {
|
||||||
|
this.setState({
|
||||||
|
columnSizes: {
|
||||||
|
...(this.state.columnSizes || {}),
|
||||||
|
...nextProps.columnSizes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.highlightedRows !== nextProps.highlightedRows) {
|
||||||
|
this.setState({highlightedRows: nextProps.highlightedRows});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if columnOrder has changed
|
||||||
|
if (nextProps.columnOrder !== this.props.columnOrder) {
|
||||||
|
if (this.tableRef && this.tableRef.current) {
|
||||||
|
this.tableRef.current.resetAfterIndex(0);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
columnOrder: nextProps.columnOrder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.props.rows.length > nextProps.rows.length &&
|
||||||
|
this.tableRef &&
|
||||||
|
this.tableRef.current
|
||||||
|
) {
|
||||||
|
// rows were filtered, we need to recalculate heights
|
||||||
|
this.tableRef.current.resetAfterIndex(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(
|
||||||
|
prevProps: ManagedTableProps,
|
||||||
|
prevState: ManagedTableState,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.props.rows.length !== prevProps.rows.length &&
|
||||||
|
this.state.shouldScrollToBottom &&
|
||||||
|
this.state.highlightedRows.size < 2
|
||||||
|
) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
} else if (
|
||||||
|
prevState.highlightedRows !== this.state.highlightedRows ||
|
||||||
|
this.firstUpdate
|
||||||
|
) {
|
||||||
|
this.scrollToHighlightedRows();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.props.stickyBottom &&
|
||||||
|
!this.state.shouldScrollToBottom &&
|
||||||
|
this.scrollRef &&
|
||||||
|
this.scrollRef.current &&
|
||||||
|
this.scrollRef.current.parentElement &&
|
||||||
|
this.scrollRef.current.parentElement instanceof HTMLElement &&
|
||||||
|
this.scrollRef.current.offsetHeight <=
|
||||||
|
this.scrollRef.current.parentElement.offsetHeight
|
||||||
|
) {
|
||||||
|
this.setState({shouldScrollToBottom: true});
|
||||||
|
}
|
||||||
|
this.firstUpdate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToHighlightedRows = () => {
|
||||||
|
const {current} = this.tableRef;
|
||||||
|
const {highlightedRows} = this.state;
|
||||||
|
if (current && highlightedRows && highlightedRows.size > 0) {
|
||||||
|
const highlightedRow = Array.from(highlightedRows)[0];
|
||||||
|
const index = this.props.rows.findIndex(
|
||||||
|
({key}) => key === highlightedRow,
|
||||||
|
);
|
||||||
|
if (index >= 0) {
|
||||||
|
current.scrollToItem(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onCopy = (withHeader: boolean) => {
|
||||||
|
clipboard.writeText(
|
||||||
|
[
|
||||||
|
...(withHeader ? [this.getHeaderText()] : []),
|
||||||
|
this.getSelectedText(),
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const {highlightedRows} = this.state;
|
||||||
|
if (highlightedRows.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
((e.metaKey && process.platform === 'darwin') ||
|
||||||
|
(e.ctrlKey && process.platform !== 'darwin')) &&
|
||||||
|
e.keyCode === 67
|
||||||
|
) {
|
||||||
|
this.onCopy(false);
|
||||||
|
} else if (
|
||||||
|
(e.keyCode === 38 || e.keyCode === 40) &&
|
||||||
|
this.props.highlightableRows
|
||||||
|
) {
|
||||||
|
// 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.onRowHighlighted(highlightedRows, () => {
|
||||||
|
const {current} = this.tableRef;
|
||||||
|
if (current) {
|
||||||
|
current.scrollToItem(newIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onRowHighlighted = (highlightedRows: Set<string>, cb?: Function) => {
|
||||||
|
if (!this.props.highlightableRows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({highlightedRows}, cb);
|
||||||
|
const {onRowHighlighted} = this.props;
|
||||||
|
if (onRowHighlighted) {
|
||||||
|
onRowHighlighted(Array.from(highlightedRows));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSort = (sortOrder: TableRowSortOrder) => {
|
||||||
|
this.setState({sortOrder});
|
||||||
|
this.props.onSort && this.props.onSort(sortOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
onColumnOrder = (columnOrder: TableColumnOrder) => {
|
||||||
|
this.setState({columnOrder});
|
||||||
|
// persist column order
|
||||||
|
window.localStorage.setItem(
|
||||||
|
this.getTableKey(),
|
||||||
|
JSON.stringify(columnOrder),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onColumnResize = (id: string, width: number | string) => {
|
||||||
|
this.setState(({columnSizes}) => ({
|
||||||
|
columnSizes: {
|
||||||
|
...columnSizes,
|
||||||
|
[id]: width,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
if (!this.props.tableKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!globalTableState[this.props.tableKey]) {
|
||||||
|
globalTableState[this.props.tableKey] = {};
|
||||||
|
}
|
||||||
|
globalTableState[this.props.tableKey][id] = width;
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
const {current: tableRef} = this.tableRef;
|
||||||
|
|
||||||
|
if (tableRef && this.props.rows.length > 1) {
|
||||||
|
tableRef.scrollToItem(this.props.rows.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHighlight = (
|
||||||
|
e: SyntheticMouseEvent<>,
|
||||||
|
row: TableBodyRow,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// prevents text selection
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
let {highlightedRows} = this.state;
|
||||||
|
|
||||||
|
const contextClick =
|
||||||
|
e.button !== 0 ||
|
||||||
|
(process.platform === 'darwin' && e.button === 0 && e.ctrlKey);
|
||||||
|
|
||||||
|
if (contextClick) {
|
||||||
|
if (!highlightedRows.has(row.key)) {
|
||||||
|
highlightedRows.clear();
|
||||||
|
highlightedRows.add(row.key);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragStartIndex = index;
|
||||||
|
document.addEventListener('mouseup', this.onStopDragSelecting);
|
||||||
|
|
||||||
|
if (
|
||||||
|
((process.platform === 'darwin' && e.metaKey) ||
|
||||||
|
(process.platform !== 'darwin' && e.ctrlKey)) &&
|
||||||
|
this.props.multiHighlight
|
||||||
|
) {
|
||||||
|
highlightedRows.add(row.key);
|
||||||
|
} else if (e.shiftKey && this.props.multiHighlight) {
|
||||||
|
// 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.onRowHighlighted(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 &&
|
||||||
|
this.props.multiHighlight &&
|
||||||
|
this.props.highlightableRows &&
|
||||||
|
!e.shiftKey // When shift key is pressed, it's a range select not a drag select
|
||||||
|
) {
|
||||||
|
current.scrollToItem(index + 1);
|
||||||
|
const startKey = this.props.rows[dragStartIndex].key;
|
||||||
|
const highlightedRows = new Set(this.selectInRange(startKey, row.key));
|
||||||
|
this.onRowHighlighted(highlightedRows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onCopyCell = (rowId: string, index: number) => {
|
||||||
|
const cellText = this.getTextContentOfRow(rowId)[index];
|
||||||
|
clipboard.writeText(cellText);
|
||||||
|
};
|
||||||
|
|
||||||
|
buildContextMenuItems: () => Array<MenuItemConstructorOptions> = () => {
|
||||||
|
const {highlightedRows} = this.state;
|
||||||
|
if (highlightedRows.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyCellSubMenu =
|
||||||
|
highlightedRows.size === 1
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Copy cell',
|
||||||
|
submenu: this.state.columnOrder
|
||||||
|
.filter(c => c.visible)
|
||||||
|
.map(c => c.key)
|
||||||
|
.map((column, index) => ({
|
||||||
|
label: this.props.columns[column].value,
|
||||||
|
click: () => {
|
||||||
|
const rowId = this.state.highlightedRows.values().next()
|
||||||
|
.value;
|
||||||
|
rowId && this.onCopyCell(rowId, index);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...copyCellSubMenu,
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
highlightedRows.size > 1
|
||||||
|
? `Copy ${highlightedRows.size} rows`
|
||||||
|
: 'Copy row',
|
||||||
|
submenu: [
|
||||||
|
{label: 'With columns header', click: () => this.onCopy(true)},
|
||||||
|
{
|
||||||
|
label: 'Without columns header',
|
||||||
|
click: () => {
|
||||||
|
this.onCopy(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Create Paste',
|
||||||
|
click: () =>
|
||||||
|
createPaste(
|
||||||
|
[this.getHeaderText(), this.getSelectedText()].join('\n'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
getHeaderText = (): string => {
|
||||||
|
return this.state.columnOrder
|
||||||
|
.filter(c => c.visible)
|
||||||
|
.map(c => c.key)
|
||||||
|
.map(key => this.props.columns[key].value)
|
||||||
|
.join('\t');
|
||||||
|
};
|
||||||
|
|
||||||
|
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 || this.getTextContentOfRow(row.key).join('\t'),
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
getTextContentOfRow = (key: string): Array<string> => {
|
||||||
|
const row = this.props.rows.find(row => row.key === key);
|
||||||
|
if (!row) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.state.columnOrder
|
||||||
|
.filter(({visible}) => visible)
|
||||||
|
.map(({key}) => textContent(row.columns[key].value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onScroll = debounce(
|
||||||
|
({
|
||||||
|
scrollDirection,
|
||||||
|
scrollOffset,
|
||||||
|
}: {
|
||||||
|
scrollDirection: 'forward' | 'backward',
|
||||||
|
scrollOffset: number,
|
||||||
|
scrollUpdateWasRequested: boolean,
|
||||||
|
}) => {
|
||||||
|
const {current} = this.scrollRef;
|
||||||
|
const parent = current ? current.parentElement : null;
|
||||||
|
if (
|
||||||
|
this.props.stickyBottom &&
|
||||||
|
current &&
|
||||||
|
parent instanceof HTMLElement &&
|
||||||
|
scrollDirection === 'forward' &&
|
||||||
|
!this.state.shouldScrollToBottom &&
|
||||||
|
current.offsetHeight - parent.offsetHeight === scrollOffset
|
||||||
|
) {
|
||||||
|
this.setState({shouldScrollToBottom: true});
|
||||||
|
} else if (
|
||||||
|
this.props.stickyBottom &&
|
||||||
|
scrollDirection === 'backward' &&
|
||||||
|
this.state.shouldScrollToBottom
|
||||||
|
) {
|
||||||
|
this.setState({shouldScrollToBottom: false});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
|
getRow = ({index, style}) => {
|
||||||
|
const {
|
||||||
|
onAddFilter,
|
||||||
|
multiline,
|
||||||
|
zebra,
|
||||||
|
rows,
|
||||||
|
horizontallyScrollable,
|
||||||
|
} = this.props;
|
||||||
|
const {columnOrder, columnSizes, highlightedRows} = this.state;
|
||||||
|
const columnKeys = columnOrder
|
||||||
|
.map(k => (k.visible ? k.key : null))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={rows[index].key}
|
||||||
|
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}
|
||||||
|
horizontallyScrollable={horizontallyScrollable}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
rowLineHeight,
|
||||||
|
hideHeader,
|
||||||
|
horizontallyScrollable,
|
||||||
|
} = this.props;
|
||||||
|
const {columnOrder, columnSizes} = this.state;
|
||||||
|
|
||||||
|
let computedWidth = 0;
|
||||||
|
if (horizontallyScrollable) {
|
||||||
|
for (let index = 0; index < columnOrder.length; index++) {
|
||||||
|
const col = columnOrder[index];
|
||||||
|
|
||||||
|
if (!col.visible) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = columnSizes[col.key];
|
||||||
|
if (isNaN(width)) {
|
||||||
|
// non-numeric columns with, can't caluclate
|
||||||
|
computedWidth = 0;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
computedWidth += parseInt(width, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container canOverflow={horizontallyScrollable}>
|
||||||
|
{hideHeader !== true && (
|
||||||
|
<TableHead
|
||||||
|
columnOrder={columnOrder}
|
||||||
|
onColumnOrder={this.onColumnOrder}
|
||||||
|
columns={columns}
|
||||||
|
onColumnResize={this.onColumnResize}
|
||||||
|
sortOrder={this.state.sortOrder}
|
||||||
|
columnSizes={columnSizes}
|
||||||
|
onSort={this.onSort}
|
||||||
|
horizontallyScrollable={horizontallyScrollable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Container>
|
||||||
|
{this.props.autoHeight ? (
|
||||||
|
this.props.rows.map((_, index) => this.getRow({index, style: {}}))
|
||||||
|
) : (
|
||||||
|
<AutoSizer>
|
||||||
|
{({width, height}) => (
|
||||||
|
<ContextMenu
|
||||||
|
buildItems={
|
||||||
|
this.props.buildContextMenuItems ||
|
||||||
|
this.buildContextMenuItems
|
||||||
|
}>
|
||||||
|
<List
|
||||||
|
itemCount={rows.length}
|
||||||
|
itemSize={index =>
|
||||||
|
(rows[index] && rows[index].height) ||
|
||||||
|
rowLineHeight ||
|
||||||
|
DEFAULT_ROW_HEIGHT
|
||||||
|
}
|
||||||
|
ref={this.tableRef}
|
||||||
|
width={Math.max(width, computedWidth)}
|
||||||
|
estimatedItemSize={rowLineHeight || DEFAULT_ROW_HEIGHT}
|
||||||
|
overscanCount={5}
|
||||||
|
innerRef={this.scrollRef}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
height={height}>
|
||||||
|
{this.getRow}
|
||||||
|
</List>
|
||||||
|
</ContextMenu>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default debounceRender(ManagedTable, 150, {maxWait: 250});
|
||||||
@@ -4003,6 +4003,11 @@ ignore@^4.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||||
|
|
||||||
|
immutable@^4.0.0-rc.12:
|
||||||
|
version "4.0.0-rc.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.12.tgz#ca59a7e4c19ae8d9bf74a97bdf0f6e2f2a5d0217"
|
||||||
|
integrity sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==
|
||||||
|
|
||||||
import-fresh@^3.0.0:
|
import-fresh@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
|
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
|
||||||
|
|||||||
Reference in New Issue
Block a user