1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-25 22:46:31 -04:00

Use react-query for path input and file browser

This commit is contained in:
Mark McDowall
2025-11-26 21:18:25 -08:00
parent 2f119fefd1
commit 91b242902d
9 changed files with 138 additions and 306 deletions
@@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import { PathInputInternal } from 'Components/Form/PathInput';
import Button from 'Components/Link/Button';
@@ -15,11 +14,10 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { kinds, scrollDirections } from 'Helpers/Props';
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
import usePaths from 'Path/usePaths';
import { useSystemStatusData } from 'System/Status/useSystemStatus';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import createPathsSelector from './createPathsSelector';
import FileBrowserRow from './FileBrowserRow';
import styles from './FileBrowserModalContent.css';
@@ -46,19 +44,25 @@ export interface FileBrowserModalContentProps {
onModalClose: () => void;
}
function FileBrowserModalContent(props: FileBrowserModalContentProps) {
const { name, value, includeFiles = true, onChange, onModalClose } = props;
const dispatch = useDispatch();
const { isWindows, mode } = useSystemStatusData();
const { isFetching, isPopulated, error, parent, directories, files, paths } =
useSelector(createPathsSelector());
function FileBrowserModalContent({
name,
value,
includeFiles = true,
onChange,
onModalClose,
}: FileBrowserModalContentProps) {
const [currentPath, setCurrentPath] = useState(value);
const scrollerRef = useRef(null);
const previousValue = usePrevious(value);
const { isWindows, mode } = useSystemStatusData();
const { isFetching, isFetched, error, data } = usePaths({
path: currentPath,
allowFoldersWithoutTrailingSlashes: true,
includeFiles,
});
const { directories, files, parent, paths } = data;
const emptyParent = parent === '';
const isWindowsService = isWindows && mode === 'service';
@@ -70,20 +74,9 @@ function FileBrowserModalContent(props: FileBrowserModalContentProps) {
[]
);
const handleRowPress = useCallback(
(path: string) => {
setCurrentPath(path);
dispatch(
fetchPaths({
path,
allowFoldersWithoutTrailingSlashes: true,
includeFiles,
})
);
},
[includeFiles, dispatch, setCurrentPath]
);
const handleRowPress = useCallback((path: string) => {
setCurrentPath(path);
}, []);
const handleOkPress = useCallback(() => {
onChange({
@@ -91,22 +84,12 @@ function FileBrowserModalContent(props: FileBrowserModalContentProps) {
value: currentPath,
});
dispatch(clearPaths());
onModalClose();
}, [name, currentPath, dispatch, onChange, onModalClose]);
}, [name, currentPath, onChange, onModalClose]);
const handleFetchPaths = useCallback(
(path: string) => {
dispatch(
fetchPaths({
path,
allowFoldersWithoutTrailingSlashes: true,
includeFiles,
})
);
},
[includeFiles, dispatch]
);
const handleFetchPaths = useCallback((path: string) => {
setCurrentPath(path);
}, []);
useEffect(() => {
if (value !== previousValue && value !== currentPath) {
@@ -114,26 +97,6 @@ function FileBrowserModalContent(props: FileBrowserModalContentProps) {
}
}, [value, previousValue, currentPath, setCurrentPath]);
useEffect(
() => {
dispatch(
fetchPaths({
path: currentPath,
allowFoldersWithoutTrailingSlashes: true,
includeFiles,
})
);
return () => {
dispatch(clearPaths());
};
},
// This should only run once when the component mounts,
// so we don't need to include the other dependencies.
// eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('FileBrowser')}</ModalHeader>
@@ -172,7 +135,7 @@ function FileBrowserModalContent(props: FileBrowserModalContentProps) {
>
{error ? <div>{translate('ErrorLoadingContents')}</div> : null}
{isPopulated && !error ? (
{isFetched && !error ? (
<Table horizontalScroll={false} columns={columns}>
<TableBody>
{emptyParent ? (
@@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { PathType } from 'App/State/PathsAppState';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import { PathType } from 'Path/usePaths';
import styles from './FileBrowserRow.css';
function getIconName(type: PathType) {
@@ -1,36 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createPathsSelector() {
return createSelector(
(state: AppState) => state.paths,
(paths) => {
const {
isFetching,
isPopulated,
error,
parent,
currentPath,
directories,
files,
} = paths;
const filteredPaths = [...directories, ...files].filter(({ path }) => {
return path.toLowerCase().startsWith(currentPath.toLowerCase());
});
return {
isFetching,
isPopulated,
error,
parent,
currentPath,
directories,
files,
paths: filteredPaths,
};
}
);
}
export default createPathsSelector;
+32 -62
View File
@@ -10,15 +10,11 @@ import {
ChangeEvent,
SuggestionsFetchRequestedParams,
} from 'react-autosuggest';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { Path } from 'App/State/PathsAppState';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons } from 'Helpers/Props';
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
import usePaths, { Path } from 'Path/usePaths';
import { InputChanged } from 'typings/inputs';
import AutoSuggestInput from './AutoSuggestInput';
import FormInputButton from './FormInputButton';
@@ -46,43 +42,27 @@ function handleSuggestionsClearRequested() {
// because we don't want to reset the paths after a path is selected.
}
function createPathsSelector() {
return createSelector(
(state: AppState) => state.paths,
(paths) => {
const { currentPath, directories, files } = paths;
const filteredPaths = [...directories, ...files].filter(({ path }) => {
return path.toLowerCase().startsWith(currentPath.toLowerCase());
});
return filteredPaths;
}
);
}
function PathInput(props: PathInputProps) {
const { includeFiles } = props;
const { includeFiles, value = '' } = props;
const [currentPath, setCurrentPath] = useState(value);
const dispatch = useDispatch();
const { data } = usePaths({
path: currentPath,
includeFiles,
});
const paths = useSelector(createPathsSelector());
const handleFetchPaths = useCallback(
(path: string) => {
dispatch(fetchPaths({ path, includeFiles }));
},
[includeFiles, dispatch]
);
const handleFetchPaths = useCallback((path: string) => {
setCurrentPath(path);
}, []);
const handleClearPaths = useCallback(() => {
dispatch(clearPaths);
}, [dispatch]);
// No-op for React Query implementation as we don't need to clear
}, []);
return (
<PathInputInternal
{...props}
paths={paths}
paths={data.paths}
onFetchPaths={handleFetchPaths}
onClearPaths={handleClearPaths}
/>
@@ -91,32 +71,22 @@ function PathInput(props: PathInputProps) {
export default PathInput;
export function PathInputInternal(props: PathInputInternalProps) {
const {
className = styles.inputWrapper,
name,
value: inputValue = '',
paths,
includeFiles,
hasButton,
hasFileBrowser = true,
onChange,
onFetchPaths,
onClearPaths,
...otherProps
} = props;
export function PathInputInternal({
className = styles.inputWrapper,
name,
value: inputValue = '',
paths,
includeFiles,
hasButton,
hasFileBrowser = true,
onChange,
onFetchPaths,
onClearPaths,
...otherProps
}: PathInputInternalProps) {
const [value, setValue] = useState(inputValue);
const [isFileBrowserModalOpen, setIsFileBrowserModalOpen] = useState(false);
const previousInputValue = usePrevious(inputValue);
const dispatch = useDispatch();
const handleFetchPaths = useCallback(
(path: string) => {
dispatch(fetchPaths({ path, includeFiles }));
},
[includeFiles, dispatch]
);
const handleInputChange = useCallback(
(_event: SyntheticEvent, { newValue }: ChangeEvent) => {
@@ -138,12 +108,12 @@ export function PathInputInternal(props: PathInputInternalProps) {
});
if (path.type !== 'file') {
handleFetchPaths(path.path);
onFetchPaths(path.path);
}
}
}
},
[name, paths, handleFetchPaths, onChange]
[name, paths, onFetchPaths, onChange]
);
const handleInputBlur = useCallback(() => {
onChange({
@@ -156,16 +126,16 @@ export function PathInputInternal(props: PathInputInternalProps) {
const handleSuggestionSelected = useCallback(
(_event: SyntheticEvent, { suggestion }: { suggestion: Path }) => {
handleFetchPaths(suggestion.path);
onFetchPaths(suggestion.path);
},
[handleFetchPaths]
[onFetchPaths]
);
const handleSuggestionsFetchRequested = useCallback(
({ value: newValue }: SuggestionsFetchRequestedParams) => {
handleFetchPaths(newValue);
onFetchPaths(newValue);
},
[handleFetchPaths]
[onFetchPaths]
);
const handleFileBrowserOpenPress = useCallback(() => {