Convert Menu components to TypeScript

This commit is contained in:
Mark McDowall 2024-12-22 15:41:53 -08:00
parent 2935d148a8
commit 12a1ef0387
No known key found for this signature in database
42 changed files with 749 additions and 1018 deletions

View file

@ -145,7 +145,7 @@ function Blocklist() {
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
(selectedFilterKey: string | number) => {
dispatch(setBlocklistFilter({ selectedFilterKey }));
},
[dispatch]

View file

@ -80,7 +80,7 @@ function History() {
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
(selectedFilterKey: string | number) => {
dispatch(setHistoryFilter({ selectedFilterKey }));
},
[dispatch]

View file

@ -185,7 +185,7 @@ function Queue() {
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
(selectedFilterKey: string | number) => {
dispatch(setQueueFilter({ selectedFilterKey }));
},
[dispatch]

View file

@ -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<T> {
selectedFilterKey: string;
filters: PropertyFilter[];
filters: Filter[];
filterBuilderProps: FilterBuilderProp<T>[];
}

View file

@ -45,7 +45,7 @@ export interface PropertyFilter {
export interface Filter {
key: string;
label: string;
label: string | (() => string);
type: string;
filters: PropertyFilter[];
}

View file

@ -132,7 +132,7 @@ function CalendarPage() {
}, [missingEpisodeIds, dispatch]);
const handleFilterSelect = useCallback(
(key: string) => {
(key: string | number) => {
dispatch(setCalendarFilter({ selectedFilterKey: key }));
},
[dispatch]

View file

@ -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]

View file

@ -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 (
<div>
<Menu
className={className}
{...otherProps}
>
<ButtonComponent
iconName={icons.FILTER}
showIndicator={selectedFilterKey !== 'all'}
text={translate('Filter')}
isDisabled={isDisabled}
/>
<FilterMenuContent
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
showCustomFilters={showCustomFilters}
onFilterSelect={onFilterSelect}
onCustomFiltersPress={this.onCustomFiltersPress}
/>
</Menu>
{
showCustomFilters ?
<FilterModalConnectorComponent
{...filterModalConnectorComponentProps}
isOpen={this.state.isFilterModalOpen}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
onFilterSelect={onFilterSelect}
onModalClose={this.onFiltersModalClose}
/> : null
}
</div>
);
}
}
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;

View file

@ -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 (
<div>
<Menu className={className} {...otherProps}>
<ButtonComponent
iconName={icons.FILTER}
showIndicator={selectedFilterKey !== 'all'}
text={translate('Filter')}
isDisabled={isDisabled}
/>
<FilterMenuContent
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
showCustomFilters={showCustomFilters}
onFilterSelect={onFilterSelect}
onCustomFiltersPress={handleCustomFiltersPress}
/>
</Menu>
{showCustomFilters ? (
<FilterModalConnectorComponent
{...filterModalConnectorComponentProps}
isOpen={isFilterModalOpen}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
onFilterSelect={onFilterSelect}
onModalClose={handleFiltersModalClose}
/>
) : null}
</div>
);
}
export default FilterMenu;

View file

@ -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 (
<MenuContent {...otherProps}>
{
filters.map((filter) => {
return (
<FilterMenuItem
key={filter.key}
filterKey={filter.key}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem>
);
})
}
{
customFilters.length > 0 ?
<MenuItemSeparator /> :
null
}
{
customFilters
.sort(sortByProp('label'))
.map((filter) => {
return (
<FilterMenuItem
key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
}
{
showCustomFilters &&
<MenuItemSeparator />
}
{
showCustomFilters &&
<MenuItem onPress={onCustomFiltersPress}>
{translate('CustomFilters')}
</MenuItem>
}
</MenuContent>
);
}
}
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;

View file

@ -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 (
<MenuContent {...otherProps}>
{filters.map((filter) => {
return (
<FilterMenuItem
key={filter.key}
filterKey={filter.key}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem>
);
})}
{customFilters.length > 0 ? <MenuItemSeparator /> : null}
{customFilters.sort(sortByProp('label')).map((filter) => {
return (
<FilterMenuItem
key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})}
{showCustomFilters && <MenuItemSeparator />}
{showCustomFilters && (
<MenuItem onPress={onCustomFiltersPress}>
{translate('CustomFilters')}
</MenuItem>
)}
</MenuContent>
);
}
export default FilterMenuContent;

View file

@ -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 (
<SelectedMenuItem
isSelected={filterKey === selectedFilterKey}
{...otherProps}
onPress={this.onPress}
/>
);
}
}
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;

View file

@ -0,0 +1,30 @@
import React, { useCallback } from 'react';
import SelectedMenuItem, { SelectedMenuItemProps } from './SelectedMenuItem';
interface FilterMenuItemProps
extends Omit<SelectedMenuItemProps, 'isSelected' | 'onPress'> {
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 (
<SelectedMenuItem
{...otherProps}
isSelected={filterKey === selectedFilterKey}
onPress={handlePress}
/>
);
}
export default FilterMenuItem;

View file

