mirror of
https://github.com/Sonarr/Sonarr
synced 2025-03-10 06:03:32 +00:00
Convert Filter components to TypeScript
This commit is contained in:
parent
e4f1b2c4ec
commit
3cd8a2a98b
55 changed files with 1644 additions and 1840 deletions
|
@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||
|
||||
function createBlocklistSelector() {
|
||||
|
@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
interface BlocklistFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
type BlocklistFilterModalProps = FilterModalProps<History>;
|
||||
|
||||
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||
const sectionItems = useSelector(createBlocklistSelector());
|
||||
|
@ -43,7 +41,6 @@ export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
|
@ -23,19 +23,16 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
interface HistoryFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
type HistoryFilterModalProps = FilterModalProps<History>;
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'history';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
(payload: { selectedFilterKey: string | number }) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -43,11 +40,10 @@ export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
customFilterType="history"
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||
|
||||
function createQueueSelector() {
|
||||
|
@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
interface QueueFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
type QueueFilterModalProps = FilterModalProps<History>;
|
||||
|
||||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||
const sectionItems = useSelector(createQueueSelector());
|
||||
|
@ -43,7 +41,6 @@ export default function QueueFilterModal(props: QueueFilterModalProps) {
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import { DateFilterValue } from 'Components/Filter/Builder/DateFilterBuilderRowValue';
|
||||
import { FilterBuilderTypes } from 'Helpers/Props/filterBuilderTypes';
|
||||
import { Error } from './AppSectionState';
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
|
@ -21,33 +24,33 @@ import SystemAppState from './SystemAppState';
|
|||
import TagsAppState from './TagsAppState';
|
||||
import WantedAppState from './WantedAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
export interface FilterBuilderPropOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FilterBuilderProp<T> {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
label: string | (() => string);
|
||||
type: FilterBuilderTypes;
|
||||
valueType?: string;
|
||||
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
|
||||
}
|
||||
|
||||
export interface PropertyFilter {
|
||||
key: string;
|
||||
value: boolean | string | number | string[] | number[];
|
||||
value: string | string[] | number[] | boolean[] | DateFilterValue;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
filters: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface CustomFilter {
|
||||
id: number;
|
||||
export interface CustomFilter extends ModelBase {
|
||||
type: string;
|
||||
label: string;
|
||||
filters: PropertyFilter[];
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { CustomFilter } from './AppState';
|
||||
|
||||
interface CustomFiltersAppState
|
||||
extends AppSectionState<CustomFilter>,
|
||||
AppSectionDeleteState {}
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export default CustomFiltersAppState;
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import { setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
|
||||
function createCalendarSelector() {
|
||||
|
@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
interface CalendarFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
type CalendarFilterModalProps = FilterModalProps<History>;
|
||||
|
||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||
const sectionItems = useSelector(createCalendarSelector());
|
||||
|
@ -43,7 +41,6 @@ export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
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,22 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: true, name: 'true' },
|
||||
{ id: false, name: 'false' },
|
||||
];
|
||||
|
||||
type BoolFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, boolean>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function BoolFilterBuilderRowValue<T>(
|
||||
props: BoolFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
return <FilterBuilderRowValue tagList={protocols} {...props} />;
|
||||
}
|
||||
|
||||
export default BoolFilterBuilderRowValue;
|
|
@ -1,177 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import { IN_LAST, IN_NEXT, NOT_IN_LAST, NOT_IN_NEXT } from 'Helpers/Props/filterTypes';
|
||||
import isString from 'Utilities/String/isString';
|
||||
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 === NOT_IN_LAST ||
|
||||
filterType === IN_NEXT ||
|
||||
filterType === NOT_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}
|
||||
type="date"
|
||||
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,158 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import {
|
||||
IN_LAST,
|
||||
IN_NEXT,
|
||||
NOT_IN_LAST,
|
||||
NOT_IN_NEXT,
|
||||
} from 'Helpers/Props/filterTypes';
|
||||
import { InputChanged, InputOnChange } from 'typings/inputs';
|
||||
import isString from 'Utilities/String/isString';
|
||||
import { FilterBuilderRowValueProps, 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: string | undefined) {
|
||||
return (
|
||||
filterType === IN_LAST ||
|
||||
filterType === NOT_IN_LAST ||
|
||||
filterType === IN_NEXT ||
|
||||
filterType === NOT_IN_NEXT
|
||||
);
|
||||
}
|
||||
|
||||
export interface DateFilterValue {
|
||||
time: string;
|
||||
value: number | null;
|
||||
}
|
||||
|
||||
interface DateFilterBuilderRowValueProps<T>
|
||||
extends Omit<
|
||||
FilterBuilderRowValueProps<T, string>,
|
||||
'filterValue' | 'onChange'
|
||||
> {
|
||||
filterValue: string | DateFilterValue;
|
||||
onChange: InputOnChange<string | DateFilterValue>;
|
||||
}
|
||||
|
||||
function DateFilterBuilderRowValue<T>({
|
||||
filterType,
|
||||
filterValue,
|
||||
onChange,
|
||||
}: DateFilterBuilderRowValueProps<T>) {
|
||||
const previousFilterType = usePrevious(filterType);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
({ value }: InputChanged<number | null>) => {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: (filterValue as DateFilterValue).time,
|
||||
value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[filterValue, onChange]
|
||||
);
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: value,
|
||||
value: (filterValue as DateFilterValue).value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[filterValue, onChange]
|
||||
);
|
||||
|
||||
const handleDateChange = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value,
|
||||
});
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousFilterType === 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: '',
|
||||
});
|
||||
}
|
||||
}, [filterType, previousFilterType, filterValue, onChange]);
|
||||
|
||||
if (
|
||||
(isInFilter(filterType) && isString(filterValue)) ||
|
||||
(!isInFilter(filterType) && !isString(filterValue))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isInFilter(filterType)) {
|
||||
const { value, time } = filterValue as DateFilterValue;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<NumberInput
|
||||
className={styles.numberInput}
|
||||
name={NAME}
|
||||
value={value}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
className={styles.selectInput}
|
||||
name={NAME}
|
||||
value={time}
|
||||
values={timeOptions}
|
||||
onChange={handleTimeChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
name={NAME}
|
||||
value={filterValue as string}
|
||||
type="date"
|
||||
placeholder="yyyy-mm-dd"
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateFilterBuilderRowValue;
|
|
@ -0,0 +1,74 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { FilterBuilderPropOption } from 'App/State/AppState';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
type DefaultFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, string>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function DefaultFilterBuilderRowValue<T>({
|
||||
filterType,
|
||||
sectionItems,
|
||||
selectedFilterBuilderProp,
|
||||
...otherProps
|
||||
}: DefaultFilterBuilderRowValueProps<T>) {
|
||||
const tagList = useMemo(() => {
|
||||
if (
|
||||
((selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
|
||||
filterType !== filterTypes.EQUAL &&
|
||||
filterType !== filterTypes.NOT_EQUAL) ||
|
||||
!selectedFilterBuilderProp.optionsSelector
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let items: FilterBuilderPropOption[] = [];
|
||||
|
||||
if (selectedFilterBuilderProp.optionsSelector) {
|
||||
items = selectedFilterBuilderProp
|
||||
.optionsSelector(sectionItems)
|
||||
.filter(
|
||||
(value, index, array) =>
|
||||
array.findIndex((v) => v.id === value.id) === index
|
||||
);
|
||||
} else {
|
||||
items = sectionItems
|
||||
.reduce<FilterBuilderPropOption[]>((acc, item) => {
|
||||
// @ts-expect-error - can't guarantee that the name property exists on the item
|
||||
const name = item[selectedFilterBuilderProp.name];
|
||||
|
||||
// DOn't add invalid values or items that already exist
|
||||
if (name && acc.findIndex((a) => a.id === name) === -1) {
|
||||
acc.push({
|
||||
id: name,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort(sortByProp('name'));
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [filterType, sectionItems, selectedFilterBuilderProp]);
|
||||
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
{...otherProps}
|
||||
filterType={filterType}
|
||||
sectionItems={sectionItems}
|
||||
selectedFilterBuilderProp={selectedFilterBuilderProp}
|
||||
tagList={tagList}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DefaultFilterBuilderRowValue;
|
|
@ -1,232 +0,0 @@
|
|||
import { maxBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
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: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError,
|
||||
dispatchSetFilter,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = maxBy(customFilters, 'id');
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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 {
|
||||
id,
|
||||
customFilterType,
|
||||
onSaveCustomFilterPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
label,
|
||||
filters
|
||||
} = this.state;
|
||||
|
||||
if (!label) {
|
||||
this.setState({
|
||||
labelErrors: [
|
||||
{
|
||||
message: translate('LabelIsRequired')
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onSaveCustomFilterPress({
|
||||
id,
|
||||
type: customFilterType,
|
||||
label,
|
||||
filters
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
isSaving,
|
||||
saveError,
|
||||
onCancelPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
label,
|
||||
filters,
|
||||
labelErrors
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('CustomFilter')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{translate('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}>
|
||||
{translate('Filters')}
|
||||
</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{
|
||||
filters.map((filter, index) => {
|
||||
return (
|
||||
<FilterBuilderRow
|
||||
key={`${filter.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={onCancelPress}>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={this.onSaveFilterPress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterBuilderModalContent.propTypes = {
|
||||
id: PropTypes.number,
|
||||
label: PropTypes.string.isRequired,
|
||||
customFilterType: PropTypes.string.isRequired,
|
||||
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default FilterBuilderModalContent;
|
|
@ -0,0 +1,208 @@
|
|||
import { maxBy } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState, {
|
||||
CustomFilter,
|
||||
FilterBuilderProp,
|
||||
PropertyFilter,
|
||||
} from 'App/State/AppState';
|
||||
import FormInputGroup, {
|
||||
ValidationMessage,
|
||||
} from 'Components/Form/FormInputGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { saveCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRow from './FilterBuilderRow';
|
||||
import styles from './FilterBuilderModalContent.css';
|
||||
|
||||
const NEW_FILTER: Partial<PropertyFilter> = {
|
||||
key: undefined,
|
||||
value: undefined,
|
||||
type: '',
|
||||
};
|
||||
|
||||
interface FilterBuilderModalContentProps<T> {
|
||||
id: number | null;
|
||||
customFilterType: string;
|
||||
sectionItems: T[];
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
customFilters: CustomFilter[];
|
||||
dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void;
|
||||
onCancelPress: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function FilterBuilderModalContent<T>({
|
||||
id,
|
||||
customFilters,
|
||||
customFilterType,
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
dispatchSetFilter,
|
||||
onCancelPress,
|
||||
onModalClose,
|
||||
}: FilterBuilderModalContentProps<T>) {
|
||||
const dispatch = useDispatch();
|
||||
const { isSaving, saveError } = useSelector(
|
||||
(state: AppState) => state.customFilters
|
||||
);
|
||||
|
||||
const { initialLabel, initialFilters } = useMemo(() => {
|
||||
if (id) {
|
||||
const customFilter = customFilters.find((c) => c.id === id);
|
||||
|
||||
if (customFilter) {
|
||||
return {
|
||||
initialLabel: customFilter.label,
|
||||
initialFilters: customFilter.filters,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
initialLabel: '',
|
||||
initialFilters: [],
|
||||
};
|
||||
}, [id, customFilters]);
|
||||
|
||||
const [label, setLabel] = useState(initialLabel);
|
||||
// Push an empty filter if there aren't any filters. FilterBuilderRow
|
||||
// will handle initializing the filter.
|
||||
const [filters, setFilters] = useState<Partial<PropertyFilter>[]>(
|
||||
initialFilters.length ? initialFilters : [NEW_FILTER]
|
||||
);
|
||||
const [labelErrors, setLabelErrors] = useState<ValidationMessage[]>([]);
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const handleLabelChange = useCallback(({ value }: InputChanged<string>) => {
|
||||
setLabel(value);
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(index: number, filter: PropertyFilter) => {
|
||||
const newFilters = [...filters];
|
||||
newFilters.splice(index, 1, filter);
|
||||
|
||||
setFilters(newFilters);
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
|
||||
const handleAddFilterPress = useCallback(() => {
|
||||
setFilters([...filters, NEW_FILTER]);
|
||||
}, [filters]);
|
||||
|
||||
const handleRemoveFilterPress = useCallback(
|
||||
(index: number) => {
|
||||
const newFilters = [...filters];
|
||||
newFilters.splice(index, 1);
|
||||
|
||||
setFilters(newFilters);
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
|
||||
const handleSaveFilterPress = useCallback(() => {
|
||||
if (!label) {
|
||||
setLabelErrors([
|
||||
{
|
||||
message: translate('LabelIsRequired'),
|
||||
},
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(saveCustomFilter({ id, type: customFilterType, label, filters }));
|
||||
}, [id, customFilterType, label, filters, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasSaving && !isSaving && !saveError) {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = maxBy(customFilters, 'id');
|
||||
|
||||
if (last) {
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
}
|
||||
}, [
|
||||
id,
|
||||
customFilters,
|
||||
isSaving,
|
||||
wasSaving,
|
||||
saveError,
|
||||
dispatchSetFilter,
|
||||
onModalClose,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('CustomFilter')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>{translate('Label')}</div>
|
||||
|
||||
<div className={styles.labelInputContainer}>
|
||||
<FormInputGroup
|
||||
name="label"
|
||||
value={label}
|
||||
type={inputTypes.TEXT}
|
||||
errors={labelErrors}
|
||||
onChange={handleLabelChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.label}>{translate('Filters')}</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{filters.map((filter, index) => {
|
||||
return (
|
||||
<FilterBuilderRow
|
||||
key={`${filter.key}-${index}`}
|
||||
index={index}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
filterKey={filter.key}
|
||||
filterValue={filter.value}
|
||||
filterType={filter.type}
|
||||
filterCount={filters.length}
|
||||
onAddPress={handleAddFilterPress}
|
||||
onRemovePress={handleRemoveFilterPress}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onCancelPress}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={handleSaveFilterPress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterBuilderModalContent;
|
|
@ -1,42 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteCustomFilter, saveCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { customFilters }) => customFilters,
|
||||
(state, { id }) => id,
|
||||
(state) => state.customFilters.isSaving,
|
||||
(state) => state.customFilters.saveError,
|
||||
(customFilters, id, isSaving, saveError) => {
|
||||
if (id) {
|
||||
const customFilter = customFilters.find((c) => c.id === id);
|
||||
|
||||
return {
|
||||
id: customFilter.id,
|
||||
label: customFilter.label,
|
||||
filters: customFilter.filters,
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: '',
|
||||
filters: [],
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onSaveCustomFilterPress: saveCustomFilter,
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent);
|
|
@ -1,309 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
|
||||
import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue';
|
||||
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
|
||||
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
||||
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
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 getDefaultFilterValue(selectedFilterBuilderProp) {
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.DATE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
if (!selectedFilterBuilderProp) {
|
||||
return FilterBuilderRowValueConnector;
|
||||
}
|
||||
|
||||
const valueType = selectedFilterBuilderProp.valueType;
|
||||
|
||||
switch (valueType) {
|
||||
case filterBuilderValueTypes.BOOL:
|
||||
return BoolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||
return HistoryEventTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.LANGUAGE:
|
||||
return LanguageFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.PROTOCOL:
|
||||
return ProtocolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY:
|
||||
return QualityFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.QUEUE_STATUS:
|
||||
return QueueStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SEASONS_MONITORED_STATUS:
|
||||
return SeasonsMonitoredStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SERIES:
|
||||
return SeriesFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SERIES_STATUS:
|
||||
return SeriesStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SERIES_TYPES:
|
||||
return SeriesTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.TAG:
|
||||
return TagFilterBuilderRowValueConnector;
|
||||
|
||||
default:
|
||||
return FilterBuilderRowValueConnector;
|
||||
}
|
||||
}
|
||||
|
||||
class FilterBuilderRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
filterKey,
|
||||
filterBuilderProps
|
||||
} = props;
|
||||
|
||||
if (filterKey) {
|
||||
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
index,
|
||||
filterKey,
|
||||
filterBuilderProps,
|
||||
onFilterChange
|
||||
} = this.props;
|
||||
|
||||
if (filterKey) {
|
||||
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFilterBuilderProp = filterBuilderProps[0];
|
||||
|
||||
const filter = {
|
||||
key: selectedFilterBuilderProp.name,
|
||||
value: getDefaultFilterValue(selectedFilterBuilderProp),
|
||||
type: getDefaultFilterType(selectedFilterBuilderProp)
|
||||
};
|
||||
|
||||
this.selectedFilterBuilderProp = 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: getDefaultFilterValue(selectedFilterBuilderProp),
|
||||
type
|
||||
};
|
||||
|
||||
this.selectedFilterBuilderProp = 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,
|
||||
sectionItems
|
||||
} = this.props;
|
||||
|
||||
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
|
||||
|
||||
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
||||
const { name, label } = availablePropFilter;
|
||||
|
||||
return {
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
}).sort(sortByProp('value'));
|
||||
|
||||
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
|
||||
filterType={filterType}
|
||||
filterValue={filterValue}
|
||||
selectedFilterBuilderProp={selectedFilterBuilderProp}
|
||||
sectionItems={sectionItems}
|
||||
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, PropTypes.object]),
|
||||
filterType: PropTypes.string,
|
||||
filterCount: PropTypes.number.isRequired,
|
||||
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
onAddPress: PropTypes.func.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default FilterBuilderRow;
|
314
frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx
Normal file
314
frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx
Normal file
|
@ -0,0 +1,314 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FilterBuilderProp, PropertyFilter } from 'App/State/AppState';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import {
|
||||
FilterBuilderTypes,
|
||||
possibleFilterTypes,
|
||||
} from 'Helpers/Props/filterBuilderTypes';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue, {
|
||||
DateFilterValue,
|
||||
} from './DateFilterBuilderRowValue';
|
||||
import DefaultFilterBuilderRowValue from './DefaultFilterBuilderRowValue';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import IndexerFilterBuilderRowValue from './IndexerFilterBuilderRowValue';
|
||||
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValue from './QualityFilterBuilderRowValue';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
|
||||
import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue';
|
||||
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
|
||||
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
||||
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValue from './TagFilterBuilderRowValue';
|
||||
import styles from './FilterBuilderRow.css';
|
||||
|
||||
function getselectedFilterBuilderProp<T>(
|
||||
filterBuilderProps: FilterBuilderProp<T>[],
|
||||
name: string
|
||||
) {
|
||||
return filterBuilderProps.find((a) => {
|
||||
return a.name === name;
|
||||
});
|
||||
}
|
||||
|
||||
function getFilterTypeOptions<T>(
|
||||
filterBuilderProps: FilterBuilderProp<T>[],
|
||||
filterKey: string | undefined
|
||||
) {
|
||||
if (!filterKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedFilterBuilderProp = getselectedFilterBuilderProp(
|
||||
filterBuilderProps,
|
||||
filterKey
|
||||
);
|
||||
|
||||
if (!selectedFilterBuilderProp) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return possibleFilterTypes[selectedFilterBuilderProp.type];
|
||||
}
|
||||
|
||||
function getDefaultFilterType<T>(
|
||||
selectedFilterBuilderProp: FilterBuilderProp<T>
|
||||
) {
|
||||
return possibleFilterTypes[selectedFilterBuilderProp.type][0].key;
|
||||
}
|
||||
|
||||
function getDefaultFilterValue<T>(
|
||||
selectedFilterBuilderProp: FilterBuilderProp<T>
|
||||
) {
|
||||
if (selectedFilterBuilderProp.type === 'date') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getRowValueConnector<T>(
|
||||
selectedFilterBuilderProp: FilterBuilderProp<T> | undefined
|
||||
) {
|
||||
if (!selectedFilterBuilderProp) {
|
||||
return DefaultFilterBuilderRowValue;
|
||||
}
|
||||
|
||||
const valueType = selectedFilterBuilderProp.valueType;
|
||||
|
||||
switch (valueType) {
|
||||
case filterBuilderValueTypes.BOOL:
|
||||
return BoolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||
return HistoryEventTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.LANGUAGE:
|
||||
return LanguageFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.PROTOCOL:
|
||||
return ProtocolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY:
|
||||
return QualityFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.QUEUE_STATUS:
|
||||
return QueueStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SEASONS_MONITORED_STATUS:
|
||||
return SeasonsMonitoredStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SERIES:
|
||||
return SeriesFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SERIES_STATUS:
|
||||
return SeriesStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SERIES_TYPES:
|
||||
return SeriesTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.TAG:
|
||||
return TagFilterBuilderRowValue;
|
||||
|
||||
default:
|
||||
return DefaultFilterBuilderRowValue;
|
||||
}
|
||||
}
|
||||
|
||||
interface FilterBuilderRowProps<T> {
|
||||
index: number;
|
||||
filterKey?: string;
|
||||
filterValue?: (DateFilterValue | string) | string[] | number[] | boolean[];
|
||||
filterType?: string;
|
||||
filterCount: number;
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
sectionItems: T[];
|
||||
onAddPress: () => void;
|
||||
onFilterChange: (index: number, filter: PropertyFilter) => void;
|
||||
onRemovePress: (index: number) => void;
|
||||
}
|
||||
|
||||
function FilterBuilderRow<T>({
|
||||
index,
|
||||
filterKey,
|
||||
filterType,
|
||||
filterValue,
|
||||
filterCount,
|
||||
filterBuilderProps,
|
||||
sectionItems,
|
||||
onAddPress,
|
||||
onFilterChange,
|
||||
onRemovePress,
|
||||
}: FilterBuilderRowProps<T>) {
|
||||
const [selectedFilterBuilderProp, setSelectedFilterBuilderProp] = useState<
|
||||
FilterBuilderProp<T> | undefined
|
||||
>(undefined);
|
||||
|
||||
const keyOptions = filterBuilderProps
|
||||
.map((availablePropFilter) => {
|
||||
const { name, label } = availablePropFilter;
|
||||
|
||||
return {
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label,
|
||||
};
|
||||
})
|
||||
.sort(sortByProp('value'));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
const handleFilterKeyChange = useCallback(
|
||||
({ value: key }: InputChanged<FilterBuilderTypes>) => {
|
||||
const selected = getselectedFilterBuilderProp(filterBuilderProps, key);
|
||||
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = getDefaultFilterType(selected);
|
||||
|
||||
const filter = {
|
||||
key,
|
||||
value: getDefaultFilterValue(selected),
|
||||
type,
|
||||
};
|
||||
|
||||
setSelectedFilterBuilderProp(selected);
|
||||
onFilterChange(index, filter);
|
||||
},
|
||||
[filterBuilderProps, index, onFilterChange]
|
||||
);
|
||||
|
||||
const handleFilterTypeChange = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
if (filterKey == null || filterValue == null || filterType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filter: PropertyFilter = {
|
||||
key: filterKey,
|
||||
value: filterValue,
|
||||
type: value,
|
||||
};
|
||||
|
||||
onFilterChange(index, filter);
|
||||
},
|
||||
[index, filterKey, filterValue, filterType, onFilterChange]
|
||||
);
|
||||
|
||||
const handleFilterValueChange = useCallback(
|
||||
({
|
||||
value,
|
||||
}: InputChanged<
|
||||
string | boolean[] | string[] | number[] | DateFilterValue
|
||||
>) => {
|
||||
if (filterKey == null || filterValue == null || filterType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filter: PropertyFilter = {
|
||||
key: filterKey,
|
||||
value,
|
||||
type: filterType,
|
||||
};
|
||||
|
||||
onFilterChange(index, filter);
|
||||
},
|
||||
[index, filterKey, filterValue, filterType, onFilterChange]
|
||||
);
|
||||
|
||||
const handleAddPress = useCallback(() => {
|
||||
onAddPress();
|
||||
}, [onAddPress]);
|
||||
|
||||
const handleRemovePress = useCallback(() => {
|
||||
onRemovePress(index);
|
||||
}, [index, onRemovePress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filterKey) {
|
||||
setSelectedFilterBuilderProp(
|
||||
filterBuilderProps.find((a) => a.name === filterKey)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = filterBuilderProps[0];
|
||||
|
||||
const filter = {
|
||||
key: selected.name,
|
||||
value: getDefaultFilterValue(selected),
|
||||
type: getDefaultFilterType(selected),
|
||||
};
|
||||
|
||||
setSelectedFilterBuilderProp(selected);
|
||||
onFilterChange(index, filter);
|
||||
}, [index, filterKey, filterBuilderProps, onFilterChange]);
|
||||
|
||||
return (
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.inputContainer}>
|
||||
{filterKey ? (
|
||||
<SelectInput
|
||||
name="key"
|
||||
value={filterKey}
|
||||
values={keyOptions}
|
||||
onChange={handleFilterKeyChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
{filterType ? (
|
||||
<SelectInput
|
||||
name="type"
|
||||
value={filterType}
|
||||
values={getFilterTypeOptions(filterBuilderProps, filterKey)}
|
||||
onChange={handleFilterTypeChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.valueInputContainer}>
|
||||
{filterValue != null && !!selectedFilterBuilderProp ? (
|
||||
<ValueComponent
|
||||
filterType={filterType}
|
||||
// @ts-expect-error - inferring the correct value type is hard
|
||||
filterValue={filterValue}
|
||||
selectedFilterBuilderProp={selectedFilterBuilderProp}
|
||||
sectionItems={sectionItems}
|
||||
onChange={handleFilterValueChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.actionsContainer}>
|
||||
<IconButton
|
||||
name={icons.SUBTRACT}
|
||||
isDisabled={filterCount === 1}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
|
||||
<IconButton name={icons.ADD} onPress={handleAddPress} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterBuilderRow;
|
|
@ -1,160 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TagInput from 'Components/Form/Tag/TagInput';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, kinds } from 'Helpers/Props';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import convertToBytes from 'Utilities/Number/convertToBytes';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
||||
|
||||
export const NAME = 'value';
|
||||
|
||||
function getTagDisplayValue(value, selectedFilterBuilderProp) {
|
||||
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
||||
return formatBytes(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
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);
|
||||
case 'm':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'g':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 't':
|
||||
return convertToBytes(value, 4, true);
|
||||
case 'kb':
|
||||
return convertToBytes(value, 1, true);
|
||||
case 'mb':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'gb':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 'tb':
|
||||
return convertToBytes(value, 4, true);
|
||||
case 'kib':
|
||||
return convertToBytes(value, 1, true);
|
||||
case 'mib':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'gib':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 'tib':
|
||||
return convertToBytes(value, 4, true);
|
||||
default:
|
||||
return parseInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||
return parseInt(input);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
class FilterBuilderRowValue extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTagAdd = (tag) => {
|
||||
const {
|
||||
filterValue,
|
||||
selectedFilterBuilderProp,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
let value = tag.id;
|
||||
|
||||
if (value == null) {
|
||||
value = getValue(tag.name, selectedFilterBuilderProp);
|
||||
}
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: [...filterValue, value]
|
||||
});
|
||||
};
|
||||
|
||||
onTagDelete = ({ index }) => {
|
||||
const {
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const value = filterValue.filter((v, i) => i !== index);
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
filterValue,
|
||||
selectedFilterBuilderProp,
|
||||
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: getTagDisplayValue(id, selectedFilterBuilderProp)
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<TagInput
|
||||
name={NAME}
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
allowNew={!tagList.length}
|
||||
kind={kinds.DEFAULT}
|
||||
delimiters={['Tab', 'Enter']}
|
||||
maxSuggestionsLength={100}
|
||||
minQueryLength={0}
|
||||
tagComponent={FilterBuilderRowValueTag}
|
||||
onTagAdd={this.onTagAdd}
|
||||
onTagDelete={this.onTagDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterBuilderRowValue.propTypes = {
|
||||
filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number])).isRequired,
|
||||
selectedFilterBuilderProp: PropTypes.object.isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
FilterBuilderRowValue.defaultProps = {
|
||||
filterValue: []
|
||||
};
|
||||
|
||||
export default FilterBuilderRowValue;
|
180
frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx
Normal file
180
frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx
Normal file
|
@ -0,0 +1,180 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { FilterBuilderProp } from 'App/State/AppState';
|
||||
import TagInput, { TagBase } from 'Components/Form/Tag/TagInput';
|
||||
import {
|
||||
filterBuilderTypes,
|
||||
filterBuilderValueTypes,
|
||||
kinds,
|
||||
} from 'Helpers/Props';
|
||||
import { InputOnChange } from 'typings/inputs';
|
||||
import convertToBytes from 'Utilities/Number/convertToBytes';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
||||
|
||||
export const NAME = 'value';
|
||||
|
||||
function getTagDisplayValue<T>(
|
||||
value: string | number | boolean,
|
||||
selectedFilterBuilderProp: FilterBuilderProp<T>
|
||||
) {
|
||||
if (
|
||||
(typeof value === 'string' || typeof value === 'number') &&
|
||||
selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES
|
||||
) {
|
||||
return formatBytes(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getValue<T>(
|
||||
input: string | number | boolean,
|
||||
selectedFilterBuilderProp: FilterBuilderProp<T>
|
||||
) {
|
||||
if (typeof input !== 'string') {
|
||||
return input;
|
||||
}
|
||||
|
||||
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);
|
||||
case 'm':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'g':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 't':
|
||||
return convertToBytes(value, 4, true);
|
||||
case 'kb':
|
||||
return convertToBytes(value, 1, true);
|
||||
case 'mb':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'gb':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 'tb':
|
||||
return convertToBytes(value, 4, true);
|
||||
case 'kib':
|
||||
return convertToBytes(value, 1, true);
|
||||
case 'mib':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'gib':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 'tib':
|
||||
return convertToBytes(value, 4, true);
|
||||
default:
|
||||
return parseInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||
return parseInt(input as string);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
interface FreeFormValue {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FilterBuilderTag<V extends string | number | boolean>
|
||||
extends TagBase {
|
||||
id: V;
|
||||
name: string | number;
|
||||
}
|
||||
|
||||
export interface FilterBuilderRowValueProps<
|
||||
T,
|
||||
V extends string | number | boolean
|
||||
> {
|
||||
filterType?: string;
|
||||
filterValue: V[];
|
||||
sectionItems: T[];
|
||||
selectedFilterBuilderProp: FilterBuilderProp<T>;
|
||||
tagList: FilterBuilderTag<V>[];
|
||||
onChange: InputOnChange<V[]>;
|
||||
}
|
||||
|
||||
function FilterBuilderRowValue<T, V extends string | number | boolean>({
|
||||
filterValue = [],
|
||||
selectedFilterBuilderProp,
|
||||
tagList,
|
||||
onChange,
|
||||
}: FilterBuilderRowValueProps<T, V>) {
|
||||
const hasItems = !!tagList.length;
|
||||
|
||||
const tags = useMemo(() => {
|
||||
return filterValue.map((id) => {
|
||||
if (hasItems) {
|
||||
const tag = tagList.find((t) => t.id === id);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: tag ? tag.name : '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: getTagDisplayValue(id, selectedFilterBuilderProp),
|
||||
};
|
||||
});
|
||||
}, [filterValue, tagList, selectedFilterBuilderProp, hasItems]);
|
||||
|
||||
const handleTagAdd = useCallback(
|
||||
(tag: FilterBuilderTag<V> | FreeFormValue) => {
|
||||
if ('id' in tag) {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: [...filterValue, tag.id],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Cast to V to avoid TS error combining V and value of V
|
||||
const value = getValue(tag.name, selectedFilterBuilderProp) as V;
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: [...filterValue, value],
|
||||
});
|
||||
},
|
||||
[filterValue, selectedFilterBuilderProp, onChange]
|
||||
);
|
||||
|
||||
const handleTagDelete = useCallback(
|
||||
({ index }: { index: number }) => {
|
||||
const value = filterValue.filter((_v, i) => i !== index);
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value,
|
||||
});
|
||||
},
|
||||
[filterValue, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<TagInput
|
||||
name={NAME}
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
allowNew={!tagList.length}
|
||||
kind={kinds.DEFAULT}
|
||||
delimiters={['Tab', 'Enter']}
|
||||
minQueryLength={0}
|
||||
tagComponent={FilterBuilderRowValueTag}
|
||||
onTagAdd={handleTagAdd}
|
||||
onTagDelete={handleTagDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValue;
|
|
@ -1,60 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
return createSelector(
|
||||
(state, { filterType }) => filterType,
|
||||
(state, { sectionItems }) => sectionItems,
|
||||
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
|
||||
(filterType, sectionItems, selectedFilterBuilderProp) => {
|
||||
if (
|
||||
(selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
|
||||
filterType !== filterTypes.EQUAL &&
|
||||
filterType !== filterTypes.NOT_EQUAL ||
|
||||
!selectedFilterBuilderProp.optionsSelector
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
if (selectedFilterBuilderProp.optionsSelector) {
|
||||
items = selectedFilterBuilderProp.optionsSelector(sectionItems);
|
||||
} else {
|
||||
items = sectionItems.reduce((acc, item) => {
|
||||
const name = item[selectedFilterBuilderProp.name];
|
||||
|
||||
if (name) {
|
||||
acc.push({
|
||||
id: name,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(sortByProp('name'));
|
||||
}
|
||||
|
||||
return _.uniqBy(items, 'id');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createTagListSelector(),
|
||||
(tagList) => {
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -1,16 +0,0 @@
|
|||
import { FilterBuilderProp } from 'App/State/AppState';
|
||||
|
||||
interface FilterBuilderRowOnChangeProps {
|
||||
name: string;
|
||||
value: unknown[];
|
||||
}
|
||||
|
||||
interface FilterBuilderRowValueProps {
|
||||
filterType?: string;
|
||||
filterValue: string | number | object | string[] | number[] | object[];
|
||||
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||
sectionItem: unknown[];
|
||||
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValueProps;
|
|
@ -1,33 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TagInputTag from 'Components/Form/Tag/TagInputTag';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './FilterBuilderRowValueTag.css';
|
||||
|
||||
function FilterBuilderRowValueTag(props) {
|
||||
return (
|
||||
<div
|
||||
className={styles.tag}
|
||||
>
|
||||
<TagInputTag
|
||||
kind={kinds.DEFAULT}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{
|
||||
props.isLastTag ?
|
||||
null :
|
||||
<div className={styles.or}>
|
||||
{translate('Or')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FilterBuilderRowValueTag.propTypes = {
|
||||
isLastTag: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default FilterBuilderRowValueTag;
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { TagBase } from 'Components/Form/Tag/TagInput';
|
||||
import TagInputTag, { TagInputTagProps } from 'Components/Form/Tag/TagInputTag';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './FilterBuilderRowValueTag.css';
|
||||
|
||||
interface FilterBuilderRowValueTagProps extends TagInputTagProps<TagBase> {
|
||||
isLastTag: boolean;
|
||||
}
|
||||
|
||||
function FilterBuilderRowValueTag({
|
||||
isLastTag,
|
||||
...otherProps
|
||||
}: FilterBuilderRowValueTagProps) {
|
||||
return (
|
||||
<div className={styles.tag}>
|
||||
<TagInputTag {...otherProps} kind={kinds.DEFAULT} />
|
||||
|
||||
{isLastTag ? null : <div className={styles.or}>{translate('Or')}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValueTag;
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{
|
||||
|
@ -42,8 +43,13 @@ const EVENT_TYPE_OPTIONS = [
|
|||
},
|
||||
];
|
||||
|
||||
function HistoryEventTypeFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
type QualityProfileFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, number>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function HistoryEventTypeFilterBuilderRowValue<T>(
|
||||
props: QualityProfileFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
type IndexerFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, number>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function IndexerFilterBuilderRowValue<T>(
|
||||
props: IndexerFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isPopulated, items } = useSelector(
|
||||
(state: AppState) => state.settings.indexers
|
||||
);
|
||||
|
||||
const tagList = useMemo(() => {
|
||||
return items.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPopulated) {
|
||||
dispatch(fetchIndexers());
|
||||
}
|
||||
}, [isPopulated, dispatch]);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default IndexerFilterBuilderRowValue;
|
|
@ -1,79 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
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);
|
|
@ -1,10 +1,18 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
type LanguageFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, number>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function LanguageFilterBuilderRowValue<T>(
|
||||
props: LanguageFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
const { items } = useSelector(createLanguagesSelector());
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={items} />;
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
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,22 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: 'torrent', name: 'Torrent' },
|
||||
{ id: 'usenet', name: 'Usenet' },
|
||||
];
|
||||
|
||||
type ProtocolFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, string>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function ProtocolFilterBuilderRowValue<T>(
|
||||
props: ProtocolFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
return <FilterBuilderRowValue tagList={protocols} {...props} />;
|
||||
}
|
||||
|
||||
export default ProtocolFilterBuilderRowValue;
|
|
@ -0,0 +1,37 @@
|
|||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
type QualityFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, number>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function QualityFilterBuilderRowValue<T>(
|
||||
props: QualityFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isSchemaPopulated, schema } = useSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles
|
||||
);
|
||||
|
||||
const tagList = useMemo(() => {
|
||||
return getQualities(schema.items);
|
||||
}, [schema]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSchemaPopulated) {
|
||||
dispatch(fetchQualityProfileSchema());
|
||||
}
|
||||
}, [isSchemaPopulated, dispatch]);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default QualityFilterBuilderRowValue;
|
|
@ -1,75 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const {
|
||||
isSchemaFetching: 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);
|
|
@ -2,9 +2,10 @@ import React from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
function createQualityProfilesSelector() {
|
||||
return createSelector(
|
||||
|
@ -15,8 +16,13 @@ function createQualityProfilesSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
function QualityProfileFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
type QualityProfileFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, number>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function QualityProfileFilterBuilderRowValue<T>(
|
||||
props: QualityProfileFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
const statusTagList = [
|
||||
{
|
||||
|
@ -60,7 +61,14 @@ const statusTagList = [
|
|||
},
|
||||
];
|
||||
|
||||
function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
type QueueStatusFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, string>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function QueueStatusFilterBuilderRowValue<T>(
|
||||
props: QueueStatusFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
return <FilterBuilderRowValue {...props} tagList={statusTagList} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,40 @@
|
|||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
const seasonsMonitoredStatusList = [
|
||||
{
|
||||
id: 'all',
|
||||
get name() {
|
||||
return translate('SeasonsMonitoredAll');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'partial',
|
||||
get name() {
|
||||
return translate('SeasonsMonitoredPartial');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'none',
|
||||
get name() {
|
||||
return translate('SeasonsMonitoredNone');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function SeasonsMonitoredStatusFilterBuilderRowValue(props) {
|
||||
type SeasonsMonitoredStatusFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, string>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function SeasonsMonitoredStatusFilterBuilderRowValue<T>(
|
||||
props: SeasonsMonitoredStatusFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={seasonsMonitoredStatusList}
|
||||
{...props}
|
||||
/>
|
||||
<FilterBuilderRowValue tagList={seasonsMonitoredStatusList} {...props} />
|
||||
);
|
||||
}
|
||||
|
|
@ -3,10 +3,18 @@ import { useSelector } from 'react-redux';
|
|||
import Series from 'Series/Series';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
type SeriesFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, number>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function SeriesFilterBuilderRowValue<T>(
|
||||
props: SeriesFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
const allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||
|
||||
const tagList = allSeries
|
||||
|
|
|
@ -1,41 +1,45 @@
|
|||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
const statusTagList = [
|
||||
{
|
||||
id: 'continuing',
|
||||
get name() {
|
||||
return translate('Continuing');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'upcoming',
|
||||
get name() {
|
||||
return translate('Upcoming');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ended',
|
||||
get name() {
|
||||
return translate('Ended');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'deleted',
|
||||
get name() {
|
||||
return translate('Deleted');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function SeriesStatusFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={statusTagList}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
type SeriesStatusFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, string>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function SeriesStatusFilterBuilderRowValue<T>(
|
||||
props: SeriesStatusFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
return <FilterBuilderRowValue tagList={statusTagList} {...props} />;
|
||||
}
|
||||
|
||||
export default SeriesStatusFilterBuilderRowValue;
|
|
@ -1,35 +0,0 @@
|
|||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const seriesTypeList = [
|
||||
{
|
||||
id: 'anime',
|
||||
get name() {
|
||||
return translate('Anime');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'daily',
|
||||
get name() {
|
||||
return translate('Daily');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'standard',
|
||||
get name() {
|
||||
return translate('Standard');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function SeriesTypeFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={seriesTypeList}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesTypeFilterBuilderRowValue;
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
const seriesTypeList = [
|
||||
{
|
||||
id: 'anime',
|
||||
get name() {
|
||||
return translate('Anime');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'daily',
|
||||
get name() {
|
||||
return translate('Daily');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'standard',
|
||||
get name() {
|
||||
return translate('Standard');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type SeriesTypeFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, string>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function SeriesTypeFilterBuilderRowValue<T>(
|
||||
props: SeriesTypeFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
return <FilterBuilderRowValue tagList={seriesTypeList} {...props} />;
|
||||
}
|
||||
|
||||
export default SeriesTypeFilterBuilderRowValue;
|
|
@ -0,0 +1,30 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
||||
type TagFilterBuilderRowValueProps<T> = Omit<
|
||||
FilterBuilderRowValueProps<T, number>,
|
||||
'tagList'
|
||||
>;
|
||||
|
||||
function TagFilterBuilderRowValue<T>(props: TagFilterBuilderRowValueProps<T>) {
|
||||
const tags = useSelector(createTagsSelector());
|
||||
|
||||
const tagList = useMemo(() => {
|
||||
return tags.map((tag) => {
|
||||
const { id, label } = tag;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: label,
|
||||
};
|
||||
});
|
||||
}, [tags]);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default TagFilterBuilderRowValue;
|
|
@ -1,27 +0,0 @@
|
|||
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,115 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './CustomFilter.css';
|
||||
|
||||
class CustomFilter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDeleting: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isDeleting && !isDeleting && this.state.isDeleting && deleteError) {
|
||||
this.setState({ isDeleting: false });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const {
|
||||
id,
|
||||
selectedFilterKey,
|
||||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditPress = () => {
|
||||
const {
|
||||
id,
|
||||
onEditPress
|
||||
} = this.props;
|
||||
|
||||
onEditPress(id);
|
||||
};
|
||||
|
||||
onRemovePress = () => {
|
||||
const {
|
||||
id,
|
||||
dispatchDeleteCustomFilter
|
||||
} = this.props;
|
||||
|
||||
this.setState({ isDeleting: true }, () => {
|
||||
dispatchDeleteCustomFilter({ id });
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
//
|
||||
// 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}
|
||||
/>
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFilter')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={this.state.isDeleting}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFilter.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onEditPress: PropTypes.func.isRequired,
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomFilter;
|
|
@ -0,0 +1,78 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './CustomFilter.css';
|
||||
|
||||
interface CustomFilterProps {
|
||||
id: number;
|
||||
label: string;
|
||||
isDeleting: boolean;
|
||||
deleteError?: Error;
|
||||
dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void;
|
||||
onEditPress: (id: number) => void;
|
||||
}
|
||||
|
||||
function CustomFilter({
|
||||
id,
|
||||
label,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
dispatchSetFilter,
|
||||
onEditPress,
|
||||
}: CustomFilterProps) {
|
||||
const dispatch = useDispatch();
|
||||
const wasDeleting = usePrevious(isDeleting);
|
||||
const [isDeletingInternal, setIsDeletingInternal] = useState(false);
|
||||
|
||||
const handleEditPress = useCallback(() => {
|
||||
onEditPress(id);
|
||||
}, [id, onEditPress]);
|
||||
|
||||
const handleRemovePress = useCallback(() => {
|
||||
setIsDeletingInternal(true);
|
||||
|
||||
dispatch(deleteCustomFilter({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasDeleting && !isDeleting && isDeletingInternal && deleteError) {
|
||||
setIsDeletingInternal(false);
|
||||
}
|
||||
}, [isDeleting, isDeletingInternal, wasDeleting, deleteError]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (isDeletingInternal) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
}
|
||||
};
|
||||
}, [isDeletingInternal, dispatchSetFilter, dispatch]);
|
||||
|
||||
return (
|
||||
<div className={styles.customFilter}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<IconButton name={icons.EDIT} onPress={handleEditPress} />
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFilter')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isDeleting}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomFilter;
|
|
@ -1,84 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFilter from './CustomFilter';
|
||||
import styles from './CustomFiltersModalContent.css';
|
||||
|
||||
function CustomFiltersModalContent(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
customFilters,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
dispatchDeleteCustomFilter,
|
||||
dispatchSetFilter,
|
||||
onAddCustomFilter,
|
||||
onEditCustomFilter,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('CustomFilters')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
<Button onPress={onAddCustomFilter}>
|
||||
{translate('AddCustomFilter')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
CustomFiltersModalContent.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onAddCustomFilter: PropTypes.func.isRequired,
|
||||
onEditCustomFilter: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomFiltersModalContent;
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState, {
|
||||
CustomFilter as CustomFilterModel,
|
||||
} from 'App/State/AppState';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFilter from './CustomFilter';
|
||||
import styles from './CustomFiltersModalContent.css';
|
||||
|
||||
interface CustomFiltersModalContentProps {
|
||||
customFilters: CustomFilterModel[];
|
||||
dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void;
|
||||
onAddCustomFilter: () => void;
|
||||
onEditCustomFilter: (id: number) => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function CustomFiltersModalContent({
|
||||
customFilters,
|
||||
dispatchSetFilter,
|
||||
onAddCustomFilter,
|
||||
onEditCustomFilter,
|
||||
onModalClose,
|
||||
}: CustomFiltersModalContentProps) {
|
||||
const { isDeleting, deleteError } = useSelector(
|
||||
(state: AppState) => state.customFilters
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('CustomFilters')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{customFilters.sort(sortByProp('label')).map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
<Button onPress={onAddCustomFilter}>
|
||||
{translate('AddCustomFilter')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomFiltersModalContent;
|
|
@ -1,23 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import CustomFiltersModalContent from './CustomFiltersModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.isDeleting,
|
||||
(state) => state.customFilters.deleteError,
|
||||
(isDeleting, deleteError) => {
|
||||
return {
|
||||
isDeleting,
|
||||
deleteError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFiltersModalContent);
|
|
@ -1,102 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
|
||||
import CustomFiltersModalContentConnector from './CustomFilters/CustomFiltersModalContentConnector';
|
||||
|
||||
class FilterModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
filterBuilder: !props.customFilters.length,
|
||||
id: null
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddCustomFilter = () => {
|
||||
this.setState({
|
||||
filterBuilder: true
|
||||
});
|
||||
};
|
||||
|
||||
onEditCustomFilter = (id) => {
|
||||
this.setState({
|
||||
filterBuilder: true,
|
||||
id
|
||||
});
|
||||
};
|
||||
|
||||
onCancelPress = () => {
|
||||
if (this.state.filterBuilder) {
|
||||
this.setState({
|
||||
filterBuilder: false,
|
||||
id: null
|
||||
});
|
||||
} else {
|
||||
this.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({
|
||||
filterBuilder: false,
|
||||
id: null
|
||||
}, () => {
|
||||
this.props.onModalClose();
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
filterBuilder,
|
||||
id
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
{
|
||||
filterBuilder ?
|
||||
<FilterBuilderModalContentConnector
|
||||
{...otherProps}
|
||||
id={id}
|
||||
onCancelPress={this.onCancelPress}
|
||||
onModalClose={this.onModalClose}
|
||||
/> :
|
||||
<CustomFiltersModalContentConnector
|
||||
{...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;
|
73
frontend/src/Components/Filter/FilterModal.tsx
Normal file
73
frontend/src/Components/Filter/FilterModal.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { CustomFilter, FilterBuilderProp } from 'App/State/AppState';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import FilterBuilderModalContent from './Builder/FilterBuilderModalContent';
|
||||
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
|
||||
|
||||
export interface FilterModalProps<T> {
|
||||
isOpen: boolean;
|
||||
customFilters: CustomFilter[];
|
||||
customFilterType: string;
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
sectionItems: T[];
|
||||
dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function FilterModal<T>({
|
||||
isOpen,
|
||||
customFilters,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: FilterModalProps<T>) {
|
||||
const [id, setId] = useState<null | number>(null);
|
||||
const [filterBuilder, setFilterBuilder] = useState(!customFilters.length);
|
||||
|
||||
const handleAddCustomFilter = useCallback(() => {
|
||||
setFilterBuilder(true);
|
||||
}, []);
|
||||
|
||||
const handleEditCustomFilter = useCallback((id: number) => {
|
||||
setId(id);
|
||||
setFilterBuilder(true);
|
||||
}, []);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
if (filterBuilder) {
|
||||
setId(null);
|
||||
setFilterBuilder(false);
|
||||
} else {
|
||||
onModalClose();
|
||||
}
|
||||
}, [filterBuilder, onModalClose]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setId(null);
|
||||
setFilterBuilder(false);
|
||||
onModalClose();
|
||||
}, [onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
{filterBuilder ? (
|
||||
<FilterBuilderModalContent
|
||||
{...otherProps}
|
||||
id={id}
|
||||
customFilters={customFilters}
|
||||
onCancelPress={handleCancelPress}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
) : (
|
||||
<CustomFiltersModalContent
|
||||
{...otherProps}
|
||||
customFilters={customFilters}
|
||||
onAddCustomFilter={handleAddCustomFilter}
|
||||
onEditCustomFilter={handleEditCustomFilter}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterModal;
|
|
@ -119,7 +119,7 @@ function getComponent(type: InputType) {
|
|||
}
|
||||
|
||||
// TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type
|
||||
interface ValidationMessage {
|
||||
export interface ValidationMessage {
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,11 +25,12 @@ function parseValue(
|
|||
}
|
||||
|
||||
interface NumberInputProps
|
||||
extends Omit<TextInputProps<number | null>, 'value'> {
|
||||
extends Omit<TextInputProps<number | null>, 'value' | 'onChange'> {
|
||||
value?: number | null;
|
||||
min?: number;
|
||||
max?: number;
|
||||
isFloat?: boolean;
|
||||
onChange: (input: InputChanged<number | null>) => void;
|
||||
}
|
||||
|
||||
function NumberInput({
|
||||
|
|
|
@ -75,7 +75,7 @@ class FilterMenu extends Component {
|
|||
</Menu>
|
||||
|
||||
{
|
||||
showCustomFilters &&
|
||||
showCustomFilters ?
|
||||
<FilterModalConnectorComponent
|
||||
{...filterModalConnectorComponentProps}
|
||||
isOpen={this.state.isFilterModalOpen}
|
||||
|
@ -84,7 +84,7 @@ class FilterMenu extends Component {
|
|||
customFilters={customFilters}
|
||||
onFilterSelect={onFilterSelect}
|
||||
onModalClose={this.onFiltersModalClose}
|
||||
/>
|
||||
/> : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
import translate from 'Utilities/String/translate';
|
||||
import * as filterTypes from './filterTypes';
|
||||
|
||||
export const ARRAY = 'array';
|
||||
export const CONTAINS = 'contains';
|
||||
export const DATE = 'date';
|
||||
export const EQUAL = 'equal';
|
||||
export const EXACT = 'exact';
|
||||
export const NUMBER = 'number';
|
||||
export const STRING = 'string';
|
||||
|
||||
export const all = [
|
||||
ARRAY,
|
||||
CONTAINS,
|
||||
DATE,
|
||||
EQUAL,
|
||||
EXACT,
|
||||
NUMBER,
|
||||
STRING
|
||||
];
|
||||
|
||||
export const possibleFilterTypes = {
|
||||
[ARRAY]: [
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain')
|
||||
}
|
||||
],
|
||||
|
||||
[CONTAINS]: [
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
}
|
||||
],
|
||||
|
||||
[DATE]: [
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterIsBefore')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterIsAfter')
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_LAST,
|
||||
value: () => translate('FilterInLast')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_LAST,
|
||||
value: () => translate('FilterNotInLast')
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_NEXT,
|
||||
value: () => translate('FilterInNext')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_NEXT,
|
||||
value: () => translate('FilterNotInNext')
|
||||
}
|
||||
],
|
||||
|
||||
[EQUAL]: [
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs')
|
||||
}
|
||||
],
|
||||
|
||||
[EXACT]: [
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterIsNot')
|
||||
}
|
||||
],
|
||||
|
||||
[NUMBER]: [
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterGreaterThan')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterGreaterThanOrEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterLessThan')
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterLessThanOrEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual')
|
||||
}
|
||||
],
|
||||
|
||||
[STRING]: [
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain')
|
||||
},
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.STARTS_WITH,
|
||||
value: () => translate('FilterStartsWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_STARTS_WITH,
|
||||
value: () => translate('FilterDoesNotStartWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.ENDS_WITH,
|
||||
value: () => translate('FilterEndsWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_ENDS_WITH,
|
||||
value: () => translate('FilterDoesNotEndWith')
|
||||
}
|
||||
]
|
||||
};
|
148
frontend/src/Helpers/Props/filterBuilderTypes.ts
Normal file
148
frontend/src/Helpers/Props/filterBuilderTypes.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
import translate from 'Utilities/String/translate';
|
||||
import * as filterTypes from './filterTypes';
|
||||
|
||||
export const ARRAY = 'array';
|
||||
export const CONTAINS = 'contains';
|
||||
export const DATE = 'date';
|
||||
export const EQUAL = 'equal';
|
||||
export const EXACT = 'exact';
|
||||
export const NUMBER = 'number';
|
||||
export const STRING = 'string';
|
||||
|
||||
export const all = [ARRAY, CONTAINS, DATE, EQUAL, EXACT, NUMBER, STRING];
|
||||
|
||||
export type FilterBuilderTypes =
|
||||
| 'array'
|
||||
| 'contains'
|
||||
| 'date'
|
||||
| 'equal'
|
||||
| 'exact'
|
||||
| 'number'
|
||||
| 'string';
|
||||
|
||||
export const possibleFilterTypes = {
|
||||
array: [
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain'),
|
||||
},
|
||||
],
|
||||
|
||||
contains: [
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains'),
|
||||
},
|
||||
],
|
||||
|
||||
date: [
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterIsBefore'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterIsAfter'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_LAST,
|
||||
value: () => translate('FilterInLast'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_LAST,
|
||||
value: () => translate('FilterNotInLast'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_NEXT,
|
||||
value: () => translate('FilterInNext'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_NEXT,
|
||||
value: () => translate('FilterNotInNext'),
|
||||
},
|
||||
],
|
||||
|
||||
equal: [
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs'),
|
||||
},
|
||||
],
|
||||
|
||||
exact: [
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterIsNot'),
|
||||
},
|
||||
],
|
||||
|
||||
number: [
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterGreaterThan'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterGreaterThanOrEqual'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterLessThan'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterLessThanOrEqual'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual'),
|
||||
},
|
||||
],
|
||||
|
||||
string: [
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.STARTS_WITH,
|
||||
value: () => translate('FilterStartsWith'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_STARTS_WITH,
|
||||
value: () => translate('FilterDoesNotStartWith'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.ENDS_WITH,
|
||||
value: () => translate('FilterEndsWith'),
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_ENDS_WITH,
|
||||
value: () => translate('FilterDoesNotEndWith'),
|
||||
},
|
||||
],
|
||||
};
|
|
@ -61,6 +61,7 @@ export type InputType =
|
|||
| 'autoComplete'
|
||||
| 'captcha'
|
||||
| 'check'
|
||||
| 'date'
|
||||
| 'device'
|
||||
| 'keyValueList'
|
||||
| 'monitorEpisodesSelect'
|
||||
|
|
|
@ -2,12 +2,13 @@ import React, { useCallback } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import InteractiveSearchType from 'InteractiveSearch/InteractiveSearchType';
|
||||
import {
|
||||
setEpisodeReleasesFilter,
|
||||
setSeasonReleasesFilter,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import Release from 'typings/Release';
|
||||
|
||||
function createReleasesSelector() {
|
||||
return createSelector(
|
||||
|
@ -27,8 +28,7 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
interface InteractiveSearchFilterModalProps {
|
||||
isOpen: boolean;
|
||||
interface InteractiveSearchFilterModalProps extends FilterModalProps<Release> {
|
||||
type: InteractiveSearchType;
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,6 @@ export default function InteractiveSearchFilterModal({
|
|||
}: InteractiveSearchFilterModalProps) {
|
||||
const sectionItems = useSelector(createReleasesSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'releases';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -54,11 +53,10 @@ export default function InteractiveSearchFilterModal({
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...otherProps}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
customFilterType="releases"
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,8 @@ import React, { useCallback } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import Series from 'Series/Series';
|
||||
import { setSeriesFilter } from 'Store/Actions/seriesIndexActions';
|
||||
|
||||
function createSeriesSelector() {
|
||||
|
@ -23,16 +24,13 @@ function createFilterBuilderPropsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
interface SeriesIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
type SeriesIndexFilterModalProps = FilterModalProps<Series>;
|
||||
|
||||
export default function SeriesIndexFilterModal(
|
||||
props: SeriesIndexFilterModalProps
|
||||
) {
|
||||
const sectionItems = useSelector(createSeriesSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'series';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -45,11 +43,10 @@ export default function SeriesIndexFilterModal(
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
customFilterType="series"
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
function convertToBytes(input: number, power: number, binaryPrefix: boolean) {
|
||||
function convertToBytes(
|
||||
input: number | string,
|
||||
power: number,
|
||||
binaryPrefix: boolean
|
||||
) {
|
||||
const size = Number(input);
|
||||
|
||||
if (isNaN(size)) {
|
||||
|
|
Loading…
Add table
Reference in a new issue