New: Project Aphrodite

This commit is contained in:
Qstick
2018-11-23 02:04:42 -05:00
parent 65efa15551
commit 8430cb40ab
1080 changed files with 73015 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
.cell {
composes: cell from './TableRowCell.css';
width: 180px;
}

View File

@@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import TableRowCell from './TableRowCell';
import styles from './RelativeDateCell.css';
function RelativeDateCell(props) {
const {
className,
date,
includeSeconds,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
component: Component,
dispatch,
...otherProps
} = props;
if (!date) {
return (
<Component
className={className}
{...otherProps}
/>
);
}
return (
<Component
className={className}
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
</Component>
);
}
RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired,
date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
component: PropTypes.func,
dispatch: PropTypes.func
};
RelativeDateCell.defaultProps = {
className: styles.cell,
includeSeconds: false,
component: TableRowCell
};
export default RelativeDateCell;

View File

@@ -0,0 +1,21 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import RelativeDateCell from './RelativeDateCell';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return _.pick(uiSettings, [
'showRelativeDates',
'shortDateFormat',
'longDateFormat',
'timeFormat'
]);
}
);
}
export default connect(createMapStateToProps, null)(RelativeDateCell);

View File

@@ -0,0 +1,11 @@
.cell {
padding: 8px;
border-top: 1px solid #eee;
line-height: 1.52857143;
}
@media only screen and (max-width: $breakpointSmall) {
.cell {
white-space: nowrap;
}
}

View File

@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styles from './TableRowCell.css';
class TableRowCell extends Component {
//
// Render
render() {
const {
className,
children,
...otherProps
} = this.props;
return (
<td
className={className}
{...otherProps}
>
{children}
</td>
);
}
}
TableRowCell.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
};
TableRowCell.defaultProps = {
className: styles.cell
};
export default TableRowCell;

View File

@@ -0,0 +1,4 @@
.cell {
composes: cell from './TableRowCell.css';
composes: link from 'Components/Link/Link.css';
}

View File

@@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
function TableRowCellButton({ className, ...otherProps }) {
return (
<Link
className={className}
component={TableRowCell}
{...otherProps}
/>
);
}
TableRowCellButton.propTypes = {
className: PropTypes.string.isRequired
};
TableRowCellButton.defaultProps = {
className: styles.cell
};
export default TableRowCellButton;

View File

@@ -0,0 +1,11 @@
.selectCell {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 30px;
}
.input {
composes: input from 'Components/Form/CheckInput.css';
margin: 0;
}

View File

@@ -0,0 +1,80 @@
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;

View File

@@ -0,0 +1,14 @@
.cell {
@add-mixin truncate;
composes: cell from 'Components/Table/Cells/TableRowCell.css';
flex-grow: 0;
flex-shrink: 1;
white-space: nowrap;
}
@media only screen and (max-width: $breakpointSmall) {
.cell {
white-space: nowrap;
}
}

View File

@@ -0,0 +1,29 @@
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;

View File

@@ -0,0 +1,11 @@
.cell {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 0 36px;
}
.input {
composes: input from 'Components/Form/CheckInput.css';
margin: 0;
}

View File

@@ -0,0 +1,82 @@
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 (
<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;

View File

@@ -0,0 +1,16 @@
.tableContainer {
overflow-x: auto;
}
.table {
max-width: 100%;
width: 100%;
border-collapse: collapse;
}
@media only screen and (max-width: $breakpointSmall) {
.tableContainer {
overflow-y: hidden;
width: 100%;
}
}

View File

@@ -0,0 +1,129 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { icons, scrollDirections } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import Scroller from 'Components/Scroller/Scroller';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
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,
selectAll,
columns,
optionsComponent,
pageSize,
canModifyColumns,
children,
onSortPress,
onTableOptionChange,
...otherProps
} = props;
return (
<Scroller
className={styles.tableContainer}
scrollDirection={scrollDirections.HORIZONTAL}
>
<table className={className}>
<TableHeader>
{
selectAll &&
<TableSelectAllHeaderCell {...otherProps} />
}
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (
(name === 'actions' || name === 'details') &&
onTableOptionChange
) {
return (
<TableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
{...otherProps}
>
<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}
>
{column.label}
</TableHeaderCell>
);
})
}
</TableHeader>
{children}
</table>
</Scroller>
);
}
Table.propTypes = {
className: PropTypes.string,
selectAll: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
optionsComponent: PropTypes.func,
pageSize: PropTypes.number,
canModifyColumns: PropTypes.bool,
children: PropTypes.node,
onSortPress: PropTypes.func,
onTableOptionChange: PropTypes.func
};
Table.defaultProps = {
className: styles.table,
selectAll: false
};
export default Table;

View File

@@ -0,0 +1,25 @@
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;

View File

@@ -0,0 +1,28 @@
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;

View File

@@ -0,0 +1,16 @@
.headerCell {
padding: 8px;
border: none !important;
text-align: left;
font-weight: bold;
}
.sortIcon {
margin-left: 10px;
}
@media only screen and (max-width: $breakpointSmall) {
.headerCell {
white-space: nowrap;
}
}

View File

@@ -0,0 +1,96 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, sortDirections } from 'Helpers/Props';
import Link from 'Components/Link/Link';
import Icon from 'Components/Icon';
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,
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}
title={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,
columnLabel: PropTypes.string,
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;

View File

@@ -0,0 +1,48 @@
.column {
display: flex;
align-items: stretch;
width: 100%;
border: 1px solid #aaa;
border-radius: 4px;
background: #fafafa;
}
.checkContainer {
position: relative;
margin-right: 4px;
margin-bottom: 7px;
margin-left: 8px;
}
.label {
display: flex;
flex-grow: 1;
margin-bottom: 0;
margin-left: 2px;
font-weight: normal;
line-height: 36px;
cursor: pointer;
}
.dragHandle {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-left: auto;
width: $dragHandleWidth;
text-align: center;
cursor: grab;
}
.dragIcon {
top: 0;
}
.isDragging {
opacity: 0.25;
}
.notDragable {
padding: 4px 0;
}

View File

@@ -0,0 +1,68 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import CheckInput from 'Components/Form/CheckInput';
import styles from './TableOptionsColumn.css';
function TableOptionsColumn(props) {
const {
name,
label,
isVisible,
isModifiable,
isDragging,
connectDragSource,
onVisibleChange
} = props;
return (
<div className={isModifiable ? undefined : styles.notDragable}>
<div
className={classNames(
styles.column,
isDragging && styles.isDragging
)}
>
<label
className={styles.label}
>
<CheckInput
containerClassName={styles.checkContainer}
name={name}
value={isVisible}
isDisabled={isModifiable === false}
onChange={onVisibleChange}
/>
{label}
</label>
{
!!connectDragSource &&
connectDragSource(
<div className={styles.dragHandle}>
<Icon
className={styles.dragIcon}
name={icons.REORDER}
/>
</div>
)
}
</div>
</div>
);
}
TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
isDragging: PropTypes.bool,
connectDragSource: PropTypes.func,
onVisibleChange: PropTypes.func.isRequired
};
export default TableOptionsColumn;

View File

@@ -0,0 +1,4 @@
.dragPreview {
width: 380px;
opacity: 0.75;
}

View File

@@ -0,0 +1,78 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { DragLayer } from 'react-dnd';
import dimensions from 'Styles/Variables/dimensions.js';
import { TABLE_COLUMN } from 'Helpers/dragTypes';
import DragPreviewLayer from 'Components/DragPreviewLayer';
import TableOptionsColumn from './TableOptionsColumn';
import styles from './TableOptionsColumnDragPreview.css';
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
function collectDragLayer(monitor) {
return {
item: monitor.getItem(),
itemType: monitor.getItemType(),
currentOffset: monitor.getSourceClientOffset()
};
}
class TableOptionsColumnDragPreview extends Component {
//
// Render
render() {
const {
item,
itemType,
currentOffset
} = this.props;
if (!currentOffset || itemType !== TABLE_COLUMN) {
return null;
}
// The offset is shifted because the drag handle is on the right edge of the
// list item and the preview is wider than the drag handle.
const { x, y } = currentOffset;
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
const style = {
position: 'absolute',
WebkitTransform: transform,
msTransform: transform,
transform
};
return (
<DragPreviewLayer>
<div
className={styles.dragPreview}
style={style}
>
<TableOptionsColumn
isDragging={false}
{...item}
/>
</div>
</DragPreviewLayer>
);
}
}
TableOptionsColumnDragPreview.propTypes = {
item: PropTypes.object,
itemType: PropTypes.string,
currentOffset: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired
})
};
export default DragLayer(collectDragLayer)(TableOptionsColumnDragPreview);

View File

@@ -0,0 +1,18 @@
.columnDragSource {
padding: 4px 0;
}
.columnPlaceholder {
width: 100%;
height: 36px;
border: 1px dotted #aaa;
border-radius: 4px;
}
.columnPlaceholderBefore {
margin-bottom: 8px;
}
.columnPlaceholderAfter {
margin-top: 8px;
}

View File

@@ -0,0 +1,164 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import { DragSource, DropTarget } from 'react-dnd';
import classNames from 'classnames';
import { TABLE_COLUMN } from 'Helpers/dragTypes';
import TableOptionsColumn from './TableOptionsColumn';
import styles from './TableOptionsColumnDragSource.css';
const columnDragSource = {
beginDrag(column) {
return column;
},
endDrag(props, monitor, component) {
props.onColumnDragEnd(monitor.getItem(), monitor.didDrop());
}
};
const columnDropTarget = {
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex === hoverIndex) {
return;
}
// When moving up, only trigger if drag position is above 50% and
// when moving down, only trigger if drag position is below 50%.
// If we're moving down the hoverIndex needs to be increased
// by one so it's ordered properly. Otherwise the hoverIndex will work.
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
props.onColumnDragMove(dragIndex, hoverIndex);
}
};
function collectDragSource(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
};
}
function collectDropTarget(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver()
};
}
class TableOptionsColumnDragSource extends Component {
//
// Render
render() {
const {
name,
label,
isVisible,
isModifiable,
index,
isDragging,
isDraggingUp,
isDraggingDown,
isOver,
connectDragSource,
connectDropTarget,
onVisibleChange
} = this.props;
const isBefore = !isDragging && isDraggingUp && isOver;
const isAfter = !isDragging && isDraggingDown && isOver;
// if (isDragging && !isOver) {
// return null;
// }
return connectDropTarget(
<div
className={classNames(
styles.columnDragSource,
isBefore && styles.isDraggingUp,
isAfter && styles.isDraggingDown
)}
>
{
isBefore &&
<div
className={classNames(
styles.columnPlaceholder,
styles.columnPlaceholderBefore
)}
/>
}
<TableOptionsColumn
name={name}
label={label}
isVisible={isVisible}
isModifiable={isModifiable}
index={index}
isDragging={isDragging}
isOver={isOver}
connectDragSource={connectDragSource}
onVisibleChange={onVisibleChange}
/>
{
isAfter &&
<div
className={classNames(
styles.columnPlaceholder,
styles.columnPlaceholderAfter
)}
/>
}
</div>
);
}
}
TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
isDragging: PropTypes.bool,
isDraggingUp: PropTypes.bool,
isDraggingDown: PropTypes.bool,
isOver: PropTypes.bool,
connectDragSource: PropTypes.func,
connectDropTarget: PropTypes.func,
onVisibleChange: PropTypes.func.isRequired,
onColumnDragMove: PropTypes.func.isRequired,
onColumnDragEnd: PropTypes.func.isRequired
};
export default DropTarget(
TABLE_COLUMN,
columnDropTarget,
collectDropTarget
)(DragSource(
TABLE_COLUMN,
columnDragSource,
collectDragSource
)(TableOptionsColumnDragSource));

