diff --git a/frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx b/frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx index ea80458f1..20226a714 100644 --- a/frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx +++ b/frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -import FilterModal from 'Components/Filter/FilterModal'; +import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal'; import { setBlocklistFilter } from 'Store/Actions/blocklistActions'; function createBlocklistSelector() { @@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() { ); } -interface BlocklistFilterModalProps { - isOpen: boolean; -} +type BlocklistFilterModalProps = FilterModalProps; export default function BlocklistFilterModal(props: BlocklistFilterModalProps) { const sectionItems = useSelector(createBlocklistSelector()); @@ -43,7 +41,6 @@ export default function BlocklistFilterModal(props: BlocklistFilterModalProps) { return ( ; export default function HistoryFilterModal(props: HistoryFilterModalProps) { const sectionItems = useSelector(createHistorySelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'history'; const dispatch = useDispatch(); const dispatchSetFilter = useCallback( - (payload: unknown) => { + (payload: { selectedFilterKey: string | number }) => { dispatch(setHistoryFilter(payload)); }, [dispatch] @@ -43,11 +40,10 @@ export default function HistoryFilterModal(props: HistoryFilterModalProps) { return ( ); diff --git a/frontend/src/Activity/Queue/QueueFilterModal.tsx b/frontend/src/Activity/Queue/QueueFilterModal.tsx index 3fce6c166..e943a04d8 100644 --- a/frontend/src/Activity/Queue/QueueFilterModal.tsx +++ b/frontend/src/Activity/Queue/QueueFilterModal.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -import FilterModal from 'Components/Filter/FilterModal'; +import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal'; import { setQueueFilter } from 'Store/Actions/queueActions'; function createQueueSelector() { @@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() { ); } -interface QueueFilterModalProps { - isOpen: boolean; -} +type QueueFilterModalProps = FilterModalProps; export default function QueueFilterModal(props: QueueFilterModalProps) { const sectionItems = useSelector(createQueueSelector()); @@ -43,7 +41,6 @@ export default function QueueFilterModal(props: QueueFilterModalProps) { return ( { name: string; - label: string; - type: string; + label: string | (() => string); + type: FilterBuilderTypes; valueType?: string; optionsSelector?: (items: T[]) => FilterBuilderPropOption[]; } export interface PropertyFilter { key: string; - value: boolean | string | number | string[] | number[]; + value: string | string[] | number[] | boolean[] | DateFilterValue; type: string; } export interface Filter { key: string; label: string; + type: string; filters: PropertyFilter[]; } -export interface CustomFilter { - id: number; +export interface CustomFilter extends ModelBase { type: string; label: string; filters: PropertyFilter[]; diff --git a/frontend/src/App/State/CustomFiltersAppState.ts b/frontend/src/App/State/CustomFiltersAppState.ts index 6ac4820c7..abe2bb371 100644 --- a/frontend/src/App/State/CustomFiltersAppState.ts +++ b/frontend/src/App/State/CustomFiltersAppState.ts @@ -1,10 +1,12 @@ import AppSectionState, { AppSectionDeleteState, + AppSectionSaveState, } from 'App/State/AppSectionState'; import { CustomFilter } from './AppState'; interface CustomFiltersAppState extends AppSectionState, - AppSectionDeleteState {} + AppSectionDeleteState, + AppSectionSaveState {} export default CustomFiltersAppState; diff --git a/frontend/src/Calendar/CalendarFilterModal.tsx b/frontend/src/Calendar/CalendarFilterModal.tsx index e26b2928b..24edba2ff 100644 --- a/frontend/src/Calendar/CalendarFilterModal.tsx +++ b/frontend/src/Calendar/CalendarFilterModal.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -import FilterModal from 'Components/Filter/FilterModal'; +import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal'; import { setCalendarFilter } from 'Store/Actions/calendarActions'; function createCalendarSelector() { @@ -23,9 +23,7 @@ function createFilterBuilderPropsSelector() { ); } -interface CalendarFilterModalProps { - isOpen: boolean; -} +type CalendarFilterModalProps = FilterModalProps; export default function CalendarFilterModal(props: CalendarFilterModalProps) { const sectionItems = useSelector(createCalendarSelector()); @@ -43,7 +41,6 @@ export default function CalendarFilterModal(props: CalendarFilterModalProps) { return ( - ); -} - -export default BoolFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/BoolFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/BoolFilterBuilderRowValue.tsx new file mode 100644 index 000000000..dd5f0c9b1 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/BoolFilterBuilderRowValue.tsx @@ -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 = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function BoolFilterBuilderRowValue( + props: BoolFilterBuilderRowValueProps +) { + return ; +} + +export default BoolFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js deleted file mode 100644 index 0193cf44f..000000000 --- a/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js +++ /dev/null @@ -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 ( -
- - - -
- ); - } - - return ( - - ); - } -} - -DateFilterBuilderRowValue.propTypes = { - filterType: PropTypes.string, - filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, - onChange: PropTypes.func.isRequired -}; - -export default DateFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.tsx new file mode 100644 index 000000000..92a059bd0 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.tsx @@ -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 + extends Omit< + FilterBuilderRowValueProps, + 'filterValue' | 'onChange' + > { + filterValue: string | DateFilterValue; + onChange: InputOnChange; +} + +function DateFilterBuilderRowValue({ + filterType, + filterValue, + onChange, +}: DateFilterBuilderRowValueProps) { + const previousFilterType = usePrevious(filterType); + + const handleValueChange = useCallback( + ({ value }: InputChanged) => { + onChange({ + name: NAME, + value: { + time: (filterValue as DateFilterValue).time, + value, + }, + }); + }, + [filterValue, onChange] + ); + + const handleTimeChange = useCallback( + ({ value }: InputChanged) => { + onChange({ + name: NAME, + value: { + time: value, + value: (filterValue as DateFilterValue).value, + }, + }); + }, + [filterValue, onChange] + ); + + const handleDateChange = useCallback( + ({ value }: InputChanged) => { + 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 ( +
+ + + +
+ ); + } + + return ( + + ); +} + +export default DateFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/DefaultFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/DefaultFilterBuilderRowValue.tsx new file mode 100644 index 000000000..b879615cd --- /dev/null +++ b/frontend/src/Components/Filter/Builder/DefaultFilterBuilderRowValue.tsx @@ -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 = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function DefaultFilterBuilderRowValue({ + filterType, + sectionItems, + selectedFilterBuilderProp, + ...otherProps +}: DefaultFilterBuilderRowValueProps) { + 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((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 ( + + ); +} + +export default DefaultFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js deleted file mode 100644 index 0c4a31657..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js +++ /dev/null @@ -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 ( - - - {translate('CustomFilter')} - - - -
-
- {translate('Label')} -
- -
- -
-
- -
- {translate('Filters')} -
- -
- { - filters.map((filter, index) => { - return ( - - ); - }) - } -
-
- - - - - - {translate('Save')} - - -
- ); - } -} - -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; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.tsx new file mode 100644 index 000000000..0ddf13d9c --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.tsx @@ -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 = { + key: undefined, + value: undefined, + type: '', +}; + +interface FilterBuilderModalContentProps { + id: number | null; + customFilterType: string; + sectionItems: T[]; + filterBuilderProps: FilterBuilderProp[]; + customFilters: CustomFilter[]; + dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void; + onCancelPress: () => void; + onModalClose: () => void; +} + +function FilterBuilderModalContent({ + id, + customFilters, + customFilterType, + sectionItems, + filterBuilderProps, + dispatchSetFilter, + onCancelPress, + onModalClose, +}: FilterBuilderModalContentProps) { + 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[]>( + initialFilters.length ? initialFilters : [NEW_FILTER] + ); + const [labelErrors, setLabelErrors] = useState([]); + const wasSaving = usePrevious(isSaving); + + const handleLabelChange = useCallback(({ value }: InputChanged) => { + 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 ( + + {translate('CustomFilter')} + + +
+
{translate('Label')}
+ +
+ +
+
+ +
{translate('Filters')}
+ +
+ {filters.map((filter, index) => { + return ( + + ); + })} +
+
+ + + + + + {translate('Save')} + + +
+ ); +} + +export default FilterBuilderModalContent; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContentConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContentConnector.js deleted file mode 100644 index 17633172b..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContentConnector.js +++ /dev/null @@ -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); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js deleted file mode 100644 index 0b00c0f03..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ /dev/null @@ -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 ( -
-
- { - filterKey && - - } -
- -
- { - filterType && - - } -
- -
- { - filterValue != null && !!selectedFilterBuilderProp && - - } -
- -
- - - -
-
- ); - } -} - -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; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx new file mode 100644 index 000000000..a97e153bf --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx @@ -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( + filterBuilderProps: FilterBuilderProp[], + name: string +) { + return filterBuilderProps.find((a) => { + return a.name === name; + }); +} + +function getFilterTypeOptions( + filterBuilderProps: FilterBuilderProp[], + filterKey: string | undefined +) { + if (!filterKey) { + return []; + } + + const selectedFilterBuilderProp = getselectedFilterBuilderProp( + filterBuilderProps, + filterKey + ); + + if (!selectedFilterBuilderProp) { + return []; + } + + return possibleFilterTypes[selectedFilterBuilderProp.type]; +} + +function getDefaultFilterType( + selectedFilterBuilderProp: FilterBuilderProp +) { + return possibleFilterTypes[selectedFilterBuilderProp.type][0].key; +} + +function getDefaultFilterValue( + selectedFilterBuilderProp: FilterBuilderProp +) { + if (selectedFilterBuilderProp.type === 'date') { + return ''; + } + + return []; +} + +function getRowValueConnector( + selectedFilterBuilderProp: FilterBuilderProp | 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 { + index: number; + filterKey?: string; + filterValue?: (DateFilterValue | string) | string[] | number[] | boolean[]; + filterType?: string; + filterCount: number; + filterBuilderProps: FilterBuilderProp[]; + sectionItems: T[]; + onAddPress: () => void; + onFilterChange: (index: number, filter: PropertyFilter) => void; + onRemovePress: (index: number) => void; +} + +function FilterBuilderRow({ + index, + filterKey, + filterType, + filterValue, + filterCount, + filterBuilderProps, + sectionItems, + onAddPress, + onFilterChange, + onRemovePress, +}: FilterBuilderRowProps) { + const [selectedFilterBuilderProp, setSelectedFilterBuilderProp] = useState< + FilterBuilderProp | 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) => { + 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) => { + 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 ( +
+
+ {filterKey ? ( + + ) : null} +
+ +
+ {filterType ? ( + + ) : null} +
+ +
+ {filterValue != null && !!selectedFilterBuilderProp ? ( + + ) : null} +
+ +
+ + + +
+
+ ); +} + +export default FilterBuilderRow; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js deleted file mode 100644 index 217626c90..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js +++ /dev/null @@ -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 ( - - ); - } -} - -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; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx new file mode 100644 index 000000000..ab76efb83 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx @@ -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( + value: string | number | boolean, + selectedFilterBuilderProp: FilterBuilderProp +) { + if ( + (typeof value === 'string' || typeof value === 'number') && + selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES + ) { + return formatBytes(value); + } + + return String(value); +} + +function getValue( + input: string | number | boolean, + selectedFilterBuilderProp: FilterBuilderProp +) { + 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 + extends TagBase { + id: V; + name: string | number; +} + +export interface FilterBuilderRowValueProps< + T, + V extends string | number | boolean +> { + filterType?: string; + filterValue: V[]; + sectionItems: T[]; + selectedFilterBuilderProp: FilterBuilderProp; + tagList: FilterBuilderTag[]; + onChange: InputOnChange; +} + +function FilterBuilderRowValue({ + filterValue = [], + selectedFilterBuilderProp, + tagList, + onChange, +}: FilterBuilderRowValueProps) { + 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 | 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 ( + + ); +} + +export default FilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js deleted file mode 100644 index d1419327a..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ /dev/null @@ -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); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts deleted file mode 100644 index 5bf9e5785..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts +++ /dev/null @@ -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; - sectionItem: unknown[]; - onChange: (payload: FilterBuilderRowOnChangeProps) => void; -} - -export default FilterBuilderRowValueProps; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js deleted file mode 100644 index 063a97346..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js +++ /dev/null @@ -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 ( -
- - - { - props.isLastTag ? - null : -
- {translate('Or')} -
- } -
- ); -} - -FilterBuilderRowValueTag.propTypes = { - isLastTag: PropTypes.bool.isRequired -}; - -export default FilterBuilderRowValueTag; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.tsx new file mode 100644 index 000000000..870a06583 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.tsx @@ -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 { + isLastTag: boolean; +} + +function FilterBuilderRowValueTag({ + isLastTag, + ...otherProps +}: FilterBuilderRowValueTagProps) { + return ( +
+ + + {isLastTag ? null :
{translate('Or')}
} +
+ ); +} + +export default FilterBuilderRowValueTag; diff --git a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx index 4ecddf646..54f341f45 100644 --- a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx @@ -1,7 +1,8 @@ import React from 'react'; import translate from 'Utilities/String/translate'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; -import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; const EVENT_TYPE_OPTIONS = [ { @@ -42,8 +43,13 @@ const EVENT_TYPE_OPTIONS = [ }, ]; -function HistoryEventTypeFilterBuilderRowValue( - props: FilterBuilderRowValueProps +type QualityProfileFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function HistoryEventTypeFilterBuilderRowValue( + props: QualityProfileFilterBuilderRowValueProps ) { return ; } diff --git a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx new file mode 100644 index 000000000..5ac621703 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx @@ -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 = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function IndexerFilterBuilderRowValue( + props: IndexerFilterBuilderRowValueProps +) { + 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 ; +} + +export default IndexerFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js deleted file mode 100644 index e834b79a3..000000000 --- a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx index e828fd848..5b2547be6 100644 --- a/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx @@ -1,10 +1,18 @@ import React from 'react'; import { useSelector } from 'react-redux'; import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; -import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; -function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) { +type LanguageFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function LanguageFilterBuilderRowValue( + props: LanguageFilterBuilderRowValueProps +) { const { items } = useSelector(createLanguagesSelector()); return ; diff --git a/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.js deleted file mode 100644 index ae63ae0eb..000000000 --- a/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.js +++ /dev/null @@ -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 ( - - ); -} - -export default ProtocolFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.tsx new file mode 100644 index 000000000..f0a37f7d8 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.tsx @@ -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 = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function ProtocolFilterBuilderRowValue( + props: ProtocolFilterBuilderRowValueProps +) { + return ; +} + +export default ProtocolFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValue.tsx new file mode 100644 index 000000000..bb4160956 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValue.tsx @@ -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 = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function QualityFilterBuilderRowValue( + props: QualityFilterBuilderRowValueProps +) { + 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 ; +} + +export default QualityFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js deleted file mode 100644 index fd6e466d4..000000000 --- a/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx index 50036cb90..9b1b159b5 100644 --- a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; import sortByProp from 'Utilities/Array/sortByProp'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; function createQualityProfilesSelector() { return createSelector( @@ -15,8 +16,13 @@ function createQualityProfilesSelector() { ); } -function QualityProfileFilterBuilderRowValue( - props: FilterBuilderRowValueProps +type QualityProfileFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function QualityProfileFilterBuilderRowValue( + props: QualityProfileFilterBuilderRowValueProps ) { const qualityProfiles = useSelector(createQualityProfilesSelector()); diff --git a/frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx index 1127493a5..b78bbbdfb 100644 --- a/frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/QueueStatusFilterBuilderRowValue.tsx @@ -1,7 +1,8 @@ import React from 'react'; import translate from 'Utilities/String/translate'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; -import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; const statusTagList = [ { @@ -60,7 +61,14 @@ const statusTagList = [ }, ]; -function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) { +type QueueStatusFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function QueueStatusFilterBuilderRowValue( + props: QueueStatusFilterBuilderRowValueProps +) { return ; } diff --git a/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.tsx similarity index 51% rename from frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js rename to frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.tsx index b84260e3c..6467663a3 100644 --- a/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.tsx @@ -1,34 +1,40 @@ import React from 'react'; import translate from 'Utilities/String/translate'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; const seasonsMonitoredStatusList = [ { id: 'all', get name() { return translate('SeasonsMonitoredAll'); - } + }, }, { id: 'partial', get name() { return translate('SeasonsMonitoredPartial'); - } + }, }, { id: 'none', get name() { return translate('SeasonsMonitoredNone'); - } - } + }, + }, ]; -function SeasonsMonitoredStatusFilterBuilderRowValue(props) { +type SeasonsMonitoredStatusFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function SeasonsMonitoredStatusFilterBuilderRowValue( + props: SeasonsMonitoredStatusFilterBuilderRowValueProps +) { return ( - + ); } diff --git a/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx index 88b34509a..6a0e60391 100644 --- a/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx @@ -3,10 +3,18 @@ import { useSelector } from 'react-redux'; import Series from 'Series/Series'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import sortByProp from 'Utilities/Array/sortByProp'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; -import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; -function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) { +type SeriesFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function SeriesFilterBuilderRowValue( + props: SeriesFilterBuilderRowValueProps +) { const allSeries: Series[] = useSelector(createAllSeriesSelector()); const tagList = allSeries diff --git a/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.tsx similarity index 53% rename from frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js rename to frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.tsx index e017f72e7..25337287e 100644 --- a/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.tsx @@ -1,41 +1,45 @@ import React from 'react'; import translate from 'Utilities/String/translate'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; const statusTagList = [ { id: 'continuing', get name() { return translate('Continuing'); - } + }, }, { id: 'upcoming', get name() { return translate('Upcoming'); - } + }, }, { id: 'ended', get name() { return translate('Ended'); - } + }, }, { id: 'deleted', get name() { return translate('Deleted'); - } - } + }, + }, ]; -function SeriesStatusFilterBuilderRowValue(props) { - return ( - - ); +type SeriesStatusFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function SeriesStatusFilterBuilderRowValue( + props: SeriesStatusFilterBuilderRowValueProps +) { + return ; } export default SeriesStatusFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.js deleted file mode 100644 index 2e62e558d..000000000 --- a/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.js +++ /dev/null @@ -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 ( - - ); -} - -export default SeriesTypeFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.tsx new file mode 100644 index 000000000..404d560eb --- /dev/null +++ b/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.tsx @@ -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 = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function SeriesTypeFilterBuilderRowValue( + props: SeriesTypeFilterBuilderRowValueProps +) { + return ; +} + +export default SeriesTypeFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValue.tsx new file mode 100644 index 000000000..e5716af14 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValue.tsx @@ -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 = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function TagFilterBuilderRowValue(props: TagFilterBuilderRowValueProps) { + const tags = useSelector(createTagsSelector()); + + const tagList = useMemo(() => { + return tags.map((tag) => { + const { id, label } = tag; + + return { + id, + name: label, + }; + }); + }, [tags]); + + return ; +} + +export default TagFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValueConnector.js deleted file mode 100644 index 60e04c446..000000000 --- a/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValueConnector.js +++ /dev/null @@ -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); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js deleted file mode 100644 index 9f378d5a2..000000000 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js +++ /dev/null @@ -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 ( -
-
- {label} -
- -
- - - -
-
- ); - } -} - -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; diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx b/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx new file mode 100644 index 000000000..ae3644f18 --- /dev/null +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx @@ -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 ( +
+
{label}
+ +
+ + + +
+
+ ); +} + +export default CustomFilter; diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js deleted file mode 100644 index 99cb6ec5c..000000000 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ /dev/null @@ -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 ( - - - {translate('CustomFilters')} - - - - { - customFilters - .sort((a, b) => sortByProp(a, b, 'label')) - .map((customFilter) => { - return ( - - ); - }) - } - -
- -
-
- - - - -
- ); -} - -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; diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.tsx b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.tsx new file mode 100644 index 000000000..29fa256be --- /dev/null +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.tsx @@ -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 ( + + {translate('CustomFilters')} + + + {customFilters.sort(sortByProp('label')).map((customFilter) => { + return ( + + ); + })} + +
+ +
+
+ + + + +
+ ); +} + +export default CustomFiltersModalContent; diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContentConnector.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContentConnector.js deleted file mode 100644 index 32425d766..000000000 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContentConnector.js +++ /dev/null @@ -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); diff --git a/frontend/src/Components/Filter/FilterModal.js b/frontend/src/Components/Filter/FilterModal.js deleted file mode 100644 index d52362d7b..000000000 --- a/frontend/src/Components/Filter/FilterModal.js +++ /dev/null @@ -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 ( - - { - filterBuilder ? - : - - } - - ); - } -} - -FilterModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default FilterModal; diff --git a/frontend/src/Components/Filter/FilterModal.tsx b/frontend/src/Components/Filter/FilterModal.tsx new file mode 100644 index 000000000..4a8224698 --- /dev/null +++ b/frontend/src/Components/Filter/FilterModal.tsx @@ -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 { + isOpen: boolean; + customFilters: CustomFilter[]; + customFilterType: string; + filterBuilderProps: FilterBuilderProp[]; + sectionItems: T[]; + dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void; + onModalClose: () => void; +} + +function FilterModal({ + isOpen, + customFilters, + onModalClose, + ...otherProps +}: FilterModalProps) { + const [id, setId] = useState(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 ( + + {filterBuilder ? ( + + ) : ( + + )} + + ); +} + +export default FilterModal; diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index 98c6e586a..14e9a5909 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -119,7 +119,7 @@ function getComponent(type: InputType) { } // TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type -interface ValidationMessage { +export interface ValidationMessage { message: string; } diff --git a/frontend/src/Components/Form/NumberInput.tsx b/frontend/src/Components/Form/NumberInput.tsx index a5e1fcb64..283a56a3b 100644 --- a/frontend/src/Components/Form/NumberInput.tsx +++ b/frontend/src/Components/Form/NumberInput.tsx @@ -25,11 +25,12 @@ function parseValue( } interface NumberInputProps - extends Omit, 'value'> { + extends Omit, 'value' | 'onChange'> { value?: number | null; min?: number; max?: number; isFloat?: boolean; + onChange: (input: InputChanged) => void; } function NumberInput({ diff --git a/frontend/src/Components/Menu/FilterMenu.js b/frontend/src/Components/Menu/FilterMenu.js index c36c4ae3b..e34a2d71a 100644 --- a/frontend/src/Components/Menu/FilterMenu.js +++ b/frontend/src/Components/Menu/FilterMenu.js @@ -75,7 +75,7 @@ class FilterMenu extends Component { { - showCustomFilters && + showCustomFilters ? + /> : null } ); diff --git a/frontend/src/Helpers/Props/filterBuilderTypes.js b/frontend/src/Helpers/Props/filterBuilderTypes.js deleted file mode 100644 index 9937fbe94..000000000 --- a/frontend/src/Helpers/Props/filterBuilderTypes.js +++ /dev/null @@ -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') - } - ] -}; diff --git a/frontend/src/Helpers/Props/filterBuilderTypes.ts b/frontend/src/Helpers/Props/filterBuilderTypes.ts new file mode 100644 index 000000000..c936464c5 --- /dev/null +++ b/frontend/src/Helpers/Props/filterBuilderTypes.ts @@ -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'), + }, + ], +}; diff --git a/frontend/src/Helpers/Props/inputTypes.ts b/frontend/src/Helpers/Props/inputTypes.ts index a0c4c817c..c707fa695 100644 --- a/frontend/src/Helpers/Props/inputTypes.ts +++ b/frontend/src/Helpers/Props/inputTypes.ts @@ -61,6 +61,7 @@ export type InputType = | 'autoComplete' | 'captcha' | 'check' + | 'date' | 'device' | 'keyValueList' | 'monitorEpisodesSelect' diff --git a/frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx b/frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx index d24615554..1e01e1763 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchFilterModal.tsx @@ -2,12 +2,13 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -import FilterModal from 'Components/Filter/FilterModal'; +import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal'; import InteractiveSearchType from 'InteractiveSearch/InteractiveSearchType'; import { setEpisodeReleasesFilter, setSeasonReleasesFilter, } from 'Store/Actions/releaseActions'; +import Release from 'typings/Release'; function createReleasesSelector() { return createSelector( @@ -27,8 +28,7 @@ function createFilterBuilderPropsSelector() { ); } -interface InteractiveSearchFilterModalProps { - isOpen: boolean; +interface InteractiveSearchFilterModalProps extends FilterModalProps { type: InteractiveSearchType; } @@ -38,7 +38,6 @@ export default function InteractiveSearchFilterModal({ }: InteractiveSearchFilterModalProps) { const sectionItems = useSelector(createReleasesSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'releases'; const dispatch = useDispatch(); @@ -54,11 +53,10 @@ export default function InteractiveSearchFilterModal({ return ( ); diff --git a/frontend/src/Series/Index/SeriesIndexFilterModal.tsx b/frontend/src/Series/Index/SeriesIndexFilterModal.tsx index add54fab6..1ddf21b3a 100644 --- a/frontend/src/Series/Index/SeriesIndexFilterModal.tsx +++ b/frontend/src/Series/Index/SeriesIndexFilterModal.tsx @@ -2,7 +2,8 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -import FilterModal from 'Components/Filter/FilterModal'; +import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal'; +import Series from 'Series/Series'; import { setSeriesFilter } from 'Store/Actions/seriesIndexActions'; function createSeriesSelector() { @@ -23,16 +24,13 @@ function createFilterBuilderPropsSelector() { ); } -interface SeriesIndexFilterModalProps { - isOpen: boolean; -} +type SeriesIndexFilterModalProps = FilterModalProps; export default function SeriesIndexFilterModal( props: SeriesIndexFilterModalProps ) { const sectionItems = useSelector(createSeriesSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'series'; const dispatch = useDispatch(); @@ -45,11 +43,10 @@ export default function SeriesIndexFilterModal( return ( ); diff --git a/frontend/src/Utilities/Number/convertToBytes.ts b/frontend/src/Utilities/Number/convertToBytes.ts index 53dbc27dd..3e60e3cd9 100644 --- a/frontend/src/Utilities/Number/convertToBytes.ts +++ b/frontend/src/Utilities/Number/convertToBytes.ts @@ -1,4 +1,8 @@ -function convertToBytes(input: number, power: number, binaryPrefix: boolean) { +function convertToBytes( + input: number | string, + power: number, + binaryPrefix: boolean +) { const size = Number(input); if (isNaN(size)) {