mirror of
https://github.com/Lidarr/Lidarr
synced 2026-01-06 23:56:11 +01:00
Merge 565bf2696e into d8f79c0189
This commit is contained in:
commit
6c1f9b79fa
59 changed files with 1096 additions and 1464 deletions
|
|
@ -9,13 +9,13 @@ export type SelectContextAction =
|
|||
| { type: 'unselectAll' }
|
||||
| {
|
||||
type: 'toggleSelected';
|
||||
id: number;
|
||||
isSelected: boolean;
|
||||
id: number | string;
|
||||
isSelected: boolean | null;
|
||||
shiftKey: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'removeItem';
|
||||
id: number;
|
||||
id: number | string;
|
||||
}
|
||||
| {
|
||||
type: 'updateItems';
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
|
|||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Column from 'Components/Table/Column';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
|
|
@ -176,10 +175,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
|
|||
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<Scroller
|
||||
className={styles.tableScroller}
|
||||
scrollDirection={ScrollDirection.Horizontal}
|
||||
>
|
||||
<Scroller className={styles.tableScroller} scrollDirection={'horizontal'}>
|
||||
<ArtistIndexTableHeader
|
||||
showBanners={showBanners}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -1,96 +1,93 @@
|
|||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ComponentClass,
|
||||
FunctionComponent,
|
||||
ComponentPropsWithoutRef,
|
||||
ElementType,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
interface ReactRouterLinkProps {
|
||||
to?: string;
|
||||
}
|
||||
export type LinkProps<C extends ElementType = 'button'> =
|
||||
ComponentPropsWithoutRef<C> & {
|
||||
component?: C;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: LinkProps<C>['disabled'];
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
};
|
||||
|
||||
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
||||
className?: string;
|
||||
component?:
|
||||
| string
|
||||
| FunctionComponent<LinkProps>
|
||||
| ComponentClass<LinkProps, unknown>;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: boolean;
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
}
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
className,
|
||||
component = 'button',
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter = false,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
export default function Link<C extends ElementType = 'button'>({
|
||||
className,
|
||||
component,
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: LinkProps<C>) {
|
||||
const Component = component || 'button';
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onPress?.(event);
|
||||
},
|
||||
[isDisabled, onPress]
|
||||
);
|
||||
|
||||
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
||||
target,
|
||||
};
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if (/\w+?:\/\//.test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Lidarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
const linkClass = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const elementProps = {
|
||||
...otherProps,
|
||||
type,
|
||||
...linkProps,
|
||||
};
|
||||
if (to) {
|
||||
const toLink = /\w+?:\/\//.test(to);
|
||||
|
||||
elementProps.onClick = onClick;
|
||||
if (toLink || noRouter) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
target={target || (toLink ? '_blank' : '_self')}
|
||||
rel={toLink ? 'noreferrer' : undefined}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement(el, elementProps);
|
||||
return (
|
||||
<RouterLink
|
||||
to={`${window.Lidarr.urlBase}/${to.replace(/^\//, '')}`}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
type={
|
||||
component === 'button' || component === 'input'
|
||||
? type || 'button'
|
||||
: type
|
||||
}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Link;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ const PageContentBody = forwardRef(
|
|||
ref={ref}
|
||||
{...otherProps}
|
||||
className={className}
|
||||
scrollDirection={ScrollDirection.Vertical}
|
||||
scrollDirection={'vertical'}
|
||||
onScroll={onScrollWrapper}
|
||||
>
|
||||
<div className={innerClassName}>{children}</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import React, {
|
||||
ComponentProps,
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import { ScrollDirection } from 'Helpers/Props/scrollDirections';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
export interface OnScroll {
|
||||
|
|
@ -24,6 +25,7 @@ interface ScrollerProps {
|
|||
scrollTop?: number;
|
||||
initialScrollTop?: number;
|
||||
children?: ReactNode;
|
||||
style?: ComponentProps<'div'>['style'];
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +35,7 @@ const Scroller = forwardRef(
|
|||
className,
|
||||
autoFocus = false,
|
||||
autoScroll = true,
|
||||
scrollDirection = ScrollDirection.Vertical,
|
||||
scrollDirection = 'vertical',
|
||||
children,
|
||||
scrollTop,
|
||||
initialScrollTop,
|
||||
|
|
@ -41,13 +43,14 @@ const Scroller = forwardRef(
|
|||
...otherProps
|
||||
} = props;
|
||||
|
||||
const internalRef = useRef();
|
||||
const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => internalRef.current!, []);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (initialScrollTop != null) {
|
||||
currentRef.current.scrollTop = initialScrollTop;
|
||||
internalRef.current!.scrollTop = initialScrollTop;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -56,16 +59,16 @@ const Scroller = forwardRef(
|
|||
|
||||
useEffect(() => {
|
||||
if (scrollTop != null) {
|
||||
currentRef.current.scrollTop = scrollTop;
|
||||
internalRef.current!.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
if (autoFocus && scrollDirection !== ScrollDirection.None) {
|
||||
currentRef.current.focus({ preventScroll: true });
|
||||
if (autoFocus && scrollDirection !== 'none') {
|
||||
internalRef.current!.focus({ preventScroll: true });
|
||||
}
|
||||
}, [autoFocus, currentRef, scrollDirection, scrollTop]);
|
||||
}, [autoFocus, scrollDirection, scrollTop]);
|
||||
|
||||
useEffect(() => {
|
||||
const div = currentRef.current;
|
||||
const div = internalRef.current!;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const scrollLeft = div.scrollLeft;
|
||||
|
|
@ -74,17 +77,17 @@ const Scroller = forwardRef(
|
|||
onScroll?.({ scrollLeft, scrollTop });
|
||||
}, 10);
|
||||
|
||||
div.addEventListener('scroll', handleScroll);
|
||||
div?.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
div.removeEventListener('scroll', handleScroll);
|
||||
div?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [currentRef, onScroll]);
|
||||
}, [onScroll]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
ref={currentRef}
|
||||
ref={internalRef}
|
||||
className={classNames(
|
||||
className,
|
||||
styles.scroller,
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './RelativeDateCell.css';
|
||||
|
||||
class RelativeDateCell extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
date,
|
||||
includeSeconds,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
component: Component,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.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.elementType,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
RelativeDateCell.defaultProps = {
|
||||
className: styles.cell,
|
||||
includeSeconds: false,
|
||||
component: TableRowCell
|
||||
};
|
||||
|
||||
export default RelativeDateCell;
|
||||
50
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
50
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './RelativeDateCell.css';
|
||||
|
||||
interface RelativeDateCellProps {
|
||||
className?: string;
|
||||
date?: string;
|
||||
includeSeconds?: boolean;
|
||||
component?: React.ElementType;
|
||||
}
|
||||
|
||||
function RelativeDateCell(props: RelativeDateCellProps) {
|
||||
const {
|
||||
className = styles.cell,
|
||||
date,
|
||||
includeSeconds = false,
|
||||
component: Component = TableRowCell,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default RelativeDateCell;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
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;
|
||||
11
frontend/src/Components/Table/Cells/TableRowCell.tsx
Normal file
11
frontend/src/Components/Table/Cells/TableRowCell.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import styles from './TableRowCell.css';
|
||||
|
||||
export type TableRowCellProps = ComponentPropsWithoutRef<'td'>;
|
||||
|
||||
export default function TableRowCell({
|
||||
className = styles.cell,
|
||||
...tdProps
|
||||
}: TableRowCellProps) {
|
||||
return <td className={className} {...tdProps} />;
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
19
frontend/src/Components/Table/Cells/TableRowCellButton.tsx
Normal file
19
frontend/src/Components/Table/Cells/TableRowCellButton.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import Link, { LinkProps } from 'Components/Link/Link';
|
||||
import TableRowCell from './TableRowCell';
|
||||
import styles from './TableRowCellButton.css';
|
||||
|
||||
interface TableRowCellButtonProps extends LinkProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function TableRowCellButton(props: TableRowCellButtonProps) {
|
||||
const { className = styles.cell, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<Link className={className} component={TableRowCell} {...otherProps} />
|
||||
);
|
||||
}
|
||||
|
||||
export default TableRowCellButton;
|
||||
|
|
@ -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,29 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './VirtualTableRowCell.css';
|
||||
|
||||
function VirtualTableRowCell(props) {
|
||||
const {
|
||||
className,
|
||||
children
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VirtualTableRowCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
};
|
||||
|
||||
VirtualTableRowCell.defaultProps = {
|
||||
className: styles.cell
|
||||
};
|
||||
|
||||
export default VirtualTableRowCell;
|
||||
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,86 +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 {
|
||||
className,
|
||||
inputClassName,
|
||||
id,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
<CheckInput
|
||||
className={inputClassName}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualTableSelectCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
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 = {
|
||||
className: styles.cell,
|
||||
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 | string;
|
||||
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,12 +1,16 @@
|
|||
import React from 'react';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
// TODO: Convert to generic so `name` can be a type
|
||||
interface Column {
|
||||
name: string;
|
||||
label: string | PropertyFunction<string> | React.ReactNode;
|
||||
className?: string;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
fixedSortDirection?: SortDirection;
|
||||
isVisible: boolean;
|
||||
isModifiable?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,143 +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
|
||||
} = 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}
|
||||
>
|
||||
{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={isSortable}
|
||||
{...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;
|
||||
66
frontend/src/Components/Table/TableHeaderCell.tsx
Normal file
66
frontend/src/Components/Table/TableHeaderCell.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
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,
|
||||
columnLabel,
|
||||
isSortable = false,
|
||||
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}
|
||||
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,180 +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 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;
|
||||
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;
|
||||
22
frontend/src/Components/Table/TableRow.tsx
Normal file
22
frontend/src/Components/Table/TableRow.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
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,
|
||||
...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,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 toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
|
||||
export type SelectedState = Record<number, boolean>;
|
||||
export type SelectedState = Record<number | string, boolean>;
|
||||
|
||||
export interface SelectState {
|
||||
selectedState: SelectedState;
|
||||
lastToggled: number | null;
|
||||
lastToggled: number | string | null;
|
||||
allSelected: boolean;
|
||||
allUnselected: boolean;
|
||||
}
|
||||
|
|
@ -20,14 +20,14 @@ export type SelectAction =
|
|||
| { type: 'unselectAll'; items: ModelBase[] }
|
||||
| {
|
||||
type: 'toggleSelected';
|
||||
id: number;
|
||||
isSelected: boolean;
|
||||
id: number | string;
|
||||
isSelected: boolean | null;
|
||||
shiftKey: boolean;
|
||||
items: ModelBase[];
|
||||
}
|
||||
| {
|
||||
type: 'removeItem';
|
||||
id: number;
|
||||
id: number | string;
|
||||
}
|
||||
| {
|
||||
type: 'updateItems';
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
enum ScrollDirection {
|
||||
Horizontal = 'horizontal',
|
||||
Vertical = 'vertical',
|
||||
None = 'none',
|
||||
Both = 'both',
|
||||
}
|
||||
|
||||
export default ScrollDirection;
|
||||
|
|
@ -4,3 +4,5 @@ export const HORIZONTAL = 'horizontal';
|
|||
export const VERTICAL = 'vertical';
|
||||
|
||||
export const all = [NONE, HORIZONTAL, VERTICAL, BOTH];
|
||||
|
||||
export type ScrollDirection = 'none' | 'both' | 'horizontal' | 'vertical';
|
||||
|
|
@ -2,3 +2,5 @@ export const ASCENDING = 'ascending';
|
|||
export const DESCENDING = 'descending';
|
||||
|
||||
export const all = [ASCENDING, DESCENDING];
|
||||
|
||||
export type SortDirection = 'ascending' | 'descending';
|
||||
|
|
@ -21,7 +21,7 @@ import {
|
|||
setManageCustomFormatsSort,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
|
|
@ -133,7 +133,7 @@ function ManageCustomFormatsModalContent(
|
|||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }: SelectStateInputProps) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
setManageDownloadClientsSort,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
|
|
@ -185,7 +185,7 @@ function ManageDownloadClientsModalContent(
|
|||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }: SelectStateInputProps) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
bulkEditImportLists,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
|
|
@ -168,7 +168,7 @@ function ManageImportListsModalContent(
|
|||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }: SelectStateInputProps) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
setManageIndexersSort,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
|
|
@ -183,7 +183,7 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }: SelectStateInputProps) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
const columns = [
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
|
|
@ -42,6 +43,7 @@ const columns = [
|
|||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
6
frontend/src/typings/Table.ts
Normal file
6
frontend/src/typings/Table.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import Column from 'Components/Table/Column';
|
||||
|
||||
export interface TableOptionsChangePayload {
|
||||
pageSize?: number;
|
||||
columns?: Column[];
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ export type InputChanged<T = unknown> = {
|
|||
|
||||
export type InputOnChange<T> = (change: InputChanged<T>) => void;
|
||||
|
||||
export type CheckInputChanged = {
|
||||
name: string;
|
||||
value: boolean;
|
||||
};
|
||||
export interface CheckInputChanged extends InputChanged<boolean> {
|
||||
shiftKey: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export interface SelectStateInputProps {
|
||||
id: number;
|
||||
value: boolean;
|
||||
id: number | string;
|
||||
value: boolean | null;
|
||||
shiftKey: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@
|
|||
"@types/react-lazyload": "3.2.3",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.19.0",
|
||||
"@types/react-virtualized": "9.22.0",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/redux-actions": "2.6.5",
|
||||
"@types/webpack-livereload-plugin": "2.3.6",
|
||||
|
|
@ -148,6 +149,9 @@
|
|||
"webpack-livereload-plugin": "3.0.2",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"string-width": "4.2.3"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.17.0",
|
||||
"yarn": "1.22.19"
|
||||
|
|
|
|||
47
yarn.lock
47
yarn.lock
|
|
@ -1528,6 +1528,14 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "1.8.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3"
|
||||
|
|
@ -2974,11 +2982,6 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1:
|
|||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
electron-to-chromium@^1.5.227:
|
||||
version "1.5.228"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz#38b849bc8714bd21fb64f5ad56bf8cfd8638e1e9"
|
||||
|
|
@ -2999,11 +3002,6 @@ emoji-regex@^8.0.0:
|
|||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
emoji-regex@^9.2.2:
|
||||
version "9.2.2"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
|
|
@ -6511,7 +6509,7 @@ string-template@~0.2.1:
|
|||
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
|
||||
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, string-width@^4.1.0, string-width@^4.2.3, string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
|
@ -6520,24 +6518,6 @@ string-template@~0.2.1:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
|
||||
dependencies:
|
||||
eastasianwidth "^0.2.0"
|
||||
emoji-regex "^9.2.2"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string.prototype.matchall@^4.0.11:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a"
|
||||
|
|
@ -6611,14 +6591,7 @@ string_decoder@~1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
|
|
|||
Loading…
Reference in a new issue