View File

@@ -0,0 +1,5 @@
.columns {
margin-top: 10px;
width: 100%;
user-select: none;
}

View File

@@ -0,0 +1,252 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { inputTypes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import FormInputGroup from 'Components/Form/FormInputGroup';
import Modal from 'Components/Modal/Modal';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import TableOptionsColumn from './TableOptionsColumn';
import TableOptionsColumnDragSource from './TableOptionsColumnDragSource';
import TableOptionsColumnDragPreview from './TableOptionsColumnDragPreview';
import styles from './TableOptionsModal.css';
class TableOptionsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPageSize: !!props.pageSize,
pageSize: props.pageSize,
pageSizeError: null,
dragIndex: null,
dropIndex: null
};
}
componentDidUpdate(prevProps) {
if (prevProps.pageSize !== this.state.pageSize) {
this.setState({ pageSize: this.props.pageSize });
}
}
//
// Listeners
onPageSizeChange = ({ value }) => {
let pageSizeError = null;
if (value < 5) {
pageSizeError = 'Page size must be at least 5';
} else if (value > 250) {
pageSizeError = 'Page size must not exceed 250';
} else {
this.props.onTableOptionChange({ pageSize: value });
}
this.setState({
pageSize: value,
pageSizeError
});
}
onVisibleChange = ({ name, value }) => {
const columns = _.cloneDeep(this.props.columns);
const column = _.find(columns, { name });
column.isVisible = value;
this.props.onTableOptionChange({ columns });
}
onColumnDragMove = (dragIndex, dropIndex) => {
if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
this.setState({
dragIndex,
dropIndex
});
}
}
onColumnDragEnd = ({ id }, didDrop) => {
const {
dragIndex,
dropIndex
} = this.state;
if (didDrop && dropIndex !== null) {
const columns = _.cloneDeep(this.props.columns);
const items = columns.splice(dragIndex, 1);
columns.splice(dropIndex, 0, items[0]);
this.props.onTableOptionChange({ columns });
}
this.setState({
dragIndex: null,
dropIndex: null
});
}
//
// Render
render() {
const {
isOpen,
columns,
canModifyColumns,
optionsComponent: OptionsComponent,
onTableOptionChange,
onModalClose
} = this.props;
const {
hasPageSize,
pageSize,
pageSizeError,
dragIndex,
dropIndex
} = this.state;
const isDragging = dropIndex !== null;
const isDraggingUp = isDragging && dropIndex < dragIndex;
const isDraggingDown = isDragging && dropIndex > dragIndex;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Table Options
</ModalHeader>
<ModalBody>
<Form>
{
hasPageSize &&
<FormGroup>
<FormLabel>Page Size</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="pageSize"
value={pageSize || 0}
helpText="Number of items to show on each page"
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
onChange={this.onPageSizeChange}
/>
</FormGroup>
}
{
!!OptionsComponent &&
<OptionsComponent
onTableOptionChange={onTableOptionChange}
/>
}
{
canModifyColumns &&
<FormGroup>
<FormLabel>Columns</FormLabel>
<div>
<FormInputHelpText
text="Choose which columns are visible and which order they appear in"
/>
<div className={styles.columns}>
{
columns.map((column, index) => {
const {
name,
label,
columnLabel,
isVisible,
isModifiable
} = column;
if (isModifiable !== false) {
return (
<TableOptionsColumnDragSource
key={name}
name={name}
label={label || columnLabel}
isVisible={isVisible}
isModifiable={true}
index={index}
isDragging={isDragging}
isDraggingUp={isDraggingUp}
isDraggingDown={isDraggingDown}
onVisibleChange={this.onVisibleChange}
onColumnDragMove={this.onColumnDragMove}
onColumnDragEnd={this.onColumnDragEnd}
/>
);
}
return (
<TableOptionsColumn
key={name}
name={name}
label={label || columnLabel}
isVisible={isVisible}
index={index}
isModifiable={false}
onVisibleChange={this.onVisibleChange}
/>
);
})
}
<TableOptionsColumnDragPreview />
</div>
</div>
</FormGroup>
}
</Form>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
TableOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
pageSize: PropTypes.number,
canModifyColumns: PropTypes.bool.isRequired,
optionsComponent: PropTypes.func,
onTableOptionChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
TableOptionsModal.defaultProps = {
canModifyColumns: true
};
export default DragDropContext(HTML5Backend)(TableOptionsModal);

View File

@@ -0,0 +1,61 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import TableOptionsModal from './TableOptionsModal';
class TableOptionsModalWrapper extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isTableOptionsModalOpen: false
};
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
}
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
}
//
// Render
render() {
const {
columns,
children,
...otherProps
} = this.props;
return (
<Fragment>
{
React.cloneElement(children, { onPress: this.onTableOptionsPress })
}
<TableOptionsModal
{...otherProps}
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
onModalClose={this.onTableOptionsModalClose}
/>
</Fragment>
);
}
}
TableOptionsModalWrapper.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
children: PropTypes.node.isRequired
};
export default TableOptionsModalWrapper;

