Convert Root Folders to Typescript

This commit is contained in:
Bogdan 2023-07-29 17:01:01 +03:00
parent 835a539275
commit abad6a9f18
11 changed files with 271 additions and 78 deletions

View File

@ -9,6 +9,7 @@ import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer'; import Indexer from 'typings/Indexer';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile'; import QualityProfile from 'typings/QualityProfile';
import RootFolder from 'typings/RootFolder';
import { UiSettings } from 'typings/UiSettings'; import { UiSettings } from 'typings/UiSettings';
export interface DownloadClientAppState export interface DownloadClientAppState
@ -34,6 +35,11 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>, extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {} AppSectionSchemaState<QualityProfile> {}
export interface RootFolderAppState
extends AppSectionState<RootFolder>,
AppSectionDeleteState,
AppSectionSaveState {}
export type LanguageSettingsAppState = AppSectionState<Language>; export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionState<UiSettings>; export type UiSettingsAppState = AppSectionState<UiSettings>;
@ -41,10 +47,11 @@ interface SettingsAppState {
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
importLists: ImportListAppState; importLists: ImportListAppState;
indexers: IndexerAppState; indexers: IndexerAppState;
notifications: NotificationAppState;
language: LanguageSettingsAppState; language: LanguageSettingsAppState;
uiSettings: UiSettingsAppState; notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState; qualityProfiles: QualityProfilesAppState;
rootFolders: RootFolderAppState;
uiSettings: UiSettingsAppState;
} }
export default SettingsAppState; export default SettingsAppState;

View File

@ -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 (
<TableRow>
<TableRowCell>
{isUnavailable ? (
<div className={styles.unavailablePath}>
{path}
<Label className={styles.unavailableLabel} kind={kinds.DANGER}>
{translate('Unavailable')}
</Label>
</div>
) : (
<Link className={styles.link} to={`/add/import/${id}`}>
{path}
</Link>
)}
</TableRowCell>
<TableRowCell className={styles.freeSpace}>
{isUnavailable || isNaN(Number(freeSpace))
? '-'
: formatBytes(freeSpace)}
</TableRowCell>
<TableRowCell className={styles.unmappedFolders}>
{isUnavailable ? '-' : unmappedFolders.length}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RemoveRootFolder')}
name={icons.REMOVE}
onPress={onDeletePress}
/>
</TableRowCell>
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', [path])}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>
</TableRow>
);
}
export default RootFolderRow;

View File

@ -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);

View File

@ -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 <LoadingIndicator />;
}
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert>
);
}
return (
<Table columns={rootFolderColumns}>
<TableBody>
{items.map((rootFolder) => {
return (
<RootFolderRow
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
accessible={rootFolder.accessible}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})}
</TableBody>
</Table>
);
}
export default RootFolders;

View File

@ -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 (
<RootFolders
{...this.props}
/>
);
}
}
RootFoldersConnector.propTypes = {
dispatchFetchRootFolders: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector);

View File

@ -10,11 +10,11 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes, kinds, sizes } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import RootFoldersConnector from 'RootFolder/RootFoldersConnector'; import RootFolders from 'RootFolder/RootFolders';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import NamingConnector from './Naming/NamingConnector'; import NamingConnector from './Naming/NamingConnector';
import AddRootFolderConnector from './RootFolder/AddRootFolderConnector'; import AddRootFolder from './RootFolder/AddRootFolder';
const rescanAfterRefreshOptions = [ const rescanAfterRefreshOptions = [
{ {
@ -479,8 +479,8 @@ class MediaManagement extends Component {
} }
<FieldSet legend={translate('RootFolders')}> <FieldSet legend={translate('RootFolders')}>
<RootFoldersConnector /> <RootFolders />
<AddRootFolderConnector /> <AddRootFolder />
</FieldSet> </FieldSet>
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>

View File

@ -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 (
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<FileBrowserModal
isOpen={isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={onNewRootFolderSelect}
onModalClose={onAddRootFolderModalClose}
/>
</div>
);
}
export default AddRootFolder;

View File

@ -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);

View File

@ -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
);
}

View File

@ -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;

View File

@ -245,6 +245,8 @@
"DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?",
"DeleteRestriction": "Delete Restriction", "DeleteRestriction": "Delete Restriction",
"DeleteRestrictionHelpText": "Are you sure you want to delete this 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)", "DeleteSelectedDownloadClients": "Delete Download Client(s)",
"DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {0} selected download client(s)?", "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {0} selected download client(s)?",
"DeleteSelectedImportLists": "Delete Import List(s)", "DeleteSelectedImportLists": "Delete Import List(s)",
@ -940,6 +942,7 @@
"RootFolder": "Root Folder", "RootFolder": "Root Folder",
"RootFolderCheckMultipleMessage": "Multiple root folders are missing: {0}", "RootFolderCheckMultipleMessage": "Multiple root folders are missing: {0}",
"RootFolderCheckSingleMessage": "Missing root folder: {0}", "RootFolderCheckSingleMessage": "Missing root folder: {0}",
"RootFolderPath": "Root Folder Path",
"RootFolders": "Root Folders", "RootFolders": "Root Folders",
"RottenTomatoesRating": "Tomato Rating", "RottenTomatoesRating": "Tomato Rating",
"RssSyncHelpText": "Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)", "RssSyncHelpText": "Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)",