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 { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
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';
|
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||||
|
|
||||||
function createBlocklistSelector() {
|
function createBlocklistSelector() {
|
||||||
|
@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlocklistFilterModalProps {
|
type BlocklistFilterModalProps = FilterModalProps<History>;
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||||
const sectionItems = useSelector(createBlocklistSelector());
|
const sectionItems = useSelector(createBlocklistSelector());
|
||||||
|
@ -43,7 +41,6 @@ export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
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';
|
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||||
|
|
||||||
function createHistorySelector() {
|
function createHistorySelector() {
|
||||||
|
@ -23,19 +23,16 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryFilterModalProps {
|
type HistoryFilterModalProps = FilterModalProps<History>;
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||||
const sectionItems = useSelector(createHistorySelector());
|
const sectionItems = useSelector(createHistorySelector());
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
const customFilterType = 'history';
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const dispatchSetFilter = useCallback(
|
const dispatchSetFilter = useCallback(
|
||||||
(payload: unknown) => {
|
(payload: { selectedFilterKey: string | number }) => {
|
||||||
dispatch(setHistoryFilter(payload));
|
dispatch(setHistoryFilter(payload));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
@ -43,11 +40,10 @@ export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
filterBuilderProps={filterBuilderProps}
|
||||||
customFilterType={customFilterType}
|
customFilterType="history"
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
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';
|
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||||
|
|
||||||
function createQueueSelector() {
|
function createQueueSelector() {
|
||||||
|
@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueFilterModalProps {
|
type QueueFilterModalProps = FilterModalProps<History>;
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||||
const sectionItems = useSelector(createQueueSelector());
|
const sectionItems = useSelector(createQueueSelector());
|
||||||
|
@ -43,7 +41,6 @@ export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
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 { Error } from './AppSectionState';
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
|
@ -21,33 +24,33 @@ import SystemAppState from './SystemAppState';
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
import WantedAppState from './WantedAppState';
|
import WantedAppState from './WantedAppState';
|
||||||
|
|
||||||
interface FilterBuilderPropOption {
|
export interface FilterBuilderPropOption {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterBuilderProp<T> {
|
export interface FilterBuilderProp<T> {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string | (() => string);
|
||||||
type: string;
|
type: FilterBuilderTypes;
|
||||||
valueType?: string;
|
valueType?: string;
|
||||||
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
|
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PropertyFilter {
|
export interface PropertyFilter {
|
||||||
key: string;
|
key: string;
|
||||||
value: boolean | string | number | string[] | number[];
|
value: string | string[] | number[] | boolean[] | DateFilterValue;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
type: string;
|
||||||
filters: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomFilter {
|
export interface CustomFilter extends ModelBase {
|
||||||
id: number;
|
|
||||||
type: string;
|
type: string;
|
||||||
label: string;
|
label: string;
|
||||||
filters: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import { CustomFilter } from './AppState';
|
import { CustomFilter } from './AppState';
|
||||||
|
|
||||||
interface CustomFiltersAppState
|
interface CustomFiltersAppState
|
||||||
extends AppSectionState<CustomFilter>,
|
extends AppSectionState<CustomFilter>,
|
||||||
AppSectionDeleteState {}
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export default CustomFiltersAppState;
|
export default CustomFiltersAppState;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
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';
|
import { setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||||
|
|
||||||
function createCalendarSelector() {
|
function createCalendarSelector() {
|
||||||
|
@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CalendarFilterModalProps {
|
type CalendarFilterModalProps = FilterModalProps<History>;
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||||
const sectionItems = useSelector(createCalendarSelector());
|
const sectionItems = useSelector(createCalendarSelector());
|
||||||
|
@ -43,7 +41,6 @@ export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
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 React from 'react';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue, {
|
||||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
FilterBuilderRowValueProps,
|
||||||
|
} from './FilterBuilderRowValue';
|
||||||
|
|
||||||
const EVENT_TYPE_OPTIONS = [
|
const EVENT_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
|
@ -42,8 +43,13 @@ const EVENT_TYPE_OPTIONS = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function HistoryEventTypeFilterBuilderRowValue(
|
type QualityProfileFilterBuilderRowValueProps<T> = Omit<
|
||||||
props: FilterBuilderRowValueProps
|
FilterBuilderRowValueProps<T, number>,
|
||||||
|
'tagList'
|
||||||
|
>;
|
||||||
|
|
||||||
|
function HistoryEventTypeFilterBuilderRowValue<T>(
|
||||||
|
props: QualityProfileFilterBuilderRowValueProps<T>
|
||||||
) {
|
) {
|
||||||
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
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 React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue, {
|
||||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
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());
|
const { items } = useSelector(createLanguagesSelector());
|
||||||
|
|
||||||
return <FilterBuilderRowValue {...props} tagList={items} />;
|
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 { useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue, {
|
||||||
|
FilterBuilderRowValueProps,
|
||||||
|
} from './FilterBuilderRowValue';
|
||||||
|
|
||||||
function createQualityProfilesSelector() {
|
function createQualityProfilesSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -15,8 +16,13 @@ function createQualityProfilesSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function QualityProfileFilterBuilderRowValue(
|
type QualityProfileFilterBuilderRowValueProps<T> = Omit<
|
||||||
props: FilterBuilderRowValueProps
|
FilterBuilderRowValueProps<T, number>,
|
||||||
|
'tagList'
|
||||||
|
>;
|
||||||
|
|
||||||
|
function QualityProfileFilterBuilderRowValue<T>(
|
||||||
|
props: QualityProfileFilterBuilderRowValueProps<T>
|
||||||
) {
|
) {
|
||||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue, {
|
||||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
FilterBuilderRowValueProps,
|
||||||
|
} from './FilterBuilderRowValue';
|
||||||
|
|
||||||
const statusTagList = [
|
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} />;
|
return <FilterBuilderRowValue {...props} tagList={statusTagList} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,40 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue, {
|
||||||
|
FilterBuilderRowValueProps,
|
||||||
|
} from './FilterBuilderRowValue';
|
||||||
|
|
||||||
const seasonsMonitoredStatusList = [
|
const seasonsMonitoredStatusList = [
|
||||||
{
|
{
|
||||||
id: 'all',
|
id: 'all',
|
||||||
get name() {
|
get name() {
|
||||||
return translate('SeasonsMonitoredAll');
|
return translate('SeasonsMonitoredAll');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'partial',
|
id: 'partial',
|
||||||
get name() {
|
get name() {
|
||||||
return translate('SeasonsMonitoredPartial');
|
return translate('SeasonsMonitoredPartial');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'none',
|
id: 'none',
|
||||||
get name() {
|
get name() {
|
||||||
return translate('SeasonsMonitoredNone');
|
return translate('SeasonsMonitoredNone');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function SeasonsMonitoredStatusFilterBuilderRowValue(props) {
|
type SeasonsMonitoredStatusFilterBuilderRowValueProps<T> = Omit<
|
||||||
|
FilterBuilderRowValueProps<T, string>,
|
||||||
|
'tagList'
|
||||||
|
>;
|
||||||
|
|
||||||
|
function SeasonsMonitoredStatusFilterBuilderRowValue<T>(
|
||||||
|
props: SeasonsMonitoredStatusFilterBuilderRowValueProps<T>
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<FilterBuilderRowValue
|
<FilterBuilderRowValue tagList={seasonsMonitoredStatusList} {...props} />
|
||||||
tagList={seasonsMonitoredStatusList}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,18 @@ import { useSelector } from 'react-redux';
|
||||||
import Series from 'Series/Series';
|
import Series from 'Series/Series';
|
||||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue, {
|
||||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
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 allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||||
|
|
||||||
const tagList = allSeries
|
const tagList = allSeries
|
||||||
|
|
|
@ -1,41 +1,45 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue, {
|
||||||
|
FilterBuilderRowValueProps,
|
||||||
|
} from './FilterBuilderRowValue';
|
||||||
|
|
||||||
const statusTagList = [
|
const statusTagList = [
|
||||||
{
|
{
|
||||||
id: 'continuing',
|
id: 'continuing',
|
||||||
get name() {
|
get name() {
|
||||||
return translate('Continuing');
|
return translate('Continuing');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'upcoming',
|
id: 'upcoming',
|
||||||
get name() {
|
get name() {
|
||||||
return translate('Upcoming');
|
return translate('Upcoming');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ended',
|
id: 'ended',
|
||||||
get name() {
|
get name() {
|
||||||
return translate('Ended');
|
return translate('Ended');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deleted',
|
id: 'deleted',
|
||||||
get name() {
|
get name() {
|
||||||
return translate('Deleted');
|
return translate('Deleted');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function SeriesStatusFilterBuilderRowValue(props) {
|
type SeriesStatusFilterBuilderRowValueProps<T> = Omit<
|
||||||
return (
|
FilterBuilderRowValueProps<T, string>,
|
||||||
<FilterBuilderRowValue
|
'tagList'
|
||||||
tagList={statusTagList}
|
>;
|
||||||
{...props}
|
|
||||||
/>
|
function SeriesStatusFilterBuilderRowValue<T>(
|
||||||
);
|
props: SeriesStatusFilterBuilderRowValueProps<T>
|
||||||
|
) {
|
||||||
|
return <FilterBuilderRowValue tagList={statusTagList} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SeriesStatusFilterBuilderRowValue;
|
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
|
// 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;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,11 +25,12 @@ function parseValue(
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NumberInputProps
|
interface NumberInputProps
|
||||||
extends Omit<TextInputProps<number | null>, 'value'> {
|
extends Omit<TextInputProps<number | null>, 'value' | 'onChange'> {
|
||||||
value?: number | null;
|
value?: number | null;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
isFloat?: boolean;
|
isFloat?: boolean;
|
||||||
|
onChange: (input: InputChanged<number | null>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NumberInput({
|
function NumberInput({
|
||||||
|
|
|
@ -75,7 +75,7 @@ class FilterMenu extends Component {
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{
|
{
|
||||||
showCustomFilters &&
|
showCustomFilters ?
|
||||||
<FilterModalConnectorComponent
|
<FilterModalConnectorComponent
|
||||||
{...filterModalConnectorComponentProps}
|
{...filterModalConnectorComponentProps}
|
||||||
isOpen={this.state.isFilterModalOpen}
|
isOpen={this.state.isFilterModalOpen}
|
||||||
|
@ -84,7 +84,7 @@ class FilterMenu extends Component {
|
||||||
customFilters={customFilters}
|
customFilters={customFilters}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
onModalClose={this.onFiltersModalClose}
|
onModalClose={this.onFiltersModalClose}
|
||||||
/>
|
/> : null
|
||||||
}
|
}
|
||||||
</div>
|
</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'
|
| 'autoComplete'
|
||||||
| 'captcha'
|
| 'captcha'
|
||||||
| 'check'
|
| 'check'
|
||||||
|
| 'date'
|
||||||
| 'device'
|
| 'device'
|
||||||
| 'keyValueList'
|
| 'keyValueList'
|
||||||
| 'monitorEpisodesSelect'
|
| 'monitorEpisodesSelect'
|
||||||
|
|
|
@ -2,12 +2,13 @@ import React, { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
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 InteractiveSearchType from 'InteractiveSearch/InteractiveSearchType';
|
||||||
import {
|
import {
|
||||||
setEpisodeReleasesFilter,
|
setEpisodeReleasesFilter,
|
||||||
setSeasonReleasesFilter,
|
setSeasonReleasesFilter,
|
||||||
} from 'Store/Actions/releaseActions';
|
} from 'Store/Actions/releaseActions';
|
||||||
|
import Release from 'typings/Release';
|
||||||
|
|
||||||
function createReleasesSelector() {
|
function createReleasesSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -27,8 +28,7 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InteractiveSearchFilterModalProps {
|
interface InteractiveSearchFilterModalProps extends FilterModalProps<Release> {
|
||||||
isOpen: boolean;
|
|
||||||
type: InteractiveSearchType;
|
type: InteractiveSearchType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,6 @@ export default function InteractiveSearchFilterModal({
|
||||||
}: InteractiveSearchFilterModalProps) {
|
}: InteractiveSearchFilterModalProps) {
|
||||||
const sectionItems = useSelector(createReleasesSelector());
|
const sectionItems = useSelector(createReleasesSelector());
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
const customFilterType = 'releases';
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
@ -54,11 +53,10 @@ export default function InteractiveSearchFilterModal({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
filterBuilderProps={filterBuilderProps}
|
||||||
customFilterType={customFilterType}
|
customFilterType="releases"
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,8 @@ import React, { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
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';
|
import { setSeriesFilter } from 'Store/Actions/seriesIndexActions';
|
||||||
|
|
||||||
function createSeriesSelector() {
|
function createSeriesSelector() {
|
||||||
|
@ -23,16 +24,13 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SeriesIndexFilterModalProps {
|
type SeriesIndexFilterModalProps = FilterModalProps<Series>;
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SeriesIndexFilterModal(
|
export default function SeriesIndexFilterModal(
|
||||||
props: SeriesIndexFilterModalProps
|
props: SeriesIndexFilterModalProps
|
||||||
) {
|
) {
|
||||||
const sectionItems = useSelector(createSeriesSelector());
|
const sectionItems = useSelector(createSeriesSelector());
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
const customFilterType = 'series';
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
@ -45,11 +43,10 @@ export default function SeriesIndexFilterModal(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
// TODO: Don't spread all the props
|
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
filterBuilderProps={filterBuilderProps}
|
||||||
customFilterType={customFilterType}
|
customFilterType="series"
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
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);
|
const size = Number(input);
|
||||||
|
|
||||||
if (isNaN(size)) {
|
if (isNaN(size)) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue