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/Artist/Index/Table/ArtistIndexRow.tsx b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx index 2f62b97ce..e9e4c4aa5 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx @@ -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, diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx b/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx index c3c8044ce..9b3038a9b 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.tsx @@ -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 (
- + = + ComponentPropsWithoutRef & { + component?: C; + to?: string; + target?: string; + isDisabled?: LinkProps['disabled']; + noRouter?: boolean; + onPress?(event: SyntheticEvent): void; + }; -export interface LinkProps extends React.HTMLProps { - className?: string; - component?: - | string - | FunctionComponent - | ComponentClass; - 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({ + className, + component, + to, + target, + type, + isDisabled, + noRouter, + onPress, + ...otherProps +}: LinkProps) { + 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 & 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 ( + + ); + } - return React.createElement(el, elementProps); + return ( + + ); + } + + return ( + + ); } - -export default Link; diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index ce9b0e7e4..2f2850c62 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -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} >
{children}
diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx index 37b16eebd..b2780551a 100644 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ b/frontend/src/Components/Scroller/Scroller.tsx @@ -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) ?? internalRef; + const internalRef = useRef(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 (
- ); - } - - return ( - - {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })} - - ); - } -} - -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; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.tsx b/frontend/src/Components/Table/Cells/RelativeDateCell.tsx new file mode 100644 index 000000000..c694c17e2 --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.tsx @@ -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 ; + } + + return ( + + {getRelativeDate(date, shortDateFormat, showRelativeDates, { + timeFormat, + includeSeconds, + timeForToday: true, + })} + + ); +} + +export default RelativeDateCell; diff --git a/frontend/src/Components/Table/Cells/TableRowCell.js b/frontend/src/Components/Table/Cells/TableRowCell.js deleted file mode 100644 index f66bbf3aa..000000000 --- a/frontend/src/Components/Table/Cells/TableRowCell.js +++ /dev/null @@ -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 ( - - {children} - - ); - } -} - -TableRowCell.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) -}; - -TableRowCell.defaultProps = { - className: styles.cell -}; - -export default TableRowCell; diff --git a/frontend/src/Components/Table/Cells/TableRowCell.tsx b/frontend/src/Components/Table/Cells/TableRowCell.tsx new file mode 100644 index 000000000..aed693222 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCell.tsx @@ -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 ; +} diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.js b/frontend/src/Components/Table/Cells/TableRowCellButton.js deleted file mode 100644 index ff50d3bc9..000000000 --- a/frontend/src/Components/Table/Cells/TableRowCellButton.js +++ /dev/null @@ -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 ( - - ); -} - -TableRowCellButton.propTypes = { - className: PropTypes.string.isRequired -}; - -TableRowCellButton.defaultProps = { - className: styles.cell -}; - -export default TableRowCellButton; diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.tsx b/frontend/src/Components/Table/Cells/TableRowCellButton.tsx new file mode 100644 index 000000000..c80a3d626 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCellButton.tsx @@ -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 ( + + ); +} + +export default TableRowCellButton; 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 02ed71fc6..000000000 --- a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js +++ /dev/null @@ -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 - - ); -} - -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 ( - - - - ); - } -} - -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; diff --git a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.tsx b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.tsx new file mode 100644 index 000000000..924ed08ad --- /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 | 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 ( + + + + ); +} + +export default VirtualTableSelectCell; diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index 31a696df7..22d22e963 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -1,12 +1,16 @@ import React from 'react'; +import { SortDirection } from 'Helpers/Props/sortDirections'; type PropertyFunction = () => T; +// TODO: Convert to generic so `name` can be a type interface Column { name: string; label: string | PropertyFunction | React.ReactNode; + className?: string; columnLabel?: string; isSortable?: boolean; + fixedSortDirection?: SortDirection; isVisible: boolean; isModifiable?: boolean; } diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js deleted file mode 100644 index 8afbf9ea0..000000000 --- a/frontend/src/Components/Table/Table.js +++ /dev/null @@ -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 ( - - - - { - selectAll ? - : - null - } - - { - columns.map((column) => { - const { - name, - isVisible - } = 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..4f770f8cb --- /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..f89ee6bcc --- /dev/null +++ b/frontend/src/Components/Table/TableHeaderCell.tsx @@ -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 ? ( + + {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 6f71ee5d2..000000000 --- a/frontend/src/Components/Table/TablePager.js +++ /dev/null @@ -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 ( -
-
- { - isFetching && - - } -
- -
-
- - - - - - - - -
- { - !isShowingPageSelect && - - {page} / {totalPages} - - } - - { - isShowingPageSelect && - - } -
- - - - - - - - -
-
- -
-
- Total records: {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..53054d5c1 --- /dev/null +++ b/frontend/src/Components/Table/TableRow.tsx @@ -0,0 +1,22 @@ +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, + ...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/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/Helpers/Props/ScrollDirection.ts b/frontend/src/Helpers/Props/ScrollDirection.ts deleted file mode 100644 index 0da932d22..000000000 --- a/frontend/src/Helpers/Props/ScrollDirection.ts +++ /dev/null @@ -1,8 +0,0 @@ -enum ScrollDirection { - Horizontal = 'horizontal', - Vertical = 'vertical', - None = 'none', - Both = 'both', -} - -export default ScrollDirection; diff --git a/frontend/src/Helpers/Props/scrollDirections.js b/frontend/src/Helpers/Props/scrollDirections.ts similarity index 71% rename from frontend/src/Helpers/Props/scrollDirections.js rename to frontend/src/Helpers/Props/scrollDirections.ts index 1ae61143b..e82fdfae6 100644 --- a/frontend/src/Helpers/Props/scrollDirections.js +++ b/frontend/src/Helpers/Props/scrollDirections.ts @@ -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'; diff --git a/frontend/src/Helpers/Props/sortDirections.js b/frontend/src/Helpers/Props/sortDirections.ts similarity index 68% rename from frontend/src/Helpers/Props/sortDirections.js rename to frontend/src/Helpers/Props/sortDirections.ts index ff3b17bb6..f082cfa59 100644 --- a/frontend/src/Helpers/Props/sortDirections.js +++ b/frontend/src/Helpers/Props/sortDirections.ts @@ -2,3 +2,5 @@ export const ASCENDING = 'ascending'; export const DESCENDING = 'descending'; export const all = [ASCENDING, DESCENDING]; + +export type SortDirection = 'ascending' | 'descending'; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx index aabaf67c1..db7ca8fc9 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'; @@ -133,7 +133,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 b2c1208cb..43c40a912 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'; @@ -185,7 +185,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/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index 4fee485c9..36a06cbee 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 997d1b566..f14723939 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'; @@ -183,7 +183,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/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/typings/Table.ts b/frontend/src/typings/Table.ts new file mode 100644 index 000000000..63c079612 --- /dev/null +++ b/frontend/src/typings/Table.ts @@ -0,0 +1,6 @@ +import Column from 'Components/Table/Column'; + +export interface TableOptionsChangePayload { + pageSize?: number; + columns?: Column[]; +} diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts index 7d202cd44..e218abbbd 100644 --- a/frontend/src/typings/inputs.ts +++ b/frontend/src/typings/inputs.ts @@ -5,7 +5,6 @@ export type InputChanged = { export type InputOnChange = (change: InputChanged) => void; -export type CheckInputChanged = { - name: string; - value: boolean; -}; +export interface CheckInputChanged extends InputChanged { + shiftKey: boolean; +} 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 8d8be81c1..e71db1711 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/yarn.lock b/yarn.lock index 1ff6e115e..0b1e764ae 100644 --- a/yarn.lock +++ b/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==