mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Project Aphrodite
This commit is contained in:
5
frontend/src/Components/Table/Cells/RelativeDateCell.css
Normal file
5
frontend/src/Components/Table/Cells/RelativeDateCell.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.cell {
|
||||
composes: cell from './TableRowCell.css';
|
||||
|
||||
width: 180px;
|
||||
}
|
60
frontend/src/Components/Table/Cells/RelativeDateCell.js
Normal file
60
frontend/src/Components/Table/Cells/RelativeDateCell.js
Normal 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;
|
@@ -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);
|
11
frontend/src/Components/Table/Cells/TableRowCell.css
Normal file
11
frontend/src/Components/Table/Cells/TableRowCell.css
Normal 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;
|
||||
}
|
||||
}
|
37
frontend/src/Components/Table/Cells/TableRowCell.js
Normal file
37
frontend/src/Components/Table/Cells/TableRowCell.js
Normal 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;
|
@@ -0,0 +1,4 @@
|
||||
.cell {
|
||||
composes: cell from './TableRowCell.css';
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
}
|
25
frontend/src/Components/Table/Cells/TableRowCellButton.js
Normal file
25
frontend/src/Components/Table/Cells/TableRowCellButton.js
Normal 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;
|
11
frontend/src/Components/Table/Cells/TableSelectCell.css
Normal file
11
frontend/src/Components/Table/Cells/TableSelectCell.css
Normal 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;
|
||||
}
|
80
frontend/src/Components/Table/Cells/TableSelectCell.js
Normal file
80
frontend/src/Components/Table/Cells/TableSelectCell.js
Normal 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;
|
14
frontend/src/Components/Table/Cells/VirtualTableRowCell.css
Normal file
14
frontend/src/Components/Table/Cells/VirtualTableRowCell.css
Normal 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;
|
||||
}
|
||||
}
|
29
frontend/src/Components/Table/Cells/VirtualTableRowCell.js
Normal file
29
frontend/src/Components/Table/Cells/VirtualTableRowCell.js
Normal 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;
|
@@ -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;
|
||||
}
|
@@ -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;
|
16
frontend/src/Components/Table/Table.css
Normal file
16
frontend/src/Components/Table/Table.css
Normal 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%;
|
||||
}
|
||||
}
|
129
frontend/src/Components/Table/Table.js
Normal file
129
frontend/src/Components/Table/Table.js
Normal 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;
|
25
frontend/src/Components/Table/TableBody.js
Normal file
25
frontend/src/Components/Table/TableBody.js
Normal 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;
|
28
frontend/src/Components/Table/TableHeader.js
Normal file
28
frontend/src/Components/Table/TableHeader.js
Normal 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;
|
16
frontend/src/Components/Table/TableHeaderCell.css
Normal file
16
frontend/src/Components/Table/TableHeaderCell.css
Normal 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;
|
||||
}
|
||||
}
|
96
frontend/src/Components/Table/TableHeaderCell.js
Normal file
96
frontend/src/Components/Table/TableHeaderCell.js
Normal 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;
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -0,0 +1,4 @@
|
||||
.dragPreview {
|
||||
width: 380px;
|
||||
opacity: 0.75;
|
||||
}
|
@@ -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);
|
@@ -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;
|
||||
}
|
@@ -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));
|
@@ -0,0 +1,5 @@
|
||||
.columns {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
}
|
252
frontend/src/Components/Table/TableOptions/TableOptionsModal.js
Normal file
252
frontend/src/Components/Table/TableOptions/TableOptionsModal.js
Normal 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);
|
@@ -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;
|
77
frontend/src/Components/Table/TablePager.css
Normal file
77
frontend/src/Components/Table/TablePager.css
Normal 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;
|
||||
}
|
||||
}
|
180
frontend/src/Components/Table/TablePager.js
Normal file
180
frontend/src/Components/Table/TablePager.js
Normal 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;
|
7
frontend/src/Components/Table/TableRow.css
Normal file
7
frontend/src/Components/Table/TableRow.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.row {
|
||||
transition: background-color 500ms;
|
||||
|
||||
&:hover {
|
||||
background-color: $tableRowHoverBackgroundColor;
|
||||
}
|
||||
}
|
33
frontend/src/Components/Table/TableRow.js
Normal file
33
frontend/src/Components/Table/TableRow.js
Normal 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;
|
4
frontend/src/Components/Table/TableRowButton.css
Normal file
4
frontend/src/Components/Table/TableRowButton.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.row {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
composes: row from './TableRow.css';
|
||||
}
|
16
frontend/src/Components/Table/TableRowButton.js
Normal file
16
frontend/src/Components/Table/TableRowButton.js
Normal 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;
|
11
frontend/src/Components/Table/TableSelectAllHeaderCell.css
Normal file
11
frontend/src/Components/Table/TableSelectAllHeaderCell.css
Normal 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;
|
||||
}
|
47
frontend/src/Components/Table/TableSelectAllHeaderCell.js
Normal file
47
frontend/src/Components/Table/TableSelectAllHeaderCell.js
Normal 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;
|
3
frontend/src/Components/Table/VirtualTable.css
Normal file
3
frontend/src/Components/Table/VirtualTable.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.tableContainer {
|
||||
width: 100%;
|
||||
}
|
167
frontend/src/Components/Table/VirtualTable.js
Normal file
167
frontend/src/Components/Table/VirtualTable.js
Normal 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;
|
3
frontend/src/Components/Table/VirtualTableBody.css
Normal file
3
frontend/src/Components/Table/VirtualTableBody.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.tableBodyContainer {
|
||||
position: relative;
|
||||
}
|
40
frontend/src/Components/Table/VirtualTableBody.js
Normal file
40
frontend/src/Components/Table/VirtualTableBody.js
Normal 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;
|
3
frontend/src/Components/Table/VirtualTableHeader.css
Normal file
3
frontend/src/Components/Table/VirtualTableHeader.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
17
frontend/src/Components/Table/VirtualTableHeader.js
Normal file
17
frontend/src/Components/Table/VirtualTableHeader.js
Normal 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;
|
16
frontend/src/Components/Table/VirtualTableHeaderCell.css
Normal file
16
frontend/src/Components/Table/VirtualTableHeaderCell.css
Normal 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;
|
||||
}
|
||||
}
|
107
frontend/src/Components/Table/VirtualTableHeaderCell.js
Normal file
107
frontend/src/Components/Table/VirtualTableHeaderCell.js
Normal 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;
|
14
frontend/src/Components/Table/VirtualTableRow.css
Normal file
14
frontend/src/Components/Table/VirtualTableRow.css
Normal 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;
|
||||
}
|
||||
}
|
34
frontend/src/Components/Table/VirtualTableRow.js
Normal file
34
frontend/src/Components/Table/VirtualTableRow.js
Normal 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;
|
@@ -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;
|
||||
}
|
@@ -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;
|
Reference in New Issue
Block a user