New: Custom Filtering for UI (#234)
parent
c6873014c7
commit
7354e02bff
@ -0,0 +1,5 @@
|
|||||||
|
.filterMenuContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.releases.items,
|
||||||
|
(state) => state.releases.filterBuilderProps,
|
||||||
|
(sectionItems, filterBuilderProps) => {
|
||||||
|
return {
|
||||||
|
sectionItems,
|
||||||
|
filterBuilderProps
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onRemoveCustomFilterPress(index) {
|
||||||
|
dispatch(releaseActions.removeReleasesCustomFilter({ index }));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSaveCustomFilterPress(payload) {
|
||||||
|
dispatch(releaseActions.saveReleasesCustomFilter(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
@ -1,6 +1,5 @@
|
|||||||
.info {
|
.info {
|
||||||
background-color: $defaultColor;
|
background-color: #fafbfc;
|
||||||
color: $white;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: $smallFontSize;
|
font-size: $smallFontSize;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
.info {
|
.info {
|
||||||
background-color: $defaultColor;
|
background-color: #fafbfc;
|
||||||
color: $white;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: $smallFontSize;
|
font-size: $smallFontSize;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
.labelContainer {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelInputContainer {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rows {
|
||||||
|
margin-bottom: 100px;
|
||||||
|
}
|
@ -0,0 +1,192 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import FilterBuilderRow from './FilterBuilderRow';
|
||||||
|
import styles from './FilterBuilderModalContent.css';
|
||||||
|
|
||||||
|
class FilterBuilderModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const filters = [...props.filters];
|
||||||
|
|
||||||
|
// Push an empty filter if there aren't any filters. FilterBuilderRow
|
||||||
|
// will handle initializing the filter.
|
||||||
|
|
||||||
|
if (!filters.length) {
|
||||||
|
filters.push({});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
label: props.label,
|
||||||
|
filters,
|
||||||
|
labelErrors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onLabelChange = ({ value }) => {
|
||||||
|
this.setState({ label: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange = (index, filter) => {
|
||||||
|
const filters = [...this.state.filters];
|
||||||
|
filters.splice(index, 1, filter);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddFilterPress = () => {
|
||||||
|
const filters = [...this.state.filters];
|
||||||
|
filters.push({});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveFilterPress = (index) => {
|
||||||
|
const filters = [...this.state.filters];
|
||||||
|
filters.splice(index, 1);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveFilterPress = () => {
|
||||||
|
const {
|
||||||
|
customFilterKey: key,
|
||||||
|
onSaveCustomFilterPress,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
filters
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
this.setState({
|
||||||
|
labelErrors: [
|
||||||
|
{
|
||||||
|
message: 'Label is required'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveCustomFilterPress({ key, label, filters });
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
sectionItems,
|
||||||
|
filterBuilderProps,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
filters,
|
||||||
|
labelErrors
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Custom Filter
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.labelContainer}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
Label
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.labelInputContainer}>
|
||||||
|
<FormInputGroup
|
||||||
|
name="label"
|
||||||
|
value={label}
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
errors={labelErrors}
|
||||||
|
onChange={this.onLabelChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.label}>Filters</div>
|
||||||
|
|
||||||
|
<div className={styles.rows}>
|
||||||
|
{
|
||||||
|
filters.map((filter, index) => {
|
||||||
|
return (
|
||||||
|
<FilterBuilderRow
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
filterKey={filter.key}
|
||||||
|
filterValue={filter.value}
|
||||||
|
filterType={filter.type}
|
||||||
|
filterCount={filters.length}
|
||||||
|
onAddPress={this.onAddFilterPress}
|
||||||
|
onRemovePress={this.onRemoveFilterPress}
|
||||||
|
onFilterChange={this.onFilterChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={this.onSaveFilterPress}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderModalContent.propTypes = {
|
||||||
|
customFilterKey: PropTypes.string,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
|
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderModalContent;
|
@ -0,0 +1,28 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { customFilters }) => customFilters,
|
||||||
|
(state, { customFilterKey }) => customFilterKey,
|
||||||
|
(customFilters, customFilterKey) => {
|
||||||
|
if (customFilterKey) {
|
||||||
|
const customFilter = customFilters.find((c) => c.key === customFilterKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customFilterKey: customFilter.key,
|
||||||
|
label: customFilter.label,
|
||||||
|
filters: customFilter.filters
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: '',
|
||||||
|
filters: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderModalContent);
|
@ -0,0 +1,32 @@
|
|||||||
|
.filterRow {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
flex: 0 1 200px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueInputContainer {
|
||||||
|
flex: 0 1 300px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsContainer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.filterRow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,248 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||||
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
|
import styles from './FilterBuilderRow.css';
|
||||||
|
|
||||||
|
function getselectedFilterBuilderProp(filterBuilderProps, name) {
|
||||||
|
return filterBuilderProps.find((a) => {
|
||||||
|
return a.name === name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterTypeOptions(filterBuilderProps, filterKey) {
|
||||||
|
const selectedFilterBuilderProp = getselectedFilterBuilderProp(filterBuilderProps, filterKey);
|
||||||
|
|
||||||
|
if (!selectedFilterBuilderProp) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultFilterType(selectedFilterBuilderProp) {
|
||||||
|
return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type][0].key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
|
if (!selectedFilterBuilderProp) {
|
||||||
|
return FilterBuilderRowValueConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueType = selectedFilterBuilderProp.valueType;
|
||||||
|
|
||||||
|
switch (valueType) {
|
||||||
|
case filterBuilderValueTypes.INDEXER:
|
||||||
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.PROTOCOL:
|
||||||
|
return ProtocolFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.QUALITY:
|
||||||
|
return QualityFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return FilterBuilderRowValueConnector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilterBuilderRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selectedFilterBuilderProp: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
filterKey,
|
||||||
|
filterBuilderProps,
|
||||||
|
onFilterChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (filterKey) {
|
||||||
|
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
|
||||||
|
this.setState({ selectedFilterBuilderProp });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedFilterBuilderProp = filterBuilderProps[0];
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
key: selectedFilterBuilderProp.name,
|
||||||
|
value: [],
|
||||||
|
type: getDefaultFilterType(selectedFilterBuilderProp)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ selectedFilterBuilderProp }, () => {
|
||||||
|
onFilterChange(index, filter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFilterKeyChange = ({ value: key }) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
filterBuilderProps,
|
||||||
|
onFilterChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const selectedFilterBuilderProp = getselectedFilterBuilderProp(filterBuilderProps, key);
|
||||||
|
const type = getDefaultFilterType(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
key,
|
||||||
|
value: [],
|
||||||
|
type
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ selectedFilterBuilderProp }, () => {
|
||||||
|
onFilterChange(index, filter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange = ({ name, value }) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
filterKey,
|
||||||
|
filterValue,
|
||||||
|
filterType,
|
||||||
|
onFilterChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
key: filterKey,
|
||||||
|
value: filterValue,
|
||||||
|
type: filterType
|
||||||
|
};
|
||||||
|
|
||||||
|
filter[name] = value;
|
||||||
|
|
||||||
|
onFilterChange(index, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddPress = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
onAddPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onAddPress(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemovePress = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
onRemovePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onRemovePress(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
filterKey,
|
||||||
|
filterType,
|
||||||
|
filterValue,
|
||||||
|
filterCount,
|
||||||
|
filterBuilderProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedFilterBuilderProp
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
||||||
|
return {
|
||||||
|
key: availablePropFilter.name,
|
||||||
|
value: availablePropFilter.label
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.filterRow}>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
{
|
||||||
|
filterKey &&
|
||||||
|
<SelectInput
|
||||||
|
name="key"
|
||||||
|
value={filterKey}
|
||||||
|
values={keyOptions}
|
||||||
|
onChange={this.onFilterKeyChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
{
|
||||||
|
filterType &&
|
||||||
|
<SelectInput
|
||||||
|
name="type"
|
||||||
|
value={filterType}
|
||||||
|
values={getFilterTypeOptions(filterBuilderProps, filterKey)}
|
||||||
|
onChange={this.onFilterChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.valueInputContainer}>
|
||||||
|
{
|
||||||
|
filterValue != null && !!selectedFilterBuilderProp &&
|
||||||
|
<ValueComponent
|
||||||
|
filterValue={filterValue}
|
||||||
|
selectedFilterBuilderProp={selectedFilterBuilderProp}
|
||||||
|
onChange={this.onFilterChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actionsContainer}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.SUBTRACT}
|
||||||
|
isDisabled={filterCount === 1}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.ADD}
|
||||||
|
onPress={this.onAddPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderRow.propTypes = {
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
filterKey: PropTypes.string,
|
||||||
|
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
||||||
|
filterType: PropTypes.string,
|
||||||
|
filterCount: PropTypes.number.isRequired,
|
||||||
|
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onFilterChange: PropTypes.func.isRequired,
|
||||||
|
onAddPress: PropTypes.func.isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderRow;
|
@ -0,0 +1,100 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds, filterBuilderTypes } from 'Helpers/Props';
|
||||||
|
import TagInput, { tagShape } from 'Components/Form/TagInput';
|
||||||
|
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
||||||
|
|
||||||
|
const NAME = 'value';
|
||||||
|
|
||||||
|
class FilterBuilderRowValue extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onTagAdd = (tag) => {
|
||||||
|
const {
|
||||||
|
filterValue,
|
||||||
|
selectedFilterBuilderProp,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let id = tag.id;
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
id = selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ?
|
||||||
|
parseInt(tag.name) :
|
||||||
|
tag.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name: NAME,
|
||||||
|
value: [...filterValue, id]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagDelete = ({ index }) => {
|
||||||
|
const {
|
||||||
|
filterValue,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const value = filterValue.filter((v, i) => i !== index);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name: NAME,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
filterValue,
|
||||||
|
tagList
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const hasItems = !!tagList.length;
|
||||||
|
|
||||||
|
const tags = filterValue.map((id) => {
|
||||||
|
if (hasItems) {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: tag && tag.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TagInput
|
||||||
|
name={NAME}
|
||||||
|
tags={tags}
|
||||||
|
tagList={tagList}
|
||||||
|
allowNew={!tagList.length}
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
delimiters={[9, 13]}
|
||||||
|
maxSuggestionsLength={100}
|
||||||
|
minQueryLength={0}
|
||||||
|
tagComponent={FilterBuilderRowValueTag}
|
||||||
|
onTagAdd={this.onTagAdd}
|
||||||
|
onTagDelete={this.onTagDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderRowValue.propTypes = {
|
||||||
|
filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired,
|
||||||
|
selectedFilterBuilderProp: PropTypes.object.isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderRowValue;
|
@ -0,0 +1,50 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { filterBuilderTypes } from 'Helpers/Props';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createTagListSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { sectionItems }) => _.get(state, sectionItems),
|
||||||
|
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
|
||||||
|
(sectionItems, selectedFilterBuilderProp) => {
|
||||||
|
if (
|
||||||
|
selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||||
|
selectedFilterBuilderProp.type === filterBuilderTypes.STRING
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
if (selectedFilterBuilderProp.optionsSelector) {
|
||||||
|
items = sectionItems.map(selectedFilterBuilderProp.optionsSelector);
|
||||||
|
} else {
|
||||||
|
items = sectionItems.map((item) => {
|
||||||
|
const name = item[selectedFilterBuilderProp.name];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: name,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.uniqBy(items, 'id');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createTagListSelector(),
|
||||||
|
(tagList) => {
|
||||||
|
return {
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
@ -0,0 +1,19 @@
|
|||||||
|
.tag {
|
||||||
|
&.isLastTag {
|
||||||
|
.or {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
composes: label from 'Components/Label.css';
|
||||||
|
|
||||||
|
border-style: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.or {
|
||||||
|
margin: 0 3px;
|
||||||
|
color: $themeDarkColor;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import TagInputTag from 'Components/Form/TagInputTag';
|
||||||
|
import styles from './FilterBuilderRowValueTag.css';
|
||||||
|
|
||||||
|
function FilterBuilderRowValueTag(props) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={styles.tag}
|
||||||
|
>
|
||||||
|
<TagInputTag
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!props.isLastTag &&
|
||||||
|
<span className={styles.or}>
|
||||||
|
or
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterBuilderRowValueTag.propTypes = {
|
||||||
|
isLastTag: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterBuilderRowValueTag;
|
@ -0,0 +1,79 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
|
import { tagShape } from 'Components/Form/TagInput';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.indexers,
|
||||||
|
(qualityProfiles) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items
|
||||||
|
} = qualityProfiles;
|
||||||
|
|
||||||
|
const tagList = items.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchIndexers: fetchIndexers
|
||||||
|
};
|
||||||
|
|
||||||
|
class IndexerFilterBuilderRowValueConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.dispatchFetchIndexers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterBuilderRowValue
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexerFilterBuilderRowValueConnector.propTypes = {
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerFilterBuilderRowValueConnector);
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
const protocols = [
|
||||||
|
{ id: 'torrent', name: 'Torrent' },
|
||||||
|
{ id: 'usenet', name: 'Usenet' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function ProtocolFilterBuilderRowValue(props) {
|
||||||
|
return (
|
||||||
|
<FilterBuilderRowValue
|
||||||
|
tagList={protocols}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolFilterBuilderRowValue;
|
@ -0,0 +1,75 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import getQualities from 'Utilities/Quality/getQualities';
|
||||||
|
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||||
|
import { tagShape } from 'Components/Form/TagInput';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.qualityProfiles,
|
||||||
|
(qualityProfiles) => {
|
||||||
|
const {
|
||||||
|
isFetchingSchema: isFetching,
|
||||||
|
isSchemaPopulated: isPopulated,
|
||||||
|
schemaError: error,
|
||||||
|
schema
|
||||||
|
} = qualityProfiles;
|
||||||
|
|
||||||
|
const tagList = getQualities(schema.items);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
class QualityFilterBuilderRowValueConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.dispatchFetchQualityProfileSchema();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterBuilderRowValue
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityFilterBuilderRowValueConnector.propTypes = {
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(QualityFilterBuilderRowValueConnector);
|
@ -0,0 +1,17 @@
|
|||||||
|
.customFilter {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: 0 1 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
flex: 0 0 60px;
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import styles from './CustomFilter.css';
|
||||||
|
|
||||||
|
class CustomFilter extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditPress = () => {
|
||||||
|
const {
|
||||||
|
customFilterKey,
|
||||||
|
onEditPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onEditPress(customFilterKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemovePress = () => {
|
||||||
|
const {
|
||||||
|
customFilterKey,
|
||||||
|
onRemovePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onRemovePress({ key: customFilterKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
label
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.customFilter}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
onPress={this.onEditPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFilter.propTypes = {
|
||||||
|
customFilterKey: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
onEditPress: PropTypes.func.isRequired,
|
||||||
|
onRemovePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomFilter;
|
@ -0,0 +1,3 @@
|
|||||||
|
.addButtonContainer {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import CustomFilter from './CustomFilter';
|
||||||
|
import styles from './CustomFiltersModalContent.css';
|
||||||
|
|
||||||
|
function CustomFiltersModalContent(props) {
|
||||||
|
const {
|
||||||
|
customFilters,
|
||||||
|
onAddCustomFilter,
|
||||||
|
onRemoveCustomFilterPress,
|
||||||
|
onEditCustomFilter,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Custom Filters
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{
|
||||||
|
customFilters.map((customFilter, index) => {
|
||||||
|
return (
|
||||||
|
<CustomFilter
|
||||||
|
key={index}
|
||||||
|
customFilterKey={customFilter.key}
|
||||||
|
label={customFilter.label}
|
||||||
|
filters={customFilter.filters}
|
||||||
|
onRemovePress={onRemoveCustomFilterPress}
|
||||||
|
onEditPress={onEditCustomFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.addButtonContainer}>
|
||||||
|
<Button onPress={onAddCustomFilter}>
|
||||||
|
Add Custom Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFiltersModalContent.propTypes = {
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onAddCustomFilter: PropTypes.func.isRequired,
|
||||||
|
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
|
onEditCustomFilter: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomFiltersModalContent;
|
@ -0,0 +1,90 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
|
||||||
|
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
|
||||||
|
|
||||||
|
class FilterModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
filterBuilder: !props.customFilters.length,
|
||||||
|
customFilterKey: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAddCustomFilter = () => {
|
||||||
|
this.setState({
|
||||||
|
filterBuilder: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditCustomFilter = (customFilterKey) => {
|
||||||
|
this.setState({
|
||||||
|
filterBuilder: true,
|
||||||
|
customFilterKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.setState({
|
||||||
|
filterBuilder: false,
|
||||||
|
customFilterKey: null
|
||||||
|
}, () => {
|
||||||
|
this.props.onModalClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
filterBuilder,
|
||||||
|
customFilterKey
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
filterBuilder ?
|
||||||
|
<FilterBuilderModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
customFilterKey={customFilterKey}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/> :
|
||||||
|
<CustomFiltersModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onAddCustomFilter={this.onAddCustomFilter}
|
||||||
|
onEditCustomFilter={this.onEditCustomFilter}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterModal;
|
@ -1,97 +1,77 @@
|
|||||||
.container {
|
.inputContainer {
|
||||||
composes: input from 'Components/Form/Input.css';
|
composes: input from 'Components/Form/Input.css';
|
||||||
|
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-wrap: wrap;
|
padding: 0;
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
|
||||||
|
|
||||||
.containerFocused {
|
|
||||||
outline: 0;
|
|
||||||
border-color: $inputFocusBorderColor;
|
|
||||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectedTagContainer {
|
&.isFocused {
|
||||||
flex: 0 0 auto;
|
outline: 0;
|
||||||
}
|
border-color: $inputFocusBorderColor;
|
||||||
|
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
|
||||||
.selectedTag {
|
}
|
||||||
composes: label from 'Components/Label.css';
|
|
||||||
|
|
||||||
border-style: none;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selected Tag Kinds */
|
.hasError {
|
||||||
|
composes: hasError from 'Components/Form/Input.css';
|
||||||
.info {
|
|
||||||
composes: info from 'Components/Label.css';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.hasWarning {
|
||||||
composes: success from 'Components/Label.css';
|
composes: hasWarning from 'Components/Form/Input.css';
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.tags {
|
||||||
composes: warning from 'Components/Label.css';
|
flex: 0 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger {
|
.input {
|
||||||
composes: danger from 'Components/Label.css';
|
flex: 1 1 0%;
|
||||||
|
margin-left: 3px;
|
||||||
|
min-width: 20%;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 0%;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchInputContainer {
|
.suggestionsContainer {
|
||||||
position: relative;
|
@add-mixin scrollbar;
|
||||||
flex: 1 0 100px;
|
@add-mixin scrollbarTrack;
|
||||||
margin-top: 1px;
|
@add-mixin scrollbarThumb;
|
||||||
padding-left: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchInput {
|
.containerOpen {
|
||||||
max-width: 100%;
|
.suggestionsContainer {
|
||||||
font-size: 13px;
|
position: absolute;
|
||||||
|
right: -1px;
|
||||||
input {
|
left: -1px;
|
||||||
margin: 0;
|
z-index: 1;
|
||||||
padding: 0;
|
overflow-y: auto;
|
||||||
max-width: 100%;
|
margin-top: 1px;
|
||||||
outline: none;
|
max-height: 110px;
|
||||||
border: 0;
|
border: 1px solid $inputBorderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions {
|
.suggestionsList {
|
||||||
position: absolute;
|
margin: 5px 0;
|
||||||
z-index: 1;
|
padding-left: 0;
|
||||||
overflow-y: auto;
|
list-style-type: none;
|
||||||
max-height: 200px;
|
}
|
||||||
width: 100%;
|
|
||||||
border: 1px solid $inputBorderColor;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: $white;
|
|
||||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 5px 0;
|
|
||||||
padding-left: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li mark {
|
.suggestion {
|
||||||
font-weight: bold;
|
padding: 0 16px;
|
||||||
}
|
cursor: default;
|
||||||
|
|
||||||
li:hover {
|
&:hover {
|
||||||
background-color: $menuItemHoverColor;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestionActive {
|
.suggestionHighlighted {
|
||||||
background-color: $menuItemHoverColor;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 6px 16px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { tagShape } from './TagInput';
|
||||||
|
import styles from './TagInputInput.css';
|
||||||
|
|
||||||
|
class TagInputInput extends Component {
|
||||||
|
|
||||||
|
onMouseDown = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFocused,
|
||||||
|
onInputContainerPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (isFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputContainerPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
tags,
|
||||||
|
inputProps,
|
||||||
|
kind,
|
||||||
|
tagComponent: TagComponent,
|
||||||
|
onTagDelete
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
component="div"
|
||||||
|
onMouseDown={this.onMouseDown}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
tags.map((tag, index) => {
|
||||||
|
return (
|
||||||
|
<TagComponent
|
||||||
|
key={tag.id}
|
||||||
|
index={index}
|
||||||
|
tag={tag}
|
||||||
|
kind={kind}
|
||||||
|
isLastTag={index === tags.length - 1}
|
||||||
|
onDelete={onTagDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<input {...inputProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagInputInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||||
|
inputProps: PropTypes.object.isRequired,
|
||||||
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
|
isFocused: PropTypes.bool.isRequired,
|
||||||
|
tagComponent: PropTypes.func.isRequired,
|
||||||
|
onTagDelete: PropTypes.func.isRequired,
|
||||||
|
onInputContainerPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
TagInputInput.defaultProps = {
|
||||||
|
className: styles.inputContainer
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagInputInput;
|
@ -0,0 +1,52 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { tagShape } from './TagInput';
|
||||||
|
|
||||||
|
class TagInputTag extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDelete = () => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
tag,
|
||||||
|
onDelete
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onDelete({
|
||||||
|
index,
|
||||||
|
id: tag.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
tag,
|
||||||
|
kind
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link onPress={this.onDelete}>
|
||||||
|
<Label kind={kind}>
|
||||||
|
{tag.name}
|
||||||
|
</Label>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagInputTag.propTypes = {
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
tag: PropTypes.shape(tagShape),
|
||||||
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
|
onDelete: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagInputTag;
|
@ -1,68 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ReactTags from 'react-tag-autocomplete';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './TagInput.css';
|
|
||||||
|
|
||||||
class TextTagInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tags,
|
|
||||||
allowNew,
|
|
||||||
kind,
|
|
||||||
placeholder,
|
|
||||||
onTagAdd,
|
|
||||||
onTagDelete
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const tagInputClassNames = {
|
|
||||||
root: styles.container,
|
|
||||||
rootFocused: styles.containerFocused,
|
|
||||||
selected: styles.selectedTagContainer,
|
|
||||||
selectedTag: classNames(styles.selectedTag, styles[kind]),
|
|
||||||
search: styles.searchInputContainer,
|
|
||||||
searchInput: styles.searchInput,
|
|
||||||
suggestions: styles.suggestions,
|
|
||||||
suggestionActive: styles.suggestionActive,
|
|
||||||
suggestionDisabled: styles.suggestionDisabled
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactTags
|
|
||||||
classNames={tagInputClassNames}
|
|
||||||
tags={tags}
|
|
||||||
allowNew={allowNew}
|
|
||||||
minQueryLength={1}
|
|
||||||
placeholder={placeholder}
|
|
||||||
handleAddition={onTagAdd}
|
|
||||||
handleDelete={onTagDelete}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagShape = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
TextTagInput.propTypes = {
|
|
||||||
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
|
||||||
allowNew: PropTypes.bool.isRequired,
|
|
||||||
kind: PropTypes.string.isRequired,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
onTagAdd: PropTypes.func.isRequired,
|
|
||||||
onTagDelete: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
TextTagInput.defaultProps = {
|
|
||||||
allowNew: true,
|
|
||||||
kind: kinds.INFO
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TextTagInput;
|
|
@ -1,42 +1,107 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Menu from 'Components/Menu/Menu';
|
import FilterMenuContent from './FilterMenuContent';
|
||||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
import Menu from './Menu';
|
||||||
|
import ToolbarMenuButton from './ToolbarMenuButton';
|
||||||
import styles from './FilterMenu.css';
|
import styles from './FilterMenu.css';
|
||||||
|
|
||||||
function FilterMenu(props) {
|
class FilterMenu extends Component {
|
||||||
const {
|
|
||||||
className,
|
//
|
||||||
children,
|
// Lifecycle
|
||||||
isDisabled,
|
|
||||||
...otherProps
|
constructor(props, context) {
|
||||||
} = props;
|
super(props, context);
|
||||||
|
|
||||||
return (
|
this.state = {
|
||||||
<Menu
|
isFilterModalOpen: false
|
||||||
className={className}
|
};
|
||||||
{...otherProps}
|
}
|
||||||
>
|
|
||||||
<ToolbarMenuButton
|
//
|
||||||
iconName={icons.FILTER}
|
// Listeners
|
||||||
text="Filter"
|
|
||||||
isDisabled={isDisabled}
|
onCustomFiltersPress = () => {
|
||||||
/>
|
this.setState({ isFilterModalOpen: true });
|
||||||
{children}
|
}
|
||||||
</Menu>
|
|
||||||
);
|
onFiltersModalClose = () => {
|
||||||
|
this.setState({ isFilterModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render(props) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
buttonComponent: ButtonComponent,
|
||||||
|
filterModalConnectorComponent: FilterModalConnectorComponent,
|
||||||
|
onFilterSelect,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const showCustomFilters = !!FilterModalConnectorComponent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Menu
|
||||||
|
className={className}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<ButtonComponent
|
||||||
|
iconName={icons.FILTER}
|
||||||
|
text="Filter"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FilterMenuContent
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
showCustomFilters={showCustomFilters}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
onCustomFiltersPress={this.onCustomFiltersPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
{
|
||||||
|
showCustomFilters &&
|
||||||
|
<FilterModalConnectorComponent
|
||||||
|
isOpen={this.state.isFilterModalOpen}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
onModalClose={this.onFiltersModalClose}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterMenu.propTypes = {
|
FilterMenu.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
children: PropTypes.node.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
buttonComponent: PropTypes.func.isRequired,
|
||||||
|
filterModalConnectorComponent: PropTypes.func,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
FilterMenu.defaultProps = {
|
FilterMenu.defaultProps = {
|
||||||
className: styles.filterMenu,
|
className: styles.filterMenu,
|
||||||
isDisabled: false
|
isDisabled: false,
|
||||||
|
buttonComponent: ToolbarMenuButton
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FilterMenu;
|
export default FilterMenu;
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import MenuContent from './MenuContent';
|
||||||
|
import FilterMenuItem from './FilterMenuItem';
|
||||||
|
import MenuItem from './MenuItem';
|
||||||
|
import MenuItemSeparator from './MenuItemSeparator';
|
||||||
|
|
||||||
|
class FilterMenuContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
showCustomFilters,
|
||||||
|
onFilterSelect,
|
||||||
|
onCustomFiltersPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuContent {...otherProps}>
|
||||||
|
{
|
||||||
|
filters.map((filter) => {
|
||||||
|
return (
|
||||||
|
<FilterMenuItem
|
||||||
|
key={filter.key}
|
||||||
|
filterKey={filter.key}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
onPress={onFilterSelect}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</FilterMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
customFilters.map((filter) => {
|
||||||
|
return (
|
||||||
|
<FilterMenuItem
|
||||||
|
key={filter.key}
|
||||||
|
filterKey={filter.key}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
onPress={onFilterSelect}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</FilterMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showCustomFilters &&
|
||||||
|
<MenuItemSeparator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showCustomFilters &&
|
||||||
|
<MenuItem onPress={onCustomFiltersPress}>
|
||||||
|
Custom Filters
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
</MenuContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterMenuContent.propTypes = {
|
||||||
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
showCustomFilters: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onCustomFiltersPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterMenuContent.defaultProps = {
|
||||||
|
showCustomFilters: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterMenuContent;
|
@ -0,0 +1,5 @@
|
|||||||
|
.separator {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 1px;
|
||||||
|
background-color: $themeDarkColor;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './MenuItemSeparator.css';
|
||||||
|
|
||||||
|
function MenuItemSeparator() {
|
||||||
|
return (
|
||||||
|
<div className={styles.separator} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuItemSeparator;
|
@ -0,0 +1,11 @@
|
|||||||
|
.menuButton {
|
||||||
|
composes: menuButton from './MenuButton.css';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import MenuButton from 'Components/Menu/MenuButton';
|
||||||
|
import styles from './PageMenuButton.css';
|
||||||
|
|
||||||
|
function PageMenuButton(props) {
|
||||||
|
const {
|
||||||
|
iconName,
|
||||||
|
text,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuButton
|
||||||
|
className={styles.menuButton}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={iconName}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.label}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</MenuButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PageMenuButton.propTypes = {
|
||||||
|
iconName: PropTypes.object.isRequired,
|
||||||
|
text: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageMenuButton;
|
@ -0,0 +1,33 @@
|
|||||||
|
import * as filterTypes from './filterTypes';
|
||||||
|
|
||||||
|
export const EXACT = 'exact';
|
||||||
|
export const NUMBER = 'number';
|
||||||
|
export const STRING = 'string';
|
||||||
|
|
||||||
|
export const all = [
|
||||||
|
EXACT,
|
||||||
|
NUMBER,
|
||||||
|
STRING
|
||||||
|
];
|
||||||
|
|
||||||
|
export const possibleFilterTypes = {
|
||||||
|
[EXACT]: [
|
||||||
|
{ 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' }
|
||||||
|
],
|
||||||
|
|
||||||
|
[STRING]: [
|
||||||
|
{ key: filterTypes.CONTAINS, value: 'Contains' },
|
||||||
|
{ key: filterTypes.EQUAL, value: 'Equal' },
|
||||||
|
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' }
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,4 @@
|
|||||||
|
export const DEFAULT = 'default';
|
||||||
|
export const INDEXER = 'indexer';
|
||||||
|
export const PROTOCOL = 'protocol';
|
||||||
|
export const QUALITY = 'quality';
|
@ -0,0 +1,65 @@
|
|||||||
|
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
||||||
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import generateUUIDv4 from 'Utilities/String/generateUUIDv4';
|
||||||
|
|
||||||
|
function createRemoveCustomFilterReducer(section) {
|
||||||
|
return (state, { payload }) => {
|
||||||
|
const newState = getSectionState(state, section);
|
||||||
|
const index = newState.customFilters.findIndex((c) => c.key === payload.key);
|
||||||
|
|
||||||
|
newState.customFilters = [...newState.customFilters];
|
||||||
|
newState.customFilters.splice(index, 1);
|
||||||
|
|
||||||
|
// Reset the selected filter to the first filter if the selected filter
|
||||||
|
// is being deleted.
|
||||||
|
// TODO: Server side collections need to have their collections refetched
|
||||||
|
|
||||||
|
if (newState.selectedFilterKey === payload.key) {
|
||||||
|
newState.selectedFilterKey = newState.filters[0].key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateSectionState(state, section, newState);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSaveCustomFilterReducer(section) {
|
||||||
|
return (state, { payload }) => {
|
||||||
|
const newState = getSectionState(state, section);
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
filters
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
let key = payload.key;
|
||||||
|
|
||||||
|
newState.customFilters = [...newState.customFilters];
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
const index = newState.customFilters.findIndex((c) => c.key === key);
|
||||||
|
|
||||||
|
newState.customFilters.splice(index, 1, { key, label, filters });
|
||||||
|
} else {
|
||||||
|
key = generateUUIDv4();
|
||||||
|
|
||||||
|
newState.customFilters.push({
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
filters
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Server side collections need to have their collections refetched
|
||||||
|
newState.selectedFilterKey = key;
|
||||||
|
|
||||||
|
return updateSectionState(state, section, newState);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createCustomFilterReducers(section, handlers) {
|
||||||
|
return {
|
||||||
|
[handlers[customFilterHandlers.REMOVE]]: createRemoveCustomFilterReducer(section),
|
||||||
|
[handlers[customFilterHandlers.SAVE]]: createSaveCustomFilterReducer(section)
|
||||||
|
};
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
import $ from 'jquery';
|
|
||||||
import updateAlbums from 'Utilities/Album/updateAlbums';
|
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
|
||||||
|
|
||||||
function createToggleAlbumMonitoredHandler(section) {
|
|
||||||
return function(payload) {
|
|
||||||
return function(dispatch, getState) {
|
|
||||||
const {
|
|
||||||
albumId,
|
|
||||||
monitored
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const state = getSectionState(getState(), section, true);
|
|
||||||
|
|
||||||
updateAlbums(dispatch, section, state.items, [albumId], {
|
|
||||||
isSaving: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const promise = $.ajax({
|
|
||||||
url: `/album/${albumId}`,
|
|
||||||
method: 'PUT',
|
|
||||||
data: JSON.stringify({ monitored }),
|
|
||||||
dataType: 'json'
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.done(() => {
|
|
||||||
updateAlbums(dispatch, section, state.items, [albumId], {
|
|
||||||
isSaving: false,
|
|
||||||
monitored
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail(() => {
|
|
||||||
updateAlbums(dispatch, section, state.items, [albumId], {
|
|
||||||
isSaving: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createToggleAlbumMonitoredHandler;
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue