diff --git a/flow-typed/electron-menu.js b/flow-typed/electron-menu.js index 46a34b912..4043a58bd 100644 --- a/flow-typed/electron-menu.js +++ b/flow-typed/electron-menu.js @@ -43,7 +43,7 @@ type Electron$MenuItemOptions = { menuItem: Electron$MenuItem, browserWindow: Object, event: Object, - ) => void, + ) => mixed, role?: Electron$MenuRoles, type?: Electron$MenuType, label?: string, diff --git a/flow-typed/npm/react-virtualized-auto-sizer_vx.x.x.js b/flow-typed/npm/react-virtualized-auto-sizer_vx.x.x.js new file mode 100644 index 000000000..578ae7136 --- /dev/null +++ b/flow-typed/npm/react-virtualized-auto-sizer_vx.x.x.js @@ -0,0 +1,39 @@ +// flow-typed signature: 914da65c76e12730f3b4e60d17889885 +// flow-typed version: <>/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'>; +} diff --git a/flow-typed/npm/react-window_vx.x.x.js b/flow-typed/npm/react-window_vx.x.x.js new file mode 100644 index 000000000..fe37ba415 --- /dev/null +++ b/flow-typed/npm/react-window_vx.x.x.js @@ -0,0 +1,39 @@ +// flow-typed signature: 351408bd2a4f82fae81f253ee1947834 +// flow-typed version: <>/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'>; +} diff --git a/package.json b/package.json index 9cbd3786e..41a1b459e 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "react-dom": "16", "react-redux": "^5.0.7", "react-test-renderer": "^16", - "react-virtualized": "^9.13.0", + "react-virtualized-auto-sizer": "^1.0.2", + "react-window": "^1.1.1", "redux": "^4.0.0", "redux-persist": "^5.10.0", "redux-persist-transform-filter": "^0.0.18", diff --git a/src/ui/components/searchable/SearchableTable.js b/src/ui/components/searchable/SearchableTable.js index 382b3398c..45a430ad3 100644 --- a/src/ui/components/searchable/SearchableTable.js +++ b/src/ui/components/searchable/SearchableTable.js @@ -71,9 +71,6 @@ class SearchableManagedTable extends PureComponent { } componentWillReceiveProps(nextProps: Props) { - // ManagedTable is a PureComponent and does not update when this.filterRows - // would return a different value. This is why we update the funtion reference - // once the results of the function changed. if ( nextProps.searchTerm !== this.props.searchTerm || !deepEqual(this.props.filters, nextProps.filters) @@ -90,13 +87,16 @@ class SearchableManagedTable extends PureComponent { searchTerm: _searchTerm, filters: _filters, innerRef, + rows, ...props } = this.props; + return ( // $FlowFixMe diff --git a/src/ui/components/table/ManagedTable.js b/src/ui/components/table/ManagedTable.js index cd7facb86..ebe034a35 100644 --- a/src/ui/components/table/ManagedTable.js +++ b/src/ui/components/table/ManagedTable.js @@ -6,7 +6,6 @@ */ import type { - TableColumnRawOrder, TableColumnOrder, TableColumnSizes, TableColumns, @@ -16,8 +15,18 @@ import type { TableBodyRow, TableOnAddFilter, } from './types.js'; + +import React from 'react'; import styled from '../../styled/index.js'; -import Table from './Table.js'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import {VariableSizeList as List} from 'react-window'; +import {clipboard} from 'electron'; +import TableHead from './TableHead.js'; +import TableRow from './TableRow.js'; +import ContextMenu from '../ContextMenu.js'; +import FlexColumn from '../FlexColumn.js'; +import createPaste from '../../../utils/createPaste.js'; +import {DEFAULT_ROW_HEIGHT} from './types'; export type ManagedTableProps = {| /** @@ -50,7 +59,7 @@ export type ManagedTableProps = {| /** * Order of columns. */ - columnOrder?: ?TableColumnRawOrder, + columnOrder?: TableColumnOrder, /** * Size of the columns. */ @@ -99,10 +108,11 @@ export type ManagedTableProps = {| |}; type ManagedTableState = {| - highlightedRows: TableHighlightedRows, + highlightedRows: Set, sortOrder: ?TableRowSortOrder, - columnOrder: ?TableColumnRawOrder, - columnSizes: ?TableColumnSizes, + columnOrder: TableColumnOrder, + columnSizes: TableColumnSizes, + shouldScrollToBottom: boolean, |}; /** @@ -111,7 +121,7 @@ type ManagedTableState = {| * If you require lower level access to the state then use [``]() * directly. */ -export default class ManagedTable extends styled.StylablePureComponent< +export default class ManagedTable extends styled.StylableComponent< ManagedTableProps, ManagedTableState, > { @@ -124,16 +134,32 @@ export default class ManagedTable extends styled.StylablePureComponent< ); }; - state = { + state: ManagedTableState = { columnOrder: JSON.parse(window.localStorage.getItem(this.getTableKey()) || 'null') || - this.props.columnOrder, - columnSizes: this.props.columnSizes, - highlightedRows: [], + this.props.columnOrder || + Object.keys(this.props.columns).map(key => ({key, visible: true})), + columnSizes: this.props.columnSizes || {}, + highlightedRows: new Set(), sortOrder: null, + shouldScrollToBottom: Boolean(this.props.stickyBottom), }; - tableRef: ?Table; + tableRef: { + current: null | List, + } = React.createRef(); + scrollRef: { + current: null | HTMLDivElement, + } = React.createRef(); + dragStartIndex: ?number = null; + + componentDidMount() { + document.addEventListener('keydown', this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.onKeyDown); + } componentWillReceiveProps(nextProps: ManagedTableProps) { // if columnSizes has changed @@ -152,20 +178,64 @@ export default class ManagedTable extends styled.StylablePureComponent< columnOrder: nextProps.columnOrder, }); } + + if ( + nextProps.filter !== this.props.filter && + this.tableRef && + this.tableRef.current + ) { + // rows were filtered, we need to recalculate heights + this.tableRef.current.resetAfterIndex(0); + } } - onHighlight = (highlightedRows: TableHighlightedRows) => { - if (this.props.highlightableRows === false) { + componentDidUpdate(prevProps: ManagedTableProps) { + if ( + this.props.rows.length !== prevProps.rows.length && + this.state.shouldScrollToBottom && + this.state.highlightedRows.size < 2 + ) { + this.scrollToBottom(); + } + } + + onCopy = () => { + clipboard.writeText(this.getSelectedText()); + }; + + onKeyDown = (e: KeyboardEvent) => { + const {highlightedRows} = this.state; + if (highlightedRows.size === 0) { return; } - if (this.props.multiHighlight !== true) { - highlightedRows = highlightedRows.slice(0, 1); - } - - this.setState({highlightedRows}); - - if (this.props.onRowHighlighted) { - this.props.onRowHighlighted(highlightedRows); + if ( + ((e.metaKey && process.platform === 'darwin') || + (e.ctrlKey && process.platform !== 'darwin')) && + e.keyCode === 67 + ) { + this.onCopy(); + } else if (e.keyCode === 38 || e.keyCode === 40) { + // arrow navigation + const {rows} = this.props; + const {highlightedRows} = this.state; + const lastItemKey = Array.from(this.state.highlightedRows).pop(); + const lastItemIndex = this.props.rows.findIndex( + row => row.key === lastItemKey, + ); + const newIndex = Math.min( + rows.length - 1, + Math.max(0, e.keyCode === 38 ? lastItemIndex - 1 : lastItemIndex + 1), + ); + if (!e.shiftKey) { + highlightedRows.clear(); + } + highlightedRows.add(rows[newIndex].key); + this.setState({highlightedRows}, () => { + const {current} = this.tableRef; + if (current) { + current.scrollToItem(newIndex); + } + }); } }; @@ -174,7 +244,6 @@ export default class ManagedTable extends styled.StylablePureComponent< }; onColumnOrder = (columnOrder: TableColumnOrder) => { - // $FlowFixMe this.setState({columnOrder}); // persist column order window.localStorage.setItem( @@ -187,45 +256,246 @@ export default class ManagedTable extends styled.StylablePureComponent< this.setState({columnSizes}); }; - setRef = (table: ?Table) => { - this.tableRef = table; - }; - scrollToBottom() { - const {tableRef} = this; - if (tableRef) { - tableRef.scrollToBottom(); + const {current} = this.tableRef; + if (current && this.props.rows.length > 1) { + current.scrollToItem(this.props.rows.length - 1); } } + onHighlight = ( + e: SyntheticMouseEvent<>, + row: TableBodyRow, + index: number, + ) => { + if (e.button !== 0) { + // Only highlight rows when using primary mouse button, + // otherwise do nothing, to not interfere context menus. + return; + } + if (e.shiftKey) { + // prevents text selection + e.preventDefault(); + } + + let {highlightedRows} = this.state; + + this.dragStartIndex = index; + document.addEventListener('mouseup', this.onStopDragSelecting); + + if ( + (e.metaKey && process.platform === 'darwin') || + (e.ctrlKey && process.platform !== 'darwin') + ) { + highlightedRows.add(row.key); + } else if (e.shiftKey) { + // range select + const lastItemKey = Array.from(this.state.highlightedRows).pop(); + highlightedRows = new Set([ + ...highlightedRows, + ...this.selectInRange(lastItemKey, row.key), + ]); + } else { + // single select + this.state.highlightedRows.clear(); + this.state.highlightedRows.add(row.key); + } + + this.setState({highlightedRows}); + }; + + onStopDragSelecting = () => { + this.dragStartIndex = null; + document.removeEventListener('mouseup', this.onStopDragSelecting); + }; + + selectInRange = (fromKey: string, toKey: string): Array => { + const selected = []; + let startIndex = -1; + let endIndex = -1; + for (let i = 0; i < this.props.rows.length; i++) { + if (this.props.rows[i].key === fromKey) { + startIndex = i; + } + if (this.props.rows[i].key === toKey) { + endIndex = i; + } + if (endIndex > -1 && startIndex > -1) { + break; + } + } + + for ( + let i = Math.min(startIndex, endIndex); + i <= Math.max(startIndex, endIndex); + i++ + ) { + try { + selected.push(this.props.rows[i].key); + } catch (e) {} + } + + return selected; + }; + + onMouseEnterRow = ( + e: SyntheticMouseEvent<>, + row: TableBodyRow, + index: number, + ) => { + const {dragStartIndex} = this; + const {current} = this.tableRef; + if (dragStartIndex && current) { + current.scrollToItem(index + 1); + const startKey = this.props.rows[dragStartIndex].key; + const highlightedRows = new Set(this.selectInRange(startKey, row.key)); + this.setState({ + highlightedRows, + }); + } + }; + + buildContextMenuItems = () => { + const {highlightedRows} = this.state; + if (highlightedRows.size === 0) { + return []; + } + + return [ + { + label: + highlightedRows.size > 1 + ? `Copy ${highlightedRows.size} rows` + : 'Copy row', + click: this.onCopy, + }, + { + label: 'Create Paste', + click: () => createPaste(this.getSelectedText()), + }, + ]; + }; + + getSelectedText = (): string => { + const {highlightedRows} = this.state; + + if (highlightedRows.size === 0) { + return ''; + } + return this.props.rows + .filter(row => highlightedRows.has(row.key)) + .map( + (row: TableBodyRow) => + row.copyText || + Array.from( + document.querySelectorAll(`[data-key='${row.key}'] > *`) || [], + ) + .map(node => node.textContent) + .join('\t'), + ) + .join('\n'); + }; + + onScroll = ({ + scrollDirection, + scrollOffset, + }: { + scrollDirection: 'forward' | 'backward', + scrollOffset: number, + scrollUpdateWasRequested: boolean, + }) => { + const {current} = this.scrollRef; + const parent = current ? current.parentElement : null; + if ( + this.props.stickyBottom && + scrollDirection === 'forward' && + !this.state.shouldScrollToBottom && + current && + parent instanceof HTMLElement && + current.offsetHeight - (scrollOffset + parent.offsetHeight) < + parent.offsetHeight + ) { + this.setState({shouldScrollToBottom: true}); + } else if ( + this.props.stickyBottom && + scrollDirection === 'backward' && + this.state.shouldScrollToBottom + ) { + this.setState({shouldScrollToBottom: false}); + } + }; + render() { - const {props, state} = this; + const { + onAddFilter, + columns, + multiline, + zebra, + rows, + rowLineHeight, + } = this.props; + + const {columnOrder, columnSizes, highlightedRows} = this.state; + const columnKeys = columnOrder + .map(k => (k.visible ? k.key : null)) + .filter(Boolean); return ( -
+ + + + + {({width, height}) => ( + + + (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}) => ( + 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} + /> + )} + + + )} + + + ); } } diff --git a/src/ui/components/table/Table.js b/src/ui/components/table/Table.js deleted file mode 100644 index 1158ae568..000000000 --- a/src/ui/components/table/Table.js +++ /dev/null @@ -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 - * [``](). - */ -export default class Table extends PureComponent { - 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) => { - 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 : ( - - ); - - return ( - - - {tableHead} - - - - - ); - } -} diff --git a/src/ui/components/table/TableBody.js b/src/ui/components/table/TableBody.js deleted file mode 100644 index 8401dc1e6..000000000 --- a/src/ui/components/table/TableBody.js +++ /dev/null @@ -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 ( - - - {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 ( - - {isFilterable && this.props.onAddFilter != null ? ( - - {value} - - ) : ( - value - )} - - ); - })} - - - ); - } -} - -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, -}; - -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 ( - - ); - }; - - 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 ( - - {this.props.rows.map(this.buildAutoElement)} - - ); - } - - let children; - - if (this.props.multiline === true) { - // multiline has a virtual list with dynamic heights - children = ( - - ); - } else { - // virtual list with a fixed row height - children = ( - - ); - } - - return {children}; - } -} diff --git a/src/ui/components/table/TableHead.js b/src/ui/components/table/TableHead.js index b0c165268..e8bebf408 100644 --- a/src/ui/components/table/TableHead.js +++ b/src/ui/components/table/TableHead.js @@ -6,7 +6,6 @@ */ import type { - TableColumnKeys, TableColumnOrder, TableColumnSizes, TableColumns, @@ -193,7 +192,6 @@ class TableHeadColumn extends PureComponent<{ export default class TableHead extends PureComponent<{ columnOrder: TableColumnOrder, onColumnOrder: ?(order: TableColumnOrder) => void, - columnKeys: TableColumnKeys, columns: TableColumns, sortOrder: ?TableRowSortOrder, onSort: ?TableOnSort, @@ -201,8 +199,15 @@ export default class TableHead extends PureComponent<{ onColumnResize: ?TableOnColumnResize, }> { buildContextMenu = (): MenuTemplate => { + const visibles = this.props.columnOrder + .map(c => (c.visible ? c.key : null)) + .filter(Boolean) + .reduce((acc, cv) => { + acc.add(cv); + return acc; + }, new Set()); return Object.keys(this.props.columns).map(key => { - const visible = this.props.columnKeys.includes(key); + const visible = visibles.has(key); return { label: this.props.columns[key].value, click: () => { diff --git a/src/ui/components/table/TableRow.js b/src/ui/components/table/TableRow.js new file mode 100644 index 000000000..3bdb74c72 --- /dev/null +++ b/src/ui/components/table/TableRow.js @@ -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 { + static defaultProps = { + zebra: true, + }; + + render() { + const { + index, + highlighted, + rowLineHeight, + row, + style, + multiline, + columnKeys, + columnSizes, + onMouseEnter, + onMouseDown, + zebra, + onAddFilter, + } = this.props; + + return ( + + {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 ( + + {isFilterable && onAddFilter != null ? ( + + {value} + + ) : ( + value + )} + + ); + })} + + ); + } +} diff --git a/src/ui/components/table/types.js b/src/ui/components/table/types.js index c5eb0ff01..345e18dea 100644 --- a/src/ui/components/table/types.js +++ b/src/ui/components/table/types.js @@ -16,8 +16,6 @@ type TableColumnOrderVal = { visible: boolean, }; -export type TableColumnRawOrder = Array; - export type TableColumnOrder = Array; export type TableColumnSizes = { diff --git a/src/ui/index.js b/src/ui/index.js index f4b1ba9f2..68b53369b 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -41,7 +41,6 @@ export type { TableColumnOrder, TableColumnSizes, } from './components/table/types.js'; -export {default as Table} from './components/table/Table.js'; export {default as ManagedTable} from './components/table/ManagedTable.js'; export type {ManagedTableProps} from './components/table/ManagedTable.js'; diff --git a/yarn.lock b/yarn.lock index be10896ee..b1a83ca01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -921,10 +921,6 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.3: - version "2.2.5" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" - cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -1297,10 +1293,6 @@ doctrine@^2.0.2, doctrine@^2.1.0: dependencies: esutils "^2.0.2" -"dom-helpers@^2.4.0 || ^3.0.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" - domexception@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -3258,7 +3250,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -3326,6 +3318,10 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +memoize-one@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" + meow@^3.1.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -4055,10 +4051,6 @@ react-is@^16.4.0: version "16.4.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf" -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - react-redux@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" @@ -4079,16 +4071,15 @@ react-test-renderer@^16: prop-types "^15.6.0" react-is "^16.4.0" -react-virtualized@^9.13.0: - version "9.19.1" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.19.1.tgz#84b53253df2d9df61c85ce037141edccc70a73fd" +react-virtualized-auto-sizer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd" + +react-window@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.1.1.tgz#8a0cf488c9db19425fb804f118f5aac6227d7fd2" dependencies: - babel-runtime "^6.26.0" - classnames "^2.2.3" - dom-helpers "^2.4.0 || ^3.0.0" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" + memoize-one "^3.1.1" react@16: version "16.4.0"