Convert ImportLists to TypeScript

(cherry picked from commit 10e3a237ef972540abcf4348bb56973d7ee19bd7)
This commit is contained in:
Mark McDowall 2025-01-01 15:52:17 -08:00 committed by Bogdan
parent 2ebf391f85
commit b99c536306
30 changed files with 1001 additions and 1406 deletions

View file

@ -15,7 +15,7 @@ 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 ImportListSettings from 'Settings/ImportLists/ImportListSettings';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import MediaManagement from 'Settings/MediaManagement/MediaManagement';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
@ -117,10 +117,7 @@ function AppRoutes() {
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/importlists"
component={ImportListSettingsConnector}
/>
<Route path="/settings/importlists" component={ImportListSettings} />
<Route path="/settings/connect" component={NotificationSettings} />

View file

@ -53,7 +53,10 @@ export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
AppSectionSaveState {}
AppSectionSaveState,
AppSectionSchemaState<Presets<ImportList>> {
isTestingAll: boolean;
}
export interface IndexerOptionsAppState
extends AppSectionItemState<IndexerOptions>,

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

@ -170,7 +170,7 @@ function EditImportListExclusionModalContent({
</ModalBody>
<ModalFooter>
{id && (
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
@ -178,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 SettingsToolbar from 'Settings/SettingsToolbar';
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')}>
<SettingsToolbar
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,305 +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,
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}
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,
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,93 +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
} 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
};
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 });
};
//
// Render
render() {
return (
<EditImportListModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
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,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListModalContentConnector);

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,114 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
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 { 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;
}
function ImportList({
id,
name,
enabled,
enableAuto,
tags,
minRefreshInterval,
}: 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]);
return (
<Card
className={styles.list}
overlayContent={true}
onPress={handleEditImportListPress}
>
<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>
<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,92 @@
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 { 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);
}, []);
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} />;
})}
<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,3 +1,4 @@
import { MovieMonitor } from 'Movie/Movie';
import Provider from './Provider';
interface ImportList extends Provider {
@ -7,6 +8,12 @@ interface ImportList extends Provider {
qualityProfileId: number;
minimumAvailability: string;
rootFolderPath: string;
monitor: MovieMonitor;
searchOnAdd: boolean;
listType: string;
listOrder: number;
minRefreshInterval: string;
name: string;
tags: number[];
}