mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-23 13:57:06 -04:00
Convert Menu components to TypeScript
This commit is contained in:
parent
2935d148a8
commit
12a1ef0387
42 changed files with 749 additions and 1018 deletions
|
@ -145,7 +145,7 @@ function Blocklist() {
|
|||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
(selectedFilterKey: string | number) => {
|
||||
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -80,7 +80,7 @@ function History() {
|
|||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
(selectedFilterKey: string | number) => {
|
||||
dispatch(setHistoryFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -185,7 +185,7 @@ function Queue() {
|
|||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
(selectedFilterKey: string | number) => {
|
||||
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -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>[];
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ export interface PropertyFilter {
|
|||
|
||||
export interface Filter {
|
||||
key: string;
|
||||
label: string;
|
||||
label: string | (() => string);
|
||||
type: string;
|
||||
filters: PropertyFilter[];
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ function CalendarPage() {
|
|||
}, [missingEpisodeIds, dispatch]);
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(key: string) => {
|
||||
(key: string | number) => {
|
||||
dispatch(setCalendarFilter({ selectedFilterKey: key }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
82
frontend/src/Components/Menu/FilterMenu.tsx
Normal file
82
frontend/src/Components/Menu/FilterMenu.tsx
Normal 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;
|
|
@ -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;
|
69
frontend/src/Components/Menu/FilterMenuContent.tsx
Normal file
69
frontend/src/Components/Menu/FilterMenuContent.tsx
Normal 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;
|
|
@ -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;
|
30
frontend/src/Components/Menu/FilterMenuItem.tsx
Normal file
30
frontend/src/Components/Menu/FilterMenuItem.tsx
Normal 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;
|
|
@ -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;
|
205
frontend/src/Components/Menu/Menu.tsx
Normal file
205
frontend/src/Components/Menu/Menu.tsx
Normal 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;
|
|
@ -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;
|
30
frontend/src/Components/Menu/MenuButton.tsx
Normal file
30
frontend/src/Components/Menu/MenuButton.tsx
Normal 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;
|
|
@ -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;
|
38
frontend/src/Components/Menu/MenuContent.tsx
Normal file
38
frontend/src/Components/Menu/MenuContent.tsx
Normal 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;
|
|
@ -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;
|
29
frontend/src/Components/Menu/MenuItem.tsx
Normal file
29
frontend/src/Components/Menu/MenuItem.tsx
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
38
frontend/src/Components/Menu/PageMenuButton.tsx
Normal file
38
frontend/src/Components/Menu/PageMenuButton.tsx
Normal 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;
|
|
@ -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;
|
41
frontend/src/Components/Menu/SelectedMenuItem.tsx
Normal file
41
frontend/src/Components/Menu/SelectedMenuItem.tsx
Normal 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;
|
|
@ -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;
|
34
frontend/src/Components/Menu/SortMenu.tsx
Normal file
34
frontend/src/Components/Menu/SortMenu.tsx
Normal 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;
|
|
@ -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;
|
36
frontend/src/Components/Menu/SortMenuItem.tsx
Normal file
36
frontend/src/Components/Menu/SortMenuItem.tsx
Normal 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;
|
|
@ -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;
|
43
frontend/src/Components/Menu/ToolbarMenuButton.tsx
Normal file
43
frontend/src/Components/Menu/ToolbarMenuButton.tsx
Normal 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;
|
|
@ -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;
|
32
frontend/src/Components/Menu/ViewMenu.tsx
Normal file
32
frontend/src/Components/Menu/ViewMenu.tsx
Normal 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;
|
|
@ -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;
|
23
frontend/src/Components/Menu/ViewMenuItem.tsx
Normal file
23
frontend/src/Components/Menu/ViewMenuItem.tsx
Normal 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;
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -10,7 +10,7 @@ interface SeriesIndexSortMenuProps {
|
|||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
isDisabled: boolean;
|
||||
onSortSelect(sortKey: string): unknown;
|
||||
onSortSelect(sortKey: string): void;
|
||||
}
|
||||
|
||||
function SeriesIndexSortMenu(props: SeriesIndexSortMenuProps) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -145,7 +145,7 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value: string) => {
|
||||
(value: string | number) => {
|
||||
dispatch(setSeriesFilter({ selectedFilterKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue