New: UI Updates, Tag manager, More custom filters (#437)
* New: UI Updates, Tag manager, More custom filters * fixup! Fix ScanFixture Unit Tests * Fixed: Sentry Errors from UI don't have release, branch, environment * Changed: Bump Mobile Detect for New Device Detection * Fixed: Build on changes to package.json * fixup! Add MetadataProfile filter option * fixup! Tag Note, Blacklist, Manual Import * fixup: Remove connectSection * fixup: root folder commentpull/447/head
parent
afa78b1d20
commit
6581b3a2c5
@ -0,0 +1,31 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as artistEditorActions from 'Store/Actions/artistEditorActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artist.items,
|
||||
(state) => state.artistEditor.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemoveCustomFilterPress(payload) {
|
||||
dispatch(artistEditorActions.removeArtistEditorCustomFilter(payload));
|
||||
},
|
||||
|
||||
onSaveCustomFilterPress(payload) {
|
||||
dispatch(artistEditorActions.saveArtistEditorCustomFilter(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
@ -0,0 +1,31 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as artistIndexActions from 'Store/Actions/artistIndexActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artist.items,
|
||||
(state) => state.artistIndex.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemoveCustomFilterPress(payload) {
|
||||
dispatch(artistIndexActions.removeArtistCustomFilter(payload));
|
||||
},
|
||||
|
||||
onSaveCustomFilterPress(payload) {
|
||||
dispatch(artistIndexActions.saveArtistCustomFilter(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
@ -1,4 +1,4 @@
|
||||
.descriptionList {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: 'continuing', name: 'Continuing' },
|
||||
{ id: 'ended', name: 'Ended' }
|
||||
];
|
||||
|
||||
function ArtistStatusFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistStatusFilterBuilderRowValue;
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: true, name: 'true' },
|
||||
{ id: false, name: 'false' }
|
||||
];
|
||||
|
||||
function BoolFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoolFilterBuilderRowValue;
|
@ -0,0 +1,15 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.numberInput {
|
||||
composes: text from 'Components/Form/TextInput.css';
|
||||
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.selectInput {
|
||||
composes: select from 'Components/Form/SelectInput.css';
|
||||
|
||||
margin-left: 3px;
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import isString from 'Utilities/String/isString';
|
||||
import { IN_LAST, IN_NEXT } from 'Helpers/Props/filterTypes';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import { NAME } from './FilterBuilderRowValue';
|
||||
import styles from './DateFilterBuilderRowValue.css';
|
||||
|
||||
const timeOptions = [
|
||||
{ key: 'seconds', value: 'seconds' },
|
||||
{ key: 'minutes', value: 'minutes' },
|
||||
{ key: 'hours', value: 'hours' },
|
||||
{ key: 'days', value: 'days' },
|
||||
{ key: 'weeks', value: 'weeks' },
|
||||
{ key: 'months', value: 'months' }
|
||||
];
|
||||
|
||||
function isInFilter(filterType) {
|
||||
return filterType === IN_LAST || filterType === IN_NEXT;
|
||||
}
|
||||
|
||||
class DateFilterBuilderRowValue extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
filterType,
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
if (isInFilter(filterType) && isString(filterValue)) {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: timeOptions[0].key,
|
||||
value: null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
filterType,
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.filterType === filterType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInFilter(filterType) && isString(filterValue)) {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: timeOptions[0].key,
|
||||
value: null
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInFilter(filterType) && !isString(filterValue)) {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onValueChange = ({ value }) => {
|
||||
const {
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
let newValue = value;
|
||||
|
||||
if (!isString(value)) {
|
||||
newValue = {
|
||||
time: filterValue.time,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
onTimeChange = ({ value }) => {
|
||||
const {
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: value,
|
||||
value: filterValue.value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
filterType,
|
||||
filterValue
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(isInFilter(filterType) && isString(filterValue)) ||
|
||||
(!isInFilter(filterType) && !isString(filterValue))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isInFilter(filterType)) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<NumberInput
|
||||
className={styles.numberInput}
|
||||
name={NAME}
|
||||
value={filterValue.value}
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
className={styles.selectInput}
|
||||
name={NAME}
|
||||
value={filterValue.time}
|
||||
values={timeOptions}
|
||||
onChange={this.onTimeChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
name={NAME}
|
||||
value={filterValue}
|
||||
placeholder="yyyy-mm-dd"
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateFilterBuilderRowValue.propTypes = {
|
||||
filterType: PropTypes.string,
|
||||
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DateFilterBuilderRowValue;
|
@ -0,0 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.languageProfiles,
|
||||
(languageProfiles) => {
|
||||
const tagList = languageProfiles.items.map((languageProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = languageProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
@ -0,0 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(metadataProfiles) => {
|
||||
const tagList = metadataProfiles.items.map((metadataProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = metadataProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
@ -0,0 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = qualityProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
@ -0,0 +1,27 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createTagsSelector(),
|
||||
(tagList) => {
|
||||
return {
|
||||
tagList: tagList.map((tag) => {
|
||||
const {
|
||||
id,
|
||||
label: name
|
||||
} = tag;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
@ -1,30 +1,39 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
|
||||
function OAuthInput(props) {
|
||||
const {
|
||||
label,
|
||||
authorizing,
|
||||
error,
|
||||
onPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SpinnerButton
|
||||
<SpinnerErrorButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={authorizing}
|
||||
error={error}
|
||||
onPress={onPress}
|
||||
>
|
||||
Start OAuth
|
||||
</SpinnerButton>
|
||||
{label}
|
||||
</SpinnerErrorButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
OAuthInput.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
authorizing: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
OAuthInput.defaultProps = {
|
||||
label: 'Start OAuth'
|
||||
};
|
||||
|
||||
export default OAuthInput;
|
||||
|
@ -1,33 +1,50 @@
|
||||
import * as filterTypes from './filterTypes';
|
||||
|
||||
export const ARRAY = 'array';
|
||||
export const DATE = 'date';
|
||||
export const EXACT = 'exact';
|
||||
export const NUMBER = 'number';
|
||||
export const STRING = 'string';
|
||||
|
||||
export const all = [
|
||||
ARRAY,
|
||||
DATE,
|
||||
EXACT,
|
||||
NUMBER,
|
||||
STRING
|
||||
];
|
||||
|
||||
export const possibleFilterTypes = {
|
||||
[ARRAY]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' },
|
||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
||||
],
|
||||
|
||||
[DATE]: [
|
||||
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
||||
{ key: filterTypes.IN_LAST, value: 'in the last' },
|
||||
{ key: filterTypes.IN_NEXT, value: 'in the next' }
|
||||
],
|
||||
|
||||
[EXACT]: [
|
||||
{ key: filterTypes.EQUAL, value: 'Is' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'Is Not' }
|
||||
{ key: filterTypes.EQUAL, value: 'is' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
||||
],
|
||||
|
||||
[NUMBER]: [
|
||||
{ key: filterTypes.EQUAL, value: 'Equal' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'Greater Than' },
|
||||
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'Greater Than or Equal' },
|
||||
{ key: filterTypes.LESS_THAN, value: 'Less Than' },
|
||||
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'Less Than or Equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' }
|
||||
{ key: filterTypes.EQUAL, value: 'equal' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'greater than' },
|
||||
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'greater than or equal' },
|
||||
{ key: filterTypes.LESS_THAN, value: 'less than' },
|
||||
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'less than or equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
|
||||
],
|
||||
|
||||
[STRING]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'Contains' },
|
||||
{ key: filterTypes.EQUAL, value: 'Equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' }
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' },
|
||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
|
||||
{ key: filterTypes.EQUAL, value: 'equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
|
||||
]
|
||||
};
|
||||
|
@ -1,4 +1,11 @@
|
||||
export const BOOL = 'bool';
|
||||
export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const INDEXER = 'indexer';
|
||||
export const LANGUAGE_PROFILE = 'languageProfile';
|
||||
export const METADATA_PROFILE = 'metadataProfile';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const QUALITY = 'quality';
|
||||
export const QUALITY_PROFILE = 'qualityProfile';
|
||||
export const ARTIST_STATUS = 'artistStatus';
|
||||
export const TAG = 'tag';
|
||||
|
@ -0,0 +1,45 @@
|
||||
import * as filterTypes from './filterTypes';
|
||||
|
||||
const filterTypePredicates = {
|
||||
[filterTypes.CONTAINS]: function(itemValue, filterValue) {
|
||||
if (Array.isArray(itemValue)) {
|
||||
return itemValue.some((v) => v === filterValue);
|
||||
}
|
||||
|
||||
return itemValue.toLowerCase().contains(filterValue.toLowerCase());
|
||||
},
|
||||
|
||||
[filterTypes.EQUAL]: function(itemValue, filterValue) {
|
||||
return itemValue === filterValue;
|
||||
},
|
||||
|
||||
[filterTypes.GREATER_THAN]: function(itemValue, filterValue) {
|
||||
return itemValue > filterValue;
|
||||
},
|
||||
|
||||
[filterTypes.GREATER_THAN_OR_EQUAL]: function(itemValue, filterValue) {
|
||||
return itemValue >= filterValue;
|
||||
},
|
||||
|
||||
[filterTypes.LESS_THAN]: function(itemValue, filterValue) {
|
||||
return itemValue < filterValue;
|
||||
},
|
||||
|
||||
[filterTypes.LESS_THAN_OR_EQUAL]: function(itemValue, filterValue) {
|
||||
return itemValue <= filterValue;
|
||||
},
|
||||
|
||||
[filterTypes.NOT_CONTAINS]: function(itemValue, filterValue) {
|
||||
if (Array.isArray(itemValue)) {
|
||||
return !itemValue.some((v) => v === filterValue);
|
||||
}
|
||||
|
||||
return !itemValue.toLowerCase().contains(filterValue.toLowerCase());
|
||||
},
|
||||
|
||||
[filterTypes.NOT_EQUAL]: function(itemValue, filterValue) {
|
||||
return itemValue !== filterValue;
|
||||
}
|
||||
};
|
||||
|
||||
export default filterTypePredicates;
|
@ -0,0 +1,18 @@
|
||||
.modalBody {
|
||||
composes: modalBody from 'Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
composes: text from 'Components/Form/TextInput.css';
|
||||
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
.season {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 40px;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue