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:
@@ -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;
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user