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 (
-
- );
- }
-
- 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 (
+
+ );
+ }
+
+ 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 &&
-
- }
-
-
- {translate('Cancel')}
-
-
-
- {translate('Ok')}
-
-
-
- );
- }
-}
-
-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}
+
+ {translate('Cancel')}
+
+ {translate('Ok')}
+
+
+ );
+}
+
+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 ?
-
:
- 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 ? (
+
+ ) : 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 (
-
+