@ -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 (
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._menuButtonId}
className={className}
>
{button}
</div>
)}
</Reference>
<Portal>
<Popper {...popperOptions[alignMenu]}>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return React.cloneElement(
childrenArray[1],
{
forwardedRef: ref,
style: {
...style,
maxHeight
},
isOpen: isMenuOpen
}
);
}}
</Popper>
</Portal>
</Manager>
);
}
}
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;

View file

@ -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<PopperProps>;
left: Partial<PopperProps>;
} = {
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 (
<Manager>
<Reference>
{({ ref }) => (
<div ref={ref} id={menuButtonId} className={className}>
{button}
</div>
)}
</Reference>
<Portal>
<Popper {...popperOptions[alignMenu]}>
{({ ref, style, scheduleUpdate }) => {
updater.current = scheduleUpdate;
return React.cloneElement(childrenArray[1] as ReactElement, {
forwardedRef: ref,
style: {
...style,
maxHeight,
},
isOpen: isMenuOpen,
});
}}
</Popper>
</Portal>
</Manager>
);
}
export default Menu;

View file

@ -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 (
<Link
className={classNames(
className,
isDisabled && styles.isDisabled
)}
isDisabled={isDisabled}
onPress={onPress}
{...otherProps}
>
{children}
</Link>
);
}
}
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;

View file

@ -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 (
<Link
className={classNames(className, isDisabled && styles.isDisabled)}
isDisabled={isDisabled}
{...otherProps}
>
{children}
</Link>
);
}
export default MenuButton;

View file

@ -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 (
<div
id={id}
ref={forwardedRef}
className={className}
style={style}
>
{
isOpen ?
<Scroller className={styles.scroller}>
{children}
</Scroller> :
null
}
</div>
);
}
}
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;

View file

@ -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<HTMLDivElement> | 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 (
<div
ref={forwardedRef}
id={id ?? generatedId}
className={className}
style={style}
>
{isOpen ? (
<Scroller className={styles.scroller}>{children}</Scroller>
) : null}
</div>
);
}
export default MenuContent;

View file

@ -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 (
<Link
className={classNames(
className,
isDisabled && styles.isDisabled
)}
isDisabled={isDisabled}
{...otherProps}
>
{children}
</Link>
);
}
}
MenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
isDisabled: PropTypes.bool.isRequired
};
MenuItem.defaultProps = {
className: styles.menuItem,
isDisabled: false
};
export default MenuItem;

View file

@ -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 (
<Link
className={classNames(className, isDisabled && styles.isDisabled)}
isDisabled={isDisabled}
{...otherProps}
>
{children}
</Link>
);
}
export default MenuItem;

View file

@ -2,9 +2,7 @@ import React from 'react';
import styles from './MenuItemSeparator.css';
function MenuItemSeparator() {
return (
<div className={styles.separator} />
);
return <div className={styles.separator} />;
}
export default MenuItemSeparator;

View file

@ -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 (
<MenuButton
className={styles.menuButton}
{...otherProps}
>
<Icon
name={iconName}
size={18}
/>
{
showIndicator ?
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
name={icons.CIRCLE}
size={9}
/>
</span> :
null
}
<div className={styles.label}>
{text}
</div>
</MenuButton>
);
}
PageMenuButton.propTypes = {
iconName: PropTypes.object.isRequired,
showIndicator: PropTypes.bool.isRequired,
text: PropTypes.string
};
PageMenuButton.defaultProps = {
showIndicator: false
};
export default PageMenuButton;

View file

@ -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 (
<MenuButton className={styles.menuButton} {...otherProps}>
<Icon name={iconName} size={18} />
{showIndicator ? (
<span
className={classNames(styles.indicatorContainer, 'fa-layers fa-fw')}
>
<Icon name={icons.CIRCLE} size={9} />
</span>
) : null}
<div className={styles.label}>{text}</div>
</MenuButton>
);
}
export default PageMenuButton;

View file

@ -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 (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
<div className={styles.item}>
{children}
<Icon
className={isSelected ? styles.isSelected : styles.isNotSelected}
name={selectedIconName}
/>
</div>
</MenuItem>
);
}
}
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;

View file

@ -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<MenuItemProps, 'onPress'> {
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 (
<MenuItem {...otherProps} onPress={handlePress}>
<div className={styles.item}>
{children}
<Icon
className={isSelected ? styles.isSelected : styles.isNotSelected}
name={selectedIconName}
/>
</div>
</MenuItem>
);
}
export default SelectedMenuItem;

View file

@ -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 (
<Menu
className={className}
{...otherProps}
>
<ToolbarMenuButton
iconName={icons.SORT}
text={translate('Sort')}
isDisabled={isDisabled}
/>
{children}
</Menu>
);
}
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;

View file

@ -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<ToolbarMenuButtonProps, 'iconName'> {
className?: string;
children: React.ReactNode;
isDisabled?: boolean;
alignMenu?: 'left' | 'right';
}
function SortMenu({
className,
children,
isDisabled = false,
...otherProps
}: SortMenuProps) {
return (
<Menu className={className} {...otherProps}>
<ToolbarMenuButton
iconName={icons.SORT}
text={translate('Sort')}
isDisabled={isDisabled}
/>
{children}
</Menu>
);
}
export default SortMenu;

