diff --git a/.devcontainer/Prowlarr.code-workspace b/.devcontainer/Prowlarr.code-workspace deleted file mode 100644 index a46158e44..000000000 --- a/.devcontainer/Prowlarr.code-workspace +++ /dev/null @@ -1,13 +0,0 @@ -// This file is used to open the backend and frontend in the same workspace, which is necessary as -// the frontend has vscode settings that are distinct from the backend -{ - "folders": [ - { - "path": ".." - }, - { - "path": "../frontend" - } - ], - "settings": {} -} diff --git a/.github/labeler.yml b/.github/labeler.yml index 74160b634..21aacef8c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,31 +1,19 @@ 'Area: API': - - changed-files: - - any-glob-to-any-file: - - src/Prowlarr.Api.V1/**/* + - src/Prowlarr.Api.V1/**/* 'Area: Db-migration': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Datastore/Migration/* + - src/NzbDrone.Core/Datastore/Migration/* 'Area: Download Clients': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Download/Clients/**/* + - src/NzbDrone.Core/Download/Clients/**/* 'Area: Indexer': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Indexers/**/* + - src/NzbDrone.Core/Indexers/**/* 'Area: Notifications': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Notifications/**/* + - src/NzbDrone.Core/Notifications/**/* 'Area: UI': - - changed-files: - - any-glob-to-any-file: - - frontend/**/* - - package.json - - yarn.lock \ No newline at end of file + - frontend/**/* + - package.json + - yarn.lock diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index ab2292824..857cfb4a7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,4 +9,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v4 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 1d50cb1f1..cf38066c5 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: '90' diff --git a/Logo/dottrace.svg b/Logo/dottrace.svg new file mode 100644 index 000000000..b879517cd --- /dev/null +++ b/Logo/dottrace.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/jetbrains.svg b/Logo/jetbrains.svg new file mode 100644 index 000000000..75d4d2177 --- /dev/null +++ b/Logo/jetbrains.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/resharper.svg b/Logo/resharper.svg new file mode 100644 index 000000000..24c987a78 --- /dev/null +++ b/Logo/resharper.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/rider.svg b/Logo/rider.svg new file mode 100644 index 000000000..82da35b0b --- /dev/null +++ b/Logo/rider.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + rider + + + + + + + + + + + + + + diff --git a/Logo/webstorm.svg b/Logo/webstorm.svg new file mode 100644 index 000000000..39ab7eb97 --- /dev/null +++ b/Logo/webstorm.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index e8c60546a..4973be54d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Prowlarr [![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop) -[![Translation status](https://translate.servarr.com/widget/servarr/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget) +[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget) [![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation/docker) ![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg) [![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers) @@ -68,16 +68,16 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## JetBrains -Thank you to [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. +Thank you to [JetBrains JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. -* [ReSharper ReSharper](http://www.jetbrains.com/resharper/) -* [WebStorm WebStorm](http://www.jetbrains.com/webstorm/) -* [Rider Rider](http://www.jetbrains.com/rider/) -* [dotTrace dotTrace](http://www.jetbrains.com/dottrace/) +- [ReSharper ReSharper](http://www.jetbrains.com/resharper/) +- [WebStorm WebStorm](http://www.jetbrains.com/webstorm/) +- [Rider Rider](http://www.jetbrains.com/rider/) +- [dotTrace dotTrace](http://www.jetbrains.com/dottrace/) ### License - [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -- Copyright 2010-2024 +- Copyright 2010-2022 Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dc667e803..4955d0476 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,18 +9,18 @@ variables: testsFolder: './_tests' yarnCacheFolder: $(Pipeline.Workspace)/.yarn nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages - majorVersion: '1.35.0' + majorVersion: '1.16.1' minorVersion: $[counter('minorVersion', 1)] prowlarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' sentryOrg: 'servarr' sentryUrl: 'https://sentry.servarr.com' - dotnetVersion: '6.0.427' + dotnetVersion: '6.0.417' nodeVersion: '20.X' innoVersion: '6.2.2' windowsImage: 'windows-2022' - linuxImage: 'ubuntu-22.04' - macImage: 'macOS-13' + linuxImage: 'ubuntu-20.04' + macImage: 'macOS-11' trigger: branches: @@ -166,10 +166,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: UseNode@1 + - task: NodeTool@0 displayName: Set Node.js version inputs: - version: $(nodeVersion) + versionSpec: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -1075,10 +1075,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: UseNode@1 + - task: NodeTool@0 displayName: Set Node.js version inputs: - version: $(nodeVersion) + versionSpec: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -1169,12 +1169,12 @@ stages: submodules: true - powershell: Set-Service SCardSvr -StartupType Manual displayName: Enable Windows Test Service - - task: SonarCloudPrepare@3 + - task: SonarCloudPrepare@1 condition: eq(variables['System.PullRequest.IsFork'], 'False') inputs: SonarCloud: 'SonarCloud' organization: 'prowlarr' - scannerMode: 'dotnet' + scannerMode: 'MSBuild' projectKey: 'Prowlarr_Prowlarr' projectName: 'Prowlarr' projectVersion: '$(prowlarrVersion)' @@ -1187,16 +1187,21 @@ stages: ./build.sh --backend -f net6.0 -r win-x64 TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage displayName: Coverage Unit Tests - - task: SonarCloudAnalyze@3 + - task: SonarCloudAnalyze@1 condition: eq(variables['System.PullRequest.IsFork'], 'False') displayName: Publish SonarCloud Results - - task: reportgenerator@5.3.11 + - task: reportgenerator@4 displayName: Generate Coverage Report inputs: reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined' reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges' - publishCodeCoverageResults: true + - task: PublishCodeCoverageResults@1 + displayName: Publish Coverage Report + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: './CoverageResults/combined/Cobertura.xml' + reportDirectory: './CoverageResults/combined/' - stage: Report_Out dependsOn: diff --git a/docs.sh b/docs.sh old mode 100755 new mode 100644 index 38b0e0fbc..ae11bc83f --- a/docs.sh +++ b/docs.sh @@ -1,18 +1,13 @@ -#!/bin/bash -set -e - -FRAMEWORK="net6.0" PLATFORM=$1 -ARCHITECTURE="${2:-x64}" if [ "$PLATFORM" = "Windows" ]; then - RUNTIME="win-$ARCHITECTURE" + RUNTIME="win-x64" elif [ "$PLATFORM" = "Linux" ]; then - RUNTIME="linux-$ARCHITECTURE" + RUNTIME="linux-x64" elif [ "$PLATFORM" = "Mac" ]; then - RUNTIME="osx-$ARCHITECTURE" + RUNTIME="osx-x64" else - echo "Platform must be provided as first argument: Windows, Linux or Mac" + echo "Platform must be provided as first arguement: Windows, Linux or Mac" exit 1 fi @@ -26,23 +21,17 @@ slnFile=src/Prowlarr.sln platform=Posix - if [ "$PLATFORM" = "Windows" ]; then - application=Prowlarr.Console.dll -else - application=Prowlarr.dll -fi - dotnet clean $slnFile -c Debug dotnet clean $slnFile -c Release dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids dotnet new tool-manifest -dotnet tool install --version 7.3.2 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 & +dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 & -sleep 45 +sleep 30 kill %1 diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 56eaaeaab..8d844cb8b 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -357,16 +357,11 @@ module.exports = { ], rules: Object.assign(typescriptEslintRecommended.rules, { - '@typescript-eslint/no-unused-vars': [ - 'error', - { - args: 'after-used', - argsIgnorePattern: '^_', - ignoreRestSiblings: true - } - ], - '@typescript-eslint/explicit-function-return-type': 'off', 'no-shadow': 'off', + // These should be enabled after cleaning things up + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + 'react/prop-types': 'off', 'prettier/prettier': 'error', 'simple-import-sort/imports': [ 'error', @@ -379,41 +374,7 @@ 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/build/webpack.config.js b/frontend/build/webpack.config.js index ceacc4f04..5336d6583 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -25,7 +25,6 @@ module.exports = (env) => { const config = { mode: isProduction ? 'production' : 'development', devtool: isProduction ? 'source-map' : 'eval-source-map', - target: 'web', stats: { children: false @@ -66,7 +65,7 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: isProduction ? '[name]-[contenthash].js' : '[name].js', + filename: '[name]-[contenthash].js', sourceMapFilename: '[file].map' }, @@ -91,7 +90,7 @@ module.exports = (env) => { new MiniCssExtractPlugin({ filename: 'Content/styles.css', - chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css' + chunkFilename: 'Content/[id]-[chunkhash].css' }), new HtmlWebpackPlugin({ @@ -170,7 +169,7 @@ module.exports = (env) => { loose: true, debug: false, useBuiltIns: 'entry', - corejs: '3.39' + corejs: 3 } ] ] @@ -191,7 +190,7 @@ module.exports = (env) => { options: { importLoaders: 1, modules: { - localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]' + localIdentName: '[name]/[local]/[hash:base64:5]' } } }, diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 89db00f8c..f657adf28 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -16,7 +16,6 @@ const mixinsFiles = [ module.exports = { plugins: [ - 'autoprefixer', ['postcss-mixins', { mixinsFiles }], diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.js similarity index 57% rename from frontend/src/App/App.tsx rename to frontend/src/App/App.js index dba90a697..1eea6e082 100644 --- a/frontend/src/App/App.tsx +++ b/frontend/src/App/App.js @@ -1,30 +1,31 @@ -import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; +import { ConnectedRouter } from 'connected-react-router'; +import PropTypes from 'prop-types'; import React from 'react'; import DocumentTitle from 'react-document-title'; import { Provider } from 'react-redux'; -import { Store } from 'redux'; import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -interface AppProps { - store: Store; - history: ConnectedRouterProps['history']; -} - -function App({ store, history }: AppProps) { +function App({ store, history }) { return ( - - - - + + + + + ); } +App.propTypes = { + store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; + export default App; diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js new file mode 100644 index 000000000..f7a578da2 --- /dev/null +++ b/frontend/src/App/AppRoutes.js @@ -0,0 +1,184 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; +import NotFound from 'Components/NotFound'; +import Switch from 'Components/Router/Switch'; +import HistoryConnector from 'History/HistoryConnector'; +import IndexerIndex from 'Indexer/Index/IndexerIndex'; +import IndexerStats from 'Indexer/Stats/IndexerStats'; +import SearchIndexConnector from 'Search/SearchIndexConnector'; +import ApplicationSettings from 'Settings/Applications/ApplicationSettings'; +import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector'; +import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; +import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; +import IndexerSettings from 'Settings/Indexers/IndexerSettings'; +import NotificationSettings from 'Settings/Notifications/NotificationSettings'; +import Settings from 'Settings/Settings'; +import TagSettings from 'Settings/Tags/TagSettings'; +import UISettingsConnector from 'Settings/UI/UISettingsConnector'; +import BackupsConnector from 'System/Backup/BackupsConnector'; +import LogsTableConnector from 'System/Events/LogsTableConnector'; +import Logs from 'System/Logs/Logs'; +import Status from 'System/Status/Status'; +import Tasks from 'System/Tasks/Tasks'; +import UpdatesConnector from 'System/Updates/UpdatesConnector'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; + +function AppRoutes(props) { + const { + app + } = props; + + return ( + + {/* + Indexers + */} + + + + { + window.Prowlarr.urlBase && + { + return ( + + ); + }} + /> + } + + + + {/* + Search + */} + + + + {/* + Activity + */} + + + + {/* + Settings + */} + + + + + + + + + + + + + + + + + + + + {/* + System + */} + + + + + + + + + + + + + + {/* + Not Found + */} + + + + ); +} + +AppRoutes.propTypes = { + app: PropTypes.func.isRequired +}; + +export default AppRoutes; diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx deleted file mode 100644 index d451a12fb..000000000 --- a/frontend/src/App/AppRoutes.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import { Redirect, Route } from 'react-router-dom'; -import NotFound from 'Components/NotFound'; -import Switch from 'Components/Router/Switch'; -import HistoryConnector from 'History/HistoryConnector'; -import IndexerIndex from 'Indexer/Index/IndexerIndex'; -import IndexerStats from 'Indexer/Stats/IndexerStats'; -import SearchIndexConnector from 'Search/SearchIndexConnector'; -import ApplicationSettings from 'Settings/Applications/ApplicationSettings'; -import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector'; -import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; -import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; -import IndexerSettings from 'Settings/Indexers/IndexerSettings'; -import NotificationSettings from 'Settings/Notifications/NotificationSettings'; -import Settings from 'Settings/Settings'; -import TagSettings from 'Settings/Tags/TagSettings'; -import UISettingsConnector from 'Settings/UI/UISettingsConnector'; -import BackupsConnector from 'System/Backup/BackupsConnector'; -import LogsTableConnector from 'System/Events/LogsTableConnector'; -import Logs from 'System/Logs/Logs'; -import Status from 'System/Status/Status'; -import Tasks from 'System/Tasks/Tasks'; -import Updates from 'System/Updates/Updates'; -import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; - -function RedirectWithUrlBase() { - return ; -} - -function AppRoutes() { - return ( - - {/* - Indexers - */} - - - - {window.Prowlarr.urlBase && ( - - )} - - - - {/* - Search - */} - - - - {/* - Activity - */} - - - - {/* - Settings - */} - - - - - - - - - - - - - - - - - - - - {/* - System - */} - - - - - - - - - - - - - - {/* - Not Found - */} - - - - ); -} - -export default AppRoutes; diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js new file mode 100644 index 000000000..abc7f8832 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector'; + +function AppUpdatedModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + ); +} + +AppUpdatedModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModal.tsx b/frontend/src/App/AppUpdatedModal.tsx deleted file mode 100644 index 696d36fb2..000000000 --- a/frontend/src/App/AppUpdatedModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { useCallback } from 'react'; -import Modal from 'Components/Modal/Modal'; -import AppUpdatedModalContent from './AppUpdatedModalContent'; - -interface AppUpdatedModalProps { - isOpen: boolean; - onModalClose: (...args: unknown[]) => unknown; -} - -function AppUpdatedModal(props: AppUpdatedModalProps) { - const { isOpen, onModalClose } = props; - - const handleModalClose = useCallback(() => { - location.reload(); - }, []); - - return ( - - - - ); -} - -export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js new file mode 100644 index 000000000..a21afbc5a --- /dev/null +++ b/frontend/src/App/AppUpdatedModalConnector.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import AppUpdatedModal from './AppUpdatedModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(null, createMapDispatchToProps)(AppUpdatedModal); diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js new file mode 100644 index 000000000..8cce1bc16 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { kinds } from 'Helpers/Props'; +import UpdateChanges from 'System/Updates/UpdateChanges'; +import translate from 'Utilities/String/translate'; +import styles from './AppUpdatedModalContent.css'; + +function mergeUpdates(items, version, prevVersion) { + let installedIndex = items.findIndex((u) => u.version === version); + let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion); + + if (installedIndex === -1) { + installedIndex = 0; + } + + if (installedPreviouslyIndex === -1) { + installedPreviouslyIndex = items.length; + } else if (installedPreviouslyIndex === installedIndex && items.length) { + installedPreviouslyIndex += 1; + } + + const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); + + if (!appliedUpdates.length) { + return null; + } + + const appliedChanges = { new: [], fixed: [] }; + appliedUpdates.forEach((u) => { + if (u.changes) { + appliedChanges.new.push(... u.changes.new); + appliedChanges.fixed.push(... u.changes.fixed); + } + }); + + const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges }); + + if (!appliedChanges.new.length && !appliedChanges.fixed.length) { + mergedUpdate.changes = null; + } + + return mergedUpdate; +} + +function AppUpdatedModalContent(props) { + const { + version, + prevVersion, + isPopulated, + error, + items, + onSeeChangesPress, + onModalClose + } = props; + + const update = mergeUpdates(items, version, prevVersion); + + return ( + + + {translate('AppUpdated')} + + + +
+ +
+ + { + isPopulated && !error && !!update && +
+ { + !update.changes && +
{translate('MaintenanceRelease')}
+ } + + { + !!update.changes && +
+
+ {translate('WhatsNew')} +
+ + + + +
+ } +
+ } + + { + !isPopulated && !error && + + } +
+ + + + + + +
+ ); +} + +AppUpdatedModalContent.propTypes = { + version: PropTypes.string.isRequired, + prevVersion: PropTypes.string, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onSeeChangesPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContent.tsx b/frontend/src/App/AppUpdatedModalContent.tsx deleted file mode 100644 index 0bd5df6d3..000000000 --- a/frontend/src/App/AppUpdatedModalContent.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import usePrevious from 'Helpers/Hooks/usePrevious'; -import { kinds } from 'Helpers/Props'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import UpdateChanges from 'System/Updates/UpdateChanges'; -import Update from 'typings/Update'; -import translate from 'Utilities/String/translate'; -import AppState from './State/AppState'; -import styles from './AppUpdatedModalContent.css'; - -function mergeUpdates(items: Update[], version: string, prevVersion?: string) { - let installedIndex = items.findIndex((u) => u.version === version); - let installedPreviouslyIndex = items.findIndex( - (u) => u.version === prevVersion - ); - - if (installedIndex === -1) { - installedIndex = 0; - } - - if (installedPreviouslyIndex === -1) { - installedPreviouslyIndex = items.length; - } else if (installedPreviouslyIndex === installedIndex && items.length) { - installedPreviouslyIndex += 1; - } - - const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); - - if (!appliedUpdates.length) { - return null; - } - - const appliedChanges: Update['changes'] = { new: [], fixed: [] }; - - appliedUpdates.forEach((u: Update) => { - if (u.changes) { - appliedChanges.new.push(...u.changes.new); - appliedChanges.fixed.push(...u.changes.fixed); - } - }); - - const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], { - changes: appliedChanges, - }); - - if (!appliedChanges.new.length && !appliedChanges.fixed.length) { - mergedUpdate.changes = null; - } - - return mergedUpdate; -} - -interface AppUpdatedModalContentProps { - onModalClose: () => void; -} - -function AppUpdatedModalContent(props: AppUpdatedModalContentProps) { - const dispatch = useDispatch(); - const { version, prevVersion } = useSelector((state: AppState) => state.app); - const { isPopulated, error, items } = useSelector( - (state: AppState) => state.system.updates - ); - const previousVersion = usePrevious(version); - - const { onModalClose } = props; - - const update = mergeUpdates(items, version, prevVersion); - - const handleSeeChangesPress = useCallback(() => { - window.location.href = `${window.Prowlarr.urlBase}/system/updates`; - }, []); - - useEffect(() => { - dispatch(fetchUpdates()); - }, [dispatch]); - - useEffect(() => { - if (version !== previousVersion) { - dispatch(fetchUpdates()); - } - }, [version, previousVersion, dispatch]); - - return ( - - {translate('AppUpdated')} - - -
- -
- - {isPopulated && !error && !!update ? ( -
- {update.changes ? ( -
- {translate('MaintenanceRelease')} -
- ) : null} - - {update.changes ? ( -
-
{translate('WhatsNew')}
- - - - -
- ) : null} -
- ) : null} - - {!isPopulated && !error ? : null} -
- - - - - - -
- ); -} - -export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js new file mode 100644 index 000000000..97dd0aeb9 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContentConnector.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import AppUpdatedModalContent from './AppUpdatedModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.version, + (state) => state.app.prevVersion, + (state) => state.system.updates, + (version, prevVersion, updates) => { + const { + isPopulated, + error, + items + } = updates; + + return { + version, + prevVersion, + isPopulated, + error, + items + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchUpdates() { + dispatch(fetchUpdates()); + }, + + onSeeChangesPress() { + window.location = `${window.Prowlarr.urlBase}/system/updates`; + } + }; +} + +class AppUpdatedModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchUpdates(); + } + + componentDidUpdate(prevProps) { + if (prevProps.version !== this.props.version) { + this.props.dispatchFetchUpdates(); + } + } + + // + // Render + + render() { + const { + dispatchFetchUpdates, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +AppUpdatedModalContentConnector.propTypes = { + version: PropTypes.string.isRequired, + dispatchFetchUpdates: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector); diff --git a/frontend/src/App/ApplyTheme.js b/frontend/src/App/ApplyTheme.js new file mode 100644 index 000000000..bd4d6a6c8 --- /dev/null +++ b/frontend/src/App/ApplyTheme.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React, { Fragment, useCallback, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import themes from 'Styles/Themes'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.ui.item.theme || window.Prowlarr.theme, + ( + theme + ) => { + return { + theme + }; + } + ); +} + +function ApplyTheme({ theme, children }) { + // Update the CSS Variables + + const updateCSSVariables = useCallback(() => { + const arrayOfVariableKeys = Object.keys(themes[theme]); + const arrayOfVariableValues = Object.values(themes[theme]); + + // Loop through each array key and set the CSS Variables + arrayOfVariableKeys.forEach((cssVariableKey, index) => { + // Based on our snippet from MDN + document.documentElement.style.setProperty( + `--${cssVariableKey}`, + arrayOfVariableValues[index] + ); + }); + }, [theme]); + + // On Component Mount and Component Update + useEffect(() => { + updateCSSVariables(theme); + }, [updateCSSVariables, theme]); + + return {children}; +} + +ApplyTheme.propTypes = { + theme: PropTypes.string.isRequired, + children: PropTypes.object.isRequired +}; + +export default connect(createMapStateToProps)(ApplyTheme); diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx deleted file mode 100644 index ec9cd037f..000000000 --- a/frontend/src/App/ApplyTheme.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import themes from 'Styles/Themes'; -import AppState from './State/AppState'; - -function createThemeSelector() { - return createSelector( - (state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme, - (theme) => { - return theme; - } - ); -} - -function ApplyTheme() { - const theme = useSelector(createThemeSelector()); - - const updateCSSVariables = useCallback(() => { - Object.entries(themes[theme]).forEach(([key, value]) => { - document.documentElement.style.setProperty(`--${key}`, value); - }); - }, [theme]); - - // On Component Mount and Component Update - useEffect(() => { - updateCSSVariables(); - }, [updateCSSVariables, theme]); - - return null; -} - -export default ApplyTheme; diff --git a/frontend/src/App/ColorImpairedContext.ts b/frontend/src/App/ColorImpairedContext.js similarity index 100% rename from frontend/src/App/ColorImpairedContext.ts rename to frontend/src/App/ColorImpairedContext.js diff --git a/frontend/src/App/ConnectionLostModal.tsx b/frontend/src/App/ConnectionLostModal.js similarity index 54% rename from frontend/src/App/ConnectionLostModal.tsx rename to frontend/src/App/ConnectionLostModal.js index f08f2c0e2..5c08f491f 100644 --- a/frontend/src/App/ConnectionLostModal.tsx +++ b/frontend/src/App/ConnectionLostModal.js @@ -1,4 +1,5 @@ -import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import Button from 'Components/Link/Button'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; @@ -9,31 +10,36 @@ import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './ConnectionLostModal.css'; -interface ConnectionLostModalProps { - isOpen: boolean; -} - -function ConnectionLostModal(props: ConnectionLostModalProps) { - const { isOpen } = props; - - const handleModalClose = useCallback(() => { - location.reload(); - }, []); +function ConnectionLostModal(props) { + const { + isOpen, + onModalClose + } = props; return ( - - - {translate('ConnectionLost')} + + + + {translate('ConnectionLost')} + -
{translate('ConnectionLostToBackend')}
+
+ {translate('ConnectionLostToBackend')} +
{translate('ConnectionLostReconnect')}
- @@ -42,4 +48,9 @@ function ConnectionLostModal(props: ConnectionLostModalProps) { ); } +ConnectionLostModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + export default ConnectionLostModal; diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js new file mode 100644 index 000000000..8ab8e3cd0 --- /dev/null +++ b/frontend/src/App/ConnectionLostModalConnector.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import ConnectionLostModal from './ConnectionLostModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal); diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index f89eb25f7..cabc39b1c 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,6 +1,5 @@ -import Column from 'Components/Table/Column'; import SortDirection from 'Helpers/Props/SortDirection'; -import { FilterBuilderProp, PropertyFilter } from './AppState'; +import { FilterBuilderProp } from './AppState'; export interface Error { responseJSON: { @@ -19,18 +18,10 @@ export interface AppSectionSaveState { } export interface PagedAppSectionState { - page: number; pageSize: number; - totalPages: number; - totalRecords?: number; -} -export interface TableAppSectionState { - columns: Column[]; } export interface AppSectionFilterState { - selectedFilterKey: string; - filters: PropertyFilter[]; filterBuilderProps: FilterBuilderProp[]; } @@ -47,7 +38,6 @@ export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; error: Error; - pendingChanges: Partial; item: T; } diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 0f0e82c0d..2490f739f 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -42,20 +42,7 @@ export interface CustomFilter { filers: PropertyFilter[]; } -export interface AppSectionState { - isConnected: boolean; - isReconnecting: boolean; - version: string; - prevVersion?: string; - dimensions: { - isSmallScreen: boolean; - width: number; - height: number; - }; -} - interface AppState { - app: AppSectionState; commands: CommandAppState; history: HistoryAppState; indexerHistory: IndexerHistoryAppState; diff --git a/frontend/src/App/State/IndexerAppState.ts b/frontend/src/App/State/IndexerAppState.ts index 4c0145d0d..d070986af 100644 --- a/frontend/src/App/State/IndexerAppState.ts +++ b/frontend/src/App/State/IndexerAppState.ts @@ -31,8 +31,6 @@ interface IndexerAppState AppSectionDeleteState, AppSectionSaveState { itemMap: Record; - - isTestingAll: boolean; } export type IndexerStatusAppState = AppSectionState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 33c6c936d..d4152431c 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -7,8 +7,7 @@ import { IndexerCategory } from 'Indexer/Indexer'; import Application from 'typings/Application'; import DownloadClient from 'typings/DownloadClient'; import Notification from 'typings/Notification'; -import General from 'typings/Settings/General'; -import UiSettings from 'typings/Settings/UiSettings'; +import { UiSettings } from 'typings/UiSettings'; export interface AppProfileAppState extends AppSectionState, @@ -25,12 +24,6 @@ export interface ApplicationAppState export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState { - isTestingAll: boolean; -} - -export interface GeneralAppState - extends AppSectionItemState, AppSectionSaveState {} export interface IndexerCategoryAppState @@ -48,7 +41,6 @@ interface SettingsAppState { appProfiles: AppProfileAppState; applications: ApplicationAppState; downloadClients: DownloadClientAppState; - general: GeneralAppState; indexerCategories: IndexerCategoryAppState; notifications: NotificationAppState; ui: UiSettingsAppState; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index 8bc1b03e2..d43c1d0ee 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,19 +1,10 @@ -import Health from 'typings/Health'; import SystemStatus from 'typings/SystemStatus'; -import Task from 'typings/Task'; -import Update from 'typings/Update'; -import AppSectionState, { AppSectionItemState } from './AppSectionState'; +import { AppSectionItemState } from './AppSectionState'; -export type HealthAppState = AppSectionState; export type SystemStatusAppState = AppSectionItemState; -export type TaskAppState = AppSectionState; -export type UpdateAppState = AppSectionState; interface SystemAppState { - health: HealthAppState; status: SystemStatusAppState; - tasks: TaskAppState; - updates: UpdateAppState; } export default SystemAppState; diff --git a/frontend/src/Components/Chart/StackedBarChart.js b/frontend/src/Components/Chart/StackedBarChart.js index b69fd8e03..3cca1ba81 100644 --- a/frontend/src/Components/Chart/StackedBarChart.js +++ b/frontend/src/Components/Chart/StackedBarChart.js @@ -46,10 +46,6 @@ class StackedBarChart extends Component { size: 14, family: defaultFontFamily } - }, - tooltip: { - mode: 'index', - position: 'average' } } }, diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js index 931557045..aac01a6b5 100644 --- a/frontend/src/Components/DescriptionList/DescriptionListItem.js +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.js @@ -10,7 +10,6 @@ class DescriptionListItem extends Component { render() { const { - className, titleClassName, descriptionClassName, title, @@ -18,7 +17,7 @@ class DescriptionListItem extends Component { } = this.props; return ( -
+
@@ -36,7 +35,6 @@ class DescriptionListItem extends Component { } DescriptionListItem.propTypes = { - className: PropTypes.string, titleClassName: PropTypes.string, descriptionClassName: PropTypes.string, title: PropTypes.string, diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 51d286311..022cf5a45 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -63,7 +63,11 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
{info.componentStack}
)} -
Version: {window.Prowlarr.version}
+ { +
+ Version: {window.Prowlarr.version} +
+ }
); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index b02844c61..51622509b 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import SelectInput from 'Components/Form/SelectInput'; import IconButton from 'Components/Link/IconButton'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue'; @@ -213,7 +212,7 @@ class FilterBuilderRow extends Component { key: name, value: typeof label === 'function' ? label() : label }; - }).sort(sortByProp('value')); + }).sort((a, b) => a.value.localeCompare(b.value)); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js index d1419327a..a7aed80b6 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { filterBuilderTypes } from 'Helpers/Props'; import * as filterTypes from 'Helpers/Props/filterTypes'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { @@ -38,7 +38,7 @@ function createTagListSelector() { } return acc; - }, []).sort(sortByProp('name')); + }, []).sort(sortByName); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index 99cb6ec5c..28eb91599 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -5,7 +5,6 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import CustomFilter from './CustomFilter'; import styles from './CustomFiltersModalContent.css'; @@ -32,7 +31,7 @@ function CustomFiltersModalContent(props) { { customFilters - .sort((a, b) => sortByProp(a, b, 'label')) + .sort((a, b) => a.label.localeCompare(b.label)) .map((customFilter) => { return ( includeNoChange, (state, { includeMixed }) => includeMixed, (appProfiles, includeNoChange, includeMixed) => { diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index 9cf7a429a..d5bbe4a2f 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -3,8 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; +import sortByName from 'Utilities/Array/sortByName'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -22,7 +21,7 @@ function createMapStateToProps() { const values = items .filter((downloadClient) => downloadClient.protocol === protocolFilter) - .sort(sortByProp('name')) + .sort(sortByName) .map((downloadClient) => ({ key: downloadClient.id, value: downloadClient.name, @@ -32,7 +31,7 @@ function createMapStateToProps() { if (includeAny) { values.unshift({ key: 0, - value: `(${translate('Any')})` + value: '(Any)' }); } diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index 79b1c999c..cc4215025 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import TextInput from './TextInput'; import styles from './EnhancedSelectInput.css'; -const MINIMUM_DISTANCE_FROM_EDGE = 10; - function isArrowKey(keyCode) { return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; } @@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component { // Listeners onComputeMaxHeight = (data) => { + const { + top, + bottom + } = data.offsets.reference; + const windowHeight = window.innerHeight; - data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; + if ((/^botton/).test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } return data; }; @@ -264,29 +271,26 @@ class EnhancedSelectInput extends Component { this.setState({ isOpen: !this.state.isOpen }); }; - onSelect = (newValue) => { - const { name, value, values, onChange } = this.props; - - if (Array.isArray(value)) { - let arrayValue = null; - const index = value.indexOf(newValue); - + onSelect = (value) => { + if (Array.isArray(this.props.value)) { + let newValue = null; + const index = this.props.value.indexOf(value); if (index === -1) { - arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v)); + newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v)); } else { - arrayValue = [...value]; - arrayValue.splice(index, 1); + newValue = [...this.props.value]; + newValue.splice(index, 1); } - onChange({ - name, - value: arrayValue + this.props.onChange({ + name: this.props.name, + value: newValue }); } else { this.setState({ isOpen: false }); - onChange({ - name, - value: newValue + this.props.onChange({ + name: this.props.name, + value }); } }; @@ -453,10 +457,6 @@ class EnhancedSelectInput extends Component { order: 851, enabled: true, fn: this.onComputeMaxHeight - }, - preventOverflow: { - enabled: true, - boundariesElement: 'viewport' } }} > @@ -485,7 +485,7 @@ class EnhancedSelectInput extends Component { values.map((v, index) => { const hasParent = v.parentKey !== undefined; const depth = hasParent ? 1 : 0; - const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey); + const parentSelected = hasParent && value.includes(v.parentKey); return ( {error.errorMessage} - - { - error.detailedDescription ? - } - tooltip={error.detailedDescription} - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> : - null - } ); }) @@ -53,18 +39,6 @@ function Form(props) { kind={kinds.WARNING} > {warning.errorMessage} - - { - warning.detailedDescription ? - } - tooltip={warning.detailedDescription} - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> : - null - } ); }) diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js new file mode 100644 index 000000000..a7145363a --- /dev/null +++ b/frontend/src/Components/Form/FormInputButton.js @@ -0,0 +1,54 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import { kinds } from 'Helpers/Props'; +import styles from './FormInputButton.css'; + +function FormInputButton(props) { + const { + className, + canSpin, + isLastButton, + ...otherProps + } = props; + + if (canSpin) { + return ( + + ); + } + + return ( +
- - diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 6eef54eab..069a4cdf7 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller'; import { icons } from 'Helpers/Props'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import dimensions from 'Styles/Variables/dimensions'; -import HealthStatus from 'System/Status/Health/HealthStatus'; +import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; @@ -87,7 +87,7 @@ const links = [ { title: () => translate('Status'), to: '/system/status', - statusComponent: HealthStatus + statusComponent: HealthStatusConnector }, { title: () => translate('Tasks'), diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css index 409062f97..5e3e3b52c 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css @@ -24,7 +24,6 @@ composes: link; padding: 10px 24px; - padding-left: 35px; } .isActiveLink { @@ -42,6 +41,10 @@ text-align: center; } +.noIcon { + margin-left: 25px; +} + .status { float: right; } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts index 5bf0eb815..77e23c767 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts @@ -8,6 +8,7 @@ interface CssExports { 'isActiveParentLink': string; 'item': string; 'link': string; + 'noIcon': string; 'status': string; } export const cssExports: CssExports; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js index 8d0e4e790..754071c79 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js @@ -63,7 +63,9 @@ class PageSidebarItem extends Component { } - {typeof title === 'function' ? title() : title} + + {typeof title === 'function' ? title() : title} + { !!StatusComponent && diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css index e9a1b666d..0b6918296 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css @@ -22,14 +22,11 @@ display: flex; align-items: center; justify-content: center; - overflow: hidden; height: 24px; } .label { padding: 0 3px; - max-width: 100%; - max-height: 100%; color: var(--toolbarLabelColor); font-size: $extraSmallFontSize; line-height: calc($extraSmallFontSize + 1px); diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js index 675bdfd02..c93603aa9 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js @@ -23,7 +23,6 @@ function PageToolbarButton(props) { isDisabled && styles.isDisabled )} isDisabled={isDisabled || isSpinning} - title={label} {...otherProps} > { - const section = 'settings.applications'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - handleCommand = (body) => { if (body.action === 'sync') { this.props.dispatchFetchCommands(); @@ -160,8 +150,8 @@ class SignalRConnector extends Component { const resource = body.resource; const status = resource.status; - // Both successful and failed commands need to be - // completed, otherwise they spin until they time out. + // Both sucessful and failed commands need to be + // completed, otherwise they spin until they timeout. if (status === 'completed' || status === 'failed') { this.props.dispatchFinishCommand(resource); @@ -170,16 +160,6 @@ class SignalRConnector extends Component { } }; - handleDownloadclient = ({ action, resource }) => { - const section = 'settings.downloadClients'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - handleHealth = () => { this.props.dispatchFetchHealth(); }; @@ -188,33 +168,14 @@ class SignalRConnector extends Component { this.props.dispatchFetchIndexerStatus(); }; - handleIndexer = ({ action, resource }) => { + handleIndexer = (body) => { + const action = body.action; const section = 'indexers'; - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); + if (action === 'updated') { + this.props.dispatchUpdateItem({ section, ...body.resource }); } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleIndexerproxy = ({ action, resource }) => { - const section = 'settings.indexerProxies'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleNotification = ({ action, resource }) => { - const section = 'settings.notifications'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); + this.props.dispatchRemoveItem({ section, id: body.resource.id }); } }; diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index 24674c3fc..31a696df7 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -2,11 +2,9 @@ import React from 'react'; type PropertyFunction = () => T; -// TODO: Convert to generic so `name` can be a type interface Column { name: string; label: string | PropertyFunction | React.ReactNode; - className?: string; columnLabel?: string; isSortable?: boolean; isVisible: boolean; diff --git a/frontend/src/Components/Table/usePaging.ts b/frontend/src/Components/Table/usePaging.ts deleted file mode 100644 index dfebb2355..000000000 --- a/frontend/src/Components/Table/usePaging.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; - -interface PagingOptions { - page: number; - totalPages: number; - gotoPage: ({ page }: { page: number }) => void; -} - -function usePaging(options: PagingOptions) { - const { page, totalPages, gotoPage } = options; - const dispatch = useDispatch(); - - const handleFirstPagePress = useCallback(() => { - dispatch(gotoPage({ page: 1 })); - }, [dispatch, gotoPage]); - - const handlePreviousPagePress = useCallback(() => { - dispatch(gotoPage({ page: Math.max(page - 1, 1) })); - }, [page, dispatch, gotoPage]); - - const handleNextPagePress = useCallback(() => { - dispatch(gotoPage({ page: Math.min(page + 1, totalPages) })); - }, [page, totalPages, dispatch, gotoPage]); - - const handleLastPagePress = useCallback(() => { - dispatch(gotoPage({ page: totalPages })); - }, [totalPages, dispatch, gotoPage]); - - const handlePageSelect = useCallback( - (page: number) => { - dispatch(gotoPage({ page })); - }, - [dispatch, gotoPage] - ); - - return useMemo(() => { - return { - handleFirstPagePress, - handlePreviousPagePress, - handleNextPagePress, - handleLastPagePress, - handlePageSelect, - }; - }, [ - handleFirstPagePress, - handlePreviousPagePress, - handleNextPagePress, - handleLastPagePress, - handlePageSelect, - ]); -} - -export default usePaging; diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js index fe700b8fe..f4d4e2af4 100644 --- a/frontend/src/Components/TagList.js +++ b/frontend/src/Components/TagList.js @@ -1,15 +1,14 @@ import PropTypes from 'prop-types'; import React from 'react'; import { kinds } from 'Helpers/Props'; -import sortByProp from 'Utilities/Array/sortByProp'; import Label from './Label'; import styles from './TagList.css'; function TagList({ tags, tagList }) { const sortedTags = tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) - .filter((tag) => !!tag) - .sort(sortByProp('label')); + .filter((t) => t !== undefined) + .sort((a, b) => a.label.localeCompare(b.label)); return (
diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css index e0f1bf5dc..bf31501dd 100644 --- a/frontend/src/Content/Fonts/fonts.css +++ b/frontend/src/Content/Fonts/fonts.css @@ -25,3 +25,14 @@ font-family: 'Ubuntu Mono'; src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype'); } + +/* + * text-security-disc + */ + +@font-face { + font-weight: normal; + font-style: normal; + font-family: 'text-security-disc'; + src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype'); +} diff --git a/frontend/src/Content/Fonts/text-security-disc.ttf b/frontend/src/Content/Fonts/text-security-disc.ttf new file mode 100644 index 000000000..86038dba8 Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.ttf differ diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff new file mode 100644 index 000000000..bc4cc324b Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.woff differ diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts deleted file mode 100644 index 417db8178..000000000 --- a/frontend/src/DownloadClient/DownloadProtocol.ts +++ /dev/null @@ -1,3 +0,0 @@ -type DownloadProtocol = 'usenet' | 'torrent' | 'unknown'; - -export default DownloadProtocol; diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts deleted file mode 100644 index 3caf66df2..000000000 --- a/frontend/src/Helpers/Hooks/useCurrentPage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useHistory } from 'react-router-dom'; - -function useCurrentPage() { - const history = useHistory(); - - return history.action === 'POP'; -} - -export default useCurrentPage; diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts index 24cffb2f1..f5b5a96f0 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, setIsOpen] = useState(initialState); + const [isOpen, setOpen] = useState(initialState); const setModalOpen = useCallback(() => { - setIsOpen(true); - }, [setIsOpen]); + setOpen(true); + }, [setOpen]); const setModalClosed = useCallback(() => { - setIsOpen(false); - }, [setIsOpen]); + setOpen(false); + }, [setOpen]); return [isOpen, setModalOpen, setModalClosed]; } diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts deleted file mode 100644 index 885c73470..000000000 --- a/frontend/src/Helpers/Props/TooltipPosition.ts +++ /dev/null @@ -1,3 +0,0 @@ -type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; - -export default TooltipPosition; diff --git a/frontend/src/Helpers/Props/align.ts b/frontend/src/Helpers/Props/align.js similarity index 100% rename from frontend/src/Helpers/Props/align.ts rename to frontend/src/Helpers/Props/align.js diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 773748996..834452242 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -43,7 +43,6 @@ import { faChevronCircleRight as fasChevronCircleRight, faChevronCircleUp as fasChevronCircleUp, faCircle as fasCircle, - faCircleDown as fasCircleDown, faCloud as fasCloud, faCloudDownloadAlt as fasCloudDownloadAlt, faCog as fasCog, @@ -142,7 +141,6 @@ export const CHECK_INDETERMINATE = fasMinus; export const CHECK_CIRCLE = fasCheckCircle; export const CHECK_SQUARE = fasSquareCheck; export const CIRCLE = fasCircle; -export const CIRCLE_DOWN = fasCircleDown; export const CIRCLE_OUTLINE = farCircle; export const CLEAR = fasTrashAlt; export const CLIPBOARD = fasCopy; diff --git a/frontend/src/Helpers/Props/kinds.ts b/frontend/src/Helpers/Props/kinds.js similarity index 72% rename from frontend/src/Helpers/Props/kinds.ts rename to frontend/src/Helpers/Props/kinds.js index 7ce606716..b0f5ac87f 100644 --- a/frontend/src/Helpers/Props/kinds.ts +++ b/frontend/src/Helpers/Props/kinds.js @@ -7,6 +7,7 @@ export const PRIMARY = 'primary'; export const PURPLE = 'purple'; export const SUCCESS = 'success'; export const WARNING = 'warning'; +export const QUEUE = 'queue'; export const all = [ DANGER, @@ -18,15 +19,5 @@ export const all = [ PURPLE, SUCCESS, WARNING, -] as const; - -export type Kind = - | 'danger' - | 'default' - | 'disabled' - | 'info' - | 'inverse' - | 'primary' - | 'purple' - | 'success' - | 'warning'; + QUEUE +]; diff --git a/frontend/src/Helpers/Props/sizes.ts b/frontend/src/Helpers/Props/sizes.js similarity index 71% rename from frontend/src/Helpers/Props/sizes.ts rename to frontend/src/Helpers/Props/sizes.js index ca7a50fbf..d7f85df5e 100644 --- a/frontend/src/Helpers/Props/sizes.ts +++ b/frontend/src/Helpers/Props/sizes.js @@ -4,6 +4,4 @@ export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const; - -export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge'; +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; diff --git a/frontend/src/History/Details/HistoryDetails.js b/frontend/src/History/Details/HistoryDetails.js index 6d5ab260e..e0ae06eb1 100644 --- a/frontend/src/History/Details/HistoryDetails.js +++ b/frontend/src/History/Details/HistoryDetails.js @@ -3,7 +3,6 @@ import React from 'react'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import Link from 'Components/Link/Link'; -import formatDateTime from 'Utilities/Date/formatDateTime'; import translate from 'Utilities/String/translate'; import styles from './HistoryDetails.css'; @@ -11,10 +10,7 @@ function HistoryDetails(props) { const { indexer, eventType, - date, - data, - shortDateFormat, - timeFormat + data } = props; if (eventType === 'indexerQuery' || eventType === 'indexerRss') { @@ -25,10 +21,7 @@ function HistoryDetails(props) { limit, offset, source, - host, - url, - elapsedTime, - cached + url } = data; return ( @@ -93,15 +86,6 @@ function HistoryDetails(props) { null } - { - data ? - : - null - } - { data ? : null } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null - } ); } @@ -135,19 +101,10 @@ function HistoryDetails(props) { if (eventType === 'releaseGrabbed') { const { source, - host, grabTitle, - url, - publishedDate, - infoUrl, - downloadClient, - downloadClientName, - elapsedTime, - grabMethod + url } = data; - const downloadClientNameInfo = downloadClientName ?? downloadClient; - return ( { @@ -168,15 +125,6 @@ function HistoryDetails(props) { null } - { - data ? - : - null - } - { data ? {infoUrl}} - /> : - null - } - - { - publishedDate ? - : - null - } - - { - downloadClientNameInfo ? - : - null - } - { data ? : null } - - { - elapsedTime ? - : - null - } - - { - grabMethod ? - : - null - } - - { - date ? - : - null - } ); } if (eventType === 'indexerAuth') { - const { elapsedTime } = data; - return ( : null } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null - } ); } @@ -297,15 +171,6 @@ function HistoryDetails(props) { title={translate('Name')} data={data.query} /> - - { - date ? - : - null - } ); } @@ -313,7 +178,6 @@ function HistoryDetails(props) { HistoryDetails.propTypes = { indexer: PropTypes.object.isRequired, eventType: PropTypes.string.isRequired, - date: PropTypes.string.isRequired, data: PropTypes.object.isRequired, shortDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired diff --git a/frontend/src/History/Details/HistoryDetailsModal.js b/frontend/src/History/Details/HistoryDetailsModal.js index 560955de3..fbc3114ad 100644 --- a/frontend/src/History/Details/HistoryDetailsModal.js +++ b/frontend/src/History/Details/HistoryDetailsModal.js @@ -29,7 +29,6 @@ function HistoryDetailsModal(props) { isOpen, eventType, indexer, - date, data, shortDateFormat, timeFormat, @@ -50,7 +49,6 @@ function HistoryDetailsModal(props) { ); } @@ -332,21 +331,6 @@ class HistoryRow extends Component { ); } - if (name === 'host') { - return ( - - { - data.host ? - data.host : - null - } - - ); - } - if (name === 'elapsedTime') { return ( ); } @@ -410,7 +393,6 @@ class HistoryRow extends Component { {value}; } else if (type === 'tmdb') { link = ( - - {value} - + {value} ); } else if (type === 'tvdb') { link = ( diff --git a/frontend/src/Indexer/Add/AddIndexerModal.js b/frontend/src/Indexer/Add/AddIndexerModal.js new file mode 100644 index 000000000..9344c8130 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import AddIndexerModalContentConnector from './AddIndexerModalContentConnector'; +import styles from './AddIndexerModal.css'; + +function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) { + return ( + + + + ); +} + +AddIndexerModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + onSelectIndexer: PropTypes.func.isRequired +}; + +export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModal.tsx b/frontend/src/Indexer/Add/AddIndexerModal.tsx deleted file mode 100644 index be22eec57..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModal.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import { clearIndexerSchema } from 'Store/Actions/indexerActions'; -import AddIndexerModalContent from './AddIndexerModalContent'; -import styles from './AddIndexerModal.css'; - -interface AddIndexerModalProps { - isOpen: boolean; - onSelectIndexer(): void; - onModalClose(): void; -} - -function AddIndexerModal({ - isOpen, - onSelectIndexer, - onModalClose, - ...otherProps -}: AddIndexerModalProps) { - const dispatch = useDispatch(); - - const onModalClosePress = useCallback(() => { - dispatch(clearIndexerSchema()); - onModalClose(); - }, [dispatch, onModalClose]); - - return ( - - - - ); -} - -export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css b/frontend/src/Indexer/Add/AddIndexerModalContent.css index e824c5475..a58eccfbc 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css @@ -19,16 +19,10 @@ margin-bottom: 16px; } -.notice { - composes: alert from '~Components/Alert.css'; - - margin-bottom: 20px; -} - .alert { composes: alert from '~Components/Alert.css'; - text-align: center; + margin-bottom: 20px; } .scroller { diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts index 5978832e4..cbedc72a4 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts @@ -10,7 +10,6 @@ interface CssExports { 'indexers': string; 'modalBody': string; 'modalFooter': string; - 'notice': string; 'scroller': string; } export const cssExports: CssExports; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.js b/frontend/src/Indexer/Add/AddIndexerModalContent.js new file mode 100644 index 000000000..f4050560e --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.js @@ -0,0 +1,324 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Alert from 'Components/Alert'; +import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput'; +import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector'; +import TextInput from 'Components/Form/TextInput'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import Scroller from 'Components/Scroller/Scroller'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { kinds, scrollDirections } from 'Helpers/Props'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; +import SelectIndexerRow from './SelectIndexerRow'; +import styles from './AddIndexerModalContent.css'; + +const columns = [ + { + name: 'protocol', + label: () => translate('Protocol'), + isSortable: true, + isVisible: true + }, + { + name: 'sortName', + label: () => translate('Name'), + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: () => translate('Language'), + isSortable: true, + isVisible: true + }, + { + name: 'description', + label: () => translate('Description'), + isSortable: false, + isVisible: true + }, + { + name: 'privacy', + label: () => translate('Privacy'), + isSortable: true, + isVisible: true + }, + { + name: 'categories', + label: () => translate('Categories'), + isSortable: false, + isVisible: true + } +]; + +const protocols = [ + { + key: 'torrent', + value: 'torrent' + }, + { + key: 'usenet', + value: 'nzb' + } +]; + +const privacyLevels = [ + { + key: 'private', + get value() { + return translate('Private'); + } + }, + { + key: 'semiPrivate', + get value() { + return translate('SemiPrivate'); + } + }, + { + key: 'public', + get value() { + return translate('Public'); + } + } +]; + +class AddIndexerModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + filter: '', + filterProtocols: [], + filterLanguages: [], + filterPrivacyLevels: [], + filterCategories: [] + }; + } + + // + // Listeners + + onFilterChange = ({ value }) => { + this.setState({ filter: value }); + }; + + // + // Render + + render() { + const { + indexers, + onIndexerSelect, + sortKey, + sortDirection, + isFetching, + isPopulated, + error, + onSortPress, + onModalClose + } = this.props; + + const languages = Array.from(new Set(indexers.map(({ language }) => language))) + .sort((a, b) => a.localeCompare(b)) + .map((language) => ({ key: language, value: language })); + + const filteredIndexers = indexers.filter((indexer) => { + const { + filter, + filterProtocols, + filterLanguages, + filterPrivacyLevels, + filterCategories + } = this.state; + + if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) { + return false; + } + + if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) { + return false; + } + + if (filterLanguages.length && !filterLanguages.includes(indexer.language)) { + return false; + } + + if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) { + return false; + } + + if (filterCategories.length) { + const { categories = [] } = indexer.capabilities || {}; + const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)]; + const flatCategories = categories + .filter((item) => item.id < 100000) + .flatMap(flat); + + if (!filterCategories.every((item) => flatCategories.includes(item))) { + return false; + } + } + + return true; + }); + + const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers')); + + return ( + + + {translate('AddIndexer')} + + + + + +
+
+ + this.setState({ filterProtocols: value })} + /> +
+ +
+ + this.setState({ filterLanguages: value })} + /> +
+ +
+ + this.setState({ filterPrivacyLevels: value })} + /> +
+ +
+ + this.setState({ filterCategories: value })} + /> +
+
+ + +
+ {translate('ProwlarrSupportsAnyIndexer')} +
+
+ + + { + isFetching ? : null + } + { + error ? {errorMessage} : null + } + { + isPopulated && !!indexers.length ? + + + { + filteredIndexers.map((indexer) => ( + + )) + } + +
: + null + } + { + isPopulated && !!indexers.length && !filteredIndexers.length ? + + {translate('NoIndexersFound')} + : + null + } +
+
+ + +
+ { + isPopulated ? + translate('CountIndexersAvailable', { count: filteredIndexers.length }) : + null + } +
+ +
+ +
+
+
+ ); + } +} + +AddIndexerModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + onSortPress: PropTypes.func.isRequired, + indexers: PropTypes.arrayOf(PropTypes.object).isRequired, + onIndexerSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddIndexerModalContent; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx deleted file mode 100644 index be1413769..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { some } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import IndexerAppState from 'App/State/IndexerAppState'; -import Alert from 'Components/Alert'; -import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput'; -import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector'; -import TextInput from 'Components/Form/TextInput'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import Scroller from 'Components/Scroller/Scroller'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { kinds, scrollDirections } from 'Helpers/Props'; -import Indexer, { IndexerCategory } from 'Indexer/Indexer'; -import { - fetchIndexerSchema, - selectIndexerSchema, - setIndexerSchemaSort, -} from 'Store/Actions/indexerActions'; -import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import { SortCallback } from 'typings/callbacks'; -import sortByProp from 'Utilities/Array/sortByProp'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import SelectIndexerRow from './SelectIndexerRow'; -import styles from './AddIndexerModalContent.css'; - -const COLUMNS = [ - { - name: 'protocol', - label: () => translate('Protocol'), - isSortable: true, - isVisible: true, - }, - { - name: 'sortName', - label: () => translate('Name'), - isSortable: true, - isVisible: true, - }, - { - name: 'language', - label: () => translate('Language'), - isSortable: true, - isVisible: true, - }, - { - name: 'description', - label: () => translate('Description'), - isSortable: false, - isVisible: true, - }, - { - name: 'privacy', - label: () => translate('Privacy'), - isSortable: true, - isVisible: true, - }, - { - name: 'categories', - label: () => translate('Categories'), - isSortable: false, - isVisible: true, - }, -]; - -const PROTOCOLS = [ - { - key: 'torrent', - value: 'torrent', - }, - { - key: 'usenet', - value: 'nzb', - }, -]; - -const PRIVACY_LEVELS = [ - { - key: 'private', - get value() { - return translate('Private'); - }, - }, - { - key: 'semiPrivate', - get value() { - return translate('SemiPrivate'); - }, - }, - { - key: 'public', - get value() { - return translate('Public'); - }, - }, -]; - -interface IndexerSchema extends Indexer { - isExistingIndexer: boolean; -} - -function createAddIndexersSelector() { - return createSelector( - createClientSideCollectionSelector('indexers.schema'), - createAllIndexersSelector(), - (indexers: IndexerAppState, allIndexers) => { - const { isFetching, isPopulated, error, items, sortDirection, sortKey } = - indexers; - - const indexerList: IndexerSchema[] = items.map((item) => { - const { definitionName } = item; - return { - ...item, - isExistingIndexer: some(allIndexers, { definitionName }), - }; - }); - - return { - isFetching, - isPopulated, - error, - indexers: indexerList, - sortKey, - sortDirection, - }; - } - ); -} - -interface AddIndexerModalContentProps { - onSelectIndexer(): void; - onModalClose(): void; -} - -function AddIndexerModalContent(props: AddIndexerModalContentProps) { - const { onSelectIndexer, onModalClose } = props; - - const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } = - useSelector(createAddIndexersSelector()); - const dispatch = useDispatch(); - - const [filter, setFilter] = useState(''); - const [filterProtocols, setFilterProtocols] = useState([]); - const [filterLanguages, setFilterLanguages] = useState([]); - const [filterPrivacyLevels, setFilterPrivacyLevels] = useState([]); - const [filterCategories, setFilterCategories] = useState([]); - - useEffect( - () => { - dispatch(fetchIndexerSchema()); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const onFilterChange = useCallback( - ({ value }: { value: string }) => { - setFilter(value); - }, - [setFilter] - ); - - const onFilterProtocolsChange = useCallback( - ({ value }: { value: string[] }) => { - setFilterProtocols(value); - }, - [setFilterProtocols] - ); - - const onFilterLanguagesChange = useCallback( - ({ value }: { value: string[] }) => { - setFilterLanguages(value); - }, - [setFilterLanguages] - ); - - const onFilterPrivacyLevelsChange = useCallback( - ({ value }: { value: string[] }) => { - setFilterPrivacyLevels(value); - }, - [setFilterPrivacyLevels] - ); - - const onFilterCategoriesChange = useCallback( - ({ value }: { value: number[] }) => { - setFilterCategories(value); - }, - [setFilterCategories] - ); - - const onIndexerSelect = useCallback( - ({ - implementation, - implementationName, - name, - }: { - implementation: string; - implementationName: string; - name: string; - }) => { - dispatch( - selectIndexerSchema({ - implementation, - implementationName, - name, - }) - ); - - onSelectIndexer(); - }, - [dispatch, onSelectIndexer] - ); - - const onSortPress = useCallback( - (sortKey, sortDirection) => { - dispatch(setIndexerSchemaSort({ sortKey, sortDirection })); - }, - [dispatch] - ); - - const languages = useMemo( - () => - Array.from(new Set(indexers.map(({ language }) => language))) - .map((language) => ({ key: language, value: language })) - .sort(sortByProp('value')), - [indexers] - ); - - const filteredIndexers = useMemo(() => { - const flat = ({ - id, - subCategories = [], - }: { - id: number; - subCategories: IndexerCategory[]; - }): number[] => [id, ...subCategories.flatMap(flat)]; - - return indexers.filter((indexer) => { - if ( - filter.length && - !indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && - !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase()) - ) { - return false; - } - - if ( - filterProtocols.length && - !filterProtocols.includes(indexer.protocol) - ) { - return false; - } - - if ( - filterLanguages.length && - !filterLanguages.includes(indexer.language) - ) { - return false; - } - - if ( - filterPrivacyLevels.length && - !filterPrivacyLevels.includes(indexer.privacy) - ) { - return false; - } - - if (filterCategories.length) { - const { categories = [] } = indexer.capabilities || {}; - - const flatCategories = categories - .filter((item) => item.id < 100000) - .flatMap(flat); - - if ( - !filterCategories.every((categoryId) => - flatCategories.includes(categoryId) - ) - ) { - return false; - } - } - - return true; - }); - }, [ - indexers, - filter, - filterProtocols, - filterLanguages, - filterPrivacyLevels, - filterCategories, - ]); - - const errorMessage = getErrorMessage( - error, - translate('UnableToLoadIndexers') - ); - - return ( - - {translate('AddIndexer')} - - - - -
-
- - - -
- -
- - - -
- -
- - -
- -
- - - -
-
- - -
{translate('ProwlarrSupportsAnyIndexer')}
-
- - - {isFetching ? : null} - - {error ? ( - - {errorMessage} - - ) : null} - - {isPopulated && !!indexers.length ? ( - - - {filteredIndexers.map((indexer) => ( - - ))} - -
- ) : null} - - {isPopulated && !!indexers.length && !filteredIndexers.length ? ( - - {translate('NoIndexersFound')} - - ) : null} -
-
- - -
- {isPopulated - ? translate('CountIndexersAvailable', { - count: filteredIndexers.length, - }) - : null} -
- -
- -
-
-
- ); -} - -export default AddIndexerModalContent; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js new file mode 100644 index 000000000..a422e0a03 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js @@ -0,0 +1,94 @@ +import { some } from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions'; +import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import AddIndexerModalContent from './AddIndexerModalContent'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector('indexers.schema'), + createAllIndexersSelector(), + (indexers, allIndexers) => { + const { + isFetching, + isPopulated, + error, + items, + sortDirection, + sortKey + } = indexers; + + const indexerList = items.map((item) => { + const { definitionName } = item; + return { + ...item, + isExistingIndexer: some(allIndexers, { definitionName }) + }; + }); + + return { + isFetching, + isPopulated, + error, + indexers: indexerList, + sortKey, + sortDirection + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexerSchema, + selectIndexerSchema, + setIndexerSchemaSort +}; + +class AddIndexerModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexerSchema(); + } + + // + // Listeners + + onIndexerSelect = ({ implementation, implementationName, name }) => { + this.props.selectIndexerSchema({ implementation, implementationName, name }); + this.props.onSelectIndexer(); + }; + + onSortPress = (sortKey, sortDirection) => { + this.props.setIndexerSchemaSort({ sortKey, sortDirection }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +AddIndexerModalContentConnector.propTypes = { + fetchIndexerSchema: PropTypes.func.isRequired, + selectIndexerSchema: PropTypes.func.isRequired, + setIndexerSchemaSort: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSelectIndexer: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector); diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.tsx b/frontend/src/Indexer/Add/SelectIndexerRow.tsx index 157050e41..ab6850573 100644 --- a/frontend/src/Indexer/Add/SelectIndexerRow.tsx +++ b/frontend/src/Indexer/Add/SelectIndexerRow.tsx @@ -2,19 +2,18 @@ import React, { useCallback } from 'react'; import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowButton from 'Components/Table/TableRowButton'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { icons } from 'Helpers/Props'; import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel'; -import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel'; import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer'; +import { IndexerCapabilities } from 'Indexer/Indexer'; +import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; import styles from './SelectIndexerRow.css'; interface SelectIndexerRowProps { name: string; - protocol: DownloadProtocol; - privacy: IndexerPrivacy; + protocol: string; + privacy: string; language: string; description: string; capabilities: IndexerCapabilities; @@ -64,9 +63,7 @@ function SelectIndexerRow(props: SelectIndexerRowProps) { {description} - - - + {translate(firstCharToUpper(privacy))} diff --git a/frontend/src/Indexer/Index/IndexerIndex.tsx b/frontend/src/Indexer/Index/IndexerIndex.tsx index e20e269f8..3407aa05c 100644 --- a/frontend/src/Indexer/Index/IndexerIndex.tsx +++ b/frontend/src/Indexer/Index/IndexerIndex.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +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'; @@ -28,17 +22,12 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal'; import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector'; import NoIndexer from 'Indexer/NoIndexer'; import { executeCommand } from 'Store/Actions/commandActions'; -import { - cloneIndexer, - fetchIndexers, - testAllIndexers, -} from 'Store/Actions/indexerActions'; +import { cloneIndexer, testAllIndexers } from 'Store/Actions/indexerActions'; import { setIndexerFilter, setIndexerSort, setIndexerTableOption, } from 'Store/Actions/indexerIndexActions'; -import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions'; import scrollPositions from 'Store/scrollPositions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -93,11 +82,6 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { ); const [isSelectMode, setIsSelectMode] = useState(false); - useEffect(() => { - dispatch(fetchIndexers()); - dispatch(fetchIndexerStatus()); - }, [dispatch]); - const onAddIndexerPress = useCallback(() => { setIsAddIndexerModalOpen(true); }, [setIsAddIndexerModalOpen]); diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx index 9d42aa389..9e2f3e0e9 100644 --- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx @@ -19,7 +19,6 @@ interface SavePayload { seedRatio?: number; seedTime?: number; packSeedTime?: number; - preferMagnetUrl?: boolean; } interface EditIndexerModalContentProps { @@ -66,9 +65,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { const [packSeedTime, setPackSeedTime] = useState( null ); - const [preferMagnetUrl, setPreferMagnetUrl] = useState< - null | string | boolean - >(null); const save = useCallback(() => { let hasChanges = false; @@ -109,11 +105,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { payload.packSeedTime = packSeedTime as number; } - if (preferMagnetUrl !== null) { - hasChanges = true; - payload.preferMagnetUrl = preferMagnetUrl === 'true'; - } - if (hasChanges) { onSavePress(payload); } @@ -127,7 +118,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { seedRatio, seedTime, packSeedTime, - preferMagnetUrl, onSavePress, onModalClose, ]); @@ -156,9 +146,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { case 'packSeedTime': setPackSeedTime(value); break; - case 'preferMagnetUrl': - setPreferMagnetUrl(value); - break; default: console.warn(`EditIndexersModalContent Unknown Input: '${name}'`); } @@ -237,7 +224,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { name="seedRatio" value={seedRatio} helpText={translate('SeedRatioHelpText')} - isFloat={true} onChange={onInputChange} /> @@ -267,18 +253,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { onChange={onInputChange} /> - - - {translate('PreferMagnetUrl')} - - - diff --git a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx index 8e30532cc..c832806ed 100644 --- a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx +++ b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx @@ -2,7 +2,6 @@ import { uniqBy } from 'lodash'; import React from 'react'; import Label from 'Components/Label'; import { IndexerCapabilities } from 'Indexer/Indexer'; -import translate from 'Utilities/String/translate'; interface CapabilitiesLabelProps { capabilities: IndexerCapabilities; @@ -39,7 +38,7 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) { ); })} - {filteredList.length === 0 ? : null} + {filteredList.length === 0 ? : null} ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css index a20efded3..a09d0218e 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css @@ -29,8 +29,7 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime, -.preferMagnetUrl { +.packSeedTime { composes: cell; flex: 0 0 90px; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts index 42821bd74..5feb0c35d 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts @@ -11,7 +11,6 @@ interface CssExports { 'id': string; 'minimumSeeders': string; 'packSeedTime': string; - 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx index e4c3cd32e..9e83e9b8d 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { useSelect } from 'App/SelectContext'; -import CheckInput from 'Components/Form/CheckInput'; +import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; @@ -15,10 +15,10 @@ import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItem import Indexer from 'Indexer/Indexer'; 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'; import IndexerStatusCell from './IndexerStatusCell'; -import PrivacyLabel from './PrivacyLabel'; import ProtocolLabel from './ProtocolLabel'; import styles from './IndexerIndexRow.css'; @@ -75,10 +75,6 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime') ?.value ?? undefined; - const preferMagnetUrl = - fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl') - ?.value ?? undefined; - const rssUrl = `${window.location.origin}${ window.Prowlarr.urlBase }/${id}/api?apikey=${encodeURIComponent( @@ -107,10 +103,6 @@ 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 }: SelectStateInputProps) => { selectDispatch({ @@ -183,7 +175,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { if (name === 'privacy') { return ( - + ); } @@ -286,21 +278,6 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { ); } - if (name === 'preferMagnetUrl') { - return ( - - {preferMagnetUrl === undefined ? null : ( - - )} - - ); - } - if (name === 'actions') { return ( columns ); -function Row({ index, style, data }: ListChildComponentProps) { +const Row: React.FC> = ({ + index, + style, + data, +}) => { const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data; if (index >= items.length) { @@ -73,7 +77,7 @@ function Row({ index, style, data }: ListChildComponentProps) { />
); -} +}; function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css index 839cd49ff..185ba0ef7 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css @@ -22,8 +22,7 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime, -.preferMagnetUrl { +.packSeedTime { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; flex: 0 0 90px; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts index 020d61f27..f6a54fa25 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts @@ -8,7 +8,6 @@ interface CssExports { 'id': string; 'minimumSeeders': string; 'packSeedTime': string; - 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx index 3aa087790..6155929df 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { Fragment, useCallback } from 'react'; import { useSelector } from 'react-redux'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -32,17 +32,19 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) { ); return ( - - {translate('ShowSearch')} + + + {translate('ShowSearch')} - - + + + ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx index 1a2350302..a3d694e9d 100644 --- a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx @@ -8,30 +8,6 @@ import translate from 'Utilities/String/translate'; import DisabledIndexerInfo from './DisabledIndexerInfo'; import styles from './IndexerStatusCell.css'; -function getIconKind(enabled: boolean, redirect: boolean) { - if (enabled) { - return redirect ? kinds.INFO : kinds.SUCCESS; - } - - return kinds.DEFAULT; -} - -function getIconName(enabled: boolean, redirect: boolean) { - if (enabled) { - return redirect ? icons.REDIRECT : icons.CHECK; - } - - return icons.BLOCKLIST; -} - -function getIconTooltip(enabled: boolean, redirect: boolean) { - if (enabled) { - return redirect ? translate('EnabledRedirected') : translate('Enabled'); - } - - return translate('Disabled'); -} - interface IndexerStatusCellProps { className: string; enabled: boolean; @@ -54,14 +30,22 @@ function IndexerStatusCell(props: IndexerStatusCellProps) { ...otherProps } = props; + const enableKind = redirect ? kinds.INFO : kinds.SUCCESS; + const enableIcon = redirect ? icons.REDIRECT : icons.CHECK; + const enableTitle = redirect + ? translate('EnabledRedirected') + : translate('Enabled'); + return ( - + { + + } {status ? ( - {translate(firstCharToUpper(privacy))} - - ); -} - -export default PrivacyLabel; diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.css b/frontend/src/Indexer/Index/Table/ProtocolLabel.css index c94e383b1..110c7e01c 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css @@ -11,7 +11,3 @@ border-color: var(--usenetColor); background-color: var(--usenetColor); } - -.unknown { - composes: label from '~Components/Label.css'; -} diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts b/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts index ba0cb260d..f3b389e3d 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts @@ -2,7 +2,6 @@ // Please do not change this file! interface CssExports { 'torrent': string; - 'unknown': string; 'usenet': string; } export const cssExports: CssExports; diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx index c1824452a..08009109e 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx @@ -1,15 +1,18 @@ import React from 'react'; import Label from 'Components/Label'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import styles from './ProtocolLabel.css'; interface ProtocolLabelProps { - protocol: DownloadProtocol; + protocol: string; } -function ProtocolLabel({ protocol }: ProtocolLabelProps) { +function ProtocolLabel(props: ProtocolLabelProps) { + const { protocol } = props; + 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/Indexer.ts b/frontend/src/Indexer/Indexer.ts index c363d472c..96a67f446 100644 --- a/frontend/src/Indexer/Indexer.ts +++ b/frontend/src/Indexer/Indexer.ts @@ -1,5 +1,4 @@ import ModelBase from 'App/ModelBase'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; export interface IndexerStatus extends ModelBase { disabledTill: Date; @@ -25,8 +24,6 @@ export interface IndexerCapabilities extends ModelBase { categories: IndexerCategory[]; } -export type IndexerPrivacy = 'public' | 'semiPrivate' | 'private'; - export interface IndexerField extends ModelBase { order: number; name: string; @@ -39,7 +36,6 @@ export interface IndexerField extends ModelBase { interface Indexer extends ModelBase { name: string; - definitionName: string; description: string; encoding: string; language: string; @@ -50,8 +46,8 @@ interface Indexer extends ModelBase { supportsSearch: boolean; supportsRedirect: boolean; supportsPagination: boolean; - protocol: DownloadProtocol; - privacy: IndexerPrivacy; + protocol: string; + privacy: string; priority: number; fields: IndexerField[]; tags: number[]; diff --git a/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx b/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx index 28d45654c..455b5106d 100644 --- a/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx +++ b/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx @@ -68,14 +68,13 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) { key={parameter.key} title={parameter.title} value={data[parameter.key as keyof HistoryData].toString()} - queryType={data.queryType} /> ); })} - + {data.source ? data.source : null} @@ -84,15 +83,14 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) { { + return { + indexer, + }; + } + ); +} + +const tabs = ['details', 'categories', 'history', 'stats']; interface IndexerInfoModalContentProps { indexerId: number; @@ -38,7 +50,9 @@ interface IndexerInfoModalContentProps { } function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { - const { indexerId, onModalClose, onCloneIndexerPress } = props; + const { indexerId, onCloneIndexerPress } = props; + + const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId)); const { id, @@ -50,55 +64,54 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { fields, tags, protocol, - privacy, capabilities = {} as IndexerCapabilities, - } = useIndexer(indexerId) as Indexer; + } = indexer as Indexer; - const [selectedTab, setSelectedTab] = useState(TABS[0]); - const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); - const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = - useState(false); - - const handleTabSelect = useCallback( - (selectedIndex: number) => { - const selectedTab = TABS[selectedIndex]; - setSelectedTab(selectedTab); - }, - [setSelectedTab] - ); - - const handleEditIndexerPress = useCallback(() => { - setIsEditIndexerModalOpen(true); - }, [setIsEditIndexerModalOpen]); - - const handleEditIndexerModalClose = useCallback(() => { - setIsEditIndexerModalOpen(false); - }, [setIsEditIndexerModalOpen]); - - const handleDeleteIndexerPress = useCallback(() => { - setIsEditIndexerModalOpen(false); - setIsDeleteIndexerModalOpen(true); - }, [setIsDeleteIndexerModalOpen]); - - const handleDeleteIndexerModalClose = useCallback(() => { - setIsDeleteIndexerModalOpen(false); - onModalClose(); - }, [setIsDeleteIndexerModalOpen, onModalClose]); - - const handleCloneIndexerPressWrapper = useCallback(() => { - onCloneIndexerPress(id); - onModalClose(); - }, [id, onCloneIndexerPress, onModalClose]); + const { onModalClose } = props; const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined); - const indexerUrl = baseUrl?.replace(/(:\/\/)api\./, '$1'); - const vipExpiration = fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined; + const [selectedTab, setSelectedTab] = useState(tabs[0]); + const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); + const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = + useState(false); + + const onTabSelect = useCallback( + (index: number) => { + const selectedTab = tabs[index]; + setSelectedTab(selectedTab); + }, + [setSelectedTab] + ); + + const onEditIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(true); + }, [setIsEditIndexerModalOpen]); + + const onEditIndexerModalClose = useCallback(() => { + setIsEditIndexerModalOpen(false); + }, [setIsEditIndexerModalOpen]); + + const onDeleteIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(false); + setIsDeleteIndexerModalOpen(true); + }, [setIsDeleteIndexerModalOpen]); + + const onDeleteIndexerModalClose = useCallback(() => { + setIsDeleteIndexerModalOpen(false); + onModalClose(); + }, [setIsDeleteIndexerModalOpen, onModalClose]); + + const onCloneIndexerPressWrapper = useCallback(() => { + onCloneIndexerPress(id); + onModalClose(); + }, [id, onCloneIndexerPress, onModalClose]); + return ( {`${name}`} @@ -106,8 +119,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { @@ -147,11 +160,6 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { title={translate('Language')} data={language ?? '-'} /> - : '-'} - /> {vipExpiration ? ( - {indexerUrl ? ( - {indexerUrl} + {baseUrl ? ( + + {baseUrl.replace(/(:\/\/)api\./, '$1')} + ) : ( '-' )} @@ -348,16 +358,16 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { -
- +
@@ -365,14 +375,14 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
); diff --git a/frontend/src/Indexer/NoIndexer.css b/frontend/src/Indexer/NoIndexer.css index 4ad534de3..38a01f391 100644 --- a/frontend/src/Indexer/NoIndexer.css +++ b/frontend/src/Indexer/NoIndexer.css @@ -1,6 +1,4 @@ .message { - composes: alert from '~Components/Alert.css'; - margin-top: 10px; margin-bottom: 30px; text-align: center; diff --git a/frontend/src/Indexer/NoIndexer.tsx b/frontend/src/Indexer/NoIndexer.tsx index bf5afa1fe..75650cad6 100644 --- a/frontend/src/Indexer/NoIndexer.tsx +++ b/frontend/src/Indexer/NoIndexer.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; @@ -15,9 +14,11 @@ function NoIndexer(props: NoIndexerProps) { if (totalItems > 0) { return ( - - {translate('AllIndexersHiddenDueToFilter')} - +
+
+ {translate('AllIndexersHiddenDueToFilter')} +
+
); } @@ -28,7 +29,7 @@ function NoIndexer(props: NoIndexerProps) {
-
diff --git a/frontend/src/Indexer/Stats/IndexerStats.tsx b/frontend/src/Indexer/Stats/IndexerStats.tsx index bccd49cbe..f7b4a9413 100644 --- a/frontend/src/Indexer/Stats/IndexerStats.tsx +++ b/frontend/src/Indexer/Stats/IndexerStats.tsx @@ -32,30 +32,23 @@ import IndexerStatsFilterModal from './IndexerStatsFilterModal'; import styles from './IndexerStats.css'; function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) { - const statistics = [...indexerStats].sort((a, b) => - a.averageResponseTime === b.averageResponseTime - ? b.averageGrabResponseTime - a.averageGrabResponseTime - : b.averageResponseTime - a.averageResponseTime - ); + const data = indexerStats.map((indexer) => { + return { + label: indexer.indexerName, + value: indexer.averageResponseTime, + }; + }); - return { - labels: statistics.map((indexer) => indexer.indexerName), - datasets: [ - { - label: translate('AverageQueries'), - data: statistics.map((indexer) => indexer.averageResponseTime), - }, - { - label: translate('AverageGrabs'), - data: statistics.map((indexer) => indexer.averageGrabResponseTime), - }, - ], - }; + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; } function getFailureRateData(indexerStats: IndexerStatsIndexer[]) { - const data = [...indexerStats] - .map((indexer) => ({ + const data = indexerStats.map((indexer) => { + return { label: indexer.indexerName, value: (indexer.numberOfFailedQueries + @@ -66,102 +59,109 @@ function getFailureRateData(indexerStats: IndexerStatsIndexer[]) { indexer.numberOfRssQueries + indexer.numberOfAuthQueries + indexer.numberOfGrabs), - })) - .filter((s) => s.value > 0); + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) { - const statistics = [...indexerStats] - .filter( - (s) => - s.numberOfQueries > 0 || - s.numberOfRssQueries > 0 || - s.numberOfAuthQueries > 0 - ) - .sort( - (a, b) => - b.numberOfQueries + - b.numberOfRssQueries + - b.numberOfAuthQueries - - (a.numberOfQueries + a.numberOfRssQueries + a.numberOfAuthQueries) - ); - - return { - labels: statistics.map((indexer) => indexer.indexerName), + const data = { + labels: indexerStats.map((indexer) => indexer.indexerName), datasets: [ { label: translate('SearchQueries'), - data: statistics.map((indexer) => indexer.numberOfQueries), + data: indexerStats.map((indexer) => indexer.numberOfQueries), }, { label: translate('RssQueries'), - data: statistics.map((indexer) => indexer.numberOfRssQueries), + data: indexerStats.map((indexer) => indexer.numberOfRssQueries), }, { label: translate('AuthQueries'), - data: statistics.map((indexer) => indexer.numberOfAuthQueries), + data: indexerStats.map((indexer) => indexer.numberOfAuthQueries), }, ], }; + + return data; } function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) { - const data = [...indexerStats] - .map((indexer) => ({ + const data = indexerStats.map((indexer) => { + return { label: indexer.indexerName, value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs, - })) - .filter((s) => s.value > 0); + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfGrabs, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfGrabs, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfQueries, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfQueries, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getHostGrabsData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfGrabs, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfGrabs, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } function getHostQueryData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfQueries, - })); + const data = indexerStats.map((indexer) => { + return { + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfQueries, + }; + }); - data.sort((a, b) => b.value - a.value); + data.sort((a, b) => { + return b.value - a.value; + }); return data; } @@ -241,9 +241,9 @@ function IndexerStats() { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} + onFilterSelect={onFilterSelect} filterModalConnectorComponent={IndexerStatsFilterModal} isDisabled={false} - onFilterSelect={onFilterSelect} /> @@ -294,7 +294,7 @@ function IndexerStats() {
- state.indexers.itemMap, - (state: AppState) => state.indexers.items, - (itemMap, allIndexers) => { - return indexerId ? allIndexers[itemMap[indexerId]] : undefined; - } - ); -} - -function useIndexer(indexerId?: number) { - return useSelector(createIndexerSelector(indexerId)); -} - -export default useIndexer; diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css b/frontend/src/Search/Mobile/SearchIndexOverview.css index e29ff1ef9..4e184bd0a 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css @@ -47,42 +47,3 @@ $hoverScale: 1.05; right: 0; white-space: nowrap; } - -.downloadLink { - composes: link from '~Components/Link/Link.css'; - - margin: 0 2px; - width: 22px; - color: var(--textColor); - text-align: center; -} - -.manualDownloadContent { - position: relative; - display: inline-block; - margin: 0 2px; - width: 22px; - height: 20.39px; - vertical-align: middle; - line-height: 20.39px; - - &:hover { - color: var(--iconButtonHoverColor); - } -} - -.interactiveIcon { - position: absolute; - top: 4px; - left: 0; - /* width: 100%; */ - text-align: center; -} - -.downloadIcon { - position: absolute; - top: 7px; - left: 8px; - /* width: 100%; */ - text-align: center; -} diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts b/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts index 68256eb25..266cf7fca 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts @@ -4,13 +4,9 @@ interface CssExports { 'actions': string; 'container': string; 'content': string; - 'downloadIcon': string; - 'downloadLink': string; 'indexerRow': string; 'info': string; 'infoRow': string; - 'interactiveIcon': string; - 'manualDownloadContent': string; 'title': string; 'titleRow': string; } diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.js b/frontend/src/Search/Mobile/SearchIndexOverview.js new file mode 100644 index 000000000..1a14ae66c --- /dev/null +++ b/frontend/src/Search/Mobile/SearchIndexOverview.js @@ -0,0 +1,234 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TextTruncate from 'react-text-truncate'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import { icons, kinds } from 'Helpers/Props'; +import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; +import CategoryLabel from 'Search/Table/CategoryLabel'; +import Peers from 'Search/Table/Peers'; +import dimensions from 'Styles/Variables/dimensions'; +import formatAge from 'Utilities/Number/formatAge'; +import formatBytes from 'Utilities/Number/formatBytes'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import styles from './SearchIndexOverview.css'; + +const columnPadding = parseInt(dimensions.movieIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); + +function getContentHeight(rowHeight, isSmallScreen) { + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + + return rowHeight - padding; +} + +function getDownloadIcon(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return icons.SPINNER; + } else if (isGrabbed) { + return icons.DOWNLOADING; + } else if (grabError) { + return icons.DOWNLOADING; + } + + return icons.DOWNLOAD; +} + +function getDownloadKind(isGrabbed, grabError) { + if (isGrabbed) { + return kinds.SUCCESS; + } + + if (grabError) { + return kinds.DANGER; + } + + return kinds.DEFAULT; +} + +function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return ''; + } else if (isGrabbed) { + return translate('AddedToDownloadClient'); + } else if (grabError) { + return grabError; + } + + return translate('AddToDownloadClient'); +} + +class SearchIndexOverview extends Component { + + // + // Listeners + + onGrabPress = () => { + const { + guid, + indexerId, + onGrabPress + } = this.props; + + onGrabPress({ + guid, + indexerId + }); + }; + + // + // Render + + render() { + const { + title, + infoUrl, + protocol, + downloadUrl, + magnetUrl, + categories, + seeders, + leechers, + indexerFlags, + size, + age, + ageHours, + ageMinutes, + indexer, + rowHeight, + isSmallScreen, + isGrabbed, + isGrabbing, + grabError + } = this.props; + + const contentHeight = getContentHeight(rowHeight, isSmallScreen); + + return ( +
+
+
+
+
+ + + +
+ +
+ + + { + downloadUrl || magnetUrl ? + : + null + } +
+
+
+ {indexer} +
+
+ + + { + protocol === 'torrent' && + + } + + + + + + + + { + indexerFlags.length ? + indexerFlags + .sort((a, b) => a.localeCompare(b)) + .map((flag, index) => { + return ( + + ); + }) : + null + } +
+
+
+
+ ); + } +} + +SearchIndexOverview.propTypes = { + guid: PropTypes.string.isRequired, + categories: PropTypes.arrayOf(PropTypes.object).isRequired, + protocol: PropTypes.string.isRequired, + age: PropTypes.number.isRequired, + ageHours: PropTypes.number.isRequired, + ageMinutes: PropTypes.number.isRequired, + publishDate: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + infoUrl: PropTypes.string.isRequired, + downloadUrl: PropTypes.string, + magnetUrl: PropTypes.string, + indexerId: PropTypes.number.isRequired, + indexer: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + files: PropTypes.number, + grabs: PropTypes.number, + seeders: PropTypes.number, + leechers: PropTypes.number, + indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired, + rowHeight: PropTypes.number.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onGrabPress: PropTypes.func.isRequired, + isGrabbing: PropTypes.bool.isRequired, + isGrabbed: PropTypes.bool.isRequired, + grabError: PropTypes.string +}; + +SearchIndexOverview.defaultProps = { + isGrabbing: false, + isGrabbed: false +}; + +export default SearchIndexOverview; diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.tsx b/frontend/src/Search/Mobile/SearchIndexOverview.tsx deleted file mode 100644 index 21a42d70c..000000000 --- a/frontend/src/Search/Mobile/SearchIndexOverview.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import TextTruncate from 'react-text-truncate'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { icons, kinds } from 'Helpers/Props'; -import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import { IndexerCategory } from 'Indexer/Indexer'; -import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal'; -import CategoryLabel from 'Search/Table/CategoryLabel'; -import Peers from 'Search/Table/Peers'; -import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; -import dimensions from 'Styles/Variables/dimensions'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatAge from 'Utilities/Number/formatAge'; -import formatBytes from 'Utilities/Number/formatBytes'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import styles from './SearchIndexOverview.css'; - -const columnPadding = parseInt(dimensions.movieIndexColumnPadding); -const columnPaddingSmallScreen = parseInt( - dimensions.movieIndexColumnPaddingSmallScreen -); - -function getDownloadIcon( - isGrabbing: boolean, - isGrabbed: boolean, - grabError?: string -) { - if (isGrabbing) { - return icons.SPINNER; - } else if (isGrabbed) { - return icons.DOWNLOADING; - } else if (grabError) { - return icons.DOWNLOADING; - } - - return icons.DOWNLOAD; -} - -function getDownloadKind(isGrabbed: boolean, grabError?: string) { - if (isGrabbed) { - return kinds.SUCCESS; - } - - if (grabError) { - return kinds.DANGER; - } - - return kinds.DEFAULT; -} - -function getDownloadTooltip( - isGrabbing: boolean, - isGrabbed: boolean, - grabError?: string -) { - if (isGrabbing) { - return ''; - } else if (isGrabbed) { - return translate('AddedToDownloadClient'); - } else if (grabError) { - return grabError; - } - - return translate('AddToDownloadClient'); -} - -interface SearchIndexOverviewProps { - guid: string; - protocol: DownloadProtocol; - age: number; - ageHours: number; - ageMinutes: number; - publishDate: string; - title: string; - infoUrl: string; - downloadUrl?: string; - magnetUrl?: string; - indexerId: number; - indexer: string; - categories: IndexerCategory[]; - size: number; - seeders?: number; - leechers?: number; - indexerFlags: string[]; - isGrabbing: boolean; - isGrabbed: boolean; - grabError?: string; - longDateFormat: string; - timeFormat: string; - rowHeight: number; - isSmallScreen: boolean; - onGrabPress(...args: unknown[]): void; -} - -function SearchIndexOverview(props: SearchIndexOverviewProps) { - const { - guid, - indexerId, - protocol, - categories, - age, - ageHours, - ageMinutes, - publishDate, - title, - infoUrl, - downloadUrl, - magnetUrl, - indexer, - size, - seeders, - leechers, - indexerFlags = [], - isGrabbing = false, - isGrabbed = false, - grabError, - longDateFormat, - timeFormat, - rowHeight, - isSmallScreen, - onGrabPress, - } = props; - - const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false); - - const { items: downloadClients } = useSelector( - createEnabledDownloadClientsSelector(protocol) - ); - - const onGrabPressWrapper = useCallback(() => { - onGrabPress({ - guid, - indexerId, - }); - }, [guid, indexerId, onGrabPress]); - - const onOverridePress = useCallback(() => { - setIsOverrideModalOpen(true); - }, [setIsOverrideModalOpen]); - - const onOverrideModalClose = useCallback(() => { - setIsOverrideModalOpen(false); - }, [setIsOverrideModalOpen]); - - const contentHeight = useMemo(() => { - const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; - - return rowHeight - padding; - }, [rowHeight, isSmallScreen]); - - return ( - <> -
-
-
-
-
- - - -
- -
- - - {downloadClients.length > 1 ? ( - -
- - - -
- - ) : null} - - {downloadUrl || magnetUrl ? ( - - ) : null} -
-
-
{indexer}
-
- - - {protocol === 'torrent' && ( - - )} - - - - - - - - {indexerFlags.length - ? indexerFlags - .sort((a, b) => - a.localeCompare(b, undefined, { numeric: true }) - ) - .map((flag, index) => { - return ( - - ); - }) - : null} -
-
-
-
- - - - ); -} - -export default SearchIndexOverview; diff --git a/frontend/src/Search/NoSearchResults.css b/frontend/src/Search/NoSearchResults.css index f17dd633e..eff6272f7 100644 --- a/frontend/src/Search/NoSearchResults.css +++ b/frontend/src/Search/NoSearchResults.css @@ -1,6 +1,4 @@ .message { - composes: alert from '~Components/Alert.css'; - margin-top: 10px; margin-bottom: 30px; text-align: center; diff --git a/frontend/src/Search/NoSearchResults.tsx b/frontend/src/Search/NoSearchResults.tsx index 46fbc85e0..4ffd1d7fd 100644 --- a/frontend/src/Search/NoSearchResults.tsx +++ b/frontend/src/Search/NoSearchResults.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import Alert from 'Components/Alert'; -import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './NoSearchResults.css'; @@ -13,16 +11,18 @@ function NoSearchResults(props: NoSearchResultsProps) { if (totalItems > 0) { return ( - - {translate('AllSearchResultsHiddenByFilter')} - +
+
+ {translate('AllIndexersHiddenDueToFilter')} +
+
); } return ( - - {translate('NoSearchResultsFound')} - +
+
{translate('NoSearchResultsFound')}
+
); } diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx deleted file mode 100644 index 7d623decd..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { sizes } from 'Helpers/Props'; -import SelectDownloadClientModalContent from './SelectDownloadClientModalContent'; - -interface SelectDownloadClientModalProps { - isOpen: boolean; - protocol: DownloadProtocol; - modalTitle: string; - onDownloadClientSelect(downloadClientId: number): void; - onModalClose(): void; -} - -function SelectDownloadClientModal(props: SelectDownloadClientModalProps) { - const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } = - props; - - return ( - - - - ); -} - -export default SelectDownloadClientModal; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx deleted file mode 100644 index 63e15808f..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import Alert from 'Components/Alert'; -import Form from 'Components/Form/Form'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { kinds } from 'Helpers/Props'; -import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; -import translate from 'Utilities/String/translate'; -import SelectDownloadClientRow from './SelectDownloadClientRow'; - -interface SelectDownloadClientModalContentProps { - protocol: DownloadProtocol; - modalTitle: string; - onDownloadClientSelect(downloadClientId: number): void; - onModalClose(): void; -} - -function SelectDownloadClientModalContent( - props: SelectDownloadClientModalContentProps -) { - const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props; - - const { isFetching, isPopulated, error, items } = useSelector( - createEnabledDownloadClientsSelector(protocol) - ); - - return ( - - - {translate('SelectDownloadClientModalTitle', { modalTitle })} - - - - {isFetching ? : null} - - {!isFetching && error ? ( - - {translate('DownloadClientsLoadError')} - - ) : null} - - {isPopulated && !error ? ( -
- {items.map((downloadClient) => { - const { id, name, priority } = downloadClient; - - return ( - - ); - })} - - ) : null} -
- - - - -
- ); -} - -export default SelectDownloadClientModalContent; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css deleted file mode 100644 index 6525db977..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css +++ /dev/null @@ -1,6 +0,0 @@ -.downloadClient { - display: flex; - justify-content: space-between; - padding: 8px; - border-bottom: 1px solid var(--borderColor); -} diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx deleted file mode 100644 index 6f98d60b4..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useCallback } from 'react'; -import Link from 'Components/Link/Link'; -import translate from 'Utilities/String/translate'; -import styles from './SelectDownloadClientRow.css'; - -interface SelectSeasonRowProps { - id: number; - name: string; - priority: number; - onDownloadClientSelect(downloadClientId: number): unknown; -} - -function SelectDownloadClientRow(props: SelectSeasonRowProps) { - const { id, name, priority, onDownloadClientSelect } = props; - - const onSeasonSelectWrapper = useCallback(() => { - onDownloadClientSelect(id); - }, [id, onDownloadClientSelect]); - - return ( - -
{name}
-
{translate('PrioritySettings', { priority })}
- - ); -} - -export default SelectDownloadClientRow; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.css b/frontend/src/Search/OverrideMatch/OverrideMatchData.css deleted file mode 100644 index bd4d2f788..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.css +++ /dev/null @@ -1,17 +0,0 @@ -.link { - composes: link from '~Components/Link/Link.css'; - - width: 100%; -} - -.placeholder { - display: inline-block; - margin: -2px 0; - width: 100%; - outline: 2px dashed var(--dangerColor); - outline-offset: -2px; -} - -.optional { - outline: 2px dashed var(--gray); -} diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts b/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts deleted file mode 100644 index dd3ac4575..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'link': string; - 'optional': string; - 'placeholder': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx deleted file mode 100644 index 82d6bd812..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import Link from 'Components/Link/Link'; -import styles from './OverrideMatchData.css'; - -interface OverrideMatchDataProps { - value?: string | number | JSX.Element | JSX.Element[]; - isDisabled?: boolean; - isOptional?: boolean; - onPress: () => void; -} - -function OverrideMatchData(props: OverrideMatchDataProps) { - const { value, isDisabled = false, isOptional, onPress } = props; - - return ( - - {(value == null || (Array.isArray(value) && value.length === 0)) && - !isDisabled ? ( - -   - - ) : ( - value - )} - - ); -} - -export default OverrideMatchData; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx deleted file mode 100644 index 16d62ea7c..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { sizes } from 'Helpers/Props'; -import OverrideMatchModalContent from './OverrideMatchModalContent'; - -interface OverrideMatchModalProps { - isOpen: boolean; - title: string; - indexerId: number; - guid: string; - protocol: DownloadProtocol; - isGrabbing: boolean; - grabError?: string; - onModalClose(): void; -} - -function OverrideMatchModal(props: OverrideMatchModalProps) { - const { - isOpen, - title, - indexerId, - guid, - protocol, - isGrabbing, - grabError, - onModalClose, - } = props; - - return ( - - - - ); -} - -export default OverrideMatchModal; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css deleted file mode 100644 index a5b4b8d52..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css +++ /dev/null @@ -1,49 +0,0 @@ -.label { - composes: label from '~Components/Label.css'; - - cursor: pointer; -} - -.item { - display: block; - margin-bottom: 5px; - margin-left: 50px; -} - -.footer { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - display: flex; - justify-content: space-between; - overflow: hidden; -} - -.error { - margin-right: 20px; - color: var(--dangerColor); - word-break: break-word; -} - -.buttons { - display: flex; -} - -@media only screen and (max-width: $breakpointSmall) { - .item { - margin-left: 0; - } - - .footer { - display: block; - } - - .error { - margin-right: 0; - margin-bottom: 10px; - } - - .buttons { - justify-content: space-between; - flex-grow: 1; - } -} diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts deleted file mode 100644 index 79c77d6b5..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'buttons': string; - 'error': string; - 'footer': string; - 'item': string; - 'label': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx deleted file mode 100644 index fbe0ec450..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import usePrevious from 'Helpers/Hooks/usePrevious'; -import { grabRelease } from 'Store/Actions/releaseActions'; -import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; -import translate from 'Utilities/String/translate'; -import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal'; -import OverrideMatchData from './OverrideMatchData'; -import styles from './OverrideMatchModalContent.css'; - -type SelectType = 'select' | 'downloadClient'; - -interface OverrideMatchModalContentProps { - indexerId: number; - title: string; - guid: string; - protocol: DownloadProtocol; - isGrabbing: boolean; - grabError?: string; - onModalClose(): void; -} - -function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { - const modalTitle = translate('ManualGrab'); - const { - indexerId, - title, - guid, - protocol, - isGrabbing, - grabError, - onModalClose, - } = props; - - const [downloadClientId, setDownloadClientId] = useState(null); - const [selectModalOpen, setSelectModalOpen] = useState( - null - ); - const previousIsGrabbing = usePrevious(isGrabbing); - - const dispatch = useDispatch(); - const { items: downloadClients } = useSelector( - createEnabledDownloadClientsSelector(protocol) - ); - - const onSelectModalClose = useCallback(() => { - setSelectModalOpen(null); - }, [setSelectModalOpen]); - - const onSelectDownloadClientPress = useCallback(() => { - setSelectModalOpen('downloadClient'); - }, [setSelectModalOpen]); - - const onDownloadClientSelect = useCallback( - (downloadClientId: number) => { - setDownloadClientId(downloadClientId); - setSelectModalOpen(null); - }, - [setDownloadClientId, setSelectModalOpen] - ); - - const onGrabPress = useCallback(() => { - dispatch( - grabRelease({ - indexerId, - guid, - downloadClientId, - }) - ); - }, [indexerId, guid, downloadClientId, dispatch]); - - useEffect(() => { - if (!isGrabbing && previousIsGrabbing) { - onModalClose(); - } - }, [isGrabbing, previousIsGrabbing, onModalClose]); - - useEffect( - () => { - dispatch(fetchDownloadClients()); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return ( - - - {translate('OverrideGrabModalTitle', { title })} - - - - - {downloadClients.length > 1 ? ( - downloadClient.id === downloadClientId - )?.name ?? translate('Default') - } - onPress={onSelectDownloadClientPress} - /> - } - /> - ) : null} - - - - -
{grabError}
- -
- - - - {translate('GrabRelease')} - -
-
- - -
- ); -} - -export default OverrideMatchModalContent; diff --git a/frontend/src/Search/SearchIndex.js b/frontend/src/Search/SearchIndex.js index d12635070..17f7c5b3a 100644 --- a/frontend/src/Search/SearchIndex.js +++ b/frontend/src/Search/SearchIndex.js @@ -282,7 +282,7 @@ class SearchIndex extends Component { const ViewComponent = getViewComponent(isSmallScreen); const isLoaded = !!(!error && isPopulated && items.length && this.scrollerRef.current); - const hasNoSearchResults = !totalItems; + const hasNoIndexer = !totalItems; return ( @@ -306,7 +306,7 @@ class SearchIndex extends Component { @@ -314,7 +314,7 @@ class SearchIndex extends Component { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} - isDisabled={hasNoSearchResults} + isDisabled={hasNoIndexer} onFilterSelect={onFilterSelect} /> diff --git a/frontend/src/Search/SearchIndexConnector.js b/frontend/src/Search/SearchIndexConnector.js index 78a9866b2..e3302e73c 100644 --- a/frontend/src/Search/SearchIndexConnector.js +++ b/frontend/src/Search/SearchIndexConnector.js @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import withScrollPosition from 'Components/withScrollPosition'; import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions'; -import { fetchDownloadClients } from 'Store/Actions/Settings/downloadClients'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector'; import SearchIndex from './SearchIndex'; @@ -56,20 +55,12 @@ function createMapDispatchToProps(dispatch, props) { dispatchClearReleases() { dispatch(clearReleases()); - }, - - dispatchFetchDownloadClients() { - dispatch(fetchDownloadClients()); } }; } class SearchIndexConnector extends Component { - componentDidMount() { - this.props.dispatchFetchDownloadClients(); - } - componentWillUnmount() { this.props.dispatchCancelFetchReleases(); this.props.dispatchClearReleases(); @@ -94,7 +85,6 @@ SearchIndexConnector.propTypes = { onBulkGrabPress: PropTypes.func.isRequired, dispatchCancelFetchReleases: PropTypes.func.isRequired, dispatchClearReleases: PropTypes.func.isRequired, - dispatchFetchDownloadClients: PropTypes.func.isRequired, items: PropTypes.arrayOf(PropTypes.object) }; diff --git a/frontend/src/Search/Table/SearchIndexItemConnector.js b/frontend/src/Search/Table/SearchIndexItemConnector.js index 4cc7fb20c..490214529 100644 --- a/frontend/src/Search/Table/SearchIndexItemConnector.js +++ b/frontend/src/Search/Table/SearchIndexItemConnector.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import { executeCommand } from 'Store/Actions/commandActions'; function createReleaseSelector() { return createSelector( @@ -36,6 +37,10 @@ function createMapStateToProps() { ); } +const mapDispatchToProps = { + dispatchExecuteCommand: executeCommand +}; + class SearchIndexItemConnector extends Component { // @@ -66,4 +71,4 @@ SearchIndexItemConnector.propTypes = { component: PropTypes.elementType.isRequired }; -export default connect(createMapStateToProps, null)(SearchIndexItemConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector); diff --git a/frontend/src/Search/Table/SearchIndexRow.css b/frontend/src/Search/Table/SearchIndexRow.css index b36ec4071..37b59a35d 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css +++ b/frontend/src/Search/Table/SearchIndexRow.css @@ -67,33 +67,3 @@ color: var(--textColor); } - -.manualDownloadContent { - position: relative; - display: inline-block; - margin: 0 2px; - width: 22px; - height: 20.39px; - vertical-align: middle; - line-height: 20.39px; - - &:hover { - color: var(--iconButtonHoverColor); - } -} - -.interactiveIcon { - position: absolute; - top: 4px; - left: 0; - /* width: 100%; */ - text-align: center; -} - -.downloadIcon { - position: absolute; - top: 7px; - left: 8px; - /* width: 100%; */ - text-align: center; -} diff --git a/frontend/src/Search/Table/SearchIndexRow.css.d.ts b/frontend/src/Search/Table/SearchIndexRow.css.d.ts index 7552b96f9..6d625f58a 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css.d.ts +++ b/frontend/src/Search/Table/SearchIndexRow.css.d.ts @@ -6,15 +6,12 @@ interface CssExports { 'category': string; 'cell': string; 'checkInput': string; - 'downloadIcon': string; 'downloadLink': string; 'externalLinks': string; 'files': string; 'grabs': string; 'indexer': string; 'indexerFlags': string; - 'interactiveIcon': string; - 'manualDownloadContent': string; 'peers': string; 'protocol': string; 'size': string; diff --git a/frontend/src/Search/Table/SearchIndexRow.js b/frontend/src/Search/Table/SearchIndexRow.js new file mode 100644 index 000000000..613605e02 --- /dev/null +++ b/frontend/src/Search/Table/SearchIndexRow.js @@ -0,0 +1,431 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; +import Popover from 'Components/Tooltip/Popover'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import formatBytes from 'Utilities/Number/formatBytes'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import CategoryLabel from './CategoryLabel'; +import Peers from './Peers'; +import ReleaseLinks from './ReleaseLinks'; +import styles from './SearchIndexRow.css'; + +function getDownloadIcon(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return icons.SPINNER; + } else if (isGrabbed) { + return icons.DOWNLOADING; + } else if (grabError) { + return icons.DOWNLOADING; + } + + return icons.DOWNLOAD; +} + +function getDownloadKind(isGrabbed, grabError) { + if (isGrabbed) { + return kinds.SUCCESS; + } + + if (grabError) { + return kinds.DANGER; + } + + return kinds.DEFAULT; +} + +function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return ''; + } else if (isGrabbed) { + return translate('AddedToDownloadClient'); + } else if (grabError) { + return grabError; + } + + return translate('AddToDownloadClient'); +} + +class SearchIndexRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isConfirmGrabModalOpen: false + }; + } + + // + // Listeners + + onGrabPress = () => { + const { + guid, + indexerId, + onGrabPress + } = this.props; + + onGrabPress({ + guid, + indexerId + }); + }; + + onSavePress = () => { + const { + downloadUrl, + fileName, + onSavePress + } = this.props; + + onSavePress({ + downloadUrl, + fileName + }); + }; + + // + // Render + + render() { + const { + guid, + protocol, + downloadUrl, + magnetUrl, + categories, + age, + ageHours, + ageMinutes, + publishDate, + title, + infoUrl, + indexer, + size, + files, + grabs, + seeders, + leechers, + imdbId, + tmdbId, + tvdbId, + tvMazeId, + indexerFlags, + columns, + isGrabbing, + isGrabbed, + grabError, + longDateFormat, + timeFormat, + isSelected, + onSelectedChange + } = this.props; + + return ( + <> + { + columns.map((column) => { + const { + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (column.name === 'select') { + return ( + + ); + } + + if (column.name === 'protocol') { + return ( + + + + ); + } + + if (column.name === 'age') { + return ( + + {formatAge(age, ageHours, ageMinutes)} + + ); + } + + if (column.name === 'sortTitle') { + return ( + + +
+ {title} +
+ +
+ ); + } + + if (column.name === 'indexer') { + return ( + + {indexer} + + ); + } + + if (column.name === 'size') { + return ( + + {formatBytes(size)} + + ); + } + + if (column.name === 'files') { + return ( + + {files} + + ); + } + + if (column.name === 'grabs') { + return ( + + {grabs} + + ); + } + + if (column.name === 'peers') { + return ( + + { + protocol === 'torrent' && + + } + + ); + } + + if (column.name === 'category') { + return ( + + + + ); + } + + if (column.name === 'indexerFlags') { + return ( + + { + !!indexerFlags.length && + + } + title={translate('IndexerFlags')} + body={ +
    + { + indexerFlags.map((flag, index) => { + return ( +
  • + {titleCase(flag)} +
  • + ); + }) + } +
+ } + position={tooltipPositions.LEFT} + /> + } +
+ ); + } + + if (column.name === 'actions') { + return ( + + + + { + downloadUrl ? + : + null + } + + { + magnetUrl ? + : + null + } + + { + imdbId || tmdbId || tvdbId || tvMazeId ? ( + + } + title={translate('Links')} + body={ + + } + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> + ) : null + } + + ); + } + + return null; + }) + } + + ); + } +} + +SearchIndexRow.propTypes = { + guid: PropTypes.string.isRequired, + categories: PropTypes.arrayOf(PropTypes.object).isRequired, + protocol: PropTypes.string.isRequired, + age: PropTypes.number.isRequired, + ageHours: PropTypes.number.isRequired, + ageMinutes: PropTypes.number.isRequired, + publishDate: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + fileName: PropTypes.string.isRequired, + infoUrl: PropTypes.string.isRequired, + downloadUrl: PropTypes.string, + magnetUrl: PropTypes.string, + indexerId: PropTypes.number.isRequired, + indexer: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + files: PropTypes.number, + grabs: PropTypes.number, + seeders: PropTypes.number, + leechers: PropTypes.number, + imdbId: PropTypes.number, + tmdbId: PropTypes.number, + tvdbId: PropTypes.number, + tvMazeId: PropTypes.number, + indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onGrabPress: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + isGrabbing: PropTypes.bool.isRequired, + isGrabbed: PropTypes.bool.isRequired, + grabError: PropTypes.string, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +SearchIndexRow.defaultProps = { + isGrabbing: false, + isGrabbed: false +}; + +export default SearchIndexRow; diff --git a/frontend/src/Search/Table/SearchIndexRow.tsx b/frontend/src/Search/Table/SearchIndexRow.tsx deleted file mode 100644 index 1136a7f64..000000000 --- a/frontend/src/Search/Table/SearchIndexRow.tsx +++ /dev/null @@ -1,395 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; -import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; -import Column from 'Components/Table/Column'; -import Popover from 'Components/Tooltip/Popover'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import { IndexerCategory } from 'Indexer/Indexer'; -import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal'; -import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; -import { SelectStateInputProps } from 'typings/props'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatAge from 'Utilities/Number/formatAge'; -import formatBytes from 'Utilities/Number/formatBytes'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import CategoryLabel from './CategoryLabel'; -import Peers from './Peers'; -import ReleaseLinks from './ReleaseLinks'; -import styles from './SearchIndexRow.css'; - -function getDownloadIcon( - isGrabbing: boolean, - isGrabbed: boolean, - grabError?: string -) { - if (isGrabbing) { - return icons.SPINNER; - } else if (isGrabbed) { - return icons.DOWNLOADING; - } else if (grabError) { - return icons.DOWNLOADING; - } - - return icons.DOWNLOAD; -} - -function getDownloadKind(isGrabbed: boolean, grabError?: string) { - if (isGrabbed) { - return kinds.SUCCESS; - } - - if (grabError) { - return kinds.DANGER; - } - - return kinds.DEFAULT; -} - -function getDownloadTooltip( - isGrabbing: boolean, - isGrabbed: boolean, - grabError?: string -) { - if (isGrabbing) { - return ''; - } else if (isGrabbed) { - return translate('AddedToDownloadClient'); - } else if (grabError) { - return grabError; - } - - return translate('AddToDownloadClient'); -} - -interface SearchIndexRowProps { - guid: string; - protocol: DownloadProtocol; - age: number; - ageHours: number; - ageMinutes: number; - publishDate: string; - title: string; - fileName: string; - infoUrl: string; - downloadUrl?: string; - magnetUrl?: string; - indexerId: number; - indexer: string; - categories: IndexerCategory[]; - size: number; - files?: number; - grabs?: number; - seeders?: number; - leechers?: number; - imdbId?: string; - tmdbId?: number; - tvdbId?: number; - tvMazeId?: number; - indexerFlags: string[]; - isGrabbing: boolean; - isGrabbed: boolean; - grabError?: string; - longDateFormat: string; - timeFormat: string; - columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; - onGrabPress(...args: unknown[]): void; - onSavePress(...args: unknown[]): void; -} - -function SearchIndexRow(props: SearchIndexRowProps) { - const { - guid, - indexerId, - protocol, - categories, - age, - ageHours, - ageMinutes, - publishDate, - title, - fileName, - infoUrl, - downloadUrl, - magnetUrl, - indexer, - size, - files, - grabs, - seeders, - leechers, - imdbId, - tmdbId, - tvdbId, - tvMazeId, - indexerFlags = [], - isGrabbing = false, - isGrabbed = false, - grabError, - longDateFormat, - timeFormat, - columns, - isSelected, - onSelectedChange, - onGrabPress, - onSavePress, - } = props; - - const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false); - - const { items: downloadClients } = useSelector( - createEnabledDownloadClientsSelector(protocol) - ); - - const onGrabPressWrapper = useCallback(() => { - onGrabPress({ - guid, - indexerId, - }); - }, [guid, indexerId, onGrabPress]); - - const onSavePressWrapper = useCallback(() => { - onSavePress({ - downloadUrl, - fileName, - }); - }, [downloadUrl, fileName, onSavePress]); - - const onOverridePress = useCallback(() => { - setIsOverrideModalOpen(true); - }, [setIsOverrideModalOpen]); - - const onOverrideModalClose = useCallback(() => { - setIsOverrideModalOpen(false); - }, [setIsOverrideModalOpen]); - - return ( - <> - {columns.map((column) => { - const { name, isVisible } = column; - - if (!isVisible) { - return null; - } - - if (name === 'select') { - return ( - - ); - } - - if (name === 'protocol') { - return ( - - - - ); - } - - if (name === 'age') { - return ( - - {formatAge(age, ageHours, ageMinutes)} - - ); - } - - if (name === 'sortTitle') { - return ( - - -
{title}
- -
- ); - } - - if (name === 'indexer') { - return ( - - {indexer} - - ); - } - - if (name === 'size') { - return ( - - {formatBytes(size)} - - ); - } - - if (name === 'files') { - return ( - - {files} - - ); - } - - if (name === 'grabs') { - return ( - - {grabs} - - ); - } - - if (name === 'peers') { - return ( - - {protocol === 'torrent' && ( - - )} - - ); - } - - if (name === 'category') { - return ( - - - - ); - } - - if (name === 'indexerFlags') { - return ( - - {!!indexerFlags.length && ( - } - title={translate('IndexerFlags')} - body={ -
    - {indexerFlags.map((flag, index) => { - return
  • {titleCase(flag)}
  • ; - })} -
- } - position={tooltipPositions.LEFT} - /> - )} -
- ); - } - - if (name === 'actions') { - return ( - - - - {downloadClients.length > 1 ? ( - -
- - - -
- - ) : null} - - {downloadUrl ? ( - - ) : null} - - {magnetUrl ? ( - - ) : null} - - {imdbId || tmdbId || tvdbId || tvMazeId ? ( - - } - title={translate('Links')} - body={ - - } - position={tooltipPositions.TOP} - /> - ) : null} -
- ); - } - - return null; - })} - - - - ); -} - -export default SearchIndexRow; diff --git a/frontend/src/Settings/Applications/ApplicationSettings.tsx b/frontend/src/Settings/Applications/ApplicationSettings.tsx index 7fc4b1d7b..c35d55e2d 100644 --- a/frontend/src/Settings/Applications/ApplicationSettings.tsx +++ b/frontend/src/Settings/Applications/ApplicationSettings.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { Fragment, useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; import { APP_INDEXER_SYNC } from 'Commands/commandNames'; @@ -56,7 +56,7 @@ function ApplicationSettings() { // @ts-ignore showSave={false} additionalButtons={ - <> + - + } /> diff --git a/frontend/src/Settings/Applications/Applications/Application.js b/frontend/src/Settings/Applications/Applications/Application.js index 086d39ee1..610cc344d 100644 --- a/frontend/src/Settings/Applications/Applications/Application.js +++ b/frontend/src/Settings/Applications/Applications/Application.js @@ -57,7 +57,6 @@ class Application extends Component { const { id, name, - enable, syncLevel, fields, tags, @@ -78,7 +77,7 @@ class Application extends Component {
{ - enable && applicationUrl ? + applicationUrl ? { return { diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx index bb81729f3..e2c36529c 100644 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx +++ b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx @@ -213,9 +213,9 @@ function ManageApplicationsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} + onSelectAllChange={onSelectAllChange} sortKey={sortKey} sortDirection={sortDirection} - onSelectAllChange={onSelectAllChange} onSortPress={onSortPress} > @@ -268,9 +268,9 @@ function ManageApplicationsModalContent( downloadClients ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index fa82d61b9..4d459d71d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -186,9 +186,9 @@ function ManageDownloadClientsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} + onSelectAllChange={onSelectAllChange} sortKey={sortKey} sortDirection={sortDirection} - onSelectAllChange={onSelectAllChange} onSortPress={onSortPress} > @@ -233,9 +233,9 @@ function ManageDownloadClientsModalContent( diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.js index 61a259258..540e29b01 100644 --- a/frontend/src/Settings/General/LoggingSettings.js +++ b/frontend/src/Settings/General/LoggingSettings.js @@ -15,14 +15,12 @@ const logLevelOptions = [ function LoggingSettings(props) { const { - advancedSettings, settings, onInputChange } = props; const { - logLevel, - logSizeLimit + logLevel } = settings; return ( @@ -39,30 +37,11 @@ function LoggingSettings(props) { {...logLevel} /> - - - {translate('LogSizeLimit')} - - - ); } LoggingSettings.propTypes = { - advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index 9cf1b7932..3bf8d43b6 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -12,6 +12,7 @@ function UpdateSettings(props) { const { advancedSettings, settings, + isWindows, packageUpdateMechanism, onInputChange } = props; @@ -37,10 +38,10 @@ function UpdateSettings(props) { value: titleCase(packageUpdateMechanism) }); } else { - updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') }); + updateOptions.push({ key: 'builtIn', value: 'Built-In' }); } - updateOptions.push({ key: 'script', value: translate('Script') }); + updateOptions.push({ key: 'script', value: 'Script' }); return (
@@ -61,58 +62,61 @@ function UpdateSettings(props) { /> -
- - {translate('Automatic')} + { + !isWindows && +
+ + {translate('Automatic')} - - + + - - {translate('Mechanism')} - - - - - { - updateMechanism.value === 'script' && - {translate('ScriptPath')} + {translate('Mechanism')} - } -
+ + { + updateMechanism.value === 'script' && + + {translate('ScriptPath')} + + + + } +
+ }
); } diff --git a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js index 0d2acae87..9d2188a7c 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js @@ -5,13 +5,13 @@ import { createSelector } from 'reselect'; import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import IndexerProxies from './IndexerProxies'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.indexerProxies', sortByProp('name')), - createSortedSectionSelector('indexers', sortByProp('name')), + createSortedSectionSelector('settings.indexerProxies', sortByName), + createSortedSectionSelector('indexers', sortByName), createTagsSelector(), (indexerProxies, indexers, tagList) => { return { diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index 6351c6f8a..b306f742a 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByProp('name')), + createSortedSectionSelector('settings.notifications', sortByName), createTagsSelector(), (notifications, tagList) => { return { diff --git a/frontend/src/Settings/Profiles/App/AppProfilesConnector.js b/frontend/src/Settings/Profiles/App/AppProfilesConnector.js index 02bf845df..a150655a6 100644 --- a/frontend/src/Settings/Profiles/App/AppProfilesConnector.js +++ b/frontend/src/Settings/Profiles/App/AppProfilesConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import AppProfiles from './AppProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.appProfiles', sortByProp('name')), + createSortedSectionSelector('settings.appProfiles', sortByName), (appProfiles) => appProfiles ); } diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 1f3de2034..b53e2fc0f 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -4,13 +4,11 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions'; import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; import Tags from './Tags'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('tags', sortByProp('label')), + (state) => state.tags, (tags) => { const isFetching = tags.isFetching || tags.details.isFetching; const error = tags.error || tags.details.error; diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index e35157dbd..ca26883fb 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -1,11 +1,8 @@ -import $ from 'jquery'; -import _ from 'lodash'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getProviderState from 'Utilities/State/getProviderState'; import { set } from '../baseActions'; const abortCurrentRequests = {}; -let lastTestData = null; export function createCancelTestProviderHandler(section) { return function(getState, payload, dispatch) { @@ -20,25 +17,10 @@ function createTestProviderHandler(section, url) { return function(getState, payload, dispatch) { dispatch(set({ section, isTesting: true })); - const { - queryParams = {}, - ...otherPayload - } = payload; - - const testData = getProviderState({ ...otherPayload }, getState, section); - const params = { ...queryParams }; - - // If the user is re-testing the same provider without changes - // force it to be tested. - - if (_.isEqual(testData, lastTestData)) { - params.forceTest = true; - } - - lastTestData = testData; + const testData = getProviderState(payload, getState, section); const ajaxOptions = { - url: `${url}/test?${$.param(params, true)}`, + url: `${url}/test`, method: 'POST', contentType: 'application/json', dataType: 'json', @@ -50,8 +32,6 @@ function createTestProviderHandler(section, url) { abortCurrentRequests[section] = abortRequest; request.done((data) => { - lastTestData = null; - dispatch(set({ section, isTesting: false, diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index c324fe227..95ea992b1 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -82,12 +82,6 @@ export const defaultState = { isSortable: false, isVisible: false }, - { - name: 'host', - label: () => translate('Host'), - isSortable: false, - isVisible: false - }, { name: 'elapsedTime', label: () => translate('ElapsedTime'), diff --git a/frontend/src/Store/Actions/indexerActions.js b/frontend/src/Store/Actions/indexerActions.js index e11051c2f..2aae11b36 100644 --- a/frontend/src/Store/Actions/indexerActions.js +++ b/frontend/src/Store/Actions/indexerActions.js @@ -3,13 +3,9 @@ import { createAction } from 'redux-actions'; import { filterTypePredicates, sortDirections } from 'Helpers/Props'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; -import createSaveProviderHandler, { - createCancelSaveProviderHandler -} from 'Store/Actions/Creators/createSaveProviderHandler'; +import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; -import createTestProviderHandler, { - createCancelTestProviderHandler -} from 'Store/Actions/Creators/createTestProviderHandler'; +import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk, handleThunks } from 'Store/thunks'; @@ -20,7 +16,6 @@ import translate from 'Utilities/String/translate'; import createBulkEditItemHandler from './Creators/createBulkEditItemHandler'; import createBulkRemoveItemHandler from './Creators/createBulkRemoveItemHandler'; import createHandleActions from './Creators/createHandleActions'; -import createClearReducer from './Creators/Reducers/createClearReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; // @@ -100,42 +95,11 @@ export const filterPredicates = { }; export const sortPredicates = { - status: function({ enable, redirect }) { - let result = 0; + vipExpiration: function(item) { + const vipExpiration = + item.fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; - if (redirect) { - result++; - } - - if (enable) { - result += 2; - } - - return result; - }, - - vipExpiration: function({ fields = [] }) { - return fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; - }, - - minimumSeeders: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.appMinimumSeeders')?.value ?? undefined; - }, - - seedRatio: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')?.value ?? undefined; - }, - - seedTime: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.seedTime')?.value ?? undefined; - }, - - packSeedTime: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')?.value ?? undefined; - }, - - preferMagnetUrl: function({ fields = [] }) { - return fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')?.value ?? undefined; + return vipExpiration; } }; @@ -146,7 +110,6 @@ export const FETCH_INDEXERS = 'indexers/fetchIndexers'; export const FETCH_INDEXER_SCHEMA = 'indexers/fetchIndexerSchema'; export const SELECT_INDEXER_SCHEMA = 'indexers/selectIndexerSchema'; export const SET_INDEXER_SCHEMA_SORT = 'indexers/setIndexerSchemaSort'; -export const CLEAR_INDEXER_SCHEMA = 'indexers/clearIndexerSchema'; export const CLONE_INDEXER = 'indexers/cloneIndexer'; export const SET_INDEXER_VALUE = 'indexers/setIndexerValue'; export const SET_INDEXER_FIELD_VALUE = 'indexers/setIndexerFieldValue'; @@ -166,7 +129,6 @@ export const fetchIndexers = createThunk(FETCH_INDEXERS); export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA); export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA); export const setIndexerSchemaSort = createAction(SET_INDEXER_SCHEMA_SORT); -export const clearIndexerSchema = createAction(CLEAR_INDEXER_SCHEMA); export const cloneIndexer = createAction(CLONE_INDEXER); export const saveIndexer = createThunk(SAVE_INDEXER); @@ -252,8 +214,6 @@ export const reducers = createHandleActions({ }); }, - [CLEAR_INDEXER_SCHEMA]: createClearReducer(schemaSection, defaultState), - [CLONE_INDEXER]: function(state, { payload }) { const id = payload.id; const newState = getSectionState(state, section); diff --git a/frontend/src/Store/Actions/indexerIndexActions.js b/frontend/src/Store/Actions/indexerIndexActions.js index a002d9b41..fa4bc3a15 100644 --- a/frontend/src/Store/Actions/indexerIndexActions.js +++ b/frontend/src/Store/Actions/indexerIndexActions.js @@ -116,12 +116,6 @@ export const defaultState = { isSortable: true, isVisible: false }, - { - name: 'preferMagnetUrl', - label: () => translate('PreferMagnetUrl'), - isSortable: true, - isVisible: false - }, { name: 'tags', label: () => translate('Tags'), diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index fd2fe441b..bdb3ce2df 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -401,16 +401,7 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ [CLEAR_RELEASES]: (state) => { - const { - sortKey, - sortDirection, - customFilters, - selectedFilterKey, - columns, - ...otherDefaultState - } = defaultState; - - return Object.assign({}, state, otherDefaultState); + return Object.assign({}, state, defaultState); }, [UPDATE_RELEASE]: (state, { payload }) => { diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index 75d2595cf..92360b589 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -110,6 +110,7 @@ export const defaultState = { { name: 'actions', columnLabel: () => translate('Actions'), + isSortable: true, isVisible: true, isModifiable: false } diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.ts b/frontend/src/Store/Selectors/createDimensionsSelector.js similarity index 69% rename from frontend/src/Store/Selectors/createDimensionsSelector.ts rename to frontend/src/Store/Selectors/createDimensionsSelector.js index b9602cb02..ce26b2e2c 100644 --- a/frontend/src/Store/Selectors/createDimensionsSelector.ts +++ b/frontend/src/Store/Selectors/createDimensionsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createDimensionsSelector() { return createSelector( - (state: AppState) => state.app.dimensions, + (state) => state.app.dimensions, (dimensions) => { return dimensions; } diff --git a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts deleted file mode 100644 index 3a581587b..000000000 --- a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSelector } from 'reselect'; -import { DownloadClientAppState } from 'App/State/SettingsAppState'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import DownloadClient from 'typings/DownloadClient'; -import sortByProp from 'Utilities/Array/sortByProp'; - -export default function createEnabledDownloadClientsSelector( - protocol: DownloadProtocol -) { - return createSelector( - createSortedSectionSelector( - 'settings.downloadClients', - sortByProp('name') - ), - (downloadClients: DownloadClientAppState) => { - const { isFetching, isPopulated, error, items } = downloadClients; - - const clients = items.filter( - (item) => item.protocol === protocol && item.enable - ); - - return { isFetching, isPopulated, error, items: clients }; - } - ); -} diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.ts b/frontend/src/Store/Selectors/createSortedSectionSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.ts rename to frontend/src/Store/Selectors/createSortedSectionSelector.js index abee01f75..331d890c9 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.js @@ -1,18 +1,14 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector( - section: string, - comparer: (a: T, b: T) => number -) { +function createSortedSectionSelector(section, comparer) { return createSelector( (state) => state, (state) => { const sectionState = getSectionState(state, section, true); - return { ...sectionState, - items: [...sectionState.items].sort(comparer), + items: [...sectionState.items].sort(comparer) }; } ); diff --git a/frontend/src/Styles/Themes/dark.js b/frontend/src/Styles/Themes/dark.js index a7cbb6de0..79911686f 100644 --- a/frontend/src/Styles/Themes/dark.js +++ b/frontend/src/Styles/Themes/dark.js @@ -188,7 +188,7 @@ module.exports = { // Charts chartBackgroundColor: '#262626', - failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','), - chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','), - chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',') + failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], + chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], + chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] }; diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js index 4dec39164..d93c5dd8c 100644 --- a/frontend/src/Styles/Themes/index.js +++ b/frontend/src/Styles/Themes/index.js @@ -2,7 +2,7 @@ import * as dark from './dark'; import * as light from './light'; const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const auto = defaultDark ? dark : light; +const auto = defaultDark ? { ...dark } : { ...light }; export default { auto, diff --git a/frontend/src/Styles/Themes/light.js b/frontend/src/Styles/Themes/light.js index f88070a0f..8a6d123b2 100644 --- a/frontend/src/Styles/Themes/light.js +++ b/frontend/src/Styles/Themes/light.js @@ -188,7 +188,7 @@ module.exports = { // Charts chartBackgroundColor: '#fff', - failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','), - chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','), - chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',') + failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], + chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], + chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] }; diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js index def48f28e..3b0077c5a 100644 --- a/frontend/src/Styles/Variables/fonts.js +++ b/frontend/src/Styles/Variables/fonts.js @@ -2,6 +2,7 @@ module.exports = { // Families defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', + passwordFamily: 'text-security-disc', // Sizes extraSmallFontSize: '11px', diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js index 39f7f1123..5dfc7f940 100644 --- a/frontend/src/System/Backup/BackupRow.js +++ b/frontend/src/System/Backup/BackupRow.js @@ -116,7 +116,6 @@ class BackupRow extends Component { @@ -139,9 +138,7 @@ class BackupRow extends Component { isOpen={isConfirmDeleteModalOpen} kind={kinds.DANGER} title={translate('DeleteBackup')} - message={translate('DeleteBackupMessageText', { - name - })} + message={translate('DeleteBackupMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeletePress} onCancel={this.onConfirmDeleteModalClose} diff --git a/frontend/src/System/Backup/Backups.js b/frontend/src/System/Backup/Backups.js index ede2f97f6..8f7a5b0a5 100644 --- a/frontend/src/System/Backup/Backups.js +++ b/frontend/src/System/Backup/Backups.js @@ -109,7 +109,7 @@ class Backups extends Component { { !isFetching && !!error && - {translate('BackupsLoadError')} + {translate('UnableToLoadBackups')} } diff --git a/frontend/src/System/Backup/RestoreBackupModalContent.js b/frontend/src/System/Backup/RestoreBackupModalContent.js index 9b5daa9f4..150c46ad6 100644 --- a/frontend/src/System/Backup/RestoreBackupModalContent.js +++ b/frontend/src/System/Backup/RestoreBackupModalContent.js @@ -14,7 +14,7 @@ import styles from './RestoreBackupModalContent.css'; function getErrorMessage(error) { if (!error || !error.responseJSON || !error.responseJSON.message) { - return translate('ErrorRestoringBackup'); + return 'Error restoring backup'; } return error.responseJSON.message; @@ -146,9 +146,7 @@ class RestoreBackupModalContent extends Component { { - !!id && translate('WouldYouLikeToRestoreBackup', { - name - }) + !!id && `Would you like to restore the backup '${name}'?` } { @@ -205,7 +203,7 @@ class RestoreBackupModalContent extends Component {
- {translate('RestartReloadNote')} + Note: Prowlarr will automatically restart and reload the UI during the restore process.
+ } + > + { + isFetching && !isPopulated && + + } + + { + !healthIssues && +
+ {translate('HealthNoIssues')} +
+ } + + { + healthIssues && + + + { + items.map((item) => { + const internalLink = getInternalLink(item.source); + const testLink = getTestLink(item.source, this.props); + + let kind = kinds.WARNING; + switch (item.type.toLowerCase()) { + case 'error': + kind = kinds.DANGER; + break; + default: + case 'warning': + kind = kinds.WARNING; + break; + case 'notice': + kind = kinds.INFO; + break; + } + + return ( + + + + + + {item.message} + + + + + { + internalLink + } + + { + !!testLink && + testLink + } + + + ); + }) + } + +
+ } + + ); + } + +} + +Health.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired, + isTestingAllApplications: PropTypes.bool.isRequired, + isTestingAllDownloadClients: PropTypes.bool.isRequired, + isTestingAllIndexers: PropTypes.bool.isRequired, + dispatchTestAllApplications: PropTypes.func.isRequired, + dispatchTestAllDownloadClients: PropTypes.func.isRequired, + dispatchTestAllIndexers: PropTypes.func.isRequired +}; + +export default Health; diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx deleted file mode 100644 index e0636961b..000000000 --- a/frontend/src/System/Status/Health/Health.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Column from 'Components/Table/Column'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableRow from 'Components/Table/TableRow'; -import { icons, kinds } from 'Helpers/Props'; -import { testAllIndexers } from 'Store/Actions/indexerActions'; -import { - testAllApplications, - testAllDownloadClients, -} from 'Store/Actions/settingsActions'; -import { fetchHealth } from 'Store/Actions/systemActions'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import createHealthSelector from './createHealthSelector'; -import HealthItemLink from './HealthItemLink'; -import styles from './Health.css'; - -const columns: Column[] = [ - { - className: styles.status, - name: 'type', - label: '', - isVisible: true, - }, - { - name: 'message', - label: () => translate('Message'), - isVisible: true, - }, - { - name: 'actions', - label: () => translate('Actions'), - isVisible: true, - }, -]; - -function Health() { - const dispatch = useDispatch(); - const { isFetching, isPopulated, items } = useSelector( - createHealthSelector() - ); - const isTestingAllApplications = useSelector( - (state: AppState) => state.settings.applications.isTestingAll - ); - const isTestingAllDownloadClients = useSelector( - (state: AppState) => state.settings.downloadClients.isTestingAll - ); - const isTestingAllIndexers = useSelector( - (state: AppState) => state.indexers.isTestingAll - ); - - const healthIssues = !!items.length; - - const handleTestAllApplicationsPress = useCallback(() => { - dispatch(testAllApplications()); - }, [dispatch]); - - const handleTestAllDownloadClientsPress = useCallback(() => { - dispatch(testAllDownloadClients()); - }, [dispatch]); - - const handleTestAllIndexersPress = useCallback(() => { - dispatch(testAllIndexers()); - }, [dispatch]); - - useEffect(() => { - dispatch(fetchHealth()); - }, [dispatch]); - - return ( -
- {translate('Health')} - - {isFetching && isPopulated ? ( - - ) : null} - - } - > - {isFetching && !isPopulated ? : null} - - {isPopulated && !healthIssues ? ( -
- {translate('NoIssuesWithYourConfiguration')} -
- ) : null} - - {healthIssues ? ( - <> - - - {items.map((item) => { - const source = item.source; - - let kind = kinds.WARNING; - switch (item.type.toLowerCase()) { - case 'error': - kind = kinds.DANGER; - break; - default: - case 'warning': - kind = kinds.WARNING; - break; - case 'notice': - kind = kinds.INFO; - break; - } - - return ( - - - - - - {item.message} - - - - - - - {source === 'ApplicationStatusCheck' || - source === 'ApplicationLongTermStatusCheck' ? ( - - ) : null} - - {source === 'IndexerStatusCheck' || - source === 'IndexerLongTermStatusCheck' ? ( - - ) : null} - - {source === 'DownloadClientStatusCheck' ? ( - - ) : null} - - - ); - })} - -
- - - - - - ) : null} -
- ); -} - -export default Health; diff --git a/frontend/src/System/Status/Health/HealthConnector.js b/frontend/src/System/Status/Health/HealthConnector.js new file mode 100644 index 000000000..687e0ed87 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthConnector.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { testAllIndexers } from 'Store/Actions/indexerActions'; +import { testAllApplications } from 'Store/Actions/Settings/applications'; +import { testAllDownloadClients } from 'Store/Actions/Settings/downloadClients'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import createHealthCheckSelector from 'Store/Selectors/createHealthCheckSelector'; +import Health from './Health'; + +function createMapStateToProps() { + return createSelector( + createHealthCheckSelector(), + (state) => state.system.health, + (state) => state.settings.applications.isTestingAll, + (state) => state.settings.downloadClients.isTestingAll, + (state) => state.indexers.isTestingAll, + (items, health, isTestingAllApplications, isTestingAllDownloadClients, isTestingAllIndexers) => { + const { + isFetching, + isPopulated + } = health; + + return { + isFetching, + isPopulated, + items, + isTestingAllApplications, + isTestingAllDownloadClients, + isTestingAllIndexers + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchHealth: fetchHealth, + dispatchTestAllApplications: testAllApplications, + dispatchTestAllDownloadClients: testAllDownloadClients, + dispatchTestAllIndexers: testAllIndexers +}; + +class HealthConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchHealth(); + } + + // + // Render + + render() { + const { + dispatchFetchHealth, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +HealthConnector.propTypes = { + dispatchFetchHealth: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector); diff --git a/frontend/src/System/Status/Health/HealthItemLink.tsx b/frontend/src/System/Status/Health/HealthItemLink.tsx deleted file mode 100644 index b7a90c783..000000000 --- a/frontend/src/System/Status/Health/HealthItemLink.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import IconButton from 'Components/Link/IconButton'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -interface HealthItemLinkProps { - source: string; -} - -function HealthItemLink(props: HealthItemLinkProps) { - const { source } = props; - - switch (source) { - case 'ApplicationStatusCheck': - case 'ApplicationLongTermStatusCheck': - return ( - - ); - case 'DownloadClientStatusCheck': - return ( - - ); - case 'NotificationStatusCheck': - return ( - - ); - case 'IndexerProxyStatusCheck': - return ( - - ); - case 'IndexerRssCheck': - case 'IndexerSearchCheck': - case 'IndexerStatusCheck': - case 'IndexerLongTermStatusCheck': - return ( - - ); - case 'UpdateCheck': - return ( - - ); - default: - return null; - } -} - -export default HealthItemLink; diff --git a/frontend/src/System/Status/Health/HealthStatus.tsx b/frontend/src/System/Status/Health/HealthStatus.tsx deleted file mode 100644 index b12fd3ebb..000000000 --- a/frontend/src/System/Status/Health/HealthStatus.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; -import usePrevious from 'Helpers/Hooks/usePrevious'; -import { fetchHealth } from 'Store/Actions/systemActions'; -import createHealthSelector from './createHealthSelector'; - -function HealthStatus() { - const dispatch = useDispatch(); - const { isConnected, isReconnecting } = useSelector( - (state: AppState) => state.app - ); - const { isPopulated, items } = useSelector(createHealthSelector()); - - const wasReconnecting = usePrevious(isReconnecting); - - const { count, errors, warnings } = useMemo(() => { - let errors = false; - let warnings = false; - - items.forEach((item) => { - if (item.type === 'error') { - errors = true; - } - - if (item.type === 'warning') { - warnings = true; - } - }); - - return { - count: items.length, - errors, - warnings, - }; - }, [items]); - - useEffect(() => { - if (!isPopulated) { - dispatch(fetchHealth()); - } - }, [isPopulated, dispatch]); - - useEffect(() => { - if (isConnected && wasReconnecting) { - dispatch(fetchHealth()); - } - }, [isConnected, wasReconnecting, dispatch]); - - return ( - - ); -} - -export default HealthStatus; diff --git a/frontend/src/System/Status/Health/HealthStatusConnector.js b/frontend/src/System/Status/Health/HealthStatusConnector.js new file mode 100644 index 000000000..e609dd712 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthStatusConnector.js @@ -0,0 +1,81 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import createHealthCheckSelector from 'Store/Selectors/createHealthCheckSelector'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app, + createHealthCheckSelector(), + (state) => state.system.health, + (app, items, health) => { + const count = items.length; + let errors = false; + let warnings = false; + + items.forEach((item) => { + if (item.type === 'error') { + errors = true; + } + + if (item.type === 'warning') { + warnings = true; + } + }); + + return { + isConnected: app.isConnected, + isReconnecting: app.isReconnecting, + isPopulated: health.isPopulated, + count, + errors, + warnings + }; + } + ); +} + +const mapDispatchToProps = { + fetchHealth +}; + +class HealthStatusConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.fetchHealth(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.isConnected && prevProps.isReconnecting) { + this.props.fetchHealth(); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +HealthStatusConnector.propTypes = { + isConnected: PropTypes.bool.isRequired, + isReconnecting: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + fetchHealth: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector); diff --git a/frontend/src/System/Status/Health/createHealthSelector.ts b/frontend/src/System/Status/Health/createHealthSelector.ts deleted file mode 100644 index f38e3fe88..000000000 --- a/frontend/src/System/Status/Health/createHealthSelector.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -function createHealthSelector() { - return createSelector( - (state: AppState) => state.system.health, - (health) => { - return health; - } - ); -} - -export default createHealthSelector; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js new file mode 100644 index 000000000..dfb23a996 --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.js @@ -0,0 +1,58 @@ +import React, { Component } from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import FieldSet from 'Components/FieldSet'; +import Link from 'Components/Link/Link'; +import translate from 'Utilities/String/translate'; + +class MoreInfo extends Component { + + // + // Render + + render() { + return ( +
+ + {translate('HomePage')} + + prowlarr.com + + + {translate('Wiki')} + + wiki.servarr.com/prowlarr + + + {translate('Reddit')} + + r/prowlarr + + + {translate('Discord')} + + prowlarr.com/discord + + + {translate('Source')} + + github.com/Prowlarr/Prowlarr + + + {translate('FeatureRequests')} + + github.com/Prowlarr/Prowlarr/issues + + + +
+ ); + } +} + +MoreInfo.propTypes = { + +}; + +export default MoreInfo; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.tsx b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx deleted file mode 100644 index 928449aed..000000000 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; -import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; -import FieldSet from 'Components/FieldSet'; -import Link from 'Components/Link/Link'; -import translate from 'Utilities/String/translate'; - -function MoreInfo() { - return ( -
- - - {translate('HomePage')} - - - prowlarr.com - - - {translate('Wiki')} - - - wiki.servarr.com/prowlarr - - - - - {translate('Reddit')} - - - r/prowlarr - - - - {translate('Discord')} - - - prowlarr.com/discord - - - - {translate('Source')} - - - - github.com/Prowlarr/Prowlarr - - - - - {translate('FeatureRequests')} - - - - github.com/Prowlarr/Prowlarr/issues - - - -
- ); -} - -export default MoreInfo; diff --git a/frontend/src/System/Status/Status.js b/frontend/src/System/Status/Status.js new file mode 100644 index 000000000..46e2d0951 --- /dev/null +++ b/frontend/src/System/Status/Status.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import translate from 'Utilities/String/translate'; +import AboutConnector from './About/AboutConnector'; +import Donations from './Donations/Donations'; +import HealthConnector from './Health/HealthConnector'; +import MoreInfo from './MoreInfo/MoreInfo'; + +class Status extends Component { + + // + // Render + + render() { + return ( + + + + + + + + + ); + } + +} + +export default Status; diff --git a/frontend/src/System/Status/Status.tsx b/frontend/src/System/Status/Status.tsx deleted file mode 100644 index 6ae088160..000000000 --- a/frontend/src/System/Status/Status.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import translate from 'Utilities/String/translate'; -import About from './About/About'; -import Donations from './Donations/Donations'; -import Health from './Health/Health'; -import MoreInfo from './MoreInfo/MoreInfo'; - -function Status() { - return ( - - - - - - - - - ); -} - -export default Status; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js new file mode 100644 index 000000000..acb8c8d36 --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js @@ -0,0 +1,203 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import { icons } from 'Helpers/Props'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import styles from './ScheduledTaskRow.css'; + +function getFormattedDates(props) { + const { + lastExecution, + nextExecution, + interval, + showRelativeDates, + shortDateFormat + } = props; + + const isDisabled = interval === 0; + + if (showRelativeDates) { + return { + lastExecutionTime: moment(lastExecution).fromNow(), + nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow() + }; + } + + return { + lastExecutionTime: formatDate(lastExecution, shortDateFormat), + nextExecutionTime: isDisabled ? '-' : formatDate(nextExecution, shortDateFormat) + }; +} + +class ScheduledTaskRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = getFormattedDates(props); + + this._updateTimeoutId = null; + } + + componentDidMount() { + this.setUpdateTimer(); + } + + componentDidUpdate(prevProps) { + const { + lastExecution, + nextExecution + } = this.props; + + if ( + lastExecution !== prevProps.lastExecution || + nextExecution !== prevProps.nextExecution + ) { + this.setState(getFormattedDates(this.props)); + } + } + + componentWillUnmount() { + if (this._updateTimeoutId) { + this._updateTimeoutId = clearTimeout(this._updateTimeoutId); + } + } + + // + // Listeners + + setUpdateTimer() { + const { interval } = this.props; + const timeout = interval < 60 ? 10000 : 60000; + + this._updateTimeoutId = setTimeout(() => { + this.setState(getFormattedDates(this.props)); + this.setUpdateTimer(); + }, timeout); + } + + // + // Render + + render() { + const { + name, + interval, + lastExecution, + lastStartTime, + lastDuration, + nextExecution, + isQueued, + isExecuting, + longDateFormat, + timeFormat, + onExecutePress + } = this.props; + + const { + lastExecutionTime, + nextExecutionTime + } = this.state; + + const isDisabled = interval === 0; + const executeNow = !isDisabled && moment().isAfter(nextExecution); + const hasNextExecutionTime = !isDisabled && !executeNow; + const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); + const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); + + return ( + + {name} + + {isDisabled ? 'disabled' : duration} + + + + {lastExecutionTime} + + + { + !hasLastStartTime && + - + } + + { + hasLastStartTime && + + {formatTimeSpan(lastDuration)} + + } + + { + isDisabled && + - + } + + { + executeNow && isQueued && + queued + } + + { + executeNow && !isQueued && + now + } + + { + hasNextExecutionTime && + + {nextExecutionTime} + + } + + + + + + ); + } +} + +ScheduledTaskRow.propTypes = { + name: PropTypes.string.isRequired, + interval: PropTypes.number.isRequired, + lastExecution: PropTypes.string.isRequired, + lastStartTime: PropTypes.string.isRequired, + lastDuration: PropTypes.string.isRequired, + nextExecution: PropTypes.string.isRequired, + isQueued: PropTypes.bool.isRequired, + isExecuting: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onExecutePress: PropTypes.func.isRequired +}; + +export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx deleted file mode 100644 index 3a3cd02de..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import moment from 'moment'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import usePrevious from 'Helpers/Hooks/usePrevious'; -import { icons } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchTask } from 'Store/Actions/systemActions'; -import createCommandSelector from 'Store/Selectors/createCommandSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import { isCommandExecuting } from 'Utilities/Command'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import styles from './ScheduledTaskRow.css'; - -interface ScheduledTaskRowProps { - id: number; - taskName: string; - name: string; - interval: number; - lastExecution: string; - lastStartTime: string; - lastDuration: string; - nextExecution: string; -} - -function ScheduledTaskRow(props: ScheduledTaskRowProps) { - const { - id, - taskName, - name, - interval, - lastExecution, - lastStartTime, - lastDuration, - nextExecution, - } = props; - - const dispatch = useDispatch(); - - const { showRelativeDates, longDateFormat, shortDateFormat, timeFormat } = - useSelector(createUISettingsSelector()); - const command = useSelector(createCommandSelector(taskName)); - - const [time, setTime] = useState(Date.now()); - - const isQueued = !!(command && command.status === 'queued'); - const isExecuting = isCommandExecuting(command); - const wasExecuting = usePrevious(isExecuting); - const isDisabled = interval === 0; - const executeNow = !isDisabled && moment().isAfter(nextExecution); - const hasNextExecutionTime = !isDisabled && !executeNow; - const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); - - const duration = useMemo(() => { - return moment - .duration(interval, 'minutes') - .humanize() - .replace(/an?(?=\s)/, '1'); - }, [interval]); - - const { lastExecutionTime, nextExecutionTime } = useMemo(() => { - const isDisabled = interval === 0; - - if (showRelativeDates && time) { - return { - lastExecutionTime: moment(lastExecution).fromNow(), - nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow(), - }; - } - - return { - lastExecutionTime: formatDate(lastExecution, shortDateFormat), - nextExecutionTime: isDisabled - ? '-' - : formatDate(nextExecution, shortDateFormat), - }; - }, [ - time, - interval, - lastExecution, - nextExecution, - showRelativeDates, - shortDateFormat, - ]); - - const handleExecutePress = useCallback(() => { - dispatch( - executeCommand({ - name: taskName, - }) - ); - }, [taskName, dispatch]); - - useEffect(() => { - if (!isExecuting && wasExecuting) { - setTimeout(() => { - dispatch(fetchTask({ id })); - }, 1000); - } - }, [id, isExecuting, wasExecuting, dispatch]); - - useEffect(() => { - const interval = setInterval(() => setTime(Date.now()), 1000); - return () => { - clearInterval(interval); - }; - }, [setTime]); - - return ( - - {name} - - {isDisabled ? 'disabled' : duration} - - - - {lastExecutionTime} - - - {hasLastStartTime ? ( - - {formatTimeSpan(lastDuration)} - - ) : ( - - - )} - - {isDisabled ? ( - - - ) : null} - - {executeNow && isQueued ? ( - queued - ) : null} - - {executeNow && !isQueued ? ( - now - ) : null} - - {hasNextExecutionTime ? ( - - {nextExecutionTime} - - ) : null} - - - - - - ); -} - -export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js new file mode 100644 index 000000000..dae790d68 --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchTask } from 'Store/Actions/systemActions'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { findCommand, isCommandExecuting } from 'Utilities/Command'; +import ScheduledTaskRow from './ScheduledTaskRow'; + +function createMapStateToProps() { + return createSelector( + (state, { taskName }) => taskName, + createCommandsSelector(), + createUISettingsSelector(), + (taskName, commands, uiSettings) => { + const command = findCommand(commands, { name: taskName }); + + return { + isQueued: !!(command && command.state === 'queued'), + isExecuting: isCommandExecuting(command), + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + const taskName = props.taskName; + + return { + dispatchFetchTask() { + dispatch(fetchTask({ + id: props.id + })); + }, + + onExecutePress() { + dispatch(executeCommand({ + name: taskName + })); + } + }; +} + +class ScheduledTaskRowConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + const { + isExecuting, + dispatchFetchTask + } = this.props; + + if (!isExecuting && prevProps.isExecuting) { + // Give the host a moment to update after the command completes + setTimeout(() => { + dispatchFetchTask(); + }, 1000); + } + } + + // + // Render + + render() { + const { + dispatchFetchTask, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +ScheduledTaskRowConnector.propTypes = { + id: PropTypes.number.isRequired, + isExecuting: PropTypes.bool.isRequired, + dispatchFetchTask: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector); diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js new file mode 100644 index 000000000..bec151613 --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js @@ -0,0 +1,85 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import translate from 'Utilities/String/translate'; +import ScheduledTaskRowConnector from './ScheduledTaskRowConnector'; + +const columns = [ + { + name: 'name', + label: () => translate('Name'), + isVisible: true + }, + { + name: 'interval', + label: () => translate('Interval'), + isVisible: true + }, + { + name: 'lastExecution', + label: () => translate('LastExecution'), + isVisible: true + }, + { + name: 'lastDuration', + label: () => translate('LastDuration'), + isVisible: true + }, + { + name: 'nextExecution', + label: () => translate('NextExecution'), + isVisible: true + }, + { + name: 'actions', + isVisible: true + } +]; + +function ScheduledTasks(props) { + const { + isFetching, + isPopulated, + items + } = props; + + return ( +
+ { + isFetching && !isPopulated && + + } + + { + isPopulated && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } +
+ ); +} + +ScheduledTasks.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx deleted file mode 100644 index fcf5764bb..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Column from 'Components/Table/Column'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { fetchTasks } from 'Store/Actions/systemActions'; -import translate from 'Utilities/String/translate'; -import ScheduledTaskRow from './ScheduledTaskRow'; - -const columns: Column[] = [ - { - name: 'name', - label: () => translate('Name'), - isVisible: true, - }, - { - name: 'interval', - label: () => translate('Interval'), - isVisible: true, - }, - { - name: 'lastExecution', - label: () => translate('LastExecution'), - isVisible: true, - }, - { - name: 'lastDuration', - label: () => translate('LastDuration'), - isVisible: true, - }, - { - name: 'nextExecution', - label: () => translate('NextExecution'), - isVisible: true, - }, - { - name: 'actions', - label: '', - isVisible: true, - }, -]; - -function ScheduledTasks() { - const dispatch = useDispatch(); - const { isFetching, isPopulated, items } = useSelector( - (state: AppState) => state.system.tasks - ); - - useEffect(() => { - dispatch(fetchTasks()); - }, [dispatch]); - - return ( -
- {isFetching && !isPopulated && } - - {isPopulated && ( - - - {items.map((item) => { - return ; - })} - -
- )} -
- ); -} - -export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js new file mode 100644 index 000000000..8f418d3bb --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchTasks } from 'Store/Actions/systemActions'; +import ScheduledTasks from './ScheduledTasks'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.tasks, + (tasks) => { + return tasks; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchTasks: fetchTasks +}; + +class ScheduledTasksConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchTasks(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ScheduledTasksConnector.propTypes = { + dispatchFetchTasks: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.tsx b/frontend/src/System/Tasks/Tasks.js similarity index 79% rename from frontend/src/System/Tasks/Tasks.tsx rename to frontend/src/System/Tasks/Tasks.js index 26473d7ba..03a3b6ce4 100644 --- a/frontend/src/System/Tasks/Tasks.tsx +++ b/frontend/src/System/Tasks/Tasks.js @@ -3,13 +3,13 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; import QueuedTasks from './Queued/QueuedTasks'; -import ScheduledTasks from './Scheduled/ScheduledTasks'; +import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; function Tasks() { return ( - + diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js new file mode 100644 index 000000000..9d6b9decc --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import styles from './UpdateChanges.css'; + +class UpdateChanges extends Component { + + // + // Render + + render() { + const { + title, + changes + } = this.props; + + if (changes.length === 0) { + return null; + } + + return ( +
+
{title}
+
    + { + changes.map((change, index) => { + const checkChange = change.replace(/#\d{3,5}\b/g, (match, contents) => { + return `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(1)})`; + }); + + return ( +
  • + +
  • + ); + }) + } +
+
+ ); + } + +} + +UpdateChanges.propTypes = { + title: PropTypes.string.isRequired, + changes: PropTypes.arrayOf(PropTypes.string) +}; + +export default UpdateChanges; diff --git a/frontend/src/System/Updates/UpdateChanges.tsx b/frontend/src/System/Updates/UpdateChanges.tsx deleted file mode 100644 index 460814cbe..000000000 --- a/frontend/src/System/Updates/UpdateChanges.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import styles from './UpdateChanges.css'; - -interface UpdateChangesProps { - title: string; - changes: string[]; -} - -function UpdateChanges(props: UpdateChangesProps) { - const { title, changes } = props; - - if (changes.length === 0) { - return null; - } - - const uniqueChanges = [...new Set(changes)]; - - return ( -
-
{title}
-
    - {uniqueChanges.map((change, index) => { - const checkChange = change.replace( - /#\d{3,5}\b/g, - (match) => - `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring( - 1 - )})` - ); - - return ( -
  • - -
  • - ); - })} -
-
- ); -} - -export default UpdateChanges; diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js new file mode 100644 index 000000000..40ab58c75 --- /dev/null +++ b/frontend/src/System/Updates/Updates.js @@ -0,0 +1,252 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons, kinds } from 'Helpers/Props'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import translate from 'Utilities/String/translate'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +class Updates extends Component { + + // + // Render + + render() { + const { + currentVersion, + isFetching, + isPopulated, + updatesError, + generalSettingsError, + items, + isInstallingUpdate, + updateMechanism, + isDocker, + updateMechanismMessage, + shortDateFormat, + longDateFormat, + timeFormat, + onInstallLatestPress + } = this.props; + + const hasError = !!(updatesError || generalSettingsError); + const hasUpdates = isPopulated && !hasError && items.length > 0; + const noUpdates = isPopulated && !hasError && !items.length; + const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + const externalUpdaterPrefix = 'Unable to update Prowlarr directly,'; + const externalUpdaterMessages = { + external: 'Prowlarr is configured to use an external update mechanism', + apt: 'use apt to install the update', + docker: 'update the docker container to receive the update' + }; + + return ( + + + { + !isPopulated && !hasError && + + } + + { + noUpdates && + + {translate('NoUpdatesAreAvailable')} + + } + + { + hasUpdateToInstall && +
+ { + (updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ? + + Install Latest + : + + + + +
+ {externalUpdaterPrefix} +
+
+ } + + { + isFetching && + + } +
+ } + + { + noUpdateToInstall && +
+ + +
+ {translate('TheLatestVersionIsAlreadyInstalled')} +
+ + { + isFetching && + + } +
+ } + + { + hasUpdates && +
+ { + items.map((update) => { + const hasChanges = !!update.changes; + + return ( +
+
+
{update.version}
+
+
+ {formatDate(update.releaseDate, shortDateFormat)} +
+ + { + update.branch === 'master' ? + null: + + } + + { + update.version === currentVersion ? + : + null + } + + { + update.version !== currentVersion && update.installedOn ? + : + null + } +
+ + { + !hasChanges && +
+ {translate('MaintenanceRelease')} +
+ } + + { + hasChanges && +
+ + + +
+ } +
+ ); + }) + } +
+ } + + { + !!updatesError && +
+ Failed to fetch updates +
+ } + + { + !!generalSettingsError && +
+ Failed to update settings +
+ } +
+
+ ); + } + +} + +Updates.propTypes = { + currentVersion: PropTypes.string.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + updatesError: PropTypes.object, + generalSettingsError: PropTypes.object, + items: PropTypes.array.isRequired, + isInstallingUpdate: PropTypes.bool.isRequired, + isDocker: PropTypes.bool.isRequired, + updateMechanism: PropTypes.string, + updateMechanismMessage: PropTypes.string, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onInstallLatestPress: PropTypes.func.isRequired +}; + +export default Updates; diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx deleted file mode 100644 index ea309a1cc..000000000 --- a/frontend/src/System/Updates/Updates.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import * as commandNames from 'Commands/commandNames'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons, kinds } from 'Helpers/Props'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import { UpdateMechanism } from 'typings/Settings/General'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import translate from 'Utilities/String/translate'; -import UpdateChanges from './UpdateChanges'; -import styles from './Updates.css'; - -const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; - -function createUpdatesSelector() { - return createSelector( - (state: AppState) => state.system.updates, - (state: AppState) => state.settings.general, - (updates, generalSettings) => { - const { error: updatesError, items } = updates; - - const isFetching = updates.isFetching || generalSettings.isFetching; - const isPopulated = updates.isPopulated && generalSettings.isPopulated; - - return { - isFetching, - isPopulated, - updatesError, - generalSettingsError: generalSettings.error, - items, - updateMechanism: generalSettings.item.updateMechanism, - }; - } - ); -} - -function Updates() { - const currentVersion = useSelector((state: AppState) => state.app.version); - const { packageUpdateMechanismMessage } = useSelector( - createSystemStatusSelector() - ); - const { shortDateFormat, longDateFormat, timeFormat } = useSelector( - createUISettingsSelector() - ); - const isInstallingUpdate = useSelector( - createCommandExecutingSelector(commandNames.APPLICATION_UPDATE) - ); - - const { - isFetching, - isPopulated, - updatesError, - generalSettingsError, - items, - updateMechanism, - } = useSelector(createUpdatesSelector()); - - const dispatch = useDispatch(); - const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); - const hasError = !!(updatesError || generalSettingsError); - const hasUpdates = isPopulated && !hasError && items.length > 0; - const noUpdates = isPopulated && !hasError && !items.length; - - const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); - const externalUpdaterMessages: Partial> = { - external: translate('ExternalUpdater'), - apt: translate('AptUpdater'), - docker: translate('DockerUpdater'), - }; - - const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => { - const majorVersion = parseInt( - currentVersion.match(VERSION_REGEX)?.[0] ?? '0' - ); - - const latestVersion = items[0]?.version; - const latestMajorVersion = parseInt( - latestVersion?.match(VERSION_REGEX)?.[0] ?? '0' - ); - - return { - isMajorUpdate: latestMajorVersion > majorVersion, - hasUpdateToInstall: items.some( - (update) => update.installable && update.latest - ), - }; - }, [currentVersion, items]); - - const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; - - const handleInstallLatestPress = useCallback(() => { - if (isMajorUpdate) { - setIsMajorUpdateModalOpen(true); - } else { - dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE })); - } - }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]); - - const handleInstallLatestMajorVersionPress = useCallback(() => { - setIsMajorUpdateModalOpen(false); - - dispatch( - executeCommand({ - name: commandNames.APPLICATION_UPDATE, - installMajorUpdate: true, - }) - ); - }, [setIsMajorUpdateModalOpen, dispatch]); - - const handleCancelMajorVersionPress = useCallback(() => { - setIsMajorUpdateModalOpen(false); - }, [setIsMajorUpdateModalOpen]); - - useEffect(() => { - dispatch(fetchUpdates()); - dispatch(fetchGeneralSettings()); - }, [dispatch]); - - return ( - - - {isPopulated || hasError ? null : } - - {noUpdates ? ( - {translate('NoUpdatesAreAvailable')} - ) : null} - - {hasUpdateToInstall ? ( -
- {updateMechanism === 'builtIn' || updateMechanism === 'script' ? ( - - {translate('InstallLatest')} - - ) : ( - <> - - -
- {externalUpdaterPrefix}{' '} - -
- - )} - - {isFetching ? ( - - ) : null} -
- ) : null} - - {noUpdateToInstall && ( -
- -
{translate('OnLatestVersion')}
- - {isFetching && ( - - )} -
- )} - - {hasUpdates && ( -
- {items.map((update) => { - return ( -
-
-
{update.version}
-
-
- {formatDate(update.releaseDate, shortDateFormat)} -
- - {update.branch === 'master' ? null : ( - - )} - - {update.version === currentVersion ? ( - - ) : null} - - {update.version !== currentVersion && update.installedOn ? ( - - ) : null} -
- - {update.changes ? ( -
- - - -
- ) : ( -
{translate('MaintenanceRelease')}
- )} -
- ); - })} -
- )} - - {updatesError ? ( - - {translate('FailedToFetchUpdates')} - - ) : null} - - {generalSettingsError ? ( - - {translate('FailedToFetchSettings')} - - ) : null} - - -
{translate('InstallMajorVersionUpdateMessage')}
-
- -
- - } - confirmLabel={translate('Install')} - onConfirm={handleInstallLatestMajorVersionPress} - onCancel={handleCancelMajorVersionPress} - /> -
-
- ); -} - -export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js new file mode 100644 index 000000000..38873a990 --- /dev/null +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -0,0 +1,101 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as commandNames from 'Commands/commandNames'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import Updates from './Updates'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.version, + createSystemStatusSelector(), + (state) => state.system.updates, + (state) => state.settings.general, + createUISettingsSelector(), + createSystemStatusSelector(), + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), + ( + currentVersion, + status, + updates, + generalSettings, + uiSettings, + systemStatus, + isInstallingUpdate + ) => { + const { + error: updatesError, + items + } = updates; + + const isFetching = updates.isFetching || generalSettings.isFetching; + const isPopulated = updates.isPopulated && generalSettings.isPopulated; + + return { + currentVersion, + isFetching, + isPopulated, + updatesError, + generalSettingsError: generalSettings.error, + items, + isInstallingUpdate, + isDocker: systemStatus.isDocker, + updateMechanism: generalSettings.item.updateMechanism, + updateMechanismMessage: status.packageUpdateMechanismMessage, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchUpdates: fetchUpdates, + dispatchFetchGeneralSettings: fetchGeneralSettings, + dispatchExecuteCommand: executeCommand +}; + +class UpdatesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchUpdates(); + this.props.dispatchFetchGeneralSettings(); + } + + // + // Listeners + + onInstallLatestPress = () => { + this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +UpdatesConnector.propTypes = { + dispatchFetchUpdates: PropTypes.func.isRequired, + dispatchFetchGeneralSettings: PropTypes.func.isRequired, + dispatchExecuteCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js new file mode 100644 index 000000000..1956d3bac --- /dev/null +++ b/frontend/src/Utilities/Array/sortByName.js @@ -0,0 +1,5 @@ +function sortByName(a, b) { + return a.name.localeCompare(b.name); +} + +export default sortByName; diff --git a/frontend/src/Utilities/Array/sortByProp.ts b/frontend/src/Utilities/Array/sortByProp.ts deleted file mode 100644 index 8fbde08c9..000000000 --- a/frontend/src/Utilities/Array/sortByProp.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { StringKey } from 'typings/Helpers/KeysMatching'; - -export function sortByProp< - // eslint-disable-next-line no-use-before-define - T extends Record, - K extends StringKey ->(sortKey: K) { - return (a: T, b: T) => { - return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true }); - }; -} - -export default sortByProp; diff --git a/frontend/src/Utilities/Number/formatBytes.ts b/frontend/src/Utilities/Number/formatBytes.js similarity index 61% rename from frontend/src/Utilities/Number/formatBytes.ts rename to frontend/src/Utilities/Number/formatBytes.js index a0ae8a985..2fb3eebe6 100644 --- a/frontend/src/Utilities/Number/formatBytes.ts +++ b/frontend/src/Utilities/Number/formatBytes.js @@ -1,16 +1,16 @@ import { filesize } from 'filesize'; -function formatBytes(input: string | number) { +function formatBytes(input) { const size = Number(input); if (isNaN(size)) { return ''; } - return `${filesize(size, { + return filesize(size, { base: 2, - round: 1, - })}`; + round: 1 + }); } export default formatBytes; diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts index 72d3adf40..1faa70ecc 100644 --- a/frontend/src/Utilities/String/translate.ts +++ b/frontend/src/Utilities/String/translate.ts @@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise { translations = data.Strings; resolve(true); - } catch { + } catch (error) { resolve(false); } }); diff --git a/frontend/src/Utilities/getPathWithUrlBase.js b/frontend/src/Utilities/getPathWithUrlBase.js new file mode 100644 index 000000000..b687f2682 --- /dev/null +++ b/frontend/src/Utilities/getPathWithUrlBase.js @@ -0,0 +1,3 @@ +export default function getPathWithUrlBase(path) { + return `${window.Prowlarr.urlBase}${path}`; +} diff --git a/frontend/src/Utilities/getPathWithUrlBase.ts b/frontend/src/Utilities/getPathWithUrlBase.ts deleted file mode 100644 index 948456728..000000000 --- a/frontend/src/Utilities/getPathWithUrlBase.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function getPathWithUrlBase(path: string) { - return `${window.Prowlarr.urlBase}${path}`; -} diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.js index 1b380851d..dae5150b7 100644 --- a/frontend/src/Utilities/getUniqueElementId.js +++ b/frontend/src/Utilities/getUniqueElementId.js @@ -1,9 +1,7 @@ let i = 0; -/** - * @deprecated Use React's useId() instead - * @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) - */ +// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + export default function getUniqueElementId() { return `id-${i++}`; } diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 000000000..acdfc6517 --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,26 @@ +import { createBrowserHistory } from 'history'; +import React from 'react'; +import { render } from 'react-dom'; +import { fetchTranslations } from 'Utilities/String/translate'; + +import './preload'; +import './polyfills'; +import 'Styles/globals.css'; +import './index.css'; + +const history = createBrowserHistory(); +const hasTranslationsError = !await fetchTranslations(); + +const { default: createAppStore } = await import('Store/createAppStore'); +const { default: App } = await import('./App/App'); + +const store = createAppStore(history); + +render( + , + document.getElementById('root') +); diff --git a/frontend/src/login.html b/frontend/src/login.html index d8af7f73f..c3773b641 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -57,8 +57,8 @@