From 4bfaab4b2189db3af8d91a4ed0fa007c0d9a34e2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 4 Apr 2023 09:21:34 -0700 Subject: [PATCH] Typings cleanup and improvements (cherry picked from commit b2c43fb2a67965d68d3d35b72302b0cddb5aca23) --- frontend/src/App/State/AppState.ts | 3 +- .../App/State/ClientSideCollectionAppState.ts | 8 ++ frontend/src/App/State/IndexerAppState.ts | 23 ++++- .../Components/Error/ErrorBoundaryError.tsx | 4 +- frontend/src/Components/Link/Link.js | 98 ------------------- frontend/src/Components/Link/Link.tsx | 96 ++++++++++++++++++ .../src/Components/Page/PageContentBody.tsx | 17 ++-- frontend/src/Components/Scroller/Scroller.tsx | 20 +++- .../src/Components/withScrollPosition.tsx | 28 +++--- frontend/src/Indexer/Index/IndexerIndex.tsx | 33 ++++--- .../Indexer/Index/IndexerIndexFilterModal.tsx | 16 ++- .../src/Indexer/Index/IndexerIndexFooter.tsx | 3 +- .../Index/Menus/IndexerIndexFilterMenu.tsx | 21 ++-- .../Index/Menus/IndexerIndexSortMenu.tsx | 20 ++-- .../Delete/DeleteIndexerModalContent.tsx | 17 ++-- .../Select/Edit/EditIndexerModalContent.tsx | 2 +- .../Select/IndexerIndexSelectAllButton.tsx | 2 +- .../Index/Select/IndexerIndexSelectFooter.tsx | 14 ++- .../Select/IndexerIndexSelectModeButton.tsx | 2 +- .../Index/Select/Tags/TagsModalContent.tsx | 26 +++-- .../Indexer/Index/Table/IndexerIndexRow.tsx | 13 ++- .../Indexer/Index/Table/IndexerIndexTable.tsx | 32 +++--- .../Index/Table/IndexerIndexTableHeader.tsx | 15 +-- .../Index/Table/IndexerIndexTableOptions.tsx | 3 +- .../src/Indexer/Index/Table/ProtocolLabel.tsx | 2 + .../Indexer/Index/Table/selectTableOptions.ts | 3 +- .../Index/createIndexerIndexItemSelector.ts | 13 +-- frontend/src/Indexer/Indexer.ts | 1 + .../Indexer/Info/IndexerInfoModalContent.tsx | 14 ++- .../Search/Menus/SearchIndexFilterMenu.tsx | 21 ++-- .../src/Search/Menus/SearchIndexSortMenu.tsx | 20 ++-- .../createClientSideCollectionSelector.js | 2 +- .../createIndexerAppProfileSelector.js | 4 +- .../Store/Selectors/createIndexerSelector.js | 26 ++--- frontend/src/Store/scrollPositions.js | 5 - frontend/src/Store/scrollPositions.ts | 5 + frontend/src/Store/thunks.js | 28 ------ frontend/src/Store/thunks.ts | 39 ++++++++ .../src/Utilities/Table/getSelectedIds.js | 15 --- .../src/Utilities/Table/getSelectedIds.ts | 18 ++++ frontend/src/typings/callbacks.ts | 6 ++ frontend/src/typings/inputs.ts | 4 + frontend/tsconfig.json | 8 ++ package.json | 3 + yarn.lock | 34 +++++++ 45 files changed, 468 insertions(+), 319 deletions(-) create mode 100644 frontend/src/App/State/ClientSideCollectionAppState.ts delete mode 100644 frontend/src/Components/Link/Link.js create mode 100644 frontend/src/Components/Link/Link.tsx delete mode 100644 frontend/src/Store/scrollPositions.js create mode 100644 frontend/src/Store/scrollPositions.ts delete mode 100644 frontend/src/Store/thunks.js create mode 100644 frontend/src/Store/thunks.ts delete mode 100644 frontend/src/Utilities/Table/getSelectedIds.js create mode 100644 frontend/src/Utilities/Table/getSelectedIds.ts create mode 100644 frontend/src/typings/callbacks.ts create mode 100644 frontend/src/typings/inputs.ts diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 6e868cce4..5c678abea 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -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; diff --git a/frontend/src/App/State/ClientSideCollectionAppState.ts b/frontend/src/App/State/ClientSideCollectionAppState.ts new file mode 100644 index 000000000..f4110ef73 --- /dev/null +++ b/frontend/src/App/State/ClientSideCollectionAppState.ts @@ -0,0 +1,8 @@ +import { CustomFilter } from './AppState'; + +interface ClientSideCollectionAppState { + totalItems: number; + customFilters: CustomFilter[]; +} + +export default ClientSideCollectionAppState; diff --git a/frontend/src/App/State/IndexerAppState.ts b/frontend/src/App/State/IndexerAppState.ts index ad7e778e6..df79776ff 100644 --- a/frontend/src/App/State/IndexerAppState.ts +++ b/frontend/src/App/State/IndexerAppState.ts @@ -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[]; + filters: Filter[]; + columns: Column[]; +} interface IndexerAppState extends AppSectionState, diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index b3db237b1..022cf5a45 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -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) { diff --git a/frontend/src/Components/Link/Link.js b/frontend/src/Components/Link/Link.js deleted file mode 100644 index 6b5baca4e..000000000 --- a/frontend/src/Components/Link/Link.js +++ /dev/null @@ -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; diff --git a/frontend/src/Components/Link/Link.tsx b/frontend/src/Components/Link/Link.tsx new file mode 100644 index 000000000..b0676febf --- /dev/null +++ b/frontend/src/Components/Link/Link.tsx @@ -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 { + className?: string; + component?: + | string + | FunctionComponent + | ComponentClass; + 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 & 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; diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index 75317f113..ce9b0e7e4 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -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 - ) => { + (props: PageContentBodyProps, ref: ForwardedRef) => { 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); } diff --git a/frontend/src/Components/Scroller/Scroller.tsx b/frontend/src/Components/Scroller/Scroller.tsx index 2bcb899aa..37b16eebd 100644 --- a/frontend/src/Components/Scroller/Scroller.tsx +++ b/frontend/src/Components/Scroller/Scroller.tsx @@ -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) => { + (props: ScrollerProps, ref: ForwardedRef) => { const { className, autoFocus = false, @@ -30,7 +42,7 @@ const Scroller = forwardRef( } = props; const internalRef = useRef(); - const currentRef = ref ?? internalRef; + const currentRef = (ref as MutableRefObject) ?? internalRef; useEffect( () => { diff --git a/frontend/src/Components/withScrollPosition.tsx b/frontend/src/Components/withScrollPosition.tsx index ec13c6ab8..f688a6253 100644 --- a/frontend/src/Components/withScrollPosition.tsx +++ b/frontend/src/Components/withScrollPosition.tsx @@ -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, + 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 ; } - ScrollPosition.propTypes = { - history: PropTypes.object.isRequired, - }; - return ScrollPosition; } diff --git a/frontend/src/Indexer/Index/IndexerIndex.tsx b/frontend/src/Indexer/Index/IndexerIndex.tsx index dcb7b8c9e..93bb2cd5e 100644 --- a/frontend/src/Indexer/Index/IndexerIndex.tsx +++ b/frontend/src/Indexer/Index/IndexerIndex.tsx @@ -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(); + const scrollerRef = useRef(null); const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); - const [jumpToCharacter, setJumpToCharacter] = useState(null); + const [jumpToCharacter, setJumpToCharacter] = useState( + 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, item) => { let char = item.sortName.charAt(0); if (!isNaN(Number(char))) { @@ -277,6 +282,8 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { 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 ( { + (indexers: IndexerAppState) => { return indexers.items.map((s) => { const { protocol, privacy, enable } = s; diff --git a/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx b/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx index 0b6021bad..57ebf7b2f 100644 --- a/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx +++ b/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx @@ -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, }; diff --git a/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx b/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx index 723db799f..088cbca90 100644 --- a/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx +++ b/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx @@ -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; diff --git a/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx index 0e27902fe..6a972b0d9 100644 --- a/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx @@ -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) {
- {translate('DeleteSelectedIndexersMessageText', [ - selectedIndexers.length, - ])} + {translate('DeleteSelectedIndexersMessageText', [indexers.length])}
    - {selectedIndexers.map((s) => { + {indexers.map((s) => { return (
  • {s.name} diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx index f3bb9cca7..1aba90a0b 100644 --- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx @@ -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); diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx index bd7682018..d6fc776d6 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx @@ -7,7 +7,7 @@ import translate from 'Utilities/String/translate'; interface IndexerIndexSelectAllButtonProps { label: string; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; } function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) { diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx index 953d0daf9..fe3082d0b 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx @@ -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); diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx index 31fa1d041..eea2fca08 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeButton.tsx @@ -7,7 +7,7 @@ interface IndexerIndexSelectModeButtonProps { label: string; iconName: IconDefinition; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; onPress: () => void; } diff --git a/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx index 964d9ad57..1964d271c 100644 --- a/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx @@ -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([]); 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] diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx index 5325028e9..fd148a038 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx @@ -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) ; + scrollerRef: RefObject; 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(null); + const listRef = useRef>(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} > { + (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 ( { + ({ name, value }: CheckInputChanged) => { onTableOptionChange({ tableOptions: { ...tableOptions, diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx index d1318678d..08009109e 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx @@ -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 ; } diff --git a/frontend/src/Indexer/Index/Table/selectTableOptions.ts b/frontend/src/Indexer/Index/Table/selectTableOptions.ts index 1578c2cf8..56a00866d 100644 --- a/frontend/src/Indexer/Index/Table/selectTableOptions.ts +++ b/frontend/src/Indexer/Index/Table/selectTableOptions.ts @@ -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 ); diff --git a/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts b/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts index 12d042f7a..7e6a4854b 100644 --- a/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts +++ b/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts @@ -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, diff --git a/frontend/src/Indexer/Indexer.ts b/frontend/src/Indexer/Indexer.ts index 5ce83264b..0f3ca88f4 100644 --- a/frontend/src/Indexer/Indexer.ts +++ b/frontend/src/Indexer/Indexer.ts @@ -45,6 +45,7 @@ interface Indexer extends ModelBase { priority: number; fields: IndexerField[]; tags: number[]; + sortName: string; status: IndexerStatus; capabilities: IndexerCapabilities; indexerUrls: string[]; diff --git a/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx b/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx index 9056f70f5..09091ee48 100644 --- a/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx +++ b/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx @@ -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')} - - {baseUrl.replace(/(:\/\/)api\./, '$1')} - + {baseUrl ? ( + + {baseUrl.replace(/(:\/\/)api\./, '$1')} + + ) : ( + '-' + )} {protocol === 'usenet' diff --git a/frontend/src/Search/Menus/SearchIndexFilterMenu.tsx b/frontend/src/Search/Menus/SearchIndexFilterMenu.tsx index 52806ff83..9a8c243b4 100644 --- a/frontend/src/Search/Menus/SearchIndexFilterMenu.tsx +++ b/frontend/src/Search/Menus/SearchIndexFilterMenu.tsx @@ -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, }; diff --git a/frontend/src/Search/Menus/SearchIndexSortMenu.tsx b/frontend/src/Search/Menus/SearchIndexSortMenu.tsx index 302ef6a10..af4042283 100644 --- a/frontend/src/Search/Menus/SearchIndexSortMenu.tsx +++ b/frontend/src/Search/Menus/SearchIndexSortMenu.tsx @@ -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; diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js index ae1031dca..1bac14f08 100644 --- a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -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) => { diff --git a/frontend/src/Store/Selectors/createIndexerAppProfileSelector.js b/frontend/src/Store/Selectors/createIndexerAppProfileSelector.js index 683f0419b..dabb98b2e 100644 --- a/frontend/src/Store/Selectors/createIndexerAppProfileSelector.js +++ b/frontend/src/Store/Selectors/createIndexerAppProfileSelector.js @@ -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; diff --git a/frontend/src/Store/Selectors/createIndexerSelector.js b/frontend/src/Store/Selectors/createIndexerSelector.js index 220f9b15e..566678313 100644 --- a/frontend/src/Store/Selectors/createIndexerSelector.js +++ b/frontend/src/Store/Selectors/createIndexerSelector.js @@ -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]]; } ); } diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js deleted file mode 100644 index 6aeed381f..000000000 --- a/frontend/src/Store/scrollPositions.js +++ /dev/null @@ -1,5 +0,0 @@ -const scrollPositions = { - indexerIndex: 0 -}; - -export default scrollPositions; diff --git a/frontend/src/Store/scrollPositions.ts b/frontend/src/Store/scrollPositions.ts new file mode 100644 index 000000000..48fc68535 --- /dev/null +++ b/frontend/src/Store/scrollPositions.ts @@ -0,0 +1,5 @@ +const scrollPositions: Record = { + indexerIndex: 0, +}; + +export default scrollPositions; diff --git a/frontend/src/Store/thunks.js b/frontend/src/Store/thunks.js deleted file mode 100644 index ebcf10917..000000000 --- a/frontend/src/Store/thunks.js +++ /dev/null @@ -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]; - }); -} - diff --git a/frontend/src/Store/thunks.ts b/frontend/src/Store/thunks.ts new file mode 100644 index 000000000..fd277211e --- /dev/null +++ b/frontend/src/Store/thunks.ts @@ -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 = {}; + +function identity(payload: T): TResult { + return payload as unknown as TResult; +} + +export function createThunk(type: string, identityFunction = identity) { + return function (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) { + const types = Object.keys(handlers); + + types.forEach((type) => { + thunks[type] = handlers[type]; + }); +} diff --git a/frontend/src/Utilities/Table/getSelectedIds.js b/frontend/src/Utilities/Table/getSelectedIds.js deleted file mode 100644 index 705f13a5d..000000000 --- a/frontend/src/Utilities/Table/getSelectedIds.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Table/getSelectedIds.ts b/frontend/src/Utilities/Table/getSelectedIds.ts new file mode 100644 index 000000000..b84db6245 --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.ts @@ -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; diff --git a/frontend/src/typings/callbacks.ts b/frontend/src/typings/callbacks.ts new file mode 100644 index 000000000..0114efeb0 --- /dev/null +++ b/frontend/src/typings/callbacks.ts @@ -0,0 +1,6 @@ +import SortDirection from 'Helpers/Props/SortDirection'; + +export type SortCallback = ( + sortKey: string, + sortDirection: SortDirection +) => void; diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts new file mode 100644 index 000000000..c0fda305c --- /dev/null +++ b/frontend/src/typings/inputs.ts @@ -0,0 +1,4 @@ +export type CheckInputChanged = { + name: string; + value: boolean; +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index dfddb15a3..354e2a5aa 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -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": { diff --git a/package.json b/package.json index 74a1e4694..ba608bfd1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 39a164da6..3936f0275 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"