mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Use react-query for root folders
New: Add tooltip for empty root folders Closes #8196
This commit is contained in:
@@ -12,8 +12,8 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import useRootFolders, { useRootFolder } from 'RootFolder/useRootFolders';
|
||||
import { clearImportSeries } from 'Store/Actions/importSeriesActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportSeriesFooter from './ImportSeriesFooter';
|
||||
import ImportSeriesTable from './ImportSeriesTable';
|
||||
@@ -27,10 +27,12 @@ function ImportSeries() {
|
||||
|
||||
const {
|
||||
isFetching: rootFoldersFetching,
|
||||
isPopulated: rootFoldersPopulated,
|
||||
isFetched: rootFoldersFetched,
|
||||
error: rootFoldersError,
|
||||
items: rootFolders,
|
||||
} = useSelector((state: AppState) => state.rootFolders);
|
||||
data: rootFolders,
|
||||
} = useRootFolders();
|
||||
|
||||
useRootFolder(rootFolderId, false);
|
||||
|
||||
const { path, unmappedFolders } = useMemo(() => {
|
||||
const rootFolder = rootFolders.find((r) => r.id === rootFolderId);
|
||||
@@ -65,8 +67,6 @@ function ImportSeries() {
|
||||
}, [unmappedFolders]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRootFolders({ id: rootFolderId, timeout: false }));
|
||||
|
||||
return () => {
|
||||
dispatch(clearImportSeries());
|
||||
};
|
||||
@@ -95,7 +95,7 @@ function ImportSeries() {
|
||||
|
||||
{!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersPopulated &&
|
||||
rootFoldersFetched &&
|
||||
!unmappedFolders.length ? (
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('AllSeriesInRootFolderHaveBeenImported', { path })}
|
||||
@@ -104,7 +104,7 @@ function ImportSeries() {
|
||||
|
||||
{!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersPopulated &&
|
||||
rootFoldersFetched &&
|
||||
!!unmappedFolders.length &&
|
||||
scrollerRef.current ? (
|
||||
<ImportSeriesTable
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
@@ -13,28 +11,24 @@ import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import {
|
||||
addRootFolder,
|
||||
fetchRootFolders,
|
||||
} from 'Store/Actions/rootFolderActions';
|
||||
import useRootFolders, { useAddRootFolder } from 'RootFolder/useRootFolders';
|
||||
import { useIsWindows } from 'System/Status/useSystemStatus';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportSeriesSelectFolder.css';
|
||||
|
||||
function ImportSeriesSelectFolder() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, isSaving, error, saveError, items } =
|
||||
useSelector((state: AppState) => state.rootFolders);
|
||||
const { isFetching, isFetched, error, data } = useRootFolders();
|
||||
const { addRootFolder, isAdding, addError } = useAddRootFolder();
|
||||
|
||||
const isWindows = useIsWindows();
|
||||
|
||||
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
const wasAdding = usePrevious(isAdding);
|
||||
|
||||
const hasRootFolders = items.length > 0;
|
||||
const hasRootFolders = data.length > 0;
|
||||
const goodFolderExample = isWindows ? 'C:\\tv shows' : '/tv shows';
|
||||
const badFolderExample = isWindows
|
||||
? 'C:\\tv shows\\the simpsons'
|
||||
@@ -50,18 +44,14 @@ function ImportSeriesSelectFolder() {
|
||||
|
||||
const handleNewRootFolderSelect = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
dispatch(addRootFolder({ path: value }));
|
||||
addRootFolder({ path: value });
|
||||
},
|
||||
[dispatch]
|
||||
[addRootFolder]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && wasSaving && !saveError) {
|
||||
items.reduce((acc, item) => {
|
||||
if (!isAdding && wasAdding && !addError) {
|
||||
data.reduce((acc, item) => {
|
||||
if (item.id > acc) {
|
||||
return item.id;
|
||||
}
|
||||
@@ -69,18 +59,18 @@ function ImportSeriesSelectFolder() {
|
||||
return acc;
|
||||
}, 0);
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, items]);
|
||||
}, [isAdding, wasAdding, addError, data]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('ImportSeries')}>
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
{isFetching && !isFetched ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{!error && isPopulated && (
|
||||
{!error && isFetched && (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
{translate('LibraryImportSeriesHeader')}
|
||||
@@ -118,17 +108,17 @@ function ImportSeriesSelectFolder() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isSaving && saveError ? (
|
||||
{!isAdding && addError ? (
|
||||
<Alert className={styles.addErrorAlert} kind={kinds.DANGER}>
|
||||
{translate('AddRootFolderError')}
|
||||
|
||||
<ul>
|
||||
{Array.isArray(saveError.responseJSON) ? (
|
||||
saveError.responseJSON.map((e, index) => {
|
||||
{Array.isArray(addError.statusBody) ? (
|
||||
addError.statusBody.map((e, index) => {
|
||||
return <li key={index}>{e.errorMessage}</li>;
|
||||
})
|
||||
) : (
|
||||
<li>{JSON.stringify(saveError.responseJSON)}</li>
|
||||
<li>{JSON.stringify(addError.statusBody)}</li>
|
||||
)}
|
||||
</ul>
|
||||
</Alert>
|
||||
|
||||
@@ -16,7 +16,6 @@ import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||
import PathsAppState from './PathsAppState';
|
||||
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||
import ReleasesAppState from './ReleasesAppState';
|
||||
import RootFolderAppState from './RootFolderAppState';
|
||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
|
||||
@@ -89,7 +88,6 @@ interface AppState {
|
||||
paths: PathsAppState;
|
||||
providerOptions: ProviderOptionsAppState;
|
||||
releases: ReleasesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
series: SeriesAppState;
|
||||
seriesHistory: SeriesHistoryAppState;
|
||||
seriesIndex: SeriesIndexAppState;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import {
|
||||
addRootFolder,
|
||||
fetchRootFolders,
|
||||
} from 'Store/Actions/rootFolderActions';
|
||||
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||
import useRootFolders, { useAddRootFolder } from 'RootFolder/useRootFolders';
|
||||
import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput, {
|
||||
EnhancedSelectInputProps,
|
||||
@@ -37,23 +32,25 @@ export interface RootFolderSelectInputProps
|
||||
includeNoChangeDisabled?: boolean;
|
||||
}
|
||||
|
||||
function createRootFolderOptionsSelector(
|
||||
const useRootFolderOptions = (
|
||||
value: string | undefined,
|
||||
includeMissingValue: boolean,
|
||||
includeNoChange: boolean,
|
||||
includeNoChangeDisabled: boolean
|
||||
) {
|
||||
return createSelector(createRootFoldersSelector(), (rootFolders) => {
|
||||
const values: RootFolderSelectInputValue[] = rootFolders.items.map(
|
||||
(rootFolder) => {
|
||||
return {
|
||||
key: rootFolder.path,
|
||||
value: rootFolder.path,
|
||||
freeSpace: rootFolder.freeSpace,
|
||||
isMissing: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
) => {
|
||||
const { data } = useRootFolders();
|
||||
|
||||
return useMemo(() => {
|
||||
const sorted = [...data].sort(sortByProp('path'));
|
||||
|
||||
const values: RootFolderSelectInputValue[] = sorted.map((rootFolder) => {
|
||||
return {
|
||||
key: rootFolder.path,
|
||||
value: rootFolder.path,
|
||||
freeSpace: rootFolder.freeSpace,
|
||||
isMissing: false,
|
||||
};
|
||||
});
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
@@ -89,13 +86,15 @@ function createRootFolderOptionsSelector(
|
||||
value: translate('AddANewPath'),
|
||||
});
|
||||
|
||||
return {
|
||||
values,
|
||||
isSaving: rootFolders.isSaving,
|
||||
saveError: rootFolders.saveError,
|
||||
};
|
||||
});
|
||||
}
|
||||
return values;
|
||||
}, [
|
||||
data,
|
||||
value,
|
||||
includeMissingValue,
|
||||
includeNoChange,
|
||||
includeNoChangeDisabled,
|
||||
]);
|
||||
};
|
||||
|
||||
function RootFolderSelectInput({
|
||||
name,
|
||||
@@ -106,19 +105,19 @@ function RootFolderSelectInput({
|
||||
onChange,
|
||||
...otherProps
|
||||
}: RootFolderSelectInputProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { values, isSaving, saveError } = useSelector(
|
||||
createRootFolderOptionsSelector(
|
||||
value,
|
||||
includeMissingValue,
|
||||
includeNoChange,
|
||||
includeNoChangeDisabled
|
||||
)
|
||||
const values = useRootFolderOptions(
|
||||
value,
|
||||
includeMissingValue,
|
||||
includeNoChange,
|
||||
includeNoChangeDisabled
|
||||
);
|
||||
|
||||
const { addRootFolder, isAdding, addError, newRootFolder } =
|
||||
useAddRootFolder();
|
||||
|
||||
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
|
||||
useState(false);
|
||||
const [newRootFolderPath, setNewRootFolderPath] = useState('');
|
||||
const previousIsSaving = usePrevious(isSaving);
|
||||
const previousIsAdding = usePrevious(isAdding);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ value: newValue }: EnhancedSelectInputChanged<string>) => {
|
||||
@@ -133,10 +132,9 @@ function RootFolderSelectInput({
|
||||
|
||||
const handleNewRootFolderSelect = useCallback(
|
||||
({ value: newValue }: InputChanged<string>) => {
|
||||
setNewRootFolderPath(newValue);
|
||||
dispatch(addRootFolder({ path: newValue }));
|
||||
addRootFolder({ path: newValue });
|
||||
},
|
||||
[setNewRootFolderPath, dispatch]
|
||||
[addRootFolder]
|
||||
);
|
||||
|
||||
const handleAddRootFolderModalClose = useCallback(() => {
|
||||
@@ -156,18 +154,17 @@ function RootFolderSelectInput({
|
||||
}
|
||||
}
|
||||
|
||||
if (previousIsSaving && !isSaving && !saveError && newRootFolderPath) {
|
||||
onChange({ name, value: newRootFolderPath });
|
||||
setNewRootFolderPath('');
|
||||
if (previousIsAdding && !isAdding && !addError && newRootFolder) {
|
||||
onChange({ name, value: newRootFolder.path });
|
||||
}
|
||||
}, [
|
||||
name,
|
||||
value,
|
||||
values,
|
||||
isSaving,
|
||||
saveError,
|
||||
previousIsSaving,
|
||||
newRootFolderPath,
|
||||
isAdding,
|
||||
addError,
|
||||
newRootFolder,
|
||||
previousIsAdding,
|
||||
onChange,
|
||||
]);
|
||||
|
||||
@@ -192,10 +189,6 @@ function RootFolderSelectInput({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnhancedSelectInput
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
finishCommand,
|
||||
updateCommand,
|
||||
} from 'Store/Actions/commandActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchSeries } from 'Store/Actions/seriesActions';
|
||||
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
|
||||
import { repopulatePage } from 'Utilities/pagePopulator';
|
||||
@@ -322,7 +321,11 @@ function SignalRListener() {
|
||||
}
|
||||
|
||||
if (name === 'rootfolder') {
|
||||
dispatch(fetchRootFolders());
|
||||
if (version < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['/rootFolder'] });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
}
|
||||
|
||||
.unavailablePath {
|
||||
.pathContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unavailableLabel {
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'freeSpace': string;
|
||||
'label': string;
|
||||
'link': string;
|
||||
'unavailableLabel': string;
|
||||
'unavailablePath': string;
|
||||
'pathContainer': string;
|
||||
'unmappedFolders': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -7,27 +6,26 @@ 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 { RootFolder, useDeleteRootFolder } from './useRootFolders';
|
||||
import styles from './RootFolderRow.css';
|
||||
|
||||
interface RootFolderRowProps {
|
||||
id: number;
|
||||
path: string;
|
||||
accessible: boolean;
|
||||
freeSpace?: number;
|
||||
unmappedFolders: object[];
|
||||
}
|
||||
type RootFolderRowProps = RootFolder;
|
||||
|
||||
function RootFolderRow(props: RootFolderRowProps) {
|
||||
const { id, path, accessible, freeSpace = 0, unmappedFolders = [] } = props;
|
||||
const {
|
||||
id,
|
||||
path,
|
||||
accessible,
|
||||
isEmpty,
|
||||
freeSpace = 0,
|
||||
unmappedFolders = [],
|
||||
} = props;
|
||||
|
||||
const isUnavailable = !accessible;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const { deleteRootFolder } = useDeleteRootFolder(id);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -38,27 +36,38 @@ function RootFolderRow(props: RootFolderRowProps) {
|
||||
}, [setIsDeleteModalOpen]);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
dispatch(deleteRootFolder({ id }));
|
||||
|
||||
deleteRootFolder();
|
||||
setIsDeleteModalOpen(false);
|
||||
}, [dispatch, id]);
|
||||
}, [deleteRootFolder]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>
|
||||
{isUnavailable ? (
|
||||
<div className={styles.unavailablePath}>
|
||||
{path}
|
||||
<div className={styles.pathContainer}>
|
||||
{isUnavailable ? (
|
||||
path
|
||||
) : (
|
||||
<Link className={styles.link} to={`/add/import/${id}`}>
|
||||
{path}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Label className={styles.unavailableLabel} kind={kinds.DANGER}>
|
||||
{isUnavailable ? (
|
||||
<Label className={styles.label} kind={kinds.DANGER}>
|
||||
{translate('Unavailable')}
|
||||
</Label>
|
||||
</div>
|
||||
) : (
|
||||
<Link className={styles.link} to={`/add/import/${id}`}>
|
||||
{path}
|
||||
</Link>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{accessible && isEmpty ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('EmptyRootFolderTooltip')}
|
||||
>
|
||||
{translate('Empty')}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.freeSpace}>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Column from 'Components/Table/Column';
|
||||
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';
|
||||
import useRootFolders from './useRootFolders';
|
||||
|
||||
const rootFolderColumns: Column[] = [
|
||||
{
|
||||
@@ -35,17 +33,9 @@ const rootFolderColumns: Column[] = [
|
||||
];
|
||||
|
||||
function RootFolders() {
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
createRootFoldersSelector()
|
||||
);
|
||||
const { isFetching, isFetched, error, data } = useRootFolders();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
if (isFetching && !isPopulated) {
|
||||
if (isFetching && !isFetched) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
@@ -58,13 +48,14 @@ function RootFolders() {
|
||||
return (
|
||||
<Table columns={rootFolderColumns}>
|
||||
<TableBody>
|
||||
{items.map((rootFolder) => {
|
||||
{data.map((rootFolder) => {
|
||||
return (
|
||||
<RootFolderRow
|
||||
key={rootFolder.id}
|
||||
id={rootFolder.id}
|
||||
path={rootFolder.path}
|
||||
accessible={rootFolder.accessible}
|
||||
isEmpty={rootFolder.isEmpty}
|
||||
freeSpace={rootFolder.freeSpace}
|
||||
unmappedFolders={rootFolder.unmappedFolders}
|
||||
/>
|
||||
|
||||
99
frontend/src/RootFolder/useRootFolders.ts
Normal file
99
frontend/src/RootFolder/useRootFolders.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
|
||||
export interface UnmappedFolder {
|
||||
name: string;
|
||||
path: string;
|
||||
relativePath: string;
|
||||
}
|
||||
|
||||
export interface RootFolder extends ModelBase {
|
||||
id: number;
|
||||
path: string;
|
||||
accessible: boolean;
|
||||
isEmpty: boolean;
|
||||
freeSpace?: number;
|
||||
unmappedFolders: UnmappedFolder[];
|
||||
}
|
||||
|
||||
interface AddRootFolder {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ROOT_FOLDERS: RootFolder[] = [];
|
||||
|
||||
const useRootFolders = () => {
|
||||
const result = useApiQuery<RootFolder[]>({
|
||||
path: '/rootFolder',
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? DEFAULT_ROOT_FOLDERS,
|
||||
};
|
||||
};
|
||||
|
||||
export const useRootFolder = (id: number, timeout: boolean) => {
|
||||
const result = useApiQuery<RootFolder[]>({
|
||||
path: `/rootFolder/${id}`,
|
||||
queryParams: { timeout },
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? DEFAULT_ROOT_FOLDERS,
|
||||
};
|
||||
};
|
||||
|
||||
export default useRootFolders;
|
||||
|
||||
export const useDeleteRootFolder = (id: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, isPending, error } = useApiMutation<unknown, void>({
|
||||
path: `/rootFolder/${id}`,
|
||||
method: 'DELETE',
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/rootFolder'] });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteRootFolder: mutate,
|
||||
isDeleting: isPending,
|
||||
deleteError: error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useAddRootFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, isPending, error, data } = useApiMutation<
|
||||
RootFolder,
|
||||
AddRootFolder
|
||||
>({
|
||||
path: '/rootFolder',
|
||||
method: 'POST',
|
||||
mutationOptions: {
|
||||
onSuccess: (newRootFolder) => {
|
||||
queryClient.setQueryData<RootFolder[]>(
|
||||
['/rootFolder'],
|
||||
(oldRootFolders = []) => {
|
||||
return [...oldRootFolders, newRootFolder];
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
addRootFolder: mutate,
|
||||
isAdding: isPending,
|
||||
addError: error,
|
||||
newRootFolder: data,
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Series from 'Series/Series';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import {
|
||||
saveSeriesEditor,
|
||||
updateSeriesMonitor,
|
||||
@@ -167,10 +166,6 @@ function SeriesIndexSelectFooter() {
|
||||
}
|
||||
}, [previousIsDeleting, isDeleting, deleteError, unselectAll]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
const anySelected = selectedCount > 0;
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,6 @@ import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import {
|
||||
cloneImportList,
|
||||
fetchImportLists,
|
||||
@@ -62,7 +61,6 @@ function ImportLists() {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchImportLists());
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
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 createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||
import { useAddRootFolder } from 'RootFolder/useRootFolders';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddRootFolder.css';
|
||||
|
||||
function AddRootFolder() {
|
||||
const { isSaving, saveError } = useSelector(createRootFoldersSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { addRootFolder, isAdding, addError } = useAddRootFolder();
|
||||
|
||||
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
|
||||
useState(false);
|
||||
@@ -24,9 +20,9 @@ function AddRootFolder() {
|
||||
|
||||
const onNewRootFolderSelect = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
dispatch(addRootFolder({ path: value }));
|
||||
addRootFolder({ path: value });
|
||||
},
|
||||
[dispatch]
|
||||
[addRootFolder]
|
||||
);
|
||||
|
||||
const onAddRootFolderModalClose = useCallback(() => {
|
||||
@@ -35,17 +31,17 @@ function AddRootFolder() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isSaving && saveError ? (
|
||||
{!isAdding && addError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddRootFolderError')}
|
||||
|
||||
<ul>
|
||||
{Array.isArray(saveError.responseJSON) ? (
|
||||
saveError.responseJSON.map((e, index) => {
|
||||
{Array.isArray(addError.statusBody) ? (
|
||||
addError.statusBody.map((e, index) => {
|
||||
return <li key={index}>{e.errorMessage}</li>;
|
||||
})
|
||||
) : (
|
||||
<li>{JSON.stringify(saveError.responseJSON)}</li>
|
||||
<li>{JSON.stringify(addError.statusBody)}</li>
|
||||
)}
|
||||
</ul>
|
||||
</Alert>
|
||||
|
||||
@@ -6,7 +6,6 @@ import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import {
|
||||
cloneAutoTagging,
|
||||
deleteAutoTagging,
|
||||
@@ -61,7 +60,6 @@ export default function AutoTaggings() {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAutoTaggings());
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,6 @@ import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import { removeItem, set, updateItem } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { fetchRootFolders } from './rootFolderActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
@@ -259,8 +258,6 @@ export const actionHandlers = handleThunks({
|
||||
|
||||
...addedIds.map((id) => removeItem({ section, id }))
|
||||
]));
|
||||
|
||||
dispatch(fetchRootFolders());
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as organizePreview from './organizePreviewActions';
|
||||
import * as paths from './pathActions';
|
||||
import * as providerOptions from './providerOptionActions';
|
||||
import * as releases from './releaseActions';
|
||||
import * as rootFolders from './rootFolderActions';
|
||||
import * as series from './seriesActions';
|
||||
import * as seriesHistory from './seriesHistoryActions';
|
||||
import * as seriesIndex from './seriesIndexActions';
|
||||
@@ -33,7 +32,6 @@ export default [
|
||||
paths,
|
||||
providerOptions,
|
||||
releases,
|
||||
rootFolders,
|
||||
series,
|
||||
seriesHistory,
|
||||
seriesIndex,
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { set, updateItem } from './baseActions';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'rootFolders';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_ROOT_FOLDERS = 'rootFolders/fetchRootFolders';
|
||||
export const ADD_ROOT_FOLDER = 'rootFolders/addRootFolder';
|
||||
export const DELETE_ROOT_FOLDER = 'rootFolders/deleteRootFolder';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchRootFolders = createThunk(FETCH_ROOT_FOLDERS);
|
||||
export const addRootFolder = createThunk(ADD_ROOT_FOLDER);
|
||||
export const deleteRootFolder = createThunk(DELETE_ROOT_FOLDER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_ROOT_FOLDERS]: createFetchHandler('rootFolders', '/rootFolder'),
|
||||
|
||||
[DELETE_ROOT_FOLDER]: createRemoveItemHandler(
|
||||
'rootFolders',
|
||||
'/rootFolder',
|
||||
(state) => state.rootFolders
|
||||
),
|
||||
|
||||
[ADD_ROOT_FOLDER]: function(getState, payload, dispatch) {
|
||||
const path = payload.path;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/rootFolder',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({ path }),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
updateItem({
|
||||
section,
|
||||
...data
|
||||
}),
|
||||
|
||||
set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({}, defaultState, section);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import RootFolderAppState from 'App/State/RootFolderAppState';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import RootFolder from 'typings/RootFolder';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
|
||||
export default function createRootFoldersSelector() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector<RootFolder, RootFolderAppState>(
|
||||
'rootFolders',
|
||||
sortByProp('path')
|
||||
),
|
||||
(rootFolders: RootFolderAppState) => rootFolders
|
||||
);
|
||||
}
|
||||
@@ -62,6 +62,7 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
Mapper.Entity<RootFolder>("RootFolders").RegisterModel()
|
||||
.Ignore(r => r.Accessible)
|
||||
.Ignore(r => r.IsEmpty)
|
||||
.Ignore(r => r.FreeSpace)
|
||||
.Ignore(r => r.TotalSpace);
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var rootFolders = _seriesService.GetAllSeriesPaths()
|
||||
.Select(s => _rootFolderService.GetBestRootFolderPath(s.Value))
|
||||
.Distinct();
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var missingRootFolders = rootFolders.Where(s => !s.IsPathValid(PathValidationType.CurrentOs) || !_diskProvider.FolderExists(s))
|
||||
.ToList();
|
||||
@@ -65,6 +66,38 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
"#missing-root-folder");
|
||||
}
|
||||
|
||||
var emptyRootFolders = rootFolders
|
||||
.Where(r => _diskProvider.FolderEmpty(r))
|
||||
.ToList();
|
||||
|
||||
if (emptyRootFolders.Any())
|
||||
{
|
||||
if (emptyRootFolders.Count == 1)
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
HealthCheckReason.RootFolderEmpty,
|
||||
_localizationService.GetLocalizedString(
|
||||
"RootFolderEmptyHealthCheckMessage",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "rootFolderPath", emptyRootFolders.First() }
|
||||
}),
|
||||
"#empty-root-folder");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
HealthCheckReason.RootFolderEmpty,
|
||||
_localizationService.GetLocalizedString(
|
||||
"RootFolderMultipleEmptyHealthCheckMessage",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "rootFolderPaths", string.Join(" | ", emptyRootFolders) }
|
||||
}),
|
||||
"#empty-root-folder");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace NzbDrone.Core.HealthCheck
|
||||
RemotePathMappingWrongOSPath,
|
||||
RemovedSeriesMultiple,
|
||||
RemovedSeriesSingle,
|
||||
RootFolderEmpty,
|
||||
RootFolderMissing,
|
||||
RootFolderMultipleMissing,
|
||||
ServerNotification,
|
||||
|
||||
@@ -619,6 +619,8 @@
|
||||
"EditSeries": "Edit Series",
|
||||
"EditSeriesModalHeader": "Edit - {title}",
|
||||
"EditSizes": "Edit Sizes",
|
||||
"Empty": "Empty",
|
||||
"EmptyRootFolderTooltip": "This root does not contain any files or folders. {appName} will not scan for changes or create empty series folders.",
|
||||
"Enable": "Enable",
|
||||
"EnableAutomaticAdd": "Enable Automatic Add",
|
||||
"EnableAutomaticAddSeriesHelpText": "Add series from this list to {appName} when syncs are performed via the UI or by {appName}",
|
||||
@@ -1820,7 +1822,9 @@
|
||||
"RetentionHelpText": "Usenet only: Set to zero to set for unlimited retention",
|
||||
"RetryingDownloadOn": "Retrying download on {date} at {time}",
|
||||
"RootFolder": "Root Folder",
|
||||
"RootFolderEmptyHealthCheckMessage": "Empty root folder: {rootFolderPath}",
|
||||
"RootFolderMissingHealthCheckMessage": "Missing root folder: {rootFolderPath}",
|
||||
"RootFolderMultipleEmptyHealthCheckMessage": "Multiple root folders are empty: {rootFolderPaths}",
|
||||
"RootFolderMultipleMissingHealthCheckMessage": "Multiple root folders are missing: {rootFolderPaths}",
|
||||
"RootFolderPath": "Root Folder Path",
|
||||
"RootFolderSelectFreeSpace": "{freeSpace} Free",
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace NzbDrone.Core.RootFolders
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public bool Accessible { get; set; }
|
||||
public bool IsEmpty { get; set; }
|
||||
public long? FreeSpace { get; set; }
|
||||
public long? TotalSpace { get; set; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user