1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-16 21:15:33 -04:00

Compare commits

..

31 Commits

Author SHA1 Message Date
Bogdan
dae5e86b2c Fixed: Skip title searches for Newznab/Torznab indexers when movie year is missing
Prevents useless text searches of `Movie Title 0` when year is missing.

Fixes #10569
2025-06-14 13:20:11 +03:00
Bogdan
71f032d175 Bump Polly to 8.6.0 2025-06-11 23:13:54 +03:00
Bogdan
5a6db29dbd Bump version to 5.27.0 2025-06-11 23:11:20 +03:00
Weblate
2dac2dd35b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Qqqqqquexx <946921515@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2025-06-11 15:01:35 +03:00
Stevie Robinson
b829638a77 Fixed: Include network drive types in Disk Space
(cherry picked from commit 9ffcd141a515e99604881a4ef383dadafef31eeb)
2025-06-10 15:31:34 +03:00
Mark McDowall
b6b7f13839 Prevent should refresh movie metadata from failing
Fixed: Prevent error checking if movie metadata should be refreshed from failing refresh movies task
(cherry picked from commit 3eed84c67938fed308e562e69cf7bcd727063803)
2025-06-10 15:31:34 +03:00
Mark McDowall
a9ad197b75 New: Update wording when removing a root folder
(cherry picked from commit 51c17fd3122f7b96a4155593d465ba32870d0c91)
2025-06-10 15:31:34 +03:00
Mark McDowall
1b28116a7e Fixed: Escape backticks in discord notifications
(cherry picked from commit 70c74fc1769f2094a14faaa103c49d744534be9f)
2025-06-10 15:31:34 +03:00
Bogdan
5870c88e1c Fix fullscreen automation screenshots 2025-06-09 22:05:09 +03:00
Servarr
0629832bd0 Automated API Docs update 2025-06-09 15:22:52 +03:00
Bogdan
430897c710 Fixed: Hide separators when page toolbar shows all buttons on small screens
Fixes #11124
2025-06-09 15:11:45 +03:00
Bogdan
9c42246eef Bump SixLabors.ImageSharp to 3.1.9 2025-06-08 11:32:36 +03:00
Weblate
489a86b253 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-06-08 10:42:02 +03:00
Robert Dailey
9c8d3b679d Add 'qualitydefinition/limits' endpoint to get size limitations
(cherry picked from commit 24f03fc1e96eba215f96312c791cf167f10499c7)
2025-06-08 10:41:37 +03:00
Bogdan
b2e51d1613 Bump version to 5.26.2 2025-06-08 10:30:06 +03:00
Michael Peleshenko
a95b1f2992 Fixed: Handling movies with empty IMDB IDs in lists clean library 2025-06-07 11:35:44 +03:00
Mark McDowall
ac33b15048 Convert Tags to TypeScript
(cherry picked from commit 60529f0bacf2398838ef8d7843490a35046a1093)
2025-06-04 22:16:24 +03:00
Bogdan
d28f03af28 Fixed: Allow more prefixes and suffixes for Release Year naming token 2025-06-04 19:50:08 +03:00
Bogdan
73b99d0be2 Add translation for missing movies count from collection 2025-06-04 18:54:09 +03:00
Stevie Robinson
15c34a61de New: Ability to clone Import Lists
(cherry picked from commit 2314d0b506e30d3a965497a052bc5e54fa0beb81)

Closes #10948
2025-06-04 18:34:13 +03:00
Mark McDowall
b99c536306 Convert ImportLists to TypeScript
(cherry picked from commit 10e3a237ef972540abcf4348bb56973d7ee19bd7)
2025-06-04 18:28:50 +03:00
Mark McDowall
2ebf391f85 Convert Media Management settings to TypeScript
(cherry picked from commit 27f81117ed188712600d8daf3ccb5121f9808458)
2025-06-04 17:50:00 +03:00
Mark McDowall
3945a2eeb8 Convert Indexer settings to TypeScript
(cherry picked from commit 6e008a8e855e67bb14b0e04bdb9042eebcacb59f)
2025-06-04 15:57:46 +03:00
Mark McDowall
e6980df590 Convert SettingsToolbar to TypeScript
(cherry picked from commit fd09ca6e719a96f760006ed0f08756faa20b6f75)
2025-06-04 14:43:30 +03:00
nuxen
187dd79b9c Fixed: Allow opening curly bracket as prefix in naming format 2025-06-03 16:27:31 +03:00
Bogdan
22ef334de6 Fix translation token for root folders load error 2025-06-03 15:22:38 +03:00
Weblate
c9eb9b8b98 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Tur3Q <andrejturan@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: cyrille <oscarboehringer@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-06-03 14:31:18 +03:00
Bogdan
9c74c40fc6 Fixed: Quality sliders on some browsers
Fixes #11109
2025-06-01 18:07:25 +03:00
Mark McDowall
8911cbe872 Sync react-slider props for Quality sliders with upstream
(cherry picked from commit 9dab2ba6e4316879e4db8db47363476a5c4f13b2)
2025-06-01 17:54:31 +03:00
Ghworg
7e541d4653 Fixed: Display media info bitrates in bits (#11087) 2025-06-01 14:50:53 +03:00
Bogdan
1cc2237ac0 Bump version to 5.26.1 2025-06-01 10:39:43 +03:00
209 changed files with 5589 additions and 6570 deletions

9
.gitignore vendored
View File

@@ -165,15 +165,12 @@ Thumbs.db
/tools/Addins/*
packages.config.md5sum
# Common IntelliJ Platform excludes
# Ignore Rider projects completely for now
.idea/
# ignore node_modules symlink
node_modules
node_modules.nosync
# API doc generation
.config/
# Ignore Jetbrains IntelliJ Workspace Directories
.idea/

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.26.0'
majorVersion: '5.27.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'

View File

@@ -108,7 +108,7 @@ class ImportMovie extends Component {
{
!rootFoldersFetching && !!rootFoldersError ?
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
{translate('RootFoldersLoadError')}
</Alert> :
null
}

View File

@@ -93,7 +93,7 @@ class ImportMovieSelectFolder extends Component {
{
!isFetching && error ?
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
{translate('RootFoldersLoadError')}
</Alert> :
null
}

View File

@@ -15,9 +15,9 @@ import MovieIndex from 'Movie/Index/MovieIndex';
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import ImportListSettings from 'Settings/ImportLists/ImportListSettings';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import MediaManagement from 'Settings/MediaManagement/MediaManagement';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles';
@@ -99,10 +99,7 @@ function AppRoutes() {
<Route exact={true} path="/settings" component={Settings} />
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route path="/settings/mediamanagement" component={MediaManagement} />
<Route path="/settings/profiles" component={Profiles} />
@@ -113,17 +110,14 @@ function AppRoutes() {
component={CustomFormatSettingsPage}
/>
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
<Route path="/settings/indexers" component={IndexerSettings} />
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/importlists"
component={ImportListSettingsConnector}
/>
<Route path="/settings/importlists" component={ImportListSettings} />
<Route path="/settings/connect" component={NotificationSettings} />

View File

@@ -43,9 +43,15 @@ export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: {
items: T[];
};
schema: T[];
selectedSchema?: T;
}
export interface AppSectionItemSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: T;
}
export interface AppSectionItemState<T> {
@@ -61,9 +67,10 @@ export interface AppSectionProviderState<T>
AppSectionSaveState {
isFetching: boolean;
isPopulated: boolean;
isTesting?: boolean;
error: Error;
items: T[];
pendingChanges: Partial<T>;
pendingChanges?: Partial<T>;
}
interface AppSectionState<T> {

View File

@@ -1,12 +1,15 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemSchemaState,
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
PagedAppSectionState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
import CustomFormat from 'typings/CustomFormat';
import DelayProfile from 'typings/DelayProfile';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
@@ -16,12 +19,34 @@ import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import IndexerOptions from 'typings/Settings/IndexerOptions';
import MediaManagement from 'typings/Settings/MediaManagement';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
type Presets<T> = T & {
presets: T[];
};
export interface AutoTaggingAppState
extends AppSectionState<AutoTagging>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface AutoTaggingSpecificationAppState
extends AppSectionState<AutoTaggingSpecification>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<AutoTaggingSpecification> {}
export interface DelayProfileAppState
extends AppSectionState<DelayProfile>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
@@ -33,6 +58,10 @@ export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface MediaManagementAppState
extends AppSectionItemState<MediaManagement>,
AppSectionSaveState {}
export interface NamingAppState
extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {}
@@ -42,12 +71,20 @@ export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<Presets<ImportList>> {
isTestingAll: boolean;
}
export interface IndexerOptionsAppState
extends AppSectionItemState<IndexerOptions>,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {
AppSectionSaveState,
AppSectionSchemaState<Presets<Indexer>> {
isTestingAll: boolean;
}
@@ -57,7 +94,7 @@ export interface NotificationAppState
export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
AppSectionItemSchemaState<QualityProfile> {}
export interface ReleaseProfilesAppState
extends AppSectionState<ReleaseProfile>,
@@ -88,15 +125,20 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
autoTaggings: AutoTaggingAppState;
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
customFormats: CustomFormatAppState;
delayProfiles: DelayProfileAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexerOptions: IndexerOptionsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
mediaManagement: MediaManagementAppState;
metadata: MetadataAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;

View File

@@ -17,7 +17,7 @@ export interface TagDetail extends ModelBase {
indexerIds: number[];
movieIds: number[];
notificationIds: number[];
restrictionIds: number[];
releaseProfileIds: number[];
}
export interface TagDetailAppState

View File

@@ -196,7 +196,7 @@ class CollectionOverview extends Component {
size={13}
/>
<span className={styles.status}>
{`${missingMovies} missing movie(s)`}
{translate('CountMissingMoviesFromLibrary', { count: missingMovies })}
</span>
</Label>

View File

@@ -5,18 +5,20 @@ import { ValidationError, ValidationWarning } from 'typings/pending';
import styles from './Form.css';
export interface FormProps {
id?: string;
children: ReactNode;
validationErrors?: ValidationError[];
validationWarnings?: ValidationWarning[];
}
function Form({
id,
children,
validationErrors = [],
validationWarnings = [],
}: FormProps) {
return (
<div>
<div id={id}>
{validationErrors.length || validationWarnings.length ? (
<div className={styles.validationFailures}>
{validationErrors.map((error, index) => {

View File

@@ -18,7 +18,7 @@ function createQualityProfilesSelector(
includeMixed: boolean
) {
return createSelector(
createSortedSectionSelector(
createSortedSectionSelector<QualityProfile, QualityProfilesAppState>(
'settings.qualityProfiles',
sortByProp<QualityProfile, 'name'>('name')
),

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { Error } from 'App/State/AppSectionState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
@@ -6,7 +7,7 @@ import { kinds } from 'Helpers/Props';
interface PageSectionContentProps {
isFetching: boolean;
isPopulated: boolean;
error?: object;
error?: Error;
errorMessage: string;
children: React.ReactNode;
}
@@ -18,7 +19,7 @@ function PageSectionContent({
errorMessage,
children,
}: PageSectionContentProps) {
if (isFetching) {
if (isFetching && !isPopulated) {
return <LoadingIndicator />;
}

View File

@@ -16,10 +16,10 @@ const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
interface PageToolbarSectionProps {
export interface PageToolbarSectionProps {
children?:
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)[];
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)[];
alignContent?: Extract<Align, keyof typeof styles>;
collapseButtons?: boolean;
}
@@ -80,8 +80,12 @@ function PageToolbarSection({
if (buttonCount - 1 === maxButtons) {
const overflowItems: PageToolbarButtonProps[] = [];
const buttonsWithoutSeparators = validChildren.filter(
(child) => Object.keys(child.props).length > 0
);
return {
buttons: validChildren,
buttons: buttonsWithoutSeparators,
buttonCount,
overflowItems,
};

View File

@@ -0,0 +1,8 @@
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
function useIsWindows() {
return useSelector((state: AppState) => state.system.status.item.isWindows);
}
export default useIsWindows;

View File

@@ -0,0 +1,8 @@
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
function useShowAdvancedSettings() {
return useSelector((state: AppState) => state.settings.advancedSettings);
}
export default useShowAdvancedSettings;

View File

@@ -1,149 +0,0 @@
// https://github.com/react-bootstrap/react-element-children
import React from 'react';
/**
* Iterates through children that are typically specified as `props.children`,
* but only maps over children that are "valid components".
*
* The mapFunction provided index will be normalised to the components mapped,
* so an invalid component would not increase the index.
*
* @param {?*} children Children tree container.
* @param {function(*, int)} func.
* @param {*} context Context for func.
* @return {object} Object containing the ordered map of results.
*/
export function map(children, func, context) {
let index = 0;
return React.Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return child;
}
return func.call(context, child, index++);
});
}
/**
* Iterates through children that are "valid components".
*
* The provided forEachFunc(child, index) will be called for each
* leaf child with the index reflecting the position relative to "valid components".
*
* @param {?*} children Children tree container.
* @param {function(*, int)} func.
* @param {*} context Context for context.
*/
export function forEach(children, func, context) {
let index = 0;
React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) {
return;
}
func.call(context, child, index++);
});
}
/**
* Count the number of "valid components" in the Children container.
*
* @param {?*} children Children tree container.
* @returns {number}
*/
export function count(children) {
let result = 0;
React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) {
return;
}
++result;
});
return result;
}
/**
* Finds children that are typically specified as `props.children`,
* but only iterates over children that are "valid components".
*
* The provided forEachFunc(child, index) will be called for each
* leaf child with the index reflecting the position relative to "valid components".
*
* @param {?*} children Children tree container.
* @param {function(*, int)} func.
* @param {*} context Context for func.
* @returns {array} of children that meet the func return statement
*/
export function filter(children, func, context) {
const result = [];
forEach(children, (child, index) => {
if (func.call(context, child, index)) {
result.push(child);
}
});
return result;
}
export function find(children, func, context) {
let result = null;
forEach(children, (child, index) => {
if (result) {
return;
}
if (func.call(context, child, index)) {
result = child;
}
});
return result;
}
export function every(children, func, context) {
let result = true;
forEach(children, (child, index) => {
if (!result) {
return;
}
if (!func.call(context, child, index)) {
result = false;
}
});
return result;
}
export function some(children, func, context) {
let result = false;
forEach(children, (child, index) => {
if (result) {
return;
}
if (func.call(context, child, index)) {
result = true;
}
});
return result;
}
export function toArray(children) {
const result = [];
forEach(children, (child) => {
result.push(child);
});
return result;
}