View File

@@ -0,0 +1,77 @@
.pager {
display: flex;
align-items: center;
justify-content: space-between;
}
.loadingContainer,
.controlsContainer,
.recordsContainer {
flex: 0 1 33%;
}
.controlsContainer {
display: flex;
justify-content: center;
}
.recordsContainer {
display: flex;
justify-content: flex-end;
}
.loading {
composes: loading from 'Components/Loading/LoadingIndicator.css';
margin: 0;
margin-left: 5px;
text-align: left;
}
.controls {
display: flex;
align-items: center;
text-align: center;
}
.pageNumber {
line-height: 30px;
}
.pageLink {
padding: 0;
width: 30px;
height: 30px;
line-height: 30px;
}
.records {
color: $disabledColor;
}
.disabledPageButton {
color: $disabledColor;
}
.pageSelect {
composes: select from 'Components/Form/SelectInput.css';
padding: 0 2px;
height: 25px;
}
@media only screen and (max-width: $breakpointSmall) {
.pager {
flex-wrap: wrap;
}
.loadingContainer,
.recordsContainer {
flex: 0 1 50%;
}
.controlsContainer {
flex: 0 1 100%;
order: -1;
}
}

View File

@@ -0,0 +1,180 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectInput from 'Components/Form/SelectInput';
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}>
Total records: {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;

View File

@@ -0,0 +1,7 @@
.row {
transition: background-color 500ms;
&:hover {
background-color: $tableRowHoverBackgroundColor;
}
}

View File

@@ -0,0 +1,33 @@
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;

View File

@@ -0,0 +1,4 @@
.row {
composes: link from 'Components/Link/Link.css';
composes: row from './TableRow.css';
}

View File

@@ -0,0 +1,16 @@
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;

View File

@@ -0,0 +1,11 @@
.selectAllHeaderCell {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
width: 30px;
}
.input {
composes: input from 'Components/Form/CheckInput.css';
margin: 0;
}

View File

@@ -0,0 +1,47 @@
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;

View File

@@ -0,0 +1,3 @@
.tableContainer {
width: 100%;
}

View File

@@ -0,0 +1,167 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { WindowScroller } from 'react-virtualized';
import { scrollDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import Scroller from 'Components/Scroller/Scroller';
import VirtualTableBody from './VirtualTableBody';
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
};
this._isInitialized = false;
}
componentDidMount() {
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
}
componentDidUpdate(prevProps, preState) {
const scrollIndex = this.props.scrollIndex;
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
const scrollTop = (scrollIndex + 1) * ROW_HEIGHT + 20;
this.props.onScroll({ scrollTop });
}
}
//
// Control
rowGetter = ({ index }) => {
return this.props.items[index];
}
//
// Listeners
onMeasure = ({ width }) => {
this.setState({
width
});
}
onSectionRendered = () => {
if (!this._isInitialized && this._contentBodyNode) {
this.props.onRender();
this._isInitialized = true;
}
}
//
// Render
render() {
const {
className,
items,
isSmallScreen,
header,
headerHeight,
scrollTop,
rowRenderer,
onScroll,
...otherProps
} = this.props;
const {
width
} = this.state;
return (
<Measure onMeasure={this.onMeasure}>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={onScroll}
>
{({ height, isScrolling }) => {
return (
<Scroller
className={className}
scrollDirection={scrollDirections.HORIZONTAL}
>
{header}
<VirtualTableBody
autoContainerWidth={true}
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowCount={items.length}
columnCount={1}
scrollTop={scrollTop}
autoHeight={true}
overscanRowCount={2}
cellRenderer={rowRenderer}
columnWidth={width}
overscanIndicesGetter={overscanIndicesGetter}
onSectionRendered={this.onSectionRendered}
{...otherProps}
/>
</Scroller>
);
}
}
</WindowScroller>
</Measure>
);
}
}
VirtualTable.propTypes = {
className: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
scrollTop: PropTypes.number.isRequired,
scrollIndex: PropTypes.number,
contentBody: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
header: PropTypes.node.isRequired,
headerHeight: PropTypes.number.isRequired,
rowRenderer: PropTypes.func.isRequired,
onRender: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
};
VirtualTable.defaultProps = {
className: styles.tableContainer,
headerHeight: 38,
onRender: () => {}
};
export default VirtualTable;

