parent
0fdeb05663
commit
699120a8fd
@ -1,80 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './TableSelectCell.css';
|
||||
|
||||
class TableSelectCell extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value: isSelected });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value: null });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ value, shiftKey }, a, b, c, d) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
id,
|
||||
isSelected,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowCell className={className}>
|
||||
<CheckInput
|
||||
className={styles.input}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
{...otherProps}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableSelectCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
TableSelectCell.defaultProps = {
|
||||
className: styles.selectCell,
|
||||
isSelected: false
|
||||
};
|
||||
|
||||
export default TableSelectCell;
|
@ -0,0 +1,59 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import TableRowCell, { TableRowCellProps } from './TableRowCell';
|
||||
import styles from './TableSelectCell.css';
|
||||
|
||||
interface TableSelectCellProps extends Omit<TableRowCellProps, 'id'> {
|
||||
className?: string;
|
||||
id: number | string;
|
||||
isSelected?: boolean;
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function TableSelectCell({
|
||||
className = styles.selectCell,
|
||||
id,
|
||||
isSelected = false,
|
||||
onSelectedChange,
|
||||
...otherProps
|
||||
}: TableSelectCellProps) {
|
||||
const initialIsSelected = useRef(isSelected);
|
||||
const handleSelectedChange = useRef(onSelectedChange);
|
||||
|
||||
handleSelectedChange.current = onSelectedChange;
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ value, shiftKey }: CheckInputChanged) => {
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
},
|
||||
[id, onSelectedChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleSelectedChange.current({
|
||||
id,
|
||||
value: initialIsSelected.current,
|
||||
shiftKey: false,
|
||||
});
|
||||
|
||||
return () => {
|
||||
handleSelectedChange.current({ id, value: null, shiftKey: false });
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<TableRowCell className={className}>
|
||||
<CheckInput
|
||||
className={styles.input}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
{...otherProps}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableSelectCell;
|
@ -1,29 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './VirtualTableRowCell.css';
|
||||
|
||||
function VirtualTableRowCell(props) {
|
||||
const {
|
||||
className,
|
||||
children
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VirtualTableRowCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
};
|
||||
|
||||
VirtualTableRowCell.defaultProps = {
|
||||
className: styles.cell
|
||||
};
|
||||
|
||||
export default VirtualTableRowCell;
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import styles from './VirtualTableRowCell.css';
|
||||
|
||||
export interface VirtualTableRowCellProps {
|
||||
className?: string;
|
||||
children?: string | React.ReactNode;
|
||||
}
|
||||
|
||||
function VirtualTableRowCell({
|
||||
className = styles.cell,
|
||||
children,
|
||||
}: VirtualTableRowCellProps) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
export default VirtualTableRowCell;
|
@ -1,83 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import VirtualTableRowCell from './VirtualTableRowCell';
|
||||
import styles from './VirtualTableSelectCell.css';
|
||||
|
||||
export function virtualTableSelectCellRenderer(cellProps) {
|
||||
const {
|
||||
cellKey,
|
||||
rowData,
|
||||
columnData,
|
||||
...otherProps
|
||||
} = cellProps;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
<VirtualTableSelectCell
|
||||
key={cellKey}
|
||||
id={rowData.name}
|
||||
isSelected={rowData.isSelected}
|
||||
{...columnData}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
class VirtualTableSelectCell extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
inputClassName,
|
||||
id,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
className={styles.cell}
|
||||
{...otherProps}
|
||||
>
|
||||
<CheckInput
|
||||
className={inputClassName}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualTableSelectCell.propTypes = {
|
||||
inputClassName: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
VirtualTableSelectCell.defaultProps = {
|
||||
inputClassName: styles.input,
|
||||
isSelected: false
|
||||
};
|
||||
|
||||
export default VirtualTableSelectCell;
|
@ -0,0 +1,46 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import VirtualTableRowCell, {
|
||||
VirtualTableRowCellProps,
|
||||
} from './VirtualTableRowCell';
|
||||
import styles from './VirtualTableSelectCell.css';
|
||||
|
||||
interface VirtualTableSelectCellProps extends VirtualTableRowCellProps {
|
||||
inputClassName?: string;
|
||||
id: number;
|
||||
isSelected?: boolean;
|
||||
isDisabled: boolean;
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function VirtualTableSelectCell({
|
||||
inputClassName = styles.input,
|
||||
id,
|
||||
isSelected = false,
|
||||
isDisabled,
|
||||
onSelectedChange,
|
||||
...otherProps
|
||||
}: VirtualTableSelectCellProps) {
|
||||
const handleChange = useCallback(
|
||||
({ value, shiftKey }: CheckInputChanged) => {
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
},
|
||||
[id, onSelectedChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell className={styles.cell} {...otherProps}>
|
||||
<CheckInput
|
||||
className={inputClassName}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default VirtualTableSelectCell;
|
@ -1,146 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { icons, scrollDirections } from 'Helpers/Props';
|
||||
import TableHeader from './TableHeader';
|
||||
import TableHeaderCell from './TableHeaderCell';
|
||||
import TableSelectAllHeaderCell from './TableSelectAllHeaderCell';
|
||||
import styles from './Table.css';
|
||||
|
||||
const tableHeaderCellProps = [
|
||||
'sortKey',
|
||||
'sortDirection'
|
||||
];
|
||||
|
||||
function getTableHeaderCellProps(props) {
|
||||
return _.reduce(tableHeaderCellProps, (result, key) => {
|
||||
if (props.hasOwnProperty(key)) {
|
||||
result[key] = props[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function Table(props) {
|
||||
const {
|
||||
className,
|
||||
horizontalScroll,
|
||||
selectAll,
|
||||
columns,
|
||||
optionsComponent,
|
||||
pageSize,
|
||||
canModifyColumns,
|
||||
children,
|
||||
onSortPress,
|
||||
onTableOptionChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
className={classNames(
|
||||
styles.tableContainer,
|
||||
horizontalScroll && styles.horizontalScroll
|
||||
)}
|
||||
scrollDirection={
|
||||
horizontalScroll ?
|
||||
scrollDirections.HORIZONTAL :
|
||||
scrollDirections.NONE
|
||||
}
|
||||
autoFocus={false}
|
||||
>
|
||||
<table className={className}>
|
||||
<TableHeader>
|
||||
{
|
||||
selectAll ?
|
||||
<TableSelectAllHeaderCell {...otherProps} /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible,
|
||||
isSortable,
|
||||
...otherColumnProps
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
(name === 'actions' || name === 'details') &&
|
||||
onTableOptionChange
|
||||
) {
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherProps}
|
||||
{...otherColumnProps}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={optionsComponent}
|
||||
pageSize={pageSize}
|
||||
canModifyColumns={canModifyColumns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</TableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={column.name}
|
||||
onSortPress={onSortPress}
|
||||
{...getTableHeaderCellProps(otherProps)}
|
||||
{...column}
|
||||
>
|
||||
{typeof column.label === 'function' ? column.label() : column.label}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
</TableHeader>
|
||||
{children}
|
||||
</table>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
|
||||
Table.propTypes = {
|
||||
...TableHeaderCell.props,
|
||||
className: PropTypes.string,
|
||||
horizontalScroll: PropTypes.bool.isRequired,
|
||||
selectAll: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
optionsComponent: PropTypes.elementType,
|
||||
pageSize: PropTypes.number,
|
||||
canModifyColumns: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
onSortPress: PropTypes.func,
|
||||
onTableOptionChange: PropTypes.func
|
||||
};
|
||||
|
||||
Table.defaultProps = {
|
||||
className: styles.table,
|
||||
horizontalScroll: true,
|
||||
selectAll: false
|
||||
};
|
||||
|
||||
export default Table;
|
@ -0,0 +1,124 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { icons, scrollDirections } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import Column from './Column';
|
||||
import TableHeader from './TableHeader';
|
||||
import TableHeaderCell from './TableHeaderCell';
|
||||
import TableSelectAllHeaderCell from './TableSelectAllHeaderCell';
|
||||
import styles from './Table.css';
|
||||
|
||||
interface TableProps {
|
||||
className?: string;
|
||||
horizontalScroll?: boolean;
|
||||
selectAll?: boolean;
|
||||
allSelected?: boolean;
|
||||
allUnselected?: boolean;
|
||||
columns: Column[];
|
||||
optionsComponent?: React.ElementType;
|
||||
pageSize?: number;
|
||||
canModifyColumns?: boolean;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
children?: React.ReactNode;
|
||||
onSortPress?: (name: string, sortDirection?: SortDirection) => void;
|
||||
onTableOptionChange?: (payload: TableOptionsChangePayload) => void;
|
||||
onSelectAllChange?: (change: CheckInputChanged) => void;
|
||||
}
|
||||
|
||||
function Table({
|
||||
className = styles.table,
|
||||
horizontalScroll = true,
|
||||
selectAll = false,
|
||||
allSelected = false,
|
||||
allUnselected = false,
|
||||
columns,
|
||||
optionsComponent,
|
||||
pageSize,
|
||||
canModifyColumns,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
children,
|
||||
onSortPress,
|
||||
onTableOptionChange,
|
||||
onSelectAllChange,
|
||||
}: TableProps) {
|
||||
return (
|
||||
<Scroller
|
||||
className={classNames(
|
||||
styles.tableContainer,
|
||||
horizontalScroll && styles.horizontalScroll
|
||||
)}
|
||||
scrollDirection={
|
||||
horizontalScroll ? scrollDirections.HORIZONTAL : scrollDirections.NONE
|
||||
}
|
||||
autoFocus={false}
|
||||
>
|
||||
<table className={className}>
|
||||
<TableHeader>
|
||||
{selectAll && onSelectAllChange ? (
|
||||
<TableSelectAllHeaderCell
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible, isSortable, ...otherColumnProps } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
(name === 'actions' || name === 'details') &&
|
||||
onTableOptionChange
|
||||
) {
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={name}
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherColumnProps}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={optionsComponent}
|
||||
pageSize={pageSize}
|
||||
canModifyColumns={canModifyColumns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton name={icons.ADVANCED_SETTINGS} />
|
||||
</TableOptionsModalWrapper>
|
||||
</TableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={column.name}
|
||||
{...column}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
{typeof column.label === 'function'
|
||||
? column.label()
|
||||
: column.label}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})}
|
||||
</TableHeader>
|
||||
{children}
|
||||
</table>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
|
||||
export default Table;
|
@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
class TableBody extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<tbody>{children}</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TableBody.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default TableBody;
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TableBodyProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function TableBody({ children }: TableBodyProps) {
|
||||
return <tbody>{children}</tbody>;
|
||||
}
|
||||
|
||||
export default TableBody;
|
@ -1,28 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
class TableHeader extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
{children}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableHeader.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default TableHeader;
|
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TableHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function TableHeader({ children }: TableHeaderProps) {
|
||||
return (
|
||||
<thead>
|
||||
<tr>{children}</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableHeader;
|
@ -1,99 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import styles from './TableHeaderCell.css';
|
||||
|
||||
class TableHeaderCell extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
fixedSortDirection
|
||||
} = this.props;
|
||||
|
||||
if (fixedSortDirection) {
|
||||
this.props.onSortPress(name, fixedSortDirection);
|
||||
} else {
|
||||
this.props.onSortPress(name);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
name,
|
||||
label,
|
||||
columnLabel,
|
||||
isSortable,
|
||||
isVisible,
|
||||
isModifiable,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
fixedSortDirection,
|
||||
children,
|
||||
onSortPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const isSorting = isSortable && sortKey === name;
|
||||
const sortIcon = sortDirection === sortDirections.ASCENDING ?
|
||||
icons.SORT_ASCENDING :
|
||||
icons.SORT_DESCENDING;
|
||||
|
||||
return (
|
||||
isSortable ?
|
||||
<Link
|
||||
{...otherProps}
|
||||
component="th"
|
||||
className={className}
|
||||
label={typeof label === 'function' ? label() : label}
|
||||
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{children}
|
||||
|
||||
{
|
||||
isSorting &&
|
||||
<Icon
|
||||
name={sortIcon}
|
||||
className={styles.sortIcon}
|
||||
/>
|
||||
}
|
||||
</Link> :
|
||||
|
||||
<th className={className}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableHeaderCell.propTypes = {
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
|
||||
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
isSortable: PropTypes.bool,
|
||||
isVisible: PropTypes.bool,
|
||||
isModifiable: PropTypes.bool,
|
||||
sortKey: PropTypes.string,
|
||||
fixedSortDirection: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
onSortPress: PropTypes.func
|
||||
};
|
||||
|
||||
TableHeaderCell.defaultProps = {
|
||||
className: styles.headerCell,
|
||||
isSortable: false
|
||||
};
|
||||
|
||||
export default TableHeaderCell;
|
@ -0,0 +1,70 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import styles from './TableHeaderCell.css';
|
||||
|
||||
interface TableHeaderCellProps {
|
||||
className?: string;
|
||||
name: string;
|
||||
label?: string | (() => string) | React.ReactNode;
|
||||
columnLabel?: string | (() => string);
|
||||
isSortable?: boolean;
|
||||
isVisible?: boolean;
|
||||
isModifiable?: boolean;
|
||||
sortKey?: string;
|
||||
fixedSortDirection?: SortDirection;
|
||||
sortDirection?: string;
|
||||
children?: React.ReactNode;
|
||||
onSortPress?: (name: string, sortDirection?: SortDirection) => void;
|
||||
}
|
||||
|
||||
function TableHeaderCell({
|
||||
className = styles.headerCell,
|
||||
name,
|
||||
label,
|
||||
columnLabel,
|
||||
isSortable = false,
|
||||
isVisible,
|
||||
isModifiable,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
fixedSortDirection,
|
||||
children,
|
||||
onSortPress,
|
||||
...otherProps
|
||||
}: TableHeaderCellProps) {
|
||||
const isSorting = isSortable && sortKey === name;
|
||||
const sortIcon =
|
||||
sortDirection === sortDirections.ASCENDING
|
||||
? icons.SORT_ASCENDING
|
||||
: icons.SORT_DESCENDING;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (fixedSortDirection) {
|
||||
onSortPress?.(name, fixedSortDirection);
|
||||
} else {
|
||||
onSortPress?.(name);
|
||||
}
|
||||
}, [name, fixedSortDirection, onSortPress]);
|
||||
|
||||
return isSortable ? (
|
||||
<Link
|
||||
{...otherProps}
|
||||
component="th"
|
||||
className={className}
|
||||
// label={typeof label === 'function' ? label() : label}
|
||||
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{children}
|
||||
|
||||
{isSorting && <Icon name={sortIcon} className={styles.sortIcon} />}
|
||||
</Link>
|
||||
) : (
|
||||
<th className={className}>{children}</th>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableHeaderCell;
|
@ -1,181 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TablePager.css';
|
||||
|
||||
class TablePager extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isShowingPageSelect: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOpenPageSelectClick = () => {
|
||||
this.setState({ isShowingPageSelect: true });
|
||||
};
|
||||
|
||||
onPageSelect = ({ value: page }) => {
|
||||
this.setState({ isShowingPageSelect: false });
|
||||
this.props.onPageSelect(parseInt(page));
|
||||
};
|
||||
|
||||
onPageSelectBlur = () => {
|
||||
this.setState({ isShowingPageSelect: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
page,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
isFetching,
|
||||
onFirstPagePress,
|
||||
onPreviousPagePress,
|
||||
onNextPagePress,
|
||||
onLastPagePress
|
||||
} = this.props;
|
||||
|
||||
const isShowingPageSelect = this.state.isShowingPageSelect;
|
||||
const pages = Array.from(new Array(totalPages), (x, i) => {
|
||||
const pageNumber = i + 1;
|
||||
|
||||
return {
|
||||
key: pageNumber,
|
||||
value: pageNumber
|
||||
};
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFirstPage = page === 1;
|
||||
const isLastPage = page === totalPages;
|
||||
|
||||
return (
|
||||
<div className={styles.pager}>
|
||||
<div className={styles.loadingContainer}>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.controlsContainer}>
|
||||
<div className={styles.controls}>
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isFirstPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isFirstPage}
|
||||
onPress={onFirstPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_FIRST} />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isFirstPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isFirstPage}
|
||||
onPress={onPreviousPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_PREVIOUS} />
|
||||
</Link>
|
||||
|
||||
<div className={styles.pageNumber}>
|
||||
{
|
||||
!isShowingPageSelect &&
|
||||
<Link
|
||||
isDisabled={totalPages === 1}
|
||||
onPress={this.onOpenPageSelectClick}
|
||||
>
|
||||
{page} / {totalPages}
|
||||
</Link>
|
||||
}
|
||||
|
||||
{
|
||||
isShowingPageSelect &&
|
||||
<SelectInput
|
||||
className={styles.pageSelect}
|
||||
name="pageSelect"
|
||||
value={page}
|
||||
values={pages}
|
||||
autoFocus={true}
|
||||
onChange={this.onPageSelect}
|
||||
onBlur={this.onPageSelectBlur}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isLastPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isLastPage}
|
||||
onPress={onNextPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_NEXT} />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isLastPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isLastPage}
|
||||
onPress={onLastPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_LAST} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.recordsContainer}>
|
||||
<div className={styles.records}>
|
||||
{translate('TotalRecords', { totalRecords })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TablePager.propTypes = {
|
||||
page: PropTypes.number,
|
||||
totalPages: PropTypes.number,
|
||||
totalRecords: PropTypes.number,
|
||||
isFetching: PropTypes.bool,
|
||||
onFirstPagePress: PropTypes.func.isRequired,
|
||||
onPreviousPagePress: PropTypes.func.isRequired,
|
||||
onNextPagePress: PropTypes.func.isRequired,
|
||||
onLastPagePress: PropTypes.func.isRequired,
|
||||
onPageSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TablePager;
|
@ -0,0 +1,159 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TablePager.css';
|
||||
|
||||
interface TablePagerProps {
|
||||
page?: number;
|
||||
totalPages?: number;
|
||||
totalRecords?: number;
|
||||
isFetching?: boolean;
|
||||
onFirstPagePress: () => void;
|
||||
onPreviousPagePress: () => void;
|
||||
onNextPagePress: () => void;
|
||||
onLastPagePress: () => void;
|
||||
onPageSelect: (page: number) => void;
|
||||
}
|
||||
|
||||
function TablePager({
|
||||
page,
|
||||
totalPages,
|
||||
totalRecords = 0,
|
||||
isFetching,
|
||||
onFirstPagePress,
|
||||
onPreviousPagePress,
|
||||
onNextPagePress,
|
||||
onLastPagePress,
|
||||
onPageSelect,
|
||||
}: TablePagerProps) {
|
||||
const [isShowingPageSelect, setIsShowingPageSelect] = useState(false);
|
||||
|
||||
const isFirstPage = page === 1;
|
||||
const isLastPage = page === totalPages;
|
||||
|
||||
const pages = useMemo(() => {
|
||||
return Array.from(new Array(totalPages), (_x, i) => {
|
||||
const pageNumber = i + 1;
|
||||
|
||||
return {
|
||||
key: pageNumber,
|
||||
value: String(pageNumber),
|
||||
};
|
||||
});
|
||||
}, [totalPages]);
|
||||
|
||||
const handleOpenPageSelectClick = useCallback(() => {
|
||||
setIsShowingPageSelect(true);
|
||||
}, []);
|
||||
|
||||
const handlePageSelect = useCallback(
|
||||
({ value }: InputChanged<number>) => {
|
||||
setIsShowingPageSelect(false);
|
||||
onPageSelect(value);
|
||||
},
|
||||
[onPageSelect]
|
||||
);
|
||||
|
||||
const handlePageSelectBlur = useCallback(() => {
|
||||
setIsShowingPageSelect(false);
|
||||
}, []);
|
||||
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.pager}>
|
||||
<div className={styles.loadingContainer}>
|
||||
{isFetching ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.controlsContainer}>
|
||||
<div className={styles.controls}>
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isFirstPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isFirstPage}
|
||||
onPress={onFirstPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_FIRST} />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isFirstPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isFirstPage}
|
||||
onPress={onPreviousPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_PREVIOUS} />
|
||||
</Link>
|
||||
|
||||
<div className={styles.pageNumber}>
|
||||
{isShowingPageSelect ? null : (
|
||||
<Link
|
||||
isDisabled={totalPages === 1}
|
||||
onPress={handleOpenPageSelectClick}
|
||||
>
|
||||
{page} / {totalPages}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{isShowingPageSelect ? (
|
||||
<SelectInput
|
||||
className={styles.pageSelect}
|
||||
name="pageSelect"
|
||||
value={page}
|
||||
values={pages}
|
||||
autoFocus={true}
|
||||
onChange={handlePageSelect}
|
||||
onBlur={handlePageSelectBlur}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isLastPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isLastPage}
|
||||
onPress={onNextPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_NEXT} />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.pageLink,
|
||||
isLastPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isLastPage}
|
||||
onPress={onLastPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_LAST} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.recordsContainer}>
|
||||
<div className={styles.records}>
|
||||
{translate('TotalRecords', { totalRecords })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TablePager;
|
@ -1,33 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './TableRow.css';
|
||||
|
||||
function TableRow(props) {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
overlayContent,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
TableRow.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.node,
|
||||
overlayContent: PropTypes.bool
|
||||
};
|
||||
|
||||
TableRow.defaultProps = {
|
||||
className: styles.row
|
||||
};
|
||||
|
||||
export default TableRow;
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import styles from './TableRow.css';
|
||||
|
||||
interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
overlayContent?: boolean;
|
||||
}
|
||||
|
||||
function TableRow({
|
||||
className = styles.row,
|
||||
children,
|
||||
overlayContent,
|
||||
...otherProps
|
||||
}: TableRowProps) {
|
||||
return (
|
||||
<tr className={className} {...otherProps}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableRow;
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TableRow from './TableRow';
|
||||
import styles from './TableRowButton.css';
|
||||
|
||||
function TableRowButton(props) {
|
||||
return (
|
||||
<Link
|
||||
className={styles.row}
|
||||
component={TableRow}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableRowButton;
|
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import Link, { LinkProps } from 'Components/Link/Link';
|
||||
import TableRow from './TableRow';
|
||||
import styles from './TableRowButton.css';
|
||||
|
||||
function TableRowButton(props: LinkProps) {
|
||||
return <Link className={styles.row} component={TableRow} {...props} />;
|
||||
}
|
||||
|
||||
export default TableRowButton;
|
@ -1,47 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import VirtualTableHeaderCell from './TableHeaderCell';
|
||||
import styles from './TableSelectAllHeaderCell.css';
|
||||
|
||||
function getValue(allSelected, allUnselected) {
|
||||
if (allSelected) {
|
||||
return true;
|
||||
} else if (allUnselected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function TableSelectAllHeaderCell(props) {
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange
|
||||
} = props;
|
||||
|
||||
const value = getValue(allSelected, allUnselected);
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.selectAllHeaderCell}
|
||||
name="selectAll"
|
||||
>
|
||||
<CheckInput
|
||||
className={styles.input}
|
||||
name="selectAll"
|
||||
value={value}
|
||||
onChange={onSelectAllChange}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
TableSelectAllHeaderCell.propTypes = {
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TableSelectAllHeaderCell;
|
@ -0,0 +1,43 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import VirtualTableHeaderCell from './TableHeaderCell';
|
||||
import styles from './TableSelectAllHeaderCell.css';
|
||||
|
||||
interface TableSelectAllHeaderCellProps {
|
||||
allSelected: boolean;
|
||||
allUnselected: boolean;
|
||||
onSelectAllChange: (change: CheckInputChanged) => void;
|
||||
}
|
||||
|
||||
function TableSelectAllHeaderCell({
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
}: TableSelectAllHeaderCellProps) {
|
||||
const value = useMemo(() => {
|
||||
if (allSelected) {
|
||||
return true;
|
||||
} else if (allUnselected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [allSelected, allUnselected]);
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.selectAllHeaderCell}
|
||||
name="selectAll"
|
||||
>
|
||||
<CheckInput
|
||||
className={styles.input}
|
||||
name="selectAll"
|
||||
value={value}
|
||||
onChange={onSelectAllChange}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableSelectAllHeaderCell;
|
@ -1,202 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import Measure from 'Components/Measure';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import styles from './VirtualTable.css';
|
||||
|
||||
const ROW_HEIGHT = 38;
|
||||
|
||||
function overscanIndicesGetter(options) {
|
||||
const {
|
||||
cellCount,
|
||||
overscanCellsCount,
|
||||
startIndex,
|
||||
stopIndex
|
||||
} = options;
|
||||
|
||||
// The default getter takes the scroll direction into account,
|
||||
// but that can cause issues. Ignore the scroll direction and
|
||||
// always over return more items.
|
||||
|
||||
const overscanStartIndex = startIndex - overscanCellsCount;
|
||||
const overscanStopIndex = stopIndex + overscanCellsCount;
|
||||
|
||||
return {
|
||||
overscanStartIndex: Math.max(0, overscanStartIndex),
|
||||
overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex)
|
||||
};
|
||||
}
|
||||
|
||||
class VirtualTable extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
scrollIndex,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: scrollIndex,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.setState({
|
||||
width
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSmallScreen,
|
||||
className,
|
||||
items,
|
||||
scroller,
|
||||
header,
|
||||
headerHeight,
|
||||
rowHeight,
|
||||
rowRenderer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width
|
||||
} = this.state;
|
||||
|
||||
const gridStyle = {
|
||||
boxSizing: undefined,
|
||||
direction: undefined,
|
||||
height: undefined,
|
||||
position: undefined,
|
||||
willChange: undefined,
|
||||
overflow: undefined,
|
||||
width: undefined
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
position: undefined
|
||||
};
|
||||
|
||||
return (
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
{header}
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
{...otherProps}
|
||||
ref={this.setGridRef}
|
||||
autoContainerWidth={true}
|
||||
autoHeight={true}
|
||||
autoWidth={true}
|
||||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={rowHeight}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
scrollTop={scrollTop}
|
||||
onScroll={onChildScroll}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={rowRenderer}
|
||||
overscanIndicesGetter={overscanIndicesGetter}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptout={true}
|
||||
className={styles.tableBodyContainer}
|
||||
style={gridStyle}
|
||||
containerStyle={containerStyle}
|
||||
/>
|
||||
</div>
|
||||
</Scroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualTable.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
scrollIndex: PropTypes.number,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
header: PropTypes.node.isRequired,
|
||||
headerHeight: PropTypes.number.isRequired,
|
||||
rowRenderer: PropTypes.func.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38,
|
||||
rowHeight: ROW_HEIGHT
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
@ -0,0 +1,167 @@
|
||||
import React, { ReactNode, useEffect, useRef } from 'react';
|
||||
import { Grid, GridCellProps, WindowScroller } from 'react-virtualized';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import styles from './VirtualTable.css';
|
||||
|
||||
const ROW_HEIGHT = 38;
|
||||
|
||||
function overscanIndicesGetter(options: {
|
||||
cellCount: number;
|
||||
overscanCellsCount: number;
|
||||
startIndex: number;
|
||||
stopIndex: number;
|
||||
}) {
|
||||
const { cellCount, overscanCellsCount, startIndex, stopIndex } = options;
|
||||
|
||||
// The default getter takes the scroll direction into account,
|
||||
// but that can cause issues. Ignore the scroll direction and
|
||||
// always over return more items.
|
||||
|
||||
const overscanStartIndex = startIndex - overscanCellsCount;
|
||||
const overscanStopIndex = stopIndex + overscanCellsCount;
|
||||
|
||||
return {
|
||||
overscanStartIndex: Math.max(0, overscanStartIndex),
|
||||
overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex),
|
||||
};
|
||||
}
|
||||
|
||||
interface VirtualTableProps<T extends ModelBase> {
|
||||
isSmallScreen: boolean;
|
||||
className?: string;
|
||||
items: T[];
|
||||
scrollIndex?: number;
|
||||
scrollTop?: number;
|
||||
scroller: Element;
|
||||
header: React.ReactNode;
|
||||
headerHeight?: number;
|
||||
rowRenderer: (rowProps: GridCellProps) => ReactNode;
|
||||
rowHeight?: number;
|
||||
}
|
||||
|
||||
function VirtualTable<T extends ModelBase>({
|
||||
isSmallScreen,
|
||||
className = styles.tableContainer,
|
||||
items,
|
||||
scroller,
|
||||
scrollIndex,
|
||||
scrollTop,
|
||||
header,
|
||||
headerHeight = 38,
|
||||
rowHeight = ROW_HEIGHT,
|
||||
rowRenderer,
|
||||
...otherProps
|
||||
}: VirtualTableProps<T>) {
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const gridRef = useRef<Grid>(null);
|
||||
const scrollRestored = useRef(false);
|
||||
const previousScrollIndex = usePrevious(scrollIndex);
|
||||
const previousItems = usePrevious(items);
|
||||
|
||||
const width = bounds.width;
|
||||
|
||||
const gridStyle = {
|
||||
boxSizing: undefined,
|
||||
direction: undefined,
|
||||
height: undefined,
|
||||
position: undefined,
|
||||
willChange: undefined,
|
||||
overflow: undefined,
|
||||
width: undefined,
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
position: undefined,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (gridRef.current && width > 0) {
|
||||
gridRef.current.recomputeGridSize();
|
||||
}
|
||||
}, [width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
gridRef.current &&
|
||||
previousItems &&
|
||||
hasDifferentItemsOrOrder(previousItems, items)
|
||||
) {
|
||||
gridRef.current.recomputeGridSize();
|
||||
}
|
||||
}, [items, previousItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gridRef.current && scrollTop && !scrollRestored.current) {
|
||||
gridRef.current.scrollToPosition({ scrollLeft: 0, scrollTop });
|
||||
scrollRestored.current = true;
|
||||
}
|
||||
}, [scrollTop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
gridRef.current &&
|
||||
scrollIndex != null &&
|
||||
scrollIndex !== previousScrollIndex
|
||||
) {
|
||||
gridRef.current.scrollToCell({
|
||||
rowIndex: scrollIndex,
|
||||
columnIndex: 0,
|
||||
});
|
||||
}
|
||||
}, [scrollIndex, previousScrollIndex]);
|
||||
|
||||
return (
|
||||
<WindowScroller scrollElement={isSmallScreen ? undefined : scroller}>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
{header}
|
||||
|
||||
{/* @ts-expect-error - ref type is incompatible */}
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
{...otherProps}
|
||||
ref={gridRef}
|
||||
autoContainerWidth={true}
|
||||
autoHeight={true}
|
||||
autoWidth={true}
|
||||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={rowHeight}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={rowRenderer}
|
||||
overscanIndicesGetter={overscanIndicesGetter}
|
||||
scrollToAlignment="start"
|
||||
isScrollingOptout={true}
|
||||
className={styles.tableBodyContainer}
|
||||
style={gridStyle}
|
||||
containerStyle={containerStyle}
|
||||
onScroll={onChildScroll}
|
||||
/>
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</WindowScroller>
|
||||
);
|
||||
}
|
||||
|
||||
export default VirtualTable;
|
@ -1,17 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './VirtualTableHeader.css';
|
||||
|
||||
function VirtualTableHeader({ children }) {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VirtualTableHeader.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default VirtualTableHeader;
|
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from './VirtualTableHeader.css';
|
||||
|
||||
interface VirtualTableHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function VirtualTableHeader({ children }: VirtualTableHeaderProps) {
|
||||
return <div className={styles.header}>{children}</div>;
|
||||
}
|
||||
|
||||
export default VirtualTableHeader;
|
@ -1,109 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import styles from './VirtualTableHeaderCell.css';
|
||||
|
||||
export function headerRenderer(headerProps) {
|
||||
const {
|
||||
columnData = {},
|
||||
dataKey,
|
||||
label
|
||||
} = headerProps;
|
||||
|
||||
return (
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
<VirtualTableHeaderCell
|
||||
name={dataKey}
|
||||
{...columnData}
|
||||
>
|
||||
{label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
class VirtualTableHeaderCell extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
fixedSortDirection
|
||||
} = this.props;
|
||||
|
||||
if (fixedSortDirection) {
|
||||
this.props.onSortPress(name, fixedSortDirection);
|
||||
} else {
|
||||
this.props.onSortPress(name);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
name,
|
||||
isSortable,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
fixedSortDirection,
|
||||
children,
|
||||
onSortPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const isSorting = isSortable && sortKey === name;
|
||||
const sortIcon = sortDirection === sortDirections.ASCENDING ?
|
||||
icons.SORT_ASCENDING :
|
||||
icons.SORT_DESCENDING;
|
||||
|
||||
return (
|
||||
isSortable ?
|
||||
<Link
|
||||
component="div"
|
||||
className={className}
|
||||
onPress={this.onPress}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
{
|
||||
isSorting &&
|
||||
<Icon
|
||||
name={sortIcon}
|
||||
className={styles.sortIcon}
|
||||
/>
|
||||
}
|
||||
</Link> :
|
||||
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualTableHeaderCell.propTypes = {
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
isSortable: PropTypes.bool,
|
||||
sortKey: PropTypes.string,
|
||||
fixedSortDirection: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
onSortPress: PropTypes.func
|
||||
};
|
||||
|
||||
VirtualTableHeaderCell.defaultProps = {
|
||||
className: styles.headerCell,
|
||||
isSortable: false
|
||||
};
|
||||
|
||||
export default VirtualTableHeaderCell;
|
@ -0,0 +1,60 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import styles from './VirtualTableHeaderCell.css';
|
||||
|
||||
interface VirtualTableHeaderCellProps {
|
||||
className?: string;
|
||||
name: string;
|
||||
isSortable?: boolean;
|
||||
sortKey?: string;
|
||||
fixedSortDirection?: SortDirection;
|
||||
sortDirection?: string;
|
||||
children?: React.ReactNode;
|
||||
onSortPress?: (name: string, sortDirection?: SortDirection) => void;
|
||||
}
|
||||
|
||||
function VirtualTableHeaderCell({
|
||||
className = styles.headerCell,
|
||||
name,
|
||||
isSortable = false,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
fixedSortDirection,
|
||||
children,
|
||||
onSortPress,
|
||||
...otherProps
|
||||
}: VirtualTableHeaderCellProps) {
|
||||
const isSorting = isSortable && sortKey === name;
|
||||
const sortIcon =
|
||||
sortDirection === sortDirections.ASCENDING
|
||||
? icons.SORT_ASCENDING
|
||||
: icons.SORT_DESCENDING;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (fixedSortDirection) {
|
||||
onSortPress?.(name, fixedSortDirection);
|
||||
} else {
|
||||
onSortPress?.(name);
|
||||
}
|
||||
}, [name, fixedSortDirection, onSortPress]);
|
||||
|
||||
return isSortable ? (
|
||||
<Link
|
||||
component="div"
|
||||
className={className}
|
||||
onPress={handlePress}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
{isSorting ? <Icon name={sortIcon} className={styles.sortIcon} /> : null}
|
||||
</Link>
|
||||
) : (
|
||||
<div className={className}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VirtualTableHeaderCell;
|
@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './VirtualTableRow.css';
|
||||
|
||||
function VirtualTableRow(props) {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
style,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={style}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VirtualTableRow.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
style: PropTypes.object.isRequired,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
VirtualTableRow.defaultProps = {
|
||||
className: styles.row
|
||||
};
|
||||
|
||||
export default VirtualTableRow;
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import styles from './VirtualTableRow.css';
|
||||
|
||||
interface VirtualTableRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
className: string;
|
||||
style: object;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function VirtualTableRow({
|
||||
className = styles.row,
|
||||
children,
|
||||
style,
|
||||
...otherProps
|
||||
}: VirtualTableRowProps) {
|
||||
return (
|
||||
<div className={className} style={style} {...otherProps}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VirtualTableRow;
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import VirtualTableRow from './VirtualTableRow';
|
||||
import styles from './VirtualTableRowButton.css';
|
||||
|
||||
function VirtualTableRowButton(props) {
|
||||
return (
|
||||
<Link
|
||||
className={styles.row}
|
||||
component={VirtualTableRow}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default VirtualTableRowButton;
|
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import Link, { LinkProps } from 'Components/Link/Link';
|
||||
import VirtualTableRow from './VirtualTableRow';
|
||||
import styles from './VirtualTableRowButton.css';
|
||||
|
||||
function VirtualTableRowButton(props: LinkProps) {
|
||||
return <Link className={styles.row} component={VirtualTableRow} {...props} />;
|
||||
}
|
||||
|
||||
export default VirtualTableRowButton;
|
@ -1,47 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import VirtualTableHeaderCell from './VirtualTableHeaderCell';
|
||||
import styles from './VirtualTableSelectAllHeaderCell.css';
|
||||
|
||||
function getValue(allSelected, allUnselected) {
|
||||
if (allSelected) {
|
||||
return true;
|
||||
} else if (allUnselected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function VirtualTableSelectAllHeaderCell(props) {
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange
|
||||
} = props;
|
||||
|
||||
const value = getValue(allSelected, allUnselected);
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.selectAllHeaderCell}
|
||||
name="selectAll"
|
||||
>
|
||||
<CheckInput
|
||||
className={styles.input}
|
||||
name="selectAll"
|
||||
value={value}
|
||||
onChange={onSelectAllChange}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
VirtualTableSelectAllHeaderCell.propTypes = {
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default VirtualTableSelectAllHeaderCell;
|
@ -0,0 +1,43 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import VirtualTableHeaderCell from './VirtualTableHeaderCell';
|
||||
import styles from './VirtualTableSelectAllHeaderCell.css';
|
||||
|
||||
interface VirtualTableSelectAllHeaderCellProps {
|
||||
allSelected: boolean;
|
||||
allUnselected: boolean;
|
||||
onSelectAllChange: (change: CheckInputChanged) => void;
|
||||
}
|
||||
|
||||
function VirtualTableSelectAllHeaderCell({
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
}: VirtualTableSelectAllHeaderCellProps) {
|
||||
const value = useMemo(() => {
|
||||
if (allSelected) {
|
||||
return true;
|
||||
} else if (allUnselected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [allSelected, allUnselected]);
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.selectAllHeaderCell}
|
||||
name="selectAll"
|
||||
>
|
||||
<CheckInput
|
||||
className={styles.input}
|
||||
name="selectAll"
|
||||
value={value}
|
||||
onChange={onSelectAllChange}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default VirtualTableSelectAllHeaderCell;
|
@ -1,5 +1,5 @@
|
||||
export interface SelectStateInputProps {
|
||||
id: number;
|
||||
value: boolean;
|
||||
id: number | string;
|
||||
value: boolean | null;
|
||||
shiftKey: boolean;
|
||||
}
|
||||
|
Loading…
Reference in new issue