View File

@@ -1,3 +0,0 @@
export default function getDisplayName(Component) {
return Component.displayName || Component.name || 'Component';
}

View File

@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
@@ -154,7 +154,7 @@ function MovieCastPoster(props: MovieCastPosterProps) {
{character}
</div>
<EditImportListModalConnector
<EditImportListModal
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}

View File

@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
@@ -152,7 +152,7 @@ function MovieCrewPoster(props: MovieCrewPosterProps) {
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
<EditImportListModalConnector
<EditImportListModal
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}

View File

@@ -82,9 +82,9 @@ function RootFolderRow(props: RootFolderRowProps) {
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', { path })}
confirmLabel={translate('Delete')}
title={translate('RemoveRootFolder')}
message={translate('RemoveRootFolderMoviesMessageText', { path })}
confirmLabel={translate('Remove')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>

View File

@@ -1,70 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) {
const {
advancedSettings,
onAdvancedSettingsPress,
showLabel
} = props;
return (
<Link
className={styles.button}
title={advancedSettings ? translate('ShownClickToHide') : translate('HiddenClickToShow')}
onPress={onAdvancedSettingsPress}
>
<Icon
name={icons.ADVANCED_SETTINGS}
size={21}
/>
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
className={styles.indicatorBackground}
name={icons.CIRCLE}
size={16}
/>
<Icon
className={advancedSettings ? styles.enabled : styles.disabled}
name={advancedSettings ? icons.CHECK : icons.CLOSE}
size={10}
/>
</span>
{
showLabel ?
<div className={styles.labelContainer}>
<div className={styles.label}>
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
</div>
</div> :
null
}
</Link>
);
}
AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
showLabel: PropTypes.bool.isRequired
};
AdvancedSettingsButton.defaultProps = {
showLabel: true
};
export default AdvancedSettingsButton;

View File

@@ -0,0 +1,67 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import translate from 'Utilities/String/translate';
import styles from './AdvancedSettingsButton.css';
interface AdvancedSettingsButtonProps {
showLabel: boolean;
}
function AdvancedSettingsButton({ showLabel }: AdvancedSettingsButtonProps) {
const showAdvancedSettings = useSelector(
(state: AppState) => state.settings.advancedSettings
);
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(toggleAdvancedSettings());
}, [dispatch]);
return (
<Link
className={styles.button}
title={
showAdvancedSettings
? translate('ShownClickToHide')
: translate('HiddenClickToShow')
}
onPress={handlePress}
>
<Icon name={icons.ADVANCED_SETTINGS} size={21} />
<span
className={classNames(styles.indicatorContainer, 'fa-layers fa-fw')}
>
<Icon
className={styles.indicatorBackground}
name={icons.CIRCLE}
size={16}
/>
<Icon
className={showAdvancedSettings ? styles.enabled : styles.disabled}
name={showAdvancedSettings ? icons.CHECK : icons.CLOSE}
size={10}
/>
</span>
{showLabel ? (
<div className={styles.labelContainer}>
<div className={styles.label}>
{showAdvancedSettings
? translate('HideAdvanced')
: translate('ShowAdvanced')}
</div>
</div>
) : null}
</Link>
);
}
export default AdvancedSettingsButton;

View File

