1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-18 21:35:27 -04:00

Use react-query for parse

This commit is contained in:
Mark McDowall
2025-11-10 21:02:11 -08:00
parent 19a65d672f
commit 263f4839ab
13 changed files with 55 additions and 333 deletions
-2
View File
@@ -14,7 +14,6 @@ import InteractiveImportAppState from './InteractiveImportAppState';
import MessagesAppState from './MessagesAppState';
import OAuthAppState from './OAuthAppState';
import OrganizePreviewAppState from './OrganizePreviewAppState';
import ParseAppState from './ParseAppState';
import PathsAppState from './PathsAppState';
import ProviderOptionsAppState from './ProviderOptionsAppState';
import ReleasesAppState from './ReleasesAppState';
@@ -91,7 +90,6 @@ interface AppState {
interactiveImport: InteractiveImportAppState;
oAuth: OAuthAppState;
organizePreview: OrganizePreviewAppState;
parse: ParseAppState;
paths: PathsAppState;
providerOptions: ProviderOptionsAppState;
releases: ReleasesAppState;
-45
View File
@@ -1,45 +0,0 @@
.inputContainer {
display: flex;
margin-bottom: 10px;
}
.inputIconContainer {
width: 58px;
height: 46px;
border: 1px solid var(--inputBorderColor);
border-right: none;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--inputIconContainerBackgroundColor);
text-align: center;
line-height: 46px;
}
.input {
composes: input from '~Components/Form/TextInput.css';
height: 46px;
border-radius: 0;
font-size: 18px;
}
.clearButton {
border: 1px solid var(--inputBorderColor);
border-left: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.message {
margin-top: 30px;
text-align: center;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-size: 24px;
}
-12
View File
@@ -1,12 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'clearButton': string;
'helpText': string;
'input': string;
'inputContainer': string;
'inputIconContainer': string;
'message': string;
}
export const cssExports: CssExports;
export default cssExports;
-110
View File
@@ -1,110 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons } from 'Helpers/Props';
import { clear, fetch } from 'Store/Actions/parseActions';
import { InputChanged } from 'typings/inputs';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import ParseResult from './ParseResult';
import parseStateSelector from './parseStateSelector';
import styles from './Parse.css';
function Parse() {
const { isFetching, error, item } = useSelector(parseStateSelector());
const [title, setTitle] = useState('');
const dispatch = useDispatch();
const onInputChange = useCallback(
({ value }: InputChanged<string>) => {
const trimmedValue = value.trim();
setTitle(value);
if (trimmedValue === '') {
dispatch(clear());
} else {
dispatch(fetch({ title: trimmedValue }));
}
},
[setTitle, dispatch]
);
const onClearPress = useCallback(() => {
setTitle('');
dispatch(clear());
}, [setTitle, dispatch]);
useEffect(
() => {
return () => {
dispatch(clear());
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return (
<PageContent title={translate('Parse')}>
<PageContentBody>
<div className={styles.inputContainer}>
<div className={styles.inputIconContainer}>
<Icon name={icons.PARSE} size={20} />
</div>
<TextInput
className={styles.input}
name="title"
value={title}
placeholder="eg. Series.Title.S01E05.720p.HDTV-RlsGroup"
autoFocus={true}
onChange={onInputChange}
/>
<Button className={styles.clearButton} onPress={onClearPress}>
<Icon name={icons.REMOVE} size={20} />
</Button>
</div>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalErrorParsing')}
</div>
<div>{getErrorMessage(error)}</div>
</div>
) : null}
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
<div className={styles.message}>
{translate('ParseModalUnableToParse')}
</div>
) : null}
{!isFetching && !error && item.parsedEpisodeInfo ? (
<ParseResult item={item} />
) : null}
{title ? null : (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalHelpText')}
</div>
<div>{translate('ParseModalHelpTextDetails')}</div>
</div>
)}
</PageContentBody>
</PageContent>
);
}
export default Parse;
+12
View File
@@ -43,3 +43,15 @@
margin-bottom: 10px;
font-size: 24px;
}
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.loading {
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin-top: 0;
}
+2
View File
@@ -6,7 +6,9 @@ interface CssExports {
'input': string;
'inputContainer': string;
'inputIconContainer': string;
'loading': string;
'message': string;
'modalFooter': string;
}
export const cssExports: CssExports;
export default cssExports;
+19 -33
View File
@@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import React, { useCallback, useState } from 'react';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@@ -8,13 +7,13 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import useDebounce from 'Helpers/Hooks/useDebounce';
import { icons } from 'Helpers/Props';
import { clear, fetch } from 'Store/Actions/parseActions';
import { InputChanged } from 'typings/inputs';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import ParseResult from './ParseResult';
import parseStateSelector from './parseStateSelector';
import useParse from './useParse';
import styles from './ParseModalContent.css';
interface ParseModalContentProps {
@@ -23,40 +22,21 @@ interface ParseModalContentProps {
function ParseModalContent(props: ParseModalContentProps) {
const { onModalClose } = props;
const { isFetching, error, item } = useSelector(parseStateSelector());
const [title, setTitle] = useState('');
const dispatch = useDispatch();
const queryTitle = useDebounce(title, title ? 300 : 0);
const { isFetching, isLoading, error, data } = useParse(queryTitle);
const onInputChange = useCallback(
({ value }: InputChanged<string>) => {
const trimmedValue = value.trim();
setTitle(value);
if (trimmedValue === '') {
dispatch(clear());
} else {
dispatch(fetch({ title: trimmedValue }));
}
},
[setTitle, dispatch]
[setTitle]
);
const onClearPress = useCallback(() => {
setTitle('');
dispatch(clear());
}, [setTitle, dispatch]);
useEffect(
() => {
return () => {
dispatch(clear());
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
}, [setTitle]);
return (
<ModalContent onModalClose={onModalClose}>
@@ -82,9 +62,9 @@ function ParseModalContent(props: ParseModalContentProps) {
</Button>
</div>
{isFetching ? <LoadingIndicator /> : null}
{isLoading ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
{!isLoading && !!error ? (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalErrorParsing')}
@@ -93,14 +73,14 @@ function ParseModalContent(props: ParseModalContentProps) {
</div>
) : null}
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
{!isLoading && title && !error && !data.parsedEpisodeInfo ? (
<div className={styles.message}>
{translate('ParseModalUnableToParse')}
</div>
) : null}
{!isFetching && !error && item.parsedEpisodeInfo ? (
<ParseResult item={item} />
{!isLoading && !error && data.parsedEpisodeInfo ? (
<ParseResult item={data} />
) : null}
{title ? null : (
@@ -113,7 +93,13 @@ function ParseModalContent(props: ParseModalContentProps) {
)}
</ModalBody>
<ModalFooter>
<ModalFooter className={styles.modalFooter}>
<div>
{isFetching && !isLoading ? (
<LoadingIndicator className={styles.loading} size={20} />
) : null}
</div>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
@@ -1,5 +1,4 @@
import ModelBase from 'App/ModelBase';
import { AppSectionItemState } from 'App/State/AppSectionState';
import Episode from 'Episode/Episode';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
@@ -48,7 +47,3 @@ export interface ParseModel extends ModelBase {
customFormats?: CustomFormat[];
customFormatScore?: number;
}
type ParseAppState = AppSectionItemState<ParseModel>;
export default ParseAppState;
+1 -1
View File
@@ -1,9 +1,9 @@
import React from 'react';
import { ParseModel } from 'App/State/ParseAppState';
import FieldSet from 'Components/FieldSet';
import EpisodeFormats from 'Episode/EpisodeFormats';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import translate from 'Utilities/String/translate';
import { ParseModel } from './ParseModel';
import ParseResultItem from './ParseResultItem';
import styles from './ParseResult.css';
-12
View File
@@ -1,12 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import ParseAppState from 'App/State/ParseAppState';
export default function parseStateSelector() {
return createSelector(
(state: AppState) => state.parse,
(parse: ParseAppState) => {
return parse;
}
);
}
+21
View File
@@ -0,0 +1,21 @@
import { keepPreviousData } from '@tanstack/react-query';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { ParseModel } from './ParseModel';
const useParse = (title: string) => {
const result = useApiQuery<ParseModel>({
path: '/parse',
queryParams: { title },
queryOptions: {
enabled: title.trim().length > 0,
placeholderData: keepPreviousData,
},
});
return {
...result,
data: result.data ?? ({} as ParseModel),
};
};
export default useParse;
-2
View File
@@ -10,7 +10,6 @@ import * as importSeries from './importSeriesActions';
import * as interactiveImportActions from './interactiveImportActions';
import * as oAuth from './oAuthActions';
import * as organizePreview from './organizePreviewActions';
import * as parse from './parseActions';
import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions';
import * as releases from './releaseActions';
@@ -35,7 +34,6 @@ export default [
interactiveImportActions,
oAuth,
organizePreview,
parse,
paths,
providerOptions,
releases,
-111
View File
@@ -1,111 +0,0 @@
import { Dispatch } from 'redux';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import AppState from 'App/State/AppState';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer';
interface FetchPayload {
title: string;
}
//
// Variables
export const section = 'parse';
let parseTimeout: number | null = null;
let abortCurrentRequest: (() => void) | null = null;
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
item: {},
};
//
// Actions Types
export const FETCH = 'parse/fetch';
export const CLEAR = 'parse/clear';
//
// Action Creators
export const fetch = createThunk(FETCH);
export const clear = createAction(CLEAR);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH]: function (
_getState: () => AppState,
payload: FetchPayload,
dispatch: Dispatch
) {
if (parseTimeout) {
clearTimeout(parseTimeout);
}
parseTimeout = window.setTimeout(async () => {
dispatch(set({ section, isFetching: true }));
if (abortCurrentRequest) {
abortCurrentRequest();
}
const { request, abortRequest } = createAjaxRequest({
url: '/parse',
data: {
title: payload.title,
},
});
try {
const data = await request;
dispatch(
batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null,
}),
])
);
} catch (error) {
dispatch(
set({
section,
isAdding: false,
isAdded: false,
addError: error,
})
);
}
abortCurrentRequest = abortRequest;
}, 300);
},
});
//
// Reducers
export const reducers = createHandleActions(
{
[CLEAR]: createClearReducer(section, defaultState),
},
defaultState,
section
);