diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index cc26a2633..ddc7300fd 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -359,11 +359,16 @@ module.exports = { ], rules: Object.assign(typescriptEslintRecommended.rules, { - 'no-shadow': 'off', - // These should be enabled after cleaning things up - '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'after-used', + argsIgnorePattern: '^_', + ignoreRestSiblings: true + } + ], '@typescript-eslint/explicit-function-return-type': 'off', - 'react/prop-types': 'off', + 'no-shadow': 'off', 'prettier/prettier': 'error', 'simple-import-sort/imports': [ 'error', @@ -376,7 +381,41 @@ module.exports = { ['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$'] ] } - ] + ], + + // React Hooks + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + + // React + 'react/function-component-definition': 'error', + 'react/hook-use-state': 'error', + 'react/jsx-boolean-value': ['error', 'always'], + 'react/jsx-curly-brace-presence': [ + 'error', + { props: 'never', children: 'never' } + ], + 'react/jsx-fragments': 'error', + 'react/jsx-handler-names': [ + 'error', + { + eventHandlerPrefix: 'on', + eventHandlerPropPrefix: 'on' + } + ], + 'react/jsx-no-bind': ['error', { ignoreRefs: true }], + 'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }], + 'react/jsx-pascal-case': ['error', { allowAllCaps: true }], + 'react/jsx-sort-props': [ + 'error', + { + callbacksLast: true, + noSortAlphabetically: true, + reservedFirst: true + } + ], + 'react/prop-types': 'off', + 'react/self-closing-comp': 'error' }) }, { diff --git a/frontend/src/Activity/Queue/QueueOptions.tsx b/frontend/src/Activity/Queue/QueueOptions.tsx index 615f6922a..17a6ac1fe 100644 --- a/frontend/src/Activity/Queue/QueueOptions.tsx +++ b/frontend/src/Activity/Queue/QueueOptions.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; import FormGroup from 'Components/Form/FormGroup'; @@ -31,19 +31,17 @@ function QueueOptions() { ); return ( - - - {translate('ShowUnknownSeriesItems')} + + {translate('ShowUnknownSeriesItems')} - - - + + ); } diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index f66a4df40..e3bf426c9 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -35,6 +35,10 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import MissingConnector from 'Wanted/Missing/MissingConnector'; +function RedirectWithUrlBase() { + return ; +} + function AppRoutes() { return ( @@ -51,9 +55,7 @@ function AppRoutes() { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore addUrlBase={false} - render={() => { - return ; - }} + render={RedirectWithUrlBase} /> )} @@ -61,21 +63,9 @@ function AppRoutes() { - { - return ; - }} - /> + - { - return ; - }} - /> + diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 14bd8a87f..870b28058 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -64,7 +64,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
{info.componentStack}
)} - {
Version: {window.Sonarr.version}
} +
Version: {window.Sonarr.version}
); diff --git a/frontend/src/Episode/EpisodeNumber.tsx b/frontend/src/Episode/EpisodeNumber.tsx index 596174499..73afdc70a 100644 --- a/frontend/src/Episode/EpisodeNumber.tsx +++ b/frontend/src/Episode/EpisodeNumber.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import Icon from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; @@ -82,9 +82,7 @@ function EpisodeNumber(props: EpisodeNumberProps) { - {showSeasonNumber && seasonNumber != null && ( - {seasonNumber}x - )} + {showSeasonNumber && seasonNumber != null && <>{seasonNumber}x} {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} @@ -111,9 +109,7 @@ function EpisodeNumber(props: EpisodeNumberProps) { /> ) : ( - {showSeasonNumber && seasonNumber != null && ( - {seasonNumber}x - )} + {showSeasonNumber && seasonNumber != null && <>{seasonNumber}x} {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber} diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts index f5b5a96f0..24cffb2f1 100644 --- a/frontend/src/Helpers/Hooks/useModalOpenState.ts +++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts @@ -3,15 +3,15 @@ import { useCallback, useState } from 'react'; export default function useModalOpenState( initialState: boolean ): [boolean, () => void, () => void] { - const [isOpen, setOpen] = useState(initialState); + const [isOpen, setIsOpen] = useState(initialState); const setModalOpen = useCallback(() => { - setOpen(true); - }, [setOpen]); + setIsOpen(true); + }, [setIsOpen]); const setModalClosed = useCallback(() => { - setOpen(false); - }, [setOpen]); + setIsOpen(false); + }, [setIsOpen]); return [isOpen, setModalOpen, setModalClosed]; } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index dbcd10613..990e0dfab 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -857,7 +857,7 @@ function InteractiveImportModalContent( @@ -865,7 +865,7 @@ function InteractiveImportModalContent( @@ -945,7 +945,7 @@ function InteractiveImportModalContent( + > = ({ - index, - style, - data, -}) => { +function Row({ index, style, data }: ListChildComponentProps) { const { items, columns, onSeriesSelect } = data; + const series = index >= items.length ? null : items[index]; - if (index >= items.length) { + const handlePress = useCallback(() => { + if (series?.id) { + onSeriesSelect(series.id); + } + }, [series?.id, onSeriesSelect]); + + if (series == null) { return null; } - const series = items[index]; - return ( > = ({ justifyContent: 'space-between', ...style, }} - onPress={() => onSeriesSelect(series.id)} + onPress={handlePress} > > = ({ /> ); -}; +} function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { const { modalTitle, onSeriesSelect, onModalClose } = props; @@ -197,9 +198,9 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) { /> diff --git a/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx b/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx index 81bf86e59..7d623decd 100644 --- a/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx +++ b/frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx @@ -17,7 +17,7 @@ function SelectDownloadClientModal(props: SelectDownloadClientModalProps) { props; return ( - + + <> - + ); } diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx index a1d0b7076..f4e05014b 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviews.tsx @@ -42,11 +42,7 @@ interface SeriesIndexOverviewsProps { isSmallScreen: boolean; } -const Row: React.FC> = ({ - index, - style, - data, -}) => { +function Row({ index, style, data }: ListChildComponentProps) { const { items, ...otherData } = data; if (index >= items.length) { @@ -60,7 +56,7 @@ const Row: React.FC> = ({ ); -}; +} function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx index 055685216..b4f859f84 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPosters.tsx @@ -60,12 +60,12 @@ const seriesIndexSelector = createSelector( } ); -const Cell: React.FC> = ({ +function Cell({ columnIndex, rowIndex, style, data, -}) => { +}: GridChildComponentProps) { const { layout, items, sortKey, isSelectMode } = data; const { columnCount, padding, posterWidth, posterHeight } = layout; const index = rowIndex * columnCount + columnIndex; @@ -92,7 +92,7 @@ const Cell: React.FC> = ({ /> ); -}; +} function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Series/Index/Table/SeriesIndexTable.tsx b/frontend/src/Series/Index/Table/SeriesIndexTable.tsx index c1401f984..e6b4ca010 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTable.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTable.tsx @@ -45,11 +45,7 @@ const columnsSelector = createSelector( (columns) => columns ); -const Row: React.FC> = ({ - index, - style, - data, -}) => { +function Row({ index, style, data }: ListChildComponentProps) { const { items, sortKey, columns, isSelectMode } = data; if (index >= items.length) { @@ -75,7 +71,7 @@ const Row: React.FC> = ({ /> ); -}; +} function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx b/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx index df648cadb..0a9cc1d17 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTableOptions.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -32,7 +32,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) { ); return ( - + <> {translate('ShowBanners')} @@ -56,7 +56,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) { onChange={onTableOptionChangeWrapper} /> - + ); } diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx index fee176554..cc02a2a9a 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import PageContent from 'Components/Page/PageContent'; @@ -17,11 +17,11 @@ function CustomFormatSettingsPage() { // @ts-ignore showSave={false} additionalButtons={ - + <> - + } /> diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index 2722f02fa..a788d824e 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -231,9 +231,9 @@ function ManageDownloadClientsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} - onSelectAllChange={onSelectAllChange} sortKey={sortKey} sortDirection={sortDirection} + onSelectAllChange={onSelectAllChange} onSortPress={onSortPress} > @@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent( @@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { + ) : null} ) : ( - + <>
@@ -171,7 +165,7 @@ function Updates() { } />
-
+ )} {isFetching ? (