View file

@ -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 (
<SelectedMenuItem
name={name}
selectedIconName={sortDirection === sortDirections.ASCENDING ? icons.SORT_ASCENDING : icons.SORT_DESCENDING}
isSelected={isSelected}
{...otherProps}
/>
);
}
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;

View file

@ -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 (
<SelectedMenuItem
name={name}
selectedIconName={
sortDirection === 'ascending'
? icons.SORT_ASCENDING
: icons.SORT_DESCENDING
}
isSelected={isSelected}
{...otherProps}
/>
);
}
export default SortMenuItem;

View file

@ -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 (
<MenuButton
className={styles.menuButton}
{...otherProps}
>
<div>
<Icon
name={iconName}
size={21}
/>
{
showIndicator ?
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
name={icons.CIRCLE}
size={9}
/>
</span> :
null
}
<div className={styles.labelContainer}>
<div className={styles.label}>
{text}
</div>
</div>
</div>
</MenuButton>
);
}
ToolbarMenuButton.propTypes = {
className: PropTypes.string,
iconName: PropTypes.object.isRequired,
showIndicator: PropTypes.bool.isRequired,
text: PropTypes.string
};
ToolbarMenuButton.defaultProps = {
showIndicator: false
};
export default ToolbarMenuButton;

View file

@ -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<MenuButtonProps, 'children'> {
className?: string;
iconName: IconName;
showIndicator?: boolean;
text?: string;
}
function ToolbarMenuButton({
iconName,
showIndicator = false,
text,
...otherProps
}: ToolbarMenuButtonProps) {
return (
<MenuButton className={styles.menuButton} {...otherProps}>
<div>
<Icon name={iconName} size={21} />
{showIndicator ? (
<span
className={classNames(styles.indicatorContainer, 'fa-layers fa-fw')}
>
<Icon name={icons.CIRCLE} size={9} />
</span>
) : null}
<div className={styles.labelContainer}>
<div className={styles.label}>{text}</div>
</div>
</div>
</MenuButton>
);
}
export default ToolbarMenuButton;

View file

@ -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 (
<Menu
{...otherProps}
>
<ToolbarMenuButton
iconName={icons.VIEW}
text={translate('View')}
isDisabled={isDisabled}
/>
{children}
</Menu>
);
}
ViewMenu.propTypes = {
children: PropTypes.node.isRequired,
isDisabled: PropTypes.bool.isRequired,
alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT])
};
ViewMenu.defaultProps = {
isDisabled: false
};
export default ViewMenu;

View file

@ -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<ToolbarMenuButtonProps, 'iconName'> {
children: React.ReactNode;
isDisabled?: boolean;
alignMenu?: 'left' | 'right';
}
function ViewMenu({
children,
isDisabled = false,
...otherProps
}: ViewMenuProps) {
return (
<Menu {...otherProps}>
<ToolbarMenuButton
iconName={icons.VIEW}
text={translate('View')}
isDisabled={isDisabled}
/>
{children}
</Menu>
);
}
export default ViewMenu;

View file

@ -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 (
<SelectedMenuItem
name={name}
isSelected={isSelected}
{...otherProps}
/>
);
}
ViewMenuItem.propTypes = {
name: PropTypes.string,
selectedView: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
onPress: PropTypes.func.isRequired
};
export default ViewMenuItem;

View file

@ -0,0 +1,23 @@
import React from 'react';
import SelectedMenuItem, { SelectedMenuItemProps } from './SelectedMenuItem';
interface ViewMenuItemProps extends Omit<SelectedMenuItemProps, 'isSelected'> {
name?: string;
selectedView: string;
children: React.ReactNode;
onPress: (view: string) => void;
}
function ViewMenuItem({
name,
selectedView,
...otherProps
}: ViewMenuItemProps) {
const isSelected = name === selectedView;
return (
<SelectedMenuItem name={name} isSelected={isSelected} {...otherProps} />
);
}
export default ViewMenuItem;

View file

@ -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);

View file

@ -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;

View file

@ -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 (
<FilterMenu
alignMenu={align.RIGHT}
alignMenu="right"
isDisabled={isDisabled}
selectedFilterKey={selectedFilterKey}
filters={filters}

View file

@ -10,7 +10,7 @@ interface SeriesIndexSortMenuProps {
sortKey?: string;
sortDirection?: SortDirection;
isDisabled: boolean;
onSortSelect(sortKey: string): unknown;
onSortSelect(sortKey: string): void;
}
function SeriesIndexSortMenu(props: SeriesIndexSortMenuProps) {

View file

@ -8,7 +8,7 @@ import translate from 'Utilities/String/translate';
interface SeriesIndexViewMenuProps {
view: string;
isDisabled: boolean;
onViewSelect(value: string): unknown;
onViewSelect(value: string): void;
}
function SeriesIndexViewMenu(props: SeriesIndexViewMenuProps) {

View file

@ -145,7 +145,7 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
);
const onFilterSelect = useCallback(
(value: string) => {
(value: string | number) => {
dispatch(setSeriesFilter({ selectedFilterKey: value }));
},
[dispatch]