1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-10-29 23:12:39 +01:00

Convert Release Profiles to TypeScript

This commit is contained in:
Bogdan 2024-09-27 08:52:32 +03:00 committed by Mark McDowall
parent 6660db22ec
commit e6e1078c15
20 changed files with 412 additions and 642 deletions

View File

@ -16,6 +16,7 @@ import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings';
export interface DownloadClientAppState
@ -49,6 +50,12 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionItemSchemaState<QualityProfile> {}
export interface ReleaseProfilesAppState
extends AppSectionState<ReleaseProfile>,
AppSectionSaveState {
pendingChanges: Partial<ReleaseProfile>;
}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
@ -83,6 +90,7 @@ interface SettingsAppState {
languages: LanguageSettingsAppState;
notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState;
releaseProfiles: ReleaseProfilesAppState;
ui: UiSettingsAppState;
}

View File

@ -272,6 +272,8 @@ FormInputGroup.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any),
placeholder: PropTypes.string,
delimiters: PropTypes.arrayOf(PropTypes.string),
isDisabled: PropTypes.bool,
type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all),
@ -284,8 +286,10 @@ FormInputGroup.propTypes = {
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
autoFocus: PropTypes.bool,
canEdit: PropTypes.bool,
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
includeAny: PropTypes.bool,
selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool,

View File

@ -19,7 +19,7 @@ function EditImportListExclusionModal(
const dispatch = useDispatch();
const onModalClosePress = useCallback(() => {
const handleModalClose = useCallback(() => {
dispatch(
clearPendingChanges({
section: 'settings.importListExclusions',
@ -29,10 +29,10 @@ function EditImportListExclusionModal(
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditImportListExclusionModalContent
{...otherProps}
onModalClose={onModalClosePress}
onModalClose={handleModalClose}
/>
</Modal>
);

View File

@ -31,12 +31,6 @@ const newImportListExclusion = {
tvdbId: 0,
};
interface EditImportListExclusionModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteImportListExclusionPress?: () => void;
}
function createImportListExclusionSelector(id?: number) {
return createSelector(
(state: AppState) => state.settings.importListExclusions,
@ -62,12 +56,24 @@ function createImportListExclusionSelector(id?: number) {
);
}
function EditImportListExclusionModalContent(
props: EditImportListExclusionModalContentProps
) {
const { id, onModalClose, onDeleteImportListExclusionPress } = props;
interface EditImportListExclusionModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteImportListExclusionPress?: () => void;
}
function EditImportListExclusionModalContent({
id,
onModalClose,
onDeleteImportListExclusionPress,
}: EditImportListExclusionModalContentProps) {
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
useSelector(createImportListExclusionSelector(id));
const { title, tvdbId } = item;
const dispatch = useDispatch();
const previousIsSaving = usePrevious(isSaving);
const dispatchSetImportListExclusionValue = (payload: {
name: string;
@ -77,20 +83,10 @@ function EditImportListExclusionModalContent(
dispatch(setImportListExclusionValue(payload));
};
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
useSelector(createImportListExclusionSelector(props.id));
const previousIsSaving = usePrevious(isSaving);
const { title, tvdbId } = item;
useEffect(() => {
if (!id) {
Object.keys(newImportListExclusion).forEach((name) => {
dispatchSetImportListExclusionValue({
name,
value:
newImportListExclusion[name as keyof typeof newImportListExclusion],
});
Object.entries(newImportListExclusion).forEach(([name, value]) => {
dispatchSetImportListExclusionValue({ name, value });
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -100,7 +96,7 @@ function EditImportListExclusionModalContent(
if (previousIsSaving && !isSaving && !saveError) {
onModalClose();
}
});
}, [previousIsSaving, isSaving, saveError, onModalClose]);
const onSavePress = useCallback(() => {
dispatch(saveImportListExclusion({ id }));

View File

@ -7,7 +7,7 @@ import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
import ReleaseProfilesConnector from './Release/ReleaseProfilesConnector';
import ReleaseProfiles from './Release/ReleaseProfiles';
// Only a single DragDrop Context can exist so it's done here to allow editing
// quality profiles and reordering delay profiles to work.
@ -26,7 +26,7 @@ class Profiles extends Component {
<DndProvider options={HTML5toTouch}>
<QualityProfilesConnector />
<DelayProfilesConnector />
<ReleaseProfilesConnector />
<ReleaseProfiles />
</DndProvider>
</PageContentBody>
</PageContent>

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 EditReleaseProfileModalContentConnector from './EditReleaseProfileModalContentConnector';
function EditReleaseProfileModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditReleaseProfileModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditReleaseProfileModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditReleaseProfileModal;

View File

@ -0,0 +1,41 @@
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 EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
interface EditReleaseProfileModalProps {
id?: number;
isOpen: boolean;
onModalClose: () => void;
onDeleteReleaseProfilePress?: () => void;
}
function EditReleaseProfileModal({
isOpen,
onModalClose,
...otherProps
}: EditReleaseProfileModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(
clearPendingChanges({
section: 'settings.releaseProfiles',
})
);
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditReleaseProfileModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditReleaseProfileModal;

View File

@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditReleaseProfileModal from './EditReleaseProfileModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditReleaseProfileModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'settings.releaseProfiles' });
this.props.onModalClose();
};
//
// Render
render() {
return (
<EditReleaseProfileModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditReleaseProfileModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(EditReleaseProfileModalConnector);

View File

@ -1,5 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@ -10,33 +12,97 @@ 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 { inputTypes, kinds } from 'Helpers/Props';
import {
saveReleaseProfile,
setReleaseProfileValue,
} from 'Store/Actions/Settings/releaseProfiles';
import selectSettings from 'Store/Selectors/selectSettings';
import { PendingSection } from 'typings/pending';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import translate from 'Utilities/String/translate';
import styles from './EditReleaseProfileModalContent.css';
const tagInputDelimiters = ['Tab', 'Enter'];
function EditReleaseProfileModalContent(props) {
const {
isSaving,
saveError,
item,
onInputChange,
onModalClose,
onSavePress,
onDeleteReleaseProfilePress,
...otherProps
} = props;
const newReleaseProfile = {
enabled: true,
required: [],
ignored: [],
tags: [],
indexerId: 0,
};
const {
id,
name,
enabled,
required,
ignored,
tags,
indexerId
} = item;
function createReleaseProfileSelector(id?: number) {
return createSelector(
(state: AppState) => state.settings.releaseProfiles,
(releaseProfiles) => {
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
releaseProfiles;
const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile;
const settings = selectSettings(mapping, pendingChanges, saveError);
return {
id,
isFetching,
error,
isSaving,
saveError,
item: settings.settings as PendingSection<ReleaseProfile>,
...settings,
};
}
);
}
interface EditReleaseProfileModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteReleaseProfilePress?: () => void;
}
function EditReleaseProfileModalContent({
id,
onModalClose,
onDeleteReleaseProfilePress,
}: EditReleaseProfileModalContentProps) {
const { item, isFetching, isSaving, error, saveError, ...otherProps } =
useSelector(createReleaseProfileSelector(id));
const { name, enabled, required, ignored, tags, indexerId } = item;
const dispatch = useDispatch();
const previousIsSaving = usePrevious(isSaving);
useEffect(() => {
if (!id) {
Object.entries(newReleaseProfile).forEach(([name, value]) => {
// @ts-expect-error 'setReleaseProfileValue' isn't typed yet
dispatch(setReleaseProfileValue({ name, value }));
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (previousIsSaving && !isSaving && !saveError) {
onModalClose();
}
}, [previousIsSaving, isSaving, saveError, onModalClose]);
const handleSavePress = useCallback(() => {
dispatch(saveReleaseProfile({ id }));
}, [dispatch, id]);
const handleInputChange = useCallback(
(payload: { name: string; value: string | number }) => {
// @ts-expect-error 'setReleaseProfileValue' isn't typed yet
dispatch(setReleaseProfileValue(payload));
},
[dispatch]
);
return (
<ModalContent onModalClose={onModalClose}>
@ -46,7 +112,6 @@ function EditReleaseProfileModalContent(props) {
<ModalBody>
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
@ -56,7 +121,7 @@ function EditReleaseProfileModalContent(props) {
{...name}
placeholder={translate('OptionalName')}
canEdit={true}
onChange={onInputChange}
onChange={handleInputChange}
/>
</FormGroup>
@ -68,7 +133,7 @@ function EditReleaseProfileModalContent(props) {
name="enabled"
helpText={translate('EnableProfileHelpText')}
{...enabled}
onChange={onInputChange}
onChange={handleInputChange}
/>
</FormGroup>
@ -85,7 +150,7 @@ function EditReleaseProfileModalContent(props) {
placeholder={translate('AddNewRestriction')}
delimiters={tagInputDelimiters}
canEdit={true}
onChange={onInputChange}
onChange={handleInputChange}
/>
</FormGroup>
@ -102,7 +167,7 @@ function EditReleaseProfileModalContent(props) {
placeholder={translate('AddNewRestriction')}
delimiters={tagInputDelimiters}
canEdit={true}
onChange={onInputChange}
onChange={handleInputChange}
/>
</FormGroup>
@ -113,10 +178,12 @@ function EditReleaseProfileModalContent(props) {
type={inputTypes.INDEXER_SELECT}
name="indexerId"
helpText={translate('ReleaseProfileIndexerHelpText')}
helpTextWarning={translate('ReleaseProfileIndexerHelpTextWarning')}
helpTextWarning={translate(
'ReleaseProfileIndexerHelpTextWarning'
)}
{...indexerId}
includeAny={true}
onChange={onInputChange}
onChange={handleInputChange}
/>
</FormGroup>
@ -128,33 +195,28 @@ function EditReleaseProfileModalContent(props) {
name="tags"
helpText={translate('ReleaseProfileTagSeriesHelpText')}
{...tags}
onChange={onInputChange}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteReleaseProfilePress}
>
{translate('Delete')}
</Button>
}
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteReleaseProfilePress}
>
{translate('Delete')}
</Button>
) : null}
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
@ -163,14 +225,4 @@ function EditReleaseProfileModalContent(props) {
);
}
EditReleaseProfileModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onDeleteReleaseProfilePress: PropTypes.func
};
export default EditReleaseProfileModalContent;

View File

@ -1,112 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveReleaseProfile, setReleaseProfileValue } from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings';
import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
const newReleaseProfile = {
enabled: true,
required: [],
ignored: [],
tags: [],
indexerId: 0
};
function createMapStateToProps() {
return createSelector(
(state, { id }) => id,
(state) => state.settings.releaseProfiles,
(id, releaseProfiles) => {
const {
isFetching,
error,
isSaving,
saveError,
pendingChanges,
items
} = releaseProfiles;
const profile = id ? items.find((i) => i.id === id) : newReleaseProfile;
const settings = selectSettings(profile, pendingChanges, saveError);
return {
id,
isFetching,
error,
isSaving,
saveError,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
setReleaseProfileValue,
saveReleaseProfile
};
class EditReleaseProfileModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.id) {
Object.keys(newReleaseProfile).forEach((name) => {
this.props.setReleaseProfileValue({
name,
value: newReleaseProfile[name]
});
});
}
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setReleaseProfileValue({ name, value });
};
onSavePress = () => {
this.props.saveReleaseProfile({ id: this.props.id });
};
//
// Render
render() {
return (
<EditReleaseProfileModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditReleaseProfileModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setReleaseProfileValue: PropTypes.func.isRequired,
saveReleaseProfile: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditReleaseProfileModalContentConnector);

View File

@ -1,206 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MiddleTruncate from 'react-middle-truncate';
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 translate from 'Utilities/String/translate';
import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector';
import styles from './ReleaseProfile.css';
class ReleaseProfile extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditReleaseProfileModalOpen: false,
isDeleteReleaseProfileModalOpen: false
};
}
//
// Listeners
onEditReleaseProfilePress = () => {
this.setState({ isEditReleaseProfileModalOpen: true });
};
onEditReleaseProfileModalClose = () => {
this.setState({ isEditReleaseProfileModalOpen: false });
};
onDeleteReleaseProfilePress = () => {
this.setState({
isEditReleaseProfileModalOpen: false,
isDeleteReleaseProfileModalOpen: true
});
};
onDeleteReleaseProfileModalClose = () => {
this.setState({ isDeleteReleaseProfileModalOpen: false });
};
onConfirmDeleteReleaseProfile = () => {
this.props.onConfirmDeleteReleaseProfile(this.props.id);
};
//
// Render
render() {
const {
id,
name,
enabled,
required,
ignored,
tags,
indexerId,
tagList,
indexerList
} = this.props;
const {
isEditReleaseProfileModalOpen,
isDeleteReleaseProfileModalOpen
} = this.state;
const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
return (
<Card
className={styles.releaseProfile}
overlayContent={true}
onPress={this.onEditReleaseProfilePress}
>
{
name ?
<div className={styles.name}>
{name}
</div> :
null
}
<div>
{
required.map((item) => {
if (!item) {
return null;
}
return (
<Label
className={styles.label}
key={item}
kind={kinds.SUCCESS}
>
<MiddleTruncate
text={item}
start={10}
end={10}
/>
</Label>
);
})
}
</div>
<div>
{
ignored.map((item) => {
if (!item) {
return null;
}
return (
<Label
className={styles.label}
key={item}
kind={kinds.DANGER}
>
<MiddleTruncate
text={item}
start={10}
end={10}
/>
</Label>
);
})
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<div>
{
!enabled &&
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
{
indexer &&
<Label
kind={kinds.INFO}
outline={true}
>
{indexer.name}
</Label>
}
</div>
<EditReleaseProfileModalConnector
id={id}
isOpen={isEditReleaseProfileModalOpen}
onModalClose={this.onEditReleaseProfileModalClose}
onDeleteReleaseProfilePress={this.onDeleteReleaseProfilePress}
/>
<ConfirmModal
isOpen={isDeleteReleaseProfileModalOpen}
kind={kinds.DANGER}
title={translate('DeleteReleaseProfile')}
message={translate('DeleteReleaseProfileMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteReleaseProfile}
onCancel={this.onDeleteReleaseProfileModalClose}
/>
</Card>
);
}
}
ReleaseProfile.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string,
enabled: PropTypes.bool.isRequired,
required: PropTypes.arrayOf(PropTypes.string).isRequired,
ignored: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerId: PropTypes.number.isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
};
ReleaseProfile.defaultProps = {
enabled: true,
required: [],
ignored: [],
indexerId: 0
};
export default ReleaseProfile;

View File

@ -0,0 +1,132 @@
import React, { useCallback } from 'react';
// @ts-expect-error 'MiddleTruncate' isn't typed
import MiddleTruncate from 'react-middle-truncate';
import { useDispatch } from 'react-redux';
import { Tag } from 'App/State/TagsAppState';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { kinds } from 'Helpers/Props';
import { deleteReleaseProfile } from 'Store/Actions/Settings/releaseProfiles';
import Indexer from 'typings/Indexer';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import translate from 'Utilities/String/translate';
import EditReleaseProfileModal from './EditReleaseProfileModal';
import styles from './ReleaseProfileRow.css';
interface ReleaseProfileProps extends ReleaseProfile {
tagList: Tag[];
indexerList: Indexer[];
}
function ReleaseProfileRow(props: ReleaseProfileProps) {
const {
id,
name,
enabled = true,
required = [],
ignored = [],
tags,
indexerId = 0,
tagList,
indexerList,
} = props;
const dispatch = useDispatch();
const [
isEditReleaseProfileModalOpen,
setEditReleaseProfileModalOpen,
setEditReleaseProfileModalClosed,
] = useModalOpenState(false);
const [
isDeleteReleaseProfileModalOpen,
setDeleteReleaseProfileModalOpen,
setDeleteReleaseProfileModalClosed,
] = useModalOpenState(false);
const handleDeletePress = useCallback(() => {
dispatch(deleteReleaseProfile({ id }));
}, [id, dispatch]);
const indexer =
indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
return (
<Card
className={styles.releaseProfile}
overlayContent={true}
onPress={setEditReleaseProfileModalOpen}
>
{name ? <div className={styles.name}>{name}</div> : null}
<div>
{required.map((item) => {
if (!item) {
return null;
}
return (
<Label key={item} className={styles.label} kind={kinds.SUCCESS}>
<MiddleTruncate text={item} start={10} end={10} />
</Label>
);
})}
</div>
<div>
{ignored.map((item) => {
if (!item) {
return null;
}
return (
<Label key={item} className={styles.label} kind={kinds.DANGER}>
<MiddleTruncate text={item} start={10} end={10} />
</Label>
);
})}
</div>
<TagList tags={tags} tagList={tagList} />
<div>
{enabled ? null : (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
)}
{indexer ? (
<Label kind={kinds.INFO} outline={true}>
{indexer.name}
</Label>
) : null}
</div>
<EditReleaseProfileModal
id={id}
isOpen={isEditReleaseProfileModalOpen}
onModalClose={setEditReleaseProfileModalClosed}
onDeleteReleaseProfilePress={setDeleteReleaseProfileModalOpen}
/>
<ConfirmModal
isOpen={isDeleteReleaseProfileModalOpen}
kind={kinds.DANGER}
title={translate('DeleteReleaseProfile')}
message={translate('DeleteReleaseProfileMessageText', {
name: name ?? id,
})}
confirmLabel={translate('Delete')}
onConfirm={handleDeletePress}
onCancel={setDeleteReleaseProfileModalClosed}
/>
</Card>
);
}
export default ReleaseProfileRow;

View File

@ -4,7 +4,7 @@
}
.addReleaseProfile {
composes: releaseProfile from '~./ReleaseProfile.css';
composes: releaseProfile from '~./ReleaseProfileRow.css';
background-color: var(--cardAlternateBackgroundColor);
color: var(--gray);

View File

@ -1,102 +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 EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector';
import ReleaseProfile from './ReleaseProfile';
import styles from './ReleaseProfiles.css';
class ReleaseProfiles extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddReleaseProfileModalOpen: false
};
}
//
// Listeners
onAddReleaseProfilePress = () => {
this.setState({ isAddReleaseProfileModalOpen: true });
};
onAddReleaseProfileModalClose = () => {
this.setState({ isAddReleaseProfileModalOpen: false });
};
//
// Render
render() {
const {
items,
tagList,
indexerList,
onConfirmDeleteReleaseProfile,
...otherProps
} = this.props;
return (
<FieldSet legend={translate('ReleaseProfiles')}>
<PageSectionContent
errorMessage={translate('ReleaseProfilesLoadError')}
{...otherProps}
>
<div className={styles.releaseProfiles}>
<Card
className={styles.addReleaseProfile}
onPress={this.onAddReleaseProfilePress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
{
items.map((item) => {
return (
<ReleaseProfile
key={item.id}
tagList={tagList}
indexerList={indexerList}
{...item}
onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile}
/>
);
})
}
</div>
<EditReleaseProfileModalConnector
isOpen={this.state.isAddReleaseProfileModalOpen}
onModalClose={this.onAddReleaseProfileModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
ReleaseProfiles.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
};
export default ReleaseProfiles;

View File

@ -0,0 +1,81 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { ReleaseProfilesAppState } 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 useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import { fetchIndexers } from 'Store/Actions/Settings/indexers';
import { fetchReleaseProfiles } from 'Store/Actions/Settings/releaseProfiles';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import translate from 'Utilities/String/translate';
import EditReleaseProfileModal from './EditReleaseProfileModal';
import ReleaseProfileRow from './ReleaseProfileRow';
import styles from './ReleaseProfiles.css';
function ReleaseProfiles() {
const { items, isFetching, isPopulated, error }: ReleaseProfilesAppState =
useSelector(createClientSideCollectionSelector('settings.releaseProfiles'));
const tagList = useSelector(createTagsSelector());
const indexerList = useSelector(
(state: AppState) => state.settings.indexers.items
);
const dispatch = useDispatch();
const [
isAddReleaseProfileModalOpen,
setAddReleaseProfileModalOpen,
setAddReleaseProfileModalClosed,
] = useModalOpenState(false);
useEffect(() => {
dispatch(fetchReleaseProfiles());
dispatch(fetchIndexers());
}, [dispatch]);
return (
<FieldSet legend={translate('ReleaseProfiles')}>
<PageSectionContent
errorMessage={translate('ReleaseProfilesLoadError')}
isFetching={isFetching}
isPopulated={isPopulated}
error={error}
>
<div className={styles.releaseProfiles}>
<Card
className={styles.addReleaseProfile}
onPress={setAddReleaseProfileModalOpen}
>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
{items.map((item) => {
return (
<ReleaseProfileRow
key={item.id}
tagList={tagList}
indexerList={indexerList}
{...item}
/>
);
})}
</div>
<EditReleaseProfileModal
isOpen={isAddReleaseProfileModalOpen}
onModalClose={setAddReleaseProfileModalClosed}
/>
</PageSectionContent>
</FieldSet>
);
}
export default ReleaseProfiles;

View File

@ -1,70 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import ReleaseProfiles from './ReleaseProfiles';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.releaseProfiles,
(state) => state.settings.indexers,
createTagsSelector(),
(releaseProfiles, indexers, tagList) => {
return {
...releaseProfiles,
tagList,
isIndexersPopulated: indexers.isPopulated,
indexerList: indexers.items
};
}
);
}
const mapDispatchToProps = {
fetchIndexers,
fetchReleaseProfiles,
deleteReleaseProfile
};
class ReleaseProfilesConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchReleaseProfiles();
if (!this.props.isIndexersPopulated) {
this.props.fetchIndexers();
}
}
//
// Listeners
onConfirmDeleteReleaseProfile = (id) => {
this.props.deleteReleaseProfile({ id });
};
//
// Render
render() {
return (
<ReleaseProfiles
{...this.props}
onConfirmDeleteReleaseProfile={this.onConfirmDeleteReleaseProfile}
/>
);
}
}
ReleaseProfilesConnector.propTypes = {
isIndexersPopulated: PropTypes.bool.isRequired,
fetchReleaseProfiles: PropTypes.func.isRequired,
deleteReleaseProfile: PropTypes.func.isRequired,
fetchIndexers: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector);

View File

@ -0,0 +1,12 @@
import ModelBase from 'App/ModelBase';
interface ReleaseProfile extends ModelBase {
name: string;
enabled: boolean;
required: string[];
ignored: string[];
indexerId: number;
tags: number[];
}
export default ReleaseProfile;

View File

@ -361,7 +361,7 @@
"DeleteQualityProfile": "Delete Quality Profile",
"DeleteQualityProfileMessageText": "Are you sure you want to delete the quality profile '{name}'?",
"DeleteReleaseProfile": "Delete Release Profile",
"DeleteReleaseProfileMessageText": "Are you sure you want to delete this release profile '{name}'?",
"DeleteReleaseProfileMessageText": "Are you sure you want to delete the release profile '{name}'?",
"DeleteRemotePathMapping": "Delete Remote Path Mapping",
"DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?",
"DeleteRootFolder": "Delete Root Folder",