diff --git a/.devcontainer/Lidarr.code-workspace b/.devcontainer/Lidarr.code-workspace deleted file mode 100644 index a46158e44..000000000 --- a/.devcontainer/Lidarr.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/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index d0fa03d5f..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,19 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet -{ - "name": "Lidarr", - "image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0", - "features": { - "ghcr.io/devcontainers/features/node:1": { - "nodeGypDependencies": true, - "version": "20", - "nvmVersion": "latest" - } - }, - "forwardPorts": [8686], - "customizations": { - "vscode": { - "extensions": ["esbenp.prettier-vscode"] - } - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 491815370..31f001e52 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -60,7 +60,6 @@ body: - Master - Develop - Nightly - - Plugins (experimental) - Other (This issue will be closed) validations: required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f33a02cd1..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for more information: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates -# https://containers.dev/guide/dependabot - -version: 2 -updates: - - package-ecosystem: "devcontainers" - directory: "/" - schedule: - interval: weekly diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml index a6246a6b3..a7fc89446 100644 --- a/.github/workflows/label-actions.yml +++ b/.github/workflows/label-actions.yml @@ -12,6 +12,6 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/label-actions@v4 + - uses: dessant/label-actions@v3 with: process-only: 'issues' 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/.gitignore b/.gitignore index a5d6bb7c8..d2dc01467 100644 --- a/.gitignore +++ b/.gitignore @@ -121,13 +121,11 @@ _artifacts _rawPackage/ _dotTrace* _tests/ -_temp* *.Result.xml coverage*.xml coverage*.json setup/Output/ *.~is -.mono # VS outout folders bin @@ -140,6 +138,12 @@ project.fragment.lock.json artifacts/ **/Properties/launchSettings.json +#VS outout folders +bin +obj +output/* + + # macOS metadata files ._* .DS_Store @@ -158,12 +162,34 @@ Thumbs.db /tools/Addins/* packages.config.md5sum + +# Common IntelliJ Platform excludes + +# User specific +**/.idea/**/workspace.xml +**/.idea/**/tasks.xml +**/.idea/shelf/* +**/.idea/dictionaries +**/.idea/.idea.Radarr.Posix +**/.idea/.idea.Radarr.Windows + +# Sensitive or high-churn files +**/.idea/**/dataSources/ +**/.idea/**/dataSources.ids +**/.idea/**/dataSources.xml +**/.idea/**/dataSources.local.xml +**/.idea/**/sqlDataSources.xml +**/.idea/**/dynamic.xml + +# Rider +# Rider auto-generates .iml files, and contentModel.xml +**/.idea/**/*.iml +**/.idea/**/contentModel.xml +**/.idea/**/modules.xml + # ignore node_modules symlink node_modules node_modules.nosync # API doc generation .config/ - -# Ignore Jetbrains IntelliJ Workspace Directories -.idea/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 7a36fefe1..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "esbenp.prettier-vscode", - "ms-dotnettools.csdevkit", - "ms-vscode-remote.remote-containers" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 74b8d418b..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md - "name": "Run Lidarr", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build dotnet", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/_output/net6.0/Lidarr", - "args": [], - "cwd": "${workspaceFolder}", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "integratedTerminal", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 4b3b00b89..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build dotnet", - "command": "dotnet", - "type": "process", - "args": [ - "msbuild", - "-restore", - "${workspaceFolder}/src/Lidarr.sln", - "-p:GenerateFullPaths=true", - "-p:Configuration=Debug", - "-p:Platform=Posix", - "-consoleloggerparameters:NoSummary;ForceNoAlign" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/src/Lidarr.sln", - "-property:GenerateFullPaths=true", - "-consoleloggerparameters:NoSummary;ForceNoAlign" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/src/Lidarr.sln" - ], - "problemMatcher": "$msCompile" - } - ] -} diff --git a/README.md b/README.md index 6e643760f..f5c8cdf84 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Lidarr [![Build Status](https://dev.azure.com/Lidarr/Lidarr/_apis/build/status/lidarr.Lidarr?branchName=develop)](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop) -[![Translation status](https://translate.servarr.com/widget/servarr/lidarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/lidarr.svg)](https://wiki.servarr.com/lidarr/installation#docker) ![Github Downloads](https://img.shields.io/github/downloads/lidarr/lidarr/total.svg) [![Backers on Open Collective](https://opencollective.com/lidarr/backers/badge.svg)](#backers) @@ -9,9 +8,6 @@ Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available. -> [!WARNING] -> NOTICE - The Lidarr Metadata Server is currently down impacting adding artists, etc. Please follow [GHI 5498](https://github.com/Lidarr/Lidarr/issues/5498) or see Discord for detaila. - ## Major Features Include: * Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 85d13499a..579799208 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: '2.13.3' + majorVersion: '2.1.7' minorVersion: $[counter('minorVersion', 1076)] lidarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(lidarrVersion)' sentryOrg: 'servarr' sentryUrl: 'https://sentry.servarr.com' - dotnetVersion: '6.0.427' - nodeVersion: '20.X' + dotnetVersion: '6.0.417' + nodeVersion: '16.X' innoVersion: '6.2.0' 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 @@ -1093,10 +1093,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 @@ -1120,19 +1120,19 @@ stages: vmImage: ${{ variables.windowsImage }} steps: - checkout: self # Need history for Sonar analysis - - task: SonarCloudPrepare@3 + - task: SonarCloudPrepare@1 env: SONAR_SCANNER_OPTS: '' inputs: SonarCloud: 'SonarCloud' organization: 'lidarr' - scannerMode: 'cli' + scannerMode: 'CLI' configMode: 'manual' cliProjectKey: 'lidarr_Lidarr.UI' cliProjectName: 'LidarrUI' cliProjectVersion: '$(lidarrVersion)' cliSources: './frontend' - - task: SonarCloudAnalyze@3 + - task: SonarCloudAnalyze@1 - job: Api_Docs displayName: API Docs @@ -1208,12 +1208,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: 'lidarr' - scannerMode: 'dotnet' + scannerMode: 'MSBuild' projectKey: 'lidarr_Lidarr' projectName: 'Lidarr' projectVersion: '$(lidarrVersion)' @@ -1226,16 +1226,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/distribution/debian/install.sh b/distribution/debian/install.sh index b71eb20c9..6eff79eaa 100644 --- a/distribution/debian/install.sh +++ b/distribution/debian/install.sh @@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ') app_guid=${app_guid:-media} echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory" -echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories" +echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories" read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty # Create User / Group as needed @@ -114,7 +114,7 @@ case "$ARCH" in esac echo "" echo "Removing previous tarballs" -# -f to Force so we fail if it doesn't exist +# -f to Force so we fail if it doesnt exist rm -f "${app^}".*.tar.gz echo "" echo "Downloading..." diff --git a/docs.sh b/docs.sh index a44dc90ce..9cbb02756 100644 --- 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,21 +21,15 @@ slnFile=src/Lidarr.sln platform=Posix -if [ "$PLATFORM" = "Windows" ]; then - application=Lidarr.Console.dll -else - application=Lidarr.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 6.6.2 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 & +dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/lidarr.console.dll" v1 & sleep 45 diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index cc26a2633..603b20a48 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -28,8 +28,7 @@ module.exports = { globals: { expect: false, chai: false, - sinon: false, - JSX: true + sinon: false }, parserOptions: { diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index 8da95337f..edb88e0e7 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -9,7 +9,7 @@ "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll": "explicit" + "source.fixAll": true }, "typescript.preferences.quoteStyle": "single", diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index d1873380e..e0ec27c27 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -26,7 +26,6 @@ module.exports = (env) => { const config = { mode: isProduction ? 'production' : 'development', devtool: isProduction ? 'source-map' : 'eval-source-map', - target: 'web', stats: { children: false @@ -68,7 +67,7 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: isProduction ? '[name]-[contenthash].js' : '[name].js', + filename: '[name]-[contenthash].js', sourceMapFilename: '[file].map' }, @@ -93,7 +92,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({ @@ -135,12 +134,6 @@ module.exports = (env) => { { source: 'frontend/src/Content/robots.txt', destination: path.join(distFolder, 'Content/robots.txt') - }, - - // manifest.json and browserconfig.xml - { - source: 'frontend/src/Content/*.(json|xml)', - destination: path.join(distFolder, 'Content') } ] } @@ -188,7 +181,7 @@ module.exports = (env) => { loose: true, debug: false, useBuiltIns: 'entry', - corejs: '3.41' + corejs: 3 } ] ] @@ -209,7 +202,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/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js index 84aa3e0f2..b90a64f47 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.js +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -172,8 +172,7 @@ function HistoryDetails(props) { if (eventType === 'downloadFailed') { const { - message, - indexer + message } = data; return ( @@ -193,14 +192,6 @@ function HistoryDetails(props) { null } - { - indexer ? ( - - ) : null} - { message ? { diff --git a/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js b/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js index cda224e2f..5a837b1eb 100644 --- a/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js +++ b/frontend/src/AddArtist/ArtistMonitorNewItemsOptionsPopoverContent.js @@ -8,17 +8,17 @@ function ArtistMonitorNewItemsOptionsPopoverContent() { ); diff --git a/frontend/src/Album/Album.ts b/frontend/src/Album/Album.ts index 86f1ed5fe..c9f10a87c 100644 --- a/frontend/src/Album/Album.ts +++ b/frontend/src/Album/Album.ts @@ -10,7 +10,6 @@ export interface Statistics { } interface Album extends ModelBase { - artistId: number; artist: Artist; foreignAlbumId: string; title: string; @@ -20,7 +19,6 @@ interface Album extends ModelBase { monitored: boolean; releaseDate: string; statistics: Statistics; - lastSearchTime?: string; isSaving?: boolean; } diff --git a/frontend/src/Album/AlbumTitleLink.js b/frontend/src/Album/AlbumTitleLink.js index e55fadfc0..8b4dfe212 100644 --- a/frontend/src/Album/AlbumTitleLink.js +++ b/frontend/src/Album/AlbumTitleLink.js @@ -4,11 +4,10 @@ import Link from 'Components/Link/Link'; function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) { const link = `/album/${foreignAlbumId}`; - const albumTitle = `${title}${disambiguation ? ` (${disambiguation})` : ''}`; return ( - - {albumTitle} + + {title}{disambiguation ? ` (${disambiguation})` : ''} ); } diff --git a/frontend/src/Album/Delete/DeleteAlbumModalContent.js b/frontend/src/Album/Delete/DeleteAlbumModalContent.js index 28505ea75..e45985c97 100644 --- a/frontend/src/Album/Delete/DeleteAlbumModalContent.js +++ b/frontend/src/Album/Delete/DeleteAlbumModalContent.js @@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component { render() { const { title, - statistics = {}, + statistics, onModalClose } = this.props; diff --git a/frontend/src/Album/Details/AlbumDetails.css b/frontend/src/Album/Details/AlbumDetails.css index a676ae574..d87920074 100644 --- a/frontend/src/Album/Details/AlbumDetails.css +++ b/frontend/src/Album/Details/AlbumDetails.css @@ -121,8 +121,6 @@ .releaseDate, .sizeOnDisk, -.albumType, -.secondaryTypes, .qualityProfileName, .links, .tags { diff --git a/frontend/src/Album/Details/AlbumDetails.css.d.ts b/frontend/src/Album/Details/AlbumDetails.css.d.ts index 1d14a0ccf..4c126c8b5 100644 --- a/frontend/src/Album/Details/AlbumDetails.css.d.ts +++ b/frontend/src/Album/Details/AlbumDetails.css.d.ts @@ -3,7 +3,6 @@ interface CssExports { 'albumNavigationButton': string; 'albumNavigationButtons': string; - 'albumType': string; 'alternateTitlesIconContainer': string; 'backdrop': string; 'backdropOverlay': string; @@ -21,7 +20,6 @@ interface CssExports { 'overview': string; 'qualityProfileName': string; 'releaseDate': string; - 'secondaryTypes': string; 'sizeOnDisk': string; 'tags': string; 'title': string; diff --git a/frontend/src/Album/Details/AlbumDetails.js b/frontend/src/Album/Details/AlbumDetails.js index fe007e168..783216a61 100644 --- a/frontend/src/Album/Details/AlbumDetails.js +++ b/frontend/src/Album/Details/AlbumDetails.js @@ -192,7 +192,6 @@ class AlbumDetails extends Component { duration, overview, albumType, - secondaryTypes, statistics = {}, monitored, releaseDate, @@ -205,7 +204,6 @@ class AlbumDetails extends Component { isFetching, isPopulated, albumsError, - tracksError, trackFilesError, hasTrackFiles, shortDateFormat, @@ -398,11 +396,10 @@ class AlbumDetails extends Component {
{ - duration ? + !!duration && {formatDuration(duration)} - : - null + } -
- - - {moment(releaseDate).format(shortDateFormat)} - -
+ + + + {moment(releaseDate).format(shortDateFormat)} + -
- - - {formatBytes(sizeOnDisk)} - -
+ + + + { + formatBytes(sizeOnDisk || 0) + } + } tooltip={ @@ -462,55 +459,32 @@ class AlbumDetails extends Component { className={styles.detailsLabel} size={sizes.LARGE} > -
- - - {monitored ? translate('Monitored') : translate('Unmonitored')} - -
+ + + + {monitored ? translate('Monitored') : translate('Unmonitored')} + { - albumType ? + !!albumType && : - null - } + - { - secondaryTypes.length ? - : - null + + {albumType} + + } -
- - - {translate('Links')} - -
+ + + + {translate('Links')} + } tooltip={ @@ -553,9 +526,8 @@ class AlbumDetails extends Component {
{ - !isPopulated && !albumsError && !tracksError && !trackFilesError ? - : - null + !isPopulated && !albumsError && !trackFilesError && + } { @@ -566,14 +538,6 @@ class AlbumDetails extends Component { null } - { - !isFetching && tracksError ? - - {translate('TracksLoadError')} - : - null - } - { !isFetching && trackFilesError ? @@ -602,14 +566,6 @@ class AlbumDetails extends Component {
} - { - isPopulated && !media.length ? - - {translate('NoMediumInformation')} - : - null - } -
{ if (track.trackFileId) { + trackCount++; trackFileCount++; + } else { + trackCount++; } totalTrackCount++; diff --git a/frontend/src/Album/Details/TrackRow.css b/frontend/src/Album/Details/TrackRow.css index 912c00101..11ebb64fa 100644 --- a/frontend/src/Album/Details/TrackRow.css +++ b/frontend/src/Album/Details/TrackRow.css @@ -35,9 +35,3 @@ width: 55px; } - -.indexerFlags { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 50px; -} diff --git a/frontend/src/Album/Details/TrackRow.css.d.ts b/frontend/src/Album/Details/TrackRow.css.d.ts index 79bbdaf43..c5644a2d4 100644 --- a/frontend/src/Album/Details/TrackRow.css.d.ts +++ b/frontend/src/Album/Details/TrackRow.css.d.ts @@ -4,7 +4,6 @@ interface CssExports { 'audio': string; 'customFormatScore': string; 'duration': string; - 'indexerFlags': string; 'monitored': string; 'size': string; 'status': string; diff --git a/frontend/src/Album/Details/TrackRow.js b/frontend/src/Album/Details/TrackRow.js index db128d493..5f60df882 100644 --- a/frontend/src/Album/Details/TrackRow.js +++ b/frontend/src/Album/Details/TrackRow.js @@ -2,19 +2,15 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import AlbumFormats from 'Album/AlbumFormats'; import EpisodeStatusConnector from 'Album/EpisodeStatusConnector'; -import IndexerFlags from 'Album/IndexerFlags'; -import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; -import Popover from 'Components/Tooltip/Popover'; import Tooltip from 'Components/Tooltip/Tooltip'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import { tooltipPositions } from 'Helpers/Props'; import MediaInfoConnector from 'TrackFile/MediaInfoConnector'; import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import formatBytes from 'Utilities/Number/formatBytes'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import translate from 'Utilities/String/translate'; import TrackActionsCell from './TrackActionsCell'; import styles from './TrackRow.css'; @@ -36,7 +32,6 @@ class TrackRow extends Component { trackFileSize, customFormats, customFormatScore, - indexerFlags, columns, deleteTrackFile } = this.props; @@ -146,30 +141,12 @@ class TrackRow extends Component { customFormats.length )} tooltip={} - position={tooltipPositions.LEFT} + position={tooltipPositions.BOTTOM} /> ); } - if (name === 'indexerFlags') { - return ( - - {indexerFlags ? ( - } - title={translate('IndexerFlags')} - body={} - position={tooltipPositions.LEFT} - /> - ) : null} - - ); - } - if (name === 'size') { return ( (indexerFlags & item.id) === item.id - ); - - return flags.length ? ( -
    - {flags.map((flag, index) => { - return
  • {flag.name}
  • ; - })} -
- ) : null; -} - -export default IndexerFlags; diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModal.js b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js index 6ce488615..5049658a0 100644 --- a/frontend/src/Album/Search/AlbumInteractiveSearchModal.js +++ b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js @@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) { return ( diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js index 370f67ab1..97261ee35 100644 --- a/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js +++ b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js @@ -7,7 +7,6 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { scrollDirections } from 'Helpers/Props'; import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector'; -import translate from 'Utilities/String/translate'; function AlbumInteractiveSearchModalContent(props) { const { @@ -19,10 +18,7 @@ function AlbumInteractiveSearchModalContent(props) { return ( - {albumTitle === undefined ? - translate('InteractiveSearchModalHeader') : - translate('InteractiveSearchModalHeaderTitle', { title: albumTitle }) - } + Interactive Search {albumId != null && `- ${albumTitle}`} @@ -36,7 +32,7 @@ function AlbumInteractiveSearchModalContent(props) { diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js index 9e8d508ac..3871b14e9 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.js @@ -12,10 +12,11 @@ function App({ store, history }) { - - - - + + + + + diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index c1004d36d..0af990f43 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector'; import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; import AddNewItemConnector from 'Search/AddNewItemConnector'; -import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage'; +import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; @@ -29,7 +29,7 @@ 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 UpdatesConnector from 'System/Updates/UpdatesConnector'; import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; @@ -184,7 +184,7 @@ function AppRoutes(props) { state.settings.ui.item.theme || window.Lidarr.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 e04dda8c4..000000000 --- a/frontend/src/App/ApplyTheme.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { Fragment, ReactNode, useCallback, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import themes from 'Styles/Themes'; -import AppState from './State/AppState'; - -interface ApplyThemeProps { - children: ReactNode; -} - -function createThemeSelector() { - return createSelector( - (state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme, - (theme) => { - return theme; - } - ); -} - -function ApplyTheme({ children }: ApplyThemeProps) { - 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 {children}; -} - -export default ApplyTheme; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index cb8da5987..04f7a609f 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -1,12 +1,8 @@ -import ParseAppState from 'App/State/ParseAppState'; import AlbumAppState from './AlbumAppState'; import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState'; -import CalendarAppState from './CalendarAppState'; -import CommandAppState from './CommandAppState'; import HistoryAppState from './HistoryAppState'; import QueueAppState from './QueueAppState'; import SettingsAppState from './SettingsAppState'; -import SystemAppState from './SystemAppState'; import TagsAppState from './TagsAppState'; import TrackFilesAppState from './TrackFilesAppState'; import TracksAppState from './TracksAppState'; @@ -44,7 +40,6 @@ export interface CustomFilter { } export interface AppSectionState { - version: string; dimensions: { isSmallScreen: boolean; width: number; @@ -57,16 +52,12 @@ interface AppState { app: AppSectionState; artist: ArtistAppState; artistIndex: ArtistIndexAppState; - calendar: CalendarAppState; - commands: CommandAppState; history: HistoryAppState; - parse: ParseAppState; queue: QueueAppState; settings: SettingsAppState; tags: TagsAppState; trackFiles: TrackFilesAppState; tracksSelection: TracksAppState; - system: SystemAppState; } export default AppState; diff --git a/frontend/src/App/State/CalendarAppState.ts b/frontend/src/App/State/CalendarAppState.ts deleted file mode 100644 index 503d2c25b..000000000 --- a/frontend/src/App/State/CalendarAppState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Album from 'Album/Album'; -import AppSectionState, { - AppSectionFilterState, -} from 'App/State/AppSectionState'; - -interface CalendarAppState - extends AppSectionState, - AppSectionFilterState {} - -export default CalendarAppState; diff --git a/frontend/src/App/State/CommandAppState.ts b/frontend/src/App/State/CommandAppState.ts deleted file mode 100644 index 1bde37371..000000000 --- a/frontend/src/App/State/CommandAppState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import AppSectionState from 'App/State/AppSectionState'; -import Command from 'Commands/Command'; - -export type CommandAppState = AppSectionState; - -export default CommandAppState; diff --git a/frontend/src/App/State/ParseAppState.ts b/frontend/src/App/State/ParseAppState.ts deleted file mode 100644 index 827d5b1a7..000000000 --- a/frontend/src/App/State/ParseAppState.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Album from 'Album/Album'; -import ModelBase from 'App/ModelBase'; -import { AppSectionItemState } from 'App/State/AppSectionState'; -import Artist from 'Artist/Artist'; -import { QualityModel } from 'Quality/Quality'; -import CustomFormat from 'typings/CustomFormat'; - -export interface ArtistTitleInfo { - title: string; -} - -export interface ParsedAlbumInfo { - albumTitle: string; - artistName: string; - artistTitleInfo: ArtistTitleInfo; - discography: boolean; - quality: QualityModel; - releaseGroup?: string; - releaseHash: string; - releaseTitle: string; - releaseTokens: string; -} - -export interface ParseModel extends ModelBase { - title: string; - parsedAlbumInfo: ParsedAlbumInfo; - artist?: Artist; - albums: Album[]; - customFormats?: CustomFormat[]; - customFormatScore?: number; -} - -type ParseAppState = AppSectionItemState; - -export default ParseAppState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index b387e13fd..8511b5e2b 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -1,28 +1,22 @@ import AppSectionState, { AppSectionDeleteState, - AppSectionItemState, AppSectionSaveState, AppSectionSchemaState, } from 'App/State/AppSectionState'; -import CustomFormat from 'typings/CustomFormat'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; import Indexer from 'typings/Indexer'; -import IndexerFlag from 'typings/IndexerFlag'; import MetadataProfile from 'typings/MetadataProfile'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; import RootFolder from 'typings/RootFolder'; -import General from 'typings/Settings/General'; -import UiSettings from 'typings/Settings/UiSettings'; +import { UiSettings } from 'typings/UiSettings'; export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, AppSectionSaveState {} -export type GeneralAppState = AppSectionItemState; - export interface ImportListAppState extends AppSectionState, AppSectionDeleteState, @@ -45,32 +39,22 @@ export interface MetadataProfilesAppState extends AppSectionState, AppSectionSchemaState {} -export interface CustomFormatAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - export interface RootFolderAppState extends AppSectionState, AppSectionDeleteState, AppSectionSaveState {} -export type IndexerFlagSettingsAppState = AppSectionState; -export type UiSettingsAppState = AppSectionItemState; +export type UiSettingsAppState = AppSectionState; interface SettingsAppState { - advancedSettings: boolean; - customFormats: CustomFormatAppState; downloadClients: DownloadClientAppState; - general: GeneralAppState; importLists: ImportListAppState; - indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; metadataProfiles: MetadataProfilesAppState; notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; rootFolders: RootFolderAppState; - ui: UiSettingsAppState; + uiSettings: UiSettingsAppState; } export default SettingsAppState; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts deleted file mode 100644 index 3c150fcfb..000000000 --- a/frontend/src/App/State/SystemAppState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import SystemStatus from 'typings/SystemStatus'; -import Update from 'typings/Update'; -import AppSectionState, { AppSectionItemState } from './AppSectionState'; - -export type SystemStatusAppState = AppSectionItemState; -export type UpdateAppState = AppSectionState; - -interface SystemAppState { - updates: UpdateAppState; - status: SystemStatusAppState; -} - -export default SystemAppState; diff --git a/frontend/src/App/State/TagsAppState.ts b/frontend/src/App/State/TagsAppState.ts index edaf3a158..d1f1d5a2f 100644 --- a/frontend/src/App/State/TagsAppState.ts +++ b/frontend/src/App/State/TagsAppState.ts @@ -1,32 +1,12 @@ import ModelBase from 'App/ModelBase'; import AppSectionState, { AppSectionDeleteState, - AppSectionSaveState, } from 'App/State/AppSectionState'; export interface Tag extends ModelBase { label: string; } -export interface TagDetail extends ModelBase { - label: string; - autoTagIds: number[]; - delayProfileIds: number[]; - downloadClientIds: []; - importListIds: number[]; - indexerIds: number[]; - notificationIds: number[]; - restrictionIds: number[]; - artistIds: number[]; -} - -export interface TagDetailAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - -interface TagsAppState extends AppSectionState, AppSectionDeleteState { - details: TagDetailAppState; -} +interface TagsAppState extends AppSectionState, AppSectionDeleteState {} export default TagsAppState; diff --git a/frontend/src/Artist/Artist.ts b/frontend/src/Artist/Artist.ts index 813dbea08..d89e32f34 100644 --- a/frontend/src/Artist/Artist.ts +++ b/frontend/src/Artist/Artist.ts @@ -23,6 +23,7 @@ export interface Ratings { interface Artist extends ModelBase { added: string; + artistMetadataId: string; foreignArtistId: string; cleanName: string; ended: boolean; diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Delete/DeleteArtistModalContent.js index ac1e2b041..0542f718b 100644 --- a/frontend/src/Artist/Delete/DeleteArtistModalContent.js +++ b/frontend/src/Artist/Delete/DeleteArtistModalContent.js @@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component { @@ -161,7 +161,9 @@ DeleteArtistModalContent.propTypes = { }; DeleteArtistModalContent.defaultProps = { - statistics: {} + statistics: { + trackFileCount: 0 + } }; export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Details/AlbumGroupInfo.js b/frontend/src/Artist/Details/AlbumGroupInfo.js index 139cd7765..0fb62d4a3 100644 --- a/frontend/src/Artist/Details/AlbumGroupInfo.js +++ b/frontend/src/Artist/Details/AlbumGroupInfo.js @@ -10,7 +10,6 @@ function AlbumGroupInfo(props) { const { totalAlbumCount, monitoredAlbumCount, - albumFileCount, trackFileCount, sizeOnDisk } = props; @@ -31,13 +30,6 @@ function AlbumGroupInfo(props) { data={monitoredAlbumCount} /> - - - {secondaryTypes.join(', ')} + { + secondaryTypes + }
); } @@ -158,7 +160,7 @@ class AlbumRow extends Component { return ( { - totalTrackCount + statistics.totalTrackCount } ); diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js index 1bfa767c3..c6fb445b7 100644 --- a/frontend/src/Artist/Details/ArtistDetails.js +++ b/frontend/src/Artist/Details/ArtistDetails.js @@ -192,7 +192,7 @@ class ArtistDetails extends Component { artistName, ratings, path, - statistics = {}, + statistics, qualityProfileId, monitored, genres, diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.js b/frontend/src/Artist/Details/ArtistDetailsSeason.js index 004613e30..37c85aa66 100644 --- a/frontend/src/Artist/Details/ArtistDetailsSeason.js +++ b/frontend/src/Artist/Details/ArtistDetailsSeason.js @@ -22,43 +22,32 @@ import styles from './ArtistDetailsSeason.css'; function getAlbumStatistics(albums) { let albumCount = 0; - let albumFileCount = 0; let trackFileCount = 0; let totalAlbumCount = 0; let monitoredAlbumCount = 0; let hasMonitoredAlbums = false; let sizeOnDisk = 0; - albums.forEach(({ monitored, releaseDate, statistics = {} }) => { - const { - trackFileCount: albumTrackFileCount = 0, - totalTrackCount: albumTotalTrackCount = 0, - sizeOnDisk: albumSizeOnDisk = 0 - } = statistics; + albums.forEach((album) => { + if (album.statistics) { + sizeOnDisk = sizeOnDisk + album.statistics.sizeOnDisk; + trackFileCount = trackFileCount + album.statistics.trackFileCount; - const hasFiles = albumTrackFileCount > 0 && albumTrackFileCount === albumTotalTrackCount; - - if (hasFiles || (monitored && isBefore(releaseDate))) { - albumCount++; + if (album.statistics.trackFileCount === album.statistics.totalTrackCount || (album.monitored && isBefore(album.airDateUtc))) { + albumCount++; + } } - if (hasFiles) { - albumFileCount++; - } - - if (monitored) { + if (album.monitored) { monitoredAlbumCount++; hasMonitoredAlbums = true; } totalAlbumCount++; - trackFileCount = trackFileCount + albumTrackFileCount; - sizeOnDisk = sizeOnDisk + albumSizeOnDisk; }); return { albumCount, - albumFileCount, totalAlbumCount, trackFileCount, monitoredAlbumCount, @@ -67,8 +56,8 @@ function getAlbumStatistics(albums) { }; } -function getAlbumCountKind(monitored, albumCount, albumFileCount) { - if (albumCount === albumFileCount && albumFileCount > 0) { +function getAlbumCountKind(monitored, albumCount, monitoredAlbumCount) { + if (albumCount === monitoredAlbumCount && monitoredAlbumCount > 0) { return kinds.SUCCESS; } @@ -203,7 +192,6 @@ class ArtistDetailsSeason extends Component { const { albumCount, - albumFileCount, totalAlbumCount, trackFileCount, monitoredAlbumCount, @@ -238,9 +226,9 @@ class ArtistDetailsSeason extends Component { anchor={ } title={translate('GroupInformation')} @@ -249,7 +237,6 @@ class ArtistDetailsSeason extends Component { diff --git a/frontend/src/Artist/Details/ArtistTagsConnector.js b/frontend/src/Artist/Details/ArtistTagsConnector.js index 1d24a5755..33ced5f0d 100644 --- a/frontend/src/Artist/Details/ArtistTagsConnector.js +++ b/frontend/src/Artist/Details/ArtistTagsConnector.js @@ -2,7 +2,6 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; import ArtistTags from './ArtistTags'; function createMapStateToProps() { @@ -13,8 +12,8 @@ function createMapStateToProps() { const tags = artist.tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) .filter((tag) => !!tag) - .sort(sortByProp('label')) - .map((tag) => tag.label); + .map((tag) => tag.label) + .sort((a, b) => a.localeCompare(b)); return { tags diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.js b/frontend/src/Artist/Edit/EditArtistModalContent.js index bca6e3ea6..82a390d84 100644 --- a/frontend/src/Artist/Edit/EditArtistModalContent.js +++ b/frontend/src/Artist/Edit/EditArtistModalContent.js @@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import Popover from 'Components/Tooltip/Popover'; -import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props'; +import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './EditArtistModalContent.css'; @@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
- + {translate('Monitored')} @@ -107,10 +107,9 @@ class EditArtistModalContent extends Component { /> - + {translate('MonitorNewItems')} - - + {translate('QualityProfile')} @@ -147,10 +146,10 @@ class EditArtistModalContent extends Component { { - showMetadataProfile ? - + showMetadataProfile && + - {translate('MetadataProfile')} + Metadata Profile - : - null + } - + {translate('Path')} @@ -191,7 +189,7 @@ class EditArtistModalContent extends Component { /> - + {translate('Tags')} @@ -211,7 +209,7 @@ class EditArtistModalContent extends Component { kind={kinds.DANGER} onPress={onDeleteArtistPress} > - {translate('Delete')} + Delete
-
- {translate('Filters')} -
+
{translate('Filters')}
{ diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 77dad7173..8be71aaff 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 ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue'; import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; @@ -11,11 +10,11 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; -import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue'; +import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector'; import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; -import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; +import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import styles from './FilterBuilderRow.css'; @@ -68,7 +67,7 @@ function getRowValueConnector(selectedFilterBuilderProp) { return IndexerFilterBuilderRowValueConnector; case filterBuilderValueTypes.METADATA_PROFILE: - return MetadataProfileFilterBuilderRowValue; + return MetadataProfileFilterBuilderRowValueConnector; case filterBuilderValueTypes.MONITOR_NEW_ITEMS: return MonitorNewItemsFilterBuilderRowValue; @@ -80,7 +79,7 @@ function getRowValueConnector(selectedFilterBuilderProp) { return QualityFilterBuilderRowValueConnector; case filterBuilderValueTypes.QUALITY_PROFILE: - return QualityProfileFilterBuilderRowValue; + return QualityProfileFilterBuilderRowValueConnector; case filterBuilderValueTypes.ARTIST: return ArtistFilterBuilderRowValue; @@ -225,7 +224,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/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx index 1b3b369be..45e6fc756 100644 --- a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx @@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [ { id: 7, get name() { - return translate('ImportCompleteFailed'); + return translate('ImportFailed'); }, }, { diff --git a/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValue.tsx deleted file mode 100644 index bbd9a8274..000000000 --- a/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValue.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; -import sortByProp from 'Utilities/Array/sortByProp'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; - -function createMetadataProfilesSelector() { - return createSelector( - (state: AppState) => state.settings.metadataProfiles.items, - (metadataProfiles) => { - return metadataProfiles; - } - ); -} - -function MetadataProfileFilterBuilderRowValue( - props: FilterBuilderRowValueProps -) { - const metadataProfiles = useSelector(createMetadataProfilesSelector()); - - const tagList = metadataProfiles - .map(({ id, name }) => ({ id, name })) - .sort(sortByProp('name')); - - return ; -} - -export default MetadataProfileFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..89d6c06b3 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.metadataProfiles, + (metadataProfiles) => { + const tagList = metadataProfiles.items.map((metadataProfile) => { + const { + id, + name + } = metadataProfile; + + return { + id, + name + }; + }); + + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx deleted file mode 100644 index 50036cb90..000000000 --- a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValue.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps'; -import sortByProp from 'Utilities/Array/sortByProp'; -import FilterBuilderRowValue from './FilterBuilderRowValue'; - -function createQualityProfilesSelector() { - return createSelector( - (state: AppState) => state.settings.qualityProfiles.items, - (qualityProfiles) => { - return qualityProfiles; - } - ); -} - -function QualityProfileFilterBuilderRowValue( - props: FilterBuilderRowValueProps -) { - const qualityProfiles = useSelector(createQualityProfilesSelector()); - - const tagList = qualityProfiles - .map(({ id, name }) => ({ id, name })) - .sort(sortByProp('name')); - - return ; -} - -export default QualityProfileFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..4a8b82283 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityProfiles, + (qualityProfiles) => { + const tagList = qualityProfiles.items.map((qualityProfile) => { + const { + id, + name + } = qualityProfile; + + return { + id, + name + }; + }); + + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js index 9f378d5a2..7407f729a 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js @@ -37,8 +37,8 @@ class CustomFilter extends Component { dispatchSetFilter } = this.props; - // Assume that delete and then unmounting means the deletion was successful. - // Moving this check to an ancestor would be more accurate, but would have + // Assume that delete and then unmounting means the delete was successful. + // Moving this check to a ancestor would be more accurate, but would have // more boilerplate. if (this.state.isDeleting && id === selectedFilterKey) { dispatchSetFilter({ selectedFilterKey: 'all' }); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index d70b97e44..cd9c07053 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 ( void; -} - -export default function ArtistTagInput(props: ArtistTagInputProps) { - const { value, onChange, ...otherProps } = props; - const isArray = Array.isArray(value); - - const handleChange = useCallback( - ({ name, value: newValue }: { name: string; value: number[] }) => { - if (isArray) { - onChange({ name, value: newValue }); - } else { - onChange({ - name, - value: newValue.length ? newValue[newValue.length - 1] : 0, - }); - } - }, - [isArray, onChange] - ); - - let finalValue: number[] = []; - - if (isArray) { - finalValue = value; - } else if (value === 0) { - finalValue = []; - } else { - finalValue = [value]; - } - - return ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore 2786 'TagInputConnector' isn't typed yet - - ); -} diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index c21f0ded6..c89016869 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -4,8 +4,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() { @@ -23,18 +22,17 @@ function createMapStateToProps() { const filteredItems = items.filter((item) => item.protocol === protocolFilter); - const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => { + const values = _.map(filteredItems.sort(sortByName), (downloadClient) => { return { key: downloadClient.id, - value: downloadClient.name, - hint: `(${downloadClient.id})` + value: downloadClient.name }; }); if (includeAny) { values.unshift({ key: 0, - value: `(${translate('Any')})` + value: '(Any)' }); } diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css index defefb18e..56f5564b9 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -19,7 +19,7 @@ .isDisabled { opacity: 0.7; - cursor: not-allowed !important; + cursor: not-allowed; } .dropdownArrowContainer { diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index 8327b9385..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; }; @@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component { order: 851, enabled: true, fn: this.onComputeMaxHeight - }, - preventOverflow: { - enabled: true, - boundariesElement: 'viewport' } }} > diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 3173b493d..c0f0bf5dd 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -4,7 +4,6 @@ import Link from 'Components/Link/Link'; import { inputTypes, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector'; -import ArtistTagInput from './ArtistTagInput'; import AutoCompleteInput from './AutoCompleteInput'; import CaptchaInputConnector from './CaptchaInputConnector'; import CheckInput from './CheckInput'; @@ -13,7 +12,6 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import FormInputHelpText from './FormInputHelpText'; -import IndexerFlagsSelectInput from './IndexerFlagsSelectInput'; import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import KeyValueListInput from './KeyValueListInput'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; @@ -49,12 +47,12 @@ function getComponent(type) { case inputTypes.DEVICE: return DeviceInputConnector; - case inputTypes.KEY_VALUE_LIST: - return KeyValueListInput; - case inputTypes.PLAYLIST: return PlaylistInputConnector; + case inputTypes.KEY_VALUE_LIST: + return KeyValueListInput; + case inputTypes.MONITOR_ALBUMS_SELECT: return MonitorAlbumsSelectInput; @@ -85,9 +83,6 @@ function getComponent(type) { case inputTypes.INDEXER_SELECT: return IndexerSelectInputConnector; - case inputTypes.INDEXER_FLAGS_SELECT: - return IndexerFlagsSelectInput; - case inputTypes.DOWNLOAD_CLIENT_SELECT: return DownloadClientSelectInputConnector; @@ -100,9 +95,6 @@ function getComponent(type) { case inputTypes.DYNAMIC_SELECT: return EnhancedSelectInputConnector; - case inputTypes.ARTIST_TAG: - return ArtistTagInput; - case inputTypes.SERIES_TYPE_SELECT: return SeriesTypeSelectInput; @@ -300,7 +292,6 @@ FormInputGroup.propTypes = { includeNoChangeDisabled: PropTypes.bool, includeNone: PropTypes.bool, selectedValueOptions: PropTypes.object, - indexerFlags: PropTypes.number, pending: PropTypes.bool, errors: PropTypes.arrayOf(PropTypes.object), warnings: PropTypes.arrayOf(PropTypes.object), diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx deleted file mode 100644 index 8dbd27a70..000000000 --- a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -const selectIndexerFlagsValues = (selectedFlags: number) => - createSelector( - (state: AppState) => state.settings.indexerFlags, - (indexerFlags) => { - const value = indexerFlags.items.reduce((acc: number[], { id }) => { - // eslint-disable-next-line no-bitwise - if ((selectedFlags & id) === id) { - acc.push(id); - } - - return acc; - }, []); - - const values = indexerFlags.items.map(({ id, name }) => ({ - key: id, - value: name, - })); - - return { - value, - values, - }; - } - ); - -interface IndexerFlagsSelectInputProps { - name: string; - indexerFlags: number; - onChange(payload: object): void; -} - -function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) { - const { indexerFlags, onChange } = props; - - const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags)); - - const onChangeWrapper = useCallback( - ({ name, value }: { name: string; value: number[] }) => { - const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0); - - onChange({ name, value: indexerFlags }); - }, - [onChange] - ); - - return ( - - ); -} - -export default IndexerFlagsSelectInput; diff --git a/frontend/src/Components/Form/IndexerSelectInputConnector.js b/frontend/src/Components/Form/IndexerSelectInputConnector.js index 5f62becbb..cd58270eb 100644 --- a/frontend/src/Components/Form/IndexerSelectInputConnector.js +++ b/frontend/src/Components/Form/IndexerSelectInputConnector.js @@ -4,8 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchIndexers } 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() { @@ -20,7 +19,7 @@ function createMapStateToProps() { items } = indexers; - const values = _.map(items.sort(sortByProp('name')), (indexer) => { + const values = _.map(items.sort(sortByName), (indexer) => { return { key: indexer.id, value: indexer.name @@ -30,7 +29,7 @@ function createMapStateToProps() { if (includeAny) { values.unshift({ key: 0, - value: `(${translate('Any')})` + value: '(Any)' }); } diff --git a/frontend/src/Components/Form/KeyValueListInput.js b/frontend/src/Components/Form/KeyValueListInput.js new file mode 100644 index 000000000..3e73d74f3 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInput.js @@ -0,0 +1,156 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import KeyValueListInputItem from './KeyValueListInputItem'; +import styles from './KeyValueListInput.css'; + +class KeyValueListInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isFocused: false + }; + } + + // + // Listeners + + onItemChange = (index, itemValue) => { + const { + name, + value, + onChange + } = this.props; + + const newValue = [...value]; + + if (index == null) { + newValue.push(itemValue); + } else { + newValue.splice(index, 1, itemValue); + } + + onChange({ + name, + value: newValue + }); + }; + + onRemoveItem = (index) => { + const { + name, + value, + onChange + } = this.props; + + const newValue = [...value]; + newValue.splice(index, 1); + + onChange({ + name, + value: newValue + }); + }; + + onFocus = () => { + this.setState({ + isFocused: true + }); + }; + + onBlur = () => { + this.setState({ + isFocused: false + }); + + const { + name, + value, + onChange + } = this.props; + + const newValue = value.reduce((acc, v) => { + if (v.key || v.value) { + acc.push(v); + } + + return acc; + }, []); + + if (newValue.length !== value.length) { + onChange({ + name, + value: newValue + }); + } + }; + + // + // Render + + render() { + const { + className, + value, + keyPlaceholder, + valuePlaceholder, + hasError, + hasWarning + } = this.props; + + const { isFocused } = this.state; + + return ( +
+ { + [...value, { key: '', value: '' }].map((v, index) => { + return ( + + ); + }) + } +
+ ); + } +} + +KeyValueListInput.propTypes = { + className: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.arrayOf(PropTypes.object).isRequired, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + keyPlaceholder: PropTypes.string, + valuePlaceholder: PropTypes.string, + onChange: PropTypes.func.isRequired +}; + +KeyValueListInput.defaultProps = { + className: styles.inputContainer, + value: [] +}; + +export default KeyValueListInput; diff --git a/frontend/src/Components/Form/KeyValueListInput.tsx b/frontend/src/Components/Form/KeyValueListInput.tsx deleted file mode 100644 index f5c6ac19b..000000000 --- a/frontend/src/Components/Form/KeyValueListInput.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import classNames from 'classnames'; -import React, { useCallback, useState } from 'react'; -import { InputOnChange } from 'typings/inputs'; -import KeyValueListInputItem from './KeyValueListInputItem'; -import styles from './KeyValueListInput.css'; - -interface KeyValue { - key: string; - value: string; -} - -export interface KeyValueListInputProps { - className?: string; - name: string; - value: KeyValue[]; - hasError?: boolean; - hasWarning?: boolean; - keyPlaceholder?: string; - valuePlaceholder?: string; - onChange: InputOnChange; -} - -function KeyValueListInput({ - className = styles.inputContainer, - name, - value = [], - hasError = false, - hasWarning = false, - keyPlaceholder, - valuePlaceholder, - onChange, -}: KeyValueListInputProps): JSX.Element { - const [isFocused, setIsFocused] = useState(false); - - const handleItemChange = useCallback( - (index: number | null, itemValue: KeyValue) => { - const newValue = [...value]; - - if (index === null) { - newValue.push(itemValue); - } else { - newValue.splice(index, 1, itemValue); - } - - onChange({ name, value: newValue }); - }, - [value, name, onChange] - ); - - const handleRemoveItem = useCallback( - (index: number) => { - const newValue = [...value]; - newValue.splice(index, 1); - onChange({ name, value: newValue }); - }, - [value, name, onChange] - ); - - const onFocus = useCallback(() => setIsFocused(true), []); - - const onBlur = useCallback(() => { - setIsFocused(false); - - const newValue = value.reduce((acc: KeyValue[], v) => { - if (v.key || v.value) { - acc.push(v); - } - return acc; - }, []); - - if (newValue.length !== value.length) { - onChange({ name, value: newValue }); - } - }, [value, name, onChange]); - - return ( -
- {[...value, { key: '', value: '' }].map((v, index) => ( - - ))} -
- ); -} - -export default KeyValueListInput; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css b/frontend/src/Components/Form/KeyValueListInputItem.css index ed82db459..dca2882a8 100644 --- a/frontend/src/Components/Form/KeyValueListInputItem.css +++ b/frontend/src/Components/Form/KeyValueListInputItem.css @@ -5,19 +5,13 @@ &:last-child { margin-bottom: 0; - border-bottom: 0; } } -.keyInputWrapper { +.inputWrapper { flex: 1 0 0; } -.valueInputWrapper { - flex: 1 0 0; - min-width: 40px; -} - .buttonWrapper { flex: 0 0 22px; } @@ -26,10 +20,6 @@ .valueInput { width: 100%; border: none; - background-color: transparent; + background-color: var(--inputBackgroundColor); color: var(--textColor); - - &::placeholder { - color: var(--helpTextColor); - } } diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts index aa0c1be13..35baf55cd 100644 --- a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts +++ b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts @@ -2,11 +2,10 @@ // Please do not change this file! interface CssExports { 'buttonWrapper': string; + 'inputWrapper': string; 'itemContainer': string; 'keyInput': string; - 'keyInputWrapper': string; 'valueInput': string; - 'valueInputWrapper': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.js b/frontend/src/Components/Form/KeyValueListInputItem.js new file mode 100644 index 000000000..5379c2129 --- /dev/null +++ b/frontend/src/Components/Form/KeyValueListInputItem.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import IconButton from 'Components/Link/IconButton'; +import { icons } from 'Helpers/Props'; +import TextInput from './TextInput'; +import styles from './KeyValueListInputItem.css'; + +class KeyValueListInputItem extends Component { + + // + // Listeners + + onKeyChange = ({ value: keyValue }) => { + const { + index, + value, + onChange + } = this.props; + + onChange(index, { key: keyValue, value }); + }; + + onValueChange = ({ value }) => { + // TODO: Validate here or validate at a lower level component + + const { + index, + keyValue, + onChange + } = this.props; + + onChange(index, { key: keyValue, value }); + }; + + onRemovePress = () => { + const { + index, + onRemove + } = this.props; + + onRemove(index); + }; + + onFocus = () => { + this.props.onFocus(); + }; + + onBlur = () => { + this.props.onBlur(); + }; + + // + // Render + + render() { + const { + keyValue, + value, + keyPlaceholder, + valuePlaceholder, + isNew + } = this.props; + + return ( +
+
+ +
+ +
+ +
+ +
+ { + isNew ? + null : + + } +
+
+ ); + } +} + +KeyValueListInputItem.propTypes = { + index: PropTypes.number, + keyValue: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + keyPlaceholder: PropTypes.string.isRequired, + valuePlaceholder: PropTypes.string.isRequired, + isNew: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + onFocus: PropTypes.func.isRequired, + onBlur: PropTypes.func.isRequired +}; + +KeyValueListInputItem.defaultProps = { + keyPlaceholder: 'Key', + valuePlaceholder: 'Value' +}; + +export default KeyValueListInputItem; diff --git a/frontend/src/Components/Form/KeyValueListInputItem.tsx b/frontend/src/Components/Form/KeyValueListInputItem.tsx deleted file mode 100644 index c63ad50a9..000000000 --- a/frontend/src/Components/Form/KeyValueListInputItem.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useCallback } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import { icons } from 'Helpers/Props'; -import TextInput from './TextInput'; -import styles from './KeyValueListInputItem.css'; - -interface KeyValueListInputItemProps { - index: number; - keyValue: string; - value: string; - keyPlaceholder?: string; - valuePlaceholder?: string; - isNew: boolean; - onChange: (index: number, itemValue: { key: string; value: string }) => void; - onRemove: (index: number) => void; - onFocus: () => void; - onBlur: () => void; -} - -function KeyValueListInputItem({ - index, - keyValue, - value, - keyPlaceholder = 'Key', - valuePlaceholder = 'Value', - isNew, - onChange, - onRemove, - onFocus, - onBlur, -}: KeyValueListInputItemProps): JSX.Element { - const handleKeyChange = useCallback( - ({ value: keyValue }: { value: string }) => { - onChange(index, { key: keyValue, value }); - }, - [index, value, onChange] - ); - - const handleValueChange = useCallback( - ({ value }: { value: string }) => { - onChange(index, { key: keyValue, value }); - }, - [index, keyValue, onChange] - ); - - const handleRemovePress = useCallback(() => { - onRemove(index); - }, [index, onRemove]); - - return ( -
-
- -
- -
- -
- -
- {isNew ? null : ( - - )} -
-
- ); -} - -export default KeyValueListInputItem; diff --git a/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js b/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js index 6e6aad5f9..3d763e713 100644 --- a/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js @@ -5,13 +5,13 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { metadataProfileNames } from 'Helpers/Props'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.metadataProfiles', sortByProp('name')), + createSortedSectionSelector('settings.metadataProfiles', sortByName), (state, { includeNoChange }) => includeNoChange, (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, (state, { includeMixed }) => includeMixed, @@ -38,7 +38,7 @@ function createMapStateToProps() { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } @@ -46,7 +46,7 @@ function createMapStateToProps() { values.unshift({ key: 'mixed', value: '(Mixed)', - isDisabled: true + disabled: true }); } diff --git a/frontend/src/Components/Form/MonitorAlbumsSelectInput.js b/frontend/src/Components/Form/MonitorAlbumsSelectInput.js index d48284c38..a10bbb776 100644 --- a/frontend/src/Components/Form/MonitorAlbumsSelectInput.js +++ b/frontend/src/Components/Form/MonitorAlbumsSelectInput.js @@ -18,15 +18,15 @@ function MonitorAlbumsSelectInput(props) { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } if (includeMixed) { values.unshift({ key: 'mixed', - value: `(${translate('Mixed')})`, - isDisabled: true + value: '(Mixed)', + disabled: true }); } diff --git a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js index 0dccc44a4..f9cc07d7d 100644 --- a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js +++ b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js @@ -18,7 +18,7 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } @@ -26,7 +26,7 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'mixed', value: '(Mixed)', - isDisabled: true + disabled: true }); } diff --git a/frontend/src/Components/Form/PasswordInput.css b/frontend/src/Components/Form/PasswordInput.css new file mode 100644 index 000000000..6cb162784 --- /dev/null +++ b/frontend/src/Components/Form/PasswordInput.css @@ -0,0 +1,5 @@ +.input { + composes: input from '~Components/Form/TextInput.css'; + + font-family: $passwordFamily; +} diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts b/frontend/src/Components/Form/PasswordInput.css.d.ts similarity index 87% rename from frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts rename to frontend/src/Components/Form/PasswordInput.css.d.ts index 3fc49a060..774807ef4 100644 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts +++ b/frontend/src/Components/Form/PasswordInput.css.d.ts @@ -1,7 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'modalBody': string; + 'input': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Components/Form/PasswordInput.js b/frontend/src/Components/Form/PasswordInput.js index dbc4cfdb4..fef54fd5a 100644 --- a/frontend/src/Components/Form/PasswordInput.js +++ b/frontend/src/Components/Form/PasswordInput.js @@ -1,5 +1,7 @@ +import PropTypes from 'prop-types'; import React from 'react'; import TextInput from './TextInput'; +import styles from './PasswordInput.css'; // Prevent a user from copying (or cutting) the password from the input function onCopy(e) { @@ -11,14 +13,17 @@ function PasswordInput(props) { return ( ); } PasswordInput.propTypes = { - ...TextInput.props + className: PropTypes.string.isRequired +}; + +PasswordInput.defaultProps = { + className: styles.input }; export default PasswordInput; diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index 311f1bbbd..9c2d124d7 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -14,8 +14,6 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.CHECK; case 'device': return inputTypes.DEVICE; - case 'keyValueList': - return inputTypes.KEY_VALUE_LIST; case 'playlist': return inputTypes.PLAYLIST; case 'password': @@ -31,8 +29,6 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.DYNAMIC_SELECT; } return inputTypes.SELECT; - case 'artistTag': - return inputTypes.ARTIST_TAG; case 'tag': return inputTypes.TEXT_TAG; case 'tagSelect': diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index d7719969a..a898de4a2 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -4,13 +4,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), + createSortedSectionSelector('settings.qualityProfiles', sortByName), (state, { includeNoChange }) => includeNoChange, (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, (state, { includeMixed }) => includeMixed, @@ -26,7 +26,7 @@ function createMapStateToProps() { values.unshift({ key: 'noChange', value: translate('NoChange'), - isDisabled: includeNoChangeDisabled + disabled: includeNoChangeDisabled }); } @@ -34,7 +34,7 @@ function createMapStateToProps() { values.unshift({ key: 'mixed', value: '(Mixed)', - isDisabled: true + disabled: true }); } diff --git a/frontend/src/Components/Form/SelectInput.js b/frontend/src/Components/Form/SelectInput.js index d43560134..553501afc 100644 --- a/frontend/src/Components/Form/SelectInput.js +++ b/frontend/src/Components/Form/SelectInput.js @@ -52,7 +52,6 @@ class SelectInput extends Component { const { key, value: optionValue, - isDisabled: optionIsDisabled = false, ...otherOptionProps } = option; @@ -60,7 +59,6 @@ class SelectInput extends Component {
diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js new file mode 100644 index 000000000..c14617b29 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; +import { align, icons, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './PageHeaderActionsMenu.css'; + +function PageHeaderActionsMenu(props) { + const { + formsAuth, + onKeyboardShortcutsPress, + onRestartPress, + onShutdownPress + } = props; + + return ( +
+ + + + + + + + + {translate('KeyboardShortcuts')} + + + + + + + {translate('Restart')} + + + + + Shutdown + + + { + formsAuth && +
+ } + + { + formsAuth && + + + {translate('Logout')} + + } + +
+
+ ); +} + +PageHeaderActionsMenu.propTypes = { + formsAuth: PropTypes.bool.isRequired, + onKeyboardShortcutsPress: PropTypes.func.isRequired, + onRestartPress: PropTypes.func.isRequired, + onShutdownPress: PropTypes.func.isRequired +}; + +export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx deleted file mode 100644 index 7a0c35c1c..000000000 --- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import Icon from 'Components/Icon'; -import Menu from 'Components/Menu/Menu'; -import MenuButton from 'Components/Menu/MenuButton'; -import MenuContent from 'Components/Menu/MenuContent'; -import MenuItem from 'Components/Menu/MenuItem'; -import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; -import { align, icons, kinds } from 'Helpers/Props'; -import { restart, shutdown } from 'Store/Actions/systemActions'; -import translate from 'Utilities/String/translate'; -import styles from './PageHeaderActionsMenu.css'; - -interface PageHeaderActionsMenuProps { - onKeyboardShortcutsPress(): void; -} - -function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) { - const { onKeyboardShortcutsPress } = props; - - const dispatch = useDispatch(); - - const { authentication, isDocker } = useSelector( - (state: AppState) => state.system.status.item - ); - - const formsAuth = authentication === 'forms'; - - const handleRestartPress = useCallback(() => { - dispatch(restart()); - }, [dispatch]); - - const handleShutdownPress = useCallback(() => { - dispatch(shutdown()); - }, [dispatch]); - - return ( -
- - - - - - - - - {translate('KeyboardShortcuts')} - - - {isDocker ? null : ( - <> - - - - - {translate('Restart')} - - - - - {translate('Shutdown')} - - - )} - - {formsAuth ? ( - <> - - - - - {translate('Logout')} - - - ) : null} - - -
- ); -} - -export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js new file mode 100644 index 000000000..3aba95065 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { restart, shutdown } from 'Store/Actions/systemActions'; +import PageHeaderActionsMenu from './PageHeaderActionsMenu'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.status, + (status) => { + return { + formsAuth: status.item.authentication === 'forms' + }; + } + ); +} + +const mapDispatchToProps = { + restart, + shutdown +}; + +class PageHeaderActionsMenuConnector extends Component { + + // + // Listeners + + onRestartPress = () => { + this.props.restart(); + }; + + onShutdownPress = () => { + this.props.shutdown(); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +PageHeaderActionsMenuConnector.propTypes = { + restart: PropTypes.func.isRequired, + shutdown: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector); diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index c84099a5e..070be9b42 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -6,14 +6,7 @@ import { createSelector } from 'reselect'; import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchArtist } from 'Store/Actions/artistActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; -import { - fetchImportLists, - fetchIndexerFlags, - fetchLanguages, - fetchMetadataProfiles, - fetchQualityProfiles, - fetchUISettings -} from 'Store/Actions/settingsActions'; +import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchTags } from 'Store/Actions/tagActions'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -51,7 +44,6 @@ const selectAppProps = createSelector( ); const selectIsPopulated = createSelector( - (state) => state.artist.isPopulated, (state) => state.customFilters.isPopulated, (state) => state.tags.isPopulated, (state) => state.settings.ui.isPopulated, @@ -59,11 +51,9 @@ const selectIsPopulated = createSelector( (state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.importLists.isPopulated, - (state) => state.settings.indexerFlags.isPopulated, (state) => state.system.status.isPopulated, (state) => state.app.translations.isPopulated, ( - artistsIsPopulated, customFiltersIsPopulated, tagsIsPopulated, uiSettingsIsPopulated, @@ -71,12 +61,10 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated, metadataProfilesIsPopulated, importListsIsPopulated, - indexerFlagsIsPopulated, systemStatusIsPopulated, translationsIsPopulated ) => { return ( - artistsIsPopulated && customFiltersIsPopulated && tagsIsPopulated && uiSettingsIsPopulated && @@ -84,7 +72,6 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated && metadataProfilesIsPopulated && importListsIsPopulated && - indexerFlagsIsPopulated && systemStatusIsPopulated && translationsIsPopulated ); @@ -92,7 +79,6 @@ const selectIsPopulated = createSelector( ); const selectErrors = createSelector( - (state) => state.artist.error, (state) => state.customFilters.error, (state) => state.tags.error, (state) => state.settings.ui.error, @@ -100,11 +86,9 @@ const selectErrors = createSelector( (state) => state.settings.qualityProfiles.error, (state) => state.settings.metadataProfiles.error, (state) => state.settings.importLists.error, - (state) => state.settings.indexerFlags.error, (state) => state.system.status.error, (state) => state.app.translations.error, ( - artistsError, customFiltersError, tagsError, uiSettingsError, @@ -112,12 +96,10 @@ const selectErrors = createSelector( qualityProfilesError, metadataProfilesError, importListsError, - indexerFlagsError, systemStatusError, translationsError ) => { const hasError = !!( - artistsError || customFiltersError || tagsError || uiSettingsError || @@ -125,7 +107,6 @@ const selectErrors = createSelector( qualityProfilesError || metadataProfilesError || importListsError || - indexerFlagsError || systemStatusError || translationsError ); @@ -139,7 +120,6 @@ const selectErrors = createSelector( qualityProfilesError, metadataProfilesError, importListsError, - indexerFlagsError, systemStatusError, translationsError }; @@ -197,9 +177,6 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchImportLists() { dispatch(fetchImportLists()); }, - dispatchFetchIndexerFlags() { - dispatch(fetchIndexerFlags()); - }, dispatchFetchUISettings() { dispatch(fetchUISettings()); }, @@ -240,7 +217,6 @@ class PageConnector extends Component { this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchMetadataProfiles(); this.props.dispatchFetchImportLists(); - this.props.dispatchFetchIndexerFlags(); this.props.dispatchFetchUISettings(); this.props.dispatchFetchStatus(); this.props.dispatchFetchTranslations(); @@ -267,7 +243,6 @@ class PageConnector extends Component { dispatchFetchQualityProfiles, dispatchFetchMetadataProfiles, dispatchFetchImportLists, - dispatchFetchIndexerFlags, dispatchFetchUISettings, dispatchFetchStatus, dispatchFetchTranslations, @@ -309,7 +284,6 @@ PageConnector.propTypes = { dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchMetadataProfiles: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, - dispatchFetchIndexerFlags: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchTranslations: PropTypes.func.isRequired, diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 365827a2b..c970589c7 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -172,7 +172,7 @@ class SignalRConnector extends Component { const status = resource.status; // Both successful and failed commands need to be - // completed, otherwise they spin until they time out. + // completed, otherwise they spin until they timeout. if (status === 'completed' || status === 'failed') { this.props.dispatchFinishCommand(resource); @@ -224,58 +224,10 @@ class SignalRConnector extends Component { repopulatePage('trackFileUpdated'); }; - 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(); }; - handleImportlist = ({ action, resource }) => { - const section = 'settings.importLists'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleIndexer = ({ action, resource }) => { - const section = 'settings.indexers'; - - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: resource.id }); - } - }; - - handleMetadata = ({ action, resource }) => { - const section = 'settings.metadata'; - - if (action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); - } - }; - - 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 }); - } - }; - handleArtist = (body) => { const action = body.action; const section = 'artist'; @@ -314,7 +266,7 @@ class SignalRConnector extends Component { handleWantedCutoff = (body) => { if (body.action === 'updated') { this.props.dispatchUpdateItem({ - section: 'wanted.cutoffUnmet', + section: 'cutoffUnmet', updateOnly: true, ...body.resource }); @@ -324,7 +276,7 @@ class SignalRConnector extends Component { handleWantedMissing = (body) => { if (body.action === 'updated') { this.props.dispatchUpdateItem({ - section: 'wanted.missing', + section: 'missing', updateOnly: true, ...body.resource }); diff --git a/frontend/src/Components/Table/Cells/TableRowCell.css b/frontend/src/Components/Table/Cells/TableRowCell.css index 7e3353c25..47ce0d22e 100644 --- a/frontend/src/Components/Table/Cells/TableRowCell.css +++ b/frontend/src/Components/Table/Cells/TableRowCell.css @@ -4,7 +4,7 @@ line-height: 1.52857143; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .cell { white-space: nowrap; } diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.css b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css index f7f3b9306..2501b7c84 100644 --- a/frontend/src/Components/Table/Cells/VirtualTableRowCell.css +++ b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css @@ -7,7 +7,7 @@ white-space: nowrap; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .cell { white-space: nowrap; } diff --git a/frontend/src/Components/Table/Table.css b/frontend/src/Components/Table/Table.css index d0507be6b..bdfdec641 100644 --- a/frontend/src/Components/Table/Table.css +++ b/frontend/src/Components/Table/Table.css @@ -10,7 +10,7 @@ border-collapse: collapse; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .tableContainer { min-width: 100%; width: fit-content; diff --git a/frontend/src/Components/Table/TableHeaderCell.css b/frontend/src/Components/Table/TableHeaderCell.css index eded9c95b..c2c4f58c8 100644 --- a/frontend/src/Components/Table/TableHeaderCell.css +++ b/frontend/src/Components/Table/TableHeaderCell.css @@ -9,7 +9,7 @@ margin-left: 10px; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .headerCell { white-space: nowrap; } diff --git a/frontend/src/Components/Table/TablePager.css b/frontend/src/Components/Table/TablePager.css index 6d184196e..d73a0d0c0 100644 --- a/frontend/src/Components/Table/TablePager.css +++ b/frontend/src/Components/Table/TablePager.css @@ -60,7 +60,7 @@ height: 25px; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .pager { flex-wrap: wrap; } diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.css b/frontend/src/Components/Table/VirtualTableHeaderCell.css index eded9c95b..c2c4f58c8 100644 --- a/frontend/src/Components/Table/VirtualTableHeaderCell.css +++ b/frontend/src/Components/Table/VirtualTableHeaderCell.css @@ -9,7 +9,7 @@ margin-left: 10px; } -@media only screen and (max-width: $breakpointMedium) { +@media only screen and (max-width: $breakpointSmall) { .headerCell { white-space: nowrap; } diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js index fe700b8fe..6da96849c 100644 --- a/frontend/src/Components/TagList.js +++ b/frontend/src/Components/TagList.js @@ -1,7 +1,6 @@ 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'; @@ -9,7 +8,7 @@ function TagList({ tags, tagList }) { const sortedTags = tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) .filter((tag) => !!tag) - .sort(sortByProp('label')); + .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/Content/Images/Icons/browserconfig.xml b/frontend/src/Content/Images/Icons/browserconfig.xml new file mode 100644 index 000000000..993924968 --- /dev/null +++ b/frontend/src/Content/Images/Icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #00ccff + + + diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json new file mode 100644 index 000000000..a7ffa6777 --- /dev/null +++ b/frontend/src/Content/Images/Icons/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Lidarr", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "../../../../", + "theme_color": "#3a3f51", + "background_color": "#3a3f51", + "display": "minimal-ui" +} diff --git a/frontend/src/Content/browserconfig.xml b/frontend/src/Content/browserconfig.xml deleted file mode 100644 index 646112d06..000000000 --- a/frontend/src/Content/browserconfig.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - #00ccff - - - - diff --git a/frontend/src/Content/manifest.json b/frontend/src/Content/manifest.json deleted file mode 100644 index 5c2b3d59d..000000000 --- a/frontend/src/Content/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "__INSTANCE_NAME__", - "icons": [ - { - "src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "__URL_BASE__/Content/Images/Icons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "start_url": "__URL_BASE__/", - "theme_color": "#3a3f51", - "background_color": "#3a3f51", - "display": "standalone" -} diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts deleted file mode 100644 index f5b5a96f0..000000000 --- a/frontend/src/Helpers/Hooks/useModalOpenState.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback, useState } from 'react'; - -export default function useModalOpenState( - initialState: boolean -): [boolean, () => void, () => void] { - const [isOpen, setOpen] = useState(initialState); - - const setModalOpen = useCallback(() => { - setOpen(true); - }, [setOpen]); - - const setModalClosed = useCallback(() => { - setOpen(false); - }, [setOpen]); - - return [isOpen, setModalOpen, setModalClosed]; -} diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index aa9c23145..77803e56e 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -32,7 +32,6 @@ import { faBookReader as fasBookReader, faBroadcastTower as fasBroadcastTower, faBug as fasBug, - faCalculator as fasCalculator, faCalendarAlt as fasCalendarAlt, faCaretDown as fasCaretDown, faCheck as fasCheck, @@ -61,7 +60,6 @@ import { faFileImport as fasFileImport, faFileInvoice as farFileInvoice, faFilter as fasFilter, - faFlag as fasFlag, faFolderOpen as fasFolderOpen, faForward as fasForward, faHeart as fasHeart, @@ -160,7 +158,6 @@ export const FILE = farFile; export const FILE_IMPORT = fasFileImport; export const FILE_MISSING = fasFileCircleQuestion; export const FILTER = fasFilter; -export const FLAG = fasFlag; export const FOLDER = farFolder; export const FOLDER_OPEN = fasFolderOpen; export const GROUP = farObjectGroup; @@ -188,7 +185,6 @@ export const PAGE_PREVIOUS = fasBackward; export const PAGE_NEXT = fasForward; export const PAGE_LAST = fasFastForward; export const PARENT = fasLevelUpAlt; -export const PARSE = fasCalculator; export const PAUSED = fasPause; export const PENDING = farClock; export const PROFILE = fasUser; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 44115c787..8ebbd540b 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -2,8 +2,8 @@ export const AUTO_COMPLETE = 'autoComplete'; export const CAPTCHA = 'captcha'; export const CHECK = 'check'; export const DEVICE = 'device'; -export const KEY_VALUE_LIST = 'keyValueList'; export const PLAYLIST = 'playlist'; +export const KEY_VALUE_LIST = 'keyValueList'; export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const FLOAT = 'float'; @@ -15,12 +15,10 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const METADATA_PROFILE_SELECT = 'metadataProfileSelect'; export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect'; export const INDEXER_SELECT = 'indexerSelect'; -export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const SELECT = 'select'; export const SERIES_TYPE_SELECT = 'artistTypeSelect'; -export const ARTIST_TAG = 'artistTag'; export const DYNAMIC_SELECT = 'dynamicSelect'; export const TAG = 'tag'; export const TAG_SELECT = 'tagSelect'; @@ -34,8 +32,8 @@ export const all = [ CAPTCHA, CHECK, DEVICE, - KEY_VALUE_LIST, PLAYLIST, + KEY_VALUE_LIST, MONITOR_ALBUMS_SELECT, MONITOR_NEW_ITEMS_SELECT, FLOAT, @@ -50,7 +48,6 @@ export const all = [ DOWNLOAD_CLIENT_SELECT, ROOT_FOLDER_SELECT, SELECT, - ARTIST_TAG, DYNAMIC_SELECT, SERIES_TYPE_SELECT, TAG, diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.js index 6ac15f3bd..d7f85df5e 100644 --- a/frontend/src/Helpers/Props/sizes.js +++ b/frontend/src/Helpers/Props/sizes.js @@ -3,5 +3,5 @@ export const SMALL = 'small'; export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; -export const EXTRA_EXTRA_LARGE = 'extraExtraLarge'; -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE]; + +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js index f8c84e54b..5f2cc9696 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js @@ -11,7 +11,6 @@ import Scroller from 'Components/Scroller/Scroller'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { scrollDirections } from 'Helpers/Props'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; import SelectAlbumRow from './SelectAlbumRow'; import styles from './SelectAlbumModalContent.css'; @@ -20,7 +19,6 @@ const columns = [ { name: 'title', label: () => translate('AlbumTitle'), - isSortable: true, isVisible: true }, { @@ -31,7 +29,6 @@ const columns = [ { name: 'releaseDate', label: () => translate('ReleaseDate'), - isSortable: true, isVisible: true }, { @@ -66,22 +63,16 @@ class SelectAlbumModalContent extends Component { render() { const { - isFetching, - isPopulated, - error, items, - sortKey, - sortDirection, - onSortPress, onAlbumSelect, - onModalClose + onModalClose, + isFetching, + ...otherProps } = this.props; const filter = this.state.filter; const filterLower = filter.toLowerCase(); - const errorMessage = getErrorMessage(error, 'Unable to load albums'); - return ( @@ -92,29 +83,27 @@ class SelectAlbumModalContent extends Component { className={styles.modalBody} scrollDirection={scrollDirections.NONE} > + { + isFetching && + + } + + - {isFetching ? : null} - - {error ?
{errorMessage}
: null} - - - - {isPopulated && !!items.length ? ( + { { @@ -133,7 +122,7 @@ class SelectAlbumModalContent extends Component { }
- ) : null} + }
@@ -148,13 +137,8 @@ class SelectAlbumModalContent extends Component { } SelectAlbumModalContent.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, - sortKey: PropTypes.string, - sortDirection: PropTypes.string, - onSortPress: PropTypes.func.isRequired, + isFetching: PropTypes.bool.isRequired, onAlbumSelect: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js index d09da0fca..12cd88e53 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js @@ -3,14 +3,18 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions'; -import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import { + clearInteractiveImportAlbums, + fetchInteractiveImportAlbums, + saveInteractiveImportItem, + setInteractiveImportAlbumsSort, + updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import SelectAlbumModalContent from './SelectAlbumModalContent'; function createMapStateToProps() { return createSelector( - createClientSideCollectionSelector('albumSelection'), + createClientSideCollectionSelector('interactiveImport.albums'), (albums) => { return albums; } @@ -18,9 +22,9 @@ function createMapStateToProps() { } const mapDispatchToProps = { - fetchAlbums, - setAlbumsSort, - clearAlbums, + fetchInteractiveImportAlbums, + setInteractiveImportAlbumsSort, + clearInteractiveImportAlbums, updateInteractiveImportItem, saveInteractiveImportItem }; @@ -35,20 +39,20 @@ class SelectAlbumModalContentConnector extends Component { artistId } = this.props; - this.props.fetchAlbums({ artistId }); + this.props.fetchInteractiveImportAlbums({ artistId }); } componentWillUnmount() { // This clears the albums for the queue and hides the queue // We'll need another place to store albums for manual import - this.props.clearAlbums(); + this.props.clearInteractiveImportAlbums(); } // // Listeners onSortPress = (sortKey, sortDirection) => { - this.props.setAlbumsSort({ sortKey, sortDirection }); + this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection }); }; onAlbumSelect = (albumId) => { @@ -78,7 +82,6 @@ class SelectAlbumModalContentConnector extends Component { return ( ); @@ -89,9 +92,9 @@ SelectAlbumModalContentConnector.propTypes = { ids: PropTypes.arrayOf(PropTypes.number).isRequired, artistId: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchAlbums: PropTypes.func.isRequired, - setAlbumsSort: PropTypes.func.isRequired, - clearAlbums: PropTypes.func.isRequired, + fetchInteractiveImportAlbums: PropTypes.func.isRequired, + setInteractiveImportAlbumsSort: PropTypes.func.isRequired, + clearInteractiveImportAlbums: PropTypes.func.isRequired, saveInteractiveImportItem: PropTypes.func.isRequired, updateInteractiveImportItem: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js index 26fa7282a..d9301cdfa 100644 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js +++ b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js @@ -1,9 +1,10 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import SelectInput from 'Components/Form/SelectInput'; +import FormInputGroup from 'Components/Form/FormInputGroup'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; +import { inputTypes } from 'Helpers/Props'; import shortenList from 'Utilities/String/shortenList'; import titleCase from 'Utilities/String/titleCase'; @@ -55,7 +56,8 @@ class SelectAlbumReleaseRow extends Component { if (name === 'release') { return ( - ({ key: r.id, diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.js deleted file mode 100644 index 04359b96a..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Modal from 'Components/Modal/Modal'; -import SelectIndexerFlagsModalContentConnector from './SelectIndexerFlagsModalContentConnector'; - -class SelectIndexerFlagsModal extends Component { - - // - // Render - - render() { - const { - isOpen, - onModalClose, - ...otherProps - } = this.props; - - return ( - - - - ); - } -} - -SelectIndexerFlagsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectIndexerFlagsModal; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css deleted file mode 100644 index 72dfb1cb6..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css +++ /dev/null @@ -1,7 +0,0 @@ -.modalBody { - composes: modalBody from '~Components/Modal/ModalBody.css'; - - display: flex; - flex: 1 1 auto; - flex-direction: column; -} diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js deleted file mode 100644 index b30f76775..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -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 { inputTypes, kinds, scrollDirections } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './SelectIndexerFlagsModalContent.css'; - -class SelectIndexerFlagsModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const { - indexerFlags - } = props; - - this.state = { - indexerFlags - }; - } - - // - // Listeners - - onIndexerFlagsChange = ({ value }) => { - this.setState({ indexerFlags: value }); - }; - - onIndexerFlagsSelect = () => { - this.props.onIndexerFlagsSelect(this.state); - }; - - // - // Render - - render() { - const { - onModalClose - } = this.props; - - const { - indexerFlags - } = this.state; - - return ( - - - Manual Import - Set indexer Flags - - - - - - - {translate('IndexerFlags')} - - - - - - - - - - - - - - ); - } -} - -SelectIndexerFlagsModalContent.propTypes = { - indexerFlags: PropTypes.number.isRequired, - onIndexerFlagsSelect: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectIndexerFlagsModalContent; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js deleted file mode 100644 index 7a9af7353..000000000 --- a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js +++ /dev/null @@ -1,54 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; -import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent'; - -const mapDispatchToProps = { - dispatchUpdateInteractiveImportItems: updateInteractiveImportItems, - dispatchSaveInteractiveImportItems: saveInteractiveImportItem -}; - -class SelectIndexerFlagsModalContentConnector extends Component { - - // - // Listeners - - onIndexerFlagsSelect = ({ indexerFlags }) => { - const { - ids, - dispatchUpdateInteractiveImportItems, - dispatchSaveInteractiveImportItems - } = this.props; - - dispatchUpdateInteractiveImportItems({ - ids, - indexerFlags - }); - - dispatchSaveInteractiveImportItems({ ids }); - - this.props.onModalClose(true); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -SelectIndexerFlagsModalContentConnector.propTypes = { - ids: PropTypes.arrayOf(PropTypes.number).isRequired, - dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, - dispatchSaveInteractiveImportItems: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css index 93d815c9c..573b16667 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css @@ -18,17 +18,12 @@ .leftButtons, .rightButtons { display: flex; + flex: 1 0 50%; flex-wrap: wrap; - min-width: 0; -} - -.leftButtons { - flex: 0 1 auto; } .rightButtons { justify-content: flex-end; - flex: 1 1 50%; } .importMode, @@ -36,7 +31,6 @@ composes: select from '~Components/Form/SelectInput.css'; margin-right: 10px; - max-width: 100%; width: auto; } @@ -49,12 +43,10 @@ .leftButtons, .rightButtons { flex-direction: column; - gap: 3px; } .leftButtons { align-items: flex-start; - max-width: fit-content; } .rightButtons { diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index d980e77ce..d1361a785 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -20,7 +20,6 @@ import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; -import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; @@ -31,7 +30,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected'; import InteractiveImportRow from './InteractiveImportRow'; import styles from './InteractiveImportModalContent.css'; -const COLUMNS = [ +const columns = [ { name: 'path', label: () => translate('Path'), @@ -80,21 +79,11 @@ const COLUMNS = [ isSortable: true, isVisible: true }, - { - name: 'indexerFlags', - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), - isSortable: true, - isVisible: true - }, { name: 'rejections', label: React.createElement(Icon, { name: icons.DANGER, - kind: kinds.DANGER, - title: () => translate('Rejections') + kind: kinds.DANGER }), isSortable: true, isVisible: true @@ -118,7 +107,6 @@ const ALBUM = 'album'; const ALBUM_RELEASE = 'albumRelease'; const RELEASE_GROUP = 'releaseGroup'; const QUALITY = 'quality'; -const INDEXER_FLAGS = 'indexerFlags'; const replaceExistingFilesOptions = { COMBINE: 'combine', @@ -313,21 +301,6 @@ class InteractiveImportModalContent extends Component { inconsistentAlbumReleases } = this.state; - const allColumns = _.cloneDeep(COLUMNS); - const columns = allColumns.map((column) => { - const showIndexerFlags = items.some((item) => item.indexerFlags); - - if (!showIndexerFlags) { - const indexerFlagsColumn = allColumns.find((c) => c.name === 'indexerFlags'); - - if (indexerFlagsColumn) { - indexerFlagsColumn.isVisible = false; - } - } - - return column; - }); - const selectedIds = this.getSelectedIds(); const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; const errorMessage = getErrorMessage(error, 'Unable to load manual import items'); @@ -337,8 +310,7 @@ class InteractiveImportModalContent extends Component { { key: ALBUM, value: translate('SelectAlbum') }, { key: ALBUM_RELEASE, value: translate('SelectAlbumRelease') }, { key: QUALITY, value: translate('SelectQuality') }, - { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }, - { key: INDEXER_FLAGS, value: translate('SelectIndexerFlags') } + { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') } ]; if (allowArtistChange) { @@ -461,7 +433,6 @@ class InteractiveImportModalContent extends Component { isSaving={isSaving} {...item} allowArtistChange={allowArtistChange} - columns={columns} onSelectedChange={this.onSelectedChange} onValidRowChange={this.onValidRowChange} /> @@ -576,13 +547,6 @@ class InteractiveImportModalContent extends Component { onModalClose={this.onSelectModalClose} /> - - 0 + quality ) { this.props.onSelectedChange({ id, value: true }); } @@ -135,10 +130,6 @@ class InteractiveImportRow extends Component { this.setState({ isSelectQualityModalOpen: true }); }; - onSelectIndexerFlagsPress = () => { - this.setState({ isSelectIndexerFlagsModalOpen: true }); - }; - onSelectArtistModalClose = (changed) => { this.setState({ isSelectArtistModalOpen: false }); this.selectRowAfterChange(changed); @@ -164,11 +155,6 @@ class InteractiveImportRow extends Component { this.selectRowAfterChange(changed); }; - onSelectIndexerFlagsModalClose = (changed) => { - this.setState({ isSelectIndexerFlagsModalOpen: false }); - this.selectRowAfterChange(changed); - }; - // // Render @@ -185,9 +171,7 @@ class InteractiveImportRow extends Component { releaseGroup, size, customFormats, - indexerFlags, rejections, - columns, isReprocessing, audioTags, additionalFile, @@ -200,8 +184,7 @@ class InteractiveImportRow extends Component { isSelectAlbumModalOpen, isSelectTrackModalOpen, isSelectReleaseGroupModalOpen, - isSelectQualityModalOpen, - isSelectIndexerFlagsModalOpen + isSelectQualityModalOpen } = this.state; const artistName = artist ? artist.artistName : ''; @@ -221,7 +204,6 @@ class InteractiveImportRow extends Component { const showTrackNumbersLoading = isReprocessing && !tracks.length; const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showQualityPlaceholder = isSelected && !quality; - const showIndexerFlagsPlaceholder = isSelected && !indexerFlags; const pathCellContents = (
@@ -237,8 +219,6 @@ class InteractiveImportRow extends Component { /> ) : pathCellContents; - const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false; - return ( - {isIndexerFlagsColumnVisible ? ( - - {showIndexerFlagsPlaceholder ? ( - - ) : ( - <> - {indexerFlags ? ( - } - title={translate('IndexerFlags')} - body={} - position={tooltipPositions.LEFT} - /> - ) : null} - - )} - - ) : null} - { rejections.length ? @@ -437,13 +395,6 @@ class InteractiveImportRow extends Component { real={quality ? quality.revision.real > 0 : false} onModalClose={this.onSelectQualityModalClose} /> - - ); } @@ -462,9 +413,7 @@ InteractiveImportRow.propTypes = { quality: PropTypes.object, size: PropTypes.number.isRequired, customFormats: PropTypes.arrayOf(PropTypes.object), - indexerFlags: PropTypes.number.isRequired, rejections: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, audioTags: PropTypes.object.isRequired, additionalFile: PropTypes.bool.isRequired, isReprocessing: PropTypes.bool, diff --git a/frontend/src/InteractiveImport/InteractiveImportModal.js b/frontend/src/InteractiveImport/InteractiveImportModal.js index 4d0e9f2f1..dda75152f 100644 --- a/frontend/src/InteractiveImport/InteractiveImportModal.js +++ b/frontend/src/InteractiveImport/InteractiveImportModal.js @@ -48,7 +48,7 @@ class InteractiveImportModal extends Component { return ( diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearch.js index 64d1ce730..6e74695b0 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ b/frontend/src/InteractiveSearch/InteractiveSearch.js @@ -65,15 +65,6 @@ const columns = [ isSortable: true, isVisible: true }, - { - name: 'indexerFlags', - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), - isSortable: true, - isVisible: true - }, { name: 'rejections', label: React.createElement(Icon, { diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index dad7242c8..ffea82600 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -35,7 +35,6 @@ } .rejected, -.indexerFlags, .download { composes: cell from '~Components/Table/Cells/TableRowCell.css'; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts index bec6dcf78..ca01c5ee6 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts @@ -5,7 +5,6 @@ interface CssExports { 'customFormatScore': string; 'download': string; 'indexer': string; - 'indexerFlags': string; 'peers': string; 'protocol': string; 'quality': string; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.js b/frontend/src/InteractiveSearch/InteractiveSearchRow.js index a139f8085..3029d2d2f 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import AlbumFormats from 'Album/AlbumFormats'; -import IndexerFlags from 'Album/IndexerFlags'; import TrackQuality from 'Album/TrackQuality'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; @@ -49,12 +48,12 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { if (isGrabbing) { return ''; } else if (isGrabbed) { - return translate('AddedToDownloadQueue'); + return 'Added to downloaded queue'; } else if (grabError) { return grabError; } - return translate('AddToDownloadQueue'); + return 'Add to downloaded queue'; } class InteractiveSearchRow extends Component { @@ -130,7 +129,6 @@ class InteractiveSearchRow extends Component { quality, customFormatScore, customFormats, - indexerFlags = 0, rejections, downloadAllowed, isGrabbing, @@ -189,21 +187,10 @@ class InteractiveSearchRow extends Component { formatCustomFormatScore(customFormatScore, customFormats.length) } tooltip={} - position={tooltipPositions.LEFT} + position={tooltipPositions.BOTTOM} /> - - {indexerFlags ? ( - } - title={translate('IndexerFlags')} - body={} - position={tooltipPositions.LEFT} - /> - ) : null} - - { !!rejections.length && @@ -249,9 +236,7 @@ class InteractiveSearchRow extends Component { isOpen={this.state.isConfirmGrabModalOpen} kind={kinds.WARNING} title={translate('GrabRelease')} - message={translate('GrabReleaseUnknownArtistOrAlbumMessageText', { - title - })} + message={translate('GrabReleaseMessageText', [title])} confirmLabel={translate('Grab')} onConfirm={this.onGrabConfirm} onCancel={this.onGrabCancel} @@ -278,7 +263,6 @@ InteractiveSearchRow.propTypes = { quality: PropTypes.object.isRequired, customFormats: PropTypes.arrayOf(PropTypes.object), customFormatScore: PropTypes.number.isRequired, - indexerFlags: PropTypes.number.isRequired, rejections: PropTypes.arrayOf(PropTypes.string).isRequired, downloadAllowed: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired, @@ -291,7 +275,6 @@ InteractiveSearchRow.propTypes = { }; InteractiveSearchRow.defaultProps = { - indexerFlags: 0, rejections: [], isGrabbing: false, isGrabbed: false diff --git a/frontend/src/Parse/Parse.css b/frontend/src/Parse/Parse.css deleted file mode 100644 index 43536452c..000000000 --- a/frontend/src/Parse/Parse.css +++ /dev/null @@ -1,45 +0,0 @@ -.inputContainer { - display: flex; - margin-bottom: 10px; -} - -.inputIconContainer { - width: 58px; - height: 46px; - border: 1px solid var(--inputBorderColor); - border-right: none; - border-radius: 4px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - background-color: var(--inputIconContainerBackgroundColor); - text-align: center; - line-height: 46px; -} - -.input { - composes: input from '~Components/Form/TextInput.css'; - - height: 46px; - border-radius: 0; - font-size: 18px; -} - -.clearButton { - border: 1px solid var(--inputBorderColor); - border-left: none; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.message { - margin-top: 30px; - text-align: center; - font-weight: 300; - font-size: $largeFontSize; -} - -.helpText { - margin-bottom: 10px; - font-size: 24px; -} diff --git a/frontend/src/Parse/Parse.css.d.ts b/frontend/src/Parse/Parse.css.d.ts deleted file mode 100644 index 4a4def577..000000000 --- a/frontend/src/Parse/Parse.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'clearButton': string; - 'helpText': string; - 'input': string; - 'inputContainer': string; - 'inputIconContainer': string; - 'message': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Parse/Parse.tsx b/frontend/src/Parse/Parse.tsx deleted file mode 100644 index 15a0deb47..000000000 --- a/frontend/src/Parse/Parse.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import TextInput from 'Components/Form/TextInput'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons } from 'Helpers/Props'; -import { clear, fetch } from 'Store/Actions/parseActions'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import ParseResult from './ParseResult'; -import parseStateSelector from './parseStateSelector'; -import styles from './Parse.css'; - -function Parse() { - const { isFetching, error, item } = useSelector(parseStateSelector()); - - const [title, setTitle] = useState(''); - const dispatch = useDispatch(); - - const onInputChange = useCallback( - ({ value }: { value: string }) => { - const trimmedValue = value.trim(); - - setTitle(value); - - if (trimmedValue === '') { - dispatch(clear()); - } else { - dispatch(fetch({ title: trimmedValue })); - } - }, - [setTitle, dispatch] - ); - - const onClearPress = useCallback(() => { - setTitle(''); - dispatch(clear()); - }, [setTitle, dispatch]); - - useEffect( - () => { - return () => { - dispatch(clear()); - }; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return ( - - -
-
- -
- - - - -
- - {isFetching ? : null} - - {!isFetching && !!error ? ( -
-
- {translate('ParseModalErrorParsing')} -
-
{getErrorMessage(error)}
-
- ) : null} - - {!isFetching && title && !error && !item.parsedAlbumInfo ? ( -
- {translate('ParseModalUnableToParse')} -
- ) : null} - - {!isFetching && !error && item.parsedAlbumInfo ? ( - - ) : null} - - {title ? null : ( -
-
- {translate('ParseModalHelpText')} -
-
{translate('ParseModalHelpTextDetails')}
-
- )} -
-
- ); -} - -export default Parse; diff --git a/frontend/src/Parse/ParseModal.tsx b/frontend/src/Parse/ParseModal.tsx deleted file mode 100644 index 0ee455bf0..000000000 --- a/frontend/src/Parse/ParseModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ParseModalContent from './ParseModalContent'; - -interface ParseModalProps { - isOpen: boolean; - onModalClose: () => void; -} - -function ParseModal(props: ParseModalProps) { - const { isOpen, onModalClose } = props; - - return ( - - - - ); -} - -export default ParseModal; diff --git a/frontend/src/Parse/ParseModalContent.css b/frontend/src/Parse/ParseModalContent.css deleted file mode 100644 index 43536452c..000000000 --- a/frontend/src/Parse/ParseModalContent.css +++ /dev/null @@ -1,45 +0,0 @@ -.inputContainer { - display: flex; - margin-bottom: 10px; -} - -.inputIconContainer { - width: 58px; - height: 46px; - border: 1px solid var(--inputBorderColor); - border-right: none; - border-radius: 4px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - background-color: var(--inputIconContainerBackgroundColor); - text-align: center; - line-height: 46px; -} - -.input { - composes: input from '~Components/Form/TextInput.css'; - - height: 46px; - border-radius: 0; - font-size: 18px; -} - -.clearButton { - border: 1px solid var(--inputBorderColor); - border-left: none; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.message { - margin-top: 30px; - text-align: center; - font-weight: 300; - font-size: $largeFontSize; -} - -.helpText { - margin-bottom: 10px; - font-size: 24px; -} diff --git a/frontend/src/Parse/ParseModalContent.css.d.ts b/frontend/src/Parse/ParseModalContent.css.d.ts deleted file mode 100644 index 4a4def577..000000000 --- a/frontend/src/Parse/ParseModalContent.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'clearButton': string; - 'helpText': string; - 'input': string; - 'inputContainer': string; - 'inputIconContainer': string; - 'message': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Parse/ParseModalContent.tsx b/frontend/src/Parse/ParseModalContent.tsx deleted file mode 100644 index d5ae93759..000000000 --- a/frontend/src/Parse/ParseModalContent.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import TextInput from 'Components/Form/TextInput'; -import Icon from 'Components/Icon'; -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 { icons } from 'Helpers/Props'; -import { clear, fetch } from 'Store/Actions/parseActions'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import ParseResult from './ParseResult'; -import parseStateSelector from './parseStateSelector'; -import styles from './ParseModalContent.css'; - -interface ParseModalContentProps { - onModalClose: () => void; -} - -function ParseModalContent(props: ParseModalContentProps) { - const { onModalClose } = props; - const { isFetching, error, item } = useSelector(parseStateSelector()); - - const [title, setTitle] = useState(''); - const dispatch = useDispatch(); - - const onInputChange = useCallback( - ({ value }: { value: string }) => { - const trimmedValue = value.trim(); - - setTitle(value); - - if (trimmedValue === '') { - dispatch(clear()); - } else { - dispatch(fetch({ title: trimmedValue })); - } - }, - [setTitle, dispatch] - ); - - const onClearPress = useCallback(() => { - setTitle(''); - dispatch(clear()); - }, [setTitle, dispatch]); - - useEffect( - () => { - return () => { - dispatch(clear()); - }; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return ( - - {translate('TestParsing')} - - -
-
- -
- - - - -
- - {isFetching ? : null} - - {!isFetching && !!error ? ( -
-
- {translate('ParseModalErrorParsing')} -
-
{getErrorMessage(error)}
-
- ) : null} - - {!isFetching && title && !error && !item.parsedAlbumInfo ? ( -
- {translate('ParseModalUnableToParse')} -
- ) : null} - - {!isFetching && !error && item.parsedAlbumInfo ? ( - - ) : null} - - {title ? null : ( -
-
- {translate('ParseModalHelpText')} -
-
{translate('ParseModalHelpTextDetails')}
-
- )} -
- - - - -
- ); -} - -export default ParseModalContent; diff --git a/frontend/src/Parse/ParseResult.css b/frontend/src/Parse/ParseResult.css deleted file mode 100644 index c49c4e3fa..000000000 --- a/frontend/src/Parse/ParseResult.css +++ /dev/null @@ -1,8 +0,0 @@ -.container { - display: flex; - flex-wrap: wrap; -} - -.column { - flex: 0 0 50%; -} diff --git a/frontend/src/Parse/ParseResult.css.d.ts b/frontend/src/Parse/ParseResult.css.d.ts deleted file mode 100644 index 653368e06..000000000 --- a/frontend/src/Parse/ParseResult.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'column': string; - 'container': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Parse/ParseResult.tsx b/frontend/src/Parse/ParseResult.tsx deleted file mode 100644 index 7e6c40d92..000000000 --- a/frontend/src/Parse/ParseResult.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import _ from 'lodash'; -import moment from 'moment'; -import React from 'react'; -import AlbumFormats from 'Album/AlbumFormats'; -import AlbumTitleLink from 'Album/AlbumTitleLink'; -import { ParseModel } from 'App/State/ParseAppState'; -import ArtistNameLink from 'Artist/ArtistNameLink'; -import FieldSet from 'Components/FieldSet'; -import translate from 'Utilities/String/translate'; -import ParseResultItem from './ParseResultItem'; -import styles from './ParseResult.css'; - -interface ParseResultProps { - item: ParseModel; -} - -function ParseResult(props: ParseResultProps) { - const { item } = props; - const { customFormats, customFormatScore, albums, parsedAlbumInfo, artist } = - item; - - const { - releaseTitle, - artistName, - albumTitle, - releaseGroup, - discography, - quality, - } = parsedAlbumInfo; - - const sortedAlbums = _.sortBy(albums, (item) => - moment(item.releaseDate).unix() - ); - - return ( -
-
- - - - - - - -
- -
-
-
- -
-
-
- -
-
-
- - 1 && !quality.revision.isRepack - ? translate('True') - : '-' - } - /> - - -
- -
- 1 ? quality.revision.version : '-' - } - /> - - -
-
-
- -
- - ) : ( - '-' - ) - } - /> - - - {sortedAlbums.map((album) => { - return ( -
- -
- ); - })} -
- ) : ( - '-' - ) - } - /> - - - ) : ( - '-' - ) - } - /> - - - -
- ); -} - -export default ParseResult; diff --git a/frontend/src/Parse/ParseResultItem.css b/frontend/src/Parse/ParseResultItem.css deleted file mode 100644 index 275fe7e1f..000000000 --- a/frontend/src/Parse/ParseResultItem.css +++ /dev/null @@ -1,21 +0,0 @@ -.item { - display: flex; -} - -.title { - margin-right: 20px; - width: 250px; - text-align: right; - font-weight: bold; -} - -@media (max-width: $breakpointSmall) { - .item { - display: block; - margin-bottom: 10px; - } - - .title { - text-align: left; - } -} diff --git a/frontend/src/Parse/ParseResultItem.css.d.ts b/frontend/src/Parse/ParseResultItem.css.d.ts deleted file mode 100644 index bcf268e50..000000000 --- a/frontend/src/Parse/ParseResultItem.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'item': string; - 'title': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Parse/ParseResultItem.tsx b/frontend/src/Parse/ParseResultItem.tsx deleted file mode 100644 index 661af448d..000000000 --- a/frontend/src/Parse/ParseResultItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { ReactNode } from 'react'; -import styles from './ParseResultItem.css'; - -interface ParseResultItemProps { - title: string; - data: string | number | ReactNode; -} - -function ParseResultItem(props: ParseResultItemProps) { - const { title, data } = props; - - return ( -
-
{title}
-
{data}
-
- ); -} - -export default ParseResultItem; diff --git a/frontend/src/Parse/ParseToolbarButton.tsx b/frontend/src/Parse/ParseToolbarButton.tsx deleted file mode 100644 index 43b8b959f..000000000 --- a/frontend/src/Parse/ParseToolbarButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { Fragment, useCallback, useState } from 'react'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import { icons } from 'Helpers/Props'; -import ParseModal from 'Parse/ParseModal'; -import translate from 'Utilities/String/translate'; - -function ParseToolbarButton() { - const [isParseModalOpen, setIsParseModalOpen] = useState(false); - - const onOpenParseModalPress = useCallback(() => { - setIsParseModalOpen(true); - }, [setIsParseModalOpen]); - - const onParseModalClose = useCallback(() => { - setIsParseModalOpen(false); - }, [setIsParseModalOpen]); - - return ( - - - - - - ); -} - -export default ParseToolbarButton; diff --git a/frontend/src/Parse/parseStateSelector.ts b/frontend/src/Parse/parseStateSelector.ts deleted file mode 100644 index 7abcfeca1..000000000 --- a/frontend/src/Parse/parseStateSelector.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import ParseAppState from 'App/State/ParseAppState'; - -export default function parseStateSelector() { - return createSelector( - (state: AppState) => state.parse, - (parse: ParseAppState) => { - return parse; - } - ); -} diff --git a/frontend/src/Search/AddNewItem.js b/frontend/src/Search/AddNewItem.js index e335ef4c2..5ec065149 100644 --- a/frontend/src/Search/AddNewItem.js +++ b/frontend/src/Search/AddNewItem.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import TextInput from 'Components/Form/TextInput'; import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; @@ -131,8 +130,7 @@ class AddNewItem extends Component {
{translate('FailedLoadingSearchResults')}
- - {getErrorMessage(error)} +
{getErrorMessage(error)}
: null } diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js new file mode 100644 index 000000000..342df29d2 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js @@ -0,0 +1,33 @@ +import React, { Component } from 'react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import translate from 'Utilities/String/translate'; +import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector'; + +class CustomFormatSettingsConnector extends Component { + + // + // Render + + render() { + return ( + + + + + + + + + + ); + } +} + +export default CustomFormatSettingsConnector; + diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx deleted file mode 100644 index 66c208f9a..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; -import ParseToolbarButton from 'Parse/ParseToolbarButton'; -import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; -import translate from 'Utilities/String/translate'; -import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector'; -import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton'; - -function CustomFormatSettingsPage() { - return ( - - - - - - - - - } - /> - - - {/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - - - - ); -} - -export default CustomFormatSettingsPage; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js index 0417d9b21..8e828620b 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import CustomFormats from './CustomFormats'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.customFormats', sortByProp('name')), + createSortedSectionSelector('settings.customFormats', sortByName), (customFormats) => customFormats ); } diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js index 3e79425cd..52b2f09f6 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { clearPendingChanges } from 'Store/Actions/baseActions'; import EditCustomFormatModal from './EditCustomFormatModal'; -import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector'; function mapStateToProps() { return {}; @@ -37,7 +36,6 @@ class EditCustomFormatModalConnector extends Component { } EditCustomFormatModalConnector.propTypes = { - ...EditCustomFormatModalContentConnector.propTypes, onModalClose: PropTypes.func.isRequired, clearPendingChanges: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css index 24830ef42..b7d3da255 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css @@ -25,8 +25,3 @@ border-radius: 4px; background-color: var(--cardCenterBackgroundColor); } - -.customFormats { - display: flex; - flex-wrap: wrap; -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts index 1aab6062e..1339caf02 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css.d.ts @@ -3,7 +3,6 @@ interface CssExports { 'addSpecification': string; 'center': string; - 'customFormats': string; 'deleteButton': string; 'rightButtons': string; } diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js index 8b06deb4b..4d8f0fd4b 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import Card from 'Components/Card'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; @@ -151,11 +150,6 @@ class EditCustomFormatModalContent extends Component {
- -
- {translate('CustomFormatsSettingsTriggerInfo')} -
-
{ specifications.map((tag) => { diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx deleted file mode 100644 index 3ff5cfa37..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageCustomFormatsEditModalContent from './ManageCustomFormatsEditModalContent'; - -interface ManageCustomFormatsEditModalProps { - isOpen: boolean; - customFormatIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -function ManageCustomFormatsEditModal( - props: ManageCustomFormatsEditModalProps -) { - const { isOpen, customFormatIds, onSavePress, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageCustomFormatsEditModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css deleted file mode 100644 index ea406894e..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.modalFooter { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - justify-content: space-between; -} - -.selected { - font-weight: bold; -} - -@media only screen and (max-width: $breakpointExtraSmall) { - .modalFooter { - flex-direction: column; - gap: 10px; - } -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts deleted file mode 100644 index cbf2d6328..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'modalFooter': string; - 'selected': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx deleted file mode 100644 index 25a2f85c2..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/Edit/ManageCustomFormatsEditModalContent.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -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 { inputTypes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './ManageCustomFormatsEditModalContent.css'; - -interface SavePayload { - includeCustomFormatWhenRenaming?: boolean; -} - -interface ManageCustomFormatsEditModalContentProps { - customFormatIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -const NO_CHANGE = 'noChange'; - -const enableOptions = [ - { - key: NO_CHANGE, - get value() { - return translate('NoChange'); - }, - isDisabled: true, - }, - { - key: 'enabled', - get value() { - return translate('Enabled'); - }, - }, - { - key: 'disabled', - get value() { - return translate('Disabled'); - }, - }, -]; - -function ManageCustomFormatsEditModalContent( - props: ManageCustomFormatsEditModalContentProps -) { - const { customFormatIds, onSavePress, onModalClose } = props; - - const [includeCustomFormatWhenRenaming, setIncludeCustomFormatWhenRenaming] = - useState(NO_CHANGE); - - const save = useCallback(() => { - let hasChanges = false; - const payload: SavePayload = {}; - - if (includeCustomFormatWhenRenaming !== NO_CHANGE) { - hasChanges = true; - payload.includeCustomFormatWhenRenaming = - includeCustomFormatWhenRenaming === 'enabled'; - } - - if (hasChanges) { - onSavePress(payload); - } - - onModalClose(); - }, [includeCustomFormatWhenRenaming, onSavePress, onModalClose]); - - const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { - switch (name) { - case 'includeCustomFormatWhenRenaming': - setIncludeCustomFormatWhenRenaming(value); - break; - default: - console.warn( - `EditCustomFormatsModalContent Unknown Input: '${name}'` - ); - } - }, - [] - ); - - const selectedCount = customFormatIds.length; - - return ( - - {translate('EditSelectedCustomFormats')} - - - - {translate('IncludeCustomFormatWhenRenaming')} - - - - - - -
- {translate('CountCustomFormatsSelected', { - count: selectedCount, - })} -
- -
- - - -
-
-
- ); -} - -export default ManageCustomFormatsEditModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx deleted file mode 100644 index dd3456437..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageCustomFormatsModalContent from './ManageCustomFormatsModalContent'; - -interface ManageCustomFormatsModalProps { - isOpen: boolean; - onModalClose(): void; -} - -function ManageCustomFormatsModal(props: ManageCustomFormatsModalProps) { - const { isOpen, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageCustomFormatsModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css deleted file mode 100644 index 6ea04a0c8..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.leftButtons, -.rightButtons { - display: flex; - flex: 1 0 50%; - flex-wrap: wrap; -} - -.rightButtons { - justify-content: flex-end; -} - -.deleteButton { - composes: button from '~Components/Link/Button.css'; - - margin-right: 10px; -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts deleted file mode 100644 index 7b392fff9..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'deleteButton': string; - 'leftButtons': string; - 'rightButtons': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx deleted file mode 100644 index aabaf67c1..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { CustomFormatAppState } from 'App/State/SettingsAppState'; -import Alert from 'Components/Alert'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -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 Column from 'Components/Table/Column'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import useSelectState from 'Helpers/Hooks/useSelectState'; -import { kinds } from 'Helpers/Props'; -import { - bulkDeleteCustomFormats, - bulkEditCustomFormats, - setManageCustomFormatsSort, -} from 'Store/Actions/settingsActions'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import { SelectStateInputProps } from 'typings/props'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import ManageCustomFormatsEditModal from './Edit/ManageCustomFormatsEditModal'; -import ManageCustomFormatsModalRow from './ManageCustomFormatsModalRow'; -import styles from './ManageCustomFormatsModalContent.css'; - -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageCustomFormatsModalRow ->['onSelectedChange']; - -const COLUMNS: Column[] = [ - { - name: 'name', - label: () => translate('Name'), - isSortable: true, - isVisible: true, - }, - { - name: 'includeCustomFormatWhenRenaming', - label: () => translate('IncludeCustomFormatWhenRenaming'), - isSortable: true, - isVisible: true, - }, - { - name: 'actions', - label: '', - isVisible: true, - }, -]; - -interface ManageCustomFormatsModalContentProps { - onModalClose(): void; -} - -function ManageCustomFormatsModalContent( - props: ManageCustomFormatsModalContentProps -) { - const { onModalClose } = props; - - const { - isFetching, - isPopulated, - isDeleting, - isSaving, - error, - items, - sortKey, - sortDirection, - }: CustomFormatAppState = useSelector( - createClientSideCollectionSelector('settings.customFormats') - ); - const dispatch = useDispatch(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - - const [selectState, setSelectState] = useSelectState(); - - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; - - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageCustomFormatsSort({ sortKey: value })); - }, - [dispatch] - ); - - const onDeletePress = useCallback(() => { - setIsDeleteModalOpen(true); - }, [setIsDeleteModalOpen]); - - const onDeleteModalClose = useCallback(() => { - setIsDeleteModalOpen(false); - }, [setIsDeleteModalOpen]); - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteCustomFormats({ ids: selectedIds })); - setIsDeleteModalOpen(false); - }, [selectedIds, dispatch]); - - const onSavePress = useCallback( - (payload: object) => { - setIsEditModalOpen(false); - - dispatch( - bulkEditCustomFormats({ - ids: selectedIds, - ...payload, - }) - ); - }, - [selectedIds, dispatch] - ); - - const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); - }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] - ); - - const errorMessage = getErrorMessage(error, 'Unable to load custom formats.'); - const anySelected = selectedCount > 0; - - return ( - - {translate('ManageCustomFormats')} - - {isFetching ? : null} - - {error ?
{errorMessage}
: null} - - {isPopulated && !error && !items.length ? ( - {translate('NoCustomFormatsFound')} - ) : null} - - {isPopulated && !!items.length && !isFetching && !isFetching ? ( - - - {items.map((item) => { - return ( - - ); - })} - -
- ) : null} -
- - -
- - {translate('Delete')} - - - - {translate('Edit')} - -
- - -
- - - - -
- ); -} - -export default ManageCustomFormatsModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css deleted file mode 100644 index 355c70378..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css +++ /dev/null @@ -1,12 +0,0 @@ -.name, -.includeCustomFormatWhenRenaming { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - word-break: break-all; -} - -.actions { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 40px; -} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts deleted file mode 100644 index d1719edd8..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'actions': string; - 'includeCustomFormatWhenRenaming': string; - 'name': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx deleted file mode 100644 index 57bb7fda0..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import Column from 'Components/Table/Column'; -import TableRow from 'Components/Table/TableRow'; -import { icons } from 'Helpers/Props'; -import { deleteCustomFormat } from 'Store/Actions/settingsActions'; -import { SelectStateInputProps } from 'typings/props'; -import translate from 'Utilities/String/translate'; -import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector'; -import styles from './ManageCustomFormatsModalRow.css'; - -interface ManageCustomFormatsModalRowProps { - id: number; - name: string; - includeCustomFormatWhenRenaming: boolean; - columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; -} - -function isDeletingSelector() { - return createSelector( - (state: AppState) => state.settings.customFormats.isDeleting, - (isDeleting) => { - return isDeleting; - } - ); -} - -function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { - const { - id, - isSelected, - name, - includeCustomFormatWhenRenaming, - onSelectedChange, - } = props; - - const dispatch = useDispatch(); - const isDeleting = useSelector(isDeletingSelector()); - - const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] = - useState(false); - - const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] = - useState(false); - - const handlelectedChange = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, - }); - }, - [onSelectedChange] - ); - - const handleEditCustomFormatModalOpen = useCallback(() => { - setIsEditCustomFormatModalOpen(true); - }, [setIsEditCustomFormatModalOpen]); - - const handleEditCustomFormatModalClose = useCallback(() => { - setIsEditCustomFormatModalOpen(false); - }, [setIsEditCustomFormatModalOpen]); - - const handleDeleteCustomFormatPress = useCallback(() => { - setIsEditCustomFormatModalOpen(false); - setIsDeleteCustomFormatModalOpen(true); - }, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]); - - const handleDeleteCustomFormatModalClose = useCallback(() => { - setIsDeleteCustomFormatModalOpen(false); - }, [setIsDeleteCustomFormatModalOpen]); - - const handleConfirmDeleteCustomFormat = useCallback(() => { - dispatch(deleteCustomFormat({ id })); - }, [id, dispatch]); - - return ( - - - - {name} - - - {includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')} - - - - - - - - - - - ); -} - -export default ManageCustomFormatsModalRow; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx deleted file mode 100644 index 91f41dc44..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsToolbarButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import ManageCustomFormatsModal from './ManageCustomFormatsModal'; - -function ManageCustomFormatsToolbarButton() { - const [isManageModalOpen, openManageModal, closeManageModal] = - useModalOpenState(false); - - return ( - <> - - - - - ); -} - -export default ManageCustomFormatsToolbarButton; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js index 19aae4694..02a31eda7 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js @@ -7,8 +7,8 @@ import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; @@ -52,13 +52,12 @@ function EditSpecificationModalContent(props) { fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
- +
\\^$.|?*+()[{ have special meanings and need escaping with a \\' }} /> + {'More details'} {'Here'}
- -
-
- + {'Regular expressions can be tested '} + Here
} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index 0dc410fcb..d9e543469 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteDownloadClient, fetchDownloadClients } 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 DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByProp('name')), + createSortedSectionSelector('settings.downloadClients', sortByName), createTagsSelector(), (downloadClients, tagList) => { return { diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index 8d7d994f7..d3d51c7fc 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -15,7 +15,6 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import styles from './EditDownloadClientModalContent.css'; @@ -38,7 +37,6 @@ class EditDownloadClientModalContent extends Component { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteDownloadClientPress, ...otherProps } = this.props; @@ -208,12 +206,6 @@ class EditDownloadClientModalContent extends Component { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -76,7 +65,6 @@ class EditDownloadClientModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -94,7 +82,6 @@ EditDownloadClientModalContentConnector.propTypes = { setDownloadClientFieldValue: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired, testDownloadClient: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index 7599cb9b0..18ae5170a 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css index 6ea04a0c8..c106388ab 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} +} \ No newline at end of file diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index b2c1208cb..2722f02fa 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; +import SortDirection from 'Helpers/Props/SortDirection'; import { bulkDeleteDownloadClients, bulkEditDownloadClients, @@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps< typeof ManageDownloadClientsModalRow >['onSelectedChange']; -const COLUMNS: Column[] = [ +const COLUMNS = [ { name: 'name', label: () => translate('Name'), @@ -82,6 +82,8 @@ const COLUMNS: Column[] = [ interface ManageDownloadClientsModalContentProps { onModalClose(): void; + sortKey?: string; + sortDirection?: SortDirection; } function ManageDownloadClientsModalContent( @@ -218,9 +220,9 @@ function ManageDownloadClientsModalContent( {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isPopulated && !error && !items.length && ( {translate('NoDownloadClientsFound')} - ) : null} + )} {isPopulated && !!items.length && !isFetching && !isFetching ? ( diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.js index 043867853..93918a3d2 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 ( @@ -41,30 +39,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 a151423e5..081f5dda2 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -18,6 +18,7 @@ function UpdateSettings(props) { const { advancedSettings, settings, + isWindows, packageUpdateMechanism, onInputChange } = props; @@ -43,10 +44,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 (
@@ -68,59 +69,62 @@ 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/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index d50fb2385..ed9582e91 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -20,7 +20,6 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import Popover from 'Components/Tooltip/Popover'; import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; -import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import translate from 'Utilities/String/translate'; import styles from './EditImportListModalContent.css'; @@ -67,7 +66,6 @@ function EditImportListModalContent(props) { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteImportListPress, showMetadataProfile, ...otherProps @@ -292,7 +290,7 @@ function EditImportListModalContent(props) { @@ -335,12 +333,6 @@ function EditImportListModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -78,7 +67,6 @@ class EditImportListModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -96,7 +84,6 @@ EditImportListModalContentConnector.propTypes = { setImportListFieldValue: PropTypes.func.isRequired, saveImportList: PropTypes.func.isRequired, testImportList: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js index 5eb47068d..5c6bad8e7 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteImportList, fetchImportLists, fetchRootFolders } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import ImportLists from './ImportLists'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.importLists', sortByProp('name')), + createSortedSectionSelector('settings.importLists', sortByName), (importLists) => importLists ); } diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx index 82f7d309c..5a651ba28 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx @@ -31,7 +31,7 @@ const autoAddOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css index 6ea04a0c8..c106388ab 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} +} \ No newline at end of file diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index 4fee485c9..60619c662 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -198,9 +198,9 @@ function ManageImportListsModalContent( {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isPopulated && !error && !items.length && ( {translate('NoImportListsFound')} - ) : null} + )} {isPopulated && !!items.length && !isFetching && !isFetching ? (
- {qualityProfile?.name ?? translate('None')} + {qualityProfile?.name ?? 'None'} @@ -72,7 +71,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) { - {enableAutomaticAdd ? translate('Yes') : translate('No')} + {enableAutomaticAdd ? 'Yes' : 'No'} diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js index bda52ac42..55235c9da 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -160,7 +160,7 @@ function EditIndexerModalContent(props) { { return { diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx index 69ad5a988..f9b051986 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - isDisabled: true, + disabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css index 6ea04a0c8..c106388ab 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.css @@ -13,4 +13,4 @@ composes: button from '~Components/Link/Button.css'; margin-right: 10px; -} +} \ No newline at end of file diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 997d1b566..8137111f5 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -10,11 +10,11 @@ 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 Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; +import SortDirection from 'Helpers/Props/SortDirection'; import { bulkDeleteIndexers, bulkEditIndexers, @@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps< typeof ManageIndexersModalRow >['onSelectedChange']; -const COLUMNS: Column[] = [ +const COLUMNS = [ { name: 'name', label: () => translate('Name'), @@ -82,6 +82,8 @@ const COLUMNS: Column[] = [ interface ManageIndexersModalContentProps { onModalClose(): void; + sortKey?: string; + sortDirection?: SortDirection; } function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { @@ -213,9 +215,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isPopulated && !error && !items.length && ( {translate('NoIndexersFound')} - ) : null} + )} {isPopulated && !!items.length && !isFetching && !isFetching ? (
- - {translate('SkipFreeSpaceCheck')} + { + !isWindows && + + + {translate('SkipFreeSpaceCheck')} + - - + + + } state.settings.advancedSettings, (state) => state.settings.namingExamples, createSettingsSectionSelector(SECTION), - (advancedSettings, namingExamples, sectionSettings) => { + (advancedSettings, examples, sectionSettings) => { return { advancedSettings, - examples: namingExamples.item, - examplesPopulated: namingExamples.isPopulated, + examples: examples.item, + examplesPopulated: !_.isEmpty(examples.item), ...sectionSettings }; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index 204c93d0e..b692362fb 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -1,6 +1,6 @@ .option { display: flex; - align-items: stretch; + align-items: center; flex-wrap: wrap; margin: 3px; border: 1px solid var(--borderColor); @@ -17,7 +17,7 @@ } .small { - width: 490px; + width: 460px; } .large { @@ -26,7 +26,7 @@ .token { flex: 0 0 50%; - padding: 6px; + padding: 6px 16px; background-color: var(--popoverTitleBackgroundColor); font-family: $monoSpaceFontFamily; } @@ -34,9 +34,9 @@ .example { display: flex; align-items: center; - justify-content: space-between; + align-self: stretch; flex: 0 0 50%; - padding: 6px; + padding: 6px 16px; background-color: var(--popoverBodyBackgroundColor); .footNote { diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js index dc91e4622..20cefc53f 100644 --- a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js +++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js @@ -75,12 +75,12 @@ class RootFolder extends Component { {path} -
+ + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } +
+ ); +} + +QueuedTasks.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default QueuedTasks; diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx deleted file mode 100644 index e79deed7c..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx +++ /dev/null @@ -1,74 +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 Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { fetchCommands } from 'Store/Actions/commandActions'; -import translate from 'Utilities/String/translate'; -import QueuedTaskRow from './QueuedTaskRow'; - -const columns = [ - { - name: 'trigger', - label: '', - isVisible: true, - }, - { - name: 'commandName', - label: () => translate('Name'), - isVisible: true, - }, - { - name: 'queued', - label: () => translate('Queued'), - isVisible: true, - }, - { - name: 'started', - label: () => translate('Started'), - isVisible: true, - }, - { - name: 'ended', - label: () => translate('Ended'), - isVisible: true, - }, - { - name: 'duration', - label: () => translate('Duration'), - isVisible: true, - }, - { - name: 'actions', - isVisible: true, - }, -]; - -export default function QueuedTasks() { - const dispatch = useDispatch(); - const { isFetching, isPopulated, items } = useSelector( - (state: AppState) => state.commands - ); - - useEffect(() => { - dispatch(fetchCommands()); - }, [dispatch]); - - return ( -
- {isFetching && !isPopulated && } - - {isPopulated && ( - - - {items.map((item) => { - return ; - })} - -
- )} -
- ); -} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js new file mode 100644 index 000000000..5fa4d9ead --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.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 { fetchCommands } from 'Store/Actions/commandActions'; +import QueuedTasks from './QueuedTasks'; + +function createMapStateToProps() { + return createSelector( + (state) => state.commands, + (commands) => { + return commands; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchCommands: fetchCommands +}; + +class QueuedTasksConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchCommands(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QueuedTasksConnector.propTypes = { + dispatchFetchCommands: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js index 03a3b6ce4..032dbede8 100644 --- a/frontend/src/System/Tasks/Tasks.js +++ b/frontend/src/System/Tasks/Tasks.js @@ -2,7 +2,7 @@ import React from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; -import QueuedTasks from './Queued/QueuedTasks'; +import QueuedTasksConnector from './Queued/QueuedTasksConnector'; import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; function Tasks() { @@ -10,7 +10,7 @@ function Tasks() { - + ); diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js new file mode 100644 index 000000000..3588069a0 --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.js @@ -0,0 +1,46 @@ +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) => { + 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 3e5ba1c9b..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{4,5}\b/g, - (match) => - `[${match}](https://github.com/Lidarr/Lidarr/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..528441cbe --- /dev/null +++ b/frontend/src/System/Updates/Updates.js @@ -0,0 +1,249 @@ +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, + 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 Lidarr directly,'; + const externalUpdaterMessages = { + external: 'Lidarr 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' ? + + Install Latest + : + + + + +
+ {externalUpdaterPrefix} +
+
+ } + + { + isFetching && + + } +
+ } + + { + noUpdateToInstall && +
+ +
+ The latest version of Lidarr is already installed +
+ + { + 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, + 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 300ab1f99..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..77d75dbda --- /dev/null +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -0,0 +1,98 @@ +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(), + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), + ( + currentVersion, + status, + updates, + generalSettings, + uiSettings, + 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, + 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/TrackFile/Editor/TrackFileEditorModalContent.js b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js index 0e387f39f..aa59e866f 100644 --- a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js +++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js @@ -146,7 +146,7 @@ class TrackFileEditorModalContent extends Component { }); return acc; - }, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]); + }, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]); const hasSelectedFiles = this.getSelectedIds().length > 0; diff --git a/frontend/src/TrackFile/TrackFile.ts b/frontend/src/TrackFile/TrackFile.ts index ef4dc65f3..ce9379816 100644 --- a/frontend/src/TrackFile/TrackFile.ts +++ b/frontend/src/TrackFile/TrackFile.ts @@ -13,7 +13,6 @@ export interface TrackFile extends ModelBase { releaseGroup: string; quality: QualityModel; customFormats: CustomFormat[]; - indexerFlags: number; mediaInfo: MediaInfo; qualityCutoffNotMet: boolean; } 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/Artist/monitorNewItemsOptions.js b/frontend/src/Utilities/Artist/monitorNewItemsOptions.js index f45095b6e..2f352be3a 100644 --- a/frontend/src/Utilities/Artist/monitorNewItemsOptions.js +++ b/frontend/src/Utilities/Artist/monitorNewItemsOptions.js @@ -4,19 +4,19 @@ const monitorNewItemsOptions = [ { key: 'all', get value() { - return translate('MonitorAllAlbums'); + return translate('AllAlbums'); } }, { key: 'none', get value() { - return translate('MonitorNoNewAlbums'); + return translate('None'); } }, { key: 'new', get value() { - return translate('MonitorNewAlbums'); + return translate('New'); } } ]; diff --git a/frontend/src/Utilities/Artist/monitorOptions.js b/frontend/src/Utilities/Artist/monitorOptions.js index a06a79a96..b5e942ae6 100644 --- a/frontend/src/Utilities/Artist/monitorOptions.js +++ b/frontend/src/Utilities/Artist/monitorOptions.js @@ -1,48 +1,11 @@ -import translate from 'Utilities/String/translate'; - const monitorOptions = [ - { - key: 'all', - get value() { - return translate('MonitorAllAlbums'); - } - }, - { - key: 'future', - get value() { - return translate('MonitorFutureAlbums'); - } - }, - { - key: 'missing', - get value() { - return translate('MonitorMissingAlbums'); - } - }, - { - key: 'existing', - get value() { - return translate('MonitorExistingAlbums'); - } - }, - { - key: 'first', - get value() { - return translate('MonitorFirstAlbum'); - } - }, - { - key: 'latest', - get value() { - return translate('MonitorLastestAlbum'); - } - }, - { - key: 'none', - get value() { - return translate('MonitorNoAlbums'); - } - } + { key: 'all', value: 'All Albums' }, + { key: 'future', value: 'Future Albums' }, + { key: 'missing', value: 'Missing Albums' }, + { key: 'existing', value: 'Existing Albums' }, + { key: 'first', value: 'Only First Album' }, + { key: 'latest', value: 'Only Latest Album' }, + { key: 'none', value: 'None' } ]; export default monitorOptions; diff --git a/frontend/src/Utilities/Date/formatDateTime.js b/frontend/src/Utilities/Date/formatDateTime.js index fb50230e1..f36f4f3e0 100644 --- a/frontend/src/Utilities/Date/formatDateTime.js +++ b/frontend/src/Utilities/Date/formatDateTime.js @@ -1,5 +1,4 @@ import moment from 'moment'; -import translate from 'Utilities/String/translate'; import formatTime from './formatTime'; import isToday from './isToday'; import isTomorrow from './isTomorrow'; @@ -11,15 +10,15 @@ function getRelativeDay(date, includeRelativeDate) { } if (isYesterday(date)) { - return translate('Yesterday'); + return 'Yesterday, '; } if (isToday(date)) { - return translate('Today'); + return 'Today, '; } if (isTomorrow(date)) { - return translate('Tomorrow'); + return 'Tomorrow, '; } return ''; @@ -34,10 +33,7 @@ function formatDateTime(date, dateFormat, timeFormat, { includeSeconds = false, const formattedDate = moment(date).format(dateFormat); const formattedTime = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); - if (relativeDay) { - return translate('FormatDateTimeRelative', { relativeDay, formattedDate, formattedTime }); - } - return translate('FormatDateTime', { formattedDate, formattedTime }); + return `${relativeDay}${formattedDate} ${formattedTime}`; } export default formatDateTime; diff --git a/frontend/src/Utilities/Date/formatShortTimeSpan.js b/frontend/src/Utilities/Date/formatShortTimeSpan.js index 148dc2627..c14251e68 100644 --- a/frontend/src/Utilities/Date/formatShortTimeSpan.js +++ b/frontend/src/Utilities/Date/formatShortTimeSpan.js @@ -1,5 +1,4 @@ import moment from 'moment'; -import translate from 'Utilities/String/translate'; function formatShortTimeSpan(timeSpan) { if (!timeSpan) { @@ -13,14 +12,14 @@ function formatShortTimeSpan(timeSpan) { const seconds = Math.floor(duration.asSeconds()); if (hours > 0) { - return translate('FormatShortTimeSpanHours', { hours }); + return `${hours} hour(s)`; } if (minutes > 0) { - return translate('FormatShortTimeSpanMinutes', { minutes }); + return `${minutes} minute(s)`; } - return translate('FormatShortTimeSpanSeconds', { seconds }); + return `${seconds} second(s)`; } export default formatShortTimeSpan; diff --git a/frontend/src/Utilities/Date/formatTimeSpan.js b/frontend/src/Utilities/Date/formatTimeSpan.js index 2422e19d5..1ebe6b9e3 100644 --- a/frontend/src/Utilities/Date/formatTimeSpan.js +++ b/frontend/src/Utilities/Date/formatTimeSpan.js @@ -1,6 +1,5 @@ import moment from 'moment'; import padNumber from 'Utilities/Number/padNumber'; -import translate from 'Utilities/String/translate'; function formatTimeSpan(timeSpan) { if (!timeSpan) { @@ -17,7 +16,7 @@ function formatTimeSpan(timeSpan) { const time = `${hours}:${minutes}:${seconds}`; if (days > 0) { - return translate('FormatTimeSpanDays', { days, time }); + return `${days}d ${time}`; } return time; diff --git a/frontend/src/Utilities/Date/getRelativeDate.ts b/frontend/src/Utilities/Date/getRelativeDate.js similarity index 54% rename from frontend/src/Utilities/Date/getRelativeDate.ts rename to frontend/src/Utilities/Date/getRelativeDate.js index 178d14fb7..0a60135ce 100644 --- a/frontend/src/Utilities/Date/getRelativeDate.ts +++ b/frontend/src/Utilities/Date/getRelativeDate.js @@ -4,35 +4,16 @@ import isInNextWeek from 'Utilities/Date/isInNextWeek'; import isToday from 'Utilities/Date/isToday'; import isTomorrow from 'Utilities/Date/isTomorrow'; import isYesterday from 'Utilities/Date/isYesterday'; -import translate from 'Utilities/String/translate'; -interface GetRelativeDateOptions { - timeFormat?: string; - includeSeconds?: boolean; - timeForToday?: boolean; -} - -function getRelativeDate( - date: string | undefined, - shortDateFormat: string, - showRelativeDates: boolean, - { - timeFormat, - includeSeconds = false, - timeForToday = false, - }: GetRelativeDateOptions = {} -) { +function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) { if (!date) { - return ''; + return null; } const isTodayDate = isToday(date); if (isTodayDate && timeForToday && timeFormat) { - return formatTime(date, timeFormat, { - includeMinuteZero: true, - includeSeconds, - }); + return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); } if (!showRelativeDates) { @@ -40,15 +21,15 @@ function getRelativeDate( } if (isYesterday(date)) { - return translate('Yesterday'); + return 'Yesterday'; } if (isTodayDate) { - return translate('Today'); + return 'Today'; } if (isTomorrow(date)) { - return translate('Tomorrow'); + return 'Tomorrow'; } if (isInNextWeek(date)) { diff --git a/frontend/src/Utilities/Number/formatAge.js b/frontend/src/Utilities/Number/formatAge.js index a8f0e9f65..b8a4aacc5 100644 --- a/frontend/src/Utilities/Number/formatAge.js +++ b/frontend/src/Utilities/Number/formatAge.js @@ -1,5 +1,3 @@ -import translate from 'Utilities/String/translate'; - function formatAge(age, ageHours, ageMinutes) { age = Math.round(age); ageHours = parseFloat(ageHours); @@ -7,13 +5,13 @@ function formatAge(age, ageHours, ageMinutes) { if (age < 2 && ageHours) { if (ageHours < 2 && !!ageMinutes) { - return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? translate('FormatAgeMinute') : translate('FormatAgeMinutes')}`; + return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? 'minute' : 'minutes'}`; } - return `${ageHours.toFixed(1)} ${ageHours === 1 ? translate('FormatAgeHour') : translate('FormatAgeHours')}`; + return `${ageHours.toFixed(1)} ${ageHours === 1 ? 'hour' : 'hours'}`; } - return `${age} ${age === 1 ? translate('FormatAgeDay') : translate('FormatAgeDays')}`; + return `${age} ${age === 1 ? 'day' : 'days'}`; } export default formatAge; diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts index 5571ef6b0..98a0418ad 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); } }); @@ -27,12 +27,6 @@ export default function translate( key: string, tokens: Record = {} ) { - const { isProduction = true } = window.Lidarr; - - if (!isProduction && !(key in translations)) { - console.warn(`Missing translation for key: ${key}`); - } - const translation = translations[key] || key; tokens.appName = 'Lidarr'; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index 6710118b1..e13d6b539 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -12,7 +12,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TablePager from 'Components/Table/TablePager'; import { align, icons, kinds } from 'Helpers/Props'; import getFilterValue from 'Utilities/Filter/getFilterValue'; @@ -174,16 +173,6 @@ class CutoffUnmet extends Component { - - - - { this.props.executeCommand({ name: commandNames.ALBUM_SEARCH, - albumIds: selected, - commandFinished: this.repopulate + albumIds: selected }); }; onSearchAllCutoffUnmetPress = () => { this.props.executeCommand({ - name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH, - commandFinished: this.repopulate + name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH }); }; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 452e2947a..785b9b1c1 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -20,7 +20,6 @@ function CutoffUnmetRow(props) { foreignAlbumId, albumType, title, - lastSearchTime, disambiguation, isSelected, columns, @@ -90,15 +89,6 @@ function CutoffUnmetRow(props) { ); } - if (name === 'albums.lastSearchTime') { - return ( - - ); - } - if (name === 'status') { return ( - - - - { this.props.executeCommand({ name: commandNames.ALBUM_SEARCH, - albumIds: selected, - commandFinished: this.repopulate + albumIds: selected }); }; onSearchAllMissingPress = () => { this.props.executeCommand({ - name: commandNames.MISSING_ALBUM_SEARCH, - commandFinished: this.repopulate + name: commandNames.MISSING_ALBUM_SEARCH }); }; diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js index 6c0b5a0c6..0eb1a0452 100644 --- a/frontend/src/Wanted/Missing/MissingRow.js +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -17,7 +17,6 @@ function MissingRow(props) { albumType, foreignAlbumId, title, - lastSearchTime, disambiguation, isSelected, columns, @@ -87,15 +86,6 @@ function MissingRow(props) { ); } - if (name === 'albums.lastSearchTime') { - return ( - - ); - } - if (name === 'actions') { return ( ); + render( + , + document.getElementById('root') + ); } diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index a893149d5..b99a39a0d 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -3,16 +3,13 @@ + + - - - - - @@ -33,7 +30,7 @@ sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png" /> - + diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 37e780919..36aed4c4b 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -14,32 +14,6 @@ window.Lidarr = await response.json(); __webpack_public_path__ = `${window.Lidarr.urlBase}/`; /* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */ -const error = console.error; - -// Monkey patch console.error to filter out some warnings from React -// TODO: Remove this after the great TypeScript migration - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function logError(...parameters: any[]) { - const filter = parameters.find((parameter) => { - return ( - typeof parameter === 'string' && - (parameter.includes( - 'Support for defaultProps will be removed from function components in a future major release' - ) || - parameter.includes( - 'findDOMNode is deprecated and will be removed in the next major release' - )) - ); - }); - - if (!filter) { - error(...parameters); - } -} - -console.error = logError; - const { bootstrap } = await import('./bootstrap'); await bootstrap(); diff --git a/frontend/src/login.html b/frontend/src/login.html index 24d086959..a65106664 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -3,19 +3,13 @@ + + - - - - - @@ -36,11 +30,7 @@ sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png" /> - + - + @@ -61,9 +54,9 @@