@@ -5,7 +5,7 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import ParseToolbarButton from 'Parse/ParseToolbarButton';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
@@ -13,9 +13,7 @@ import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCusto
function CustomFormatSettingsPage() {
return (
<PageContent title={translate('CustomFormatsSettings')}>
<SettingsToolbarConnector
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<SettingsToolbar
showSave={false}
additionalButtons={
<>

View File

@@ -5,7 +5,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
@@ -71,7 +71,7 @@ class DownloadClientSettings extends Component {
return (
<PageContent title={translate('DownloadClientSettings')}>
<SettingsToolbarConnector
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={

View File

@@ -38,7 +38,6 @@ class EditDownloadClientModalContent extends Component {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteDownloadClientPress,
...otherProps
} = this.props;
@@ -198,8 +197,6 @@ class EditDownloadClientModalContent extends Component {
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
@@ -243,7 +240,6 @@ EditDownloadClientModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func
};

View File

@@ -6,8 +6,7 @@ import {
saveDownloadClient,
setDownloadClientFieldValue,
setDownloadClientValue,
testDownloadClient,
toggleAdvancedSettings
testDownloadClient
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -29,8 +28,7 @@ const mapDispatchToProps = {
setDownloadClientValue,
setDownloadClientFieldValue,
saveDownloadClient,
testDownloadClient,
toggleAdvancedSettings
testDownloadClient
};
class EditDownloadClientModalContentConnector extends Component {
@@ -63,10 +61,6 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -76,7 +70,6 @@ class EditDownloadClientModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -94,7 +87,6 @@ EditDownloadClientModalContentConnector.propTypes = {
setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired,
testDownloadClient: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -8,7 +8,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { kinds } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import AnalyticSettings from './AnalyticSettings';
import BackupSettings from './BackupSettings';
@@ -113,7 +113,7 @@ class GeneralSettings extends Component {
return (
<PageContent title={translate('GeneralSettings')}>
<SettingsToolbarConnector
<SettingsToolbar
{...otherProps}
/>

View File

@@ -46,7 +46,6 @@ function createImportListExclusionSelector(id?: number) {
const settings = selectSettings(mapping, pendingChanges, saveError);
return {
id,
isFetching,
error,
isSaving,
@@ -171,7 +170,7 @@ function EditImportListExclusionModalContent({
</ModalBody>
<ModalFooter>
{id && (
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
@@ -179,7 +178,7 @@ function EditImportListExclusionModalContent({
>
{translate('Delete')}
</Button>
)}
) : null}
<Button onPress={onModalClose}>{translate('Cancel')}</Button>

View File

@@ -1,123 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import ImportListExclusions from './ImportListExclusions/ImportListExclusions';
import ImportListsConnector from './ImportLists/ImportListsConnector';
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
import ImportListOptions from './Options/ImportListOptions';
class ImportListSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false,
isManageImportListsOpen: false
};
}
//
// Listeners
setChildSave = (saveCallback) => {
this._saveCallback = saveCallback;
};
onChildStateChange = (payload) => {
this.setState(payload);
};
onManageImportListsPress = () => {
this.setState({ isManageImportListsOpen: true });
};
onManageImportListsModalClose = () => {
this.setState({ isManageImportListsOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
}
};
//
// Render
render() {
const {
isTestingAll,
dispatchTestAllImportLists
} = this.props;
const {
isSaving,
hasPendingChanges,
isManageImportListsOpen
} = this.state;
return (
<PageContent title={translate('ImportListSettings')}>
<SettingsToolbarConnector
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllLists')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={dispatchTestAllImportLists}
/>
<PageToolbarButton
label={translate('ManageLists')}
iconName={icons.MANAGE}
onPress={this.onManageImportListsPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
/>
<PageContentBody>
<ImportListsConnector />
<ImportListOptions
setChildSave={this.setChildSave}
onChildStateChange={this.onChildStateChange}
/>
<ImportListExclusions />
<ManageImportListsModal
isOpen={isManageImportListsOpen}
onModalClose={this.onManageImportListsModalClose}
/>
</PageContentBody>
</PageContent>
);
}
}
ImportListSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
dispatchTestAllImportLists: PropTypes.func.isRequired
};
export default ImportListSettings;

View File

@@ -0,0 +1,107 @@
import React, { useCallback, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import { testAllImportLists } from 'Store/Actions/settingsActions';
import {
SaveCallback,
SettingsStateChange,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
import ImportListExclusions from './ImportListExclusions/ImportListExclusions';
import ImportLists from './ImportLists/ImportLists';
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
import ImportListOptions from './Options/ImportListOptions';
function ImportListSettings() {
const dispatch = useDispatch();
const isTestingAll = useSelector(
(state: AppState) => state.settings.importLists.isTestingAll
);
const saveOptions = useRef<() => void>();
const [isSaving, setIsSaving] = useState(false);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const [isManageImportListsModalOpen, setIsManageImportListsModalOpen] =
useState(false);
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
saveOptions.current = saveCallback;
}, []);
const handleChildStateChange = useCallback(
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
setIsSaving(isSaving);
setHasPendingChanges(hasPendingChanges);
},
[]
);
const handleManageImportListsPress = useCallback(() => {
setIsManageImportListsModalOpen(true);
}, []);
const handleManageImportListsModalClose = useCallback(() => {
setIsManageImportListsModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
saveOptions.current?.();
}, []);
const handleTestAllIndexersPress = useCallback(() => {
dispatch(testAllImportLists());
}, [dispatch]);
return (
<PageContent title={translate('ImportListSettings')}>
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllLists')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={handleTestAllIndexersPress}
/>
<PageToolbarButton
label={translate('ManageLists')}
iconName={icons.MANAGE}
onPress={handleManageImportListsPress}
/>
</>
}
onSavePress={handleSavePress}
/>
<PageContentBody>
<ImportLists />
<ImportListOptions
setChildSave={handleSetChildSave}
onChildStateChange={handleChildStateChange}
/>
<ImportListExclusions />
<ManageImportListsModal
isOpen={isManageImportListsModalOpen}
onModalClose={handleManageImportListsModalClose}
/>
</PageContentBody>
</PageContent>
);
}
export default ImportListSettings;

View File

@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { testAllImportLists } from 'Store/Actions/settingsActions';
import ImportListSettings from './ImportListSettings';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.importLists.isTestingAll,
(isTestingAll) => {
return {
isTestingAll
};
}
);
}
const mapDispatchToProps = {
dispatchTestAllImportLists: testAllImportLists
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListSettings);

View File

@@ -1,117 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddImportListPresetMenuItem from './AddImportListPresetMenuItem';
import styles from './AddImportListItem.css';
class AddImportListItem extends Component {
//
// Listeners
onImportListSelect = () => {
const {
implementation,
implementationName,
minRefreshInterval
} = this.props;
this.props.onImportListSelect({ implementation, implementationName, minRefreshInterval });
};
//
// Render
render() {
const {
implementation,
implementationName,
minRefreshInterval,
infoLink,
presets,
onImportListSelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.list}
>
<Link
className={styles.underlay}
onPress={this.onImportListSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onImportListSelect}
>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
{translate('Presets')}
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddImportListPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
minRefreshInterval={minRefreshInterval}
onPress={onImportListSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddImportListItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
minRefreshInterval: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onImportListSelect: PropTypes.func.isRequired
};
export default AddImportListItem;

View File

@@ -0,0 +1,91 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import { selectImportListSchema } from 'Store/Actions/settingsActions';
import ImportList from 'typings/ImportList';
import translate from 'Utilities/String/translate';
import AddImportListPresetMenuItem from './AddImportListPresetMenuItem';
import styles from './AddImportListItem.css';
interface AddImportListItemProps {
implementation: string;
implementationName: string;
minRefreshInterval: string;
infoLink: string;
presets?: ImportList[];
onImportListSelect: () => void;
}
function AddImportListItem({
implementation,
implementationName,
minRefreshInterval,
infoLink,
presets,
onImportListSelect,
}: AddImportListItemProps) {
const dispatch = useDispatch();
const hasPresets = !!(presets && presets.length);
const handleImportListSelect = useCallback(() => {
dispatch(
selectImportListSchema({
implementation,
implementationName,
})
);
onImportListSelect();
}, [implementation, implementationName, dispatch, onImportListSelect]);
return (
<div className={styles.list}>
<Link className={styles.underlay} onPress={handleImportListSelect} />
<div className={styles.overlay}>
<div className={styles.name}>{implementationName}</div>
<div className={styles.actions}>
{hasPresets && (
<span>
<Button size={sizes.SMALL} onPress={handleImportListSelect}>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
{translate('Presets')}
</Button>
<MenuContent>
{presets.map((preset) => {
return (
<AddImportListPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
minRefreshInterval={minRefreshInterval}
onPress={onImportListSelect}
/>
);
})}
</MenuContent>
</Menu>
</span>
)}
<Button to={infoLink} size={sizes.SMALL}>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
export default AddImportListItem;

View File

@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddImportListModalContentConnector from './AddImportListModalContentConnector';
function AddImportListModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddImportListModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddImportListModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddImportListModal;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddImportListModalContent, {
AddImportListModalContentProps,
} from './AddImportListModalContent';
interface AddImportListModalProps extends AddImportListModalContentProps {
isOpen: boolean;
}
function AddImportListModal({
isOpen,
onModalClose,
...otherProps
}: AddImportListModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<AddImportListModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default AddImportListModal;

View File

@@ -1,115 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { kinds } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import AddImportListItem from './AddImportListItem';
import styles from './AddImportListModalContent.css';
class AddImportListModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
listGroups,
onImportListSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddImportList')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching ?
<LoadingIndicator /> :
null
}
{
!isSchemaFetching && !!schemaError ?
<Alert kind={kinds.DANGER}>
{translate('AddListError')}
</Alert> :
null
}
{
isSchemaPopulated && !schemaError ?
<div>
<Alert kind={kinds.INFO}>
<div>
{translate('SupportedListsMovie')}
</div>
<div>
{translate('SupportedListsMoreInfo')}
</div>
</Alert>
{
Object.keys(listGroups).map((key) => {
return (
<FieldSet key={key} legend={translate('TypeOfList', {
typeOfList: titleCase(key)
})}
>
<div className={styles.lists}>
{
listGroups[key].map((list) => {
return (
<AddImportListItem
key={list.implementation}
implementation={list.implementation}
{...list}
onImportListSelect={onImportListSelect}
/>
);
})
}
</div>
</FieldSet>
);
})
}
</div> :
null
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddImportListModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
listGroups: PropTypes.object.isRequired,
onImportListSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddImportListModalContent;

View File

@@ -0,0 +1,108 @@
import React, { useEffect, useMemo } 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 Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { kinds } from 'Helpers/Props';
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
import ImportList from 'typings/ImportList';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import AddImportListItem from './AddImportListItem';
import styles from './AddImportListModalContent.css';
export interface AddImportListModalContentProps {
onImportListSelect: () => void;
onModalClose: () => void;
}
function AddImportListModalContent({
onImportListSelect,
onModalClose,
}: AddImportListModalContentProps) {
const dispatch = useDispatch();
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
useSelector((state: AppState) => state.settings.importLists);
const listGroups = useMemo(() => {
const result = schema.reduce<Record<string, ImportList[]>>((acc, item) => {
if (!acc[item.listType]) {
acc[item.listType] = [];
}
acc[item.listType].push(item);
return acc;
}, {});
// Sort the lists by listOrder after grouping them
Object.keys(result).forEach((key) => {
result[key].sort((a, b) => {
return a.listOrder - b.listOrder;
});
});
return result;
}, [schema]);
useEffect(() => {
dispatch(fetchImportListSchema());
}, [dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddImportList')}</ModalHeader>
<ModalBody>
{isSchemaFetching ? <LoadingIndicator /> : null}
{!isSchemaFetching && !!schemaError ? (
<Alert kind={kinds.DANGER}>{translate('AddListError')}</Alert>
) : null}
{isSchemaPopulated && !schemaError ? (
<div>
<Alert kind={kinds.INFO}>
<div>{translate('SupportedListsMovie')}</div>
<div>{translate('SupportedListsMoreInfo')}</div>
</Alert>
{Object.keys(listGroups).map((key) => {
return (
<FieldSet
key={key}
legend={translate('TypeOfList', {
typeOfList: titleCase(key),
})}
>
<div className={styles.lists}>
{listGroups[key].map((list) => {
return (
<AddImportListItem
key={list.implementation}
{...list}
implementation={list.implementation}
onImportListSelect={onImportListSelect}
/>
);
})}
</div>
</FieldSet>
);
})}
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default AddImportListModalContent;

View File

@@ -1,76 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchImportListSchema, selectImportListSchema } from 'Store/Actions/settingsActions';
import AddImportListModalContent from './AddImportListModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.importLists,
(importLists) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = importLists;
const listGroups = _.chain(schema)
.sortBy((o) => o.listOrder)
.groupBy('listType')
.value();
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
listGroups
};
}
);
}
const mapDispatchToProps = {
fetchImportListSchema,
selectImportListSchema
};
class AddImportListModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchImportListSchema();
}
//
// Listeners
onImportListSelect = ({ implementation, implementationName, name, minRefreshInterval }) => {
this.props.selectImportListSchema({ implementation, implementationName, presetName: name, minRefreshInterval });
this.props.onModalClose({ listSelected: true });
};
//
// Render
render() {
return (
<AddImportListModalContent
{...this.props}
onImportListSelect={this.onImportListSelect}
/>
);
}
}
AddImportListModalContentConnector.propTypes = {
fetchImportListSchema: PropTypes.func.isRequired,
selectImportListSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddImportListModalContentConnector);

View File

@@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddImportListPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation,
implementationName,
minRefreshInterval
} = this.props;
this.props.onPress({
name,
implementation,
implementationName,
minRefreshInterval
});
};
//
// Render
render() {
const {
name,
implementation,
implementationName,
minRefreshInterval,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddImportListPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
minRefreshInterval: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddImportListPresetMenuItem;

View File

@@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem';
import { selectImportListSchema } from 'Store/Actions/settingsActions';
interface AddImportListPresetMenuItemProps
extends Omit<MenuItemProps, 'children'> {
name: string;
implementation: string;
implementationName: string;
minRefreshInterval: string;
onPress: () => void;
}
function AddImportListPresetMenuItem({
name,
implementation,
implementationName,
minRefreshInterval,
onPress,
...otherProps
}: AddImportListPresetMenuItemProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(
selectImportListSchema({
implementation,
implementationName,
presetName: name,
})
);
onPress();
}, [name, implementation, implementationName, dispatch, onPress]);
return (
<MenuItem {...otherProps} onPress={handlePress}>
{name}
</MenuItem>
);
}
export default AddImportListPresetMenuItem;

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditImportListModalContentConnector from './EditImportListModalContentConnector';
function EditImportListModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditImportListModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditImportListModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditImportListModal;

View File

@@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
cancelSaveImportList,
cancelTestImportList,
} from 'Store/Actions/settingsActions';
import EditImportListModalContent, {
EditImportListModalContentProps,
} from './EditImportListModalContent';
const section = 'settings.importLists';
interface EditImportListModalProps extends EditImportListModalContentProps {
isOpen: boolean;
}
function EditImportListModal({
isOpen,
onModalClose,
...otherProps
}: EditImportListModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section }));
dispatch(cancelTestImportList({ section }));
dispatch(cancelSaveImportList({ section }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<EditImportListModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditImportListModal;

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveImportList, cancelTestImportList } from 'Store/Actions/settingsActions';
import EditImportListModal from './EditImportListModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.importLists';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestImportList() {
dispatch(cancelTestImportList({ section }));
},
dispatchCancelSaveImportList() {
dispatch(cancelSaveImportList({ section }));
}
};
}
class EditImportListModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestImportList();
this.props.dispatchCancelSaveImportList();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestImportList,
dispatchCancelSaveImportList,
...otherProps
} = this.props;
return (
<EditImportListModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditImportListModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestImportList: PropTypes.func.isRequired,
dispatchCancelSaveImportList: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditImportListModalConnector);

View File

@@ -1,308 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css';
function EditImportListModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteImportListPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
enabled,
enableAuto,
minRefreshInterval,
monitor,
minimumAvailability,
qualityProfileId,
rootFolderPath,
searchOnAdd,
tags,
fields,
message
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? translate('EditImportListImplementation', { implementationName }) : translate('AddImportListImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{
isFetching ?
<LoadingIndicator /> :
null
}
{
!isFetching && !!error ?
<Alert kind={kinds.DANGER}>
{translate('AddListError')}
</Alert> :
null
}
{
!isFetching && !error ?
<Form
{...otherProps}
>
{
!!message &&
<Alert
className={styles.message}
kind={message.value.type}
>
{message.value.message}
</Alert>
}
<Alert
kind={kinds.INFO}
className={styles.message}
>
{translate('ListWillRefreshEveryInterval', {
refreshInterval: formatShortTimeSpan(minRefreshInterval.value)
})}
</Alert>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText={translate('ListEnabledHelpText')}
{...enabled}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAuto"
helpText={translate('EnableAutomaticAddMovieHelpText')}
{...enableAuto}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Monitor')}</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor"
helpText={translate('ListMonitorMovieHelpText')}
{...monitor}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchOnAdd"
helpText={translate('ListSearchOnAddMovieHelpText')}
{...searchOnAdd}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
onChange={onInputChange}
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
helpText={translate('ListQualityProfileHelpText')}
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
helpText={translate('ListRootFolderHelpText')}
{...rootFolderPath}
includeMissingValue={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RadarrTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('ListTagsHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="importList"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
</Form> :
null
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteImportListPress}
>
{translate('Delete')}
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditImportListModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteImportListPress: PropTypes.func
};
export default EditImportListModalContent;

View File

@@ -0,0 +1,313 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import { ImportListAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 Popover from 'Components/Tooltip/Popover';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
} from 'Store/Actions/settingsActions';
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
import ImportList from 'typings/ImportList';
import { InputChanged } from 'typings/inputs';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css';
export interface EditImportListModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteImportListPress?: () => void;
}
function EditImportListModalContent({
id,
onModalClose,
onDeleteImportListPress,
}: EditImportListModalContentProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isFetching,
isSaving,
isTesting = false,
error,
saveError,
item,
validationErrors,
validationWarnings,
} = useSelector(
createProviderSettingsSelectorHook<ImportList, ImportListAppState>(
'importLists',
id
)
);
const wasSaving = usePrevious(isSaving);
const {
implementationName,
name,
enabled,
enableAuto,
minRefreshInterval,
monitor,
minimumAvailability,
rootFolderPath,
qualityProfileId,
searchOnAdd,
tags,
fields,
} = item;
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setImportListValue(change));
},
[dispatch]
);
const handleFieldChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setImportListFieldValue(change));
},
[dispatch]
);
const handleTestPress = useCallback(() => {
dispatch(testImportList({ id }));
}, [id, dispatch]);
const handleSavePress = useCallback(() => {
dispatch(saveImportList({ id }));
}, [id, dispatch]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id
? translate('EditImportListImplementation', { implementationName })
: translate('AddImportListImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('AddListError')}</Alert>
) : null}
{!isFetching && !error ? (
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<Alert kind={kinds.INFO} className={styles.message}>
{translate('ListWillRefreshEveryInterval', {
refreshInterval: formatShortTimeSpan(minRefreshInterval.value),
})}
</Alert>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText={translate('ListEnabledHelpText')}
{...enabled}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAuto"
helpText={translate('EnableAutomaticAddMovieHelpText')}
{...enableAuto}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Monitor')}</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor"
helpText={translate('ListMonitorMovieHelpText')}
{...monitor}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchOnAdd"
helpText={translate('ListSearchOnAddMovieHelpText')}
{...searchOnAdd}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon className={styles.labelIcon} name={icons.INFO} />
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
helpText={translate('ListQualityProfileHelpText')}
{...qualityProfileId}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
helpText={translate('ListRootFolderHelpText')}
{...rootFolderPath}
includeMissingValue={true}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RadarrTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('ListTagsHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
{fields?.length ? (
<div>
{fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
{...field}
advancedSettings={showAdvancedSettings}
provider="importList"
providerData={item}
onChange={handleFieldChange}
/>
);
})}
</div>
) : null}
</Form>
) : null}
</ModalBody>
<ModalFooter>
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteImportListPress}
>
{translate('Delete')}
</Button>
) : null}
<AdvancedSettingsButton showLabel={false} />
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={handleTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditImportListModalContent;

View File

@@ -1,101 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditImportListModalContent from './EditImportListModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('importLists'),
(advancedSettings, importList) => {
return {
advancedSettings,
...importList
};
}
);
}
const mapDispatchToProps = {
setImportListValue,
setImportListFieldValue,
saveImportList,
testImportList,
toggleAdvancedSettings
};
class EditImportListModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setImportListValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setImportListFieldValue({ name, value });
};
onSavePress = () => {
this.props.saveImportList({ id: this.props.id });
};
onTestPress = () => {
this.props.testImportList({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
render() {
return (
<EditImportListModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditImportListModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setImportListValue: PropTypes.func.isRequired,
setImportListFieldValue: PropTypes.func.isRequired,
saveImportList: PropTypes.func.isRequired,
testImportList: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListModalContentConnector);

View File

@@ -4,6 +4,11 @@
width: 290px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@@ -12,6 +17,12 @@
font-size: 24px;
}
.cloneButton {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.enabled {
display: flex;
flex-wrap: wrap;

View File

@@ -1,9 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cloneButton': string;
'enabled': string;
'list': string;
'name': string;
'nameContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,143 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import EditImportListModalConnector from './EditImportListModalConnector';
import styles from './ImportList.css';
class ImportList extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditImportListModalOpen: false,
isDeleteImportListModalOpen: false
};
}
//
// Listeners
onEditImportListPress = () => {
this.setState({ isEditImportListModalOpen: true });
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
onDeleteImportListPress = () => {
this.setState({
isEditImportListModalOpen: false,
isDeleteImportListModalOpen: true
});
};
onDeleteImportListModalClose = () => {
this.setState({ isDeleteImportListModalOpen: false });
};
onConfirmDeleteImportList = () => {
this.props.onConfirmDeleteImportList(this.props.id);
};
//
// Render
render() {
const {
id,
name,
enabled,
enableAuto,
tags,
tagList,
minRefreshInterval
} = this.props;
return (
<Card
className={styles.list}
overlayContent={true}
onPress={this.onEditImportListPress}
>
<div className={styles.name}>
{name}
</div>
<div className={styles.enabled}>
{
enabled ?
<Label kind={kinds.SUCCESS}>
{translate('Enabled')}
</Label> :
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
{
enableAuto ?
<Label kind={kinds.SUCCESS}>
{translate('AutomaticAdd')}
</Label> :
null
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<div className={styles.enabled}>
<Label kind={kinds.DEFAULT} title='List Refresh Interval'>
{`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`}
</Label>
</div>
<EditImportListModalConnector
id={id}
isOpen={this.state.isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
onDeleteImportListPress={this.onDeleteImportListPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportList}
onCancel={this.onDeleteImportListModalClose}
/>
</Card>
);
}
}
ImportList.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
enableAuto: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
minRefreshInterval: PropTypes.string.isRequired,
onConfirmDeleteImportList: PropTypes.func.isRequired
};
export default ImportList;

View File

@@ -0,0 +1,130 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import { deleteImportList } from 'Store/Actions/settingsActions';
import useTags from 'Tags/useTags';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import EditImportListModal from './EditImportListModal';
import styles from './ImportList.css';
interface ImportListProps {
id: number;
name: string;
enabled: boolean;
enableAuto: boolean;
tags: number[];
minRefreshInterval: string;
onCloneImportListPress: (id: number) => void;
}
function ImportList({
id,
name,
enabled,
enableAuto,
tags,
minRefreshInterval,
onCloneImportListPress,
}: ImportListProps) {
const dispatch = useDispatch();
const tagList = useTags();
const [isEditImportListModalOpen, setIsEditImportListModalOpen] =
useState(false);
const [isDeleteImportListModalOpen, setIsDeleteImportListModalOpen] =
useState(false);
const handleEditImportListPress = useCallback(() => {
setIsEditImportListModalOpen(true);
}, []);
const handleEditImportListModalClose = useCallback(() => {
setIsEditImportListModalOpen(false);
}, []);
const handleDeleteImportListPress = useCallback(() => {
setIsEditImportListModalOpen(false);
setIsDeleteImportListModalOpen(true);
}, []);
const handleDeleteImportListModalClose = useCallback(() => {
setIsDeleteImportListModalOpen(false);
}, []);
const handleConfirmDeleteImportList = useCallback(() => {
dispatch(deleteImportList({ id }));
}, [id, dispatch]);
const handleCloneImportListPress = useCallback(() => {
onCloneImportListPress(id);
}, [id, onCloneImportListPress]);
return (
<Card
className={styles.list}
overlayContent={true}
onPress={handleEditImportListPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneImportList')}
name={icons.CLONE}
onPress={handleCloneImportListPress}
/>
</div>
<div className={styles.enabled}>
{enabled ? (
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
) : (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
)}
{enableAuto ? (
<Label kind={kinds.SUCCESS}>{translate('AutomaticAdd')}</Label>
) : null}
</div>
<TagList tags={tags} tagList={tagList} />
<div className={styles.enabled}>
<Label kind={kinds.DEFAULT} title="List Refresh Interval">
{`${translate('Refresh')}: ${formatShortTimeSpan(
minRefreshInterval
)}`}
</Label>
</div>
<EditImportListModal
id={id}
isOpen={isEditImportListModalOpen}
onModalClose={handleEditImportListModalClose}
onDeleteImportListPress={handleDeleteImportListPress}
/>
<ConfirmModal
isOpen={isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={handleConfirmDeleteImportList}
onCancel={handleDeleteImportListModalClose}
/>
</Card>
);
}
export default ImportList;

View File

@@ -1,118 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddImportListModal from './AddImportListModal';
import EditImportListModalConnector from './EditImportListModalConnector';
import ImportList from './ImportList';
import styles from './ImportLists.css';
class ImportLists extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddImportListModalOpen: false,
isEditImportListModalOpen: false
};
}
//
// Listeners
onAddImportListPress = () => {
this.setState({ isAddImportListModalOpen: true });
};
onAddImportListModalClose = ({ listSelected = false } = {}) => {
this.setState({
isAddImportListModalOpen: false,
isEditImportListModalOpen: listSelected
});
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
//
// Render
render() {
const {
items,
tagList,
onConfirmDeleteImportList,
...otherProps
} = this.props;
const {
isAddImportListModalOpen,
isEditImportListModalOpen
} = this.state;
return (
<FieldSet legend={translate('ImportLists')} >
<PageSectionContent
errorMessage={translate('ImportListsLoadError')}
{...otherProps}
>
<div className={styles.lists}>
{
items.map((item) => {
return (
<ImportList
key={item.id}
{...item}
tagList={tagList}
onConfirmDeleteImportList={onConfirmDeleteImportList}
/>
);
})
}
<Card
className={styles.addList}
onPress={this.onAddImportListPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddImportListModal
isOpen={isAddImportListModalOpen}
onModalClose={this.onAddImportListModalClose}
/>
<EditImportListModalConnector
isOpen={isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
ImportLists.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteImportList: PropTypes.func.isRequired
};
export default ImportLists;

View File

@@ -0,0 +1,109 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ImportListAppState } from 'App/State/SettingsAppState';
import Card from 'Components/Card';
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,
} from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import ImportListModel from 'typings/ImportList';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import AddImportListModal from './AddImportListModal';
import EditImportListModal from './EditImportListModal';
import ImportList from './ImportList';
import styles from './ImportLists.css';
function ImportLists() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items, error } = useSelector(
createSortedSectionSelector<ImportListModel, ImportListAppState>(
'settings.importLists',
sortByProp('name')
)
);
const [isAddImportListModalOpen, setIsAddImportListModalOpen] =
useState(false);
const [isEditImportListModalOpen, setIsEditImportListModalOpen] =
useState(false);
const handleAddImportListPress = useCallback(() => {
setIsAddImportListModalOpen(true);
}, []);
const handleAddImportListModalClose = useCallback(() => {
setIsAddImportListModalOpen(false);
}, []);
const handleImportListSelect = useCallback(() => {
setIsAddImportListModalOpen(false);
setIsEditImportListModalOpen(true);
}, []);
const handleEditImportListModalClose = useCallback(() => {
setIsEditImportListModalOpen(false);
}, []);
const handleCloneImportListPress = useCallback(
(id: number) => {
dispatch(cloneImportList({ id }));
setIsEditImportListModalOpen(true);
},
[dispatch]
);
useEffect(() => {
dispatch(fetchImportLists());
dispatch(fetchRootFolders());
}, [dispatch]);
return (
<FieldSet legend={translate('ImportLists')}>
<PageSectionContent
errorMessage={translate('ImportListsLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
>
<div className={styles.lists}>
{items.map((item) => {
return (
<ImportList
key={item.id}
{...item}
onCloneImportListPress={handleCloneImportListPress}
/>
);
})}
<Card className={styles.addList} onPress={handleAddImportListPress}>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
</div>
<AddImportListModal
isOpen={isAddImportListModalOpen}
onImportListSelect={handleImportListSelect}
onModalClose={handleAddImportListModalClose}
/>
<EditImportListModal
isOpen={isEditImportListModalOpen}
onModalClose={handleEditImportListModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
export default ImportLists;

View File

@@ -1,67 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import ImportLists from './ImportLists';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.importLists', sortByProp('name')),
createTagsSelector(),
(importLists, tagList) => {
return {
...importLists,
tagList
};
}
);
}
const mapDispatchToProps = {
fetchImportLists,
deleteImportList,
fetchRootFolders
};
class ImportListsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchImportLists();
this.props.fetchRootFolders();
}
//
// Listeners
onConfirmDeleteImportList = (id) => {
this.props.deleteImportList({ id });
};
//
// Render
render() {
return (
<ImportLists
{...this.props}
onConfirmDeleteImportList={this.onConfirmDeleteImportList}
/>
);
}
}
ImportListsConnector.propTypes = {
fetchImportLists: PropTypes.func.isRequired,
deleteImportList: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListsConnector);

View File

@@ -1,14 +1,14 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
@@ -20,53 +20,62 @@ import createSettingsSectionSelector from 'Store/Selectors/createSettingsSection
import translate from 'Utilities/String/translate';
const SECTION = 'importListOptions';
const cleanLibraryLevelOptions = [
{ key: 'disabled', value: () => translate('Disabled') },
{ key: 'logOnly', value: () => translate('LogOnly') },
{ key: 'keepAndUnmonitor', value: () => translate('KeepAndUnmonitorMovie') },
{ key: 'removeAndKeep', value: () => translate('RemoveMovieAndKeepFiles') },
const cleanLibraryLevelOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
{
key: 'logOnly',
get value() {
return translate('LogOnly');
},
},
{
key: 'keepAndUnmonitor',
get value() {
return translate('KeepAndUnmonitorMovie');
},
},
{
key: 'removeAndKeep',
get value() {
return translate('RemoveMovieAndKeepFiles');
},
},
{
key: 'removeAndDelete',
value: () => translate('RemoveMovieAndDeleteFiles'),
get value() {
return translate('RemoveMovieAndDeleteFiles');
},
},
];
function createImportListOptionsSelector() {
return createSelector(
(state: AppState) => state.settings.advancedSettings,
createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => {
return {
advancedSettings,
save: sectionSettings.isSaving,
...sectionSettings,
};
}
);
}
interface ImportListOptionsPageProps {
interface ImportListOptionsProps {
setChildSave(saveCallback: () => void): void;
onChildStateChange(payload: unknown): void;
}
function ImportListOptions(props: ImportListOptionsPageProps) {
const { setChildSave, onChildStateChange } = props;
function ImportListOptions({
setChildSave,
onChildStateChange,
}: ImportListOptionsProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isSaving,
hasPendingChanges,
advancedSettings,
isFetching,
error,
settings,
hasSettings,
} = useSelector(createImportListOptionsSelector());
} = useSelector(createSettingsSectionSelector(SECTION));
const { listSyncLevel } = settings;
const dispatch = useDispatch();
const onInputChange = useCallback(
({ name, value }: { name: string; value: unknown }) => {
// @ts-expect-error 'setImportListOptionsValue' isn't typed yet
@@ -80,7 +89,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
setChildSave(() => dispatch(saveImportListOptions()));
return () => {
dispatch(clearPendingChanges({ section: SECTION }));
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
};
}, [dispatch, setChildSave]);
@@ -91,16 +100,11 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
});
}, [onChildStateChange, isSaving, hasPendingChanges]);
const translatedLevelOptions = cleanLibraryLevelOptions.map(
({ key, value }) => {
return {
key,
value: value(),
};
}
);
if (!showAdvancedSettings) {
return null;
}
return advancedSettings ? (
return (
<FieldSet legend={translate('Options')}>
{isFetching ? <LoadingIndicator /> : null}
@@ -110,12 +114,12 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
{hasSettings && !isFetching && !error ? (
<Form>
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('CleanLibraryLevel')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="listSyncLevel"
values={translatedLevelOptions}
values={cleanLibraryLevelOptions}
helpText={translate('ListSyncLevelHelpText')}
onChange={onInputChange}
{...listSyncLevel}
@@ -124,7 +128,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
</Form>
) : null}
</FieldSet>
) : null;
);
}
export default ImportListOptions;

View File

@@ -1,120 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import IndexersConnector from './Indexers/IndexersConnector';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
class IndexerSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false,
isManageIndexersOpen: false
};
}
//
// Listeners
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
};
onChildStateChange = (payload) => {
this.setState(payload);
};
onManageIndexersPress = () => {
this.setState({ isManageIndexersOpen: true });
};
onManageIndexersModalClose = () => {
this.setState({ isManageIndexersOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
}
};
//
// Render
render() {
const {
isTestingAll,
dispatchTestAllIndexers
} = this.props;
const {
isSaving,
hasPendingChanges,
isManageIndexersOpen
} = this.state;
return (
<PageContent title={translate('IndexerSettings')}>
<SettingsToolbarConnector
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={dispatchTestAllIndexers}
/>
<PageToolbarButton
label={translate('ManageIndexers')}
iconName={icons.MANAGE}
onPress={this.onManageIndexersPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
/>
<PageContentBody>
<IndexersConnector />
<IndexerOptionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<ManageIndexersModal
isOpen={isManageIndexersOpen}
onModalClose={this.onManageIndexersModalClose}
/>
</PageContentBody>
</PageContent>
);
}
}
IndexerSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
dispatchTestAllIndexers: PropTypes.func.isRequired
};
export default IndexerSettings;

