@bigfishtv/cockpit
Version:
335 lines (298 loc) • 10.7 kB
JavaScript
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>
)
}
}