mirror of
https://github.com/Radarr/Radarr
synced 2025-12-06 08:28:50 +01:00
New: Kometa metadata file creation disabled
(cherry picked from commit c62fc9d05bb9e1fe51b454d78e80bd9250e31f89) Closes #10738
This commit is contained in:
parent
01a53d3624
commit
a6d727fe2a
35 changed files with 697 additions and 740 deletions
|
|
@ -210,7 +210,6 @@ module.exports = {
|
||||||
'no-undef-init': 'off',
|
'no-undef-init': 'off',
|
||||||
'no-undefined': 'off',
|
'no-undefined': 'off',
|
||||||
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
||||||
'no-use-before-define': 'error',
|
|
||||||
|
|
||||||
// Node.js and CommonJS
|
// Node.js and CommonJS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
import { ValidationFailure } from 'typings/pending';
|
||||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
status?: number;
|
||||||
message: string;
|
responseJSON:
|
||||||
};
|
| {
|
||||||
|
message: string | undefined;
|
||||||
|
}
|
||||||
|
| ValidationFailure[]
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionDeleteState {
|
export interface AppSectionDeleteState {
|
||||||
|
|
@ -51,6 +56,16 @@ export interface AppSectionItemState<T> {
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionProviderState<T>
|
||||||
|
extends AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
items: T[];
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppSectionState<T> {
|
interface AppSectionState<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
|
|
|
||||||
6
frontend/src/App/State/MetadataAppState.ts
Normal file
6
frontend/src/App/State/MetadataAppState.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
||||||
|
import Metadata from 'typings/Metadata';
|
||||||
|
|
||||||
|
interface MetadataAppState extends AppSectionProviderState<Metadata> {}
|
||||||
|
|
||||||
|
export default MetadataAppState;
|
||||||
|
|
@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
import NamingExample from 'typings/Settings/NamingExample';
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
import MetadataAppState from './MetadataAppState';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
|
|
@ -97,6 +98,7 @@ interface SettingsAppState {
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
metadata: MetadataAppState;
|
||||||
naming: NamingAppState;
|
naming: NamingAppState;
|
||||||
namingExamples: NamingExamplesAppState;
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,8 @@ ProviderFieldFormGroup.propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
advanced: PropTypes.bool.isRequired,
|
advanced: PropTypes.bool.isRequired,
|
||||||
hidden: PropTypes.string,
|
hidden: PropTypes.string,
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
provider: PropTypes.string,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ function createImportListExclusionSelector(id?: number) {
|
||||||
importListExclusions;
|
importListExclusions;
|
||||||
|
|
||||||
const mapping = id
|
const mapping = id
|
||||||
? items.find((i) => i.id === id)
|
? items.find((i) => i.id === id)!
|
||||||
: newImportListExclusion;
|
: newImportListExclusion;
|
||||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 EditMetadataModalContentConnector from './EditMetadataModalContentConnector';
|
|
||||||
|
|
||||||
function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<EditMetadataModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditMetadataModal;
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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 EditMetadataModalContent, {
|
||||||
|
EditMetadataModalContentProps,
|
||||||
|
} from './EditMetadataModalContent';
|
||||||
|
|
||||||
|
interface EditMetadataModalProps extends EditMetadataModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditMetadataModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: EditMetadataModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
dispatch(clearPendingChanges({ section: 'metadata' }));
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
|
<EditMetadataModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMetadataModal;
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import EditMetadataModal from './EditMetadataModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
const section = 'settings.metadata';
|
|
||||||
|
|
||||||
return {
|
|
||||||
dispatchClearPendingChanges() {
|
|
||||||
dispatch(clearPendingChanges({ section }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditMetadataModalConnector extends Component {
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.dispatchClearPendingChanges({ section: 'metadata' });
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditMetadataModal
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModalConnector.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(EditMetadataModalConnector);
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
.message {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
7
frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts
vendored
Normal file
7
frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
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 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 } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function EditMetadataModalContent(props) {
|
|
||||||
const {
|
|
||||||
advancedSettings,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item,
|
|
||||||
onInputChange,
|
|
||||||
onFieldChange,
|
|
||||||
onModalClose,
|
|
||||||
onSavePress,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
enable,
|
|
||||||
fields
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('EditMetadata', { metadataType: name.value })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form {...otherProps}>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Enable')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="enable"
|
|
||||||
helpText={translate('EnableMetadataHelpText')}
|
|
||||||
{...enable}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
fields.map((field) => {
|
|
||||||
return (
|
|
||||||
<ProviderFieldFormGroup
|
|
||||||
key={field.name}
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
provider="metadata"
|
|
||||||
{...field}
|
|
||||||
isDisabled={!enable.value}
|
|
||||||
onChange={onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
error={saveError}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModalContent.propTypes = {
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
isSaving: 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,
|
|
||||||
onDeleteMetadataPress: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditMetadataModalContent;
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
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 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 } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
saveMetadata,
|
||||||
|
setMetadataFieldValue,
|
||||||
|
setMetadataValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditMetadataModalContent.css';
|
||||||
|
|
||||||
|
export interface EditMetadataModalContentProps {
|
||||||
|
id: number;
|
||||||
|
advancedSettings: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditMetadataModalContent({
|
||||||
|
id,
|
||||||
|
advancedSettings,
|
||||||
|
onModalClose,
|
||||||
|
}: EditMetadataModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { isSaving, saveError, pendingChanges, items } = useSelector(
|
||||||
|
(state: AppState) => state.settings.metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
const { settings, ...otherSettings } = useMemo(() => {
|
||||||
|
const item = items.find((item) => item.id === id)!;
|
||||||
|
|
||||||
|
return selectSettings(item, pendingChanges, saveError);
|
||||||
|
}, [id, items, pendingChanges, saveError]);
|
||||||
|
|
||||||
|
const { name, enable, fields, message } = settings;
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error not typed
|
||||||
|
dispatch(setMetadataValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFieldChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error not typed
|
||||||
|
dispatch(setMetadataFieldValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSavePress = useCallback(() => {
|
||||||
|
dispatch(saveMetadata({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('EditMetadata', { metadataType: name.value })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form {...otherSettings}>
|
||||||
|
{message ? (
|
||||||
|
<Alert className={styles.message} kind={message.value.type}>
|
||||||
|
{message.value.message}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Enable')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="enable"
|
||||||
|
helpText={translate('EnableMetadataHelpText')}
|
||||||
|
{...enable}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{fields.map((field) => {
|
||||||
|
return (
|
||||||
|
<ProviderFieldFormGroup
|
||||||
|
key={field.name}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
provider="metadata"
|
||||||
|
{...field}
|
||||||
|
isDisabled={!enable.value}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMetadataModalContent;
|
||||||
|
|
@ -1,95 +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 { saveMetadata, setMetadataFieldValue, setMetadataValue } from 'Store/Actions/settingsActions';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import EditMetadataModalContent from './EditMetadataModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.advancedSettings,
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.settings.metadata,
|
|
||||||
(advancedSettings, id, metadata) => {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
pendingChanges,
|
|
||||||
items
|
|
||||||
} = metadata;
|
|
||||||
|
|
||||||
const settings = selectSettings(_.find(items, { id }), pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
advancedSettings,
|
|
||||||
id,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item: settings.settings,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setMetadataValue,
|
|
||||||
setMetadataFieldValue,
|
|
||||||
saveMetadata
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditMetadataModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
|
||||||
this.props.onModalClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setMetadataValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFieldChange = ({ name, value }) => {
|
|
||||||
this.props.setMetadataFieldValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
this.props.saveMetadata({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditMetadataModalContent
|
|
||||||
{...this.props}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onFieldChange={this.onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModalContentConnector.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
setMetadataValue: PropTypes.func.isRequired,
|
|
||||||
setMetadataFieldValue: PropTypes.func.isRequired,
|
|
||||||
saveMetadata: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataModalContentConnector);
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Card from 'Components/Card';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EditMetadataModalConnector from './EditMetadataModalConnector';
|
|
||||||
import styles from './Metadata.css';
|
|
||||||
|
|
||||||
class Metadata extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditMetadataModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditMetadataPress = () => {
|
|
||||||
this.setState({ isEditMetadataModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditMetadataModalClose = () => {
|
|
||||||
this.setState({ isEditMetadataModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
enable,
|
|
||||||
fields
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const metadataFields = [];
|
|
||||||
const imageFields = [];
|
|
||||||
|
|
||||||
fields.forEach((field) => {
|
|
||||||
if (field.section === 'metadata') {
|
|
||||||
metadataFields.push(field);
|
|
||||||
} else {
|
|
||||||
imageFields.push(field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={styles.metadata}
|
|
||||||
overlayContent={true}
|
|
||||||
onPress={this.onEditMetadataPress}
|
|
||||||
>
|
|
||||||
<div className={styles.name}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
enable ?
|
|
||||||
<Label kind={kinds.SUCCESS}>
|
|
||||||
{translate('Enabled')}
|
|
||||||
</Label> :
|
|
||||||
<Label
|
|
||||||
kind={kinds.DISABLED}
|
|
||||||
outline={true}
|
|
||||||
>
|
|
||||||
{translate('Disabled')}
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
enable && !!metadataFields.length &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.section}>
|
|
||||||
{translate('Metadata')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
metadataFields.map((field) => {
|
|
||||||
if (!field.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={field.label}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
enable && !!imageFields.length &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.section}>
|
|
||||||
{translate('Images')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
imageFields.map((field) => {
|
|
||||||
if (!field.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={field.label}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<EditMetadataModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={this.state.isEditMetadataModalOpen}
|
|
||||||
onModalClose={this.onEditMetadataModalClose}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
enable: PropTypes.bool.isRequired,
|
|
||||||
fields: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Metadata;
|
|
||||||
107
frontend/src/Settings/Metadata/Metadata/Metadata.tsx
Normal file
107
frontend/src/Settings/Metadata/Metadata/Metadata.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Field from 'typings/Field';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditMetadataModal from './EditMetadataModal';
|
||||||
|
import styles from './Metadata.css';
|
||||||
|
|
||||||
|
interface MetadataProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
fields: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metadata({ id, name, enable, fields }: MetadataProps) {
|
||||||
|
const [isEditMetadataModalOpen, setIsEditMetadataModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { metadataFields, imageFields } = useMemo(() => {
|
||||||
|
return fields.reduce<{ metadataFields: Field[]; imageFields: Field[] }>(
|
||||||
|
(acc, field) => {
|
||||||
|
if (field.section === 'metadata') {
|
||||||
|
acc.metadataFields.push(field);
|
||||||
|
} else {
|
||||||
|
acc.imageFields.push(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ metadataFields: [], imageFields: [] }
|
||||||
|
);
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
const handleOpenPress = useCallback(() => {
|
||||||
|
setIsEditMetadataModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
setIsEditMetadataModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.metadata}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={handleOpenPress}
|
||||||
|
>
|
||||||
|
<div className={styles.name}>{name}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{enable ? (
|
||||||
|
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
|
||||||
|
) : (
|
||||||
|
<Label kind={kinds.DISABLED} outline={true}>
|
||||||
|
{translate('Disabled')}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enable && metadataFields.length ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.section}>{translate('Metadata')}</div>
|
||||||
|
|
||||||
|
{metadataFields.map((field) => {
|
||||||
|
if (!field.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label key={field.label} kind={kinds.SUCCESS}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{enable && imageFields.length ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.section}>{translate('Images')}</div>
|
||||||
|
|
||||||
|
{imageFields.map((field) => {
|
||||||
|
if (!field.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label key={field.label} kind={kinds.SUCCESS}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<EditMetadataModal
|
||||||
|
advancedSettings={false}
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditMetadataModalOpen}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadata;
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import Metadata from './Metadata';
|
|
||||||
import styles from './Metadatas.css';
|
|
||||||
|
|
||||||
function Metadatas(props) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('Metadata')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('MetadataLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.metadatas}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<Metadata
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadatas.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Metadatas;
|
|
||||||
52
frontend/src/Settings/Metadata/Metadata/Metadatas.tsx
Normal file
52
frontend/src/Settings/Metadata/Metadata/Metadatas.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import MetadataAppState from 'App/State/MetadataAppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import { fetchMetadata } from 'Store/Actions/settingsActions';
|
||||||
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import MetadataType from 'typings/Metadata';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import Metadata from './Metadata';
|
||||||
|
import styles from './Metadatas.css';
|
||||||
|
|
||||||
|
function createMetadatasSelector() {
|
||||||
|
return createSelector(
|
||||||
|
createSortedSectionSelector<MetadataType>(
|
||||||
|
'settings.metadata',
|
||||||
|
sortByProp('name')
|
||||||
|
),
|
||||||
|
(metadata: MetadataAppState) => metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metadatas() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, error, items, ...otherProps } = useSelector(
|
||||||
|
createMetadatasSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchMetadata());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('Metadata')}>
|
||||||
|
<PageSectionContent
|
||||||
|
isFetching={isFetching}
|
||||||
|
errorMessage={translate('MetadataLoadError')}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.metadatas}>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <Metadata key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadatas;
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchMetadata } from 'Store/Actions/settingsActions';
|
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import Metadatas from './Metadatas';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSortedSectionSelector('settings.metadata', sortByProp('name')),
|
|
||||||
(metadata) => metadata
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchMetadata
|
|
||||||
};
|
|
||||||
|
|
||||||
class MetadatasConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Metadatas
|
|
||||||
{...this.props}
|
|
||||||
onConfirmDeleteMetadata={this.onConfirmDeleteMetadata}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MetadatasConnector.propTypes = {
|
|
||||||
fetchMetadata: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
|
||||||
|
|
@ -3,7 +3,7 @@ import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MetadatasConnector from './Metadata/MetadatasConnector';
|
import Metadatas from './Metadata/Metadatas';
|
||||||
import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
|
import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
|
||||||
|
|
||||||
class MetadataSettings extends Component {
|
class MetadataSettings extends Component {
|
||||||
|
|
@ -62,7 +62,7 @@ class MetadataSettings extends Component {
|
||||||
onChildStateChange={this.onChildStateChange}
|
onChildStateChange={this.onChildStateChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetadatasConnector />
|
<Metadatas />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,15 @@ import {
|
||||||
setReleaseProfileValue,
|
setReleaseProfileValue,
|
||||||
} from 'Store/Actions/Settings/releaseProfiles';
|
} from 'Store/Actions/Settings/releaseProfiles';
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import { PendingSection } from 'typings/pending';
|
|
||||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditReleaseProfileModalContent.css';
|
import styles from './EditReleaseProfileModalContent.css';
|
||||||
|
|
||||||
const tagInputDelimiters = ['Tab', 'Enter'];
|
const tagInputDelimiters = ['Tab', 'Enter'];
|
||||||
|
|
||||||
const newReleaseProfile = {
|
const newReleaseProfile: ReleaseProfile = {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
required: [],
|
required: [],
|
||||||
ignored: [],
|
ignored: [],
|
||||||
|
|
@ -41,8 +42,12 @@ function createReleaseProfileSelector(id?: number) {
|
||||||
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
|
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
|
||||||
releaseProfiles;
|
releaseProfiles;
|
||||||
|
|
||||||
const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile;
|
const mapping = id ? items.find((i) => i.id === id)! : newReleaseProfile;
|
||||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
const settings = selectSettings<ReleaseProfile>(
|
||||||
|
mapping,
|
||||||
|
pendingChanges,
|
||||||
|
saveError
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|
@ -50,7 +55,7 @@ function createReleaseProfileSelector(id?: number) {
|
||||||
error,
|
error,
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
item: settings.settings as PendingSection<ReleaseProfile>,
|
item: settings.settings,
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
function getValidationFailures(saveError) {
|
|
||||||
if (!saveError || saveError.status !== 400) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.cloneDeep(saveError.responseJSON);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapFailure(failure) {
|
|
||||||
return {
|
|
||||||
message: failure.errorMessage,
|
|
||||||
link: failure.infoLink,
|
|
||||||
detailedMessage: failure.detailedDescription
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectSettings(item, pendingChanges, saveError) {
|
|
||||||
const validationFailures = getValidationFailures(saveError);
|
|
||||||
|
|
||||||
// Merge all settings from the item along with pending
|
|
||||||
// changes to ensure any settings that were not included
|
|
||||||
// with the item are included.
|
|
||||||
const allSettings = Object.assign({}, item, pendingChanges);
|
|
||||||
|
|
||||||
const settings = _.reduce(allSettings, (result, value, key) => {
|
|
||||||
if (key === 'fields') {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a flattened value
|
|
||||||
if (key === 'implementationName') {
|
|
||||||
result.implementationName = item[key];
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const setting = {
|
|
||||||
value: item[key],
|
|
||||||
errors: _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === key.toLowerCase() && !failure.isWarning;
|
|
||||||
}), mapFailure),
|
|
||||||
|
|
||||||
warnings: _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === key.toLowerCase() && failure.isWarning;
|
|
||||||
}), mapFailure)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (pendingChanges.hasOwnProperty(key)) {
|
|
||||||
setting.previousValue = setting.value;
|
|
||||||
setting.value = pendingChanges[key];
|
|
||||||
setting.pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result[key] = setting;
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const fields = _.reduce(item.fields, (result, f) => {
|
|
||||||
const field = Object.assign({ pending: false }, f);
|
|
||||||
const hasPendingFieldChange = pendingChanges.fields && pendingChanges.fields.hasOwnProperty(field.name);
|
|
||||||
|
|
||||||
if (hasPendingFieldChange) {
|
|
||||||
field.previousValue = field.value;
|
|
||||||
field.value = pendingChanges.fields[field.name];
|
|
||||||
field.pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
field.errors = _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && !failure.isWarning;
|
|
||||||
}), mapFailure);
|
|
||||||
|
|
||||||
field.warnings = _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && failure.isWarning;
|
|
||||||
}), mapFailure);
|
|
||||||
|
|
||||||
result.push(field);
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (fields.length) {
|
|
||||||
settings.fields = fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationErrors = _.filter(validationFailures, (failure) => {
|
|
||||||
return !failure.isWarning;
|
|
||||||
});
|
|
||||||
|
|
||||||
const validationWarnings = _.filter(validationFailures, (failure) => {
|
|
||||||
return failure.isWarning;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings,
|
|
||||||
hasPendingChanges: !_.isEmpty(pendingChanges),
|
|
||||||
hasSettings: !_.isEmpty(settings),
|
|
||||||
pendingChanges
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default selectSettings;
|
|
||||||
168
frontend/src/Store/Selectors/selectSettings.ts
Normal file
168
frontend/src/Store/Selectors/selectSettings.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { cloneDeep, isEmpty } from 'lodash';
|
||||||
|
import { Error } from 'App/State/AppSectionState';
|
||||||
|
import Field from 'typings/Field';
|
||||||
|
import {
|
||||||
|
Failure,
|
||||||
|
Pending,
|
||||||
|
PendingField,
|
||||||
|
PendingSection,
|
||||||
|
ValidationError,
|
||||||
|
ValidationFailure,
|
||||||
|
ValidationWarning,
|
||||||
|
} from 'typings/pending';
|
||||||
|
|
||||||
|
interface ValidationFailures {
|
||||||
|
errors: ValidationError[];
|
||||||
|
warnings: ValidationWarning[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidationFailures(saveError?: Error): ValidationFailures {
|
||||||
|
if (!saveError || saveError.status !== 400) {
|
||||||
|
return {
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloneDeep(saveError.responseJSON as ValidationFailure[]).reduce(
|
||||||
|
(acc: ValidationFailures, failure: ValidationFailure) => {
|
||||||
|
if (failure.isWarning) {
|
||||||
|
acc.warnings.push(failure as ValidationWarning);
|
||||||
|
} else {
|
||||||
|
acc.errors.push(failure as ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFailures(failures: ValidationFailure[], key: string) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = failures.length - 1; i >= 0; i--) {
|
||||||
|
if (failures[i].propertyName.toLowerCase() === key.toLowerCase()) {
|
||||||
|
result.unshift(mapFailure(failures[i]));
|
||||||
|
|
||||||
|
failures.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFailure(failure: ValidationFailure): Failure {
|
||||||
|
return {
|
||||||
|
errorMessage: failure.errorMessage,
|
||||||
|
infoLink: failure.infoLink,
|
||||||
|
detailedDescription: failure.detailedDescription,
|
||||||
|
|
||||||
|
// TODO: Remove these renamed properties
|
||||||
|
message: failure.errorMessage,
|
||||||
|
link: failure.infoLink,
|
||||||
|
detailedMessage: failure.detailedDescription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelBaseSetting {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[id: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSettings<T extends ModelBaseSetting>(
|
||||||
|
item: T,
|
||||||
|
pendingChanges: Partial<ModelBaseSetting>,
|
||||||
|
saveError?: Error
|
||||||
|
) {
|
||||||
|
const { errors, warnings } = getValidationFailures(saveError);
|
||||||
|
|
||||||
|
// Merge all settings from the item along with pending
|
||||||
|
// changes to ensure any settings that were not included
|
||||||
|
// with the item are included.
|
||||||
|
const allSettings = Object.assign({}, item, pendingChanges);
|
||||||
|
|
||||||
|
const settings = Object.keys(allSettings).reduce(
|
||||||
|
(acc: PendingSection<T>, key) => {
|
||||||
|
if (key === 'fields') {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a flattened value
|
||||||
|
if (key === 'implementationName') {
|
||||||
|
acc.implementationName = item[key];
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setting: Pending<T> = {
|
||||||
|
value: item[key],
|
||||||
|
pending: false,
|
||||||
|
errors: getFailures(errors, key),
|
||||||
|
warnings: getFailures(warnings, key),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pendingChanges.hasOwnProperty(key)) {
|
||||||
|
setting.previousValue = setting.value;
|
||||||
|
setting.value = pendingChanges[key];
|
||||||
|
setting.pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - This is a valid key
|
||||||
|
acc[key] = setting;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as PendingSection<T>
|
||||||
|
);
|
||||||
|
|
||||||
|
if ('fields' in item) {
|
||||||
|
const fields =
|
||||||
|
(item.fields as Field[]).reduce((acc: PendingField<T>[], f) => {
|
||||||
|
const field: PendingField<T> = Object.assign(
|
||||||
|
{ pending: false, errors: [], warnings: [] },
|
||||||
|
f
|
||||||
|
);
|
||||||
|
|
||||||
|
if ('fields' in pendingChanges) {
|
||||||
|
const pendingChangesFields = pendingChanges.fields as Record<
|
||||||
|
string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (pendingChangesFields.hasOwnProperty(field.name)) {
|
||||||
|
field.previousValue = field.value;
|
||||||
|
field.value = pendingChangesFields[field.name];
|
||||||
|
field.pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
field.errors = getFailures(errors, field.name);
|
||||||
|
field.warnings = getFailures(warnings, field.name);
|
||||||
|
|
||||||
|
acc.push(field);
|
||||||
|
return acc;
|
||||||
|
}, []) ?? [];
|
||||||
|
|
||||||
|
if (fields.length) {
|
||||||
|
settings.fields = fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationErrors = errors;
|
||||||
|
const validationWarnings = warnings;
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings,
|
||||||
|
hasPendingChanges: !isEmpty(pendingChanges),
|
||||||
|
hasSettings: !isEmpty(settings),
|
||||||
|
pendingChanges,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default selectSettings;
|
||||||
|
|
@ -1,27 +1,13 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
|
|
||||||
export interface Field {
|
export type Protocol = 'torrent' | 'usenet' | 'unknown';
|
||||||
order: number;
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
value: boolean | number | string;
|
|
||||||
type: string;
|
|
||||||
advanced: boolean;
|
|
||||||
privacy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DownloadClient extends ModelBase {
|
interface DownloadClient extends Provider {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
protocol: string;
|
protocol: Protocol;
|
||||||
priority: number;
|
priority: number;
|
||||||
removeCompletedDownloads: boolean;
|
removeCompletedDownloads: boolean;
|
||||||
removeFailedDownloads: boolean;
|
removeFailedDownloads: boolean;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
23
frontend/src/typings/Field.ts
Normal file
23
frontend/src/typings/Field.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
export interface FieldSelectOption<T> {
|
||||||
|
value: T;
|
||||||
|
name: string;
|
||||||
|
order: number;
|
||||||
|
hint?: string;
|
||||||
|
parentValue?: T;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
additionalProperties?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Field {
|
||||||
|
order: number;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
value: boolean | number | string | number[];
|
||||||
|
section: string;
|
||||||
|
hidden: 'hidden' | 'hiddenIfNotSet' | 'visible';
|
||||||
|
type: string;
|
||||||
|
advanced: boolean;
|
||||||
|
privacy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Field;
|
||||||
|
|
@ -1,28 +1,12 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
|
|
||||||
export interface Field {
|
interface ImportList extends Provider {
|
||||||
order: number;
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
value: boolean | number | string;
|
|
||||||
type: string;
|
|
||||||
advanced: boolean;
|
|
||||||
privacy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportList extends ModelBase {
|
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
enableAuto: boolean;
|
enableAuto: boolean;
|
||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
minimumAvailability: string;
|
minimumAvailability: string;
|
||||||
rootFolderPath: string;
|
rootFolderPath: string;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,11 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
|
|
||||||
export interface Field {
|
interface Indexer extends Provider {
|
||||||
order: number;
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
value: boolean | number | string;
|
|
||||||
type: string;
|
|
||||||
advanced: boolean;
|
|
||||||
privacy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Indexer extends ModelBase {
|
|
||||||
enableRss: boolean;
|
enableRss: boolean;
|
||||||
enableAutomaticSearch: boolean;
|
enableAutomaticSearch: boolean;
|
||||||
enableInteractiveSearch: boolean;
|
enableInteractiveSearch: boolean;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
7
frontend/src/typings/Metadata.ts
Normal file
7
frontend/src/typings/Metadata.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Provider from './Provider';
|
||||||
|
|
||||||
|
interface Metadata extends Provider {
|
||||||
|
enable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadata;
|
||||||
|
|
@ -1,23 +1,7 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
|
|
||||||
export interface Field {
|
interface Notification extends Provider {
|
||||||
order: number;
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
value: boolean | number | string;
|
|
||||||
type: string;
|
|
||||||
advanced: boolean;
|
|
||||||
privacy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Notification extends ModelBase {
|
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
frontend/src/typings/Provider.ts
Normal file
20
frontend/src/typings/Provider.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import Field from './Field';
|
||||||
|
|
||||||
|
export interface ProviderMessage {
|
||||||
|
message: string;
|
||||||
|
type: Extract<Kind, 'info' | 'error' | 'warning'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Provider extends ModelBase {
|
||||||
|
name: string;
|
||||||
|
fields: Field[];
|
||||||
|
implementationName: string;
|
||||||
|
implementation: string;
|
||||||
|
configContract: string;
|
||||||
|
infoLink: string;
|
||||||
|
message: ProviderMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Provider;
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
import Field from './Field';
|
||||||
|
|
||||||
export interface ValidationFailure {
|
export interface ValidationFailure {
|
||||||
|
isWarning: boolean;
|
||||||
propertyName: string;
|
propertyName: string;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
infoLink?: string;
|
||||||
|
detailedDescription?: string;
|
||||||
severity: 'error' | 'warning';
|
severity: 'error' | 'warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -12,12 +17,47 @@ export interface ValidationWarning extends ValidationFailure {
|
||||||
isWarning: true;
|
isWarning: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Pending<T> {
|
export interface Failure {
|
||||||
value: T;
|
errorMessage: ValidationFailure['errorMessage'];
|
||||||
errors: ValidationError[];
|
infoLink: ValidationFailure['infoLink'];
|
||||||
warnings: ValidationWarning[];
|
detailedDescription: ValidationFailure['detailedDescription'];
|
||||||
|
|
||||||
|
// TODO: Remove these renamed properties
|
||||||
|
|
||||||
|
message: ValidationFailure['errorMessage'];
|
||||||
|
link: ValidationFailure['infoLink'];
|
||||||
|
detailedMessage: ValidationFailure['detailedDescription'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PendingSection<T> = {
|
export interface Pending<T> {
|
||||||
[K in keyof T]: Pending<T[K]>;
|
value: T;
|
||||||
|
errors: Failure[];
|
||||||
|
warnings: Failure[];
|
||||||
|
pending: boolean;
|
||||||
|
previousValue?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingField<T>
|
||||||
|
extends Field,
|
||||||
|
Omit<Pending<T>, 'previousValue' | 'value'> {
|
||||||
|
previousValue?: Field['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// export type PendingSection<T> = {
|
||||||
|
// [K in keyof T]: Pending<T[K]>;
|
||||||
|
// };
|
||||||
|
|
||||||
|
type Mapped<T> = {
|
||||||
|
[Prop in keyof T]: {
|
||||||
|
value: T[Prop];
|
||||||
|
errors: Failure[];
|
||||||
|
warnings: Failure[];
|
||||||
|
pending?: boolean;
|
||||||
|
previousValue?: T[Prop];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PendingSection<T> = Mapped<T> & {
|
||||||
|
implementationName?: string;
|
||||||
|
fields?: PendingField<T>[];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Extras.Metadata.Files;
|
using NzbDrone.Core.Extras.Metadata.Files;
|
||||||
using NzbDrone.Core.MediaCover;
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa
|
namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa
|
||||||
{
|
{
|
||||||
|
|
@ -15,13 +14,15 @@ public class KometaMetadata : MetadataBase<KometaMetadataSettings>
|
||||||
{
|
{
|
||||||
private static readonly Regex MovieImagesRegex = new (@"^(?:poster|background)\.(?:png|jpe?g)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex MovieImagesRegex = new (@"^(?:poster|background)\.(?:png|jpe?g)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private readonly IMapCoversToLocal _mediaCoverService;
|
private readonly ILocalizationService _localizationService;
|
||||||
|
|
||||||
public override string Name => "Kometa";
|
public override string Name => "Kometa";
|
||||||
|
|
||||||
public KometaMetadata(IMapCoversToLocal mediaCoverService)
|
public override ProviderMessage Message => new (_localizationService.GetLocalizedString("MetadataKometaDeprecated"), ProviderMessageType.Warning);
|
||||||
|
|
||||||
|
public KometaMetadata(ILocalizationService localizationService)
|
||||||
{
|
{
|
||||||
_mediaCoverService = mediaCoverService;
|
_localizationService = localizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override MetadataFile FindMetadataFile(Movie movie, string path)
|
public override MetadataFile FindMetadataFile(Movie movie, string path)
|
||||||
|
|
@ -56,31 +57,7 @@ public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFil
|
||||||
|
|
||||||
public override List<ImageFileResult> MovieImages(Movie movie)
|
public override List<ImageFileResult> MovieImages(Movie movie)
|
||||||
{
|
{
|
||||||
if (!Settings.MovieImages)
|
return new List<ImageFileResult>();
|
||||||
{
|
|
||||||
return new List<ImageFileResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProcessMovieImages(movie).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<ImageFileResult> ProcessMovieImages(Movie movie)
|
|
||||||
{
|
|
||||||
foreach (var image in movie.MovieMetadata.Value.Images.Where(i => i.CoverType is MediaCoverTypes.Poster or MediaCoverTypes.Fanart))
|
|
||||||
{
|
|
||||||
var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType);
|
|
||||||
|
|
||||||
var filename = image.CoverType switch
|
|
||||||
{
|
|
||||||
MediaCoverTypes.Poster => "poster",
|
|
||||||
MediaCoverTypes.Fanart => "background",
|
|
||||||
_ => throw new ArgumentOutOfRangeException($"{image.CoverType} is not supported")
|
|
||||||
};
|
|
||||||
|
|
||||||
var destination = filename + Path.GetExtension(source);
|
|
||||||
|
|
||||||
yield return new ImageFileResult(destination, source);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ public class KometaMetadataSettings : IProviderConfig
|
||||||
|
|
||||||
public KometaMetadataSettings()
|
public KometaMetadataSettings()
|
||||||
{
|
{
|
||||||
MovieImages = true;
|
Deprecated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[FieldDefinition(0, Label = "MetadataSettingsMovieImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "poster.jpg, background.jpg")]
|
[FieldDefinition(0, Label = "MetadataKometaDeprecatedSetting", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, Hidden = HiddenType.Hidden)]
|
||||||
public bool MovieImages { get; set; }
|
public bool Deprecated { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
34
src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs
Normal file
34
src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Extras.Metadata;
|
||||||
|
using NzbDrone.Core.Extras.Metadata.Consumers.Kometa;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
using NzbDrone.Core.ThingiProvider.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.HealthCheck.Checks
|
||||||
|
{
|
||||||
|
[CheckOn(typeof(ProviderUpdatedEvent<IMetadata>))]
|
||||||
|
public class MetadataCheck : HealthCheckBase
|
||||||
|
{
|
||||||
|
private readonly IMetadataFactory _metadataFactory;
|
||||||
|
|
||||||
|
public MetadataCheck(IMetadataFactory metadataFactory, ILocalizationService localizationService)
|
||||||
|
: base(localizationService)
|
||||||
|
{
|
||||||
|
_metadataFactory = metadataFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override HealthCheck Check()
|
||||||
|
{
|
||||||
|
var enabled = _metadataFactory.Enabled();
|
||||||
|
|
||||||
|
if (enabled.Any(m => m.Definition.Implementation == nameof(KometaMetadata)))
|
||||||
|
{
|
||||||
|
return new HealthCheck(GetType(),
|
||||||
|
HealthCheckResult.Warning,
|
||||||
|
$"{_localizationService.GetLocalizedString("MetadataKometaDeprecated")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HealthCheck(GetType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -919,6 +919,8 @@
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
"Message": "Message",
|
"Message": "Message",
|
||||||
"Metadata": "Metadata",
|
"Metadata": "Metadata",
|
||||||
|
"MetadataKometaDeprecated": "Kometa files will no longer be created, support will be removed completely in v6",
|
||||||
|
"MetadataKometaDeprecatedSetting": "Deprecated",
|
||||||
"MetadataLoadError": "Unable to load Metadata",
|
"MetadataLoadError": "Unable to load Metadata",
|
||||||
"MetadataSettings": "Metadata Settings",
|
"MetadataSettings": "Metadata Settings",
|
||||||
"MetadataSettingsMovieImages": "Movie Images",
|
"MetadataSettingsMovieImages": "Movie Images",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue