diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index e249f2d20..aeb1cbf68 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -10,6 +10,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 @@ -35,6 +36,11 @@ export interface QualityProfilesAppState extends AppSectionState, AppSectionSchemaState {} +export interface RootFolderAppState + extends AppSectionState, + AppSectionDeleteState, + AppSectionSaveState {} + export type LanguageSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionItemState; @@ -45,6 +51,7 @@ interface SettingsAppState { languages: LanguageSettingsAppState; notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; + rootFolders: RootFolderAppState; ui: UiSettingsAppState; } diff --git a/frontend/src/RootFolder/RootFolderRow.js b/frontend/src/RootFolder/RootFolderRow.js deleted file mode 100644 index f3550b12e..000000000 --- a/frontend/src/RootFolder/RootFolderRow.js +++ /dev/null @@ -1,81 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons, kinds } from 'Helpers/Props'; -import formatBytes from 'Utilities/Number/formatBytes'; -import styles from './RootFolderRow.css'; - -function RootFolderRow(props) { - const { - id, - path, - accessible, - freeSpace, - unmappedFolders, - onDeletePress - } = props; - - const isUnavailable = !accessible; - - return ( - - - { - isUnavailable ? -
- {path} - - -
: - - - {path} - - } -
- - - {(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)} - - - - {isUnavailable ? '-' : unmappedFolders.length} - - - - - -
- ); -} - -RootFolderRow.propTypes = { - id: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - accessible: PropTypes.bool.isRequired, - freeSpace: PropTypes.number, - unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired, - onDeletePress: PropTypes.func.isRequired -}; - -RootFolderRow.defaultProps = { - unmappedFolders: [] -}; - -export default RootFolderRow; diff --git a/frontend/src/RootFolder/RootFolderRow.tsx b/frontend/src/RootFolder/RootFolderRow.tsx new file mode 100644 index 000000000..bf8ac6f7a --- /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.js b/frontend/src/RootFolder/RootFolders.js deleted file mode 100644 index 0b4f157b5..000000000 --- a/frontend/src/RootFolder/RootFolders.js +++ /dev/null @@ -1,83 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -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 RootFolderRowConnector from './RootFolderRowConnector'; - -const rootFolderColumns = [ - { - name: 'path', - label: 'Path', - isVisible: true - }, - { - name: 'freeSpace', - label: 'Free Space', - isVisible: true - }, - { - name: 'unmappedFolders', - label: 'Unmapped Folders', - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -function RootFolders(props) { - const { - isFetching, - isPopulated, - error, - items - } = props; - - if (isFetching && !isPopulated) { - return ( - - ); - } - - if (!isFetching && !!error) { - return ( - Unable to load root folders - ); - } - - return ( - - - { - items.map((rootFolder) => { - return ( - - ); - }) - } - -
- ); -} - -RootFolders.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default RootFolders; 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 7e426076a..3ad5f777e 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -10,10 +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 episodeTitleRequiredOptions = [ { key: 'always', value: 'Always' }, @@ -452,9 +453,9 @@ class MediaManagement extends Component { : null } -
- - +
+ +
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.js b/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.js deleted file mode 100644 index 29800512e..000000000 --- a/frontend/src/Settings/MediaManagement/RootFolder/AddRootFolder.js +++ /dev/null @@ -1,71 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -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 styles from './AddRootFolder.css'; - -class AddRootFolder extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddNewRootFolderModalOpen: false - }; - } - - // - // Lifecycle - - onAddNewRootFolderPress = () => { - this.setState({ isAddNewRootFolderModalOpen: true }); - }; - - onNewRootFolderSelect = ({ value }) => { - this.props.onNewRootFolderSelect(value); - }; - - onAddRootFolderModalClose = () => { - this.setState({ isAddNewRootFolderModalOpen: false }); - }; - - // - // Render - - render() { - return ( -
- - - -
- ); - } -} - -AddRootFolder.propTypes = { - onNewRootFolderSelect: PropTypes.func.isRequired -}; - -export default AddRootFolder; 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 0db381137..3d9244add 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -7,6 +7,7 @@ "AddAutoTag": "Add Auto Tag", "AddCondition": "Add Condition", "AddNew": "Add New", + "AddRootFolder": "Add Root Folder", "Added": "Added", "AddingTag": "Adding tag", "Age": "Age", @@ -76,6 +77,8 @@ "DeleteConditionMessageText": "Are you sure you want to delete the condition '{0}'?", "DeleteCustomFormat": "Delete Custom Format", "DeleteCustomFormatMessageText": "Are you sure you want to delete the custom format '{0}'?", + "DeleteRootFolder": "Delete Root Folder", + "DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{path}'?", "DeleteSelectedDownloadClients": "Delete Download Client(s)", "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {count} selected download client(s)?", "DeleteSelectedImportLists": "Delete Import List(s)", @@ -295,6 +298,7 @@ "RemoveFailedDownloads": "Remove Failed Downloads", "RemoveFromDownloadClient": "Remove From Download Client", "RemoveFromDownloadClientHelpTextWarning": "Removing will remove the download and the file(s) from the download client.", + "RemoveRootFolder": "Remove root folder", "RemoveSelectedItem": "Remove Selected Item", "RemoveSelectedItemQueueMessageText": "Are you sure you want to remove 1 item from the queue?", "RemoveSelectedItems": "Remove Selected Items", @@ -323,6 +327,7 @@ "RootFolderMissingHealthCheckMessage": "Missing root folder: {0}", "RootFolderMultipleMissingHealthCheckMessage": "Multiple root folders are missing: {0}", "RootFolderPath": "Root Folder Path", + "RootFolders": "Root Folders", "Runtime": "Runtime", "Save": "Save", "SceneNumbering": "Scene Numbering", @@ -370,7 +375,10 @@ "UI Language": "UI Language", "UnableToLoadAutoTagging": "Unable to load auto tagging", "UnableToLoadBackups": "Unable to load backups", + "UnableToLoadRootFolders": "Unable to load root folders", "UnableToUpdateSonarrDirectly": "Unable to update Sonarr directly,", + "Unavailable": "Unavailable", + "UnmappedFolders": "Unmapped Folders", "Unmonitored": "Unmonitored", "UnmonitoredOnly": "Unmonitored Only", "UpdateAvailableHealthCheckMessage": "New update is available",