mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-30 23:45:46 +02:00
Typings cleanup and improvements
(cherry picked from commit b2c43fb2a67965d68d3d35b72302b0cddb5aca23)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import IndexerAppState from './IndexerAppState';
|
||||
import IndexerAppState, { IndexerIndexAppState } from './IndexerAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface CustomFilter {
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
indexerIndex: IndexerIndexAppState;
|
||||
indexers: IndexerAppState;
|
||||
settings: SettingsAppState;
|
||||
tags: TagsAppState;
|
||||
|
8
frontend/src/App/State/ClientSideCollectionAppState.ts
Normal file
8
frontend/src/App/State/ClientSideCollectionAppState.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { CustomFilter } from './AppState';
|
||||
|
||||
interface ClientSideCollectionAppState {
|
||||
totalItems: number;
|
||||
customFilters: CustomFilter[];
|
||||
}
|
||||
|
||||
export default ClientSideCollectionAppState;
|
@@ -1,8 +1,29 @@
|
||||
import Indexer from 'typings/Indexer';
|
||||
import Column from 'Components/Table/Column';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from './AppSectionState';
|
||||
import { Filter, FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface IndexerIndexAppState {
|
||||
isTestingAll: boolean;
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
secondarySortKey: string;
|
||||
secondarySortDirection: SortDirection;
|
||||
view: string;
|
||||
|
||||
tableOptions: {
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
selectedFilterKey: string;
|
||||
filterBuilderProps: FilterBuilderProp<Indexer>[];
|
||||
filters: Filter[];
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
interface IndexerAppState
|
||||
extends AppSectionState<Indexer>,
|
||||
|
@@ -23,7 +23,9 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||
info,
|
||||
} = props;
|
||||
|
||||
const [detailedError, setDetailedError] = useState(null);
|
||||
const [detailedError, setDetailedError] = useState<
|
||||
StackTrace.StackFrame[] | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
@@ -1,98 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
class Link extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onClick = (event) => {
|
||||
const {
|
||||
isDisabled,
|
||||
onPress
|
||||
} = this.props;
|
||||
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
component,
|
||||
to,
|
||||
target,
|
||||
isDisabled,
|
||||
noRouter,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const linkProps = { target };
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if ((/\w+?:\/\//).test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = otherProps.type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const props = {
|
||||
...otherProps,
|
||||
...linkProps
|
||||
};
|
||||
|
||||
props.onClick = this.onClick;
|
||||
|
||||
return (
|
||||
React.createElement(el, props)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
className: PropTypes.string,
|
||||
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
to: PropTypes.string,
|
||||
target: PropTypes.string,
|
||||
isDisabled: PropTypes.bool,
|
||||
noRouter: PropTypes.bool,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
Link.defaultProps = {
|
||||
component: 'button',
|
||||
noRouter: false
|
||||
};
|
||||
|
||||
export default Link;
|
96
frontend/src/Components/Link/Link.tsx
Normal file
96
frontend/src/Components/Link/Link.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ComponentClass,
|
||||
FunctionComponent,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
interface ReactRouterLinkProps {
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
||||
className?: string;
|
||||
component?:
|
||||
| string
|
||||
| FunctionComponent<LinkProps>
|
||||
| ComponentClass<LinkProps, unknown>;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: boolean;
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
}
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
className,
|
||||
component = 'button',
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter = false,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
}
|
||||
},
|
||||
[isDisabled, onPress]
|
||||
);
|
||||
|
||||
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
||||
target,
|
||||
};
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if (/\w+?:\/\//.test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const elementProps = {
|
||||
...otherProps,
|
||||
type,
|
||||
...linkProps,
|
||||
};
|
||||
|
||||
elementProps.onClick = onClick;
|
||||
|
||||
return React.createElement(el, elementProps);
|
||||
}
|
||||
|
||||
export default Link;
|
@@ -1,22 +1,19 @@
|
||||
import React, { forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
interface PageContentBodyProps {
|
||||
className: string;
|
||||
innerClassName: string;
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
children: ReactNode;
|
||||
initialScrollTop?: number;
|
||||
onScroll?: (payload) => void;
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
}
|
||||
|
||||
const PageContentBody = forwardRef(
|
||||
(
|
||||
props: PageContentBodyProps,
|
||||
ref: React.MutableRefObject<HTMLDivElement>
|
||||
) => {
|
||||
(props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const {
|
||||
className = styles.contentBody,
|
||||
innerClassName = styles.innerContentBody,
|
||||
@@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
|
||||
} = props;
|
||||
|
||||
const onScrollWrapper = useCallback(
|
||||
(payload) => {
|
||||
(payload: OnScroll) => {
|
||||
if (onScroll && !isLocked()) {
|
||||
onScroll(payload);
|
||||
}
|
||||
|
@@ -1,9 +1,21 @@
|
||||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
export interface OnScroll {
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
}
|
||||
|
||||
interface ScrollerProps {
|
||||
className?: string;
|
||||
scrollDirection?: ScrollDirection;
|
||||
@@ -12,11 +24,11 @@ interface ScrollerProps {
|
||||
scrollTop?: number;
|
||||
initialScrollTop?: number;
|
||||
children?: ReactNode;
|
||||
onScroll?: (payload) => void;
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
}
|
||||
|
||||
const Scroller = forwardRef(
|
||||
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
|
||||
(props: ScrollerProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const {
|
||||
className,
|
||||
autoFocus = false,
|
||||
@@ -30,7 +42,7 @@ const Scroller = forwardRef(
|
||||
} = props;
|
||||
|
||||
const internalRef = useRef();
|
||||
const currentRef = ref ?? internalRef;
|
||||
const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
|
@@ -1,24 +1,30 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
|
||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
function ScrollPosition(props) {
|
||||
interface WrappedComponentProps {
|
||||
initialScrollTop: number;
|
||||
}
|
||||
|
||||
interface ScrollPositionProps {
|
||||
history: RouteComponentProps['history'];
|
||||
location: RouteComponentProps['location'];
|
||||
match: RouteComponentProps['match'];
|
||||
}
|
||||
|
||||
function withScrollPosition(
|
||||
WrappedComponent: React.FC<WrappedComponentProps>,
|
||||
scrollPositionKey: string
|
||||
) {
|
||||
function ScrollPosition(props: ScrollPositionProps) {
|
||||
const { history } = props;
|
||||
|
||||
const initialScrollTop =
|
||||
history.action === 'POP' ||
|
||||
(history.location.state && history.location.state.restoreScrollPosition)
|
||||
? scrollPositions[scrollPositionKey]
|
||||
: 0;
|
||||
history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
|
||||
|
||||
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
|
||||
}
|
||||
|
||||
ScrollPosition.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
return ScrollPosition;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import IndexerAppState, {
|
||||
IndexerIndexAppState,
|
||||
} from 'App/State/IndexerAppState';
|
||||
import { APP_INDEXER_SYNC } from 'Commands/commandNames';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -64,19 +68,20 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
} = useSelector(
|
||||
createIndexerClientSideCollectionItemsSelector('indexerIndex')
|
||||
);
|
||||
}: IndexerAppState & IndexerIndexAppState & ClientSideCollectionAppState =
|
||||
useSelector(createIndexerClientSideCollectionItemsSelector('indexerIndex'));
|
||||
|
||||
const isSyncingIndexers = useSelector(
|
||||
createCommandExecutingSelector(APP_INDEXER_SYNC)
|
||||
);
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
const dispatch = useDispatch();
|
||||
const scrollerRef = useRef<HTMLDivElement>();
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
|
||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
const onAppIndexerSyncPress = useCallback(() => {
|
||||
@@ -112,37 +117,37 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
}, [isSelectMode, setIsSelectMode]);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setIndexerTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSortSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setIndexerSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setIndexerFilter({ selectedFilterKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onJumpBarItemPress = useCallback(
|
||||
(character) => {
|
||||
(character: string) => {
|
||||
setJumpToCharacter(character);
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
({ scrollTop }) => {
|
||||
setJumpToCharacter(null);
|
||||
scrollPositions.seriesIndex = scrollTop;
|
||||
({ scrollTop }: { scrollTop: number }) => {
|
||||
setJumpToCharacter(undefined);
|
||||
scrollPositions.indexerIndex = scrollTop;
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
@@ -155,7 +160,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
};
|
||||
}
|
||||
|
||||
const characters = items.reduce((acc, item) => {
|
||||
const characters = items.reduce((acc: Record<string, number>, item) => {
|
||||
let char = item.sortName.charAt(0);
|
||||
|
||||
if (!isNaN(Number(char))) {
|
||||
@@ -277,6 +282,8 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
<PageContentBody
|
||||
ref={scrollerRef}
|
||||
className={styles.contentBody}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
initialScrollTop={props.initialScrollTop}
|
||||
onScroll={onScroll}
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setIndexerFilter } from 'Store/Actions/indexerIndexActions';
|
||||
|
||||
function createIndexerSelector() {
|
||||
return createSelector(
|
||||
(state) => state.indexers.items,
|
||||
(state: AppState) => state.indexers.items,
|
||||
(indexers) => {
|
||||
return indexers;
|
||||
}
|
||||
@@ -15,14 +16,20 @@ function createIndexerSelector() {
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.indexerIndex.filterBuilderProps,
|
||||
(state: AppState) => state.indexerIndex.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function IndexerIndexFilterModal(props) {
|
||||
interface IndexerIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function IndexerIndexFilterModal(
|
||||
props: IndexerIndexFilterModalProps
|
||||
) {
|
||||
const sectionItems = useSelector(createIndexerSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'indexerIndex';
|
||||
@@ -30,7 +37,7 @@ export default function IndexerIndexFilterModal(props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setIndexerFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
@@ -38,6 +45,7 @@ export default function IndexerIndexFilterModal(props) {
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import IndexerAppState from 'App/State/IndexerAppState';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
@@ -13,7 +14,7 @@ import styles from './IndexerIndexFooter.css';
|
||||
function createUnoptimizedSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers', 'indexerIndex'),
|
||||
(indexers) => {
|
||||
(indexers: IndexerAppState) => {
|
||||
return indexers.items.map((s) => {
|
||||
const { protocol, privacy, enable } = s;
|
||||
|
||||
|
@@ -1,10 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { CustomFilter } from 'App/State/AppState';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
import IndexerIndexFilterModal from 'Indexer/Index/IndexerIndexFilterModal';
|
||||
|
||||
function IndexerIndexFilterMenu(props) {
|
||||
interface IndexerIndexFilterMenuProps {
|
||||
selectedFilterKey: string | number;
|
||||
filters: object[];
|
||||
customFilters: CustomFilter[];
|
||||
isDisabled: boolean;
|
||||
onFilterSelect(filterName: string): unknown;
|
||||
}
|
||||
|
||||
function IndexerIndexFilterMenu(props: IndexerIndexFilterMenuProps) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
@@ -26,15 +34,6 @@ function IndexerIndexFilterMenu(props) {
|
||||
);
|
||||
}
|
||||
|
||||
IndexerIndexFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
IndexerIndexFilterMenu.defaultProps = {
|
||||
showCustomFilters: false,
|
||||
};
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SortMenu from 'Components/Menu/SortMenu';
|
||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function IndexerIndexSortMenu(props) {
|
||||
interface IndexerIndexSortMenuProps {
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
isDisabled: boolean;
|
||||
onSortSelect(sortKey: string): unknown;
|
||||
}
|
||||
|
||||
function IndexerIndexSortMenu(props: IndexerIndexSortMenuProps) {
|
||||
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
|
||||
|
||||
return (
|
||||
@@ -79,11 +86,4 @@ function IndexerIndexSortMenu(props) {
|
||||
);
|
||||
}
|
||||
|
||||
IndexerIndexSortMenu.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default IndexerIndexSortMenu;
|
||||
|
@@ -7,6 +7,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import { bulkDeleteIndexers } from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -20,15 +21,15 @@ interface DeleteIndexerModalContentProps {
|
||||
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
const { indexerIds, onModalClose } = props;
|
||||
|
||||
const allIndexers = useSelector(createAllIndexersSelector());
|
||||
const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const selectedIndexers = useMemo(() => {
|
||||
const indexers = indexerIds.map((id) => {
|
||||
const indexers = useMemo((): Indexer[] => {
|
||||
const indexerList = indexerIds.map((id) => {
|
||||
return allIndexers.find((s) => s.id === id);
|
||||
});
|
||||
}) as Indexer[];
|
||||
|
||||
return orderBy(indexers, ['sortName']);
|
||||
return orderBy(indexerList, ['sortName']);
|
||||
}, [indexerIds, allIndexers]);
|
||||
|
||||
const onDeleteIndexerConfirmed = useCallback(() => {
|
||||
@@ -47,13 +48,11 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{translate('DeleteSelectedIndexersMessageText', [
|
||||
selectedIndexers.length,
|
||||
])}
|
||||
{translate('DeleteSelectedIndexersMessageText', [indexers.length])}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{selectedIndexers.map((s) => {
|
||||
{indexers.map((s) => {
|
||||
return (
|
||||
<li key={s.id}>
|
||||
<span>{s.name}</span>
|
||||
|
@@ -107,7 +107,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: { name: string; value: string }) => {
|
||||
switch (name) {
|
||||
case 'enable':
|
||||
setEnable(value);
|
||||
|
@@ -7,7 +7,7 @@ import translate from 'Utilities/String/translate';
|
||||
interface IndexerIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
}
|
||||
|
||||
function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
|
||||
|
@@ -15,6 +15,16 @@ import EditIndexerModal from './Edit/EditIndexerModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
import styles from './IndexerIndexSelectFooter.css';
|
||||
|
||||
interface SavePayload {
|
||||
enable?: boolean;
|
||||
appProfileId?: number;
|
||||
priority?: number;
|
||||
minimumSeeders?: number;
|
||||
seedRatio?: number;
|
||||
seedTime?: number;
|
||||
packSeedTime?: number;
|
||||
}
|
||||
|
||||
const indexersEditorSelector = createSelector(
|
||||
(state: AppState) => state.indexers,
|
||||
(indexers) => {
|
||||
@@ -60,7 +70,7 @@ function IndexerIndexSelectFooter() {
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onSavePress = useCallback(
|
||||
(payload) => {
|
||||
(payload: SavePayload) => {
|
||||
setIsSavingIndexer(true);
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
@@ -83,7 +93,7 @@ function IndexerIndexSelectFooter() {
|
||||
}, [setIsTagsModalOpen]);
|
||||
|
||||
const onApplyTagsPress = useCallback(
|
||||
(tags, applyTags) => {
|
||||
(tags: number[], applyTags: string) => {
|
||||
setIsSavingTags(true);
|
||||
setIsTagsModalOpen(false);
|
||||
|
||||
|
@@ -7,7 +7,7 @@ interface IndexerIndexSelectModeButtonProps {
|
||||
label: string;
|
||||
iconName: IconDefinition;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { concat, uniq } from 'lodash';
|
||||
import { uniq } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -12,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -26,29 +28,35 @@ interface TagsModalContentProps {
|
||||
function TagsModalContent(props: TagsModalContentProps) {
|
||||
const { indexerIds, onModalClose, onApplyTagsPress } = props;
|
||||
|
||||
const allIndexers = useSelector(createAllIndexersSelector());
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
|
||||
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||
|
||||
const [tags, setTags] = useState<number[]>([]);
|
||||
const [applyTags, setApplyTags] = useState('add');
|
||||
|
||||
const indexerTags = useMemo(() => {
|
||||
const indexers = indexerIds.map((id) => {
|
||||
return allIndexers.find((s) => s.id === id);
|
||||
});
|
||||
const tags = indexerIds.reduce((acc: number[], id) => {
|
||||
const s = allIndexers.find((s) => s.id === id);
|
||||
|
||||
return uniq(concat(...indexers.map((s) => s.tags)));
|
||||
if (s) {
|
||||
acc.push(...s.tags);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return uniq(tags);
|
||||
}, [indexerIds, allIndexers]);
|
||||
|
||||
const onTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: number[] }) => {
|
||||
setTags(value);
|
||||
},
|
||||
[setTags]
|
||||
);
|
||||
|
||||
const onApplyTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setApplyTags(value);
|
||||
},
|
||||
[setApplyTags]
|
||||
|
@@ -13,6 +13,7 @@ import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector';
|
||||
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CapabilitiesLabel from './CapabilitiesLabel';
|
||||
@@ -100,12 +101,8 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
setIsDeleteIndexerModalOpen(false);
|
||||
}, [setIsDeleteIndexerModalOpen]);
|
||||
|
||||
const checkInputCallback = useCallback(() => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
}, []);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }) => {
|
||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
id,
|
||||
@@ -202,6 +199,8 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
@@ -213,6 +212,8 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
|
||||
if (name === 'vipExpiration') {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
@@ -266,6 +267,8 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
className={styles[column.name]}
|
||||
>
|
||||
<IconButton
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Column from 'Components/Table/Column';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
@@ -13,7 +14,6 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import IndexerIndexRow from './IndexerIndexRow';
|
||||
import IndexerIndexTableHeader from './IndexerIndexTableHeader';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
import styles from './IndexerIndexTable.css';
|
||||
|
||||
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||
@@ -30,17 +30,17 @@ interface RowItemData {
|
||||
|
||||
interface IndexerIndexTableProps {
|
||||
items: Indexer[];
|
||||
sortKey?: string;
|
||||
sortKey: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const columnsSelector = createSelector(
|
||||
(state) => state.indexerIndex.columns,
|
||||
(state: AppState) => state.indexerIndex.columns,
|
||||
(columns) => columns
|
||||
);
|
||||
|
||||
@@ -91,22 +91,21 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
} = props;
|
||||
|
||||
const columns = useSelector(columnsSelector);
|
||||
const { showBanners } = useSelector(selectTableOptions);
|
||||
const listRef = useRef<List>(null);
|
||||
const listRef = useRef<List<RowItemData>>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
return showBanners ? 70 : 38;
|
||||
}, [showBanners]);
|
||||
const rowHeight = 38;
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef.current as HTMLElement;
|
||||
const current = scrollerRef?.current as HTMLElement;
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -119,10 +118,10 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
height: window.innerHeight,
|
||||
height: windowHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
@@ -165,7 +164,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
}
|
||||
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
||||
scrollerRef?.current?.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
@@ -177,7 +176,6 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
|
||||
scrollDirection={ScrollDirection.Horizontal}
|
||||
>
|
||||
<IndexerIndexTableHeader
|
||||
showBanners={showBanners}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
|
@@ -14,12 +14,11 @@ import {
|
||||
setIndexerSort,
|
||||
setIndexerTableOption,
|
||||
} from 'Store/Actions/indexerIndexActions';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import IndexerIndexTableOptions from './IndexerIndexTableOptions';
|
||||
import styles from './IndexerIndexTableHeader.css';
|
||||
|
||||
interface IndexerIndexTableHeaderProps {
|
||||
showBanners: boolean;
|
||||
columns: Column[];
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
@@ -32,21 +31,21 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
|
||||
const [selectState, selectDispatch] = useSelect();
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setIndexerSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setIndexerTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }: SelectStateInputProps) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
selectDispatch({
|
||||
type: value ? 'selectAll' : 'unselectAll',
|
||||
});
|
||||
@@ -93,7 +92,11 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={classNames(styles[name])}
|
||||
className={classNames(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
styles[name]
|
||||
)}
|
||||
name={name}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
|
@@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
|
||||
@@ -19,7 +20,7 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
|
||||
const { showSearchAction } = tableOptions;
|
||||
|
||||
const onTableOptionChangeWrapper = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
onTableOptionChange({
|
||||
tableOptions: {
|
||||
...tableOptions,
|
||||
|
@@ -11,6 +11,8 @@ function ProtocolLabel(props: ProtocolLabelProps) {
|
||||
|
||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(7053)
|
||||
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectTableOptions = createSelector(
|
||||
(state) => state.indexerIndex.tableOptions,
|
||||
(state: AppState) => state.indexerIndex.tableOptions,
|
||||
(tableOptions) => tableOptions
|
||||
);
|
||||
|
||||
|
@@ -1,26 +1,17 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector';
|
||||
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
|
||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||
import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
||||
function createIndexerIndexItemSelector(indexerId: number) {
|
||||
return createSelector(
|
||||
createIndexerSelector(indexerId),
|
||||
createIndexerSelectorForHook(indexerId),
|
||||
createIndexerAppProfileSelector(indexerId),
|
||||
createIndexerStatusSelector(indexerId),
|
||||
createUISettingsSelector(),
|
||||
(indexer: Indexer, appProfile, status, uiSettings) => {
|
||||
// If a series is deleted this selector may fire before the parent
|
||||
// selectors, which will result in an undefined series, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show a series that has no information available.
|
||||
|
||||
if (!indexer) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
indexer,
|
||||
appProfile,
|
||||
|
@@ -45,6 +45,7 @@ interface Indexer extends ModelBase {
|
||||
priority: number;
|
||||
fields: IndexerField[];
|
||||
tags: number[];
|
||||
sortName: string;
|
||||
status: IndexerStatus;
|
||||
capabilities: IndexerCapabilities;
|
||||
indexerUrls: string[];
|
||||
|
@@ -22,13 +22,13 @@ import { kinds } from 'Helpers/Props';
|
||||
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
|
||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './IndexerInfoModalContent.css';
|
||||
|
||||
function createIndexerInfoItemSelector(indexerId: number) {
|
||||
return createSelector(
|
||||
createIndexerSelector(indexerId),
|
||||
createIndexerSelectorForHook(indexerId),
|
||||
(indexer: Indexer) => {
|
||||
return {
|
||||
indexer,
|
||||
@@ -130,9 +130,13 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
{translate('IndexerSite')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={baseUrl}>
|
||||
{baseUrl.replace(/(:\/\/)api\./, '$1')}
|
||||
</Link>
|
||||
{baseUrl ? (
|
||||
<Link to={baseUrl}>
|
||||
{baseUrl.replace(/(:\/\/)api\./, '$1')}
|
||||
</Link>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemTitle>
|
||||
{protocol === 'usenet'
|
||||
|
@@ -1,10 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { CustomFilter } from 'App/State/AppState';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SearchIndexFilterModalConnector from 'Search/SearchIndexFilterModalConnector';
|
||||
|
||||
function SearchIndexFilterMenu(props) {
|
||||
interface SearchIndexFilterMenuProps {
|
||||
selectedFilterKey: string | number;
|
||||
filters: object[];
|
||||
customFilters: CustomFilter[];
|
||||
isDisabled: boolean;
|
||||
onFilterSelect(filterName: string): unknown;
|
||||
}
|
||||
|
||||
function SearchIndexFilterMenu(props: SearchIndexFilterMenuProps) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
@@ -26,15 +34,6 @@ function SearchIndexFilterMenu(props) {
|
||||
);
|
||||
}
|
||||
|
||||
SearchIndexFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SearchIndexFilterMenu.defaultProps = {
|
||||
showCustomFilters: false,
|
||||
};
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SortMenu from 'Components/Menu/SortMenu';
|
||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function SearchIndexSortMenu(props) {
|
||||
interface SearchIndexSortMenuProps {
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
isDisabled: boolean;
|
||||
onSortSelect(sortKey: string): unknown;
|
||||
}
|
||||
|
||||
function SearchIndexSortMenu(props: SearchIndexSortMenuProps) {
|
||||
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
|
||||
|
||||
return (
|
||||
@@ -97,11 +104,4 @@ function SearchIndexSortMenu(props) {
|
||||
);
|
||||
}
|
||||
|
||||
SearchIndexSortMenu.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SearchIndexSortMenu;
|
||||
|
@@ -108,7 +108,7 @@ function sort(items, state) {
|
||||
return _.orderBy(items, clauses, orders);
|
||||
}
|
||||
|
||||
function createCustomFiltersSelector(type, alternateType) {
|
||||
export function createCustomFiltersSelector(type, alternateType) {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.items,
|
||||
(customFilters) => {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import createIndexerSelector from './createIndexerSelector';
|
||||
import { createIndexerSelectorForHook } from './createIndexerSelector';
|
||||
|
||||
function createIndexerAppProfileSelector(indexerId) {
|
||||
return createSelector(
|
||||
(state) => state.settings.appProfiles.items,
|
||||
createIndexerSelector(indexerId),
|
||||
createIndexerSelectorForHook(indexerId),
|
||||
(appProfiles, indexer = {}) => {
|
||||
return appProfiles.find((profile) => {
|
||||
return profile.id === indexer.appProfileId;
|
||||
|
@@ -1,22 +1,22 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
function createIndexerSelector(id) {
|
||||
if (id == null) {
|
||||
return createSelector(
|
||||
(state, { indexerId }) => indexerId,
|
||||
(state) => state.indexers.itemMap,
|
||||
(state) => state.indexers.items,
|
||||
(indexerId, itemMap, allIndexers) => {
|
||||
return allIndexers[itemMap[indexerId]];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function createIndexerSelectorForHook(indexerId) {
|
||||
return createSelector(
|
||||
(state) => state.indexers.itemMap,
|
||||
(state) => state.indexers.items,
|
||||
(itemMap, allIndexers) => {
|
||||
return allIndexers[itemMap[id]];
|
||||
return indexerId ? allIndexers[itemMap[indexerId]]: undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createIndexerSelector() {
|
||||
return createSelector(
|
||||
(state, { indexerId }) => indexerId,
|
||||
(state) => state.indexers.itemMap,
|
||||
(state) => state.indexers.items,
|
||||
(indexerId, itemMap, allIndexers) => {
|
||||
return allIndexers[itemMap[indexerId]];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
const scrollPositions = {
|
||||
indexerIndex: 0
|
||||
};
|
||||
|
||||
export default scrollPositions;
|
5
frontend/src/Store/scrollPositions.ts
Normal file
5
frontend/src/Store/scrollPositions.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const scrollPositions: Record<string, number> = {
|
||||
indexerIndex: 0,
|
||||
};
|
||||
|
||||
export default scrollPositions;
|
@@ -1,28 +0,0 @@
|
||||
const thunks = {};
|
||||
|
||||
function identity(payload) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function createThunk(type, identityFunction = identity) {
|
||||
return function(payload = {}) {
|
||||
return function(dispatch, getState) {
|
||||
const thunk = thunks[type];
|
||||
|
||||
if (thunk) {
|
||||
return thunk(getState, identityFunction(payload), dispatch);
|
||||
}
|
||||
|
||||
throw Error(`Thunk handler has not been registered for ${type}`);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleThunks(handlers) {
|
||||
const types = Object.keys(handlers);
|
||||
|
||||
types.forEach((type) => {
|
||||
thunks[type] = handlers[type];
|
||||
});
|
||||
}
|
||||
|
39
frontend/src/Store/thunks.ts
Normal file
39
frontend/src/Store/thunks.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Dispatch } from 'redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
type GetState = () => AppState;
|
||||
type Thunk = (
|
||||
getState: GetState,
|
||||
identityFn: never,
|
||||
dispatch: Dispatch
|
||||
) => unknown;
|
||||
|
||||
const thunks: Record<string, Thunk> = {};
|
||||
|
||||
function identity<T, TResult>(payload: T): TResult {
|
||||
return payload as unknown as TResult;
|
||||
}
|
||||
|
||||
export function createThunk(type: string, identityFunction = identity) {
|
||||
return function <T>(payload?: T) {
|
||||
return function (dispatch: Dispatch, getState: GetState) {
|
||||
const thunk = thunks[type];
|
||||
|
||||
if (thunk) {
|
||||
const finalPayload = payload ?? {};
|
||||
|
||||
return thunk(getState, identityFunction(finalPayload), dispatch);
|
||||
}
|
||||
|
||||
throw Error(`Thunk handler has not been registered for ${type}`);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleThunks(handlers: Record<string, Thunk>) {
|
||||
const types = Object.keys(handlers);
|
||||
|
||||
types.forEach((type) => {
|
||||
thunks[type] = handlers[type];
|
||||
});
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function getSelectedIds(selectedState, { parseIds = true } = {}) {
|
||||
return _.reduce(selectedState, (result, value, id) => {
|
||||
if (value) {
|
||||
const parsedId = parseIds ? parseInt(id) : id;
|
||||
|
||||
result.push(parsedId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default getSelectedIds;
|
18
frontend/src/Utilities/Table/getSelectedIds.ts
Normal file
18
frontend/src/Utilities/Table/getSelectedIds.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { reduce } from 'lodash';
|
||||
import { SelectedState } from 'Helpers/Hooks/useSelectState';
|
||||
|
||||
function getSelectedIds(selectedState: SelectedState): number[] {
|
||||
return reduce(
|
||||
selectedState,
|
||||
(result: number[], value, id) => {
|
||||
if (value) {
|
||||
result.push(parseInt(id));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export default getSelectedIds;
|
6
frontend/src/typings/callbacks.ts
Normal file
6
frontend/src/typings/callbacks.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
|
||||
export type SortCallback = (
|
||||
sortKey: string,
|
||||
sortDirection: SortDirection
|
||||
) => void;
|
4
frontend/src/typings/inputs.ts
Normal file
4
frontend/src/typings/inputs.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type CheckInputChanged = {
|
||||
name: string;
|
||||
value: boolean;
|
||||
};
|
@@ -7,7 +7,15 @@
|
||||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": ["node_modules/@types", "typings"],
|
||||
"paths": {
|
||||
|
Reference in New Issue
Block a user