diff --git a/frontend/src/App/App.js b/frontend/src/App/App.tsx similarity index 71% rename from frontend/src/App/App.js rename to frontend/src/App/App.tsx index 754c75035..6c2d799f3 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.tsx @@ -1,13 +1,19 @@ -import { ConnectedRouter } from 'connected-react-router'; +import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; import PropTypes from 'prop-types'; import React from 'react'; import DocumentTitle from 'react-document-title'; import { Provider } from 'react-redux'; +import { Store } from 'redux'; import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -function App({ store, history }) { +interface AppProps { + store: Store; + history: ConnectedRouterProps['history']; +} + +function App({ store, history }: AppProps) { return ( @@ -24,7 +30,7 @@ function App({ store, history }) { App.propTypes = { store: PropTypes.object.isRequired, - history: PropTypes.object.isRequired + history: PropTypes.object.isRequired, }; export default App; diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.tsx similarity index 52% rename from frontend/src/App/AppRoutes.js rename to frontend/src/App/AppRoutes.tsx index a5bb0b33c..f66a4df40 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.tsx @@ -35,60 +35,37 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import MissingConnector from 'Wanted/Missing/MissingConnector'; -function AppRoutes(props) { - const { - app - } = props; - +function AppRoutes() { return ( {/* Series */} - + - { - window.Sonarr.urlBase && - { - return ( - - ); - }} - /> - } + {window.Sonarr.urlBase && ( + { + return ; + }} + /> + )} - + - + { - return ( - - ); + return ; }} /> @@ -96,96 +73,57 @@ function AppRoutes(props) { path="/seasonpass" exact={true} render={() => { - return ( - - ); + return ; }} /> - + {/* Calendar */} - + {/* Activity */} - + - + - + {/* Wanted */} - + - + {/* Settings */} - + - + - + - + - + - + - + - + - + {/* System */} - + - + - + - + - + - + {/* Not Found */} - + ); } AppRoutes.propTypes = { - app: PropTypes.func.isRequired + app: PropTypes.func.isRequired, }; export default AppRoutes; diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js deleted file mode 100644 index abc7f8832..000000000 --- a/frontend/src/App/AppUpdatedModal.js +++ /dev/null @@ -1,30 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector'; - -function AppUpdatedModal(props) { - const { - isOpen, - onModalClose - } = props; - - return ( - - - - ); -} - -AppUpdatedModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModal.tsx b/frontend/src/App/AppUpdatedModal.tsx new file mode 100644 index 000000000..696d36fb2 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.tsx @@ -0,0 +1,28 @@ +import React, { useCallback } from 'react'; +import Modal from 'Components/Modal/Modal'; +import AppUpdatedModalContent from './AppUpdatedModalContent'; + +interface AppUpdatedModalProps { + isOpen: boolean; + onModalClose: (...args: unknown[]) => unknown; +} + +function AppUpdatedModal(props: AppUpdatedModalProps) { + const { isOpen, onModalClose } = props; + + const handleModalClose = useCallback(() => { + location.reload(); + }, []); + + return ( + + + + ); +} + +export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js deleted file mode 100644 index a21afbc5a..000000000 --- a/frontend/src/App/AppUpdatedModalConnector.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux'; -import AppUpdatedModal from './AppUpdatedModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - location.reload(); - } - }; -} - -export default connect(null, createMapDispatchToProps)(AppUpdatedModal); diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js deleted file mode 100644 index 8cce1bc16..000000000 --- a/frontend/src/App/AppUpdatedModalContent.js +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -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 { kinds } from 'Helpers/Props'; -import UpdateChanges from 'System/Updates/UpdateChanges'; -import translate from 'Utilities/String/translate'; -import styles from './AppUpdatedModalContent.css'; - -function mergeUpdates(items, version, prevVersion) { - let installedIndex = items.findIndex((u) => u.version === version); - let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion); - - if (installedIndex === -1) { - installedIndex = 0; - } - - if (installedPreviouslyIndex === -1) { - installedPreviouslyIndex = items.length; - } else if (installedPreviouslyIndex === installedIndex && items.length) { - installedPreviouslyIndex += 1; - } - - const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); - - if (!appliedUpdates.length) { - return null; - } - - const appliedChanges = { new: [], fixed: [] }; - appliedUpdates.forEach((u) => { - if (u.changes) { - appliedChanges.new.push(... u.changes.new); - appliedChanges.fixed.push(... u.changes.fixed); - } - }); - - const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges }); - - if (!appliedChanges.new.length && !appliedChanges.fixed.length) { - mergedUpdate.changes = null; - } - - return mergedUpdate; -} - -function AppUpdatedModalContent(props) { - const { - version, - prevVersion, - isPopulated, - error, - items, - onSeeChangesPress, - onModalClose - } = props; - - const update = mergeUpdates(items, version, prevVersion); - - return ( - - - {translate('AppUpdated')} - - - -
- -
- - { - isPopulated && !error && !!update && -
- { - !update.changes && -
{translate('MaintenanceRelease')}
- } - - { - !!update.changes && -
-
- {translate('WhatsNew')} -
- - - - -
- } -
- } - - { - !isPopulated && !error && - - } -
- - - - - - -
- ); -} - -AppUpdatedModalContent.propTypes = { - version: PropTypes.string.isRequired, - prevVersion: PropTypes.string, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onSeeChangesPress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContent.tsx b/frontend/src/App/AppUpdatedModalContent.tsx new file mode 100644 index 000000000..6553d6270 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.tsx @@ -0,0 +1,145 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +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 usePrevious from 'Helpers/Hooks/usePrevious'; +import { kinds } from 'Helpers/Props'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import UpdateChanges from 'System/Updates/UpdateChanges'; +import Update from 'typings/Update'; +import translate from 'Utilities/String/translate'; +import AppState from './State/AppState'; +import styles from './AppUpdatedModalContent.css'; + +function mergeUpdates(items: Update[], version: string, prevVersion?: string) { + let installedIndex = items.findIndex((u) => u.version === version); + let installedPreviouslyIndex = items.findIndex( + (u) => u.version === prevVersion + ); + + if (installedIndex === -1) { + installedIndex = 0; + } + + if (installedPreviouslyIndex === -1) { + installedPreviouslyIndex = items.length; + } else if (installedPreviouslyIndex === installedIndex && items.length) { + installedPreviouslyIndex += 1; + } + + const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); + + if (!appliedUpdates.length) { + return null; + } + + const appliedChanges: Update['changes'] = { new: [], fixed: [] }; + + appliedUpdates.forEach((u: Update) => { + if (u.changes) { + appliedChanges.new.push(...u.changes.new); + appliedChanges.fixed.push(...u.changes.fixed); + } + }); + + const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], { + changes: appliedChanges, + }); + + if (!appliedChanges.new.length && !appliedChanges.fixed.length) { + mergedUpdate.changes = null; + } + + return mergedUpdate; +} + +interface AppUpdatedModalContentProps { + onModalClose: () => void; +} + +function AppUpdatedModalContent(props: AppUpdatedModalContentProps) { + const dispatch = useDispatch(); + const { version, prevVersion } = useSelector((state: AppState) => state.app); + const { isPopulated, error, items } = useSelector( + (state: AppState) => state.system.updates + ); + const previousVersion = usePrevious(version); + + const { onModalClose } = props; + + const update = mergeUpdates(items, version, prevVersion); + + const handleSeeChangesPress = useCallback(() => { + window.location.href = `${window.Sonarr.urlBase}/system/updates`; + }, []); + + useEffect(() => { + dispatch(fetchUpdates()); + }, [dispatch]); + + useEffect(() => { + if (version !== previousVersion) { + dispatch(fetchUpdates()); + } + }, [version, previousVersion, dispatch]); + + return ( + + {translate('AppUpdated')} + + +
+ +
+ + {isPopulated && !error && !!update ? ( +
+ {update.changes ? ( +
+ {translate('MaintenanceRelease')} +
+ ) : null} + + {update.changes ? ( +
+
{translate('WhatsNew')}
+ + + + +
+ ) : null} +
+ ) : null} + + {!isPopulated && !error ? : null} +
+ + + + + + +
+ ); +} + +export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js deleted file mode 100644 index 4100ee674..000000000 --- a/frontend/src/App/AppUpdatedModalContentConnector.js +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import AppUpdatedModalContent from './AppUpdatedModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.version, - (state) => state.app.prevVersion, - (state) => state.system.updates, - (version, prevVersion, updates) => { - const { - isPopulated, - error, - items - } = updates; - - return { - version, - prevVersion, - isPopulated, - error, - items - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchFetchUpdates() { - dispatch(fetchUpdates()); - }, - - onSeeChangesPress() { - window.location = `${window.Sonarr.urlBase}/system/updates`; - } - }; -} - -class AppUpdatedModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchUpdates(); - } - - componentDidUpdate(prevProps) { - if (prevProps.version !== this.props.version) { - this.props.dispatchFetchUpdates(); - } - } - - // - // Render - - render() { - const { - dispatchFetchUpdates, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -AppUpdatedModalContentConnector.propTypes = { - version: PropTypes.string.isRequired, - dispatchFetchUpdates: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector); diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx index ad3ad69e9..ce598f2dc 100644 --- a/frontend/src/App/ApplyTheme.tsx +++ b/frontend/src/App/ApplyTheme.tsx @@ -1,13 +1,9 @@ -import React, { Fragment, ReactNode, useCallback, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import themes from 'Styles/Themes'; import AppState from './State/AppState'; -interface ApplyThemeProps { - children: ReactNode; -} - function createThemeSelector() { return createSelector( (state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme, @@ -17,7 +13,7 @@ function createThemeSelector() { ); } -function ApplyTheme({ children }: ApplyThemeProps) { +function ApplyTheme() { const theme = useSelector(createThemeSelector()); const updateCSSVariables = useCallback(() => { @@ -31,7 +27,7 @@ function ApplyTheme({ children }: ApplyThemeProps) { updateCSSVariables(); }, [updateCSSVariables, theme]); - return {children}; + return null; } export default ApplyTheme; diff --git a/frontend/src/App/ColorImpairedContext.js b/frontend/src/App/ColorImpairedContext.ts similarity index 100% rename from frontend/src/App/ColorImpairedContext.js rename to frontend/src/App/ColorImpairedContext.ts diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.tsx similarity index 54% rename from frontend/src/App/ConnectionLostModal.js rename to frontend/src/App/ConnectionLostModal.tsx index 5c08f491f..f08f2c0e2 100644 --- a/frontend/src/App/ConnectionLostModal.js +++ b/frontend/src/App/ConnectionLostModal.tsx @@ -1,5 +1,4 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback } from 'react'; import Button from 'Components/Link/Button'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; @@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './ConnectionLostModal.css'; -function ConnectionLostModal(props) { - const { - isOpen, - onModalClose - } = props; +interface ConnectionLostModalProps { + isOpen: boolean; +} + +function ConnectionLostModal(props: ConnectionLostModalProps) { + const { isOpen } = props; + + const handleModalClose = useCallback(() => { + location.reload(); + }, []); return ( - - - - {translate('ConnectionLost')} - + + + {translate('ConnectionLost')} -
- {translate('ConnectionLostToBackend')} -
+
{translate('ConnectionLostToBackend')}
{translate('ConnectionLostReconnect')}
- @@ -48,9 +42,4 @@ function ConnectionLostModal(props) { ); } -ConnectionLostModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - export default ConnectionLostModal; diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js deleted file mode 100644 index 8ab8e3cd0..000000000 --- a/frontend/src/App/ConnectionLostModalConnector.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux'; -import ConnectionLostModal from './ConnectionLostModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - location.reload(); - } - }; -} - -export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal); diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 744f11f31..212a24ad1 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -49,6 +49,7 @@ export interface AppSectionState { isConnected: boolean; isReconnecting: boolean; version: string; + prevVersion?: string; dimensions: { isSmallScreen: boolean; width: number; diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js index 4b24a8231..1386865e8 100644 --- a/frontend/src/Components/Page/Page.js +++ b/frontend/src/Components/Page/Page.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; +import AppUpdatedModal from 'App/AppUpdatedModal'; import ColorImpairedContext from 'App/ColorImpairedContext'; -import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; +import ConnectionLostModal from 'App/ConnectionLostModal'; import SignalRConnector from 'Components/SignalRConnector'; import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal'; import locationShape from 'Helpers/Props/Shapes/locationShape'; @@ -101,12 +101,12 @@ class Page extends Component { {children} - - diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx index e3a3076c1..c0a5fb882 100644 --- a/frontend/src/System/Updates/Updates.tsx +++ b/frontend/src/System/Updates/Updates.tsx @@ -198,8 +198,6 @@ function Updates() { {hasUpdates && (
{items.map((update) => { - const hasChanges = !!update.changes; - return (
@@ -249,7 +247,7 @@ function Updates() { ) : null}
- {hasChanges ? ( + {update.changes ? (