mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-13 18:27:08 -07:00
New: UI Updates (Backup Restore in App, Profile Cloning)
UI Pulls from Sonarr
This commit is contained in:
parent
80a5701b99
commit
744742b5ff
80 changed files with 2376 additions and 795 deletions
42
frontend/src/Settings/General/AnalyticSettings.js
Normal file
42
frontend/src/Settings/General/AnalyticSettings.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function AnalyticSettings(props) {
|
||||
const {
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
analyticsEnabled
|
||||
} = settings;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Analytics">
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Send Anonymous Usage Data</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="analyticsEnabled"
|
||||
helpText="Send anonymous usage and error information to Sonarr's servers. This includes information on your browser, which Lidarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes."
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...analyticsEnabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
AnalyticSettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AnalyticSettings;
|
82
frontend/src/Settings/General/BackupSettings.js
Normal file
82
frontend/src/Settings/General/BackupSettings.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function BackupSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
backupFolder,
|
||||
backupInterval,
|
||||
backupRetention
|
||||
} = settings;
|
||||
|
||||
if (!advancedSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldSet legend="Backups">
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="backupFolder"
|
||||
helpText="Relative paths will be under Lidarr's AppData directory"
|
||||
onChange={onInputChange}
|
||||
{...backupFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Interval</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="backupInterval"
|
||||
helpText="Interval in days"
|
||||
onChange={onInputChange}
|
||||
{...backupInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Retention</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="backupRetention"
|
||||
helpText="Retention in days. Automatic backups older the retention will be cleaned up automatically"
|
||||
onChange={onInputChange}
|
||||
{...backupRetention}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
BackupSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BackupSettings;
|
|
@ -1,20 +1,20 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import AnalyticSettings from './AnalyticSettings';
|
||||
import BackupSettings from './BackupSettings';
|
||||
import HostSettings from './HostSettings';
|
||||
import LoggingSettings from './LoggingSettings';
|
||||
import ProxySettings from './ProxySettings';
|
||||
import SecuritySettings from './SecuritySettings';
|
||||
import UpdateSettings from './UpdateSettings';
|
||||
|
||||
class GeneralSettings extends Component {
|
||||
|
||||
|
@ -25,7 +25,6 @@ class GeneralSettings extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmApiKeyResetModalOpen: false,
|
||||
isRestartRequiredModalOpen: false
|
||||
};
|
||||
}
|
||||
|
@ -76,23 +75,6 @@ class GeneralSettings extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onApikeyFocus = (event) => {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
onResetApiKeyPress = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmResetApiKey = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: false });
|
||||
this.props.onConfirmResetApiKey();
|
||||
}
|
||||
|
||||
onCloseResetApiKeyModal = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmRestart = () => {
|
||||
this.setState({ isRestartRequiredModalOpen: false });
|
||||
this.props.onConfirmRestart();
|
||||
|
@ -118,67 +100,10 @@ class GeneralSettings extends Component {
|
|||
isWindows,
|
||||
mode,
|
||||
onInputChange,
|
||||
onConfirmResetApiKey,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isConfirmApiKeyResetModalOpen,
|
||||
isRestartRequiredModalOpen
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
bindAddress,
|
||||
port,
|
||||
urlBase,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertHash,
|
||||
launchBrowser,
|
||||
authenticationMethod,
|
||||
username,
|
||||
password,
|
||||
apiKey,
|
||||
proxyEnabled,
|
||||
proxyType,
|
||||
proxyHostname,
|
||||
proxyPort,
|
||||
proxyUsername,
|
||||
proxyPassword,
|
||||
proxyBypassFilter,
|
||||
proxyBypassLocalAddresses,
|
||||
logLevel,
|
||||
analyticsEnabled,
|
||||
branch,
|
||||
updateAutomatically,
|
||||
updateMechanism,
|
||||
updateScriptPath
|
||||
} = settings;
|
||||
|
||||
const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None' },
|
||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||
];
|
||||
|
||||
const proxyTypeOptions = [
|
||||
{ key: 'http', value: 'HTTP(S)' },
|
||||
{ key: 'socks4', value: 'Socks4' },
|
||||
{ key: 'socks5', value: 'Socks5 (Support TOR)' }
|
||||
];
|
||||
|
||||
const logLevelOptions = [
|
||||
{ key: 'info', value: 'Info' },
|
||||
{ key: 'debug', value: 'Debug' },
|
||||
{ key: 'trace', value: 'Trace' }
|
||||
];
|
||||
|
||||
const updateOptions = [
|
||||
{ key: 'builtIn', value: 'Built-In' },
|
||||
{ key: 'script', value: 'Script' }
|
||||
];
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
return (
|
||||
<PageContent title="General Settings">
|
||||
<SettingsToolbarConnector
|
||||
|
@ -202,425 +127,55 @@ class GeneralSettings extends Component {
|
|||
id="generalSettings"
|
||||
{...otherProps}
|
||||
>
|
||||
<FieldSet
|
||||
legend="Start-Up"
|
||||
>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Bind Address</FormLabel>
|
||||
<HostSettings
|
||||
advancedSettings={advancedSettings}
|
||||
settings={settings}
|
||||
isWindows={isWindows}
|
||||
mode={mode}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="bindAddress"
|
||||
helpText="Valid IP4 address or '*' for all interfaces"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...bindAddress}
|
||||
/>
|
||||
</FormGroup>
|
||||
<SecuritySettings
|
||||
settings={settings}
|
||||
isResettingApiKey={isResettingApiKey}
|
||||
onInputChange={onInputChange}
|
||||
onConfirmResetApiKey={onConfirmResetApiKey}
|
||||
/>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Port Number</FormLabel>
|
||||
<ProxySettings
|
||||
settings={settings}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="port"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...port}
|
||||
/>
|
||||
</FormGroup>
|
||||
<LoggingSettings
|
||||
settings={settings}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>URL Base</FormLabel>
|
||||
<AnalyticSettings
|
||||
settings={settings}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="urlBase"
|
||||
helpText="For reverse proxy support, default is empty"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...urlBase}
|
||||
/>
|
||||
</FormGroup>
|
||||
<UpdateSettings
|
||||
advancedSettings={advancedSettings}
|
||||
settings={settings}
|
||||
isMono={isMono}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>Enable SSL</FormLabel>
|
||||
<BackupSettings
|
||||
advancedSettings={advancedSettings}
|
||||
settings={settings}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableSsl"
|
||||
helpText=" Requires restart running as administrator to take effect"
|
||||
onChange={onInputChange}
|
||||
{...enableSsl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
enableSsl.value &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Port</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="sslPort"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslPort}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
isWindows && enableSsl.value &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Cert Hash</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="sslCertHash"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslCertHash}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
mode !== 'service' &&
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Open browser on start</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="launchBrowser"
|
||||
helpText=" Open a web browser and navigate to Lidarr homepage on app start."
|
||||
onChange={onInputChange}
|
||||
{...launchBrowser}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet
|
||||
legend="Security"
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>Authentication</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText="Require Username and Password to access Lidarr"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="apiKey"
|
||||
readOnly={true}
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
buttons={[
|
||||
<ClipboardButton
|
||||
key="copy"
|
||||
value={apiKey.value}
|
||||
kind={kinds.DEFAULT}
|
||||
/>,
|
||||
|
||||
<FormInputButton
|
||||
key="reset"
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onResetApiKeyPress}
|
||||
>
|
||||
<Icon name={isResettingApiKey ? `${icons.REFRESH} fa-spin` : icons.REFRESH} />
|
||||
</FormInputButton>
|
||||
]}
|
||||
onChange={onInputChange}
|
||||
onFocus={this.onApikeyFocus}
|
||||
{...apiKey}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet
|
||||
legend="Proxy Settings"
|
||||
>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Use Proxy</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proxyEnabled"
|
||||
onChange={onInputChange}
|
||||
{...proxyEnabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
proxyEnabled.value &&
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>Proxy Type</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="proxyType"
|
||||
values={proxyTypeOptions}
|
||||
onChange={onInputChange}
|
||||
{...proxyType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Hostname</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyHostname"
|
||||
onChange={onInputChange}
|
||||
{...proxyHostname}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Port</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="proxyPort"
|
||||
onChange={onInputChange}
|
||||
{...proxyPort}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyUsername"
|
||||
helpText="You only need to enter a username and password if one is required. Leave them blank otherwise."
|
||||
onChange={onInputChange}
|
||||
{...proxyUsername}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="proxyPassword"
|
||||
helpText="You only need to enter a username and password if one is required. Leave them blank otherwise."
|
||||
onChange={onInputChange}
|
||||
{...proxyPassword}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Ignored Addresses</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyBypassFilter"
|
||||
helpText="Use ',' as a separator, and '*.' as a wildcard for subdomains"
|
||||
onChange={onInputChange}
|
||||
{...proxyBypassFilter}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Bypass Proxy for Local Addresses</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proxyBypassLocalAddresses"
|
||||
onChange={onInputChange}
|
||||
{...proxyBypassLocalAddresses}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
}
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet
|
||||
legend="Logging"
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>Log Level</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="logLevel"
|
||||
values={logLevelOptions}
|
||||
helpTextWarning={logLevel.value === 'trace' ? 'Trace logging should only be enabled temporarily' : undefined}
|
||||
onChange={onInputChange}
|
||||
{...logLevel}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet
|
||||
legend="Analytics"
|
||||
>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Send Anonymous Usage Data</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="analyticsEnabled"
|
||||
helpText="Send anonymous usage and error information to Lidarr's servers. This includes information on your browser, which Lidarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes."
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...analyticsEnabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
{
|
||||
advancedSettings &&
|
||||
<FieldSet
|
||||
legend="Updates"
|
||||
>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Branch</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="branch"
|
||||
helpText="Branch to use to update Lidarr"
|
||||
helpLink="https://github.com/lidarr/Lidarr/wiki/Release-Branches"
|
||||
onChange={onInputChange}
|
||||
{...branch}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>Automatic</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText="Automatically download and install updates. You will still be able to install from System: Updates"
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Mechanism</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText="Use Lidarr's built-in updater or a script"
|
||||
helpLink="https://github.com/lidarr/Lidarr/wiki/Updating"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Script Path</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText="Path to a custom script that takes an extracted update package and handle the remainder of the update process"
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</FieldSet>
|
||||
}
|
||||
</Form>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmApiKeyResetModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Reset API Key"
|
||||
message="Are you sure you want to reset your API Key?"
|
||||
confirmLabel="Reset"
|
||||
onConfirm={this.onConfirmResetApiKey}
|
||||
onCancel={this.onCloseResetApiKeyModal}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isRestartRequiredModalOpen}
|
||||
isOpen={this.state.isRestartRequiredModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Restart Lidarr"
|
||||
message="Lidarr requires a restart to apply changes, do you want to restart now?"
|
||||
|
|
150
frontend/src/Settings/General/HostSettings.js
Normal file
150
frontend/src/Settings/General/HostSettings.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function HostSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isWindows,
|
||||
mode,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
bindAddress,
|
||||
port,
|
||||
urlBase,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertHash,
|
||||
launchBrowser
|
||||
} = settings;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Host">
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Bind Address</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="bindAddress"
|
||||
helpText="Valid IP4 address or '*' for all interfaces"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...bindAddress}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Port Number</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="port"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...port}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>URL Base</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="urlBase"
|
||||
helpText="For reverse proxy support, default is empty"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...urlBase}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>Enable SSL</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableSsl"
|
||||
helpText=" Requires restart running as administrator to take effect"
|
||||
onChange={onInputChange}
|
||||
{...enableSsl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
enableSsl.value &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Port</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="sslPort"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslPort}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
isWindows && enableSsl.value &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>SSL Cert Hash</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="sslCertHash"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...sslCertHash}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
mode !== 'service' &&
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Open browser on start</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="launchBrowser"
|
||||
helpText=" Open a web browser and navigate to Lidarr homepage on app start."
|
||||
onChange={onInputChange}
|
||||
{...launchBrowser}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
HostSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default HostSettings;
|
48
frontend/src/Settings/General/LoggingSettings.js
Normal file
48
frontend/src/Settings/General/LoggingSettings.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function LoggingSettings(props) {
|
||||
const {
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
logLevel
|
||||
} = settings;
|
||||
|
||||
const logLevelOptions = [
|
||||
{ key: 'info', value: 'Info' },
|
||||
{ key: 'debug', value: 'Debug' },
|
||||
{ key: 'trace', value: 'Trace' }
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldSet legend="Logging">
|
||||
<FormGroup>
|
||||
<FormLabel>Log Level</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="logLevel"
|
||||
values={logLevelOptions}
|
||||
helpTextWarning={logLevel.value === 'trace' ? 'Trace logging should only be enabled temporarily' : undefined}
|
||||
onChange={onInputChange}
|
||||
{...logLevel}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
LoggingSettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LoggingSettings;
|
139
frontend/src/Settings/General/ProxySettings.js
Normal file
139
frontend/src/Settings/General/ProxySettings.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function ProxySettings(props) {
|
||||
const {
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
proxyEnabled,
|
||||
proxyType,
|
||||
proxyHostname,
|
||||
proxyPort,
|
||||
proxyUsername,
|
||||
proxyPassword,
|
||||
proxyBypassFilter,
|
||||
proxyBypassLocalAddresses
|
||||
} = settings;
|
||||
|
||||
const proxyTypeOptions = [
|
||||
{ key: 'http', value: 'HTTP(S)' },
|
||||
{ key: 'socks4', value: 'Socks4' },
|
||||
{ key: 'socks5', value: 'Socks5 (Support TOR)' }
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldSet legend="Proxy">
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Use Proxy</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proxyEnabled"
|
||||
onChange={onInputChange}
|
||||
{...proxyEnabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
proxyEnabled.value &&
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>Proxy Type</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="proxyType"
|
||||
values={proxyTypeOptions}
|
||||
onChange={onInputChange}
|
||||
{...proxyType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Hostname</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyHostname"
|
||||
onChange={onInputChange}
|
||||
{...proxyHostname}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Port</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="proxyPort"
|
||||
onChange={onInputChange}
|
||||
{...proxyPort}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyUsername"
|
||||
helpText="You only need to enter a username and password if one is required. Leave them blank otherwise."
|
||||
onChange={onInputChange}
|
||||
{...proxyUsername}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="proxyPassword"
|
||||
helpText="You only need to enter a username and password if one is required. Leave them blank otherwise."
|
||||
onChange={onInputChange}
|
||||
{...proxyPassword}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Ignored Addresses</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="proxyBypassFilter"
|
||||
helpText="Use ',' as a separator, and '*.' as a wildcard for subdomains"
|
||||
onChange={onInputChange}
|
||||
{...proxyBypassFilter}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Bypass Proxy for Local Addresses</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proxyBypassLocalAddresses"
|
||||
onChange={onInputChange}
|
||||
{...proxyBypassLocalAddresses}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
ProxySettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ProxySettings;
|
170
frontend/src/Settings/General/SecuritySettings.js
Normal file
170
frontend/src/Settings/General/SecuritySettings.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, inputTypes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
|
||||
class SecuritySettings extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmApiKeyResetModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onApikeyFocus = (event) => {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
onResetApiKeyPress = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmResetApiKey = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: false });
|
||||
this.props.onConfirmResetApiKey();
|
||||
}
|
||||
|
||||
onCloseResetApiKeyModal = () => {
|
||||
this.setState({ isConfirmApiKeyResetModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
settings,
|
||||
isResettingApiKey,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
username,
|
||||
password,
|
||||
apiKey
|
||||
} = settings;
|
||||
|
||||
const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None' },
|
||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||
];
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
return (
|
||||
<FieldSet legend="Security">
|
||||
<FormGroup>
|
||||
<FormLabel>Authentication</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText="Require Username and Password to access Lidarr"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
<FormGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="apiKey"
|
||||
readOnly={true}
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
buttons={[
|
||||
<ClipboardButton
|
||||
key="copy"
|
||||
value={apiKey.value}
|
||||
kind={kinds.DEFAULT}
|
||||
/>,
|
||||
|
||||
<FormInputButton
|
||||
key="reset"
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onResetApiKeyPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.REFRESH}
|
||||
isSpinning={isResettingApiKey}
|
||||
/>
|
||||
</FormInputButton>
|
||||
]}
|
||||
onChange={onInputChange}
|
||||
onFocus={this.onApikeyFocus}
|
||||
{...apiKey}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isConfirmApiKeyResetModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Reset API Key"
|
||||
message="Are you sure you want to reset your API Key?"
|
||||
confirmLabel="Reset"
|
||||
onConfirm={this.onConfirmResetApiKey}
|
||||
onCancel={this.onCloseResetApiKeyModal}
|
||||
/>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SecuritySettings.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
isResettingApiKey: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onConfirmResetApiKey: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SecuritySettings;
|
117
frontend/src/Settings/General/UpdateSettings.js
Normal file
117
frontend/src/Settings/General/UpdateSettings.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function UpdateSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isMono,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
branch,
|
||||
updateAutomatically,
|
||||
updateMechanism,
|
||||
updateScriptPath
|
||||
} = settings;
|
||||
|
||||
if (!advancedSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateOptions = [
|
||||
{ key: 'builtIn', value: 'Built-In' },
|
||||
{ key: 'script', value: 'Script' }
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldSet legend="Updates">
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Branch</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="branch"
|
||||
helpText="Branch to use to update Lidarr"
|
||||
helpLink="https://github.com/Lidarr/Lidarr/wiki/Release-Branches"
|
||||
onChange={onInputChange}
|
||||
{...branch}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>Automatic</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText="Automatically download and install updates. You will still be able to install from System: Updates"
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Mechanism</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText="Use Lidarr's built-in updater or a script"
|
||||
helpLink="https://github.com/Lidarr/Lidarr/wiki/Updating"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Script Path</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText="Path to a custom script that takes an extracted update package and handle the remainder of the update process"
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
UpdateSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
isMono: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default UpdateSettings;
|
|
@ -96,43 +96,19 @@ class Naming extends Component {
|
|||
if (examples.singleTrackExample) {
|
||||
standardTrackFormatHelpTexts.push(`Single Track: ${examples.singleTrackExample}`);
|
||||
} else {
|
||||
standardTrackFormatErrors.push('Single Track: Invalid Format');
|
||||
standardTrackFormatErrors.push({ message: 'Single Track: Invalid Format' });
|
||||
}
|
||||
|
||||
// if (examples.multiEpisodeExample) {
|
||||
// standardTrackFormatHelpTexts.push(`Multi Episode: ${examples.multiEpisodeExample}`);
|
||||
// } else {
|
||||
// standardTrackFormatErrors.push('Multi Episode: Invalid Format');
|
||||
// }
|
||||
|
||||
// if (examples.dailyEpisodeExample) {
|
||||
// dailyEpisodeFormatHelpTexts.push(`Example: ${examples.dailyEpisodeExample}`);
|
||||
// } else {
|
||||
// dailyEpisodeFormatErrors.push('Invalid Format');
|
||||
// }
|
||||
|
||||
// if (examples.animeEpisodeExample) {
|
||||
// animeEpisodeFormatHelpTexts.push(`Single Episode: ${examples.animeEpisodeExample}`);
|
||||
// } else {
|
||||
// animeEpisodeFormatErrors.push('Single Episode: Invalid Format');
|
||||
// }
|
||||
|
||||
// if (examples.animeMultiEpisodeExample) {
|
||||
// animeEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.animeMultiEpisodeExample}`);
|
||||
// } else {
|
||||
// animeEpisodeFormatErrors.push('Multi Episode: Invalid Format');
|
||||
// }
|
||||
|
||||
if (examples.artistFolderExample) {
|
||||
artistFolderFormatHelpTexts.push(`Example: ${examples.artistFolderExample}`);
|
||||
} else {
|
||||
artistFolderFormatErrors.push('Invalid Format');
|
||||
artistFolderFormatErrors.push({ message: 'Invalid Format' });
|
||||
}
|
||||
|
||||
if (examples.albumFolderExample) {
|
||||
albumFolderFormatHelpTexts.push(`Example: ${examples.albumFolderExample}`);
|
||||
} else {
|
||||
albumFolderFormatErrors.push('Invalid Format');
|
||||
albumFolderFormatErrors.push({ message: 'Invalid Format' });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,9 @@ class NamingModal extends Component {
|
|||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._selectionStart = null;
|
||||
this._selectionEnd = null;
|
||||
|
||||
this.state = {
|
||||
case: 'title'
|
||||
};
|
||||
|
@ -33,6 +36,40 @@ class NamingModal extends Component {
|
|||
this.setState({ case: event.value });
|
||||
}
|
||||
|
||||
onInputSelectionChange = (selectionStart, selectionEnd) => {
|
||||
this._selectionStart = selectionStart;
|
||||
this._selectionEnd = selectionEnd;
|
||||
}
|
||||
|
||||
onOptionPress = ({ isFullFilename, tokenValue }) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
|
||||
const selectionStart = this._selectionStart;
|
||||
const selectionEnd = this._selectionEnd;
|
||||
|
||||
if (isFullFilename) {
|
||||
onInputChange({ name, value: tokenValue });
|
||||
} else if (selectionStart == null) {
|
||||
onInputChange({
|
||||
name,
|
||||
value: `${value}${tokenValue}`
|
||||
});
|
||||
} else {
|
||||
const start = value.substring(0, selectionStart);
|
||||
const end = value.substring(selectionEnd);
|
||||
const newValue = `${start}${tokenValue}${end}`;
|
||||
|
||||
onInputChange({ name, value: newValue });
|
||||
this._selectionStart = newValue.length - 1;
|
||||
this._selectionEnd = newValue.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -188,7 +225,7 @@ class NamingModal extends Component {
|
|||
isFullFilename={true}
|
||||
tokenCase={this.state.case}
|
||||
size={sizes.LARGE}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -210,7 +247,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -234,7 +271,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -255,7 +292,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -281,7 +318,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -302,7 +339,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -323,7 +360,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -350,7 +387,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -371,7 +408,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -392,7 +429,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -413,7 +450,7 @@ class NamingModal extends Component {
|
|||
token={token}
|
||||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -435,7 +472,7 @@ class NamingModal extends Component {
|
|||
example={example}
|
||||
tokenCase={this.state.case}
|
||||
size={sizes.LARGE}
|
||||
onInputChange={onInputChange}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -452,6 +489,7 @@ class NamingModal extends Component {
|
|||
name={name}
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
onSelectionChange={this.onInputSelectionChange}
|
||||
/>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
|
|
|
@ -12,30 +12,21 @@ class NamingOption extends Component {
|
|||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
token,
|
||||
tokenCase,
|
||||
isFullFilename,
|
||||
onInputChange
|
||||
onPress
|
||||
} = this.props;
|
||||
|
||||
let newValue = token;
|
||||
let tokenValue = token;
|
||||
|
||||
if (tokenCase === 'lower') {
|
||||
newValue = token.toLowerCase();
|
||||
tokenValue = token.toLowerCase();
|
||||
} else if (tokenCase === 'upper') {
|
||||
newValue = token.toUpperCase();
|
||||
tokenValue = token.toUpperCase();
|
||||
}
|
||||
|
||||
if (isFullFilename) {
|
||||
onInputChange({ name, value: newValue });
|
||||
} else {
|
||||
onInputChange({
|
||||
name,
|
||||
value: `${value}${newValue}`
|
||||
});
|
||||
}
|
||||
onPress({ isFullFilename, tokenValue });
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -67,14 +58,12 @@ class NamingOption extends Component {
|
|||
}
|
||||
|
||||
NamingOption.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired,
|
||||
example: PropTypes.string.isRequired,
|
||||
tokenCase: PropTypes.string.isRequired,
|
||||
isFullFilename: PropTypes.bool.isRequired,
|
||||
size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]),
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
NamingOption.defaultProps = {
|
||||
|
|
|
@ -67,7 +67,7 @@ class EditLanguageProfileModalContentConnector extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
if (!this.props.id && !this.props.isPopulated) {
|
||||
this.props.fetchLanguageProfileSchema();
|
||||
}
|
||||
}
|
||||
|
@ -176,6 +176,7 @@ class EditLanguageProfileModalContentConnector extends Component {
|
|||
EditLanguageProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
|
@ -12,8 +17,15 @@
|
|||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from 'Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.languages {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import EditLanguageProfileModalConnector from './EditLanguageProfileModalConnector';
|
||||
import styles from './LanguageProfile.css';
|
||||
|
@ -47,6 +48,15 @@ class LanguageProfile extends Component {
|
|||
this.props.onConfirmDeleteLanguageProfile(this.props.id);
|
||||
}
|
||||
|
||||
onCloneLanguageProfilePress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneLanguageProfilePress
|
||||
} = this.props;
|
||||
|
||||
onCloneLanguageProfilePress(id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -62,10 +72,20 @@ class LanguageProfile extends Component {
|
|||
return (
|
||||
<Card
|
||||
className={styles.languageProfile}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditLanguageProfilePress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title="Clone Profile"
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneLanguageProfilePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.languages}>
|
||||
|
@ -118,7 +138,8 @@ LanguageProfile.propTypes = {
|
|||
cutoff: PropTypes.object.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired
|
||||
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired,
|
||||
onCloneLanguageProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LanguageProfile;
|
||||
|
|
|
@ -26,6 +26,11 @@ class LanguageProfiles extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onCloneLanguageProfilePress = (id) => {
|
||||
this.props.onCloneLanguageProfilePress(id);
|
||||
this.setState({ isLanguageProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditLanguageProfilePress = () => {
|
||||
this.setState({ isLanguageProfileModalOpen: true });
|
||||
}
|
||||
|
@ -62,6 +67,7 @@ class LanguageProfiles extends Component {
|
|||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteLanguageProfile={onConfirmDeleteLanguageProfile}
|
||||
onCloneLanguageProfilePress={this.onCloneLanguageProfilePress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -96,7 +102,8 @@ LanguageProfiles.propTypes = {
|
|||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired
|
||||
onConfirmDeleteLanguageProfile: PropTypes.func.isRequired,
|
||||
onCloneLanguageProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LanguageProfiles;
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchLanguageProfiles, deleteLanguageProfile } from 'Store/Actions/settingsActions';
|
||||
import { fetchLanguageProfiles, deleteLanguageProfile, cloneLanguageProfile } from 'Store/Actions/settingsActions';
|
||||
import LanguageProfiles from './LanguageProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -19,8 +19,9 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchLanguageProfiles,
|
||||
deleteLanguageProfile
|
||||
dispatchFetchLanguageProfiles: fetchLanguageProfiles,
|
||||
dispatchDeleteLanguageProfile: deleteLanguageProfile,
|
||||
dispatchCloneLanguageProfile: cloneLanguageProfile
|
||||
};
|
||||
|
||||
class LanguageProfilesConnector extends Component {
|
||||
|
@ -29,14 +30,18 @@ class LanguageProfilesConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchLanguageProfiles();
|
||||
this.props.dispatchFetchLanguageProfiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteLanguageProfile = (id) => {
|
||||
this.props.deleteLanguageProfile({ id });
|
||||
this.props.dispatchDeleteLanguageProfile({ id });
|
||||
}
|
||||
|
||||
onCloneLanguageProfilePress = (id) => {
|
||||
this.props.dispatchCloneLanguageProfile({ id });
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -46,6 +51,7 @@ class LanguageProfilesConnector extends Component {
|
|||
return (
|
||||
<LanguageProfiles
|
||||
onConfirmDeleteLanguageProfile={this.onConfirmDeleteLanguageProfile}
|
||||
onCloneLanguageProfilePress={this.onCloneLanguageProfilePress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
|
@ -53,8 +59,9 @@ class LanguageProfilesConnector extends Component {
|
|||
}
|
||||
|
||||
LanguageProfilesConnector.propTypes = {
|
||||
fetchLanguageProfiles: PropTypes.func.isRequired,
|
||||
deleteLanguageProfile: PropTypes.func.isRequired
|
||||
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteLanguageProfile: PropTypes.func.isRequired,
|
||||
dispatchCloneLanguageProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(LanguageProfilesConnector);
|
||||
|
|
|
@ -92,7 +92,7 @@ class EditMetadataProfileModalContentConnector extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
if (!this.props.id && !this.props.isPopulated) {
|
||||
this.props.fetchMetadataProfileSchema();
|
||||
}
|
||||
}
|
||||
|
@ -162,6 +162,7 @@ class EditMetadataProfileModalContentConnector extends Component {
|
|||
EditMetadataProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
|
@ -12,8 +17,15 @@
|
|||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from 'Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.albumTypes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector';
|
||||
import styles from './MetadataProfile.css';
|
||||
|
@ -47,6 +48,17 @@ class MetadataProfile extends Component {
|
|||
this.props.onConfirmDeleteMetadataProfile(this.props.id);
|
||||
}
|
||||
|
||||
onCloneMetadataProfilePress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneMetadataProfilePress
|
||||
} = this.props;
|
||||
|
||||
onCloneMetadataProfilePress(id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -62,10 +74,20 @@ class MetadataProfile extends Component {
|
|||
return (
|
||||
<Card
|
||||
className={styles.metadataProfile}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditMetadataProfilePress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title="Clone Profile"
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneMetadataProfilePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.albumTypes}>
|
||||
|
@ -136,7 +158,9 @@ MetadataProfile.propTypes = {
|
|||
primaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
secondaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired
|
||||
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired,
|
||||
onCloneMetadataProfilePress: PropTypes.func.isRequired
|
||||
|
||||
};
|
||||
|
||||
export default MetadataProfile;
|
||||
|
|
|
@ -26,6 +26,11 @@ class MetadataProfiles extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onCloneMetadataProfilePress = (id) => {
|
||||
this.props.onCloneMetadataProfilePress(id);
|
||||
this.setState({ isMetadataProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditMetadataProfilePress = () => {
|
||||
this.setState({ isMetadataProfileModalOpen: true });
|
||||
}
|
||||
|
@ -62,6 +67,7 @@ class MetadataProfiles extends Component {
|
|||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteMetadataProfile={onConfirmDeleteMetadataProfile}
|
||||
onCloneMetadataProfilePress={this.onCloneMetadataProfilePress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -96,7 +102,8 @@ MetadataProfiles.propTypes = {
|
|||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired
|
||||
onConfirmDeleteMetadataProfile: PropTypes.func.isRequired,
|
||||
onCloneMetadataProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MetadataProfiles;
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchMetadataProfiles, deleteMetadataProfile } from 'Store/Actions/settingsActions';
|
||||
import { fetchMetadataProfiles, deleteMetadataProfile, cloneMetadataProfile } from 'Store/Actions/settingsActions';
|
||||
import MetadataProfiles from './MetadataProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -19,8 +19,9 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchMetadataProfiles,
|
||||
deleteMetadataProfile
|
||||
dispatchFetchMetadataProfiles: fetchMetadataProfiles,
|
||||
dispatchDeleteMetadataProfile: deleteMetadataProfile,
|
||||
dispatchCloneMetadataProfile: cloneMetadataProfile
|
||||
};
|
||||
|
||||
class MetadataProfilesConnector extends Component {
|
||||
|
@ -29,14 +30,18 @@ class MetadataProfilesConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchMetadataProfiles();
|
||||
this.props.dispatchFetchMetadataProfiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteMetadataProfile = (id) => {
|
||||
this.props.deleteMetadataProfile({ id });
|
||||
this.props.dispatchDeleteMetadataProfile({ id });
|
||||
}
|
||||
|
||||
onCloneMetadataProfilePress = (id) => {
|
||||
this.props.dispatchCloneMetadataProfile({ id });
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -46,6 +51,7 @@ class MetadataProfilesConnector extends Component {
|
|||
return (
|
||||
<MetadataProfiles
|
||||
onConfirmDeleteMetadataProfile={this.onConfirmDeleteMetadataProfile}
|
||||
onCloneMetadataProfilePress={this.onCloneMetadataProfilePress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
|
@ -53,8 +59,9 @@ class MetadataProfilesConnector extends Component {
|
|||
}
|
||||
|
||||
MetadataProfilesConnector.propTypes = {
|
||||
fetchMetadataProfiles: PropTypes.func.isRequired,
|
||||
deleteMetadataProfile: PropTypes.func.isRequired
|
||||
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteMetadataProfile: PropTypes.func.isRequired,
|
||||
dispatchCloneMetadataProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadataProfilesConnector);
|
||||
|
|
|
@ -94,12 +94,12 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||
dragQualityIndex: null,
|
||||
dropQualityIndex: null,
|
||||
dropPosition: null,
|
||||
editGroups: true
|
||||
editGroups: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
if (!this.props.id && !this.props.isPopulated) {
|
||||
this.props.fetchQualityProfileSchema();
|
||||
}
|
||||
}
|
||||
|
@ -429,6 +429,7 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||
EditQualityProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
|
@ -12,10 +17,17 @@
|
|||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from 'Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.qualities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
|
||||
|
@ -48,6 +49,17 @@ class QualityProfile extends Component {
|
|||
this.props.onConfirmDeleteQualityProfile(this.props.id);
|
||||
}
|
||||
|
||||
onCloneQualityProfilePress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneQualityProfilePress
|
||||
} = this.props;
|
||||
|
||||
onCloneQualityProfilePress(id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -63,10 +75,20 @@ class QualityProfile extends Component {
|
|||
return (
|
||||
<Card
|
||||
className={styles.qualityProfile}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditQualityProfilePress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title="Clone Profile"
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneQualityProfilePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.qualities}>
|
||||
|
@ -157,7 +179,8 @@ QualityProfile.propTypes = {
|
|||
cutoff: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired
|
||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
|
||||
onCloneQualityProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityProfile;
|
||||
|
|
|
@ -25,10 +25,6 @@ class QualityProfileItems extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onToggleEditGroupsMode();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
|
|
|
@ -26,6 +26,11 @@ class QualityProfiles extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onCloneQualityProfilePress = (id) => {
|
||||
this.props.onCloneQualityProfilePress(id);
|
||||
this.setState({ isQualityProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditQualityProfilePress = () => {
|
||||
this.setState({ isQualityProfileModalOpen: true });
|
||||
}
|
||||
|
@ -51,7 +56,7 @@ class QualityProfiles extends Component {
|
|||
>
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load Quality Profiles"
|
||||
{...otherProps}
|
||||
{...otherProps}c={true}
|
||||
>
|
||||
<div className={styles.qualityProfiles}>
|
||||
{
|
||||
|
@ -62,6 +67,7 @@ class QualityProfiles extends Component {
|
|||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteQualityProfile={onConfirmDeleteQualityProfile}
|
||||
onCloneQualityProfilePress={this.onCloneQualityProfilePress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -95,7 +101,8 @@ QualityProfiles.propTypes = {
|
|||
error: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired
|
||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
|
||||
onCloneQualityProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityProfiles;
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchQualityProfiles, deleteQualityProfile } from 'Store/Actions/settingsActions';
|
||||
import { fetchQualityProfiles, deleteQualityProfile, cloneQualityProfile } from 'Store/Actions/settingsActions';
|
||||
import QualityProfiles from './QualityProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -17,8 +17,9 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQualityProfiles,
|
||||
deleteQualityProfile
|
||||
dispatchFetchQualityProfiles: fetchQualityProfiles,
|
||||
dispatchDeleteQualityProfile: deleteQualityProfile,
|
||||
dispatchCloneQualityProfile: cloneQualityProfile
|
||||
};
|
||||
|
||||
class QualityProfilesConnector extends Component {
|
||||
|
@ -27,14 +28,18 @@ class QualityProfilesConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchQualityProfiles();
|
||||
this.props.dispatchFetchQualityProfiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteQualityProfile = (id) => {
|
||||
this.props.deleteQualityProfile({ id });
|
||||
this.props.dispatchDeleteQualityProfile({ id });
|
||||
}
|
||||
|
||||
onCloneQualityProfilePress = (id) => {
|
||||
this.props.dispatchCloneQualityProfile({ id });
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -44,6 +49,7 @@ class QualityProfilesConnector extends Component {
|
|||
return (
|
||||
<QualityProfiles
|
||||
onConfirmDeleteQualityProfile={this.onConfirmDeleteQualityProfile}
|
||||
onCloneQualityProfilePress={this.onCloneQualityProfilePress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
|
@ -51,8 +57,9 @@ class QualityProfilesConnector extends Component {
|
|||
}
|
||||
|
||||
QualityProfilesConnector.propTypes = {
|
||||
fetchQualityProfiles: PropTypes.func.isRequired,
|
||||
deleteQualityProfile: PropTypes.func.isRequired
|
||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteQualityProfile: PropTypes.func.isRequired,
|
||||
dispatchCloneQualityProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue