UNPKG

@bigfishtv/cockpit

Version:

335 lines (298 loc) 10.7 kB
import PropTypes from 'prop-types' import React, { Component } from 'react' import deepEqual from 'deep-equal' import keyCode from 'keycode' import _isEqual from 'lodash/isEqual' import _debounce from 'lodash/debounce' import { formatDate } from '../../utils/timeUtils' import { getDataHash } from '../../utils/lifecycleUtils' import { sortByObjectKey, getFieldsFromSchemaAndAssociations, normalizeFields } from '../../utils/tableUtils' import { isCtrlKeyPressed, isShiftKeyPressed } from '../../utils/selectKeyUtils' import * as SortTypes from '../../constants/SortTypes' import FixedDataTable from './FixedDataTable' import FixedDataTableCheckboxSelectCell from './cell/FixedDataTableCheckboxSelectCell' import FixedDataTableCheckboxSelectHeaderCell from './cell/FixedDataTableCheckboxSelectHeaderCell' const dateTimeFormat = 'YYYY-MM-DDTHH:mm:ssZ' const _defaultProps = { uncontrolled: false, allowSelection: true, multiselect: true, stickySelect: false, data: [], selectedIds: [], checkboxSelection: false, tableWidth: 600, tableHeight: 500, fixedWidth: false, fixedHeight: false, cellWidth: 180, rowHeight: 42, negativeHeight: 167, associations: [], defaultSortField: 'modified', defaultSortDirection: SortTypes.DESC, componentResolver: () => null, attributeModifier: () => ({}), onSelectionChange: () => console.warn('[TableView] no onSelectionChange prop provided'), onSelect: () => console.warn('[TableView] no onSelect prop provided'), onSortChange: (key, direction, type) => {}, } /* { key: 'enabled', value: ' ', width: 30, Cell: FixedDataTableStatusCell, }, */ /** * */ export default class TableView extends Component { static defaultProps = _defaultProps static propTypes = { /** Array of objects representing table rows */ data: PropTypes.arrayOf(PropTypes.object), /** Array of objects representing table columns/schema */ fields: PropTypes.arrayOf(PropTypes.object), /** array of table schema passed in from backend */ schema: PropTypes.array, /** array of entity assocations passed in from backend */ assocations: PropTypes.array, /** Array of selected row ids */ selectedIds: PropTypes.arrayOf(PropTypes.number), /** Whether or not component should control its own sorting */ uncontrolled: PropTypes.bool, /** Whether or not to inject a checkbox column to control selection state */ checkboxSelection: PropTypes.bool, tableWidth: PropTypes.number, tableHeight: PropTypes.number, fixedHeight: PropTypes.bool, fixedWidth: PropTypes.bool, cellWidth: PropTypes.number, rowHeight: PropTypes.number, headerHeight: PropTypes.number, /** column key to sort by, can even be nested e.g. 'collection.title' */ defaultSortField: PropTypes.string, defaultSortDirections: PropTypes.oneOf([SortTypes.ASC, SortTypes.DESC]), /** On row(s) selection change */ onSelectionChange: PropTypes.func, /** On row double click */ onSelect: PropTypes.func, /** Component to replace default HeaderCell -- invisibly passed into FixedDataTable */ HeaderCell: PropTypes.func, /** Component to replace default table cell component -- invisibly passed into FixedDataTable */ DefaultCell: PropTypes.func, } constructor(props) { super(props) this.lastSelectedId = null this.lastSelectedList = [] this.handleResize = _debounce(this.handleResize, 200) this.fieldsHash = getDataHash(this.getTableFields(props)) const columnKey = localStorage[this.fieldsHash + '_sortField'] || props.defaultSortField const sortDirection = localStorage[this.fieldsHash + '_sortDirection'] || props.defaultSortDirection const data = (props.data || []).sort(sortByObjectKey(columnKey, sortDirection)) this.state = { inited: false, data, preSortedData: props.data, selectedIds: props.selectedIds, tableWidth: props.tableWidth, tableHeight: props.tableHeight, columnKey, sortDirection, } } getTableFields(props = this.props) { let tableFields = [] if (props.schema instanceof Array && props.schema.length) tableFields = getFieldsFromSchemaAndAssociations( props.schema, props.associations, props.componentResolver, props.attributeModifier ) else if (props.fields instanceof Array && props.fields.length) tableFields = normalizeFields(props.fields) if (props.checkboxSelection) { tableFields = [ { fixed: true, resizable: false, order: -999, width: 28, Cell: FixedDataTableCheckboxSelectCell, HeaderCell: FixedDataTableCheckboxSelectHeaderCell, }, ...tableFields, ] } return tableFields } componentDidUpdate() { localStorage[this.fieldsHash + '_sortField'] = this.state.columnKey localStorage[this.fieldsHash + '_sortDirection'] = this.state.sortDirection } componentDidMount() { window.addEventListener('resize', this.handleResize) window.addEventListener('keydown', this.handleKeyDown) this.handleResize() } componentWillUnmount() { window.removeEventListener('resize', this.handleResize) window.removeEventListener('keydown', this.handleKeyDown) } componentWillReceiveProps(nextProps) { Object.keys(nextProps).map(key => { if (key == 'data') { if (!deepEqual(this.state.preSortedData, nextProps.data)) { const data = this.sortData( nextProps.data.map(item => { // this is temporary because dummy data had weird date format, won't break when changed to real data but should be removed if ('modified' in item) item.modified = formatDate(item.modified, dateTimeFormat) if ('created' in item) item.created = formatDate(item.created, dateTimeFormat) return item }) ) this.setState({ data, preSortedData: nextProps.data }) } // if prop key is in state then update it in state } else if ( key in this.state && nextProps[key] != _defaultProps[key] && !_isEqual(nextProps[key], this.state[key]) ) { this.setState({ [key]: nextProps[key] }) } }) } handleResize = () => { const tableContainer = this.refs.tableContainer if (!tableContainer) return const boundingRect = tableContainer.getBoundingClientRect() const { negativeHeight, fixedHeight, tableHeight, fixedWidth, tableWidth } = this.props // shortcut logic so we not constantly setting state when not needed if (this.state.inited && fixedWidth && fixedHeight) return this.setState({ inited: true, tableWidth: fixedWidth ? tableWidth : boundingRect.width, tableHeight: fixedHeight ? tableHeight : window.innerHeight - negativeHeight, }) } handleKeyDown = event => { const { data } = this.state if (!data.length) return const key = keyCode(event) const activeElement = document.activeElement if (!activeElement || (activeElement.nodeName !== 'INPUT' && activeElement.nodeName !== 'TEXTAREA')) { if (key === 'up') { event.preventDefault() const index = this.lastSelectedId === null ? data.length - 1 : data.reduce((value, row, i) => (row.id === this.lastSelectedId ? i - 1 : value), null) this.handleSelect(data[index <= 0 ? 0 : index]) } else if (key === 'down') { event.preventDefault() const index = this.lastSelectedId === null ? 0 : data.reduce((value, row, i) => (row.id === this.lastSelectedId ? i + 1 : value), null) this.handleSelect(data[index >= data.length - 1 ? data.length - 1 : index]) } else if (key == 'esc') { event.preventDefault() this.handleDeselectAll() } else if (key == 'a' && isCtrlKeyPressed()) { event.preventDefault() this.handleSelectAll() } } } handleSelect = row => { const { multiselect, allowSelection, stickySelect, checkboxSelection } = this.props if (!allowSelection) return let selectedIds = this.state.selectedIds if (!multiselect || (!checkboxSelection && !isCtrlKeyPressed() && !isShiftKeyPressed())) { selectedIds = selectedIds.length === 1 && selectedIds[0] === row.id && !stickySelect ? [] : [row.id] } else if (isShiftKeyPressed()) { const { data } = this.state const lastIndex = data.indexOf(data.filter(item => item.id === this.lastSelectedId)[0]) const nextIndex = data.indexOf(data.filter(item => item.id === row.id)[0]) const lower = Math.min(lastIndex, nextIndex) const upper = Math.max(lastIndex, nextIndex) selectedIds = [ ...this.lastSelectedList, ...data.filter((item, i) => i >= lower && i <= upper).map(item => item.id), ] } else if (isCtrlKeyPressed() || checkboxSelection) { selectedIds = selectedIds.indexOf(row.id) >= 0 ? selectedIds.filter(id => id !== row.id) : [...selectedIds, row.id] } // remove any duplicate ids selectedIds = selectedIds.filter((val, i) => selectedIds.indexOf(val) === i) this.lastSelectedId = row.id this.lastSelectedList = selectedIds.slice() if (!this.props.uncontrolled) { this.props.onSelectionChange(selectedIds) } else { this.setState({ selectedIds }) } } handleSelectAll() { const { data } = this.state const selectedIds = data.map(row => row.id) this.lastSelectedList = selectedIds.slice() if (!this.props.uncontrolled) { this.props.onSelectionChange(selectedIds) } else { this.setState({ selectedIds }) } } handleDeselectAll() { this.lastSelectedList = [] const selectedIds = [] if (!this.props.uncontrolled) { this.props.onSelectionChange(selectedIds) } else { this.setState({ selectedIds }) } } handleSelected = row => { this.props.onSelect(row) } onSortChange = (columnKey, sortDirection, sortType) => { const data = [...this.state.data].sort(sortByObjectKey(columnKey, sortDirection, sortType)) this.props.onSortChange(columnKey, sortDirection, sortType) this.setState({ data, columnKey, sortDirection }) } sortData(data) { const { columnKey, sortDirection } = this.state return data.sort(sortByObjectKey(columnKey, sortDirection)) } render() { const { inited, data, selectedIds, tableWidth, tableHeight, columnKey, sortDirection } = this.state const { cellWidth, rowHeight } = this.props const tableFields = this.getTableFields() return ( <div ref="tableContainer" style={!inited ? { opacity: 0 } : {}}> <FixedDataTable {...this.props} uncontrolled={false} data={data} fields={tableFields} selectedIds={selectedIds} tableWidth={tableWidth} tableHeight={tableHeight} cellWidth={cellWidth} rowHeight={rowHeight} columnKey={columnKey} sortDirection={sortDirection} onSelect={this.handleSelect} onSelected={this.handleSelected} onSortChange={this.onSortChange} onSelectionChange={this.props.onSelectionChange} /> </div> ) } }