From abad6a9f18bd0cd6096c93e4c55e0228b14d8c2c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 29 Jul 2023 17:01:01 +0300 Subject: [PATCH] Convert Root Folders to Typescript --- frontend/src/App/State/SettingsAppState.ts | 11 ++- frontend/src/RootFolder/RootFolderRow.tsx | 95 +++++++++++++++++++ .../src/RootFolder/RootFolderRowConnector.js | 13 --- frontend/src/RootFolder/RootFolders.tsx | 82 ++++++++++++++++ .../src/RootFolder/RootFoldersConnector.js | 46 --------- .../MediaManagement/MediaManagement.js | 8 +- .../RootFolder/AddRootFolder.tsx | 54 +++++++++++ .../RootFolder/AddRootFolderConnector.js | 13 --- .../Selectors/createRootFoldersSelector.ts | 13 +++ frontend/src/typings/RootFolder.ts | 11 +++ src/NzbDrone.Core/Localization/Core/en.json | 3 + 11 files changed, 271 insertions(+), 78 deletions(-) create mode 100644 frontend/src/RootFolder/RootFolderRow.tsx delete mode 100644 frontend/src/RootFolder/RootFolderRowConnector.js create mode 100644 frontend/src/RootFolder/RootFolders.tsx delete mode 100644 frontend/src/RootFolder/RootFoldersConnector.js create mode 100644 frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.tsx delete mode 100644 frontend/src/Settings/MediaManagement/RootFolder/AddRootFolderConnector.js create mode 100644 frontend/src/Store/Selectors/createRootFoldersSelector.ts create mode 100644 frontend/src/typings/RootFolder.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 6960db2f9..7630ad2d7 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -9,6 +9,7 @@ import ImportList from 'typings/ImportList'; import Indexer from 'typings/Indexer'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; +import RootFolder from 'typings/RootFolder'; import { UiSettings } from 'typings/UiSettings'; export interface DownloadClientAppState @@ -34,6 +35,11 @@ export interface QualityProfilesAppState extends AppSectionState, AppSectionSchemaState {} +export interface RootFolderAppState + extends AppSectionState, + AppSectionDeleteState, + AppSectionSaveState {} + export type LanguageSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionState; @@ -41,10 +47,11 @@ interface SettingsAppState { downloadClients: DownloadClientAppState; importLists: ImportListAppState; indexers: IndexerAppState; - notifications: NotificationAppState; language: LanguageSettingsAppState; - uiSettings: UiSettingsAppState; + notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; + rootFolders: RootFolderAppState; + uiSettings: UiSettingsAppState; } export default SettingsAppState; diff --git a/frontend/src/RootFolder/RootFolderRow.tsx b/frontend/src/RootFolder/RootFolderRow.tsx new file mode 100644 index 000000000..48389d6fe --- /dev/null +++ b/frontend/src/RootFolder/RootFolderRow.tsx @@ -0,0 +1,95 @@ +import React, { useCallback, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import { icons, kinds } from 'Helpers/Props'; +import { deleteRootFolder } from 'Store/Actions/rootFolderActions'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; +import styles from './RootFolderRow.css'; + +interface RootFolderRowProps { + id: number; + path: string; + accessible: boolean; + freeSpace?: number; + unmappedFolders: object[]; +} + +function RootFolderRow(props: RootFolderRowProps) { + const { id, path, accessible, freeSpace, unmappedFolders = [] } = props; + + const isUnavailable = !accessible; + + const dispatch = useDispatch(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const onDeletePress = useCallback(() => { + setIsDeleteModalOpen(true); + }, [setIsDeleteModalOpen]); + + const onDeleteModalClose = useCallback(() => { + setIsDeleteModalOpen(false); + }, [setIsDeleteModalOpen]); + + const onConfirmDelete = useCallback(() => { + dispatch(deleteRootFolder({ id })); + + setIsDeleteModalOpen(false); + }, [dispatch, id]); + + return ( + + + {isUnavailable ? ( +
+ {path} + + +
+ ) : ( + + {path} + + )} +
+ + + {isUnavailable || isNaN(Number(freeSpace)) + ? '-' + : formatBytes(freeSpace)} + + + + {isUnavailable ? '-' : unmappedFolders.length} + + + + + + + +
+ ); +} + +export default RootFolderRow; diff --git a/frontend/src/RootFolder/RootFolderRowConnector.js b/frontend/src/RootFolder/RootFolderRowConnector.js deleted file mode 100644 index ab0848e87..000000000 --- a/frontend/src/RootFolder/RootFolderRowConnector.js +++ /dev/null @@ -1,13 +0,0 @@ -import { connect } from 'react-redux'; -import { deleteRootFolder } from 'Store/Actions/rootFolderActions'; -import RootFolderRow from './RootFolderRow'; - -function createMapDispatchToProps(dispatch, props) { - return { - onDeletePress() { - dispatch(deleteRootFolder({ id: props.id })); - } - }; -} - -export default connect(null, createMapDispatchToProps)(RootFolderRow); diff --git a/frontend/src/RootFolder/RootFolders.tsx b/frontend/src/RootFolder/RootFolders.tsx new file mode 100644 index 000000000..961797abe --- /dev/null +++ b/frontend/src/RootFolder/RootFolders.tsx @@ -0,0 +1,82 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { kinds } from 'Helpers/Props'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector'; +import translate from 'Utilities/String/translate'; +import RootFolderRow from './RootFolderRow'; + +const rootFolderColumns = [ + { + name: 'path', + get label() { + return translate('Path'); + }, + isVisible: true, + }, + { + name: 'freeSpace', + get label() { + return translate('FreeSpace'); + }, + isVisible: true, + }, + { + name: 'unmappedFolders', + get label() { + return translate('UnmappedFolders'); + }, + isVisible: true, + }, + { + name: 'actions', + isVisible: true, + }, +]; + +function RootFolders() { + const { isFetching, isPopulated, error, items } = useSelector( + createRootFoldersSelector() + ); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchRootFolders()); + }, [dispatch]); + + if (isFetching && !isPopulated) { + return ; + } + + if (!isFetching && !!error) { + return ( + {translate('UnableToLoadRootFolders')} + ); + } + + return ( + + + {items.map((rootFolder) => { + return ( + + ); + })} + +
+ ); +} + +export default RootFolders; diff --git a/frontend/src/RootFolder/RootFoldersConnector.js b/frontend/src/RootFolder/RootFoldersConnector.js deleted file mode 100644 index 39f140bcc..000000000 --- a/frontend/src/RootFolder/RootFoldersConnector.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; -import RootFolders from './RootFolders'; - -function createMapStateToProps() { - return createSelector( - (state) => state.rootFolders, - (rootFolders) => { - return rootFolders; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchRootFolders: fetchRootFolders -}; - -class RootFoldersConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchRootFolders(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -RootFoldersConnector.propTypes = { - dispatchFetchRootFolders: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector); diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index 56831feae..e41e27106 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -10,11 +10,11 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import RootFoldersConnector from 'RootFolder/RootFoldersConnector'; +import RootFolders from 'RootFolder/RootFolders'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; import NamingConnector from './Naming/NamingConnector'; -import AddRootFolderConnector from './RootFolder/AddRootFolderConnector'; +import AddRootFolder from './RootFolder/AddRootFolder'; const rescanAfterRefreshOptions = [ { @@ -479,8 +479,8 @@ class MediaManagement extends Component { }
- - + +
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.tsx b/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.tsx new file mode 100644 index 000000000..54a35e5bf --- /dev/null +++ b/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.tsx @@ -0,0 +1,54 @@ +import React, { useCallback, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import { addRootFolder } from 'Store/Actions/rootFolderActions'; +import translate from 'Utilities/String/translate'; +import styles from './AddRootFolder.css'; + +function AddRootFolder() { + const dispatch = useDispatch(); + + const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] = + useState(false); + + const onAddNewRootFolderPress = useCallback(() => { + setIsAddNewRootFolderModalOpen(true); + }, [setIsAddNewRootFolderModalOpen]); + + const onNewRootFolderSelect = useCallback( + ({ value }: { value: string }) => { + dispatch(addRootFolder({ path: value })); + }, + [dispatch] + ); + + const onAddRootFolderModalClose = useCallback(() => { + setIsAddNewRootFolderModalOpen(false); + }, [setIsAddNewRootFolderModalOpen]); + + return ( +
+ + + +
+ ); +} + +export default AddRootFolder; diff --git a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolderConnector.js b/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolderConnector.js deleted file mode 100644 index f32a1aec0..000000000 --- a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolderConnector.js +++ /dev/null @@ -1,13 +0,0 @@ -import { connect } from 'react-redux'; -import { addRootFolder } from 'Store/Actions/rootFolderActions'; -import AddRootFolder from './AddRootFolder'; - -function createMapDispatchToProps(dispatch) { - return { - onNewRootFolderSelect(path) { - dispatch(addRootFolder({ path })); - } - }; -} - -export default connect(null, createMapDispatchToProps)(AddRootFolder); diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts new file mode 100644 index 000000000..6f91cb0c2 --- /dev/null +++ b/frontend/src/Store/Selectors/createRootFoldersSelector.ts @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; +import { RootFolderAppState } from 'App/State/SettingsAppState'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import RootFolder from 'typings/RootFolder'; + +export default function createRootFoldersSelector() { + return createSelector( + createSortedSectionSelector('rootFolders', (a: RootFolder, b: RootFolder) => + a.path.localeCompare(b.path) + ), + (rootFolders: RootFolderAppState) => rootFolders + ); +} diff --git a/frontend/src/typings/RootFolder.ts b/frontend/src/typings/RootFolder.ts new file mode 100644 index 000000000..8d45263e0 --- /dev/null +++ b/frontend/src/typings/RootFolder.ts @@ -0,0 +1,11 @@ +import ModelBase from 'App/ModelBase'; + +interface RootFolder extends ModelBase { + id: number; + path: string; + accessible: boolean; + freeSpace?: number; + unmappedFolders: object[]; +} + +export default RootFolder; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 30e52c6cf..b28e1b6e9 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -245,6 +245,8 @@ "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRestriction": "Delete Restriction", "DeleteRestrictionHelpText": "Are you sure you want to delete this restriction?", + "DeleteRootFolder": "Delete Root Folder", + "DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{0}'?", "DeleteSelectedDownloadClients": "Delete Download Client(s)", "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {0} selected download client(s)?", "DeleteSelectedImportLists": "Delete Import List(s)", @@ -940,6 +942,7 @@ "RootFolder": "Root Folder", "RootFolderCheckMultipleMessage": "Multiple root folders are missing: {0}", "RootFolderCheckSingleMessage": "Missing root folder: {0}", + "RootFolderPath": "Root Folder Path", "RootFolders": "Root Folders", "RottenTomatoesRating": "Tomato Rating", "RssSyncHelpText": "Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)",