mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-12 16:13:58 -07:00
New: Release Profiles, Frontend updates (#580)
* New: Release Profiles - UI Updates * New: Release Profiles - API Changes * New: Release Profiles - Test Updates * New: Release Profiles - Backend Updates * New: Interactive Artist Search * New: Change Montiored on Album Details Page * New: Show Duration on Album Details Page * Fixed: Manual Import not working if no albums are Missing * Fixed: Sort search input by sortTitle * Fixed: Queue columnLabel throwing JS error
This commit is contained in:
parent
f126eafd26
commit
3f064c94b9
409 changed files with 6882 additions and 3176 deletions
|
@ -19,8 +19,10 @@ function getTagDisplayValue(value, selectedFilterBuilderProp) {
|
|||
function getValue(input, selectedFilterBuilderProp) {
|
||||
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
||||
const match = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||
|
||||
if (match && match.length > 1) {
|
||||
const [, value, unit] = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||
|
||||
switch (unit.toLowerCase()) {
|
||||
case 'k':
|
||||
return convertToBytes(value, 1, true);
|
||||
|
@ -118,6 +120,7 @@ class FilterBuilderRowValue extends Component {
|
|||
name: tag && tag.name
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: getTagDisplayValue(id, selectedFilterBuilderProp)
|
||||
|
|
|
@ -12,7 +12,7 @@ function createMapStateToProps() {
|
|||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const {
|
||||
isFetchingSchema: isFetching,
|
||||
isSchemaFetching: isFetching,
|
||||
isSchemaPopulated: isPopulated,
|
||||
schemaError: error,
|
||||
schema
|
||||
|
|
|
@ -44,7 +44,7 @@ class AlbumReleaseSelectInputConnector extends Component {
|
|||
albumReleases
|
||||
} = this.props;
|
||||
|
||||
let updatedReleases = _.map(albumReleases.value, (e) => ({ ...e, monitored: false }));
|
||||
const updatedReleases = _.map(albumReleases.value, (e) => ({ ...e, monitored: false }));
|
||||
_.find(updatedReleases, { foreignReleaseId: value }).monitored = true;
|
||||
|
||||
this.props.onChange({ name, value: updatedReleases });
|
||||
|
|
58
frontend/src/Components/Form/AutoCompleteInput.css
Normal file
58
frontend/src/Components/Form/AutoCompleteInput.css
Normal file
|
@ -0,0 +1,58 @@
|
|||
.input {
|
||||
composes: input from 'Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from 'Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasWarning {
|
||||
composes: hasWarning from 'Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.container {
|
||||
@add-mixin scrollbar;
|
||||
@add-mixin scrollbarTrack;
|
||||
@add-mixin scrollbarThumb;
|
||||
}
|
||||
|
||||
.inputContainerOpen {
|
||||
.container {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
width: 100%;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 5px 0;
|
||||
padding-left: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.match {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
162
frontend/src/Components/Form/AutoCompleteInput.js
Normal file
162
frontend/src/Components/Form/AutoCompleteInput.js
Normal file
|
@ -0,0 +1,162 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import classNames from 'classnames';
|
||||
import jdu from 'jdu';
|
||||
import styles from './AutoCompleteInput.css';
|
||||
|
||||
class AutoCompleteInput extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
suggestions: []
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSuggestionValue(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
renderSuggestion(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = (event, { newValue }) => {
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
onInputKeyDown = (event) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const { suggestions } = this.state;
|
||||
|
||||
if (
|
||||
event.key === 'Tab' &&
|
||||
suggestions.length &&
|
||||
suggestions[0] !== this.props.value
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
if (value) {
|
||||
onChange({
|
||||
name,
|
||||
value: suggestions[0]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onInputBlur = () => {
|
||||
this.setState({ suggestions: [] });
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = ({ value }) => {
|
||||
const { values } = this.props;
|
||||
const lowerCaseValue = jdu.replace(value).toLowerCase();
|
||||
|
||||
const filteredValues = values.filter((v) => {
|
||||
return jdu.replace(v).toLowerCase().contains(lowerCaseValue);
|
||||
});
|
||||
|
||||
this.setState({ suggestions: filteredValues });
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.setState({ suggestions: [] });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
inputClassName,
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
hasError,
|
||||
hasWarning
|
||||
} = this.props;
|
||||
|
||||
const { suggestions } = this.state;
|
||||
|
||||
const inputProps = {
|
||||
className: classNames(
|
||||
inputClassName,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
),
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
autoComplete: 'off',
|
||||
spellCheck: false,
|
||||
onChange: this.onInputChange,
|
||||
onKeyDown: this.onInputKeyDown,
|
||||
onBlur: this.onInputBlur
|
||||
};
|
||||
|
||||
const theme = {
|
||||
container: styles.inputContainer,
|
||||
containerOpen: styles.inputContainerOpen,
|
||||
suggestionsContainer: styles.container,
|
||||
suggestionsList: styles.list,
|
||||
suggestion: styles.listItem,
|
||||
suggestionHighlighted: styles.highlighted
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Autosuggest
|
||||
id={name}
|
||||
inputProps={inputProps}
|
||||
theme={theme}
|
||||
suggestions={suggestions}
|
||||
getSuggestionValue={this.getSuggestionValue}
|
||||
renderSuggestion={this.renderSuggestion}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AutoCompleteInput.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
inputClassName: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
values: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AutoCompleteInput.defaultProps = {
|
||||
className: styles.inputWrapper,
|
||||
inputClassName: styles.input,
|
||||
value: ''
|
||||
};
|
||||
|
||||
export default AutoCompleteInput;
|
3
frontend/src/Components/Form/Form.css
Normal file
3
frontend/src/Components/Form/Form.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.validationFailures {
|
||||
margin-bottom: 20px;
|
||||
}
|
|
@ -2,37 +2,42 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Alert from 'Components/Alert';
|
||||
import styles from './Form.css';
|
||||
|
||||
function Form({ children, validationErrors, validationWarnings, ...otherProps }) {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{
|
||||
validationErrors.map((error, index) => {
|
||||
return (
|
||||
<Alert
|
||||
key={index}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{error.errorMessage}
|
||||
</Alert>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
validationErrors.length || validationWarnings.length ?
|
||||
<div className={styles.validationFailures}>
|
||||
{
|
||||
validationErrors.map((error, index) => {
|
||||
return (
|
||||
<Alert
|
||||
key={index}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{error.errorMessage}
|
||||
</Alert>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
validationWarnings.map((warning, index) => {
|
||||
return (
|
||||
<Alert
|
||||
key={index}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{warning.errorMessage}
|
||||
</Alert>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{
|
||||
validationWarnings.map((warning, index) => {
|
||||
return (
|
||||
<Alert
|
||||
key={index}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{warning.errorMessage}
|
||||
</Alert>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -2,9 +2,11 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Link from 'Components/Link/Link';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
import DeviceInputConnector from './DeviceInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
|
||||
import NumberInput from './NumberInput';
|
||||
import OAuthInputConnector from './OAuthInputConnector';
|
||||
|
@ -25,6 +27,9 @@ import styles from './FormInputGroup.css';
|
|||
|
||||
function getComponent(type) {
|
||||
switch (type) {
|
||||
case inputTypes.AUTO_COMPLETE:
|
||||
return AutoCompleteInput;
|
||||
|
||||
case inputTypes.CAPTCHA:
|
||||
return CaptchaInputConnector;
|
||||
|
||||
|
@ -34,6 +39,9 @@ function getComponent(type) {
|
|||
case inputTypes.DEVICE:
|
||||
return DeviceInputConnector;
|
||||
|
||||
case inputTypes.KEY_VALUE_LIST:
|
||||
return KeyValueListInput;
|
||||
|
||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||
return MonitorAlbumsSelectInput;
|
||||
|
||||
|
|
21
frontend/src/Components/Form/KeyValueListInput.css
Normal file
21
frontend/src/Components/Form/KeyValueListInput.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.inputContainer {
|
||||
composes: input from 'Components/Form/Input.css';
|
||||
|
||||
position: relative;
|
||||
min-height: 35px;
|
||||
height: auto;
|
||||
|
||||
&.isFocused {
|
||||
outline: 0;
|
||||
border-color: $inputFocusBorderColor;
|
||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
|
||||
}
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from 'Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasWarning {
|
||||
composes: hasWarning from 'Components/Form/Input.css';
|
||||
}
|
152
frontend/src/Components/Form/KeyValueListInput.js
Normal file
152
frontend/src/Components/Form/KeyValueListInput.js
Normal file
|
@ -0,0 +1,152 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import KeyValueListInputItem from './KeyValueListInputItem';
|
||||
import styles from './KeyValueListInput.css';
|
||||
|
||||
class KeyValueListInput extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isFocused: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onItemChange = (index, itemValue) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = [...value];
|
||||
|
||||
if (index == null) {
|
||||
newValue.push(itemValue);
|
||||
} else {
|
||||
newValue.splice(index, 1, itemValue);
|
||||
}
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveItem = (index) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = [...value];
|
||||
newValue.splice(index, 1);
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({
|
||||
isFocused: true
|
||||
});
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({
|
||||
isFocused: false
|
||||
});
|
||||
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = value.reduce((acc, v) => {
|
||||
if (v.key || v.value) {
|
||||
acc.push(v);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (newValue.length !== value.length) {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
value,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder
|
||||
} = this.props;
|
||||
|
||||
const { isFocused } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
className,
|
||||
isFocused && styles.isFocused
|
||||
)}
|
||||
>
|
||||
{
|
||||
[...value, { key: '', value: '' }].map((v, index) => {
|
||||
return (
|
||||
<KeyValueListInputItem
|
||||
key={index}
|
||||
index={index}
|
||||
keyValue={v.key}
|
||||
value={v.value}
|
||||
keyPlaceholder={keyPlaceholder}
|
||||
valuePlaceholder={valuePlaceholder}
|
||||
isNew={index === value.length}
|
||||
onChange={this.onItemChange}
|
||||
onRemove={this.onRemoveItem}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
KeyValueListInput.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
keyPlaceholder: PropTypes.string,
|
||||
valuePlaceholder: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
KeyValueListInput.defaultProps = {
|
||||
className: styles.inputContainer,
|
||||
value: []
|
||||
};
|
||||
|
||||
export default KeyValueListInput;
|
14
frontend/src/Components/Form/KeyValueListInputItem.css
Normal file
14
frontend/src/Components/Form/KeyValueListInputItem.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
.itemContainer {
|
||||
display: flex;
|
||||
margin-bottom: 3px;
|
||||
border-bottom: 1px solid $inputBorderColor;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.keyInput,
|
||||
.valueInput {
|
||||
border: none;
|
||||
}
|
117
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
117
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './KeyValueListInputItem.css';
|
||||
|
||||
class KeyValueListInputItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onKeyChange = ({ value: keyValue }) => {
|
||||
const {
|
||||
index,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
onChange(index, { key: keyValue, value });
|
||||
}
|
||||
|
||||
onValueChange = ({ value }) => {
|
||||
// TODO: Validate here or validate at a lower level component
|
||||
|
||||
const {
|
||||
index,
|
||||
keyValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
onChange(index, { key: keyValue, value });
|
||||
}
|
||||
|
||||
onRemovePress = () => {
|
||||
const {
|
||||
index,
|
||||
onRemove
|
||||
} = this.props;
|
||||
|
||||
onRemove(index);
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.props.onFocus();
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.props.onBlur();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
keyValue,
|
||||
value,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
isNew
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer}>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
name="key"
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
onChange={this.onKeyChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
value={value}
|
||||
placeholder={valuePlaceholder}
|
||||
onChange={this.onValueChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
|
||||
{
|
||||
!isNew &&
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
KeyValueListInputItem.propTypes = {
|
||||
index: PropTypes.number,
|
||||
keyValue: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
keyPlaceholder: PropTypes.string.isRequired,
|
||||
valuePlaceholder: PropTypes.string.isRequired,
|
||||
isNew: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
KeyValueListInputItem.defaultProps = {
|
||||
keyPlaceholder: 'Key',
|
||||
valuePlaceholder: 'Value'
|
||||
};
|
||||
|
||||
export default KeyValueListInputItem;
|
|
@ -1,17 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import monitorOptions from 'Utilities/Artist/monitorOptions';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
const monitorOptions = [
|
||||
{ key: 'all', value: 'All Albums' },
|
||||
{ key: 'future', value: 'Future Albums' },
|
||||
{ key: 'missing', value: 'Missing Albums' },
|
||||
{ key: 'existing', value: 'Existing Albums' },
|
||||
{ key: 'first', value: 'Only First Album' },
|
||||
{ key: 'latest', value: 'Only Latest Album' },
|
||||
{ key: 'none', value: 'None' }
|
||||
];
|
||||
|
||||
function MonitorAlbumsSelectInput(props) {
|
||||
const {
|
||||
includeNoChange,
|
||||
|
|
|
@ -2,44 +2,91 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import TextInput from './TextInput';
|
||||
|
||||
function parseValue(props, value) {
|
||||
const {
|
||||
isFloat,
|
||||
min,
|
||||
max
|
||||
} = props;
|
||||
|
||||
if (value == null || value === '') {
|
||||
return min;
|
||||
}
|
||||
|
||||
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
||||
|
||||
if (min != null && newValue != null && newValue < min) {
|
||||
newValue = min;
|
||||
} else if (max != null && newValue != null && newValue > max) {
|
||||
newValue = max;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
class NumberInput extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
value: props.value == null ? '' : props.value.toString(),
|
||||
isFocused: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { value } = this.props;
|
||||
|
||||
if (value !== prevProps.value && !this.state.isFocused) {
|
||||
this.setState({
|
||||
value: value == null ? '' : value.toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
let newValue = null;
|
||||
|
||||
if (value) {
|
||||
newValue = this.props.isFloat ? parseFloat(value) : parseInt(value);
|
||||
}
|
||||
this.setState({ value });
|
||||
|
||||
this.props.onChange({
|
||||
name,
|
||||
value: newValue
|
||||
value: parseValue(this.props, value)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ isFocused: true });
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
let newValue = value;
|
||||
const { value } = this.state;
|
||||
const parsedValue = parseValue(this.props, value);
|
||||
const stringValue = parsedValue == null ? '' : parsedValue.toString();
|
||||
|
||||
if (min != null && newValue != null && newValue < min) {
|
||||
newValue = min;
|
||||
} else if (max != null && newValue != null && newValue > max) {
|
||||
newValue = max;
|
||||
if (stringValue === value) {
|
||||
this.setState({ isFocused: false });
|
||||
} else {
|
||||
this.setState({
|
||||
value: stringValue,
|
||||
isFocused: false
|
||||
});
|
||||
}
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
value: parsedValue
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -47,18 +94,16 @@ class NumberInput extends Component {
|
|||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
const value = this.state.value;
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
{...this.props}
|
||||
type="number"
|
||||
value={value == null ? '' : value}
|
||||
{...otherProps}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
}
|
||||
|
||||
.freeSpace {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex: 1 0 0;
|
||||
flex: 0 0 auto;
|
||||
margin-left: 15px;
|
||||
color: $gray;
|
||||
text-align: right;
|
||||
|
|
|
@ -33,7 +33,10 @@ class TagInputTag extends Component {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link onPress={this.onDelete}>
|
||||
<Link
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
<Label kind={kind}>
|
||||
{tag.name}
|
||||
</Label>
|
||||
|
|
|
@ -127,6 +127,7 @@ class TextInput extends Component {
|
|||
hasError,
|
||||
hasWarning,
|
||||
hasButton,
|
||||
step,
|
||||
onBlur
|
||||
} = this.props;
|
||||
|
||||
|
@ -146,6 +147,7 @@ class TextInput extends Component {
|
|||
)}
|
||||
name={name}
|
||||
value={value}
|
||||
step={step}
|
||||
onChange={this.onChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={onBlur}
|
||||
|
@ -168,6 +170,7 @@ TextInput.propTypes = {
|
|||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
hasButton: PropTypes.bool,
|
||||
step: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
|
|
|
@ -6,10 +6,18 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: $infoColor;
|
||||
}
|
||||
|
||||
.pink {
|
||||
color: $pink;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: $successColor;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
border-color: $disabledColor;
|
||||
background-color: $disabledColor;
|
||||
|
||||
&.outline {
|
||||
color: $disabledColor;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
border-color: $infoColor;
|
||||
background-color: $infoColor;
|
||||
|
@ -92,7 +101,7 @@
|
|||
.large {
|
||||
padding: 3px 7px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
font-size: $defaultFontSize;
|
||||
}
|
||||
|
||||
/** Outline **/
|
||||
|
|
|
@ -24,7 +24,6 @@ function IconButton(props) {
|
|||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
|
|
|
@ -91,8 +91,8 @@ class ArtistSearchInput extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onChange = (event, { newValue }) => {
|
||||
if (!newValue) {
|
||||
onChange = (event, { newValue, method }) => {
|
||||
if (method === 'up' || method === 'down') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,7 @@ class ArtistSearchInput extends Component {
|
|||
if (!suggestions.length || highlightedSectionIndex && (event.key !== 'ArrowDown' || event.key !== 'ArrowUp')) {
|
||||
this.props.onGoToAddNewArtist(value);
|
||||
this._autosuggest.input.blur();
|
||||
this.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -129,6 +130,9 @@ class ArtistSearchInput extends Component {
|
|||
} else {
|
||||
this.goToArtist(suggestions[highlightedSuggestionIndex]);
|
||||
}
|
||||
|
||||
this._autosuggest.input.blur();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
|
@ -142,9 +146,15 @@ class ArtistSearchInput extends Component {
|
|||
// Check the title first and if there isn't a match fallback to
|
||||
// the alternate titles and finally the tags.
|
||||
|
||||
if (value.length === 1) {
|
||||
return (
|
||||
artist.cleanName.startsWith(lowerCaseValue) ||
|
||||
artist.tags.some((tag) => tag.cleanLabel.startsWith(lowerCaseValue))
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
artist.cleanName.contains(lowerCaseValue) ||
|
||||
// artist.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.contains(lowerCaseValue)) ||
|
||||
artist.tags.some((tag) => tag.cleanLabel.contains(lowerCaseValue))
|
||||
);
|
||||
});
|
||||
|
@ -153,7 +163,9 @@ class ArtistSearchInput extends Component {
|
|||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.reset();
|
||||
this.setState({
|
||||
suggestions: []
|
||||
});
|
||||
}
|
||||
|
||||
onSuggestionSelected = (event, { suggestion }) => {
|
||||
|
|
|
@ -58,10 +58,10 @@ function createCleanArtistSelector() {
|
|||
})
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
if (a.cleanName < b.cleanName) {
|
||||
if (a.sortName < b.sortName) {
|
||||
return -1;
|
||||
}
|
||||
if (a.cleanName > b.cleanName) {
|
||||
if (a.sortName > b.sortName) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,19 @@
|
|||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
height: $headerHeight;
|
||||
background-color: #00a65b;
|
||||
background-color: $themeAlternateBlue;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 0 0 $sidebarWidth;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
|
|
@ -51,7 +51,10 @@ class PageHeader extends Component {
|
|||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.logoContainer}>
|
||||
<Link to={`${window.Lidarr.urlBase}/`}>
|
||||
<Link
|
||||
className={styles.logoLink}
|
||||
to={`${window.Lidarr.urlBase}/`}
|
||||
>
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Lidarr.urlBase}/Content/Images/logo.svg`}
|
||||
|
|
|
@ -2,8 +2,9 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import SignalRConnector from 'Components/SignalRConnector';
|
||||
import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||
import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
||||
import PageHeader from './Header/PageHeader';
|
||||
import PageSidebar from './Sidebar/PageSidebar';
|
||||
import styles from './Page.css';
|
||||
|
@ -73,39 +74,42 @@ class Page extends Component {
|
|||
children,
|
||||
isSmallScreen,
|
||||
isSidebarVisible,
|
||||
enableColorImpairedMode,
|
||||
onSidebarToggle,
|
||||
onSidebarVisibleChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<SignalRConnector />
|
||||
<ColorImpairedContext.Provider value={enableColorImpairedMode}>
|
||||
<div className={className}>
|
||||
<SignalRConnector />
|
||||
|
||||
<PageHeader
|
||||
onSidebarToggle={onSidebarToggle}
|
||||
/>
|
||||
|
||||
<div className={styles.main}>
|
||||
<PageSidebar
|
||||
location={location}
|
||||
isSmallScreen={isSmallScreen}
|
||||
isSidebarVisible={isSidebarVisible}
|
||||
onSidebarVisibleChange={onSidebarVisibleChange}
|
||||
<PageHeader
|
||||
onSidebarToggle={onSidebarToggle}
|
||||
/>
|
||||
|
||||
{children}
|
||||
<div className={styles.main}>
|
||||
<PageSidebar
|
||||
location={location}
|
||||
isSmallScreen={isSmallScreen}
|
||||
isSidebarVisible={isSidebarVisible}
|
||||
onSidebarVisibleChange={onSidebarVisibleChange}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<AppUpdatedModalConnector
|
||||
isOpen={this.state.isUpdatedModalOpen}
|
||||
onModalClose={this.onUpdatedModalClose}
|
||||
/>
|
||||
|
||||
<ConnectionLostModalConnector
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AppUpdatedModalConnector
|
||||
isOpen={this.state.isUpdatedModalOpen}
|
||||
onModalClose={this.onUpdatedModalClose}
|
||||
/>
|
||||
|
||||
<ConnectionLostModalConnector
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
</div>
|
||||
</ColorImpairedContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +122,7 @@ Page.propTypes = {
|
|||
isSidebarVisible: PropTypes.bool.isRequired,
|
||||
isUpdated: PropTypes.bool.isRequired,
|
||||
isDisconnected: PropTypes.bool.isRequired,
|
||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||
onResize: PropTypes.func.isRequired,
|
||||
onSidebarToggle: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint max-params: 0 */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -33,30 +32,45 @@ function createMapStateToProps() {
|
|||
(state) => state.artist,
|
||||
(state) => state.customFilters,
|
||||
(state) => state.tags,
|
||||
(state) => state.settings,
|
||||
(state) => state.settings.ui,
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(state) => state.settings.languageProfiles,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(state) => state.settings.importLists,
|
||||
(state) => state.app,
|
||||
createDimensionsSelector(),
|
||||
(artist, customFilters, tags, settings, app, dimensions) => {
|
||||
(
|
||||
artist,
|
||||
customFilters,
|
||||
tags,
|
||||
uiSettings,
|
||||
qualityProfiles,
|
||||
languageProfiles,
|
||||
metadataProfiles,
|
||||
importLists,
|
||||
app,
|
||||
dimensions
|
||||
) => {
|
||||
const isPopulated = (
|
||||
artist.isPopulated &&
|
||||
customFilters.isPopulated &&
|
||||
tags.isPopulated &&
|
||||
settings.qualityProfiles.isPopulated &&
|
||||
settings.languageProfiles.isPopulated &&
|
||||
settings.metadataProfiles.isPopulated &&
|
||||
settings.importLists.isPopulated &&
|
||||
settings.ui.isPopulated
|
||||
qualityProfiles.isPopulated &&
|
||||
languageProfiles.isPopulated &&
|
||||
metadataProfiles.isPopulated &&
|
||||
importLists.isPopulated &&
|
||||
uiSettings.isPopulated
|
||||
);
|
||||
|
||||
const hasError = !!(
|
||||
artist.error ||
|
||||
customFilters.error ||
|
||||
tags.error ||
|
||||
settings.qualityProfiles.error ||
|
||||
settings.languageProfiles.error ||
|
||||
settings.metadataProfiles.error ||
|
||||
settings.importLists.error ||
|
||||
settings.ui.error
|
||||
qualityProfiles.error ||
|
||||
languageProfiles.error ||
|
||||
metadataProfiles.error ||
|
||||
importLists.error ||
|
||||
uiSettings.error
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -65,13 +79,14 @@ function createMapStateToProps() {
|
|||
artistError: artist.error,
|
||||
customFiltersError: tags.error,
|
||||
tagsError: tags.error,
|
||||
qualityProfilesError: settings.qualityProfiles.error,
|
||||
languageProfilesError: settings.languageProfiles.error,
|
||||
metadataProfilesError: settings.metadataProfiles.error,
|
||||
importListsError: settings.importLists.error,
|
||||
uiSettingsError: settings.ui.error,
|
||||
qualityProfilesError: qualityProfiles.error,
|
||||
languageProfilesError: languageProfiles.error,
|
||||
metadataProfilesError: metadataProfiles.error,
|
||||
importListsError: importLists.error,
|
||||
uiSettingsError: uiSettings.error,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
isSidebarVisible: app.isSidebarVisible,
|
||||
enableColorImpairedMode: uiSettings.item.enableColorImpairedMode,
|
||||
version: app.version,
|
||||
isUpdated: app.isUpdated,
|
||||
isDisconnected: app.isDisconnected
|
||||
|
|
|
@ -22,6 +22,19 @@
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.overflowMenuButton {
|
||||
composes: menuButton from 'Components/Menu/ToolbarMenuButton.css';
|
||||
}
|
||||
|
||||
.overflowMenuItemIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.overflowMenuButton {
|
||||
&::after {
|
||||
margin-left: 0;
|
||||
content: '\25BE';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,6 +160,7 @@ class PageToolbarSection extends Component {
|
|||
!!overflowItems.length &&
|
||||
<Menu>
|
||||
<ToolbarMenuButton
|
||||
className={styles.overflowMenuButton}
|
||||
iconName={icons.OVERFLOW}
|
||||
text="More"
|
||||
/>
|
||||
|
@ -179,14 +180,13 @@ class PageToolbarSection extends Component {
|
|||
return (
|
||||
<MenuItem
|
||||
key={label}
|
||||
isDisabled={isDisabled}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<SpinnerIcon
|
||||
className={styles.overflowMenuItemIcon}
|
||||
name={iconName}
|
||||
spinningName={spinningName}
|
||||
isDisabled={isDisabled}
|
||||
isSpinning={isSpinning}
|
||||
/>
|
||||
{label}
|
||||
|
|
|
@ -47,6 +47,10 @@
|
|||
|
||||
.danger {
|
||||
background-color: $dangerColor;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color($dangerColor shade(5%)), color($dangerColor shade(5%)) 5px, color($dangerColor shade(15%)) 5px, color($dangerColor shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
|
@ -59,6 +63,10 @@
|
|||
|
||||
.warning {
|
||||
background-color: $warningColor;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, $warningColor, $warningColor 5px, color($warningColor tint(15%)) 5px, color($warningColor tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import styles from './ProgressBar.css';
|
||||
|
||||
function ProgressBar(props) {
|
||||
|
@ -23,55 +24,65 @@ function ProgressBar(props) {
|
|||
const actualWidth = width ? `${width}px` : '100%';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
styles[size]
|
||||
)}
|
||||
title={title}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
{
|
||||
showText && !!width &&
|
||||
<ColorImpairedConsumer>
|
||||
{(enableColorImpairedMode) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.backTextContainer}
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
styles[size]
|
||||
)}
|
||||
title={title}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div className={styles.backText}>
|
||||
<div>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
showText && width ?
|
||||
<div
|
||||
className={styles.backTextContainer}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div className={styles.backText}>
|
||||
<div>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind]
|
||||
)}
|
||||
aria-valuenow={progress}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={{ width: progressPercent }}
|
||||
/>
|
||||
{
|
||||
showText &&
|
||||
<div
|
||||
className={styles.frontTextContainer}
|
||||
style={{ width: progressPercent }}
|
||||
>
|
||||
<div
|
||||
className={styles.frontText}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
aria-valuenow={progress}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={{ width: progressPercent }}
|
||||
/>
|
||||
|
||||
{
|
||||
showText ?
|
||||
<div
|
||||
className={styles.frontTextContainer}
|
||||
style={{ width: progressPercent }}
|
||||
>
|
||||
<div
|
||||
className={styles.frontText}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div>
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ColorImpairedConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
|||
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
|
||||
function getState(status) {
|
||||
|
@ -70,6 +71,7 @@ const mapDispatchToProps = {
|
|||
dispatchFetchHealth: fetchHealth,
|
||||
dispatchFetchQueue: fetchQueue,
|
||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchFetchTags: fetchTags,
|
||||
dispatchFetchTagDetails: fetchTagDetails
|
||||
};
|
||||
|
@ -202,6 +204,7 @@ class SignalRConnector extends Component {
|
|||
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||
|
||||
// Repopulate the page to handle recently imported file
|
||||
repopulatePage('trackFileUpdated');
|
||||
} else if (body.action === 'deleted') {
|
||||
|
@ -278,6 +281,10 @@ class SignalRConnector extends Component {
|
|||
// No-op for now, we may want this later
|
||||
}
|
||||
|
||||
handleRootfolder = () => {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
handleTag = (body) => {
|
||||
if (body.action === 'sync') {
|
||||
this.props.dispatchFetchTags();
|
||||
|
@ -386,6 +393,7 @@ SignalRConnector.propTypes = {
|
|||
dispatchFetchHealth: PropTypes.func.isRequired,
|
||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint max-params: 0 */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { icons, scrollDirections } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TableHeader from './TableHeader';
|
||||
import TableHeaderCell from './TableHeaderCell';
|
||||
import TableSelectAllHeaderCell from './TableSelectAllHeaderCell';
|
||||
|
@ -25,123 +25,95 @@ function getTableHeaderCellProps(props) {
|
|||
}, {});
|
||||
}
|
||||
|
||||
class Table extends Component {
|
||||
function Table(props) {
|
||||
const {
|
||||
className,
|
||||
selectAll,
|
||||
columns,
|
||||
optionsComponent,
|
||||
pageSize,
|
||||
canModifyColumns,
|
||||
children,
|
||||
onSortPress,
|
||||
onTableOptionChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
return (
|
||||
<Scroller
|
||||
className={styles.tableContainer}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
<table className={className}>
|
||||
<TableHeader>
|
||||
{
|
||||
selectAll &&
|
||||
<TableSelectAllHeaderCell {...otherProps} />
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
this.state = {
|
||||
isTableOptionsModalOpen: false
|
||||
};
|
||||
}
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTableOptionsPress = () => {
|
||||
this.setState({ isTableOptionsModalOpen: true });
|
||||
}
|
||||
|
||||
onTableOptionsModalClose = () => {
|
||||
this.setState({ isTableOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
selectAll,
|
||||
columns,
|
||||
pageSize,
|
||||
canModifyColumns,
|
||||
children,
|
||||
onSortPress,
|
||||
onTableOptionChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
className={styles.tableContainer}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
<table className={className}>
|
||||
<TableHeader>
|
||||
{
|
||||
selectAll &&
|
||||
<TableSelectAllHeaderCell {...otherProps} />
|
||||
}
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((name === 'actions' || name === 'details') && onTableOptionChange) {
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherProps}
|
||||
if (
|
||||
(name === 'actions' || name === 'details') &&
|
||||
onTableOptionChange
|
||||
) {
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={optionsComponent}
|
||||
pageSize={pageSize}
|
||||
canModifyColumns={canModifyColumns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
onPress={this.onTableOptionsPress}
|
||||
/>
|
||||
</TableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={column.name}
|
||||
onSortPress={onSortPress}
|
||||
{...getTableHeaderCellProps(otherProps)}
|
||||
{...column}
|
||||
>
|
||||
{column.label}
|
||||
</TableOptionsModalWrapper>
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
!!onTableOptionChange &&
|
||||
<TableOptionsModal
|
||||
isOpen={this.state.isTableOptionsModalOpen}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
canModifyColumns={canModifyColumns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
onModalClose={this.onTableOptionsModalClose}
|
||||
/>
|
||||
}
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={column.name}
|
||||
onSortPress={onSortPress}
|
||||
{...getTableHeaderCellProps(otherProps)}
|
||||
{...column}
|
||||
>
|
||||
{column.label}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
</TableHeader>
|
||||
{children}
|
||||
</table>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
</TableHeader>
|
||||
{children}
|
||||
</table>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
|
||||
Table.propTypes = {
|
||||
className: PropTypes.string,
|
||||
selectAll: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
optionsComponent: PropTypes.func,
|
||||
pageSize: PropTypes.number,
|
||||
canModifyColumns: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
|
|
|
@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
|
|||
const {
|
||||
className,
|
||||
name,
|
||||
columnLabel,
|
||||
isSortable,
|
||||
isVisible,
|
||||
isModifiable,
|
||||
|
@ -49,10 +50,11 @@ class TableHeaderCell extends Component {
|
|||
return (
|
||||
isSortable ?
|
||||
<Link
|
||||
{...otherProps}
|
||||
component="th"
|
||||
className={className}
|
||||
title={columnLabel}
|
||||
onPress={this.onPress}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
|
@ -75,7 +77,7 @@ class TableHeaderCell extends Component {
|
|||
TableHeaderCell.propTypes = {
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
columnLabel: PropTypes.string,
|
||||
isSortable: PropTypes.bool,
|
||||
isVisible: PropTypes.bool,
|
||||
isModifiable: PropTypes.bool,
|
||||
|
|
|
@ -75,6 +75,4 @@ TableOptionsColumnDragPreview.propTypes = {
|
|||
})
|
||||
};
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
export default DragLayer(collectDragLayer)(TableOptionsColumnDragPreview);
|
||||
/* eslint-enable new-cap */
|
||||
|
|
|
@ -153,7 +153,6 @@ TableOptionsColumnDragSource.propTypes = {
|
|||
onColumnDragEnd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
export default DropTarget(
|
||||
TABLE_COLUMN,
|
||||
columnDropTarget,
|
||||
|
@ -163,4 +162,3 @@ export default DropTarget(
|
|||
columnDragSource,
|
||||
collectDragSource
|
||||
)(TableOptionsColumnDragSource));
|
||||
/* eslint-enable new-cap */
|
||||
|
|
|
@ -249,6 +249,4 @@ TableOptionsModal.defaultProps = {
|
|||
canModifyColumns: true
|
||||
};
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
export default DragDropContext(HTML5Backend)(TableOptionsModal);
|
||||
/* eslint-enable new-cap */
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import TableOptionsModal from './TableOptionsModal';
|
||||
|
||||
class TableOptionsModalWrapper extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isTableOptionsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTableOptionsPress = () => {
|
||||
this.setState({ isTableOptionsModalOpen: true });
|
||||
}
|
||||
|
||||
onTableOptionsModalClose = () => {
|
||||
this.setState({ isTableOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
columns,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
React.cloneElement(children, { onPress: this.onTableOptionsPress })
|
||||
}
|
||||
|
||||
<TableOptionsModal
|
||||
{...otherProps}
|
||||
isOpen={this.state.isTableOptionsModalOpen}
|
||||
columns={columns}
|
||||
onModalClose={this.onTableOptionsModalClose}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableOptionsModalWrapper.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default TableOptionsModalWrapper;
|
|
@ -100,5 +100,6 @@
|
|||
}
|
||||
|
||||
.body {
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import TetherComponent from 'react-tether';
|
||||
import classNames from 'classnames';
|
||||
import isMobileUtil from 'Utilities/isMobile';
|
||||
import { tooltipPositions } from 'Helpers/Props';
|
||||
import styles from './Popover.css';
|
||||
|
||||
|
@ -67,7 +68,9 @@ class Popover extends Component {
|
|||
// Listeners
|
||||
|
||||
onClick = () => {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
if (isMobileUtil()) {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
|
@ -105,7 +108,7 @@ class Popover extends Component {
|
|||
>
|
||||
<span
|
||||
className={className}
|
||||
// onClick={this.onClick}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
|
@ -114,28 +117,28 @@ class Popover extends Component {
|
|||
|
||||
{
|
||||
this.state.isOpen &&
|
||||
<div
|
||||
className={styles.popoverContainer}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<div className={styles.popover}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.arrow,
|
||||
styles[position]
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={styles.popoverContainer}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<div className={styles.popover}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.arrow,
|
||||
styles[position]
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
{body}
|
||||
<div className={styles.body}>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</TetherComponent>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import TetherComponent from 'react-tether';
|
||||
import classNames from 'classnames';
|
||||
import isMobileUtil from 'Utilities/isMobile';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import styles from './Tooltip.css';
|
||||
|
||||
|
@ -67,12 +68,14 @@ class Tooltip extends Component {
|
|||
// Listeners
|
||||
|
||||
onClick = () => {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
if (isMobileUtil()) {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (this._closeTimeout) {
|
||||
clearTimeout(this._closeTimeout);
|
||||
this._closeTimeout = clearTimeout(this._closeTimeout);
|
||||
}
|
||||
|
||||
this.setState({ isOpen: true });
|
||||
|
@ -105,7 +108,7 @@ class Tooltip extends Component {
|
|||
>
|
||||
<span
|
||||
className={className}
|
||||
// onClick={this.onClick}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
|
|
25
frontend/src/Components/withCurrentPage.js
Normal file
25
frontend/src/Components/withCurrentPage.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
function withCurrentPage(WrappedComponent) {
|
||||
function CurrentPage(props) {
|
||||
const {
|
||||
history
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
useCurrentPage={history.action === 'POP'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
CurrentPage.propTypes = {
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
return CurrentPage;
|
||||
}
|
||||
|
||||
export default withCurrentPage;
|
Loading…
Add table
Add a link
Reference in a new issue