diff --git a/frontend/src/Activity/Queue/QueueStatus.tsx b/frontend/src/Activity/Queue/QueueStatus.tsx index 2bd7f6d79..31a28f35c 100644 --- a/frontend/src/Activity/Queue/QueueStatus.tsx +++ b/frontend/src/Activity/Queue/QueueStatus.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Icon, { IconProps } from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; import { icons, kinds } from 'Helpers/Props'; -import TooltipPosition from 'Helpers/Props/TooltipPosition'; +import { TooltipPosition } from 'Helpers/Props/tooltipPositions'; import { QueueTrackedDownloadState, QueueTrackedDownloadStatus, diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index f89eb25f7..27dc78aa1 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,5 +1,5 @@ import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import { FilterBuilderProp, PropertyFilter } from './AppState'; export interface Error { diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index a1011b6c1..f33fcc692 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -8,6 +8,7 @@ import MovieCreditAppState from './MovieCreditAppState'; import MovieFilesAppState from './MovieFilesAppState'; import MoviesAppState, { MovieIndexAppState } from './MoviesAppState'; import ParseAppState from './ParseAppState'; +import PathsAppState from './PathsAppState'; import QueueAppState from './QueueAppState'; import RootFolderAppState from './RootFolderAppState'; import SettingsAppState from './SettingsAppState'; @@ -71,6 +72,7 @@ interface AppState { movieIndex: MovieIndexAppState; movies: MoviesAppState; parse: ParseAppState; + paths: PathsAppState; queue: QueueAppState; rootFolders: RootFolderAppState; settings: SettingsAppState; diff --git a/frontend/src/App/State/MoviesAppState.ts b/frontend/src/App/State/MoviesAppState.ts index 20b706c24..13df31221 100644 --- a/frontend/src/App/State/MoviesAppState.ts +++ b/frontend/src/App/State/MoviesAppState.ts @@ -3,7 +3,7 @@ import AppSectionState, { AppSectionSaveState, } from 'App/State/AppSectionState'; import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import Movie from 'Movie/Movie'; import { Filter, FilterBuilderProp } from './AppState'; diff --git a/frontend/src/App/State/PathsAppState.ts b/frontend/src/App/State/PathsAppState.ts new file mode 100644 index 000000000..068a48dc0 --- /dev/null +++ b/frontend/src/App/State/PathsAppState.ts @@ -0,0 +1,29 @@ +interface BasePath { + name: string; + path: string; + size: number; + lastModified: string; +} + +interface File extends BasePath { + type: 'file'; +} + +interface Folder extends BasePath { + type: 'folder'; +} + +export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent'; +export type Path = File | Folder; + +interface PathsAppState { + currentPath: string; + isFetching: boolean; + isPopulated: boolean; + error: Error; + directories: Folder[]; + files: File[]; + parent: string | null; +} + +export default PathsAppState; diff --git a/frontend/src/Components/Alert.js b/frontend/src/Components/Alert.js deleted file mode 100644 index 418cbf5e6..000000000 --- a/frontend/src/Components/Alert.js +++ /dev/null @@ -1,34 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { kinds } from 'Helpers/Props'; -import styles from './Alert.css'; - -function Alert(props) { - const { className, kind, children, ...otherProps } = props; - - return ( -
- {children} -
- ); -} - -Alert.propTypes = { - className: PropTypes.string, - kind: PropTypes.oneOf(kinds.all), - children: PropTypes.node.isRequired -}; - -Alert.defaultProps = { - className: styles.alert, - kind: kinds.INFO -}; - -export default Alert; diff --git a/frontend/src/Components/Alert.tsx b/frontend/src/Components/Alert.tsx new file mode 100644 index 000000000..92c89e741 --- /dev/null +++ b/frontend/src/Components/Alert.tsx @@ -0,0 +1,18 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Kind } from 'Helpers/Props/kinds'; +import styles from './Alert.css'; + +interface AlertProps { + className?: string; + kind?: Extract; + children: React.ReactNode; +} + +function Alert(props: AlertProps) { + const { className = styles.alert, kind = 'info', children } = props; + + return
{children}
; +} + +export default Alert; diff --git a/frontend/src/Components/Card.js b/frontend/src/Components/Card.js deleted file mode 100644 index c5a4d164c..000000000 --- a/frontend/src/Components/Card.js +++ /dev/null @@ -1,60 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Link from 'Components/Link/Link'; -import styles from './Card.css'; - -class Card extends Component { - - // - // Render - - render() { - const { - className, - overlayClassName, - overlayContent, - children, - onPress - } = this.props; - - if (overlayContent) { - return ( -
- - -
- {children} -
-
- ); - } - - return ( - - {children} - - ); - } -} - -Card.propTypes = { - className: PropTypes.string.isRequired, - overlayClassName: PropTypes.string.isRequired, - overlayContent: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, - onPress: PropTypes.func.isRequired -}; - -Card.defaultProps = { - className: styles.card, - overlayClassName: styles.overlay, - overlayContent: false -}; - -export default Card; diff --git a/frontend/src/Components/Card.tsx b/frontend/src/Components/Card.tsx new file mode 100644 index 000000000..24588c841 --- /dev/null +++ b/frontend/src/Components/Card.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Link, { LinkProps } from 'Components/Link/Link'; +import styles from './Card.css'; + +interface CardProps extends Pick { + // TODO: Consider using different properties for classname depending if it's overlaying content or not + className?: string; + overlayClassName?: string; + overlayContent?: boolean; + children: React.ReactNode; +} + +function Card(props: CardProps) { + const { + className = styles.card, + overlayClassName = styles.overlay, + overlayContent = false, + children, + onPress, + } = props; + + if (overlayContent) { + return ( +
+ + +
{children}
+
+ ); + } + + return ( + + {children} + + ); +} + +export default Card; diff --git a/frontend/src/Components/DescriptionList/DescriptionList.js b/frontend/src/Components/DescriptionList/DescriptionList.js deleted file mode 100644 index be2c87c55..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionList.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './DescriptionList.css'; - -class DescriptionList extends Component { - - // - // Render - - render() { - const { - className, - children - } = this.props; - - return ( -
- {children} -
- ); - } -} - -DescriptionList.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.node -}; - -DescriptionList.defaultProps = { - className: styles.descriptionList -}; - -export default DescriptionList; diff --git a/frontend/src/Components/DescriptionList/DescriptionList.tsx b/frontend/src/Components/DescriptionList/DescriptionList.tsx new file mode 100644 index 000000000..6deee77e5 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionList.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './DescriptionList.css'; + +interface DescriptionListProps { + className?: string; + children?: React.ReactNode; +} + +function DescriptionList(props: DescriptionListProps) { + const { className = styles.descriptionList, children } = props; + + return
{children}
; +} + +export default DescriptionList; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js deleted file mode 100644 index 931557045..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionListItem.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import DescriptionListItemDescription from './DescriptionListItemDescription'; -import DescriptionListItemTitle from './DescriptionListItemTitle'; - -class DescriptionListItem extends Component { - - // - // Render - - render() { - const { - className, - titleClassName, - descriptionClassName, - title, - data - } = this.props; - - return ( -
- - {title} - - - - {data} - -
- ); - } -} - -DescriptionListItem.propTypes = { - className: PropTypes.string, - titleClassName: PropTypes.string, - descriptionClassName: PropTypes.string, - title: PropTypes.string, - data: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) -}; - -export default DescriptionListItem; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.tsx b/frontend/src/Components/DescriptionList/DescriptionListItem.tsx new file mode 100644 index 000000000..13a7efdd0 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import DescriptionListItemDescription, { + DescriptionListItemDescriptionProps, +} from './DescriptionListItemDescription'; +import DescriptionListItemTitle, { + DescriptionListItemTitleProps, +} from './DescriptionListItemTitle'; + +interface DescriptionListItemProps { + className?: string; + titleClassName?: DescriptionListItemTitleProps['className']; + descriptionClassName?: DescriptionListItemDescriptionProps['className']; + title?: DescriptionListItemTitleProps['children']; + data?: DescriptionListItemDescriptionProps['children']; +} + +function DescriptionListItem(props: DescriptionListItemProps) { + const { className, titleClassName, descriptionClassName, title, data } = + props; + + return ( +
+ + {title} + + + + {data} + +
+ ); +} + +export default DescriptionListItem; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js deleted file mode 100644 index 4ef3c015e..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './DescriptionListItemDescription.css'; - -function DescriptionListItemDescription(props) { - const { - className, - children - } = props; - - return ( -
- {children} -
- ); -} - -DescriptionListItemDescription.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) -}; - -DescriptionListItemDescription.defaultProps = { - className: styles.description -}; - -export default DescriptionListItemDescription; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.tsx b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.tsx new file mode 100644 index 000000000..e08c117dc --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; +import styles from './DescriptionListItemDescription.css'; + +export interface DescriptionListItemDescriptionProps { + className?: string; + children?: ReactNode; +} + +function DescriptionListItemDescription( + props: DescriptionListItemDescriptionProps +) { + const { className = styles.description, children } = props; + + return
{children}
; +} + +export default DescriptionListItemDescription; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js deleted file mode 100644 index e1632c1cf..000000000 --- a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './DescriptionListItemTitle.css'; - -function DescriptionListItemTitle(props) { - const { - className, - children - } = props; - - return ( -
- {children} -
- ); -} - -DescriptionListItemTitle.propTypes = { - className: PropTypes.string.isRequired, - children: PropTypes.string -}; - -DescriptionListItemTitle.defaultProps = { - className: styles.title -}; - -export default DescriptionListItemTitle; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.tsx b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.tsx new file mode 100644 index 000000000..59ea6955c --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.tsx @@ -0,0 +1,15 @@ +import React, { ReactNode } from 'react'; +import styles from './DescriptionListItemTitle.css'; + +export interface DescriptionListItemTitleProps { + className?: string; + children?: ReactNode; +} + +function DescriptionListItemTitle(props: DescriptionListItemTitleProps) { + const { className = styles.title, children } = props; + + return
{children}
; +} + +export default DescriptionListItemTitle; diff --git a/frontend/src/Components/DragPreviewLayer.js b/frontend/src/Components/DragPreviewLayer.js deleted file mode 100644 index a111df70e..000000000 --- a/frontend/src/Components/DragPreviewLayer.js +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './DragPreviewLayer.css'; - -function DragPreviewLayer({ children, ...otherProps }) { - return ( -
- {children} -
- ); -} - -DragPreviewLayer.propTypes = { - children: PropTypes.node, - className: PropTypes.string -}; - -DragPreviewLayer.defaultProps = { - className: styles.dragLayer -}; - -export default DragPreviewLayer; diff --git a/frontend/src/Components/DragPreviewLayer.tsx b/frontend/src/Components/DragPreviewLayer.tsx new file mode 100644 index 000000000..2e578504b --- /dev/null +++ b/frontend/src/Components/DragPreviewLayer.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styles from './DragPreviewLayer.css'; + +interface DragPreviewLayerProps { + className?: string; + children?: React.ReactNode; +} + +function DragPreviewLayer({ + className = styles.dragLayer, + children, + ...otherProps +}: DragPreviewLayerProps) { + return ( +
+ {children} +
+ ); +} + +export default DragPreviewLayer; diff --git a/frontend/src/Components/Error/ErrorBoundary.js b/frontend/src/Components/Error/ErrorBoundary.js deleted file mode 100644 index 88412ad19..000000000 --- a/frontend/src/Components/Error/ErrorBoundary.js +++ /dev/null @@ -1,62 +0,0 @@ -import * as sentry from '@sentry/browser'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - -class ErrorBoundary extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - error: null, - info: null - }; - } - - componentDidCatch(error, info) { - this.setState({ - error, - info - }); - - sentry.captureException(error); - } - - // - // Render - - render() { - const { - children, - errorComponent: ErrorComponent, - ...otherProps - } = this.props; - - const { - error, - info - } = this.state; - - if (error) { - return ( - - ); - } - - return children; - } -} - -ErrorBoundary.propTypes = { - children: PropTypes.node.isRequired, - errorComponent: PropTypes.elementType.isRequired -}; - -export default ErrorBoundary; diff --git a/frontend/src/Components/Error/ErrorBoundary.tsx b/frontend/src/Components/Error/ErrorBoundary.tsx new file mode 100644 index 000000000..6b27f7a09 --- /dev/null +++ b/frontend/src/Components/Error/ErrorBoundary.tsx @@ -0,0 +1,46 @@ +import * as sentry from '@sentry/browser'; +import React, { Component, ErrorInfo } from 'react'; + +interface ErrorBoundaryProps { + children: React.ReactNode; + errorComponent: React.ElementType; +} + +interface ErrorBoundaryState { + error: Error | null; + info: ErrorInfo | null; +} + +// Class component until componentDidCatch is supported in functional components +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + + this.state = { + error: null, + info: null, + }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + this.setState({ + error, + info, + }); + + sentry.captureException(error); + } + + render() { + const { children, errorComponent: ErrorComponent } = this.props; + const { error, info } = this.state; + + if (error) { + return ; + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/Components/FieldSet.js b/frontend/src/Components/FieldSet.js deleted file mode 100644 index 8243fd00c..000000000 --- a/frontend/src/Components/FieldSet.js +++ /dev/null @@ -1,41 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { sizes } from 'Helpers/Props'; -import styles from './FieldSet.css'; - -class FieldSet extends Component { - - // - // Render - - render() { - const { - size, - legend, - children - } = this.props; - - return ( -
- - {legend} - - {children} -
- ); - } - -} - -FieldSet.propTypes = { - size: PropTypes.oneOf(sizes.all).isRequired, - legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - children: PropTypes.node -}; - -FieldSet.defaultProps = { - size: sizes.MEDIUM -}; - -export default FieldSet; diff --git a/frontend/src/Components/FieldSet.tsx b/frontend/src/Components/FieldSet.tsx new file mode 100644 index 000000000..c2ff03a7f --- /dev/null +++ b/frontend/src/Components/FieldSet.tsx @@ -0,0 +1,29 @@ +import classNames from 'classnames'; +import React, { ComponentProps } from 'react'; +import { sizes } from 'Helpers/Props'; +import { Size } from 'Helpers/Props/sizes'; +import styles from './FieldSet.css'; + +interface FieldSetProps { + size?: Size; + legend?: ComponentProps<'legend'>['children']; + children?: React.ReactNode; +} + +function FieldSet({ size = sizes.MEDIUM, legend, children }: FieldSetProps) { + return ( +
+ + {legend} + + {children} +
+ ); +} + +export default FieldSet; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.js b/frontend/src/Components/FileBrowser/FileBrowserModal.js deleted file mode 100644 index 6b58dbb8c..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserModal.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Modal from 'Components/Modal/Modal'; -import FileBrowserModalContentConnector from './FileBrowserModalContentConnector'; -import styles from './FileBrowserModal.css'; - -class FileBrowserModal extends Component { - - // - // Render - - render() { - const { - isOpen, - onModalClose, - ...otherProps - } = this.props; - - return ( - - - - ); - } -} - -FileBrowserModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default FileBrowserModal; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.tsx b/frontend/src/Components/FileBrowser/FileBrowserModal.tsx new file mode 100644 index 000000000..0925890de --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModal.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import FileBrowserModalContent, { + FileBrowserModalContentProps, +} from './FileBrowserModalContent'; +import styles from './FileBrowserModal.css'; + +interface FileBrowserModalProps extends FileBrowserModalContentProps { + isOpen: boolean; + onModalClose: () => void; +} + +function FileBrowserModal(props: FileBrowserModalProps) { + const { isOpen, onModalClose, ...otherProps } = props; + + return ( + + + + ); +} + +export default FileBrowserModal; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js deleted file mode 100644 index 4241bdf6d..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ /dev/null @@ -1,250 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import PathInput from 'Components/Form/PathInput'; -import Button from 'Components/Link/Button'; -import Link from 'Components/Link/Link'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -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 Scroller from 'Components/Scroller/Scroller'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { kinds, scrollDirections } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import FileBrowserRow from './FileBrowserRow'; -import styles from './FileBrowserModalContent.css'; - -const columns = [ - { - name: 'type', - label: () => translate('Type'), - isVisible: true - }, - { - name: 'name', - label: () => translate('Name'), - isVisible: true - } -]; - -class FileBrowserModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scrollerRef = React.createRef(); - - this.state = { - isFileBrowserModalOpen: false, - currentPath: props.value - }; - } - - componentDidUpdate(prevProps, prevState) { - const { - currentPath - } = this.props; - - if ( - currentPath !== this.state.currentPath && - currentPath !== prevState.currentPath - ) { - this.setState({ currentPath }); - this._scrollerRef.current.scrollTop = 0; - } - } - - // - // Listeners - - onPathInputChange = ({ value }) => { - this.setState({ currentPath: value }); - }; - - onRowPress = (path) => { - this.props.onFetchPaths(path); - }; - - onOkPress = () => { - this.props.onChange({ - name: this.props.name, - value: this.state.currentPath - }); - - this.props.onClearPaths(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - parent, - directories, - files, - isWindowsService, - onModalClose, - ...otherProps - } = this.props; - - const emptyParent = parent === ''; - - return ( - - - File Browser - - - - { - isWindowsService && - - - {translate('MappedDrivesRunningAsService')} - . - - } - - - - - { - !!error && -
- {translate('ErrorLoadingContents')} -
- } - - { - isPopulated && !error && - - - { - emptyParent && - - } - - { - !emptyParent && parent && - - } - - { - directories.map((directory) => { - return ( - - ); - }) - } - - { - files.map((file) => { - return ( - - ); - }) - } - -
- } -
-
- - - { - isFetching && - - } - - - - - -
- ); - } -} - -FileBrowserModalContent.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - parent: PropTypes.string, - currentPath: PropTypes.string.isRequired, - directories: PropTypes.arrayOf(PropTypes.object).isRequired, - files: PropTypes.arrayOf(PropTypes.object).isRequired, - isWindowsService: PropTypes.bool.isRequired, - onFetchPaths: PropTypes.func.isRequired, - onClearPaths: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default FileBrowserModalContent; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx new file mode 100644 index 000000000..03fd575fd --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx @@ -0,0 +1,237 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import PathInput from 'Components/Form/PathInput'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +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 Scroller from 'Components/Scroller/Scroller'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { kinds, scrollDirections } from 'Helpers/Props'; +import { clearPaths, fetchPaths } from 'Store/Actions/pathActions'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import createPathsSelector from './createPathsSelector'; +import FileBrowserRow from './FileBrowserRow'; +import styles from './FileBrowserModalContent.css'; + +const columns: Column[] = [ + { + name: 'type', + label: () => translate('Type'), + isVisible: true, + }, + { + name: 'name', + label: () => translate('Name'), + isVisible: true, + }, +]; + +const handleClearPaths = () => {}; + +export interface FileBrowserModalContentProps { + name: string; + value: string; + includeFiles?: boolean; + onChange: (args: InputChanged) => unknown; + onModalClose: () => void; +} + +function FileBrowserModalContent(props: FileBrowserModalContentProps) { + const { name, value, includeFiles = true, onChange, onModalClose } = props; + + const dispatch = useDispatch(); + + const { isWindows, mode } = useSelector(createSystemStatusSelector()); + const { isFetching, isPopulated, error, parent, directories, files, paths } = + useSelector(createPathsSelector()); + + const [currentPath, setCurrentPath] = useState(value); + const scrollerRef = useRef(null); + const previousValue = usePrevious(value); + + const emptyParent = parent === ''; + const isWindowsService = isWindows && mode === 'service'; + + const handlePathInputChange = useCallback( + ({ value }: InputChanged) => { + setCurrentPath(value); + }, + [] + ); + + const handleRowPress = useCallback( + (path: string) => { + setCurrentPath(path); + + dispatch( + fetchPaths({ + path, + allowFoldersWithoutTrailingSlashes: true, + includeFiles, + }) + ); + }, + [includeFiles, dispatch, setCurrentPath] + ); + + const handleOkPress = useCallback(() => { + onChange({ + name, + value: currentPath, + }); + + dispatch(clearPaths()); + onModalClose(); + }, [name, currentPath, dispatch, onChange, onModalClose]); + + const handleFetchPaths = useCallback( + (path: string) => { + dispatch( + fetchPaths({ + path, + allowFoldersWithoutTrailingSlashes: true, + includeFiles, + }) + ); + }, + [includeFiles, dispatch] + ); + + useEffect(() => { + if (value !== previousValue && value !== currentPath) { + setCurrentPath(value); + } + }, [value, previousValue, currentPath, setCurrentPath]); + + useEffect( + () => { + dispatch( + fetchPaths({ + path: currentPath, + allowFoldersWithoutTrailingSlashes: true, + includeFiles, + }) + ); + + return () => { + dispatch(clearPaths()); + }; + }, + // This should only run once when the component mounts, + // so we don't need to include the other dependencies. + // eslint-disable-next-line react-hooks/exhaustive-deps + [dispatch] + ); + + return ( + + {translate('FileBrowser')} + + + {isWindowsService ? ( + + + + ) : null} + + + + + {error ?
{translate('ErrorLoadingContents')}
: null} + + {isPopulated && !error ? ( + + + {emptyParent ? ( + + ) : null} + + {!emptyParent && parent ? ( + + ) : null} + + {directories.map((directory) => { + return ( + + ); + })} + + {files.map((file) => { + return ( + + ); + })} + +
+ ) : null} +
+
+ + + {isFetching ? ( + + ) : null} + + + + + +
+ ); +} + +export default FileBrowserModalContent; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js b/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js deleted file mode 100644 index 1a3a41ef0..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js +++ /dev/null @@ -1,119 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPaths, fetchPaths } from 'Store/Actions/pathActions'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import FileBrowserModalContent from './FileBrowserModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.paths, - createSystemStatusSelector(), - (paths, systemStatus) => { - const { - isFetching, - isPopulated, - error, - parent, - currentPath, - directories, - files - } = paths; - - const filteredPaths = _.filter([...directories, ...files], ({ path }) => { - return path.toLowerCase().startsWith(currentPath.toLowerCase()); - }); - - return { - isFetching, - isPopulated, - error, - parent, - currentPath, - directories, - files, - paths: filteredPaths, - isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service' - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchPaths: fetchPaths, - dispatchClearPaths: clearPaths -}; - -class FileBrowserModalContentConnector extends Component { - - // Lifecycle - - componentDidMount() { - const { - value, - includeFiles, - dispatchFetchPaths - } = this.props; - - dispatchFetchPaths({ - path: value, - allowFoldersWithoutTrailingSlashes: true, - includeFiles - }); - } - - // - // Listeners - - onFetchPaths = (path) => { - const { - includeFiles, - dispatchFetchPaths - } = this.props; - - dispatchFetchPaths({ - path, - allowFoldersWithoutTrailingSlashes: true, - includeFiles - }); - }; - - onClearPaths = () => { - // this.props.dispatchClearPaths(); - }; - - onModalClose = () => { - this.props.dispatchClearPaths(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -FileBrowserModalContentConnector.propTypes = { - value: PropTypes.string, - includeFiles: PropTypes.bool.isRequired, - dispatchFetchPaths: PropTypes.func.isRequired, - dispatchClearPaths: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -FileBrowserModalContentConnector.defaultProps = { - includeFiles: false -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector); diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.js b/frontend/src/Components/FileBrowser/FileBrowserRow.js deleted file mode 100644 index 06bb3029d..000000000 --- a/frontend/src/Components/FileBrowser/FileBrowserRow.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRowButton from 'Components/Table/TableRowButton'; -import { icons } from 'Helpers/Props'; -import styles from './FileBrowserRow.css'; - -function getIconName(type) { - switch (type) { - case 'computer': - return icons.COMPUTER; - case 'drive': - return icons.DRIVE; - case 'file': - return icons.FILE; - case 'parent': - return icons.PARENT; - default: - return icons.FOLDER; - } -} - -class FileBrowserRow extends Component { - - // - // Listeners - - onPress = () => { - this.props.onPress(this.props.path); - }; - - // - // Render - - render() { - const { - type, - name - } = this.props; - - return ( - - - - - - {name} - - ); - } - -} - -FileBrowserRow.propTypes = { - type: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired -}; - -export default FileBrowserRow; diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.tsx b/frontend/src/Components/FileBrowser/FileBrowserRow.tsx new file mode 100644 index 000000000..fe47f1664 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserRow.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from 'react'; +import { PathType } from 'App/State/PathsAppState'; +import Icon from 'Components/Icon'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import styles from './FileBrowserRow.css'; + +function getIconName(type: PathType) { + switch (type) { + case 'computer': + return icons.COMPUTER; + case 'drive': + return icons.DRIVE; + case 'file': + return icons.FILE; + case 'parent': + return icons.PARENT; + default: + return icons.FOLDER; + } +} + +interface FileBrowserRowProps { + type: PathType; + name: string; + path: string; + onPress: (path: string) => void; +} + +function FileBrowserRow(props: FileBrowserRowProps) { + const { type, name, path, onPress } = props; + + const handlePress = useCallback(() => { + onPress(path); + }, [path, onPress]); + + return ( + + + + + + {name} + + ); +} + +export default FileBrowserRow; diff --git a/frontend/src/Components/FileBrowser/createPathsSelector.ts b/frontend/src/Components/FileBrowser/createPathsSelector.ts new file mode 100644 index 000000000..5da830bd5 --- /dev/null +++ b/frontend/src/Components/FileBrowser/createPathsSelector.ts @@ -0,0 +1,36 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createPathsSelector() { + return createSelector( + (state: AppState) => state.paths, + (paths) => { + const { + isFetching, + isPopulated, + error, + parent, + currentPath, + directories, + files, + } = paths; + + const filteredPaths = [...directories, ...files].filter(({ path }) => { + return path.toLowerCase().startsWith(currentPath.toLowerCase()); + }); + + return { + isFetching, + isPopulated, + error, + parent, + currentPath, + directories, + files, + paths: filteredPaths, + }; + } + ); +} + +export default createPathsSelector; diff --git a/frontend/src/Components/Loading/LoadingIndicator.js b/frontend/src/Components/Loading/LoadingIndicator.js deleted file mode 100644 index ffed05b1b..000000000 --- a/frontend/src/Components/Loading/LoadingIndicator.js +++ /dev/null @@ -1,51 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import styles from './LoadingIndicator.css'; - -function LoadingIndicator({ className, rippleClassName, size }) { - const sizeInPx = `${size}px`; - const width = sizeInPx; - const height = sizeInPx; - - return ( -
-
-
- -
- -
-
-
- ); -} - -LoadingIndicator.propTypes = { - className: PropTypes.string, - rippleClassName: PropTypes.string, - size: PropTypes.number -}; - -LoadingIndicator.defaultProps = { - className: styles.loading, - rippleClassName: styles.ripple, - size: 50 -}; - -export default LoadingIndicator; diff --git a/frontend/src/Components/Loading/LoadingIndicator.tsx b/frontend/src/Components/Loading/LoadingIndicator.tsx new file mode 100644 index 000000000..06b5ecf3c --- /dev/null +++ b/frontend/src/Components/Loading/LoadingIndicator.tsx @@ -0,0 +1,36 @@ +import classNames from 'classnames'; +import React from 'react'; +import styles from './LoadingIndicator.css'; + +interface LoadingIndicatorProps { + className?: string; + rippleClassName?: string; + size?: number; +} + +function LoadingIndicator({ + className = styles.loading, + rippleClassName = styles.ripple, + size = 50, +}: LoadingIndicatorProps) { + const sizeInPx = `${size}px`; + const width = sizeInPx; + const height = sizeInPx; + + return ( +
+
+
+ +
+ +
+
+
+ ); +} + +export default LoadingIndicator; diff --git a/frontend/src/Components/Loading/LoadingMessage.js b/frontend/src/Components/Loading/LoadingMessage.tsx similarity index 63% rename from frontend/src/Components/Loading/LoadingMessage.js rename to frontend/src/Components/Loading/LoadingMessage.tsx index 5ab83c35f..3bc256a6e 100644 --- a/frontend/src/Components/Loading/LoadingMessage.js +++ b/frontend/src/Components/Loading/LoadingMessage.tsx @@ -8,21 +8,21 @@ const messages = [ 'Bleep Bloop.', 'Locating the required gigapixels to render...', 'Spinning up the hamster wheel...', - 'At least you\'re not on hold', + "At least you're not on hold", 'Hum something loud while others stare', 'Loading humorous message... Please Wait', - 'I could\'ve been faster in Python', - 'Don\'t forget to rewind your movies', + "I could've been faster in Python", + "Don't forget to rewind your movies", 'Congratulations! You are the 1000th visitor.', - 'HELP! I\'m being held hostage and forced to write these stupid lines!', + "HELP! I'm being held hostage and forced to write these stupid lines!", 'RE-calibrating the internet...', - 'I\'ll be here all week', - 'Don\'t forget to tip your waitress', + "I'll be here all week", + "Don't forget to tip your waitress", 'Apply directly to the forehead', - 'Loading Battlestation' + 'Loading Battlestation', ]; -let message = null; +let message: string | null = null; function LoadingMessage() { if (!message) { @@ -30,11 +30,7 @@ function LoadingMessage() { message = messages[index]; } - return ( -
- {message} -
- ); + return
{message}
; } export default LoadingMessage; diff --git a/frontend/src/Components/Markdown/InlineMarkdown.js b/frontend/src/Components/Markdown/InlineMarkdown.js deleted file mode 100644 index 993bb241e..000000000 --- a/frontend/src/Components/Markdown/InlineMarkdown.js +++ /dev/null @@ -1,74 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Link from 'Components/Link/Link'; - -class InlineMarkdown extends Component { - - // - // Render - - render() { - const { - className, - data, - blockClassName - } = this.props; - - // For now only replace links or code blocks (not both) - const markdownBlocks = []; - if (data) { - const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g); - - let endIndex = 0; - let match = null; - - while ((match = linkRegex.exec(data)) !== null) { - if (match.index > endIndex) { - markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); - } - - markdownBlocks.push({match[1]}); - endIndex = match.index + match[0].length; - } - - if (endIndex !== data.length && markdownBlocks.length > 0) { - markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); - } - - const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g); - - endIndex = 0; - match = null; - let matchedCode = false; - - while ((match = codeRegex.exec(data)) !== null) { - matchedCode = true; - - if (match.index > endIndex) { - markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); - } - - markdownBlocks.push({match[0].substring(1, match[0].length - 1)}); - endIndex = match.index + match[0].length; - } - - if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) { - markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); - } - - if (markdownBlocks.length === 0) { - markdownBlocks.push(data); - } - } - - return {markdownBlocks}; - } -} - -InlineMarkdown.propTypes = { - className: PropTypes.string, - data: PropTypes.string, - blockClassName: PropTypes.string -}; - -export default InlineMarkdown; diff --git a/frontend/src/Components/Markdown/InlineMarkdown.tsx b/frontend/src/Components/Markdown/InlineMarkdown.tsx new file mode 100644 index 000000000..80e99336a --- /dev/null +++ b/frontend/src/Components/Markdown/InlineMarkdown.tsx @@ -0,0 +1,75 @@ +import React, { ReactElement } from 'react'; +import Link from 'Components/Link/Link'; + +interface InlineMarkdownProps { + className?: string; + data?: string; + blockClassName?: string; +} + +function InlineMarkdown(props: InlineMarkdownProps) { + const { className, data, blockClassName } = props; + + // For now only replace links or code blocks (not both) + const markdownBlocks: (ReactElement | string)[] = []; + + if (data) { + const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g); + + let endIndex = 0; + let match = null; + + while ((match = linkRegex.exec(data)) !== null) { + if (match.index > endIndex) { + markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); + } + + markdownBlocks.push( + + {match[1]} + + ); + endIndex = match.index + match[0].length; + } + + if (endIndex !== data.length && markdownBlocks.length > 0) { + markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); + } + + const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g); + + endIndex = 0; + match = null; + let matchedCode = false; + + while ((match = codeRegex.exec(data)) !== null) { + matchedCode = true; + + if (match.index > endIndex) { + markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); + } + + markdownBlocks.push( + + {match[0].substring(1, match[0].length - 1)} + + ); + endIndex = match.index + match[0].length; + } + + if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) { + markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); + } + + if (markdownBlocks.length === 0) { + markdownBlocks.push(data); + } + } + + return {markdownBlocks}; +} + +export default InlineMarkdown; diff --git a/frontend/src/Components/MonitorToggleButton.js b/frontend/src/Components/MonitorToggleButton.js deleted file mode 100644 index 442010df8..000000000 --- a/frontend/src/Components/MonitorToggleButton.js +++ /dev/null @@ -1,79 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import { icons } from 'Helpers/Props'; -import styles from './MonitorToggleButton.css'; - -function getTooltip(monitored, isDisabled) { - if (isDisabled) { - return 'Cannot toggle monitored state when movie is unmonitored'; - } - - if (monitored) { - return 'Monitored, click to unmonitor'; - } - - return 'Unmonitored, click to monitor'; -} - -class MonitorToggleButton extends Component { - - // - // Listeners - - onPress = (event) => { - const shiftKey = event.nativeEvent.shiftKey; - - this.props.onPress(!this.props.monitored, { shiftKey }); - }; - - // - // Render - - render() { - const { - className, - monitored, - isDisabled, - isSaving, - size, - ...otherProps - } = this.props; - - const iconName = monitored ? icons.MONITORED : icons.UNMONITORED; - - return ( - - ); - } -} - -MonitorToggleButton.propTypes = { - className: PropTypes.string.isRequired, - monitored: PropTypes.bool.isRequired, - size: PropTypes.number, - isDisabled: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - onPress: PropTypes.func.isRequired -}; - -MonitorToggleButton.defaultProps = { - className: styles.toggleButton, - isDisabled: false, - isSaving: false -}; - -export default MonitorToggleButton; diff --git a/frontend/src/Components/MonitorToggleButton.tsx b/frontend/src/Components/MonitorToggleButton.tsx new file mode 100644 index 000000000..721bd87e1 --- /dev/null +++ b/frontend/src/Components/MonitorToggleButton.tsx @@ -0,0 +1,65 @@ +import classNames from 'classnames'; +import React, { SyntheticEvent, useCallback, useMemo } from 'react'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './MonitorToggleButton.css'; + +interface MonitorToggleButtonProps { + className?: string; + monitored: boolean; + size?: number; + isDisabled?: boolean; + isSaving?: boolean; + onPress: (value: boolean, options: { shiftKey: boolean }) => unknown; +} + +function MonitorToggleButton(props: MonitorToggleButtonProps) { + const { + className = styles.toggleButton, + monitored, + isDisabled = false, + isSaving = false, + size, + onPress, + ...otherProps + } = props; + + const iconName = monitored ? icons.MONITORED : icons.UNMONITORED; + + const title = useMemo(() => { + if (isDisabled) { + return 'Cannot toggle monitored state when movie is unmonitored'; + } + + if (monitored) { + return translate('ToggleMonitoredToUnmonitored'); + } + + return translate('ToggleUnmonitoredToMonitored'); + }, [monitored, isDisabled]); + + const handlePress = useCallback( + (event: SyntheticEvent) => { + const shiftKey = event.nativeEvent.shiftKey; + + onPress(!monitored, { shiftKey }); + }, + [monitored, onPress] + ); + + return ( + + ); +} + +export default MonitorToggleButton; diff --git a/frontend/src/Components/NotFound.js b/frontend/src/Components/NotFound.tsx similarity index 61% rename from frontend/src/Components/NotFound.js rename to frontend/src/Components/NotFound.tsx index cd424ce09..991fc91bb 100644 --- a/frontend/src/Components/NotFound.js +++ b/frontend/src/Components/NotFound.tsx @@ -1,16 +1,19 @@ -import PropTypes from 'prop-types'; import React from 'react'; import PageContent from 'Components/Page/PageContent'; import translate from 'Utilities/String/translate'; import styles from './NotFound.css'; -function NotFound({ message }) { +interface NotFoundProps { + message?: string; +} + +function NotFound(props: NotFoundProps) { + const { message = translate('DefaultNotFoundMessage') } = props; + return (
-
- {message} -
+
{message}
{children}
diff --git a/frontend/src/Components/Portal.js b/frontend/src/Components/Portal.js deleted file mode 100644 index 2e5237093..000000000 --- a/frontend/src/Components/Portal.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; - -function Portal(props) { - const { children, target } = props; - return ReactDOM.createPortal(children, target); -} - -Portal.propTypes = { - children: PropTypes.node.isRequired, - target: PropTypes.object.isRequired -}; - -Portal.defaultProps = { - target: document.getElementById('portal-root') -}; - -export default Portal; diff --git a/frontend/src/Components/Portal.tsx b/frontend/src/Components/Portal.tsx new file mode 100644 index 000000000..1cc1c7da6 --- /dev/null +++ b/frontend/src/Components/Portal.tsx @@ -0,0 +1,20 @@ +import ReactDOM from 'react-dom'; + +interface PortalProps { + children: Parameters[0]; + target?: Parameters[1]; +} + +const defaultTarget = document.getElementById('portal-root'); + +function Portal(props: PortalProps) { + const { children, target = defaultTarget } = props; + + if (!target) { + return null; + } + + return ReactDOM.createPortal(children, target); +} + +export default Portal; diff --git a/frontend/src/Components/Router/Switch.js b/frontend/src/Components/Router/Switch.js deleted file mode 100644 index 6479d5291..000000000 --- a/frontend/src/Components/Router/Switch.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Switch as RouterSwitch } from 'react-router-dom'; -import { map } from 'Helpers/elementChildren'; -import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; - -class Switch extends Component { - - // - // Render - - render() { - const { - children - } = this.props; - - return ( - - { - map(children, (child) => { - const { - path: childPath, - addUrlBase = true - } = child.props; - - if (!childPath) { - return child; - } - - const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath; - - return React.cloneElement(child, { path }); - }) - } - - ); - } -} - -Switch.propTypes = { - children: PropTypes.node.isRequired -}; - -export default Switch; diff --git a/frontend/src/Components/Router/Switch.tsx b/frontend/src/Components/Router/Switch.tsx new file mode 100644 index 000000000..032471681 --- /dev/null +++ b/frontend/src/Components/Router/Switch.tsx @@ -0,0 +1,38 @@ +import React, { Children, ReactElement, ReactNode } from 'react'; +import { Switch as RouterSwitch } from 'react-router-dom'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; + +interface ExtendedRoute { + path: string; + addUrlBase?: boolean; +} + +interface SwitchProps { + children: ReactNode; +} + +function Switch({ children }: SwitchProps) { + return ( + + {Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child; + } + + const elementChild: ReactElement = child; + + const { path: childPath, addUrlBase = true } = elementChild.props; + + if (!childPath) { + return child; + } + + const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath; + + return React.cloneElement(child, { path }); + })} + + ); +} + +export default Switch; diff --git a/frontend/src/Components/Scroller/OverlayScroller.js b/frontend/src/Components/Scroller/OverlayScroller.js deleted file mode 100644 index e590c42b2..000000000 --- a/frontend/src/Components/Scroller/OverlayScroller.js +++ /dev/null @@ -1,179 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Scrollbars } from 'react-custom-scrollbars-2'; -import { scrollDirections } from 'Helpers/Props'; -import styles from './OverlayScroller.css'; - -const SCROLLBAR_SIZE = 10; - -class OverlayScroller extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scroller = null; - this._isScrolling = false; - } - - componentDidUpdate(prevProps) { - const { - scrollTop - } = this.props; - - if ( - !this._isScrolling && - scrollTop != null && - scrollTop !== prevProps.scrollTop - ) { - this._scroller.scrollTop(scrollTop); - } - } - - // - // Control - - _setScrollRef = (ref) => { - this._scroller = ref; - - if (ref) { - this.props.registerScroller(ref.view); - } - }; - - _renderThumb = (props) => { - return ( -
- ); - }; - - _renderTrackHorizontal = ({ style, props }) => { - const finalStyle = { - ...style, - right: 2, - bottom: 2, - left: 2, - borderRadius: 3, - height: SCROLLBAR_SIZE - }; - - return ( -
- ); - }; - - _renderTrackVertical = ({ style, props }) => { - const finalStyle = { - ...style, - right: 2, - bottom: 2, - top: 2, - borderRadius: 3, - width: SCROLLBAR_SIZE - }; - - return ( -
- ); - }; - - _renderView = (props) => { - return ( -
- ); - }; - - // - // Listers - - onScrollStart = () => { - this._isScrolling = true; - }; - - onScrollStop = () => { - this._isScrolling = false; - }; - - onScroll = (event) => { - const { - scrollTop, - scrollLeft - } = event.currentTarget; - - this._isScrolling = true; - const onScroll = this.props.onScroll; - - if (onScroll) { - onScroll({ scrollTop, scrollLeft }); - } - }; - - // - // Render - - render() { - const { - autoHide, - autoScroll, - children - } = this.props; - - return ( - - {children} - - ); - } - -} - -OverlayScroller.propTypes = { - className: PropTypes.string, - trackClassName: PropTypes.string, - scrollTop: PropTypes.number, - scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired, - autoHide: PropTypes.bool.isRequired, - autoScroll: PropTypes.bool.isRequired, - children: PropTypes.node, - onScroll: PropTypes.func, - registerScroller: PropTypes.func -}; - -OverlayScroller.defaultProps = { - className: styles.scroller, - trackClassName: styles.thumb, - scrollDirection: scrollDirections.VERTICAL, - autoHide: false, - autoScroll: true, - registerScroller: () => { /* no-op */ } -}; - -export default OverlayScroller; diff --git a/frontend/src/Components/Scroller/OverlayScroller.tsx b/frontend/src/Components/Scroller/OverlayScroller.tsx new file mode 100644 index 000000000..b242642e8 --- /dev/null +++ b/frontend/src/Components/Scroller/OverlayScroller.tsx @@ -0,0 +1,127 @@ +import React, { ComponentPropsWithoutRef, useCallback, useRef } from 'react'; +import { Scrollbars } from 'react-custom-scrollbars-2'; +import { ScrollDirection } from 'Helpers/Props/scrollDirections'; +import { OnScroll } from './Scroller'; +import styles from './OverlayScroller.css'; + +const SCROLLBAR_SIZE = 10; + +interface OverlayScrollerProps { + className?: string; + trackClassName?: string; + scrollTop?: number; + scrollDirection: ScrollDirection; + autoHide: boolean; + autoScroll: boolean; + children?: React.ReactNode; + onScroll?: (payload: OnScroll) => void; +} + +interface ScrollbarTrackProps { + style: React.CSSProperties; + props: ComponentPropsWithoutRef<'div'>; +} + +function OverlayScroller(props: OverlayScrollerProps) { + const { + autoHide = false, + autoScroll = true, + className = styles.scroller, + trackClassName = styles.thumb, + children, + onScroll, + } = props; + const scrollBarRef = useRef(null); + const isScrolling = useRef(false); + + const handleScrollStart = useCallback(() => { + isScrolling.current = true; + }, []); + const handleScrollStop = useCallback(() => { + isScrolling.current = false; + }, []); + + const handleScroll = useCallback(() => { + if (!scrollBarRef.current) { + return; + } + + const { scrollTop, scrollLeft } = scrollBarRef.current.getValues(); + isScrolling.current = true; + + if (onScroll) { + onScroll({ scrollTop, scrollLeft }); + } + }, [onScroll]); + + const renderThumb = useCallback( + (props: ComponentPropsWithoutRef<'div'>) => { + return
; + }, + [trackClassName] + ); + + const renderTrackHorizontal = useCallback( + ({ style, props: trackProps }: ScrollbarTrackProps) => { + const finalStyle = { + ...style, + right: 2, + bottom: 2, + left: 2, + borderRadius: 3, + height: SCROLLBAR_SIZE, + }; + + return ( +
+ ); + }, + [] + ); + + const renderTrackVertical = useCallback( + ({ style, props: trackProps }: ScrollbarTrackProps) => { + const finalStyle = { + ...style, + right: 2, + bottom: 2, + top: 2, + borderRadius: 3, + width: SCROLLBAR_SIZE, + }; + + return ( +
+ ); + }, + [] + ); + + const renderView = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: any) => { + return
; + }, + [className] + ); + + return ( + + {children} + + ); +} + +export default OverlayScroller; diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx index 37b16eebd..95f85c119 100644 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ b/frontend/src/Components/Scroller/Scroller.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useRef, } from 'react'; -import ScrollDirection from 'Helpers/Props/ScrollDirection'; +import { ScrollDirection } from 'Helpers/Props/scrollDirections'; import styles from './Scroller.css'; export interface OnScroll { @@ -33,7 +33,7 @@ const Scroller = forwardRef( className, autoFocus = false, autoScroll = true, - scrollDirection = ScrollDirection.Vertical, + scrollDirection = 'vertical', children, scrollTop, initialScrollTop, @@ -59,7 +59,7 @@ const Scroller = forwardRef( currentRef.current.scrollTop = scrollTop; } - if (autoFocus && scrollDirection !== ScrollDirection.None) { + if (autoFocus && scrollDirection !== 'none') { currentRef.current.focus({ preventScroll: true }); } }, [autoFocus, currentRef, scrollDirection, scrollTop]); diff --git a/frontend/src/Components/SpinnerIcon.tsx b/frontend/src/Components/SpinnerIcon.tsx index 27ddadc41..d9124d692 100644 --- a/frontend/src/Components/SpinnerIcon.tsx +++ b/frontend/src/Components/SpinnerIcon.tsx @@ -10,11 +10,13 @@ export interface SpinnerIconProps extends IconProps { export default function SpinnerIcon({ name, spinningName = icons.SPINNER, + isSpinning, ...otherProps }: SpinnerIconProps) { return ( ); diff --git a/frontend/src/Components/Tooltip/Popover.js b/frontend/src/Components/Tooltip/Popover.js deleted file mode 100644 index 1fe92fcbf..000000000 --- a/frontend/src/Components/Tooltip/Popover.js +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { tooltipPositions } from 'Helpers/Props'; -import Tooltip from './Tooltip'; -import styles from './Popover.css'; - -function Popover(props) { - const { - title, - body, - ...otherProps - } = props; - - return ( - -
- {title} -
- -
- {body} -
-
- } - /> - ); -} - -Popover.propTypes = { - className: PropTypes.string, - bodyClassName: PropTypes.string, - anchor: PropTypes.node.isRequired, - title: PropTypes.string.isRequired, - body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - position: PropTypes.oneOf(tooltipPositions.all), - canFlip: PropTypes.bool -}; - -export default Popover; diff --git a/frontend/src/Components/Tooltip/Popover.tsx b/frontend/src/Components/Tooltip/Popover.tsx new file mode 100644 index 000000000..4c6781343 --- /dev/null +++ b/frontend/src/Components/Tooltip/Popover.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Tooltip, { TooltipProps } from './Tooltip'; +import styles from './Popover.css'; + +interface PopoverProps extends Omit { + title: string; + body: React.ReactNode; +} + +function Popover({ title, body, ...otherProps }: PopoverProps) { + return ( + +
{title}
+ +
{body}
+
+ } + /> + ); +} + +export default Popover; diff --git a/frontend/src/Components/Tooltip/Tooltip.js b/frontend/src/Components/Tooltip/Tooltip.js deleted file mode 100644 index 1499e7451..000000000 --- a/frontend/src/Components/Tooltip/Tooltip.js +++ /dev/null @@ -1,235 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Manager, Popper, Reference } from 'react-popper'; -import Portal from 'Components/Portal'; -import { kinds, tooltipPositions } from 'Helpers/Props'; -import dimensions from 'Styles/Variables/dimensions'; -import { isMobile as isMobileUtil } from 'Utilities/browser'; -import styles from './Tooltip.css'; - -let maxWidth = null; - -function getMaxWidth() { - const windowWidth = window.innerWidth; - - if (windowWidth >= parseInt(dimensions.breakpointLarge)) { - maxWidth = 800; - } else if (windowWidth >= parseInt(dimensions.breakpointMedium)) { - maxWidth = 650; - } else if (windowWidth >= parseInt(dimensions.breakpointSmall)) { - maxWidth = 500; - } else { - maxWidth = 450; - } - - return maxWidth; -} - -class Tooltip extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scheduleUpdate = null; - this._closeTimeout = null; - this._maxWidth = maxWidth || getMaxWidth(); - - this.state = { - isOpen: false - }; - } - - componentDidUpdate() { - if (this._scheduleUpdate && this.state.isOpen) { - this._scheduleUpdate(); - } - } - - componentWillUnmount() { - if (this._closeTimeout) { - this._closeTimeout = clearTimeout(this._closeTimeout); - } - } - - // - // Control - - computeMaxSize = (data) => { - const { - top, - right, - bottom, - left - } = data.offsets.reference; - - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - - if ((/^top/).test(data.placement)) { - data.styles.maxHeight = top - 20; - } else if ((/^bottom/).test(data.placement)) { - data.styles.maxHeight = windowHeight - bottom - 20; - } else if ((/^right/).test(data.placement)) { - data.styles.maxWidth = Math.min(this._maxWidth, windowWidth - right - 20); - data.styles.maxHeight = top - 20; - } else { - data.styles.maxWidth = Math.min(this._maxWidth, left - 20); - data.styles.maxHeight = top - 20; - } - - return data; - }; - - // - // Listeners - - onMeasure = ({ width }) => { - this.setState({ width }); - }; - - onClick = () => { - if (isMobileUtil()) { - this.setState({ isOpen: !this.state.isOpen }); - } - }; - - onMouseEnter = () => { - if (this._closeTimeout) { - this._closeTimeout = clearTimeout(this._closeTimeout); - } - - this.setState({ isOpen: true }); - }; - - onMouseLeave = () => { - this._closeTimeout = setTimeout(() => { - this.setState({ isOpen: false }); - }, 100); - }; - - // - // Render - - render() { - const { - className, - bodyClassName, - anchor, - tooltip, - kind, - position, - canFlip - } = this.props; - - return ( - - - {({ ref }) => ( - - {anchor} - - )} - - - - - {({ ref, style, placement, arrowProps, scheduleUpdate }) => { - this._scheduleUpdate = scheduleUpdate; - - const popperPlacement = placement ? placement.split('-')[0] : position; - const vertical = popperPlacement === 'top' || popperPlacement === 'bottom'; - - return ( -
-
- { - this.state.isOpen ? -
-
- {tooltip} -
-
: - null - } -
- ); - }} - - - - ); - } -} - -Tooltip.propTypes = { - className: PropTypes.string, - bodyClassName: PropTypes.string.isRequired, - anchor: PropTypes.node.isRequired, - tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]), - position: PropTypes.oneOf(tooltipPositions.all), - canFlip: PropTypes.bool.isRequired -}; - -Tooltip.defaultProps = { - bodyClassName: styles.body, - kind: kinds.DEFAULT, - position: tooltipPositions.TOP, - canFlip: false -}; - -export default Tooltip; diff --git a/frontend/src/Components/Tooltip/Tooltip.tsx b/frontend/src/Components/Tooltip/Tooltip.tsx new file mode 100644 index 000000000..43150c755 --- /dev/null +++ b/frontend/src/Components/Tooltip/Tooltip.tsx @@ -0,0 +1,226 @@ +import classNames from 'classnames'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Manager, Popper, Reference } from 'react-popper'; +import Portal from 'Components/Portal'; +import { kinds, tooltipPositions } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import dimensions from 'Styles/Variables/dimensions'; +import { isMobile as isMobileUtil } from 'Utilities/browser'; +import styles from './Tooltip.css'; + +export interface TooltipProps { + className?: string; + bodyClassName?: string; + anchor: React.ReactNode; + tooltip: string | React.ReactNode; + kind?: Extract; + position?: (typeof tooltipPositions.all)[number]; + canFlip?: boolean; +} +function Tooltip(props: TooltipProps) { + const { + className, + bodyClassName = styles.body, + anchor, + tooltip, + kind = kinds.DEFAULT, + position = tooltipPositions.TOP, + canFlip = false, + } = props; + + const closeTimeout = useRef>(); + const updater = useRef<(() => void) | null>(null); + const [isOpen, setIsOpen] = useState(false); + + const handleClick = useCallback(() => { + if (!isMobileUtil()) { + return; + } + + setIsOpen((isOpen) => { + return !isOpen; + }); + }, [setIsOpen]); + + const handleMouseEnterAnchor = useCallback(() => { + // Mobile will fire mouse enter and click events rapidly, + // this causes the tooltip not to open on the first press. + // Ignore the mouse enter event on mobile. + + if (isMobileUtil()) { + return; + } + + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + } + + setIsOpen(true); + }, [setIsOpen]); + + const handleMouseEnterTooltip = useCallback(() => { + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + } + + setIsOpen(true); + }, [setIsOpen]); + + const handleMouseLeave = useCallback(() => { + // Still listen for mouse leave on mobile to allow clicks outside to close the tooltip. + + clearTimeout(closeTimeout.current); + closeTimeout.current = setTimeout(() => { + setIsOpen(false); + }, 100); + }, [setIsOpen]); + + const maxWidth = useMemo(() => { + const windowWidth = window.innerWidth; + + if (windowWidth >= parseInt(dimensions.breakpointLarge)) { + return 800; + } else if (windowWidth >= parseInt(dimensions.breakpointMedium)) { + return 650; + } else if (windowWidth >= parseInt(dimensions.breakpointSmall)) { + return 500; + } + + return 450; + }, []); + + const computeMaxSize = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (data: any) => { + const { top, right, bottom, left } = data.offsets.reference; + + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + if (/^top/.test(data.placement)) { + data.styles.maxHeight = top - 20; + } else if (/^bottom/.test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom - 20; + } else if (/^right/.test(data.placement)) { + data.styles.maxWidth = Math.min(maxWidth, windowWidth - right - 20); + data.styles.maxHeight = top - 20; + } else { + data.styles.maxWidth = Math.min(maxWidth, left - 20); + data.styles.maxHeight = top - 20; + } + + return data; + }, + [maxWidth] + ); + + useEffect(() => { + if (updater.current && isOpen) { + updater.current(); + } + }); + + useEffect(() => { + return () => { + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + } + }; + }, []); + + return ( + + + {({ ref }) => ( + + {anchor} + + )} + + + + + {({ ref, style, placement, arrowProps, scheduleUpdate }) => { + updater.current = scheduleUpdate; + + const popperPlacement = placement + ? placement.split('-')[0] + : position; + const vertical = + popperPlacement === 'top' || popperPlacement === 'bottom'; + + return ( +
+
+ {isOpen ? ( +
+
{tooltip}
+
+ ) : null} +
+ ); + }} + + + + ); +} + +export default Tooltip; 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/SortDirection.ts b/frontend/src/Helpers/Props/SortDirection.ts deleted file mode 100644 index ac027fadc..000000000 --- a/frontend/src/Helpers/Props/SortDirection.ts +++ /dev/null @@ -1,6 +0,0 @@ -enum SortDirection { - Ascending = 'ascending', - Descending = 'descending', -} - -export default SortDirection; diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts deleted file mode 100644 index 885c73470..000000000 --- a/frontend/src/Helpers/Props/TooltipPosition.ts +++ /dev/null @@ -1,3 +0,0 @@ -type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; - -export default TooltipPosition; diff --git a/frontend/src/Helpers/Props/align.ts b/frontend/src/Helpers/Props/align.ts index f381959c6..06b19ca11 100644 --- a/frontend/src/Helpers/Props/align.ts +++ b/frontend/src/Helpers/Props/align.ts @@ -3,3 +3,5 @@ export const CENTER = 'center'; export const RIGHT = 'right'; export const all = [LEFT, CENTER, RIGHT]; + +export type Align = 'left' | 'center' | 'right'; 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/Helpers/Props/tooltipPositions.js b/frontend/src/Helpers/Props/tooltipPositions.ts similarity index 50% rename from frontend/src/Helpers/Props/tooltipPositions.js rename to frontend/src/Helpers/Props/tooltipPositions.ts index bca3c4ed4..9dcd543a3 100644 --- a/frontend/src/Helpers/Props/tooltipPositions.js +++ b/frontend/src/Helpers/Props/tooltipPositions.ts @@ -3,9 +3,6 @@ export const RIGHT = 'right'; export const BOTTOM = 'bottom'; export const LEFT = 'left'; -export const all = [ - TOP, - RIGHT, - BOTTOM, - LEFT -]; +export const all = [TOP, RIGHT, BOTTOM, LEFT]; + +export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; diff --git a/frontend/src/Movie/Details/MovieDetailsLinks.css b/frontend/src/Movie/Details/MovieDetailsLinks.css index ae160b2c5..aa192ef66 100644 --- a/frontend/src/Movie/Details/MovieDetailsLinks.css +++ b/frontend/src/Movie/Details/MovieDetailsLinks.css @@ -13,3 +13,10 @@ cursor: pointer; } + +@media only screen and (max-width: $breakpointExtraSmall) { + .links { + display: flex; + flex-flow: column wrap; + } +} diff --git a/frontend/src/Movie/Index/Menus/MovieIndexSortMenu.tsx b/frontend/src/Movie/Index/Menus/MovieIndexSortMenu.tsx index bf740141f..1b48da4e8 100644 --- a/frontend/src/Movie/Index/Menus/MovieIndexSortMenu.tsx +++ b/frontend/src/Movie/Index/Menus/MovieIndexSortMenu.tsx @@ -3,7 +3,7 @@ import MenuContent from 'Components/Menu/MenuContent'; import SortMenu from 'Components/Menu/SortMenu'; import SortMenuItem from 'Components/Menu/SortMenuItem'; import { align } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import translate from 'Utilities/String/translate'; interface MovieIndexSortMenuProps { diff --git a/frontend/src/Movie/Index/MovieIndex.tsx b/frontend/src/Movie/Index/MovieIndex.tsx index 49eaf4eda..972e65088 100644 --- a/frontend/src/Movie/Index/MovieIndex.tsx +++ b/frontend/src/Movie/Index/MovieIndex.tsx @@ -22,7 +22,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import withScrollPosition from 'Components/withScrollPosition'; import { align, icons, kinds } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { DESCENDING } from 'Helpers/Props/sortDirections'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import NoMovie from 'Movie/NoMovie'; import { executeCommand } from 'Store/Actions/commandActions'; @@ -211,7 +211,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => { const order = Object.keys(characters).sort(); // Reverse if sorting descending - if (sortDirection === SortDirection.Descending) { + if (sortDirection === DESCENDING) { order.reverse(); } diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPosters.tsx b/frontend/src/Movie/Index/Posters/MovieIndexPosters.tsx index 5b2d7b2bf..cef7ec461 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPosters.tsx +++ b/frontend/src/Movie/Index/Posters/MovieIndexPosters.tsx @@ -5,7 +5,7 @@ import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import useMeasure from 'Helpers/Hooks/useMeasure'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { SortDirection } from 'Helpers/Props/sortDirections'; import MovieIndexPoster from 'Movie/Index/Posters/MovieIndexPoster'; import Movie from 'Movie/Movie'; import dimensions from 'Styles/Variables/dimensions'; diff --git a/frontend/src/Movie/Index/Table/MovieIndexTable.tsx b/frontend/src/Movie/Index/Table/MovieIndexTable.tsx index 51d84d8df..b2c6dea4b 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexTable.tsx +++ b/frontend/src/Movie/Index/Table/MovieIndexTable.tsx @@ -7,8 +7,7 @@ import AppState from 'App/State/AppState'; 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 { SortDirection } from 'Helpers/Props/sortDirections'; import Movie from 'Movie/Movie'; import dimensions from 'Styles/Variables/dimensions'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; @@ -170,10 +169,7 @@ function MovieIndexTable(props: MovieIndexTableProps) { return (
- +