mirror of
https://github.com/Radarr/Radarr
synced 2025-12-06 16:32:36 +01:00
Convert Table to TypeScript
(cherry picked from commit 699120a8fd54be9e70fb9a83298f94c8cb6a80bb)
This commit is contained in:
parent
9228e5dea0
commit
8caa839d99
55 changed files with 1011 additions and 1287 deletions
|
|
@ -9,13 +9,13 @@ export type SelectContextAction =
|
||||||
| { type: 'unselectAll' }
|
| { type: 'unselectAll' }
|
||||||
| {
|
| {
|
||||||
type: 'toggleSelected';
|
type: 'toggleSelected';
|
||||||
id: number;
|
id: number | string;
|
||||||
isSelected: boolean;
|
isSelected: boolean | null;
|
||||||
shiftKey: boolean;
|
shiftKey: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'removeItem';
|
type: 'removeItem';
|
||||||
id: number;
|
id: number | string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'updateItems';
|
type: 'updateItems';
|
||||||
|
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import TableRowCell from './TableRowCell';
|
|
||||||
import styles from './TableSelectCell.css';
|
|
||||||
|
|
||||||
class TableSelectCell extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
isSelected,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelectedChange({ id, value: isSelected });
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelectedChange({ id, value: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ value, shiftKey }, a, b, c, d) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelectedChange({ id, value, shiftKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
id,
|
|
||||||
isSelected,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell className={className}>
|
|
||||||
<CheckInput
|
|
||||||
className={styles.input}
|
|
||||||
name={id.toString()}
|
|
||||||
value={isSelected}
|
|
||||||
{...otherProps}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableSelectCell.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
TableSelectCell.defaultProps = {
|
|
||||||
className: styles.selectCell,
|
|
||||||
isSelected: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableSelectCell;
|
|
||||||
59
frontend/src/Components/Table/Cells/TableSelectCell.tsx
Normal file
59
frontend/src/Components/Table/Cells/TableSelectCell.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import TableRowCell, { TableRowCellProps } from './TableRowCell';
|
||||||
|
import styles from './TableSelectCell.css';
|
||||||
|
|
||||||
|
interface TableSelectCellProps extends Omit<TableRowCellProps, 'id'> {
|
||||||
|
className?: string;
|
||||||
|
id: number | string;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableSelectCell({
|
||||||
|
className = styles.selectCell,
|
||||||
|
id,
|
||||||
|
isSelected = false,
|
||||||
|
onSelectedChange,
|
||||||
|
...otherProps
|
||||||
|
}: TableSelectCellProps) {
|
||||||
|
const initialIsSelected = useRef(isSelected);
|
||||||
|
const handleSelectedChange = useRef(onSelectedChange);
|
||||||
|
|
||||||
|
handleSelectedChange.current = onSelectedChange;
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ value, shiftKey }: CheckInputChanged) => {
|
||||||
|
onSelectedChange({ id, value, shiftKey });
|
||||||
|
},
|
||||||
|
[id, onSelectedChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSelectedChange.current({
|
||||||
|
id,
|
||||||
|
value: initialIsSelected.current,
|
||||||
|
shiftKey: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
handleSelectedChange.current({ id, value: null, shiftKey: false });
|
||||||
|
};
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={className}>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.input}
|
||||||
|
name={id.toString()}
|
||||||
|
value={isSelected}
|
||||||
|
{...otherProps}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableSelectCell;
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
function VirtualTableRowCell(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
title
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualTableRowCell.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
|
||||||
title: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
VirtualTableRowCell.defaultProps = {
|
|
||||||
className: styles.cell,
|
|
||||||
title: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VirtualTableRowCell;
|
|
||||||
16
frontend/src/Components/Table/Cells/VirtualTableRowCell.tsx
Normal file
16
frontend/src/Components/Table/Cells/VirtualTableRowCell.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
export interface VirtualTableRowCellProps {
|
||||||
|
className?: string;
|
||||||
|
children?: string | React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VirtualTableRowCell({
|
||||||
|
className = styles.cell,
|
||||||
|
children,
|
||||||
|
}: VirtualTableRowCellProps) {
|
||||||
|
return <div className={className}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTableRowCell;
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import VirtualTableRowCell from './VirtualTableRowCell';
|
|
||||||
import styles from './VirtualTableSelectCell.css';
|
|
||||||
|
|
||||||
export function virtualTableSelectCellRenderer(cellProps) {
|
|
||||||
const {
|
|
||||||
cellKey,
|
|
||||||
rowData,
|
|
||||||
columnData,
|
|
||||||
...otherProps
|
|
||||||
} = cellProps;
|
|
||||||
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
<VirtualTableSelectCell
|
|
||||||
key={cellKey}
|
|
||||||
id={rowData.name}
|
|
||||||
isSelected={rowData.isSelected}
|
|
||||||
{...columnData}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class VirtualTableSelectCell extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ value, shiftKey }) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelectedChange({ id, value, shiftKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
inputClassName,
|
|
||||||
id,
|
|
||||||
isSelected,
|
|
||||||
isDisabled,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
className={styles.cell}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<CheckInput
|
|
||||||
className={inputClassName}
|
|
||||||
name={id.toString()}
|
|
||||||
value={isSelected}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualTableSelectCell.propTypes = {
|
|
||||||
inputClassName: PropTypes.string.isRequired,
|
|
||||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
VirtualTableSelectCell.defaultProps = {
|
|
||||||
inputClassName: styles.input,
|
|
||||||
isSelected: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VirtualTableSelectCell;
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import VirtualTableRowCell, {
|
||||||
|
VirtualTableRowCellProps,
|
||||||
|
} from './VirtualTableRowCell';
|
||||||
|
import styles from './VirtualTableSelectCell.css';
|
||||||
|
|
||||||
|
interface VirtualTableSelectCellProps extends VirtualTableRowCellProps {
|
||||||
|
inputClassName?: string;
|
||||||
|
id: number;
|
||||||
|
isSelected?: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VirtualTableSelectCell({
|
||||||
|
inputClassName = styles.input,
|
||||||
|
id,
|
||||||
|
isSelected = false,
|
||||||
|
isDisabled,
|
||||||
|
onSelectedChange,
|
||||||
|
...otherProps
|
||||||
|
}: VirtualTableSelectCellProps) {
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ value, shiftKey }: CheckInputChanged) => {
|
||||||
|
onSelectedChange({ id, value, shiftKey });
|
||||||
|
},
|
||||||
|
[id, onSelectedChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell className={styles.cell} {...otherProps}>
|
||||||
|
<CheckInput
|
||||||
|
className={inputClassName}
|
||||||
|
name={id.toString()}
|
||||||
|
value={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTableSelectCell;
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import { icons, scrollDirections } from 'Helpers/Props';
|
|
||||||
import TableHeader from './TableHeader';
|
|
||||||
import TableHeaderCell from './TableHeaderCell';
|
|
||||||
import TableSelectAllHeaderCell from './TableSelectAllHeaderCell';
|
|
||||||
import styles from './Table.css';
|
|
||||||
|
|
||||||
const tableHeaderCellProps = [
|
|
||||||
'sortKey',
|
|
||||||
'sortDirection'
|
|
||||||
];
|
|
||||||
|
|
||||||
function getTableHeaderCellProps(props) {
|
|
||||||
return _.reduce(tableHeaderCellProps, (result, key) => {
|
|
||||||
if (props.hasOwnProperty(key)) {
|
|
||||||
result[key] = props[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function Table(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
horizontalScroll,
|
|
||||||
selectAll,
|
|
||||||
columns,
|
|
||||||
optionsComponent,
|
|
||||||
pageSize,
|
|
||||||
canModifyColumns,
|
|
||||||
children,
|
|
||||||
onSortPress,
|
|
||||||
onTableOptionChange,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Scroller
|
|
||||||
className={classNames(
|
|
||||||
styles.tableContainer,
|
|
||||||
horizontalScroll && styles.horizontalScroll
|
|
||||||
)}
|
|
||||||
scrollDirection={
|
|
||||||
horizontalScroll ?
|
|
||||||
scrollDirections.HORIZONTAL :
|
|
||||||
scrollDirections.NONE
|
|
||||||
}
|
|
||||||
autoFocus={false}
|
|
||||||
>
|
|
||||||
<table className={className}>
|
|
||||||
<TableHeader>
|
|
||||||
{
|
|
||||||
selectAll ?
|
|
||||||
<TableSelectAllHeaderCell {...otherProps} /> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible,
|
|
||||||
isSortable,
|
|
||||||
...otherColumnProps
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(name === 'actions' || name === 'details') &&
|
|
||||||
onTableOptionChange
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<TableHeaderCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
name={name}
|
|
||||||
isSortable={false}
|
|
||||||
{...otherProps}
|
|
||||||
{...otherColumnProps}
|
|
||||||
>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
columns={columns}
|
|
||||||
optionsComponent={optionsComponent}
|
|
||||||
pageSize={pageSize}
|
|
||||||
canModifyColumns={canModifyColumns}
|
|
||||||
onTableOptionChange={onTableOptionChange}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
name={icons.ADVANCED_SETTINGS}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
</TableHeaderCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableHeaderCell
|
|
||||||
key={column.name}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
{...getTableHeaderCellProps(otherProps)}
|
|
||||||
{...column}
|
|
||||||
>
|
|
||||||
{typeof column.label === 'function' ? column.label() : column.label}
|
|
||||||
</TableHeaderCell>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</TableHeader>
|
|
||||||
{children}
|
|
||||||
</table>
|
|
||||||
</Scroller>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Table.propTypes = {
|
|
||||||
...TableHeaderCell.props,
|
|
||||||
className: PropTypes.string,
|
|
||||||
horizontalScroll: PropTypes.bool.isRequired,
|
|
||||||
selectAll: PropTypes.bool.isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
optionsComponent: PropTypes.elementType,
|
|
||||||
pageSize: PropTypes.number,
|
|
||||||
canModifyColumns: PropTypes.bool,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onSortPress: PropTypes.func,
|
|
||||||
onTableOptionChange: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
Table.defaultProps = {
|
|
||||||
className: styles.table,
|
|
||||||
horizontalScroll: true,
|
|
||||||
selectAll: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Table;
|
|
||||||
124
frontend/src/Components/Table/Table.tsx
Normal file
124
frontend/src/Components/Table/Table.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import { icons, scrollDirections } from 'Helpers/Props';
|
||||||
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import Column from './Column';
|
||||||
|
import TableHeader from './TableHeader';
|
||||||
|
import TableHeaderCell from './TableHeaderCell';
|
||||||
|
import TableSelectAllHeaderCell from './TableSelectAllHeaderCell';
|
||||||
|
import styles from './Table.css';
|
||||||
|
|
||||||
|
interface TableProps {
|
||||||
|
className?: string;
|
||||||
|
horizontalScroll?: boolean;
|
||||||
|
selectAll?: boolean;
|
||||||
|
allSelected?: boolean;
|
||||||
|
allUnselected?: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
optionsComponent?: React.ElementType;
|
||||||
|
pageSize?: number;
|
||||||
|
canModifyColumns?: boolean;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onSortPress?: (name: string, sortDirection?: SortDirection) => void;
|
||||||
|
onTableOptionChange?: (payload: TableOptionsChangePayload) => void;
|
||||||
|
onSelectAllChange?: (change: CheckInputChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Table({
|
||||||
|
className = styles.table,
|
||||||
|
horizontalScroll = true,
|
||||||
|
selectAll = false,
|
||||||
|
allSelected = false,
|
||||||
|
allUnselected = false,
|
||||||
|
columns,
|
||||||
|
optionsComponent,
|
||||||
|
pageSize,
|
||||||
|
canModifyColumns,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
children,
|
||||||
|
onSortPress,
|
||||||
|
onTableOptionChange,
|
||||||
|
onSelectAllChange,
|
||||||
|
}: TableProps) {
|
||||||
|
return (
|
||||||
|
<Scroller
|
||||||
|
className={classNames(
|
||||||
|
styles.tableContainer,
|
||||||
|
horizontalScroll && styles.horizontalScroll
|
||||||
|
)}
|
||||||
|
scrollDirection={
|
||||||
|
horizontalScroll ? scrollDirections.HORIZONTAL : scrollDirections.NONE
|
||||||
|
}
|
||||||
|
autoFocus={false}
|
||||||
|
>
|
||||||
|
<table className={className}>
|
||||||
|
<TableHeader>
|
||||||
|
{selectAll && onSelectAllChange ? (
|
||||||
|
<TableSelectAllHeaderCell
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible, isSortable, ...otherColumnProps } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(name === 'actions' || name === 'details') &&
|
||||||
|
onTableOptionChange
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<TableHeaderCell
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
isSortable={false}
|
||||||
|
{...otherColumnProps}
|
||||||
|
>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
optionsComponent={optionsComponent}
|
||||||
|
pageSize={pageSize}
|
||||||
|
canModifyColumns={canModifyColumns}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
|
<IconButton name={icons.ADVANCED_SETTINGS} />
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
</TableHeaderCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHeaderCell
|
||||||
|
key={column.name}
|
||||||
|
{...column}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
>
|
||||||
|
{typeof column.label === 'function'
|
||||||
|
? column.label()
|
||||||
|
: column.label}
|
||||||
|
</TableHeaderCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableHeader>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</Scroller>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Table;
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
class TableBody extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tbody>{children}</tbody>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
TableBody.propTypes = {
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableBody;
|
|
||||||
11
frontend/src/Components/Table/TableBody.tsx
Normal file
11
frontend/src/Components/Table/TableBody.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TableBodyProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ children }: TableBodyProps) {
|
||||||
|
return <tbody>{children}</tbody>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableBody;
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
class TableHeader extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{children}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableHeader.propTypes = {
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableHeader;
|
|
||||||
15
frontend/src/Components/Table/TableHeader.tsx
Normal file
15
frontend/src/Components/Table/TableHeader.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TableHeaderProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ children }: TableHeaderProps) {
|
||||||
|
return (
|
||||||
|
<thead>
|
||||||
|
<tr>{children}</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableHeader;
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
|
||||||
import styles from './TableHeaderCell.css';
|
|
||||||
|
|
||||||
class TableHeaderCell extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
fixedSortDirection
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (fixedSortDirection) {
|
|
||||||
this.props.onSortPress(name, fixedSortDirection);
|
|
||||||
} else {
|
|
||||||
this.props.onSortPress(name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
columnLabel,
|
|
||||||
isSortable,
|
|
||||||
isVisible,
|
|
||||||
isModifiable,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
fixedSortDirection,
|
|
||||||
children,
|
|
||||||
onSortPress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isSorting = isSortable && sortKey === name;
|
|
||||||
const sortIcon = sortDirection === sortDirections.ASCENDING ?
|
|
||||||
icons.SORT_ASCENDING :
|
|
||||||
icons.SORT_DESCENDING;
|
|
||||||
|
|
||||||
return (
|
|
||||||
isSortable ?
|
|
||||||
<Link
|
|
||||||
{...otherProps}
|
|
||||||
component="th"
|
|
||||||
className={className}
|
|
||||||
label={typeof label === 'function' ? label() : label}
|
|
||||||
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{
|
|
||||||
isSorting &&
|
|
||||||
<Icon
|
|
||||||
name={sortIcon}
|
|
||||||
className={styles.sortIcon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</Link> :
|
|
||||||
|
|
||||||
<th className={className}>
|
|
||||||
{children}
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableHeaderCell.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
|
|
||||||
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
|
||||||
isSortable: PropTypes.bool,
|
|
||||||
isVisible: PropTypes.bool,
|
|
||||||
isModifiable: PropTypes.bool,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
fixedSortDirection: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onSortPress: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
TableHeaderCell.defaultProps = {
|
|
||||||
className: styles.headerCell,
|
|
||||||
isSortable: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableHeaderCell;
|
|
||||||
70
frontend/src/Components/Table/TableHeaderCell.tsx
Normal file
70
frontend/src/Components/Table/TableHeaderCell.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons, sortDirections } from 'Helpers/Props';
|
||||||
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
import styles from './TableHeaderCell.css';
|
||||||
|
|
||||||
|
interface TableHeaderCellProps {
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
label?: string | (() => string) | React.ReactNode;
|
||||||
|
columnLabel?: string | (() => string);
|
||||||
|
isSortable?: boolean;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isModifiable?: boolean;
|
||||||
|
sortKey?: string;
|
||||||
|
fixedSortDirection?: SortDirection;
|
||||||
|
sortDirection?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onSortPress?: (name: string, sortDirection?: SortDirection) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeaderCell({
|
||||||
|
className = styles.headerCell,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
columnLabel,
|
||||||
|
isSortable = false,
|
||||||
|
isVisible,
|
||||||
|
isModifiable,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
fixedSortDirection,
|
||||||
|
children,
|
||||||
|
onSortPress,
|
||||||
|
...otherProps
|
||||||
|
}: TableHeaderCellProps) {
|
||||||
|
const isSorting = isSortable && sortKey === name;
|
||||||
|
const sortIcon =
|
||||||
|
sortDirection === sortDirections.ASCENDING
|
||||||
|
? icons.SORT_ASCENDING
|
||||||
|
: icons.SORT_DESCENDING;
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
if (fixedSortDirection) {
|
||||||
|
onSortPress?.(name, fixedSortDirection);
|
||||||
|
} else {
|
||||||
|
onSortPress?.(name);
|
||||||
|
}
|
||||||
|
}, [name, fixedSortDirection, onSortPress]);
|
||||||
|
|
||||||
|
return isSortable ? (
|
||||||
|
<Link
|
||||||
|
{...otherProps}
|
||||||
|
component="th"
|
||||||
|
className={className}
|
||||||
|
// label={typeof label === 'function' ? label() : label}
|
||||||
|
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{isSorting && <Icon name={sortIcon} className={styles.sortIcon} />}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<th className={className}>{children}</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableHeaderCell;
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './TablePager.css';
|
|
||||||
|
|
||||||
class TablePager extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isShowingPageSelect: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOpenPageSelectClick = () => {
|
|
||||||
this.setState({ isShowingPageSelect: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = ({ value: page }) => {
|
|
||||||
this.setState({ isShowingPageSelect: false });
|
|
||||||
this.props.onPageSelect(parseInt(page));
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelectBlur = () => {
|
|
||||||
this.setState({ isShowingPageSelect: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
totalRecords,
|
|
||||||
isFetching,
|
|
||||||
onFirstPagePress,
|
|
||||||
onPreviousPagePress,
|
|
||||||
onNextPagePress,
|
|
||||||
onLastPagePress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isShowingPageSelect = this.state.isShowingPageSelect;
|
|
||||||
const pages = Array.from(new Array(totalPages), (x, i) => {
|
|
||||||
const pageNumber = i + 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: pageNumber,
|
|
||||||
value: pageNumber
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFirstPage = page === 1;
|
|
||||||
const isLastPage = page === totalPages;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.pager}>
|
|
||||||
<div className={styles.loadingContainer}>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.controlsContainer}>
|
|
||||||
<div className={styles.controls}>
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
styles.pageLink,
|
|
||||||
isFirstPage && styles.disabledPageButton
|
|
||||||
)}
|
|
||||||
isDisabled={isFirstPage}
|
|
||||||
onPress={onFirstPagePress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.PAGE_FIRST} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
styles.pageLink,
|
|
||||||
isFirstPage && styles.disabledPageButton
|
|
||||||
)}
|
|
||||||
isDisabled={isFirstPage}
|
|
||||||
onPress={onPreviousPagePress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.PAGE_PREVIOUS} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className={styles.pageNumber}>
|
|
||||||
{
|
|
||||||
!isShowingPageSelect &&
|
|
||||||
<Link
|
|
||||||
isDisabled={totalPages === 1}
|
|
||||||
onPress={this.onOpenPageSelectClick}
|
|
||||||
>
|
|
||||||
{page} / {totalPages}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isShowingPageSelect &&
|
|
||||||
<SelectInput
|
|
||||||
className={styles.pageSelect}
|
|
||||||
name="pageSelect"
|
|
||||||
value={page}
|
|
||||||
values={pages}
|
|
||||||
autoFocus={true}
|
|
||||||
onChange={this.onPageSelect}
|
|
||||||
onBlur={this.onPageSelectBlur}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
styles.pageLink,
|
|
||||||
isLastPage && styles.disabledPageButton
|
|
||||||
)}
|
|
||||||
isDisabled={isLastPage}
|
|
||||||
onPress={onNextPagePress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.PAGE_NEXT} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
styles.pageLink,
|
|
||||||
isLastPage && styles.disabledPageButton
|
|
||||||
)}
|
|
||||||
isDisabled={isLastPage}
|
|
||||||
onPress={onLastPagePress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.PAGE_LAST} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.recordsContainer}>
|
|
||||||
<div className={styles.records}>
|
|
||||||
{translate('TotalRecords', { totalRecords })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
TablePager.propTypes = {
|
|
||||||
page: PropTypes.number,
|
|
||||||
totalPages: PropTypes.number,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isFetching: PropTypes.bool,
|
|
||||||
onFirstPagePress: PropTypes.func.isRequired,
|
|
||||||
onPreviousPagePress: PropTypes.func.isRequired,
|
|
||||||
onNextPagePress: PropTypes.func.isRequired,
|
|
||||||
onLastPagePress: PropTypes.func.isRequired,
|
|
||||||
onPageSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TablePager;
|
|
||||||
159
frontend/src/Components/Table/TablePager.tsx
Normal file
159
frontend/src/Components/Table/TablePager.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TablePager.css';
|
||||||
|
|
||||||
|
interface TablePagerProps {
|
||||||
|
page?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
totalRecords?: number;
|
||||||
|
isFetching?: boolean;
|
||||||
|
onFirstPagePress: () => void;
|
||||||
|
onPreviousPagePress: () => void;
|
||||||
|
onNextPagePress: () => void;
|
||||||
|
onLastPagePress: () => void;
|
||||||
|
onPageSelect: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TablePager({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
totalRecords = 0,
|
||||||
|
isFetching,
|
||||||
|
onFirstPagePress,
|
||||||
|
onPreviousPagePress,
|
||||||
|
onNextPagePress,
|
||||||
|
onLastPagePress,
|
||||||
|
onPageSelect,
|
||||||
|
}: TablePagerProps) {
|
||||||
|
const [isShowingPageSelect, setIsShowingPageSelect] = useState(false);
|
||||||
|
|
||||||
|
const isFirstPage = page === 1;
|
||||||
|
const isLastPage = page === totalPages;
|
||||||
|
|
||||||
|
const pages = useMemo(() => {
|
||||||
|
return Array.from(new Array(totalPages), (_x, i) => {
|
||||||
|
const pageNumber = i + 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: pageNumber,
|
||||||
|
value: String(pageNumber),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [totalPages]);
|
||||||
|
|
||||||
|
const handleOpenPageSelectClick = useCallback(() => {
|
||||||
|
setIsShowingPageSelect(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePageSelect = useCallback(
|
||||||
|
({ value }: InputChanged<number>) => {
|
||||||
|
setIsShowingPageSelect(false);
|
||||||
|
onPageSelect(value);
|
||||||
|
},
|
||||||
|
[onPageSelect]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePageSelectBlur = useCallback(() => {
|
||||||
|
setIsShowingPageSelect(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.pager}>
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
{isFetching ? (
|
||||||
|
<LoadingIndicator className={styles.loading} size={20} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.controlsContainer}>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
styles.pageLink,
|
||||||
|
isFirstPage && styles.disabledPageButton
|
||||||
|
)}
|
||||||
|
isDisabled={isFirstPage}
|
||||||
|
onPress={onFirstPagePress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.PAGE_FIRST} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
styles.pageLink,
|
||||||
|
isFirstPage && styles.disabledPageButton
|
||||||
|
)}
|
||||||
|
isDisabled={isFirstPage}
|
||||||
|
onPress={onPreviousPagePress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.PAGE_PREVIOUS} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className={styles.pageNumber}>
|
||||||
|
{isShowingPageSelect ? null : (
|
||||||
|
<Link
|
||||||
|
isDisabled={totalPages === 1}
|
||||||
|
onPress={handleOpenPageSelectClick}
|
||||||
|
>
|
||||||
|
{page} / {totalPages}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isShowingPageSelect ? (
|
||||||
|
<SelectInput
|
||||||
|
className={styles.pageSelect}
|
||||||
|
name="pageSelect"
|
||||||
|
value={page}
|
||||||
|
values={pages}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={handlePageSelect}
|
||||||
|
onBlur={handlePageSelectBlur}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
styles.pageLink,
|
||||||
|
isLastPage && styles.disabledPageButton
|
||||||
|
)}
|
||||||
|
isDisabled={isLastPage}
|
||||||
|
onPress={onNextPagePress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.PAGE_NEXT} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
styles.pageLink,
|
||||||
|
isLastPage && styles.disabledPageButton
|
||||||
|
)}
|
||||||
|
isDisabled={isLastPage}
|
||||||
|
onPress={onLastPagePress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.PAGE_LAST} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.recordsContainer}>
|
||||||
|
<div className={styles.records}>
|
||||||
|
{translate('TotalRecords', { totalRecords })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TablePager;
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './TableRow.css';
|
|
||||||
|
|
||||||
function TableRow(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
overlayContent,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
className={className}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TableRow.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
overlayContent: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
TableRow.defaultProps = {
|
|
||||||
className: styles.row
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableRow;
|
|
||||||
23
frontend/src/Components/Table/TableRow.tsx
Normal file
23
frontend/src/Components/Table/TableRow.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './TableRow.css';
|
||||||
|
|
||||||
|
interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
overlayContent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({
|
||||||
|
className = styles.row,
|
||||||
|
children,
|
||||||
|
overlayContent,
|
||||||
|
...otherProps
|
||||||
|
}: TableRowProps) {
|
||||||
|
return (
|
||||||
|
<tr className={className} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableRow;
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import TableRow from './TableRow';
|
|
||||||
import styles from './TableRowButton.css';
|
|
||||||
|
|
||||||
function TableRowButton(props) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={styles.row}
|
|
||||||
component={TableRow}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TableRowButton;
|
|
||||||
10
frontend/src/Components/Table/TableRowButton.tsx
Normal file
10
frontend/src/Components/Table/TableRowButton.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link, { LinkProps } from 'Components/Link/Link';
|
||||||
|
import TableRow from './TableRow';
|
||||||
|
import styles from './TableRowButton.css';
|
||||||
|
|
||||||
|
function TableRowButton(props: LinkProps) {
|
||||||
|
return <Link className={styles.row} component={TableRow} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableRowButton;
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import VirtualTableHeaderCell from './TableHeaderCell';
|
|
||||||
import styles from './TableSelectAllHeaderCell.css';
|
|
||||||
|
|
||||||
function getValue(allSelected, allUnselected) {
|
|
||||||
if (allSelected) {
|
|
||||||
return true;
|
|
||||||
} else if (allUnselected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableSelectAllHeaderCell(props) {
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
onSelectAllChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const value = getValue(allSelected, allUnselected);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.selectAllHeaderCell}
|
|
||||||
name="selectAll"
|
|
||||||
>
|
|
||||||
<CheckInput
|
|
||||||
className={styles.input}
|
|
||||||
name="selectAll"
|
|
||||||
value={value}
|
|
||||||
onChange={onSelectAllChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TableSelectAllHeaderCell.propTypes = {
|
|
||||||
allSelected: PropTypes.bool.isRequired,
|
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableSelectAllHeaderCell;
|
|
||||||
43
frontend/src/Components/Table/TableSelectAllHeaderCell.tsx
Normal file
43
frontend/src/Components/Table/TableSelectAllHeaderCell.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import VirtualTableHeaderCell from './TableHeaderCell';
|
||||||
|
import styles from './TableSelectAllHeaderCell.css';
|
||||||
|
|
||||||
|
interface TableSelectAllHeaderCellProps {
|
||||||
|
allSelected: boolean;
|
||||||
|
allUnselected: boolean;
|
||||||
|
onSelectAllChange: (change: CheckInputChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableSelectAllHeaderCell({
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
onSelectAllChange,
|
||||||
|
}: TableSelectAllHeaderCellProps) {
|
||||||
|
const value = useMemo(() => {
|
||||||
|
if (allSelected) {
|
||||||
|
return true;
|
||||||
|
} else if (allUnselected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [allSelected, allUnselected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.selectAllHeaderCell}
|
||||||
|
name="selectAll"
|
||||||
|
>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.input}
|
||||||
|
name="selectAll"
|
||||||
|
value={value}
|
||||||
|
onChange={onSelectAllChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableSelectAllHeaderCell;
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
|
||||||
import styles from './VirtualTable.css';
|
|
||||||
|
|
||||||
const ROW_HEIGHT = 38;
|
|
||||||
|
|
||||||
function overscanIndicesGetter(options) {
|
|
||||||
const {
|
|
||||||
cellCount,
|
|
||||||
overscanCellsCount,
|
|
||||||
startIndex,
|
|
||||||
stopIndex
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// The default getter takes the scroll direction into account,
|
|
||||||
// but that can cause issues. Ignore the scroll direction and
|
|
||||||
// always over return more items.
|
|
||||||
|
|
||||||
const overscanStartIndex = startIndex - overscanCellsCount;
|
|
||||||
const overscanStopIndex = stopIndex + overscanCellsCount;
|
|
||||||
|
|
||||||
return {
|
|
||||||
overscanStartIndex: Math.max(0, overscanStartIndex),
|
|
||||||
overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class VirtualTable extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
width: 0,
|
|
||||||
scrollRestored: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this._grid = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
scrollIndex,
|
|
||||||
scrollTop
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
scrollRestored
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
|
|
||||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
|
||||||
this._grid.recomputeGridSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
|
|
||||||
this.setState({ scrollRestored: true });
|
|
||||||
this._grid.scrollToPosition({ scrollTop });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
|
||||||
this._grid.scrollToCell({
|
|
||||||
rowIndex: scrollIndex,
|
|
||||||
columnIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setGridRef = (ref) => {
|
|
||||||
this._grid = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.setState({
|
|
||||||
width
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isSmallScreen,
|
|
||||||
className,
|
|
||||||
items,
|
|
||||||
scroller,
|
|
||||||
header,
|
|
||||||
headerHeight,
|
|
||||||
rowHeight,
|
|
||||||
rowRenderer,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const gridStyle = {
|
|
||||||
boxSizing: undefined,
|
|
||||||
direction: undefined,
|
|
||||||
height: undefined,
|
|
||||||
position: undefined,
|
|
||||||
willChange: undefined,
|
|
||||||
overflow: undefined,
|
|
||||||
width: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerStyle = {
|
|
||||||
position: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WindowScroller
|
|
||||||
scrollElement={isSmallScreen ? undefined : scroller}
|
|
||||||
>
|
|
||||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
|
||||||
if (!height) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
<Scroller
|
|
||||||
className={className}
|
|
||||||
scrollDirection={scrollDirections.HORIZONTAL}
|
|
||||||
>
|
|
||||||
{header}
|
|
||||||
<div ref={registerChild}>
|
|
||||||
<Grid
|
|
||||||
{...otherProps}
|
|
||||||
ref={this.setGridRef}
|
|
||||||
autoContainerWidth={true}
|
|
||||||
autoHeight={true}
|
|
||||||
autoWidth={true}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
headerHeight={height - headerHeight}
|
|
||||||
rowHeight={rowHeight}
|
|
||||||
rowCount={items.length}
|
|
||||||
columnCount={1}
|
|
||||||
columnWidth={width}
|
|
||||||
scrollTop={scrollTop}
|
|
||||||
onScroll={onChildScroll}
|
|
||||||
overscanRowCount={2}
|
|
||||||
cellRenderer={rowRenderer}
|
|
||||||
overscanIndicesGetter={overscanIndicesGetter}
|
|
||||||
scrollToAlignment={'start'}
|
|
||||||
isScrollingOptout={true}
|
|
||||||
className={styles.tableBodyContainer}
|
|
||||||
style={gridStyle}
|
|
||||||
containerStyle={containerStyle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Scroller>
|
|
||||||
</Measure>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</WindowScroller>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualTable.propTypes = {
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
scrollIndex: PropTypes.number,
|
|
||||||
scrollTop: PropTypes.number,
|
|
||||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
|
||||||
header: PropTypes.node.isRequired,
|
|
||||||
headerHeight: PropTypes.number.isRequired,
|
|
||||||
rowRenderer: PropTypes.func.isRequired,
|
|
||||||
rowHeight: PropTypes.number.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
VirtualTable.defaultProps = {
|
|
||||||
className: styles.tableContainer,
|
|
||||||
headerHeight: 38,
|
|
||||||
rowHeight: ROW_HEIGHT
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VirtualTable;
|
|
||||||
167
frontend/src/Components/Table/VirtualTable.tsx
Normal file
167
frontend/src/Components/Table/VirtualTable.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
import React, { ReactNode, useEffect, useRef } from 'react';
|
||||||
|
import { Grid, GridCellProps, WindowScroller } from 'react-virtualized';
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import styles from './VirtualTable.css';
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 38;
|
||||||
|
|
||||||
|
function overscanIndicesGetter(options: {
|
||||||
|
cellCount: number;
|
||||||
|
overscanCellsCount: number;
|
||||||
|
startIndex: number;
|
||||||
|
stopIndex: number;
|
||||||
|
}) {
|
||||||
|
const { cellCount, overscanCellsCount, startIndex, stopIndex } = options;
|
||||||
|
|
||||||
|
// The default getter takes the scroll direction into account,
|
||||||
|
// but that can cause issues. Ignore the scroll direction and
|
||||||
|
// always over return more items.
|
||||||
|
|
||||||
|
const overscanStartIndex = startIndex - overscanCellsCount;
|
||||||
|
const overscanStopIndex = stopIndex + overscanCellsCount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
overscanStartIndex: Math.max(0, overscanStartIndex),
|
||||||
|
overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VirtualTableProps<T extends ModelBase> {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
className?: string;
|
||||||
|
items: T[];
|
||||||
|
scrollIndex?: number;
|
||||||
|
scrollTop?: number;
|
||||||
|
scroller: Element;
|
||||||
|
header: React.ReactNode;
|
||||||
|
headerHeight?: number;
|
||||||
|
rowRenderer: (rowProps: GridCellProps) => ReactNode;
|
||||||
|
rowHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VirtualTable<T extends ModelBase>({
|
||||||
|
isSmallScreen,
|
||||||
|
className = styles.tableContainer,
|
||||||
|
items,
|
||||||
|
scroller,
|
||||||
|
scrollIndex,
|
||||||
|
scrollTop,
|
||||||
|
header,
|
||||||
|
headerHeight = 38,
|
||||||
|
rowHeight = ROW_HEIGHT,
|
||||||
|
rowRenderer,
|
||||||
|
...otherProps
|
||||||
|
}: VirtualTableProps<T>) {
|
||||||
|
const [measureRef, bounds] = useMeasure();
|
||||||
|
const gridRef = useRef<Grid>(null);
|
||||||
|
const scrollRestored = useRef(false);
|
||||||
|
const previousScrollIndex = usePrevious(scrollIndex);
|
||||||
|
const previousItems = usePrevious(items);
|
||||||
|
|
||||||
|
const width = bounds.width;
|
||||||
|
|
||||||
|
const gridStyle = {
|
||||||
|
boxSizing: undefined,
|
||||||
|
direction: undefined,
|
||||||
|
height: undefined,
|
||||||
|
position: undefined,
|
||||||
|
willChange: undefined,
|
||||||
|
overflow: undefined,
|
||||||
|
width: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
position: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gridRef.current && width > 0) {
|
||||||
|
gridRef.current.recomputeGridSize();
|
||||||
|
}
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
gridRef.current &&
|
||||||
|
previousItems &&
|
||||||
|
hasDifferentItemsOrOrder(previousItems, items)
|
||||||
|
) {
|
||||||
|
gridRef.current.recomputeGridSize();
|
||||||
|
}
|
||||||
|
}, [items, previousItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gridRef.current && scrollTop && !scrollRestored.current) {
|
||||||
|
gridRef.current.scrollToPosition({ scrollLeft: 0, scrollTop });
|
||||||
|
scrollRestored.current = true;
|
||||||
|
}
|
||||||
|
}, [scrollTop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
gridRef.current &&
|
||||||
|
scrollIndex != null &&
|
||||||
|
scrollIndex !== previousScrollIndex
|
||||||
|
) {
|
||||||
|
gridRef.current.scrollToCell({
|
||||||
|
rowIndex: scrollIndex,
|
||||||
|
columnIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [scrollIndex, previousScrollIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WindowScroller scrollElement={isSmallScreen ? undefined : scroller}>
|
||||||
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||||
|
if (!height) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div ref={measureRef}>
|
||||||
|
<Scroller
|
||||||
|
className={className}
|
||||||
|
scrollDirection={scrollDirections.HORIZONTAL}
|
||||||
|
>
|
||||||
|
{header}
|
||||||
|
|
||||||
|
{/* @ts-expect-error - ref type is incompatible */}
|
||||||
|
<div ref={registerChild}>
|
||||||
|
<Grid
|
||||||
|
{...otherProps}
|
||||||
|
ref={gridRef}
|
||||||
|
autoContainerWidth={true}
|
||||||
|
autoHeight={true}
|
||||||
|
autoWidth={true}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
headerHeight={height - headerHeight}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
rowCount={items.length}
|
||||||
|
columnCount={1}
|
||||||
|
columnWidth={width}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={rowRenderer}
|
||||||
|
overscanIndicesGetter={overscanIndicesGetter}
|
||||||
|
scrollToAlignment="start"
|
||||||
|
isScrollingOptout={true}
|
||||||
|
className={styles.tableBodyContainer}
|
||||||
|
style={gridStyle}
|
||||||
|
containerStyle={containerStyle}
|
||||||
|
onScroll={onChildScroll}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Scroller>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</WindowScroller>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTable;
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './VirtualTableHeader.css';
|
|
||||||
|
|
||||||
function VirtualTableHeader({ children }) {
|
|
||||||
return (
|
|
||||||
<div className={styles.header}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualTableHeader.propTypes = {
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VirtualTableHeader;
|
|
||||||
12
frontend/src/Components/Table/VirtualTableHeader.tsx
Normal file
12
frontend/src/Components/Table/VirtualTableHeader.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './VirtualTableHeader.css';
|
||||||
|
|
||||||
|
interface VirtualTableHeaderProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VirtualTableHeader({ children }: VirtualTableHeaderProps) {
|
||||||
|
return <div className={styles.header}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTableHeader;
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
|
||||||
import styles from './VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
export function headerRenderer(headerProps) {
|
|
||||||
const {
|
|
||||||
columnData = {},
|
|
||||||
dataKey,
|
|
||||||
label
|
|
||||||
} = headerProps;
|
|
||||||
|
|
||||||
return (
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
name={dataKey}
|
|
||||||
{...columnData}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class VirtualTableHeaderCell extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
fixedSortDirection
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (fixedSortDirection) {
|
|
||||||
this.props.onSortPress(name, fixedSortDirection);
|
|
||||||
} else {
|
|
||||||
this.props.onSortPress(name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
isSortable,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
fixedSortDirection,
|
|
||||||
children,
|
|
||||||
onSortPress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isSorting = isSortable && sortKey === name;
|
|
||||||
const sortIcon = sortDirection === sortDirections.ASCENDING ?
|
|
||||||
icons.SORT_ASCENDING :
|
|
||||||
icons.SORT_DESCENDING;
|
|
||||||
|
|
||||||
return (
|
|
||||||
isSortable ?
|
|
||||||
<Link
|
|
||||||
component="div"
|
|
||||||
className={className}
|
|
||||||
onPress={this.onPress}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{
|
|
||||||
isSorting &&
|
|
||||||
<Icon
|
|
||||||
name={sortIcon}
|
|
||||||
className={styles.sortIcon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</Link> :
|
|
||||||
|
|
||||||
<div className={className}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualTableHeaderCell.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
||||||
isSortable: PropTypes.bool,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
fixedSortDirection: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onSortPress: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
VirtualTableHeaderCell.defaultProps = {
|
|
||||||
className: styles.headerCell,
|
|
||||||
isSortable: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VirtualTableHeaderCell;
|
|
||||||
60
frontend/src/Components/Table/VirtualTableHeaderCell.tsx
Normal file
60
frontend/src/Components/Table/VirtualTableHeaderCell.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons, sortDirections } from 'Helpers/Props';
|
||||||
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
import styles from './VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
interface VirtualTableHeaderCellProps {
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
isSortable?: boolean;
|
||||||
|
sortKey?: string;
|
||||||
|
fixedSortDirection?: SortDirection;
|
||||||
|
sortDirection?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onSortPress?: (name: string, sortDirection?: SortDirection) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VirtualTableHeaderCell({
|
||||||
|
className = styles.headerCell,
|
||||||
|
name,
|
||||||
|
isSortable = false,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
fixedSortDirection,
|
||||||
|
children,
|
||||||
|
onSortPress,
|
||||||
|
...otherProps
|
||||||
|
}: VirtualTableHeaderCellProps) {
|
||||||
|
const isSorting = isSortable && sortKey === name;
|
||||||
|
const sortIcon =
|
||||||
|
sortDirection === sortDirections.ASCENDING
|
||||||
|
? icons.SORT_ASCENDING
|
||||||
|
: icons.SORT_DESCENDING;
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
if (fixedSortDirection) {
|
||||||
|
onSortPress?.(name, fixedSortDirection);
|
||||||
|
} else {
|
||||||
|
onSortPress?.(name);
|
||||||
|
}
|
||||||
|
}, [name, fixedSortDirection, onSortPress]);
|
||||||
|
|
||||||
|
return isSortable ? (
|
||||||
|
<Link
|
||||||
|
component="div"
|
||||||
|
className={className}
|
||||||
|
onPress={handlePress}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{isSorting ? <Icon name={sortIcon} className={styles.sortIcon} /> : null}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className={className}>{children}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTableHeaderCell;
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './VirtualTableRow.css';
|
|
||||||
|
|
||||||
function VirtualTableRow(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
style,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualTableRow.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
style: PropTypes.object.isRequired,
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
VirtualTableRow.defaultProps = {
|
|
||||||
className: styles.row
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VirtualTableRow;
|
|
||||||
23
frontend/src/Components/Table/VirtualTableRow.tsx
Normal file
23
frontend/src/Components/Table/VirtualTableRow.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './VirtualTableRow.css';
|
||||||
|
|
||||||
|
interface VirtualTableRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
className: string;
|
||||||
|
style: object;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VirtualTableRow({
|
||||||
|
className = styles.row,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...otherProps
|
||||||
|
}: VirtualTableRowProps) {
|
||||||
|
return (
|
||||||
|
<div className={className} style={style} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTableRow;
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import VirtualTableRow from './VirtualTableRow';
|
|
||||||
import styles from './VirtualTableRowButton.css';
|
|
||||||
|
|
||||||
function VirtualTableRowButton(props) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={styles.row}
|
|
||||||
component={VirtualTableRow}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default VirtualTableRowButton;
|
|
||||||
10
frontend/src/Components/Table/VirtualTableRowButton.tsx
Normal file
10
frontend/src/Components/Table/VirtualTableRowButton.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link, { LinkProps } from 'Components/Link/Link';
|
||||||
|
import VirtualTableRow from './VirtualTableRow';
|
||||||
|
import styles from './VirtualTableRowButton.css';
|
||||||
|
|
||||||
|
function VirtualTableRowButton(props: LinkProps) {
|
||||||
|
return <Link className={styles.row} component={VirtualTableRow} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTableRowButton;
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import VirtualTableHeaderCell from './VirtualTableHeaderCell';
|
|
||||||
import styles from './VirtualTableSelectAllHeaderCell.css';
|
|
||||||
|
|
||||||
function getValue(allSelected, allUnselected) {
|
|
||||||
if (allSelected) {
|
|
||||||
return true;
|
|
||||||
} else if (allUnselected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function VirtualTableSelectAllHeaderCell(props) {
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
onSelectAllChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const value = getValue(allSelected, allUnselected);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.selectAllHeaderCell}
|
|
||||||
name="selectAll"
|
|
||||||
>
|
|
||||||
<CheckInput
|
|
||||||
className={styles.input}
|
|
||||||
name="selectAll"
|
|
||||||
value={value}
|
|
||||||
onChange={onSelectAllChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualTableSelectAllHeaderCell.propTypes = {
|
|
||||||
allSelected: PropTypes.bool.isRequired,
|
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VirtualTableSelectAllHeaderCell;
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import VirtualTableHeaderCell from './VirtualTableHeaderCell';
|
||||||
|
import styles from './VirtualTableSelectAllHeaderCell.css';
|
||||||
|
|
||||||
|
interface VirtualTableSelectAllHeaderCellProps {
|
||||||
|
allSelected: boolean;
|
||||||
|
allUnselected: boolean;
|
||||||
|
onSelectAllChange: (change: CheckInputChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VirtualTableSelectAllHeaderCell({
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
onSelectAllChange,
|
||||||
|
}: VirtualTableSelectAllHeaderCellProps) {
|
||||||
|
const value = useMemo(() => {
|
||||||
|
if (allSelected) {
|
||||||
|
return true;
|
||||||
|
} else if (allUnselected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [allSelected, allUnselected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
className={styles.selectAllHeaderCell}
|
||||||
|
name="selectAll"
|
||||||
|
>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.input}
|
||||||
|
name="selectAll"
|
||||||
|
value={value}
|
||||||
|
onChange={onSelectAllChange}
|
||||||
|
/>
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualTableSelectAllHeaderCell;
|
||||||
|
|
@ -5,11 +5,11 @@ import areAllSelected from 'Utilities/Table/areAllSelected';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
|
||||||
export type SelectedState = Record<number, boolean>;
|
export type SelectedState = Record<number | string, boolean>;
|
||||||
|
|
||||||
export interface SelectState {
|
export interface SelectState {
|
||||||
selectedState: SelectedState;
|
selectedState: SelectedState;
|
||||||
lastToggled: number | null;
|
lastToggled: number | string | null;
|
||||||
allSelected: boolean;
|
allSelected: boolean;
|
||||||
allUnselected: boolean;
|
allUnselected: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -20,14 +20,14 @@ export type SelectAction =
|
||||||
| { type: 'unselectAll'; items: ModelBase[] }
|
| { type: 'unselectAll'; items: ModelBase[] }
|
||||||
| {
|
| {
|
||||||
type: 'toggleSelected';
|
type: 'toggleSelected';
|
||||||
id: number;
|
id: number | string;
|
||||||
isSelected: boolean;
|
isSelected: boolean | null;
|
||||||
shiftKey: boolean;
|
shiftKey: boolean;
|
||||||
items: ModelBase[];
|
items: ModelBase[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'removeItem';
|
type: 'removeItem';
|
||||||
id: number;
|
id: number | string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'updateItems';
|
type: 'updateItems';
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
|
@ -20,29 +21,34 @@ import FavoriteFolderRow from './FavoriteFolderRow';
|
||||||
import RecentFolderRow from './RecentFolderRow';
|
import RecentFolderRow from './RecentFolderRow';
|
||||||
import styles from './InteractiveImportSelectFolderModalContent.css';
|
import styles from './InteractiveImportSelectFolderModalContent.css';
|
||||||
|
|
||||||
const favoriteFoldersColumns = [
|
const favoriteFoldersColumns: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'folder',
|
name: 'folder',
|
||||||
label: () => translate('Folder'),
|
label: () => translate('Folder'),
|
||||||
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: '',
|
label: '',
|
||||||
|
isVisible: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const recentFoldersColumns = [
|
const recentFoldersColumns: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'folder',
|
name: 'folder',
|
||||||
label: () => translate('Folder'),
|
label: () => translate('Folder'),
|
||||||
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lastUsed',
|
name: 'lastUsed',
|
||||||
label: () => translate('LastUsed'),
|
label: () => translate('LastUsed'),
|
||||||
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: '',
|
label: '',
|
||||||
|
isVisible: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ import {
|
||||||
} from 'Store/Actions/movieFileActions';
|
} from 'Store/Actions/movieFileActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SortCallback } from 'typings/callbacks';
|
import { SortCallback } from 'typings/callbacks';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
@ -360,7 +360,7 @@ function InteractiveImportModalContent(
|
||||||
}, [previousIsDeleting, isDeleting, deleteError, onModalClose]);
|
}, [previousIsDeleting, isDeleting, deleteError, onModalClose]);
|
||||||
|
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }: SelectStateInputProps) => {
|
({ value }: CheckInputChanged) => {
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
},
|
},
|
||||||
[items, setSelectState]
|
[items, setSelectState]
|
||||||
|
|
@ -378,8 +378,8 @@ function InteractiveImportModalContent(
|
||||||
|
|
||||||
setWithoutMovieFileIdRowsSelected(
|
setWithoutMovieFileIdRowsSelected(
|
||||||
hasMovieFileId || !value
|
hasMovieFileId || !value
|
||||||
? without(withoutMovieFileIdRowsSelected, id)
|
? without(withoutMovieFileIdRowsSelected, id as number)
|
||||||
: [...withoutMovieFileIdRowsSelected, id]
|
: [...withoutMovieFileIdRowsSelected, id as number]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -128,8 +128,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
}
|
}
|
||||||
}, [id, movie, quality, languages, isSelected, onValidRowChange]);
|
}, [id, movie, quality, languages, isSelected, onValidRowChange]);
|
||||||
|
|
||||||
const onSelectedChangeWrapper = useCallback(
|
const handleSelectedChange = useCallback(
|
||||||
(result: SelectedChangeProps) => {
|
(result: SelectStateInputProps) => {
|
||||||
onSelectedChange({
|
onSelectedChange({
|
||||||
...result,
|
...result,
|
||||||
hasMovieFileId: !!movieFileId,
|
hasMovieFileId: !!movieFileId,
|
||||||
|
|
@ -271,7 +271,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
<TableSelectCell
|
<TableSelectCell
|
||||||
id={id}
|
id={id}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onSelectedChange={onSelectedChangeWrapper}
|
onSelectedChange={handleSelectedChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.relativePath} title={relativePath}>
|
<TableRowCell className={styles.relativePath} title={relativePath}>
|
||||||
|
|
|
||||||
|
|
@ -315,12 +315,8 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||||
|
|
||||||
if (name === 'path') {
|
if (name === 'path') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
key={name}
|
<span title={path}>{path}</span>
|
||||||
className={styles[name]}
|
|
||||||
title={path}
|
|
||||||
>
|
|
||||||
{path}
|
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
|
@ -10,7 +11,7 @@ import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import RootFolderRow from './RootFolderRow';
|
import RootFolderRow from './RootFolderRow';
|
||||||
|
|
||||||
const rootFolderColumns = [
|
const rootFolderColumns: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'path',
|
name: 'path',
|
||||||
label: () => translate('Path'),
|
label: () => translate('Path'),
|
||||||
|
|
@ -28,6 +29,7 @@ const rootFolderColumns = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
|
label: '',
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
setManageCustomFormatsSort,
|
setManageCustomFormatsSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
@ -133,7 +133,7 @@ function ManageCustomFormatsModalContent(
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }: SelectStateInputProps) => {
|
({ value }: CheckInputChanged) => {
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
},
|
},
|
||||||
[items, setSelectState]
|
[items, setSelectState]
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
setManageDownloadClientsSort,
|
setManageDownloadClientsSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
@ -185,7 +185,7 @@ function ManageDownloadClientsModalContent(
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }: SelectStateInputProps) => {
|
({ value }: CheckInputChanged) => {
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
},
|
},
|
||||||
[items, setSelectState]
|
[items, setSelectState]
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import {
|
import {
|
||||||
bulkDeleteImportListExclusions,
|
bulkDeleteImportListExclusions,
|
||||||
clearImportListExclusions,
|
clearImportListExclusions,
|
||||||
|
|
@ -156,8 +157,8 @@ function ImportListExclusions() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSortPress = useCallback(
|
const handleSortPress = useCallback(
|
||||||
(sortKey: { sortKey: string }) => {
|
(sortKey: string, sortDirection?: SortDirection) => {
|
||||||
dispatch(setImportListExclusionSort({ sortKey }));
|
dispatch(setImportListExclusionSort({ sortKey, sortDirection }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
bulkEditImportLists,
|
bulkEditImportLists,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
@ -180,7 +180,7 @@ function ManageImportListsModalContent(
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }: SelectStateInputProps) => {
|
({ value }: CheckInputChanged) => {
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
},
|
},
|
||||||
[items, setSelectState]
|
[items, setSelectState]
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
setManageIndexersSort,
|
setManageIndexersSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
@ -183,7 +183,7 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }: SelectStateInputProps) => {
|
({ value }: CheckInputChanged) => {
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
},
|
},
|
||||||
[items, setSelectState]
|
[items, setSelectState]
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import FieldSet from 'Components/FieldSet';
|
import FieldSet from 'Components/FieldSet';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueuedTaskRow from './QueuedTaskRow';
|
import QueuedTaskRow from './QueuedTaskRow';
|
||||||
|
|
||||||
const columns = [
|
const columns: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'trigger',
|
name: 'trigger',
|
||||||
label: '',
|
label: '',
|
||||||
|
|
@ -42,6 +43,7 @@ const columns = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
|
label: '',
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
function getToggledRange(items, id, lastToggled) {
|
|
||||||
const lastToggledIndex = _.findIndex(items, { id: lastToggled });
|
|
||||||
const changedIndex = _.findIndex(items, { id });
|
|
||||||
let lower = 0;
|
|
||||||
let upper = 0;
|
|
||||||
|
|
||||||
if (lastToggledIndex > changedIndex) {
|
|
||||||
lower = changedIndex;
|
|
||||||
upper = lastToggledIndex + 1;
|
|
||||||
} else {
|
|
||||||
lower = lastToggledIndex;
|
|
||||||
upper = changedIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lower,
|
|
||||||
upper
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getToggledRange;
|
|
||||||
27
frontend/src/Utilities/Table/getToggledRange.ts
Normal file
27
frontend/src/Utilities/Table/getToggledRange.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
function getToggledRange<T extends ModelBase>(
|
||||||
|
items: T[],
|
||||||
|
id: number | string,
|
||||||
|
lastToggled: number | string
|
||||||
|
) {
|
||||||
|
const lastToggledIndex = items.findIndex((item) => item.id === lastToggled);
|
||||||
|
const changedIndex = items.findIndex((item) => item.id === id);
|
||||||
|
let lower = 0;
|
||||||
|
let upper = 0;
|
||||||
|
|
||||||
|
if (lastToggledIndex > changedIndex) {
|
||||||
|
lower = changedIndex;
|
||||||
|
upper = lastToggledIndex + 1;
|
||||||
|
} else {
|
||||||
|
lower = lastToggledIndex;
|
||||||
|
upper = changedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lower,
|
||||||
|
upper,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getToggledRange;
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import areAllSelected from './areAllSelected';
|
|
||||||
import getToggledRange from './getToggledRange';
|
|
||||||
|
|
||||||
function toggleSelected(selectedState, items, id, selected, shiftKey) {
|
|
||||||
const lastToggled = selectedState.lastToggled;
|
|
||||||
const nextSelectedState = {
|
|
||||||
...selectedState.selectedState,
|
|
||||||
[id]: selected
|
|
||||||
};
|
|
||||||
|
|
||||||
if (selected == null) {
|
|
||||||
delete nextSelectedState[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shiftKey && lastToggled) {
|
|
||||||
const { lower, upper } = getToggledRange(items, id, lastToggled);
|
|
||||||
|
|
||||||
for (let i = lower; i < upper; i++) {
|
|
||||||
nextSelectedState[items[i].id] = selected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...areAllSelected(nextSelectedState),
|
|
||||||
lastToggled: id,
|
|
||||||
selectedState: nextSelectedState
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default toggleSelected;
|
|
||||||
39
frontend/src/Utilities/Table/toggleSelected.ts
Normal file
39
frontend/src/Utilities/Table/toggleSelected.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import { SelectState } from 'Helpers/Hooks/useSelectState';
|
||||||
|
import areAllSelected from './areAllSelected';
|
||||||
|
import getToggledRange from './getToggledRange';
|
||||||
|
|
||||||
|
function toggleSelected<T extends ModelBase>(
|
||||||
|
selectState: SelectState,
|
||||||
|
items: T[],
|
||||||
|
id: number | string,
|
||||||
|
selected: boolean | null,
|
||||||
|
shiftKey: boolean
|
||||||
|
) {
|
||||||
|
const lastToggled = selectState.lastToggled;
|
||||||
|
const nextSelectedState = {
|
||||||
|
...selectState.selectedState,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selected == null) {
|
||||||
|
delete nextSelectedState[id];
|
||||||
|
} else {
|
||||||
|
nextSelectedState[id] = selected;
|
||||||
|
|
||||||
|
if (shiftKey && lastToggled) {
|
||||||
|
const { lower, upper } = getToggledRange(items, id, lastToggled);
|
||||||
|
|
||||||
|
for (let i = lower; i < upper; i++) {
|
||||||
|
nextSelectedState[items[i].id] = selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...areAllSelected(nextSelectedState),
|
||||||
|
lastToggled: id,
|
||||||
|
selectedState: nextSelectedState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default toggleSelected;
|
||||||
|
|
@ -2,5 +2,5 @@ import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
|
||||||
export type SortCallback = (
|
export type SortCallback = (
|
||||||
sortKey: string,
|
sortKey: string,
|
||||||
sortDirection: SortDirection
|
sortDirection?: SortDirection
|
||||||
) => void;
|
) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export interface SelectStateInputProps {
|
export interface SelectStateInputProps {
|
||||||
id: number;
|
id: number | string;
|
||||||
value: boolean;
|
value: boolean | null;
|
||||||
shiftKey: boolean;
|
shiftKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@
|
||||||
"@types/react-lazyload": "3.2.3",
|
"@types/react-lazyload": "3.2.3",
|
||||||
"@types/react-router-dom": "5.3.3",
|
"@types/react-router-dom": "5.3.3",
|
||||||
"@types/react-text-truncate": "0.19.0",
|
"@types/react-text-truncate": "0.19.0",
|
||||||
|
"@types/react-virtualized": "9.22.0",
|
||||||
"@types/react-window": "1.8.8",
|
"@types/react-window": "1.8.8",
|
||||||
"@types/redux-actions": "2.6.5",
|
"@types/redux-actions": "2.6.5",
|
||||||
"@types/webpack-livereload-plugin": "2.3.6",
|
"@types/webpack-livereload-plugin": "2.3.6",
|
||||||
|
|
|
||||||
|
|
@ -1474,6 +1474,14 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-virtualized@9.22.0":
|
||||||
|
version "9.22.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.22.0.tgz#2ff9b3692fa04a429df24ffc7d181d9f33b3831d"
|
||||||
|
integrity sha512-JL/YCCFZ123za//cj10Apk54F0UGFMrjOE0QHTuXt1KBMFrzLOGv9/x6Uc/pZ0Gaf4o6w61Fostvlw0DwuPXig==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-window@1.8.8":
|
"@types/react-window@1.8.8":
|
||||||
version "1.8.8"
|
version "1.8.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3"
|
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue