Convert Root Folders to Typescript

This commit is contained in:
Bogdan 2023-07-29 16:43:16 +03:00 committed by Mark McDowall
parent 8bd91bd86b
commit a5aab810d7
14 changed files with 276 additions and 312 deletions

View File

@ -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<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
export interface RootFolderAppState
extends AppSectionState<RootFolder>,
AppSectionDeleteState,
AppSectionSaveState {}
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
@ -45,6 +51,7 @@ interface SettingsAppState {
languages: LanguageSettingsAppState;
notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState;
rootFolders: RootFolderAppState;
ui: UiSettingsAppState;
}

View File

@ -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 (
<TableRow>
<TableRowCell>
{
isUnavailable ?
<div className={styles.unavailablePath}>
{path}
<Label
className={styles.unavailableLabel}
kind={kinds.DANGER}
>
Unavailable
</Label>
</div> :
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
}
</TableRowCell>
<TableRowCell className={styles.freeSpace}>
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
</TableRowCell>
<TableRowCell className={styles.unmappedFolders}>
{isUnavailable ? '-' : unmappedFolders.length}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title="Remove root folder"
name={icons.REMOVE}
onPress={onDeletePress}
/>
</TableRowCell>
</TableRow>
);
}
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;

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

@ -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 (
<LoadingIndicator />
);
}
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>Unable to load root folders</Alert>
);
}
return (
<Table
columns={rootFolderColumns}
>
<TableBody>
{
items.map((rootFolder) => {
return (
<RootFolderRowConnector
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
accessible={rootFolder.accessible}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})
}
</TableBody>
</Table>
);
}
RootFolders.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default RootFolders;

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,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 {
</Form> : null
}
<FieldSet legend="Root Folders">
<RootFoldersConnector />
<AddRootFolderConnector />
<FieldSet legend={translate('RootFolders')}>
<RootFolders />
<AddRootFolder />
</FieldSet>
</PageContentBody>
</PageContent>

View File

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

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

@ -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",