View File

@@ -0,0 +1,104 @@
import React, { useCallback, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import { testAllIndexers } from 'Store/Actions/settingsActions';
import {
SaveCallback,
SettingsStateChange,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
import Indexers from './Indexers/Indexers';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptions from './Options/IndexerOptions';
function IndexerSettings() {
const dispatch = useDispatch();
const isTestingAll = useSelector(
(state: AppState) => state.settings.indexers.isTestingAll
);
const saveOptions = useRef<() => void>();
const [isSaving, setIsSaving] = useState(false);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const [isManageIndexersModalOpen, setIsManageIndexersModalOpen] =
useState(false);
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
saveOptions.current = saveCallback;
}, []);
const handleChildStateChange = useCallback(
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
setIsSaving(isSaving);
setHasPendingChanges(hasPendingChanges);
},
[]
);
const handleManageIndexersPress = useCallback(() => {
setIsManageIndexersModalOpen(true);
}, []);
const handleManageIndexersModalClose = useCallback(() => {
setIsManageIndexersModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
saveOptions.current?.();
}, []);
const handleTestAllIndexersPress = useCallback(() => {
dispatch(testAllIndexers());
}, [dispatch]);
return (
<PageContent title={translate('IndexerSettings')}>
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={handleTestAllIndexersPress}
/>
<PageToolbarButton
label={translate('ManageIndexers')}
iconName={icons.MANAGE}
onPress={handleManageIndexersPress}
/>
</>
}
onSavePress={handleSavePress}
/>
<PageContentBody>
<Indexers />
<IndexerOptions
setChildSave={handleSetChildSave}
onChildStateChange={handleChildStateChange}
/>
<ManageIndexersModal
isOpen={isManageIndexersModalOpen}
onModalClose={handleManageIndexersModalClose}
/>
</PageContentBody>
</PageContent>
);
}
export default IndexerSettings;

View File

@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { testAllIndexers } from 'Store/Actions/settingsActions';
import IndexerSettings from './IndexerSettings';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers.isTestingAll,
(isTestingAll) => {
return {
isTestingAll
};
}
);
}
const mapDispatchToProps = {
dispatchTestAllIndexers: testAllIndexers
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSettings);

View File

