1
0
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:
Mark McDowall
2025-11-24 21:25:47 -08:00
parent 449caa12e3
commit 7a5157df29
23 changed files with 267 additions and 274 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}
/>

View 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,
};
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -107,6 +107,7 @@ namespace NzbDrone.Core.HealthCheck
RemotePathMappingWrongOSPath,
RemovedSeriesMultiple,
RemovedSeriesSingle,
RootFolderEmpty,
RootFolderMissing,
RootFolderMultipleMissing,
ServerNotification,

View File

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

View File

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