diff --git a/frontend/src/App/SelectContext.tsx b/frontend/src/App/SelectContext.tsx index 66be388ce..eca22c6c7 100644 --- a/frontend/src/App/SelectContext.tsx +++ b/frontend/src/App/SelectContext.tsx @@ -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'; diff --git a/frontend/src/Components/Form/SelectInput.tsx b/frontend/src/Components/Form/SelectInput.tsx index 4716c2dfd..02689e151 100644 --- a/frontend/src/Components/Form/SelectInput.tsx +++ b/frontend/src/Components/Form/SelectInput.tsx @@ -4,7 +4,7 @@ import { InputChanged } from 'typings/inputs'; import styles from './SelectInput.css'; interface SelectInputOption { - key: string; + key: string | number; value: string | number | (() => string | number); } diff --git a/frontend/src/Components/Table/Cells/TableSelectCell.js b/frontend/src/Components/Table/Cells/TableSelectCell.js deleted file mode 100644 index a2a297f2e..000000000 --- a/frontend/src/Components/Table/Cells/TableSelectCell.js +++ /dev/null @@ -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 ( - - - - ); - } -} - -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; diff --git a/frontend/src/Components/Table/Cells/TableSelectCell.tsx b/frontend/src/Components/Table/Cells/TableSelectCell.tsx new file mode 100644 index 000000000..1f9e4b200 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableSelectCell.tsx @@ -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 { + 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 ( + + + + ); +} + +export default TableSelectCell; diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.js b/frontend/src/Components/Table/Cells/VirtualTableRowCell.js deleted file mode 100644 index 42999216f..000000000 --- a/frontend/src/Components/Table/Cells/VirtualTableRowCell.js +++ /dev/null @@ -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 ( -
- {children} -
- ); -} - -VirtualTableRowCell.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) -}; - -VirtualTableRowCell.defaultProps = { - className: styles.cell -}; - -export default VirtualTableRowCell; diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.tsx b/frontend/src/Components/Table/Cells/VirtualTableRowCell.tsx new file mode 100644 index 000000000..6a3307c2a --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableRowCell.tsx @@ -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
{children}
; +} + +export default VirtualTableRowCell; diff --git a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js deleted file mode 100644 index f4d2a4684..000000000 --- a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js +++ /dev/null @@ -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 - - ); -} - -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 ( - - - - ); - } -} - -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; diff --git a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.tsx b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.tsx new file mode 100644 index 000000000..73cb7e523 --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.tsx @@ -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 ( + + + + ); +} + +export default VirtualTableSelectCell; diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js deleted file mode 100644 index 4c970e469..000000000 --- a/frontend/src/Components/Table/Table.js +++ /dev/null @@ -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 ( - - - - { - selectAll ? - : - null - } - - { - columns.map((column) => { - const { - name, - isVisible, - isSortable, - ...otherColumnProps - } = column; - - if (!isVisible) { - return null; - } - - if ( - (name === 'actions' || name === 'details') && - onTableOptionChange - ) { - return ( - - - - - - ); - } - - return ( - - {typeof column.label === 'function' ? column.label() : column.label} - - ); - }) - } - - - {children} -
-
- ); -} - -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; diff --git a/frontend/src/Components/Table/Table.tsx b/frontend/src/Components/Table/Table.tsx new file mode 100644 index 000000000..c72b68b96 --- /dev/null +++ b/frontend/src/Components/Table/Table.tsx @@ -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 ( + + + + {selectAll && onSelectAllChange ? ( + + ) : null} + + {columns.map((column) => { + const { name, isVisible, isSortable, ...otherColumnProps } = column; + + if (!isVisible) { + return null; + } + + if ( + (name === 'actions' || name === 'details') && + onTableOptionChange + ) { + return ( + + + + + + ); + } + + return ( + + {typeof column.label === 'function' + ? column.label() + : column.label} + + ); + })} + + {children} +
+
+ ); +} + +export default Table; diff --git a/frontend/src/Components/Table/TableBody.js b/frontend/src/Components/Table/TableBody.js deleted file mode 100644 index 5cc60d6f4..000000000 --- a/frontend/src/Components/Table/TableBody.js +++ /dev/null @@ -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 ( - {children} - ); - } - -} - -TableBody.propTypes = { - children: PropTypes.node -}; - -export default TableBody; diff --git a/frontend/src/Components/Table/TableBody.tsx b/frontend/src/Components/Table/TableBody.tsx new file mode 100644 index 000000000..3bd267d5d --- /dev/null +++ b/frontend/src/Components/Table/TableBody.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +interface TableBodyProps { + children?: React.ReactNode; +} + +function TableBody({ children }: TableBodyProps) { + return {children}; +} + +export default TableBody; diff --git a/frontend/src/Components/Table/TableHeader.js b/frontend/src/Components/Table/TableHeader.js deleted file mode 100644 index 81943e919..000000000 --- a/frontend/src/Components/Table/TableHeader.js +++ /dev/null @@ -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 ( - - - {children} - - - ); - } -} - -TableHeader.propTypes = { - children: PropTypes.node -}; - -export default TableHeader; diff --git a/frontend/src/Components/Table/TableHeader.tsx b/frontend/src/Components/Table/TableHeader.tsx new file mode 100644 index 000000000..ad00b1d60 --- /dev/null +++ b/frontend/src/Components/Table/TableHeader.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +interface TableHeaderProps { + children?: React.ReactNode; +} + +function TableHeader({ children }: TableHeaderProps) { + return ( + + {children} + + ); +} + +export default TableHeader; diff --git a/frontend/src/Components/Table/TableHeaderCell.js b/frontend/src/Components/Table/TableHeaderCell.js deleted file mode 100644 index b0ed5c571..000000000 --- a/frontend/src/Components/Table/TableHeaderCell.js +++ /dev/null @@ -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 ? - - {children} - - { - isSorting && - - } - : - - - {children} - - ); - } -} - -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; diff --git a/frontend/src/Components/Table/TableHeaderCell.tsx b/frontend/src/Components/Table/TableHeaderCell.tsx new file mode 100644 index 000000000..13b8cf0f7 --- /dev/null +++ b/frontend/src/Components/Table/TableHeaderCell.tsx @@ -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 ? ( + + {children} + + {isSorting && } + + ) : ( + {children} + ); +} + +export default TableHeaderCell; diff --git a/frontend/src/Components/Table/TablePager.js b/frontend/src/Components/Table/TablePager.js deleted file mode 100644 index d58824169..000000000 --- a/frontend/src/Components/Table/TablePager.js +++ /dev/null @@ -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 ( -
-
- { - isFetching && - - } -
- -
-
- - - - - - - - -
- { - !isShowingPageSelect && - - {page} / {totalPages} - - } - - { - isShowingPageSelect && - - } -
- - - - - - - - -
-
- -
-
- {translate('TotalRecords', { totalRecords })} -
-
-
- ); - } - -} - -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; diff --git a/frontend/src/Components/Table/TablePager.tsx b/frontend/src/Components/Table/TablePager.tsx new file mode 100644 index 000000000..d21833de1 --- /dev/null +++ b/frontend/src/Components/Table/TablePager.tsx @@ -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) => { + setIsShowingPageSelect(false); + onPageSelect(value); + }, + [onPageSelect] + ); + + const handlePageSelectBlur = useCallback(() => { + setIsShowingPageSelect(false); + }, []); + + if (!page) { + return null; + } + + return ( +
+
+ {isFetching ? ( + + ) : null} +
+ +
+
+ + + + + + + + +
+ {isShowingPageSelect ? null : ( + + {page} / {totalPages} + + )} + + {isShowingPageSelect ? ( + + ) : null} +
+ + + + + + + + +
+
+ +
+
+ {translate('TotalRecords', { totalRecords })} +
+
+
+ ); +} + +export default TablePager; diff --git a/frontend/src/Components/Table/TableRow.js b/frontend/src/Components/Table/TableRow.js deleted file mode 100644 index c76083183..000000000 --- a/frontend/src/Components/Table/TableRow.js +++ /dev/null @@ -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 ( - - {children} - - ); -} - -TableRow.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.node, - overlayContent: PropTypes.bool -}; - -TableRow.defaultProps = { - className: styles.row -}; - -export default TableRow; diff --git a/frontend/src/Components/Table/TableRow.tsx b/frontend/src/Components/Table/TableRow.tsx new file mode 100644 index 000000000..3898da853 --- /dev/null +++ b/frontend/src/Components/Table/TableRow.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import styles from './TableRow.css'; + +interface TableRowProps extends React.HTMLAttributes { + className?: string; + children?: React.ReactNode; + overlayContent?: boolean; +} + +function TableRow({ + className = styles.row, + children, + overlayContent, + ...otherProps +}: TableRowProps) { + return ( + + {children} + + ); +} + +export default TableRow; diff --git a/frontend/src/Components/Table/TableRowButton.js b/frontend/src/Components/Table/TableRowButton.js deleted file mode 100644 index 7ff679673..000000000 --- a/frontend/src/Components/Table/TableRowButton.js +++ /dev/null @@ -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 ( - - ); -} - -export default TableRowButton; diff --git a/frontend/src/Components/Table/TableRowButton.tsx b/frontend/src/Components/Table/TableRowButton.tsx new file mode 100644 index 000000000..15aeac2a8 --- /dev/null +++ b/frontend/src/Components/Table/TableRowButton.tsx @@ -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 ; +} + +export default TableRowButton; diff --git a/frontend/src/Components/Table/TableSelectAllHeaderCell.js b/frontend/src/Components/Table/TableSelectAllHeaderCell.js deleted file mode 100644 index c889c32ae..000000000 --- a/frontend/src/Components/Table/TableSelectAllHeaderCell.js +++ /dev/null @@ -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 ( - - - - ); -} - -TableSelectAllHeaderCell.propTypes = { - allSelected: PropTypes.bool.isRequired, - allUnselected: PropTypes.bool.isRequired, - onSelectAllChange: PropTypes.func.isRequired -}; - -export default TableSelectAllHeaderCell; diff --git a/frontend/src/Components/Table/TableSelectAllHeaderCell.tsx b/frontend/src/Components/Table/TableSelectAllHeaderCell.tsx new file mode 100644 index 000000000..418d3adce --- /dev/null +++ b/frontend/src/Components/Table/TableSelectAllHeaderCell.tsx @@ -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 ( + + + + ); +} + +export default TableSelectAllHeaderCell; diff --git a/frontend/src/Components/Table/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js deleted file mode 100644 index 5473413cb..000000000 --- a/frontend/src/Components/Table/VirtualTable.js +++ /dev/null @@ -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 ( - - {({ height, registerChild, onChildScroll, scrollTop }) => { - if (!height) { - return null; - } - return ( - - - {header} -
- -
-
-
- ); - } - } -
- ); - } -} - -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; diff --git a/frontend/src/Components/Table/VirtualTable.tsx b/frontend/src/Components/Table/VirtualTable.tsx new file mode 100644 index 000000000..362be113b --- /dev/null +++ b/frontend/src/Components/Table/VirtualTable.tsx @@ -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 { + 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({ + isSmallScreen, + className = styles.tableContainer, + items, + scroller, + scrollIndex, + scrollTop, + header, + headerHeight = 38, + rowHeight = ROW_HEIGHT, + rowRenderer, + ...otherProps +}: VirtualTableProps) { + const [measureRef, bounds] = useMeasure(); + const gridRef = useRef(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 ( + + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return null; + } + return ( +
+ + {header} + + {/* @ts-expect-error - ref type is incompatible */} +
+ +
+
+
+ ); + }} +
+ ); +} + +export default VirtualTable; diff --git a/frontend/src/Components/Table/VirtualTableHeader.js b/frontend/src/Components/Table/VirtualTableHeader.js deleted file mode 100644 index cf6a0f47b..000000000 --- a/frontend/src/Components/Table/VirtualTableHeader.js +++ /dev/null @@ -1,17 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './VirtualTableHeader.css'; - -function VirtualTableHeader({ children }) { - return ( -
- {children} -
- ); -} - -VirtualTableHeader.propTypes = { - children: PropTypes.node -}; - -export default VirtualTableHeader; diff --git a/frontend/src/Components/Table/VirtualTableHeader.tsx b/frontend/src/Components/Table/VirtualTableHeader.tsx new file mode 100644 index 000000000..5e9db83dc --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeader.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './VirtualTableHeader.css'; + +interface VirtualTableHeaderProps { + children?: React.ReactNode; +} + +function VirtualTableHeader({ children }: VirtualTableHeaderProps) { + return
{children}
; +} + +export default VirtualTableHeader; diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.js b/frontend/src/Components/Table/VirtualTableHeaderCell.js deleted file mode 100644 index 55c688d01..000000000 --- a/frontend/src/Components/Table/VirtualTableHeaderCell.js +++ /dev/null @@ -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 - - {label} - - ); -} - -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 ? - - {children} - - { - isSorting && - - } - : - -
- {children} -
- ); - } -} - -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; diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.tsx b/frontend/src/Components/Table/VirtualTableHeaderCell.tsx new file mode 100644 index 000000000..fdaa53612 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeaderCell.tsx @@ -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 ? ( + + {children} + + {isSorting ? : null} + + ) : ( +
{children}
+ ); +} + +export default VirtualTableHeaderCell; diff --git a/frontend/src/Components/Table/VirtualTableRow.js b/frontend/src/Components/Table/VirtualTableRow.js deleted file mode 100644 index 0a423902e..000000000 --- a/frontend/src/Components/Table/VirtualTableRow.js +++ /dev/null @@ -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 ( -
- {children} -
- ); -} - -VirtualTableRow.propTypes = { - className: PropTypes.string.isRequired, - style: PropTypes.object.isRequired, - children: PropTypes.node -}; - -VirtualTableRow.defaultProps = { - className: styles.row -}; - -export default VirtualTableRow; diff --git a/frontend/src/Components/Table/VirtualTableRow.tsx b/frontend/src/Components/Table/VirtualTableRow.tsx new file mode 100644 index 000000000..dcdb3da4f --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableRow.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import styles from './VirtualTableRow.css'; + +interface VirtualTableRowProps extends React.HTMLAttributes { + className: string; + style: object; + children?: React.ReactNode; +} + +function VirtualTableRow({ + className = styles.row, + children, + style, + ...otherProps +}: VirtualTableRowProps) { + return ( +
+ {children} +
+ ); +} + +export default VirtualTableRow; diff --git a/frontend/src/Components/Table/VirtualTableRowButton.js b/frontend/src/Components/Table/VirtualTableRowButton.js deleted file mode 100644 index ba63c1648..000000000 --- a/frontend/src/Components/Table/VirtualTableRowButton.js +++ /dev/null @@ -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 ( - - ); -} - -export default VirtualTableRowButton; diff --git a/frontend/src/Components/Table/VirtualTableRowButton.tsx b/frontend/src/Components/Table/VirtualTableRowButton.tsx new file mode 100644 index 000000000..6dde9dc41 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableRowButton.tsx @@ -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 ; +} + +export default VirtualTableRowButton; diff --git a/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js deleted file mode 100644 index 58b246763..000000000 --- a/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js +++ /dev/null @@ -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 ( - - - - ); -} - -VirtualTableSelectAllHeaderCell.propTypes = { - allSelected: PropTypes.bool.isRequired, - allUnselected: PropTypes.bool.isRequired, - onSelectAllChange: PropTypes.func.isRequired -}; - -export default VirtualTableSelectAllHeaderCell; diff --git a/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.tsx b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.tsx new file mode 100644 index 000000000..be91ef58f --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.tsx @@ -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 ( + + + + ); +} + +export default VirtualTableSelectAllHeaderCell; diff --git a/frontend/src/Helpers/Hooks/useSelectState.tsx b/frontend/src/Helpers/Hooks/useSelectState.tsx index 8fb96e42a..4e1038bb6 100644 --- a/frontend/src/Helpers/Hooks/useSelectState.tsx +++ b/frontend/src/Helpers/Hooks/useSelectState.tsx @@ -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; +export type SelectedState = Record; 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'; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx index 1e0143b40..1e5d45a09 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx @@ -129,7 +129,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { ); const onSortPress = useCallback( - (newSortKey: string, newSortDirection: SortDirection) => { + (newSortKey: string, newSortDirection?: SortDirection) => { dispatch( setEpisodesSort({ sortKey: newSortKey, diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx index 01b4e4bff..4e17acece 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx @@ -10,6 +10,7 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; +import Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { icons, kinds, sizes } from 'Helpers/Props'; @@ -20,29 +21,34 @@ import FavoriteFolderRow from './FavoriteFolderRow'; import RecentFolderRow from './RecentFolderRow'; import styles from './InteractiveImportSelectFolderModalContent.css'; -const favoriteFoldersColumns = [ +const favoriteFoldersColumns: Column[] = [ { name: 'folder', label: () => translate('Folder'), + isVisible: true, }, { name: 'actions', label: '', + isVisible: true, }, ]; -const recentFoldersColumns = [ +const recentFoldersColumns: Column[] = [ { name: 'folder', label: () => translate('Folder'), + isVisible: true, }, { name: 'lastUsed', label: () => translate('LastUsed'), + isVisible: true, }, { name: 'actions', label: '', + isVisible: true, }, ]; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index 2b37ca34b..922f4d5ac 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -58,7 +58,7 @@ import { } from 'Store/Actions/interactiveImportActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { SortCallback } from 'typings/callbacks'; -import { SelectStateInputProps } from 'typings/props'; +import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import translate from 'Utilities/String/translate'; @@ -431,7 +431,7 @@ function InteractiveImportModalContent( }, [previousIsDeleting, isDeleting, deleteError, onModalClose]); const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { + ({ value }: CheckInputChanged) => { setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); }, [items, setSelectState] @@ -449,8 +449,8 @@ function InteractiveImportModalContent( setWithoutEpisodeFileIdRowsSelected( hasEpisodeFileId || !value - ? without(withoutEpisodeFileIdRowsSelected, id) - : [...withoutEpisodeFileIdRowsSelected, id] + ? without(withoutEpisodeFileIdRowsSelected, id as number) + : [...withoutEpisodeFileIdRowsSelected, id as number] ); }, [ diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 1ea5be1f7..de2514a59 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -169,8 +169,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { onValidRowChange, ]); - const onSelectedChangeWrapper = useCallback( - (result: SelectedChangeProps) => { + const handleSelectedChange = useCallback( + (result: SelectStateInputProps) => { onSelectedChange({ ...result, hasEpisodeFileId: !!episodeFileId, @@ -398,7 +398,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.tsx b/frontend/src/InteractiveSearch/InteractiveSearch.tsx index 73163cab0..c92615547 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearch.tsx @@ -147,7 +147,7 @@ function InteractiveSearch({ type, searchPayload }: InteractiveSearchProps) { ); const handleSortPress = useCallback( - (sortKey: string, sortDirection: SortDirection) => { + (sortKey: string, sortDirection?: SortDirection) => { dispatch(setReleasesSort({ sortKey, sortDirection })); }, [dispatch] diff --git a/frontend/src/RootFolder/RootFolders.tsx b/frontend/src/RootFolder/RootFolders.tsx index 3e21845a7..3c90d8d1d 100644 --- a/frontend/src/RootFolder/RootFolders.tsx +++ b/frontend/src/RootFolder/RootFolders.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import Alert from 'Components/Alert'; 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 { kinds } from 'Helpers/Props'; @@ -10,7 +11,7 @@ import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector import translate from 'Utilities/String/translate'; import RootFolderRow from './RootFolderRow'; -const rootFolderColumns = [ +const rootFolderColumns: Column[] = [ { name: 'path', label: () => translate('Path'), @@ -28,6 +29,7 @@ const rootFolderColumns = [ }, { name: 'actions', + label: '', isVisible: true, }, ]; diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx index f7857017b..49dace806 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx @@ -8,11 +8,11 @@ import HeartRating from 'Components/HeartRating'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import SeriesTagList from 'Components/SeriesTagList'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import Column from 'Components/Table/Column'; -import SeriesTagList from 'Components/SeriesTagList'; import { icons } from 'Helpers/Props'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModal from 'Series/Edit/EditSeriesModal'; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx index 891c9941f..c89b936c0 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx @@ -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'; @@ -135,7 +135,7 @@ function ManageCustomFormatsModalContent( ); const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { + ({ value }: CheckInputChanged) => { setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); }, [items, setSelectState] diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index 1d078b5b2..5c96aee66 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -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'; @@ -187,7 +187,7 @@ function ManageDownloadClientsModalContent( ); const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { + ({ value }: CheckInputChanged) => { setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); }, [items, setSelectState] diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx index e51ef316f..f8b02e427 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -19,6 +19,7 @@ import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import usePrevious from 'Helpers/Hooks/usePrevious'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { icons, kinds } from 'Helpers/Props'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { bulkDeleteImportListExclusions, clearImportListExclusions, @@ -150,8 +151,8 @@ function ImportListExclusions() { }); const handleSortPress = useCallback( - (sortKey: { sortKey: string }) => { - dispatch(setImportListExclusionSort({ sortKey })); + (sortKey: string, sortDirection?: SortDirection) => { + dispatch(setImportListExclusionSort({ sortKey, sortDirection })); }, [dispatch] ); diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index 4d7ce02a6..cac00e259 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -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] diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index a79143c99..18e20a6e5 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -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'; @@ -185,7 +185,7 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { ); const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { + ({ value }: CheckInputChanged) => { setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); }, [items, setSelectState] diff --git a/frontend/src/System/Events/LogsTableRow.tsx b/frontend/src/System/Events/LogsTableRow.tsx index a4e539c67..350a24b95 100644 --- a/frontend/src/System/Events/LogsTableRow.tsx +++ b/frontend/src/System/Events/LogsTableRow.tsx @@ -54,7 +54,7 @@ function LogsTableRow({ }, []); return ( - + {columns.map((column) => { const { name, isVisible } = column; diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx index e79deed7c..ec4438b00 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx @@ -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, }, ]; diff --git a/frontend/src/Utilities/Table/getToggledRange.ts b/frontend/src/Utilities/Table/getToggledRange.ts index 59a098e17..34c2648c9 100644 --- a/frontend/src/Utilities/Table/getToggledRange.ts +++ b/frontend/src/Utilities/Table/getToggledRange.ts @@ -2,8 +2,8 @@ import ModelBase from 'App/ModelBase'; function getToggledRange( items: T[], - id: number, - lastToggled: number + id: number | string, + lastToggled: number | string ) { const lastToggledIndex = items.findIndex((item) => item.id === lastToggled); const changedIndex = items.findIndex((item) => item.id === id); diff --git a/frontend/src/Utilities/Table/toggleSelected.ts b/frontend/src/Utilities/Table/toggleSelected.ts index e3510572a..df51ddb25 100644 --- a/frontend/src/Utilities/Table/toggleSelected.ts +++ b/frontend/src/Utilities/Table/toggleSelected.ts @@ -6,25 +6,26 @@ import getToggledRange from './getToggledRange'; function toggleSelected( selectState: SelectState, items: T[], - id: number, - selected: boolean, + id: number | string, + selected: boolean | null, shiftKey: boolean ) { const lastToggled = selectState.lastToggled; const nextSelectedState = { ...selectState.selectedState, - [id]: selected, }; if (selected == null) { delete nextSelectedState[id]; - } + } else { + nextSelectedState[id] = selected; - if (shiftKey && lastToggled) { - const { lower, upper } = getToggledRange(items, id, lastToggled); + if (shiftKey && lastToggled) { + const { lower, upper } = getToggledRange(items, id, lastToggled); - for (let i = lower; i < upper; i++) { - nextSelectedState[items[i].id] = selected; + for (let i = lower; i < upper; i++) { + nextSelectedState[items[i].id] = selected; + } } } diff --git a/frontend/src/typings/callbacks.ts b/frontend/src/typings/callbacks.ts index 2f8f6a36d..464005351 100644 --- a/frontend/src/typings/callbacks.ts +++ b/frontend/src/typings/callbacks.ts @@ -2,5 +2,5 @@ import { SortDirection } from 'Helpers/Props/sortDirections'; export type SortCallback = ( sortKey: string, - sortDirection: SortDirection + sortDirection?: SortDirection ) => void; diff --git a/frontend/src/typings/props.ts b/frontend/src/typings/props.ts index 5b87e36b3..c1c025fac 100644 --- a/frontend/src/typings/props.ts +++ b/frontend/src/typings/props.ts @@ -1,5 +1,5 @@ export interface SelectStateInputProps { - id: number; - value: boolean; + id: number | string; + value: boolean | null; shiftKey: boolean; } diff --git a/package.json b/package.json index 30df90a47..ae50ea367 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@types/react-router-dom": "5.3.3", "@types/react-slider": "1.3.6", "@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", diff --git a/yarn.lock b/yarn.lock index a8ebf899c..d44a954e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1479,6 +1479,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"