@@ -1,113 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
import styles from './AddIndexerItem.css';
class AddIndexerItem extends Component {
//
// Listeners
onIndexerSelect = () => {
const {
implementation,
implementationName
} = this.props;
this.props.onIndexerSelect({ implementation, implementationName });
};
//
// Render
render() {
const {
implementation,
implementationName,
infoLink,
presets,
onIndexerSelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.indexer}
>
<Link
className={styles.underlay}
onPress={this.onIndexerSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onIndexerSelect}
>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
{translate('Presets')}
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddIndexerPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onIndexerSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddIndexerItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onIndexerSelect: PropTypes.func.isRequired
};
export default AddIndexerItem;

View File

@@ -0,0 +1,88 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
import Indexer from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
import styles from './AddIndexerItem.css';
interface AddIndexerItemProps {
implementation: string;
implementationName: string;
infoLink: string;
presets?: Indexer[];
onIndexerSelect: () => void;
}
function AddIndexerItem({
implementation,
implementationName,
infoLink,
presets,
onIndexerSelect,
}: AddIndexerItemProps) {
const dispatch = useDispatch();
const hasPresets = !!presets && !!presets.length;
const handleIndexerSelect = useCallback(() => {
dispatch(
selectIndexerSchema({
implementation,
implementationName,
})
);
onIndexerSelect();
}, [implementation, implementationName, dispatch, onIndexerSelect]);
return (
<div className={styles.indexer}>
<Link className={styles.underlay} onPress={handleIndexerSelect} />
<div className={styles.overlay}>
<div className={styles.name}>{implementationName}</div>
<div className={styles.actions}>
{hasPresets && (
<span>
<Button size={sizes.SMALL} onPress={handleIndexerSelect}>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
{translate('Presets')}
</Button>
<MenuContent>
{presets.map((preset) => {
return (
<AddIndexerPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onIndexerSelect}
/>
);
})}
</MenuContent>
</Menu>
</span>
)}
<Button to={infoLink} size={sizes.SMALL}>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
export default AddIndexerItem;

View File

@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModal;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerModalContent from './AddIndexerModalContent';
interface AddIndexerModalProps {
isOpen: boolean;
onIndexerSelect: () => void;
onModalClose: () => void;
}
function AddIndexerModal({
isOpen,
onIndexerSelect,
onModalClose,
}: AddIndexerModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<AddIndexerModalContent
onIndexerSelect={onIndexerSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default AddIndexerModal;

View File

@@ -1,122 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerItem from './AddIndexerItem';
import styles from './AddIndexerModalContent.css';
class AddIndexerModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetIndexers,
torrentIndexers,
onIndexerSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddIndexer')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</Alert>
}
{
isSchemaPopulated && !schemaError &&
<div>
<Alert kind={kinds.INFO}>
<div>
{translate('SupportedIndexers')}
</div>
<div>
{translate('SupportedIndexersMoreInfo')}
</div>
</Alert>
<FieldSet legend={translate('Usenet')}>
<div className={styles.indexers}>
{
usenetIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
}
</div>
</FieldSet>
<FieldSet legend={translate('Torrents')}>
<div className={styles.indexers}>
{
torrentIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
}
</div>
</FieldSet>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddIndexerModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModalContent;

View File

@@ -0,0 +1,116 @@
import React, { useEffect, useMemo } 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 Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { kinds } from 'Helpers/Props';
import { fetchIndexerSchema } from 'Store/Actions/settingsActions';
import Indexer from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import AddIndexerItem from './AddIndexerItem';
import styles from './AddIndexerModalContent.css';
interface AddIndexerModalContentProps {
onIndexerSelect: () => void;
onModalClose: () => void;
}
function AddIndexerModalContent({
onIndexerSelect,
onModalClose,
}: AddIndexerModalContentProps) {
const dispatch = useDispatch();
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
useSelector((state: AppState) => state.settings.indexers);
const { usenetIndexers, torrentIndexers } = useMemo(() => {
return schema.reduce<{
usenetIndexers: Indexer[];
torrentIndexers: Indexer[];
}>(
(acc, item) => {
if (item.protocol === 'usenet') {
acc.usenetIndexers.push(item);
} else if (item.protocol === 'torrent') {
acc.torrentIndexers.push(item);
}
return acc;
},
{
usenetIndexers: [],
torrentIndexers: [],
}
);
}, [schema]);
useEffect(() => {
dispatch(fetchIndexerSchema());
}, [dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
<ModalBody>
{isSchemaFetching ? <LoadingIndicator /> : null}
{!isSchemaFetching && !!schemaError ? (
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
{isSchemaPopulated && !schemaError ? (
<div>
<Alert kind={kinds.INFO}>
<div>{translate('SupportedIndexers')}</div>
<div>{translate('SupportedIndexersMoreInfo')}</div>
</Alert>
<FieldSet legend={translate('Usenet')}>
<div className={styles.indexers}>
{usenetIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
{...indexer}
implementation={indexer.implementation}
onIndexerSelect={onIndexerSelect}
/>
);
})}
</div>
</FieldSet>
<FieldSet legend={translate('Torrents')}>
<div className={styles.indexers}>
{torrentIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
{...indexer}
implementation={indexer.implementation}
onIndexerSelect={onIndexerSelect}
/>
);
})}
</div>
</FieldSet>
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default AddIndexerModalContent;

View File

@@ -1,75 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerSchema, selectIndexerSchema } from 'Store/Actions/settingsActions';
import AddIndexerModalContent from './AddIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(indexers) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = indexers;
const usenetIndexers = _.filter(schema, { protocol: 'usenet' });
const torrentIndexers = _.filter(schema, { protocol: 'torrent' });
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetIndexers,
torrentIndexers
};
}
);
}
const mapDispatchToProps = {
fetchIndexerSchema,
selectIndexerSchema
};
class AddIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerSchema();
}
//
// Listeners
onIndexerSelect = ({ implementation, implementationName, name }) => {
this.props.selectIndexerSchema({ implementation, implementationName, presetName: name });
this.props.onModalClose({ indexerSelected: true });
};
//
// Render
render() {
return (
<AddIndexerModalContent
{...this.props}
onIndexerSelect={this.onIndexerSelect}
/>
);
}
}
AddIndexerModalContentConnector.propTypes = {
fetchIndexerSchema: PropTypes.func.isRequired,
selectIndexerSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);

View File

@@ -1,53 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddIndexerPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation,
implementationName
} = this.props;
this.props.onPress({
name,
implementation,
implementationName
});
};
//
// Render
render() {
const {
name,
implementation,
implementationName,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddIndexerPresetMenuItem;

View File

@@ -0,0 +1,42 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem';
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
interface AddIndexerPresetMenuItemProps
extends Omit<MenuItemProps, 'children'> {
name: string;
implementation: string;
implementationName: string;
onPress: () => void;
}
function AddIndexerPresetMenuItem({
name,
implementation,
implementationName,
onPress,
...otherProps
}: AddIndexerPresetMenuItemProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(
selectIndexerSchema({
implementation,
implementationName,
presetName: name,
})
);
onPress();
}, [name, implementation, implementationName, dispatch, onPress]);
return (
<MenuItem {...otherProps} onPress={handlePress}>
{name}
</MenuItem>
);
}
export default AddIndexerPresetMenuItem;

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditIndexerModalContentConnector from './EditIndexerModalContentConnector';
function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditIndexerModal;

View File

@@ -0,0 +1,45 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
cancelSaveIndexer,
cancelTestIndexer,
} from 'Store/Actions/settingsActions';
import EditIndexerModalContent, {
EditIndexerModalContentProps,
} from './EditIndexerModalContent';
const section = 'settings.indexers';
interface EditIndexerModalProps extends EditIndexerModalContentProps {
isOpen: boolean;
}
function EditIndexerModal({
isOpen,
onModalClose,
...otherProps
}: EditIndexerModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section }));
dispatch(cancelTestIndexer({ section }));
dispatch(cancelSaveIndexer({ section }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditIndexerModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditIndexerModal;

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveIndexer, cancelTestIndexer } from 'Store/Actions/settingsActions';
import EditIndexerModal from './EditIndexerModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.indexers';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestIndexer() {
dispatch(cancelTestIndexer({ section }));
},
dispatchCancelSaveIndexer() {
dispatch(cancelSaveIndexer({ section }));
}
};
}
class EditIndexerModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestIndexer();
this.props.dispatchCancelSaveIndexer();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestIndexer,
dispatchCancelSaveIndexer,
...otherProps
} = this.props;
return (
<EditIndexerModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditIndexerModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestIndexer: PropTypes.func.isRequired,
dispatchCancelSaveIndexer: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditIndexerModalConnector);

View File

@@ -1,255 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
function EditIndexerModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteIndexerPress,
onAdvancedSettingsPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss,
supportsSearch,
tags,
fields,
priority,
protocol,
downloadClientId
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? translate('EditIndexerImplementation', { implementationName }) : translate('AddIndexerImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</Alert>
}
{
!isFetching && !error &&
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={supportsRss.value ? translate('EnableRssHelpText') : undefined}
helpTextWarning={supportsRss.value ? undefined : translate('RssIsNotSupportedWithThisIndexer')}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={supportsSearch.value ? translate('EnableAutomaticSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={supportsSearch.value ? translate('EnableInteractiveSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagMovieHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditIndexerModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func
};
export default EditIndexerModalContent;

View File

@@ -0,0 +1,299 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 usePrevious from 'Helpers/Hooks/usePrevious';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import {
saveIndexer,
setIndexerFieldValue,
setIndexerValue,
testIndexer,
} from 'Store/Actions/settingsActions';
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
import Indexer from 'typings/Indexer';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
export interface EditIndexerModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteIndexerPress?: () => void;
}
function EditIndexerModalContent({
id,
onModalClose,
onDeleteIndexerPress,
}: EditIndexerModalContentProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isFetching,
error,
isSaving,
isTesting = false,
saveError,
item,
validationErrors,
validationWarnings,
} = useSelector(
createProviderSettingsSelectorHook<Indexer, IndexerAppState>('indexers', id)
);
const wasSaving = usePrevious(isSaving);
const {
implementationName = '',
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss,
supportsSearch,
tags,
fields,
priority,
protocol,
downloadClientId,
} = item;
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setIndexerValue(change));
},
[dispatch]
);
const handleFieldChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setIndexerFieldValue(change));
},
[dispatch]
);
const handleSavePress = useCallback(() => {
dispatch(saveIndexer({ id }));
}, [id, dispatch]);
const handleTestPress = useCallback(() => {
dispatch(testIndexer({ id }));
}, [id, dispatch]);
useEffect(() => {
if (!isSaving && wasSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id
? translate('EditIndexerImplementation', { implementationName })
: translate('AddIndexerImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
{!isFetching && !error ? (
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={
supportsRss.value ? translate('EnableRssHelpText') : undefined
}
helpTextWarning={
supportsRss.value
? undefined
: translate('RssIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={
supportsSearch.value
? translate('EnableAutomaticSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={
supportsSearch.value
? translate('EnableInteractiveSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={handleInputChange}
/>
</FormGroup>
{fields?.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={showAdvancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={handleFieldChange}
/>
);
})}
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagMovieHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
) : null}
</ModalBody>
<ModalFooter>
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
) : null}
<AdvancedSettingsButton showLabel={false} />
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={handleTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditIndexerModalContent;

View File

@@ -1,95 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer, toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditIndexerModalContent from './EditIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('indexers'),
(advancedSettings, indexer) => {
return {
advancedSettings,
...indexer
};
}
);
}
const mapDispatchToProps = {
setIndexerValue,
setIndexerFieldValue,
saveIndexer,
testIndexer,
toggleAdvancedSettings
};
class EditIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setIndexerFieldValue({ name, value });
};
onSavePress = () => {
this.props.saveIndexer({ id: this.props.id });
};
onTestPress = () => {
this.props.testIndexer({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
render() {
return (
<EditIndexerModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditIndexerModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setIndexerValue: PropTypes.func.isRequired,
setIndexerFieldValue: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);

View File

@@ -1,181 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditIndexerModalConnector from './EditIndexerModalConnector';
import styles from './Indexer.css';
class Indexer extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditIndexerModalOpen: false,
isDeleteIndexerModalOpen: false
};
}
//
// Listeners
onEditIndexerPress = () => {
this.setState({ isEditIndexerModalOpen: true });
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onDeleteIndexerPress = () => {
this.setState({
isEditIndexerModalOpen: false,
isDeleteIndexerModalOpen: true
});
};
onDeleteIndexerModalClose = () => {
this.setState({ isDeleteIndexerModalOpen: false });
};
onConfirmDeleteIndexer = () => {
this.props.onConfirmDeleteIndexer(this.props.id);
};
onCloneIndexerPress = () => {
const {
id,
onCloneIndexerPress
} = this.props;
onCloneIndexerPress(id);
};
//
// Render
render() {
const {
id,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
tagList,
supportsRss,
supportsSearch,
priority,
showPriority
} = this.props;
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={this.onEditIndexerPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneIndexer')}
name={icons.CLONE}
onPress={this.onCloneIndexerPress}
/>
</div>
<div className={styles.enabled}>
{
supportsRss && enableRss &&
<Label kind={kinds.SUCCESS}>
{translate('Rss')}
</Label>
}
{
supportsSearch && enableAutomaticSearch &&
<Label kind={kinds.SUCCESS}>
{translate('AutomaticSearch')}
</Label>
}
{
supportsSearch && enableInteractiveSearch &&
<Label kind={kinds.SUCCESS}>
{translate('InteractiveSearch')}
</Label>
}
{
showPriority &&
<Label kind={kinds.DEFAULT}>
{translate('Priority')}: {priority}
</Label>
}
{
!enableRss && !enableAutomaticSearch && !enableInteractiveSearch &&
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditIndexerModalConnector
id={id}
isOpen={this.state.isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
onDeleteIndexerPress={this.onDeleteIndexerPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose}
/>
</Card>
);
}
}
Indexer.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired,
onCloneIndexerPress: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired,
priority: PropTypes.number.isRequired,
showPriority: PropTypes.bool.isRequired
};
export default Indexer;

View File

@@ -0,0 +1,131 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import { deleteIndexer } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import IndexerModel from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import EditIndexerModal from './EditIndexerModal';
import styles from './Indexer.css';
interface IndexerProps extends IndexerModel {
showPriority: boolean;
onCloneIndexerPress: (id: number) => void;
}
function Indexer({
id,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
supportsRss,
supportsSearch,
priority,
showPriority,
onCloneIndexerPress,
}: IndexerProps) {
const dispatch = useDispatch();
const tagList = useSelector(createTagsSelector());
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const handleEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, []);
const handleEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, []);
const handleDeleteIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(false);
setIsDeleteIndexerModalOpen(true);
}, []);
const handleDeleteIndexerModalClose = useCallback(() => {
setIsDeleteIndexerModalOpen(false);
}, []);
const handleConfirmDeleteIndexer = useCallback(() => {
dispatch(deleteIndexer({ id }));
}, [id, dispatch]);
const handleCloneIndexerPress = useCallback(() => {
onCloneIndexerPress(id);
}, [id, onCloneIndexerPress]);
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={handleEditIndexerPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneIndexer')}
name={icons.CLONE}
onPress={handleCloneIndexerPress}
/>
</div>
<div className={styles.enabled}>
{supportsRss && enableRss ? (
<Label kind={kinds.SUCCESS}>{translate('Rss')}</Label>
) : null}
{supportsSearch && enableAutomaticSearch ? (
<Label kind={kinds.SUCCESS}>{translate('AutomaticSearch')}</Label>
) : null}
{supportsSearch && enableInteractiveSearch ? (
<Label kind={kinds.SUCCESS}>{translate('InteractiveSearch')}</Label>
) : null}
{showPriority ? (
<Label kind={kinds.DEFAULT}>
{translate('Priority')}: {priority}
</Label>
) : null}
{!enableRss && !enableAutomaticSearch && !enableInteractiveSearch ? (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
) : null}
</div>
<TagList tags={tags} tagList={tagList} />
<EditIndexerModal
id={id}
isOpen={isEditIndexerModalOpen}
onModalClose={handleEditIndexerModalClose}
onDeleteIndexerPress={handleDeleteIndexerPress}
/>
<ConfirmModal
isOpen={isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={handleConfirmDeleteIndexer}
onCancel={handleDeleteIndexerModalClose}
/>
</Card>
);
}
export default Indexer;

View File

@@ -1,129 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerModal from './AddIndexerModal';
import EditIndexerModalConnector from './EditIndexerModalConnector';
import Indexer from './Indexer';
import styles from './Indexers.css';
class Indexers extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false
};
}
//
// Listeners
onAddIndexerPress = () => {
this.setState({ isAddIndexerModalOpen: true });
};
onCloneIndexerPress = (id) => {
this.props.dispatchCloneIndexer({ id });
this.setState({ isEditIndexerModalOpen: true });
};
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
this.setState({
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: indexerSelected
});
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
//
// Render
render() {
const {
items,
tagList,
dispatchCloneIndexer,
onConfirmDeleteIndexer,
...otherProps
} = this.props;
const {
isAddIndexerModalOpen,
isEditIndexerModalOpen
} = this.state;
const showPriority = items.some((index) => index.priority !== 25);
return (
<FieldSet legend={translate('Indexers')}>
<PageSectionContent
errorMessage={translate('IndexersLoadError')}
{...otherProps}
>
<div className={styles.indexers}>
{
items.map((item) => {
return (
<Indexer
key={item.id}
{...item}
tagList={tagList}
showPriority={showPriority}
onCloneIndexerPress={this.onCloneIndexerPress}
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
/>
);
})
}
<Card
className={styles.addIndexer}
onPress={this.onAddIndexerPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={this.onAddIndexerModalClose}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
Indexers.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};
export default Indexers;

View File

@@ -0,0 +1,105 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import { cloneIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import IndexerModel from 'typings/Indexer';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import AddIndexerModal from './AddIndexerModal';
import EditIndexerModal from './EditIndexerModal';
import Indexer from './Indexer';
import styles from './Indexers.css';
function Indexers() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items, error } = useSelector(
createSortedSectionSelector<IndexerModel, IndexerAppState>(
'settings.indexers',
sortByProp('name')
)
);
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const showPriority = items.some((index) => index.priority !== 25);
const handleAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true);
}, []);
const handleCloneIndexerPress = useCallback(
(id: number) => {
dispatch(cloneIndexer({ id }));
setIsEditIndexerModalOpen(true);
},
[dispatch]
);
const handleIndexerSelect = useCallback(() => {
setIsAddIndexerModalOpen(false);
setIsEditIndexerModalOpen(true);
}, []);
const handleAddIndexerModalClose = useCallback(() => {
setIsAddIndexerModalOpen(false);
}, []);
const handleEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, []);
useEffect(() => {
dispatch(fetchIndexers());
}, [dispatch]);
return (
<FieldSet legend={translate('Indexers')}>
<PageSectionContent
errorMessage={translate('IndexersLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
>
<div className={styles.indexers}>
{items.map((item) => {
return (
<Indexer
key={item.id}
{...item}
showPriority={showPriority}
onCloneIndexerPress={handleCloneIndexerPress}
/>
);
})}
<Card className={styles.addIndexer} onPress={handleAddIndexerPress}>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
</div>
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onIndexerSelect={handleIndexerSelect}
onModalClose={handleAddIndexerModalClose}
/>
<EditIndexerModal
isOpen={isEditIndexerModalOpen}
onModalClose={handleEditIndexerModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
export default Indexers;

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import Indexers from './Indexers';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.indexers', sortByProp('name')),
createTagsSelector(),
(indexers, tagList) => {
return {
...indexers,
tagList
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers,
dispatchDeleteIndexer: deleteIndexer,
dispatchCloneIndexer: cloneIndexer
};
class IndexersConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchIndexers();
}
//
// Listeners
onConfirmDeleteIndexer = (id) => {
this.props.dispatchDeleteIndexer({ id });
};
//
// Render
render() {
return (
<Indexers
{...this.props}
onConfirmDeleteIndexer={this.onConfirmDeleteIndexer}
/>
);
}
}
IndexersConnector.propTypes = {
dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchDeleteIndexer: PropTypes.func.isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);

View File

@@ -1,174 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function IndexerOptions(props) {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
onInputChange,
onWhitelistedSubtitleChange
} = props;
return (
<FieldSet legend={translate('Options')}>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('IndexerOptionsLoadError')}
</Alert>
}
{
hasSettings && !isFetching && !error &&
<Form>
<FormGroup>
<FormLabel>{translate('MinimumAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
unit="minutes"
helpText={translate('MinimumAgeHelpText')}
onChange={onInputChange}
{...settings.minimumAge}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Retention')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
unit="days"
helpText={translate('RetentionHelpText')}
onChange={onInputChange}
{...settings.retention}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MaximumSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
unit="MB"
helpText={translate('MaximumSizeHelpText')}
onChange={onInputChange}
{...settings.maximumSize}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="preferIndexerFlags"
helpText={translate('PreferIndexerFlagsHelpText')}
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
onChange={onInputChange}
{...settings.preferIndexerFlags}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="availabilityDelay"
unit="days"
helpText={translate('AvailabilityDelayHelpText')}
onChange={onInputChange}
{...settings.availabilityDelay}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
max={120}
unit="minutes"
helpText={translate('RssSyncIntervalHelpText')}
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
helpLink="https://wiki.servarr.com/radarr/faq#how-does-radarr-work"
onChange={onInputChange}
{...settings.rssSyncInterval}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_TAG}
name="whitelistedHardcodedSubs"
helpText={translate('WhitelistedHardcodedSubsHelpText')}
onChange={onWhitelistedSubtitleChange}
{...settings.whitelistedHardcodedSubs}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="allowHardcodedSubs"
helpText={translate('AllowHardcodedSubsHelpText')}
onChange={onInputChange}
{...settings.allowHardcodedSubs}
/>
</FormGroup>
</Form>
}
</FieldSet>
);
}
IndexerOptions.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onWhitelistedSubtitleChange: PropTypes.func.isRequired
};
export default IndexerOptions;

View File

@@ -0,0 +1,210 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
fetchIndexerOptions,
saveIndexerOptions,
setIndexerOptionsValue,
} from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { InputChanged } from 'typings/inputs';
import {
OnChildStateChange,
SetChildSave,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
const SECTION = 'indexerOptions';
interface IndexerOptionsProps {
setChildSave: SetChildSave;
onChildStateChange: OnChildStateChange;
}
function IndexerOptions({
setChildSave,
onChildStateChange,
}: IndexerOptionsProps) {
const dispatch = useDispatch();
const {
isFetching,
isPopulated,
isSaving,
error,
settings,
hasSettings,
hasPendingChanges,
} = useSelector(createSettingsSectionSelector(SECTION));
const showAdvancedSettings = useShowAdvancedSettings();
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions aren't typed
dispatch(setIndexerOptionsValue(change));
},
[dispatch]
);
const handleWhitelistedSubtitleChange = useCallback(
({ name, value }: InputChanged<string[] | null>) => {
// @ts-expect-error - actions aren't typed
dispatch(setIndexerOptionsValue({ name, value: value?.join(',') }));
},
[dispatch]
);
useEffect(() => {
dispatch(fetchIndexerOptions());
setChildSave(() => dispatch(saveIndexerOptions()));
}, [dispatch, setChildSave]);
useEffect(() => {
onChildStateChange({
isSaving,
hasPendingChanges,
});
}, [hasPendingChanges, isSaving, onChildStateChange]);
useEffect(() => {
return () => {
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
};
}, [dispatch]);
return (
<FieldSet legend={translate('Options')}>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('IndexerOptionsLoadError')}
</Alert>
) : null}
{hasSettings && isPopulated && !error ? (
<Form>
<FormGroup>
<FormLabel>{translate('MinimumAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
unit="minutes"
helpText={translate('MinimumAgeHelpText')}
onChange={handleInputChange}
{...settings.minimumAge}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Retention')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
unit="days"
helpText={translate('RetentionHelpText')}
onChange={handleInputChange}
{...settings.retention}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MaximumSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
unit="MB"
helpText={translate('MaximumSizeHelpText')}
onChange={handleInputChange}
{...settings.maximumSize}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="preferIndexerFlags"
helpText={translate('PreferIndexerFlagsHelpText')}
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
onChange={handleInputChange}
{...settings.preferIndexerFlags}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="availabilityDelay"
unit="days"
helpText={translate('AvailabilityDelayHelpText')}
onChange={handleInputChange}
{...settings.availabilityDelay}
/>
</FormGroup>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
max={120}
unit="minutes"
helpText={translate('RssSyncIntervalHelpText')}
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
helpLink="https://wiki.servarr.com/radarr/faq#how-does-radarr-work"
onChange={handleInputChange}
{...settings.rssSyncInterval}
/>
</FormGroup>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_TAG}
name="whitelistedHardcodedSubs"
helpText={translate('WhitelistedHardcodedSubsHelpText')}
onChange={handleWhitelistedSubtitleChange}
{...settings.whitelistedHardcodedSubs}
/>
</FormGroup>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="allowHardcodedSubs"
helpText={translate('AllowHardcodedSubsHelpText')}
onChange={handleInputChange}
{...settings.allowHardcodedSubs}
/>
</FormGroup>
</Form>
) : null}
</FieldSet>
);
}
export default IndexerOptions;

View File

@@ -1,106 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchIndexerOptions, saveIndexerOptions, setIndexerOptionsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import IndexerOptions from './IndexerOptions';
const SECTION = 'indexerOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => {
return {
advancedSettings,
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexerOptions: fetchIndexerOptions,
dispatchSetIndexerOptionsValue: setIndexerOptionsValue,
dispatchSaveIndexerOptions: saveIndexerOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class IndexerOptionsConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
dispatchFetchIndexerOptions,
dispatchSaveIndexerOptions,
onChildMounted
} = this.props;
dispatchFetchIndexerOptions();
onChildMounted(dispatchSaveIndexerOptions);
}
componentDidUpdate(prevProps) {
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetIndexerOptionsValue({ name, value });
};
onWhitelistedSubtitleChange = ({ name, value }) => {
this.props.dispatchSetIndexerOptionsValue({ name, value: value.join(',') });
};
//
// Render
render() {
return (
<IndexerOptions
onInputChange={this.onInputChange}
onWhitelistedSubtitleChange={this.onWhitelistedSubtitleChange}
{...this.props}
/>
);
}
}
IndexerOptionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
dispatchFetchIndexerOptions: PropTypes.func.isRequired,
dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
dispatchSaveIndexerOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector);

View File

@@ -1,500 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import RootFolders from 'RootFolder/RootFolders';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import Naming from './Naming/Naming';
import AddRootFolder from './RootFolder/AddRootFolder';
const rescanAfterRefreshOptions = [
{
key: 'always',
get value() {
return translate('Always');
}
},
{
key: 'afterManual',
get value() {
return translate('AfterManualRefresh');
}
},
{
key: 'never',
get value() {
return translate('Never');
}
}
];
const downloadPropersAndRepacksOptions = [
{
key: 'preferAndUpgrade',
get value() {
return translate('PreferAndUpgrade');
}
},
{
key: 'doNotUpgrade',
get value() {
return translate('DoNotUpgradeAutomatically');
}
},
{
key: 'doNotPrefer',
get value() {
return translate('DoNotPrefer');
}
}
];
const fileDateOptions = [
{
key: 'none',
get value() {
return translate('None');
}
},
{
key: 'cinemas',
get value() {
return translate('InCinemasDate');
}
},
{
key: 'release',
get value() {
return translate('PhysicalReleaseDate');
}
}
];
class MediaManagement extends Component {
//
// Render
render() {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
isWindows,
onInputChange,
onSavePress,
...otherProps
} = this.props;
return (
<PageContent title={translate('MediaManagementSettings')}>
<SettingsToolbarConnector
advancedSettings={advancedSettings}
{...otherProps}
onSavePress={onSavePress}
/>
<PageContentBody>
<Naming />
{
isFetching ?
<FieldSet legend={translate('NamingSettings')}>
<LoadingIndicator />
</FieldSet> : null
}
{
!isFetching && error ?
<FieldSet legend={translate('NamingSettings')}>
<Alert kind={kinds.DANGER}>
{translate('MediaManagementSettingsLoadError')}
</Alert>
</FieldSet> : null
}
{
hasSettings && !isFetching && !error ?
<Form
id="mediaManagementSettings"
{...otherProps}
>
{
advancedSettings ?
<FieldSet legend={translate('Folders')}>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('CreateEmptyMovieFolders')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
isDisabled={settings.deleteEmptyFolders.value && !settings.createEmptyMovieFolders.value}
name="createEmptyMovieFolders"
helpText={translate('CreateEmptyMovieFoldersHelpText')}
onChange={onInputChange}
{...settings.createEmptyMovieFolders}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('DeleteEmptyFolders')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
isDisabled={settings.createEmptyMovieFolders.value && !settings.deleteEmptyFolders.value}
name="deleteEmptyFolders"
helpText={translate('DeleteEmptyFoldersHelpText')}
onChange={onInputChange}
{...settings.deleteEmptyFolders}
/>
</FormGroup>
</FieldSet> : null
}
{
advancedSettings ?
<FieldSet
legend={translate('Importing')}
>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipFreeSpaceCheckWhenImporting"
helpText={translate('SkipFreeSpaceCheckHelpText')}
onChange={onInputChange}
{...settings.skipFreeSpaceCheckWhenImporting}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('MinimumFreeSpace')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
unit='MB'
name="minimumFreeSpaceWhenImporting"
helpText={translate('MinimumFreeSpaceHelpText')}
onChange={onInputChange}
{...settings.minimumFreeSpaceWhenImporting}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('UseHardlinksInsteadOfCopy')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="copyUsingHardlinks"
helpText={translate('CopyUsingHardlinksMovieHelpText')}
helpTextWarning={translate('CopyUsingHardlinksHelpTextWarning')}
onChange={onInputChange}
{...settings.copyUsingHardlinks}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('ImportUsingScript')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="useScriptImport"
helpText={translate('ImportUsingScriptHelpText')}
onChange={onInputChange}
{...settings.useScriptImport}
/>
</FormGroup>
{
settings.useScriptImport.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
includeFiles={true}
name="scriptImportPath"
helpText={translate('ImportScriptPathHelpText')}
onChange={onInputChange}
{...settings.scriptImportPath}
/>
</FormGroup> : null
}
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="importExtraFiles"
helpText={translate('ImportExtraFilesMovieHelpText')}
onChange={onInputChange}
{...settings.importExtraFiles}
/>
</FormGroup>
{
settings.importExtraFiles.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="extraFileExtensions"
helpTexts={[
translate('ExtraFileExtensionsHelpText'),
translate('ExtraFileExtensionsHelpTextsExamples')
]}
onChange={onInputChange}
{...settings.extraFileExtensions}
/>
</FormGroup> : null
}
</FieldSet> : null
}
<FieldSet
legend={translate('FileManagement')}
>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('IgnoreDeletedMovies')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="autoUnmonitorPreviouslyDownloadedMovies"
helpText={translate('AutoUnmonitorPreviouslyDownloadedMoviesHelpText')}
onChange={onInputChange}
{...settings.autoUnmonitorPreviouslyDownloadedMovies}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('DownloadPropersAndRepacks')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="downloadPropersAndRepacks"
helpTexts={[
translate('DownloadPropersAndRepacksHelpText'),
translate('DownloadPropersAndRepacksHelpTextCustomFormat')
]}
helpTextWarning={
settings.downloadPropersAndRepacks.value === 'doNotPrefer' ?
translate('DownloadPropersAndRepacksHelpTextWarning') :
undefined
}
values={downloadPropersAndRepacksOptions}
onChange={onInputChange}
{...settings.downloadPropersAndRepacks}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AnalyseVideoFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableMediaInfo"
helpText={translate('AnalyseVideoFilesHelpText')}
onChange={onInputChange}
{...settings.enableMediaInfo}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RescanMovieFolderAfterRefresh')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="rescanAfterRefresh"
helpText={translate('RescanAfterRefreshMovieHelpText')}
helpTextWarning={translate('RescanAfterRefreshHelpTextWarning')}
values={rescanAfterRefreshOptions}
onChange={onInputChange}
{...settings.rescanAfterRefresh}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChangeFileDate')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="fileDate"
helpText={translate('ChangeFileDateHelpText')}
values={fileDateOptions}
onChange={onInputChange}
{...settings.fileDate}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RecyclingBin')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="recycleBin"
helpText={translate('RecyclingBinHelpText')}
onChange={onInputChange}
{...settings.recycleBin}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RecyclingBinCleanup')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="recycleBinCleanupDays"
helpText={translate('RecyclingBinCleanupHelpText')}
helpTextWarning={translate('RecyclingBinCleanupHelpTextWarning')}
min={0}
onChange={onInputChange}
{...settings.recycleBinCleanupDays}
/>
</FormGroup>
</FieldSet>
{
advancedSettings && !isWindows ?
<FieldSet
legend={translate('Permissions')}
>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('SetPermissions')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="setPermissionsLinux"
helpText={translate('SetPermissionsLinuxHelpText')}
helpTextWarning={translate('SetPermissionsLinuxHelpTextWarning')}
onChange={onInputChange}
{...settings.setPermissionsLinux}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChmodFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.UMASK}
name="chmodFolder"
helpText={translate('ChmodFolderHelpText')}
helpTextWarning={translate('ChmodFolderHelpTextWarning')}
onChange={onInputChange}
{...settings.chmodFolder}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChownGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="chownGroup"
helpText={translate('ChownGroupHelpText')}
helpTextWarning={translate('ChownGroupHelpTextWarning')}
values={fileDateOptions}
onChange={onInputChange}
{...settings.chownGroup}
/>
</FormGroup>
</FieldSet> : null
}
</Form> : null
}
<FieldSet legend={translate('RootFolders')}>
<RootFolders />
<AddRootFolder />
</FieldSet>
</PageContentBody>
</PageContent>
);
}
}
MediaManagement.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired,
onSavePress: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired
};
export default MediaManagement;

View File

@@ -0,0 +1,530 @@
import React, { useCallback, useEffect } 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 Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import RootFolders from 'RootFolder/RootFolders';
import SettingsToolbar from 'Settings/SettingsToolbar';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
fetchMediaManagementSettings,
saveMediaManagementSettings,
saveNamingSettings,
setMediaManagementSettingsValue,
} from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import useIsWindows from 'System/useIsWindows';
import { InputChanged } from 'typings/inputs';
import isEmpty from 'Utilities/Object/isEmpty';
import translate from 'Utilities/String/translate';
import Naming from './Naming/Naming';
import AddRootFolder from './RootFolder/AddRootFolder';
const SECTION = 'mediaManagement';
const rescanAfterRefreshOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'always',
get value() {
return translate('Always');
},
},
{
key: 'afterManual',
get value() {
return translate('AfterManualRefresh');
},
},
{
key: 'never',
get value() {
return translate('Never');
},
},
];
const downloadPropersAndRepacksOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'preferAndUpgrade',
get value() {
return translate('PreferAndUpgrade');
},
},
{
key: 'doNotUpgrade',
get value() {
return translate('DoNotUpgradeAutomatically');
},
},
{
key: 'doNotPrefer',
get value() {
return translate('DoNotPrefer');
},
},
];
const fileDateOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'none',
get value() {
return translate('None');
},
},
{
key: 'cinemas',
get value() {
return translate('InCinemasDate');
},
},
{
key: 'release',
get value() {
return translate('PhysicalReleaseDate');
},
},
];
function MediaManagement() {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const hasNamingPendingChanges = !isEmpty(
useSelector((state: AppState) => state.settings.naming.pendingChanges)
);
const isWindows = useIsWindows();
const {
isFetching,
isPopulated,
isSaving,
error,
settings,
hasSettings,
hasPendingChanges,
validationErrors,
validationWarnings,
} = useSelector(createSettingsSectionSelector(SECTION));
const handleSavePress = useCallback(() => {
dispatch(saveMediaManagementSettings());
dispatch(saveNamingSettings());
}, [dispatch]);
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setMediaManagementSettingsValue(change));
},
[dispatch]
);
useEffect(() => {
dispatch(fetchMediaManagementSettings());
return () => {
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
};
}, [dispatch]);
return (
<PageContent title={translate('MediaManagementSettings')}>
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasNamingPendingChanges || hasPendingChanges}
onSavePress={handleSavePress}
/>
<PageContentBody>
<Naming />
{isFetching ? (
<FieldSet legend={translate('NamingSettings')}>
<LoadingIndicator />
</FieldSet>
) : null}
{!isFetching && error ? (
<FieldSet legend={translate('NamingSettings')}>
<Alert kind={kinds.DANGER}>
{translate('MediaManagementSettingsLoadError')}
</Alert>
</FieldSet>
) : null}
{hasSettings && isPopulated && !error ? (
<Form
id="mediaManagementSettings"
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
{showAdvancedSettings ? (
<FieldSet legend={translate('Folders')}>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('CreateEmptyMovieFolders')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
isDisabled={
settings.deleteEmptyFolders.value &&
!settings.createEmptyMovieFolders.value
}
name="createEmptyMovieFolders"
helpText={translate('CreateEmptyMovieFoldersHelpText')}
onChange={handleInputChange}
{...settings.createEmptyMovieFolders}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('DeleteEmptyFolders')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
isDisabled={
settings.createEmptyMovieFolders.value &&
!settings.deleteEmptyFolders.value
}
name="deleteEmptyFolders"
helpText={translate('DeleteEmptyMovieFoldersHelpText')}
onChange={handleInputChange}
{...settings.deleteEmptyFolders}
/>
</FormGroup>
</FieldSet>
) : null}
{showAdvancedSettings ? (
<FieldSet legend={translate('Importing')}>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipFreeSpaceCheckWhenImporting"
helpText={translate('SkipFreeSpaceCheckHelpText')}
onChange={handleInputChange}
{...settings.skipFreeSpaceCheckWhenImporting}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('MinimumFreeSpace')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
unit="MB"
name="minimumFreeSpaceWhenImporting"
helpText={translate('MinimumFreeSpaceHelpText')}
onChange={handleInputChange}
{...settings.minimumFreeSpaceWhenImporting}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>
{translate('UseHardlinksInsteadOfCopy')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="copyUsingHardlinks"
helpText={translate('CopyUsingHardlinksMovieHelpText')}
helpTextWarning={translate(
'CopyUsingHardlinksHelpTextWarning'
)}
onChange={handleInputChange}
{...settings.copyUsingHardlinks}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('ImportUsingScript')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="useScriptImport"
helpText={translate('ImportUsingScriptHelpText')}
onChange={handleInputChange}
{...settings.useScriptImport}
/>
</FormGroup>
{settings.useScriptImport.value ? (
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
includeFiles={true}
name="scriptImportPath"
helpText={translate('ImportScriptPathHelpText')}
onChange={handleInputChange}
{...settings.scriptImportPath}
/>
</FormGroup>
) : null}
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="importExtraFiles"
helpText={translate('ImportExtraFilesMovieHelpText')}
onChange={handleInputChange}
{...settings.importExtraFiles}
/>
</FormGroup>
{settings.importExtraFiles.value ? (
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="extraFileExtensions"
helpTexts={[
translate('ExtraFileExtensionsHelpText'),
translate('ExtraFileExtensionsHelpTextsExamples'),
]}
onChange={handleInputChange}
{...settings.extraFileExtensions}
/>
</FormGroup>
) : null}
</FieldSet>
) : null}
<FieldSet legend={translate('FileManagement')}>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('UnmonitorDeletedMovies')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="autoUnmonitorPreviouslyDownloadedMovies"
helpText={translate('UnmonitorDeletedMoviesHelpText')}
onChange={handleInputChange}
{...settings.autoUnmonitorPreviouslyDownloadedMovies}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('DownloadPropersAndRepacks')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="downloadPropersAndRepacks"
helpTexts={[
translate('DownloadPropersAndRepacksHelpText'),
translate('DownloadPropersAndRepacksHelpTextCustomFormat'),
]}
helpTextWarning={
settings.downloadPropersAndRepacks.value === 'doNotPrefer'
? translate('DownloadPropersAndRepacksHelpTextWarning')
: undefined
}
values={downloadPropersAndRepacksOptions}
onChange={handleInputChange}
{...settings.downloadPropersAndRepacks}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AnalyseVideoFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableMediaInfo"
helpText={translate('AnalyseVideoFilesHelpText')}
onChange={handleInputChange}
{...settings.enableMediaInfo}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>
{translate('RescanMovieFolderAfterRefresh')}
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="rescanAfterRefresh"
helpText={translate('RescanAfterRefreshMovieHelpText')}
helpTextWarning={translate(
'RescanAfterRefreshHelpTextWarning'
)}
values={rescanAfterRefreshOptions}
onChange={handleInputChange}
{...settings.rescanAfterRefresh}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChangeFileDate')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="fileDate"
helpText={translate('ChangeFileDateHelpText')}
values={fileDateOptions}
onChange={handleInputChange}
{...settings.fileDate}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RecyclingBin')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="recycleBin"
helpText={translate('RecyclingBinHelpText')}
includeFiles={false}
onChange={handleInputChange}
{...settings.recycleBin}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RecyclingBinCleanup')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="recycleBinCleanupDays"
helpText={translate('RecyclingBinCleanupHelpText')}
helpTextWarning={translate(
'RecyclingBinCleanupHelpTextWarning'
)}
min={0}
onChange={handleInputChange}
{...settings.recycleBinCleanupDays}
/>
</FormGroup>
</FieldSet>
{showAdvancedSettings && !isWindows ? (
<FieldSet legend={translate('Permissions')}>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('SetPermissions')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="setPermissionsLinux"
helpText={translate('SetPermissionsLinuxHelpText')}
helpTextWarning={translate(
'SetPermissionsLinuxHelpTextWarning'
)}
onChange={handleInputChange}
{...settings.setPermissionsLinux}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChmodFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.UMASK}
name="chmodFolder"
helpText={translate('ChmodFolderHelpText')}
helpTextWarning={translate('ChmodFolderHelpTextWarning')}
onChange={handleInputChange}
{...settings.chmodFolder}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChownGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="chownGroup"
helpText={translate('ChownGroupHelpText')}
helpTextWarning={translate('ChownGroupHelpTextWarning')}
onChange={handleInputChange}
{...settings.chownGroup}
/>
</FormGroup>
</FieldSet>
) : null}
</Form>
) : null}
<FieldSet legend={translate('RootFolders')}>
<RootFolders />
<AddRootFolder />
</FieldSet>
</PageContentBody>
</PageContent>
);
}
export default MediaManagement;

View File

@@ -1,86 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchMediaManagementSettings, saveMediaManagementSettings, saveNamingSettings, setMediaManagementSettingsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import MediaManagement from './MediaManagement';
const SECTION = 'mediaManagement';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
(state) => state.settings.naming,
createSettingsSectionSelector(SECTION),
createSystemStatusSelector(),
(advancedSettings, namingSettings, sectionSettings, systemStatus) => {
return {
advancedSettings,
...sectionSettings,
hasPendingChanges: !_.isEmpty(namingSettings.pendingChanges) || sectionSettings.hasPendingChanges,
isWindows: systemStatus.isWindows
};
}
);
}
const mapDispatchToProps = {
fetchMediaManagementSettings,
setMediaManagementSettingsValue,
saveMediaManagementSettings,
saveNamingSettings,
clearPendingChanges
};
class MediaManagementConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchMediaManagementSettings();
}
componentWillUnmount() {
this.props.clearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setMediaManagementSettingsValue({ name, value });
};
onSavePress = () => {
this.props.saveMediaManagementSettings();
this.props.saveNamingSettings();
};
//
// Render
render() {
return (
<MediaManagement
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
{...this.props}
/>
);
}
}
MediaManagementConnector.propTypes = {
fetchMediaManagementSettings: PropTypes.func.isRequired,
setMediaManagementSettingsValue: PropTypes.func.isRequired,
saveMediaManagementSettings: PropTypes.func.isRequired,
saveNamingSettings: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MediaManagementConnector);

View File

@@ -73,7 +73,7 @@ function Naming() {
dispatch(fetchNamingExamples());
return () => {
dispatch(clearPendingChanges({ section: SECTION }));
dispatch(clearPendingChanges({ section: 'settings.naming' }));
};
}, [dispatch]);

View File