View File

@@ -0,0 +1,3 @@
.tableBodyContainer {
position: relative;
}

View File

@@ -0,0 +1,40 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid } from 'react-virtualized';
import styles from './VirtualTableBody.css';
class VirtualTableBody extends Component {
//
// Render
render() {
return (
<Grid
{...this.props}
style={{
boxSizing: undefined,
direction: undefined,
height: undefined,
position: undefined,
willChange: undefined,
overflow: undefined,
width: undefined
}}
containerStyle={{
position: undefined
}}
/>
);
}
}
VirtualTableBody.propTypes = {
className: PropTypes.string.isRequired
};
VirtualTableBody.defaultProps = {
className: styles.tableBodyContainer
};
export default VirtualTableBody;

View File

@@ -0,0 +1,3 @@
.header {
display: flex;
}

View File

@@ -0,0 +1,17 @@
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;

View File

@@ -0,0 +1,16 @@
.headerCell {
padding: 8px;
border: none !important;
text-align: left;
font-weight: bold;
}
.sortIcon {
margin-left: 10px;
}
@media only screen and (max-width: $breakpointSmall) {
.headerCell {
white-space: nowrap;
}
}

View File

@@ -0,0 +1,107 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, sortDirections } from 'Helpers/Props';
import Link from 'Components/Link/Link';
import Icon from 'Components/Icon';
import styles from './VirtualTableHeaderCell.css';
export function headerRenderer(headerProps) {
const {
columnData = {},
dataKey,
label
} = headerProps;
return (
<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;

View File

@@ -0,0 +1,14 @@
.row {
display: flex;
transition: background-color 500ms;
&:hover {
background-color: #fafbfc;
}
}
@media only screen and (max-width: $breakpointMedium) {
.row {
overflow-x: visible !important;
}
}

View File

@@ -0,0 +1,34 @@
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;

View File

@@ -0,0 +1,11 @@
.selectAllHeaderCell {
composes: headerCell from 'Components/Table/TableHeaderCell.css';
flex: 0 0 36px;
}
.input {
composes: input from 'Components/Form/CheckInput.css';
margin: 0;
}

View File

@@ -0,0 +1,47 @@
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;