Typings cleanup and improvements

(cherry picked from commit b2c43fb2a67965d68d3d35b72302b0cddb5aca23)
pull/1787/head
Mark McDowall 1 year ago committed by Bogdan
parent 5764950b10
commit 4bfaab4b21

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

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

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

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

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

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

@ -0,0 +1,6 @@
import SortDirection from 'Helpers/Props/SortDirection';
export type SortCallback = (
sortKey: string,
sortDirection: SortDirection
) => void;

@ -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": {

@ -96,6 +96,9 @@
"@babel/preset-env": "7.22.9",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.5",
"@types/lodash": "4.14.194",
"@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.14.1",
"@types/react-window": "1.8.5",
"@types/webpack-livereload-plugin": "2.3.3",
"@typescript-eslint/eslint-plugin": "5.59.5",

@ -1409,6 +1409,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/history@^4.7.11":
version "4.7.11"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@ -1432,6 +1437,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@4.14.194":
version "4.14.194"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76"
integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==
"@types/minimist@^1.2.0":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
@ -1493,6 +1503,30 @@
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@5.3.3":
version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.20"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c"
integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-text-truncate@0.14.1":
version "0.14.1"
resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28"
integrity sha512-yCtOOOJzrsfWF6TbnTDZz0gM5JYOxJmewExaTJTv01E7yrmpkNcmVny2fAtsNgSFCp8k2VgCePBoIvFBpKyEOw==
dependencies:
"@types/react" "*"
"@types/react-window@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"

Loading…
Cancel
Save