@@ -14,7 +14,7 @@ import styles from './Metadatas.css';
function createMetadatasSelector() {
return createSelector(
createSortedSectionSelector<MetadataType>(
createSortedSectionSelector<MetadataType, MetadataAppState>(
'settings.metadata',
sortByProp('name')
),

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import Metadatas from './Metadata/Metadatas';
import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
@@ -50,7 +50,7 @@ class MetadataSettings extends Component {
return (
<PageContent title={translate('MetadataSettings')}>
<SettingsToolbarConnector
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}

View File

@@ -1,14 +1,14 @@
import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import NotificationsConnector from './Notifications/NotificationsConnector';
function NotificationSettings() {
return (
<PageContent title={translate('ConnectSettings')}>
<SettingsToolbarConnector
<SettingsToolbar
showSave={false}
/>

View File

@@ -33,7 +33,6 @@ function EditNotificationModalContent(props) {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteNotificationPress,
...otherProps
} = props;
@@ -139,8 +138,6 @@ function EditNotificationModalContent(props) {
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
@@ -183,7 +180,6 @@ EditNotificationModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteNotificationPress: PropTypes.func
};

View File

@@ -6,8 +6,7 @@ import {
saveNotification,
setNotificationFieldValues,
setNotificationValue,
testNotification,
toggleAdvancedSettings
testNotification
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditNotificationModalContent from './EditNotificationModalContent';
@@ -29,8 +28,7 @@ const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValues,
saveNotification,
testNotification,
toggleAdvancedSettings
testNotification
};
class EditNotificationModalContentConnector extends Component {
@@ -63,10 +61,6 @@ class EditNotificationModalContentConnector extends Component {
this.props.testNotification({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -76,7 +70,6 @@ class EditNotificationModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -94,7 +87,6 @@ EditNotificationModalContentConnector.propTypes = {
setNotificationFieldValues: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -1,77 +0,0 @@
import PropTypes from 'prop-types';
import React, { useEffect } from 'react';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
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 { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function PendingChangesModal(props) {
const {
isOpen,
onConfirm,
onCancel,
bindShortcut,
unbindShortcut
} = props;
useEffect(() => {
if (isOpen) {
bindShortcut('enter', onConfirm);
return () => unbindShortcut('enter', onConfirm);
}
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
return (
<Modal
isOpen={isOpen}
onModalClose={onCancel}
>
<ModalContent onModalClose={onCancel}>
<ModalHeader>{translate('UnsavedChanges')}</ModalHeader>
<ModalBody>
{translate('PendingChangesMessage')}
</ModalBody>
<ModalFooter>
<Button
kind={kinds.DEFAULT}
onPress={onCancel}
>
{translate('PendingChangesStayReview')}
</Button>
<Button
autoFocus={true}
kind={kinds.DANGER}
onPress={onConfirm}
>
{translate('PendingChangesDiscardChanges')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
PendingChangesModal.propTypes = {
className: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
kind: PropTypes.oneOf(kinds.all),
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired,
unbindShortcut: PropTypes.func.isRequired
};
PendingChangesModal.defaultProps = {
kind: kinds.PRIMARY
};
export default keyboardShortcuts(PendingChangesModal);

View File

@@ -0,0 +1,55 @@
import React, { useEffect } from 'react';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
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 useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
interface PendingChangesModalProps {
className?: string;
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
}
function PendingChangesModal({
isOpen,
onConfirm,
onCancel,
}: PendingChangesModalProps) {
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
useEffect(() => {
if (isOpen) {
bindShortcut('acceptConfirmModal', onConfirm);
}
return () => unbindShortcut('acceptConfirmModal');
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
return (
<Modal isOpen={isOpen} onModalClose={onCancel}>
<ModalContent onModalClose={onCancel}>
<ModalHeader>{translate('UnsavedChanges')}</ModalHeader>
<ModalBody>{translate('PendingChangesMessage')}</ModalBody>
<ModalFooter>
<Button kind={kinds.DEFAULT} onPress={onCancel}>
{translate('PendingChangesStayReview')}
</Button>
<Button autoFocus={true} kind={kinds.DANGER} onPress={onConfirm}>
{translate('PendingChangesDiscardChanges')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default PendingChangesModal;

View File

@@ -3,7 +3,7 @@ import { DndProvider } from 'react-dnd-multi-backend';
import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
@@ -20,7 +20,7 @@ class Profiles extends Component {
render() {
return (
<PageContent title={translate('Profiles')}>
<SettingsToolbarConnector showSave={false} />
<SettingsToolbar showSave={false} />
<PageContentBody>
<DndProvider options={HTML5toTouch}>

View File

@@ -51,7 +51,6 @@ function createReleaseProfileSelector(id?: number) {
);
return {
id,
isFetching,
error,
isSaving,

View File

@@ -24,19 +24,19 @@
height: 20px;
}
.bar {
.track {
top: 9px;
margin: 0 5px;
height: 3px;
background-color: var(--sliderAccentColor);
box-shadow: 0 0 0 #000;
&:nth-child(3n+1) {
&:nth-child(3n + 1) {
background-color: #ddd;
}
}
.handle {
.thumb {
top: 1px;
z-index: 0 !important;
width: 18px;

View File

@@ -1,8 +1,6 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'bar': string;
'handle': string;
'megabytesPerMinute': string;
'quality': string;
'qualityDefinition': string;
@@ -10,7 +8,9 @@ interface CssExports {
'sizeLimit': string;
'sizes': string;
'slider': string;
'thumb': string;
'title': string;
'track': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -55,6 +55,27 @@ class QualityDefinition extends Component {
};
}
//
// Control
trackRenderer(props, state) {
return (
<div
{...props}
className={styles.track}
/>
);
}
thumbRenderer(props, state) {
return (
<div
{...props}
className={styles.thumb}
/>
);
}
//
// Listeners
@@ -174,6 +195,7 @@ class QualityDefinition extends Component {
<div className={styles.sizeLimit}>
<ReactSlider
className={styles.slider}
min={slider.min}
max={slider.max}
step={slider.step}
@@ -182,9 +204,9 @@ class QualityDefinition extends Component {
withTracks={true}
allowCross={false}
snapDragDisabled={true}
className={styles.slider}
trackClassName={styles.bar}
thumbClassName={styles.handle}
pearling={true}
renderThumb={this.thumbRenderer}
renderTrack={this.trackRenderer}
onChange={this.onSliderChange}
onAfterChange={this.onAfterSliderChange}
/>

View File

@@ -5,7 +5,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
@@ -64,7 +64,7 @@ class Quality extends Component {
return (
<PageContent title={translate('QualitySettings')}>
<SettingsToolbarConnector
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={

View File

@@ -3,21 +3,16 @@ import Link from 'Components/Link/Link';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate';
import SettingsToolbarConnector from './SettingsToolbarConnector';
import SettingsToolbar from './SettingsToolbar';
import styles from './Settings.css';
function Settings() {
return (
<PageContent title={translate('Settings')}>
<SettingsToolbarConnector
hasPendingChanges={false}
/>
<SettingsToolbar hasPendingChanges={false} />
<PageContentBody>
<Link
className={styles.link}
to="/settings/mediamanagement"
>
<Link className={styles.link} to="/settings/mediamanagement">
{translate('MediaManagement')}
</Link>
@@ -25,10 +20,7 @@ function Settings() {
{translate('MediaManagementSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/profiles"
>
<Link className={styles.link} to="/settings/profiles">
{translate('Profiles')}
</Link>
@@ -36,10 +28,7 @@ function Settings() {
{translate('ProfilesSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/quality"
>
<Link className={styles.link} to="/settings/quality">
{translate('Quality')}
</Link>
@@ -47,10 +36,7 @@ function Settings() {
{translate('QualitySettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/customformats"
>
<Link className={styles.link} to="/settings/customformats">
{translate('CustomFormats')}
</Link>
@@ -58,10 +44,7 @@ function Settings() {
{translate('CustomFormatsSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/indexers"
>
<Link className={styles.link} to="/settings/indexers">
{translate('Indexers')}
</Link>
@@ -69,10 +52,7 @@ function Settings() {
{translate('IndexersSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/downloadclients"
>
<Link className={styles.link} to="/settings/downloadclients">
{translate('DownloadClients')}
</Link>
@@ -80,10 +60,7 @@ function Settings() {
{translate('DownloadClientsSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/importlists"
>
<Link className={styles.link} to="/settings/importlists">
{translate('ImportLists')}
</Link>
@@ -91,10 +68,7 @@ function Settings() {
{translate('ImportListsSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/connect"
>
<Link className={styles.link} to="/settings/connect">
{translate('Connect')}
</Link>
@@ -102,10 +76,7 @@ function Settings() {
{translate('ConnectSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/metadata"
>
<Link className={styles.link} to="/settings/metadata">
{translate('Metadata')}
</Link>
@@ -113,21 +84,13 @@ function Settings() {
{translate('MetadataSettingsMovieSummary')}
</div>
<Link
className={styles.link}
to="/settings/tags"
>
<Link className={styles.link} to="/settings/tags">
{translate('Tags')}
</Link>
<div className={styles.summary}>
{translate('TagsSettingsSummary')}
</div>
<div className={styles.summary}>{translate('TagsSettingsSummary')}</div>
<Link
className={styles.link}
to="/settings/general"
>
<Link className={styles.link} to="/settings/general">
{translate('General')}
</Link>
@@ -135,22 +98,14 @@ function Settings() {
{translate('GeneralSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/ui"
>
<Link className={styles.link} to="/settings/ui">
{translate('Ui')}
</Link>
<div className={styles.summary}>
{translate('UiSettingsSummary')}
</div>
<div className={styles.summary}>{translate('UiSettingsSummary')}</div>
</PageContentBody>
</PageContent>
);
}
Settings.propTypes = {
};
export default Settings;

View File

@@ -1,106 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AdvancedSettingsButton from './AdvancedSettingsButton';
import PendingChangesModal from './PendingChangesModal';
class SettingsToolbar extends Component {
//
// Lifecycle
componentDidMount() {
this.props.bindShortcut(shortcuts.SAVE_SETTINGS.key, this.saveSettings, { isGlobal: true });
}
//
// Control
saveSettings = (event) => {
event.preventDefault();
const {
hasPendingChanges,
onSavePress
} = this.props;
if (hasPendingChanges) {
onSavePress();
}
};
//
// Render
render() {
const {
advancedSettings,
showSave,
isSaving,
hasPendingChanges,
hasPendingLocation,
additionalButtons,
onSavePress,
onConfirmNavigation,
onCancelNavigation,
onAdvancedSettingsPress
} = this.props;
return (
<PageToolbar>
<PageToolbarSection>
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
/>
{
showSave &&
<PageToolbarButton
label={hasPendingChanges ? translate('SaveChanges') : translate('NoChanges')}
iconName={icons.SAVE}
isSpinning={isSaving}
isDisabled={!hasPendingChanges}
onPress={onSavePress}
/>
}
{
additionalButtons
}
</PageToolbarSection>
<PendingChangesModal
isOpen={hasPendingLocation}
onConfirm={onConfirmNavigation}
onCancel={onCancelNavigation}
/>
</PageToolbar>
);
}
}
SettingsToolbar.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
showSave: PropTypes.bool.isRequired,
isSaving: PropTypes.bool,
hasPendingLocation: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool,
additionalButtons: PropTypes.node,
onSavePress: PropTypes.func,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onConfirmNavigation: PropTypes.func.isRequired,
onCancelNavigation: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired
};
SettingsToolbar.defaultProps = {
showSave: true
};
export default keyboardShortcuts(SettingsToolbar);

View File

@@ -0,0 +1,149 @@
import { Action, Location, UnregisterCallback } from 'history';
import React, {
ReactElement,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useHistory } from 'react-router';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AdvancedSettingsButton from './AdvancedSettingsButton';
import PendingChangesModal from './PendingChangesModal';
interface SettingsToolbarProps {
showSave?: boolean;
isSaving?: boolean;
hasPendingChanges?: boolean;
// TODO: This should do type checking like PageToolbarSectionProps,
// but this works for the time being.
additionalButtons?: ReactElement | null;
onSavePress?: () => void;
}
function SettingsToolbar({
showSave = true,
isSaving,
hasPendingChanges,
additionalButtons = null,
onSavePress,
}: SettingsToolbarProps) {
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
const history = useHistory();
const [nextLocation, setNextLocation] = useState<Location | null>(null);
const [nextLocationAction, setNextLocationAction] = useState<Action | null>(
null
);
const hasConfirmed = useRef(false);
const unblocker = useRef<UnregisterCallback>();
const handleConfirmNavigation = useCallback(() => {
if (!nextLocation) {
return;
}
const path = `${nextLocation.pathname}${nextLocation.search}`;
hasConfirmed.current = true;
if (nextLocationAction === 'PUSH') {
history.push(path);
} else {
// Unfortunately back and forward both use POP,
// which means we don't actually know which direction
// the user wanted to go, assuming back.
history.goBack();
}
}, [nextLocation, nextLocationAction, history]);
const handleCancelNavigation = useCallback(() => {
setNextLocation(null);
setNextLocationAction(null);
hasConfirmed.current = false;
}, []);
const handleRouterLeaving = useCallback(
(routerLocation: Location, routerAction: Action) => {
if (hasConfirmed.current) {
setNextLocation(null);
setNextLocationAction(null);
hasConfirmed.current = false;
return;
}
if (hasPendingChanges) {
setNextLocation(routerLocation);
setNextLocationAction(routerAction);
return false;
}
return;
},
[hasPendingChanges]
);
useEffect(() => {
unblocker.current = history.block(handleRouterLeaving);
return () => {
unblocker.current?.();
};
}, [history, handleRouterLeaving]);
useEffect(() => {
bindShortcut(
'saveSettings',
() => {
if (hasPendingChanges) {
onSavePress?.();
}
},
{
isGlobal: true,
}
);
return () => {
unbindShortcut('saveSettings');
};
}, [hasPendingChanges, bindShortcut, unbindShortcut, onSavePress]);
return (
<PageToolbar>
<PageToolbarSection>
<AdvancedSettingsButton showLabel={true} />
{showSave ? (
<PageToolbarButton
label={
hasPendingChanges
? translate('SaveChanges')
: translate('NoChanges')
}
iconName={icons.SAVE}
isSpinning={isSaving}
isDisabled={!hasPendingChanges}
onPress={onSavePress}
/>
) : null}
{additionalButtons}
</PageToolbarSection>
<PendingChangesModal
isOpen={nextLocation !== null}
onConfirm={handleConfirmNavigation}
onCancel={handleCancelNavigation}
/>
</PageToolbar>
);
}
export default SettingsToolbar;

Some files were not shown because too many files have changed in this diff Show More