diff --git a/frontend/src/Activity/Blocklist/Blocklist.tsx b/frontend/src/Activity/Blocklist/Blocklist.tsx index 4163bc9ca..75afecce0 100644 --- a/frontend/src/Activity/Blocklist/Blocklist.tsx +++ b/frontend/src/Activity/Blocklist/Blocklist.tsx @@ -145,7 +145,7 @@ function Blocklist() { }); const handleFilterSelect = useCallback( - (selectedFilterKey: string) => { + (selectedFilterKey: string | number) => { dispatch(setBlocklistFilter({ selectedFilterKey })); }, [dispatch] diff --git a/frontend/src/Activity/History/History.tsx b/frontend/src/Activity/History/History.tsx index 9f00a1ab3..e153ffa1e 100644 --- a/frontend/src/Activity/History/History.tsx +++ b/frontend/src/Activity/History/History.tsx @@ -80,7 +80,7 @@ function History() { }); const handleFilterSelect = useCallback( - (selectedFilterKey: string) => { + (selectedFilterKey: string | number) => { dispatch(setHistoryFilter({ selectedFilterKey })); }, [dispatch] diff --git a/frontend/src/Activity/Queue/Queue.tsx b/frontend/src/Activity/Queue/Queue.tsx index bd063e69a..9271d58fe 100644 --- a/frontend/src/Activity/Queue/Queue.tsx +++ b/frontend/src/Activity/Queue/Queue.tsx @@ -185,7 +185,7 @@ function Queue() { }); const handleFilterSelect = useCallback( - (selectedFilterKey: string) => { + (selectedFilterKey: string | number) => { dispatch(setQueueFilter({ selectedFilterKey })); }, [dispatch] diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index 4e9dbe7a0..771ce2c19 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,7 +1,7 @@ import Column from 'Components/Table/Column'; import { SortDirection } from 'Helpers/Props/sortDirections'; import { ValidationFailure } from 'typings/pending'; -import { FilterBuilderProp, PropertyFilter } from './AppState'; +import { Filter, FilterBuilderProp } from './AppState'; export interface Error { status?: number; @@ -35,7 +35,7 @@ export interface TableAppSectionState { export interface AppSectionFilterState { selectedFilterKey: string; - filters: PropertyFilter[]; + filters: Filter[]; filterBuilderProps: FilterBuilderProp[]; } diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index f55116fb0..e34a745cc 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -45,7 +45,7 @@ export interface PropertyFilter { export interface Filter { key: string; - label: string; + label: string | (() => string); type: string; filters: PropertyFilter[]; } diff --git a/frontend/src/Calendar/CalendarPage.tsx b/frontend/src/Calendar/CalendarPage.tsx index ec2f3ecb1..dcd644b9d 100644 --- a/frontend/src/Calendar/CalendarPage.tsx +++ b/frontend/src/Calendar/CalendarPage.tsx @@ -132,7 +132,7 @@ function CalendarPage() { }, [missingEpisodeIds, dispatch]); const handleFilterSelect = useCallback( - (key: string) => { + (key: string | number) => { dispatch(setCalendarFilter({ selectedFilterKey: key })); }, [dispatch] diff --git a/frontend/src/Calendar/Header/CalendarHeader.tsx b/frontend/src/Calendar/Header/CalendarHeader.tsx index 2faaca25e..94bc8635e 100644 --- a/frontend/src/Calendar/Header/CalendarHeader.tsx +++ b/frontend/src/Calendar/Header/CalendarHeader.tsx @@ -2,7 +2,6 @@ import moment from 'moment'; import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; -import { CalendarView } from 'Calendar/calendarViews'; import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -37,7 +36,7 @@ function CalendarHeader() { const { longDateFormat } = useSelector(createUISettingsSelector()); const handleViewChange = useCallback( - (newView: CalendarView) => { + (newView: string) => { dispatch(setCalendarView({ view: newView })); }, [dispatch] diff --git a/frontend/src/Components/Menu/FilterMenu.js b/frontend/src/Components/Menu/FilterMenu.js deleted file mode 100644 index e34a2d71a..000000000 --- a/frontend/src/Components/Menu/FilterMenu.js +++ /dev/null @@ -1,112 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import FilterMenuContent from './FilterMenuContent'; -import Menu from './Menu'; -import ToolbarMenuButton from './ToolbarMenuButton'; -import styles from './FilterMenu.css'; - -class FilterMenu extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isFilterModalOpen: false - }; - } - - // - // Listeners - - onCustomFiltersPress = () => { - this.setState({ isFilterModalOpen: true }); - }; - - onFiltersModalClose = () => { - this.setState({ isFilterModalOpen: false }); - }; - - // - // Render - - render(props) { - const { - className, - isDisabled, - selectedFilterKey, - filters, - customFilters, - buttonComponent: ButtonComponent, - filterModalConnectorComponent: FilterModalConnectorComponent, - filterModalConnectorComponentProps, - onFilterSelect, - ...otherProps - } = this.props; - - const showCustomFilters = !!FilterModalConnectorComponent; - - return ( -
- - - - - - - - { - showCustomFilters ? - : null - } -
- ); - } -} - -FilterMenu.propTypes = { - className: PropTypes.string, - isDisabled: PropTypes.bool.isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - buttonComponent: PropTypes.elementType.isRequired, - filterModalConnectorComponent: PropTypes.elementType, - filterModalConnectorComponentProps: PropTypes.object, - onFilterSelect: PropTypes.func.isRequired -}; - -FilterMenu.defaultProps = { - className: styles.filterMenu, - isDisabled: false, - buttonComponent: ToolbarMenuButton -}; - -export default FilterMenu; diff --git a/frontend/src/Components/Menu/FilterMenu.tsx b/frontend/src/Components/Menu/FilterMenu.tsx new file mode 100644 index 000000000..cbf4f1d3f --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenu.tsx @@ -0,0 +1,82 @@ +import React, { useCallback, useState } from 'react'; +import { CustomFilter, Filter } from 'App/State/AppState'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import FilterMenuContent from './FilterMenuContent'; +import Menu from './Menu'; +import ToolbarMenuButton from './ToolbarMenuButton'; +import styles from './FilterMenu.css'; + +interface FilterMenuProps { + className?: string; + alignMenu: 'left' | 'right'; + isDisabled?: boolean; + selectedFilterKey: string | number; + filters: Filter[]; + customFilters: CustomFilter[]; + buttonComponent?: React.ElementType; + filterModalConnectorComponent?: React.ElementType; + filterModalConnectorComponentProps?: object; + onFilterSelect: (filter: number | string) => void; +} + +function FilterMenu({ + className = styles.filterMenu, + isDisabled = false, + selectedFilterKey, + filters, + customFilters, + buttonComponent: ButtonComponent = ToolbarMenuButton, + filterModalConnectorComponent: FilterModalConnectorComponent, + filterModalConnectorComponentProps, + onFilterSelect, + ...otherProps +}: FilterMenuProps) { + const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); + + const showCustomFilters = !!FilterModalConnectorComponent; + + const handleCustomFiltersPress = useCallback(() => { + setIsFilterModalOpen(true); + }, []); + + const handleFiltersModalClose = useCallback(() => { + setIsFilterModalOpen(false); + }, []); + + return ( +
+ + + + + + + {showCustomFilters ? ( + + ) : null} +
+ ); +} + +export default FilterMenu; diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js deleted file mode 100644 index 7bc23c066..000000000 --- a/frontend/src/Components/Menu/FilterMenuContent.js +++ /dev/null @@ -1,95 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import FilterMenuItem from './FilterMenuItem'; -import MenuContent from './MenuContent'; -import MenuItem from './MenuItem'; -import MenuItemSeparator from './MenuItemSeparator'; - -class FilterMenuContent extends Component { - - // - // Render - - render() { - const { - selectedFilterKey, - filters, - customFilters, - showCustomFilters, - onFilterSelect, - onCustomFiltersPress, - ...otherProps - } = this.props; - - return ( - - { - filters.map((filter) => { - return ( - - {typeof filter.label === 'function' ? filter.label() : filter.label} - - ); - }) - } - - { - customFilters.length > 0 ? - : - null - } - - { - customFilters - .sort(sortByProp('label')) - .map((filter) => { - return ( - - {filter.label} - - ); - }) - } - - { - showCustomFilters && - - } - - { - showCustomFilters && - - {translate('CustomFilters')} - - } - - ); - } -} - -FilterMenuContent.propTypes = { - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - showCustomFilters: PropTypes.bool.isRequired, - onFilterSelect: PropTypes.func.isRequired, - onCustomFiltersPress: PropTypes.func.isRequired -}; - -FilterMenuContent.defaultProps = { - showCustomFilters: false -}; - -export default FilterMenuContent; diff --git a/frontend/src/Components/Menu/FilterMenuContent.tsx b/frontend/src/Components/Menu/FilterMenuContent.tsx new file mode 100644 index 000000000..2f0783774 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenuContent.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { CustomFilter, Filter } from 'App/State/AppState'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import FilterMenuItem from './FilterMenuItem'; +import MenuContent from './MenuContent'; +import MenuItem from './MenuItem'; +import MenuItemSeparator from './MenuItemSeparator'; + +interface FilterMenuContentProps { + selectedFilterKey: string | number; + filters: Filter[]; + customFilters: CustomFilter[]; + showCustomFilters: boolean; + onFilterSelect: (filter: number | string) => void; + onCustomFiltersPress: () => void; +} + +function FilterMenuContent({ + selectedFilterKey, + filters, + customFilters, + showCustomFilters = false, + onFilterSelect, + onCustomFiltersPress, + ...otherProps +}: FilterMenuContentProps) { + return ( + + {filters.map((filter) => { + return ( + + {typeof filter.label === 'function' ? filter.label() : filter.label} + + ); + })} + + {customFilters.length > 0 ? : null} + + {customFilters.sort(sortByProp('label')).map((filter) => { + return ( + + {filter.label} + + ); + })} + + {showCustomFilters && } + + {showCustomFilters && ( + + {translate('CustomFilters')} + + )} + + ); +} + +export default FilterMenuContent; diff --git a/frontend/src/Components/Menu/FilterMenuItem.js b/frontend/src/Components/Menu/FilterMenuItem.js deleted file mode 100644 index 85ac48c78..000000000 --- a/frontend/src/Components/Menu/FilterMenuItem.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import SelectedMenuItem from './SelectedMenuItem'; - -class FilterMenuItem extends Component { - - // - // Listeners - - onPress = () => { - const { - filterKey, - onPress - } = this.props; - - onPress(filterKey); - }; - - // - // Render - - render() { - const { - filterKey, - selectedFilterKey, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -FilterMenuItem.propTypes = { - filterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - onPress: PropTypes.func.isRequired -}; - -export default FilterMenuItem; diff --git a/frontend/src/Components/Menu/FilterMenuItem.tsx b/frontend/src/Components/Menu/FilterMenuItem.tsx new file mode 100644 index 000000000..0f27b04a5 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenuItem.tsx @@ -0,0 +1,30 @@ +import React, { useCallback } from 'react'; +import SelectedMenuItem, { SelectedMenuItemProps } from './SelectedMenuItem'; + +interface FilterMenuItemProps + extends Omit { + filterKey: string | number; + selectedFilterKey: string | number; + onPress: (filter: number | string) => void; +} + +function FilterMenuItem({ + filterKey, + selectedFilterKey, + onPress, + ...otherProps +}: FilterMenuItemProps) { + const handlePress = useCallback(() => { + onPress(filterKey); + }, [filterKey, onPress]); + + return ( + + ); +} + +export default FilterMenuItem; diff --git a/frontend/src/Components/Menu/Menu.js b/frontend/src/Components/Menu/Menu.js deleted file mode 100644 index 8f0b2996c..000000000 --- a/frontend/src/Components/Menu/Menu.js +++ /dev/null @@ -1,252 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Manager, Popper, Reference } from 'react-popper'; -import Portal from 'Components/Portal'; -import { align } from 'Helpers/Props'; -import getUniqueElementId from 'Utilities/getUniqueElementId'; -import styles from './Menu.css'; - -const sharedPopperOptions = { - modifiers: { - preventOverflow: { - padding: 0 - }, - flip: { - padding: 0 - } - } -}; - -const popperOptions = { - [align.RIGHT]: { - ...sharedPopperOptions, - placement: 'bottom-end' - }, - - [align.LEFT]: { - ...sharedPopperOptions, - placement: 'bottom-start' - } -}; - -class Menu extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._scheduleUpdate = null; - this._menuButtonId = getUniqueElementId(); - this._menuContentId = getUniqueElementId(); - - this.state = { - isMenuOpen: false, - maxHeight: 0 - }; - } - - componentDidMount() { - this.setMaxHeight(); - } - - componentDidUpdate() { - if (this._scheduleUpdate) { - this._scheduleUpdate(); - } - } - - componentWillUnmount() { - this._removeListener(); - } - - // - // Control - - getMaxHeight() { - if (!this.props.enforceMaxHeight) { - return; - } - - const menuButton = document.getElementById(this._menuButtonId); - - if (!menuButton) { - return; - } - - const { bottom } = menuButton.getBoundingClientRect(); - const maxHeight = window.innerHeight - bottom; - - return maxHeight; - } - - setMaxHeight() { - const maxHeight = this.getMaxHeight(); - - if (maxHeight !== this.state.maxHeight) { - this.setState({ - maxHeight - }); - } - } - - _addListener() { - // Listen to resize events on the window and scroll events - // on all elements to ensure the menu is the best size possible. - // Listen for click events on the window to support closing the - // menu on clicks outside. - - window.addEventListener('resize', this.onWindowResize); - window.addEventListener('scroll', this.onWindowScroll, { capture: true }); - window.addEventListener('click', this.onWindowClick); - window.addEventListener('touchstart', this.onTouchStart); - } - - _removeListener() { - window.removeEventListener('resize', this.onWindowResize); - window.removeEventListener('scroll', this.onWindowScroll, { capture: true }); - window.removeEventListener('click', this.onWindowClick); - window.removeEventListener('touchstart', this.onTouchStart); - } - - // - // Listeners - - onWindowClick = (event) => { - const menuButton = document.getElementById(this._menuButtonId); - - if (!menuButton) { - return; - } - - if (!menuButton.contains(event.target) && this.state.isMenuOpen) { - this.setState({ isMenuOpen: false }); - this._removeListener(); - } - }; - - onTouchStart = (event) => { - const menuButton = document.getElementById(this._menuButtonId); - const menuContent = document.getElementById(this._menuContentId); - - if (!menuButton || !menuContent) { - return; - } - - if (event.targetTouches.length !== 1) { - return; - } - - const target = event.targetTouches[0].target; - - if ( - !menuButton.contains(target) && - !menuContent.contains(target) && - this.state.isMenuOpen - ) { - this.setState({ isMenuOpen: false }); - this._removeListener(); - } - }; - - onWindowResize = () => { - this.setMaxHeight(); - }; - - onWindowScroll = (event) => { - if (this.state.isMenuOpen) { - this.setMaxHeight(); - } - }; - - onMenuButtonPress = () => { - const state = { - isMenuOpen: !this.state.isMenuOpen - }; - - if (this.state.isMenuOpen) { - this._removeListener(); - } else { - state.maxHeight = this.getMaxHeight(); - this._addListener(); - } - - this.setState(state); - }; - - // - // Render - - render() { - const { - className, - children, - alignMenu - } = this.props; - - const { - maxHeight, - isMenuOpen - } = this.state; - - const childrenArray = React.Children.toArray(children); - const button = React.cloneElement( - childrenArray[0], - { - onPress: this.onMenuButtonPress - } - ); - - return ( - - - {({ ref }) => ( -
- {button} -
- )} -
- - - - {({ ref, style, scheduleUpdate }) => { - this._scheduleUpdate = scheduleUpdate; - - return React.cloneElement( - childrenArray[1], - { - forwardedRef: ref, - style: { - ...style, - maxHeight - }, - isOpen: isMenuOpen - } - ); - }} - - -
- ); - } -} - -Menu.propTypes = { - className: PropTypes.string, - children: PropTypes.node.isRequired, - alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT]), - enforceMaxHeight: PropTypes.bool.isRequired -}; - -Menu.defaultProps = { - className: styles.menu, - alignMenu: align.LEFT, - enforceMaxHeight: true -}; - -export default Menu; diff --git a/frontend/src/Components/Menu/Menu.tsx b/frontend/src/Components/Menu/Menu.tsx new file mode 100644 index 000000000..ca935a0e6 --- /dev/null +++ b/frontend/src/Components/Menu/Menu.tsx @@ -0,0 +1,205 @@ +import React, { + ReactElement, + useCallback, + useEffect, + useId, + useRef, + useState, +} from 'react'; +import { Manager, Popper, PopperProps, Reference } from 'react-popper'; +import Portal from 'Components/Portal'; +import styles from './Menu.css'; + +const sharedPopperOptions = { + modifiers: { + preventOverflow: { + padding: 0, + }, + flip: { + padding: 0, + }, + }, +}; + +const popperOptions: { + right: Partial; + left: Partial; +} = { + right: { + ...sharedPopperOptions, + placement: 'bottom-end', + }, + + left: { + ...sharedPopperOptions, + placement: 'bottom-start', + }, +}; + +interface MenuProps { + className?: string; + children: React.ReactNode; + alignMenu?: 'left' | 'right'; + enforceMaxHeight?: boolean; +} + +function Menu({ + className = styles.menu, + children, + alignMenu = 'left', + enforceMaxHeight = true, +}: MenuProps) { + const updater = useRef<(() => void) | null>(null); + const menuButtonId = useId(); + const menuContentId = useId(); + const [maxHeight, setMaxHeight] = useState(0); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const updateMaxHeight = useCallback(() => { + const menuButton = document.getElementById(menuButtonId); + + if (!menuButton) { + setMaxHeight(0); + + return; + } + + const { bottom } = menuButton.getBoundingClientRect(); + const height = window.innerHeight - bottom; + + setMaxHeight(height); + }, [menuButtonId]); + + const handleWindowClick = useCallback( + (event: MouseEvent) => { + const menuButton = document.getElementById(menuButtonId); + + if (!menuButton) { + return; + } + + if (!menuButton.contains(event.target as Node)) { + setIsMenuOpen(false); + } + }, + [menuButtonId] + ); + + const handleTouchStart = useCallback( + (event: TouchEvent) => { + const menuButton = document.getElementById(menuButtonId); + const menuContent = document.getElementById(menuContentId); + + if (!menuButton || !menuContent) { + return; + } + + if (event.targetTouches.length !== 1) { + return; + } + + const target = event.targetTouches[0].target; + + if ( + !menuButton.contains(target as Node) && + !menuContent.contains(target as Node) + ) { + setIsMenuOpen(false); + } + }, + [menuButtonId, menuContentId] + ); + + const handleWindowResize = useCallback(() => { + updateMaxHeight(); + }, [updateMaxHeight]); + + const handleWindowScroll = useCallback(() => { + if (isMenuOpen) { + updateMaxHeight(); + } + }, [isMenuOpen, updateMaxHeight]); + + const handleMenuButtonPress = useCallback(() => { + setIsMenuOpen((isOpen) => !isOpen); + }, []); + + const childrenArray = React.Children.toArray(children); + const button = React.cloneElement(childrenArray[0] as ReactElement, { + onPress: handleMenuButtonPress, + }); + + useEffect(() => { + if (enforceMaxHeight) { + updateMaxHeight(); + } + }, [enforceMaxHeight, updateMaxHeight]); + + useEffect(() => { + if (updater.current && isMenuOpen) { + updater.current(); + } + }, [isMenuOpen]); + + useEffect(() => { + // Listen to resize events on the window and scroll events + // on all elements to ensure the menu is the best size possible. + // Listen for click events on the window to support closing the + // menu on clicks outside. + + if (!isMenuOpen) { + return; + } + + window.addEventListener('resize', handleWindowResize); + window.addEventListener('scroll', handleWindowScroll, { capture: true }); + window.addEventListener('click', handleWindowClick); + window.addEventListener('touchstart', handleTouchStart); + + return () => { + window.removeEventListener('resize', handleWindowResize); + window.removeEventListener('scroll', handleWindowScroll, { + capture: true, + }); + window.removeEventListener('click', handleWindowClick); + window.removeEventListener('touchstart', handleTouchStart); + }; + }, [ + isMenuOpen, + handleWindowResize, + handleWindowScroll, + handleWindowClick, + handleTouchStart, + ]); + + return ( + + + {({ ref }) => ( +
+ {button} +
+ )} +
+ + + + {({ ref, style, scheduleUpdate }) => { + updater.current = scheduleUpdate; + + return React.cloneElement(childrenArray[1] as ReactElement, { + forwardedRef: ref, + style: { + ...style, + maxHeight, + }, + isOpen: isMenuOpen, + }); + }} + + +
+ ); +} + +export default Menu; diff --git a/frontend/src/Components/Menu/MenuButton.js b/frontend/src/Components/Menu/MenuButton.js deleted file mode 100644 index fb091eca1..000000000 --- a/frontend/src/Components/Menu/MenuButton.js +++ /dev/null @@ -1,49 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Link from 'Components/Link/Link'; -import styles from './MenuButton.css'; - -class MenuButton extends Component { - - // - // Render - - render() { - const { - className, - children, - isDisabled, - onPress, - ...otherProps - } = this.props; - - return ( - - {children} - - ); - } -} - -MenuButton.propTypes = { - className: PropTypes.string, - children: PropTypes.node.isRequired, - isDisabled: PropTypes.bool.isRequired, - onPress: PropTypes.func -}; - -MenuButton.defaultProps = { - className: styles.menuButton, - isDisabled: false -}; - -export default MenuButton; diff --git a/frontend/src/Components/Menu/MenuButton.tsx b/frontend/src/Components/Menu/MenuButton.tsx new file mode 100644 index 000000000..043b2c915 --- /dev/null +++ b/frontend/src/Components/Menu/MenuButton.tsx @@ -0,0 +1,30 @@ +import classNames from 'classnames'; +import React from 'react'; +import Link from 'Components/Link/Link'; +import styles from './MenuButton.css'; + +export interface MenuButtonProps { + className?: string; + children: React.ReactNode; + isDisabled?: boolean; + onPress?: () => void; +} + +function MenuButton({ + className = styles.menuButton, + children, + isDisabled = false, + ...otherProps +}: MenuButtonProps) { + return ( + + {children} + + ); +} + +export default MenuButton; diff --git a/frontend/src/Components/Menu/MenuContent.js b/frontend/src/Components/Menu/MenuContent.js deleted file mode 100644 index 82d52f379..000000000 --- a/frontend/src/Components/Menu/MenuContent.js +++ /dev/null @@ -1,55 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Scroller from 'Components/Scroller/Scroller'; -import getUniqueElementId from 'Utilities/getUniqueElementId'; -import styles from './MenuContent.css'; - -class MenuContent extends Component { - - // - // Render - - render() { - const { - forwardedRef, - className, - id, - children, - style, - isOpen - } = this.props; - - return ( -
- { - isOpen ? - - {children} - : - null - } -
- ); - } -} - -MenuContent.propTypes = { - forwardedRef: PropTypes.func, - className: PropTypes.string, - id: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, - style: PropTypes.object, - isOpen: PropTypes.bool -}; - -MenuContent.defaultProps = { - className: styles.menuContent, - id: getUniqueElementId() -}; - -export default MenuContent; diff --git a/frontend/src/Components/Menu/MenuContent.tsx b/frontend/src/Components/Menu/MenuContent.tsx new file mode 100644 index 000000000..f13945418 --- /dev/null +++ b/frontend/src/Components/Menu/MenuContent.tsx @@ -0,0 +1,38 @@ +import React, { CSSProperties, LegacyRef, useId } from 'react'; +import Scroller from 'Components/Scroller/Scroller'; +import styles from './MenuContent.css'; + +interface MenuContentProps { + forwardedRef?: LegacyRef | undefined; + className?: string; + id?: string; + children: React.ReactNode; + style?: CSSProperties; + isOpen?: boolean; +} + +function MenuContent({ + forwardedRef, + className = styles.menuContent, + id, + children, + style, + isOpen, +}: MenuContentProps) { + const generatedId = useId(); + + return ( +
+ {isOpen ? ( + {children} + ) : null} +
+ ); +} + +export default MenuContent; diff --git a/frontend/src/Components/Menu/MenuItem.js b/frontend/src/Components/Menu/MenuItem.js deleted file mode 100644 index 15a4dd145..000000000 --- a/frontend/src/Components/Menu/MenuItem.js +++ /dev/null @@ -1,46 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Link from 'Components/Link/Link'; -import styles from './MenuItem.css'; - -class MenuItem extends Component { - - // - // Render - - render() { - const { - className, - children, - isDisabled, - ...otherProps - } = this.props; - - return ( - - {children} - - ); - } -} - -MenuItem.propTypes = { - className: PropTypes.string, - children: PropTypes.node.isRequired, - isDisabled: PropTypes.bool.isRequired -}; - -MenuItem.defaultProps = { - className: styles.menuItem, - isDisabled: false -}; - -export default MenuItem; diff --git a/frontend/src/Components/Menu/MenuItem.tsx b/frontend/src/Components/Menu/MenuItem.tsx new file mode 100644 index 000000000..287b02d9e --- /dev/null +++ b/frontend/src/Components/Menu/MenuItem.tsx @@ -0,0 +1,29 @@ +import classNames from 'classnames'; +import React from 'react'; +import Link, { LinkProps } from 'Components/Link/Link'; +import styles from './MenuItem.css'; + +export interface MenuItemProps extends LinkProps { + className?: string; + children: React.ReactNode; + isDisabled?: boolean; +} + +function MenuItem({ + className = styles.menuItem, + children, + isDisabled = false, + ...otherProps +}: MenuItemProps) { + return ( + + {children} + + ); +} + +export default MenuItem; diff --git a/frontend/src/Components/Menu/MenuItemSeparator.js b/frontend/src/Components/Menu/MenuItemSeparator.tsx similarity index 71% rename from frontend/src/Components/Menu/MenuItemSeparator.js rename to frontend/src/Components/Menu/MenuItemSeparator.tsx index e586670c9..c5b3c8924 100644 --- a/frontend/src/Components/Menu/MenuItemSeparator.js +++ b/frontend/src/Components/Menu/MenuItemSeparator.tsx @@ -2,9 +2,7 @@ import React from 'react'; import styles from './MenuItemSeparator.css'; function MenuItemSeparator() { - return ( -
- ); + return
; } export default MenuItemSeparator; diff --git a/frontend/src/Components/Menu/PageMenuButton.js b/frontend/src/Components/Menu/PageMenuButton.js deleted file mode 100644 index 8c7a86740..000000000 --- a/frontend/src/Components/Menu/PageMenuButton.js +++ /dev/null @@ -1,60 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import MenuButton from 'Components/Menu/MenuButton'; -import { icons } from 'Helpers/Props'; -import styles from './PageMenuButton.css'; - -function PageMenuButton(props) { - const { - iconName, - showIndicator, - text, - ...otherProps - } = props; - - return ( - - - - { - showIndicator ? - - - : - null - } - -
- {text} -
-
- ); -} - -PageMenuButton.propTypes = { - iconName: PropTypes.object.isRequired, - showIndicator: PropTypes.bool.isRequired, - text: PropTypes.string -}; - -PageMenuButton.defaultProps = { - showIndicator: false -}; - -export default PageMenuButton; diff --git a/frontend/src/Components/Menu/PageMenuButton.tsx b/frontend/src/Components/Menu/PageMenuButton.tsx new file mode 100644 index 000000000..2476aa75f --- /dev/null +++ b/frontend/src/Components/Menu/PageMenuButton.tsx @@ -0,0 +1,38 @@ +import { IconName } from '@fortawesome/free-regular-svg-icons'; +import classNames from 'classnames'; +import React from 'react'; +import Icon from 'Components/Icon'; +import MenuButton from 'Components/Menu/MenuButton'; +import { icons } from 'Helpers/Props'; +import styles from './PageMenuButton.css'; + +interface PageMenuButtonProps { + iconName: IconName; + showIndicator: boolean; + text?: string; +} + +function PageMenuButton({ + iconName, + showIndicator = false, + text, + ...otherProps +}: PageMenuButtonProps) { + return ( + + + + {showIndicator ? ( + + + + ) : null} + +
{text}
+
+ ); +} + +export default PageMenuButton; diff --git a/frontend/src/Components/Menu/SelectedMenuItem.js b/frontend/src/Components/Menu/SelectedMenuItem.js deleted file mode 100644 index b80f4f305..000000000 --- a/frontend/src/Components/Menu/SelectedMenuItem.js +++ /dev/null @@ -1,63 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import { icons } from 'Helpers/Props'; -import MenuItem from './MenuItem'; -import styles from './SelectedMenuItem.css'; - -class SelectedMenuItem extends Component { - - // - // Listeners - - onPress = () => { - const { - name, - onPress - } = this.props; - - onPress(name); - }; - - // - // Render - - render() { - const { - children, - selectedIconName, - isSelected, - ...otherProps - } = this.props; - - return ( - -
- {children} - - -
-
- ); - } -} - -SelectedMenuItem.propTypes = { - name: PropTypes.string, - children: PropTypes.node.isRequired, - selectedIconName: PropTypes.object.isRequired, - isSelected: PropTypes.bool.isRequired, - onPress: PropTypes.func.isRequired -}; - -SelectedMenuItem.defaultProps = { - selectedIconName: icons.CHECK -}; - -export default SelectedMenuItem; diff --git a/frontend/src/Components/Menu/SelectedMenuItem.tsx b/frontend/src/Components/Menu/SelectedMenuItem.tsx new file mode 100644 index 000000000..8350af2ad --- /dev/null +++ b/frontend/src/Components/Menu/SelectedMenuItem.tsx @@ -0,0 +1,41 @@ +import React, { useCallback } from 'react'; +import Icon, { IconName } from 'Components/Icon'; +import { icons } from 'Helpers/Props'; +import MenuItem, { MenuItemProps } from './MenuItem'; +import styles from './SelectedMenuItem.css'; + +export interface SelectedMenuItemProps extends Omit { + name?: string; + children: React.ReactNode; + selectedIconName?: IconName; + isSelected: boolean; + onPress: (name: string) => void; +} + +function SelectedMenuItem({ + children, + name, + selectedIconName = icons.CHECK, + isSelected, + onPress, + ...otherProps +}: SelectedMenuItemProps) { + const handlePress = useCallback(() => { + onPress(name ?? ''); + }, [name, onPress]); + + return ( + +
+ {children} + + +
+
+ ); +} + +export default SelectedMenuItem; diff --git a/frontend/src/Components/Menu/SortMenu.js b/frontend/src/Components/Menu/SortMenu.js deleted file mode 100644 index 10a8e162f..000000000 --- a/frontend/src/Components/Menu/SortMenu.js +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Menu from 'Components/Menu/Menu'; -import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; -import { align, icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -function SortMenu(props) { - const { - className, - children, - isDisabled, - ...otherProps - } = props; - - return ( - - - {children} - - ); -} - -SortMenu.propTypes = { - className: PropTypes.string, - children: PropTypes.node.isRequired, - isDisabled: PropTypes.bool.isRequired, - alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT]) -}; - -SortMenu.defaultProps = { - isDisabled: false -}; - -export default SortMenu; diff --git a/frontend/src/Components/Menu/SortMenu.tsx b/frontend/src/Components/Menu/SortMenu.tsx new file mode 100644 index 000000000..7795910ae --- /dev/null +++ b/frontend/src/Components/Menu/SortMenu.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton, { + ToolbarMenuButtonProps, +} from 'Components/Menu/ToolbarMenuButton'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +interface SortMenuProps extends Omit { + className?: string; + children: React.ReactNode; + isDisabled?: boolean; + alignMenu?: 'left' | 'right'; +} + +function SortMenu({ + className, + children, + isDisabled = false, + ...otherProps +}: SortMenuProps) { + return ( + + + {children} + + ); +} + +export default SortMenu; diff --git a/frontend/src/Components/Menu/SortMenuItem.js b/frontend/src/Components/Menu/SortMenuItem.js deleted file mode 100644 index b4ed64dcb..000000000 --- a/frontend/src/Components/Menu/SortMenuItem.js +++ /dev/null @@ -1,38 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { icons, sortDirections } from 'Helpers/Props'; -import SelectedMenuItem from './SelectedMenuItem'; - -function SortMenuItem(props) { - const { - name, - sortKey, - sortDirection, - ...otherProps - } = props; - - const isSelected = name === sortKey; - - return ( - - ); -} - -SortMenuItem.propTypes = { - name: PropTypes.string, - sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), - children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, - onPress: PropTypes.func.isRequired -}; - -SortMenuItem.defaultProps = { - name: null -}; - -export default SortMenuItem; diff --git a/frontend/src/Components/Menu/SortMenuItem.tsx b/frontend/src/Components/Menu/SortMenuItem.tsx new file mode 100644 index 000000000..c9f50d3b7 --- /dev/null +++ b/frontend/src/Components/Menu/SortMenuItem.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { icons } from 'Helpers/Props'; +import { SortDirection } from 'Helpers/Props/sortDirections'; +import SelectedMenuItem from './SelectedMenuItem'; + +interface SortMenuItemProps { + name?: string; + sortKey?: string; + sortDirection?: SortDirection; + children: string | React.ReactNode; + onPress: (sortKey: string) => void; +} + +function SortMenuItem({ + name, + sortKey, + sortDirection, + ...otherProps +}: SortMenuItemProps) { + const isSelected = name === sortKey; + + return ( + + ); +} + +export default SortMenuItem; diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.js b/frontend/src/Components/Menu/ToolbarMenuButton.js deleted file mode 100644 index e193bce3f..000000000 --- a/frontend/src/Components/Menu/ToolbarMenuButton.js +++ /dev/null @@ -1,65 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import MenuButton from 'Components/Menu/MenuButton'; -import { icons } from 'Helpers/Props'; -import styles from './ToolbarMenuButton.css'; - -function ToolbarMenuButton(props) { - const { - iconName, - showIndicator, - text, - ...otherProps - } = props; - - return ( - -
- - - { - showIndicator ? - - - : - null - } - -
-
- {text} -
-
-
-
- ); -} - -ToolbarMenuButton.propTypes = { - className: PropTypes.string, - iconName: PropTypes.object.isRequired, - showIndicator: PropTypes.bool.isRequired, - text: PropTypes.string -}; - -ToolbarMenuButton.defaultProps = { - showIndicator: false -}; - -export default ToolbarMenuButton; diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.tsx b/frontend/src/Components/Menu/ToolbarMenuButton.tsx new file mode 100644 index 000000000..92796cca6 --- /dev/null +++ b/frontend/src/Components/Menu/ToolbarMenuButton.tsx @@ -0,0 +1,43 @@ +import classNames from 'classnames'; +import React from 'react'; +import Icon, { IconName } from 'Components/Icon'; +import MenuButton, { MenuButtonProps } from 'Components/Menu/MenuButton'; +import { icons } from 'Helpers/Props'; +import styles from './ToolbarMenuButton.css'; + +export interface ToolbarMenuButtonProps + extends Omit { + className?: string; + iconName: IconName; + showIndicator?: boolean; + text?: string; +} + +function ToolbarMenuButton({ + iconName, + showIndicator = false, + text, + ...otherProps +}: ToolbarMenuButtonProps) { + return ( + +
+ + + {showIndicator ? ( + + + + ) : null} + +
+
{text}
+
+
+
+ ); +} + +export default ToolbarMenuButton; diff --git a/frontend/src/Components/Menu/ViewMenu.js b/frontend/src/Components/Menu/ViewMenu.js deleted file mode 100644 index a7e56a8c5..000000000 --- a/frontend/src/Components/Menu/ViewMenu.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Menu from 'Components/Menu/Menu'; -import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; -import { align, icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -function ViewMenu(props) { - const { - children, - isDisabled, - ...otherProps - } = props; - - return ( - - - {children} - - ); -} - -ViewMenu.propTypes = { - children: PropTypes.node.isRequired, - isDisabled: PropTypes.bool.isRequired, - alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT]) -}; - -ViewMenu.defaultProps = { - isDisabled: false -}; - -export default ViewMenu; diff --git a/frontend/src/Components/Menu/ViewMenu.tsx b/frontend/src/Components/Menu/ViewMenu.tsx new file mode 100644 index 000000000..d0debb8f9 --- /dev/null +++ b/frontend/src/Components/Menu/ViewMenu.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton, { + ToolbarMenuButtonProps, +} from 'Components/Menu/ToolbarMenuButton'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +interface ViewMenuProps extends Omit { + children: React.ReactNode; + isDisabled?: boolean; + alignMenu?: 'left' | 'right'; +} + +function ViewMenu({ + children, + isDisabled = false, + ...otherProps +}: ViewMenuProps) { + return ( + + + {children} + + ); +} + +export default ViewMenu; diff --git a/frontend/src/Components/Menu/ViewMenuItem.js b/frontend/src/Components/Menu/ViewMenuItem.js deleted file mode 100644 index d2b37d1d4..000000000 --- a/frontend/src/Components/Menu/ViewMenuItem.js +++ /dev/null @@ -1,30 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import SelectedMenuItem from './SelectedMenuItem'; - -function ViewMenuItem(props) { - const { - name, - selectedView, - ...otherProps - } = props; - - const isSelected = name === selectedView; - - return ( - - ); -} - -ViewMenuItem.propTypes = { - name: PropTypes.string, - selectedView: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, - onPress: PropTypes.func.isRequired -}; - -export default ViewMenuItem; diff --git a/frontend/src/Components/Menu/ViewMenuItem.tsx b/frontend/src/Components/Menu/ViewMenuItem.tsx new file mode 100644 index 000000000..7407065bf --- /dev/null +++ b/frontend/src/Components/Menu/ViewMenuItem.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import SelectedMenuItem, { SelectedMenuItemProps } from './SelectedMenuItem'; + +interface ViewMenuItemProps extends Omit { + name?: string; + selectedView: string; + children: React.ReactNode; + onPress: (view: string) => void; +} + +function ViewMenuItem({ + name, + selectedView, + ...otherProps +}: ViewMenuItemProps) { + const isSelected = name === selectedView; + + return ( + + ); +} + +export default ViewMenuItem; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index 990e0dfab..2b37ca34b 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -77,8 +77,6 @@ type SelectType = | 'indexerFlags' | 'releaseType'; -type FilterExistingFiles = 'all' | 'new'; - // TODO: This feels janky to do, but not sure of a better way currently type OnSelectedChangeCallback = React.ComponentProps< typeof InteractiveImportRow @@ -641,10 +639,8 @@ function InteractiveImportModalContent( [dispatch] ); - const onFilterExistingFilesChange = useCallback< - (value: FilterExistingFiles) => void - >( - (value) => { + const onFilterExistingFilesChange = useCallback( + (value: string | undefined) => { const filter = value !== 'all'; setFilterExistingFiles(filter); diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.tsx b/frontend/src/InteractiveSearch/InteractiveSearch.tsx index 9dff36198..73163cab0 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearch.tsx @@ -137,7 +137,7 @@ function InteractiveSearch({ type, searchPayload }: InteractiveSearchProps) { const dispatch = useDispatch(); const handleFilterSelect = useCallback( - (selectedFilterKey: string) => { + (selectedFilterKey: string | number) => { const action = type === 'episode' ? setEpisodeReleasesFilter : setSeasonReleasesFilter; diff --git a/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx b/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx index aebeac134..ade2d442d 100644 --- a/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx +++ b/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx @@ -1,15 +1,14 @@ import React from 'react'; -import { CustomFilter } from 'App/State/AppState'; +import { CustomFilter, Filter } from 'App/State/AppState'; import FilterMenu from 'Components/Menu/FilterMenu'; -import { align } from 'Helpers/Props'; import SeriesIndexFilterModal from 'Series/Index/SeriesIndexFilterModal'; interface SeriesIndexFilterMenuProps { selectedFilterKey: string | number; - filters: object[]; + filters: Filter[]; customFilters: CustomFilter[]; isDisabled: boolean; - onFilterSelect(filterName: string): unknown; + onFilterSelect: (filter: number | string) => void; } function SeriesIndexFilterMenu(props: SeriesIndexFilterMenuProps) { @@ -23,7 +22,7 @@ function SeriesIndexFilterMenu(props: SeriesIndexFilterMenuProps) { return ( { ); const onFilterSelect = useCallback( - (value: string) => { + (value: string | number) => { dispatch(setSeriesFilter({ selectedFilterKey: value })); }, [dispatch]